use crate::crypto::DSha256Hash;
pub(crate) const B58_ALPHABET: &[u8; 58] =
b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
pub(crate) const B58_BYTE_MAP: [i8; 256] = [
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, -1, -1, -1, -1, -1, -1, 9, 10, 11, 12, 13, 14, 15, 16, -1,
17, 18, 19, 20, 21, -1, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, -1, -1, -1, -1, -1, -1, 33,
34, 35, 36, 37, 38, 39, 40, 41, 42, 43, -1, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
57, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
];
#[derive(thiserror::Error, Clone, Debug, Eq, PartialEq)]
pub enum Error {
#[error("Invalid B58 character: {0}")]
InvalidChar(char),
#[error("Invalid B58 checksum - expected {0}, got {1}")]
InvalidChecksum(String, String),
#[error("Invalid B58 address version: {0}")]
InvalidAddressVersion(u8),
#[error(transparent)]
IntConversionError(#[from] std::num::TryFromIntError),
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum BitcoinAddressVersion {
MainnetP2PKH = 0,
MainnetP2SH = 5,
TestnetP2PKH = 111,
TestnetP2SH = 196,
}
impl BitcoinAddressVersion {
pub fn from_u8(v: u8) -> Result<Self, Error> {
match v {
0 => Ok(BitcoinAddressVersion::MainnetP2PKH),
5 => Ok(BitcoinAddressVersion::MainnetP2SH),
111 => Ok(BitcoinAddressVersion::TestnetP2PKH),
196 => Ok(BitcoinAddressVersion::TestnetP2SH),
_ => Err(Error::InvalidAddressVersion(v)),
}
}
}
pub fn b58_encode(data: &[u8]) -> String {
let mut zeros = 0;
while zeros < data.len() && data[zeros] == 0 {
zeros += 1;
}
let mut result = Vec::with_capacity(data.len() * 138 / 100 + 1);
let mut buff = data.to_vec();
while !buff.is_empty() {
let mut rem = 0;
let mut temp = Vec::with_capacity(buff.len());
for b in &buff {
let cur = (rem << 8) + u32::from(*b);
let div = cur / 58;
rem = cur % 58;
if !temp.is_empty() || div != 0 {
#[allow(clippy::cast_possible_truncation)]
temp.push(div as u8);
}
}
result.push(B58_ALPHABET[rem as usize] as char);
buff = temp;
}
for _ in 0..zeros {
result.push(B58_ALPHABET[0] as char);
}
result.reverse();
result.into_iter().collect()
}
pub fn b58_decode(encoded: impl Into<String>) -> Result<Vec<u8>, Error> {
let encoded: String = encoded.into();
if encoded.is_empty() {
return Ok(vec![]);
}
let mut zeros = 0;
while zeros < encoded.len() && encoded.as_bytes()[zeros] == b'1' {
zeros += 1;
}
let mut buff = vec![0u8; encoded.len() * 733 / 1000 + 1];
for c in encoded.chars() {
let index = B58_BYTE_MAP[c as usize];
if index == -1 {
return Err(Error::InvalidChar(c));
}
let mut carry = u32::try_from(index)?;
for byte in buff.iter_mut().rev() {
carry += u32::from(*byte) * 58;
*byte = (carry & 0xFF) as u8;
carry >>= 8;
}
}
while !buff.is_empty() && buff[0] == 0 {
buff.remove(0);
}
let mut result = vec![0u8; zeros];
result.extend_from_slice(&buff);
Ok(result)
}
pub fn base58check_encode(hash: &[u8], version: BitcoinAddressVersion) -> String {
let version = version as u8;
let mut payload = Vec::with_capacity(21);
payload.push(version);
payload.extend_from_slice(hash);
let sha = DSha256Hash::from_slice(&payload);
let bytes = sha.as_bytes();
let checksum = &bytes[0..4];
let mut data = Vec::with_capacity(25);
data.push(version);
data.extend_from_slice(hash);
data.extend_from_slice(checksum);
b58_encode(&data)
}
pub fn base58check_decode(
address: impl Into<String>,
) -> Result<(Vec<u8>, BitcoinAddressVersion), Error> {
let address: String = address.into();
let buffer = b58_decode(address)?;
let buffer_len = buffer.len();
let checksum = &buffer[buffer_len - 4..];
let data = buffer[1..buffer_len - 4].to_vec();
let sha = DSha256Hash::from_slice(&buffer[0..buffer_len - 4]);
let bytes = sha.as_bytes();
for i in 0..4 {
if checksum[i] != bytes[i] {
return Err(Error::InvalidChecksum(
format!("{checksum:?}"),
format!("{bytes:?}"),
));
}
}
Ok((data, BitcoinAddressVersion::from_u8(buffer[0])?))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_b58_normal_input() {
let input = b"ji1bAyOeHisrHIT1zCvy4RQ78zYZ";
let encoded = b58_encode(input);
let decoded = b58_decode(encoded).unwrap();
assert_eq!(decoded, input);
}
#[test]
fn test_b58_long_input() {
let input = b"ji1bAyOeHisrHIT1zCvy4RtPee3l3BQYI7AxPryRcuXQ4myW4cZlJF861RZif9lgrigWw0bKMXtMUwngfm51jNWDXBdqjYG4PFg6fOFhne7jh61VrhDhdOBSq6UTXlbYYHB4HISWsoYj0tL2S9YfHiHbAlLscNrdngkcPKQaXa1WqcdDnMlnDXsWT9JAHdudnNzyX3nRWrZCZZpw9BTHxYKB5ptcjU7T12MpDFIlELprtqtNGyPeTNgbOG1x2yz8XElLoziXrIdgZczGBSmbrFAiA0FOb4zxVi10pLVt9x3zAtakfO5KQvjCWKDnWmK2nE8DTY3fcn3QqYBxA0756mNrzPjvfikqyv5FUmwHvuDtaLtU2U3XycFoSVkohOste3rrR7sCPtCgug0AtWXCJSsuCFSafFemQwz1sCqBETy6dTUiezAb6XTA9VtMMTJetOeoNIYTGAZp9CDHWVUAtpQhylBHwfb2rzlijR6nhwYXpMQ78zYZ";
let encoded = b58_encode(input);
let decoded = b58_decode(encoded).unwrap();
assert_eq!(decoded, input);
}
#[test]
fn test_b58_randomized_input() {
use rand::thread_rng;
use rand::Rng;
use rand::RngCore;
let mut rng = thread_rng();
for _ in 0..100 {
let len = rng.gen_range(0..=1000);
let mut input = vec![0u8; len];
rng.fill_bytes(&mut input);
let encoded = b58_encode(&input);
let decoded = b58_decode(encoded).unwrap();
assert_eq!(decoded, input);
}
}
#[test]
fn test_b58_check() {
let dummy = vec![
(
BitcoinAddressVersion::MainnetP2PKH,
vec![
"1FzTxL9Mxnm2fdmnQEArfhzJHevwbvcH6d",
"1111111111111111111114oLvT2",
"11111111111111111111BZbvjr",
"12Tbp525fpnBRiSt4iPxXkxMyf5Ze1UeZu",
"12Tbp525fpnBRiSt4iPxXkxMyf5ZWzA5TC",
],
),
(
BitcoinAddressVersion::MainnetP2SH,
vec![
"3GgUssdoWh5QkoUDXKqT6LMESBDf8aqp2y",
"31h1vYVSYuKP6AhS86fbRdMw9XHieotbST",
"31h1vYVSYuKP6AhS86fbRdMw9XHiiQ93Mb",
"339cjcWXDj6ZWt9KBp4YxPKJ8BNH7gn2Nw",
"339cjcWXDj6ZWt9KBp4YxPKJ8BNH14Nnx4",
],
),
(
BitcoinAddressVersion::TestnetP2PKH,
vec![
"mvWRFPELmpCHSkFQ7o9EVdCd9eXeUTa9T8",
"mfWxJ45yp2SFn7UciZyNpvDKrzbhyfKrY8",
"mfWxJ45yp2SFn7UciZyNpvDKrzbi36LaVX",
"mgyZ7874UrDSCpvVnHNLMgAgqegGZBks3w",
"mgyZ7874UrDSCpvVnHNLMgAgqegGQUXx9c",
],
),
(
BitcoinAddressVersion::TestnetP2SH,
vec![
"2N8EgwcZq89akxb6mCTTKiHLVeXRpxjuy98",
"2MsFDzHRUAMpjHxKyoEHU3aMCMsVtMqs1PV",
"2MsFDzHRUAMpjHxKyoEHU3aMCMsVtXMsfu8",
"2MthpoMSYqBbuifmrrwgRaLJZLXaSyK2Rai",
"2MthpoMSYqBbuifmrrwgRaLJZLXaSoxBM5T",
],
),
];
for (version, addresses) in dummy {
for address in addresses {
let (decoded_data, decoded_version) = base58check_decode(address).unwrap();
let encoded = base58check_encode(&decoded_data, decoded_version);
assert_eq!(encoded, address);
assert_eq!(decoded_version, version);
}
}
}
#[test]
fn test_b58_error() {
for c in "^&*(@#%!~`?><,.;:{]}[{|)-_=+§äöüßÄÖÜ".chars() {
let input = format!("{}", c);
let decoded = b58_decode(input);
assert_eq!(decoded, Err(Error::InvalidChar(c)));
}
}
}