daemon: use multipath descriptors

In config, expect to be given a multipath descriptor that contains a
derivation path for both receive and change addresses, but only for
those.

Instead of 'xpub/*', start using 'xpub/0/*' and 'xpub/1/*'.

When creating the watchonly wallet on bitcoind import both the receive
and change descriptors.

When polling, check for coins on both descriptors.
This commit is contained in:
Antoine Poinsot 2022-10-24 08:55:58 +02:00
parent d4db804e4b
commit 1320ee30ba
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
11 changed files with 211 additions and 112 deletions

View File

@ -351,13 +351,19 @@ impl BitcoinD {
None
}
// TODO: rescan feature will probably need another timestamp than 'now'
fn import_descriptor(&self, descriptor: &InheritanceDescriptor) -> Option<String> {
let descriptors = vec![serde_json::json!({
"desc": descriptor.to_string(),
"timestamp": "now",
"active": false,
})];
// Import the receive and change descriptors from the multipath descriptor to bitcoind.
fn import_descriptor(&self, desc: &InheritanceDescriptor) -> Option<String> {
let descriptors = [desc.receive_descriptor(), desc.change_descriptor()]
.iter()
.map(|desc| {
// TODO: rescan feature will probably need another timestamp than 'now'
serde_json::json!({
"desc": desc.to_string(),
"timestamp": "now",
"active": false,
})
})
.collect();
let res = self.make_wallet_request("importdescriptors", &params!(Json::Array(descriptors)));
let all_succeeded = res
@ -471,9 +477,11 @@ impl BitcoinD {
}
// Check our main descriptor is imported in this wallet.
if !self
.list_descriptors()
.contains(&main_descriptor.to_string())
let receive_desc = main_descriptor.receive_descriptor();
let change_desc = main_descriptor.change_descriptor();
let desc_list = self.list_descriptors();
if !desc_list.contains(&receive_desc.to_string())
|| !desc_list.contains(&change_desc.to_string())
{
return Err(BitcoindError::MissingDescriptor);
}

View File

@ -62,7 +62,7 @@ pub trait BitcoinInterface: Send {
fn received_coins(
&self,
tip: &BlockChainTip,
desc: &descriptors::InheritanceDescriptor,
descs: &[descriptors::InheritanceDescriptor],
) -> Vec<UTxO>;
/// Get all coins that were confirmed, and at what height and time.
@ -116,7 +116,7 @@ impl BitcoinInterface for d::BitcoinD {
fn received_coins(
&self,
tip: &BlockChainTip,
desc: &descriptors::InheritanceDescriptor,
descs: &[descriptors::InheritanceDescriptor],
) -> Vec<UTxO> {
// TODO: don't assume only a single descriptor is loaded on the wo wallet
let lsb_res = self.list_since_block(&tip.hash);
@ -132,7 +132,10 @@ impl BitcoinInterface for d::BitcoinD {
address,
parent_descs,
} = entry;
if parent_descs.iter().any(|parent_desc| desc == parent_desc) {
if parent_descs
.iter()
.any(|parent_desc| descs.iter().any(|desc| desc == parent_desc))
{
Some(UTxO {
outpoint,
amount,
@ -306,9 +309,9 @@ impl BitcoinInterface for sync::Arc<sync::Mutex<dyn BitcoinInterface + 'static>>
fn received_coins(
&self,
tip: &BlockChainTip,
desc: &descriptors::InheritanceDescriptor,
descs: &[descriptors::InheritanceDescriptor],
) -> Vec<UTxO> {
self.lock().unwrap().received_coins(tip, desc)
self.lock().unwrap().received_coins(tip, descs)
}
fn confirmed_coins(

View File

@ -27,14 +27,14 @@ fn update_coins(
bit: &impl BitcoinInterface,
db_conn: &mut Box<dyn DatabaseConnection>,
previous_tip: &BlockChainTip,
desc: &descriptors::InheritanceDescriptor,
descs: &[descriptors::InheritanceDescriptor],
) -> UpdatedCoins {
let curr_coins = db_conn.coins();
log::debug!("Current coins: {:?}", curr_coins);
// Start by fetching newly received coins.
let mut received = Vec::new();
for utxo in bit.received_coins(previous_tip, desc) {
for utxo in bit.received_coins(previous_tip, descs) {
if let Some(derivation_index) = db_conn.derivation_index_by_address(&utxo.address) {
if !curr_coins.contains_key(&utxo.outpoint) {
let UTxO {
@ -159,7 +159,7 @@ fn new_tip(bit: &impl BitcoinInterface, current_tip: &BlockChainTip) -> TipUpdat
fn updates(
bit: &impl BitcoinInterface,
db: &impl DatabaseInterface,
desc: &descriptors::InheritanceDescriptor,
descs: &[descriptors::InheritanceDescriptor],
) {
let mut db_conn = db.connection();
@ -173,18 +173,18 @@ fn updates(
// between our former chain and the new one, then restart fresh.
db_conn.rollback_tip(&new_tip);
log::info!("Tip was rolled back to '{}'.", new_tip);
return updates(bit, db, desc);
return updates(bit, db, descs);
}
};
// Then check the state of our coins. Do it even if the tip did not change since last poll, as
// we may have unconfirmed transactions.
let updated_coins = update_coins(bit, &mut db_conn, &current_tip, desc);
let updated_coins = update_coins(bit, &mut db_conn, &current_tip, descs);
// If the tip changed while we were polling our Bitcoin interface, start over.
if bit.chain_tip() != latest_tip {
log::info!("Chain tip changed while we were updating our state. Starting over.");
return updates(bit, db, desc);
return updates(bit, db, descs);
}
// The chain tip did not change since we started our updates. Record them and the latest tip.
@ -223,6 +223,7 @@ pub fn looper(
) {
let mut last_poll = None;
let mut synced = false;
let descs = [desc.receive_descriptor(), desc.change_descriptor()];
maybe_initialize_tip(&bit, &db);
@ -254,6 +255,6 @@ pub fn looper(
}
}
updates(&bit, &db, &desc);
updates(&bit, &db, &descs);
}
}

View File

@ -572,7 +572,7 @@ mod tests {
assert_eq!(
addr,
bitcoin::Address::from_str(
"bc1qgudekhcrejgtlx3yhlvdul7t4q76e5lhm0vtcsndxs6aslh4r9jsqkqhwu"
"bc1q9ksrc647hx8zp2cewl8p5f487dgux3777yees8rjcx46t4daqzzqt7yga8"
)
.unwrap()
);

View File

@ -228,7 +228,7 @@ mod tests {
data_dir = "/home/wizardsardine/custom/folder/"
daemon = false
log_level = "debug"
main_descriptor = "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))#y5wcna2d"
main_descriptor = "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#5f6qd0d9"
[bitcoin_config]
network = "bitcoin"
@ -245,7 +245,7 @@ mod tests {
data_dir = '/home/wizardsardine/custom/folder/'
daemon = false
log_level = 'TRACE'
main_descriptor = 'wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))#y5wcna2d'
main_descriptor = 'wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#5f6qd0d9'
[bitcoin_config]
network = 'bitcoin'
@ -266,7 +266,7 @@ mod tests {
log_level = "trace"
data_dir = "/home/wizardsardine/custom/folder/"
main_descriptor = "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))#y5wcna2e"
main_descriptor = "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#y5wcna2e"
[bitcoin_config]
network = "bitcoin"

View File

@ -484,7 +484,7 @@ mod tests {
use bitcoin::{hashes::Hash, util::bip32};
fn dummy_options() -> FreshDbOptions {
let desc_str = "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))#y5wcna2d";
let desc_str = "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#5f6qd0d9";
let main_descriptor = InheritanceDescriptor::from_str(desc_str).unwrap();
FreshDbOptions {
bitcoind_network: bitcoin::Network::Bitcoin,
@ -533,7 +533,7 @@ mod tests {
.to_string()
.contains("Database was created for network");
fs::remove_file(&db_path).unwrap();
let other_desc_str = "wsh(andor(pk(tpubDExU4YLJkyQ9RRbVScQq2brFxWWha7WmAUByPWyaWYwmcTv3Shx8aHp6mVwuE5n4TeM4z5DTWGf2YhNPmXtfvyr8cUDVvA3txdrFnFgNdF7/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))";
let other_desc_str = "wsh(andor(pk(tpubDExU4YLJkyQ9RRbVScQq2brFxWWha7WmAUByPWyaWYwmcTv3Shx8aHp6mVwuE5n4TeM4z5DTWGf2YhNPmXtfvyr8cUDVvA3txdrFnFgNdF7/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))";
let other_desc = InheritanceDescriptor::from_str(other_desc_str).unwrap();
let db = SqliteDb::new(db_path.clone(), Some(options.clone()), &secp).unwrap();
db.sanity_check(bitcoin::Network::Bitcoin, &other_desc)

View File

@ -31,7 +31,11 @@ impl std::fmt::Display for DescCreationError {
match self {
Self::InsaneTimelock(tl) => write!(f, "Timelock value '{}' isn't safe to use", tl),
Self::InvalidKey(key) => {
write!(f, "Invalid key '{}'. Need a wildcard ('ranged') xpub", key)
write!(
f,
"Invalid key '{}'. Need a wildcard ('ranged') xpub with a multipath for (and only for) deriving change addresses. That is, an xpub of the form 'xpub.../<0;1>/*'.",
key
)
}
Self::Miniscript(e) => write!(f, "Miniscript error: '{}'.", e),
Self::IncompatibleDesc => write!(f, "Descriptor is not compatible."),
@ -167,12 +171,22 @@ fn csv_check(csv_value: u32) -> Result<(), DescCreationError> {
.map_err(|_| DescCreationError::InsaneTimelock(csv_value))
}
fn is_unhardened_deriv(key: &descriptor::DescriptorPublicKey) -> bool {
// We require the descriptor key to:
// - Be deriveable (to contain a wildcard)
// - Be multipath (to contain a step in the derivation path with multiple indexes)
// - The multipath step to only contain two indexes, 0 and 1.
fn is_valid_desc_key(key: &descriptor::DescriptorPublicKey) -> bool {
match *key {
descriptor::DescriptorPublicKey::Single(..)
| descriptor::DescriptorPublicKey::MultiXPub(..) => false,
descriptor::DescriptorPublicKey::XPub(ref xpub) => {
descriptor::DescriptorPublicKey::Single(..) | descriptor::DescriptorPublicKey::XPub(..) => {
false
}
descriptor::DescriptorPublicKey::MultiXPub(ref xpub) => {
// Rust-miniscript enforces BIP389 which states that all paths must have the same len.
let len = xpub.derivation_paths.get(0).expect("Cannot be empty").len();
xpub.wildcard == descriptor::Wildcard::Unhardened
&& xpub.derivation_paths.len() == 2
&& xpub.derivation_paths[0][len - 1] == 0.into()
&& xpub.derivation_paths[1][len - 1] == 1.into()
}
}
}
@ -203,7 +217,7 @@ impl str::FromStr for InheritanceDescriptor {
_ => return Err(DescCreationError::IncompatibleDesc),
};
let invalid_key = ms.iter_pk().find_map(|pk| {
if is_unhardened_deriv(&pk) {
if is_valid_desc_key(&pk) {
None
} else {
Some(pk)
@ -269,6 +283,53 @@ impl PartialEq<descriptor::Descriptor<descriptor::DescriptorPublicKey>> for Inhe
}
}
// Derive a **single-path** descriptor at a **unhardened** derivation index. Will panic if either
// of these isn't true.
fn derive_desc(
desc: &descriptor::Descriptor<descriptor::DescriptorPublicKey>,
index: bip32::ChildNumber,
secp: &secp256k1::Secp256k1<impl secp256k1::Verification>,
) -> descriptor::Descriptor<DerivedPublicKey> {
assert!(index.is_normal());
// Unfortunately we can't just use `self.0.at_derivation_index().derived_descriptor()`
// since it would return a raw public key, but we need the origin too.
// TODO: upstream our DerivedPublicKey stuff to rust-miniscript.
//
// So we roll our own translation.
struct Derivator<'a, C: secp256k1::Verification>(u32, &'a secp256k1::Secp256k1<C>);
impl<'a, C: secp256k1::Verification>
Translator<descriptor::DescriptorPublicKey, DerivedPublicKey, descriptor::ConversionError>
for Derivator<'a, C>
{
fn pk(
&mut self,
pk: &descriptor::DescriptorPublicKey,
) -> Result<DerivedPublicKey, descriptor::ConversionError> {
let definite_key = pk
.clone()
.at_derivation_index(self.0)
.expect("We disallow multipath keys.");
let origin = (
definite_key.master_fingerprint(),
definite_key
.full_derivation_path()
.expect("We disallow multipath keys."),
);
let key = definite_key.derive_public_key(self.1)?;
Ok(DerivedPublicKey { origin, key })
}
translate_hash_clone!(
descriptor::DescriptorPublicKey,
DerivedPublicKey,
descriptor::ConversionError
);
}
desc.translate_pk(&mut Derivator(index.into(), secp))
.expect("May only fail on hardened derivation indexes, but we ruled out this case.")
}
impl InheritanceDescriptor {
pub fn new(
owner_key: descriptor::DescriptorPublicKey,
@ -285,7 +346,7 @@ impl InheritanceDescriptor {
if let Some(key) = vec![&owner_key, &heir_key]
.iter()
.find(|k| !is_unhardened_deriv(k))
.find(|k| !is_valid_desc_key(k))
{
return Err(DescCreationError::InvalidKey((**key).clone()));
}
@ -325,7 +386,7 @@ impl InheritanceDescriptor {
/// Whether all xpubs contained in this descriptor are for the passed expected network.
pub fn all_xpubs_net_is(&self, expected_net: bitcoin::Network) -> bool {
self.0.for_each_key(|xpub| {
if let descriptor::DescriptorPublicKey::XPub(xpub) = xpub {
if let descriptor::DescriptorPublicKey::MultiXPub(xpub) = xpub {
xpub.xkey.network == expected_net
} else {
false
@ -333,6 +394,46 @@ impl InheritanceDescriptor {
})
}
// TODO: Use a newtype to differentiate single and multi path descriptors.
// TODO: Cache it inside the struct, it's very inefficient to use into_single_descriptors() for
// every single derivation.
pub fn receive_descriptor(&self) -> InheritanceDescriptor {
let singlepath_descs = self
.0
.clone()
.into_single_descriptors()
.expect("Can't error, all paths have the same length");
assert_eq!(singlepath_descs.len(), 2);
// We use /0/* for receiving, so it's the first descriptor between <0;1>.
// FIXME: don't rely on ordering.
InheritanceDescriptor(
singlepath_descs
.into_iter()
.next()
.expect("Just checked the length"),
)
}
pub fn change_descriptor(&self) -> InheritanceDescriptor {
let singlepath_descs = self
.0
.clone()
.into_single_descriptors()
.expect("Can't error, all paths have the same length");
assert_eq!(singlepath_descs.len(), 2);
// We use /1/* for change, so it's the second descriptor between <0;1>.
// FIXME: don't rely on ordering.
InheritanceDescriptor(
singlepath_descs
.into_iter()
.rev()
.next()
.expect("Just checked the length"),
)
}
/// Derive this descriptor at a given index for a receiving address.
///
/// # Panics
@ -344,48 +445,7 @@ impl InheritanceDescriptor {
) -> DerivedInheritanceDescriptor {
assert!(index.is_normal());
// Unfortunately we can't just use `self.0.at_derivation_index().derived_descriptor()`
// since it would return a raw public key, but we need the origin too.
// TODO: upstream our DerivedPublicKey stuff to rust-miniscript.
//
// So we roll our own translation.
struct Derivator<'a, C: secp256k1::Verification>(u32, &'a secp256k1::Secp256k1<C>);
impl<'a, C: secp256k1::Verification>
Translator<
descriptor::DescriptorPublicKey,
DerivedPublicKey,
descriptor::ConversionError,
> for Derivator<'a, C>
{
fn pk(
&mut self,
pk: &descriptor::DescriptorPublicKey,
) -> Result<DerivedPublicKey, descriptor::ConversionError> {
let definite_key = pk
.clone()
.at_derivation_index(self.0)
.expect("We disallow multipath keys.");
let origin = (
definite_key.master_fingerprint(),
definite_key
.full_derivation_path()
.expect("We disallow multipath keys."),
);
let key = definite_key.derive_public_key(self.1)?;
Ok(DerivedPublicKey { origin, key })
}
translate_hash_clone!(
descriptor::DescriptorPublicKey,
DerivedPublicKey,
descriptor::ConversionError
);
}
let desc = self
.0
.translate_pk(&mut Derivator(index.into(), secp))
.expect("May only fail on hardened derivation indexes, but we ruled out this case.");
DerivedInheritanceDescriptor(desc)
DerivedInheritanceDescriptor(derive_desc(&self.receive_descriptor().0, index, secp))
}
/// Get the value (in blocks) of the relative timelock for the heir's spending path.
@ -478,10 +538,10 @@ mod tests {
#[test]
fn inheritance_descriptor_creation() {
let owner_key = descriptor::DescriptorPublicKey::from_str("xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/*").unwrap();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/*").unwrap();
let owner_key = descriptor::DescriptorPublicKey::from_str("xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap();
let timelock = 52560;
assert_eq!(InheritanceDescriptor::new(owner_key, heir_key, timelock).unwrap().to_string(), "wsh(or_d(pk(xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/*),and_v(v:pkh(xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/*),older(52560))))#eeyujkt7");
assert_eq!(InheritanceDescriptor::new(owner_key, heir_key, timelock).unwrap().to_string(), "wsh(or_d(pk(xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:pkh(xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*),older(52560))))#8n2ydpkt");
// We prevent footguns with timelocks by requiring a u16. Note how the following wouldn't
// compile:
@ -489,30 +549,36 @@ mod tests {
//InheritanceDescriptor::new(owner_key.clone(), heir_key.clone(), (1 << 31) + 1).unwrap_err();
//InheritanceDescriptor::new(owner_key, heir_key, (1 << 22) + 1).unwrap_err();
let owner_key = descriptor::DescriptorPublicKey::from_str("[aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/*").unwrap();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/*").unwrap();
let owner_key = descriptor::DescriptorPublicKey::from_str("[aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/<0;1>/*").unwrap();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*").unwrap();
let timelock = 57600;
assert_eq!(InheritanceDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap().to_string(), "wsh(or_d(pk([aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/*),and_v(v:pkh(xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/*),older(57600))))#8kamh6y8");
assert_eq!(InheritanceDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap().to_string(), "wsh(or_d(pk([aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/<0;1>/*),and_v(v:pkh(xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*),older(57600))))#l6dlpc2l");
// We can't pass a raw key, an xpub that is not deriveable, or only hardened derivable
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/354").unwrap();
// We can't pass a raw key, an xpub that is not deriveable, only hardened derivable,
// without both the change and receive derivation paths, or with more than 2 different
// derivation paths.
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/<0;1>/354").unwrap();
InheritanceDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/*'").unwrap();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/<0;1>/*'").unwrap();
InheritanceDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err();
let heir_key = descriptor::DescriptorPublicKey::from_str(
"02e24913be26dbcfdf8e8e94870b28725cdae09b448b6c127767bf0154e3a3c8e5",
)
.unwrap();
InheritanceDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/*'").unwrap();
InheritanceDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/<0;1;2>/*'").unwrap();
InheritanceDescriptor::new(owner_key, heir_key, timelock).unwrap_err();
}
#[test]
fn inheritance_descriptor_derivation() {
let secp = secp256k1::Secp256k1::verification_only();
let desc = InheritanceDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))#y5wcna2d").unwrap();
let desc = InheritanceDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#5f6qd0d9").unwrap();
let der_desc = desc.derive_receive(11.into(), &secp);
assert_eq!(
"bc1qvjzcg25nsxmfccct0txjvljxjwn68htkrw57jqmjhfzvhyd2z4msc74w65",
"bc1q26gtczlz03u6juf5cxppapk4sr4fyz53s3g4zs2cgactcahqv6yqc2t8e6",
der_desc.address(bitcoin::Network::Bitcoin).to_string()
);
@ -525,13 +591,13 @@ mod tests {
#[test]
fn inheritance_descriptor_tl_value() {
let desc = InheritanceDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(1),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))").unwrap();
let desc = InheritanceDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(1),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
assert_eq!(desc.timelock_value(), 1);
let desc = InheritanceDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(42000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))").unwrap();
let desc = InheritanceDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(42000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
assert_eq!(desc.timelock_value(), 42000);
let desc = InheritanceDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(65535),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))").unwrap();
let desc = InheritanceDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(65535),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
assert_eq!(desc.timelock_value(), 0xffff);
}

View File

@ -511,11 +511,14 @@ mod tests {
stream.flush().unwrap();
}
// Send them a response to 'listdescriptors' with the main descriptor
fn complete_desc_check(server: &net::TcpListener, desc: &str) {
// Send them a response to 'listdescriptors' with the receive and change descriptors
fn complete_desc_check(server: &net::TcpListener, receive_desc: &str, change_desc: &str) {
let net_resp = [
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"descriptors\":[{\"desc\":\"".as_bytes(),
desc.as_bytes(),
receive_desc.as_bytes(),
"\"},".as_bytes(),
"{\"desc\":\"".as_bytes(),
change_desc.as_bytes(),
"\"}]}}\n".as_bytes(),
]
.concat();
@ -595,8 +598,10 @@ mod tests {
};
// Create a dummy config with this bitcoind
let desc_str = "wsh(andor(pk(xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/*),older(10000),pk(xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/*)))#tk6wzexy";
let desc_str = "wsh(andor(pk(xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*),older(10000),pk(xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*)))#yudtr0k5";
let desc = InheritanceDescriptor::from_str(desc_str).unwrap();
let receive_desc = desc.receive_descriptor();
let change_desc = desc.change_descriptor();
let config = Config {
bitcoin_config,
bitcoind_config: Some(bitcoind_config),
@ -621,7 +626,7 @@ mod tests {
complete_version_check(&server);
complete_network_check(&server);
complete_wallet_check(&server, &wo_path);
complete_desc_check(&server, desc_str);
complete_desc_check(&server, &receive_desc.to_string(), &change_desc.to_string());
complete_tip_init(&server);
complete_sync_check(&server);
daemon_thread.join().unwrap();
@ -636,7 +641,7 @@ mod tests {
complete_version_check(&server);
complete_network_check(&server);
complete_wallet_check(&server, &wo_path);
complete_desc_check(&server, desc_str);
complete_desc_check(&server, &receive_desc.to_string(), &change_desc.to_string());
complete_sync_check(&server);
daemon_thread.join().unwrap();

View File

@ -47,7 +47,7 @@ impl BitcoinInterface for DummyBitcoind {
fn received_coins(
&self,
_: &BlockChainTip,
_: &descriptors::InheritanceDescriptor,
_: &[descriptors::InheritanceDescriptor],
) -> Vec<UTxO> {
Vec::new()
}
@ -274,8 +274,8 @@ impl DummyMinisafe {
poll_interval_secs: time::Duration::from_secs(2),
};
let owner_key = descriptor::DescriptorPublicKey::from_str("xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/*").unwrap();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/*").unwrap();
let owner_key = descriptor::DescriptorPublicKey::from_str("xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*").unwrap();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*").unwrap();
let desc =
crate::descriptors::InheritanceDescriptor::new(owner_key, heir_key, 10_000).unwrap();
let config = Config {

View File

@ -121,7 +121,7 @@ def minisafed(bitcoind, directory):
owner_hd = BIP32.from_seed(os.urandom(32), network="test")
owner_xpub = owner_hd.get_xpub()
main_desc = Descriptor.from_str(f"wsh(or_d(pk({owner_xpub}/*),and_v(v:pkh(tpubD9vQiBdDxYzU4cVFtApWj4devZrvcfWaPXX1zHdDc7GPfUsDKqGnbhraccfm7BAXgRgUbVQUV2v2o4NitjGEk7hpbuP85kvBrD4ahFDtNBJ/*),older(65000))))")
main_desc = Descriptor.from_str(f"wsh(or_d(pk({owner_xpub}/<0;1>/*),and_v(v:pkh(tpubD9vQiBdDxYzU4cVFtApWj4devZrvcfWaPXX1zHdDc7GPfUsDKqGnbhraccfm7BAXgRgUbVQUV2v2o4NitjGEk7hpbuP85kvBrD4ahFDtNBJ/<0;1>/*),older(65000))))")
minisafed = Minisafed(
datadir,

View File

@ -28,7 +28,7 @@ class Minisafed(TailableProc):
self,
datadir,
owner_hd,
main_desc,
multi_desc,
bitcoind_rpc_port,
bitcoind_cookie_path,
):
@ -37,7 +37,8 @@ class Minisafed(TailableProc):
self.prefix = os.path.split(datadir)[-1]
self.owner_hd = owner_hd
self.main_desc = main_desc
self.multi_desc = multi_desc
self.receive_desc, self.change_desc = multi_desc.singlepath_descriptors()
self.conf_file = os.path.join(datadir, "config.toml")
self.cmd_line = [MINISAFED_PATH, "--conf", f"{self.conf_file}"]
@ -49,7 +50,7 @@ class Minisafed(TailableProc):
f.write("daemon = false\n")
f.write(f"log_level = '{LOG_LEVEL}'\n")
f.write(f'main_descriptor = "{main_desc}"\n')
f.write(f'main_descriptor = "{multi_desc}"\n')
f.write("[bitcoin_config]\n")
f.write('network = "regtest"\n')
@ -71,24 +72,32 @@ class Minisafed(TailableProc):
# Sign each input.
for i, psbt_in in enumerate(psbt.i):
# First, gather the needed information from the PSBT input.
# 'hd_keypaths' is of the form {pubkey: (fingerprint (4 bytes), derivation index (4 bytes))}
# 'hd_keypaths' is of the form {pubkey: (fingerprint (4 bytes), derivation path (n * 4 bytes))}
fing_der = next(iter(psbt_in.map[PSBT_IN_BIP32_DERIVATION].values()))
der_index = int.from_bytes(fing_der[4:], byteorder="little", signed=True)
raw_der_path = fing_der[4:]
der_path = [
int.from_bytes(raw_der_path[i : i + 4], byteorder="little", signed=True)
for i in range(0, len(raw_der_path), 4)
]
script_code = psbt_in.map[PSBT_IN_WITNESS_SCRIPT]
# Now sign the transaction with the key of the "owner" (the participant that
# can sign immediately without a timelock)
sighash = sighash_all_witness(script_code, psbt, i)
privkey = coincurve.PrivateKey(
self.owner_hd.get_privkey_from_path([der_index])
self.owner_hd.get_privkey_from_path(der_path)
)
pubkey = privkey.public_key.format()
assert pubkey in psbt_in.map[PSBT_IN_BIP32_DERIVATION].keys(), (
der_path,
fing_der,
pubkey,
psbt_in.map[PSBT_IN_BIP32_DERIVATION].keys(),
)
sig = privkey.sign(sighash, hasher=None) + b"\x01"
logging.debug(f"Adding signature {sig.hex()} for pubkey {pubkey.hex()}")
logging.debug(
f"Adding signature {sig.hex()} for pubkey {pubkey.hex()} (path {der_path})"
)
assert PSBT_IN_PARTIAL_SIG not in psbt_in.map
psbt_in.map[PSBT_IN_PARTIAL_SIG] = {pubkey: sig}
@ -108,12 +117,19 @@ class Minisafed(TailableProc):
# First, gather the needed information from the PSBT input.
# 'hd_keypaths' is of the form {pubkey: (fingerprint, derivation index)}
fing_der = next(iter(psbt_in.map[PSBT_IN_BIP32_DERIVATION].values()))
der_index = int.from_bytes(fing_der[4:], byteorder="little", signed=True)
raw_der_path = fing_der[4:]
der_path = [
int.from_bytes(raw_der_path[i : i + 4], byteorder="little", signed=True)
for i in range(0, len(raw_der_path), 4)
]
assert len(der_path) == 2
# Create a copy of the descriptor to derive it at the index used in this input.
# Then create a satisfaction for it using the signature we just created.
desc = Descriptor.from_str(str(self.main_desc))
desc.derive(der_index)
desc = Descriptor.from_str(
str(self.receive_desc if der_path[0] == 0 else self.change_desc)
)
desc.derive(der_path[1])
sat_material = SatisfactionMaterial(
signatures=psbt_in.map[PSBT_IN_PARTIAL_SIG],
)