use std::fmt;
use std::str::FromStr;
use bitcoin::hashes::sha256::Hash as Sha256Hash;
use bitcoin::hashes::Hash;
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::nut10::get_pubkeys_and_required_sigs;
use crate::nut11::extract_signatures_from_witness;
use crate::util::{hex, unix_time};
use crate::SpendingConditions;
pub mod serde_htlc_witness;
#[derive(Debug, Error)]
pub enum Error {
#[error("Secret is not a HTLC secret")]
IncorrectSecretKind,
#[error("Locktime in past")]
LocktimeInPast,
#[error("Invalid signature")]
InvalidSignature,
#[error("Hash required")]
HashRequired,
#[error("Hash is not valid")]
InvalidHash,
#[error("Preimage does not match")]
Preimage,
#[error("Preimage must be valid hex encoding")]
InvalidHexPreimage,
#[error("Preimage must be exactly 32 bytes (64 hex characters)")]
PreimageInvalidSize,
#[error("Witness did not provide signatures")]
SignaturesNotProvided,
#[error("SIG_ALL proofs must be verified using a different method")]
SigAllNotSupportedHere,
#[error("HTLC spend conditions are not met")]
SpendConditionsNotMet,
#[error(transparent)]
HexError(#[from] hex::Error),
#[error(transparent)]
Secp256k1(#[from] bitcoin::secp256k1::Error),
#[error(transparent)]
NUT11(#[from] super::nut11::Error),
#[error(transparent)]
Serde(#[from] serde_json::Error),
}
#[derive(Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct HTLCWitness {
pub preimage: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub signatures: Option<Vec<String>>,
}
impl fmt::Debug for HTLCWitness {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HTLCWitness")
.field("preimage", &"[REDACTED]")
.field("signatures", &self.signatures)
.finish()
}
}
impl HTLCWitness {
pub fn preimage_data(&self) -> Result<[u8; 32], Error> {
const REQUIRED_PREIMAGE_BYTES: usize = 32;
let preimage_bytes = hex::decode(&self.preimage).map_err(|_| Error::InvalidHexPreimage)?;
if preimage_bytes.len() != REQUIRED_PREIMAGE_BYTES {
return Err(Error::PreimageInvalidSize);
}
let mut array = [0u8; 32];
array.copy_from_slice(&preimage_bytes);
Ok(array)
}
}
impl Proof {
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()
.map_err(|_| Error::SpendConditionsNotMet)?;
if spending_conditions.sig_flag == super::SigFlag::SigAll {
return Err(Error::SigAllNotSupportedHere);
}
if secret.kind() != super::Kind::HTLC {
return Err(Error::IncorrectSecretKind);
}
let now = unix_time();
let requirements =
super::nut10::get_pubkeys_and_required_sigs(&secret, now).map_err(|err| match err {
super::nut10::Error::NUT14(nut14_err) => nut14_err,
_ => Error::SpendConditionsNotMet,
})?;
let htlc_witness = match &self.witness {
Some(Witness::HTLCWitness(witness)) => witness,
_ => {
if let Some(refund_path) = &requirements.refund_path {
if refund_path.required_sigs == 0 {
return Ok(());
}
}
return Err(Error::IncorrectSecretKind);
}
};
let preimage_result = verify_htlc_preimage(htlc_witness, &secret);
if preimage_result.is_ok() {
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 {
if refund_path.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, &refund_path.pubkeys, &signatures)?;
if valid_sig_count >= refund_path.required_sigs {
Ok(())
} else {
Err(Error::NUT11(super::nut11::Error::SpendConditionsNotMet))
}
} else {
preimage_result
}
}
#[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,
}))
}
}
impl SpendingConditions {
pub fn new_htlc(preimage: String, conditions: Option<Conditions>) -> Result<Self, Error> {
const MAX_PREIMAGE_BYTES: usize = 32;
let preimage_bytes = hex::decode(preimage)?;
if preimage_bytes.len() != MAX_PREIMAGE_BYTES {
return Err(Error::PreimageInvalidSize);
}
let htlc = Sha256Hash::hash(&preimage_bytes);
Ok(Self::HTLCConditions {
data: htlc,
conditions,
})
}
pub fn new_htlc_hash(hash: &str, conditions: Option<Conditions>) -> Result<Self, Error> {
let hash = Sha256Hash::from_str(hash).map_err(|_| Error::InvalidHash)?;
Ok(Self::HTLCConditions {
data: hash,
conditions,
})
}
}
fn verify_htlc_preimage(witness: &HTLCWitness, secret: &Secret) -> Result<(), Error> {
use bitcoin::hashes::sha256::Hash as Sha256Hash;
use bitcoin::hashes::Hash;
let hash_lock =
Sha256Hash::from_str(secret.secret_data().data()).map_err(|_| Error::InvalidHash)?;
let preimage_bytes = witness.preimage_data()?;
let preimage_hash = Sha256Hash::hash(&preimage_bytes);
if hash_lock.ne(&preimage_hash) {
return Err(Error::Preimage);
}
Ok(())
}
pub(crate) fn verify_sig_all_htlc(first_input: &Proof, msg_to_sign: String) -> Result<(), Error> {
let first_secret =
Secret::try_from(&first_input.secret).map_err(|_| Error::IncorrectSecretKind)?;
let current_time = crate::util::unix_time();
let requirements = get_pubkeys_and_required_sigs(&first_secret, current_time)
.map_err(|_| Error::SpendConditionsNotMet)?;
let htlc_witness = match first_input.witness.as_ref() {
Some(super::Witness::HTLCWitness(witness)) => Some(witness),
_ => None,
};
let preimage_valid = htlc_witness
.map(|w| verify_htlc_preimage(w, &first_secret).is_ok())
.unwrap_or(false);
if !preimage_valid {
if let Some(refund_path) = &requirements.refund_path {
if refund_path.required_sigs == 0 {
return Ok(());
}
}
}
let first_witness = first_input
.witness
.as_ref()
.ok_or(Error::SignaturesNotProvided)?;
if preimage_valid {
if requirements.required_sigs == 0 {
return Ok(());
}
let signatures = extract_signatures_from_witness(first_witness)?;
let valid_sig_count = super::nut11::valid_signatures(
msg_to_sign.as_bytes(),
&requirements.pubkeys,
&signatures,
)
.map_err(|_| Error::InvalidSignature)?;
if valid_sig_count >= requirements.required_sigs {
Ok(())
} else {
Err(Error::SpendConditionsNotMet)
}
} else if let Some(refund_path) = &requirements.refund_path {
let signatures = extract_signatures_from_witness(first_witness)?;
let valid_sig_count = super::nut11::valid_signatures(
msg_to_sign.as_bytes(),
&refund_path.pubkeys,
&signatures,
)
.map_err(|_| Error::InvalidSignature)?;
if valid_sig_count >= refund_path.required_sigs {
Ok(())
} else {
Err(Error::SpendConditionsNotMet)
}
} else {
Err(Error::SpendConditionsNotMet)
}
}
#[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;
use crate::{SecretData, SecretKey};
#[allow(clippy::use_debug)]
#[test]
fn htlc_witness_debug_redacts_preimage() {
let preimage = "known-htlc-preimage";
let signature = "public-signature";
let witness = HTLCWitness {
preimage: preimage.to_string(),
signatures: Some(vec![signature.to_string()]),
};
let debug = format!("{witness:?}");
assert!(!debug.contains(preimage));
assert!(debug.contains("preimage: \"[REDACTED]\""));
assert!(debug.contains(signature));
}
fn htlc_proof(
preimage_bytes: [u8; 32],
conditions: Option<Conditions>,
witness: Option<Witness>,
) -> Proof {
let hash = Sha256Hash::hash(&preimage_bytes);
let nut10_secret =
Nut10Secret::new(Kind::HTLC, SecretData::new(hash.to_string(), conditions));
let secret: SecretString = nut10_secret.try_into().unwrap();
Proof {
amount: crate::Amount::ONE,
keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
secret,
c: crate::nuts::nut01::PublicKey::from_hex(
"02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
)
.unwrap(),
witness,
dleq: None,
p2pk_e: None,
}
}
#[test]
fn test_verify_htlc_valid() {
let preimage_bytes = [42u8; 32]; let hash = Sha256Hash::hash(&preimage_bytes);
let hash_str = hash.to_string();
let nut10_secret = Nut10Secret::new(
Kind::HTLC,
SecretData::new(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,
p2pk_e: None,
};
assert!(proof.verify_htlc().is_ok());
}
#[test]
fn test_htlc_preimage_size_boundaries() {
let valid_preimage = hex::encode([42u8; 32]);
let short_preimage = hex::encode([42u8; 31]);
let long_preimage = hex::encode([42u8; 33]);
assert!(SpendingConditions::new_htlc(valid_preimage.clone(), None).is_ok());
assert!(matches!(
SpendingConditions::new_htlc(short_preimage.clone(), None),
Err(Error::PreimageInvalidSize)
));
assert!(matches!(
SpendingConditions::new_htlc(long_preimage.clone(), None),
Err(Error::PreimageInvalidSize)
));
assert!(HTLCWitness {
preimage: valid_preimage,
signatures: None,
}
.preimage_data()
.is_ok());
assert!(matches!(
HTLCWitness {
preimage: short_preimage,
signatures: None,
}
.preimage_data(),
Err(Error::PreimageInvalidSize)
));
assert!(matches!(
HTLCWitness {
preimage: long_preimage,
signatures: None,
}
.preimage_data(),
Err(Error::PreimageInvalidSize)
));
}
#[test]
fn test_verify_htlc_wrong_preimage() {
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,
SecretData::new(hash_str, None::<Vec<Vec<String>>>),
);
let secret: SecretString = nut10_secret.try_into().unwrap();
let wrong_preimage_bytes = [99u8; 32]; 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,
p2pk_e: None,
};
let result = proof.verify_htlc();
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::Preimage));
}
#[test]
fn test_verify_htlc_requires_refund_signature_when_refund_path_is_not_anyone_can_spend() {
let refund_key = SecretKey::generate().public_key();
let proof = htlc_proof(
[42u8; 32],
Some(Conditions {
locktime: Some(1),
refund_keys: Some(vec![refund_key]),
num_sigs_refund: Some(1),
..Default::default()
}),
None,
);
assert!(matches!(
proof.verify_htlc(),
Err(Error::IncorrectSecretKind)
));
}
#[test]
fn test_verify_htlc_rejects_insufficient_receiver_signatures() {
let required_key = SecretKey::generate().public_key();
let wrong_key = SecretKey::generate();
let mut proof = htlc_proof(
[42u8; 32],
Some(Conditions {
pubkeys: Some(vec![required_key]),
num_sigs: Some(1),
..Default::default()
}),
None,
);
let signature = wrong_key.sign(proof.secret.as_bytes()).unwrap();
proof.witness = Some(Witness::HTLCWitness(HTLCWitness {
preimage: hex::encode([42u8; 32]),
signatures: Some(vec![signature.to_string()]),
}));
assert!(matches!(
proof.verify_htlc(),
Err(Error::NUT11(
crate::nuts::nut11::Error::SpendConditionsNotMet
))
));
}
#[test]
fn test_verify_htlc_rejects_insufficient_refund_signatures() {
let refund_key = SecretKey::generate().public_key();
let wrong_key = SecretKey::generate();
let mut proof = htlc_proof(
[42u8; 32],
Some(Conditions {
locktime: Some(1),
refund_keys: Some(vec![refund_key]),
num_sigs_refund: Some(1),
..Default::default()
}),
None,
);
let signature = wrong_key.sign(proof.secret.as_bytes()).unwrap();
proof.witness = Some(Witness::HTLCWitness(HTLCWitness {
preimage: hex::encode([99u8; 32]),
signatures: Some(vec![signature.to_string()]),
}));
assert!(matches!(
proof.verify_htlc(),
Err(Error::NUT11(
crate::nuts::nut11::Error::SpendConditionsNotMet
))
));
}
#[test]
fn test_verify_sig_all_htlc_allows_expired_anyone_can_spend_refund_path() {
let proof = htlc_proof(
[42u8; 32],
Some(Conditions {
locktime: Some(1),
sig_flag: crate::nuts::SigFlag::SigAll,
..Default::default()
}),
Some(Witness::HTLCWitness(HTLCWitness {
preimage: hex::encode([99u8; 32]),
signatures: None,
})),
);
assert!(verify_sig_all_htlc(&proof, "sig-all message".to_string()).is_ok());
}
#[test]
fn test_verify_htlc_invalid_hash() {
let invalid_hash = "not_a_valid_hash";
let nut10_secret = Nut10Secret::new(
Kind::HTLC,
SecretData::new(invalid_hash.to_string(), None::<Vec<Vec<String>>>),
);
let secret: SecretString = nut10_secret.try_into().unwrap();
let preimage_bytes = [42u8; 32]; 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,
p2pk_e: None,
};
let result = proof.verify_htlc();
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::InvalidHash));
}
#[test]
fn test_htlc_num_sigs_zero_bypasses_signature_requirement() {
let pubkey = crate::nuts::nut01::PublicKey::from_hex(
"02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
)
.unwrap();
let preimage_bytes = [42u8; 32];
let hash = Sha256Hash::hash(&preimage_bytes);
let hash_str = hash.to_string();
let tags = vec![
vec!["pubkeys".to_string(), pubkey.to_string()],
vec!["n_sigs".to_string(), "0".to_string()],
];
let nut10_secret = Nut10Secret::new(Kind::HTLC, SecretData::new(hash_str, Some(tags)));
let conditions_res = crate::nuts::nut10::Conditions::try_from(
nut10_secret.secret_data().tags().cloned().unwrap(),
);
assert!(
conditions_res.is_err(),
"Conditions should fail to parse due to n_sigs=0"
);
}
#[test]
fn test_verify_sig_all_htlc_nsigs_zero_bypasses_sig_check() {
let preimage_bytes = [42u8; 32];
let hash = Sha256Hash::hash(&preimage_bytes);
let hash_str = hash.to_string();
let required_pubkey = crate::nuts::nut01::PublicKey::from_hex(
"02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
)
.unwrap();
let tags = vec![
vec!["pubkeys".to_string(), required_pubkey.to_string()],
vec!["n_sigs".to_string(), "0".to_string()],
vec!["sigflag".to_string(), "SIG_ALL".to_string()],
];
let nut10_secret = Nut10Secret::new(Kind::HTLC, SecretData::new(hash_str, Some(tags)));
let conditions_res = crate::nuts::nut10::Conditions::try_from(
nut10_secret.secret_data().tags().cloned().unwrap(),
);
assert!(
conditions_res.is_err(),
"Conditions should fail to parse due to n_sigs=0"
);
}
#[test]
fn test_verify_htlc_wrong_witness_type() {
let preimage = "test_preimage";
let hash = Sha256Hash::hash(preimage.as_bytes());
let hash_str = hash.to_string();
let nut10_secret = Nut10Secret::new(
Kind::HTLC,
SecretData::new(hash_str, None::<Vec<Vec<String>>>),
);
let secret: SecretString = nut10_secret.try_into().unwrap();
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,
p2pk_e: None,
};
let result = proof.verify_htlc();
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::IncorrectSecretKind));
}
#[test]
fn test_add_preimage() {
let preimage_bytes = [42u8; 32]; let hash = Sha256Hash::hash(&preimage_bytes);
let hash_str = hash.to_string();
let nut10_secret = Nut10Secret::new(
Kind::HTLC,
SecretData::new(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,
p2pk_e: None,
};
assert!(proof.witness.is_none());
let preimage_hex = hex::encode(preimage_bytes);
proof.add_preimage(preimage_hex.clone());
assert!(proof.witness.is_some());
if let Some(Witness::HTLCWitness(witness)) = &proof.witness {
assert_eq!(witness.preimage, preimage_hex);
} else {
panic!("Expected HTLCWitness");
}
assert!(proof.verify_htlc().is_ok());
}
#[test]
fn test_htlc_locktime_and_refund_keys_logic() {
use crate::nuts::nut01::PublicKey;
use crate::nuts::nut10::Conditions;
let correct_preimage_bytes = [42u8; 32]; let hash = Sha256Hash::hash(&correct_preimage_bytes);
let hash_str = hash.to_string();
let wrong_preimage_bytes = [99u8; 32];
let refund_pubkey = PublicKey::from_hex(
"02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
)
.unwrap();
let conditions_with_refund = Conditions {
locktime: Some(1), pubkeys: None,
refund_keys: Some(vec![refund_pubkey]), num_sigs: None,
sig_flag: crate::nuts::nut11::SigFlag::default(),
num_sigs_refund: None,
};
let nut10_secret = Nut10Secret::new(
Kind::HTLC,
SecretData::new(hash_str, Some(conditions_with_refund)),
);
let secret: SecretString = nut10_secret.try_into().unwrap();
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,
p2pk_e: None,
};
let result = proof.verify_htlc();
assert!(
result.is_err(),
"Should fail when using refund path with refund keys but no signature"
);
}
#[test]
fn test_htlc_generated_empty_refund_keys_are_omitted() {
use crate::nuts::nut10::Conditions;
let preimage_bytes = [42u8; 32];
let hash = Sha256Hash::hash(&preimage_bytes);
let hash_str = hash.to_string();
let conditions = Conditions {
locktime: Some(1),
pubkeys: None,
refund_keys: Some(vec![]),
num_sigs: None,
sig_flag: crate::nuts::nut11::SigFlag::default(),
num_sigs_refund: None,
};
let nut10_secret =
Nut10Secret::new(Kind::HTLC, SecretData::new(hash_str, Some(conditions)));
let secret: SecretString = nut10_secret.try_into().unwrap();
let htlc_witness = HTLCWitness {
preimage: hex::encode([0xffu8; 32]),
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,
p2pk_e: None,
};
assert!(proof.verify_htlc().is_ok());
}
#[test]
fn test_htlc_empty_refund_tag_is_rejected() {
let preimage_bytes = [42u8; 32];
let hash = Sha256Hash::hash(&preimage_bytes);
let hash_str = hash.to_string();
let tags = vec![
vec!["locktime".to_string(), "1".to_string()],
vec!["refund".to_string()],
];
let nut10_secret = Nut10Secret::new(Kind::HTLC, SecretData::new(hash_str, Some(tags)));
let secret: SecretString = nut10_secret.try_into().unwrap();
let htlc_witness = HTLCWitness {
preimage: hex::encode([0xffu8; 32]),
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,
p2pk_e: None,
};
assert!(matches!(
proof.verify_htlc(),
Err(Error::SpendConditionsNotMet)
));
}
}