From 10856798f530987aeb9aef351e681b0a33e6f4ae Mon Sep 17 00:00:00 2001 From: Chris Guida Date: Thu, 14 Jan 2021 19:46:28 -0600 Subject: [PATCH 01/13] Add classes and utilities for parsing undo data in rev*.dat files --- blockchain_parser/block.py | 6 +- blockchain_parser/blockchain.py | 14 ++- blockchain_parser/input.py | 4 +- blockchain_parser/output.py | 4 +- blockchain_parser/tests/test_utils.py | 10 +-- blockchain_parser/transaction.py | 10 +-- blockchain_parser/undo.py | 122 ++++++++++++++++++++++++++ blockchain_parser/utils.py | 86 +++++++++++++++++- examples/ordered-blocks.py | 2 +- examples/undo-blocks.py | 16 ++++ 10 files changed, 254 insertions(+), 20 deletions(-) create mode 100644 blockchain_parser/undo.py create mode 100644 examples/undo-blocks.py diff --git a/blockchain_parser/block.py b/blockchain_parser/block.py index f00db41..adbdb78 100644 --- a/blockchain_parser/block.py +++ b/blockchain_parser/block.py @@ -11,7 +11,7 @@ from .transaction import Transaction from .block_header import BlockHeader -from .utils import format_hash, decode_varint, double_sha256 +from .utils import format_hash, decode_compactsize, double_sha256 def get_block_transactions(raw_hex): @@ -23,7 +23,7 @@ def get_block_transactions(raw_hex): # Decoding the number of transactions, offset is the size of # the varint (1 to 9 bytes) - n_transactions, offset = decode_varint(transaction_data) + n_transactions, offset = decode_compactsize(transaction_data) for i in range(n_transactions): # Try from 1024 (1KiB) -> 1073741824 (1GiB) slice widths @@ -77,7 +77,7 @@ def n_transactions(self): as there's no need to parse all transactions to get this information """ if self._n_transactions is None: - self._n_transactions = decode_varint(self.hex[80:])[0] + self._n_transactions = decode_compactsize(self.hex[80:])[0] return self._n_transactions diff --git a/blockchain_parser/blockchain.py b/blockchain_parser/blockchain.py index 454e7c2..5bd82ae 100644 --- a/blockchain_parser/blockchain.py +++ b/blockchain_parser/blockchain.py @@ -42,10 +42,22 @@ def get_files(path): files = map(lambda x: os.path.join(path, x), files) return sorted(files) +def get_undo_files(path): + """ + Given the path to the .bitcoin directory, returns the sorted list of rev*.dat + files contained in that directory + """ + if not stat.S_ISDIR(os.stat(path)[stat.ST_MODE]): + return [path] + files = os.listdir(path) + files = [f for f in files if f.startswith("rev") and f.endswith(".dat")] + files = map(lambda x: os.path.join(path, x), files) + return sorted(files) + def get_blocks(blockfile): """ - Given the name of a .blk file, for every block contained in the file, + Given the name of a .dat file, for every block contained in the file, yields its raw hexadecimal value """ with open(blockfile, "rb") as f: diff --git a/blockchain_parser/input.py b/blockchain_parser/input.py index 564d9ae..09d974b 100644 --- a/blockchain_parser/input.py +++ b/blockchain_parser/input.py @@ -9,7 +9,7 @@ # modified, propagated, or distributed except according to the terms contained # in the LICENSE file. -from .utils import decode_varint, decode_uint32, format_hash +from .utils import decode_compactsize, decode_uint32, format_hash from .script import Script @@ -23,7 +23,7 @@ def __init__(self, raw_hex): self._sequence_number = None self._witnesses = [] - self._script_length, varint_length = decode_varint(raw_hex[36:]) + self._script_length, varint_length = decode_compactsize(raw_hex[36:]) self._script_start = 36 + varint_length self.size = self._script_start + self._script_length + 4 diff --git a/blockchain_parser/output.py b/blockchain_parser/output.py index 5ef1a58..7a9011b 100644 --- a/blockchain_parser/output.py +++ b/blockchain_parser/output.py @@ -9,7 +9,7 @@ # modified, propagated, or distributed except according to the terms contained # in the LICENSE file. -from .utils import decode_varint, decode_uint64 +from .utils import decode_compactsize, decode_uint64, decode_varint, decompress_txout_amt from .script import Script from .address import Address @@ -22,7 +22,7 @@ def __init__(self, raw_hex): self._script = None self._addresses = None - script_length, varint_size = decode_varint(raw_hex[8:]) + script_length, varint_size = decode_compactsize(raw_hex[8:]) script_start = 8 + varint_size self._script_hex = raw_hex[script_start:script_start+script_length] diff --git a/blockchain_parser/tests/test_utils.py b/blockchain_parser/tests/test_utils.py index e70cb25..9acde55 100644 --- a/blockchain_parser/tests/test_utils.py +++ b/blockchain_parser/tests/test_utils.py @@ -44,12 +44,12 @@ def test_decode_uint64(self): for uint64, value in uint64_dict.items(): self.assertEqual(utils.decode_uint64(a2b_hex(uint64)), value) - def test_decode_varint(self): + def test_decode_compactsize(self): case1 = a2b_hex("fa") - self.assertEqual(utils.decode_varint(case1), (250, 1)) + self.assertEqual(utils.decode_compactsize(case1), (250, 1)) case2 = a2b_hex("fd0100") - self.assertEqual(utils.decode_varint(case2), (1, 3)) + self.assertEqual(utils.decode_compactsize(case2), (1, 3)) case3 = a2b_hex("fe01000000") - self.assertEqual(utils.decode_varint(case3), (1, 5)) + self.assertEqual(utils.decode_compactsize(case3), (1, 5)) case4 = a2b_hex("ff0100000000000000") - self.assertEqual(utils.decode_varint(case4), (1, 9)) + self.assertEqual(utils.decode_compactsize(case4), (1, 9)) diff --git a/blockchain_parser/transaction.py b/blockchain_parser/transaction.py index 9277882..9dd8a79 100644 --- a/blockchain_parser/transaction.py +++ b/blockchain_parser/transaction.py @@ -11,7 +11,7 @@ from math import ceil -from .utils import decode_varint, decode_uint32, double_sha256, format_hash +from .utils import decode_compactsize, decode_uint32, double_sha256, format_hash from .input import Input from .output import Output @@ -44,7 +44,7 @@ def __init__(self, raw_hex): self.is_segwit = True offset += 2 - self.n_inputs, varint_size = decode_varint(raw_hex[offset:]) + self.n_inputs, varint_size = decode_compactsize(raw_hex[offset:]) offset += varint_size self.inputs = [] @@ -53,7 +53,7 @@ def __init__(self, raw_hex): offset += input.size self.inputs.append(input) - self.n_outputs, varint_size = decode_varint(raw_hex[offset:]) + self.n_outputs, varint_size = decode_compactsize(raw_hex[offset:]) offset += varint_size self.outputs = [] @@ -65,10 +65,10 @@ def __init__(self, raw_hex): if self.is_segwit: self._offset_before_tx_witnesses = offset for inp in self.inputs: - tx_witnesses_n, varint_size = decode_varint(raw_hex[offset:]) + tx_witnesses_n, varint_size = decode_compactsize(raw_hex[offset:]) offset += varint_size for j in range(tx_witnesses_n): - component_length, varint_size = decode_varint( + component_length, varint_size = decode_compactsize( raw_hex[offset:]) offset += varint_size witness = raw_hex[offset:offset + component_length] diff --git a/blockchain_parser/undo.py b/blockchain_parser/undo.py new file mode 100644 index 0000000..140e620 --- /dev/null +++ b/blockchain_parser/undo.py @@ -0,0 +1,122 @@ +# Copyright (C) 2015-2016 The bitcoin-blockchain-parser developers +# +# This file is part of bitcoin-blockchain-parser. +# +# It is subject to the license terms in the LICENSE file found in the top-level +# directory of this distribution. +# +# No part of bitcoin-blockchain-parser, including this file, may be copied, +# modified, propagated, or distributed except according to the terms contained +# in the LICENSE file. + +from .utils import decode_varint, decode_compactsize, decompress_txout_amt + +class BlockUndo(object): + """ + Represents a block of spent transaction outputs (coins), as encoded + in the undo rev*.dat files + """ + def __init__(self, raw_hex): + self._raw_hex = raw_hex + self.spends = [] + num_txs, pos = decode_compactsize(raw_hex) + # print("found %d" % num_txs + " transactions") + for i in range(num_txs): + # print("calling SpentOutput with raw_hex %s", raw_hex) + txn = SpentTransaction(raw_hex=raw_hex[pos:]) + self.spends.append(txn) + # print("found transaction #%d length %d hex: " % (i, txn.len), raw_hex[pos:pos+txn.len].hex()) + pos += txn.len + + +class SpentTransaction(object): + """Represents the script portion of a spent Transaction output""" + def __init__(self, raw_hex=None): + self._raw_hex = raw_hex + self.outputs = [] + # print("decoding compactsize for hex: ", raw_hex.hex()) + self.output_len, pos = decode_compactsize(raw_hex) + # print("found %d" % self.output_len + " outputs") + for i in range(self.output_len): + output = SpentOutput(raw_hex=raw_hex[pos:]) + self.outputs.append(output) + # print("found output #%d length %d hex: " % (i, output.len), raw_hex[pos:pos+output.len].hex()) + pos += output.len + self.len = pos + + @classmethod + def from_hex(cls, hex_): + return cls(hex_) + + +class SpentOutput(object): + """Represents a spent Transaction output""" + + def __init__(self, raw_hex=None): + # print("decoding output: ", raw_hex.hex()) + self._raw_hex = raw_hex + pos = 0 + # self.version = raw_hex[pos] + # pos += 1 + + # decode height code + height_code, height_code_len = decode_varint(raw_hex[pos:]) + # print("found height code : ", height_code, height_code_len) + if height_code % 2 == 1: + self.is_coinbase = True + height_code -= 1 + else: + self.is_coinbase = False + self.height = height_code // 2 + + # print("found height: ", self.height) + + # skip byte reserved only for backwards compatibility, should always be 0x00 + pos += height_code_len + 1 + + # decode compressed txout amount + compressed_amt, compressed_amt_len = decode_varint(raw_hex[pos:]) + self.amt = decompress_txout_amt(compressed_amt) + pos += compressed_amt_len + + # get script + script_hex, script_pub_key_len = SpentScriptPubKey.extract_from_hex(raw_hex[pos:]) + self.script_pub_key = SpentScriptPubKey(script_hex) + self.len = pos + self.script_pub_key.len + + @classmethod + def from_hex(cls, hex_): + return cls(hex_) + + +class SpentScriptPubKey(object): + """Represents the script portion of a spent Transaction output""" + def __init__(self, raw_hex=None): + self._raw_hex = raw_hex + self.len = len(raw_hex) + # self.script_hex = raw_hex[1:] + + @classmethod + def from_hex(cls, hex_): + return cls(hex_) + + @classmethod + def extract_from_hex(cls, raw_hex): + """ + docstring + """ + if raw_hex[0] in (0x00, 0x01): + return (raw_hex[:21], 21) + elif raw_hex[0] in (0x02, 0x03): + return (raw_hex[:33], 33) + elif raw_hex[0] in (0x04, 0x05): + # print("found strange script type: ", raw_hex[0]) + return (raw_hex[:33], 33) + else: + # print("found strange script type: ", raw_hex[0]) + # print("decoding compactsize for raw hex: ", raw_hex.hex()) + script_len_code, script_len_code_len = decode_varint(raw_hex) + # print("script_len_code, script_len_code_len: (%s, %s)" % (script_len_code, script_len_code_len)) + real_script_len = script_len_code - 6 + # print("real_script_len: %d" % real_script_len) + return (raw_hex[:script_len_code_len+real_script_len], real_script_len) diff --git a/blockchain_parser/utils.py b/blockchain_parser/utils.py index 18ce8d8..e54dd51 100644 --- a/blockchain_parser/utils.py +++ b/blockchain_parser/utils.py @@ -39,7 +39,7 @@ def decode_uint64(data): return struct.unpack(" 0) size = int(data[0]) assert(size <= 255) @@ -59,3 +59,87 @@ def decode_varint(data): size = struct.calcsize(format_) return struct.unpack(format_, data[1:size+1])[0], size + 1 + + +def decode_varint(raw_hex): + """ + Reads the weird format of VarInt present in src/serialize.h of bitcoin core + and being used for storing data in the leveldb. + This is not the VARINT format described for general bitcoin serialization + use. + """ + n = 0 + pos = 0 + while True: + try: + data = raw_hex[pos] + except IndexError as e: + print("IndexError caught on raw_hex: ", raw_hex, e) + raise e + pos += 1 + n = (n << 7) | (data & 0x7f) + if data & 0x80 == 0: + return n, pos + n += 1 + + +def decompress_txout_amt(amount_compressed_int): + # (this function stolen from https://github.com/sr-gi/bitcoin_tools and modified to remove bug) + # No need to do any work if it's zero. + if amount_compressed_int == 0: + return 0 + + # The decompressed amount is either of the following two equations: + # x = 1 + 10*(9*n + d - 1) + e + # x = 1 + 10*(n - 1) + 9 + amount_compressed_int -= 1 + + # The decompressed amount is now one of the following two equations: + # x = 10*(9*n + d - 1) + e + # x = 10*(n - 1) + 9 + exponent = amount_compressed_int % 10 + + # integer division + amount_compressed_int //= 10 + + # The decompressed amount is now one of the following two equations: + # x = 9*n + d - 1 | where e < 9 + # x = n - 1 | where e = 9 + n = 0 + if exponent < 9: + lastDigit = amount_compressed_int%9 + 1 + # integer division + amount_compressed_int //= 9 + n = amount_compressed_int*10 + lastDigit + else: + n = amount_compressed_int + 1 + + # Apply the exponent. + return n * 10**exponent + + +def compress_txout_amt(n): + """ Compresses the Satoshi amount of a UTXO to be stored in the LevelDB. Code is a port from the Bitcoin Core C++ + source: + https://github.com/bitcoin/bitcoin/blob/v0.13.2/src/compressor.cpp#L133#L160 + :param n: Satoshi amount to be compressed. + :type n: int + :return: The compressed amount of Satoshis. + :rtype: int + (this function stolen from https://github.com/sr-gi/bitcoin_tools and modified to remove bug) + """ + + if n == 0: + return 0 + e = 0 + while ((n % 10) == 0) and e < 9: + n //= 10 + e += 1 + + if e < 9: + d = (n % 10) + assert (1 <= d <= 9) + n //= 10 + return 1 + (n * 9 + d - 1) * 10 + e + else: + return 1 + (n - 1) * 10 + 9 diff --git a/examples/ordered-blocks.py b/examples/ordered-blocks.py index 061e804..426c9d4 100644 --- a/examples/ordered-blocks.py +++ b/examples/ordered-blocks.py @@ -10,4 +10,4 @@ # `index` directory (LevelDB index) being maintained by bitcoind. It contains # .ldb files and is present inside the `blocks` directory for block in blockchain.get_ordered_blocks(sys.argv[1] + '/index', end=1000): - print("height=%d block=%s" % (block.height, block.hash)) \ No newline at end of file + print("height=%d block=%s" % (block.height, block.hash)) diff --git a/examples/undo-blocks.py b/examples/undo-blocks.py new file mode 100644 index 0000000..4164324 --- /dev/null +++ b/examples/undo-blocks.py @@ -0,0 +1,16 @@ +import os +import plyvel +from blockchain_parser.blockchain import * +from blockchain_parser.output import * +from blockchain_parser.utils import * +from blockchain_parser.blockchain import Blockchain + +undo_files = get_undo_files(os.path.expanduser('~/.bitcoin/blocks')) +undo_block_ctr = 0 +for i, file_name in enumerate(undo_files): + print("parsing undo file #%d" % i) + for j, block_raw in enumerate(get_blocks(file_name)): + undo_block_ctr += 1 + if j % 1000 == 0 or (i == 1 and j > 9000): + print("parsing undo block #%d in file #%d block #%d" % (undo_block_ctr, i, j)) + block_undo_current = BlockUndo(block_raw) From e37d2deb4f90d684ccd9068a910110e8de8d08fd Mon Sep 17 00:00:00 2001 From: Chris Guida Date: Thu, 14 Jan 2021 19:57:29 -0600 Subject: [PATCH 02/13] Fix undo-blocks.py --- examples/undo-blocks.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/undo-blocks.py b/examples/undo-blocks.py index 4164324..34f7f88 100644 --- a/examples/undo-blocks.py +++ b/examples/undo-blocks.py @@ -1,9 +1,8 @@ import os -import plyvel from blockchain_parser.blockchain import * -from blockchain_parser.output import * -from blockchain_parser.utils import * from blockchain_parser.blockchain import Blockchain +from blockchain_parser.utils import * +from blockchain_parser.undo import * undo_files = get_undo_files(os.path.expanduser('~/.bitcoin/blocks')) undo_block_ctr = 0 From 1edd9381d9338b0c6bb4e207060aa6a378225753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Wahrst=C3=A4tter?= <51536394+Nerolation@users.noreply.github.com> Date: Sat, 5 Mar 2022 16:28:30 +0100 Subject: [PATCH 03/13] Add taproot support --- blockchain_parser/address.py | 17 +++- blockchain_parser/output.py | 10 +- blockchain_parser/script.py | 9 +- blockchain_parser/utils_taproot.py | 143 +++++++++++++++++++++++++++++ 4 files changed, 174 insertions(+), 5 deletions(-) create mode 100644 blockchain_parser/utils_taproot.py diff --git a/blockchain_parser/address.py b/blockchain_parser/address.py index 1dc0855..f4372b8 100644 --- a/blockchain_parser/address.py +++ b/blockchain_parser/address.py @@ -12,6 +12,8 @@ from bitcoin import base58 from bitcoin.bech32 import CBech32Data from .utils import btc_ripemd160, double_sha256 +from .utils_taproot import from_taproot +from binascii import b2a_hex class Address(object): @@ -44,21 +46,30 @@ def from_bech32(cls, hash, segwit_version): """Constructs an Address object from a bech32 hash.""" return cls(hash, None, None, "bech32", segwit_version) + @classmethod + def from_bech32m(cls, hash, segwit_version): + """Constructs an Address object from a bech32m script.""" + return cls(hash, None, None, "bech32m", segwit_version) + @property def hash(self): """Returns the RIPEMD-160 hash corresponding to this address""" if self.public_key is not None and self._hash is None: self._hash = btc_ripemd160(self.public_key) - return self._hash @property def address(self): """Returns the encoded representation of this address. - If SegWit, it's encoded using bech32, otherwise using base58 + If Taproot, it's encoded using bech32m, + if SegWit, it's encoded using bech32, + otherwise using base58 """ if self._address is None: - if self.type != "bech32": + if self.type == "bech32m": + tweaked_pubkey = b2a_hex(self.hash).decode("ascii") + self._address = from_taproot(tweaked_pubkey) + elif self.type != "bech32": version = b'\x00' if self.type == "normal" else b'\x05' checksum = double_sha256(version + self.hash) diff --git a/blockchain_parser/output.py b/blockchain_parser/output.py index 5ef1a58..adf190a 100644 --- a/blockchain_parser/output.py +++ b/blockchain_parser/output.py @@ -77,7 +77,9 @@ def addresses(self): elif self.type == "p2wsh": address = Address.from_bech32(self.script.operations[1], 0) self._addresses.append(address) - + elif self.type == "p2tr": + address = Address.from_bech32m(self.script.operations[1], 1) + self._addresses.append(address) return self._addresses def is_return(self): @@ -104,6 +106,9 @@ def is_p2wpkh(self): def is_p2wsh(self): return self.script.is_p2wsh() + def is_p2tr(self): + return self.script.is_p2tr() + @property def type(self): """Returns the output's script type as a string""" @@ -132,4 +137,7 @@ def type(self): if self.is_p2wsh(): return "p2wsh" + if self.is_p2tr(): + return "p2tr" + return "unknown" diff --git a/blockchain_parser/script.py b/blockchain_parser/script.py index d63ebae..e1fd371 100644 --- a/blockchain_parser/script.py +++ b/blockchain_parser/script.py @@ -11,6 +11,7 @@ from bitcoin.core.script import * from binascii import b2a_hex +from .utils_taproot import from_taproot def is_public_key(hex_data): @@ -107,6 +108,12 @@ def is_p2wsh(self): def is_p2wpkh(self): return self.script.is_witness_v0_keyhash() + def is_p2tr(self): + taproot = from_taproot(b2a_hex(self.operations[1]).decode("ascii")) + return self.operations[0] == 1 \ + and isinstance(taproot, str) \ + and taproot.startswith("bc1p") + def is_pubkey(self): return len(self.operations) == 2 \ and self.operations[-1] == OP_CHECKSIG \ @@ -142,4 +149,4 @@ def is_unknown(self): return not self.is_pubkeyhash() and not self.is_pubkey() \ and not self.is_p2sh() and not self.is_multisig() \ and not self.is_return() and not self.is_p2wpkh() \ - and not self.is_p2wsh() + and not self.is_p2wsh() and not self.is_p2tr() diff --git a/blockchain_parser/utils_taproot.py b/blockchain_parser/utils_taproot.py new file mode 100644 index 0000000..c52ed02 --- /dev/null +++ b/blockchain_parser/utils_taproot.py @@ -0,0 +1,143 @@ +# Copyright (C) 2015-2016 The bitcoin-blockchain-parser developers +# +# This file is part of bitcoin-blockchain-parser. +# +# It is subject to the license terms in the LICENSE file found in the top-level +# directory of this distribution. +# +# No part of bitcoin-blockchain-parser, including this file, may be copied, +# modified, propagated, or distributed except according to the terms contained +# in the LICENSE file. +# +# Encoding/Decoding written by Pieter Wuille (2017) +# and adapted by Anton Wahrstätter (2022) +# https://github.com/Bytom/python-bytomlib/blob/master/pybtmsdk/segwit_addr.py + +from enum import Enum + + +class Encoding(Enum): + """Enumeration type to list the various supported encodings.""" + BECH32 = 1 + BECH32M = 2 + + +CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" +BECH32M_CONST = 0x2bc830a3 + + +def bech32_polymod(values): + """Internal function that computes the Bech32 checksum.""" + generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + chk = 1 + for value in values: + top = chk >> 25 + chk = (chk & 0x1ffffff) << 5 ^ value + for i in range(5): + chk ^= generator[i] if ((top >> i) & 1) else 0 + return chk + + +def bech32_hrp_expand(hrp): + """Expand the HRP into values for checksum computation.""" + return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] + + +def bech32_verify_checksum(hrp, data): + """Verify a checksum given HRP and converted data characters.""" + const = bech32_polymod(bech32_hrp_expand(hrp) + data) + if const == 1: + return Encoding.BECH32 + if const == BECH32M_CONST: + return Encoding.BECH32M + return None + + +def bech32_create_checksum(hrp, data, spec): + """Compute the checksum values given HRP and data.""" + values = bech32_hrp_expand(hrp) + data + const = BECH32M_CONST if spec == Encoding.BECH32M else 1 + polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const + return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] + + +def bech32_encode(hrp, data, spec): + """Compute a Bech32 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/Bech32m string, and determine HRP and data.""" + if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or + (bech.lower() != bech and bech.upper() != bech)): + 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, None) + if not all(x in CHARSET for x in bech[pos+1:]): + return (None, None, None) + hrp = bech[:pos] + data = [CHARSET.find(x) for x in bech[pos+1:]] + spec = bech32_verify_checksum(hrp, data) + if spec is None: + return (None, None, None) + return (hrp, data[:-6], spec) + + +def convertbits(data, frombits, tobits, pad=True): + """General power-of-2 base conversion.""" + acc = 0 + bits = 0 + ret = [] + maxv = (1 << tobits) - 1 + max_acc = (1 << (frombits + tobits - 1)) - 1 + for value in data: + if value < 0 or (value >> frombits): + return None + acc = ((acc << frombits) | value) & max_acc + bits += frombits + while bits >= tobits: + bits -= tobits + ret.append((acc >> bits) & maxv) + if pad: + if bits: + ret.append((acc << (tobits - bits)) & maxv) + elif bits >= frombits or ((acc << (tobits - bits)) & maxv): + return None + return ret + + +def decode(hrp, addr): + """Decode a segwit address.""" + hrpgot, data, spec = bech32_decode(addr) + if hrpgot != hrp: + return (None, None) + decoded = convertbits(data[1:], 5, 8, False) + if decoded is None or len(decoded) < 2 or len(decoded) > 40: + return (None, None) + if data[0] > 16: + return (None, None) + if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: + return (None, None) + if data[0] == 0 and spec != Encoding.BECH32 \ + or data[0] != 0 and spec != Encoding.BECH32M: + return (None, None) + return (data[0], decoded) + + +def encode(witprog): + hrp, witver = "bc", 1 + """Encode a segwit address.""" + spec = Encoding.BECH32M + ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5), spec) + if decode(hrp, ret) == (None, None): + return None + return ret + + +def from_taproot(tpk): + """Input Tweaked Public Key.""" + tpk = [int(tpk[i:i+2], 16) for i in range(0, len(tpk), 2)] + return encode(tpk) From edd892784eb5853d25849e8443a8279f12a23ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Wahrst=C3=A4tter?= <51536394+Nerolation@users.noreply.github.com> Date: Sat, 5 Mar 2022 16:37:53 +0100 Subject: [PATCH 04/13] PEP 8 fix --- blockchain_parser/utils_taproot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blockchain_parser/utils_taproot.py b/blockchain_parser/utils_taproot.py index c52ed02..169ffc2 100644 --- a/blockchain_parser/utils_taproot.py +++ b/blockchain_parser/utils_taproot.py @@ -21,7 +21,7 @@ class Encoding(Enum): BECH32 = 1 BECH32M = 2 - + CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" BECH32M_CONST = 0x2bc830a3 @@ -123,7 +123,7 @@ def decode(hrp, addr): return (None, None) if data[0] == 0 and spec != Encoding.BECH32 \ or data[0] != 0 and spec != Encoding.BECH32M: - return (None, None) + return (None, None) return (data[0], decoded) From 1cfb674bc2e2f676bd39328b1b72113a3efaae6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Wahrst=C3=A4tter?= <51536394+Nerolation@users.noreply.github.com> Date: Tue, 22 Mar 2022 07:54:15 +0100 Subject: [PATCH 05/13] Add tests for taproot --- blockchain_parser/tests/test_taproot.py | 55 +++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 blockchain_parser/tests/test_taproot.py diff --git a/blockchain_parser/tests/test_taproot.py b/blockchain_parser/tests/test_taproot.py new file mode 100644 index 0000000..9ebec0b --- /dev/null +++ b/blockchain_parser/tests/test_taproot.py @@ -0,0 +1,55 @@ +# Copyright (C) 2015-2016 The bitcoin-blockchain-parser developers +# +# This file is part of bitcoin-blockchain-parser. +# +# It is subject to the license terms in the LICENSE file found in the top-level +# directory of this distribution. +# +# No part of bitcoin-blockchain-parser, including this file, may be copied, +# modified, propagated, or distributed except according to the terms contained +# in the LICENSE file. +# +# The transactions were taken from +# https://bitcoin.stackexchange.com/questions/110995/how +# -can-i-find-samples-for-p2tr-transactions-on-mainnet +# +# 33e7…9036, the first P2TR transaction +# 3777…35c8, the first transaction with both a P2TR scriptpath and a P2TR keypath input +# 83c8…7d82, with multiple P2TR keypath inputs +# 905e…d530, the first scriptpath 2-of-2 multisig spend +# 2eb8…b272, the first use of the new Tapscript opcode OP_CHECKSIGADD +# +# THESE TRANSACTIONS ARE INCLUDED IN BLK02804.DAT + +import os +import sys +sys.path.append('../..') +from blockchain_parser.blockchain import Blockchain + + +FIRST_TAPROOT = "33e794d097969002ee05d336686fc03c9e15a597c1b9827669460fac98799036" +FIRST_TAPROOT_2X_P2TR = "37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8" +MULTIPLE_P2TR_INPUTS = "83c8e0289fecf93b5a284705396f5a652d9886cbd26236b0d647655ad8a37d82" +FIRST_2_OF_2_SPEND = "905ecdf95a84804b192f4dc221cfed4d77959b81ed66013a7e41a6e61e7ed530" +USING_OP_CHECKSIGADD = "2eb8dbaa346d4be4e82fe444c2f0be00654d8cfd8c4a9a61b11aeaab8c00b272" + + +TAPROOTS = [FIRST_TAPROOT, + FIRST_TAPROOT_2X_P2TR, + MULTIPLE_P2TR_INPUTS, + FIRST_2_OF_2_SPEND, + USING_OP_CHECKSIGADD] + + +blockchain = Blockchain(os.path.expanduser('../../blocks')) +for block in blockchain.get_unordered_blocks(): + for tx in block.transactions: + if tx.txid in TAPROOTS: + print("{:<15}{}".format("Tx ID: ", tx.txid)) + for tx_input in tx.inputs: + print("{:<15}{}".format("Input Tx ID: ",tx_input.transaction_hash)) + for tx_output in tx.outputs: + for addr in tx_output.addresses: + print("{:<15}{}".format("Address: ", addr.address)) + print("{:<15}{:,.0f} s".format("Value: ", tx_output.value)) + print("----------------------------------------------------------------") \ No newline at end of file From ab640a027e5a0011df5ab1974336313ed35ea3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Wahrst=C3=A4tter?= <51536394+Nerolation@users.noreply.github.com> Date: Sat, 2 Apr 2022 08:55:25 +0200 Subject: [PATCH 06/13] Fix is_type defect Early scripts represent non-byte types in operations[0] --- blockchain_parser/script.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/blockchain_parser/script.py b/blockchain_parser/script.py index e1fd371..94f76e9 100644 --- a/blockchain_parser/script.py +++ b/blockchain_parser/script.py @@ -109,10 +109,11 @@ def is_p2wpkh(self): return self.script.is_witness_v0_keyhash() def is_p2tr(self): - taproot = from_taproot(b2a_hex(self.operations[1]).decode("ascii")) - return self.operations[0] == 1 \ - and isinstance(taproot, str) \ - and taproot.startswith("bc1p") + if type(self.operations[1]) == bytes: + taproot = from_taproot(b2a_hex(self.operations[1]).decode("ascii")) + return self.operations[0] == 1 \ + and isinstance(taproot, str) \ + and taproot.startswith("bc1p") def is_pubkey(self): return len(self.operations) == 2 \ From e2781d3df38a1c23d85e9d81d2b64d54b56a4541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Wahrst=C3=A4tter?= <51536394+Nerolation@users.noreply.github.com> Date: Sat, 2 Apr 2022 09:05:26 +0200 Subject: [PATCH 07/13] Fix out of range defect --- blockchain_parser/script.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blockchain_parser/script.py b/blockchain_parser/script.py index 94f76e9..f725b7d 100644 --- a/blockchain_parser/script.py +++ b/blockchain_parser/script.py @@ -109,7 +109,7 @@ def is_p2wpkh(self): return self.script.is_witness_v0_keyhash() def is_p2tr(self): - if type(self.operations[1]) == bytes: + if len(self.operations) > 1 and type(self.operations[1]) == bytes: taproot = from_taproot(b2a_hex(self.operations[1]).decode("ascii")) return self.operations[0] == 1 \ and isinstance(taproot, str) \ From 790c925ec907a61ec7a91dbbe8c66180712efadd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Wahrst=C3=A4tter?= <51536394+Nerolation@users.noreply.github.com> Date: Mon, 29 Aug 2022 14:40:12 +0200 Subject: [PATCH 08/13] Add Taproot support --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cad8dc2..935c919 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This Python 3 library provides a parser for the raw data stored by bitcoind. - Detects outputs types - Detects addresses in outputs - Interprets scripts -- Supports SegWit +- Supports SegWit and Taproot - Supports ordered block parsing ## Installing From 173dfaf9a7d07bf9ade6d36f2a9658c4d3c9c6cd Mon Sep 17 00:00:00 2001 From: Chris Guida Date: Fri, 19 Jan 2024 15:04:01 -0600 Subject: [PATCH 09/13] save progress --- blockchain_parser/output.py | 2 +- blockchain_parser/undo.py | 66 ++++++++++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/blockchain_parser/output.py b/blockchain_parser/output.py index 7a9011b..894fc81 100644 --- a/blockchain_parser/output.py +++ b/blockchain_parser/output.py @@ -9,7 +9,7 @@ # modified, propagated, or distributed except according to the terms contained # in the LICENSE file. -from .utils import decode_compactsize, decode_uint64, decode_varint, decompress_txout_amt +from .utils import decode_compactsize, decode_uint64 from .script import Script from .address import Address diff --git a/blockchain_parser/undo.py b/blockchain_parser/undo.py index 140e620..8c0f169 100644 --- a/blockchain_parser/undo.py +++ b/blockchain_parser/undo.py @@ -1,4 +1,4 @@ -# Copyright (C) 2015-2016 The bitcoin-blockchain-parser developers +# Copyright (C) 2015-2020 The bitcoin-blockchain-parser developers # # This file is part of bitcoin-blockchain-parser. # @@ -11,6 +11,50 @@ from .utils import decode_varint, decode_compactsize, decompress_txout_amt +def decompress_script(raw_hex): + script_type = raw_hex[0] + compressed_script = raw_hex[1:] + + # def decompress_script(compressed_script, script_type): + """ Takes CScript as stored in leveldb and returns it in uncompressed form + (de)compression scheme is defined in bitcoin/src/compressor.cpp + :param compressed_script: raw script bytes hexlified (data in decode_utxo) + :type compressed_script: str + :param script_type: first byte of script data (out_type in decode_utxo) + :type script_type: int + :return: the decompressed CScript + :rtype: str + (this code adapted from https://github.com/sr-gi/bitcoin_tools) + """ + + if script_type == 0: + if len(compressed_script) != 20: + raise Exception("Compressed script has wrong size") + script = OutputScript.P2PKH(compressed_script, hash160=True) + + elif script_type == 1: + if len(compressed_script) != 20: + raise Exception("Compressed script has wrong size") + script = OutputScript.P2SH(compressed_script) + + elif script_type in [2, 3]: + if len(compressed_script) != 33: + raise Exception("Compressed script has wrong size") + script = OutputScript.P2PK(compressed_script) + + elif script_type in [4, 5]: + if len(compressed_script) != 33: + raise Exception("Compressed script has wrong size") + prefix = format(script_type - 2, '02') + script = OutputScript.P2PK(get_uncompressed_pk(prefix + compressed_script[2:])) + + else: + assert len(compressed_script) / 2 == script_type - NSPECIALSCRIPTS + script = OutputScript.from_hex(compressed_script) + + return script.content + + class BlockUndo(object): """ Represents a block of spent transaction outputs (coins), as encoded @@ -80,14 +124,22 @@ def __init__(self, raw_hex=None): pos += compressed_amt_len # get script - script_hex, script_pub_key_len = SpentScriptPubKey.extract_from_hex(raw_hex[pos:]) - self.script_pub_key = SpentScriptPubKey(script_hex) - self.len = pos + self.script_pub_key.len + script_hex, script_pub_key_compressed_len = SpentScriptPubKey.extract_from_hex(raw_hex[pos:]) + self.script_pub_key_compressed = SpentScriptPubKey(script_hex) + self.len = pos + self.script_pub_key_compressed.len @classmethod def from_hex(cls, hex_): return cls(hex_) + + @property + def script(self): + if not self.script: + self.script = decompress_script(self.script_pub_key_compressed) + return self.script + + class SpentScriptPubKey(object): """Represents the script portion of a spent Transaction output""" @@ -120,3 +172,9 @@ def extract_from_hex(cls, raw_hex): real_script_len = script_len_code - 6 # print("real_script_len: %d" % real_script_len) return (raw_hex[:script_len_code_len+real_script_len], real_script_len) + + @property + def script(self): + if not self.script: + self.script = decompress_script(self._raw_hex) + return self.script From 15f95ef8766383305b83a78b3252e81ba3e5873c Mon Sep 17 00:00:00 2001 From: Antoine Le Calvez Date: Sat, 23 Mar 2024 15:32:04 +0100 Subject: [PATCH 10/13] Correct long description --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 214e177..da354c7 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,8 @@ author='Antoine Le Calvez', author_email='antoine@p2sh.info', description='Bitcoin blockchain parser', + long_description=open('README.md').read(), + long_description_content_type='text/markdown', test_suite='blockchain_parser.tests', classifiers=[ 'Development Status :: 5 - Production/Stable', From af5f3390f2611602cdd0b3dd36f01f8b698d76b1 Mon Sep 17 00:00:00 2001 From: Antoine Le Calvez Date: Sat, 23 Mar 2024 15:45:38 +0100 Subject: [PATCH 11/13] Remove setup.cfg --- setup.cfg | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index b88034e..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -description-file = README.md From 2d89d26267e53b955874dad8107db55c8bdfbfa6 Mon Sep 17 00:00:00 2001 From: Antoine Le Calvez Date: Sat, 23 Mar 2024 17:26:50 +0100 Subject: [PATCH 12/13] Remove getBlockIndexes method, rename orphan to stale --- blockchain_parser/blockchain.py | 40 +++++++++++---------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/blockchain_parser/blockchain.py b/blockchain_parser/blockchain.py index 55d8b41..4349467 100644 --- a/blockchain_parser/blockchain.py +++ b/blockchain_parser/blockchain.py @@ -86,8 +86,6 @@ class Blockchain(object): def __init__(self, path): self.path = path - self.blockIndexes = None - self.indexPath = None def get_unordered_blocks(self): """Yields the blocks contained in the .blk files as is, @@ -97,20 +95,6 @@ def get_unordered_blocks(self): for raw_block in get_blocks(blk_file): yield Block(raw_block, None, os.path.split(blk_file)[1]) - def __getBlockIndexes(self, index): - """There is no method of leveldb to close the db (and release the lock). - This creates problem during concurrent operations. - This function also provides caching of indexes. - """ - if self.indexPath != index: - db = plyvel.DB(index, compression=None) - self.blockIndexes = [DBBlockIndex(format_hash(k[1:]), v) - for k, v in db.iterator() if k[0] == ord('b')] - db.close() - self.blockIndexes.sort(key=lambda x: x.height) - self.indexPath = index - return self.blockIndexes - def _index_confirmed(self, chain_indexes, num_confirmations=6): """Check if the first block index in "chain_indexes" has at least "num_confirmation" (6) blocks built on top of it. @@ -166,22 +150,24 @@ def get_ordered_blocks(self, index, start=0, end=None, cache=None): blockIndexes = pickle.load(f) if blockIndexes is None: - # build the block index - blockIndexes = self.__getBlockIndexes(index) + with plyvel.DB(index, compression=None) as db: + # Block index entries are stored with keys prefixed by 'b' + with db.iterator(prefix=b'b') as iterator: + blockIndexes = [DBBlockIndex(format_hash(k[1:]), v) for k, v in iterator] + if cache and not os.path.exists(cache): # cache the block index for re-use next time with open(cache, 'wb') as f: pickle.dump(blockIndexes, f) - # remove small forks that may have occurred while the node was live. # Occasionally a node will receive two different solutions to a block - # at the same time. The Leveldb index saves both, not pruning the + # at the same time. The node saves both to disk, not pruning the # block that leads to a shorter chain once the fork is settled without - # "-reindex"ing the bitcoind block data. This leads to at least two - # blocks with the same height in the database. + # "-reindex"ing the bitcoind block data. This leads to sometimes there + # being two blocks with the same height in the database. # We throw out blocks that don't have at least 6 other blocks on top of # it (6 confirmations). - orphans = [] # hold blocks that are orphans with < 6 blocks on top + stale_blocks = [] # hold hashes of blocks that are stale with < 6 blocks on top last_height = -1 for i, blockIdx in enumerate(blockIndexes): if last_height > -1: @@ -196,18 +182,18 @@ def get_ordered_blocks(self, index, start=0, end=None, cache=None): # if this block is confirmed, the unconfirmed block is # the previous one. Remove it. - orphans.append(blockIndexes[i - 1].hash) + stale_blocks.append(blockIndexes[i - 1].hash) else: # if this block isn't confirmed, remove it. - orphans.append(blockIndexes[i].hash) + stale_blocks.append(blockIndexes[i].hash) last_height = blockIdx.height - # filter out the orphan blocks, so we are left only with block indexes + # filter out stale blocks, so we are left only with block indexes # that have been confirmed # (or are new enough that they haven't yet been confirmed) - blockIndexes = list(filter(lambda block: block.hash not in orphans, blockIndexes)) + blockIndexes = list(filter(lambda block: block.hash not in stale_blocks, blockIndexes)) if end is None: end = len(blockIndexes) From f519fa05f4dec4dfd5ae2a3e1ea9cf1db97cebb2 Mon Sep 17 00:00:00 2001 From: Antoine Le Calvez Date: Sat, 23 Mar 2024 17:52:01 +0100 Subject: [PATCH 13/13] Fix tests for Taproot, use pytest instead of coverage --- README.md | 2 +- blockchain_parser/tests/test_address.py | 13 ++++++ blockchain_parser/tests/test_script.py | 23 +++++++++++ blockchain_parser/tests/test_taproot.py | 55 ------------------------- requirements.txt | 2 +- tests.sh | 5 --- 6 files changed, 38 insertions(+), 62 deletions(-) delete mode 100644 blockchain_parser/tests/test_taproot.py delete mode 100755 tests.sh diff --git a/README.md b/README.md index af81b4a..197465f 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ pip install -r requirements.txt Run the test suite by lauching ``` -./tests.sh +pytest ``` ## Examples diff --git a/blockchain_parser/tests/test_address.py b/blockchain_parser/tests/test_address.py index 779a30b..a7848b2 100644 --- a/blockchain_parser/tests/test_address.py +++ b/blockchain_parser/tests/test_address.py @@ -30,3 +30,16 @@ def test_from_ripemd160(self): ripemd160 = "010966776006953D5567439E5E39F86A0D273BEE" address = Address.from_ripemd160(a2b_hex(ripemd160)) self.assertEqual(address.address, "16UwLL9Risc3QfPqBUvKofHmBQ7wMtjvM") + + def test_from_bech32(self): + # Example sourced from https://en.bitcoin.it/wiki/Bech32 + bech32 = "751e76e8199196d454941c45d1b3a323f1433bd6" + address = Address.from_bech32(a2b_hex(bech32), segwit_version=0) + self.assertEqual(address.address, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") + + def test_from_bech32m(self): + # https://blockstream.info/tx/33e794d097969002ee05d336686fc03c9e15a597c1b9827669460fac98799036?expand + # Second output + bech32m = "a37c3903c8d0db6512e2b40b0dffa05e5a3ab73603ce8c9c4b7771e5412328f9" + address = Address.from_bech32m(a2b_hex(bech32m), segwit_version=1) + self.assertEqual(address.address, "bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3297") diff --git a/blockchain_parser/tests/test_script.py b/blockchain_parser/tests/test_script.py index 1badaa1..c01f62d 100644 --- a/blockchain_parser/tests/test_script.py +++ b/blockchain_parser/tests/test_script.py @@ -27,6 +27,7 @@ def test_op_return_script(self): self.assertFalse(script.is_pubkeyhash()) self.assertFalse(script.is_unknown()) self.assertTrue(script.is_return()) + self.assertFalse(script.is_p2tr()) def test_unknown_script(self): case = "40" @@ -40,6 +41,7 @@ def test_unknown_script(self): self.assertFalse(script.is_pubkeyhash()) self.assertTrue(script.is_unknown()) self.assertFalse(script.is_return()) + self.assertFalse(script.is_p2tr()) case = "" script = Script.from_hex(a2b_hex(case)) @@ -52,6 +54,7 @@ def test_unknown_script(self): self.assertFalse(script.is_pubkeyhash()) self.assertTrue(script.is_unknown()) self.assertFalse(script.is_return()) + self.assertFalse(script.is_p2tr()) def test_multisig_script(self): case = "514104cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef58bbfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4410461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34bfa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af52ae" @@ -64,6 +67,7 @@ def test_multisig_script(self): self.assertFalse(script.is_pubkeyhash()) self.assertFalse(script.is_unknown()) self.assertFalse(script.is_return()) + self.assertFalse(script.is_p2tr()) def test_p2sh_script(self): case = "a91428ad3e63dcae36e5010527578e2eef0e9eeaf3e487" @@ -76,6 +80,7 @@ def test_p2sh_script(self): self.assertFalse(script.is_pubkeyhash()) self.assertFalse(script.is_unknown()) self.assertFalse(script.is_return()) + self.assertFalse(script.is_p2tr()) def test_p2wpkh_script(self): case = "0014c958269b5b6469b6e4b87de1062028ad3bb83cc2" @@ -88,6 +93,7 @@ def test_p2wpkh_script(self): self.assertFalse(script.is_pubkeyhash()) self.assertFalse(script.is_unknown()) self.assertFalse(script.is_return()) + self.assertFalse(script.is_p2tr()) def test_p2wsh_script(self): case = "0020701a8d401c84fb13e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d" @@ -100,6 +106,7 @@ def test_p2wsh_script(self): self.assertFalse(script.is_pubkeyhash()) self.assertFalse(script.is_unknown()) self.assertFalse(script.is_return()) + self.assertFalse(script.is_p2tr()) def test_pubkeyhash_script(self): case = "76a914e9629ef6f5b82564a9b2ecae6c288c56fb33710888ac" @@ -112,6 +119,7 @@ def test_pubkeyhash_script(self): self.assertTrue(script.is_pubkeyhash()) self.assertFalse(script.is_unknown()) self.assertFalse(script.is_return()) + self.assertFalse(script.is_p2tr()) def test_pubkey_script(self): script = Script.from_hex(a2b_hex("4104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac")) @@ -123,3 +131,18 @@ def test_pubkey_script(self): self.assertFalse(script.is_pubkeyhash()) self.assertFalse(script.is_unknown()) self.assertFalse(script.is_return()) + self.assertFalse(script.is_p2tr()) + + def test_taproot_script(self): + # https://blockstream.info/tx/33e794d097969002ee05d336686fc03c9e15a597c1b9827669460fac98799036?expand + # Second output + script = Script.from_hex(a2b_hex("5120a37c3903c8d0db6512e2b40b0dffa05e5a3ab73603ce8c9c4b7771e5412328f9")) + self.assertFalse(script.is_pubkey()) + self.assertFalse(script.is_multisig()) + self.assertFalse(script.is_p2sh()) + self.assertFalse(script.is_p2wpkh()) + self.assertFalse(script.is_p2wsh()) + self.assertFalse(script.is_pubkeyhash()) + self.assertFalse(script.is_unknown()) + self.assertFalse(script.is_return()) + self.assertTrue(script.is_p2tr()) diff --git a/blockchain_parser/tests/test_taproot.py b/blockchain_parser/tests/test_taproot.py deleted file mode 100644 index 9ebec0b..0000000 --- a/blockchain_parser/tests/test_taproot.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (C) 2015-2016 The bitcoin-blockchain-parser developers -# -# This file is part of bitcoin-blockchain-parser. -# -# It is subject to the license terms in the LICENSE file found in the top-level -# directory of this distribution. -# -# No part of bitcoin-blockchain-parser, including this file, may be copied, -# modified, propagated, or distributed except according to the terms contained -# in the LICENSE file. -# -# The transactions were taken from -# https://bitcoin.stackexchange.com/questions/110995/how -# -can-i-find-samples-for-p2tr-transactions-on-mainnet -# -# 33e7…9036, the first P2TR transaction -# 3777…35c8, the first transaction with both a P2TR scriptpath and a P2TR keypath input -# 83c8…7d82, with multiple P2TR keypath inputs -# 905e…d530, the first scriptpath 2-of-2 multisig spend -# 2eb8…b272, the first use of the new Tapscript opcode OP_CHECKSIGADD -# -# THESE TRANSACTIONS ARE INCLUDED IN BLK02804.DAT - -import os -import sys -sys.path.append('../..') -from blockchain_parser.blockchain import Blockchain - - -FIRST_TAPROOT = "33e794d097969002ee05d336686fc03c9e15a597c1b9827669460fac98799036" -FIRST_TAPROOT_2X_P2TR = "37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8" -MULTIPLE_P2TR_INPUTS = "83c8e0289fecf93b5a284705396f5a652d9886cbd26236b0d647655ad8a37d82" -FIRST_2_OF_2_SPEND = "905ecdf95a84804b192f4dc221cfed4d77959b81ed66013a7e41a6e61e7ed530" -USING_OP_CHECKSIGADD = "2eb8dbaa346d4be4e82fe444c2f0be00654d8cfd8c4a9a61b11aeaab8c00b272" - - -TAPROOTS = [FIRST_TAPROOT, - FIRST_TAPROOT_2X_P2TR, - MULTIPLE_P2TR_INPUTS, - FIRST_2_OF_2_SPEND, - USING_OP_CHECKSIGADD] - - -blockchain = Blockchain(os.path.expanduser('../../blocks')) -for block in blockchain.get_unordered_blocks(): - for tx in block.transactions: - if tx.txid in TAPROOTS: - print("{:<15}{}".format("Tx ID: ", tx.txid)) - for tx_input in tx.inputs: - print("{:<15}{}".format("Input Tx ID: ",tx_input.transaction_hash)) - for tx_output in tx.outputs: - for addr in tx_output.addresses: - print("{:<15}{}".format("Address: ", addr.address)) - print("{:<15}{:,.0f} s".format("Value: ", tx_output.value)) - print("----------------------------------------------------------------") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c751a70..d5b28b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ python-bitcoinlib==0.11.0 plyvel==1.5.1 ripemd-hash==1.0.1 -coverage==7.4.4 +pytest==8.1.1 diff --git a/tests.sh b/tests.sh deleted file mode 100755 index 4615bcb..0000000 --- a/tests.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -coverage run --append --include='blockchain_parser/*' --omit='*/tests/*' setup.py test -coverage report -coverage erase