Skip to content

Commit da2bb69

Browse files
committed
Implement Bech32m encoding/decoding
1 parent 4ba1bab commit da2bb69

File tree

6 files changed

+82
-58
lines changed

6 files changed

+82
-58
lines changed

src/bech32.cpp

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2017 Pieter Wuille
1+
// Copyright (c) 2017, 2021 Pieter Wuille
22
// Distributed under the MIT software license, see the accompanying
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

@@ -7,6 +7,9 @@
77

88
#include <assert.h>
99

10+
namespace bech32
11+
{
12+
1013
namespace
1114
{
1215

@@ -27,6 +30,12 @@ const int8_t CHARSET_REV[128] = {
2730
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1
2831
};
2932

33+
/* Determine the final constant to use for the specified encoding. */
34+
uint32_t EncodingConstant(Encoding encoding) {
35+
assert(encoding == Encoding::BECH32 || encoding == Encoding::BECH32M);
36+
return encoding == Encoding::BECH32 ? 1 : 0x2bc830a3;
37+
}
38+
3039
/** This function will compute what 6 5-bit values to XOR into the last 6 input values, in order to
3140
* make the checksum 0. These 6 values are packed together in a single 30-bit integer. The higher
3241
* bits correspond to earlier values. */
@@ -111,21 +120,24 @@ data ExpandHRP(const std::string& hrp)
111120
}
112121

113122
/** Verify a checksum. */
114-
bool VerifyChecksum(const std::string& hrp, const data& values)
123+
Encoding VerifyChecksum(const std::string& hrp, const data& values)
115124
{
116125
// PolyMod computes what value to xor into the final values to make the checksum 0. However,
117126
// if we required that the checksum was 0, it would be the case that appending a 0 to a valid
118127
// list of values would result in a new valid list. For that reason, Bech32 requires the
119-
// resulting checksum to be 1 instead.
120-
return PolyMod(Cat(ExpandHRP(hrp), values)) == 1;
128+
// resulting checksum to be 1 instead. In Bech32m, this constant was amended.
129+
const uint32_t check = PolyMod(Cat(ExpandHRP(hrp), values));
130+
if (check == EncodingConstant(Encoding::BECH32)) return Encoding::BECH32;
131+
if (check == EncodingConstant(Encoding::BECH32M)) return Encoding::BECH32M;
132+
return Encoding::INVALID;
121133
}
122134

123135
/** Create a checksum. */
124-
data CreateChecksum(const std::string& hrp, const data& values)
136+
data CreateChecksum(Encoding encoding, const std::string& hrp, const data& values)
125137
{
126138
data enc = Cat(ExpandHRP(hrp), values);
127139
enc.resize(enc.size() + 6); // Append 6 zeroes
128-
uint32_t mod = PolyMod(enc) ^ 1; // Determine what to XOR into those 6 zeroes.
140+
uint32_t mod = PolyMod(enc) ^ EncodingConstant(encoding); // Determine what to XOR into those 6 zeroes.
129141
data ret(6);
130142
for (size_t i = 0; i < 6; ++i) {
131143
// Convert the 5-bit groups in mod to checksum values.
@@ -136,16 +148,13 @@ data CreateChecksum(const std::string& hrp, const data& values)
136148

137149
} // namespace
138150

139-
namespace bech32
140-
{
141-
142-
/** Encode a Bech32 string. */
143-
std::string Encode(const std::string& hrp, const data& values) {
151+
/** Encode a Bech32 or Bech32m string. */
152+
std::string Encode(Encoding encoding, const std::string& hrp, const data& values) {
144153
// First ensure that the HRP is all lowercase. BIP-173 requires an encoder
145154
// to return a lowercase Bech32 string, but if given an uppercase HRP, the
146155
// result will always be invalid.
147156
for (const char& c : hrp) assert(c < 'A' || c > 'Z');
148-
data checksum = CreateChecksum(hrp, values);
157+
data checksum = CreateChecksum(encoding, hrp, values);
149158
data combined = Cat(values, checksum);
150159
std::string ret = hrp + '1';
151160
ret.reserve(ret.size() + combined.size());
@@ -155,8 +164,8 @@ std::string Encode(const std::string& hrp, const data& values) {
155164
return ret;
156165
}
157166

158-
/** Decode a Bech32 string. */
159-
std::pair<std::string, data> Decode(const std::string& str) {
167+
/** Decode a Bech32 or Bech32m string. */
168+
DecodeResult Decode(const std::string& str) {
160169
bool lower = false, upper = false;
161170
for (size_t i = 0; i < str.size(); ++i) {
162171
unsigned char c = str[i];
@@ -183,10 +192,9 @@ std::pair<std::string, data> Decode(const std::string& str) {
183192
for (size_t i = 0; i < pos; ++i) {
184193
hrp += LowerCase(str[i]);
185194
}
186-
if (!VerifyChecksum(hrp, values)) {
187-
return {};
188-
}
189-
return {hrp, data(values.begin(), values.end() - 6)};
195+
Encoding result = VerifyChecksum(hrp, values);
196+
if (result == Encoding::INVALID) return {};
197+
return {result, std::move(hrp), data(values.begin(), values.end() - 6)};
190198
}
191199

192200
} // namespace bech32

src/bech32.h

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2017 Pieter Wuille
1+
// Copyright (c) 2017, 2021 Pieter Wuille
22
// Distributed under the MIT software license, see the accompanying
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

@@ -7,7 +7,7 @@
77
// separator character (1), and a base32 data section, the last
88
// 6 characters of which are a checksum.
99
//
10-
// For more information, see BIP 173.
10+
// For more information, see BIP 173 and BIP 350.
1111

1212
#ifndef BITCOIN_BECH32_H
1313
#define BITCOIN_BECH32_H
@@ -19,11 +19,29 @@
1919
namespace bech32
2020
{
2121

22-
/** Encode a Bech32 string. If hrp contains uppercase characters, this will cause an assertion error. */
23-
std::string Encode(const std::string& hrp, const std::vector<uint8_t>& values);
22+
enum class Encoding {
23+
INVALID, //!< Failed decoding
2424

25-
/** Decode a Bech32 string. Returns (hrp, data). Empty hrp means failure. */
26-
std::pair<std::string, std::vector<uint8_t>> Decode(const std::string& str);
25+
BECH32, //!< Bech32 encoding as defined in BIP173
26+
BECH32M, //!< Bech32m encoding as defined in BIP350
27+
};
28+
29+
/** Encode a Bech32 or Bech32m string. If hrp contains uppercase characters, this will cause an
30+
* assertion error. Encoding must be one of BECH32 or BECH32M. */
31+
std::string Encode(Encoding encoding, const std::string& hrp, const std::vector<uint8_t>& values);
32+
33+
struct DecodeResult
34+
{
35+
Encoding encoding; //!< What encoding was detected in the result; Encoding::INVALID if failed.
36+
std::string hrp; //!< The human readable part
37+
std::vector<uint8_t> data; //!< The payload (excluding checksum)
38+
39+
DecodeResult() : encoding(Encoding::INVALID) {}
40+
DecodeResult(Encoding enc, std::string&& h, std::vector<uint8_t>&& d) : encoding(enc), hrp(std::move(h)), data(std::move(d)) {}
41+
};
42+
43+
/** Decode a Bech32 string. */
44+
DecodeResult Decode(const std::string& str);
2745

2846
} // namespace bech32
2947

src/bench/bech32.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ static void Bech32Encode(benchmark::Bench& bench)
1919
tmp.reserve(1 + 32 * 8 / 5);
2020
ConvertBits<8, 5, true>([&](unsigned char c) { tmp.push_back(c); }, v.begin(), v.end());
2121
bench.batch(v.size()).unit("byte").run([&] {
22-
bech32::Encode("bc", tmp);
22+
bech32::Encode(bech32::Encoding::BECH32, "bc", tmp);
2323
});
2424
}
2525

src/key_io.cpp

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,15 @@ class DestinationEncoder
4343
std::vector<unsigned char> data = {0};
4444
data.reserve(33);
4545
ConvertBits<8, 5, true>([&](unsigned char c) { data.push_back(c); }, id.begin(), id.end());
46-
return bech32::Encode(m_params.Bech32HRP(), data);
46+
return bech32::Encode(bech32::Encoding::BECH32, m_params.Bech32HRP(), data);
4747
}
4848

4949
std::string operator()(const WitnessV0ScriptHash& id) const
5050
{
5151
std::vector<unsigned char> data = {0};
5252
data.reserve(53);
5353
ConvertBits<8, 5, true>([&](unsigned char c) { data.push_back(c); }, id.begin(), id.end());
54-
return bech32::Encode(m_params.Bech32HRP(), data);
54+
return bech32::Encode(bech32::Encoding::BECH32, m_params.Bech32HRP(), data);
5555
}
5656

5757
std::string operator()(const WitnessUnknown& id) const
@@ -62,7 +62,7 @@ class DestinationEncoder
6262
std::vector<unsigned char> data = {(unsigned char)id.version};
6363
data.reserve(1 + (id.length * 8 + 4) / 5);
6464
ConvertBits<8, 5, true>([&](unsigned char c) { data.push_back(c); }, id.program, id.program + id.length);
65-
return bech32::Encode(m_params.Bech32HRP(), data);
65+
return bech32::Encode(bech32::Encoding::BECH32, m_params.Bech32HRP(), data);
6666
}
6767

6868
std::string operator()(const CNoDestination& no) const { return {}; }
@@ -95,20 +95,18 @@ CTxDestination DecodeDestination(const std::string& str, const CChainParams& par
9595
error_str = "Invalid prefix for Base58-encoded address";
9696
}
9797
data.clear();
98-
auto bech = bech32::Decode(str);
99-
if (bech.second.size() > 0) {
98+
const auto dec = bech32::Decode(str);
99+
if (dec.encoding == bech32::Encoding::BECH32 && dec.data.size() > 0) {
100+
// Bech32 decoding
100101
error_str = "";
101-
102-
if (bech.first != params.Bech32HRP()) {
102+
if (dec.hrp != params.Bech32HRP()) {
103103
error_str = "Invalid prefix for Bech32 address";
104104
return CNoDestination();
105105
}
106-
107-
// Bech32 decoding
108-
int version = bech.second[0]; // The first 5 bit symbol is the witness version (0-16)
106+
int version = dec.data[0]; // The first 5 bit symbol is the witness version (0-16)
109107
// The rest of the symbols are converted witness program bytes.
110-
data.reserve(((bech.second.size() - 1) * 5) / 8);
111-
if (ConvertBits<5, 8, false>([&](unsigned char c) { data.push_back(c); }, bech.second.begin() + 1, bech.second.end())) {
108+
data.reserve(((dec.data.size() - 1) * 5) / 8);
109+
if (ConvertBits<5, 8, false>([&](unsigned char c) { data.push_back(c); }, dec.data.begin() + 1, dec.data.end())) {
112110
if (version == 0) {
113111
{
114112
WitnessV0KeyHash keyid;

src/test/bech32_tests.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ BOOST_AUTO_TEST_CASE(bip173_testvectors_valid)
2222
"?1ezyfcl",
2323
};
2424
for (const std::string& str : CASES) {
25-
auto ret = bech32::Decode(str);
26-
BOOST_CHECK(!ret.first.empty());
27-
std::string recode = bech32::Encode(ret.first, ret.second);
25+
const auto dec = bech32::Decode(str);
26+
BOOST_CHECK(dec.encoding == bech32::Encoding::BECH32);
27+
std::string recode = bech32::Encode(bech32::Encoding::BECH32, dec.hrp, dec.data);
2828
BOOST_CHECK(!recode.empty());
2929
BOOST_CHECK(CaseInsensitiveEqual(str, recode));
3030
}
@@ -49,8 +49,8 @@ BOOST_AUTO_TEST_CASE(bip173_testvectors_invalid)
4949
"A12uEL5L",
5050
};
5151
for (const std::string& str : CASES) {
52-
auto ret = bech32::Decode(str);
53-
BOOST_CHECK(ret.first.empty());
52+
const auto dec = bech32::Decode(str);
53+
BOOST_CHECK(dec.encoding != bech32::Encoding::BECH32);
5454
}
5555
}
5656

src/test/fuzz/bech32.cpp

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,28 @@
1616
FUZZ_TARGET(bech32)
1717
{
1818
const std::string random_string(buffer.begin(), buffer.end());
19-
const std::pair<std::string, std::vector<uint8_t>> r1 = bech32::Decode(random_string);
20-
if (r1.first.empty()) {
21-
assert(r1.second.empty());
19+
const auto r1 = bech32::Decode(random_string);
20+
if (r1.hrp.empty()) {
21+
assert(r1.encoding == bech32::Encoding::INVALID);
22+
assert(r1.data.empty());
2223
} else {
23-
const std::string& hrp = r1.first;
24-
const std::vector<uint8_t>& data = r1.second;
25-
const std::string reencoded = bech32::Encode(hrp, data);
24+
assert(r1.encoding != bech32::Encoding::INVALID);
25+
const std::string reencoded = bech32::Encode(r1.encoding, r1.hrp, r1.data);
2626
assert(CaseInsensitiveEqual(random_string, reencoded));
2727
}
2828

2929
std::vector<unsigned char> input;
3030
ConvertBits<8, 5, true>([&](unsigned char c) { input.push_back(c); }, buffer.begin(), buffer.end());
31-
const std::string encoded = bech32::Encode("bc", input);
32-
assert(!encoded.empty());
3331

34-
const std::pair<std::string, std::vector<uint8_t>> r2 = bech32::Decode(encoded);
35-
if (r2.first.empty()) {
36-
assert(r2.second.empty());
37-
} else {
38-
const std::string& hrp = r2.first;
39-
const std::vector<uint8_t>& data = r2.second;
40-
assert(hrp == "bc");
41-
assert(data == input);
32+
if (input.size() + 3 + 6 <= 90) {
33+
// If it's possible to encode input in Bech32(m) without exceeding the 90-character limit:
34+
for (auto encoding : {bech32::Encoding::BECH32, bech32::Encoding::BECH32M}) {
35+
const std::string encoded = bech32::Encode(encoding, "bc", input);
36+
assert(!encoded.empty());
37+
const auto r2 = bech32::Decode(encoded);
38+
assert(r2.encoding == encoding);
39+
assert(r2.hrp == "bc");
40+
assert(r2.data == input);
41+
}
4242
}
4343
}

0 commit comments

Comments
 (0)