Skip to content

Commit dfaa0c2

Browse files
Martino Salvettidgpv
authored andcommitted
Add Bech32 encoding/decoding; add CBech32Data
1 parent 05cbb3c commit dfaa0c2

File tree

5 files changed

+283
-0
lines changed

5 files changed

+283
-0
lines changed

bitcoin/bech32.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
24+
class Bech32Error(Exception):
25+
pass
26+
27+
class Bech32ChecksumError(Bech32Error):
28+
pass
29+
30+
class CBech32Data(bytes):
31+
"""Bech32-encoded data
32+
33+
Includes a witver and checksum.
34+
"""
35+
def __new__(cls, s):
36+
"""from bech32 addr to """
37+
witver, data = decode('bc', s) # TODO: bc from bitcoin.params
38+
if witver is None and data is None:
39+
raise Bech32Error('Bech32 decoding error')
40+
41+
return cls.from_bytes(witver, data)
42+
43+
def __init__(self, s):
44+
"""Initialize from bech32-encoded string
45+
46+
Note: subclasses put your initialization routines here, but ignore the
47+
argument - that's handled by __new__(), and .from_bytes() will call
48+
__init__() with None in place of the string.
49+
"""
50+
51+
@classmethod
52+
def from_bytes(cls, witver, witprog):
53+
"""Instantiate from witver and data"""
54+
if not (0 <= witver <= 16):
55+
raise ValueError('witver must be in range 0 to 16 inclusive; got %d' % witver)
56+
self = bytes.__new__(cls, witprog)
57+
self.witver = witver
58+
59+
return self
60+
61+
def to_bytes(self):
62+
"""Convert to bytes instance
63+
64+
Note that it's the data represented that is converted; the checkum and
65+
witver is not included.
66+
"""
67+
return b'' + self
68+
69+
def __str__(self):
70+
"""Convert to string"""
71+
return encode('bc', self.witver, self) # TODO: bc from bitcoin.params
72+
73+
def __repr__(self):
74+
return '%s(%r)' % (self.__class__.__name__, str(self))

bitcoin/segwit_addr.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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+
24+
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
25+
26+
27+
def bech32_polymod(values):
28+
"""Internal function that computes the Bech32 checksum."""
29+
generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
30+
chk = 1
31+
for value in values:
32+
top = chk >> 25
33+
chk = (chk & 0x1ffffff) << 5 ^ value
34+
for i in range(5):
35+
chk ^= generator[i] if ((top >> i) & 1) else 0
36+
return chk
37+
38+
39+
def bech32_hrp_expand(hrp):
40+
"""Expand the HRP into values for checksum computation."""
41+
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
42+
43+
44+
def bech32_verify_checksum(hrp, data):
45+
"""Verify a checksum given HRP and converted data characters."""
46+
return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1
47+
48+
49+
def bech32_create_checksum(hrp, data):
50+
"""Compute the checksum values given HRP and data."""
51+
values = bech32_hrp_expand(hrp) + data
52+
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1
53+
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
54+
55+
56+
def bech32_encode(hrp, data):
57+
"""Compute a Bech32 string given HRP and data values."""
58+
combined = data + bech32_create_checksum(hrp, data)
59+
return hrp + '1' + ''.join([CHARSET[d] for d in combined])
60+
61+
62+
def bech32_decode(bech):
63+
"""Validate a Bech32 string, and determine HRP and data."""
64+
if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
65+
(bech.lower() != bech and bech.upper() != bech)):
66+
return (None, None)
67+
bech = bech.lower()
68+
pos = bech.rfind('1')
69+
if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
70+
return (None, None)
71+
if not all(x in CHARSET for x in bech[pos+1:]):
72+
return (None, None)
73+
hrp = bech[:pos]
74+
data = [CHARSET.find(x) for x in bech[pos+1:]]
75+
if not bech32_verify_checksum(hrp, data):
76+
return (None, None)
77+
return (hrp, data[:-6])
78+
79+
80+
def convertbits(data, frombits, tobits, pad=True):
81+
"""General power-of-2 base conversion."""
82+
acc = 0
83+
bits = 0
84+
ret = []
85+
maxv = (1 << tobits) - 1
86+
max_acc = (1 << (frombits + tobits - 1)) - 1
87+
for value in data:
88+
if value < 0 or (value >> frombits):
89+
return None
90+
acc = ((acc << frombits) | value) & max_acc
91+
bits += frombits
92+
while bits >= tobits:
93+
bits -= tobits
94+
ret.append((acc >> bits) & maxv)
95+
if pad:
96+
if bits:
97+
ret.append((acc << (tobits - bits)) & maxv)
98+
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
99+
return None
100+
return ret
101+
102+
103+
def decode(hrp, addr):
104+
"""Decode a segwit address."""
105+
hrpgot, data = bech32_decode(addr)
106+
if hrpgot != hrp:
107+
return (None, None)
108+
decoded = convertbits(data[1:], 5, 8, False)
109+
if decoded is None or len(decoded) < 2 or len(decoded) > 40:
110+
return (None, None)
111+
if data[0] > 16:
112+
return (None, None)
113+
if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:
114+
return (None, None)
115+
return (data[0], decoded)
116+
117+
118+
def encode(hrp, witver, witprog):
119+
"""Encode a segwit address."""
120+
ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5))
121+
if decode(hrp, ret) == (None, None):
122+
return None
123+
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: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
18+
from binascii import unhexlify
19+
20+
from bitcoin.core.script import CScript, OP_0, OP_1, OP_16
21+
from bitcoin.bech32 import *
22+
23+
24+
def load_test_vectors(name):
25+
with open(os.path.dirname(__file__) + '/data/' + name, 'r') as fd:
26+
for testcase in json.load(fd):
27+
yield testcase
28+
29+
def to_scriptPubKey(witver, witprog):
30+
"""Decoded bech32 address to script"""
31+
return CScript([witver]) + CScript(bytes(witprog))
32+
33+
class Test_bech32(unittest.TestCase):
34+
35+
def op_decode(self, witver):
36+
"""OP encoding to int"""
37+
if witver == OP_0:
38+
return 0
39+
if OP_1 <= witver <= OP_16:
40+
return witver - OP_1 + 1
41+
self.fail('Wrong witver: %d' % witver)
42+
43+
def test_encode_decode(self):
44+
for exp_bin, exp_bech32 in load_test_vectors('bech32_encode_decode.json'):
45+
exp_bin = unhexlify(exp_bin.encode('utf8'))
46+
witver = self.op_decode(exp_bin[0])
47+
hrp = exp_bech32[:exp_bech32.rindex('1')].lower()
48+
self.assertEqual(exp_bin[1], len(exp_bin[2:]))
49+
act_bech32 = encode(hrp, witver, exp_bin[2:])
50+
act_bin = decode(hrp, exp_bech32)
51+
52+
self.assertEqual(act_bech32.lower(), exp_bech32.lower())
53+
self.assertEqual(to_scriptPubKey(*act_bin), exp_bin)
54+
55+
class Test_CBech32Data(unittest.TestCase):
56+
def test_from_data(self):
57+
b = CBech32Data.from_bytes(0, unhexlify('751e76e8199196d454941c45d1b3a323f1433bd6'))
58+
self.assertEqual(b.witver, 0)
59+
self.assertEqual(str(b).upper(), 'BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4')
60+
61+
def test_invalid_bech32_exception(self):
62+
63+
for invalid, _ in load_test_vectors("bech32_invalid.json"):
64+
msg = '%r should have raised Bech32Error but did not' % invalid
65+
with self.assertRaises(Bech32Error, msg=msg):
66+
CBech32Data(invalid)

0 commit comments

Comments
 (0)