rustywallet_address/encoding/
base58.rs

1//! Base58Check encoding for Bitcoin Legacy addresses.
2
3use crate::error::AddressError;
4use sha2::{Digest, Sha256};
5
6/// Base58Check encoder/decoder.
7pub struct Base58Check;
8
9impl Base58Check {
10    /// Encode data with version byte using Base58Check.
11    ///
12    /// Format: Base58(version + payload + checksum)
13    /// where checksum = first 4 bytes of SHA256(SHA256(version + payload))
14    pub fn encode(version: u8, payload: &[u8]) -> String {
15        let mut data = Vec::with_capacity(1 + payload.len() + 4);
16        data.push(version);
17        data.extend_from_slice(payload);
18
19        // Calculate checksum
20        let checksum = Self::checksum(&data);
21        data.extend_from_slice(&checksum);
22
23        bs58::encode(data).into_string()
24    }
25
26    /// Decode Base58Check encoded string.
27    ///
28    /// Returns (version, payload) if valid.
29    pub fn decode(s: &str) -> Result<(u8, Vec<u8>), AddressError> {
30        let data = bs58::decode(s)
31            .into_vec()
32            .map_err(|e| AddressError::InvalidBase58(e.to_string()))?;
33
34        if data.len() < 5 {
35            return Err(AddressError::InvalidBase58("Data too short".to_string()));
36        }
37
38        let (payload_with_version, checksum) = data.split_at(data.len() - 4);
39        let expected_checksum = Self::checksum(payload_with_version);
40
41        if checksum != expected_checksum {
42            return Err(AddressError::ChecksumMismatch);
43        }
44
45        let version = payload_with_version[0];
46        let payload = payload_with_version[1..].to_vec();
47
48        Ok((version, payload))
49    }
50
51    /// Calculate double SHA256 checksum (first 4 bytes).
52    fn checksum(data: &[u8]) -> [u8; 4] {
53        let hash1 = Sha256::digest(data);
54        let hash2 = Sha256::digest(hash1);
55        let mut checksum = [0u8; 4];
56        checksum.copy_from_slice(&hash2[..4]);
57        checksum
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn test_base58check_roundtrip() {
67        let payload = [0x01, 0x02, 0x03, 0x04, 0x05];
68        let encoded = Base58Check::encode(0x00, &payload);
69        let (version, decoded) = Base58Check::decode(&encoded).unwrap();
70        assert_eq!(version, 0x00);
71        assert_eq!(decoded, payload);
72    }
73
74    #[test]
75    fn test_base58check_invalid_checksum() {
76        let encoded = Base58Check::encode(0x00, &[0x01, 0x02, 0x03]);
77        // Corrupt the encoded string
78        let mut chars: Vec<char> = encoded.chars().collect();
79        if let Some(c) = chars.last_mut() {
80            *c = if *c == '1' { '2' } else { '1' };
81        }
82        let corrupted: String = chars.into_iter().collect();
83        assert!(Base58Check::decode(&corrupted).is_err());
84    }
85}