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(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum Error {
#[error("Bad character encountered: {0}")]
BadChar(char),
#[error("Bad checksum - expected {0}, received: {1}")]
BadChecksum(String, String),
#[error("Unknown address version, received: {0}")]
UnknownAddressVersion(u8),
#[error(transparent)]
TryFromInt(#[from] std::num::TryFromIntError),
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Version {
MainnetP2PKH = 0,
MainnetP2SH = 5,
TestnetP2PKH = 111,
TestnetP2SH = 196,
}
pub fn b58_encode<T>(slice: T) -> String
where
T: AsRef<[u8]>,
{
let data = slice.as_ref();
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<T>(str: T) -> Result<Vec<u8>, Error>
where
T: Into<String>,
{
let str: String = str.into();
if str.is_empty() {
return Ok(vec![]);
}
let mut zeros = 0;
while zeros < str.len() && str.as_bytes()[zeros] == b'1' {
zeros += 1;
}
let mut buff = vec![0u8; str.len() * 733 / 1000 + 1];
for c in str.chars() {
let index = B58_BYTE_MAP[c as usize];
if index == -1 {
return Err(Error::BadChar(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<T>(hash: T, version: u8) -> String
where
T: AsRef<[u8]>,
{
let hash = hash.as_ref();
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<T>(str: T) -> Result<(Vec<u8>, u8), Error>
where
T: Into<String>,
{
let str: String = str.into();
let buff = b58_decode(str)?;
let buff_max = buff.len();
let checksum = &buff[buff_max - 4..];
let data = buff[1..buff_max - 4].to_vec();
let sha = DSha256Hash::from_slice(&buff[0..buff_max - 4]);
let bytes = sha.as_bytes();
for i in 0..4 {
if checksum[i] != bytes[i] {
return Err(Error::BadChecksum(
format!("{checksum:?}"),
format!("{bytes:?}"),
));
}
}
Ok((data, buff[0]))
}
#[cfg(test)]
mod tests {
use rand::thread_rng;
use rand::Rng;
use rand::RngCore;
use super::*;
#[test]
fn test_crypto_b58_normal_roundtrip() {
let input = b"ji1bAyOeHisrHIT1zCvy4RQ78zYZ";
let encoded = b58_encode(input);
let decoded = b58_decode(encoded).unwrap();
assert_eq!(decoded, input);
}
#[test]
fn test_crypto_b58_long_roundtrip() {
let input = b"ji1bAyOeHisrHIT1zCvy4RtPee3l3BQYI7AxPryRcuXQ4myW4cZlJF861RZif9lgrigWw0bKMXtMUwngfm51jNWDXBdqjYG4PFg6fOFhne7jh61VrhDhdOBSq6UTXlbYYHB4HISWsoYj0tL2S9YfHiHbAlLscNrdngkcPKQaXa1WqcdDnMlnDXsWT9JAHdudnNzyX3nRWrZCZZpw9BTHxYKB5ptcjU7T12MpDFIlELprtqtNGyPeTNgbOG1x2yz8XElLoziXrIdgZczGBSmbrFAiA0FOb4zxVi10pLVt9x3zAtakfO5KQvjCWKDnWmK2nE8DTY3fcn3QqYBxA0756mNrzPjvfikqyv5FUmwHvuDtaLtU2U3XycFoSVkohOste3rrR7sCPtCgug0AtWXCJSsuCFSafFemQwz1sCqBETy6dTUiezAb6XTA9VtMMTJetOeoNIYTGAZp9CDHWVUAtpQhylBHwfb2rzlijR6nhwYXpMQ78zYZ";
let encoded = b58_encode(input);
let decoded = b58_decode(encoded).unwrap();
assert_eq!(decoded, input);
}
#[test]
fn test_crypto_b58_randomized_roundtrip() {
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_crypto_b58_check_roundtrip() {
let dummy = vec![
(
Version::MainnetP2PKH,
vec![
"1FzTxL9Mxnm2fdmnQEArfhzJHevwbvcH6d",
"1111111111111111111114oLvT2",
"11111111111111111111BZbvjr",
"12Tbp525fpnBRiSt4iPxXkxMyf5Ze1UeZu",
"12Tbp525fpnBRiSt4iPxXkxMyf5ZWzA5TC",
],
),
(
Version::MainnetP2SH,
vec![
"3GgUssdoWh5QkoUDXKqT6LMESBDf8aqp2y",
"31h1vYVSYuKP6AhS86fbRdMw9XHieotbST",
"31h1vYVSYuKP6AhS86fbRdMw9XHiiQ93Mb",
"339cjcWXDj6ZWt9KBp4YxPKJ8BNH7gn2Nw",
"339cjcWXDj6ZWt9KBp4YxPKJ8BNH14Nnx4",
],
),
(
Version::TestnetP2PKH,
vec![
"mvWRFPELmpCHSkFQ7o9EVdCd9eXeUTa9T8",
"mfWxJ45yp2SFn7UciZyNpvDKrzbhyfKrY8",
"mfWxJ45yp2SFn7UciZyNpvDKrzbi36LaVX",
"mgyZ7874UrDSCpvVnHNLMgAgqegGZBks3w",
"mgyZ7874UrDSCpvVnHNLMgAgqegGQUXx9c",
],
),
(
Version::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 as u8);
}
}
}
#[test]
fn test_crypto_b58_error() {
for c in "^&*(@#%!~`?><,.;:{]}[{|)-_=+§äöüßÄÖÜ".chars() {
let input = format!("{}", c);
let decoded = b58_decode(input);
assert_eq!(decoded, Err(Error::BadChar(c)));
}
}
}