#![cfg(feature = "derive")]
use bitcoin::Network;
use bitcoin::bip32::{DerivationPath, Xpriv, Xpub};
use bitcoin::secp256k1::Secp256k1;
use md_codec::tree::{Body, Node};
use md_codec::use_site_path::UseSitePath;
use md_codec::{Descriptor, OriginPath, PathComponent, PathDecl, PathDeclPaths, Tag, TlvSection};
use std::str::FromStr;
const ABANDON_MNEMONIC: &str =
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
fn account_xpub_bytes(path_str: &str) -> [u8; 65] {
let mn = bip39::Mnemonic::parse(ABANDON_MNEMONIC).expect("known good mnemonic");
let seed = mn.to_seed("");
let secp = Secp256k1::new();
let master = Xpriv::new_master(Network::Bitcoin, &seed).expect("seed → master");
let path = DerivationPath::from_str(path_str).expect("valid path");
let account_xpriv = master.derive_priv(&secp, &path).expect("derive priv");
let account_xpub = Xpub::from_priv(&secp, &account_xpriv);
let mut out = [0u8; 65];
out[..32].copy_from_slice(account_xpub.chain_code.as_ref());
out[32..].copy_from_slice(&account_xpub.public_key.serialize());
out
}
fn origin(components: &[(bool, u32)]) -> OriginPath {
OriginPath {
components: components
.iter()
.map(|&(hardened, value)| PathComponent { hardened, value })
.collect(),
}
}
fn pkk(index: u8) -> Node {
Node {
tag: Tag::PkK,
body: Body::KeyArg { index },
}
}
#[test]
fn bip84_wpkh_receive_address_zero() {
let xpub_bytes = account_xpub_bytes("m/84'/0'/0'");
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(origin(&[(true, 84), (true, 0), (true, 0)])),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0u8, xpub_bytes)]);
t
},
};
let addr = d.derive_address(0, 0, Network::Bitcoin).unwrap();
assert_eq!(
addr.assume_checked().to_string(),
"bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"
);
}
#[test]
fn bip84_wpkh_receive_address_one() {
let xpub_bytes = account_xpub_bytes("m/84'/0'/0'");
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(origin(&[(true, 84), (true, 0), (true, 0)])),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0u8, xpub_bytes)]);
t
},
};
let addr = d.derive_address(0, 1, Network::Bitcoin).unwrap();
assert_eq!(
addr.assume_checked().to_string(),
"bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g"
);
}
#[test]
fn bip84_wpkh_change_address_zero() {
let xpub_bytes = account_xpub_bytes("m/84'/0'/0'");
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(origin(&[(true, 84), (true, 0), (true, 0)])),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0u8, xpub_bytes)]);
t
},
};
let addr = d.derive_address(1, 0, Network::Bitcoin).unwrap();
assert_eq!(
addr.assume_checked().to_string(),
"bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el"
);
}
#[test]
fn bip86_tr_keypath_only_receive_address_zero() {
let xpub_bytes = account_xpub_bytes("m/86'/0'/0'");
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(origin(&[(true, 86), (true, 0), (true, 0)])),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Tr,
body: Body::Tr {
is_nums: false,
key_index: 0,
tree: None,
},
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0u8, xpub_bytes)]);
t
},
};
let addr = d.derive_address(0, 0, Network::Bitcoin).unwrap();
assert_eq!(
addr.assume_checked().to_string(),
"bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr"
);
}
#[test]
fn bip44_pkh_receive_address_zero() {
let xpub_bytes = account_xpub_bytes("m/44'/0'/0'");
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(origin(&[(true, 44), (true, 0), (true, 0)])),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Pkh,
body: Body::KeyArg { index: 0 },
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0u8, xpub_bytes)]);
t
},
};
let addr = d.derive_address(0, 0, Network::Bitcoin).unwrap();
assert_eq!(
addr.assume_checked().to_string(),
"1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA"
);
}
#[test]
fn bip84_wpkh_testnet_address() {
let xpub_bytes = account_xpub_bytes("m/84'/0'/0'");
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(origin(&[(true, 84), (true, 0), (true, 0)])),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0u8, xpub_bytes)]);
t
},
};
let addr = d.derive_address(0, 0, Network::Testnet).unwrap();
let s = addr.assume_checked().to_string();
assert!(s.starts_with("tb1q"), "expected testnet bech32, got {s}");
}
#[test]
fn wsh_sortedmulti_2_of_3_address() {
use bitcoin::bip32::ChildNumber;
let xpub_a = account_xpub_bytes("m/48'/0'/0'/2'");
let xpub_b = account_xpub_bytes("m/48'/0'/1'/2'");
let xpub_c = account_xpub_bytes("m/48'/0'/2'/2'");
let d = Descriptor {
n: 3,
path_decl: PathDecl {
n: 3,
paths: PathDeclPaths::Shared(origin(&[(true, 48), (true, 0), (true, 0), (true, 2)])),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 2,
indices: vec![0, 1, 2],
},
}]),
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0u8, xpub_a), (1u8, xpub_b), (2u8, xpub_c)]);
t
},
};
let addr = d.derive_address(0, 0, Network::Bitcoin).unwrap();
let got = addr.assume_checked().to_string();
let secp = Secp256k1::verification_only();
let mut pks: Vec<bitcoin::secp256k1::PublicKey> = vec![];
for bytes in [&xpub_a, &xpub_b, &xpub_c] {
let mut chain_code = [0u8; 32];
chain_code.copy_from_slice(&bytes[..32]);
let pubkey = bitcoin::secp256k1::PublicKey::from_slice(&bytes[32..]).unwrap();
let xpub = Xpub {
network: bitcoin::NetworkKind::Main,
depth: 0,
parent_fingerprint: Default::default(),
child_number: ChildNumber::Normal { index: 0 },
public_key: pubkey,
chain_code: bitcoin::bip32::ChainCode::from(chain_code),
};
let leaf = xpub
.derive_pub(
&secp,
&[
ChildNumber::Normal { index: 0 },
ChildNumber::Normal { index: 0 },
],
)
.unwrap();
pks.push(leaf.public_key);
}
pks.sort_by_key(|p| p.serialize());
let mut b = bitcoin::blockdata::script::Builder::new().push_int(2);
for p in &pks {
b = b.push_key(&bitcoin::PublicKey::new(*p));
}
let script = b
.push_int(3)
.push_opcode(bitcoin::opcodes::all::OP_CHECKMULTISIG)
.into_script();
let expected = bitcoin::Address::p2wsh(&script, Network::Bitcoin).to_string();
assert_eq!(got, expected);
assert!(
got.starts_with("bc1q"),
"expected mainnet wsh bech32, got {got}"
);
}
#[test]
fn sh_wsh_sortedmulti_2_of_3_address() {
use bitcoin::bip32::ChildNumber;
let xpub_a = account_xpub_bytes("m/48'/0'/0'/1'");
let xpub_b = account_xpub_bytes("m/48'/0'/1'/1'");
let xpub_c = account_xpub_bytes("m/48'/0'/2'/1'");
let d = Descriptor {
n: 3,
path_decl: PathDecl {
n: 3,
paths: PathDeclPaths::Shared(origin(&[(true, 48), (true, 0), (true, 0), (true, 1)])),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Sh,
body: Body::Children(vec![Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 2,
indices: vec![0, 1, 2],
},
}]),
}]),
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0u8, xpub_a), (1u8, xpub_b), (2u8, xpub_c)]);
t
},
};
let addr = d.derive_address(0, 0, Network::Bitcoin).unwrap();
let got = addr.assume_checked().to_string();
let secp = Secp256k1::verification_only();
let mut pks: Vec<bitcoin::secp256k1::PublicKey> = vec![];
for bytes in [&xpub_a, &xpub_b, &xpub_c] {
let mut chain_code = [0u8; 32];
chain_code.copy_from_slice(&bytes[..32]);
let pubkey = bitcoin::secp256k1::PublicKey::from_slice(&bytes[32..]).unwrap();
let xpub = Xpub {
network: bitcoin::NetworkKind::Main,
depth: 0,
parent_fingerprint: Default::default(),
child_number: ChildNumber::Normal { index: 0 },
public_key: pubkey,
chain_code: bitcoin::bip32::ChainCode::from(chain_code),
};
let leaf = xpub
.derive_pub(
&secp,
&[
ChildNumber::Normal { index: 0 },
ChildNumber::Normal { index: 0 },
],
)
.unwrap();
pks.push(leaf.public_key);
}
pks.sort_by_key(|p| p.serialize());
let mut b = bitcoin::blockdata::script::Builder::new().push_int(2);
for p in &pks {
b = b.push_key(&bitcoin::PublicKey::new(*p));
}
let script = b
.push_int(3)
.push_opcode(bitcoin::opcodes::all::OP_CHECKMULTISIG)
.into_script();
let expected = bitcoin::Address::p2shwsh(&script, Network::Bitcoin).to_string();
assert_eq!(got, expected);
assert!(
got.starts_with('3'),
"expected mainnet P2SH-form, got {got}"
);
}
fn xpub_bytes_to_string(bytes: &[u8; 65]) -> String {
use bitcoin::NetworkKind;
use bitcoin::bip32::{ChainCode, ChildNumber, Fingerprint, Xpub};
use bitcoin::secp256k1::PublicKey;
let mut chain_code = [0u8; 32];
chain_code.copy_from_slice(&bytes[..32]);
let pk = PublicKey::from_slice(&bytes[32..]).unwrap();
let xpub = Xpub {
network: NetworkKind::Main,
depth: 0,
parent_fingerprint: Fingerprint::default(),
child_number: ChildNumber::Normal { index: 0 },
public_key: pk,
chain_code: ChainCode::from(chain_code),
};
xpub.to_string()
}
fn miniscript_direct_address(
descriptor_str: &str,
chain: u32,
index: u32,
network: Network,
) -> String {
let desc = miniscript::Descriptor::<miniscript::DescriptorPublicKey>::from_str(descriptor_str)
.expect("parse descriptor template");
let single = if desc.is_multipath() {
let alternatives = desc
.clone()
.into_single_descriptors()
.expect("split multipath");
alternatives
.into_iter()
.nth(chain as usize)
.expect("chain in range")
} else {
desc
};
let definite = single.at_derivation_index(index).expect("derivation idx");
definite.address(network).expect("address").to_string()
}
const MULTIPATH_TAIL: &str = "/<0;1>/*";
#[test]
fn sh_sortedmulti_2_of_3_address() {
let xpub_a = account_xpub_bytes("m/45'/0'");
let xpub_b = account_xpub_bytes("m/45'/1'");
let xpub_c = account_xpub_bytes("m/45'/2'");
let d = Descriptor {
n: 3,
path_decl: PathDecl {
n: 3,
paths: PathDeclPaths::Divergent(vec![
origin(&[(true, 45), (true, 0)]),
origin(&[(true, 45), (true, 1)]),
origin(&[(true, 45), (true, 2)]),
]),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Sh,
body: Body::Children(vec![Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 2,
indices: vec![0, 1, 2],
},
}]),
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0u8, xpub_a), (1u8, xpub_b), (2u8, xpub_c)]);
t
},
};
let got = d
.derive_address(0, 0, Network::Bitcoin)
.unwrap()
.assume_checked()
.to_string();
let template = format!(
"sh(sortedmulti(2,{a}{m},{b}{m},{c}{m}))",
a = xpub_bytes_to_string(&xpub_a),
b = xpub_bytes_to_string(&xpub_b),
c = xpub_bytes_to_string(&xpub_c),
m = MULTIPATH_TAIL,
);
let expected = miniscript_direct_address(&template, 0, 0, Network::Bitcoin);
assert_eq!(got, expected);
assert!(
got.starts_with('3'),
"expected mainnet P2SH-form, got {got}"
);
}
#[test]
fn tr_nums_single_pk_leaf_address() {
let xpub_a = account_xpub_bytes("m/86'/0'/0'");
let nums = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0";
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(origin(&[(true, 86), (true, 0), (true, 0)])),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Tr,
body: Body::Tr {
is_nums: true,
key_index: 0,
tree: Some(Box::new(pkk(0))),
},
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0u8, xpub_a)]);
t
},
};
let got = d
.derive_address(0, 0, Network::Bitcoin)
.unwrap()
.assume_checked()
.to_string();
let template = format!(
"tr({nums},pk({a}{m}))",
a = xpub_bytes_to_string(&xpub_a),
m = MULTIPATH_TAIL,
);
let expected = miniscript_direct_address(&template, 0, 0, Network::Bitcoin);
assert_eq!(got, expected);
assert!(got.starts_with("bc1p"), "expected mainnet P2TR, got {got}");
}
#[test]
fn tr_single_pk_leaf_address() {
let xpub_a = account_xpub_bytes("m/86'/0'/0'");
let xpub_b = account_xpub_bytes("m/86'/0'/1'");
let d = Descriptor {
n: 2,
path_decl: PathDecl {
n: 2,
paths: PathDeclPaths::Divergent(vec![
origin(&[(true, 86), (true, 0), (true, 0)]),
origin(&[(true, 86), (true, 0), (true, 1)]),
]),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Tr,
body: Body::Tr {
is_nums: false,
key_index: 0,
tree: Some(Box::new(pkk(1))),
},
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0u8, xpub_a), (1u8, xpub_b)]);
t
},
};
let got = d
.derive_address(0, 0, Network::Bitcoin)
.unwrap()
.assume_checked()
.to_string();
let template = format!(
"tr({a}{m},pk({b}{m}))",
a = xpub_bytes_to_string(&xpub_a),
b = xpub_bytes_to_string(&xpub_b),
m = MULTIPATH_TAIL,
);
let expected = miniscript_direct_address(&template, 0, 0, Network::Bitcoin);
assert_eq!(got, expected);
}
#[test]
fn tr_multi_a_2_of_3_leaf_address() {
let xpub_a = account_xpub_bytes("m/86'/0'/0'");
let xpub_b = account_xpub_bytes("m/86'/0'/1'");
let xpub_c = account_xpub_bytes("m/86'/0'/2'");
let xpub_d = account_xpub_bytes("m/86'/0'/3'");
let d = Descriptor {
n: 4,
path_decl: PathDecl {
n: 4,
paths: PathDeclPaths::Divergent(vec![
origin(&[(true, 86), (true, 0), (true, 0)]),
origin(&[(true, 86), (true, 0), (true, 1)]),
origin(&[(true, 86), (true, 0), (true, 2)]),
origin(&[(true, 86), (true, 0), (true, 3)]),
]),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Tr,
body: Body::Tr {
is_nums: false,
key_index: 0,
tree: Some(Box::new(Node {
tag: Tag::MultiA,
body: Body::MultiKeys {
k: 2,
indices: vec![1, 2, 3],
},
})),
},
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![
(0u8, xpub_a),
(1u8, xpub_b),
(2u8, xpub_c),
(3u8, xpub_d),
]);
t
},
};
let got = d
.derive_address(0, 0, Network::Bitcoin)
.unwrap()
.assume_checked()
.to_string();
let template = format!(
"tr({a}{m},multi_a(2,{b}{m},{c}{m},{d}{m}))",
a = xpub_bytes_to_string(&xpub_a),
b = xpub_bytes_to_string(&xpub_b),
c = xpub_bytes_to_string(&xpub_c),
d = xpub_bytes_to_string(&xpub_d),
m = MULTIPATH_TAIL,
);
let expected = miniscript_direct_address(&template, 0, 0, Network::Bitcoin);
assert_eq!(got, expected);
}
#[test]
fn wsh_check_pk_k_address() {
let xpub_a = account_xpub_bytes("m/84'/0'/0'");
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(origin(&[(true, 84), (true, 0), (true, 0)])),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wsh,
body: Body::Children(vec![pkk(0)]),
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0u8, xpub_a)]);
t
},
};
let got = d
.derive_address(0, 0, Network::Bitcoin)
.unwrap()
.assume_checked()
.to_string();
let template = format!(
"wsh(pk({a}{m}))",
a = xpub_bytes_to_string(&xpub_a),
m = MULTIPATH_TAIL,
);
let expected = miniscript_direct_address(&template, 0, 0, Network::Bitcoin);
assert_eq!(got, expected);
}
#[test]
fn tr_branching_two_leaf_address() {
let xpub_a = account_xpub_bytes("m/86'/0'/0'");
let xpub_b = account_xpub_bytes("m/86'/0'/1'");
let xpub_c = account_xpub_bytes("m/86'/0'/2'");
let d = Descriptor {
n: 3,
path_decl: PathDecl {
n: 3,
paths: PathDeclPaths::Divergent(vec![
origin(&[(true, 86), (true, 0), (true, 0)]),
origin(&[(true, 86), (true, 0), (true, 1)]),
origin(&[(true, 86), (true, 0), (true, 2)]),
]),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Tr,
body: Body::Tr {
is_nums: false,
key_index: 0,
tree: Some(Box::new(Node {
tag: Tag::TapTree,
body: Body::Children(vec![pkk(1), pkk(2)]),
})),
},
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0u8, xpub_a), (1u8, xpub_b), (2u8, xpub_c)]);
t
},
};
let got = d
.derive_address(0, 0, Network::Bitcoin)
.unwrap()
.assume_checked()
.to_string();
let template = format!(
"tr({a}{m},{{pk({b}{m}),pk({c}{m})}})",
a = xpub_bytes_to_string(&xpub_a),
b = xpub_bytes_to_string(&xpub_b),
c = xpub_bytes_to_string(&xpub_c),
m = MULTIPATH_TAIL,
);
let expected = miniscript_direct_address(&template, 0, 0, Network::Bitcoin);
assert_eq!(got, expected);
}
#[test]
fn tr_branching_with_multi_a_address() {
let xpub_a = account_xpub_bytes("m/86'/0'/0'");
let xpub_b = account_xpub_bytes("m/86'/0'/1'");
let xpub_c = account_xpub_bytes("m/86'/0'/2'");
let xpub_d = account_xpub_bytes("m/86'/0'/3'");
let d = Descriptor {
n: 4,
path_decl: PathDecl {
n: 4,
paths: PathDeclPaths::Divergent(vec![
origin(&[(true, 86), (true, 0), (true, 0)]),
origin(&[(true, 86), (true, 0), (true, 1)]),
origin(&[(true, 86), (true, 0), (true, 2)]),
origin(&[(true, 86), (true, 0), (true, 3)]),
]),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Tr,
body: Body::Tr {
is_nums: false,
key_index: 0,
tree: Some(Box::new(Node {
tag: Tag::TapTree,
body: Body::Children(vec![
pkk(1),
Node {
tag: Tag::MultiA,
body: Body::MultiKeys {
k: 2,
indices: vec![2, 3],
},
},
]),
})),
},
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![
(0u8, xpub_a),
(1u8, xpub_b),
(2u8, xpub_c),
(3u8, xpub_d),
]);
t
},
};
let got = d
.derive_address(0, 0, Network::Bitcoin)
.unwrap()
.assume_checked()
.to_string();
let template = format!(
"tr({a}{m},{{pk({b}{m}),multi_a(2,{c}{m},{d}{m})}})",
a = xpub_bytes_to_string(&xpub_a),
b = xpub_bytes_to_string(&xpub_b),
c = xpub_bytes_to_string(&xpub_c),
d = xpub_bytes_to_string(&xpub_d),
m = MULTIPATH_TAIL,
);
let expected = miniscript_direct_address(&template, 0, 0, Network::Bitcoin);
assert_eq!(got, expected);
}
#[test]
fn wsh_and_v_address() {
let xpub_a = account_xpub_bytes("m/84'/0'/0'");
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(origin(&[(true, 84), (true, 0), (true, 0)])),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::AndV,
body: Body::Children(vec![
Node {
tag: Tag::Verify,
body: Body::Children(vec![pkk(0)]),
},
Node {
tag: Tag::Older,
body: Body::Timelock(144),
},
]),
}]),
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0u8, xpub_a)]);
t
},
};
let got = d
.derive_address(0, 0, Network::Bitcoin)
.unwrap()
.assume_checked()
.to_string();
let template = format!(
"wsh(and_v(v:pk({a}{m}),older(144)))",
a = xpub_bytes_to_string(&xpub_a),
m = MULTIPATH_TAIL,
);
let expected = miniscript_direct_address(&template, 0, 0, Network::Bitcoin);
assert_eq!(got, expected);
}
#[test]
fn wsh_thresh_address() {
let xpub_a = account_xpub_bytes("m/48'/0'/0'/2'");
let xpub_b = account_xpub_bytes("m/48'/0'/1'/2'");
let xpub_c = account_xpub_bytes("m/48'/0'/2'/2'");
let d = Descriptor {
n: 3,
path_decl: PathDecl {
n: 3,
paths: PathDeclPaths::Divergent(vec![
origin(&[(true, 48), (true, 0), (true, 0), (true, 2)]),
origin(&[(true, 48), (true, 0), (true, 1), (true, 2)]),
origin(&[(true, 48), (true, 0), (true, 2), (true, 2)]),
]),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::Thresh,
body: Body::Variable {
k: 2,
children: vec![
pkk(0),
Node {
tag: Tag::Swap,
body: Body::Children(vec![pkk(1)]),
},
Node {
tag: Tag::Swap,
body: Body::Children(vec![pkk(2)]),
},
],
},
}]),
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0u8, xpub_a), (1u8, xpub_b), (2u8, xpub_c)]);
t
},
};
let got = d
.derive_address(0, 0, Network::Bitcoin)
.unwrap()
.assume_checked()
.to_string();
let template = format!(
"wsh(thresh(2,pk({a}{m}),s:pk({b}{m}),s:pk({c}{m})))",
a = xpub_bytes_to_string(&xpub_a),
b = xpub_bytes_to_string(&xpub_b),
c = xpub_bytes_to_string(&xpub_c),
m = MULTIPATH_TAIL,
);
let expected = miniscript_direct_address(&template, 0, 0, Network::Bitcoin);
assert_eq!(got, expected);
}
#[test]
fn round_trip_then_derive_address() {
let xpub_bytes = account_xpub_bytes("m/84'/0'/0'");
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(origin(&[(true, 84), (true, 0), (true, 0)])),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0u8, xpub_bytes)]);
t
},
};
let direct = d
.derive_address(0, 0, Network::Bitcoin)
.unwrap()
.assume_checked()
.to_string();
let s = md_codec::encode_md1_string(&d).unwrap();
let decoded = md_codec::decode_md1_string(&s).unwrap();
let after = decoded
.derive_address(0, 0, Network::Bitcoin)
.unwrap()
.assume_checked()
.to_string();
assert_eq!(direct, after);
assert_eq!(after, "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu");
}