Skip to content

Commit

Permalink
Make validateaddress take address_type and return error in all cases
Browse files Browse the repository at this point in the history
  • Loading branch information
meshcollider committed Feb 19, 2020
1 parent ac7c0e9 commit 09ea579
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 19 deletions.
6 changes: 4 additions & 2 deletions doc/release-notes-16807.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
Updated RPCs
------------

- The `validateaddress` RPC now returns an `error` message and `error_index` in
the case of an invalid address, diagnosing the error as if it was a Bech32 address.
- The `validateaddress` RPC now accepts an additional `address_type` parameter. If provided,
an invalid address will additionally return an `error` message with a reason for the invalidity.
If `address_type` is `bech32`, an `error_index` will also be returned, indicating the position
of an error in the address.
53 changes: 53 additions & 0 deletions src/key_io.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,56 @@ bool IsValidDestinationString(const std::string& str)
{
return IsValidDestinationString(str, Params());
}

std::pair<int, std::string> LocateErrorInDestinationString(const std::string& str, const std::string& address_type)
{
std::vector<unsigned char> data;
if (address_type == "legacy" || address_type == "p2sh-segwit") {
uint160 hash;
const std::vector<unsigned char>& pubkey_prefix = Params().Base58Prefix(CChainParams::PUBKEY_ADDRESS);
const std::vector<unsigned char>& script_prefix = Params().Base58Prefix(CChainParams::SCRIPT_ADDRESS);
if (str.length() > 35) {
return {0, "Length exceeds maximum for legacy and P2SH addresses"};
}
if (!DecodeBase58(str, data, 21)) {
return {0, "Invalid Base58 character in address"};
}
if (!DecodeBase58Check(str, data, 21)) {
return {0, "Invalid checksum for Base58 address"};
}
if (data.size() != hash.size() + pubkey_prefix.size() && data.size() != hash.size() + script_prefix.size()) {
return {0, "Invalid length for Base58 address"};
}
if (!std::equal(pubkey_prefix.begin(), pubkey_prefix.end(), data.begin()) &&
!std::equal(script_prefix.begin(), script_prefix.end(), data.begin())) {
return {0, "Invalid prefix for Base58 address"};
}
} else if (address_type == "bech32") {
auto bech = bech32::Decode(str);
if (bech.second.size() > 0) {
if (bech.first != Params().Bech32HRP()) {
return {0, "Invalid HRP for Bech32 address"};
}
int version = bech.second[0];
if (version > 16) {
return {0, "Invalid witness version"};
}
if (data.size() < 2) {
return {0, "Invalid data length"};
}
data.reserve(((bech.second.size() - 1) * 5) / 8);
if (ConvertBits<5, 8, false>([&](unsigned char c) { data.push_back(c); }, bech.second.begin() + 1, bech.second.end())) {
WitnessV0KeyHash keyid;
WitnessV0ScriptHash scriptid;
if (version == 0 && data.size() != keyid.size() && data.size() != scriptid.size()) {
return {0, "Invalid data length for version 0 witness"};
}
}
} else {
std::string error;
int pos = bech32::LocateError(str, error);
return {pos, error};
}
}
return {0, "Unknown address type"};
}
2 changes: 2 additions & 0 deletions src/key_io.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ CTxDestination DecodeDestination(const std::string& str);
bool IsValidDestinationString(const std::string& str);
bool IsValidDestinationString(const std::string& str, const CChainParams& params);

std::pair<int, std::string> LocateErrorInDestinationString(const std::string& str, const std::string& address_type);

#endif // BITCOIN_KEY_IO_H
14 changes: 8 additions & 6 deletions src/rpc/misc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ static UniValue validateaddress(const JSONRPCRequest& request)
"\nReturn information about the given bitcoin address.\n",
{
{"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The bitcoin address to validate"},
{"address_type", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "The address type provided, used to detect errors. Options are \"legacy\", \"p2sh-segwit\", and \"bech32\"."},
},
RPCResult{
"{\n"
Expand All @@ -42,8 +43,8 @@ static UniValue validateaddress(const JSONRPCRequest& request)
" \"iswitness\" : true|false, (boolean) If the address is a witness address\n"
" \"witness_version\" : version (numeric, optional) The version number of the witness program\n"
" \"witness_program\" : \"hex\" (string, optional) The hex value of the witness program\n"
" \"error\" : \"message\" (string, optional) The error message if the address is an invalid bech32 address\n"
" \"error_index\" : xxxxx (numeric, optional) The index of the first invalid character if the address is encoded with bech32\n"
" \"error\" : \"message\" (string, optional) The error message if the provided address is invalid and address type is provided\n"
" \"error_index\" : xxxxx (numeric, optional) The index of the first invalid character (if the address type provided is bech32)\n"
"}\n"
},
RPCExamples{
Expand All @@ -68,11 +69,12 @@ static UniValue validateaddress(const JSONRPCRequest& request)

UniValue detail = DescribeAddress(dest);
ret.pushKVs(detail);
} else {
} else if (!request.params[1].isNull()) {
std::string address_type = request.params[1].get_str();
std::string error;
int pos = bech32::LocateError(address, error);
ret.pushKV("error", error);
ret.pushKV("error_index", pos);
auto res = LocateErrorInDestinationString(address, address_type);
ret.pushKV("error", res.second);
if (address_type == "bech32") ret.pushKV("error_index", res.first);
}
return ret;
}
Expand Down
68 changes: 57 additions & 11 deletions test/functional/rpc_validateaddress.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,80 +11,126 @@ def set_test_params(self):
self.num_nodes = 1

def run_test(self):

# Valid address
address = "bcrt1q049ldschfnwystcqnsvyfpj23mpsg3jcedq9xv"
res = self.nodes[0].validateaddress(address)
res = self.nodes[0].validateaddress(address, "bech32")
assert_equal(res['isvalid'], True)
assert 'error' not in res
assert 'error_index' not in res

# Valid capitalised address
address = "BCRT1QPLMTZKC2XHARPPZDLNPAQL78RSHJ68U33RAH7R"
res = self.nodes[0].validateaddress(address)
res = self.nodes[0].validateaddress(address, "bech32")
assert_equal(res['isvalid'], True)
assert 'error' not in res
assert 'error_index' not in res

# Address with no '1' separator
# Invalid address without type specified
address = "bcrtq049ldschfnwystcqnsvyfpj23mpsg3jcedq9xv"
res = self.nodes[0].validateaddress(address)
assert_equal(res['isvalid'], False)
assert 'error' not in res
assert 'error_index' not in res

# Address with no '1' separator
address = "bcrtq049ldschfnwystcqnsvyfpj23mpsg3jcedq9xv"
res = self.nodes[0].validateaddress(address, "bech32")
assert_equal(res['isvalid'], False)
assert_equal(res['error_index'], 0)
assert_equal(res['error'], "Missing separator")

# Address with no HRP
address = "1q049ldschfnwystcqnsvyfpj23mpsg3jcedq9xv"
res = self.nodes[0].validateaddress(address)
res = self.nodes[0].validateaddress(address, "bech32")
assert_equal(res['isvalid'], False)
assert_equal(res['error_index'], 0)
assert_equal(res['error'], "Invalid separator position")

# Address with an invalid bech32 encoding character
address = "bcrt1q04oldschfnwystcqnsvyfpj23mpsg3jcedq9xv"
res = self.nodes[0].validateaddress(address)
res = self.nodes[0].validateaddress(address, "bech32")
assert_equal(res['isvalid'], False)
assert_equal(res['error_index'], 8)
assert_equal(res['error'], "Invalid Base 32 character")

# Address with one error
address = "bcrt1q049edschfnwystcqnsvyfpj23mpsg3jcedq9xv"
res = self.nodes[0].validateaddress(address)
res = self.nodes[0].validateaddress(address, "bech32")
assert_equal(res['isvalid'], False)
assert_equal(res['error_index'], 9)
assert_equal(res['error'], "Invalid")

# Capitalised address with one error
address = "BCRT1QPLMTZKC2XHARPPZDLNPAQL78RSHJ68U32RAH7R"
res = self.nodes[0].validateaddress(address)
res = self.nodes[0].validateaddress(address, "bech32")
assert_equal(res['isvalid'], False)
assert_equal(res['error_index'], 38)
assert_equal(res['error'], "Invalid")

# Valid multisig address
address = "bcrt1qdg3myrgvzw7ml9q0ejxhlkyxm7vl9r56yzkfgvzclrf4hkpx9yfqhpsuks"
res = self.nodes[0].validateaddress(address)
res = self.nodes[0].validateaddress(address, "bech32")
assert_equal(res['isvalid'], True)

# Multisig address with 2 errors
address = "bcrt1qdg3myrgvzw7ml8q0ejxhlkyxn7vl9r56yzkfgvzclrf4hkpx9yfqhpsuks"
res = self.nodes[0].validateaddress(address)
res = self.nodes[0].validateaddress(address, "bech32")
assert_equal(res['isvalid'], False)
assert_equal(res['error_index'], 19)
assert_equal(res['error'], "Invalid")

# Address with 2 errors
address = "bcrt1qfsawymtaadjet5sl7p9zw273wpvu9rgwq93y52"
res = self.nodes[0].validateaddress(address)
res = self.nodes[0].validateaddress(address, "bech32")
assert_equal(res['isvalid'], False)
assert_equal(res['error_index'], 22)
assert_equal(res['error'], "Invalid")

# Invalid but also too long
address = "bcrt1q049edschfnwystcqnsvyfpj23mpsg3jcedq9xv049edschfnwystcqnsvyfpj23mpsg3jcedq9xv049edschfnwystcqnsvyfpj23m"
res = self.nodes[0].validateaddress(address)
res = self.nodes[0].validateaddress(address, "bech32")
assert_equal(res['isvalid'], False)
assert_equal(res['error_index'], 90)
assert_equal(res['error'], "String too long")

# Legacy address with no errors
address = "muehQnn2P5NiiRCGg9HewAWrXLRy2d7TZE"
res = self.nodes[0].validateaddress(address, "legacy")
assert_equal(res['isvalid'], True)

# Legacy address with a non-Base58 character
address = "muehQnn2P5NiiRCGO9HewAWrXLRy2d7TZEaaaaaaa"
res = self.nodes[0].validateaddress(address, "legacy")
assert_equal(res['isvalid'], False)
assert_equal(res['error'], "Length exceeds maximum for legacy and P2SH addresses")

# Legacy address with one error
address = "muehQnn2P5NhiRCGg9HewAWrXLRy2d7TZE"
res = self.nodes[0].validateaddress(address, "legacy")
assert_equal(res['isvalid'], False)
assert_equal(res['error'], "Invalid checksum for Base58 address")
assert 'error_index' not in res

# Legacy address with a non-Base58 character
address = "muehQnn2P5NiiRCGO9HewAWrXLRy2d7TZE"
res = self.nodes[0].validateaddress(address, "legacy")
assert_equal(res['isvalid'], False)
assert_equal(res['error'], "Invalid Base58 character in address")

# Legacy address with incorrect prefix for current network
address = "1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2"
res = self.nodes[0].validateaddress(address, "legacy")
assert_equal(res['isvalid'], False)
assert_equal(res['error'], "Invalid prefix for Base58 address")

# Bech32 address with incorrect HRP for current network
address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"
res = self.nodes[0].validateaddress(address, "bech32")
assert_equal(res['isvalid'], False)
assert_equal(res['error'], "Invalid HRP for Bech32 address")
assert_equal(res['error_index'], 0)


if __name__ == '__main__':
ValidateaddressTest().main()

0 comments on commit 09ea579

Please sign in to comment.