Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 26 additions & 15 deletions bitcoin/segwit_addr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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
6 changes: 3 additions & 3 deletions bitcoin/tests/data/bech32_encode_decode.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[
["0014751e76e8199196d454941c45d1b3a323f1433bd6", "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4"],
["00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7"],
["5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6", "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx"],
["6002751e", "BC1SW50QA3JX3S"],
["5210751e76e8199196d454941c45d1b3a323", "bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj"],
["5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6", "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y"],
["6002751e", "BC1SW50QGDZ25J"],
["5210751e76e8199196d454941c45d1b3a323", "bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs"],
["0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy"]
]
5 changes: 4 additions & 1 deletion bitcoin/tests/data/bech32_invalid.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
]
11 changes: 10 additions & 1 deletion bitcoin/tests/test_bech32.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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'))
Expand Down