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:
parent
d4db804e4b
commit
1320ee30ba
@ -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", ¶ms!(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);
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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, ¤t_tip, desc);
|
||||
let updated_coins = update_coins(bit, &mut db_conn, ¤t_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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -572,7 +572,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
addr,
|
||||
bitcoin::Address::from_str(
|
||||
"bc1qgudekhcrejgtlx3yhlvdul7t4q76e5lhm0vtcsndxs6aslh4r9jsqkqhwu"
|
||||
"bc1q9ksrc647hx8zp2cewl8p5f487dgux3777yees8rjcx46t4daqzzqt7yga8"
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
17
src/lib.rs
17
src/lib.rs
@ -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();
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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],
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user