umsh_core/
base58.rs

1//! Fixed-width base58 codec for 32-byte addresses.
2//!
3//! UMSH addresses render as exactly 44 base58 digits (Bitcoin alphabet),
4//! left-padded with `1` — the zero digit — so that character positions are
5//! stable across all key values. See the "Addressing" chapter of the protocol
6//! specification.
7
8use core::fmt::Write;
9
10use crate::error::AddressParseError;
11
12/// Base58 digit alphabet (Bitcoin variant).
13const ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
14
15/// Length of the fixed-width base58 encoding of a 32-byte value.
16pub const ENCODED_LEN: usize = 44;
17
18/// Encode 32 bytes as exactly [`ENCODED_LEN`] base58 digits.
19pub fn encode(bytes: &[u8; 32]) -> [u8; ENCODED_LEN] {
20    let mut out = [ALPHABET[0]; ENCODED_LEN];
21    let mut num = *bytes;
22    for slot in out.iter_mut().rev() {
23        let mut rem = 0u32;
24        for byte in num.iter_mut() {
25            let acc = (rem << 8) | u32::from(*byte);
26            *byte = (acc / 58) as u8;
27            rem = acc % 58;
28        }
29        *slot = ALPHABET[rem as usize];
30    }
31    out
32}
33
34/// Decode exactly [`ENCODED_LEN`] base58 digits into 32 bytes.
35pub fn decode(digits: &[u8]) -> Result<[u8; 32], AddressParseError> {
36    if digits.len() != ENCODED_LEN {
37        return Err(AddressParseError::InvalidLength);
38    }
39    let mut out = [0u8; 32];
40    for &digit in digits {
41        let mut carry = u32::from(digit_value(digit)?);
42        for byte in out.iter_mut().rev() {
43            let acc = u32::from(*byte) * 58 + carry;
44            *byte = acc as u8;
45            carry = acc >> 8;
46        }
47        if carry != 0 {
48            return Err(AddressParseError::Overflow);
49        }
50    }
51    Ok(out)
52}
53
54fn digit_value(digit: u8) -> Result<u8, AddressParseError> {
55    ALPHABET
56        .iter()
57        .position(|&c| c == digit)
58        .map(|index| index as u8)
59        .ok_or(AddressParseError::InvalidCharacter)
60}
61
62/// Write the star-truncated hint rendering defined in the addressing chapter.
63///
64/// The hint is encoded twice — padded to 32 bytes with 0x00 and with 0xFF —
65/// and the longest common prefix of the two encodings is emitted, up to
66/// `budget` characters, followed by a single `*` where they diverge. Every
67/// emitted non-`*` character is guaranteed to match the full base58 rendering
68/// of any public key that matches the hint.
69pub(crate) fn fmt_hint(
70    f: &mut core::fmt::Formatter<'_>,
71    hint: &[u8],
72    budget: usize,
73) -> core::fmt::Result {
74    let mut lo = [0x00u8; 32];
75    let mut hi = [0xFFu8; 32];
76    lo[..hint.len()].copy_from_slice(hint);
77    hi[..hint.len()].copy_from_slice(hint);
78    let lo = encode(&lo);
79    let hi = encode(&hi);
80    for (&a, &b) in lo.iter().zip(hi.iter()).take(budget) {
81        if a != b {
82            return f.write_char('*');
83        }
84        f.write_char(char::from(a))?;
85    }
86    Ok(())
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    const ZERO: [u8; 32] = [0u8; 32];
94    const MAX: [u8; 32] = [0xFFu8; 32];
95
96    fn leading_zero_key() -> [u8; 32] {
97        let mut key = [0u8; 32];
98        for (i, byte) in key.iter_mut().enumerate() {
99            *byte = i as u8;
100        }
101        key
102    }
103
104    #[test]
105    fn encode_matches_reference_vectors() {
106        assert_eq!(
107            &encode(&ZERO),
108            b"11111111111111111111111111111111111111111111"
109        );
110        assert_eq!(
111            &encode(&MAX),
112            b"JEKNVnkbo3jma5nREBBJCDoXFVeKkD56V3xKrvRmWxFG"
113        );
114
115        let mut one = ZERO;
116        one[31] = 1;
117        assert_eq!(
118            &encode(&one),
119            b"11111111111111111111111111111111111111111112"
120        );
121
122        assert_eq!(
123            &encode(&leading_zero_key()),
124            b"111thX6LZfHDZZKUs92febYZhYRcXddmzfzF2NvTkPNE"
125        );
126    }
127
128    #[test]
129    fn decode_round_trips() {
130        for key in [ZERO, MAX, leading_zero_key()] {
131            assert_eq!(decode(&encode(&key)).unwrap(), key);
132        }
133    }
134
135    #[test]
136    fn decode_rejects_bad_input() {
137        assert_eq!(decode(b"7NeD"), Err(AddressParseError::InvalidLength));
138        // '0', 'O', 'I', and 'l' are excluded from the base58 alphabet.
139        for bad in [b'0', b'O', b'I', b'l'] {
140            let mut digits = *b"11111111111111111111111111111111111111111111";
141            digits[10] = bad;
142            assert_eq!(decode(&digits), Err(AddressParseError::InvalidCharacter));
143        }
144        assert_eq!(
145            decode(&[b'z'; ENCODED_LEN]),
146            Err(AddressParseError::Overflow)
147        );
148    }
149}