//! NUT-14: Hashed Time Lock Contacts (HTLC) //! //! 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>, } 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 = witness_signatures .iter() .map(|s| Signature::from_str(s)) .collect::, _>>()?; 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 = witness_signatures .iter() .map(|s| Signature::from_str(s)) .collect::, _>>()?; 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::>>); 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::>>); 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::>>, ); 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::>>); 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::>>); 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" ); } }