Changeset View
Changeset View
Standalone View
Standalone View
test/functional/chronik_script_history.py
Show All 22 Lines | |||||
from test_framework.txtools import pad_tx | from test_framework.txtools import pad_tx | ||||
from test_framework.util import assert_equal, iter_chunks | from test_framework.util import assert_equal, iter_chunks | ||||
class ChronikScriptHistoryTest(BitcoinTestFramework): | class ChronikScriptHistoryTest(BitcoinTestFramework): | ||||
def set_test_params(self): | def set_test_params(self): | ||||
self.setup_clean_chain = True | self.setup_clean_chain = True | ||||
self.num_nodes = 1 | self.num_nodes = 1 | ||||
self.extra_args = [['-chronik']] | self.extra_args = [["-chronik"]] | ||||
self.rpc_timeout = 240 | self.rpc_timeout = 240 | ||||
def skip_test_if_missing_module(self): | def skip_test_if_missing_module(self): | ||||
self.skip_if_no_chronik() | self.skip_if_no_chronik() | ||||
def run_test(self): | def run_test(self): | ||||
from test_framework.chronik.client import ChronikClient, pb | from test_framework.chronik.client import ChronikClient, pb | ||||
from test_framework.chronik.test_data import genesis_cb_tx | from test_framework.chronik.test_data import genesis_cb_tx | ||||
node = self.nodes[0] | node = self.nodes[0] | ||||
chronik = ChronikClient('127.0.0.1', node.chronik_port, timeout=4) | chronik = ChronikClient("127.0.0.1", node.chronik_port, timeout=4) | ||||
peer = node.add_p2p_connection(P2PDataStore()) | peer = node.add_p2p_connection(P2PDataStore()) | ||||
mocktime = 1300000000 | mocktime = 1300000000 | ||||
node.setmocktime(mocktime) | node.setmocktime(mocktime) | ||||
assert_equal( | assert_equal( | ||||
chronik.script('', '').history().err(400).msg, | chronik.script("", "").history().err(400).msg, "400: Unknown script type: " | ||||
'400: Unknown script type: ') | ) | ||||
assert_equal( | assert_equal( | ||||
chronik.script('foo', '').history().err(400).msg, | chronik.script("foo", "").history().err(400).msg, | ||||
'400: Unknown script type: foo') | "400: Unknown script type: foo", | ||||
) | |||||
assert_equal( | assert_equal( | ||||
chronik.script('p2pkh', 'LILALI').history().err(400).msg, | chronik.script("p2pkh", "LILALI").history().err(400).msg, | ||||
"400: Invalid hex: Invalid character 'L' at position 0") | "400: Invalid hex: Invalid character 'L' at position 0", | ||||
) | |||||
assert_equal( | assert_equal( | ||||
chronik.script('other', 'LILALI').history().err(400).msg, | chronik.script("other", "LILALI").history().err(400).msg, | ||||
"400: Invalid hex: Invalid character 'L' at position 0") | "400: Invalid hex: Invalid character 'L' at position 0", | ||||
) | |||||
assert_equal( | assert_equal( | ||||
chronik.script('p2pkh', '').history().err(400).msg, | chronik.script("p2pkh", "").history().err(400).msg, | ||||
'400: Invalid payload for P2PKH: Invalid length, ' + | "400: Invalid payload for P2PKH: Invalid length, " | ||||
'expected 20 bytes but got 0 bytes') | + "expected 20 bytes but got 0 bytes", | ||||
) | |||||
assert_equal( | assert_equal( | ||||
chronik.script('p2pkh', 'aA').history().err(400).msg, | chronik.script("p2pkh", "aA").history().err(400).msg, | ||||
'400: Invalid payload for P2PKH: Invalid length, ' + | "400: Invalid payload for P2PKH: Invalid length, " | ||||
'expected 20 bytes but got 1 bytes') | + "expected 20 bytes but got 1 bytes", | ||||
) | |||||
assert_equal( | assert_equal( | ||||
chronik.script('p2sh', 'aaBB').history().err(400).msg, | chronik.script("p2sh", "aaBB").history().err(400).msg, | ||||
'400: Invalid payload for P2SH: Invalid length, ' + | "400: Invalid payload for P2SH: Invalid length, " | ||||
'expected 20 bytes but got 2 bytes') | + "expected 20 bytes but got 2 bytes", | ||||
) | |||||
assert_equal( | assert_equal( | ||||
chronik.script('p2pk', 'aaBBcc').history().err(400).msg, | chronik.script("p2pk", "aaBBcc").history().err(400).msg, | ||||
'400: Invalid payload for P2PK: Invalid length, ' + | "400: Invalid payload for P2PK: Invalid length, " | ||||
'expected one of [33, 65] but got 3 bytes') | + "expected one of [33, 65] but got 3 bytes", | ||||
) | |||||
assert_equal( | assert_equal( | ||||
chronik.script('p2pk', GENESIS_CB_PK) | chronik.script("p2pk", GENESIS_CB_PK) | ||||
.history(page=0, page_size=201).err(400).msg, | .history(page=0, page_size=201) | ||||
'400: Requested page size 201 is too big, maximum is 200') | .err(400) | ||||
.msg, | |||||
"400: Requested page size 201 is too big, maximum is 200", | |||||
) | |||||
assert_equal( | assert_equal( | ||||
chronik.script('p2pk', GENESIS_CB_PK) | chronik.script("p2pk", GENESIS_CB_PK) | ||||
.history(page=0, page_size=0).err(400).msg, | .history(page=0, page_size=0) | ||||
'400: Requested page size 0 is too small, minimum is 1') | .err(400) | ||||
.msg, | |||||
"400: Requested page size 0 is too small, minimum is 1", | |||||
) | |||||
assert_equal( | assert_equal( | ||||
chronik.script('p2pk', GENESIS_CB_PK) | chronik.script("p2pk", GENESIS_CB_PK) | ||||
.history(page=0, page_size=2**32).err(400).msg, | .history(page=0, page_size=2**32) | ||||
'400: Invalid param page_size: 4294967296, ' + | .err(400) | ||||
'number too large to fit in target type') | .msg, | ||||
"400: Invalid param page_size: 4294967296, " | |||||
+ "number too large to fit in target type", | |||||
) | |||||
assert_equal( | assert_equal( | ||||
chronik.script('p2pk', GENESIS_CB_PK) | chronik.script("p2pk", GENESIS_CB_PK) | ||||
.history(page=2**32, page_size=1).err(400).msg, | .history(page=2**32, page_size=1) | ||||
'400: Invalid param page: 4294967296, ' + | .err(400) | ||||
'number too large to fit in target type') | .msg, | ||||
"400: Invalid param page: 4294967296, " | |||||
+ "number too large to fit in target type", | |||||
) | |||||
# Handle overflow gracefully on 32-bit | # Handle overflow gracefully on 32-bit | ||||
assert_equal( | assert_equal( | ||||
chronik.script('p2pk', GENESIS_CB_PK) | chronik.script("p2pk", GENESIS_CB_PK) | ||||
.history(page=2**32 - 1, page_size=200) | .history(page=2**32 - 1, page_size=200) | ||||
.ok(), | .ok(), | ||||
pb.TxHistoryPage(num_pages=1, num_txs=1)) | pb.TxHistoryPage(num_pages=1, num_txs=1), | ||||
) | |||||
genesis_db_script_history = chronik.script('p2pk', GENESIS_CB_PK).history().ok() | genesis_db_script_history = chronik.script("p2pk", GENESIS_CB_PK).history().ok() | ||||
assert_equal(genesis_db_script_history, | assert_equal( | ||||
pb.TxHistoryPage(txs=[genesis_cb_tx()], | genesis_db_script_history, | ||||
num_pages=1, | pb.TxHistoryPage(txs=[genesis_cb_tx()], num_pages=1, num_txs=1), | ||||
num_txs=1)) | ) | ||||
script_type = 'p2sh' | script_type = "p2sh" | ||||
payload_hex = P2SH_OP_TRUE[2:-1].hex() | payload_hex = P2SH_OP_TRUE[2:-1].hex() | ||||
def check_tx_history(mempooltxs, blocktxs, *, page_size=25): | def check_tx_history(mempooltxs, blocktxs, *, page_size=25): | ||||
pages = list(iter_chunks(mempooltxs + blocktxs, page_size)) | pages = list(iter_chunks(mempooltxs + blocktxs, page_size)) | ||||
for page_num, page_txs in enumerate(pages): | for page_num, page_txs in enumerate(pages): | ||||
script_history = ( | script_history = ( | ||||
chronik.script(script_type, payload_hex) | chronik.script(script_type, payload_hex) | ||||
.history(page=page_num, page_size=page_size).ok() | .history(page=page_num, page_size=page_size) | ||||
.ok() | |||||
) | ) | ||||
assert_equal(script_history.num_pages, len(pages)) | assert_equal(script_history.num_pages, len(pages)) | ||||
assert_equal(script_history.num_txs, len(mempooltxs) + len(blocktxs)) | assert_equal(script_history.num_txs, len(mempooltxs) + len(blocktxs)) | ||||
for tx_idx, entry in enumerate(page_txs): | for tx_idx, entry in enumerate(page_txs): | ||||
script_tx = script_history.txs[tx_idx] | script_tx = script_history.txs[tx_idx] | ||||
if 'txid' in entry: | if "txid" in entry: | ||||
assert_equal(script_tx.txid[::-1].hex(), entry['txid']) | assert_equal(script_tx.txid[::-1].hex(), entry["txid"]) | ||||
if 'time_first_seen' in entry: | if "time_first_seen" in entry: | ||||
assert_equal( | assert_equal( | ||||
script_tx.time_first_seen, | script_tx.time_first_seen, entry["time_first_seen"] | ||||
entry['time_first_seen']) | ) | ||||
if 'block' in entry: | if "block" in entry: | ||||
block_height, block_hash = entry['block'] | block_height, block_hash = entry["block"] | ||||
assert_equal(script_tx.block, pb.BlockMetadata( | assert_equal( | ||||
script_tx.block, | |||||
pb.BlockMetadata( | |||||
hash=bytes.fromhex(block_hash)[::-1], | hash=bytes.fromhex(block_hash)[::-1], | ||||
height=block_height, | height=block_height, | ||||
timestamp=script_tx.block.timestamp, | timestamp=script_tx.block.timestamp, | ||||
)) | ), | ||||
) | |||||
# Generate 101 blocks to some address and verify pages | # Generate 101 blocks to some address and verify pages | ||||
blockhashes = self.generatetoaddress(node, 101, ADDRESS_ECREG_P2SH_OP_TRUE) | blockhashes = self.generatetoaddress(node, 101, ADDRESS_ECREG_P2SH_OP_TRUE) | ||||
blocktxs = [{'block': (i, blockhashes[i - 1])} for i in range(101, 0, -1)] | blocktxs = [{"block": (i, blockhashes[i - 1])} for i in range(101, 0, -1)] | ||||
check_tx_history([], blocktxs) | check_tx_history([], blocktxs) | ||||
check_tx_history([], blocktxs, page_size=200) | check_tx_history([], blocktxs, page_size=200) | ||||
# Undo last block & check history | # Undo last block & check history | ||||
node.invalidateblock(blockhashes[-1]) | node.invalidateblock(blockhashes[-1]) | ||||
check_tx_history([], blocktxs[1:]) | check_tx_history([], blocktxs[1:]) | ||||
check_tx_history([], blocktxs[1:], page_size=200) | check_tx_history([], blocktxs[1:], page_size=200) | ||||
# Create 1 block manually (with out-of-order block time) | # Create 1 block manually (with out-of-order block time) | ||||
coinbase_tx = create_coinbase(101) | coinbase_tx = create_coinbase(101) | ||||
coinbase_tx.vout[0].scriptPubKey = P2SH_OP_TRUE | coinbase_tx.vout[0].scriptPubKey = P2SH_OP_TRUE | ||||
coinbase_tx.rehash() | coinbase_tx.rehash() | ||||
block = create_block(int(blockhashes[-2], 16), | block = create_block(int(blockhashes[-2], 16), coinbase_tx, mocktime + 1000) | ||||
coinbase_tx, | |||||
mocktime + 1000) | |||||
block.solve() | block.solve() | ||||
peer.send_blocks_and_test([block], node) | peer.send_blocks_and_test([block], node) | ||||
blockhashes[-1] = block.hash | blockhashes[-1] = block.hash | ||||
# Blocks still ordered by block height | # Blocks still ordered by block height | ||||
blocktxs = [{'block': (i, blockhashes[i - 1])} for i in range(101, 0, -1)] | blocktxs = [{"block": (i, blockhashes[i - 1])} for i in range(101, 0, -1)] | ||||
check_tx_history([], blocktxs) | check_tx_history([], blocktxs) | ||||
check_tx_history([], blocktxs, page_size=200) | check_tx_history([], blocktxs, page_size=200) | ||||
# Generate 900 more blocks and verify | # Generate 900 more blocks and verify | ||||
# Total of 1001 txs for this script (a page in the DB is 1000 entries long) | # Total of 1001 txs for this script (a page in the DB is 1000 entries long) | ||||
blockhashes += self.generatetoaddress(node, 900, ADDRESS_ECREG_P2SH_OP_TRUE) | blockhashes += self.generatetoaddress(node, 900, ADDRESS_ECREG_P2SH_OP_TRUE) | ||||
blocktxs = [{'block': (i, blockhashes[i - 1])} for i in range(1001, 0, -1)] | blocktxs = [{"block": (i, blockhashes[i - 1])} for i in range(1001, 0, -1)] | ||||
check_tx_history([], blocktxs, page_size=200) | check_tx_history([], blocktxs, page_size=200) | ||||
coinvalue = 5000000000 | coinvalue = 5000000000 | ||||
cointxids = [] | cointxids = [] | ||||
for coinblockhash in blockhashes[:100]: | for coinblockhash in blockhashes[:100]: | ||||
coinblock = node.getblock(coinblockhash) | coinblock = node.getblock(coinblockhash) | ||||
cointxids.append(coinblock['tx'][0]) | cointxids.append(coinblock["tx"][0]) | ||||
mempool_txs = [] | mempool_txs = [] | ||||
mempool_txids = [] | mempool_txids = [] | ||||
# Send 10 mempool txs, each with their own mocktime | # Send 10 mempool txs, each with their own mocktime | ||||
mocktime_offsets = [0, 10, 10, 5, 0, 0, 12, 12, 10, 5] | mocktime_offsets = [0, 10, 10, 5, 0, 0, 12, 12, 10, 5] | ||||
for mocktime_offset in mocktime_offsets: | for mocktime_offset in mocktime_offsets: | ||||
cointxid = cointxids.pop(0) | cointxid = cointxids.pop(0) | ||||
tx = CTransaction() | tx = CTransaction() | ||||
tx.nVersion = 1 | tx.nVersion = 1 | ||||
tx.vin = [CTxIn(outpoint=COutPoint(int(cointxid, 16), 0), | tx.vin = [ | ||||
scriptSig=SCRIPTSIG_OP_TRUE)] | CTxIn( | ||||
outpoint=COutPoint(int(cointxid, 16), 0), | |||||
scriptSig=SCRIPTSIG_OP_TRUE, | |||||
) | |||||
] | |||||
tx.vout = [CTxOut(coinvalue - 1000, P2SH_OP_TRUE)] | tx.vout = [CTxOut(coinvalue - 1000, P2SH_OP_TRUE)] | ||||
pad_tx(tx) | pad_tx(tx) | ||||
mempool_txs.append(tx) | mempool_txs.append(tx) | ||||
node.setmocktime(mocktime + mocktime_offset) | node.setmocktime(mocktime + mocktime_offset) | ||||
txid = node.sendrawtransaction(tx.serialize().hex()) | txid = node.sendrawtransaction(tx.serialize().hex()) | ||||
mempool_txids.append(txid) | mempool_txids.append(txid) | ||||
def tx_sort_key(entry): | def tx_sort_key(entry): | ||||
time_first_seen = entry['time_first_seen'] | time_first_seen = entry["time_first_seen"] | ||||
txid = entry['txid'] | txid = entry["txid"] | ||||
if time_first_seen == 0: | if time_first_seen == 0: | ||||
time_first_seen = 1 << 64 | time_first_seen = 1 << 64 | ||||
if entry.get('is_coinbase', False): | if entry.get("is_coinbase", False): | ||||
txid = '' | txid = "" | ||||
return (time_first_seen, txid) | return (time_first_seen, txid) | ||||
mempooltxs = sorted([{'time_first_seen': mocktime + offset, 'txid': txid} | mempooltxs = sorted( | ||||
for (offset, txid) | [ | ||||
in zip(mocktime_offsets, mempool_txids)], | {"time_first_seen": mocktime + offset, "txid": txid} | ||||
for (offset, txid) in zip(mocktime_offsets, mempool_txids) | |||||
], | |||||
key=tx_sort_key, | key=tx_sort_key, | ||||
reverse=True) | reverse=True, | ||||
) | |||||
page_sizes = [1, 5, 7, 25, 111, 200] | page_sizes = [1, 5, 7, 25, 111, 200] | ||||
for page_size in page_sizes: | for page_size in page_sizes: | ||||
check_tx_history(mempooltxs, blocktxs, page_size=page_size) | check_tx_history(mempooltxs, blocktxs, page_size=page_size) | ||||
# Mine block with 5 conflicting txs | # Mine block with 5 conflicting txs | ||||
mine_txs = mempool_txs[5:] | mine_txs = mempool_txs[5:] | ||||
newblocktxs = [entry for entry in mempooltxs if entry['txid'] | newblocktxs = [ | ||||
not in mempool_txids[:5]] | entry for entry in mempooltxs if entry["txid"] not in mempool_txids[:5] | ||||
] | |||||
for idx, tx in enumerate(mempool_txs[:5]): | for idx, tx in enumerate(mempool_txs[:5]): | ||||
tx.nLockTime = 12 | tx.nLockTime = 12 | ||||
tx.rehash() | tx.rehash() | ||||
mine_txs.append(tx) | mine_txs.append(tx) | ||||
newblocktxs.append({'time_first_seen': 0, 'txid': tx.hash}) | newblocktxs.append({"time_first_seen": 0, "txid": tx.hash}) | ||||
height = 1002 | height = 1002 | ||||
coinbase_tx = create_coinbase(height) | coinbase_tx = create_coinbase(height) | ||||
coinbase_tx.vout[0].scriptPubKey = P2SH_OP_TRUE | coinbase_tx.vout[0].scriptPubKey = P2SH_OP_TRUE | ||||
coinbase_tx.rehash() | coinbase_tx.rehash() | ||||
block = create_block(int(blockhashes[-1], 16), | block = create_block(int(blockhashes[-1], 16), coinbase_tx, mocktime + 1100) | ||||
coinbase_tx, | |||||
mocktime + 1100) | |||||
block.nVersion = 5 | block.nVersion = 5 | ||||
block.vtx += mine_txs | block.vtx += mine_txs | ||||
make_conform_to_ctor(block) | make_conform_to_ctor(block) | ||||
block.hashMerkleRoot = block.calc_merkle_root() | block.hashMerkleRoot = block.calc_merkle_root() | ||||
block.solve() | block.solve() | ||||
peer.send_blocks_and_test([block], node) | peer.send_blocks_and_test([block], node) | ||||
newblocktxs.append( | newblocktxs.append( | ||||
{'time_first_seen': 0, 'txid': coinbase_tx.hash, 'is_coinbase': True}) | {"time_first_seen": 0, "txid": coinbase_tx.hash, "is_coinbase": True} | ||||
) | |||||
newblocktxs.sort(key=tx_sort_key, reverse=True) | newblocktxs.sort(key=tx_sort_key, reverse=True) | ||||
for blocktx in newblocktxs: | for blocktx in newblocktxs: | ||||
blocktx['block'] = (height, block.hash) | blocktx["block"] = (height, block.hash) | ||||
check_tx_history([], newblocktxs + blocktxs, page_size=25) | check_tx_history([], newblocktxs + blocktxs, page_size=25) | ||||
check_tx_history([], newblocktxs + blocktxs, page_size=200) | check_tx_history([], newblocktxs + blocktxs, page_size=200) | ||||
# Order for different page sizes is not guaranteed within blocks. | # Order for different page sizes is not guaranteed within blocks. | ||||
txs_individually = [ | txs_individually = [ | ||||
chronik.script( | chronik.script(script_type, payload_hex) | ||||
script_type, payload_hex).history(page=i, page_size=1).ok().txs[0] | .history(page=i, page_size=1) | ||||
.ok() | |||||
.txs[0] | |||||
for i in range(20) | for i in range(20) | ||||
] | ] | ||||
txs_bulk = list(chronik.script( | txs_bulk = list( | ||||
script_type, payload_hex).history(page=0, page_size=20).ok().txs) | chronik.script(script_type, payload_hex) | ||||
.history(page=0, page_size=20) | |||||
.ok() | |||||
.txs | |||||
) | |||||
# Contain the same txs, but not necessarily in the same order | # Contain the same txs, but not necessarily in the same order | ||||
assert_equal(sorted(txs_individually, key=lambda tx: tx.txid), | assert_equal( | ||||
sorted(txs_bulk, key=lambda tx: tx.txid)) | sorted(txs_individually, key=lambda tx: tx.txid), | ||||
sorted(txs_bulk, key=lambda tx: tx.txid), | |||||
) | |||||
if __name__ == '__main__': | if __name__ == "__main__": | ||||
ChronikScriptHistoryTest().main() | ChronikScriptHistoryTest().main() |