diff --git a/bitcoin/segwit_addr.py b/bitcoin/segwit_addr.py index c762fdd8..1bd170c4 100644 --- a/bitcoin/segwit_addr.py +++ b/bitcoin/segwit_addr.py @@ -40,40 +40,45 @@ def bech32_hrp_expand(hrp): return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] +BECH32_CONST = 1 +BECH32M_CONST = 0x2bc830a3 + + def bech32_verify_checksum(hrp, data): """Verify a checksum given HRP and converted data characters.""" - return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1 + return bech32_polymod(bech32_hrp_expand(hrp) + data) == BECH32_CONST -def bech32_create_checksum(hrp, data): +def bech32_create_checksum(hrp, data, spec=BECH32_CONST): """Compute the checksum values given HRP and data.""" values = bech32_hrp_expand(hrp) + data - polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1 + polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ spec return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] -def bech32_encode(hrp, data): - """Compute a Bech32 string given HRP and data values.""" - combined = data + bech32_create_checksum(hrp, data) +def bech32_encode(hrp, data, spec=BECH32_CONST): + """Compute a Bech32 or Bech32m string given HRP and data values.""" + combined = data + bech32_create_checksum(hrp, data, spec) return hrp + '1' + ''.join([CHARSET[d] for d in combined]) def bech32_decode(bech): - """Validate a Bech32 string, and determine HRP and data.""" + """Validate a Bech32 or Bech32m string, and determine HRP, data, and spec.""" if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (bech.lower() != bech and bech.upper() != bech)): - return (None, None) + return (None, None, None) bech = bech.lower() pos = bech.rfind('1') if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: - return (None, None) + return (None, None, None) if not all(x in CHARSET for x in bech[pos+1:]): - return (None, None) + return (None, None, None) hrp = bech[:pos] data = [CHARSET.find(x) for x in bech[pos+1:]] - if not bech32_verify_checksum(hrp, data): - return (None, None) - return (hrp, data[:-6]) + spec = bech32_polymod(bech32_hrp_expand(hrp) + data) + if spec not in (BECH32_CONST, BECH32M_CONST): + return (None, None, None) + return (hrp, data[:-6], spec) def convertbits(data, frombits, tobits, pad=True): @@ -101,7 +106,7 @@ def convertbits(data, frombits, tobits, pad=True): def decode(hrp, addr): """Decode a segwit address.""" - hrpgot, data = bech32_decode(addr) + hrpgot, data, spec = bech32_decode(addr) if hrpgot != hrp: return (None, None) decoded = convertbits(data[1:], 5, 8, False) @@ -111,12 +116,18 @@ def decode(hrp, addr): return (None, None) if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: return (None, None) + # BIP350: v0 is Bech32, v1 and later are Bech32m. + if data[0] == 0 and spec != BECH32_CONST: + return (None, None) + if data[0] != 0 and spec != BECH32M_CONST: + return (None, None) return (data[0], decoded) def encode(hrp, witver, witprog): """Encode a segwit address.""" - ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5)) + spec = BECH32_CONST if witver == 0 else BECH32M_CONST + ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5), spec) if decode(hrp, ret) == (None, None): return None return ret diff --git a/bitcoin/tests/data/bech32_encode_decode.json b/bitcoin/tests/data/bech32_encode_decode.json index d8b2b0eb..c16d46cb 100644 --- a/bitcoin/tests/data/bech32_encode_decode.json +++ b/bitcoin/tests/data/bech32_encode_decode.json @@ -1,8 +1,8 @@ [ ["0014751e76e8199196d454941c45d1b3a323f1433bd6", "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4"], ["00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7"], -["5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6", "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx"], -["6002751e", "BC1SW50QA3JX3S"], -["5210751e76e8199196d454941c45d1b3a323", "bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj"], +["5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6", "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y"], +["6002751e", "BC1SW50QGDZ25J"], +["5210751e76e8199196d454941c45d1b3a323", "bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs"], ["0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy"] ] diff --git a/bitcoin/tests/data/bech32_invalid.json b/bitcoin/tests/data/bech32_invalid.json index aeb898b0..53b0a5df 100644 --- a/bitcoin/tests/data/bech32_invalid.json +++ b/bitcoin/tests/data/bech32_invalid.json @@ -8,5 +8,8 @@ ["tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", "Mixed case"], ["bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du", "zero padding of more than 4 bits"], ["tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", "Non-zero padding in 8-to-5 conversion"], - ["bc1gmk9yu", "Empty data section"] + ["bc1gmk9yu", "Empty data section"], +["bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", "Bech32 checksum on witness version 1"], +["BC1SW50QA3JX3S", "Bech32 checksum on witness version 16"], +["bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", "Bech32 checksum on witness version 2"] ] \ No newline at end of file diff --git a/bitcoin/tests/test_bech32.py b/bitcoin/tests/test_bech32.py index 06da1244..f39cd91e 100644 --- a/bitcoin/tests/test_bech32.py +++ b/bitcoin/tests/test_bech32.py @@ -17,7 +17,7 @@ from bitcoin.core.script import CScript, OP_0, OP_1, OP_16 from bitcoin.bech32 import * -from bitcoin.segwit_addr import encode, decode +from bitcoin.segwit_addr import encode, decode, bech32_encode, convertbits def load_test_vectors(name): @@ -51,6 +51,15 @@ def test_encode_decode(self): self.assertEqual(act_bech32.lower(), exp_bech32.lower()) self.assertEqual(to_scriptPubKey(*act_bin), bytes(exp_bin)) + def test_v1_rejects_bech32_checksum(self): + prog = list(range(32)) + legacy = bech32_encode("bc", [1] + convertbits(prog, 8, 5)) + self.assertEqual(decode("bc", legacy), (None, None)) + modern = encode("bc", 1, prog) + witver, decoded = decode("bc", modern) + self.assertEqual(witver, 1) + self.assertEqual(decoded, prog) + class Test_CBech32Data(unittest.TestCase): def test_from_data(self): b = CBech32Data.from_bytes(0, unhexlify('751e76e8199196d454941c45d1b3a323f1433bd6'))