Skip to content

Enhanced error messages for invalid network prefix during address parsing.#27260

Draft
portlandhodl wants to merge 3 commits intobitcoin:masterfrom
portlandhodl:26290
Draft

Enhanced error messages for invalid network prefix during address parsing.#27260
portlandhodl wants to merge 3 commits intobitcoin:masterfrom
portlandhodl:26290

Conversation

@portlandhodl
Copy link

@portlandhodl portlandhodl commented Mar 15, 2023

Improve Address Decoding Error Messages

Issue

DecodeDestination does 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_bech32 variable 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

  1. 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.

  2. Gather decoding information: After Bech32 decoding, we also attempt DecodeBase58 (with a length of 100) and DecodeBase58Check. This provides enough information to properly diagnose errors.

  3. 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)

  • Decode Bech32 upfront using bech32::Decode(str) instead of checking prefix first
  • Use structured bindings for cleaner code: auto [bech32_encoding, bech32_hrp, bech32_chars] = bech32::Decode(str)
  • Improved error handling flow with clearer branching

3. Improved Error Messages

Scenario Previous Error New Error
Base58 address with wrong prefix "Invalid or unsupported Base58-encoded address." "Invalid Base58 address. Expected prefix 3, or 1"
Bech32 address with wrong HRP "Invalid or unsupported Segwit (Bech32) or Base58 encoding." "Invalid or unsupported prefix for Segwit (Bech32) address (expected bc, got tb)"
Invalid Base58 checksum "Invalid or unsupported Segwit (Bech32) or Base58 encoding." "Invalid checksum or length of Base58 address (P2PKH or P2SH)"
Ambiguous invalid address "Invalid or unsupported Segwit (Bech32) or Base58 encoding." "Address is not valid Base58 or Bech32"
Bech32 with checksum error N/A "Bech32 address decoded with error: Invalid Bech32 checksum"

4. Test Updates

  • Added test/functional/data/rpc_validateaddress.json with comprehensive test vectors for both mainnet and signet
  • Updated test/functional/rpc_validateaddress.py to use data-driven testing across multiple networks
  • Updated test/functional/rpc_invalid_address_message.py with new error message expectations

Consistency Improvements

  • Removed inconsistent periods at the end of some error messages
  • Changed "Bech32 v0" to "SegWit v0" for clarity

Reference

@DrahtBot
Copy link
Contributor

DrahtBot commented Mar 15, 2023

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage & Benchmarks

For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/27260.

Reviews

See the guideline for information on the review process.

Type Reviewers
Concept ACK Sjors, rkrux, jonatack, l0rinc
Approach ACK RandyMcMillan

If your review is incorrectly listed, please copy-paste <!--meta-tag:bot-skip--> into the comment that the bot should ignore.

Conflicts

No conflicts as of last run.

LLM Linter (✨ experimental)

Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

  • DecodeBase58Check(str, data, 21) in src/key_io.cpp
  • DecodeBase58(str, data, 100) in src/key_io.cpp

2026-03-21 06:13:51

@fanquake
Copy link
Member

Please keep your changes, and "fixes" commits squashed. Looks like your changing the file perms on at least one file, is that intentional?

@portlandhodl
Copy link
Author

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.

@portlandhodl portlandhodl force-pushed the 26290 branch 7 times, most recently from c98f7cc to 911c8b6 Compare March 16, 2023 00:10
@portlandhodl portlandhodl changed the title rpc: enhanced error message for invalid network prefix during address parsing. Enhanced error messages for invalid network prefix during address parsing. Mar 16, 2023
@portlandhodl portlandhodl force-pushed the 26290 branch 6 times, most recently from 3c0fcda to aff7635 Compare March 19, 2023 08:50
@maflcko
Copy link
Member

maflcko commented May 18, 2023

Needs rebase on current master, if still relevant

@portlandhodl portlandhodl force-pushed the 26290 branch 2 times, most recently from ef46a30 to 5ff2490 Compare May 19, 2023 03:09
@portlandhodl
Copy link
Author

Needs rebase on current master, if still relevant

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 Win64 native [vs2022] which seems to be failing for the majority of PRs that are on master due to warnings.

@maflcko
Copy link
Member

maflcko commented May 19, 2023

Thanks, the reason I mentioned it was the silent merge conflict: key_io.cpp:152:48: error: ‘const class CChainParams’ has no member named ‘NetworkIDString’, which is now fixed

@portlandhodl
Copy link
Author

Thanks, the reason I mentioned it was the silent merge conflict: key_io.cpp:152:48: error: ‘const class CChainParams’ has no member named ‘NetworkIDString’, which is now fixed

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

@portlandhodl
Copy link
Author

This still stands:

The main commit fd02e76 might be easier to review if you split it up. For example one commit that improves the base58 vs bech32 detection, another commit that that improves base58 error handling and finally one for bech32.

Thank you! I will have this completed.

Copy link
Contributor

@rkrux rkrux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +48 to +49
case ChainType::MAIN:
return "Bitcoin";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirming - Is using sentence case for mainnet correct? Rest of them use lower case.

Copy link
Contributor

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@@ -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"};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"};

Copy link
Contributor

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

@rkrux
Copy link
Contributor

rkrux commented Feb 22, 2025

#31603 (review)

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.

Copy link
Member

@jonatack jonatack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK

@DrahtBot
Copy link
Contributor

🚧 At least one of the CI tasks failed.
Debug: https://github.com/bitcoin/bitcoin/runs/37667214061

Hints

Try to run the tests locally, according to the documentation. However, a CI failure may still
happen due to a number of reasons, for example:

  • Possibly due to a silent merge conflict (the changes in this pull request being
    incompatible with the current code in the target branch). If so, make sure to rebase on the latest
    commit of the target branch.

  • A sanitizer issue, which can only be found by compiling with the sanitizer and running the
    affected test.

  • An intermittent issue.

Leave a comment here, if you need help tracking down a confusing failure.

@portlandhodl
Copy link
Author

portlandhodl commented Feb 24, 2025

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,
Russeree

@DrahtBot DrahtBot mentioned this pull request Mar 3, 2025
@Sjors
Copy link
Member

Sjors commented Apr 1, 2025

I will be working on addressing some of the more recent comments in the coming day.

Is this still in the pipeline, or do you prefer if we review as-is?

@portlandhodl
Copy link
Author

I should have done all of the splitting up as well as revised the tests to match for the new address decoding conditions.

@Sjors
Copy link
Member

Sjors commented May 15, 2025

Any particular reason for marking this draft?

@portlandhodl
Copy link
Author

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.

@portlandhodl
Copy link
Author

2f2470e

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!
A lot of errors are now properly handed on a per network basis!

Thanks, and ready for review!

Copy link
Contributor

@l0rinc l0rinc left a 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.

@@ -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)')
Copy link
Contributor

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.

Comment on lines -115 to -118
# (
# "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
# "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262",
# ),
Copy link
Contributor

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment on lines +228 to +233
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
Copy link
Contributor

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)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@@ -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"};
Copy link
Contributor

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

Comment on lines +48 to +49
case ChainType::MAIN:
return "Bitcoin";
Copy link
Contributor

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

@achow101
Copy link
Member

Are you still working on this?

@portlandhodl
Copy link
Author

Actually yes! Found some time this week. Please allow for a push today!

Thanks!

@Sjors
Copy link
Member

Sjors commented Nov 11, 2025

CI fails with:

Bech32 address decoded with error: Invalid character or mixed case == Bech32(m) address decoded with error: Invalid character or mixed case

Copy link
Member

@Sjors Sjors left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +48 to +49
case ChainType::MAIN:
return "Bitcoin";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines -115 to -118
# (
# "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
# "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262",
# ),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment on lines +228 to +233
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@portlandhodl
Copy link
Author

portlandhodl commented Jan 8, 2026

My main other suggestion is to drop the first commit (and don't add the network name to error messages).

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.

@Sjors
Copy link
Member

Sjors commented Jan 8, 2026

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?

@portlandhodl
Copy link
Author

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.

@DrahtBot
Copy link
Contributor

DrahtBot commented Jan 8, 2026

🚧 At least one of the CI tasks failed.
Task macOS-cross to arm64: https://github.com/bitcoin/bitcoin/actions/runs/20810552653/job/59773530053
LLM reason (✨ experimental): Build failed due to undefined symbol MAX_BASE58_CHARS in key_io.cpp (use of undeclared MAX_BASE58_CHARS).

Hints

Try to run the tests locally, according to the documentation. However, a CI failure may still
happen due to a number of reasons, for example:

  • Possibly due to a silent merge conflict (the changes in this pull request being
    incompatible with the current code in the target branch). If so, make sure to rebase on the latest
    commit of the target branch.

  • A sanitizer issue, which can only be found by compiling with the sanitizer and running the
    affected test.

  • An intermittent issue.

Leave a comment here, if you need help tracking down a confusing failure.

@portlandhodl
Copy link
Author

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?

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.

@Sjors
Copy link
Member

Sjors commented Jan 9, 2026

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?

@portlandhodl
Copy link
Author

portlandhodl commented Jan 9, 2026

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

@l0rinc
Copy link
Contributor

l0rinc commented Jan 13, 2026

@portlandhodl, ping us when the PR is ready for review again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.