Skip to main content

bsv_primitives/base58/
mod.rs

1//! Base58 encoding and decoding with optional checksum support.
2//!
3//! Provides raw Base58 encode/decode (matching the Go BSV SDK's
4//! `compat/base58` package) and Base58Check encode/decode (with
5//! double-SHA-256 checksum) used for WIF private keys and Bitcoin
6//! addresses.
7
8use crate::hash::sha256d;
9use crate::PrimitivesError;
10
11/// Bitcoin's modified Base58 alphabet.
12///
13/// Excludes 0, O, I, l to reduce visual ambiguity.
14const _ALPHABET: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
15
16/// Encode a byte slice to a Base58 string.
17///
18/// Uses Bitcoin's modified Base58 alphabet. Leading zero bytes
19/// are encoded as leading '1' characters.
20///
21/// # Arguments
22/// * `data` - The bytes to encode.
23///
24/// # Returns
25/// A Base58-encoded string.
26pub fn encode(data: &[u8]) -> String {
27    bs58::encode(data)
28        .with_alphabet(bs58::Alphabet::BITCOIN)
29        .into_string()
30}
31
32/// Decode a Base58 string to a byte vector.
33///
34/// Leading '1' characters decode to leading zero bytes.
35///
36/// # Arguments
37/// * `s` - The Base58 string to decode.
38///
39/// # Returns
40/// `Ok(Vec<u8>)` on success, or an error for invalid characters.
41pub fn decode(s: &str) -> Result<Vec<u8>, PrimitivesError> {
42    bs58::decode(s)
43        .with_alphabet(bs58::Alphabet::BITCOIN)
44        .into_vec()
45        .map_err(|e| PrimitivesError::InvalidBase58(e.to_string()))
46}
47
48/// Encode a byte slice with a 4-byte double-SHA-256 checksum appended (Base58Check).
49///
50/// The checksum is the first 4 bytes of SHA-256d(data). The result
51/// is `encode(data || checksum)`.
52///
53/// # Arguments
54/// * `data` - The bytes to encode (typically version byte + payload).
55///
56/// # Returns
57/// A Base58Check-encoded string.
58pub fn check_encode(data: &[u8]) -> String {
59    let checksum = sha256d(data);
60    let mut payload = data.to_vec();
61    payload.extend_from_slice(&checksum[..4]);
62    encode(&payload)
63}
64
65/// Decode a Base58Check string, verifying the 4-byte checksum.
66///
67/// Strips and validates the trailing 4-byte double-SHA-256 checksum.
68///
69/// # Arguments
70/// * `s` - The Base58Check string to decode.
71///
72/// # Returns
73/// `Ok(Vec<u8>)` of the payload (without checksum) on success, or an
74/// error for invalid encoding or checksum mismatch.
75pub fn check_decode(s: &str) -> Result<Vec<u8>, PrimitivesError> {
76    let decoded = decode(s)?;
77    if decoded.len() < 4 {
78        return Err(PrimitivesError::InvalidBase58(
79            "data too short for checksum".to_string(),
80        ));
81    }
82    let (payload, checksum) = decoded.split_at(decoded.len() - 4);
83    let expected = sha256d(payload);
84    if checksum != &expected[..4] {
85        return Err(PrimitivesError::ChecksumMismatch);
86    }
87    Ok(payload.to_vec())
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    // -- Tests from Go SDK base58_test.go TestBase58 --
95
96    #[test]
97    fn test_base58_empty_string() {
98        let input = hex::decode("").unwrap();
99        assert_eq!(encode(&input), "");
100        let decoded = decode("").unwrap();
101        assert_eq!(decoded, input);
102    }
103
104    #[test]
105    fn test_base58_single_zero_byte() {
106        let input = hex::decode("00").unwrap();
107        assert_eq!(encode(&input), "1");
108        let decoded = decode("1").unwrap();
109        assert_eq!(decoded, input);
110    }
111
112    #[test]
113    fn test_base58_decoded_address() {
114        let input = hex::decode("00010966776006953D5567439E5E39F86A0D273BEED61967F6").unwrap();
115        let encoded = encode(&input);
116        assert_eq!(encoded, "16UwLL9Risc3QfPqBUvKofHmBQ7wMtjvM");
117        let decoded = decode("16UwLL9Risc3QfPqBUvKofHmBQ7wMtjvM").unwrap();
118        assert_eq!(decoded, input);
119    }
120
121    #[test]
122    fn test_base58_decoded_hash() {
123        let input = hex::decode("0123456789ABCDEF").unwrap();
124        let encoded = encode(&input);
125        assert_eq!(encoded, "C3CPq7c8PY");
126        let decoded = decode("C3CPq7c8PY").unwrap();
127        assert_eq!(decoded, input);
128    }
129
130    #[test]
131    fn test_base58_leading_zeros() {
132        let input = hex::decode("000000287FB4CD").unwrap();
133        let encoded = encode(&input);
134        assert_eq!(encoded, "111233QC4");
135        let decoded = decode("111233QC4").unwrap();
136        assert_eq!(decoded, input);
137    }
138
139    // -- Tests from Go SDK base58_test.go TestBase58DecodeInvalid --
140
141    #[test]
142    fn test_base58_decode_invalid_character() {
143        assert!(decode("invalid!@#$%").is_err());
144    }
145
146    #[test]
147    fn test_base58_decode_mixed_valid_invalid() {
148        assert!(decode("1234!@#$%").is_err());
149    }
150
151    // -- Tests from Go SDK base58_test.go TestBase58EncodeEdgeCases --
152
153    #[test]
154    fn test_base58_encode_nil_input() {
155        assert_eq!(encode(&[]), "");
156    }
157
158    #[test]
159    fn test_base58_encode_all_zeros() {
160        assert_eq!(encode(&[0, 0, 0, 0]), "1111");
161    }
162
163    #[test]
164    fn test_base58_encode_large_number() {
165        assert_eq!(encode(&[255, 255, 255, 255]), "7YXq9G");
166    }
167
168    // -- Base58Check tests --
169
170    #[test]
171    fn test_base58_check_roundtrip() {
172        let payload = hex::decode("00f54a5851e9372b87810a8e60cdd2e7cfd80b6e31").unwrap();
173        let encoded = check_encode(&payload);
174        let decoded = check_decode(&encoded).unwrap();
175        assert_eq!(decoded, payload);
176    }
177
178    #[test]
179    fn test_base58_check_bad_checksum() {
180        // Encode then tamper with the last character.
181        let payload = vec![0x80, 0x01, 0x02, 0x03];
182        let mut encoded = check_encode(&payload);
183        let last = encoded.pop().unwrap();
184        let replacement = if last == '1' { '2' } else { '1' };
185        encoded.push(replacement);
186        assert!(check_decode(&encoded).is_err());
187    }
188}