mk_codec/bytecode/
xpub_compact.rs1use bitcoin::NetworkKind;
18use bitcoin::bip32::{ChainCode, ChildNumber, DerivationPath, Fingerprint, Xpub};
19use bitcoin::secp256k1::PublicKey;
20
21use crate::consts::XPUB_COMPACT_BYTES;
22use crate::error::{Error, Result};
23
24const MAINNET_XPUB_VERSION: [u8; 4] = [0x04, 0x88, 0xB2, 0x1E];
26
27const TESTNET_XPUB_VERSION: [u8; 4] = [0x04, 0x35, 0x87, 0xCF];
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct XpubCompact {
33 pub version: [u8; 4],
35 pub parent_fingerprint: [u8; 4],
37 pub chain_code: [u8; 32],
39 pub public_key: [u8; 33],
41}
42
43impl XpubCompact {
44 pub fn from_xpub(xpub: &Xpub) -> Self {
46 let version = network_to_version(xpub.network);
47 XpubCompact {
48 version,
49 parent_fingerprint: xpub.parent_fingerprint.to_bytes(),
50 chain_code: xpub.chain_code.to_bytes(),
51 public_key: xpub.public_key.serialize(),
52 }
53 }
54}
55
56fn network_to_version(network: NetworkKind) -> [u8; 4] {
57 match network {
58 NetworkKind::Main => MAINNET_XPUB_VERSION,
59 NetworkKind::Test => TESTNET_XPUB_VERSION,
60 }
61}
62
63fn version_to_network(version: [u8; 4]) -> Result<NetworkKind> {
64 match version {
65 MAINNET_XPUB_VERSION => Ok(NetworkKind::Main),
66 TESTNET_XPUB_VERSION => Ok(NetworkKind::Test),
67 other => Err(Error::InvalidXpubVersion(u32::from_be_bytes(other))),
68 }
69}
70
71pub fn reconstruct_xpub(compact: &XpubCompact, origin_path: &DerivationPath) -> Result<Xpub> {
86 let network = version_to_network(compact.version)?;
87 let components: Vec<ChildNumber> = origin_path.into_iter().copied().collect();
88 let depth = components.len() as u8;
89 let child_number = components
93 .last()
94 .copied()
95 .expect("origin_path must be non-empty per SPEC §3.5");
96 let public_key = PublicKey::from_slice(&compact.public_key)
97 .map_err(|e| Error::InvalidXpubPublicKey(format!("{e}")))?;
98 Ok(Xpub {
99 network,
100 depth,
101 parent_fingerprint: Fingerprint::from(compact.parent_fingerprint),
102 child_number,
103 public_key,
104 chain_code: ChainCode::from(compact.chain_code),
105 })
106}
107
108pub fn encode_xpub_compact(compact: &XpubCompact, out: &mut Vec<u8>) {
110 out.extend_from_slice(&compact.version);
111 out.extend_from_slice(&compact.parent_fingerprint);
112 out.extend_from_slice(&compact.chain_code);
113 out.extend_from_slice(&compact.public_key);
114}
115
116pub fn decode_xpub_compact(cursor: &mut &[u8]) -> Result<XpubCompact> {
118 if cursor.len() < XPUB_COMPACT_BYTES {
119 return Err(Error::UnexpectedEnd);
120 }
121 let version: [u8; 4] = cursor[0..4].try_into().unwrap();
122 let _ = version_to_network(version)?;
125 let parent_fingerprint: [u8; 4] = cursor[4..8].try_into().unwrap();
126 let chain_code: [u8; 32] = cursor[8..40].try_into().unwrap();
127 let public_key: [u8; 33] = cursor[40..73].try_into().unwrap();
128 *cursor = &cursor[XPUB_COMPACT_BYTES..];
129 Ok(XpubCompact {
130 version,
131 parent_fingerprint,
132 chain_code,
133 public_key,
134 })
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::bytecode::test_helpers::synthetic_xpub;
141 use std::str::FromStr;
142
143 #[test]
144 fn round_trip_full_xpub_depth_4() {
145 let path = DerivationPath::from_str("m/48'/0'/0'/2'").unwrap();
146 let xpub_full = synthetic_xpub(&path);
147 let compact = XpubCompact::from_xpub(&xpub_full);
148 let mut wire = Vec::new();
150 encode_xpub_compact(&compact, &mut wire);
151 assert_eq!(wire.len(), XPUB_COMPACT_BYTES);
152 let mut cursor: &[u8] = &wire;
154 let decoded = decode_xpub_compact(&mut cursor).unwrap();
155 assert_eq!(decoded, compact);
156 assert!(cursor.is_empty());
157 let reconstructed = reconstruct_xpub(&decoded, &path).unwrap();
159 assert_eq!(reconstructed.depth, 4);
160 assert_eq!(reconstructed.network, xpub_full.network);
161 assert_eq!(
162 reconstructed.parent_fingerprint,
163 xpub_full.parent_fingerprint
164 );
165 assert_eq!(reconstructed.chain_code, xpub_full.chain_code);
166 assert_eq!(reconstructed.public_key, xpub_full.public_key);
167 assert_eq!(reconstructed.child_number, xpub_full.child_number);
169 }
170
171 #[test]
172 fn rejects_invalid_version() {
173 let mut wire = vec![0xDE, 0xAD, 0xBE, 0xEF];
175 wire.extend_from_slice(&[0u8; 4 + 32 + 33]);
176 let mut cursor: &[u8] = &wire;
177 assert!(matches!(
178 decode_xpub_compact(&mut cursor),
179 Err(Error::InvalidXpubVersion(_)),
180 ));
181 }
182
183 #[test]
184 fn rejects_truncated_input() {
185 let wire = vec![0x04, 0x88]; let mut cursor: &[u8] = &wire;
187 assert!(matches!(
188 decode_xpub_compact(&mut cursor),
189 Err(Error::UnexpectedEnd),
190 ));
191 }
192}