mirror of
https://github.com/Retropex/bitcoin.git
synced 2025-05-13 03:30:42 +02:00
Merge 27351 via codex32-28+knots
This commit is contained in:
commit
64f3666c9e
@ -139,6 +139,7 @@ BITCOIN_CORE_H = \
|
|||||||
checkqueue.h \
|
checkqueue.h \
|
||||||
clientversion.h \
|
clientversion.h \
|
||||||
cluster_linearize.h \
|
cluster_linearize.h \
|
||||||
|
codex32.h \
|
||||||
coins.h \
|
coins.h \
|
||||||
common/args.h \
|
common/args.h \
|
||||||
common/bloom.h \
|
common/bloom.h \
|
||||||
@ -682,6 +683,7 @@ libbitcoin_common_a_SOURCES = \
|
|||||||
bech32.cpp \
|
bech32.cpp \
|
||||||
chainparamsbase.cpp \
|
chainparamsbase.cpp \
|
||||||
chainparams.cpp \
|
chainparams.cpp \
|
||||||
|
codex32.cpp \
|
||||||
coins.cpp \
|
coins.cpp \
|
||||||
common/args.cpp \
|
common/args.cpp \
|
||||||
common/bloom.cpp \
|
common/bloom.cpp \
|
||||||
|
@ -84,6 +84,7 @@ BITCOIN_TESTS =\
|
|||||||
test/bswap_tests.cpp \
|
test/bswap_tests.cpp \
|
||||||
test/checkqueue_tests.cpp \
|
test/checkqueue_tests.cpp \
|
||||||
test/cluster_linearize_tests.cpp \
|
test/cluster_linearize_tests.cpp \
|
||||||
|
test/codex32_tests.cpp \
|
||||||
test/coins_tests.cpp \
|
test/coins_tests.cpp \
|
||||||
test/coinscachepair_tests.cpp \
|
test/coinscachepair_tests.cpp \
|
||||||
test/coinstatsindex_tests.cpp \
|
test/coinstatsindex_tests.cpp \
|
||||||
|
129
src/bech32.cpp
129
src/bech32.cpp
@ -17,22 +17,7 @@ namespace bech32
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
typedef std::vector<uint8_t> data;
|
typedef internal::data 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
|
|
||||||
};
|
|
||||||
|
|
||||||
/** We work with the finite field GF(1024) defined as a degree 2 extension of the base field GF(32)
|
/** 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.
|
* 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();
|
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)
|
std::vector<unsigned char> PreparePolynomialCoefficients(const std::string& hrp, const data& values)
|
||||||
{
|
{
|
||||||
data ret;
|
data ret;
|
||||||
@ -323,39 +357,9 @@ std::vector<unsigned char> PreparePolynomialCoefficients(const std::string& hrp,
|
|||||||
return ret;
|
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. */
|
/** Encode a hrpstring without concerning ourselves with checksum validity */
|
||||||
data CreateChecksum(Encoding encoding, const std::string& hrp, const data& values)
|
std::string Encode(const std::string& hrp, const data& values, const data& checksum) {
|
||||||
{
|
|
||||||
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) {
|
|
||||||
// First ensure that the HRP is all lowercase. BIP-173 and BIP350 require an encoder
|
// 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
|
// to return a lowercase Bech32/Bech32m string, but if given an uppercase HRP, the
|
||||||
// result will always be invalid.
|
// result will always be invalid.
|
||||||
@ -366,17 +370,17 @@ std::string Encode(Encoding encoding, const std::string& hrp, const data& values
|
|||||||
ret += hrp;
|
ret += hrp;
|
||||||
ret += '1';
|
ret += '1';
|
||||||
for (const uint8_t& i : values) ret += CHARSET[i];
|
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;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Decode a Bech32 or Bech32m string. */
|
/** Decode a hrpstring without concerning ourselves with checksum validity */
|
||||||
DecodeResult Decode(const std::string& str, CharLimit limit) {
|
std::pair<std::string, data> Decode(const std::string& str, CharLimit limit, size_t checksum_length) {
|
||||||
std::vector<int> errors;
|
std::vector<int> errors;
|
||||||
if (!CheckCharacters(str, errors)) return {};
|
if (!CheckCharacters(str, errors)) return {};
|
||||||
size_t pos = str.rfind('1');
|
size_t pos = str.rfind('1');
|
||||||
if (str.size() > limit) return {};
|
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 {};
|
return {};
|
||||||
}
|
}
|
||||||
data values(str.size() - 1 - pos);
|
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) {
|
for (size_t i = 0; i < pos; ++i) {
|
||||||
hrp += LowerCase(str[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 {};
|
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. */
|
/** 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);
|
data values(length);
|
||||||
for (size_t i = pos + 1; i < str.size(); ++i) {
|
for (size_t i = pos + 1; i < str.size(); ++i) {
|
||||||
unsigned char c = str[i];
|
unsigned char c = str[i];
|
||||||
int8_t rev = CHARSET_REV[c];
|
int8_t rev = internal::CHARSET_REV[c];
|
||||||
if (rev == -1) {
|
if (rev == -1) {
|
||||||
error_locations.push_back(i);
|
error_locations.push_back(i);
|
||||||
return std::make_pair("Invalid Base 32 character", std::move(error_locations));
|
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;
|
std::vector<int> possible_errors;
|
||||||
// Recall that (expanded hrp + values) is interpreted as a list of coefficients of a polynomial
|
// 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).
|
// 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);
|
uint32_t residue = PolyMod(enc) ^ EncodingConstant(encoding);
|
||||||
|
|
||||||
// All valid codewords should be multiples of G(x), so this remainder (after XORing with the encoding
|
// All valid codewords should be multiples of G(x), so this remainder (after XORing with the encoding
|
||||||
|
19
src/bech32.h
19
src/bech32.h
@ -37,6 +37,7 @@ enum class Encoding {
|
|||||||
* and we would never encode an address with such a massive value */
|
* and we would never encode an address with such a massive value */
|
||||||
enum CharLimit : size_t {
|
enum CharLimit : size_t {
|
||||||
BECH32 = 90, //!< BIP173/350 imposed character limit for Bech32(m) encoded addresses. This guarantees finding up to 4 errors.
|
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
|
/** 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. */
|
/** 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);
|
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
|
} // namespace bech32
|
||||||
|
|
||||||
#endif // BITCOIN_BECH32_H
|
#endif // BITCOIN_BECH32_H
|
||||||
|
424
src/codex32.cpp
Normal file
424
src/codex32.cpp
Normal 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
118
src/codex32.h
Normal 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
|
@ -257,6 +257,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||||||
{ "importmulti", 1, "options" },
|
{ "importmulti", 1, "options" },
|
||||||
{ "importmulti", 1, "rescan" },
|
{ "importmulti", 1, "rescan" },
|
||||||
{ "importdescriptors", 0, "requests" },
|
{ "importdescriptors", 0, "requests" },
|
||||||
|
{ "importdescriptors", 1, "seeds" },
|
||||||
{ "listdescriptors", 0, "private" },
|
{ "listdescriptors", 0, "private" },
|
||||||
{ "verifychain", 0, "checklevel" },
|
{ "verifychain", 0, "checklevel" },
|
||||||
{ "verifychain", 1, "nblocks" },
|
{ "verifychain", 1, "nblocks" },
|
||||||
|
@ -77,6 +77,16 @@ bool FlatSigningProvider::GetTaprootBuilder(const XOnlyPubKey& output_key, Tapro
|
|||||||
return LookupHelper(tr_trees, output_key, builder);
|
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)
|
FlatSigningProvider& FlatSigningProvider::Merge(FlatSigningProvider&& b)
|
||||||
{
|
{
|
||||||
scripts.merge(b.scripts);
|
scripts.merge(b.scripts);
|
||||||
|
@ -219,6 +219,7 @@ struct FlatSigningProvider final : public SigningProvider
|
|||||||
bool GetTaprootSpendData(const XOnlyPubKey& output_key, TaprootSpendData& spenddata) const override;
|
bool GetTaprootSpendData(const XOnlyPubKey& output_key, TaprootSpendData& spenddata) const override;
|
||||||
bool GetTaprootBuilder(const XOnlyPubKey& output_key, TaprootBuilder& builder) const override;
|
bool GetTaprootBuilder(const XOnlyPubKey& output_key, TaprootBuilder& builder) const override;
|
||||||
|
|
||||||
|
void AddMasterKey(const CExtKey& key);
|
||||||
FlatSigningProvider& Merge(FlatSigningProvider&& b) LIFETIMEBOUND;
|
FlatSigningProvider& Merge(FlatSigningProvider&& b) LIFETIMEBOUND;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
377
src/test/codex32_tests.cpp
Normal file
377
src/test/codex32_tests.cpp
Normal 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()
|
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include <chain.h>
|
#include <chain.h>
|
||||||
#include <clientversion.h>
|
#include <clientversion.h>
|
||||||
|
#include <codex32.h>
|
||||||
#include <core_io.h>
|
#include <core_io.h>
|
||||||
#include <hash.h>
|
#include <hash.h>
|
||||||
#include <interfaces/chain.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()
|
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 warnings(UniValue::VARR);
|
||||||
UniValue result(UniValue::VOBJ);
|
UniValue result(UniValue::VOBJ);
|
||||||
@ -1491,6 +1492,10 @@ UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, const in
|
|||||||
|
|
||||||
// Parse descriptor string
|
// Parse descriptor string
|
||||||
FlatSigningProvider keys;
|
FlatSigningProvider keys;
|
||||||
|
for (const auto& mk : master_keys) {
|
||||||
|
keys.AddMasterKey(mk);
|
||||||
|
}
|
||||||
|
|
||||||
std::string error;
|
std::string error;
|
||||||
auto parsed_desc = Parse(descriptor, keys, error, /* require_checksum = */ true);
|
auto parsed_desc = Parse(descriptor, keys, error, /* require_checksum = */ true);
|
||||||
if (!parsed_desc) {
|
if (!parsed_desc) {
|
||||||
@ -1643,6 +1648,15 @@ RPCHelpMan importdescriptors()
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
RPCArgOptions{.oneline_description="requests"}},
|
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{
|
||||||
RPCResult::Type::ARR, "", "Response is an array with the same size as the input that has the execution result",
|
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 now = 0;
|
||||||
int64_t lowest_timestamp = 0;
|
int64_t lowest_timestamp = 0;
|
||||||
bool rescan = false;
|
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);
|
UniValue response(UniValue::VARR);
|
||||||
{
|
{
|
||||||
LOCK(pwallet->cs_wallet);
|
LOCK(pwallet->cs_wallet);
|
||||||
@ -1706,7 +1762,7 @@ RPCHelpMan importdescriptors()
|
|||||||
for (const UniValue& request : requests.getValues()) {
|
for (const UniValue& request : requests.getValues()) {
|
||||||
// This throws an error if "timestamp" doesn't exist
|
// This throws an error if "timestamp" doesn't exist
|
||||||
const int64_t timestamp = std::max(GetImportTimestamp(request, now), minimum_timestamp);
|
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);
|
response.push_back(result);
|
||||||
|
|
||||||
if (lowest_timestamp > timestamp ) {
|
if (lowest_timestamp > timestamp ) {
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
namespace wallet {
|
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);
|
EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
|
||||||
|
|
||||||
namespace WalletTool {
|
namespace WalletTool {
|
||||||
|
@ -309,6 +309,7 @@ BASE_SCRIPTS = [
|
|||||||
'mempool_expiry.py',
|
'mempool_expiry.py',
|
||||||
'wallet_import_with_label.py --legacy-wallet',
|
'wallet_import_with_label.py --legacy-wallet',
|
||||||
'wallet_importdescriptors.py --descriptors',
|
'wallet_importdescriptors.py --descriptors',
|
||||||
|
'wallet_importseed.py --descriptors',
|
||||||
'wallet_upgradewallet.py --legacy-wallet',
|
'wallet_upgradewallet.py --legacy-wallet',
|
||||||
'wallet_crosschain.py',
|
'wallet_crosschain.py',
|
||||||
'mining_basic.py',
|
'mining_basic.py',
|
||||||
|
269
test/functional/wallet_importseed.py
Executable file
269
test/functional/wallet_importseed.py
Executable 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()
|
Loading…
Reference in New Issue
Block a user