Enhanced error messages for invalid network prefix during address parsing.#27260
Enhanced error messages for invalid network prefix during address parsing.#27260portlandhodl wants to merge 3 commits intobitcoin:masterfrom
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 copy-paste ConflictsNo conflicts as of last run. LLM Linter (✨ experimental)Possible places where named args for integral literals may be used (e.g.
2026-03-21 06:13:51 |
|
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 |
Thank you! I will have this completed. |
rkrux
left a comment
There was a problem hiding this comment.
I have reviewed the first 2 commits only as I found the 3rd commit to be difficult to review. I agree with the idea of splitting the last commit into multiple commits as mentioned previously: #27260 (comment).
Overall I agree with the idea of improving the error messages when the address decoding fails but I don't think I agree with the below statement because from the POV of the network that address in indeed invalid. Ref: https://en.bitcoin.it/wiki/BIP_0173#:~:text=The%20human%2Dreadable%20part%20%22bc%22%5B7%5D%20for%20mainnet%2C%20and%20%22tb%22%5B8%5D%20for%20testnet.
The current error not a valid Bech32 or Base58 encoding for a valid address on a different network is entirely incorrect.
Concept ACK @ 70deb6737903556e2cf030d5a29106f01b43a574
src/util/chaintype.cpp
Outdated
| case ChainType::MAIN: | ||
| return "Bitcoin"; |
There was a problem hiding this comment.
Confirming - Is using sentence case for mainnet correct? Rest of them use lower case.
There was a problem hiding this comment.
In 055dbae Added chaintype user-facing string display:
This is used as follows in later commits:
Invalid Base58 %s address.
In the original code it would not specify the network:
Invalid or unsupported Base58-encoded address.
I don't think it's very useful to add the network name to address. Certainly not for mainnet, because most users have no idea (and no need to know) about test networks.
My suggestion would be drop this helper entirely.
src/kernel/chainparams.cpp
Outdated
| @@ -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.
Super nit: Personal preference as I find it congruent with the English language and it's slightly easier on the eyes. Same for all these other newly added constants.
- base58EncodedPrefixes[PUBKEY_ADDRESS] = {"m","n"};
+ base58EncodedPrefixes[PUBKEY_ADDRESS] = {"m", "n"};There was a problem hiding this comment.
Yes, please format the modified code according to the specified formatter
|
Not exactly what this PR is fixing but I unintentionally ended up trying network inconsistent keys in my node and faced a not so detailed error, which took some debugging to figure out. So I'm definitely in favour of better error parsing like this PR intends to do. |
|
🚧 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. |
|
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! |
l0rinc
left a comment
There was a problem hiding this comment.
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.
| @@ -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.
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.
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?
There was a problem hiding this comment.
In f8513e38b369889df0b402f06d67fcf656495d53 [Functional] Introduce muti network testing:
These were disabled in #27727, IIUC in favour the official test vectors. Seems fine to delete them. If they're actually interesting, then they can be restored and added to the BIP.
(nit: typo in commit message)
| 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.
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)There was a problem hiding this comment.
In f8513e38b369889df0b402f06d67fcf656495d53 [Functional] Introduce muti network testing:
The elegant solution imo is to move these test vectors into a JSON file, see e.g. rpc_getblockstats.py.
But I think the current approach is fine. You could also duplicate the body of test_validateaddress_on_network into test_validateaddress_mainnet and test_validateaddress_signet rather than copying the constants. Each for loop could then be preceded with self.log.debug("Valid mainnet addresses"), etc.
src/kernel/chainparams.cpp
Outdated
| @@ -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.
Yes, please format the modified code according to the specified formatter
src/util/chaintype.cpp
Outdated
| case ChainType::MAIN: | ||
| return "Bitcoin"; |
|
Are you still working on this? |
|
Actually yes! Found some time this week. Please allow for a push today! Thanks! |
|
CI fails with: |
There was a problem hiding this comment.
Thanks for splitting base58 and bech32 stuff into separate commits. I think that 04d5d4a Base58 decoding logic + bech32 decoding network awareness could itself be split, see inline comment from @l0rinc, since it still contains a lot of bech32 changes despite the commit name.
My main other suggestion is to drop the first commit (and don't add the network name to error messages).
See inline comment for how to fix the test.
src/util/chaintype.cpp
Outdated
| case ChainType::MAIN: | ||
| return "Bitcoin"; |
There was a problem hiding this comment.
In 055dbae Added chaintype user-facing string display:
This is used as follows in later commits:
Invalid Base58 %s address.
In the original code it would not specify the network:
Invalid or unsupported Base58-encoded address.
I don't think it's very useful to add the network name to address. Certainly not for mainnet, because most users have no idea (and no need to know) about test networks.
My suggestion would be drop this helper entirely.
| # ( | ||
| # "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", | ||
| # "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", | ||
| # ), |
There was a problem hiding this comment.
In f8513e38b369889df0b402f06d67fcf656495d53 [Functional] Introduce muti network testing:
These were disabled in #27727, IIUC in favour the official test vectors. Seems fine to delete them. If they're actually interesting, then they can be restored and added to the BIP.
(nit: typo in commit message)
| 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.
In f8513e38b369889df0b402f06d67fcf656495d53 [Functional] Introduce muti network testing:
The elegant solution imo is to move these test vectors into a JSON file, see e.g. rpc_getblockstats.py.
But I think the current approach is fine. You could also duplicate the body of test_validateaddress_on_network into test_validateaddress_mainnet and test_validateaddress_signet rather than copying the constants. Each for loop could then be preceded with self.log.debug("Valid mainnet addresses"), etc.
src/key_io.cpp
Outdated
| auto check_base58 = [&]() { return DecodeBase58Check(str, data, MAX_BASE58_CHECK_CHARS); }; | ||
|
|
||
|
|
||
| if (!is_bech32 && check_base58()) { |
There was a problem hiding this comment.
In 04d5d4a Base58 decoding logic + bech32 decoding network awareness: splitting bech32 and base58 handling in an earlier commit might also help to focus the current commit more on base58. I still a lot of lines touching bech32 code.
Okay I, set aside some time to work on this PR tonight. If there is no reason to add the network name to the error messages then this whole PR is kind of a moot point and should be closed. #26290 I am completely okay with this if that is the case. Edit: I see what you mean remove the network name and only display the prefixes. |
|
Quick note as of 6bcaa46. IIUC you rebased, and then dropped 055dbae Added chaintype user-facing string display, adjusting things where needed. But I guess you're still working on potentially refactoring 870c850 Base58 decoding logic + bech32 decoding network awareness and 2df89b6 Bech32 decoding logic based on earlier reviews? |
Absolutely, this is the correct understanding of this, will re-request and pull out of draft when ready. I may respond to a few of the critiques, during this refactor. Thanks again for the review. |
|
🚧 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. |
ef5e517 I am using this as a setup commit for the more logic like sections that are handled in key_io.cpp One of the hardest portions seems to be is that at some point this all converges into a case where you have to test that you don't have valid base58 or valid bech32 which inherently will have to run checks for both. |
|
The "test max 6 ancestor commits" job fails at eacf78c General address decoding logic exe order. This should fix it, by moving later changes into that commit: Sjors@aabb7df Some of the commit messages have weird trailing characters (adasda, etc). Noticed there's still a lot of bech32 related changes in c2c8454 Base58 decoding logic: despite the commit title, maybe split it further? |
Thank you for the fixup! Applied Edit- Per the messages, it was getting late and I was flogging the rebase -i and made a mistake |
|
@portlandhodl, ping us when the PR is ready for review again. |
Improve Address Decoding Error Messages
Issue
DecodeDestinationdoes not properly handle errors when a user inputs a valid address for the wrong network (e.g., a testnet address while running the client on mainnet).The previous error messages were misleading. For example, "Invalid or unsupported Segwit (Bech32) or Base58 encoding" was displayed for a valid Bech32 address on a different network. This occurred because the
is_bech32variable only checked if the prefix matched the current network's HRP. If it didn't match, the code would fall through to Base58 decoding logic regardless of whether the string was actually a valid Bech32 address.Solution
Implementation Approach
Decode first, then validate: Instead of checking the prefix before decoding, we now decode the string using
bech32::Decode(str)upfront. This takes minimal CPU cycles and is acceptable since address validation is not a frequent operation.Gather decoding information: After Bech32 decoding, we also attempt
DecodeBase58(with a length of 100) andDecodeBase58Check. This provides enough information to properly diagnose errors.Provide network-aware error messages: When an address has an invalid prefix, the error message now includes the expected network values.
Changes Made
1. Add Base58 Encoded Prefixes to ChainParams (
src/kernel/chainparams.cpp,src/kernel/chainparams.h)2. Refactor Address Decoding Logic (
src/key_io.cpp)bech32::Decode(str)instead of checking prefix firstauto [bech32_encoding, bech32_hrp, bech32_chars] = bech32::Decode(str)3. Improved Error Messages
4. Test Updates
test/functional/data/rpc_validateaddress.jsonwith comprehensive test vectors for both mainnet and signettest/functional/rpc_validateaddress.pyto use data-driven testing across multiple networkstest/functional/rpc_invalid_address_message.pywith new error message expectationsConsistency Improvements
Reference