use crate::bitstream::{BitWriter, re_emit_bits};
use crate::canonicalize::{canonicalize_placeholder_indices, expand_per_at_n};
use crate::encode::{Descriptor, encode_payload};
use crate::error::Error;
use crate::phrase::Phrase;
use crate::varint::write_varint;
use bitcoin::hashes::{Hash, sha256};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Md1EncodingId([u8; 16]);
impl Md1EncodingId {
pub fn new(bytes: [u8; 16]) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
pub fn fingerprint(&self) -> [u8; 4] {
let mut fp = [0u8; 4];
fp.copy_from_slice(&self.0[0..4]);
fp
}
}
pub fn compute_md1_encoding_id(d: &Descriptor) -> Result<Md1EncodingId, Error> {
let (bytes, _bit_len) = encode_payload(d)?;
let hash = sha256::Hash::hash(&bytes);
let mut id = [0u8; 16];
id.copy_from_slice(&hash.to_byte_array()[0..16]);
Ok(Md1EncodingId(id))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WalletDescriptorTemplateId([u8; 16]);
impl WalletDescriptorTemplateId {
pub fn new(bytes: [u8; 16]) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
}
pub fn compute_wallet_descriptor_template_id(
d: &Descriptor,
) -> Result<WalletDescriptorTemplateId, Error> {
let mut w = BitWriter::new();
let kiw = d.key_index_width();
d.use_site_path.write(&mut w)?;
crate::tree::write_node(&mut w, &d.tree, kiw)?;
if let Some(overrides) = &d.tlv.use_site_path_overrides {
let mut sub = BitWriter::new();
for (idx, path) in overrides {
sub.write_bits(u64::from(*idx), kiw as usize);
path.write(&mut sub)?;
}
let bit_len = sub.bit_len();
w.write_bits(u64::from(crate::tlv::TLV_USE_SITE_PATH_OVERRIDES), 5);
crate::varint::write_varint(&mut w, bit_len as u32)?;
let payload = sub.into_bytes();
let mut subr = crate::bitstream::BitReader::new(&payload);
let mut remaining = bit_len;
while remaining > 0 {
let chunk = remaining.min(8);
let bits = subr.read_bits(chunk)?;
w.write_bits(bits, chunk);
remaining -= chunk;
}
}
let bytes = w.into_bytes();
let hash = sha256::Hash::hash(&bytes);
let mut id = [0u8; 16];
id.copy_from_slice(&hash.to_byte_array()[0..16]);
Ok(WalletDescriptorTemplateId(id))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WalletPolicyId([u8; 16]);
impl WalletPolicyId {
pub fn new(bytes: [u8; 16]) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
pub fn to_phrase(&self) -> Result<Phrase, Error> {
Phrase::from_id_bytes(self.as_bytes())
}
}
pub fn compute_wallet_policy_id(d: &Descriptor) -> Result<WalletPolicyId, Error> {
let mut d_canonical = d.clone();
canonicalize_placeholder_indices(&mut d_canonical)?;
let d = &d_canonical;
let mut tree_w = BitWriter::new();
crate::tree::write_node(&mut tree_w, &d.tree, d.key_index_width())?;
let canonical_template_tree_bytes = tree_w.into_bytes();
let expanded = expand_per_at_n(d)?;
let mut records_concat: Vec<u8> = Vec::new();
for e in &expanded {
let mut path_scratch = BitWriter::new();
e.origin_path.write(&mut path_scratch)?;
let path_bit_len = path_scratch.bit_len();
let path_bytes = path_scratch.into_bytes();
let mut us_scratch = BitWriter::new();
e.use_site_path.write(&mut us_scratch)?;
let use_site_bit_len = us_scratch.bit_len();
let us_bytes = us_scratch.into_bytes();
let mut record_bw = BitWriter::new();
write_varint(&mut record_bw, path_bit_len as u32)?;
re_emit_bits(&mut record_bw, &path_bytes, path_bit_len)?;
write_varint(&mut record_bw, use_site_bit_len as u32)?;
re_emit_bits(&mut record_bw, &us_bytes, use_site_bit_len)?;
let record_bytes = record_bw.into_bytes();
let fp_present = e.fingerprint.is_some();
let xpub_present = e.xpub.is_some();
let presence_byte = ((fp_present as u8) | ((xpub_present as u8) << 1)) & 0b0000_0011;
records_concat.push(presence_byte);
records_concat.extend_from_slice(&record_bytes);
if let Some(fp) = e.fingerprint {
records_concat.extend_from_slice(&fp);
}
if let Some(xpub) = e.xpub {
records_concat.extend_from_slice(&xpub);
}
}
let mut hash_input: Vec<u8> =
Vec::with_capacity(canonical_template_tree_bytes.len() + records_concat.len());
hash_input.extend_from_slice(&canonical_template_tree_bytes);
hash_input.extend_from_slice(&records_concat);
let hash = sha256::Hash::hash(&hash_input);
let mut id = [0u8; 16];
id.copy_from_slice(&hash.to_byte_array()[0..16]);
Ok(WalletPolicyId(id))
}
pub fn validate_presence_byte(byte: u8) -> Result<(), Error> {
let reserved_bits = byte & 0b1111_1100;
if reserved_bits != 0 {
return Err(Error::InvalidPresenceByte { reserved_bits });
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::origin_path::{OriginPath, PathComponent, PathDecl, PathDeclPaths};
use crate::tag::Tag;
use crate::tlv::TlvSection;
use crate::tree::{Body, Node};
use crate::use_site_path::UseSitePath;
fn bip84_descriptor() -> Descriptor {
Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(OriginPath {
components: vec![
PathComponent {
hardened: true,
value: 84,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 0,
},
],
}),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
},
tlv: TlvSection::new_empty(),
}
}
#[test]
fn md1_encoding_id_deterministic() {
let d = bip84_descriptor();
let id1 = compute_md1_encoding_id(&d).unwrap();
let id2 = compute_md1_encoding_id(&d).unwrap();
assert_eq!(id1, id2);
}
#[test]
fn md1_encoding_id_differs_for_different_paths() {
let d1 = bip84_descriptor();
let mut d2 = bip84_descriptor();
if let PathDeclPaths::Shared(p) = &mut d2.path_decl.paths {
p.components[2] = PathComponent {
hardened: true,
value: 1,
};
}
let id1 = compute_md1_encoding_id(&d1).unwrap();
let id2 = compute_md1_encoding_id(&d2).unwrap();
assert_ne!(id1, id2);
}
#[test]
fn wdt_id_invariant_to_origin_path_change() {
let d1 = bip84_descriptor();
let mut d2 = bip84_descriptor();
if let PathDeclPaths::Shared(p) = &mut d2.path_decl.paths {
p.components[2] = PathComponent {
hardened: true,
value: 1,
};
}
let id1 = compute_wallet_descriptor_template_id(&d1).unwrap();
let id2 = compute_wallet_descriptor_template_id(&d2).unwrap();
assert_eq!(id1, id2);
}
#[test]
fn wdt_id_differs_for_different_use_site_paths() {
let d1 = bip84_descriptor();
let mut d2 = bip84_descriptor();
d2.use_site_path = UseSitePath {
multipath: None,
wildcard_hardened: false,
};
let id1 = compute_wallet_descriptor_template_id(&d1).unwrap();
let id2 = compute_wallet_descriptor_template_id(&d2).unwrap();
assert_ne!(id1, id2);
}
#[test]
fn wdt_id_invariant_to_fingerprint_addition() {
let d1 = bip84_descriptor();
let mut d2 = bip84_descriptor();
d2.tlv.fingerprints = Some(vec![(0u8, [0xaa, 0xbb, 0xcc, 0xdd])]);
let id1 = compute_wallet_descriptor_template_id(&d1).unwrap();
let id2 = compute_wallet_descriptor_template_id(&d2).unwrap();
assert_eq!(id1, id2);
}
fn deterministic_xpub() -> [u8; 65] {
let mut x = [0u8; 65];
for b in x.iter_mut().take(32) {
*b = 0x11;
}
x[32] = 0x02;
for b in x.iter_mut().skip(33) {
*b = 0x22;
}
x
}
fn cell_7_wpkh_descriptor() -> Descriptor {
Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(OriginPath {
components: vec![
PathComponent {
hardened: true,
value: 84,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 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.fingerprints = Some(vec![(0u8, [0xDE, 0xAD, 0xBE, 0xEF])]);
t.pubkeys = Some(vec![(0u8, deterministic_xpub())]);
t
},
}
}
#[test]
fn golden_vector_wpkh_cell_7() {
let d = cell_7_wpkh_descriptor();
let path = match &d.path_decl.paths {
PathDeclPaths::Shared(p) => p.clone(),
_ => panic!("test fixture is shared"),
};
let mut path_scratch = crate::bitstream::BitWriter::new();
path.write(&mut path_scratch).unwrap();
let path_bit_len = path_scratch.bit_len();
let path_bytes = path_scratch.into_bytes();
assert_eq!(path_bit_len, 26, "BIP-84 origin path is 26 bits");
assert_eq!(path_bytes, vec![0x3b, 0xd4, 0x84, 0x00]);
let mut us_scratch = crate::bitstream::BitWriter::new();
d.use_site_path.write(&mut us_scratch).unwrap();
let use_site_bit_len = us_scratch.bit_len();
let us_bytes = us_scratch.into_bytes();
assert_eq!(use_site_bit_len, 16, "<0;1>/* use-site is 16 bits");
assert_eq!(us_bytes, vec![0x80, 0x06]);
let mut record_bw = crate::bitstream::BitWriter::new();
crate::varint::write_varint(&mut record_bw, path_bit_len as u32).unwrap();
crate::bitstream::re_emit_bits(&mut record_bw, &path_bytes, path_bit_len).unwrap();
crate::varint::write_varint(&mut record_bw, use_site_bit_len as u32).unwrap();
crate::bitstream::re_emit_bits(&mut record_bw, &us_bytes, use_site_bit_len).unwrap();
let varint_path_cost = 4 + (32 - (path_bit_len as u32).leading_zeros()) as usize;
let varint_us_cost = 4 + (32 - (use_site_bit_len as u32).leading_zeros()) as usize;
let expected_record_bits =
varint_path_cost + path_bit_len + varint_us_cost + use_site_bit_len;
assert_eq!(record_bw.bit_len(), expected_record_bits);
assert_eq!(record_bw.bit_len(), 60, "cell-7 record is 60 bits");
let record_bytes = record_bw.into_bytes();
assert_eq!(
record_bytes,
vec![0x5d, 0x1d, 0xea, 0x42, 0x0b, 0x08, 0x00, 0x60]
);
let mut tree_w = crate::bitstream::BitWriter::new();
crate::tree::write_node(&mut tree_w, &d.tree, d.key_index_width()).unwrap();
let tree_bytes = tree_w.into_bytes();
assert_eq!(tree_bytes, vec![0x00]);
let presence_byte: u8 = 0x03;
let fp = [0xDE, 0xAD, 0xBE, 0xEF];
let xpub = deterministic_xpub();
let mut expected_hash_input: Vec<u8> = Vec::new();
expected_hash_input.extend_from_slice(&tree_bytes);
expected_hash_input.push(presence_byte);
expected_hash_input.extend_from_slice(&record_bytes);
expected_hash_input.extend_from_slice(&fp);
expected_hash_input.extend_from_slice(&xpub);
assert_eq!(expected_hash_input.len(), 79);
let expected_hex = "00035d1dea420b080060deadbeef\
1111111111111111111111111111111111111111111111111111111111111111\
02\
2222222222222222222222222222222222222222222222222222222222222222";
assert_eq!(hex(&expected_hash_input), expected_hex);
let expected_id: [u8; 16] = [
0x66, 0x50, 0xb9, 0x80, 0x3b, 0x3c, 0x66, 0x21, 0x01, 0x40, 0x54, 0x0d, 0xa8, 0xd7,
0x65, 0xa0,
];
let id = compute_wallet_policy_id(&d).unwrap();
assert_eq!(*id.as_bytes(), expected_id);
}
fn hex(bs: &[u8]) -> String {
let mut s = String::with_capacity(bs.len() * 2);
for b in bs {
s.push_str(&format!("{:02x}", b));
}
s
}
#[test]
fn walletpolicyid_stable_across_origin_elision() {
let d_explicit = cell_7_wpkh_descriptor();
let mut d_override = cell_7_wpkh_descriptor();
let bip84 = match &d_override.path_decl.paths {
PathDeclPaths::Shared(p) => p.clone(),
_ => panic!(),
};
d_override.tlv.origin_path_overrides = Some(vec![(0u8, bip84)]);
let id1 = compute_wallet_policy_id(&d_explicit).unwrap();
let id2 = compute_wallet_policy_id(&d_override).unwrap();
assert_eq!(id1, id2);
}
#[test]
fn walletpolicyid_stable_across_use_site_elision() {
let d_baseline = cell_7_wpkh_descriptor();
let mut d_override = cell_7_wpkh_descriptor();
d_override.use_site_path = UseSitePath {
multipath: None,
wildcard_hardened: false,
};
d_override.tlv.use_site_path_overrides =
Some(vec![(0u8, UseSitePath::standard_multipath())]);
let id1 = compute_wallet_policy_id(&d_baseline).unwrap();
let id2 = compute_wallet_policy_id(&d_override).unwrap();
assert_eq!(id1, id2);
}
#[test]
fn walletpolicyid_template_only_differs_from_full_cell_7() {
let full = cell_7_wpkh_descriptor();
let mut template_only = cell_7_wpkh_descriptor();
template_only.tlv.fingerprints = None;
template_only.tlv.pubkeys = None;
let id_full = compute_wallet_policy_id(&full).unwrap();
let id_template = compute_wallet_policy_id(&template_only).unwrap();
assert_ne!(id_full, id_template);
}
#[test]
fn walletpolicyid_partial_keys_distinct() {
#[allow(dead_code)]
fn pkk(index: u8) -> Node {
Node {
tag: Tag::PkK,
body: Body::KeyArg { index },
}
}
let bip48_2 = OriginPath {
components: vec![
PathComponent {
hardened: true,
value: 48,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 2,
},
],
};
let mk_d = |fps: Option<Vec<(u8, [u8; 4])>>, pks: Option<Vec<(u8, [u8; 65])>>| Descriptor {
n: 2,
path_decl: PathDecl {
n: 2,
paths: PathDeclPaths::Shared(bip48_2.clone()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::Multi,
body: Body::MultiKeys {
k: 2,
indices: vec![0, 1],
},
}]),
},
tlv: {
let mut t = TlvSection::new_empty();
t.fingerprints = fps;
t.pubkeys = pks;
t
},
};
let xpub = deterministic_xpub();
let d_full = mk_d(
Some(vec![(0, [0x11; 4]), (1, [0x22; 4])]),
Some(vec![(0, xpub), (1, xpub)]),
);
let d_mixed = mk_d(Some(vec![(0, [0x11; 4])]), Some(vec![(0, xpub)]));
let id_full = compute_wallet_policy_id(&d_full).unwrap();
let id_mixed = compute_wallet_policy_id(&d_mixed).unwrap();
assert_ne!(id_full, id_mixed);
}
#[test]
fn walletpolicyid_wrapper_context_in_template_hash() {
let d_wpkh = cell_7_wpkh_descriptor();
let mut d_pkh = cell_7_wpkh_descriptor();
d_pkh.tree = Node {
tag: Tag::Pkh,
body: Body::KeyArg { index: 0 },
};
d_pkh.path_decl = PathDecl {
n: 1,
paths: PathDeclPaths::Shared(OriginPath {
components: vec![
PathComponent {
hardened: true,
value: 44,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 0,
},
],
}),
};
d_pkh.path_decl = d_wpkh.path_decl.clone();
let id_wpkh = compute_wallet_policy_id(&d_wpkh).unwrap();
let id_pkh = compute_wallet_policy_id(&d_pkh).unwrap();
assert_ne!(id_wpkh, id_pkh);
}
#[test]
fn walletpolicyid_reserved_bits_masking_property() {
let common = vec![0x00u8, 0x42, 0x42, 0x42];
let candidates = [0b0000_0011u8, 0b1111_1111u8];
let mask = 0b0000_0011u8;
let masked_a = candidates[0] & mask;
let masked_b = candidates[1] & mask;
assert_eq!(masked_a, masked_b);
let mut input_a = common.clone();
input_a.push(masked_a);
let mut input_b = common.clone();
input_b.push(masked_b);
let h_a = bitcoin::hashes::sha256::Hash::hash(&input_a);
let h_b = bitcoin::hashes::sha256::Hash::hash(&input_b);
assert_eq!(h_a, h_b);
let mut unmasked_a = common.clone();
unmasked_a.push(candidates[0]);
let mut unmasked_b = common.clone();
unmasked_b.push(candidates[1]);
let h_a_raw = bitcoin::hashes::sha256::Hash::hash(&unmasked_a);
let h_b_raw = bitcoin::hashes::sha256::Hash::hash(&unmasked_b);
assert_ne!(h_a_raw, h_b_raw);
}
#[test]
fn walletpolicyid_to_phrase_returns_12_bip39_words() {
let d = cell_7_wpkh_descriptor();
let id = compute_wallet_policy_id(&d).unwrap();
let phrase = id.to_phrase().unwrap();
assert_eq!(phrase.0.len(), 12);
for word in &phrase.0 {
assert!(!word.is_empty());
}
}
#[test]
fn compute_wallet_policy_id_canonicalizes_first() {
#[allow(dead_code)]
fn pkk(index: u8) -> Node {
Node {
tag: Tag::PkK,
body: Body::KeyArg { index },
}
}
let xpub_a = deterministic_xpub();
let mut xpub_b = deterministic_xpub();
xpub_b[0] = 0x33;
let bip48_2 = OriginPath {
components: vec![
PathComponent {
hardened: true,
value: 48,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 2,
},
],
};
let d_non_canonical = Descriptor {
n: 2,
path_decl: PathDecl {
n: 2,
paths: PathDeclPaths::Shared(bip48_2.clone()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::Multi,
body: Body::MultiKeys {
k: 2,
indices: vec![1, 0],
},
}]),
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0, xpub_a), (1, xpub_b)]);
t
},
};
let d_canonical = Descriptor {
n: 2,
path_decl: PathDecl {
n: 2,
paths: PathDeclPaths::Shared(bip48_2),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::Multi,
body: Body::MultiKeys {
k: 2,
indices: vec![0, 1],
},
}]),
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0, xpub_b), (1, xpub_a)]);
t
},
};
let id_nc = compute_wallet_policy_id(&d_non_canonical).unwrap();
let id_c = compute_wallet_policy_id(&d_canonical).unwrap();
assert_eq!(id_nc, id_c);
}
#[test]
fn validate_presence_byte_accepts_all_four_legal_combinations() {
for byte in [0b00, 0b01, 0b10, 0b11] {
validate_presence_byte(byte).unwrap();
}
}
#[test]
fn validate_presence_byte_rejects_lowest_reserved_bit() {
let err = validate_presence_byte(0b0000_0100).unwrap_err();
assert!(matches!(
err,
Error::InvalidPresenceByte {
reserved_bits: 0b0000_0100
}
));
}
#[test]
fn validate_presence_byte_rejects_high_reserved_bit_with_legal_low_bits() {
let err = validate_presence_byte(0b1000_0011).unwrap_err();
assert!(matches!(
err,
Error::InvalidPresenceByte {
reserved_bits: 0b1000_0000
}
));
}
#[test]
fn validate_presence_byte_rejects_all_bits_set() {
let err = validate_presence_byte(0xFF).unwrap_err();
assert!(matches!(
err,
Error::InvalidPresenceByte {
reserved_bits: 0b1111_1100
}
));
}
}