diff --git a/src/Makefile.am b/src/Makefile.am index e7cd2e9c08..77fc92ee81 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -139,6 +139,7 @@ BITCOIN_CORE_H = \ checkqueue.h \ clientversion.h \ cluster_linearize.h \ + codex32.h \ coins.h \ common/args.h \ common/bloom.h \ @@ -682,6 +683,7 @@ libbitcoin_common_a_SOURCES = \ bech32.cpp \ chainparamsbase.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 efd33cab83..3aa667b373 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -84,6 +84,7 @@ BITCOIN_TESTS =\ test/bswap_tests.cpp \ test/checkqueue_tests.cpp \ test/cluster_linearize_tests.cpp \ + test/codex32_tests.cpp \ test/coins_tests.cpp \ test/coinscachepair_tests.cpp \ test/coinstatsindex_tests.cpp \ diff --git a/src/bech32.cpp b/src/bech32.cpp index 5694ad54c8..786f8d9010 100644 --- a/src/bech32.cpp +++ b/src/bech32.cpp @@ -17,22 +17,7 @@ namespace bech32 namespace { -typedef std::vector data; - -/** The Bech32 and Bech32m character set for encoding. */ -const char* CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; - -/** The Bech32 and Bech32m character set for decoding. */ -const int8_t CHARSET_REV[128] = { - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, - -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, - 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, - -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, - 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 -}; +typedef internal::data data; /** We work with the finite field GF(1024) defined as a degree 2 extension of the base field GF(32) * The defining polynomial of the extension is x^2 + 9x + 23. @@ -308,6 +293,55 @@ bool CheckCharacters(const std::string& str, std::vector& errors) return errors.empty(); } +/** Verify a checksum. */ +Encoding VerifyChecksum(const std::string& hrp, const data& values) +{ + // PolyMod computes what value to xor into the final values to make the checksum 0. However, + // if we required that the checksum was 0, it would be the case that appending a 0 to a valid + // list of values would result in a new valid list. For that reason, Bech32 requires the + // resulting checksum to be 1 instead. In Bech32m, this constant was amended. See + // https://gist.github.com/sipa/14c248c288c3880a3b191f978a34508e for details. + auto enc = internal::PreparePolynomialCoefficients(hrp, values); + const uint32_t check = PolyMod(enc); + if (check == EncodingConstant(Encoding::BECH32)) return Encoding::BECH32; + if (check == EncodingConstant(Encoding::BECH32M)) return Encoding::BECH32M; + return Encoding::INVALID; +} + +/** Create a checksum. */ +data CreateChecksum(Encoding encoding, const std::string& hrp, const data& values) +{ + auto enc = internal::PreparePolynomialCoefficients(hrp, values); + enc.insert(enc.end(), CHECKSUM_SIZE, 0x00); + uint32_t mod = PolyMod(enc) ^ EncodingConstant(encoding); // Determine what to XOR into those 6 zeroes. + data ret(CHECKSUM_SIZE); + for (size_t i = 0; i < CHECKSUM_SIZE; ++i) { + // Convert the 5-bit groups in mod to checksum values. + ret[i] = (mod >> (5 * (5 - i))) & 31; + } + return ret; +} + +} // namespace + +namespace internal { + +/** The Bech32 and Bech32m character set for encoding. */ +const char* CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + +/** The Bech32 and Bech32m character set for decoding. */ +const int8_t CHARSET_REV[128] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 +}; + + std::vector PreparePolynomialCoefficients(const std::string& hrp, const data& values) { data ret; @@ -323,39 +357,9 @@ std::vector PreparePolynomialCoefficients(const std::string& hrp, return ret; } -/** Verify a checksum. */ -Encoding VerifyChecksum(const std::string& hrp, const data& values) -{ - // PolyMod computes what value to xor into the final values to make the checksum 0. However, - // if we required that the checksum was 0, it would be the case that appending a 0 to a valid - // list of values would result in a new valid list. For that reason, Bech32 requires the - // resulting checksum to be 1 instead. In Bech32m, this constant was amended. See - // https://gist.github.com/sipa/14c248c288c3880a3b191f978a34508e for details. - auto enc = PreparePolynomialCoefficients(hrp, values); - const uint32_t check = PolyMod(enc); - if (check == EncodingConstant(Encoding::BECH32)) return Encoding::BECH32; - if (check == EncodingConstant(Encoding::BECH32M)) return Encoding::BECH32M; - return Encoding::INVALID; -} -/** Create a checksum. */ -data CreateChecksum(Encoding encoding, const std::string& hrp, const data& values) -{ - auto enc = PreparePolynomialCoefficients(hrp, values); - enc.insert(enc.end(), CHECKSUM_SIZE, 0x00); - uint32_t mod = PolyMod(enc) ^ EncodingConstant(encoding); // Determine what to XOR into those 6 zeroes. - data ret(CHECKSUM_SIZE); - for (size_t i = 0; i < CHECKSUM_SIZE; ++i) { - // Convert the 5-bit groups in mod to checksum values. - ret[i] = (mod >> (5 * (5 - i))) & 31; - } - return ret; -} - -} // namespace - -/** Encode a Bech32 or Bech32m string. */ -std::string Encode(Encoding encoding, const std::string& hrp, const data& values) { +/** Encode a hrpstring without concerning ourselves with checksum validity */ +std::string Encode(const std::string& hrp, const data& values, const data& checksum) { // First ensure that the HRP is all lowercase. BIP-173 and BIP350 require an encoder // to return a lowercase Bech32/Bech32m string, but if given an uppercase HRP, the // result will always be invalid. @@ -366,17 +370,17 @@ std::string Encode(Encoding encoding, const std::string& hrp, const data& values ret += hrp; ret += '1'; for (const uint8_t& i : values) ret += CHARSET[i]; - for (const uint8_t& i : CreateChecksum(encoding, hrp, values)) ret += CHARSET[i]; + for (const uint8_t& i : checksum) ret += CHARSET[i]; return ret; } -/** Decode a Bech32 or Bech32m string. */ -DecodeResult Decode(const std::string& str, CharLimit limit) { +/** Decode a hrpstring without concerning ourselves with checksum validity */ +std::pair Decode(const std::string& str, CharLimit limit, size_t checksum_length) { std::vector errors; if (!CheckCharacters(str, errors)) return {}; size_t pos = str.rfind('1'); if (str.size() > limit) return {}; - if (pos == str.npos || pos == 0 || pos + CHECKSUM_SIZE >= str.size()) { + if (pos == str.npos || pos == 0 || pos + checksum_length >= str.size()) { return {}; } data values(str.size() - 1 - pos); @@ -394,9 +398,22 @@ DecodeResult Decode(const std::string& str, CharLimit limit) { for (size_t i = 0; i < pos; ++i) { hrp += LowerCase(str[i]); } - Encoding result = VerifyChecksum(hrp, values); + return std::make_pair(hrp, values); +} + +} // namespace internal + +/** Encode a Bech32 or Bech32m string. */ +std::string Encode(Encoding encoding, const std::string& hrp, const data& values) { + return internal::Encode(hrp, values, CreateChecksum(encoding, hrp, values)); +} + +/** Decode a Bech32 or Bech32m string. */ +DecodeResult Decode(const std::string& str, CharLimit limit) { + auto res = internal::Decode(str, limit, CHECKSUM_SIZE); + Encoding result = VerifyChecksum(res.first, res.second); if (result == Encoding::INVALID) return {}; - return {result, std::move(hrp), data(values.begin(), values.end() - CHECKSUM_SIZE)}; + return {result, std::move(res.first), data(res.second.begin(), res.second.end() - CHECKSUM_SIZE)}; } /** Find index of an incorrect character in a Bech32 string. */ @@ -432,7 +449,7 @@ std::pair> LocateErrors(const std::string& str, Ch data values(length); for (size_t i = pos + 1; i < str.size(); ++i) { unsigned char c = str[i]; - int8_t rev = CHARSET_REV[c]; + int8_t rev = internal::CHARSET_REV[c]; if (rev == -1) { error_locations.push_back(i); return std::make_pair("Invalid Base 32 character", std::move(error_locations)); @@ -447,7 +464,7 @@ std::pair> LocateErrors(const std::string& str, Ch std::vector possible_errors; // Recall that (expanded hrp + values) is interpreted as a list of coefficients of a polynomial // over GF(32). PolyMod computes the "remainder" of this polynomial modulo the generator G(x). - auto enc = PreparePolynomialCoefficients(hrp, values); + auto enc = internal::PreparePolynomialCoefficients(hrp, values); uint32_t residue = PolyMod(enc) ^ EncodingConstant(encoding); // All valid codewords should be multiples of G(x), so this remainder (after XORing with the encoding diff --git a/src/bech32.h b/src/bech32.h index 33d1ca1935..3d49588e18 100644 --- a/src/bech32.h +++ b/src/bech32.h @@ -37,6 +37,7 @@ enum class Encoding { * and we would never encode an address with such a massive value */ enum CharLimit : size_t { BECH32 = 90, //!< BIP173/350 imposed character limit for Bech32(m) encoded addresses. This guarantees finding up to 4 errors. + CODEX32 = 127, }; /** Encode a Bech32 or Bech32m string. If hrp contains uppercase characters, this will cause an @@ -59,6 +60,24 @@ DecodeResult Decode(const std::string& str, CharLimit limit = CharLimit::BECH32) /** Return the positions of errors in a Bech32 string. */ std::pair> LocateErrors(const std::string& str, CharLimit limit = CharLimit::BECH32); +// The internal namespace is used for things shared between bech32(m) and codex32. +// These functions should not be used except by other hrpstring-encoded codes. +namespace internal { +typedef std::vector data; + +extern const char* CHARSET; +extern const int8_t CHARSET_REV[128]; + +std::vector PreparePolynomialCoefficients(const std::string& hrp, const data& values); + +/** Encode a hrpstring without concerning ourselves with checksum validity */ +std::string Encode(const std::string& hrp, const data& values, const data& checksum); + +/** Decode a hrpstring without concerning ourselves with checksum validity */ +std::pair Decode(const std::string& str, CharLimit limit, size_t checksum_length); + +} // namespace internal + } // namespace bech32 #endif // BITCOIN_BECH32_H diff --git a/src/codex32.cpp b/src/codex32.cpp new file mode 100644 index 0000000000..d5e8085e54 --- /dev/null +++ b/src/codex32.cpp @@ -0,0 +1,424 @@ +// 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]; +} + +uint8_t gf32_div(uint8_t x, uint8_t y) { + assert(y != 0); + if (x == 0) { + return 0; + } + return GF32_EXP[(GF32_LOG[x] + 31 - 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 enc = bech32::internal::PreparePolynomialCoefficients(hrp, values); + auto res = PolyMod(enc, 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 = bech32::internal::PreparePolynomialCoefficients(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; +} + +// Given a set of share indices and a target index `idx`, which must be in the set, +// compute the Lagrange basis polynomial for `idx` evaluated at the point `eval`. +// +// All inputs are GF32 elements, rather than array indices or anything else. +uint8_t lagrange_coefficient(std::vector& indices, uint8_t idx, uint8_t eval) { + uint8_t num = 1; + uint8_t den = 1; + for (const auto idx_i : indices) { + if (idx_i != idx) { + num = gf32_mul(num, idx_i ^ eval); + den = gf32_mul(den, idx_i ^ idx); + } + } + + // return num / den + return gf32_div(num, den); +} + +} // namespace + +std::string ErrorString(Error e) { + switch (e) { + case OK: return "ok"; + case BAD_CHECKSUM: return "bad checksum"; + case BECH32_DECODE: return "bech32 decode failure (invalid character, no HRP, or inconsistent case)"; + case INVALID_HRP: return "hrp differed from 'ms'"; + case INVALID_ID_LEN: return "seed ID was not 4 characters"; + case INVALID_ID_CHAR: return "seed ID used a non-bech32 character"; + case INVALID_LENGTH: return "invalid length"; + case INVALID_K: return "invalid threshold (k) value"; + case INVALID_SHARE_IDX: return "invalid share index"; + case TOO_FEW_SHARES: return "tried to derive a share but did not have enough input shares"; + case DUPLICATE_SHARE: return "tried to derive a share but two input shares had the same index"; + case MISMATCH_K: return "tried to derive a share but input shares had inconsistent threshold (k) values"; + case MISMATCH_ID: return "tried to derive a share but input shares had inconsistent seed IDs"; + case MISMATCH_LENGTH: return "tried to derive a share but input shares had inconsistent lengths"; + } + assert(0); +} + +/** 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, bech32::CharLimit::CODEX32, bech32::CHECKSUM_SIZE); + + if (str.size() > bech32::CharLimit::CODEX32) { + 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()); +} + +Result::Result(const std::vector& shares, char output_idx) { + m_valid = OK; + + int8_t oidx = bech32::internal::CHARSET_REV[(unsigned char) output_idx]; + if (oidx == -1) { + m_valid = INVALID_SHARE_IDX; + } + if (shares.empty()) { + m_valid = TOO_FEW_SHARES; + return; + } + size_t k = shares[0].GetK(); + if (k > shares.size()) { + m_valid = TOO_FEW_SHARES; + } + if (m_valid != OK) { + return; + } + + std::vector indices; + indices.reserve(shares.size()); + for (size_t i = 0; i < shares.size(); ++i) { + // Currently the only supported hrp is "ms" so it is impossible to violate this + assert (shares[0].m_hrp == shares[i].m_hrp); + if (shares[0].m_data[0] != shares[i].m_data[0]) { + m_valid = MISMATCH_K; + } + for (size_t j = 1; j < 5; ++j) { + if (shares[0].m_data[j] != shares[i].m_data[j]) { + m_valid = MISMATCH_ID; + } + } + if (shares[i].m_data.size() != shares[0].m_data.size()) { + m_valid = MISMATCH_LENGTH; + } + + indices.push_back(shares[i].m_data[5]); + for (size_t j = i + 1; j < shares.size(); ++j) { + if (shares[i].m_data[5] == shares[j].m_data[5]) { + m_valid = DUPLICATE_SHARE; + } + } + } + + m_hrp = shares[0].m_hrp; + m_data.reserve(shares[0].m_data.size()); + for (size_t j = 0; j < shares[0].m_data.size(); ++j) { + m_data.push_back(0); + } + + for (size_t i = 0; i < shares.size(); ++i) { + uint8_t lagrange_coeff = lagrange_coefficient(indices, shares[i].m_data[5], oidx); + for (size_t j = 0; j < m_data.size(); ++j) { + m_data[j] ^= gf32_mul(lagrange_coeff, shares[i].m_data[j]); + } + } +} + +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..93d9f813f9 --- /dev/null +++ b/src/codex32.h @@ -0,0 +1,118 @@ +// 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, + TOO_FEW_SHARES, + DUPLICATE_SHARE, + MISMATCH_K, + MISMATCH_ID, + MISMATCH_LENGTH, +}; + +std::string ErrorString(Error e); + +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); + + /** Construct a codex32 result by interpolating a set of input shares to obtain an output share + * + * Requires that all input shares have the same k and seed ID */ + Result(const std::vector& shares, char output_idx); + + /** 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/rpc/client.cpp b/src/rpc/client.cpp index 6cb2d97016..a0bf078f8d 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -257,6 +257,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "importmulti", 1, "options" }, { "importmulti", 1, "rescan" }, { "importdescriptors", 0, "requests" }, + { "importdescriptors", 1, "seeds" }, { "listdescriptors", 0, "private" }, { "verifychain", 0, "checklevel" }, { "verifychain", 1, "nblocks" }, diff --git a/src/script/signingprovider.cpp b/src/script/signingprovider.cpp index baabd4d5b5..d406fb9f1e 100644 --- a/src/script/signingprovider.cpp +++ b/src/script/signingprovider.cpp @@ -77,6 +77,16 @@ bool FlatSigningProvider::GetTaprootBuilder(const XOnlyPubKey& output_key, Tapro return LookupHelper(tr_trees, output_key, builder); } +void FlatSigningProvider::AddMasterKey(const CExtKey& key) +{ + CPubKey pubkey = key.Neuter().pubkey; + const auto id = pubkey.GetID(); + KeyOriginInfo origin; + std::copy(key.vchFingerprint, key.vchFingerprint + sizeof(key.vchFingerprint), origin.fingerprint); + origins[id] = std::make_pair(pubkey, origin); + keys[id] = key.key; +} + FlatSigningProvider& FlatSigningProvider::Merge(FlatSigningProvider&& b) { scripts.merge(b.scripts); diff --git a/src/script/signingprovider.h b/src/script/signingprovider.h index efdfd9ee56..c4ea98edfd 100644 --- a/src/script/signingprovider.h +++ b/src/script/signingprovider.h @@ -219,6 +219,7 @@ struct FlatSigningProvider final : public SigningProvider bool GetTaprootSpendData(const XOnlyPubKey& output_key, TaprootSpendData& spenddata) const override; bool GetTaprootBuilder(const XOnlyPubKey& output_key, TaprootBuilder& builder) const override; + void AddMasterKey(const CExtKey& key); FlatSigningProvider& Merge(FlatSigningProvider&& b) LIFETIMEBOUND; }; diff --git a/src/test/codex32_tests.cpp b/src/test/codex32_tests.cpp new file mode 100644 index 0000000000..17427add89 --- /dev/null +++ b/src/test/codex32_tests.cpp @@ -0,0 +1,377 @@ +// 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())); + + const auto d = codex32::Result{{input_a, input_c}, 'd'}; + BOOST_CHECK(d.IsValid()); + BOOST_CHECK_EQUAL(d.GetHrp(), "ms"); + BOOST_CHECK_EQUAL(d.GetK(), 2); + BOOST_CHECK_EQUAL(d.GetIdString(), "name"); + BOOST_CHECK_EQUAL(d.GetShareIndex(), 'd'); + BOOST_CHECK(CaseInsensitiveEqual("MS12NAMEDLL4F8JLH4E5VDVULDLFXU2JHDNLSM97XVENRXEG", d.Encode())); + + const auto err1 = codex32::Result{{}, 's'}; + BOOST_CHECK_EQUAL(err1.error(), codex32::TOO_FEW_SHARES); + const auto err2 = codex32::Result{{input_c}, 's'}; + BOOST_CHECK_EQUAL(err2.error(), codex32::TOO_FEW_SHARES); + const auto err3 = codex32::Result{{input_a, input_c}, 'b'}; + BOOST_CHECK_EQUAL(err3.error(), codex32::INVALID_SHARE_IDX); + + const auto s = codex32::Result{{input_a, input_c}, 's'}; + BOOST_CHECK(s.IsValid()); + BOOST_CHECK_EQUAL(s.GetHrp(), "ms"); + BOOST_CHECK_EQUAL(s.GetK(), 2); + BOOST_CHECK_EQUAL(s.GetIdString(), "name"); + BOOST_CHECK_EQUAL(s.GetShareIndex(), 's'); + BOOST_CHECK(CaseInsensitiveEqual("MS12NAMES6XQGUZTTXKEQNJSJZV4JV3NZ5K3KWGSPHUH6EVW", s.Encode())); + + const auto seed = s.GetPayload(); + BOOST_CHECK_EQUAL(seed.size(), 16); + BOOST_CHECK_EQUAL(HexStr(seed), "d1808e096b35b209ca12132b264662a5"); +} + +BOOST_AUTO_TEST_CASE(codex32_bip93_vector_3) +{ + const auto s = codex32::Result("ms", 3, "Cash", 's', ParseHex("ffeeddccbbaa99887766554433221100")); + BOOST_CHECK(s.IsValid()); + BOOST_CHECK_EQUAL(s.GetIdString(), "cash"); + BOOST_CHECK_EQUAL(s.GetShareIndex(), 's'); + BOOST_CHECK_EQUAL(s.GetK(), 3); + BOOST_CHECK_EQUAL(s.Encode(), "ms13cashsllhdmn9m42vcsamx24zrxgs3qqjzqud4m0d6nln"); + + const auto a = codex32::Result{"ms13casha320zyxwvutsrqpnmlkjhgfedca2a8d0zehn8a0t"}; + BOOST_CHECK(a.IsValid()); + BOOST_CHECK_EQUAL(a.GetIdString(), "cash"); + BOOST_CHECK_EQUAL(a.GetShareIndex(), 'a'); + BOOST_CHECK_EQUAL(a.GetK(), 3); + + const auto c = codex32::Result{"ms13cashcacdefghjklmnpqrstuvwxyz023949xq35my48dr"}; + BOOST_CHECK(c.IsValid()); + BOOST_CHECK_EQUAL(c.GetIdString(), "cash"); + BOOST_CHECK_EQUAL(c.GetShareIndex(), 'c'); + BOOST_CHECK_EQUAL(c.GetK(), 3); + + const auto err1 = codex32::Result{{}, 'd'}; + BOOST_CHECK_EQUAL(err1.error(), codex32::TOO_FEW_SHARES); + const auto err2 = codex32::Result{{a, c}, 'd'}; + BOOST_CHECK_EQUAL(err2.error(), codex32::TOO_FEW_SHARES); + const auto err3 = codex32::Result{{s, a}, 'd'}; + BOOST_CHECK_EQUAL(err3.error(), codex32::TOO_FEW_SHARES); + const auto err4 = codex32::Result{{s, s, a}, 'd'}; + BOOST_CHECK_EQUAL(err4.error(), codex32::DUPLICATE_SHARE); + + const auto d = codex32::Result{{s, a, c}, 'd'}; + BOOST_CHECK(d.IsValid()); + BOOST_CHECK_EQUAL(d.GetIdString(), "cash"); + BOOST_CHECK_EQUAL(d.GetShareIndex(), 'd'); + BOOST_CHECK_EQUAL(d.GetK(), 3); + BOOST_CHECK_EQUAL(d.Encode(), "ms13cashd0wsedstcdcts64cd7wvy4m90lm28w4ffupqs7rm"); + + const auto e = codex32::Result{{a, c, d}, 'e'}; + BOOST_CHECK(e.IsValid()); + BOOST_CHECK_EQUAL(e.GetIdString(), "cash"); + BOOST_CHECK_EQUAL(e.GetShareIndex(), 'e'); + BOOST_CHECK_EQUAL(e.GetK(), 3); + BOOST_CHECK_EQUAL(e.Encode(), "ms13casheekgpemxzshcrmqhaydlp6yhms3ws7320xyxsar9"); + + const auto f = codex32::Result{{a, s, d}, 'f'}; + BOOST_CHECK(f.IsValid()); + BOOST_CHECK_EQUAL(f.GetIdString(), "cash"); + BOOST_CHECK_EQUAL(f.GetShareIndex(), 'f'); + BOOST_CHECK_EQUAL(f.GetK(), 3); + BOOST_CHECK_EQUAL(f.Encode(), "ms13cashf8jh6sdrkpyrsp5ut94pj8ktehhw2hfvyrj48704"); + + // Mismatched data + const auto g1 = codex32::Result{"ms", 2, "cash", 'g', ParseHex("ffeeddccbbaa99887766554433221100")}; + BOOST_CHECK(g1.IsValid()); + const auto err_s1 = codex32::Result{{a, c, g1}, 's'}; + BOOST_CHECK_EQUAL(err_s1.error(), codex32::MISMATCH_K); + + const auto g2 = codex32::Result{"ms", 3, "leet", 'g', ParseHex("ffeeddccbbaa99887766554433221100")}; + BOOST_CHECK(g2.IsValid()); + const auto err_s2 = codex32::Result{{a, c, g2}, 's'}; + BOOST_CHECK_EQUAL(err_s2.error(), codex32::MISMATCH_ID); + + const auto g3 = codex32::Result{"ms", 3, "cash", 'g', ParseHex("ffeeddccbbaa99887766554433221100ab")}; + BOOST_CHECK(g3.IsValid()); + const auto err_s3 = codex32::Result{{a, c, g3}, 's'}; + BOOST_CHECK_EQUAL(err_s3.error(), codex32::MISMATCH_LENGTH); +} + +BOOST_AUTO_TEST_CASE(codex32_bip93_vector_4) +{ + const std::string seed = "ffeeddccbbaa99887766554433221100ffeeddccbbaa99887766554433221100"; + const auto s = codex32::Result{"ms", 0, "leet", 's', ParseHex(seed)}; + BOOST_CHECK(s.IsValid()); + BOOST_CHECK_EQUAL(s.GetIdString(), "leet"); + BOOST_CHECK_EQUAL(s.GetShareIndex(), 's'); + BOOST_CHECK_EQUAL(s.GetK(), 0); + BOOST_CHECK_EQUAL(HexStr(s.GetPayload()), seed); + BOOST_CHECK_EQUAL(s.Encode(), "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqqtum9pgv99ycma"); + + std::vector alternates = { + "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqqtum9pgv99ycma", + "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqpj82dp34u6lqtd", + "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqzsrs4pnh7jmpj5", + "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqrfcpap2w8dqezy", + "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqy5tdvphn6znrf0", + "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyq9dsuypw2ragmel", + "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqx05xupvgp4v6qx", + "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyq8k0h5p43c2hzsk", + "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqgum7hplmjtr8ks", + "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqf9q0lpxzt5clxq", + "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyq28y48pyqfuu7le", + "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqt7ly0paesr8x0f", + "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqvrvg7pqydv5uyz", + "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqd6hekpea5n0y5j", + "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqwcnrwpmlkmt9dt", + "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyq0pgjxpzx0ysaam", + }; + for (const auto& alt : alternates) { + const auto s_alt = codex32::Result{alt}; + BOOST_CHECK(s_alt.IsValid()); + BOOST_CHECK_EQUAL(HexStr(s_alt.GetPayload()), seed); + } +} + +BOOST_AUTO_TEST_CASE(codex32_bip93_vector_5) +{ + const auto s = codex32::Result{"MS100C8VSM32ZXFGUHPCHTLUPZRY9X8GF2TVDW0S3JN54KHCE6MUA7LQPZYGSFJD6AN074RXVCEMLH8WU3TK925ACDEFGHJKLMNPQRSTUVWXY06FHPV80UNDVARHRAK"}; + BOOST_CHECK(s.IsValid()); + BOOST_CHECK_EQUAL(s.error(), codex32::OK); + BOOST_CHECK_EQUAL(s.GetIdString(), "0c8v"); + BOOST_CHECK_EQUAL(s.GetShareIndex(), 's'); + BOOST_CHECK_EQUAL(s.GetK(), 0); + BOOST_CHECK_EQUAL(HexStr(s.GetPayload()), "dc5423251cb87175ff8110c8531d0952d8d73e1194e95b5f19d6f9df7c01111104c9baecdfea8cccc677fb9ddc8aec5553b86e528bcadfdcc201c17c638c47e9"); +} + +BOOST_AUTO_TEST_CASE(codex32_errors) +{ + const std::vector errs = { + codex32::Result("ms", 3, "cash", 's', ParseHex("ffeeddccbbaa99887766554433221100")), + // bad hrp + codex32::Result("bc", 3, "cash", 's', ParseHex("ffeeddccbbaa99887766554433221100")), + // bad ID len + codex32::Result("ms", 3, "cas", 's', ParseHex("ffeeddccbbaa99887766554433221100")), + codex32::Result("ms", 3, "cashcashcash", 's', ParseHex("ffeeddccbbaa99887766554433221100")), + codex32::Result("ms", 3, "", 's', ParseHex("ffeeddccbbaa99887766554433221100")), + // bad id char + codex32::Result("ms", 3, "bash", 's', ParseHex("ffeeddccbbaa99887766554433221100")), + // bad k + codex32::Result("ms", 1, "cash", 's', ParseHex("ffeeddccbbaa99887766554433221100")), + codex32::Result("ms", 10, "cash", 's', ParseHex("ffeeddccbbaa99887766554433221100")), + codex32::Result("ms", 100000, "cash", 's', ParseHex("ffeeddccbbaa99887766554433221100")), + // bad share idx + codex32::Result("ms", 100000, "cash", 'b', ParseHex("ffeeddccbbaa99887766554433221100")), + codex32::Result("ms", 100000, "cash", 'i', ParseHex("ffeeddccbbaa99887766554433221100")), + codex32::Result("ms", 100000, "cash", '1', ParseHex("ffeeddccbbaa99887766554433221100")), + codex32::Result("ms", 100000, "cash", 'o', ParseHex("ffeeddccbbaa99887766554433221100")), + codex32::Result("ms", 100000, "cash", ' ', ParseHex("ffeeddccbbaa99887766554433221100")), + }; + + BOOST_CHECK_EQUAL(errs[0].error(), codex32::OK); + BOOST_CHECK_EQUAL(errs[1].error(), codex32::INVALID_HRP); + BOOST_CHECK_EQUAL(errs[2].error(), codex32::INVALID_ID_LEN); + BOOST_CHECK_EQUAL(errs[3].error(), codex32::INVALID_ID_LEN); + BOOST_CHECK_EQUAL(errs[4].error(), codex32::INVALID_ID_LEN); + BOOST_CHECK_EQUAL(errs[5].error(), codex32::INVALID_ID_CHAR); + BOOST_CHECK_EQUAL(errs[6].error(), codex32::INVALID_K); + BOOST_CHECK_EQUAL(errs[7].error(), codex32::INVALID_K); + BOOST_CHECK_EQUAL(errs[8].error(), codex32::INVALID_K); + BOOST_CHECK_EQUAL(errs[9].error(), codex32::INVALID_SHARE_IDX); + BOOST_CHECK_EQUAL(errs[10].error(), codex32::INVALID_SHARE_IDX); + BOOST_CHECK_EQUAL(errs[11].error(), codex32::INVALID_SHARE_IDX); + BOOST_CHECK_EQUAL(errs[12].error(), codex32::INVALID_SHARE_IDX); + BOOST_CHECK_EQUAL(errs[13].error(), codex32::INVALID_SHARE_IDX); + +} + +BOOST_AUTO_TEST_CASE(codex32_bip93_invalid_1) +{ + std::vector bad_checksum = { + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxve740yyge2ghq", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxve740yyge2ghp", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxlk3yepcstwr", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxx6pgnv7jnpcsp", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxx0cpvr7n4geq", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxm5252y7d3lr", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxrd9sukzl05ej", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxc55srw5jrm0", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxgc7rwhtudwc", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxx4gy22afwghvs", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxme084q0vpht7pe0", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxme084q0vpht7pew", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxqyadsp3nywm8a", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxzvg7ar4hgaejk", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxcznau0advgxqe", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxch3jrc6j5040j", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx52gxl6ppv40mcv", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx7g4g2nhhle8fk", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx63m45uj8ss4x8", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxy4r708q7kg65x", + }; + for (const auto& bad : bad_checksum) { + auto res = codex32::Result{bad}; + BOOST_CHECK_EQUAL(res.error(), codex32::BAD_CHECKSUM); + } +} + +BOOST_AUTO_TEST_CASE(codex32_bip93_invalid_2) +{ + std::vector bad_checksum = { + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxurfvwmdcmymdufv", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxcsyppjkd8lz4hx3", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxu6hwvl5p0l9xf3c", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxwqey9rfs6smenxa", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxv70wkzrjr4ntqet", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx3hmlrmpa4zl0v", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxrfggf88znkaup", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxpt7l4aycv9qzj", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxus27z9xtyxyw3", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxcwm4re8fs78vn", + }; + for (const auto& bad : bad_checksum) { + auto res = codex32::Result{bad}; + BOOST_CHECK(res.error() == codex32::BAD_CHECKSUM || res.error() == codex32::INVALID_LENGTH); + } +} + +BOOST_AUTO_TEST_CASE(codex32_bip93_invalid_3) +{ + std::vector bad_checksum = { + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxw0a4c70rfefn4", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxk4pavy5n46nea", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxx9lrwar5zwng4w", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxr335l5tv88js3", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxvu7q9nz8p7dj68v", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxpq6k542scdxndq3", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxkmfw6jm270mz6ej", + "ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxzhddxw99w7xws", + "ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxx42cux6um92rz", + "ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxarja5kqukdhy9", + "ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxky0ua3ha84qk8", + "ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx9eheesxadh2n2n9", + "ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx9llwmgesfulcj2z", + "ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx02ev7caq6n9fgkf", + }; + for (const auto& bad : bad_checksum) { + auto res = codex32::Result{bad}; + BOOST_CHECK_EQUAL(res.error(), codex32::INVALID_LENGTH); + } +} + +BOOST_AUTO_TEST_CASE(codex32_bip93_invalid_hrp) +{ + std::vector bad_checksum = { + "0fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "ms0fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "m10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "s10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "0fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxhkd4f70m8lgws", + "10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxhkd4f70m8lgws", + "m10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxx8t28z74x8hs4l", + "s10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxh9d0fhnvfyx3x", + }; + for (const auto& bad : bad_checksum) { + auto res = codex32::Result{bad}; + BOOST_CHECK(res.error() == codex32::INVALID_HRP || res.error() == codex32::BECH32_DECODE); + } +} + +BOOST_AUTO_TEST_CASE(codex32_bip93_invalid_case) +{ + std::vector bad_checksum = { + "Ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "mS10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "MS10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "ms10FAUXsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "ms10fauxSxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "ms10fauxsXXXXXXXXXXXXXXXXXXXXXXXXXXuqxkk05lyf3x2", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxUQXKK05LYF3X2", + }; + for (const auto& bad : bad_checksum) { + auto res = codex32::Result{bad}; + BOOST_CHECK_EQUAL(res.error(), codex32::BECH32_DECODE); + } +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index b1f76b1926..a153cbbcb6 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -216,7 +217,7 @@ RPCHelpMan importprivkey() }; } -UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); +UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, const int64_t timestamp, const std::vector& master_keys = {}) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); RPCHelpMan importaddress() { @@ -1474,7 +1475,7 @@ RPCHelpMan importmulti() }; } -UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) +UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, const int64_t timestamp, const std::vector& master_keys) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) { UniValue warnings(UniValue::VARR); UniValue result(UniValue::VOBJ); @@ -1491,6 +1492,10 @@ UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, const in // Parse descriptor string FlatSigningProvider keys; + for (const auto& mk : master_keys) { + keys.AddMasterKey(mk); + } + std::string error; auto parsed_desc = Parse(descriptor, keys, error, /* require_checksum = */ true); if (!parsed_desc) { @@ -1643,6 +1648,15 @@ RPCHelpMan importdescriptors() }, }, RPCArgOptions{.oneline_description="requests"}}, + {"seeds", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "BIP32 master seeds for the above descriptors", + { + {"shares", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "a codex32 (BIP 93) encoded seed, or list of codex32-encoded shares", + { + {"share 1", RPCArg::Type::STR, RPCArg::Optional::OMITTED, ""}, + }, + }, + }, + RPCArgOptions{.oneline_description="seeds"}}, }, RPCResult{ RPCResult::Type::ARR, "", "Response is an array with the same size as the input that has the execution result", @@ -1695,6 +1709,48 @@ RPCHelpMan importdescriptors() int64_t now = 0; int64_t lowest_timestamp = 0; bool rescan = false; + + // Parse codex32 strings + std::vector master_keys; + if (main_request.params[1].isArray()) { + const auto& req_seeds = main_request.params[1].get_array(); + master_keys.reserve(req_seeds.size()); + for (size_t i = 0; i < req_seeds.size(); ++i) { + const auto& req_shares = req_seeds[i].get_array(); + std::vector shares; + shares.reserve(req_shares.size()); + for (size_t j = 0; j < req_shares.size(); ++j) { + if (!req_shares[j].isStr()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "codex32 shares must be strings"); + } + codex32::Result key_res{req_shares[j].get_str()}; + if (!key_res.IsValid()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid codex32 share: " + codex32::ErrorString(key_res.error())); + } + shares.push_back(key_res); + } + + // Recover seed + std::vector seed; + if (shares.size() == 1) { + if (shares[0].GetShareIndex() != 's') { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid codex32: single share must be the S share"); + } + seed = shares[0].GetPayload(); + } else { + codex32::Result s{shares, 's'}; + if (!s.IsValid()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Failed to derive codex32 seed: " + codex32::ErrorString(s.error())); + } + seed = s.GetPayload(); + } + + CExtKey master_key; + master_key.SetSeed(Span{(std::byte*) seed.data(), seed.size()}); + master_keys.push_back(master_key); + } + } + UniValue response(UniValue::VARR); { LOCK(pwallet->cs_wallet); @@ -1706,7 +1762,7 @@ RPCHelpMan importdescriptors() for (const UniValue& request : requests.getValues()) { // This throws an error if "timestamp" doesn't exist const int64_t timestamp = std::max(GetImportTimestamp(request, now), minimum_timestamp); - const UniValue result = ProcessDescriptorImport(*pwallet, request, timestamp); + const UniValue result = ProcessDescriptorImport(*pwallet, request, timestamp, master_keys); response.push_back(result); if (lowest_timestamp > timestamp ) { diff --git a/src/wallet/wallettool.cpp b/src/wallet/wallettool.cpp index 54d73212d7..10be4f92f2 100644 --- a/src/wallet/wallettool.cpp +++ b/src/wallet/wallettool.cpp @@ -22,7 +22,7 @@ namespace wallet { -UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, const int64_t timestamp) +UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, const int64_t timestamp, const std::vector& master_keys = {}) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); namespace WalletTool { diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index d326492fb7..2756dacaae 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -309,6 +309,7 @@ BASE_SCRIPTS = [ 'mempool_expiry.py', 'wallet_import_with_label.py --legacy-wallet', 'wallet_importdescriptors.py --descriptors', + 'wallet_importseed.py --descriptors', 'wallet_upgradewallet.py --legacy-wallet', 'wallet_crosschain.py', 'mining_basic.py', diff --git a/test/functional/wallet_importseed.py b/test/functional/wallet_importseed.py new file mode 100755 index 0000000000..a964e8dbf6 --- /dev/null +++ b/test/functional/wallet_importseed.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python3 +# Copyright (c) 2013 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test the 'seeds' argument to the importdescriptors RPC + +Test importingi seeds by using the BIP 93 test vectors to verify that imported +seeds are compatible with descriptors containing the corresponding xpubs, that +the wallet is able to recognize and send funds, and that the wallet can derive +addresses, when given only seeds as private data.""" + +import time + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_raises_rpc_error, +) + + +class ImportDescriptorsTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser, legacy=False) + + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.wallet_names = [] + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def run_test(self): + test_start = int(time.time()) + + # Spend/receive tests + self.nodes[0].createwallet(wallet_name='w0', descriptors=True) + self.nodes[0].createwallet(wallet_name='w1', descriptors=True, blank=True) + w0 = self.nodes[0].get_wallet_rpc('w0') + w1 = self.nodes[0].get_wallet_rpc('w1') + + self.generatetoaddress(self.nodes[0], 2, w0.getnewaddress()) + self.generate(self.nodes[0], 100) + + # Test 1: send coins to wallet, check they are not received, then import + # the descriptor and make sure they are recognized. Send them + # back and repeat. Uses single codex32 seed. + # + # xpub converted from BIP 93 test vector 1 xpriv using rust-bitcoin + xpub = "tpubD6NzVbkrYhZ4YAqhvsGTCD5axU32P9MH7ySPr38icriLyJc4KcCvwVzE3rsi" \ + "XaAHBC8QtYWhiBGdc6aZRmroQShGcWygQfErbvLULfJSi8j" + descriptors = [ + f"wsh(pk({xpub}/55/*))", + f"tr({xpub}/1/2/3/4/5/*)", + f"pkh({xpub}/*)", + f"wpkh({xpub}/*)", + f"rawtr({xpub}/1/2/3/*)", + ] + assert_raises_rpc_error(-4, "This wallet has no available keys", w1.getnewaddress) + for descriptor in descriptors: + descriptor_chk = w0.getdescriptorinfo(descriptor)["descriptor"] + addr = w0.deriveaddresses(descriptor_chk, range=[0, 20])[0] + + assert w0.getbalance() > 99 # sloppy balance checks, to account for fees + w0.sendtoaddress(addr, 95) + self.generate(self.nodes[0], 1) + assert w0.getbalance() < 5 + + w1.importdescriptors( + [{"desc": descriptor_chk, "timestamp": test_start, "range": 0, "active": True}], + [["ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw"]], + ) + + assert w1.getbalance() > 94 + w1.sendtoaddress(w0.getnewaddress(), 95, "", "", True) + self.generate(self.nodes[0], 1) + assert w0.getbalance() > 99 + w1.getnewaddress() # no failure now + + # Test 2: deriveaddresses on hardened keys fails before import, succeeds after. + # Uses single codex32 seed in 2 shares. + # + # xpub converted from BIP 93 test vector 2 xpriv using rust-bitcoin + self.nodes[0].createwallet(wallet_name='w2', descriptors=True, blank=True) + w2 = self.nodes[0].get_wallet_rpc('w2') + + xpub = "tpubD6NzVbkrYhZ4Wf289qp46iFM6zACTdXTqqrA3pKUV8bF8SNBcYS8xvVPZg43" \ + "6YhSuCqTKLfnDkmwi9TE6fa5cvxm3NHRCBbgJoC6YgsQBFY" + descriptor = f"tr([fab6868a/1h/2]{xpub}/1h/2/*h)" + descriptor_chk = w2.getdescriptorinfo(descriptor)["descriptor"] + assert_raises_rpc_error( + -4, + "This wallet has no available keys", + w2.getnewaddress, + address_type="bech32m", + ) + + # Try importing descriptor with wrong seed + err = w2.importdescriptors( + [{"desc": descriptor_chk, "timestamp": test_start, "active": True, "range": [0, 20]}], + [["ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw"]], + ) + assert "Cannot expand descriptor." in err[0]["error"]["message"] + assert_raises_rpc_error( + -4, + "This wallet has no available keys", + w2.getnewaddress, + address_type="bech32m", + ) + + # Try various failure cases + assert_raises_rpc_error( + -5, + "single share must be the S share", + w2.importdescriptors, + [{"desc": descriptor_chk, "timestamp": test_start, "active": True, "range": [0, 20]}], + [["MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM"]], + ) + + assert_raises_rpc_error( + -5, + "two input shares had the same index", + w2.importdescriptors, + [{"desc": descriptor_chk, "timestamp": test_start, "active": True, "range": [0, 20]}], + [[ + "MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM", + "MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM", + ]], + ) + + assert_raises_rpc_error( + -5, + "input shares had inconsistent seed IDs", + w2.importdescriptors, + [{"desc": descriptor_chk, "timestamp": test_start, "active": True, "range": [0, 20]}], + [[ + "MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM", + "ms13cashcacdefghjklmnpqrstuvwxyz023949xq35my48dr", + ]], + ) + + # Do it correctly + w2.importdescriptors( + [{"desc": descriptor_chk, "timestamp": test_start, "active": True, "range": [0, 20]}], + [[ + "MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM", + "MS12NAMECACDEFGHJKLMNPQRSTUVWXYZ023FTR2GDZMPY6PN", + ]], + ) + # getnewaddress no longer fails. Annoyingl, deriveaddresses will + w2.getnewaddress(address_type="bech32m") + assert_raises_rpc_error( + -5, + "Cannot derive script without private keys", + w2.deriveaddresses, + descriptor_chk, + 0, + ) + # Do it again, to see if nothing breaks + w2.importdescriptors( + [{"desc": descriptor_chk, "timestamp": test_start, "active": True, "range": [0, 20]}], + [[ + "MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM", + "MS12NAMECACDEFGHJKLMNPQRSTUVWXYZ023FTR2GDZMPY6PN", + ]], + ) + + # Test 3: multiple seeds, multiple descriptors + # + # xpubs converted from BIP 93 test vector 3, 4 and 5 xprivs using rust-bitcoin + + self.nodes[0].createwallet(wallet_name='w3', descriptors=True, blank=True) + w3 = self.nodes[0].get_wallet_rpc('w3') + xpub1 = "tpubD6NzVbkrYhZ4WNNA2qNKYbaxKR3TYtP2n5bNSj6JKzYsVUPxahe2vWJKwiX2" \ + "wfoTJyERQNJ8YnmJvprMHygyaXziTdyFVsSGNmfQtDCCSJ3" # vector 3 + xpub2 = "tpubD6NzVbkrYhZ4Y9KL2R346X9ZwcN16c37vjXuZEhDV2LaMt84zqVbKVbVAw1z" \ + "nMksNtdKnSRZQXyBL9qJaNnq9BkjtRBdsQbxkTbSGZGrcG6" # vector 4 + xpub3 = "tpubD6NzVbkrYhZ4Ykomd4u92cmRCkhZtctLkKU3vCVi7DKBAopRDWVpq6wEGoq7" \ + "xYbCQQjEGM8KkqxvQDoLa3sdfpzTBv1yodq4FKwrCdxweHE" # vector 5 + + descriptor1 = f"rawtr({xpub1}/1/2h/*)" + descriptor1_chk = w3.getdescriptorinfo(descriptor1)["descriptor"] + descriptor2 = f"wpkh({xpub2}/1h/2/*)" + descriptor2_chk = w3.getdescriptorinfo(descriptor2)["descriptor"] + descriptor3 = f"pkh({xpub3}/1h/2/3/4/5/6/7/8/9/10/*)" + descriptor3_chk = w3.getdescriptorinfo(descriptor3)["descriptor"] + + assert_raises_rpc_error( + -4, + "This wallet has no available keys", + w3.getnewaddress, + address_type="bech32m", + ) + assert_raises_rpc_error( + -4, + "This wallet has no available keys", + w3.getnewaddress, + address_type="bech32", + ) + assert_raises_rpc_error( + -4, + "This wallet has no available keys", + w3.getnewaddress, + address_type="legacy", + ) + + # First try without enough input shares. + assert_raises_rpc_error( + -5, + "did not have enough input shares", + w3.importdescriptors, + [ + {"desc": descriptor1_chk, "timestamp": test_start, "active": True, "range": 10}, + {"desc": descriptor2_chk, "timestamp": test_start, "active": True, "range": 15}, + ], + [[ + "ms13casheekgpemxzshcrmqhaydlp6yhms3ws7320xyxsar9", + "ms13cashf8jh6sdrkpyrsp5ut94pj8ktehhw2hfvyrj48704", + ], [ + "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqqtum9pgv99ycma", + ]], + ) + # Wallet still doesn't work, even the descriptor whose seed was correctly specified + assert_raises_rpc_error( + -4, + "This wallet has no available keys", + w3.getnewaddress, + address_type="bech32", + ) + + # Do it properly + w3.importdescriptors( + [ + {"desc": descriptor1_chk, "timestamp": test_start, "active": True, "range": 10}, + {"desc": descriptor2_chk, "timestamp": test_start, "active": True, "range": 15}, + {"desc": descriptor3_chk, "timestamp": test_start, "active": True, "range": 15}, + ], + [[ + "ms13cashd0wsedstcdcts64cd7wvy4m90lm28w4ffupqs7rm", + "ms13casheekgpemxzshcrmqhaydlp6yhms3ws7320xyxsar9", + "ms13cashf8jh6sdrkpyrsp5ut94pj8ktehhw2hfvyrj48704", + ], [ + "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqqtum9pgv99ycma", + ]], + ) + # All good now for the two descriptors that had seeds + w3.getnewaddress(address_type="bech32") + w3.getnewaddress(address_type="bech32m") + # but the one without a seed still doesn't work + assert_raises_rpc_error( + -12, + "No legacy addresses available", + w3.getnewaddress, + address_type="legacy", + ) + + # Ok, try to import the legacy one separately. + w3.importdescriptors( + [{"desc": descriptor3_chk, "timestamp": test_start, "active": True, "range": 15}], + [["MS100C8VSM32ZXFGUHPCHTLUPZRY9X8GF2TVDW0S3JN54KHCE6MUA7LQPZYGSFJD" # concat string + "6AN074RXVCEMLH8WU3TK925ACDEFGHJKLMNPQRSTUVWXY06FHPV80UNDVARHRAK"]], + ) + # And all is well! + w3.getnewaddress(address_type="bech32") + w3.getnewaddress(address_type="bech32m") + w3.getnewaddress(address_type="legacy") + + +if __name__ == '__main__': + ImportDescriptorsTest(__file__).main()