diff --git a/libraries/libfc/src/crypto/base58.cpp b/libraries/libfc/src/crypto/base58.cpp index 0b55f23f85..348dc9bdaf 100644 --- a/libraries/libfc/src/crypto/base58.cpp +++ b/libraries/libfc/src/crypto/base58.cpp @@ -1,650 +1,188 @@ +// Copyright (c) 2014-2024 The Bitcoin Core developers // Copyright (c) 2009-2010 Satoshi Nakamoto -// Copyright (c) 2009-2012 The Bitcoin Developers -// Distributed under the MIT/X11 software license, see the accompanying +// Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. - - // -// Why base-58 instead of standard base-64 encoding? -// - Don't want 0OIl characters that look the same in some fonts and -// could be used to create visually identical looking account numbers. -// - A string with non-alphanumeric characters is not as easily accepted as an account number. -// - E-mail usually won't line-break if there's no punctuation to break at. -// - Doubleclicking selects the whole number as one word if it's all alphanumeric. -// -#ifndef BITCOIN_BASE58_H -#define BITCOIN_BASE58_H - -#include -#include -#include -#include +// Pure-bytes base58 encode/decode adapted from Bitcoin Core's +// src/base58.cpp. No OpenSSL BIGNUM dependency. The algorithm runs in +// O(n*m) on small fixed-size keys/signatures, which is negligible in +// practice and removes the per-call heap allocations OpenSSL's BIGNUM +// path introduced. #include -#include -#include #include -#include -#include -#include - -/** Errors thrown by the bignum class */ -class bignum_error : public std::runtime_error -{ -public: - explicit bignum_error(const std::string& str) : std::runtime_error(str) {} -}; - - -/** RAII encapsulated BN_CTX (OpenSSL bignum context) */ -class CAutoBN_CTX -{ -protected: - BN_CTX* pctx; - BN_CTX* operator=(BN_CTX* pnew) { return pctx = pnew; } - -public: - CAutoBN_CTX() - { - pctx = BN_CTX_new(); - if (pctx == NULL) - throw bignum_error("CAutoBN_CTX : BN_CTX_new() returned NULL"); - } - - ~CAutoBN_CTX() - { - if (pctx != NULL) - BN_CTX_free(pctx); - } - - operator BN_CTX*() { return pctx; } - BN_CTX& operator*() { return *pctx; } - BN_CTX** operator&() { return &pctx; } - bool operator!() { return (pctx == NULL); } -}; - - -/** C++ wrapper for BIGNUM (OpenSSL bignum) */ -class CBigNum -{ - BIGNUM* bn; -public: - CBigNum() - : bn(BN_new()) {} - - CBigNum(const CBigNum& b) - : CBigNum() - { - if (!BN_copy(bn, b.bn)) - { - BN_clear_free(bn); - throw bignum_error("CBigNum::CBigNum(const CBigNum&) : BN_copy failed"); - } - } - - CBigNum& operator=(const CBigNum& b) - { - if (!BN_copy(bn, b.bn)) - throw bignum_error("CBigNum::operator= : BN_copy failed"); - return (*this); - } - - ~CBigNum() - { - BN_clear_free(bn); - } - - //CBigNum(char n) is not portable. Use 'signed char' or 'unsigned char'. - CBigNum(signed char n) :CBigNum() { if (n >= 0) setulong(n); else setint64(n); } - CBigNum(short n) :CBigNum() { if (n >= 0) setulong(n); else setint64(n); } - CBigNum(int n) :CBigNum() { if (n >= 0) setulong(n); else setint64(n); } - CBigNum(int64_t n) :CBigNum() { setint64(n); } - CBigNum(unsigned char n) :CBigNum() { setulong(n); } - CBigNum(unsigned short n) :CBigNum() { setulong(n); } - CBigNum(unsigned int n) :CBigNum() { setulong(n); } - CBigNum(uint64_t n) :CBigNum() { setuint64(n); } - - explicit CBigNum(const std::vector& vch) - : CBigNum() - { - setvch(vch); - } - - void setulong(unsigned long n) - { - if (!BN_set_word(bn, n)) - throw bignum_error("CBigNum conversion from unsigned long : BN_set_word failed"); - } - - unsigned long getulong() const - { - return BN_get_word(bn); - } - - unsigned int getuint() const - { - return BN_get_word(bn); - } - - int getint() const - { - unsigned long n = BN_get_word(bn); - if (!BN_is_negative(bn)) - return (n > (unsigned long)std::numeric_limits::max() ? std::numeric_limits::max() : n); - else - return (n > (unsigned long)std::numeric_limits::max() ? std::numeric_limits::min() : -(int)n); - } - - void setint64(int64_t n) - { - unsigned char pch[sizeof(n) + 6]; - unsigned char* p = pch + 4; - bool fNegative = false; - if (n < (int64_t)0) - { - n = -n; - fNegative = true; - } - bool fLeadingZeroes = true; - for (int i = 0; i < 8; i++) - { - unsigned char c = (n >> 56) & 0xff; - n <<= 8; - if (fLeadingZeroes) - { - if (c == 0) - continue; - if (c & 0x80) - *p++ = (fNegative ? 0x80 : 0); - else if (fNegative) - c |= 0x80; - fLeadingZeroes = false; - } - *p++ = c; - } - unsigned int nSize = p - (pch + 4); - pch[0] = (nSize >> 24) & 0xff; - pch[1] = (nSize >> 16) & 0xff; - pch[2] = (nSize >> 8) & 0xff; - pch[3] = (nSize) & 0xff; - BN_mpi2bn(pch, p - pch, bn); - } - - void setuint64(uint64_t n) - { - unsigned char pch[sizeof(n) + 6]; - unsigned char* p = pch + 4; - bool fLeadingZeroes = true; - for (int i = 0; i < 8; i++) - { - unsigned char c = (n >> 56) & 0xff; - n <<= 8; - if (fLeadingZeroes) - { - if (c == 0) - continue; - if (c & 0x80) - *p++ = 0; - fLeadingZeroes = false; - } - *p++ = c; - } - unsigned int nSize = p - (pch + 4); - pch[0] = (nSize >> 24) & 0xff; - pch[1] = (nSize >> 16) & 0xff; - pch[2] = (nSize >> 8) & 0xff; - pch[3] = (nSize) & 0xff; - BN_mpi2bn(pch, p - pch, bn); - } - - - void setvch(const std::vector& vch) - { - std::vector vch2(vch.size() + 4); - unsigned int nSize = vch.size(); - // BIGNUM's byte stream format expects 4 bytes of - // big endian size data info at the front - vch2[0] = (nSize >> 24) & 0xff; - vch2[1] = (nSize >> 16) & 0xff; - vch2[2] = (nSize >> 8) & 0xff; - vch2[3] = (nSize >> 0) & 0xff; - // swap data to big endian - reverse_copy(vch.begin(), vch.end(), vch2.begin() + 4); - BN_mpi2bn(&vch2[0], vch2.size(), bn); - } - - std::vector getvch() const - { - unsigned int nSize = BN_bn2mpi(bn, NULL); - if (nSize <= 4) - return std::vector(); - std::vector vch(nSize); - BN_bn2mpi(bn, &vch[0]); - vch.erase(vch.begin(), vch.begin() + 4); - reverse(vch.begin(), vch.end()); - return vch; - } - - CBigNum& SetCompact(unsigned int nCompact) - { - unsigned int nSize = nCompact >> 24; - std::vector vch(4 + nSize); - vch[3] = nSize; - if (nSize >= 1) vch[4] = (nCompact >> 16) & 0xff; - if (nSize >= 2) vch[5] = (nCompact >> 8) & 0xff; - if (nSize >= 3) vch[6] = (nCompact >> 0) & 0xff; - BN_mpi2bn(&vch[0], vch.size(), bn); - return *this; - } - - unsigned int GetCompact() const - { - unsigned int nSize = BN_bn2mpi(bn, NULL); - std::vector vch(nSize); - nSize -= 4; - BN_bn2mpi(bn, &vch[0]); - unsigned int nCompact = nSize << 24; - if (nSize >= 1) nCompact |= (vch[4] << 16); - if (nSize >= 2) nCompact |= (vch[5] << 8); - if (nSize >= 3) nCompact |= (vch[6] << 0); - return nCompact; - } - - void SetHex(const std::string& str) - { - // skip 0x - const char* psz = str.c_str(); - while (isspace(*psz)) - psz++; - bool fNegative = false; - if (*psz == '-') - { - fNegative = true; - psz++; - } - if (psz[0] == '0' && tolower(psz[1]) == 'x') - psz += 2; - while (isspace(*psz)) - psz++; - - // hex string to bignum - static signed char phexdigit[256] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,1,2,3,4,5,6,7,8,9,0,0,0,0,0,0, 0,0xa,0xb,0xc,0xd,0xe,0xf,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0xa,0xb,0xc,0xd,0xe,0xf,0,0,0,0,0,0,0,0,0 }; - *this = 0; - while (isxdigit(*psz)) - { - *this <<= 4; - int n = phexdigit[(unsigned char)*psz++]; - *this += n; - } - if (fNegative) - BN_set_negative(bn, 1); - } - - std::string ToString(int nBase=10) const - { - CAutoBN_CTX pctx; - CBigNum bnBase = nBase; - CBigNum bn0 = 0; - std::string str; - CBigNum bn = *this; - BN_set_negative(bn.bn, false); - CBigNum dv; - CBigNum rem; - if (BN_cmp(bn.bn, bn0.bn) == 0) - return "0"; - while (BN_cmp(bn.bn, bn0.bn) > 0) - { - if (!BN_div(dv.bn, rem.bn, bn.bn, bnBase.bn, pctx)) - throw bignum_error("CBigNum::ToString() : BN_div failed"); - bn = dv; - unsigned int c = rem.getulong(); - str += "0123456789abcdef"[c]; - } - if (BN_is_negative(this->bn)) - str += "-"; - reverse(str.begin(), str.end()); - return str; - } - - std::string GetHex() const - { - return ToString(16); - } - - - - bool operator!() const - { - return BN_is_zero(bn); - } - - CBigNum& operator+=(const CBigNum& b) - { - if (!BN_add(bn, bn, b.bn)) - throw bignum_error("CBigNum::operator+= : BN_add failed"); - return *this; - } - - CBigNum& operator-=(const CBigNum& b) - { - if (!BN_sub(bn, bn, b.bn)) - throw bignum_error("CBigNum::operator-= : BN_sub failed"); - return *this; - } - - CBigNum& operator*=(const CBigNum& b) - { - CAutoBN_CTX pctx; - if (!BN_mul(bn, bn, b.bn, pctx)) - throw bignum_error("CBigNum::operator*= : BN_mul failed"); - return *this; - } - - CBigNum& operator/=(const CBigNum& b) - { - CAutoBN_CTX pctx; - if (!BN_div(bn, NULL, bn, b.bn, pctx)) - throw bignum_error("CBigNum::operator/= : BN_div failed"); - return *this; - } - - CBigNum& operator%=(const CBigNum& b) - { - CAutoBN_CTX pctx; - if (!BN_div(NULL, bn, bn, b.bn, pctx)) - throw bignum_error("CBigNum::operator%= : BN_div failed"); - return *this; - } - - CBigNum& operator<<=(unsigned int shift) - { - if (!BN_lshift(bn, bn, shift)) - throw bignum_error("CBigNum:operator<<= : BN_lshift failed"); - return *this; - } - - CBigNum& operator>>=(unsigned int shift) - { - // Note: BN_rshift segfaults on 64-bit if 2^shift is greater than the number - // if built on ubuntu 9.04 or 9.10, probably depends on version of openssl - CBigNum a = 1; - a <<= shift; - if (BN_cmp(a.bn, bn) > 0) - { - *this = 0; - return *this; - } - - if (!BN_rshift(bn, bn, shift)) - throw bignum_error("CBigNum:operator>>= : BN_rshift failed"); - return *this; - } - - - CBigNum& operator++() - { - // prefix operator - if (!BN_add(bn, bn, BN_value_one())) - throw bignum_error("CBigNum::operator++ : BN_add failed"); - return *this; - } +#include - const CBigNum operator++(int) - { - // postfix operator - const CBigNum ret = *this; - ++(*this); - return ret; - } - - CBigNum& operator--() - { - // prefix operator - CBigNum r; - if (!BN_sub(r.bn, bn, BN_value_one())) - throw bignum_error("CBigNum::operator-- : BN_sub failed"); - *this = r; - return *this; - } - - const CBigNum operator--(int) - { - // postfix operator - const CBigNum ret = *this; - --(*this); - return ret; - } +#include +#include +#include +#include +#include - const BIGNUM* to_bignum() const { - return bn; - } - BIGNUM* to_bignum() { - return bn; - } +namespace { + +/// Alphabet used for base58 encoding (no 0OIl to avoid visually similar glyphs). +constexpr const char* b58_alphabet = + "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + +/// Inline capacity for the working buffer used inside encode/decode. +/// Covers any input up to ~92 bytes without touching the heap; signatures +/// are 65 bytes, public keys 33, private keys 32, so all production callers +/// stay on the stack. Larger inputs fall back to a heap allocation. +constexpr size_t b58_inline_capacity = 128; + +/// Reverse lookup table: maps an ASCII byte to its base58 digit value, or -1 +/// if the byte is not a valid base58 character. Sized at 256 so any unsigned +/// byte indexes safely. +constexpr int8_t b58_reverse[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, }; - - -inline const CBigNum operator+(const CBigNum& a, const CBigNum& b) -{ - CBigNum r; - if (!BN_add(r.to_bignum(), a.to_bignum(), b.to_bignum())) - throw bignum_error("CBigNum::operator+ : BN_add failed"); - return r; -} - -inline const CBigNum operator-(const CBigNum& a, const CBigNum& b) -{ - CBigNum r; - if (!BN_sub(r.to_bignum(), a.to_bignum(), b.to_bignum())) - throw bignum_error("CBigNum::operator- : BN_sub failed"); - return r; -} - -inline const CBigNum operator-(const CBigNum& a) -{ - CBigNum r(a); - BN_set_negative(r.to_bignum(), !BN_is_negative(r.to_bignum())); - return r; -} - -inline const CBigNum operator*(const CBigNum& a, const CBigNum& b) -{ - CAutoBN_CTX pctx; - CBigNum r; - if (!BN_mul(r.to_bignum(), a.to_bignum(), b.to_bignum(), pctx)) - throw bignum_error("CBigNum::operator* : BN_mul failed"); - return r; -} - -inline const CBigNum operator/(const CBigNum& a, const CBigNum& b) -{ - CAutoBN_CTX pctx; - CBigNum r; - if (!BN_div(r.to_bignum(), NULL, a.to_bignum(), b.to_bignum(), pctx)) - throw bignum_error("CBigNum::operator/ : BN_div failed"); - return r; -} - -inline const CBigNum operator%(const CBigNum& a, const CBigNum& b) -{ - CAutoBN_CTX pctx; - CBigNum r; - if (!BN_mod(r.to_bignum(), a.to_bignum(), b.to_bignum(), pctx)) - throw bignum_error("CBigNum::operator% : BN_div failed"); - return r; -} - -inline const CBigNum operator<<(const CBigNum& a, unsigned int shift) -{ - CBigNum r; - if (!BN_lshift(r.to_bignum(), a.to_bignum(), shift)) - throw bignum_error("CBigNum:operator<< : BN_lshift failed"); - return r; -} - -inline const CBigNum operator>>(const CBigNum& a, unsigned int shift) -{ - CBigNum r = a; - r >>= shift; - return r; -} - -inline bool operator==(const CBigNum& a, const CBigNum& b) { return (BN_cmp(a.to_bignum(), b.to_bignum()) == 0); } -inline bool operator!=(const CBigNum& a, const CBigNum& b) { return (BN_cmp(a.to_bignum(), b.to_bignum()) != 0); } -inline bool operator<=(const CBigNum& a, const CBigNum& b) { return (BN_cmp(a.to_bignum(), b.to_bignum()) <= 0); } -inline bool operator>=(const CBigNum& a, const CBigNum& b) { return (BN_cmp(a.to_bignum(), b.to_bignum()) >= 0); } -inline bool operator<(const CBigNum& a, const CBigNum& b) { return (BN_cmp(a.to_bignum(), b.to_bignum()) < 0); } -inline bool operator>(const CBigNum& a, const CBigNum& b) { return (BN_cmp(a.to_bignum(), b.to_bignum()) > 0); } - - -static const char* pszBase58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; - -// Encode a byte sequence as a base58-encoded string -inline std::string EncodeBase58(const unsigned char* pbegin, const unsigned char* pend, const fc::yield_function_t& yield) -{ - CAutoBN_CTX pctx; - CBigNum bn58 = 58; - CBigNum bn0 = 0; - - // Convert big endian data to little endian - // Extra zero at the end make sure bignum will interpret as a positive number - std::vector vchTmp(pend-pbegin+1, 0); - yield(); - reverse_copy(pbegin, pend, vchTmp.begin()); - yield(); - - // Convert little endian data to bignum - CBigNum bn; - bn.setvch(vchTmp); - - // Convert bignum to std::string - std::string str; - // Expected size increase from base58 conversion is approximately 137% - // use 138% to be safe - str.reserve((pend - pbegin) * 138 / 100 + 1); - CBigNum dv; - CBigNum rem; - while (bn > bn0) - { - yield(); - if (!BN_div(dv.to_bignum(), rem.to_bignum(), bn.to_bignum(), bn58.to_bignum(), pctx)) - throw bignum_error("EncodeBase58 : BN_div failed"); - bn = dv; - unsigned int c = rem.getulong(); - str += pszBase58[c]; - } - - // Leading zeroes encoded as base58 zeros - for (const unsigned char* p = pbegin; p < pend && *p == 0; p++) - str += pszBase58[0]; - - yield(); - // Convert little endian std::string to big endian - reverse(str.begin(), str.end()); -// slog( "Encode '%s'", str.c_str() ); - yield(); - - return str; -} - -// Encode a byte vector as a base58-encoded string -inline std::string EncodeBase58(const std::vector& vch, const fc::yield_function_t& yield) -{ - return EncodeBase58(&vch[0], &vch[0] + vch.size(), yield); +/// Encode the byte range [pbegin, pend) as a base58 string. +/// +/// Algorithm: treat the input as a big-endian unsigned integer and +/// repeatedly divide by 58, collecting remainders as base58 digits. +/// The division is implemented in-place on a working buffer that holds +/// the partial quotient in base58. Leading zero bytes in the input are +/// preserved as leading '1' characters in the output. +/// +/// `yield` is invoked periodically so a deadline-bound caller +/// (abi_serializer with a deadline yield) can interrupt the O(n*m) +/// loop before it finishes on attacker-controlled large inputs. +std::string encode_base58(const unsigned char* pbegin, const unsigned char* pend, + const fc::yield_function_t& yield) { + size_t zeroes = 0; + while (pbegin != pend && *pbegin == 0) { + ++pbegin; + ++zeroes; + } + // log(256) / log(58) ~= 1.366, round up via *138/100 + 1. + const size_t size = static_cast(pend - pbegin) * 138 / 100 + 1; + boost::container::small_vector b58(size); + size_t length = 0; + size_t outer = 0; + while (pbegin != pend) { + if ((++outer & 0x3F) == 0) // every 64 input bytes + yield(); + int carry = *pbegin; + size_t i = 0; + // b58 = b58 * 256 + carry, walking from least to most significant digit. + for (auto it = b58.rbegin(); (carry != 0 || i < length) && it != b58.rend(); ++it, ++i) { + carry += 256 * (*it); + *it = static_cast(carry % 58); + carry /= 58; + } + assert(carry == 0); + length = i; + ++pbegin; + } + auto it = b58.begin() + (size - length); + while (it != b58.end() && *it == 0) + ++it; + std::string str; + str.reserve(zeroes + static_cast(b58.end() - it)); + str.assign(zeroes, '1'); + while (it != b58.end()) + str += b58_alphabet[*(it++)]; + return str; +} + +/// Decode a base58-encoded NUL-terminated string into a byte vector. +/// Returns false on invalid input (unknown character, trailing garbage). +/// +/// Mirrors encode_base58 in reverse: walks the input characters and +/// builds the big-endian base256 representation by repeatedly +/// multiplying the working buffer by 58 and adding the current digit. +bool decode_base58(const char* psz, std::vector& vch) { + vch.clear(); + while (*psz && std::isspace(static_cast(*psz))) + ++psz; + size_t zeroes = 0; + while (*psz == '1') { + ++zeroes; + ++psz; + } + const size_t psz_len = std::strlen(psz); + // log(58) / log(256) ~= 0.733, round up via *733/1000 + 1. + const size_t size = psz_len * 733 / 1000 + 1; + boost::container::small_vector b256(size); + size_t length = 0; + while (*psz && !std::isspace(static_cast(*psz))) { + const int digit = b58_reverse[static_cast(*psz)]; + if (digit < 0) + return false; + int carry = digit; + size_t i = 0; + // b256 = b256 * 58 + carry. + for (auto it = b256.rbegin(); (carry != 0 || i < length) && it != b256.rend(); ++it, ++i) { + carry += 58 * (*it); + *it = static_cast(carry % 256); + carry /= 256; + } + assert(carry == 0); + length = i; + ++psz; + } + while (std::isspace(static_cast(*psz))) + ++psz; + if (*psz != 0) + return false; + auto it = b256.begin() + (size - length); + vch.reserve(zeroes + static_cast(b256.end() - it)); + vch.assign(zeroes, 0x00); + while (it != b256.end()) + vch.push_back(*(it++)); + return true; } -// Decode a base58-encoded string psz into byte vector vchRet -// returns true if decoding is succesful -inline bool DecodeBase58(const char* psz, std::vector& vchRet) -{ - CAutoBN_CTX pctx; - vchRet.clear(); - CBigNum bn58 = 58; - CBigNum bn = 0; - CBigNum bnChar; - while (isspace(*psz)) - psz++; +} // namespace - // Convert big endian string to bignum - for (const char* p = psz; *p; p++) - { - const char* p1 = strchr(pszBase58, *p); - if (p1 == NULL) - { - while (isspace(*p)) - p++; - if (*p != '\0') { - //slog( "%s '%c'", pszBase58,*p ); - return false; - } - break; - } - bnChar.setulong(p1 - pszBase58); - if (!BN_mul(bn.to_bignum(), bn.to_bignum(), bn58.to_bignum(), pctx)) - throw bignum_error("DecodeBase58 : BN_mul failed"); - bn += bnChar; - } - - // Get bignum as little endian data - std::vector vchTmp = bn.getvch(); - - // Trim off sign byte if present - if (vchTmp.size() >= 2 && vchTmp.end()[-1] == 0 && vchTmp.end()[-2] >= 0x80) - vchTmp.erase(vchTmp.end()-1); - - // Restore leading zeros - int nLeadingZeros = 0; - for (const char* p = psz; *p == pszBase58[0]; p++) - nLeadingZeros++; - vchRet.assign(nLeadingZeros + vchTmp.size(), 0); +namespace fc { - // Convert little endian data to big endian - reverse_copy(vchTmp.begin(), vchTmp.end(), vchRet.end() - vchTmp.size()); - return true; +std::string to_base58(const char* d, size_t s, const fc::yield_function_t& yield) { + const auto* p = reinterpret_cast(d); + return encode_base58(p, p + s, yield); } -// Decode a base58-encoded string str into byte vector vchRet -// returns true if decoding is succesful -inline bool DecodeBase58(const std::string& str, std::vector& vchRet) -{ - return DecodeBase58(str.c_str(), vchRet); +std::string to_base58(const std::vector& d, const fc::yield_function_t& yield) { + if (d.empty()) + return std::string(); + return to_base58(d.data(), d.size(), yield); } - -namespace fc { - -std::string to_base58( const char* d, size_t s, const fc::yield_function_t& yield ) { - return EncodeBase58( (const unsigned char*)d, (const unsigned char*)d+s, yield ); +std::vector from_base58(const std::string& base58_str) { + std::vector out; + if (!decode_base58(base58_str.c_str(), out)) { + FC_THROW_EXCEPTION(parse_error_exception, "Unable to decode base58 string {}", base58_str); + } + return std::vector(reinterpret_cast(out.data()), + reinterpret_cast(out.data()) + out.size()); } -std::string to_base58( const std::vector& d, const fc::yield_function_t& yield ) -{ - if( d.size() ) - return to_base58( d.data(), d.size(), yield ); - return std::string(); -} -std::vector from_base58( const std::string& base58_str ) { +size_t from_base58(const std::string& base58_str, char* out_data, size_t out_data_len) { std::vector out; - if( !DecodeBase58( base58_str.c_str(), out ) ) { - FC_THROW_EXCEPTION( parse_error_exception, "Unable to decode base58 string {}", base58_str ); + if (!decode_base58(base58_str.c_str(), out)) { + FC_THROW_EXCEPTION(parse_error_exception, "Unable to decode base58 string {}", base58_str); } - return std::vector((const char*)out.data(), ((const char*)out.data())+out.size() ); -} -/** - * @return the number of bytes decoded - */ -size_t from_base58( const std::string& base58_str, char* out_data, size_t out_data_len ) { - //slog( "%s", base58_str.c_str() ); - std::vector out; - if( !DecodeBase58( base58_str.c_str(), out ) ) { - FC_THROW_EXCEPTION( parse_error_exception, "Unable to decode base58 string {}", base58_str ); - } - FC_ASSERT( out.size() <= out_data_len ); - memcpy( out_data, out.data(), out.size() ); - return out.size(); -} + FC_ASSERT(out.size() <= out_data_len); + std::memcpy(out_data, out.data(), out.size()); + return out.size(); } -#endif +} // namespace fc diff --git a/libraries/libfc/test/CMakeLists.txt b/libraries/libfc/test/CMakeLists.txt index d101d2354b..94e008788b 100644 --- a/libraries/libfc/test/CMakeLists.txt +++ b/libraries/libfc/test/CMakeLists.txt @@ -1,5 +1,6 @@ add_executable( test_fc + crypto/test_base58.cpp crypto/test_blake2.cpp crypto/test_bls.cpp crypto/test_cypher_suites.cpp diff --git a/libraries/libfc/test/crypto/test_base58.cpp b/libraries/libfc/test/crypto/test_base58.cpp new file mode 100644 index 0000000000..71dfcf63b4 --- /dev/null +++ b/libraries/libfc/test/crypto/test_base58.cpp @@ -0,0 +1,175 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +/// Convert a lowercase hex string to a byte vector. Used to keep Bitcoin +/// Core's published test vector format readable in source. +std::vector hex_to_bytes(const std::string& hex) { + std::vector out(hex.size() / 2); + fc::from_hex(hex, out.data(), out.size()); + return out; +} + +/// Bitcoin Core's standard base58 test vectors, taken verbatim from +/// src/test/data/base58_encode_decode.json. Passing these gives us +/// bit-identical encoding to every Bitcoin-derived implementation. +const std::vector> k_bitcoin_vectors = { + {"", ""}, + {"61", "2g"}, + {"626262", "a3gV"}, + {"636363", "aPEr"}, + {"73696d706c792061206c6f6e6720737472696e67", "2cFupjhnEsSn59qHXstmK2ffpLv2"}, + {"00eb15231dfceb60925886b67d065299925915aeb172c06647", "1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L"}, + {"516b6fcd0f", "ABnLTmg"}, + {"bf4f89001e670274dd", "3SEo3LWLoPntC"}, + {"572e4794", "3EFU7m"}, + {"ecac89cad93923c02321", "EJDM8drfXA6uyA"}, + {"10c8511e", "Rt5zm"}, + {"00000000000000000000", "1111111111"}, +}; + +} // namespace + +BOOST_AUTO_TEST_SUITE(base58_tests) + +BOOST_AUTO_TEST_CASE(empty_input_encodes_to_empty_string) { + BOOST_CHECK_EQUAL(fc::to_base58(nullptr, 0, fc::yield_function_t{}), ""); + BOOST_CHECK_EQUAL(fc::to_base58(std::vector{}, fc::yield_function_t{}), ""); +} + +BOOST_AUTO_TEST_CASE(empty_string_decodes_to_empty_vector) { + auto out = fc::from_base58(""); + BOOST_CHECK(out.empty()); +} + +BOOST_AUTO_TEST_CASE(leading_zero_bytes_encode_as_leading_ones) { + // Single zero byte -> "1" + { + const char one_zero[1] = {0}; + BOOST_CHECK_EQUAL(fc::to_base58(one_zero, 1, fc::yield_function_t{}), "1"); + } + // Four zero bytes -> "1111" + { + const char four_zeros[4] = {0, 0, 0, 0}; + BOOST_CHECK_EQUAL(fc::to_base58(four_zeros, 4, fc::yield_function_t{}), "1111"); + } + // 32 zero bytes -> 32 '1's (typical pubkey-shaped input) + { + std::vector thirty_two_zeros(32, 0); + BOOST_CHECK_EQUAL(fc::to_base58(thirty_two_zeros, fc::yield_function_t{}), std::string(32, '1')); + } +} + +BOOST_AUTO_TEST_CASE(leading_ones_decode_to_leading_zero_bytes) { + auto out = fc::from_base58("1111111111"); + BOOST_CHECK_EQUAL(out.size(), 10u); + for (char b : out) + BOOST_CHECK_EQUAL(b, 0); +} + +BOOST_AUTO_TEST_CASE(bitcoin_test_vectors_match) { + for (const auto& [hex_in, b58_expected] : k_bitcoin_vectors) { + const auto bytes = hex_to_bytes(hex_in); + const auto encoded = fc::to_base58(bytes.data(), bytes.size(), fc::yield_function_t{}); + BOOST_CHECK_EQUAL(encoded, b58_expected); + + const auto decoded = fc::from_base58(b58_expected); + BOOST_CHECK_EQUAL_COLLECTIONS(decoded.begin(), decoded.end(), + bytes.begin(), bytes.end()); + } +} + +BOOST_AUTO_TEST_CASE(roundtrip_random_signature_sized_inputs) { + // 65 bytes mirrors the secp256k1/r1 compact-signature size that + // dominates trace_api/get_block allocation profiles. + std::mt19937_64 rng(0xC0FFEEULL); + std::uniform_int_distribution byte_dist(0, 255); + for (int trial = 0; trial < 32; ++trial) { + std::vector in(65); + for (auto& b : in) + b = static_cast(byte_dist(rng)); + const auto encoded = fc::to_base58(in, fc::yield_function_t{}); + const auto decoded = fc::from_base58(encoded); + BOOST_CHECK_EQUAL_COLLECTIONS(decoded.begin(), decoded.end(), + in.begin(), in.end()); + } +} + +BOOST_AUTO_TEST_CASE(roundtrip_random_public_key_sized_inputs) { + std::mt19937_64 rng(0xBADCAFEULL); + std::uniform_int_distribution byte_dist(0, 255); + for (int trial = 0; trial < 32; ++trial) { + std::vector in(33); + for (auto& b : in) + b = static_cast(byte_dist(rng)); + const auto encoded = fc::to_base58(in, fc::yield_function_t{}); + const auto decoded = fc::from_base58(encoded); + BOOST_CHECK_EQUAL_COLLECTIONS(decoded.begin(), decoded.end(), + in.begin(), in.end()); + } +} + +BOOST_AUTO_TEST_CASE(roundtrip_random_private_key_sized_inputs) { + std::mt19937_64 rng(0xDEADBEEFULL); + std::uniform_int_distribution byte_dist(0, 255); + for (int trial = 0; trial < 32; ++trial) { + std::vector in(32); + for (auto& b : in) + b = static_cast(byte_dist(rng)); + const auto encoded = fc::to_base58(in, fc::yield_function_t{}); + const auto decoded = fc::from_base58(encoded); + BOOST_CHECK_EQUAL_COLLECTIONS(decoded.begin(), decoded.end(), + in.begin(), in.end()); + } +} + +BOOST_AUTO_TEST_CASE(invalid_characters_throw) { + // 0, O, I, l, and various punctuation are intentionally absent from + // the base58 alphabet to avoid visually-similar glyphs. Each must + // be rejected. + BOOST_CHECK_THROW(fc::from_base58("0OIl"), fc::parse_error_exception); + BOOST_CHECK_THROW(fc::from_base58("hello!"), fc::parse_error_exception); + BOOST_CHECK_THROW(fc::from_base58("ab cd"), fc::parse_error_exception); +} + +BOOST_AUTO_TEST_CASE(leading_and_trailing_whitespace_tolerated) { + // Old BIGNUM implementation skipped leading and trailing whitespace + // around an otherwise valid base58 string; preserve that behavior so + // existing callers parsing keys with stray whitespace keep working. + BOOST_CHECK_NO_THROW(fc::from_base58(" 2g")); + BOOST_CHECK_NO_THROW(fc::from_base58("2g ")); + const auto a = fc::from_base58("2g"); + const auto b = fc::from_base58(" 2g "); + BOOST_CHECK_EQUAL_COLLECTIONS(a.begin(), a.end(), b.begin(), b.end()); +} + +BOOST_AUTO_TEST_CASE(from_base58_into_fixed_buffer_matches_vector_overload) { + // The two from_base58 overloads share an underlying decode; verify + // the fixed-buffer variant writes the same bytes and reports the + // correct length. + const std::string b58 = "2cFupjhnEsSn59qHXstmK2ffpLv2"; + const auto expected = fc::from_base58(b58); + std::vector buf(expected.size() + 8, char{0x55}); + const size_t written = fc::from_base58(b58, buf.data(), buf.size()); + BOOST_CHECK_EQUAL(written, expected.size()); + BOOST_CHECK_EQUAL_COLLECTIONS(buf.begin(), buf.begin() + written, + expected.begin(), expected.end()); +} + +BOOST_AUTO_TEST_CASE(from_base58_into_undersized_buffer_throws) { + const std::string b58 = "2cFupjhnEsSn59qHXstmK2ffpLv2"; + const auto expected = fc::from_base58(b58); + std::vector buf(expected.size() - 1); + BOOST_CHECK_THROW(fc::from_base58(b58, buf.data(), buf.size()), fc::assert_exception); +} + +BOOST_AUTO_TEST_SUITE_END()