Skip to content

Commit ea765ab

Browse files
committed
Merge #189: Bech32 address support
1547251 Bitcoin's default Regtest bech32 HRP is 'bcrt' not 'tb' (John Villar) e8d2037 Add tests (genme) 542ac35 Add python2 support to CBech32BitcoinAddress (genme) e872989 add str() tests for P2WPKHBitcoinAddress and P2WSHBitcoinAddress (Dmitry Petukhov) 7c2b4e1 add Bech32-encoded address support (Dmitry Petukhov) 737380e Add python2 support to bech32 (Martino Salvetti) 01083b7 Add BECH32_HRP in network parameters (Martino Salvetti) dfaa0c2 Add Bech32 encoding/decoding; add CBech32Data (Martino Salvetti) Pull request description: Add Bech32-encoded address support This is based on #154 Introduce CBase58Address and CBech32Address make CBitcoinAddress a wrapper that returns CBase58Address or CBech32Address depending on input data introduce to_redeemScript() method, because for P2WPKHBitcoinAddress redeem script is not the same as scriptPubKey. unfortunately, for P2WSHBitcoinAddress to_redeemScript() cannot be implemented, as we need all pubkeys for that, and not only the hash. Top commit has no ACKs. Tree-SHA512: c2ea9a690c5f1a3d4f6f5d043396e8265adcfa0dc582daafb122a3f56e7eda2656051a07c294ea377034a221d83cb7864bc57265be0d02903f002412d86e384c
2 parents ecf07cb + 37330fe commit ea765ab

File tree

9 files changed

+504
-18
lines changed

9 files changed

+504
-18
lines changed

bitcoin/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class MainParams(bitcoin.core.CoreMainParams):
3131
BASE58_PREFIXES = {'PUBKEY_ADDR':0,
3232
'SCRIPT_ADDR':5,
3333
'SECRET_KEY' :128}
34+
BECH32_HRP = 'bc'
3435

3536
class TestNetParams(bitcoin.core.CoreTestNetParams):
3637
MESSAGE_START = b'\x0b\x11\x09\x07'
@@ -43,6 +44,7 @@ class TestNetParams(bitcoin.core.CoreTestNetParams):
4344
BASE58_PREFIXES = {'PUBKEY_ADDR':111,
4445
'SCRIPT_ADDR':196,
4546
'SECRET_KEY' :239}
47+
BECH32_HRP = 'tb'
4648

4749
class RegTestParams(bitcoin.core.CoreRegTestParams):
4850
MESSAGE_START = b'\xfa\xbf\xb5\xda'
@@ -52,6 +54,7 @@ class RegTestParams(bitcoin.core.CoreRegTestParams):
5254
BASE58_PREFIXES = {'PUBKEY_ADDR':111,
5355
'SCRIPT_ADDR':196,
5456
'SECRET_KEY' :239}
57+
BECH32_HRP = 'bcrt'
5558

5659
"""Master global setting for what chain params we're using.
5760

bitcoin/bech32.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Copyright (C) 2017 The python-bitcoinlib developers
2+
#
3+
# This file is part of python-bitcoinlib.
4+
#
5+
# It is subject to the license terms in the LICENSE file found in the top-level
6+
# directory of this distribution.
7+
#
8+
# No part of python-bitcoinlib, including this file, may be copied, modified,
9+
# propagated, or distributed except according to the terms contained in the
10+
# LICENSE file.
11+
12+
"""Bech32 encoding and decoding"""
13+
14+
import sys
15+
_bchr = chr
16+
_bord = ord
17+
if sys.version > '3':
18+
long = int
19+
_bchr = lambda x: bytes([x])
20+
_bord = lambda x: x
21+
22+
from bitcoin.segwit_addr import encode, decode
23+
import bitcoin
24+
25+
class Bech32Error(Exception):
26+
pass
27+
28+
class Bech32ChecksumError(Bech32Error):
29+
pass
30+
31+
class CBech32Data(bytes):
32+
"""Bech32-encoded data
33+
34+
Includes a witver and checksum.
35+
"""
36+
def __new__(cls, s):
37+
"""from bech32 addr to """
38+
witver, data = decode(bitcoin.params.BECH32_HRP, s)
39+
if witver is None and data is None:
40+
raise Bech32Error('Bech32 decoding error')
41+
42+
return cls.from_bytes(witver, data)
43+
44+
def __init__(self, s):
45+
"""Initialize from bech32-encoded string
46+
47+
Note: subclasses put your initialization routines here, but ignore the
48+
argument - that's handled by __new__(), and .from_bytes() will call
49+
__init__() with None in place of the string.
50+
"""
51+
52+
@classmethod
53+
def from_bytes(cls, witver, witprog):
54+
"""Instantiate from witver and data"""
55+
if not (0 <= witver <= 16):
56+
raise ValueError('witver must be in range 0 to 16 inclusive; got %d' % witver)
57+
self = bytes.__new__(cls, witprog)
58+
self.witver = witver
59+
60+
return self
61+
62+
def to_bytes(self):
63+
"""Convert to bytes instance
64+
65+
Note that it's the data represented that is converted; the checkum and
66+
witver is not included.
67+
"""
68+
return b'' + self
69+
70+
def __str__(self):
71+
"""Convert to string"""
72+
return encode(bitcoin.params.BECH32_HRP, self.witver, self)
73+
74+
def __repr__(self):
75+
return '%s(%r)' % (self.__class__.__name__, str(self))
76+
77+
__all__ = (
78+
'Bech32Error',
79+
'Bech32ChecksumError',
80+
'CBech32Data',
81+
)

bitcoin/segwit_addr.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Copyright (c) 2017 Pieter Wuille
2+
#
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy
4+
# of this software and associated documentation files (the "Software"), to deal
5+
# in the Software without restriction, including without limitation the rights
6+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
# copies of the Software, and to permit persons to whom the Software is
8+
# furnished to do so, subject to the following conditions:
9+
#
10+
# The above copyright notice and this permission notice shall be included in
11+
# all copies or substantial portions of the Software.
12+
#
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
# THE SOFTWARE.
20+
21+
"""Reference implementation for Bech32 and segwit addresses."""
22+
23+
import sys
24+
_bchr = chr
25+
_bord = lambda x: ord(x) if isinstance(x, str) else x
26+
if sys.version > '3':
27+
long = int
28+
_bchr = lambda x: bytes([x])
29+
_bord = lambda x: x
30+
31+
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
32+
33+
34+
def bech32_polymod(values):
35+
"""Internal function that computes the Bech32 checksum."""
36+
generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
37+
chk = 1
38+
for value in values:
39+
top = chk >> 25
40+
chk = (chk & 0x1ffffff) << 5 ^ value
41+
for i in range(5):
42+
chk ^= generator[i] if ((top >> i) & 1) else 0
43+
return chk
44+
45+
46+
def bech32_hrp_expand(hrp):
47+
"""Expand the HRP into values for checksum computation."""
48+
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
49+
50+
51+
def bech32_verify_checksum(hrp, data):
52+
"""Verify a checksum given HRP and converted data characters."""
53+
return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1
54+
55+
56+
def bech32_create_checksum(hrp, data):
57+
"""Compute the checksum values given HRP and data."""
58+
values = bech32_hrp_expand(hrp) + data
59+
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1
60+
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
61+
62+
63+
def bech32_encode(hrp, data):
64+
"""Compute a Bech32 string given HRP and data values."""
65+
combined = data + bech32_create_checksum(hrp, data)
66+
return hrp + '1' + ''.join([CHARSET[d] for d in combined])
67+
68+
69+
def bech32_decode(bech):
70+
"""Validate a Bech32 string, and determine HRP and data."""
71+
if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
72+
(bech.lower() != bech and bech.upper() != bech)):
73+
return (None, None)
74+
bech = bech.lower()
75+
pos = bech.rfind('1')
76+
if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
77+
return (None, None)
78+
if not all(x in CHARSET for x in bech[pos+1:]):
79+
return (None, None)
80+
hrp = bech[:pos]
81+
data = [CHARSET.find(x) for x in bech[pos+1:]]
82+
if not bech32_verify_checksum(hrp, data):
83+
return (None, None)
84+
return (hrp, data[:-6])
85+
86+
87+
def convertbits(data, frombits, tobits, pad=True):
88+
"""General power-of-2 base conversion."""
89+
acc = 0
90+
bits = 0
91+
ret = []
92+
maxv = (1 << tobits) - 1
93+
max_acc = (1 << (frombits + tobits - 1)) - 1
94+
for value in data:
95+
value = _bord(value)
96+
if value < 0 or (value >> frombits):
97+
return None
98+
acc = ((acc << frombits) | value) & max_acc
99+
bits += frombits
100+
while bits >= tobits:
101+
bits -= tobits
102+
ret.append((acc >> bits) & maxv)
103+
if pad:
104+
if bits:
105+
ret.append((acc << (tobits - bits)) & maxv)
106+
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
107+
return None
108+
return ret
109+
110+
111+
def decode(hrp, addr):
112+
"""Decode a segwit address."""
113+
hrpgot, data = bech32_decode(addr)
114+
if hrpgot != hrp:
115+
return (None, None)
116+
decoded = convertbits(data[1:], 5, 8, False)
117+
if decoded is None or len(decoded) < 2 or len(decoded) > 40:
118+
return (None, None)
119+
if data[0] > 16:
120+
return (None, None)
121+
if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:
122+
return (None, None)
123+
return (data[0], decoded)
124+
125+
126+
def encode(hrp, witver, witprog):
127+
"""Encode a segwit address."""
128+
ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5))
129+
if decode(hrp, ret) == (None, None):
130+
return None
131+
return ret
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[
2+
["0014751e76e8199196d454941c45d1b3a323f1433bd6", "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4"],
3+
["00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7"],
4+
["5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6", "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx"],
5+
["6002751e", "BC1SW50QA3JX3S"],
6+
["5210751e76e8199196d454941c45d1b3a323", "bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj"],
7+
["0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy"]
8+
]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
["tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", "Invalid human-readable part"],
3+
["bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", "Invalid checksum"],
4+
["BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", "Invalid witness version"],
5+
["bc1rw5uspcuh", "Invalid program length"],
6+
["bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", "Invalid program length"],
7+
["BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", "Invalid program length for witness version 0 (per BIP141)"],
8+
["tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", "Mixed case"],
9+
["bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du", "zero padding of more than 4 bits"],
10+
["tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", "Non-zero padding in 8-to-5 conversion"],
11+
["bc1gmk9yu", "Empty data section"]
12+
]

bitcoin/tests/test_bech32.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Copyright (C) 2013-2014 The python-bitcoinlib developers
2+
#
3+
# This file is part of python-bitcoinlib.
4+
#
5+
# It is subject to the license terms in the LICENSE file found in the top-level
6+
# directory of this distribution.
7+
#
8+
# No part of python-bitcoinlib, including this file, may be copied, modified,
9+
# propagated, or distributed except according to the terms contained in the
10+
# LICENSE file.
11+
12+
from __future__ import absolute_import, division, print_function, unicode_literals
13+
14+
import json
15+
import os
16+
import unittest
17+
import array
18+
import sys
19+
_bchr = chr
20+
_bord = ord
21+
_tobytes = lambda x: array.array('B', x).tostring()
22+
if sys.version > '3':
23+
long = int
24+
_bchr = lambda x: bytes([x])
25+
_bord = lambda x: x
26+
_tobytes = bytes
27+
28+
from binascii import unhexlify
29+
30+
from bitcoin.core.script import CScript, OP_0, OP_1, OP_16
31+
from bitcoin.bech32 import *
32+
from bitcoin.segwit_addr import encode, decode
33+
34+
35+
def load_test_vectors(name):
36+
with open(os.path.dirname(__file__) + '/data/' + name, 'r') as fd:
37+
for testcase in json.load(fd):
38+
yield testcase
39+
40+
def to_scriptPubKey(witver, witprog):
41+
"""Decoded bech32 address to script"""
42+
return CScript([witver]) + CScript(_tobytes(witprog))
43+
44+
class Test_bech32(unittest.TestCase):
45+
46+
def op_decode(self, witver):
47+
"""OP encoding to int"""
48+
if witver == OP_0:
49+
return 0
50+
if OP_1 <= witver <= OP_16:
51+
return witver - OP_1 + 1
52+
self.fail('Wrong witver: %d' % witver)
53+
54+
def test_encode_decode(self):
55+
for exp_bin, exp_bech32 in load_test_vectors('bech32_encode_decode.json'):
56+
exp_bin = [_bord(y) for y in unhexlify(exp_bin.encode('utf8'))]
57+
witver = self.op_decode(exp_bin[0])
58+
hrp = exp_bech32[:exp_bech32.rindex('1')].lower()
59+
self.assertEqual(exp_bin[1], len(exp_bin[2:]))
60+
act_bech32 = encode(hrp, witver, exp_bin[2:])
61+
act_bin = decode(hrp, exp_bech32)
62+
63+
self.assertEqual(act_bech32.lower(), exp_bech32.lower())
64+
self.assertEqual(to_scriptPubKey(*act_bin), _tobytes(exp_bin))
65+
66+
class Test_CBech32Data(unittest.TestCase):
67+
def test_from_data(self):
68+
b = CBech32Data.from_bytes(0, unhexlify('751e76e8199196d454941c45d1b3a323f1433bd6'))
69+
self.assertEqual(b.witver, 0)
70+
self.assertEqual(str(b).upper(), 'BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4')
71+
72+
def test_invalid_bech32_exception(self):
73+
74+
for invalid, _ in load_test_vectors("bech32_invalid.json"):
75+
msg = '%r should have raised Bech32Error but did not' % invalid
76+
with self.assertRaises(Bech32Error, msg=msg):
77+
CBech32Data(invalid)

bitcoin/tests/test_segwit.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ def test_p2wpkh_signaturehash(self):
3636
scriptpubkey = CScript(x('00141d0f172a0ecb48aee1be1f2687d2963ae33f71a1'))
3737
value = int(6*COIN)
3838

39-
address = CBitcoinAddress.from_scriptPubKey(scriptpubkey)
40-
self.assertEqual(SignatureHash(address.to_scriptPubKey(), CTransaction.deserialize(unsigned_tx),
39+
address = CBech32BitcoinAddress.from_scriptPubKey(scriptpubkey)
40+
self.assertEqual(SignatureHash(address.to_redeemScript(), CTransaction.deserialize(unsigned_tx),
4141
1, SIGHASH_ALL, value, SIGVERSION_WITNESS_V0),
4242
x('c37af31116d1b27caf68aae9e3ac82f1477929014d5b917657d0eb49478cb670'))
4343

@@ -47,8 +47,8 @@ def test_p2sh_p2wpkh_signaturehash(self):
4747
redeemscript = CScript(x('001479091972186c449eb1ded22b78e40d009bdf0089'))
4848
value = int(10*COIN)
4949

50-
address = CBitcoinAddress.from_scriptPubKey(redeemscript)
51-
self.assertEqual(SignatureHash(address.to_scriptPubKey(), CTransaction.deserialize(unsigned_tx),
50+
address = CBase58BitcoinAddress.from_scriptPubKey(redeemscript)
51+
self.assertEqual(SignatureHash(address.to_redeemScript(), CTransaction.deserialize(unsigned_tx),
5252
0, SIGHASH_ALL, value, SIGVERSION_WITNESS_V0),
5353
x('64f3b0f4dd2bb3aa1ce8566d220cc74dda9df97d8490cc81d89d735c92e59fb6'))
5454

0 commit comments

Comments
 (0)