|
|
@@ -146,6 +146,45 @@ impl Proof {
|
|
|
let witness_signatures = witness_signatures.ok_or(Error::SignaturesNotProvided)?;
|
|
|
|
|
|
let mut pubkeys = spending_conditions.pubkeys.clone().unwrap_or_default();
|
|
|
+ // NUT-11 enforcement per spec:
|
|
|
+ // - If locktime has passed and refund keys are present, spend must be authorized by
|
|
|
+ // refund pubkeys (n_sigs_refund-of-refund). This supersedes normal pubkey enforcement
|
|
|
+ // after expiry.
|
|
|
+ // - If locktime has passed and no refund keys are present, proof becomes spendable
|
|
|
+ // without further key checks (anyone-can-spend behavior).
|
|
|
+ // - Otherwise (before locktime), enforce normal multisig on the set of authorized
|
|
|
+ // pubkeys: Secret.data plus optional `pubkeys` tag, requiring n_sigs unique signers.
|
|
|
+
|
|
|
+ let now = unix_time();
|
|
|
+
|
|
|
+ if let Some(locktime) = spending_conditions.locktime {
|
|
|
+ if now >= locktime {
|
|
|
+ if let Some(refund_keys) = spending_conditions.refund_keys.clone() {
|
|
|
+ let needed_refund_sigs =
|
|
|
+ spending_conditions.num_sigs_refund.unwrap_or(1) as usize;
|
|
|
+ let mut valid_pubkeys = HashSet::new();
|
|
|
+
|
|
|
+ // After locktime, require signatures from refund keys
|
|
|
+ for s in witness_signatures.iter() {
|
|
|
+ let sig = Signature::from_str(s).map_err(|_| Error::InvalidSignature)?;
|
|
|
+ for v in &refund_keys {
|
|
|
+ if v.verify(msg, &sig).is_ok() {
|
|
|
+ valid_pubkeys.insert(v);
|
|
|
+ if valid_pubkeys.len() >= needed_refund_sigs {
|
|
|
+ return Ok(());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // If locktime and refund keys were specified they must sign after locktime
|
|
|
+ return Err(Error::SpendConditionsNotMet);
|
|
|
+ } else {
|
|
|
+ // If only locktime is specified, consider it spendable after locktime
|
|
|
+ return Ok(());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
if secret.kind().eq(&Kind::P2PK) {
|
|
|
pubkeys.push(PublicKey::from_str(secret.secret_data().data())?);
|
|
|
@@ -175,34 +214,6 @@ impl Proof {
|
|
|
return Ok(());
|
|
|
}
|
|
|
|
|
|
- if let (Some(locktime), Some(refund_keys)) = (
|
|
|
- spending_conditions.locktime,
|
|
|
- spending_conditions.refund_keys,
|
|
|
- ) {
|
|
|
- let needed_refund_sigs = spending_conditions.num_sigs_refund.unwrap_or(1) as usize;
|
|
|
-
|
|
|
- let mut valid_pubkeys = HashSet::new();
|
|
|
-
|
|
|
- // If lock time has passed check if refund witness signature is valid
|
|
|
- if locktime.lt(&unix_time()) {
|
|
|
- for s in witness_signatures.iter() {
|
|
|
- for v in &refund_keys {
|
|
|
- let sig = Signature::from_str(s).map_err(|_| Error::InvalidSignature)?;
|
|
|
-
|
|
|
- if v.verify(msg, &sig).is_ok() {
|
|
|
- if !valid_pubkeys.insert(v) {
|
|
|
- return Err(Error::DuplicateSignature);
|
|
|
- }
|
|
|
-
|
|
|
- if valid_pubkeys.len() >= needed_refund_sigs {
|
|
|
- return Ok(());
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
Err(Error::SpendConditionsNotMet)
|
|
|
}
|
|
|
}
|
|
|
@@ -502,7 +513,7 @@ impl From<Conditions> for Vec<Vec<String>> {
|
|
|
refund_keys,
|
|
|
num_sigs,
|
|
|
sig_flag,
|
|
|
- num_sigs_refund: _,
|
|
|
+ num_sigs_refund,
|
|
|
} = conditions;
|
|
|
|
|
|
let mut tags = Vec::new();
|
|
|
@@ -522,6 +533,11 @@ impl From<Conditions> for Vec<Vec<String>> {
|
|
|
if let Some(refund_keys) = refund_keys {
|
|
|
tags.push(Tag::Refund(refund_keys).as_vec())
|
|
|
}
|
|
|
+
|
|
|
+ if let Some(num_sigs_refund) = num_sigs_refund {
|
|
|
+ tags.push(Tag::NSigsRefund(num_sigs_refund).as_vec())
|
|
|
+ }
|
|
|
+
|
|
|
tags.push(Tag::SigFlag(sig_flag).as_vec());
|
|
|
tags
|
|
|
}
|
|
|
@@ -577,13 +593,22 @@ impl TryFrom<Vec<Vec<String>>> for Conditions {
|
|
|
None
|
|
|
};
|
|
|
|
|
|
+ let num_sigs_refund = if let Some(tag) = tags.get(&TagKind::NSigsRefund) {
|
|
|
+ match tag {
|
|
|
+ Tag::NSigsRefund(num_sigs) => Some(*num_sigs),
|
|
|
+ _ => None,
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ None
|
|
|
+ };
|
|
|
+
|
|
|
Ok(Conditions {
|
|
|
locktime,
|
|
|
pubkeys,
|
|
|
refund_keys,
|
|
|
num_sigs,
|
|
|
sig_flag,
|
|
|
- num_sigs_refund: None,
|
|
|
+ num_sigs_refund,
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
@@ -619,7 +644,7 @@ impl fmt::Display for TagKind {
|
|
|
Self::Refund => write!(f, "refund"),
|
|
|
Self::Pubkeys => write!(f, "pubkeys"),
|
|
|
Self::NSigsRefund => write!(f, "n_sigs_refund"),
|
|
|
- Self::Custom(kind) => write!(f, "{kind}"),
|
|
|
+ Self::Custom(c) => write!(f, "{c}"),
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -635,6 +660,7 @@ where
|
|
|
"locktime" => Self::Locktime,
|
|
|
"refund" => Self::Refund,
|
|
|
"pubkeys" => Self::Pubkeys,
|
|
|
+ "n_sigs_refund" => Self::NSigsRefund,
|
|
|
t => Self::Custom(t.to_owned()),
|
|
|
}
|
|
|
}
|
|
|
@@ -741,6 +767,10 @@ pub enum Tag {
|
|
|
Refund(Vec<PublicKey>),
|
|
|
/// Pubkeys [`Tag`]
|
|
|
PubKeys(Vec<PublicKey>),
|
|
|
+ /// Number of Sigs refund [`Tag`]
|
|
|
+ NSigsRefund(u64),
|
|
|
+ /// Custom tag
|
|
|
+ Custom(String, Vec<String>),
|
|
|
}
|
|
|
|
|
|
impl Tag {
|
|
|
@@ -752,6 +782,8 @@ impl Tag {
|
|
|
Self::LockTime(_) => TagKind::Locktime,
|
|
|
Self::Refund(_) => TagKind::Refund,
|
|
|
Self::PubKeys(_) => TagKind::Pubkeys,
|
|
|
+ Self::NSigsRefund(_) => TagKind::NSigsRefund,
|
|
|
+ Self::Custom(tag, _) => TagKind::Custom(tag.to_string()),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -792,7 +824,16 @@ where
|
|
|
|
|
|
Ok(Self::PubKeys(pubkeys))
|
|
|
}
|
|
|
- _ => Err(Error::UnknownTag),
|
|
|
+ TagKind::NSigsRefund => Ok(Tag::NSigsRefund(tag[1].as_ref().parse()?)),
|
|
|
+ TagKind::Custom(name) => {
|
|
|
+ let tags = tag
|
|
|
+ .iter()
|
|
|
+ .skip(1)
|
|
|
+ .map(|p| p.as_ref().to_string())
|
|
|
+ .collect::<Vec<String>>();
|
|
|
+
|
|
|
+ Ok(Self::Custom(name, tags))
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -818,6 +859,18 @@ impl From<Tag> for Vec<String> {
|
|
|
}
|
|
|
tag
|
|
|
}
|
|
|
+ Tag::NSigsRefund(num_sigs) => {
|
|
|
+ vec![TagKind::NSigsRefund.to_string(), num_sigs.to_string()]
|
|
|
+ }
|
|
|
+ Tag::Custom(name, c) => {
|
|
|
+ let mut tag = vec![name];
|
|
|
+
|
|
|
+ for t in c {
|
|
|
+ tag.push(t);
|
|
|
+ }
|
|
|
+
|
|
|
+ tag
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -1360,6 +1413,61 @@ mod tests {
|
|
|
assert!(invalid_proof.verify_p2pk().is_err());
|
|
|
}
|
|
|
|
|
|
+ #[test]
|
|
|
+ fn sig_with_non_refund_keys_after_locktime() {
|
|
|
+ let secret_key =
|
|
|
+ SecretKey::from_str("99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37")
|
|
|
+ .unwrap();
|
|
|
+
|
|
|
+ let signing_key_two =
|
|
|
+ SecretKey::from_str("0000000000000000000000000000000000000000000000000000000000000001")
|
|
|
+ .unwrap();
|
|
|
+
|
|
|
+ let signing_key_three =
|
|
|
+ SecretKey::from_str("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f")
|
|
|
+ .unwrap();
|
|
|
+ let v_key: PublicKey = secret_key.public_key();
|
|
|
+ let v_key_two: PublicKey = signing_key_two.public_key();
|
|
|
+ let v_key_three: PublicKey = signing_key_three.public_key();
|
|
|
+
|
|
|
+ let conditions = Conditions {
|
|
|
+ locktime: Some(21),
|
|
|
+ pubkeys: Some(vec![v_key_three]),
|
|
|
+ refund_keys: Some(vec![v_key, v_key_two]),
|
|
|
+ num_sigs: None,
|
|
|
+ sig_flag: SigFlag::SigInputs,
|
|
|
+ num_sigs_refund: Some(2),
|
|
|
+ };
|
|
|
+
|
|
|
+ let secret: Secret = Nut10Secret::new(Kind::P2PK, v_key.to_string(), Some(conditions))
|
|
|
+ .try_into()
|
|
|
+ .unwrap();
|
|
|
+
|
|
|
+ let mut proof = Proof {
|
|
|
+ keyset_id: Id::from_str("009a1f293253e41e").unwrap(),
|
|
|
+ amount: Amount::ZERO,
|
|
|
+ secret,
|
|
|
+ c: PublicKey::from_str(
|
|
|
+ "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
|
|
|
+ )
|
|
|
+ .unwrap(),
|
|
|
+ witness: Some(Witness::P2PKWitness(P2PKWitness { signatures: vec![] })),
|
|
|
+ dleq: None,
|
|
|
+ };
|
|
|
+
|
|
|
+ proof.sign_p2pk(signing_key_three.clone()).unwrap();
|
|
|
+
|
|
|
+ assert!(proof.verify_p2pk().is_err());
|
|
|
+
|
|
|
+ proof.witness = None;
|
|
|
+
|
|
|
+ proof.sign_p2pk(secret_key).unwrap();
|
|
|
+ assert!(proof.verify_p2pk().is_err());
|
|
|
+ proof.sign_p2pk(signing_key_two).unwrap();
|
|
|
+
|
|
|
+ assert!(proof.verify_p2pk().is_ok());
|
|
|
+ }
|
|
|
+
|
|
|
// Helper functions for melt request tests
|
|
|
fn create_test_proof(secret: Secret, pubkey: PublicKey, id: &str) -> Proof {
|
|
|
Proof {
|