Skip to main content

chains_sdk/
encoding.rs

1//! Shared encoding utilities used across all chain modules.
2//!
3//! Centralizes Bitcoin compact-size (varint), Bech32/Bech32m, and Base58Check
4//! encoding so chain modules don't duplicate these building blocks.
5
6use crate::crypto;
7use crate::error::SignerError;
8
9// ─── Compact Size (Bitcoin VarInt) ──────────────────────────────────
10
11/// Encode a Bitcoin compact-size integer into a buffer.
12///
13/// | Range | Encoding |
14/// |-------|----------|
15/// | 0–0xFC | 1 byte |
16/// | 0xFD–0xFFFF | 0xFD + 2 bytes LE |
17/// | 0x10000–0xFFFFFFFF | 0xFE + 4 bytes LE |
18/// | 0x100000000+ | 0xFF + 8 bytes LE |
19pub fn encode_compact_size(buf: &mut Vec<u8>, value: u64) {
20    if value < 0xFD {
21        buf.push(value as u8);
22    } else if value <= 0xFFFF {
23        buf.push(0xFD);
24        buf.extend_from_slice(&(value as u16).to_le_bytes());
25    } else if value <= 0xFFFF_FFFF {
26        buf.push(0xFE);
27        buf.extend_from_slice(&(value as u32).to_le_bytes());
28    } else {
29        buf.push(0xFF);
30        buf.extend_from_slice(&value.to_le_bytes());
31    }
32}
33
34/// Read a Bitcoin compact-size integer from a byte slice at the given offset.
35///
36/// Advances `offset` past the consumed bytes.
37pub fn read_compact_size(data: &[u8], offset: &mut usize) -> Result<u64, SignerError> {
38    if *offset >= data.len() {
39        return Err(SignerError::EncodingError(
40            "compact size: unexpected EOF".into(),
41        ));
42    }
43    let first = data[*offset];
44    *offset += 1;
45    match first {
46        0x00..=0xFC => Ok(first as u64),
47        0xFD => {
48            if *offset + 2 > data.len() {
49                return Err(SignerError::EncodingError(
50                    "compact size: truncated u16".into(),
51                ));
52            }
53            let val = u16::from_le_bytes([data[*offset], data[*offset + 1]]);
54            *offset += 2;
55            Ok(val as u64)
56        }
57        0xFE => {
58            if *offset + 4 > data.len() {
59                return Err(SignerError::EncodingError(
60                    "compact size: truncated u32".into(),
61                ));
62            }
63            let mut buf = [0u8; 4];
64            buf.copy_from_slice(&data[*offset..*offset + 4]);
65            *offset += 4;
66            Ok(u32::from_le_bytes(buf) as u64)
67        }
68        0xFF => {
69            if *offset + 8 > data.len() {
70                return Err(SignerError::EncodingError(
71                    "compact size: truncated u64".into(),
72                ));
73            }
74            let mut buf = [0u8; 8];
75            buf.copy_from_slice(&data[*offset..*offset + 8]);
76            *offset += 8;
77            Ok(u64::from_le_bytes(buf))
78        }
79    }
80}
81
82// ─── Bech32 / Bech32m ───────────────────────────────────────────────
83
84/// Encode a SegWit/Taproot address using Bech32 (v0) or Bech32m (v1+).
85///
86/// Automatically selects the correct variant based on witness version.
87pub fn bech32_encode(
88    hrp: &str,
89    witness_version: u8,
90    program: &[u8],
91) -> Result<String, SignerError> {
92    use bech32::Hrp;
93    let hrp =
94        Hrp::parse(hrp).map_err(|e| SignerError::EncodingError(format!("bech32 hrp: {e}")))?;
95    let version = bech32::Fe32::try_from(witness_version)
96        .map_err(|e| SignerError::EncodingError(format!("witness version: {e}")))?;
97    bech32::segwit::encode(hrp, version, program)
98        .map_err(|e| SignerError::EncodingError(format!("bech32 encode: {e}")))
99}
100
101// ─── Base58Check ────────────────────────────────────────────────────
102
103/// Encode data with Base58Check: `Base58(version ‖ payload ‖ checksum[0..4])`.
104///
105/// Used by Bitcoin P2PKH, WIF, xprv/xpub, XRP, and NEO addresses.
106pub fn base58check_encode(version: u8, payload: &[u8]) -> String {
107    let mut data = Vec::with_capacity(1 + payload.len() + 4);
108    data.push(version);
109    data.extend_from_slice(payload);
110    let checksum = crypto::double_sha256(&data);
111    data.extend_from_slice(&checksum[..4]);
112    bs58::encode(&data).into_string()
113}
114
115/// Decode a Base58Check string. Returns `(version, payload)`.
116///
117/// Validates the 4-byte checksum.
118pub fn base58check_decode(s: &str) -> Result<(u8, Vec<u8>), SignerError> {
119    let decoded = bs58::decode(s)
120        .into_vec()
121        .map_err(|e| SignerError::EncodingError(format!("base58: {e}")))?;
122    if decoded.len() < 5 {
123        return Err(SignerError::EncodingError("base58check too short".into()));
124    }
125    let payload_end = decoded.len() - 4;
126    let checksum = crypto::double_sha256(&decoded[..payload_end]);
127    use subtle::ConstantTimeEq;
128    if checksum[..4].ct_eq(&decoded[payload_end..]).unwrap_u8() != 1 {
129        return Err(SignerError::EncodingError(
130            "base58check: invalid checksum".into(),
131        ));
132    }
133    Ok((decoded[0], decoded[1..payload_end].to_vec()))
134}
135
136#[cfg(test)]
137#[allow(clippy::unwrap_used, clippy::expect_used)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_compact_size_roundtrip() {
143        for val in [
144            0u64,
145            1,
146            252,
147            253,
148            0xFFFF,
149            0x10000,
150            0xFFFF_FFFF,
151            0x1_0000_0000,
152        ] {
153            let mut buf = Vec::new();
154            encode_compact_size(&mut buf, val);
155            let mut offset = 0;
156            let parsed = read_compact_size(&buf, &mut offset).expect("ok");
157            assert_eq!(parsed, val, "failed for {val}");
158            assert_eq!(offset, buf.len());
159        }
160    }
161
162    #[test]
163    fn test_compact_size_single_byte() {
164        let mut buf = Vec::new();
165        encode_compact_size(&mut buf, 42);
166        assert_eq!(buf, vec![42]);
167    }
168
169    #[test]
170    fn test_compact_size_eof() {
171        let mut offset = 0;
172        assert!(read_compact_size(&[], &mut offset).is_err());
173    }
174
175    #[test]
176    fn test_base58check_roundtrip() {
177        let encoded = base58check_encode(0x00, &[0xAA; 20]);
178        let (version, payload) = base58check_decode(&encoded).expect("ok");
179        assert_eq!(version, 0x00);
180        assert_eq!(payload, vec![0xAA; 20]);
181    }
182
183    #[test]
184    fn test_base58check_invalid_checksum() {
185        let mut encoded = base58check_encode(0x00, &[0xBB; 20]);
186        // Corrupt the last character
187        encoded.pop();
188        encoded.push('1');
189        // Might decode but checksum should fail (or bs58 decode fails)
190        let result = base58check_decode(&encoded);
191        assert!(result.is_err());
192    }
193
194    #[test]
195    fn test_base58check_too_short() {
196        assert!(base58check_decode("1").is_err());
197    }
198
199    #[test]
200    fn test_bech32_encode_v0() {
201        let addr = bech32_encode("bc", 0, &[0xAA; 20]).expect("ok");
202        assert!(addr.starts_with("bc1q"));
203    }
204
205    #[test]
206    fn test_bech32_encode_v1() {
207        let addr = bech32_encode("bc", 1, &[0xBB; 32]).expect("ok");
208        assert!(addr.starts_with("bc1p"));
209    }
210
211    #[test]
212    fn test_bech32_encode_testnet() {
213        let addr = bech32_encode("tb", 0, &[0xCC; 20]).expect("ok");
214        assert!(addr.starts_with("tb1q"));
215    }
216
217    // ─── Bech32 Known Address Vectors ───────────────────────────
218
219    #[test]
220    fn test_bech32_bip173_p2wpkh_vector() {
221        // BIP-173 test vector: P2WPKH bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
222        let program = hex::decode("751e76e8199196d454941c45d1b3a323f1433bd6").unwrap();
223        let addr = bech32_encode("bc", 0, &program).unwrap();
224        assert_eq!(addr, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4");
225    }
226
227    #[test]
228    fn test_bech32_bip350_p2tr_vector() {
229        // BIP-350: P2TR address with witness version 1
230        let program =
231            hex::decode("a60869f0dbcf1dc659c9cecbee736b12006a35d655ac7e1caeff5ebc1085a044")
232                .unwrap();
233        let addr = bech32_encode("bc", 1, &program).unwrap();
234        assert!(addr.starts_with("bc1p"));
235        assert_eq!(addr.len(), 62); // Bech32m P2TR addresses are 62 chars
236    }
237
238    #[test]
239    fn test_bech32_invalid_hrp() {
240        assert!(bech32_encode("", 0, &[0; 20]).is_err());
241    }
242
243    // ─── Base58Check Known Address Vectors ──────────────────────
244
245    #[test]
246    fn test_base58check_bitcoin_p2pkh_genesis() {
247        // Bitcoin genesis coinbase P2PKH: HASH160 of generator pubkey
248        // Version 0x00 + 751e76e8199196d454941c45d1b3a323f1433bd6
249        let hash160 = hex::decode("751e76e8199196d454941c45d1b3a323f1433bd6").unwrap();
250        let addr = base58check_encode(0x00, &hash160);
251        assert_eq!(addr, "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH");
252    }
253
254    #[test]
255    fn test_base58check_decode_known_address() {
256        let (version, payload) = base58check_decode("1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH").unwrap();
257        assert_eq!(version, 0x00);
258        assert_eq!(
259            hex::encode(payload),
260            "751e76e8199196d454941c45d1b3a323f1433bd6"
261        );
262    }
263
264    // ─── Compact Size Boundary Values ───────────────────────────
265
266    #[test]
267    fn test_compact_size_boundary_252() {
268        let mut buf = Vec::new();
269        encode_compact_size(&mut buf, 252);
270        assert_eq!(buf.len(), 1); // single byte
271        assert_eq!(buf[0], 252);
272    }
273
274    #[test]
275    fn test_compact_size_boundary_253() {
276        let mut buf = Vec::new();
277        encode_compact_size(&mut buf, 253);
278        assert_eq!(buf[0], 0xFD); // 3 bytes: 0xFD + u16 LE
279        assert_eq!(buf.len(), 3);
280        let mut offset = 0;
281        assert_eq!(read_compact_size(&buf, &mut offset).unwrap(), 253);
282    }
283
284    #[test]
285    fn test_compact_size_truncated_u16() {
286        let buf = vec![0xFD, 0x01]; // need 2 bytes, only 1
287        let mut offset = 0;
288        assert!(read_compact_size(&buf, &mut offset).is_err());
289    }
290
291    #[test]
292    fn test_compact_size_truncated_u32() {
293        let buf = vec![0xFE, 0x01, 0x00]; // need 4 bytes, only 2
294        let mut offset = 0;
295        assert!(read_compact_size(&buf, &mut offset).is_err());
296    }
297
298    #[test]
299    fn test_compact_size_truncated_u64() {
300        let buf = vec![0xFF, 0x01, 0x00, 0x00, 0x00]; // need 8 bytes, only 4
301        let mut offset = 0;
302        assert!(read_compact_size(&buf, &mut offset).is_err());
303    }
304}