descriptors: prevent using a signer more than once in a single path
This is necessary to support signers which sign for a single key at once. It also doesn't make any sense to reuse a signer within the same spending path, so rule it out before it creates any new edge cases. For more about the Bitbox signer support, which motivated this change, see https://github.com/wizardsardine/liana/pull/706#issuecomment-1744705808.
This commit is contained in:
parent
0a95266cce
commit
730409eb52
@ -17,6 +17,8 @@ pub enum LianaPolicyError {
|
|||||||
InsaneTimelock(u32),
|
InsaneTimelock(u32),
|
||||||
InvalidKey(Box<descriptor::DescriptorPublicKey>),
|
InvalidKey(Box<descriptor::DescriptorPublicKey>),
|
||||||
DuplicateKey(Box<descriptor::DescriptorPublicKey>),
|
DuplicateKey(Box<descriptor::DescriptorPublicKey>),
|
||||||
|
/// The same signer was used more than once in a single spending path.
|
||||||
|
DuplicateOriginSamePath(Box<descriptor::DescriptorPublicKey>),
|
||||||
InvalidMultiThresh(usize),
|
InvalidMultiThresh(usize),
|
||||||
InvalidMultiKeys(usize),
|
InvalidMultiKeys(usize),
|
||||||
IncompatibleDesc,
|
IncompatibleDesc,
|
||||||
@ -44,6 +46,9 @@ impl std::fmt::Display for LianaPolicyError {
|
|||||||
Self::DuplicateKey(key) => {
|
Self::DuplicateKey(key) => {
|
||||||
write!(f, "Duplicate key '{}'.", key)
|
write!(f, "Duplicate key '{}'.", key)
|
||||||
}
|
}
|
||||||
|
Self::DuplicateOriginSamePath(key) => {
|
||||||
|
write!(f, "Key '{}' is derived from the same origin as another key present in the same spending path. It is not possible to use a signer more than once within a single spending path.", key)
|
||||||
|
}
|
||||||
Self::IncompatibleDesc => write!(
|
Self::IncompatibleDesc => write!(
|
||||||
f,
|
f,
|
||||||
"Descriptor is not compatible with a Liana spending policy."
|
"Descriptor is not compatible with a Liana spending policy."
|
||||||
@ -83,7 +88,13 @@ impl DescKeyChecker {
|
|||||||
/// - The multipath step to only contain two indexes. These can be any indexes, which is
|
/// - The multipath step to only contain two indexes. These can be any indexes, which is
|
||||||
/// useful for deriving multiple keys from the same xpub.
|
/// useful for deriving multiple keys from the same xpub.
|
||||||
/// - Be 'signable' by an external signer (to contain an origin)
|
/// - Be 'signable' by an external signer (to contain an origin)
|
||||||
pub fn check(&mut self, key: &descriptor::DescriptorPublicKey) -> Result<(), LianaPolicyError> {
|
///
|
||||||
|
/// This returns the origin fingerprint for this xpub, to make it possible for the caller to
|
||||||
|
/// check the same signer is never used twice in the same spending path.
|
||||||
|
pub fn check(
|
||||||
|
&mut self,
|
||||||
|
key: &descriptor::DescriptorPublicKey,
|
||||||
|
) -> Result<bip32::Fingerprint, LianaPolicyError> {
|
||||||
if let descriptor::DescriptorPublicKey::MultiXPub(ref xpub) = *key {
|
if let descriptor::DescriptorPublicKey::MultiXPub(ref xpub) = *key {
|
||||||
let key_identifier = (xpub.xkey, xpub.derivation_paths.clone());
|
let key_identifier = (xpub.xkey, xpub.derivation_paths.clone());
|
||||||
// First make sure it's not a duplicate and record seeing it.
|
// First make sure it's not a duplicate and record seeing it.
|
||||||
@ -91,20 +102,21 @@ impl DescKeyChecker {
|
|||||||
return Err(LianaPolicyError::DuplicateKey(key.clone().into()));
|
return Err(LianaPolicyError::DuplicateKey(key.clone().into()));
|
||||||
}
|
}
|
||||||
self.keys_set.insert(key_identifier);
|
self.keys_set.insert(key_identifier);
|
||||||
// Then perform the contextless checks.
|
// Then perform the contextless checks (origin, deriv paths, ..).
|
||||||
let der_paths = xpub.derivation_paths.paths();
|
|
||||||
let first_der_path = der_paths.get(0).expect("Cannot be empty");
|
|
||||||
// Technically the xpub could be for the master xpub and not have an origin. But it's
|
// Technically the xpub could be for the master xpub and not have an origin. But it's
|
||||||
// unlikely (and easily fixable) while users shooting themselves in the foot by
|
// unlikely (and easily fixable) while users shooting themselves in the foot by
|
||||||
// forgetting to provide the origin is so likely that it's worth ruling out xpubs
|
// forgetting to provide the origin is so likely that it's worth ruling out xpubs
|
||||||
// without origin entirely.
|
// without origin entirely.
|
||||||
|
if let Some(ref origin) = xpub.origin {
|
||||||
|
let der_paths = xpub.derivation_paths.paths();
|
||||||
|
let first_der_path = der_paths.get(0).expect("Cannot be empty");
|
||||||
// We also rule out xpubs with hardened derivation steps (non-normalized xpubs).
|
// We also rule out xpubs with hardened derivation steps (non-normalized xpubs).
|
||||||
let valid = xpub.origin.is_some()
|
let valid = xpub.wildcard == descriptor::Wildcard::Unhardened
|
||||||
&& xpub.wildcard == descriptor::Wildcard::Unhardened
|
|
||||||
&& der_paths.len() == 2
|
&& der_paths.len() == 2
|
||||||
&& first_der_path.into_iter().all(|step| step.is_normal());
|
&& first_der_path.into_iter().all(|step| step.is_normal());
|
||||||
if valid {
|
if valid {
|
||||||
return Ok(());
|
return Ok(origin.0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(LianaPolicyError::InvalidKey(key.clone().into()))
|
Err(LianaPolicyError::InvalidKey(key.clone().into()))
|
||||||
@ -387,10 +399,24 @@ impl LianaPolicy {
|
|||||||
let mut key_checker = DescKeyChecker::new();
|
let mut key_checker = DescKeyChecker::new();
|
||||||
for path in spending_paths {
|
for path in spending_paths {
|
||||||
match path {
|
match path {
|
||||||
PathInfo::Single(ref key) => key_checker.check(key)?,
|
PathInfo::Single(ref key) => {
|
||||||
|
let _ = key_checker.check(key)?;
|
||||||
|
}
|
||||||
PathInfo::Multi(_, ref keys) => {
|
PathInfo::Multi(_, ref keys) => {
|
||||||
|
// Record the origins of the keys for this spending path. If any two keys share
|
||||||
|
// the same origin, they are from the same signer. We restrict using a signer
|
||||||
|
// more than once within a single spending path as it can lead to surprising
|
||||||
|
// behaviour. For details see:
|
||||||
|
// https://github.com/wizardsardine/liana/pull/706#issuecomment-1744705808
|
||||||
|
let mut origin_fingerprints = HashSet::with_capacity(keys.len());
|
||||||
for key in keys {
|
for key in keys {
|
||||||
key_checker.check(key)?
|
let fg = key_checker.check(key)?;
|
||||||
|
if origin_fingerprints.contains(&fg) {
|
||||||
|
return Err(LianaPolicyError::DuplicateOriginSamePath(
|
||||||
|
key.clone().into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
origin_fingerprints.insert(fg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -436,8 +462,8 @@ impl LianaPolicy {
|
|||||||
}
|
}
|
||||||
.ok_or(LianaPolicyError::IncompatibleDesc)?;
|
.ok_or(LianaPolicyError::IncompatibleDesc)?;
|
||||||
|
|
||||||
// Fetch the two spending paths' semantic policies. The primary path is identified as the
|
// Fetch all spending paths' semantic policies. The primary path is identified as the only
|
||||||
// only one that isn't timelocked.
|
// one that isn't timelocked.
|
||||||
let (mut primary_path, mut recovery_paths) = (None::<PathInfo>, BTreeMap::new());
|
let (mut primary_path, mut recovery_paths) = (None::<PathInfo>, BTreeMap::new());
|
||||||
for sub in subs {
|
for sub in subs {
|
||||||
// This is a (multi)key check. It must be the primary path.
|
// This is a (multi)key check. It must be the primary path.
|
||||||
@ -456,7 +482,8 @@ impl LianaPolicy {
|
|||||||
primary_path = Some(PathInfo::from_primary_path(sub)?);
|
primary_path = Some(PathInfo::from_primary_path(sub)?);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If it's not a simple (multi)key check, it must be the timelocked recovery path.
|
// If it's not a simple (multi)key check, it must be (one of) the timelocked
|
||||||
|
// recovery path(s).
|
||||||
let (timelock, path_info) = PathInfo::from_recovery_path(sub)?;
|
let (timelock, path_info) = PathInfo::from_recovery_path(sub)?;
|
||||||
if recovery_paths.contains_key(&timelock) {
|
if recovery_paths.contains_key(&timelock) {
|
||||||
return Err(LianaPolicyError::IncompatibleDesc);
|
return Err(LianaPolicyError::IncompatibleDesc);
|
||||||
|
|||||||
@ -583,23 +583,71 @@ mod tests {
|
|||||||
3,
|
3,
|
||||||
vec![
|
vec![
|
||||||
descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap(),
|
descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap(),
|
||||||
descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(),
|
descriptor::DescriptorPublicKey::from_str("[abcdef02]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(),
|
||||||
descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*").unwrap()
|
descriptor::DescriptorPublicKey::from_str("[abcdef03]xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*").unwrap()
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
let recovery_keys = PathInfo::Multi(
|
let recovery_keys = PathInfo::Multi(
|
||||||
2,
|
2,
|
||||||
vec![
|
vec![
|
||||||
descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*").unwrap(),
|
descriptor::DescriptorPublicKey::from_str("[abcdef05]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*").unwrap(),
|
||||||
descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*").unwrap(),
|
descriptor::DescriptorPublicKey::from_str("[abcdef04]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*").unwrap(),
|
||||||
descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(),
|
descriptor::DescriptorPublicKey::from_str("[abcdef02]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
LianaPolicy::new(
|
let err = LianaPolicy::new(
|
||||||
primary_keys,
|
primary_keys,
|
||||||
[(26352, recovery_keys)].iter().cloned().collect(),
|
[(26352, recovery_keys)].iter().cloned().collect(),
|
||||||
)
|
)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
assert!(matches!(err, LianaPolicyError::DuplicateKey(_)));
|
||||||
|
|
||||||
|
// You can't pass duplicate signers in the primary path.
|
||||||
|
let primary_keys = PathInfo::Multi(
|
||||||
|
2,
|
||||||
|
vec![
|
||||||
|
descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap(),
|
||||||
|
descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
let recovery_keys = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef02]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*").unwrap());
|
||||||
|
let err = LianaPolicy::new(
|
||||||
|
primary_keys,
|
||||||
|
[(26352, recovery_keys)].iter().cloned().collect(),
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(err, LianaPolicyError::DuplicateOriginSamePath(_)));
|
||||||
|
|
||||||
|
// You can't pass duplicate signers in the recovery path.
|
||||||
|
let recovery_keys = PathInfo::Multi(
|
||||||
|
2,
|
||||||
|
vec![
|
||||||
|
descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap(),
|
||||||
|
descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
let primary_keys = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef02]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*").unwrap());
|
||||||
|
let err = LianaPolicy::new(
|
||||||
|
primary_keys,
|
||||||
|
[(26352, recovery_keys)].iter().cloned().collect(),
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(err, LianaPolicyError::DuplicateOriginSamePath(_)));
|
||||||
|
|
||||||
|
// But the same signer can absolutely be used across spending paths.
|
||||||
|
let primary_keys = PathInfo::Multi(
|
||||||
|
2,
|
||||||
|
vec![
|
||||||
|
descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap(),
|
||||||
|
descriptor::DescriptorPublicKey::from_str("[abcdef02]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
let recovery_keys = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*").unwrap());
|
||||||
|
LianaPolicy::new(
|
||||||
|
primary_keys,
|
||||||
|
[(26352, recovery_keys)].iter().cloned().collect(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// No origin in one of the keys
|
// No origin in one of the keys
|
||||||
let owner_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
|
let owner_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
|
||||||
@ -614,7 +662,7 @@ mod tests {
|
|||||||
LianaPolicy::new(owner_key, [(timelock, heir_key)].iter().cloned().collect()).unwrap_err();
|
LianaPolicy::new(owner_key, [(timelock, heir_key)].iter().cloned().collect()).unwrap_err();
|
||||||
|
|
||||||
// A 1-of-N multisig as primary path.
|
// A 1-of-N multisig as primary path.
|
||||||
LianaDescriptor::from_str("wsh(or_d(multi(1,[573fb35b/48'/1'/0'/2']tpubDFKp9T7WAYDcENSjoifkrpq1gMDF47KGJcJrpxzX23Qor8wuGbrEVs9utNq1MDS8E2WXJSBk1qoPQLpwyokW7DiUNPwFuxQkL7owNkLAb9W/<0;1>/*,[573fb35b/48'/1'/1'/2']tpubDFGezyzuHJPhdP3jHGW7v7Hwes4Hihqv5W2yyCmRY9VZJCRchETvxrMC8uECeJZdxQ14V4iD4DecoArkUSDwj8ogYE9WEv4MNZr12thNHCs/<0;1>/*),and_v(v:multi(2,[573fb35b/48'/1'/2'/2']tpubDDwxQauiaU964vPzt5Vd7jnDHEUtp2Vc34PaWpEXg5TQ3bRccxnc1MKKh88Hi7xiMeZo9Tm6fBcq4UGXqnDtGUniJLjqAD8SjQ8Eci3aSR7/<0;1>/*,[573fb35b/48'/1'/3'/2']tpubDE37XAVB5CQ1x85md3BQ5uHCoMwT5fgT8X13zzCUQ3x5o2jskYxKjj7Qcxt1Jpj4QB8tqspn2dooPCekRuQDYrDHov7J1ueUNu2wcvgRDxr/<0;1>/*),older(1000))))#qjx6ycpc").unwrap();
|
LianaDescriptor::from_str("wsh(or_d(multi(1,[573fb35b/48'/1'/0'/2']tpubDFKp9T7WAYDcENSjoifkrpq1gMDF47KGJcJrpxzX23Qor8wuGbrEVs9utNq1MDS8E2WXJSBk1qoPQLpwyokW7DiUNPwFuxQkL7owNkLAb9W/<0;1>/*,[573fb35c/48'/1'/1'/2']tpubDFGezyzuHJPhdP3jHGW7v7Hwes4Hihqv5W2yyCmRY9VZJCRchETvxrMC8uECeJZdxQ14V4iD4DecoArkUSDwj8ogYE9WEv4MNZr12thNHCs/<0;1>/*),and_v(v:multi(2,[573fb35b/48'/1'/2'/2']tpubDDwxQauiaU964vPzt5Vd7jnDHEUtp2Vc34PaWpEXg5TQ3bRccxnc1MKKh88Hi7xiMeZo9Tm6fBcq4UGXqnDtGUniJLjqAD8SjQ8Eci3aSR7/<0;1>/*,[573fb35c/48'/1'/3'/2']tpubDE37XAVB5CQ1x85md3BQ5uHCoMwT5fgT8X13zzCUQ3x5o2jskYxKjj7Qcxt1Jpj4QB8tqspn2dooPCekRuQDYrDHov7J1ueUNu2wcvgRDxr/<0;1>/*),older(1000))))#fccaqlhh").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -771,11 +819,11 @@ mod tests {
|
|||||||
// A descriptor with single keys in both primary and recovery paths
|
// A descriptor with single keys in both primary and recovery paths
|
||||||
roundtrip("wsh(or_d(pk([aabbccdd]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:pkh([aabbccdd]xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*),older(52560))))#7437yjrs");
|
roundtrip("wsh(or_d(pk([aabbccdd]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:pkh([aabbccdd]xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*),older(52560))))#7437yjrs");
|
||||||
// One with a multisig in both paths
|
// One with a multisig in both paths
|
||||||
roundtrip("wsh(or_d(multi(3,[aabbccdd]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*,[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*,[aabbccdd]xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*),and_v(v:multi(2,[aabbccdd]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*,[aabbccdd]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*,[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*),older(26352))))#ypwt7h7e");
|
roundtrip("wsh(or_d(multi(3,[aabbccdd]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*,[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*,[aabb0022]xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*),and_v(v:multi(2,[aabbccdd]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*,[aabb0011]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*,[aabb0022/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*),older(26352))))#csjdk94l");
|
||||||
// A single key as primary path, a multisig as recovery
|
// A single key as primary path, a multisig as recovery
|
||||||
roundtrip("wsh(or_d(pk([aabbccdd]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:multi(2,[aabbccdd]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*,[aabbccdd]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*,[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*),older(26352))))#7du8x4v7");
|
roundtrip("wsh(or_d(pk([aabbccdd]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:multi(2,[aabbccdd]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*,[aabb0011]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*,[aabb0022/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*),older(26352))))#sc9gw0z0");
|
||||||
// The other way around
|
// The other way around
|
||||||
roundtrip("wsh(or_d(multi(3,[aabbccdd]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*,[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*,[aabbccdd]xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*),and_v(v:pk([aabbccdd]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*),older(26352))))#0y77q9d6");
|
roundtrip("wsh(or_d(multi(3,[aabbccdd]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*,[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*,[aabb0022]xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*),and_v(v:pk([aabbccdd]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*),older(26352))))#kjajav3j");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn psbt_from_str(psbt_str: &str) -> Psbt {
|
fn psbt_from_str(psbt_str: &str) -> Psbt {
|
||||||
|
|||||||
@ -362,9 +362,9 @@ mod tests {
|
|||||||
let secp = secp256k1::Secp256k1::new();
|
let secp = secp256k1::Secp256k1::new();
|
||||||
let network = bitcoin::Network::Bitcoin;
|
let network = bitcoin::Network::Bitcoin;
|
||||||
|
|
||||||
// Create a Liana descriptor with as primary path a 2-of-3 with two hot signers (2 keys are
|
// Create a Liana descriptor with as primary path a 2-of-3 with three hot signers and a
|
||||||
// on the same signer) and a single hot signer as recovery path. Use various random
|
// single hot signer as recovery path. (The recovery path signer is also used in the
|
||||||
// derivation paths.
|
// primary path.) Use various random derivation paths.
|
||||||
let (prim_signer_a, prim_signer_b, recov_signer) = (
|
let (prim_signer_a, prim_signer_b, recov_signer) = (
|
||||||
HotSigner::generate(network).unwrap(),
|
HotSigner::generate(network).unwrap(),
|
||||||
HotSigner::generate(network).unwrap(),
|
HotSigner::generate(network).unwrap(),
|
||||||
@ -395,9 +395,9 @@ mod tests {
|
|||||||
wildcard: Wildcard::Unhardened,
|
wildcard: Wildcard::Unhardened,
|
||||||
});
|
});
|
||||||
let origin_der = bip32::DerivationPath::from_str("m/18'/25'").unwrap();
|
let origin_der = bip32::DerivationPath::from_str("m/18'/25'").unwrap();
|
||||||
let xkey = prim_signer_b.xpub_at(&origin_der, &secp);
|
let xkey = recov_signer.xpub_at(&origin_der, &secp);
|
||||||
let prim_key_c = DescriptorPublicKey::MultiXPub(DescriptorMultiXKey {
|
let prim_key_c = DescriptorPublicKey::MultiXPub(DescriptorMultiXKey {
|
||||||
origin: Some((prim_signer_b.fingerprint(&secp), origin_der)),
|
origin: Some((recov_signer.fingerprint(&secp), origin_der)),
|
||||||
xkey,
|
xkey,
|
||||||
derivation_paths: DerivPaths::new(vec![
|
derivation_paths: DerivPaths::new(vec![
|
||||||
bip32::DerivationPath::from_str("m/0").unwrap(),
|
bip32::DerivationPath::from_str("m/0").unwrap(),
|
||||||
@ -466,15 +466,14 @@ mod tests {
|
|||||||
outputs: Vec::new(),
|
outputs: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sign the PSBT with the two primary signers. The second signer will sign for the two keys
|
// Sign the PSBT with the two primary signers. The recovery signer will sign for the two keys
|
||||||
// that it manages.
|
// that it manages.
|
||||||
// We can also add a signature for the recovery key with the recovery signer.
|
|
||||||
let psbt = dummy_psbt.clone();
|
let psbt = dummy_psbt.clone();
|
||||||
assert!(psbt.inputs[0].partial_sigs.is_empty());
|
assert!(psbt.inputs[0].partial_sigs.is_empty());
|
||||||
let psbt = prim_signer_a.sign_psbt(psbt, &secp).unwrap();
|
let psbt = prim_signer_a.sign_psbt(psbt, &secp).unwrap();
|
||||||
assert_eq!(psbt.inputs[0].partial_sigs.len(), 1);
|
assert_eq!(psbt.inputs[0].partial_sigs.len(), 1);
|
||||||
let psbt = prim_signer_b.sign_psbt(psbt, &secp).unwrap();
|
let psbt = prim_signer_b.sign_psbt(psbt, &secp).unwrap();
|
||||||
assert_eq!(psbt.inputs[0].partial_sigs.len(), 3);
|
assert_eq!(psbt.inputs[0].partial_sigs.len(), 2);
|
||||||
let psbt = recov_signer.sign_psbt(psbt, &secp).unwrap();
|
let psbt = recov_signer.sign_psbt(psbt, &secp).unwrap();
|
||||||
assert_eq!(psbt.inputs[0].partial_sigs.len(), 4);
|
assert_eq!(psbt.inputs[0].partial_sigs.len(), 4);
|
||||||
|
|
||||||
@ -490,7 +489,7 @@ mod tests {
|
|||||||
let psbt = prim_signer_a.sign_psbt(psbt, &secp).unwrap();
|
let psbt = prim_signer_a.sign_psbt(psbt, &secp).unwrap();
|
||||||
assert_eq!(psbt.inputs[0].partial_sigs.len(), 1);
|
assert_eq!(psbt.inputs[0].partial_sigs.len(), 1);
|
||||||
let psbt = prim_signer_b.sign_psbt(psbt, &secp).unwrap();
|
let psbt = prim_signer_b.sign_psbt(psbt, &secp).unwrap();
|
||||||
assert_eq!(psbt.inputs[0].partial_sigs.len(), 3);
|
assert_eq!(psbt.inputs[0].partial_sigs.len(), 2);
|
||||||
let psbt = recov_signer.sign_psbt(psbt, &secp).unwrap();
|
let psbt = recov_signer.sign_psbt(psbt, &secp).unwrap();
|
||||||
assert_eq!(psbt.inputs[0].partial_sigs.len(), 4);
|
assert_eq!(psbt.inputs[0].partial_sigs.len(), 4);
|
||||||
|
|
||||||
@ -538,7 +537,7 @@ mod tests {
|
|||||||
assert!(psbt
|
assert!(psbt
|
||||||
.inputs
|
.inputs
|
||||||
.iter()
|
.iter()
|
||||||
.all(|psbt_in| psbt_in.partial_sigs.len() == 3));
|
.all(|psbt_in| psbt_in.partial_sigs.len() == 2));
|
||||||
let psbt = recov_signer.sign_psbt(psbt, &secp).unwrap();
|
let psbt = recov_signer.sign_psbt(psbt, &secp).unwrap();
|
||||||
assert!(psbt
|
assert!(psbt
|
||||||
.inputs
|
.inputs
|
||||||
@ -573,7 +572,7 @@ mod tests {
|
|||||||
psbt.inputs[0].bip32_derivation.clear();
|
psbt.inputs[0].bip32_derivation.clear();
|
||||||
let psbt = prim_signer_b.sign_psbt(psbt, &secp).unwrap();
|
let psbt = prim_signer_b.sign_psbt(psbt, &secp).unwrap();
|
||||||
assert!(psbt.inputs[0].partial_sigs.is_empty());
|
assert!(psbt.inputs[0].partial_sigs.is_empty());
|
||||||
assert_eq!(psbt.inputs[1].partial_sigs.len(), 2);
|
assert_eq!(psbt.inputs[1].partial_sigs.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user