address: reject v2-v16 segwit addresses encoded with bech32

BIP-350 requires that segregated witness outputs of version 1 through 16
use the bech32m checksum, while only version 0 uses bech32. decodeSegWitAddress
only special-cased versions 0 and 1, so a witness program with version 2-16
encoded using the legacy bech32 checksum decoded successfully, in violation of
the spec and contrary to the BIP-350 reference decoder (which rejects any
non-zero witness version that is not bech32m).

Generalize the version 1 check to cover all versions >= 1, matching the
reference decode() function. Add the relevant BIP-350 INVALID_ADDRESS vectors
(v2 and v16 encoded with bech32) as a regression test; they decoded
successfully before this change and are now rejected.

Signed-off-by: Lrifton92 <Lrifton92@users.noreply.github.com>
This commit is contained in:
Lrifton92 2026-06-11 16:56:09 +02:00
parent e454fa7762
commit 27bbee94a5
2 changed files with 68 additions and 5 deletions

View file

@ -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

View file

@ -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)
}
}
}