Merge #17: Typesafe descriptor management

ee86a1bd5ebbda39885f1e8869bf8ae03552d682 descriptors: add a helper to get the value of the timelock (Antoine Poinsot)
0fd57db8a893ae2621ca5f3cc03c6393c3a65646 Use the descriptor newtype instead of the raw miniscript Descriptor type (Antoine Poinsot)
44eb0fad9b758f7b81caea735b1a6ea41b29ebd6 descriptors: introduce a newtype for derived descriptors (Antoine Poinsot)
869d370daa354c76292517448646d0b7f76facba descriptors: introduce an InheritanceDescriptor type (Antoine Poinsot)

Pull request description:

  This introduces types for our inheritance descriptor. This is taken and adapted from revaultd, where it turned out to be very helpful.

  This also adds a helper to query the timelock of the heir's spending path in the inheritance descriptor for downstream users to be able to compute the expiration block of each coin.

  This is based on #13.

ACKs for top commit:
  edouardparis:
    utACK ee86a1bd5ebbda39885f1e8869bf8ae03552d682

Tree-SHA512: 948720967052fcd0e83c3f061f981b25d79c4cc62bbd521de5ea27ee0f08ed1c50c3c089c9cb0729e1bdc59f4acfed1fa05decdbe4fd8b549abd3345c34d66a5
This commit is contained in:
edouard 2022-09-15 09:43:15 +02:00
commit dab40527ae
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
10 changed files with 408 additions and 130 deletions

View File

@ -1,7 +1,7 @@
///! Implementation of the Bitcoin interface using bitcoind.
///!
///! We use the RPC interface and a watchonly descriptor wallet.
use crate::{bitcoin::BlockChainTip, config};
use crate::{bitcoin::BlockChainTip, config, descriptors::InheritanceDescriptor};
use std::{collections::HashSet, convert::TryInto, fs, io, str::FromStr, time::Duration};
@ -10,7 +10,7 @@ use jsonrpc::{
client::Client,
simple_http::{self, SimpleHttpTransport},
};
use miniscript::{bitcoin, Descriptor, DescriptorPublicKey};
use miniscript::bitcoin;
use serde_json::Value as Json;
@ -354,7 +354,7 @@ impl BitcoinD {
}
// TODO: rescan feature will probably need another timestamp than 'now'
fn import_descriptor(&self, descriptor: &Descriptor<DescriptorPublicKey>) -> Option<String> {
fn import_descriptor(&self, descriptor: &InheritanceDescriptor) -> Option<String> {
let descriptors = vec![serde_json::json!({
"desc": descriptor.to_string(),
"timestamp": "now",
@ -400,7 +400,7 @@ impl BitcoinD {
/// Create the watchonly wallet on bitcoind, and import it the main descriptor.
pub fn create_watchonly_wallet(
&self,
main_descriptor: &Descriptor<DescriptorPublicKey>,
main_descriptor: &InheritanceDescriptor,
) -> Result<(), BitcoindError> {
// Remove any leftover. This can happen if we delete the watchonly wallet but don't restart
// bitcoind.
@ -440,7 +440,7 @@ impl BitcoinD {
/// Perform various sanity checks on the bitcoind instance.
pub fn sanity_check(
&self,
main_descriptor: &Descriptor<DescriptorPublicKey>,
main_descriptor: &InheritanceDescriptor,
config_network: bitcoin::Network,
) -> Result<(), BitcoindError> {
// Check the minimum supported bitcoind version

View File

@ -7,15 +7,11 @@ mod utils;
use crate::{
bitcoin::BitcoinInterface,
database::{Coin, DatabaseInterface},
DaemonControl, VERSION,
descriptors, DaemonControl, VERSION,
};
use utils::{deser_amount_from_sats, ser_amount};
use miniscript::{
bitcoin,
descriptor::{self, DescriptorTrait},
TranslatePk2,
};
use miniscript::bitcoin;
use serde::{Deserialize, Serialize};
impl DaemonControl {
@ -37,17 +33,13 @@ impl DaemonControl {
pub fn get_new_address(&self) -> GetAddressResult {
let mut db_conn = self.db.connection();
let index = db_conn.derivation_index();
// TODO: handle should we wrap around instead of failing?
// TODO: should we wrap around instead of failing?
db_conn.increment_derivation_index(&self.secp);
let address = self
.config
.main_descriptor
// TODO: have a descriptor newtype along with a derived descriptor one.
.derive(index.into())
.translate_pk2(|xpk| xpk.derive_public_key(&self.secp))
.expect("All pubkeys were derived, no wildcard.")
.address(self.config.bitcoin_config.network)
.expect("It's a wsh() descriptor");
.derive(index.into(), &self.secp)
.address(self.config.bitcoin_config.network);
GetAddressResult { address }
}
@ -78,7 +70,7 @@ impl DaemonControl {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetInfoDescriptors {
pub main: descriptor::Descriptor<descriptor::DescriptorPublicKey>,
pub main: descriptors::InheritanceDescriptor,
}
/// Information about the daemon

View File

@ -1,10 +1,8 @@
use crate::descriptors::InheritanceDescriptor;
use std::{net::SocketAddr, path::PathBuf, str::FromStr, time::Duration};
use miniscript::{
bitcoin::Network,
descriptor::{Descriptor, DescriptorPublicKey},
ForEach, ForEachKey,
};
use miniscript::{bitcoin::Network, DescriptorPublicKey, ForEach, ForEachKey};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
@ -94,7 +92,7 @@ pub struct Config {
deserialize_with = "deserialize_fromstr",
serialize_with = "serialize_to_string"
)]
pub main_descriptor: Descriptor<DescriptorPublicKey>,
pub main_descriptor: InheritanceDescriptor,
/// Settings for the Bitcoin interface
pub bitcoin_config: BitcoinConfig,
/// Settings specific to bitcoind as the Bitcoin interface
@ -115,7 +113,7 @@ pub enum ConfigError {
DatadirNotFound,
FileNotFound,
ReadingFile(String),
UnexpectedDescriptor(Descriptor<DescriptorPublicKey>),
UnexpectedDescriptor(InheritanceDescriptor),
Unexpected(String),
}
@ -205,7 +203,7 @@ impl Config {
Network::Bitcoin => Network::Bitcoin,
_ => Network::Testnet,
};
let unexpected_net = self.main_descriptor.for_each_key(|pkpkh| {
let unexpected_net = self.main_descriptor.as_inner().for_each_key(|pkpkh| {
let xpub = match pkpkh {
// For DescriptorPublicKey, Pk::Hash == Self.
ForEach::Key(xpub) => xpub,
@ -242,7 +240,7 @@ mod tests {
data_dir = "/home/wizardsardine/custom/folder/"
daemon = false
log_level = "debug"
main_descriptor = "wsh(andor(thresh(1,pk(xpub6BaZSKgpaVvibu2k78QsqeDWXp92xLHZxiu1WoqLB9hKhsBf3miBUDX7PJLgSPvkj66ThVHTqdnbXpeu8crXFmDUd4HeM4s4miQS2xsv3Qb/*)),and_v(v:multi(2,03b506a1dbe57b4bf48c95e0c7d417b87dd3b4349d290d2e7e9ba72c912652d80a,0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce),older(4)),thresh(2,pkh(xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU/*),a:pkh(xpub6AaffFGfH6WXfm6pwWzmUMuECQnoLeB3agMKaLyEBZ5ZVfwtnS5VJKqXBt8o5ooCWVy2H87GsZshp7DeKE25eWLyd1Ccuh2ZubQUkgpiVux/*))))#532k8uvf"
main_descriptor = "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))#y5wcna2d"
[bitcoin_config]
network = "bitcoin"
@ -259,7 +257,7 @@ mod tests {
data_dir = '/home/wizardsardine/custom/folder/'
daemon = false
log_level = 'TRACE'
main_descriptor = 'wsh(andor(thresh(1,pk(xpub6BaZSKgpaVvibu2k78QsqeDWXp92xLHZxiu1WoqLB9hKhsBf3miBUDX7PJLgSPvkj66ThVHTqdnbXpeu8crXFmDUd4HeM4s4miQS2xsv3Qb/*)),and_v(v:multi(2,03b506a1dbe57b4bf48c95e0c7d417b87dd3b4349d290d2e7e9ba72c912652d80a,0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce),older(4)),thresh(2,pkh(xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU/*),a:pkh(xpub6AaffFGfH6WXfm6pwWzmUMuECQnoLeB3agMKaLyEBZ5ZVfwtnS5VJKqXBt8o5ooCWVy2H87GsZshp7DeKE25eWLyd1Ccuh2ZubQUkgpiVux/*))))#532k8uvf'
main_descriptor = 'wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))#y5wcna2d'
[bitcoin_config]
network = 'bitcoin'
@ -280,8 +278,7 @@ mod tests {
log_level = "trace"
data_dir = "/home/wizardsardine/custom/folder/"
# The main descriptor semantics aren't checked, yet.
main_descriptor = "wsh(andor(thresh(1,pk(xpub6BaZSKgpaVvibu2k78QsqeDWXp92xLHZxiu1WoqLB9hKhsBf3miBUDX7PJLgSPvkj66ThVHTqdnbXpeu8crXFmDUd4HeM4s4miQS2xsv3Qb/*)),and_v(v:multi(2,03b506a1dbe57b4bf48c95e0c7d417b87dd3b4349d290d2e7e9ba72c912652d80a,0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce),older(4)),thresh(2,pkh(xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU/*),a:pkh(xpub6AaffFGfH6WXfm6pwWzmUMuECQnoLeB3agMKaLyEBZ5ZVfwtnS5VJKqXBt8o5ooCWVy2H87GsZshp7DeKE25eWLyd1Ccuh2ZubQUkgpiVux/*))))#532k88vf"
main_descriptor = "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))#y5wcna2e"
[bitcoin_config]
network = "bitcoin"
@ -301,7 +298,7 @@ mod tests {
data_dir = "/home/wizardsardine/custom/folder/"
# The main descriptor semantics aren't checked, yet.
main_descriptor = "wsh(andor(thresh(1,pk(xpub6BaZSKgpaVvibu2k78QsqeDWXp92xLHZxiu1WoqLB9hKhsBf3miBUDX7PJLgSPvkj66ThVHTqdnbXpeu8crXFmDUd4HeM4s4miQS2xsv3Qb/*)),and_v(v:multi(2,03b506a1dbe57b4bf48c95e0c7d417b87dd3b4349d290d2e7e9ba72c912652d80a,0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce),older(4)),thresh(2,pkh(xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU/*),a:pkh(xpub6AaffFGfH6WXfm6pwWzmUMuECQnoLeB3agMKaLyEBZ5ZVfwtnS5VJKqXBt8o5ooCWVy2H87GsZshp7DeKE25eWLyd1Ccuh2ZubQUkgpiVux/*))))#532k8uvf"
main_descriptor = ""
[bitcoin_config]
poll_interval_secs = 18

View File

@ -18,14 +18,12 @@ use crate::{
},
Coin,
},
descriptors::InheritanceDescriptor,
};
use std::{convert::TryInto, fmt, io, path};
use miniscript::{
bitcoin::{self, secp256k1},
Descriptor, DescriptorPublicKey, DescriptorTrait, TranslatePk2,
};
use miniscript::bitcoin::{self, secp256k1};
const DB_VERSION: i64 = 0;
@ -35,7 +33,7 @@ pub enum SqliteDbError {
FileNotFound(path::PathBuf),
UnsupportedVersion(i64),
InvalidNetwork(bitcoin::Network),
DescriptorMismatch(Descriptor<DescriptorPublicKey>),
DescriptorMismatch(InheritanceDescriptor),
Rusqlite(rusqlite::Error),
}
@ -79,7 +77,7 @@ impl From<rusqlite::Error> for SqliteDbError {
#[derive(Debug, Clone)]
pub struct FreshDbOptions {
pub bitcoind_network: bitcoin::Network,
pub main_descriptor: Descriptor<DescriptorPublicKey>,
pub main_descriptor: InheritanceDescriptor,
}
#[derive(Debug, Clone)]
@ -118,7 +116,7 @@ impl SqliteDb {
pub fn sanity_check(
&self,
bitcoind_network: bitcoin::Network,
main_descriptor: &Descriptor<DescriptorPublicKey>,
main_descriptor: &InheritanceDescriptor,
) -> Result<(), SqliteDbError> {
let mut conn = self.connection()?;
@ -239,11 +237,8 @@ impl SqliteConn {
let next_la_index = next_index + LOOK_AHEAD_LIMIT - 1;
let next_la_address = db_wallet
.main_descriptor
.derive(next_la_index)
.translate_pk2(|xpk| xpk.derive_public_key(secp))
.expect("All pubkeys were derived, no wildcard.")
.address(network)
.expect("It's a wsh() descriptor");
.derive(next_la_index.into(), &secp)
.address(network);
db_tx
.execute(
"INSERT INTO addresses (address, derivation_index) VALUES (?1, ?2)",
@ -344,11 +339,10 @@ mod tests {
use std::{collections::HashSet, fs, path, str::FromStr};
use bitcoin::{hashes::Hash, util::bip32};
use miniscript::{DescriptorTrait, TranslatePk2};
fn dummy_options() -> FreshDbOptions {
let desc_str = "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))#y5wcna2d";
let main_descriptor = Descriptor::<DescriptorPublicKey>::from_str(desc_str).unwrap();
let main_descriptor = InheritanceDescriptor::from_str(desc_str).unwrap();
FreshDbOptions {
bitcoind_network: bitcoin::Network::Bitcoin,
main_descriptor,
@ -396,8 +390,8 @@ mod tests {
.to_string()
.contains("Database was created for network");
fs::remove_file(&db_path).unwrap();
let other_desc_str = "wsh(andor(pk(037a27a76ebf33594c785e4fa41607860a960bb5aa3039654297b05bff57e4f9a9),older(10000),pk(0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce)))";
let other_desc = Descriptor::<DescriptorPublicKey>::from_str(other_desc_str).unwrap();
let other_desc_str = "wsh(andor(pk(tpubDExU4YLJkyQ9RRbVScQq2brFxWWha7WmAUByPWyaWYwmcTv3Shx8aHp6mVwuE5n4TeM4z5DTWGf2YhNPmXtfvyr8cUDVvA3txdrFnFgNdF7/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))";
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)
.unwrap_err()
@ -522,33 +516,24 @@ mod tests {
// There is the index for the first index
let addr = options
.main_descriptor
.derive(0)
.translate_pk2(|xpk| xpk.derive_public_key(&secp))
.expect("All pubkeys were derived, no wildcard.")
.address(options.bitcoind_network)
.expect("Always a P2WSH address");
.derive(0.into(), &secp)
.address(options.bitcoind_network);
let db_addr = conn.db_address(&addr).unwrap();
assert_eq!(db_addr.derivation_index, 0.into());
// There is the index for the 199th index (look-ahead limit)
let addr = options
.main_descriptor
.derive(199)
.translate_pk2(|xpk| xpk.derive_public_key(&secp))
.expect("All pubkeys were derived, no wildcard.")
.address(options.bitcoind_network)
.expect("Always a P2WSH address");
.derive(199.into(), &secp)
.address(options.bitcoind_network);
let db_addr = conn.db_address(&addr).unwrap();
assert_eq!(db_addr.derivation_index, 199.into());
// And not for the 200th one.
let addr = options
.main_descriptor
.derive(200)
.translate_pk2(|xpk| xpk.derive_public_key(&secp))
.expect("All pubkeys were derived, no wildcard.")
.address(options.bitcoind_network)
.expect("Always a P2WSH address");
.derive(200.into(), &secp)
.address(options.bitcoind_network);
assert!(conn.db_address(&addr).is_none());
// But if we increment the deposit derivation index, the 200th one will be there.

View File

@ -1,9 +1,8 @@
use crate::descriptors::InheritanceDescriptor;
use std::{convert::TryFrom, str::FromStr};
use miniscript::{
bitcoin::{self, consensus::encode, util::bip32},
Descriptor, DescriptorPublicKey,
};
use miniscript::bitcoin::{self, consensus::encode, util::bip32};
pub const SCHEMA: &str = "\
CREATE TABLE version (
@ -86,7 +85,7 @@ impl TryFrom<&rusqlite::Row<'_>> for DbTip {
pub struct DbWallet {
pub id: i64,
pub timestamp: u32,
pub main_descriptor: Descriptor<DescriptorPublicKey>,
pub main_descriptor: InheritanceDescriptor,
pub deposit_derivation_index: bip32::ChildNumber,
}
@ -98,7 +97,7 @@ impl TryFrom<&rusqlite::Row<'_>> for DbWallet {
let timestamp = row.get(1)?;
let desc_str: String = row.get(2)?;
let main_descriptor = Descriptor::<DescriptorPublicKey>::from_str(&desc_str)
let main_descriptor = InheritanceDescriptor::from_str(&desc_str)
.expect("Insane database: can't parse deposit descriptor");
let der_idx: u32 = row.get(3)?;

View File

@ -2,7 +2,7 @@ use crate::database::sqlite::{schema::SCHEMA, FreshDbOptions, SqliteDbError, DB_
use std::{convert::TryInto, fs, path, time};
use miniscript::{bitcoin::secp256k1, DescriptorTrait, TranslatePk2};
use miniscript::bitcoin::secp256k1;
pub const LOOK_AHEAD_LIMIT: u32 = 200;
@ -106,11 +106,8 @@ pub fn create_fresh_db(
// TODO: have this as a helper in descriptors.rs
let address = options
.main_descriptor
.derive(index)
.translate_pk2(|xpk| xpk.derive_public_key(secp))
.expect("All pubkeys were derived, no wildcard.")
.address(options.bitcoind_network)
.expect("Always a P2WSH address");
.derive(index.into(), secp)
.address(options.bitcoind_network);
query += &format!(
"INSERT INTO addresses (address, derivation_index) VALUES (\"{}\", {});\n",
address, index

View File

@ -1,24 +1,32 @@
use miniscript::{
descriptor,
bitcoin::{self, hashes::hash160, hashes::Hash, secp256k1, util::bip32},
descriptor::{self, DescriptorTrait},
miniscript::{
decode::Terminal,
iter::PkPkh,
limits::{SEQUENCE_LOCKTIME_DISABLE_FLAG, SEQUENCE_LOCKTIME_TYPE_FLAG},
Miniscript,
},
ScriptContext,
policy::{Liftable, Semantic as SemanticPolicy},
MiniscriptKey, ScriptContext, ToPublicKey, TranslatePk2,
};
use std::{error, fmt, sync};
use std::{error, fmt, io::Write, str, sync};
use serde::{Deserialize, Serialize};
// Flag applied to the nSequence and CSV value before comparing them.
//
// <https://github.com/bitcoin/bitcoin/blob/4a540683ec40393d6369da1a9e02e45614db936d/src/primitives/transaction.h#L87-L89>
pub const SEQUENCE_LOCKTIME_MASK: u32 = 0x00_00_ff_ff;
#[derive(Debug, Clone)]
#[derive(Debug)]
pub enum DescCreationError {
InsaneTimelock(u32),
InvalidKey(descriptor::DescriptorPublicKey),
Miniscript(miniscript::Error),
IncompatibleDesc,
DerivedKeyParsing,
}
impl std::fmt::Display for DescCreationError {
@ -28,20 +36,126 @@ impl std::fmt::Display for DescCreationError {
Self::InvalidKey(key) => {
write!(f, "Invalid key '{}'. Need a wildcard ('ranged') xpub", key)
}
Self::Miniscript(e) => write!(f, "Miniscript error: '{}'.", e),
Self::IncompatibleDesc => write!(f, "Descriptor is not compatible."),
Self::DerivedKeyParsing => write!(f, "Parsing derived key,"),
}
}
}
impl error::Error for DescCreationError {}
/// A public key used in derived descriptors
#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)]
pub struct DerivedPublicKey {
/// Fingerprint of the master xpub and the derivation index used. We don't use a path
/// since we never derive at more than one depth.
pub origin: (bip32::Fingerprint, bip32::ChildNumber),
/// The actual key
pub key: bitcoin::PublicKey,
}
impl fmt::Display for DerivedPublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (fingerprint, deriv_index) = &self.origin;
write!(f, "[")?;
for byte in fingerprint.as_bytes().iter() {
write!(f, "{:02x}", byte)?;
}
write!(f, "/{}", deriv_index)?;
write!(f, "]{}", self.key)
}
}
impl str::FromStr for DerivedPublicKey {
type Err = DescCreationError;
fn from_str(s: &str) -> Result<DerivedPublicKey, Self::Err> {
// The key is always of the form:
// [ fingerprint / index ]<key>
// 1 + 8 + 1 + 1 + 1 + 66 minimum
if s.len() < 78 {
return Err(DescCreationError::DerivedKeyParsing);
}
// Non-ASCII?
for ch in s.as_bytes() {
if *ch < 20 || *ch > 127 {
return Err(DescCreationError::DerivedKeyParsing);
}
}
if s.chars().next().expect("Size checked above") != '[' {
return Err(DescCreationError::DerivedKeyParsing);
}
let mut parts = s[1..].split(']');
let fg_deriv = parts.next().ok_or(DescCreationError::DerivedKeyParsing)?;
let key_str = parts.next().ok_or(DescCreationError::DerivedKeyParsing)?;
if fg_deriv.len() < 10 {
return Err(DescCreationError::DerivedKeyParsing);
}
let fingerprint = bip32::Fingerprint::from_str(&fg_deriv[..8])
.map_err(|_| DescCreationError::DerivedKeyParsing)?;
let deriv_index = bip32::ChildNumber::from_str(&fg_deriv[9..])
.map_err(|_| DescCreationError::DerivedKeyParsing)?;
if deriv_index.is_hardened() {
return Err(DescCreationError::DerivedKeyParsing);
}
let key = bitcoin::PublicKey::from_str(&key_str)
.map_err(|_| DescCreationError::DerivedKeyParsing)?;
Ok(DerivedPublicKey {
key,
origin: (fingerprint, deriv_index),
})
}
}
impl MiniscriptKey for DerivedPublicKey {
// This allows us to be able to derive keys and key source even for PkH s
type Hash = Self;
fn is_uncompressed(&self) -> bool {
self.key.is_uncompressed()
}
fn to_pubkeyhash(&self) -> Self::Hash {
self.clone()
}
}
impl ToPublicKey for DerivedPublicKey {
fn to_public_key(&self) -> bitcoin::PublicKey {
self.key
}
fn hash_to_hash160(derived_key: &Self) -> hash160::Hash {
let mut engine = hash160::Hash::engine();
engine
.write_all(&derived_key.key.key.serialize())
.expect("engines don't error");
hash160::Hash::from_engine(engine)
}
}
// We require the locktime to:
// - not be disabled
// - be in number of blocks
// - be 'clean' / minimal, ie all bits without consensus meaning should be 0
fn csv_check(csv: u32) -> bool {
(csv & SEQUENCE_LOCKTIME_DISABLE_FLAG) == 0
fn csv_check(csv: u32) -> Result<(), DescCreationError> {
if (csv & SEQUENCE_LOCKTIME_DISABLE_FLAG) == 0
&& (csv & SEQUENCE_LOCKTIME_TYPE_FLAG) == 0
&& (csv & SEQUENCE_LOCKTIME_MASK) == csv
{
Ok(())
} else {
Err(DescCreationError::InsaneTimelock(csv))
}
}
fn is_unhardened_deriv(key: &descriptor::DescriptorPublicKey) -> bool {
@ -53,52 +167,219 @@ fn is_unhardened_deriv(key: &descriptor::DescriptorPublicKey) -> bool {
}
}
/// Create a Miniscript descriptor with a main, unencombered, branch (the main owner of the coins)
/// A Miniscript descriptor with a main, unencombered, branch (the main owner of the coins)
/// and a timelocked branch (the heir).
pub fn inheritance_descriptor(
owner_key: descriptor::DescriptorPublicKey,
heir_key: descriptor::DescriptorPublicKey,
timelock: u32,
) -> Result<descriptor::Descriptor<descriptor::DescriptorPublicKey>, DescCreationError> {
if !csv_check(timelock) {
return Err(DescCreationError::InsaneTimelock(timelock));
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InheritanceDescriptor(descriptor::Descriptor<descriptor::DescriptorPublicKey>);
/// Derived (containing only raw Bitcoin public keys) version of the inheritance descriptor.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DerivedInheritanceDescriptor(descriptor::Descriptor<DerivedPublicKey>);
impl fmt::Display for InheritanceDescriptor {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
if let Some(key) = vec![&owner_key, &heir_key]
.iter()
.find(|k| !is_unhardened_deriv(k))
{
return Err(DescCreationError::InvalidKey((**key).clone()));
impl str::FromStr for InheritanceDescriptor {
type Err = DescCreationError;
fn from_str(s: &str) -> Result<InheritanceDescriptor, Self::Err> {
let wsh_desc = descriptor::Wsh::<descriptor::DescriptorPublicKey>::from_str(s)
.map_err(DescCreationError::Miniscript)?;
let ms = match wsh_desc.as_inner() {
descriptor::WshInner::Ms(ms) => ms,
_ => return Err(DescCreationError::IncompatibleDesc),
};
let invalid_key = ms.iter_pk_pkh().find_map(|pk_pkh| {
let pk = match pk_pkh {
PkPkh::PlainPubkey(pk) => pk,
PkPkh::HashedPubkey(pk) => pk,
};
if is_unhardened_deriv(&pk) {
None
} else {
Some(pk)
}
});
if let Some(key) = invalid_key {
return Err(DescCreationError::InvalidKey(key));
}
// Semantic of the Miniscript must be either the owner now, or the heir after
// a timelock.
let policy = ms
.lift()
.expect("Lifting can't fail on a Miniscript")
.normalized();
let subs = match policy {
SemanticPolicy::Threshold(1, subs) => Some(subs),
_ => None,
}
.ok_or(DescCreationError::IncompatibleDesc)?;
if subs.len() != 2 {
return Err(DescCreationError::IncompatibleDesc);
}
// Owner branch
subs.iter()
.find(|s| matches!(s, SemanticPolicy::KeyHash(_)))
.ok_or(DescCreationError::IncompatibleDesc)?;
// Heir branch
let heir_subs = subs
.iter()
.find_map(|s| match s {
SemanticPolicy::Threshold(2, subs) => Some(subs),
_ => None,
})
.ok_or(DescCreationError::IncompatibleDesc)?;
if heir_subs.len() != 2 {
return Err(DescCreationError::IncompatibleDesc);
}
// Must be timelocked
let csv = heir_subs
.iter()
.find_map(|s| match s {
SemanticPolicy::Older(csv) => Some(csv),
_ => None,
})
.ok_or(DescCreationError::IncompatibleDesc)?;
csv_check(*csv)?;
// And key locked
heir_subs
.iter()
.find(|s| matches!(s, SemanticPolicy::KeyHash(_)))
.ok_or(DescCreationError::IncompatibleDesc)?;
Ok(InheritanceDescriptor(descriptor::Descriptor::Wsh(wsh_desc)))
}
}
let owner_pk = Miniscript::from_ast(Terminal::Check(sync::Arc::from(
Miniscript::from_ast(Terminal::PkK(owner_key)).expect("TODO"),
)))
.expect("Well typed");
impl InheritanceDescriptor {
pub fn new(
owner_key: descriptor::DescriptorPublicKey,
heir_key: descriptor::DescriptorPublicKey,
timelock: u32,
) -> Result<InheritanceDescriptor, DescCreationError> {
csv_check(timelock)?;
let heir_pkh = Miniscript::from_ast(Terminal::Check(sync::Arc::from(
Miniscript::from_ast(Terminal::PkH(heir_key)).expect("TODO"),
)))
.expect("Well typed");
if let Some(key) = vec![&owner_key, &heir_key]
.iter()
.find(|k| !is_unhardened_deriv(k))
{
return Err(DescCreationError::InvalidKey((**key).clone()));
}
let heir_timelock = Terminal::Older(timelock);
let heir_branch = Miniscript::from_ast(Terminal::AndV(
Miniscript::from_ast(Terminal::Verify(heir_pkh.into()))
.expect("Well typed")
.into(),
Miniscript::from_ast(heir_timelock)
.expect("Well typed")
.into(),
))
.expect("Well typed");
let tl_miniscript = Miniscript::from_ast(Terminal::OrD(owner_pk.into(), heir_branch.into()))
let owner_pk = Miniscript::from_ast(Terminal::Check(sync::Arc::from(
Miniscript::from_ast(Terminal::PkK(owner_key)).expect("TODO"),
)))
.expect("Well typed");
miniscript::Segwitv0::check_local_validity(&tl_miniscript).expect("Miniscript must be sane");
Ok(descriptor::Descriptor::Wsh(
descriptor::Wsh::new(tl_miniscript).expect("Must pass sanity checks"),
))
let heir_pkh = Miniscript::from_ast(Terminal::Check(sync::Arc::from(
Miniscript::from_ast(Terminal::PkH(heir_key)).expect("TODO"),
)))
.expect("Well typed");
let heir_timelock = Terminal::Older(timelock);
let heir_branch = Miniscript::from_ast(Terminal::AndV(
Miniscript::from_ast(Terminal::Verify(heir_pkh.into()))
.expect("Well typed")
.into(),
Miniscript::from_ast(heir_timelock)
.expect("Well typed")
.into(),
))
.expect("Well typed");
let tl_miniscript =
Miniscript::from_ast(Terminal::OrD(owner_pk.into(), heir_branch.into()))
.expect("Well typed");
miniscript::Segwitv0::check_local_validity(&tl_miniscript)
.expect("Miniscript must be sane");
Ok(InheritanceDescriptor(descriptor::Descriptor::Wsh(
descriptor::Wsh::new(tl_miniscript).expect("Must pass sanity checks"),
)))
}
pub fn as_inner(&self) -> &descriptor::Descriptor<descriptor::DescriptorPublicKey> {
&self.0
}
/// Derive this descriptor at a given index.
pub fn derive(
&self,
index: bip32::ChildNumber,
secp: &secp256k1::Secp256k1<impl secp256k1::Verification>,
) -> DerivedInheritanceDescriptor {
assert!(index.is_normal());
let desc = self
.0
.derive(index.into())
.translate_pk2(|xpk| {
xpk.derive_public_key(secp).map(|key| {
// FIXME: rust-miniscript will panic if we call
// xpk.master_fingerprint() on a key without origin
let origin = match xpk {
descriptor::DescriptorPublicKey::XPub(..) => {
(xpk.master_fingerprint(), index)
}
_ => unreachable!("All keys are always xpubs"),
};
DerivedPublicKey { key, origin }
})
})
.expect("All pubkeys are derived, no wildcard.");
DerivedInheritanceDescriptor(desc)
}
/// Get the value (in blocks) of the relative timelock for the heir's spending path.
pub fn timelock_value(&self) -> u32 {
let wsh_desc = match &self.0 {
descriptor::Descriptor::Wsh(desc) => desc,
_ => unreachable!(),
};
let ms = match wsh_desc.as_inner() {
descriptor::WshInner::Ms(ms) => ms,
_ => unreachable!(),
};
let policy = ms
.lift()
.expect("Lifting can't fail on a Miniscript")
.normalized();
let subs = match policy {
SemanticPolicy::Threshold(1, subs) => subs,
_ => unreachable!(),
};
let heir_subs = subs
.iter()
.find_map(|s| match s {
SemanticPolicy::Threshold(2, subs) => Some(subs),
_ => None,
})
.expect("Always present");
let csv = heir_subs
.iter()
.find_map(|s| match s {
SemanticPolicy::Older(csv) => Some(csv),
_ => None,
})
.expect("Always present");
*csv
}
}
impl DerivedInheritanceDescriptor {
pub fn address(&self, network: bitcoin::Network) -> bitcoin::Address {
self.0
.address(network)
.expect("A P2WSH always has an address")
}
}
#[cfg(test)]
@ -112,27 +393,52 @@ mod tests {
let owner_key = descriptor::DescriptorPublicKey::from_str(&"xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/*").unwrap();
let heir_key = descriptor::DescriptorPublicKey::from_str(&"xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/*").unwrap();
let timelock = 52560;
assert_eq!(inheritance_descriptor(owner_key.clone(), heir_key.clone(), timelock).unwrap().to_string(), "wsh(or_d(pk(xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/*),and_v(v:pkh(xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/*),older(52560))))#eeyujkt7");
assert_eq!(InheritanceDescriptor::new(owner_key.clone(), heir_key.clone(), timelock).unwrap().to_string(), "wsh(or_d(pk(xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/*),and_v(v:pkh(xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/*),older(52560))))#eeyujkt7");
// We prevent footguns with timelocks
inheritance_descriptor(owner_key.clone(), heir_key.clone(), 0x00_01_0f_00).unwrap_err();
inheritance_descriptor(owner_key.clone(), heir_key.clone(), (1 << 31) + 1).unwrap_err();
inheritance_descriptor(owner_key.clone(), heir_key.clone(), (1 << 22) + 1).unwrap_err();
InheritanceDescriptor::new(owner_key.clone(), heir_key.clone(), 0x00_01_0f_00).unwrap_err();
InheritanceDescriptor::new(owner_key.clone(), heir_key.clone(), (1 << 31) + 1).unwrap_err();
InheritanceDescriptor::new(owner_key.clone(), heir_key.clone(), (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 timelock = 57600;
assert_eq!(inheritance_descriptor(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/*),and_v(v:pkh(xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/*),older(57600))))#8kamh6y8");
// 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();
inheritance_descriptor(owner_key.clone(), heir_key, timelock).unwrap_err();
InheritanceDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err();
let heir_key = descriptor::DescriptorPublicKey::from_str(&"xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/*'").unwrap();
inheritance_descriptor(owner_key.clone(), heir_key, timelock).unwrap_err();
InheritanceDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err();
let heir_key = descriptor::DescriptorPublicKey::from_str(
&"02e24913be26dbcfdf8e8e94870b28725cdae09b448b6c127767bf0154e3a3c8e5",
)
.unwrap();
inheritance_descriptor(owner_key.clone(), heir_key, timelock).unwrap_err();
InheritanceDescriptor::new(owner_key.clone(), 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 der_desc = desc.derive(11.into(), &secp);
assert_eq!(
"bc1qvjzcg25nsxmfccct0txjvljxjwn68htkrw57jqmjhfzvhyd2z4msc74w65",
der_desc.address(bitcoin::Network::Bitcoin).to_string()
);
}
#[test]
fn inheritance_descriptor_tl_value() {
let desc = InheritanceDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(1),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))").unwrap();
assert_eq!(desc.timelock_value(), 1);
let desc = InheritanceDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(42000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))").unwrap();
assert_eq!(desc.timelock_value(), 42000);
let desc = InheritanceDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(65535),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))").unwrap();
assert_eq!(desc.timelock_value(), 0xffff);
}
// TODO: test error conditions of deserialization.
}

View File

@ -377,10 +377,11 @@ mod tests {
use super::*;
use crate::{
config::{BitcoinConfig, BitcoindConfig},
descriptors::InheritanceDescriptor,
testutils::*,
};
use miniscript::{bitcoin, Descriptor, DescriptorPublicKey};
use miniscript::bitcoin;
use std::{
fs,
io::{BufRead, BufReader, Write},
@ -588,7 +589,7 @@ mod tests {
// Create a dummy config with this bitcoind
let desc_str = "wsh(andor(pk(xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/*),older(10000),pk(xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/*)))#tk6wzexy";
let desc = Descriptor::<DescriptorPublicKey>::from_str(desc_str).unwrap();
let desc = InheritanceDescriptor::from_str(desc_str).unwrap();
let config = Config {
bitcoin_config,
bitcoind_config: Some(bitcoind_config),

View File

@ -177,7 +177,8 @@ impl DummyMinisafe {
let owner_key = descriptor::DescriptorPublicKey::from_str("xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/*").unwrap();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/*").unwrap();
let desc = crate::descriptors::inheritance_descriptor(owner_key, heir_key, 10_000).unwrap();
let desc =
crate::descriptors::InheritanceDescriptor::new(owner_key, heir_key, 10_000).unwrap();
let config = Config {
bitcoin_config,
bitcoind_config: None,

View File

@ -117,7 +117,7 @@ def minisafed(bitcoind, directory):
os.makedirs(datadir, exist_ok=True)
bitcoind_cookie = os.path.join(bitcoind.bitcoin_dir, "regtest", ".cookie")
main_desc = "wsh(or_d(pk(tpubD9vQiBdDxYzU1V5D5UUmMTXF9FZC13PuQDs4aiv6rF7UCKQFvtVKZguYakX12C2bt8736ksioxu9Y9Nmp18gj4jDeNJEEqrBPEZXAxe5YcQ/*),and_v(v:pkh(tpubD9vQiBdDxYzU4cVFtApWj4devZrvcfWaPXX1zHdDc7GPfUsDKqGnbhraccfm7BAXgRgUbVQUV2v2o4NitjGEk7hpbuP85kvBrD4ahFDtNBJ/*),older(157680))))"
main_desc = "wsh(or_d(pk(tpubD9vQiBdDxYzU1V5D5UUmMTXF9FZC13PuQDs4aiv6rF7UCKQFvtVKZguYakX12C2bt8736ksioxu9Y9Nmp18gj4jDeNJEEqrBPEZXAxe5YcQ/*),and_v(v:pkh(tpubD9vQiBdDxYzU4cVFtApWj4devZrvcfWaPXX1zHdDc7GPfUsDKqGnbhraccfm7BAXgRgUbVQUV2v2o4NitjGEk7hpbuP85kvBrD4ahFDtNBJ/*),older(65000))))"
minisafed = Minisafed(
datadir,