Skip to content

Commit 95d19f8

Browse files
committed
Merge bitcoin/bitcoin#16807: Let validateaddress locate error in Bech32 address
88cc481 Modify copyright header on Bech32 code (Samuel Dobson) 5599813 Add lots of comments to Bech32 (Samuel Dobson) 2eb5792 Add release notes for validateaddress Bech32 error detection (MeshCollider) 42d6a02 Refactor and add more tests for validateaddress (Samuel Dobson) c4979f7 Add boost tests for bech32 error detection (MeshCollider) 02a7bde Add error_locations to validateaddress RPC (Samuel Dobson) b62b67e Add Bech32 error location function (Samuel Dobson) 0b06e72 More detailed error checking for base58 addresses (Samuel Dobson) Pull request description: Addresses (partially) #16779 - no GUI change in this PR Adds a LocateError function the bech32 library, which is then called by `validateaddress` RPC, (and then eventually from a GUI tool too, future work). I think modifying validateaddress is nicer than adding a separate RPC for this. Includes tests. Based on https://github.com/sipa/bech32/blob/master/ecc/javascript/bech32_ecc.js Credit to sipa for that code ACKs for top commit: laanwj: Code review and manually tested ACK 88cc481 ryanofsky: Code review ACK 88cc481 with caveat that I only checked the new `LocateErrors` code to try to verify it didn't have unsafe or unexpected operations or loop forever or crash. Did not try to verify behavior corresponds to the spec. In the worst case bugs here should just affect error messages not actual decoding of addresses so this seemed ok. w0xlt: tACK 88cc481 Tree-SHA512: 9c7fe9745bc7527f80a30bd4c1e3034e16b96a02cc7f6c268f91bfad08a6965a8064fe44230aa3f87e4fa3c938f662ff4446bc682c83cb48c1a3f95cf4186688
2 parents ee7e061 + 88cc481 commit 95d19f8

File tree

8 files changed

+607
-63
lines changed

8 files changed

+607
-63
lines changed

doc/release-notes-16807.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Updated RPCs
2+
------------
3+
4+
- The `validateaddress` RPC now optionally returns an `error_locations` array, with the indices of
5+
invalid characters in the address. For example, this will return the locations of up to two Bech32
6+
errors.

src/bech32.cpp

Lines changed: 443 additions & 8 deletions
Large diffs are not rendered by default.

src/bech32.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright (c) 2017, 2021 Pieter Wuille
2+
// Copyright (c) 2021 The Bitcoin Core developers
23
// Distributed under the MIT software license, see the accompanying
34
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
45

@@ -44,6 +45,9 @@ struct DecodeResult
4445
/** Decode a Bech32 or Bech32m string. */
4546
DecodeResult Decode(const std::string& str);
4647

48+
/** Return the positions of errors in a Bech32 string. */
49+
std::string LocateErrors(const std::string& str, std::vector<int>& error_locations);
50+
4751
} // namespace bech32
4852

4953
#endif // BITCOIN_BECH32_H

src/key_io.cpp

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,16 @@ class DestinationEncoder
7676
std::string operator()(const CNoDestination& no) const { return {}; }
7777
};
7878

79-
CTxDestination DecodeDestination(const std::string& str, const CChainParams& params, std::string& error_str)
79+
CTxDestination DecodeDestination(const std::string& str, const CChainParams& params, std::string& error_str, std::vector<int>* error_locations)
8080
{
8181
std::vector<unsigned char> data;
8282
uint160 hash;
8383
error_str = "";
84-
if (DecodeBase58Check(str, data, 21)) {
84+
85+
// Note this will be false if it is a valid Bech32 address for a different network
86+
bool is_bech32 = (ToLower(str.substr(0, params.Bech32HRP().size())) == params.Bech32HRP());
87+
88+
if (!is_bech32 && DecodeBase58Check(str, data, 21)) {
8589
// base58-encoded Bitcoin addresses.
8690
// Public-key-hash-addresses have version 0 (or 111 testnet).
8791
// The data vector contains RIPEMD160(SHA256(pubkey)), where pubkey is the serialized public key.
@@ -98,15 +102,27 @@ CTxDestination DecodeDestination(const std::string& str, const CChainParams& par
98102
return ScriptHash(hash);
99103
}
100104

101-
// Set potential error message.
102-
// This message may be changed if the address can also be interpreted as a Bech32 address.
103-
error_str = "Invalid prefix for Base58-encoded address";
105+
if (!std::equal(script_prefix.begin(), script_prefix.end(), data.begin()) &&
106+
!std::equal(pubkey_prefix.begin(), pubkey_prefix.end(), data.begin())) {
107+
error_str = "Invalid prefix for Base58-encoded address";
108+
} else {
109+
error_str = "Invalid length for Base58 address";
110+
}
111+
return CNoDestination();
112+
} else if (!is_bech32) {
113+
// Try Base58 decoding without the checksum, using a much larger max length
114+
if (!DecodeBase58(str, data, 100)) {
115+
error_str = "Invalid HRP or Base58 character in address";
116+
} else {
117+
error_str = "Invalid checksum or length of Base58 address";
118+
}
119+
return CNoDestination();
104120
}
121+
105122
data.clear();
106123
const auto dec = bech32::Decode(str);
107124
if ((dec.encoding == bech32::Encoding::BECH32 || dec.encoding == bech32::Encoding::BECH32M) && dec.data.size() > 0) {
108125
// Bech32 decoding
109-
error_str = "";
110126
if (dec.hrp != params.Bech32HRP()) {
111127
error_str = "Invalid prefix for Bech32 address";
112128
return CNoDestination();
@@ -168,8 +184,13 @@ CTxDestination DecodeDestination(const std::string& str, const CChainParams& par
168184
}
169185
}
170186

171-
// Set error message if address can't be interpreted as Base58 or Bech32.
172-
if (error_str.empty()) error_str = "Invalid address format";
187+
// Perform Bech32 error location
188+
if (!error_locations) {
189+
std::vector<int> dummy_errors;
190+
error_str = bech32::LocateErrors(str, dummy_errors);
191+
} else {
192+
error_str = bech32::LocateErrors(str, *error_locations);
193+
}
173194

174195
return CNoDestination();
175196
}
@@ -258,9 +279,9 @@ std::string EncodeDestination(const CTxDestination& dest)
258279
return std::visit(DestinationEncoder(Params()), dest);
259280
}
260281

261-
CTxDestination DecodeDestination(const std::string& str, std::string& error_msg)
282+
CTxDestination DecodeDestination(const std::string& str, std::string& error_msg, std::vector<int>* error_locations)
262283
{
263-
return DecodeDestination(str, Params(), error_msg);
284+
return DecodeDestination(str, Params(), error_msg, error_locations);
264285
}
265286

266287
CTxDestination DecodeDestination(const std::string& str)
@@ -272,7 +293,7 @@ CTxDestination DecodeDestination(const std::string& str)
272293
bool IsValidDestinationString(const std::string& str, const CChainParams& params)
273294
{
274295
std::string error_msg;
275-
return IsValidDestination(DecodeDestination(str, params, error_msg));
296+
return IsValidDestination(DecodeDestination(str, params, error_msg, nullptr));
276297
}
277298

278299
bool IsValidDestinationString(const std::string& str)

src/key_io.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ std::string EncodeExtPubKey(const CExtPubKey& extpubkey);
2323

2424
std::string EncodeDestination(const CTxDestination& dest);
2525
CTxDestination DecodeDestination(const std::string& str);
26-
CTxDestination DecodeDestination(const std::string& str, std::string& error_msg);
26+
CTxDestination DecodeDestination(const std::string& str, std::string& error_msg, std::vector<int>* error_locations = nullptr);
2727
bool IsValidDestinationString(const std::string& str);
2828
bool IsValidDestinationString(const std::string& str, const CChainParams& params);
2929

src/rpc/misc.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ static RPCHelpMan validateaddress()
5252
{RPCResult::Type::NUM, "witness_version", /* optional */ true, "The version number of the witness program"},
5353
{RPCResult::Type::STR_HEX, "witness_program", /* optional */ true, "The hex value of the witness program"},
5454
{RPCResult::Type::STR, "error", /* optional */ true, "Error message, if any"},
55+
{RPCResult::Type::ARR, "error_locations", "Indices of likely error locations in address, if known (e.g. Bech32 errors)",
56+
{
57+
{RPCResult::Type::NUM, "index", "index of a potential error"},
58+
}},
5559
}
5660
},
5761
RPCExamples{
@@ -61,7 +65,8 @@ static RPCHelpMan validateaddress()
6165
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
6266
{
6367
std::string error_msg;
64-
CTxDestination dest = DecodeDestination(request.params[0].get_str(), error_msg);
68+
std::vector<int> error_locations;
69+
CTxDestination dest = DecodeDestination(request.params[0].get_str(), error_msg, &error_locations);
6570
const bool isValid = IsValidDestination(dest);
6671
CHECK_NONFATAL(isValid == error_msg.empty());
6772

@@ -77,6 +82,9 @@ static RPCHelpMan validateaddress()
7782
UniValue detail = DescribeAddress(dest);
7883
ret.pushKVs(detail);
7984
} else {
85+
UniValue error_indices(UniValue::VARR);
86+
for (int i : error_locations) error_indices.push_back(i);
87+
ret.pushKV("error_locations", error_indices);
8088
ret.pushKV("error", error_msg);
8189
}
8290

src/test/bech32_tests.cpp

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright (c) 2017 Pieter Wuille
2+
// Copyright (c) 2021 The Bitcoin Core developers
23
// Distributed under the MIT software license, see the accompanying
34
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
45

@@ -68,10 +69,36 @@ BOOST_AUTO_TEST_CASE(bech32_testvectors_invalid)
6869
"1qzzfhee",
6970
"a12UEL5L",
7071
"A12uEL5L",
72+
"abcdef1qpzrz9x8gf2tvdw0s3jn54khce6mua7lmqqqxw",
7173
};
74+
static const std::pair<std::string, int> ERRORS[] = {
75+
{"Invalid character or mixed case", 0},
76+
{"Invalid character or mixed case", 0},
77+
{"Invalid character or mixed case", 0},
78+
{"Bech32 string too long", 90},
79+
{"Missing separator", -1},
80+
{"Invalid separator position", 0},
81+
{"Invalid Base 32 character", 2},
82+
{"Invalid separator position", 2},
83+
{"Invalid character or mixed case", 8},
84+
{"Invalid checksum", -1}, // The checksum is calculated using the uppercase form so the entire string is invalid, not just a few characters
85+
{"Invalid separator position", 0},
86+
{"Invalid separator position", 0},
87+
{"Invalid character or mixed case", 3},
88+
{"Invalid character or mixed case", 3},
89+
{"Invalid checksum", 11}
90+
};
91+
int i = 0;
7292
for (const std::string& str : CASES) {
93+
const auto& err = ERRORS[i];
7394
const auto dec = bech32::Decode(str);
7495
BOOST_CHECK(dec.encoding == bech32::Encoding::INVALID);
96+
std::vector<int> error_locations;
97+
std::string error = bech32::LocateErrors(str, error_locations);
98+
BOOST_CHECK_EQUAL(err.first, error);
99+
if (err.second == -1) BOOST_CHECK(error_locations.empty());
100+
else BOOST_CHECK_EQUAL(err.second, error_locations[0]);
101+
i++;
75102
}
76103
}
77104

@@ -91,11 +118,37 @@ BOOST_AUTO_TEST_CASE(bech32m_testvectors_invalid)
91118
"au1s5cgom",
92119
"M1VUXWEZ",
93120
"16plkw9",
94-
"1p2gdwpf"
121+
"1p2gdwpf",
122+
"abcdef1l7aum6echk45nj2s0wdvt2fg8x9yrzpqzd3ryx",
123+
};
124+
static const std::pair<std::string, int> ERRORS[] = {
125+
{"Invalid character or mixed case", 0},
126+
{"Invalid character or mixed case", 0},
127+
{"Invalid character or mixed case", 0},
128+
{"Bech32 string too long", 90},
129+
{"Missing separator", -1},
130+
{"Invalid separator position", 0},
131+
{"Invalid Base 32 character", 2},
132+
{"Invalid Base 32 character", 3},
133+
{"Invalid separator position", 2},
134+
{"Invalid Base 32 character", 8},
135+
{"Invalid Base 32 character", 7},
136+
{"Invalid checksum", -1},
137+
{"Invalid separator position", 0},
138+
{"Invalid separator position", 0},
139+
{"Invalid checksum", 21},
95140
};
141+
int i = 0;
96142
for (const std::string& str : CASES) {
143+
const auto& err = ERRORS[i];
97144
const auto dec = bech32::Decode(str);
98145
BOOST_CHECK(dec.encoding == bech32::Encoding::INVALID);
146+
std::vector<int> error_locations;
147+
std::string error = bech32::LocateErrors(str, error_locations);
148+
BOOST_CHECK_EQUAL(err.first, error);
149+
if (err.second == -1) BOOST_CHECK(error_locations.empty());
150+
else BOOST_CHECK_EQUAL(err.second, error_locations[0]);
151+
i++;
99152
}
100153
}
101154

test/functional/rpc_invalid_address_message.py

Lines changed: 58 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,79 +12,96 @@
1212
)
1313

1414
BECH32_VALID = 'bcrt1qtmp74ayg7p24uslctssvjm06q5phz4yrxucgnv'
15+
BECH32_VALID_CAPITALS = 'BCRT1QPLMTZKC2XHARPPZDLNPAQL78RSHJ68U33RAH7R'
16+
BECH32_VALID_MULTISIG = 'bcrt1qdg3myrgvzw7ml9q0ejxhlkyxm7vl9r56yzkfgvzclrf4hkpx9yfqhpsuks'
17+
1518
BECH32_INVALID_BECH32 = 'bcrt1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqdmchcc'
1619
BECH32_INVALID_BECH32M = 'bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7k35mrzd'
1720
BECH32_INVALID_VERSION = 'bcrt130xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqynjegk'
1821
BECH32_INVALID_SIZE = 'bcrt1s0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v8n0nx0muaewav25430mtr'
1922
BECH32_INVALID_V0_SIZE = 'bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kqqq5k3my'
2023
BECH32_INVALID_PREFIX = 'bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx'
24+
BECH32_TOO_LONG = 'bcrt1q049edschfnwystcqnsvyfpj23mpsg3jcedq9xv049edschfnwystcqnsvyfpj23mpsg3jcedq9xv049edschfnwystcqnsvyfpj23m'
25+
BECH32_ONE_ERROR = 'bcrt1q049edschfnwystcqnsvyfpj23mpsg3jcedq9xv'
26+
BECH32_ONE_ERROR_CAPITALS = 'BCRT1QPLMTZKC2XHARPPZDLNPAQL78RSHJ68U32RAH7R'
27+
BECH32_TWO_ERRORS = 'bcrt1qax9suht3qv95sw33xavx8crpxduefdrsvgsklu' # should be bcrt1qax9suht3qv95sw33wavx8crpxduefdrsvgsklx
28+
BECH32_NO_SEPARATOR = 'bcrtq049ldschfnwystcqnsvyfpj23mpsg3jcedq9xv'
29+
BECH32_INVALID_CHAR = 'bcrt1q04oldschfnwystcqnsvyfpj23mpsg3jcedq9xv'
30+
BECH32_MULTISIG_TWO_ERRORS = 'bcrt1qdg3myrgvzw7ml8q0ejxhlkyxn7vl9r56yzkfgvzclrf4hkpx9yfqhpsuks'
31+
BECH32_WRONG_VERSION = 'bcrt1ptmp74ayg7p24uslctssvjm06q5phz4yrxucgnv'
2132

2233
BASE58_VALID = 'mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn'
2334
BASE58_INVALID_PREFIX = '17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem'
35+
BASE58_INVALID_CHECKSUM = 'mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJJfn'
36+
BASE58_INVALID_LENGTH = '2VKf7XKMrp4bVNVmuRbyCewkP8FhGLP2E54LHDPakr9Sq5mtU2'
2437

2538
INVALID_ADDRESS = 'asfah14i8fajz0123f'
39+
INVALID_ADDRESS_2 = '1q049ldschfnwystcqnsvyfpj23mpsg3jcedq9xv'
2640

2741
class InvalidAddressErrorMessageTest(BitcoinTestFramework):
2842
def set_test_params(self):
2943
self.setup_clean_chain = True
3044
self.num_nodes = 1
3145

32-
def test_validateaddress(self):
33-
node = self.nodes[0]
34-
35-
# Bech32
36-
info = node.validateaddress(BECH32_INVALID_SIZE)
37-
assert not info['isvalid']
38-
assert_equal(info['error'], 'Invalid Bech32 address data size')
39-
40-
info = node.validateaddress(BECH32_INVALID_PREFIX)
41-
assert not info['isvalid']
42-
assert_equal(info['error'], 'Invalid prefix for Bech32 address')
43-
44-
info = node.validateaddress(BECH32_INVALID_BECH32)
45-
assert not info['isvalid']
46-
assert_equal(info['error'], 'Version 1+ witness address must use Bech32m checksum')
47-
48-
info = node.validateaddress(BECH32_INVALID_BECH32M)
49-
assert not info['isvalid']
50-
assert_equal(info['error'], 'Version 0 witness address must use Bech32 checksum')
51-
52-
info = node.validateaddress(BECH32_INVALID_V0_SIZE)
53-
assert not info['isvalid']
54-
assert_equal(info['error'], 'Invalid Bech32 v0 address data size')
55-
56-
info = node.validateaddress(BECH32_VALID)
46+
def check_valid(self, addr):
47+
info = self.nodes[0].validateaddress(addr)
5748
assert info['isvalid']
5849
assert 'error' not in info
50+
assert 'error_locations' not in info
5951

60-
info = node.validateaddress(BECH32_INVALID_VERSION)
61-
assert not info['isvalid']
62-
assert_equal(info['error'], 'Invalid Bech32 address witness version')
63-
64-
# Base58
65-
info = node.validateaddress(BASE58_INVALID_PREFIX)
66-
assert not info['isvalid']
67-
assert_equal(info['error'], 'Invalid prefix for Base58-encoded address')
52+
def check_invalid(self, addr, error_str, error_locations=None):
53+
res = self.nodes[0].validateaddress(addr)
54+
assert not res['isvalid']
55+
assert_equal(res['error'], error_str)
56+
if error_locations:
57+
assert_equal(res['error_locations'], error_locations)
58+
else:
59+
assert_equal(res['error_locations'], [])
6860

69-
info = node.validateaddress(BASE58_VALID)
70-
assert info['isvalid']
71-
assert 'error' not in info
61+
def test_validateaddress(self):
62+
# Invalid Bech32
63+
self.check_invalid(BECH32_INVALID_SIZE, 'Invalid Bech32 address data size')
64+
self.check_invalid(BECH32_INVALID_PREFIX, 'Invalid HRP or Base58 character in address')
65+
self.check_invalid(BECH32_INVALID_BECH32, 'Version 1+ witness address must use Bech32m checksum')
66+
self.check_invalid(BECH32_INVALID_BECH32M, 'Version 0 witness address must use Bech32 checksum')
67+
self.check_invalid(BECH32_INVALID_VERSION, 'Invalid Bech32 address witness version')
68+
self.check_invalid(BECH32_INVALID_V0_SIZE, 'Invalid Bech32 v0 address data size')
69+
self.check_invalid(BECH32_TOO_LONG, 'Bech32 string too long', list(range(90, 108)))
70+
self.check_invalid(BECH32_ONE_ERROR, 'Invalid checksum', [9])
71+
self.check_invalid(BECH32_TWO_ERRORS, 'Invalid checksum', [22, 43])
72+
self.check_invalid(BECH32_ONE_ERROR_CAPITALS, 'Invalid checksum', [38])
73+
self.check_invalid(BECH32_NO_SEPARATOR, 'Missing separator')
74+
self.check_invalid(BECH32_INVALID_CHAR, 'Invalid Base 32 character', [8])
75+
self.check_invalid(BECH32_MULTISIG_TWO_ERRORS, 'Invalid checksum', [19, 30])
76+
self.check_invalid(BECH32_WRONG_VERSION, 'Invalid checksum', [5])
77+
78+
# Valid Bech32
79+
self.check_valid(BECH32_VALID)
80+
self.check_valid(BECH32_VALID_CAPITALS)
81+
self.check_valid(BECH32_VALID_MULTISIG)
82+
83+
# Invalid Base58
84+
self.check_invalid(BASE58_INVALID_PREFIX, 'Invalid prefix for Base58-encoded address')
85+
self.check_invalid(BASE58_INVALID_CHECKSUM, 'Invalid checksum or length of Base58 address')
86+
self.check_invalid(BASE58_INVALID_LENGTH, 'Invalid checksum or length of Base58 address')
87+
88+
# Valid Base58
89+
self.check_valid(BASE58_VALID)
7290

7391
# Invalid address format
74-
info = node.validateaddress(INVALID_ADDRESS)
75-
assert not info['isvalid']
76-
assert_equal(info['error'], 'Invalid address format')
92+
self.check_invalid(INVALID_ADDRESS, 'Invalid HRP or Base58 character in address')
93+
self.check_invalid(INVALID_ADDRESS_2, 'Invalid HRP or Base58 character in address')
7794

7895
def test_getaddressinfo(self):
7996
node = self.nodes[0]
8097

8198
assert_raises_rpc_error(-5, "Invalid Bech32 address data size", node.getaddressinfo, BECH32_INVALID_SIZE)
8299

83-
assert_raises_rpc_error(-5, "Invalid prefix for Bech32 address", node.getaddressinfo, BECH32_INVALID_PREFIX)
100+
assert_raises_rpc_error(-5, "Invalid HRP or Base58 character in address", node.getaddressinfo, BECH32_INVALID_PREFIX)
84101

85102
assert_raises_rpc_error(-5, "Invalid prefix for Base58-encoded address", node.getaddressinfo, BASE58_INVALID_PREFIX)
86103

87-
assert_raises_rpc_error(-5, "Invalid address format", node.getaddressinfo, INVALID_ADDRESS)
104+
assert_raises_rpc_error(-5, "Invalid HRP or Base58 character in address", node.getaddressinfo, INVALID_ADDRESS)
88105

89106
def run_test(self):
90107
self.test_validateaddress()

0 commit comments

Comments
 (0)