diff --git a/src/Makefile.am b/src/Makefile.am index b5d5c4652a..8fff5eabdc 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -134,6 +134,7 @@ BITCOIN_CORE_H = \ chainparamsseeds.h \ checkqueue.h \ clientversion.h \ + codex32.h \ coins.h \ common/args.h \ common/bloom.h \ @@ -670,6 +671,7 @@ libbitcoin_common_a_SOURCES = \ base58.cpp \ bech32.cpp \ chainparams.cpp \ + codex32.cpp \ coins.cpp \ common/args.cpp \ common/bloom.cpp \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 9f9bdbbd0c..e50467924b 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -83,6 +83,7 @@ BITCOIN_TESTS =\ test/bloom_tests.cpp \ test/bswap_tests.cpp \ test/checkqueue_tests.cpp \ + test/codex32_tests.cpp \ test/coins_tests.cpp \ test/coinstatsindex_tests.cpp \ test/compilerbug_tests.cpp \ diff --git a/src/codex32.cpp b/src/codex32.cpp new file mode 100644 index 0000000000..06632f3e98 --- /dev/null +++ b/src/codex32.cpp @@ -0,0 +1,319 @@ +// Copyright (c) 2017, 2021 Pieter Wuille +// Copyright (c) 2021-2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include + +#include +#include +#include +#include + +namespace codex32 +{ + +namespace +{ + +typedef bech32::internal::data data; + +// Build multiplication and logarithm tables for GF(32). +// +// We represent GF(32) as an extension of GF(2) by appending a root, alpha, of the +// polynomial x^5 + x^3 + 1. All elements of GF(32) can be represented as degree-4 +// polynomials in alpha. So e.g. 1 is represented by 1, alpha by 2, alpha^2 by 4, +// and so on. +// +// alpha is also a generator of the multiplicative group of the field. So every nonzero +// element in GF(32) can be represented as alpha^i, for some i in {0, 1, ..., 31}. +// This representation makes multiplication and division very easy, since it is just +// addition and subtraction in the exponent. +// +// These tables allow converting from the normal binary representation of GF(32) elements +// to the power-of-alpha one. +constexpr std::pair, std::array> GenerateGF32Tables() { + // We use these tables to perform arithmetic in GF(32) below, when constructing the + // tables for GF(1024). + std::array GF32_EXP{}; + std::array GF32_LOG{}; + + // fmod encodes the defining polynomial of GF(32) over GF(2), x^5 + x^3 + 1. + // Because coefficients in GF(2) are binary digits, the coefficients are packed as 101001. + const int fmod = 41; + + // Elements of GF(32) are encoded as vectors of length 5 over GF(2), that is, + // 5 binary digits. Each element (b_4, b_3, b_2, b_1, b_0) encodes a polynomial + // b_4*x^4 + b_3*x^3 + b_2*x^2 + b_1*x^1 + b_0 (modulo fmod). + // For example, 00001 = 1 is the multiplicative identity. + GF32_EXP[0] = 1; + GF32_LOG[0] = -1; + GF32_LOG[1] = 0; + int v = 1; + for (int i = 1; i < 31; ++i) { + // Multiplication by x is the same as shifting left by 1, as + // every coefficient of the polynomial is moved up one place. + v = v << 1; + // If the polynomial now has an x^5 term, we subtract fmod from it + // to remain working modulo fmod. Subtraction is the same as XOR in characteristic + // 2 fields. + if (v & 32) v ^= fmod; + GF32_EXP[i] = v; + GF32_LOG[v] = i; + } + + return std::make_pair(GF32_EXP, GF32_LOG); +} + +constexpr auto tables32 = GenerateGF32Tables(); +constexpr const std::array& GF32_EXP = tables32.first; +constexpr const std::array& GF32_LOG = tables32.second; + +uint8_t gf32_mul(uint8_t x, uint8_t y) { + if (x == 0 || y == 0) { + return 0; + } + return GF32_EXP[(GF32_LOG[x] + GF32_LOG[y]) % 31]; +} + +// The bech32 string "secretshare32" +constexpr const std::array CODEX32_M = { + 16, 25, 24, 3, 25, 11, 16, 23, 29, 3, 25, 17, 10 +}; + +// The bech32 string "secretshare32ex" +constexpr const std::array CODEX32_LONG_M = { + 16, 25, 24, 3, 25, 11, 16, 23, 29, 3, 25, 17, 10, 25, 6, +}; + +// The generator for the codex32 checksum, not including the leading x^13 term +constexpr const std::array CODEX32_GEN = { + 25, 27, 17, 8, 0, 25, 25, 25, 31, 27, 24, 16, 16, +}; + +// The generator for the long codex32 checksum, not including the leading x^15 term +constexpr const std::array CODEX32_LONG_GEN = { + 15, 10, 25, 26, 9, 25, 21, 6, 23, 21, 6, 5, 22, 4, 23, +}; + +/** This function will compute what 5-bit values to XOR into the last + * input values, in order to make the checksum 0. These values are returned in an array + * whose length is implied by the type of the generator polynomial (`CODEX32_GEN` or + * `CODEX32_LONG_GEN`) that is passed in. The result should be xored with the target + * residue ("secretshare32" or "secretshare32ex". */ +template +Residue PolyMod(const data& v, const Residue& gen) +{ + // The input is interpreted as a list of coefficients of a polynomial over F = GF(32), + // in the same way as in bech32. The block comment in bech32::::PolyMod + // provides more details. + // + // Unlike bech32, the output consists of 13 5-bit values, rather than 6, so they cannot + // be packed into a uint32_t, or even a uint64_t. + // + // Like bech32 we have a generator polynomial which defines the BCH code. For "short" + // strings, whose data part is 93 characters or less, we use + // g(x) = x^13 + {25}x^12 + {27}x^11 + {17}x^10 + {8}x^9 + {0}x^8 + {25}x^7 + // + {25}x^6 + {25}x^5 + {31}x^4 + {27}x^3 + {24}x^2 + {16}x + {16} + // + // For long strings, whose data part is more than 93 characters, we use + // g(x) = x^15 + {15}x^14 + {10}x^13 + {25}x^12 + {26}x^11 + {9}x^10 + // + {25}x^9 + {21}x^8 + {6}x^7 + {23}x^6 + {21}x^5 + {6}x^4 + // + {5}x^3 + {22}x^2 + {4}x^1 + {23} + // + // In both cases g is chosen in such a way that the resulting code is a BCH code which + // can detect up to 8 errors in a window of 93 characters. Unlike bech32, no further + // optimization was done to achieve more detection capability than the design parameters. + // + // For information about the {n} encoding of GF32 elements, see the block comment in + // bech32::::PolyMod. + Residue res{}; + res[gen.size() - 1] = 1; + for (const auto v_i : v) { + // We want to update `res` to correspond to a polynomial with one extra term. That is, + // we first multiply it by x and add the next character, which is done by left-shifting + // the entire array and adding the next character to the open slot. + // + // We then reduce it module g, which involves taking the shifted-off character, multiplying + // it by g, and adding it to the result of the previous step. This makes sense because after + // multiplying by x, `res` has the same degree as g, so reduction by g simply requires + // dividing the most significant coefficient of `res` by the most significant coefficient of + // g (which is 1), then subtracting that multiple of g. + // + // Recall that we are working in a characteristic-2 field, so that subtraction is the same + // thing as addition. + + // Multiply by x + uint8_t shift = res[0]; + for (size_t i = 1; i < res.size(); ++i) { + res[i - 1] = res[i]; + } + // Add the next value + res[res.size() - 1] = v_i; + // Reduce + if (shift != 0) { + for(size_t i = 0; i < res.size(); ++i) { + if (gen[i] != 0) { + res[i] ^= gf32_mul(gen[i], shift); + } + } + } + } + return res; +} + +/** Verify a checksum. */ +template +bool VerifyChecksum(const std::string& hrp, const data& values, const Residue& gen, const Residue& target) +{ + auto res = PolyMod(Cat(bech32::internal::ExpandHRP(hrp), values), gen); + for (size_t i = 0; i < res.size(); ++i) { + if (res[i] != target[i]) { + return 0; + } + } + return 1; +} + +/** Create a checksum. */ +template +data CreateChecksum(const std::string& hrp, const data& values, const Residue& gen, const Residue& target) +{ + data enc = Cat(bech32::internal::ExpandHRP(hrp), values); + enc.resize(enc.size() + gen.size()); + const auto checksum = PolyMod(enc, gen); + data ret(gen.size()); + for (size_t i = 0; i < checksum.size(); ++i) { + ret[i] = checksum[i] ^ target[i]; + } + return ret; +} + +} // namespace + +/** Encode a codex32 string. */ +std::string Result::Encode() const { + assert(IsValid()); + + const data checksum = m_data.size() <= 80 + ? CreateChecksum(m_hrp, m_data, CODEX32_GEN, CODEX32_M) + : CreateChecksum(m_hrp, m_data, CODEX32_LONG_GEN, CODEX32_LONG_M); + return bech32::internal::Encode(m_hrp, m_data, checksum); +} + +/** Decode a codex32 string */ +Result::Result(const std::string& str) { + m_valid = OK; + + auto res = bech32::internal::Decode(str, 127, 6); + + if (str.size() > 127) { + m_valid = INVALID_LENGTH; + // Early return since if we failed the max size check, Decode did not give us any data. + return; + } else if (res.first.empty() && res.second.empty()) { + m_valid = BECH32_DECODE; + return; + } else if (res.first != "ms") { + m_valid = INVALID_HRP; + // Early return since if the HRP is wrong, all bets are off and no point continuing + return; + } + m_hrp = std::move(res.first); + + if (res.second.size() >= 45 && res.second.size() <= 90) { + // If, after converting back to base-256, we have 5 or more bits of data + // remaining, it means that we had an entire character of useless data, + // which shouldn't have been included. + if (((res.second.size() - 6 - 13) * 5) % 8 > 4) { + m_valid = INVALID_LENGTH; + } else if (VerifyChecksum(m_hrp, res.second, CODEX32_GEN, CODEX32_M)) { + m_data = data(res.second.begin(), res.second.end() - 13); + } else { + m_valid = BAD_CHECKSUM; + } + } else if (res.second.size() >= 96 && res.second.size() <= 124) { + if (((res.second.size() - 6 - 15) * 5) % 8 > 4) { + m_valid = INVALID_LENGTH; + } else if (VerifyChecksum(m_hrp, res.second, CODEX32_LONG_GEN, CODEX32_LONG_M)) { + m_data = data(res.second.begin(), res.second.end() - 15); + } else { + m_valid = BAD_CHECKSUM; + } + } else { + m_valid = INVALID_LENGTH; + } + + if (m_valid == OK) { + auto k = bech32::internal::CHARSET[res.second[0]]; + if (k < '0' || k == '1' || k > '9') { + m_valid = INVALID_K; + } + if (k == '0' && m_data[5] != 16) { + // If the threshold is 0, the only allowable share is S + m_valid = INVALID_SHARE_IDX; + } + } +} + +Result::Result(std::string&& hrp, size_t k, const std::string& id, char share_idx, const std::vector& data) { + m_valid = OK; + if (hrp != "ms") { + m_valid = INVALID_HRP; + } + m_hrp = hrp; + if (k == 1 || k > 9) { + m_valid = INVALID_K; + } + if (id.size() != 4) { + m_valid = INVALID_ID_LEN; + } + int8_t sidx = bech32::internal::CHARSET_REV[(unsigned char) share_idx]; + if (sidx == -1) { + m_valid = INVALID_SHARE_IDX; + } + if (k == 0 && sidx != 16) { + // If the threshold is 0, the only allowable share is S + m_valid = INVALID_SHARE_IDX; + } + for (size_t i = 0; i < id.size(); ++i) { + if (bech32::internal::CHARSET_REV[(unsigned char) id[i]] == -1) { + m_valid = INVALID_ID_CHAR; + } + } + + if (m_valid != OK) { + // early bail before allocating memory + return; + } + + m_data.reserve(6 + ((data.size() * 8) + 4) / 5); + m_data.push_back(bech32::internal::CHARSET_REV['0' + k]); + m_data.push_back(bech32::internal::CHARSET_REV[(unsigned char) id[0]]); + m_data.push_back(bech32::internal::CHARSET_REV[(unsigned char) id[1]]); + m_data.push_back(bech32::internal::CHARSET_REV[(unsigned char) id[2]]); + m_data.push_back(bech32::internal::CHARSET_REV[(unsigned char) id[3]]); + m_data.push_back(sidx); + ConvertBits<8, 5, true>([&](unsigned char c) { m_data.push_back(c); }, data.begin(), data.end()); +} + +std::string Result::GetIdString() const { + assert(IsValid()); + + std::string ret; + ret.reserve(4); + ret.push_back(bech32::internal::CHARSET[m_data[1]]); + ret.push_back(bech32::internal::CHARSET[m_data[2]]); + ret.push_back(bech32::internal::CHARSET[m_data[3]]); + ret.push_back(bech32::internal::CHARSET[m_data[4]]); + return ret; +} + +size_t Result::GetK() const { + assert(IsValid()); + return bech32::internal::CHARSET[m_data[0]] - '0'; +} + +} // namespace codex32 diff --git a/src/codex32.h b/src/codex32.h new file mode 100644 index 0000000000..7dd49c6fc8 --- /dev/null +++ b/src/codex32.h @@ -0,0 +1,106 @@ +// Copyright (c) 2023 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +// codex32 is a string encoding format for BIP-32 seeds. Like bech32 and +// bech32m, the outputs consist of a human-readable part (alphanumeric), +// a separator character (1), and a base32 data section. The final 13 +// characters are a checksum. +// +// For more information, see BIP 93. + +#ifndef BITCOIN_CODEX32_H +#define BITCOIN_CODEX32_H + +#include +#include +#include +#include +#include + +#include +#include + +namespace codex32 +{ + +enum Error { + OK, + BAD_CHECKSUM, + BECH32_DECODE, + INVALID_HRP, + INVALID_ID_LEN, + INVALID_ID_CHAR, + INVALID_LENGTH, + INVALID_K, + INVALID_SHARE_IDX, +}; + +class Result +{ +public: + /** Construct a codex32 result by parsing a string */ + Result(const std::string& str); + + /** Construct a codex32 directly from a HRP, k, seed ID, share index and payload + * + * This constructor requires the hrp to be the lowercase string "ms", but will + * ignore the case of `id` and `share_idx`. */ + Result(std::string&& hrp, size_t k, const std::string& id, char share_idx, const std::vector& data); + + /** Boolean indicating whether the data was successfully parsed. + * + * If this returns false, most of the other methods on this class will assert. */ + bool IsValid() const { + return m_valid == OK; + } + + /** Accessor for the specific parsing/construction error */ + Error error() const { + return m_valid; + } + + /** Accessor for the human-readable part of the codex32 string */ + const std::string& GetHrp() const { + assert(IsValid()); + return m_hrp; + } + + /** Accessor for the seed ID, in string form */ + std::string GetIdString() const; + + /** Accessor for the secret sharing threshold; 0 for a bare seed; (size_t)-1 if unavailable/invalid */ + size_t GetK() const; + + /** Accessor for the share index; (uint8_t)-1 if unavailable/invalid */ + char GetShareIndex() const { + assert(IsValid()); + return bech32::internal::CHARSET[m_data[5]]; + } + + /** Accessor for the binary payload data (in base 256, not gf32) */ + std::vector GetPayload() const { + assert(IsValid()); + + std::vector ret; + ret.reserve(((m_data.size() - 6) * 5) / 8); + // Note that `ConvertBits` returns a bool indicating whether or not nonzero bits + // were discarded. In BIP 93, we discard bits regardless of whether they are 0, + // so this is not an error and does not need to be checked. + ConvertBits<5, 8, false>([&](unsigned char c) { ret.push_back(c); }, m_data.begin() + 6, m_data.end()); + return ret; + }; + + /** (Re-)encode the codex32 data as a hrp string */ + std::string Encode() const; + +private: + Error m_valid; //!< codex32::OK if the string was decoded correctly + + std::string m_hrp; //!< The human readable part + std::vector m_data; //!< The payload (remaining data, excluding checksum) +}; + +} // namespace codex32 + +#endif // BITCOIN_CODEX32_H diff --git a/src/test/codex32_tests.cpp b/src/test/codex32_tests.cpp new file mode 100644 index 0000000000..8df80ab390 --- /dev/null +++ b/src/test/codex32_tests.cpp @@ -0,0 +1,84 @@ +// Copyright (c) 2023 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include + +#include + +#include +#include + +BOOST_AUTO_TEST_SUITE(codex32_tests) + +BOOST_AUTO_TEST_CASE(codex32_bip93_misc_invalid) +{ + // This example uses a "0" threshold with a non-"s" index + const std::string input1 = "ms10fauxxxxxxxxxxxxxxxxxxxxxxxxxxxx0z26tfn0ulw3p"; + const auto dec1 = codex32::Result{input1}; + BOOST_CHECK_EQUAL(dec1.error(), codex32::INVALID_SHARE_IDX); + + // This example has a threshold that is not a digit. + const std::string input2 = "ms1fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxda3kr3s0s2swg"; + const auto dec2 = codex32::Result{input2}; + BOOST_CHECK_EQUAL(dec2.error(), codex32::INVALID_K); +} + +BOOST_AUTO_TEST_CASE(codex32_bip93_vector_1) +{ + const std::string input = "ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw"; + + const auto dec = codex32::Result{input}; + BOOST_CHECK(dec.IsValid()); + BOOST_CHECK_EQUAL(dec.error(), codex32::OK); + BOOST_CHECK_EQUAL(dec.GetHrp(), "ms"); + BOOST_CHECK_EQUAL(dec.GetK(), 0); + BOOST_CHECK_EQUAL(dec.GetIdString(), "test"); + BOOST_CHECK_EQUAL(dec.GetShareIndex(), 's'); + + const auto payload = dec.GetPayload(); + BOOST_CHECK_EQUAL(payload.size(), 16); + BOOST_CHECK_EQUAL(HexStr(payload), "318c6318c6318c6318c6318c6318c631"); + + // Try re-encoding + BOOST_CHECK_EQUAL(input, dec.Encode()); + + // Try directly constructing the share + const auto direct = codex32::Result("ms", 0, "test", 's', payload); + BOOST_CHECK(direct.IsValid()); + BOOST_CHECK_EQUAL(direct.error(), codex32::OK); + BOOST_CHECK_EQUAL(direct.GetIdString(), "test"); + BOOST_CHECK_EQUAL(direct.GetShareIndex(), 's'); + + // We cannot check that the codex32 string is exactly the same as the + // input, since it will not be -- the test vector has nonzero trailing + // bits while our code will always choose zero trailing bits. But we + // can at least check that the payloads are identical. + const auto payload_direct = direct.GetPayload(); + BOOST_CHECK_EQUAL(HexStr(payload), HexStr(payload_direct)); +} + +BOOST_AUTO_TEST_CASE(codex32_bip93_vector_2) +{ + const std::string input_a = "MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM"; + const auto dec_a = codex32::Result{input_a}; + BOOST_CHECK(dec_a.IsValid()); + BOOST_CHECK_EQUAL(dec_a.GetHrp(), "ms"); + BOOST_CHECK_EQUAL(dec_a.GetK(), 2); + BOOST_CHECK_EQUAL(dec_a.GetIdString(), "name"); + BOOST_CHECK_EQUAL(dec_a.GetShareIndex(), 'a'); + BOOST_CHECK(CaseInsensitiveEqual(input_a, dec_a.Encode())); + + const std::string input_c = "MS12NAMECACDEFGHJKLMNPQRSTUVWXYZ023FTR2GDZMPY6PN"; + const auto dec_c = codex32::Result{input_c}; + BOOST_CHECK(dec_c.IsValid()); + BOOST_CHECK_EQUAL(dec_c.GetHrp(), "ms"); + BOOST_CHECK_EQUAL(dec_c.GetK(), 2); + BOOST_CHECK_EQUAL(dec_c.GetIdString(), "name"); + BOOST_CHECK_EQUAL(dec_c.GetShareIndex(), 'c'); + BOOST_CHECK(CaseInsensitiveEqual(input_c, dec_c.Encode())); +} + +BOOST_AUTO_TEST_SUITE_END()