mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-02 11:45:51 +01:00
Fix p2pk (#996)
* fix: refund keys must sign after locktime * fix: custom tags * refactor: locktime verification and refund
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user