cashaddr/
decode.rs

1use std::fmt::{self, Display};
2use std::str::FromStr;
3
4use super::{convert_bits, expand_prefix, polymod};
5use super::{HashType, Payload};
6
7const SIZE_MASK: u8 = 0x07;
8const TYPE_MASK: u8 = 0x78;
9
10type Result<T> = std::result::Result<T, Error>;
11
12// The cashaddr character set for decoding
13#[rustfmt::skip]
14const CHARSET_REV: [Option<u8>; 128] = [
15    None,     None,     None,     None,     None,     None,     None,     None,
16    None,     None,     None,     None,     None,     None,     None,     None,
17    None,     None,     None,     None,     None,     None,     None,     None,
18    None,     None,     None,     None,     None,     None,     None,     None,
19    None,     None,     None,     None,     None,     None,     None,     None,
20    None,     None,     None,     None,     None,     None,     None,     None,
21    Some(15), None,     Some(10), Some(17), Some(21), Some(20), Some(26), Some(30),
22    Some(7),  Some(5),  None,     None,     None,     None,     None,     None,
23    None,     Some(29), None,     Some(24), Some(13), Some(25), Some(9),  Some(8),
24    Some(23), None,     Some(18), Some(22), Some(31), Some(27), Some(19), None,
25    Some(1),  Some(0),  Some(3),  Some(16), Some(11), Some(28), Some(12), Some(14),
26    Some(6),  Some(4),  Some(2),  None,     None,     None,     None,     None,
27    None,     Some(29),  None,    Some(24), Some(13), Some(25), Some(9),  Some(8),
28    Some(23), None,     Some(18), Some(22), Some(31), Some(27), Some(19), None,
29    Some(1),  Some(0),  Some(3),  Some(16), Some(11), Some(28), Some(12), Some(14),
30    Some(6),  Some(4),  Some(2),  None,     None,     None,     None,     None,
31];
32
33/// Error that occurs during cashaddr decoding
34#[derive(Debug, PartialEq, Eq)]
35pub enum Error {
36    /// Invalid character encountered during decoding
37    InvalidChar(char),
38    /// Invalid input length
39    InvalidLength(usize),
40    /// Checksum failed during decoding. Inner value is the value of checksum computed by
41    /// polymod(expanded prefix + paylaod), Note this is different from the _encoded checksum_
42    /// which is just the last 8 characters (40 bits) of the payload
43    ChecksumFailed(u64),
44    /// Invalid Version byte encountered during decoding
45    InvalidVersion(u8),
46}
47
48impl Display for Error {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match self {
51            Self::InvalidChar(c) => write!(f, "Invalid Character `{c}` encountered during decode."),
52            Self::InvalidLength(len) => write!(f, "Invalid hash length detected: {}", len),
53            Self::ChecksumFailed(cs) => write!(f, "Checksum failed validation: {}", cs),
54            Self::InvalidVersion(vbit) => write!(f, "Invalid version byte detected {:X}", vbit),
55        }
56    }
57}
58
59impl std::error::Error for Error {}
60
61impl FromStr for Payload {
62    type Err = Error;
63
64    fn from_str(addr_str: &str) -> Result<Self> {
65        // Fail fast on empty strings
66        if addr_str.is_empty() {
67            return Err(Error::InvalidLength(0));
68        }
69
70        let (prefix, payload_str) = addr_str
71            .split_once(":")
72            .unwrap_or(("bitcoincash", addr_str));
73
74        // Decode payload to 5 bit array
75        let payload_5_bits: Vec<u8> = payload_str
76            .chars()
77            .map(|c| match CHARSET_REV.get(c as usize) {
78                Some(Some(d)) => Ok(*d as u8),
79                _ => Err(Error::InvalidChar(c)),
80            })
81            .collect::<Result<_>>()?;
82
83        // Verify the checksum
84        let checksum = polymod(&[&expand_prefix(prefix), &payload_5_bits[..]].concat());
85        if checksum != 0 {
86            return Err(Error::ChecksumFailed(checksum));
87        }
88        let checksum: u64 = payload_5_bits
89            .iter()
90            .rev()
91            .take(8)
92            .enumerate()
93            .map(|(i, &val)| (val as u64) << (5 * i))
94            .sum();
95
96        // Convert from 5 bit array to byte array
97        let len_5_bit = payload_5_bits.len();
98        let payload = convert_bits(&payload_5_bits[..(len_5_bit - 8)], 5, 8, false);
99
100        // Verify the version byte
101        let version = payload[0];
102
103        // Check length
104        let body = &payload[1..];
105        let body_len = body.len();
106        let version_size = version & SIZE_MASK;
107
108        match version_size {
109            0x00 if body_len != 20 => Err(Error::InvalidLength(body_len)),
110            0x01 if body_len != 24 => Err(Error::InvalidLength(body_len)),
111            0x02 if body_len != 28 => Err(Error::InvalidLength(body_len)),
112            0x03 if body_len != 32 => Err(Error::InvalidLength(body_len)),
113            0x04 if body_len != 40 => Err(Error::InvalidLength(body_len)),
114            0x05 if body_len != 48 => Err(Error::InvalidLength(body_len)),
115            0x06 if body_len != 56 => Err(Error::InvalidLength(body_len)),
116            0x07 if body_len != 64 => Err(Error::InvalidLength(body_len)),
117            _ => Ok(()),
118        }?;
119
120        // Extract the hash type and return
121        let version_type = version & TYPE_MASK;
122        let hash_type = HashType::try_from(version_type >> 3)?;
123
124        Ok(Payload {
125            payload: body.to_vec(),
126            hash_type,
127            checksum,
128        })
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use hex_literal::hex;
136
137    use crate::test_vectors::{TestCase, TEST_VECTORS};
138
139    #[test]
140    fn decode() {
141        for tc in TEST_VECTORS
142            .lines()
143            .map(|s| TestCase::try_from(s).expect("Failed to parse test vector"))
144        {
145            let payload: Payload = tc.cashaddr.parse().expect("could not parse");
146            assert_eq!(payload.payload, tc.pl, "Incorrect payload parsed");
147            assert_eq!(payload.hash_type, tc.hashtype, "Incorrect Hash Type parsed")
148        }
149    }
150    #[test]
151    fn case_insensitive() {
152        let cashaddr = "bitcoincash:qr6m7j9njldWWzlg9v7v53unlr4JKmx6Eylep8ekg2";
153        let addr: Payload = cashaddr.parse().unwrap();
154        let payload = hex!("F5BF48B397DAE70BE82B3CCA4793F8EB2B6CDAC9");
155        assert_eq!(payload, addr.payload.as_ref());
156        assert_eq!(HashType::P2PKH, addr.hash_type);
157    }
158    #[test]
159    fn checksum() {
160        let cashaddr = "bitcoincash:qr6m7j9njldwwzlg9v7v53unlr3jkmx6eylep8ekg2";
161        match cashaddr.parse::<Payload>() {
162            Err(Error::ChecksumFailed(_)) => (),
163            Err(e) => panic!("Expected ChecksumFailed but found {e:?}"),
164            Ok(_) => panic!(
165                "Payload successfully parsed from cashaddr with invalid checksum. cashaddr was {}",
166                cashaddr,
167            ),
168        }
169    }
170    #[test]
171    fn invalid_char() {
172        match "bitcoincash:qr6m7j9njlbWWzlg9v7v53unlr4JKmx6Eylep8ekg2".parse::<Payload>() {
173            Err(Error::InvalidChar('b')) => (),
174            Err(e) => panic!("Failed to detect invalid char, instead detected {:?}", e),
175            Ok(_) => panic!("Failed to detect invalid char"),
176        }
177    }
178}