Skip to main content

mina_base58/
lib.rs

1//! Mina base58check encoding and decoding.
2//!
3//! Implements the base58check scheme used by the Mina protocol: a
4//! single version byte followed by the payload, with a 4-byte
5//! double-SHA256 checksum appended before base58 encoding.
6
7#![no_std]
8#![deny(missing_docs)]
9#![deny(unsafe_code)]
10#![deny(clippy::all)]
11#![deny(clippy::pedantic)]
12#![deny(clippy::nursery)]
13
14extern crate alloc;
15
16/// Version bytes for Mina base58check encodings.
17pub mod version;
18
19use alloc::{format, string::String, vec::Vec};
20use sha2::{Digest, Sha256};
21use subtle::ConstantTimeEq;
22use thiserror::Error;
23
24/// Errors that can occur when decoding a base58check string.
25#[derive(Error, Debug, Clone, PartialEq, Eq)]
26pub enum DecodeError {
27    /// The input is not valid base58.
28    ///
29    /// The contained string carries the detail from the underlying
30    /// `bs58` decoder (e.g. invalid character and position).
31    #[error("invalid base58: {0}")]
32    InvalidBase58(String),
33    /// The decoded data is shorter than the 5-byte minimum
34    /// (1 version byte + 4 checksum bytes).
35    #[error("decoded data too short")]
36    TooShort,
37    /// The trailing 4-byte checksum does not match the data.
38    #[error("invalid checksum")]
39    InvalidChecksum,
40    /// The version byte does not match the expected value.
41    #[error("invalid version byte: expected {expected:#04x}, found {found:#04x}")]
42    InvalidVersion {
43        /// The version byte that was expected.
44        expected: u8,
45        /// The version byte that was found.
46        found: u8,
47    },
48}
49
50/// Double-SHA256 checksum of `data`.
51#[must_use]
52pub(crate) fn checksum(data: &[u8]) -> [u8; 4] {
53    let hash = Sha256::digest(&Sha256::digest(data)[..]);
54    let mut out = [0u8; 4];
55    out.copy_from_slice(&hash[..4]);
56    out
57}
58
59/// Constant-time comparison of two 4-byte checksums.
60///
61/// Uses [`subtle::ConstantTimeEq`] to prevent timing side-channels
62/// that could reveal how many leading checksum bytes matched.
63fn checksum_verify(got: [u8; 4], expected: [u8; 4]) -> bool {
64    got.ct_eq(&expected).into()
65}
66
67/// Encode `payload` with a leading `version` byte in base58check.
68///
69/// Prepends the version byte, computes a 4-byte double-SHA256 checksum
70/// over `[version || payload]`, appends it, and base58-encodes.
71#[must_use]
72pub fn encode(version: u8, payload: &[u8]) -> String {
73    let mut buf = Vec::with_capacity(1 + payload.len() + 4);
74    buf.push(version);
75    buf.extend_from_slice(payload);
76    let cs = checksum(&buf);
77    buf.extend_from_slice(&cs);
78    bs58::encode(buf).into_string()
79}
80
81/// Decode a base58check string, returning `(version, payload)`.
82///
83/// # Errors
84///
85/// Returns an error if the input is not valid base58, is too short,
86/// or has an invalid checksum.
87#[must_use = "decoding result must be used"]
88pub fn decode(b58: &str) -> Result<(u8, Vec<u8>), DecodeError> {
89    let mut raw = decode_raw(b58)?;
90    let version = raw[0];
91    raw.drain(..1);
92    Ok((version, raw))
93}
94
95/// Decode a base58check string and verify the version byte.
96///
97/// # Errors
98///
99/// Returns an error if decoding fails or the version byte does not
100/// match `expected`.
101#[must_use = "decoding result must be used"]
102pub fn decode_version(b58: &str, expected: u8) -> Result<Vec<u8>, DecodeError> {
103    let (version, payload) = decode(b58)?;
104    if version != expected {
105        return Err(DecodeError::InvalidVersion {
106            expected,
107            found: version,
108        });
109    }
110    Ok(payload)
111}
112
113/// Encode raw bytes (which already contain any version/structure bytes)
114/// with an appended 4-byte double-SHA256 checksum.
115#[must_use]
116pub fn encode_raw(raw: &[u8]) -> String {
117    let cs = checksum(raw);
118    let mut buf = Vec::with_capacity(raw.len() + 4);
119    buf.extend_from_slice(raw);
120    buf.extend_from_slice(&cs);
121    bs58::encode(buf).into_string()
122}
123
124/// Decode a base58check string, verify the checksum, and return the raw
125/// bytes (without the trailing checksum but including any version bytes).
126///
127/// # Errors
128///
129/// Returns an error if the input is not valid base58, is too short,
130/// or has an invalid checksum.
131#[must_use = "decoding result must be used"]
132pub fn decode_raw(b58: &str) -> Result<Vec<u8>, DecodeError> {
133    let mut bytes = bs58::decode(b58)
134        .into_vec()
135        .map_err(|e| DecodeError::InvalidBase58(format!("{e}")))?;
136    if bytes.len() < 5 {
137        return Err(DecodeError::TooShort);
138    }
139    let data_len = bytes.len() - 4;
140    let got = [
141        bytes[data_len],
142        bytes[data_len + 1],
143        bytes[data_len + 2],
144        bytes[data_len + 3],
145    ];
146    if !checksum_verify(got, checksum(&bytes[..data_len])) {
147        return Err(DecodeError::InvalidChecksum);
148    }
149    bytes.truncate(data_len);
150    Ok(bytes)
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    // ================================================================
158    // checksum tests
159    // ================================================================
160
161    #[test]
162    fn test_checksum_is_deterministic() {
163        let data = b"hello world";
164        assert_eq!(checksum(data), checksum(data));
165    }
166
167    #[test]
168    fn test_checksum_differs_for_different_data() {
169        assert_ne!(checksum(b"aaa"), checksum(b"bbb"));
170    }
171
172    #[test]
173    fn test_checksum_is_four_bytes() {
174        let cs = checksum(b"any data");
175        assert_eq!(cs.len(), 4);
176    }
177
178    #[test]
179    fn test_checksum_verify_equal() {
180        assert!(checksum_verify(
181            [0xAA, 0xBB, 0xCC, 0xDD],
182            [0xAA, 0xBB, 0xCC, 0xDD]
183        ));
184    }
185
186    #[test]
187    fn test_checksum_verify_rejects_each_byte() {
188        let expected = [0xAA, 0xBB, 0xCC, 0xDD];
189        for i in 0..4 {
190            let mut bad = expected;
191            bad[i] ^= 0x01;
192            assert!(
193                !checksum_verify(bad, expected),
194                "byte {i} flip not detected"
195            );
196        }
197    }
198}