address: reject v2-v16 segwit addresses encoded with bech32
What changed, and why it matters
This commit fixes a bug in how btcd decodes certain Bitcoin SegWit addresses. Under the BIP-350 standard, newer witness versions (versions 2 through 16) must use a newer checksum called bech32m, but btcd was only enforcing that rule for version 1. As a result, it wrongly accepted v2-v16 addresses encoded with the older bech32 format. The patch now rejects those invalid addresses and adds test cases to prevent the bug from returning. This is a standards-compliance fix; it does not by itself steal funds, but accepting non-standard addresses could lead to wallet or payment-processing mistakes.
Treat as a low-to-moderate correctness and standards-compliance fix. Merge the patch and run the new regression tests. Review any downstream code that may have relied on the previous permissive decoding behavior, especially wallets or APIs that accepted v2-v16 bech32 addresses, and consider whether previously accepted invalid addresses need to be re-validated or rejected.
Security signals we found
BIP-350 standards non-compliance in SegWit address decoding
Acceptance of bech32-encoded v2-v16 witness programs that should require bech32m
Reference test vectors added as regression tests
Defensive hardening of address parsing
Evidence from the diff
The change is in address/address.go’s decodeSegWitAddress. Previously the function checked: if version==0 require bech32.Version0; if version==1 require bech32.VersionM. It did not check versions 2-16, so a v2-v16 witness program encoded with bech32 (Version0) decoded successfully, contrary to BIP-350 and the reference decoder. The patch generalizes the version>=1 branch to require bech32.VersionM for all non-zero witness versions. A new regression test file, address/bip350_diff_test.go, adds BIP-350 INVALID_ADDRESS vectors for v2 and v16 encoded with bech32, plus related valid/invalid vectors, confirming they are now rejected.
Changed components
btcd address packageaddress/address.go decodeSegWitAddress functionSegWit/bech32/bech32m address decodingInspect captured patch +68 / −5
diff --git a/address/address.go b/address/address.go
index aa24752..5f25a84 100644
--- a/address/address.go
+++ b/address/address.go
@@ -275,16 +275,19 @@ func decodeSegWitAddress(address string) (byte, []byte, error) {
"version 0: %v", len(regrouped))
}
- // For witness version 0, the bech32 encoding must be used.
+ // Per BIP-350, witness version 0 must use the bech32 encoding, while
+ // witness versions 1 through 16 must use the bech32m encoding. Previously
+ // only versions 0 and 1 were checked, which let a v2-v16 program encoded
+ // with the legacy bech32 checksum decode successfully in violation of the
+ // spec.
if version == 0 && bech32version != bech32.Version0 {
return 0, nil, fmt.Errorf("invalid checksum expected bech32 " +
"encoding for address with witness version 0")
}
- // For witness version 1, the bech32m encoding must be used.
- if version == 1 && bech32version != bech32.VersionM {
- return 0, nil, fmt.Errorf("invalid checksum expected bech32m " +
- "encoding for address with witness version 1")
+ if version >= 1 && bech32version != bech32.VersionM {
+ return 0, nil, fmt.Errorf("invalid checksum expected bech32m "+
+ "encoding for address with witness version %d", version)
}
return version, regrouped, nil
diff --git a/address/bip350_diff_test.go b/address/bip350_diff_test.go
new file mode 100644
index 0000000..e8da621
--- /dev/null
+++ b/address/bip350_diff_test.go
@@ -0,0 +1,60 @@
+package address
+
+import (
+ "testing"
+)
+
+// TestSegWitAddressBIP350Vectors runs a subset of the official BIP-173/BIP-350
+// reference segwit address vectors against decodeSegWitAddress, which implements
+// the reference decode() function from those BIPs. It specifically guards the
+// rule that witness versions 1 through 16 MUST use the bech32m encoding (BIP-350
+// line "Addresses for segregated witness outputs version 1 through 16 use
+// Bech32m"), not just version 1.
+func TestSegWitAddressBIP350Vectors(t *testing.T) {
+ // Valid Bech32m vectors (BIP-350). These MUST decode without error. The HRP
+ // is validated at a higher layer; here we only validate segwit-encoding
+ // rules, so the HRP value is irrelevant.
+ valid := []string{
+ "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y",
+ "BC1SW50QGDZ25J",
+ "bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs",
+ "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0",
+ // Valid Bech32 v0 vectors.
+ "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4",
+ "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
+ }
+ for _, addr := range valid {
+ if _, _, err := decodeSegWitAddress(addr); err != nil {
+ t.Errorf("BIP350 valid vector rejected: %q -> %v", addr, err)
+ }
+ }
+
+ // Invalid vectors from the BIP-350 INVALID_ADDRESS list whose defect lies in
+ // the segwit-encoding rules (not merely the HRP). Each MUST be rejected.
+ invalid := []struct {
+ name string
+ addr string
+ }{
+ // Witness version 1, Bech32 instead of Bech32m.
+ {"v1-bech32-not-bech32m", "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqh2y7hd"},
+ // Witness version 2, Bech32 instead of Bech32m. This is the vector that
+ // previously decoded successfully because only v0/v1 were checked.
+ {"v2-bech32-not-bech32m", "tb1z0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqglt7rf"},
+ // Witness version 16, Bech32 instead of Bech32m.
+ {"v16-bech32-not-bech32m", "BC1S0XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ54WELL"},
+ // Witness version 0, Bech32m instead of Bech32.
+ {"v0-bech32m-not-bech32", "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kemeawh"},
+ // Invalid program length (1 byte).
+ {"program-length-1", "bc1pw5dgrnzv"},
+ // Invalid program length (41 bytes).
+ {"program-length-41", "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v8n0nx0muaewav253zgeav"},
+ // Invalid program length for witness version 0 (per BIP-141).
+ {"v0-program-length-16", "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P"},
+ }
+ for _, tc := range invalid {
+ if _, _, err := decodeSegWitAddress(tc.addr); err == nil {
+ t.Errorf("BIP350 invalid vector %s accepted: %q (expected error)",
+ tc.name, tc.addr)
+ }
+ }
+}
Why this scored 52/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.