Merge 27351 via codex32-28+knots

This commit is contained in:
Luke Dashjr 2025-03-05 03:27:08 +00:00
commit 64f3666c9e
14 changed files with 1356 additions and 60 deletions

View File

@ -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 \

View File

@ -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 \

View File

@ -17,22 +17,7 @@ namespace bech32
namespace
{
typedef std::vector<uint8_t> 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<int>& 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<unsigned char> PreparePolynomialCoefficients(const std::string& hrp, const data& values)
{
data ret;
@ -323,39 +357,9 @@ std::vector<unsigned char> 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<std::string, data> Decode(const std::string& str, CharLimit limit, size_t checksum_length) {
std::vector<int> 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<std::string, std::vector<int>> 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<std::string, std::vector<int>> LocateErrors(const std::string& str, Ch
std::vector<int> 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

View File

@ -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<std::string, std::vector<int>> 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<uint8_t> data;
extern const char* CHARSET;
extern const int8_t CHARSET_REV[128];
std::vector<unsigned char> 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<std::string, data> Decode(const std::string& str, CharLimit limit, size_t checksum_length);
} // namespace internal
} // namespace bech32
#endif // BITCOIN_BECH32_H

424
src/codex32.cpp Normal file
View File

@ -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 <bech32.h>
#include <codex32.h>
#include <util/vector.h>
#include <array>
#include <assert.h>
#include <numeric>
#include <optional>
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<int8_t, 31>, std::array<int8_t, 32>> GenerateGF32Tables() {
// We use these tables to perform arithmetic in GF(32) below, when constructing the
// tables for GF(1024).
std::array<int8_t, 31> GF32_EXP{};
std::array<int8_t, 32> 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<int8_t, 31>& GF32_EXP = tables32.first;
constexpr const std::array<int8_t, 32>& 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<uint8_t, 13> CODEX32_M = {
16, 25, 24, 3, 25, 11, 16, 23, 29, 3, 25, 17, 10
};
// The bech32 string "secretshare32ex"
constexpr const std::array<uint8_t, 15> 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<uint8_t, 13> 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<uint8_t, 15> 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 <checksum length>
* 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 <typename Residue>
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::<anonymous>::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::<anonymous>::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 <typename Residue>
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 <typename Residue>
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<uint8_t>& 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<unsigned char>& 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<Result>& 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<uint8_t> 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

118
src/codex32.h Normal file
View File

@ -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 <assert.h>
#include <stdint.h>
#include <array>
#include <string>
#include <vector>
#include <bech32.h>
#include <util/strencodings.h>
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<unsigned char>& 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<Result>& 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<unsigned char> GetPayload() const {
assert(IsValid());
std::vector<unsigned char> 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<uint8_t> m_data; //!< The payload (remaining data, excluding checksum)
};
} // namespace codex32
#endif // BITCOIN_CODEX32_H

View File

@ -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" },

View File

@ -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);

View File

@ -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;
};

377
src/test/codex32_tests.cpp Normal file
View File

@ -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 <bech32.h>
#include <codex32.h>
#include <test/util/str.h>
#include <boost/test/unit_test.hpp>
#include <array>
#include <string>
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<std::string> 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<codex32::Result> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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()

View File

@ -6,6 +6,7 @@
#include <chain.h>
#include <clientversion.h>
#include <codex32.h>
#include <core_io.h>
#include <hash.h>
#include <interfaces/chain.h>
@ -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<CExtKey>& 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<CExtKey>& 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<CExtKey> 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<codex32::Result> 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<unsigned char> 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 ) {

View File

@ -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<CExtKey>& master_keys = {})
EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
namespace WalletTool {

View File

@ -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',

View File

@ -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()