Skip to content

Commit 1edd938

Browse files
authored
Add taproot support
1 parent d763ca6 commit 1edd938

File tree

4 files changed

+174
-5
lines changed

4 files changed

+174
-5
lines changed

blockchain_parser/address.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from bitcoin import base58
1313
from bitcoin.bech32 import CBech32Data
1414
from .utils import btc_ripemd160, double_sha256
15+
from .utils_taproot import from_taproot
16+
from binascii import b2a_hex
1517

1618

1719
class Address(object):
@@ -44,21 +46,30 @@ def from_bech32(cls, hash, segwit_version):
4446
"""Constructs an Address object from a bech32 hash."""
4547
return cls(hash, None, None, "bech32", segwit_version)
4648

49+
@classmethod
50+
def from_bech32m(cls, hash, segwit_version):
51+
"""Constructs an Address object from a bech32m script."""
52+
return cls(hash, None, None, "bech32m", segwit_version)
53+
4754
@property
4855
def hash(self):
4956
"""Returns the RIPEMD-160 hash corresponding to this address"""
5057
if self.public_key is not None and self._hash is None:
5158
self._hash = btc_ripemd160(self.public_key)
52-
5359
return self._hash
5460

5561
@property
5662
def address(self):
5763
"""Returns the encoded representation of this address.
58-
If SegWit, it's encoded using bech32, otherwise using base58
64+
If Taproot, it's encoded using bech32m,
65+
if SegWit, it's encoded using bech32,
66+
otherwise using base58
5967
"""
6068
if self._address is None:
61-
if self.type != "bech32":
69+
if self.type == "bech32m":
70+
tweaked_pubkey = b2a_hex(self.hash).decode("ascii")
71+
self._address = from_taproot(tweaked_pubkey)
72+
elif self.type != "bech32":
6273
version = b'\x00' if self.type == "normal" else b'\x05'
6374
checksum = double_sha256(version + self.hash)
6475

blockchain_parser/output.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ def addresses(self):
7777
elif self.type == "p2wsh":
7878
address = Address.from_bech32(self.script.operations[1], 0)
7979
self._addresses.append(address)
80-
80+
elif self.type == "p2tr":
81+
address = Address.from_bech32m(self.script.operations[1], 1)
82+
self._addresses.append(address)
8183
return self._addresses
8284

8385
def is_return(self):
@@ -104,6 +106,9 @@ def is_p2wpkh(self):
104106
def is_p2wsh(self):
105107
return self.script.is_p2wsh()
106108

109+
def is_p2tr(self):
110+
return self.script.is_p2tr()
111+
107112
@property
108113
def type(self):
109114
"""Returns the output's script type as a string"""
@@ -132,4 +137,7 @@ def type(self):
132137
if self.is_p2wsh():
133138
return "p2wsh"
134139

140+
if self.is_p2tr():
141+
return "p2tr"
142+
135143
return "unknown"

blockchain_parser/script.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from bitcoin.core.script import *
1313
from binascii import b2a_hex
14+
from .utils_taproot import from_taproot
1415

1516

1617
def is_public_key(hex_data):
@@ -107,6 +108,12 @@ def is_p2wsh(self):
107108
def is_p2wpkh(self):
108109
return self.script.is_witness_v0_keyhash()
109110

111+
def is_p2tr(self):
112+
taproot = from_taproot(b2a_hex(self.operations[1]).decode("ascii"))
113+
return self.operations[0] == 1 \
114+
and isinstance(taproot, str) \
115+
and taproot.startswith("bc1p")
116+
110117
def is_pubkey(self):
111118
return len(self.operations) == 2 \
112119
and self.operations[-1] == OP_CHECKSIG \
@@ -142,4 +149,4 @@ def is_unknown(self):
142149
return not self.is_pubkeyhash() and not self.is_pubkey() \
143150
and not self.is_p2sh() and not self.is_multisig() \
144151
and not self.is_return() and not self.is_p2wpkh() \
145-
and not self.is_p2wsh()
152+
and not self.is_p2wsh() and not self.is_p2tr()

blockchain_parser/utils_taproot.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Copyright (C) 2015-2016 The bitcoin-blockchain-parser developers
2+
#
3+
# This file is part of bitcoin-blockchain-parser.
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 bitcoin-blockchain-parser, including this file, may be copied,
9+
# modified, propagated, or distributed except according to the terms contained
10+
# in the LICENSE file.
11+
#
12+
# Encoding/Decoding written by Pieter Wuille (2017)
13+
# and adapted by Anton Wahrstätter (2022)
14+
# https://github.com/Bytom/python-bytomlib/blob/master/pybtmsdk/segwit_addr.py
15+
16+
from enum import Enum
17+
18+
19+
class Encoding(Enum):
20+
"""Enumeration type to list the various supported encodings."""
21+
BECH32 = 1
22+
BECH32M = 2
23+
24+
25+
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
26+
BECH32M_CONST = 0x2bc830a3
27+
28+
29+
def bech32_polymod(values):
30+
"""Internal function that computes the Bech32 checksum."""
31+
generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
32+
chk = 1
33+
for value in values:
34+
top = chk >> 25
35+
chk = (chk & 0x1ffffff) << 5 ^ value
36+
for i in range(5):
37+
chk ^= generator[i] if ((top >> i) & 1) else 0
38+
return chk
39+
40+
41+
def bech32_hrp_expand(hrp):
42+
"""Expand the HRP into values for checksum computation."""
43+
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
44+
45+
46+
def bech32_verify_checksum(hrp, data):
47+
"""Verify a checksum given HRP and converted data characters."""
48+
const = bech32_polymod(bech32_hrp_expand(hrp) + data)
49+
if const == 1:
50+
return Encoding.BECH32
51+
if const == BECH32M_CONST:
52+
return Encoding.BECH32M
53+
return None
54+
55+
56+
def bech32_create_checksum(hrp, data, spec):
57+
"""Compute the checksum values given HRP and data."""
58+
values = bech32_hrp_expand(hrp) + data
59+
const = BECH32M_CONST if spec == Encoding.BECH32M else 1
60+
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const
61+
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
62+
63+
64+
def bech32_encode(hrp, data, spec):
65+
"""Compute a Bech32 string given HRP and data values."""
66+
combined = data + bech32_create_checksum(hrp, data, spec)
67+
return hrp + '1' + ''.join([CHARSET[d] for d in combined])
68+
69+
70+
def bech32_decode(bech):
71+
"""Validate a Bech32/Bech32m string, and determine HRP and data."""
72+
if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
73+
(bech.lower() != bech and bech.upper() != bech)):
74+
return (None, None, None)
75+
bech = bech.lower()
76+
pos = bech.rfind('1')
77+
if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
78+
return (None, None, None)
79+
if not all(x in CHARSET for x in bech[pos+1:]):
80+
return (None, None, None)
81+
hrp = bech[:pos]
82+
data = [CHARSET.find(x) for x in bech[pos+1:]]
83+
spec = bech32_verify_checksum(hrp, data)
84+
if spec is None:
85+
return (None, None, None)
86+
return (hrp, data[:-6], spec)
87+
88+
89+
def convertbits(data, frombits, tobits, pad=True):
90+
"""General power-of-2 base conversion."""
91+
acc = 0
92+
bits = 0
93+
ret = []
94+
maxv = (1 << tobits) - 1
95+
max_acc = (1 << (frombits + tobits - 1)) - 1
96+
for value in data:
97+
if value < 0 or (value >> frombits):
98+
return None
99+
acc = ((acc << frombits) | value) & max_acc
100+
bits += frombits
101+
while bits >= tobits:
102+
bits -= tobits
103+
ret.append((acc >> bits) & maxv)
104+
if pad:
105+
if bits:
106+
ret.append((acc << (tobits - bits)) & maxv)
107+
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
108+
return None
109+
return ret
110+
111+
112+
def decode(hrp, addr):
113+
"""Decode a segwit address."""
114+
hrpgot, data, spec = bech32_decode(addr)
115+
if hrpgot != hrp:
116+
return (None, None)
117+
decoded = convertbits(data[1:], 5, 8, False)
118+
if decoded is None or len(decoded) < 2 or len(decoded) > 40:
119+
return (None, None)
120+
if data[0] > 16:
121+
return (None, None)
122+
if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:
123+
return (None, None)
124+
if data[0] == 0 and spec != Encoding.BECH32 \
125+
or data[0] != 0 and spec != Encoding.BECH32M:
126+
return (None, None)
127+
return (data[0], decoded)
128+
129+
130+
def encode(witprog):
131+
hrp, witver = "bc", 1
132+
"""Encode a segwit address."""
133+
spec = Encoding.BECH32M
134+
ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5), spec)
135+
if decode(hrp, ret) == (None, None):
136+
return None
137+
return ret
138+
139+
140+
def from_taproot(tpk):
141+
"""Input Tweaked Public Key."""
142+
tpk = [int(tpk[i:i+2], 16) for i in range(0, len(tpk), 2)]
143+
return encode(tpk)

0 commit comments

Comments
 (0)