Implement Bech32m encoding/decoding

This commit is contained in:
Pieter Wuille 2021-01-05 12:55:15 -08:00
parent 4ba1bab443
commit da2bb6976d
6 changed files with 82 additions and 58 deletions

View File

@ -1,4 +1,4 @@
// Copyright (c) 2017 Pieter Wuille // Copyright (c) 2017, 2021 Pieter Wuille
// Distributed under the MIT software license, see the accompanying // Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
@ -7,6 +7,9 @@
#include <assert.h> #include <assert.h>
namespace bech32
{
namespace namespace
{ {
@ -27,6 +30,12 @@ const int8_t CHARSET_REV[128] = {
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1
}; };
/* Determine the final constant to use for the specified encoding. */
uint32_t EncodingConstant(Encoding encoding) {
assert(encoding == Encoding::BECH32 || encoding == Encoding::BECH32M);
return encoding == Encoding::BECH32 ? 1 : 0x2bc830a3;
}
/** This function will compute what 6 5-bit values to XOR into the last 6 input values, in order to /** This function will compute what 6 5-bit values to XOR into the last 6 input values, in order to
* make the checksum 0. These 6 values are packed together in a single 30-bit integer. The higher * make the checksum 0. These 6 values are packed together in a single 30-bit integer. The higher
* bits correspond to earlier values. */ * bits correspond to earlier values. */
@ -111,21 +120,24 @@ data ExpandHRP(const std::string& hrp)
} }
/** Verify a checksum. */ /** Verify a checksum. */
bool VerifyChecksum(const std::string& hrp, const data& values) 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, // 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 // 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 // list of values would result in a new valid list. For that reason, Bech32 requires the
// resulting checksum to be 1 instead. // resulting checksum to be 1 instead. In Bech32m, this constant was amended.
return PolyMod(Cat(ExpandHRP(hrp), values)) == 1; const uint32_t check = PolyMod(Cat(ExpandHRP(hrp), values));
if (check == EncodingConstant(Encoding::BECH32)) return Encoding::BECH32;
if (check == EncodingConstant(Encoding::BECH32M)) return Encoding::BECH32M;
return Encoding::INVALID;
} }
/** Create a checksum. */ /** Create a checksum. */
data CreateChecksum(const std::string& hrp, const data& values) data CreateChecksum(Encoding encoding, const std::string& hrp, const data& values)
{ {
data enc = Cat(ExpandHRP(hrp), values); data enc = Cat(ExpandHRP(hrp), values);
enc.resize(enc.size() + 6); // Append 6 zeroes enc.resize(enc.size() + 6); // Append 6 zeroes
uint32_t mod = PolyMod(enc) ^ 1; // Determine what to XOR into those 6 zeroes. uint32_t mod = PolyMod(enc) ^ EncodingConstant(encoding); // Determine what to XOR into those 6 zeroes.
data ret(6); data ret(6);
for (size_t i = 0; i < 6; ++i) { for (size_t i = 0; i < 6; ++i) {
// Convert the 5-bit groups in mod to checksum values. // Convert the 5-bit groups in mod to checksum values.
@ -136,16 +148,13 @@ data CreateChecksum(const std::string& hrp, const data& values)
} // namespace } // namespace
namespace bech32 /** Encode a Bech32 or Bech32m string. */
{ std::string Encode(Encoding encoding, const std::string& hrp, const data& values) {
/** Encode a Bech32 string. */
std::string Encode(const std::string& hrp, const data& values) {
// First ensure that the HRP is all lowercase. BIP-173 requires an encoder // First ensure that the HRP is all lowercase. BIP-173 requires an encoder
// to return a lowercase Bech32 string, but if given an uppercase HRP, the // to return a lowercase Bech32 string, but if given an uppercase HRP, the
// result will always be invalid. // result will always be invalid.
for (const char& c : hrp) assert(c < 'A' || c > 'Z'); for (const char& c : hrp) assert(c < 'A' || c > 'Z');
data checksum = CreateChecksum(hrp, values); data checksum = CreateChecksum(encoding, hrp, values);
data combined = Cat(values, checksum); data combined = Cat(values, checksum);
std::string ret = hrp + '1'; std::string ret = hrp + '1';
ret.reserve(ret.size() + combined.size()); ret.reserve(ret.size() + combined.size());
@ -155,8 +164,8 @@ std::string Encode(const std::string& hrp, const data& values) {
return ret; return ret;
} }
/** Decode a Bech32 string. */ /** Decode a Bech32 or Bech32m string. */
std::pair<std::string, data> Decode(const std::string& str) { DecodeResult Decode(const std::string& str) {
bool lower = false, upper = false; bool lower = false, upper = false;
for (size_t i = 0; i < str.size(); ++i) { for (size_t i = 0; i < str.size(); ++i) {
unsigned char c = str[i]; unsigned char c = str[i];
@ -183,10 +192,9 @@ std::pair<std::string, data> Decode(const std::string& str) {
for (size_t i = 0; i < pos; ++i) { for (size_t i = 0; i < pos; ++i) {
hrp += LowerCase(str[i]); hrp += LowerCase(str[i]);
} }
if (!VerifyChecksum(hrp, values)) { Encoding result = VerifyChecksum(hrp, values);
return {}; if (result == Encoding::INVALID) return {};
} return {result, std::move(hrp), data(values.begin(), values.end() - 6)};
return {hrp, data(values.begin(), values.end() - 6)};
} }
} // namespace bech32 } // namespace bech32

View File

@ -1,4 +1,4 @@
// Copyright (c) 2017 Pieter Wuille // Copyright (c) 2017, 2021 Pieter Wuille
// Distributed under the MIT software license, see the accompanying // Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
@ -7,7 +7,7 @@
// separator character (1), and a base32 data section, the last // separator character (1), and a base32 data section, the last
// 6 characters of which are a checksum. // 6 characters of which are a checksum.
// //
// For more information, see BIP 173. // For more information, see BIP 173 and BIP 350.
#ifndef BITCOIN_BECH32_H #ifndef BITCOIN_BECH32_H
#define BITCOIN_BECH32_H #define BITCOIN_BECH32_H
@ -19,11 +19,29 @@
namespace bech32 namespace bech32
{ {
/** Encode a Bech32 string. If hrp contains uppercase characters, this will cause an assertion error. */ enum class Encoding {
std::string Encode(const std::string& hrp, const std::vector<uint8_t>& values); INVALID, //!< Failed decoding
/** Decode a Bech32 string. Returns (hrp, data). Empty hrp means failure. */ BECH32, //!< Bech32 encoding as defined in BIP173
std::pair<std::string, std::vector<uint8_t>> Decode(const std::string& str); BECH32M, //!< Bech32m encoding as defined in BIP350
};
/** Encode a Bech32 or Bech32m string. If hrp contains uppercase characters, this will cause an
* assertion error. Encoding must be one of BECH32 or BECH32M. */
std::string Encode(Encoding encoding, const std::string& hrp, const std::vector<uint8_t>& values);
struct DecodeResult
{
Encoding encoding; //!< What encoding was detected in the result; Encoding::INVALID if failed.
std::string hrp; //!< The human readable part
std::vector<uint8_t> data; //!< The payload (excluding checksum)
DecodeResult() : encoding(Encoding::INVALID) {}
DecodeResult(Encoding enc, std::string&& h, std::vector<uint8_t>&& d) : encoding(enc), hrp(std::move(h)), data(std::move(d)) {}
};
/** Decode a Bech32 string. */
DecodeResult Decode(const std::string& str);
} // namespace bech32 } // namespace bech32

View File

@ -19,7 +19,7 @@ static void Bech32Encode(benchmark::Bench& bench)
tmp.reserve(1 + 32 * 8 / 5); tmp.reserve(1 + 32 * 8 / 5);
ConvertBits<8, 5, true>([&](unsigned char c) { tmp.push_back(c); }, v.begin(), v.end()); ConvertBits<8, 5, true>([&](unsigned char c) { tmp.push_back(c); }, v.begin(), v.end());
bench.batch(v.size()).unit("byte").run([&] { bench.batch(v.size()).unit("byte").run([&] {
bech32::Encode("bc", tmp); bech32::Encode(bech32::Encoding::BECH32, "bc", tmp);
}); });
} }

View File

@ -43,7 +43,7 @@ public:
std::vector<unsigned char> data = {0}; std::vector<unsigned char> data = {0};
data.reserve(33); data.reserve(33);
ConvertBits<8, 5, true>([&](unsigned char c) { data.push_back(c); }, id.begin(), id.end()); ConvertBits<8, 5, true>([&](unsigned char c) { data.push_back(c); }, id.begin(), id.end());
return bech32::Encode(m_params.Bech32HRP(), data); return bech32::Encode(bech32::Encoding::BECH32, m_params.Bech32HRP(), data);
} }
std::string operator()(const WitnessV0ScriptHash& id) const std::string operator()(const WitnessV0ScriptHash& id) const
@ -51,7 +51,7 @@ public:
std::vector<unsigned char> data = {0}; std::vector<unsigned char> data = {0};
data.reserve(53); data.reserve(53);
ConvertBits<8, 5, true>([&](unsigned char c) { data.push_back(c); }, id.begin(), id.end()); ConvertBits<8, 5, true>([&](unsigned char c) { data.push_back(c); }, id.begin(), id.end());
return bech32::Encode(m_params.Bech32HRP(), data); return bech32::Encode(bech32::Encoding::BECH32, m_params.Bech32HRP(), data);
} }
std::string operator()(const WitnessUnknown& id) const std::string operator()(const WitnessUnknown& id) const
@ -62,7 +62,7 @@ public:
std::vector<unsigned char> data = {(unsigned char)id.version}; std::vector<unsigned char> data = {(unsigned char)id.version};
data.reserve(1 + (id.length * 8 + 4) / 5); data.reserve(1 + (id.length * 8 + 4) / 5);
ConvertBits<8, 5, true>([&](unsigned char c) { data.push_back(c); }, id.program, id.program + id.length); ConvertBits<8, 5, true>([&](unsigned char c) { data.push_back(c); }, id.program, id.program + id.length);
return bech32::Encode(m_params.Bech32HRP(), data); return bech32::Encode(bech32::Encoding::BECH32, m_params.Bech32HRP(), data);
} }
std::string operator()(const CNoDestination& no) const { return {}; } std::string operator()(const CNoDestination& no) const { return {}; }
@ -95,20 +95,18 @@ CTxDestination DecodeDestination(const std::string& str, const CChainParams& par
error_str = "Invalid prefix for Base58-encoded address"; error_str = "Invalid prefix for Base58-encoded address";
} }
data.clear(); data.clear();
auto bech = bech32::Decode(str); const auto dec = bech32::Decode(str);
if (bech.second.size() > 0) { if (dec.encoding == bech32::Encoding::BECH32 && dec.data.size() > 0) {
// Bech32 decoding
error_str = ""; error_str = "";
if (dec.hrp != params.Bech32HRP()) {
if (bech.first != params.Bech32HRP()) {
error_str = "Invalid prefix for Bech32 address"; error_str = "Invalid prefix for Bech32 address";
return CNoDestination(); return CNoDestination();
} }
int version = dec.data[0]; // The first 5 bit symbol is the witness version (0-16)
// Bech32 decoding
int version = bech.second[0]; // The first 5 bit symbol is the witness version (0-16)
// The rest of the symbols are converted witness program bytes. // The rest of the symbols are converted witness program bytes.
data.reserve(((bech.second.size() - 1) * 5) / 8); data.reserve(((dec.data.size() - 1) * 5) / 8);
if (ConvertBits<5, 8, false>([&](unsigned char c) { data.push_back(c); }, bech.second.begin() + 1, bech.second.end())) { if (ConvertBits<5, 8, false>([&](unsigned char c) { data.push_back(c); }, dec.data.begin() + 1, dec.data.end())) {
if (version == 0) { if (version == 0) {
{ {
WitnessV0KeyHash keyid; WitnessV0KeyHash keyid;

View File

@ -22,9 +22,9 @@ BOOST_AUTO_TEST_CASE(bip173_testvectors_valid)
"?1ezyfcl", "?1ezyfcl",
}; };
for (const std::string& str : CASES) { for (const std::string& str : CASES) {
auto ret = bech32::Decode(str); const auto dec = bech32::Decode(str);
BOOST_CHECK(!ret.first.empty()); BOOST_CHECK(dec.encoding == bech32::Encoding::BECH32);
std::string recode = bech32::Encode(ret.first, ret.second); std::string recode = bech32::Encode(bech32::Encoding::BECH32, dec.hrp, dec.data);
BOOST_CHECK(!recode.empty()); BOOST_CHECK(!recode.empty());
BOOST_CHECK(CaseInsensitiveEqual(str, recode)); BOOST_CHECK(CaseInsensitiveEqual(str, recode));
} }
@ -49,8 +49,8 @@ BOOST_AUTO_TEST_CASE(bip173_testvectors_invalid)
"A12uEL5L", "A12uEL5L",
}; };
for (const std::string& str : CASES) { for (const std::string& str : CASES) {
auto ret = bech32::Decode(str); const auto dec = bech32::Decode(str);
BOOST_CHECK(ret.first.empty()); BOOST_CHECK(dec.encoding != bech32::Encoding::BECH32);
} }
} }

View File

@ -16,28 +16,28 @@
FUZZ_TARGET(bech32) FUZZ_TARGET(bech32)
{ {
const std::string random_string(buffer.begin(), buffer.end()); const std::string random_string(buffer.begin(), buffer.end());
const std::pair<std::string, std::vector<uint8_t>> r1 = bech32::Decode(random_string); const auto r1 = bech32::Decode(random_string);
if (r1.first.empty()) { if (r1.hrp.empty()) {
assert(r1.second.empty()); assert(r1.encoding == bech32::Encoding::INVALID);
assert(r1.data.empty());
} else { } else {
const std::string& hrp = r1.first; assert(r1.encoding != bech32::Encoding::INVALID);
const std::vector<uint8_t>& data = r1.second; const std::string reencoded = bech32::Encode(r1.encoding, r1.hrp, r1.data);
const std::string reencoded = bech32::Encode(hrp, data);
assert(CaseInsensitiveEqual(random_string, reencoded)); assert(CaseInsensitiveEqual(random_string, reencoded));
} }
std::vector<unsigned char> input; std::vector<unsigned char> input;
ConvertBits<8, 5, true>([&](unsigned char c) { input.push_back(c); }, buffer.begin(), buffer.end()); ConvertBits<8, 5, true>([&](unsigned char c) { input.push_back(c); }, buffer.begin(), buffer.end());
const std::string encoded = bech32::Encode("bc", input);
assert(!encoded.empty());
const std::pair<std::string, std::vector<uint8_t>> r2 = bech32::Decode(encoded); if (input.size() + 3 + 6 <= 90) {
if (r2.first.empty()) { // If it's possible to encode input in Bech32(m) without exceeding the 90-character limit:
assert(r2.second.empty()); for (auto encoding : {bech32::Encoding::BECH32, bech32::Encoding::BECH32M}) {
} else { const std::string encoded = bech32::Encode(encoding, "bc", input);
const std::string& hrp = r2.first; assert(!encoded.empty());
const std::vector<uint8_t>& data = r2.second; const auto r2 = bech32::Decode(encoded);
assert(hrp == "bc"); assert(r2.encoding == encoding);
assert(data == input); assert(r2.hrp == "bc");
assert(r2.data == input);
}
} }
} }