feat: refund multi sig

This commit is contained in:
thesimplekid
2025-06-28 11:22:55 +01:00
parent 238b09d56a
commit b4728d7257
3 changed files with 35 additions and 7 deletions

View File

@@ -178,20 +178,29 @@ impl Proof {
spending_conditions.locktime, spending_conditions.locktime,
spending_conditions.refund_keys, 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 lock time has passed check if refund witness signature is valid
if locktime.lt(&unix_time()) { if locktime.lt(&unix_time()) {
for s in witness_signatures.iter() { for s in witness_signatures.iter() {
for v in &refund_keys { for v in &refund_keys {
let sig = Signature::from_str(s).map_err(|_| Error::InvalidSignature)?; let sig = Signature::from_str(s).map_err(|_| Error::InvalidSignature)?;
// As long as there is one valid refund signature it can be spent
if v.verify(msg, &sig).is_ok() { 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(()); return Ok(());
} }
} }
} }
} }
} }
}
Err(Error::SpendConditionsNotMet) Err(Error::SpendConditionsNotMet)
} }
@@ -443,7 +452,7 @@ pub struct Conditions {
/// Refund keys /// Refund keys
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub refund_keys: Option<Vec<PublicKey>>, pub refund_keys: Option<Vec<PublicKey>>,
/// Numbedr of signatures required /// Number of signatures required
/// ///
/// Default is 1 /// Default is 1
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -452,6 +461,11 @@ pub struct Conditions {
/// ///
/// Default [`SigFlag::SigInputs`] /// Default [`SigFlag::SigInputs`]
pub sig_flag: SigFlag, pub sig_flag: SigFlag,
/// Number of refund signatures required
///
/// Default is 1
#[serde(skip_serializing_if = "Option::is_none")]
pub num_sigs_refund: Option<u64>,
} }
impl Conditions { impl Conditions {
@@ -462,6 +476,7 @@ impl Conditions {
refund_keys: Option<Vec<PublicKey>>, refund_keys: Option<Vec<PublicKey>>,
num_sigs: Option<u64>, num_sigs: Option<u64>,
sig_flag: Option<SigFlag>, sig_flag: Option<SigFlag>,
num_sigs_refund: Option<u64>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
if let Some(locktime) = locktime { if let Some(locktime) = locktime {
ensure_cdk!(locktime.ge(&unix_time()), Error::LocktimeInPast); ensure_cdk!(locktime.ge(&unix_time()), Error::LocktimeInPast);
@@ -473,6 +488,7 @@ impl Conditions {
refund_keys, refund_keys,
num_sigs, num_sigs,
sig_flag: sig_flag.unwrap_or_default(), sig_flag: sig_flag.unwrap_or_default(),
num_sigs_refund,
}) })
} }
} }
@@ -484,6 +500,7 @@ impl From<Conditions> for Vec<Vec<String>> {
refund_keys, refund_keys,
num_sigs, num_sigs,
sig_flag, sig_flag,
num_sigs_refund: _,
} = conditions; } = conditions;
let mut tags = Vec::new(); let mut tags = Vec::new();
@@ -564,6 +581,7 @@ impl TryFrom<Vec<Vec<String>>> for Conditions {
refund_keys, refund_keys,
num_sigs, num_sigs,
sig_flag, sig_flag,
num_sigs_refund: None,
}) })
} }
} }
@@ -583,6 +601,9 @@ pub enum TagKind {
Refund, Refund,
/// Pubkey /// Pubkey
Pubkeys, Pubkeys,
/// Number signatures required
#[serde(rename = "n_sigs_refund")]
NSigsRefund,
/// Custom tag kind /// Custom tag kind
Custom(String), Custom(String),
} }
@@ -595,6 +616,7 @@ impl fmt::Display for TagKind {
Self::Locktime => write!(f, "locktime"), Self::Locktime => write!(f, "locktime"),
Self::Refund => write!(f, "refund"), Self::Refund => write!(f, "refund"),
Self::Pubkeys => write!(f, "pubkeys"), Self::Pubkeys => write!(f, "pubkeys"),
Self::NSigsRefund => write!(f, "n_sigs_refund"),
Self::Custom(kind) => write!(f, "{kind}"), Self::Custom(kind) => write!(f, "{kind}"),
} }
} }
@@ -857,6 +879,7 @@ mod tests {
.unwrap()]), .unwrap()]),
num_sigs: Some(2), num_sigs: Some(2),
sig_flag: SigFlag::SigAll, sig_flag: SigFlag::SigAll,
num_sigs_refund: None,
}; };
let secret: Nut10Secret = Nut10Secret::new(Kind::P2PK, data.to_string(), Some(conditions)); let secret: Nut10Secret = Nut10Secret::new(Kind::P2PK, data.to_string(), Some(conditions));
@@ -891,6 +914,7 @@ mod tests {
refund_keys: Some(vec![v_key]), refund_keys: Some(vec![v_key]),
num_sigs: Some(2), num_sigs: Some(2),
sig_flag: SigFlag::SigInputs, sig_flag: SigFlag::SigInputs,
num_sigs_refund: None,
}; };
let secret: Secret = Nut10Secret::new(Kind::P2PK, v_key.to_string(), Some(conditions)) let secret: Secret = Nut10Secret::new(Kind::P2PK, v_key.to_string(), Some(conditions))

View File

@@ -161,6 +161,7 @@ pub async fn create_request(
refund_keys: None, refund_keys: None,
num_sigs: Some(num_sigs), num_sigs: Some(num_sigs),
sig_flag: SigFlag::SigInputs, sig_flag: SigFlag::SigInputs,
num_sigs_refund: None,
}; };
// Try to parse the hash // Try to parse the hash
@@ -186,6 +187,7 @@ pub async fn create_request(
refund_keys: None, refund_keys: None,
num_sigs: Some(num_sigs), num_sigs: Some(num_sigs),
sig_flag: SigFlag::SigInputs, sig_flag: SigFlag::SigInputs,
num_sigs_refund: None,
}; };
// Create HTLC conditions with the hash and pubkeys in conditions // Create HTLC conditions with the hash and pubkeys in conditions
@@ -203,6 +205,7 @@ pub async fn create_request(
refund_keys: None, refund_keys: None,
num_sigs: Some(num_sigs), num_sigs: Some(num_sigs),
sig_flag: SigFlag::SigInputs, sig_flag: SigFlag::SigInputs,
num_sigs_refund: None,
}), }),
)) ))
} }

View File

@@ -118,6 +118,7 @@ pub async fn send(
refund_keys, refund_keys,
sub_command_args.required_sigs, sub_command_args.required_sigs,
None, None,
None,
) )
.unwrap(); .unwrap();
@@ -155,8 +156,8 @@ pub async fn send(
refund_keys, refund_keys,
sub_command_args.required_sigs, sub_command_args.required_sigs,
None, None,
) None,
.unwrap(); )?;
Some(SpendingConditions::new_htlc_hash(hash, Some(conditions))?) Some(SpendingConditions::new_htlc_hash(hash, Some(conditions))?)
} }
@@ -187,8 +188,8 @@ pub async fn send(
refund_keys, refund_keys,
sub_command_args.required_sigs, sub_command_args.required_sigs,
None, None,
) None,
.unwrap(); )?;
Some(SpendingConditions::P2PKConditions { Some(SpendingConditions::P2PKConditions {
data: data_pubkey, data: data_pubkey,