use bitcoin::NetworkKind;
use bitcoin::bip32::{ChainCode, ChildNumber, DerivationPath, Fingerprint, Xpub};
use bitcoin::secp256k1::PublicKey;
use crate::consts::XPUB_COMPACT_BYTES;
use crate::error::{Error, Result};
const MAINNET_XPUB_VERSION: [u8; 4] = [0x04, 0x88, 0xB2, 0x1E];
const TESTNET_XPUB_VERSION: [u8; 4] = [0x04, 0x35, 0x87, 0xCF];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XpubCompact {
pub version: [u8; 4],
pub parent_fingerprint: [u8; 4],
pub chain_code: [u8; 32],
pub public_key: [u8; 33],
}
impl XpubCompact {
pub fn from_xpub(xpub: &Xpub) -> Self {
let version = network_to_version(xpub.network);
XpubCompact {
version,
parent_fingerprint: xpub.parent_fingerprint.to_bytes(),
chain_code: xpub.chain_code.to_bytes(),
public_key: xpub.public_key.serialize(),
}
}
}
fn network_to_version(network: NetworkKind) -> [u8; 4] {
match network {
NetworkKind::Main => MAINNET_XPUB_VERSION,
NetworkKind::Test => TESTNET_XPUB_VERSION,
}
}
fn version_to_network(version: [u8; 4]) -> Result<NetworkKind> {
match version {
MAINNET_XPUB_VERSION => Ok(NetworkKind::Main),
TESTNET_XPUB_VERSION => Ok(NetworkKind::Test),
other => Err(Error::InvalidXpubVersion(u32::from_be_bytes(other))),
}
}
pub fn reconstruct_xpub(compact: &XpubCompact, origin_path: &DerivationPath) -> Result<Xpub> {
let network = version_to_network(compact.version)?;
let components: Vec<ChildNumber> = origin_path.into_iter().copied().collect();
let depth = components.len() as u8;
let child_number = components
.last()
.copied()
.expect("origin_path must be non-empty per SPEC §3.5");
let public_key = PublicKey::from_slice(&compact.public_key)
.map_err(|e| Error::InvalidXpubPublicKey(format!("{e}")))?;
Ok(Xpub {
network,
depth,
parent_fingerprint: Fingerprint::from(compact.parent_fingerprint),
child_number,
public_key,
chain_code: ChainCode::from(compact.chain_code),
})
}
pub fn encode_xpub_compact(compact: &XpubCompact, out: &mut Vec<u8>) {
out.extend_from_slice(&compact.version);
out.extend_from_slice(&compact.parent_fingerprint);
out.extend_from_slice(&compact.chain_code);
out.extend_from_slice(&compact.public_key);
}
pub fn decode_xpub_compact(cursor: &mut &[u8]) -> Result<XpubCompact> {
if cursor.len() < XPUB_COMPACT_BYTES {
return Err(Error::UnexpectedEnd);
}
let version: [u8; 4] = cursor[0..4].try_into().unwrap();
let _ = version_to_network(version)?;
let parent_fingerprint: [u8; 4] = cursor[4..8].try_into().unwrap();
let chain_code: [u8; 32] = cursor[8..40].try_into().unwrap();
let public_key: [u8; 33] = cursor[40..73].try_into().unwrap();
*cursor = &cursor[XPUB_COMPACT_BYTES..];
Ok(XpubCompact {
version,
parent_fingerprint,
chain_code,
public_key,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bytecode::test_helpers::synthetic_xpub;
use std::str::FromStr;
#[test]
fn round_trip_full_xpub_depth_4() {
let path = DerivationPath::from_str("m/48'/0'/0'/2'").unwrap();
let xpub_full = synthetic_xpub(&path);
let compact = XpubCompact::from_xpub(&xpub_full);
let mut wire = Vec::new();
encode_xpub_compact(&compact, &mut wire);
assert_eq!(wire.len(), XPUB_COMPACT_BYTES);
let mut cursor: &[u8] = &wire;
let decoded = decode_xpub_compact(&mut cursor).unwrap();
assert_eq!(decoded, compact);
assert!(cursor.is_empty());
let reconstructed = reconstruct_xpub(&decoded, &path).unwrap();
assert_eq!(reconstructed.depth, 4);
assert_eq!(reconstructed.network, xpub_full.network);
assert_eq!(
reconstructed.parent_fingerprint,
xpub_full.parent_fingerprint
);
assert_eq!(reconstructed.chain_code, xpub_full.chain_code);
assert_eq!(reconstructed.public_key, xpub_full.public_key);
assert_eq!(reconstructed.child_number, xpub_full.child_number);
}
#[test]
fn rejects_invalid_version() {
let mut wire = vec![0xDE, 0xAD, 0xBE, 0xEF];
wire.extend_from_slice(&[0u8; 4 + 32 + 33]);
let mut cursor: &[u8] = &wire;
assert!(matches!(
decode_xpub_compact(&mut cursor),
Err(Error::InvalidXpubVersion(_)),
));
}
#[test]
fn rejects_truncated_input() {
let wire = vec![0x04, 0x88]; let mut cursor: &[u8] = &wire;
assert!(matches!(
decode_xpub_compact(&mut cursor),
Err(Error::UnexpectedEnd),
));
}
}