| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507 |
- //! NUT-14: Hashed Time Lock Contacts (HTLC)
- //!
- //! <https://github.com/cashubtc/nuts/blob/main/14.md>
- use std::str::FromStr;
- use bitcoin::secp256k1::schnorr::Signature;
- use serde::{Deserialize, Serialize};
- use thiserror::Error;
- use super::nut00::Witness;
- use super::nut10::Secret;
- use super::nut11::valid_signatures;
- use super::{Conditions, Proof};
- use crate::util::{hex, unix_time};
- pub mod serde_htlc_witness;
- /// NUT14 Errors
- #[derive(Debug, Error)]
- pub enum Error {
- /// Incorrect secret kind
- #[error("Secret is not a HTLC secret")]
- IncorrectSecretKind,
- /// HTLC locktime has already passed
- #[error("Locktime in past")]
- LocktimeInPast,
- /// Hash Required
- #[error("Hash required")]
- HashRequired,
- /// Hash is not valid
- #[error("Hash is not valid")]
- InvalidHash,
- /// Preimage does not match
- #[error("Preimage does not match")]
- Preimage,
- /// HTLC preimage must be valid hex encoding
- #[error("Preimage must be valid hex encoding")]
- InvalidHexPreimage,
- /// HTLC preimage must be exactly 32 bytes
- #[error("Preimage must be exactly 32 bytes (64 hex characters)")]
- PreimageInvalidSize,
- /// Witness Signatures not provided
- #[error("Witness did not provide signatures")]
- SignaturesNotProvided,
- /// SIG_ALL not supported in this context
- #[error("SIG_ALL proofs must be verified using a different method")]
- SigAllNotSupportedHere,
- /// Secp256k1 error
- #[error(transparent)]
- Secp256k1(#[from] bitcoin::secp256k1::Error),
- /// NUT11 Error
- #[error(transparent)]
- NUT11(#[from] super::nut11::Error),
- #[error(transparent)]
- /// Serde Error
- Serde(#[from] serde_json::Error),
- }
- /// HTLC Witness
- #[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
- #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
- pub struct HTLCWitness {
- /// Preimage
- pub preimage: String,
- /// Signatures
- #[serde(skip_serializing_if = "Option::is_none")]
- pub signatures: Option<Vec<String>>,
- }
- impl HTLCWitness {
- /// Decode the preimage from hex and verify it's exactly 32 bytes
- ///
- /// Returns the 32-byte preimage data if valid, or an error if:
- /// - The hex decoding fails
- /// - The decoded data is not exactly 32 bytes
- pub fn preimage_data(&self) -> Result<[u8; 32], Error> {
- const REQUIRED_PREIMAGE_BYTES: usize = 32;
- // Decode the 64-character hex string to bytes
- let preimage_bytes = hex::decode(&self.preimage).map_err(|_| Error::InvalidHexPreimage)?;
- // Verify the preimage is exactly 32 bytes
- if preimage_bytes.len() != REQUIRED_PREIMAGE_BYTES {
- return Err(Error::PreimageInvalidSize);
- }
- // Convert to fixed-size array
- let mut array = [0u8; 32];
- array.copy_from_slice(&preimage_bytes);
- Ok(array)
- }
- }
- impl Proof {
- /// Verify HTLC
- ///
- /// Per NUT-14, there are two spending pathways:
- /// 1. Receiver path (preimage + pubkeys): ALWAYS available
- /// 2. Sender/Refund path (refund keys, no preimage): available AFTER locktime
- ///
- /// The verification tries to determine which path is being used based on
- /// the witness provided, then validates accordingly.
- pub fn verify_htlc(&self) -> Result<(), Error> {
- let secret: Secret = self.secret.clone().try_into()?;
- let spending_conditions: Conditions = secret
- .secret_data()
- .tags()
- .cloned()
- .unwrap_or_default()
- .try_into()?;
- if spending_conditions.sig_flag == super::SigFlag::SigAll {
- return Err(Error::SigAllNotSupportedHere);
- }
- if secret.kind() != super::Kind::HTLC {
- return Err(Error::IncorrectSecretKind);
- }
- // Get the spending requirements (includes both receiver and refund paths)
- let now = unix_time();
- let requirements =
- super::nut10::get_pubkeys_and_required_sigs(&secret, now).map_err(Error::NUT11)?;
- // Try to extract HTLC witness - must be correct type
- let htlc_witness = match &self.witness {
- Some(Witness::HTLCWitness(witness)) => witness,
- _ => {
- // Wrong witness type or no witness
- // If refund path is available with 0 required sigs, anyone can spend
- if let Some(refund_path) = &requirements.refund_path {
- if refund_path.required_sigs == 0 {
- return Ok(());
- }
- }
- return Err(Error::IncorrectSecretKind);
- }
- };
- // Try to verify the preimage and capture the specific error if it fails
- let preimage_result = super::nut10::verify_htlc_preimage(htlc_witness, &secret);
- // Determine which path to use:
- // - If preimage is valid → use receiver path (always available)
- // - If preimage is invalid/missing → try refund path (if available)
- if preimage_result.is_ok() {
- // Receiver path: preimage valid, now check signatures against pubkeys
- if requirements.required_sigs == 0 {
- return Ok(());
- }
- let witness_signatures = htlc_witness
- .signatures
- .as_ref()
- .ok_or(Error::SignaturesNotProvided)?;
- let signatures: Vec<Signature> = witness_signatures
- .iter()
- .map(|s| Signature::from_str(s))
- .collect::<Result<Vec<_>, _>>()?;
- let msg: &[u8] = self.secret.as_bytes();
- let valid_sig_count = valid_signatures(msg, &requirements.pubkeys, &signatures)?;
- if valid_sig_count >= requirements.required_sigs {
- Ok(())
- } else {
- Err(Error::NUT11(super::nut11::Error::SpendConditionsNotMet))
- }
- } else if let Some(refund_path) = &requirements.refund_path {
- // Refund path: preimage not valid/provided, but locktime has passed
- // Check signatures against refund keys
- if refund_path.required_sigs == 0 {
- // Anyone can spend (locktime passed, no refund keys)
- return Ok(());
- }
- let witness_signatures = htlc_witness
- .signatures
- .as_ref()
- .ok_or(Error::SignaturesNotProvided)?;
- let signatures: Vec<Signature> = witness_signatures
- .iter()
- .map(|s| Signature::from_str(s))
- .collect::<Result<Vec<_>, _>>()?;
- let msg: &[u8] = self.secret.as_bytes();
- let valid_sig_count = valid_signatures(msg, &refund_path.pubkeys, &signatures)?;
- if valid_sig_count >= refund_path.required_sigs {
- Ok(())
- } else {
- Err(Error::NUT11(super::nut11::Error::SpendConditionsNotMet))
- }
- } else {
- // No valid preimage and refund path not available (locktime not passed)
- // Return the specific error from preimage verification
- preimage_result
- }
- }
- /// Add Preimage
- #[inline]
- pub fn add_preimage(&mut self, preimage: String) {
- let signatures = self
- .witness
- .as_ref()
- .map(super::nut00::Witness::signatures)
- .unwrap_or_default();
- self.witness = Some(Witness::HTLCWitness(HTLCWitness {
- preimage,
- signatures,
- }))
- }
- }
- #[cfg(test)]
- mod tests {
- use bitcoin::hashes::sha256::Hash as Sha256Hash;
- use bitcoin::hashes::Hash;
- use super::*;
- use crate::nuts::nut00::Witness;
- use crate::nuts::nut10::Kind;
- use crate::nuts::Nut10Secret;
- use crate::secret::Secret as SecretString;
- /// Tests that verify_htlc correctly accepts a valid HTLC with the correct preimage.
- ///
- /// This test ensures that a properly formed HTLC proof with the correct preimage
- /// passes verification.
- ///
- /// Mutant testing: Combined with negative tests, this catches mutations that
- /// replace verify_htlc with Ok(()) since the negative tests will fail.
- #[test]
- fn test_verify_htlc_valid() {
- // Create a valid HTLC secret with a known preimage (32 bytes)
- let preimage_bytes = [42u8; 32]; // 32-byte preimage
- let hash = Sha256Hash::hash(&preimage_bytes);
- let hash_str = hash.to_string();
- let nut10_secret = Nut10Secret::new(Kind::HTLC, hash_str, None::<Vec<Vec<String>>>);
- let secret: SecretString = nut10_secret.try_into().unwrap();
- let htlc_witness = HTLCWitness {
- preimage: hex::encode(&preimage_bytes),
- signatures: None,
- };
- let proof = Proof {
- amount: crate::Amount::from(1),
- keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
- secret,
- c: crate::nuts::nut01::PublicKey::from_hex(
- "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
- )
- .unwrap(),
- witness: Some(Witness::HTLCWitness(htlc_witness)),
- dleq: None,
- };
- // Valid HTLC should verify successfully
- assert!(proof.verify_htlc().is_ok());
- }
- /// Tests that verify_htlc correctly rejects an HTLC with a wrong preimage.
- ///
- /// This test is critical for security - if the verification function doesn't properly
- /// check the preimage against the hash, an attacker could spend HTLC-locked funds
- /// without knowing the correct preimage.
- ///
- /// Mutant testing: Catches mutations that replace verify_htlc with Ok(()) or remove
- /// the preimage verification logic.
- #[test]
- fn test_verify_htlc_wrong_preimage() {
- // Create an HTLC secret with a specific hash (32 bytes)
- let correct_preimage_bytes = [42u8; 32];
- let hash = Sha256Hash::hash(&correct_preimage_bytes);
- let hash_str = hash.to_string();
- let nut10_secret = Nut10Secret::new(Kind::HTLC, hash_str, None::<Vec<Vec<String>>>);
- let secret: SecretString = nut10_secret.try_into().unwrap();
- // Use a different preimage in the witness
- let wrong_preimage_bytes = [99u8; 32]; // Different from correct preimage
- let htlc_witness = HTLCWitness {
- preimage: hex::encode(&wrong_preimage_bytes),
- signatures: None,
- };
- let proof = Proof {
- amount: crate::Amount::from(1),
- keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
- secret,
- c: crate::nuts::nut01::PublicKey::from_hex(
- "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
- )
- .unwrap(),
- witness: Some(Witness::HTLCWitness(htlc_witness)),
- dleq: None,
- };
- // Verification should fail with wrong preimage
- let result = proof.verify_htlc();
- assert!(result.is_err());
- assert!(matches!(result.unwrap_err(), Error::Preimage));
- }
- /// Tests that verify_htlc correctly rejects an HTLC with an invalid hash format.
- ///
- /// This test ensures that the verification function properly validates that the
- /// hash in the secret data is a valid SHA256 hash.
- ///
- /// Mutant testing: Catches mutations that replace verify_htlc with Ok(()) or
- /// remove the hash validation logic.
- #[test]
- fn test_verify_htlc_invalid_hash() {
- // Create an HTLC secret with an invalid hash (not a valid hex string)
- let invalid_hash = "not_a_valid_hash";
- let nut10_secret = Nut10Secret::new(
- Kind::HTLC,
- invalid_hash.to_string(),
- None::<Vec<Vec<String>>>,
- );
- let secret: SecretString = nut10_secret.try_into().unwrap();
- let preimage_bytes = [42u8; 32]; // Valid 32-byte preimage
- let htlc_witness = HTLCWitness {
- preimage: hex::encode(&preimage_bytes),
- signatures: None,
- };
- let proof = Proof {
- amount: crate::Amount::from(1),
- keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
- secret,
- c: crate::nuts::nut01::PublicKey::from_hex(
- "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
- )
- .unwrap(),
- witness: Some(Witness::HTLCWitness(htlc_witness)),
- dleq: None,
- };
- // Verification should fail with invalid hash
- let result = proof.verify_htlc();
- assert!(result.is_err());
- assert!(matches!(result.unwrap_err(), Error::InvalidHash));
- }
- /// Tests that verify_htlc correctly rejects an HTLC with the wrong witness type.
- ///
- /// This test ensures that the verification function checks that the witness is
- /// of the correct type (HTLCWitness) and not some other witness type.
- ///
- /// Mutant testing: Catches mutations that replace verify_htlc with Ok(()) or
- /// remove the witness type check.
- #[test]
- fn test_verify_htlc_wrong_witness_type() {
- // Create an HTLC secret
- let preimage = "test_preimage";
- let hash = Sha256Hash::hash(preimage.as_bytes());
- let hash_str = hash.to_string();
- let nut10_secret = Nut10Secret::new(Kind::HTLC, hash_str, None::<Vec<Vec<String>>>);
- let secret: SecretString = nut10_secret.try_into().unwrap();
- // Create proof with wrong witness type (P2PKWitness instead of HTLCWitness)
- let proof = Proof {
- amount: crate::Amount::from(1),
- keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
- secret,
- c: crate::nuts::nut01::PublicKey::from_hex(
- "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
- )
- .unwrap(),
- witness: Some(Witness::P2PKWitness(super::super::nut11::P2PKWitness {
- signatures: vec![],
- })),
- dleq: None,
- };
- // Verification should fail with wrong witness type
- let result = proof.verify_htlc();
- assert!(result.is_err());
- assert!(matches!(result.unwrap_err(), Error::IncorrectSecretKind));
- }
- /// Tests that add_preimage correctly adds a preimage to the proof.
- ///
- /// This test ensures that add_preimage actually modifies the witness and doesn't
- /// just return without doing anything.
- ///
- /// Mutant testing: Catches mutations that replace add_preimage with () without
- /// actually adding the preimage.
- #[test]
- fn test_add_preimage() {
- let preimage_bytes = [42u8; 32]; // 32-byte preimage
- let hash = Sha256Hash::hash(&preimage_bytes);
- let hash_str = hash.to_string();
- let nut10_secret = Nut10Secret::new(Kind::HTLC, hash_str, None::<Vec<Vec<String>>>);
- let secret: SecretString = nut10_secret.try_into().unwrap();
- let mut proof = Proof {
- amount: crate::Amount::from(1),
- keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
- secret,
- c: crate::nuts::nut01::PublicKey::from_hex(
- "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
- )
- .unwrap(),
- witness: None,
- dleq: None,
- };
- // Initially, witness should be None
- assert!(proof.witness.is_none());
- // Add preimage (hex-encoded)
- let preimage_hex = hex::encode(&preimage_bytes);
- proof.add_preimage(preimage_hex.clone());
- // After adding, witness should be Some with HTLCWitness
- assert!(proof.witness.is_some());
- if let Some(Witness::HTLCWitness(witness)) = &proof.witness {
- assert_eq!(witness.preimage, preimage_hex);
- } else {
- panic!("Expected HTLCWitness");
- }
- // The proof with added preimage should verify successfully
- assert!(proof.verify_htlc().is_ok());
- }
- /// Tests that verify_htlc requires BOTH locktime expired AND no refund keys for "anyone can spend".
- ///
- /// This test verifies that when locktime has passed and refund keys are present,
- /// a signature from the refund keys is required (not anyone-can-spend).
- ///
- /// Per NUT-14: After locktime, the refund path requires signatures from refund keys.
- /// The "anyone can spend" case only applies when locktime passed AND no refund keys.
- #[test]
- fn test_htlc_locktime_and_refund_keys_logic() {
- use crate::nuts::nut01::PublicKey;
- use crate::nuts::nut11::Conditions;
- let correct_preimage_bytes = [42u8; 32]; // 32-byte preimage
- let hash = Sha256Hash::hash(&correct_preimage_bytes);
- let hash_str = hash.to_string();
- // Use WRONG preimage to force using refund path (not receiver path)
- let wrong_preimage_bytes = [99u8; 32];
- // Test: Locktime has passed (locktime=1) but refund keys ARE present
- // Since we provide wrong preimage, receiver path fails, so we try refund path.
- // Refund path with refund keys present should require a signature.
- let refund_pubkey = PublicKey::from_hex(
- "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
- )
- .unwrap();
- let conditions_with_refund = Conditions {
- locktime: Some(1), // Locktime in past (current time is much larger)
- pubkeys: None,
- refund_keys: Some(vec![refund_pubkey]), // Refund key present
- num_sigs: None,
- sig_flag: crate::nuts::nut11::SigFlag::default(),
- num_sigs_refund: None,
- };
- let nut10_secret = Nut10Secret::new(Kind::HTLC, hash_str, Some(conditions_with_refund));
- let secret: SecretString = nut10_secret.try_into().unwrap();
- let htlc_witness = HTLCWitness {
- preimage: hex::encode(&wrong_preimage_bytes), // Wrong preimage!
- signatures: None, // No signature provided
- };
- let proof = Proof {
- amount: crate::Amount::from(1),
- keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
- secret,
- c: crate::nuts::nut01::PublicKey::from_hex(
- "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
- )
- .unwrap(),
- witness: Some(Witness::HTLCWitness(htlc_witness)),
- dleq: None,
- };
- // Should FAIL because:
- // 1. Wrong preimage means receiver path fails
- // 2. Falls back to refund path (locktime passed)
- // 3. Refund keys are present, so signature is required
- // 4. No signature provided
- let result = proof.verify_htlc();
- assert!(
- result.is_err(),
- "Should fail when using refund path with refund keys but no signature"
- );
- }
- }
|