Skip to main content

kobe_primitives/
encoding.rs

1//! Shared address-encoding primitives used by multiple chain crates.
2//!
3//! - [`hash160`] — Bitcoin / Cosmos / XRPL account-id style digests
4//! - [`double_sha256`] — Bitcoin-style checksums
5//! - [`base58check_encode`] — Bitcoin alphabet + 4-byte double-SHA-256 checksum
6//!
7//! Chain crates keep HRP, alphabet variants (XRPL), and script templates
8//! locally; only the repeated hash / `Base58Check` steps live here.
9
10use alloc::string::String;
11use alloc::vec::Vec;
12
13use ripemd::Ripemd160;
14use sha2::{Digest, Sha256};
15
16/// `RIPEMD-160(SHA-256(data))` → 20 bytes.
17///
18/// Used by Bitcoin P2PKH/P2WPKH, Cosmos SDK bech32 payloads, and XRPL
19/// classic account IDs.
20#[must_use]
21pub fn hash160(data: &[u8]) -> [u8; 20] {
22    let sha = Sha256::digest(data);
23    Ripemd160::digest(sha).into()
24}
25
26/// `SHA-256(SHA-256(data))` → 32 bytes.
27///
28/// Used for Bitcoin `Base58Check` / WIF checksums and XRPL classic address
29/// checksums (first 4 bytes).
30#[must_use]
31pub fn double_sha256(data: &[u8]) -> [u8; 32] {
32    let first = Sha256::digest(data);
33    Sha256::digest(first).into()
34}
35
36/// Bitcoin-alphabet `Base58Check`: `Base58(payload ‖ checksum4)`.
37///
38/// `checksum4` is the first 4 bytes of [`double_sha256`] over `payload`.
39/// Callers supply the full pre-checksum body (e.g. `version ‖ hash160` for
40/// P2PKH, or WIF's `version ‖ sk ‖ 0x01`).
41#[must_use]
42pub fn base58check_encode(payload: &[u8]) -> String {
43    let checksum = double_sha256(payload);
44    let mut buf = Vec::with_capacity(payload.len() + 4);
45    buf.extend_from_slice(payload);
46    buf.extend_from_slice(&checksum[..4]);
47    bs58::encode(buf).into_string()
48}
49
50/// Convenience: version byte + 20-byte payload (`P2PKH` / `P2SH` style).
51#[must_use]
52#[allow(
53    clippy::indexing_slicing,
54    reason = "fixed-size 21-byte Base58Check body is sized by construction"
55)]
56pub fn base58check_versioned(version: u8, payload_20: &[u8; 20]) -> String {
57    let mut body = [0u8; 21];
58    body[0] = version;
59    body[1..].copy_from_slice(payload_20);
60    base58check_encode(&body)
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn hash160_empty_known() {
69        // RIPEMD160(SHA256("")) — cross-checked with Python hashlib.
70        let h = hash160(b"");
71        assert_eq!(hex::encode(h), "b472a266d0bd89c13706a4132ccfb16f7c3b9fcb");
72    }
73
74    #[test]
75    fn base58check_p2pkh_mainnet_prefix() {
76        // Arbitrary 20-byte payload; only checks encoding shape.
77        let payload = [0x11u8; 20];
78        let encoded = base58check_versioned(0x00, &payload);
79        assert!(encoded.starts_with('1'));
80    }
81
82    #[test]
83    fn double_sha256_deterministic() {
84        let a = double_sha256(b"kobe");
85        let b = double_sha256(b"kobe");
86        assert_eq!(a, b);
87        assert_ne!(a, double_sha256(b"other"));
88    }
89}