-
Notifications
You must be signed in to change notification settings - Fork 37.7k
Enhanced error messages for invalid network prefix during address parsing. #27260
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code Coverage & BenchmarksFor details see: https://corecheck.dev/bitcoin/bitcoin/pulls/27260. ReviewsSee the guideline for information on the review process.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update. ConflictsReviewers, this pull request conflicts with the following ones: If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. |
Please keep your changes, and "fixes" commits squashed. Looks like your changing the file perms on at least one file, is that intentional? |
Will do. No it was not intentional but was a side effect of moving 3 files over scp to a different host. This has been resolved and commits squashed. |
c98f7cc
to
911c8b6
Compare
3c0fcda
to
aff7635
Compare
Needs rebase on current master, if still relevant |
ef46a30
to
5ff2490
Compare
I believe this PR is still very relevant because it substantially improves the logic around address decoding and specifically the displaying of errors in the GUI and RPC. With that said I have rebased and this code is passing all tests other than |
Thanks, the reason I mentioned it was the silent merge conflict: |
Is there a good way to detect these silent conflicts earlier? Usually I get notified via email when there are issues that need rebased; this is the first time one has happened without a notification. Thanks |
🚧 At least one of the CI tasks failed. HintsTry to run the tests locally, according to the documentation. However, a CI failure may still
Leave a comment here, if you need help tracking down a confusing failure. |
Hey, Sorry for the delayed updates to this PR, I just force pushed after quash the split Base58 and bech32 decoding portions. Per the request of @Sjors . There also was a substantial effort to create a proper set of functional tests that actually test properly. Examples would be that now with the correct network decoding some errors became network aware and as such needed to be converted to the correct network. Example would be the bech32 padding issue. test/functional/rpc_validateaddress.py line 55 I will be working on addressing some of the more recent comments in the coming day. Thanks, |
Is this still in the pipeline, or do you prefer if we review as-is? |
I should have done all of the splitting up as well as revised the tests to match for the new address decoding conditions. |
Any particular reason for marking this draft? |
Yes, I was going to work on the tests for this PR to enable testing with multiple networks, currently the tests had to be refactored to work with mainnet based addresses because of the network awareness. |
Co-authored-by: D++ <82842780+dplusplus1024@users.noreply.github.com>
This is the one! with this commit I have enabled multi network functional testing [Signet, Mainnet] this enables proof that under different networks the results will match expectations. This also had a few benefits too! Previously commented out tests are now valid! Thanks, and ready for review! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK, we should indeed make the error messages more user friendly.
It seems to me the PR is heading in the right direction, but we need to separate handling unrelated code paths - bech32 and base58 are different enough that they deserve their own helper method, not just buried somewhere in an if condition.
The functional tests could also use better separation, without favoring one or the other in the default params and the commit messages should explain the context better.
self.check_invalid(BECH32_INVALID_CHAR, 'Invalid Base 32 character', [8]) | ||
self.check_invalid(BECH32_MULTISIG_TWO_ERRORS, 'Invalid Bech32 checksum', [19, 30]) | ||
self.check_invalid(BECH32_WRONG_VERSION, 'Invalid Bech32 checksum', [5]) | ||
self.check_invalid(BECH32_TOO_LONG, 'Bech32(m) address decoded with error: Bech32 string too long', list(range(90, 108))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Q: when do we refer to it as Bech32
and when Bech32(m)
?
@@ -62,12 +62,12 @@ def check_invalid(self, addr, error_str, error_locations=None): | |||
|
|||
def test_validateaddress(self): | |||
# Invalid Bech32 | |||
self.check_invalid(BECH32_INVALID_SIZE, "Invalid Bech32 address program size (41 bytes)") | |||
self.check_invalid(BECH32_INVALID_PREFIX, 'Invalid or unsupported prefix for Segwit (Bech32) address (expected bcrt, got bc)') | |||
self.check_invalid(BECH32_INVALID_SIZE, 'Invalid Bech32 address program size (41 bytes)') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would help with the review if commits only had a single focus: separating low risk refactors with higher risk behavioral changes.
# ( | ||
# "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", | ||
# "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", | ||
# ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the story behind these, could we add them to the test suite instead of deleting them?
Why were they committed like this in the first place?
if network_name == "signet": | ||
INVALID_DATA = INVALID_DATA_SIGNET | ||
VALID_DATA = VALID_DATA_SIGNET | ||
else: | ||
INVALID_DATA = INVALID_DATA_MAINNET | ||
VALID_DATA = VALID_DATA_MAINNET |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could we rather store them in a dictionary above, keyed by network_name
?
INVALID_DATA = {
"mainnet": [
("tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", "Invalid or unsupported prefix for Segwit (Bech32) Bitcoin address (expected bc, got tc)", []),
...
],
"signet": [
("tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", "Invalid or unsupported prefix for Segwit (Bech32) signet address (expected tb, got tc)", []),
...
],
}
VALID_DATA = {
"mainnet": [
("BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", "0014751e76e8199196d454941c45d1b3a323f1433bd6"),
...
],
"signet": [
("tb1pfees9rn5nz", "51024e73"),
...
],
}
and
def test_validateaddress_on_network(self, network_name):
self.log.info(f"Testing validateaddress on {network_name}")
for (addr, error, locs) in INVALID_DATA[network_name]:
self.check_invalid(addr, error, locs)
for (addr, spk) in VALID_DATA[network_name]:
self.check_valid(addr, spk)
# Switch to mainnet tests | ||
self.stop_nodes() | ||
self.nodes.clear() | ||
self.chain = "" # Switch to mainnet | ||
self.extra_args = [["-prune=899"]] | ||
self.setup_chain() | ||
self.setup_network() | ||
self.test_validateaddress_on_network("mainnet") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure this is the best way to test this, starting it up in signet mode and stopping and clearing and overwriting the args - can we rather make them parameterizable, something like:
class ValidateAddressSignetTest(BaseValidateAddressTest):
def set_test_params(self):
super().set_test_params()
self.network_name = "signet"
self.chain = "signet"
self.extra_args = [["-prune=899", "-signet"]]
class ValidateAddressMainnetTest(BaseValidateAddressTest):
def set_test_params(self):
super().set_test_params()
self.network_name = "mainnet"
self.chain = ""
self.extra_args = [["-prune=899"]]
if __name__ == "__main__":
ValidateAddressSignetTest(__file__).main()
ValidateAddressMainnetTest(__file__).main()
(if this is even possible, haven't tried it, my point is better separation of concerns)
@@ -273,6 +279,12 @@ class CTestNetParams : public CChainParams { | |||
base58Prefixes[EXT_PUBLIC_KEY] = {0x04, 0x35, 0x87, 0xCF}; | |||
base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94}; | |||
|
|||
base58EncodedPrefixes[PUBKEY_ADDRESS] = {"m","n"}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, please format the modified code according to the specified formatter
base58EncodedPrefixes[SECRET_KEY] = {"9","c"}; | ||
base58EncodedPrefixes[EXT_PUBLIC_KEY] = {"tpub"}; | ||
base58EncodedPrefixes[EXT_SECRET_KEY] = {"tpriv"}; | ||
|
||
bech32_hrp = "tb"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as mentioned by @Sjors, I think we may want to leave room for Silent Payment prefixes as well: 9219e4f
(#28201)
Probably nothing to do yet, just good to know about it - cc: @josibake
@@ -41,3 +41,20 @@ std::optional<ChainType> ChainTypeFromString(std::string_view chain) | |||
return std::nullopt; | |||
} | |||
} | |||
|
|||
std::string ChainTypeToDisplayString(ChainType chain) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or maybe only store the difference:
std::string ChainTypeToDisplayString(ChainType chain)
{
switch (chain) {
case ChainType::MAIN: return "Bitcoin";
case ChainType::TESTNET: return "testnet";
default: return ChainTypeToString(chain);
}
}
case ChainType::MAIN: | ||
return "Bitcoin"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it is kinda' weird
std::string ChainTypeToDisplayString(ChainType chain); | ||
|
||
std::string ChainTypeToString(ChainType chain); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The header order should ideally be the same as the implementation, unless there's a reason for it:
std::string ChainTypeToDisplayString(ChainType chain); | |
std::string ChainTypeToString(ChainType chain); | |
std::string ChainTypeToString(ChainType chain); | |
std::string ChainTypeToDisplayString(ChainType chain); |
Issue:
Simply
DecodeDestination
does not handle errors where the user inputs a valid address for the wrong network. e.x. testnet while running client on mainnetThe current error
not a valid Bech32 or Base58 encoding
for a valid address on a different network is entirely incorrect. This is because of the is_bech32 variable used at the core ofDecodeDestination
logic only checks that the prefix matches. If it doesn't it just starts running everything asDecodeBase58Check
regardless if the Bech32 String was actually valid.Proposed Solution:
Base58 Addresses with invalid prefixes will now display network and expected values.
Invalid or unsupported Base58-encoded address.
Invalid or unsupported Base58 testnet address. Expected prefix m, n, or 2
Bech32 Addresses with invalid prefixes will now display network and expected values. The current from of the error is completely incorrect when the user passes valid Bech32 for the wrong network.
Invalid or unsupported Segwit (Bech32) or Base58 encoding.
Invalid chain prefix for testnet. Expected tb got bc
Reference
#26290
Implementation :
Simply put, don't delay the attempt to decode the string as Bech32 using Bech32::Decode(str). This takes a minimal amount CPU cycles to perform and is acceptable to do since this operation is not performed often.
Once you get the decoding status of the string as bech32, do the same with DecodeBase58 using a len of 100 and DecodeBase58Check. This gives you enough information to start properly decoding errors.
After you have decoded the address in various formats run though the logic and display errors for invalid addresses with the network names and expected values when the user just misses the prefix.
Update 1: #27260 (comment)
Other Notes