blockchain-parser-2 0.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- blockchain_parser/__init__.py +12 -0
- blockchain_parser/address.py +83 -0
- blockchain_parser/block.py +104 -0
- blockchain_parser/block_header.py +90 -0
- blockchain_parser/blockchain.py +302 -0
- blockchain_parser/index.py +81 -0
- blockchain_parser/input.py +78 -0
- blockchain_parser/output.py +145 -0
- blockchain_parser/script.py +164 -0
- blockchain_parser/tests/__init__.py +10 -0
- blockchain_parser/tests/data/bech32_p2wpkh.txt +1 -0
- blockchain_parser/tests/data/bech32_p2wsh.txt +1 -0
- blockchain_parser/tests/data/bip69_false.txt +1 -0
- blockchain_parser/tests/data/bip69_true.txt +1 -0
- blockchain_parser/tests/data/genesis_block.txt +1 -0
- blockchain_parser/tests/data/invalid_tx.txt +1 -0
- blockchain_parser/tests/data/large_tx.txt +1 -0
- blockchain_parser/tests/data/scripts_invalid.txt +1 -0
- blockchain_parser/tests/data/segwit.txt +1 -0
- blockchain_parser/tests/data/size_non_segwit.txt +1 -0
- blockchain_parser/tests/data/size_segwit.txt +1 -0
- blockchain_parser/tests/test_address.py +45 -0
- blockchain_parser/tests/test_block.py +55 -0
- blockchain_parser/tests/test_blockchain.py +79 -0
- blockchain_parser/tests/test_index.py +48 -0
- blockchain_parser/tests/test_output.py +66 -0
- blockchain_parser/tests/test_script.py +171 -0
- blockchain_parser/tests/test_transaction.py +138 -0
- blockchain_parser/tests/test_undo.py +70 -0
- blockchain_parser/tests/test_utils.py +55 -0
- blockchain_parser/tests/utils.py +9 -0
- blockchain_parser/transaction.py +196 -0
- blockchain_parser/undo.py +198 -0
- blockchain_parser/utils.py +152 -0
- blockchain_parser/utils_taproot.py +143 -0
- blockchain_parser_2-0.1.6.dist-info/METADATA +143 -0
- blockchain_parser_2-0.1.6.dist-info/RECORD +40 -0
- blockchain_parser_2-0.1.6.dist-info/WHEEL +5 -0
- blockchain_parser_2-0.1.6.dist-info/licenses/LICENSE +177 -0
- blockchain_parser_2-0.1.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
__version__ = "0.1.6"
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
from bitcoin import base58
|
|
13
|
+
from bitcoin.bech32 import CBech32Data
|
|
14
|
+
from .utils import btc_ripemd160, double_sha256
|
|
15
|
+
from .utils_taproot import from_taproot
|
|
16
|
+
from binascii import b2a_hex
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Address(object):
|
|
20
|
+
"""Represents a bitcoin address"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, hash, public_key, address, type, segwit_version):
|
|
23
|
+
self._hash = hash
|
|
24
|
+
self.public_key = public_key
|
|
25
|
+
self._address = address
|
|
26
|
+
self.type = type
|
|
27
|
+
self._segwit_version = segwit_version
|
|
28
|
+
|
|
29
|
+
def __repr__(self):
|
|
30
|
+
return "Address(addr=%s)" % self.address
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_public_key(cls, public_key):
|
|
34
|
+
"""Constructs an Address object from a public key"""
|
|
35
|
+
return cls(None, public_key, None, "normal", None)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_ripemd160(cls, hash, type="normal"):
|
|
39
|
+
"""Constructs an Address object from a RIPEMD-160 hash, it may be a
|
|
40
|
+
normal address or a P2SH address, the latter is indicated by setting
|
|
41
|
+
type to 'p2sh'"""
|
|
42
|
+
return cls(hash, None, None, type, None)
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_bech32(cls, hash, segwit_version):
|
|
46
|
+
"""Constructs an Address object from a bech32 hash."""
|
|
47
|
+
return cls(hash, None, None, "bech32", segwit_version)
|
|
48
|
+
|
|
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
|
+
|
|
54
|
+
@property
|
|
55
|
+
def hash(self):
|
|
56
|
+
"""Returns the RIPEMD-160 hash corresponding to this address"""
|
|
57
|
+
if self.public_key is not None and self._hash is None:
|
|
58
|
+
self._hash = btc_ripemd160(self.public_key)
|
|
59
|
+
return self._hash
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def address(self):
|
|
63
|
+
"""Returns the encoded representation of this address.
|
|
64
|
+
If Taproot, it's encoded using bech32m,
|
|
65
|
+
if SegWit, it's encoded using bech32,
|
|
66
|
+
otherwise using base58
|
|
67
|
+
"""
|
|
68
|
+
if self._address is None:
|
|
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":
|
|
73
|
+
version = b'\x00' if self.type == "normal" else b'\x05'
|
|
74
|
+
checksum = double_sha256(version + self.hash)
|
|
75
|
+
|
|
76
|
+
self._address = base58.encode(version + self.hash + checksum[:4])
|
|
77
|
+
else:
|
|
78
|
+
bech_encoded = CBech32Data.from_bytes(self._segwit_version, self._hash)
|
|
79
|
+
self._address = str(bech_encoded)
|
|
80
|
+
return self._address
|
|
81
|
+
|
|
82
|
+
def is_p2sh(self):
|
|
83
|
+
return self.type == "p2sh"
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
from .transaction import Transaction
|
|
13
|
+
from .block_header import BlockHeader
|
|
14
|
+
from .utils import format_hash, decode_compactsize, double_sha256
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_block_transactions(raw_hex):
|
|
18
|
+
"""Given the raw hexadecimal representation of a block,
|
|
19
|
+
yields the block's transactions
|
|
20
|
+
"""
|
|
21
|
+
# Skipping the header
|
|
22
|
+
transaction_data = raw_hex[80:]
|
|
23
|
+
|
|
24
|
+
# Decoding the number of transactions, offset is the size of
|
|
25
|
+
# the varint (1 to 9 bytes)
|
|
26
|
+
n_transactions, offset = decode_compactsize(transaction_data)
|
|
27
|
+
|
|
28
|
+
for i in range(n_transactions):
|
|
29
|
+
transaction = None
|
|
30
|
+
# Try from 1024 (1KiB) -> 512MiB slice widths
|
|
31
|
+
for j in range(0, 20):
|
|
32
|
+
try:
|
|
33
|
+
offset_e = offset + (1024 * 2 ** j)
|
|
34
|
+
transaction = Transaction.from_hex(
|
|
35
|
+
transaction_data[offset:offset_e])
|
|
36
|
+
break
|
|
37
|
+
except Exception:
|
|
38
|
+
transaction = None
|
|
39
|
+
if transaction is None:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
"failed to parse transaction %d at offset %d" % (i, offset)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
yield transaction
|
|
45
|
+
# Skipping to the next transaction
|
|
46
|
+
offset += transaction.size
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Block(object):
|
|
50
|
+
"""
|
|
51
|
+
Represents a Bitcoin block, contains its header and its transactions.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, raw_hex, height=None, blk_file=None):
|
|
55
|
+
self.hex = raw_hex
|
|
56
|
+
self._hash = None
|
|
57
|
+
self._transactions = None
|
|
58
|
+
self._header = None
|
|
59
|
+
self._n_transactions = None
|
|
60
|
+
self.size = len(raw_hex)
|
|
61
|
+
self.height = height
|
|
62
|
+
self.blk_file = blk_file
|
|
63
|
+
|
|
64
|
+
def __repr__(self):
|
|
65
|
+
return "Block(%s)" % self.hash
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_hex(cls, raw_hex):
|
|
69
|
+
"""Builds a block object from its bytes representation"""
|
|
70
|
+
return cls(raw_hex)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def hash(self):
|
|
74
|
+
"""Returns the block's hash (double sha256 of its 80 bytes header"""
|
|
75
|
+
if self._hash is None:
|
|
76
|
+
self._hash = format_hash(double_sha256(self.hex[:80]))
|
|
77
|
+
return self._hash
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def n_transactions(self):
|
|
81
|
+
"""Return the number of transactions contained in this block,
|
|
82
|
+
it is faster to use this than to use len(block.transactions)
|
|
83
|
+
as there's no need to parse all transactions to get this information
|
|
84
|
+
"""
|
|
85
|
+
if self._n_transactions is None:
|
|
86
|
+
self._n_transactions = decode_compactsize(self.hex[80:])[0]
|
|
87
|
+
|
|
88
|
+
return self._n_transactions
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def transactions(self):
|
|
92
|
+
"""Returns a list of the block's transactions represented
|
|
93
|
+
as Transaction objects"""
|
|
94
|
+
if self._transactions is None:
|
|
95
|
+
self._transactions = list(get_block_transactions(self.hex))
|
|
96
|
+
|
|
97
|
+
return self._transactions
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def header(self):
|
|
101
|
+
"""Returns a BlockHeader object corresponding to this block"""
|
|
102
|
+
if self._header is None:
|
|
103
|
+
self._header = BlockHeader.from_hex(self.hex[:80])
|
|
104
|
+
return self._header
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
from datetime import datetime, timezone
|
|
13
|
+
from bitcoin.core import CBlockHeader
|
|
14
|
+
|
|
15
|
+
from .utils import decode_uint32, format_hash
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BlockHeader(object):
|
|
19
|
+
"""Represents a block header"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, raw_hex):
|
|
22
|
+
self._version = None
|
|
23
|
+
self._previous_block_hash = None
|
|
24
|
+
self._merkle_root = None
|
|
25
|
+
self._timestamp = None
|
|
26
|
+
self._bits = None
|
|
27
|
+
self._nonce = None
|
|
28
|
+
self._difficulty = None
|
|
29
|
+
|
|
30
|
+
self.hex = raw_hex[:80]
|
|
31
|
+
|
|
32
|
+
def __repr__(self):
|
|
33
|
+
return "BlockHeader(previous_block_hash=%s)" % self.previous_block_hash
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_hex(cls, raw_hex):
|
|
37
|
+
"""Builds a BlockHeader object from its bytes representation"""
|
|
38
|
+
return cls(raw_hex)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def version(self):
|
|
42
|
+
"""Return the block's version"""
|
|
43
|
+
if self._version is None:
|
|
44
|
+
self._version = decode_uint32(self.hex[:4])
|
|
45
|
+
return self._version
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def previous_block_hash(self):
|
|
49
|
+
"""Return the hash of the previous block"""
|
|
50
|
+
if self._previous_block_hash is None:
|
|
51
|
+
self._previous_block_hash = format_hash(self.hex[4:36])
|
|
52
|
+
return self._previous_block_hash
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def merkle_root(self):
|
|
56
|
+
"""Returns the block's merkle root"""
|
|
57
|
+
if self._merkle_root is None:
|
|
58
|
+
self._merkle_root = format_hash(self.hex[36:68])
|
|
59
|
+
return self._merkle_root
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def timestamp(self):
|
|
63
|
+
"""Returns the timestamp of the block as a UTC datetime object"""
|
|
64
|
+
if self._timestamp is None:
|
|
65
|
+
self._timestamp = datetime.fromtimestamp(
|
|
66
|
+
decode_uint32(self.hex[68:72]), timezone.utc
|
|
67
|
+
)
|
|
68
|
+
return self._timestamp
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def bits(self):
|
|
72
|
+
"""Returns the bits (difficulty target) of the block"""
|
|
73
|
+
if self._bits is None:
|
|
74
|
+
self._bits = decode_uint32(self.hex[72:76])
|
|
75
|
+
return self._bits
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def nonce(self):
|
|
79
|
+
"""Returns the block's nonce"""
|
|
80
|
+
if self._nonce is None:
|
|
81
|
+
self._nonce = decode_uint32(self.hex[76:80])
|
|
82
|
+
return self._nonce
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def difficulty(self):
|
|
86
|
+
"""Returns the block's difficulty target as a float"""
|
|
87
|
+
if self._difficulty is None:
|
|
88
|
+
self._difficulty = CBlockHeader.calc_difficulty(self.bits)
|
|
89
|
+
|
|
90
|
+
return self._difficulty
|
|
@@ -0,0 +1,302 @@
|
|
|
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
|
+
import os
|
|
13
|
+
import mmap
|
|
14
|
+
import struct
|
|
15
|
+
import pickle
|
|
16
|
+
import stat
|
|
17
|
+
|
|
18
|
+
from blockchain_parser.transaction import Transaction
|
|
19
|
+
from blockchain_parser.index import DBTransactionIndex
|
|
20
|
+
from blockchain_parser import utils
|
|
21
|
+
from binascii import unhexlify
|
|
22
|
+
from binascii import hexlify
|
|
23
|
+
from .block import Block
|
|
24
|
+
from .index import DBBlockIndex
|
|
25
|
+
from .utils import format_hash
|
|
26
|
+
from .block_header import BlockHeader
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Constant separating blocks in the .blk files
|
|
30
|
+
BITCOIN_CONSTANT = b"\xf9\xbe\xb4\xd9"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_files(path):
|
|
34
|
+
"""
|
|
35
|
+
Given the path to the .bitcoin directory, returns the sorted list of .blk
|
|
36
|
+
files contained in that directory
|
|
37
|
+
"""
|
|
38
|
+
if not stat.S_ISDIR(os.stat(path)[stat.ST_MODE]):
|
|
39
|
+
return [path]
|
|
40
|
+
files = os.listdir(path)
|
|
41
|
+
files = [f for f in files if f.startswith("blk") and f.endswith(".dat")]
|
|
42
|
+
files = map(lambda x: os.path.join(path, x), files)
|
|
43
|
+
return sorted(files)
|
|
44
|
+
|
|
45
|
+
def get_undo_files(path):
|
|
46
|
+
"""
|
|
47
|
+
Given the path to the .bitcoin directory, returns the sorted list of rev*.dat
|
|
48
|
+
files contained in that directory
|
|
49
|
+
"""
|
|
50
|
+
if not stat.S_ISDIR(os.stat(path)[stat.ST_MODE]):
|
|
51
|
+
return [path]
|
|
52
|
+
files = os.listdir(path)
|
|
53
|
+
files = [f for f in files if f.startswith("rev") and f.endswith(".dat")]
|
|
54
|
+
files = map(lambda x: os.path.join(path, x), files)
|
|
55
|
+
return sorted(files)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_blocks(blockfile):
|
|
59
|
+
"""
|
|
60
|
+
Given the name of a .dat file, for every block contained in the file,
|
|
61
|
+
yields its raw hexadecimal value
|
|
62
|
+
"""
|
|
63
|
+
with open(blockfile, "rb") as f:
|
|
64
|
+
if os.name == 'nt':
|
|
65
|
+
size = os.path.getsize(f.name)
|
|
66
|
+
raw_data = mmap.mmap(f.fileno(), size, access=mmap.ACCESS_READ)
|
|
67
|
+
else:
|
|
68
|
+
# Unix-only call, will not work on Windows, see python doc.
|
|
69
|
+
raw_data = mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ)
|
|
70
|
+
length = len(raw_data)
|
|
71
|
+
offset = 0
|
|
72
|
+
block_count = 0
|
|
73
|
+
while offset < (length - 4):
|
|
74
|
+
if raw_data[offset:offset+4] == BITCOIN_CONSTANT:
|
|
75
|
+
offset += 4
|
|
76
|
+
size = struct.unpack("<I", raw_data[offset:offset+4])[0]
|
|
77
|
+
offset += 4 + size
|
|
78
|
+
block_count += 1
|
|
79
|
+
yield raw_data[offset-size:offset]
|
|
80
|
+
else:
|
|
81
|
+
offset += 1
|
|
82
|
+
raw_data.close()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_block(blockfile, offset):
|
|
86
|
+
"""Extracts a single block from the blockfile at the given offset"""
|
|
87
|
+
with open(blockfile, "rb") as f:
|
|
88
|
+
f.seek(offset - 4) # Size is present 4 bytes before the db offset
|
|
89
|
+
size, = struct.unpack("<I", f.read(4))
|
|
90
|
+
return f.read(size)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _chain_work(bits):
|
|
94
|
+
"""Return the proof-of-work contributed by a compact nBits target."""
|
|
95
|
+
if not bits:
|
|
96
|
+
return 0
|
|
97
|
+
exponent = bits >> 24
|
|
98
|
+
mantissa = bits & 0xffffff
|
|
99
|
+
if mantissa == 0:
|
|
100
|
+
return 0
|
|
101
|
+
if exponent <= 3:
|
|
102
|
+
target = mantissa >> (8 * (3 - exponent))
|
|
103
|
+
else:
|
|
104
|
+
target = mantissa << (8 * (exponent - 3))
|
|
105
|
+
if target <= 0:
|
|
106
|
+
return 0
|
|
107
|
+
return (1 << 256) // (target + 1)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def select_main_chain(block_indexes):
|
|
111
|
+
"""Return the most-work chain, oldest block first.
|
|
112
|
+
|
|
113
|
+
LevelDB stores block index entries under their hash, so iteration order
|
|
114
|
+
is not block height. Forks are resolved by total chain work, the same
|
|
115
|
+
rule Bitcoin Core uses to choose the active chain.
|
|
116
|
+
"""
|
|
117
|
+
if not block_indexes:
|
|
118
|
+
return []
|
|
119
|
+
|
|
120
|
+
by_hash = {}
|
|
121
|
+
for block in block_indexes:
|
|
122
|
+
by_hash[block.hash] = block
|
|
123
|
+
|
|
124
|
+
has_successor = {block.prev_hash for block in block_indexes}
|
|
125
|
+
tips = [block for block in block_indexes if block.hash not in has_successor]
|
|
126
|
+
if not tips:
|
|
127
|
+
tips = list(block_indexes)
|
|
128
|
+
|
|
129
|
+
memo = {}
|
|
130
|
+
|
|
131
|
+
def work_of(start_block):
|
|
132
|
+
stack = [start_block]
|
|
133
|
+
stacked = {start_block.hash}
|
|
134
|
+
while stack:
|
|
135
|
+
current = stack[-1]
|
|
136
|
+
if current.hash in memo:
|
|
137
|
+
stack.pop()
|
|
138
|
+
stacked.discard(current.hash)
|
|
139
|
+
continue
|
|
140
|
+
prev = by_hash.get(current.prev_hash)
|
|
141
|
+
if prev is not None and prev.hash not in memo:
|
|
142
|
+
if prev.hash in stacked:
|
|
143
|
+
prev = None
|
|
144
|
+
else:
|
|
145
|
+
stack.append(prev)
|
|
146
|
+
stacked.add(prev.hash)
|
|
147
|
+
continue
|
|
148
|
+
prev_work = memo[prev.hash] if prev is not None else 0
|
|
149
|
+
memo[current.hash] = prev_work + _chain_work(
|
|
150
|
+
getattr(current, "bits", None)
|
|
151
|
+
)
|
|
152
|
+
stack.pop()
|
|
153
|
+
stacked.discard(current.hash)
|
|
154
|
+
return memo[start_block.hash]
|
|
155
|
+
|
|
156
|
+
for tip in tips:
|
|
157
|
+
work_of(tip)
|
|
158
|
+
|
|
159
|
+
best = max(
|
|
160
|
+
tips,
|
|
161
|
+
key=lambda block: (memo[block.hash], block.height, block.hash),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
ordered = []
|
|
165
|
+
seen = set()
|
|
166
|
+
current = best
|
|
167
|
+
while current is not None and current.hash not in seen:
|
|
168
|
+
seen.add(current.hash)
|
|
169
|
+
ordered.append(current)
|
|
170
|
+
current = by_hash.get(current.prev_hash)
|
|
171
|
+
ordered.reverse()
|
|
172
|
+
return ordered
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def iter_chain_indexes(ordered, start=0, end=None):
|
|
176
|
+
"""Select main-chain index entries for a height window.
|
|
177
|
+
|
|
178
|
+
When ``end`` is greater than ``start``, heights ``start`` inclusive
|
|
179
|
+
through ``end`` exclusive are returned. When ``start`` is greater than
|
|
180
|
+
``end``, heights from ``start`` down to ``end`` are returned and both
|
|
181
|
+
ends are included. Entries without block data are omitted.
|
|
182
|
+
"""
|
|
183
|
+
if not ordered:
|
|
184
|
+
return []
|
|
185
|
+
|
|
186
|
+
by_height = {}
|
|
187
|
+
for block in ordered:
|
|
188
|
+
by_height[block.height] = block
|
|
189
|
+
|
|
190
|
+
if end is None:
|
|
191
|
+
end = max(by_height) + 1
|
|
192
|
+
|
|
193
|
+
if end < start:
|
|
194
|
+
heights = range(start, end - 1, -1)
|
|
195
|
+
else:
|
|
196
|
+
heights = range(start, end)
|
|
197
|
+
|
|
198
|
+
selected = []
|
|
199
|
+
for height in heights:
|
|
200
|
+
block = by_height.get(height)
|
|
201
|
+
if block is None:
|
|
202
|
+
continue
|
|
203
|
+
if getattr(block, "file", -1) == -1 or getattr(block, "data_pos", -1) == -1:
|
|
204
|
+
continue
|
|
205
|
+
selected.append(block)
|
|
206
|
+
return selected
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _load_block_indexes(index_path, cache):
|
|
210
|
+
"""Load block index entries from a pickle cache or from LevelDB."""
|
|
211
|
+
block_indexes = None
|
|
212
|
+
if cache and os.path.exists(cache):
|
|
213
|
+
with open(cache, 'rb') as f:
|
|
214
|
+
block_indexes = pickle.load(f)
|
|
215
|
+
if not block_indexes or not hasattr(block_indexes[0], "bits"):
|
|
216
|
+
block_indexes = None
|
|
217
|
+
|
|
218
|
+
if block_indexes is None:
|
|
219
|
+
import plyvel
|
|
220
|
+
with plyvel.DB(index_path, compression=None) as db:
|
|
221
|
+
# Block index entries are stored with keys prefixed by 'b'
|
|
222
|
+
with db.iterator(prefix=b'b') as iterator:
|
|
223
|
+
block_indexes = [
|
|
224
|
+
DBBlockIndex(format_hash(k[1:]), v) for k, v in iterator
|
|
225
|
+
]
|
|
226
|
+
if cache:
|
|
227
|
+
with open(cache, 'wb') as f:
|
|
228
|
+
pickle.dump(block_indexes, f)
|
|
229
|
+
return block_indexes
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class Blockchain(object):
|
|
233
|
+
"""Represent the blockchain contained in the series of .blk files
|
|
234
|
+
maintained by bitcoind.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
def __init__(self, path):
|
|
238
|
+
self.path = path
|
|
239
|
+
|
|
240
|
+
def get_unordered_blocks(self):
|
|
241
|
+
"""Yields the blocks contained in the .blk files as is,
|
|
242
|
+
without ordering them according to height.
|
|
243
|
+
"""
|
|
244
|
+
for blk_file in get_files(self.path):
|
|
245
|
+
for raw_block in get_blocks(blk_file):
|
|
246
|
+
yield Block(raw_block, None, os.path.split(blk_file)[1])
|
|
247
|
+
|
|
248
|
+
def get_ordered_blocks(self, index, start=0, end=None, cache=None):
|
|
249
|
+
"""Yield main-chain blocks in height order.
|
|
250
|
+
|
|
251
|
+
``index`` is the path of Bitcoin Core's LevelDB block index. ``start``
|
|
252
|
+
and ``end`` are block heights. When ``end`` is greater than ``start``,
|
|
253
|
+
heights ``start`` inclusive through ``end`` exclusive are yielded.
|
|
254
|
+
When ``start`` is greater than ``end``, blocks are yielded from
|
|
255
|
+
``start`` down to ``end`` and both heights are included.
|
|
256
|
+
|
|
257
|
+
``cache`` stores the parsed index with pickle. Delete that file to
|
|
258
|
+
pick up blocks written after the cache was created. Header-only and
|
|
259
|
+
pruned entries are skipped.
|
|
260
|
+
"""
|
|
261
|
+
block_indexes = _load_block_indexes(index, cache)
|
|
262
|
+
ordered = select_main_chain(block_indexes)
|
|
263
|
+
for blk_idx in iter_chain_indexes(ordered, start, end):
|
|
264
|
+
blk_file = os.path.join(self.path, "blk%05d.dat" % blk_idx.file)
|
|
265
|
+
yield Block(get_block(blk_file, blk_idx.data_pos), blk_idx.height)
|
|
266
|
+
|
|
267
|
+
def get_transaction(self, txid, db):
|
|
268
|
+
"""Yields the transaction contained in the .blk files as a python
|
|
269
|
+
object, similar to
|
|
270
|
+
https://developer.bitcoin.org/reference/rpc/getrawtransaction.html
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
byte_arr = bytearray.fromhex(txid)
|
|
274
|
+
byte_arr.reverse()
|
|
275
|
+
tx_hash = hexlify(b't').decode('utf-8') + \
|
|
276
|
+
hexlify(byte_arr).decode('utf-8')
|
|
277
|
+
|
|
278
|
+
tx_hash_fmtd = unhexlify(tx_hash)
|
|
279
|
+
raw_hex = db.get(tx_hash_fmtd)
|
|
280
|
+
if raw_hex is None:
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
tx_idx = DBTransactionIndex(utils.format_hash(tx_hash_fmtd), raw_hex)
|
|
284
|
+
blk_file = os.path.join(self.path, "blk%05d.dat" % tx_idx.blockfile_no)
|
|
285
|
+
raw_hex = get_block(blk_file, tx_idx.file_offset)
|
|
286
|
+
|
|
287
|
+
offset = tx_idx.block_offset
|
|
288
|
+
|
|
289
|
+
transaction_data = raw_hex[80:]
|
|
290
|
+
block_header_data = raw_hex[:80]
|
|
291
|
+
# Try from 1024 (1KiB) -> 1073741824 (1GiB) slice widths
|
|
292
|
+
for j in range(0, 20):
|
|
293
|
+
try:
|
|
294
|
+
block_header = BlockHeader.from_hex(block_header_data)
|
|
295
|
+
offset_e = offset + (1024 * 2 ** j)
|
|
296
|
+
transaction = Transaction.from_hex(
|
|
297
|
+
transaction_data[offset:offset_e])
|
|
298
|
+
return [block_header, transaction]
|
|
299
|
+
except Exception:
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
return None
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from struct import unpack
|
|
2
|
+
|
|
3
|
+
from .utils import format_hash
|
|
4
|
+
|
|
5
|
+
BLOCK_HAVE_DATA = 8
|
|
6
|
+
BLOCK_HAVE_UNDO = 16
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _read_varint(raw_hex):
|
|
10
|
+
"""
|
|
11
|
+
Reads the weird format of VarInt present in src/serialize.h of bitcoin core
|
|
12
|
+
and being used for storing data in the leveldb.
|
|
13
|
+
This is not the VARINT format described for general bitcoin serialization
|
|
14
|
+
use.
|
|
15
|
+
"""
|
|
16
|
+
n = 0
|
|
17
|
+
pos = 0
|
|
18
|
+
while True:
|
|
19
|
+
data = raw_hex[pos]
|
|
20
|
+
pos += 1
|
|
21
|
+
n = (n << 7) | (data & 0x7f)
|
|
22
|
+
if data & 0x80 == 0:
|
|
23
|
+
return n, pos
|
|
24
|
+
n += 1
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DBBlockIndex(object):
|
|
28
|
+
def __init__(self, blk_hash, raw_hex):
|
|
29
|
+
self.hash = blk_hash
|
|
30
|
+
pos = 0
|
|
31
|
+
n_version, i = _read_varint(raw_hex[pos:])
|
|
32
|
+
pos += i
|
|
33
|
+
self.height, i = _read_varint(raw_hex[pos:])
|
|
34
|
+
pos += i
|
|
35
|
+
self.status, i = _read_varint(raw_hex[pos:])
|
|
36
|
+
pos += i
|
|
37
|
+
self.n_tx, i = _read_varint(raw_hex[pos:])
|
|
38
|
+
pos += i
|
|
39
|
+
if self.status & (BLOCK_HAVE_DATA | BLOCK_HAVE_UNDO):
|
|
40
|
+
self.file, i = _read_varint(raw_hex[pos:])
|
|
41
|
+
pos += i
|
|
42
|
+
else:
|
|
43
|
+
self.file = -1
|
|
44
|
+
|
|
45
|
+
if self.status & BLOCK_HAVE_DATA:
|
|
46
|
+
self.data_pos, i = _read_varint(raw_hex[pos:])
|
|
47
|
+
pos += i
|
|
48
|
+
else:
|
|
49
|
+
self.data_pos = -1
|
|
50
|
+
if self.status & BLOCK_HAVE_UNDO:
|
|
51
|
+
self.undo_pos, i = _read_varint(raw_hex[pos:])
|
|
52
|
+
pos += i
|
|
53
|
+
|
|
54
|
+
assert (pos + 80 == len(raw_hex))
|
|
55
|
+
self.version, p, m, self.time, self.bits, self.nonce = unpack(
|
|
56
|
+
"<I32s32sIII",
|
|
57
|
+
raw_hex[-80:]
|
|
58
|
+
)
|
|
59
|
+
self.prev_hash = format_hash(p)
|
|
60
|
+
self.merkle_root = format_hash(m)
|
|
61
|
+
|
|
62
|
+
def __repr__(self):
|
|
63
|
+
return "DBBlockIndex(%s, height=%d, file_no=%d, file_pos=%d)" \
|
|
64
|
+
% (self.hash, self.height, self.file, self.data_pos)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DBTransactionIndex(object):
|
|
68
|
+
def __init__(self, txn_hash, raw_hex):
|
|
69
|
+
self.hash = txn_hash
|
|
70
|
+
pos = 0
|
|
71
|
+
self.blockfile_no, i = _read_varint(raw_hex[pos:])
|
|
72
|
+
pos += i
|
|
73
|
+
self.file_offset, i = _read_varint(raw_hex[pos:])
|
|
74
|
+
pos += i
|
|
75
|
+
self.block_offset, i = _read_varint(raw_hex[pos:])
|
|
76
|
+
|
|
77
|
+
def __repr__(self):
|
|
78
|
+
return "DBTransactionIndex(%s, blockfile_no=%d, " \
|
|
79
|
+
"file_offset=%d, block_offset=%d)" \
|
|
80
|
+
% (self.hash, self.blockfile_no,
|
|
81
|
+
self.file_offset, self.block_offset)
|