-
Notifications
You must be signed in to change notification settings - Fork 336
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add extended descriptor multisig metadata to policy extraction #626
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,10 +41,10 @@ use std::collections::{BTreeMap, HashSet, VecDeque}; | |
use std::fmt; | ||
|
||
use serde::ser::SerializeMap; | ||
use serde::{Serialize, Serializer}; | ||
use serde::{Deserialize, Serialize, Serializer}; | ||
|
||
use bitcoin::hashes::*; | ||
use bitcoin::util::bip32::Fingerprint; | ||
use bitcoin::util::bip32::{DerivationPath, ExtendedPubKey, Fingerprint}; | ||
use bitcoin::{PublicKey, XOnlyPublicKey}; | ||
|
||
use miniscript::descriptor::{ | ||
|
@@ -76,6 +76,19 @@ pub enum PkOrF { | |
XOnlyPubkey(XOnlyPublicKey), | ||
/// An extended key fingerprint | ||
Fingerprint(Fingerprint), | ||
/// serialization for xpub origin metadata | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we call this "An XPub with origin metadata"?? As per the other item docs and sounds a bit more clear to me. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see the confusion. Basically because there's already something called It's been a while since I've spent time in this code, so it's possible I'm missing something, but basically what I was originally thinking was where OTOH, maybe I'm misunderstanding your comment and you're just asking to update this code comment 😅. In which case, maybe "serialization for xpub with derivation metadata" would be more clear to avoid the naming clash with origin? |
||
XpubOrigin(XpubOrigin), | ||
} | ||
|
||
/// Raw public key or extended key fingerprint | ||
#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq, Hash)] | ||
pub struct XpubOrigin { | ||
#[serde(skip_serializing_if = "Option::is_none")] | ||
fingerprint: Option<Fingerprint>, | ||
#[serde(skip_serializing_if = "Option::is_none")] | ||
derivation_path: Option<DerivationPath>, | ||
#[serde(skip_serializing_if = "Option::is_none")] | ||
xkey: Option<ExtendedPubKey>, | ||
Comment on lines
+83
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to me representing a full XPub, not just the origin.. So we can name it better as just an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you referring to the struct In the contexts I'm most familiar working in, an xpub origin has more information than the xpub (extended public key) itself. Namely the origin is the xpub and the information describing how it was derived: fingerprint for the root ( As for renaming There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was actually referring to the struct name There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OTOH if I am not wrong, after parsing a descriptor we will always have the origin information? If no fingerprint and path is provided in the descriptor, bdk assumes it as the master key? If that's the case we can simply name this struct There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Even if bdk never puts an xpub w/o origin details, if we're given one to interpret can you safely assume it's the root key? You don't need the origin information for watch-only wallets for example since you only need those details to tell the master key how to sign. We do something like this for unchained wallet configurations (which I want to use this tooling to convert to descriptors) where we only show actual origin information for keys that are yours (the clients) and blank out the origin info for those you don't have permissions to sign with in our system (for example the unchained key). We do this to preserve privacy and avoid data leaks. The xpub's relation to the master seed (origin information) should be of no concern to the other party that's signing if that key is not going to be signed with. So, in this context the origin information is optional and this would be true for all keys if you only want a watch-only descriptor. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes I agree.. For watch-only we don't need the origin info and its not prudent to assume its a master key.. ACK on that.. |
||
} | ||
|
||
impl PkOrF { | ||
|
@@ -89,11 +102,29 @@ impl PkOrF { | |
key: SinglePubKey::XOnly(pk), | ||
.. | ||
}) => PkOrF::XOnlyPubkey(*pk), | ||
DescriptorPublicKey::XPub(xpub) => PkOrF::Fingerprint(xpub.root_fingerprint(secp)), | ||
DescriptorPublicKey::XPub(xpub) => PkOrF::XpubOrigin(XpubOrigin { | ||
fingerprint: Some(xpub.root_fingerprint(secp)), | ||
derivation_path: xpub.origin.as_ref().map(|origin| origin.clone().1), | ||
xkey: Some(xpub.xkey), | ||
}), | ||
} | ||
} | ||
} | ||
|
||
/// script type used to encode multisig script | ||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)] | ||
pub enum MultisigScriptType { | ||
/// P2SH | ||
#[serde(rename = "P2SH")] | ||
P2sh, | ||
/// P2SH-P2WSH | ||
#[serde(rename = "P2SH_P2WSH")] | ||
P2shP2wsh, | ||
/// P2WSH | ||
#[serde(rename = "P2WSH")] | ||
P2wsh, | ||
} | ||
Comment on lines
+114
to
+126
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep. Good point. I assume you're recommending the Enum be renamed to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup sounds good.. I think bdk can already recognize |
||
|
||
/// An item that needs to be satisfied | ||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] | ||
#[serde(tag = "type", rename_all = "UPPERCASE")] | ||
|
@@ -139,6 +170,9 @@ pub enum SatisfiableItem { | |
keys: Vec<PkOrF>, | ||
/// The required threshold count | ||
threshold: usize, | ||
/// The script type used to encode | ||
#[serde(skip_serializing_if = "Option::is_none")] | ||
script_type: Option<MultisigScriptType>, | ||
Comment on lines
+173
to
+175
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am feeling like we should be able to set the |
||
}, | ||
|
||
// Complex item | ||
|
@@ -578,6 +612,7 @@ impl Policy { | |
threshold: usize, | ||
sorted: bool, | ||
secp: &SecpCtx, | ||
script_type: Option<MultisigScriptType>, | ||
) -> Result<Option<Policy>, PolicyError> { | ||
if threshold == 0 { | ||
return Ok(None); | ||
|
@@ -621,6 +656,7 @@ impl Policy { | |
let mut policy: Policy = SatisfiableItem::Multisig { | ||
keys: parsed_keys, | ||
threshold, | ||
script_type, | ||
} | ||
.into(); | ||
policy.contribution = contribution; | ||
|
@@ -690,7 +726,9 @@ impl Policy { | |
|
||
Ok(requirements) | ||
} | ||
SatisfiableItem::Multisig { keys, threshold } => { | ||
SatisfiableItem::Multisig { | ||
keys, threshold, .. | ||
} => { | ||
if selected.len() < *threshold { | ||
return Err(PolicyError::NotEnoughItemsSelected(self.id.clone())); | ||
} | ||
|
@@ -951,7 +989,7 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic | |
Some(SatisfiableItem::Hash160Preimage { hash: *hash }.into()) | ||
} | ||
Terminal::Multi(k, pks) | Terminal::MultiA(k, pks) => { | ||
Policy::make_multisig::<Ctx>(pks, signers, build_sat, *k, false, secp)? | ||
Policy::make_multisig::<Ctx>(pks, signers, build_sat, *k, false, secp, None)? | ||
} | ||
// Identities | ||
Terminal::Alt(inner) | ||
|
@@ -1047,6 +1085,7 @@ impl ExtractPolicy for Descriptor<DescriptorPublicKey> { | |
signers: &SignersContainer, | ||
build_sat: BuildSatisfaction, | ||
secp: &SecpCtx, | ||
script_type: Option<MultisigScriptType>, | ||
) -> Result<Option<Policy>, Error> { | ||
Ok(Policy::make_multisig::<Ctx>( | ||
keys.pks.as_ref(), | ||
|
@@ -1055,6 +1094,7 @@ impl ExtractPolicy for Descriptor<DescriptorPublicKey> { | |
keys.k, | ||
true, | ||
secp, | ||
script_type, | ||
)?) | ||
} | ||
|
||
|
@@ -1079,17 +1119,33 @@ impl ExtractPolicy for Descriptor<DescriptorPublicKey> { | |
secp, | ||
))), | ||
ShInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?), | ||
ShInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp), | ||
ShInner::SortedMulti(ref keys) => make_sortedmulti( | ||
keys, | ||
signers, | ||
build_sat, | ||
secp, | ||
Some(MultisigScriptType::P2sh), | ||
), | ||
ShInner::Wsh(wsh) => match wsh.as_inner() { | ||
WshInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?), | ||
WshInner::SortedMulti(ref keys) => { | ||
make_sortedmulti(keys, signers, build_sat, secp) | ||
} | ||
WshInner::SortedMulti(ref keys) => make_sortedmulti( | ||
keys, | ||
signers, | ||
build_sat, | ||
secp, | ||
Some(MultisigScriptType::P2shP2wsh), | ||
), | ||
}, | ||
}, | ||
Descriptor::Wsh(wsh) => match wsh.as_inner() { | ||
WshInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?), | ||
WshInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp), | ||
WshInner::SortedMulti(ref keys) => make_sortedmulti( | ||
keys, | ||
signers, | ||
build_sat, | ||
secp, | ||
Some(MultisigScriptType::P2wsh), | ||
), | ||
}, | ||
Descriptor::Bare(ms) => Ok(ms.as_inner().extract_policy(signers, build_sat, secp)?), | ||
Descriptor::Tr(tr) => { | ||
|
@@ -1172,6 +1228,7 @@ mod test { | |
.unwrap() | ||
.unwrap(); | ||
|
||
println!("{:?}", policy); | ||
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint)); | ||
assert!(matches!(&policy.contribution, Satisfaction::None)); | ||
|
||
|
@@ -1184,7 +1241,6 @@ mod test { | |
.extract_policy(&signers_container, BuildSatisfaction::None, &secp) | ||
.unwrap() | ||
.unwrap(); | ||
|
||
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint)); | ||
assert!( | ||
matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None) | ||
|
@@ -1208,7 +1264,7 @@ mod test { | |
.unwrap(); | ||
|
||
assert!( | ||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize | ||
matches!(&policy.item, Multisig { keys, threshold, .. } if threshold == &2usize | ||
&& keys[0] == PkOrF::Fingerprint(fingerprint0) | ||
&& keys[1] == PkOrF::Fingerprint(fingerprint1)) | ||
); | ||
|
@@ -1238,8 +1294,9 @@ mod test { | |
.extract_policy(&signers_container, BuildSatisfaction::None, &secp) | ||
.unwrap() | ||
.unwrap(); | ||
|
||
assert!( | ||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize | ||
matches!(&policy.item, Multisig { keys, threshold, .. } if threshold == &2usize | ||
&& keys[0] == PkOrF::Fingerprint(fingerprint0) | ||
&& keys[1] == PkOrF::Fingerprint(fingerprint1)) | ||
); | ||
|
@@ -1272,7 +1329,7 @@ mod test { | |
.unwrap(); | ||
|
||
assert!( | ||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &1 | ||
matches!(&policy.item, Multisig { keys, threshold, .. } if threshold == &1 | ||
&& keys[0] == PkOrF::Fingerprint(fingerprint0) | ||
&& keys[1] == PkOrF::Fingerprint(fingerprint1)) | ||
); | ||
|
@@ -1304,7 +1361,7 @@ mod test { | |
.unwrap(); | ||
|
||
assert!( | ||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2 | ||
matches!(&policy.item, Multisig { keys, threshold, .. } if threshold == &2 | ||
&& keys[0] == PkOrF::Fingerprint(fingerprint0) | ||
&& keys[1] == PkOrF::Fingerprint(fingerprint1)) | ||
); | ||
|
@@ -1376,10 +1433,12 @@ mod test { | |
.unwrap(); | ||
|
||
assert!( | ||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &1 | ||
&& keys[0] == PkOrF::Fingerprint(fingerprint0) | ||
&& keys[1] == PkOrF::Fingerprint(fingerprint1)) | ||
matches!(&policy.item, Multisig { keys, threshold, .. } if threshold == &1 | ||
&& matches!(&keys[0], PkOrF::XpubOrigin(XpubOrigin{ fingerprint, .. }) if fingerprint.unwrap() == fingerprint0) | ||
&& matches!(&keys[1], PkOrF::XpubOrigin(XpubOrigin{ fingerprint, .. }) if fingerprint.unwrap() == fingerprint1) | ||
) | ||
); | ||
|
||
assert!( | ||
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2 | ||
&& m == &1 | ||
|
@@ -1910,6 +1969,67 @@ mod test { | |
Satisfaction::Complete { | ||
condition: Default::default() | ||
} | ||
) | ||
} | ||
|
||
fn setup_descriptor_key<Ctx: ScriptContext>( | ||
xpub: &str, | ||
fingerprint: &str, | ||
path: &str, | ||
) -> DescriptorKey<Ctx> { | ||
use miniscript::descriptor::{DescriptorXKey, Wildcard}; | ||
let xfp = Fingerprint::from_str(fingerprint).unwrap(); | ||
let origin_path = bip32::DerivationPath::from_str(&path).unwrap(); | ||
let origin: bip32::KeySource = (xfp, origin_path); | ||
let xpub = bip32::ExtendedPubKey::from_str(xpub).unwrap(); | ||
let derivation_path = bip32::DerivationPath::from_str("m/0").unwrap(); | ||
let xkey = DescriptorPublicKey::XPub(DescriptorXKey { | ||
origin: Some(origin), | ||
xkey: xpub, | ||
derivation_path, | ||
wildcard: Wildcard::Unhardened, | ||
}); | ||
xkey.into_descriptor_key().unwrap() | ||
} | ||
|
||
#[test] | ||
fn test_extract_multisig_descriptor_policy() { | ||
let secp = Secp256k1::new(); | ||
let pubkey1 = "xpub6FDrnnUsgQSwRFazYbVDs9eadQaNV13f5dtQDoWrCuMNq2qgMH7GevctMAm3PeHq3KBkh9BgA8iPfaHYACHFpfueYdeAUtjjEH3vMJWEKfu"; | ||
let pubkey2 = "xpub6EgGHjcvovyN3nK921zAGPfuB41cJXkYRdt3tLGmiMyvbgHpss4X1eRZwShbEBb1znz2e2bCkCED87QZpin3sSYKbmCzQ9Sc7LaV98ngdeX"; | ||
let xfp1 = "9d120b19"; | ||
let xfp2 = "5c9e228d"; | ||
let path = "m/48'/0'/0'/2"; | ||
|
||
let pubkey_alice = setup_descriptor_key(pubkey1, xfp1, path); | ||
let pubkey_bob = setup_descriptor_key(pubkey2, xfp2, path); | ||
let (desc, _, _) = descriptor!(wsh(sortedmulti(1, pubkey_alice, pubkey_bob))).unwrap(); | ||
|
||
let (wallet_desc, keymap) = desc | ||
.into_wallet_descriptor(&secp, Network::Bitcoin) | ||
.unwrap(); | ||
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); | ||
|
||
let policy = wallet_desc | ||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp) | ||
.unwrap() | ||
.unwrap(); | ||
|
||
assert!( | ||
matches!(&policy.item, SatisfiableItem::Multisig { keys, script_type, threshold, .. } if threshold == &1 | ||
&& script_type.unwrap() == MultisigScriptType::P2wsh | ||
&& keys.len() == 2 | ||
&& matches!(&keys[0], PkOrF::XpubOrigin(XpubOrigin{ fingerprint, derivation_path, xkey }) | ||
if fingerprint.unwrap().to_string() == xfp1 | ||
&& derivation_path.as_ref().unwrap().to_string() == path | ||
&& xkey.unwrap().to_string() == pubkey1 | ||
) | ||
&& matches!(&keys[1], PkOrF::XpubOrigin(XpubOrigin{ fingerprint, derivation_path, xkey }) | ||
if fingerprint.unwrap().to_string() == xfp2 | ||
&& derivation_path.as_ref().unwrap().to_string() == path | ||
&& xkey.unwrap().to_string() == pubkey2 | ||
) | ||
) | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like we also don't need this
FingerPrint
anymore as its included in the XpubOrigin??There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep! Good catch.