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> {
87 let network = version_to_network(compact.version)?;
88 let components: Vec<ChildNumber> = origin_path.into_iter().copied().collect();
89 let depth = components.len() as u8;
90 let child_number = components
95 .last()
96 .copied()
97 .unwrap_or(ChildNumber::Normal { index: 0 });
98 let public_key = PublicKey::from_slice(&compact.public_key)
99 .map_err(|e| Error::InvalidXpubPublicKey(format!("{e}")))?;
100 Ok(Xpub {
101 network,
102 depth,
103 parent_fingerprint: Fingerprint::from(compact.parent_fingerprint),
104 child_number,
105 public_key,
106 chain_code: ChainCode::from(compact.chain_code),
107 })
108}
109
110pub fn encode_xpub_compact(compact: &XpubCompact, out: &mut Vec<u8>) {
112 out.extend_from_slice(&compact.version);
113 out.extend_from_slice(&compact.parent_fingerprint);
114 out.extend_from_slice(&compact.chain_code);
115 out.extend_from_slice(&compact.public_key);
116}
117
118pub fn decode_xpub_compact(cursor: &mut &[u8]) -> Result<XpubCompact> {
120 if cursor.len() < XPUB_COMPACT_BYTES {
121 return Err(Error::UnexpectedEnd);
122 }
123 let version: [u8; 4] = cursor[0..4].try_into().unwrap();
124 let _ = version_to_network(version)?;
127 let parent_fingerprint: [u8; 4] = cursor[4..8].try_into().unwrap();
128 let chain_code: [u8; 32] = cursor[8..40].try_into().unwrap();
129 let public_key: [u8; 33] = cursor[40..73].try_into().unwrap();
130 *cursor = &cursor[XPUB_COMPACT_BYTES..];
131 Ok(XpubCompact {
132 version,
133 parent_fingerprint,
134 chain_code,
135 public_key,
136 })
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::bytecode::test_helpers::synthetic_xpub;
143 use std::str::FromStr;
144
145 #[test]
146 fn round_trip_full_xpub_depth_4() {
147 let path = DerivationPath::from_str("m/48'/0'/0'/2'").unwrap();
148 let xpub_full = synthetic_xpub(&path);
149 let compact = XpubCompact::from_xpub(&xpub_full);
150 let mut wire = Vec::new();
152 encode_xpub_compact(&compact, &mut wire);
153 assert_eq!(wire.len(), XPUB_COMPACT_BYTES);
154 let mut cursor: &[u8] = &wire;
156 let decoded = decode_xpub_compact(&mut cursor).unwrap();
157 assert_eq!(decoded, compact);
158 assert!(cursor.is_empty());
159 let reconstructed = reconstruct_xpub(&decoded, &path).unwrap();
161 assert_eq!(reconstructed.depth, 4);
162 assert_eq!(reconstructed.network, xpub_full.network);
163 assert_eq!(
164 reconstructed.parent_fingerprint,
165 xpub_full.parent_fingerprint
166 );
167 assert_eq!(reconstructed.chain_code, xpub_full.chain_code);
168 assert_eq!(reconstructed.public_key, xpub_full.public_key);
169 assert_eq!(reconstructed.child_number, xpub_full.child_number);
171 }
172
173 #[test]
174 fn reconstruct_depth0_empty_path() {
175 let path = DerivationPath::from_str("m").unwrap(); let xpub_full = synthetic_xpub(&path); let compact = XpubCompact::from_xpub(&xpub_full);
178 let reconstructed = reconstruct_xpub(&compact, &path).unwrap();
179 assert_eq!(reconstructed.depth, 0);
180 assert_eq!(reconstructed.child_number, ChildNumber::Normal { index: 0 });
181 assert_eq!(
182 reconstructed.parent_fingerprint,
183 xpub_full.parent_fingerprint
184 );
185 assert_eq!(reconstructed.chain_code, xpub_full.chain_code);
186 assert_eq!(reconstructed.public_key, xpub_full.public_key);
187 assert_eq!(reconstructed.network, xpub_full.network);
188 }
189
190 #[test]
191 fn rejects_invalid_version() {
192 let mut wire = vec![0xDE, 0xAD, 0xBE, 0xEF];
194 wire.extend_from_slice(&[0u8; 4 + 32 + 33]);
195 let mut cursor: &[u8] = &wire;
196 assert!(matches!(
197 decode_xpub_compact(&mut cursor),
198 Err(Error::InvalidXpubVersion(_)),
199 ));
200 }
201
202 #[test]
203 fn rejects_truncated_input() {
204 let wire = vec![0x04, 0x88]; let mut cursor: &[u8] = &wire;
206 assert!(matches!(
207 decode_xpub_compact(&mut cursor),
208 Err(Error::UnexpectedEnd),
209 ));
210 }
211}