Changeset View
Changeset View
Standalone View
Standalone View
test/functional/mempool_packages.py
#!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||
# Copyright (c) 2014-2019 The Bitcoin Core developers | # Copyright (c) 2014-2019 The Bitcoin Core developers | ||||
# Distributed under the MIT software license, see the accompanying | # Distributed under the MIT software license, see the accompanying | ||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php. | # file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
"""Test descendant package tracking code.""" | """Test descendant package tracking code.""" | ||||
from decimal import Decimal | from decimal import Decimal | ||||
from test_framework.p2p import P2PTxInvStore | from test_framework.p2p import P2PTxInvStore | ||||
from test_framework.test_framework import BitcoinTestFramework | from test_framework.test_framework import BitcoinTestFramework | ||||
from test_framework.util import assert_equal, assert_raises_rpc_error, satoshi_round | from test_framework.util import assert_equal, satoshi_round | ||||
# default limits | |||||
MAX_ANCESTORS = 50 | MAX_ANCESTORS = 50 | ||||
MAX_DESCENDANTS = 50 | |||||
# custom limits for node1 | |||||
MAX_ANCESTORS_CUSTOM = 5 | |||||
FAR_IN_THE_FUTURE = 2000000000 | |||||
class MempoolPackagesTest(BitcoinTestFramework): | class MempoolPackagesTest(BitcoinTestFramework): | ||||
def set_test_params(self): | def set_test_params(self): | ||||
self.num_nodes = 2 | self.num_nodes = 2 | ||||
common_params = [ | self.extra_args = [["-maxorphantx=1000"]] * self.num_nodes | ||||
"-maxorphantx=1000", | |||||
"-deprecatedrpc=mempool_ancestors_descendants", | |||||
# This test tests mempool ancestor chain limits, which are no longer | |||||
# enforced after wellington, so we need to force wellington to | |||||
# activate in the distant future | |||||
f"-wellingtonactivationtime={FAR_IN_THE_FUTURE}", | |||||
] | |||||
self.extra_args = [ | |||||
common_params, | |||||
common_params + [f"-limitancestorcount={MAX_ANCESTORS_CUSTOM}"], | |||||
] | |||||
def skip_test_if_missing_module(self): | def skip_test_if_missing_module(self): | ||||
self.skip_if_no_wallet() | self.skip_if_no_wallet() | ||||
# Build a transaction that spends parent_txid:vout | # Build a transaction that spends parent_txid:vout | ||||
# Return amount sent | # Return amount sent | ||||
def chain_transaction(self, node, parent_txid, vout, value, fee, num_outputs): | def chain_transaction(self, node, parent_txid, vout, value, fee, num_outputs): | ||||
send_value = satoshi_round((value - fee) / num_outputs) | send_value = satoshi_round((value - fee) / num_outputs) | ||||
Show All 11 Lines | class MempoolPackagesTest(BitcoinTestFramework): | ||||
def run_test(self): | def run_test(self): | ||||
# Mine some blocks and have them mature. | # Mine some blocks and have them mature. | ||||
# keep track of invs | # keep track of invs | ||||
peer_inv_store = self.nodes[0].add_p2p_connection(P2PTxInvStore()) | peer_inv_store = self.nodes[0].add_p2p_connection(P2PTxInvStore()) | ||||
self.generate(self.nodes[0], 101) | self.generate(self.nodes[0], 101) | ||||
utxo = self.nodes[0].listunspent(10) | utxo = self.nodes[0].listunspent(10) | ||||
txid = utxo[0]["txid"] | txid = utxo[0]["txid"] | ||||
vout = utxo[0]["vout"] | |||||
value = utxo[0]["amount"] | value = utxo[0]["amount"] | ||||
assert "ancestorcount" not in utxo[0] | |||||
assert "ancestorsize" not in utxo[0] | |||||
assert "ancestorfees" not in utxo[0] | |||||
fee = Decimal("100") | fee = Decimal("100") | ||||
# MAX_ANCESTORS transactions off a confirmed tx should be fine | # MAX_ANCESTORS transactions off a confirmed tx should be fine | ||||
chain = [] | chain = [] | ||||
ancestor_size = 0 | |||||
ancestor_fees = Decimal(0) | |||||
for i in range(MAX_ANCESTORS): | for i in range(MAX_ANCESTORS): | ||||
(txid, sent_value) = self.chain_transaction( | (txid, sent_value) = self.chain_transaction( | ||||
self.nodes[0], txid, 0, value, fee, 1 | self.nodes[0], txid, 0, value, fee, 1 | ||||
) | ) | ||||
value = sent_value | value = sent_value | ||||
chain.append(txid) | chain.append(txid) | ||||
# Check that listunspent ancestor{count, size, fees} yield the | |||||
# correct results | |||||
wallet_unspent = self.nodes[0].listunspent(minconf=0) | |||||
this_unspent = next( | |||||
utxo_info for utxo_info in wallet_unspent if utxo_info["txid"] == txid | |||||
) | |||||
assert_equal(this_unspent["ancestorcount"], i + 1) | |||||
ancestor_size += self.nodes[0].getrawtransaction(txid=txid, verbose=True)[ | |||||
"size" | |||||
] | |||||
assert_equal(this_unspent["ancestorsize"], ancestor_size) | |||||
ancestor_fees -= self.nodes[0].gettransaction(txid=txid)["fee"] | |||||
assert_equal(this_unspent["ancestorfees"], ancestor_fees) | |||||
# Wait until mempool transactions have passed initial broadcast | # Wait until mempool transactions have passed initial broadcast | ||||
# (sent inv and received getdata) | # (sent inv and received getdata) | ||||
# Otherwise, getrawmempool may be inconsistent with getmempoolentry if | # Otherwise, getrawmempool may be inconsistent with getmempoolentry if | ||||
# unbroadcast changes in between | # unbroadcast changes in between | ||||
peer_inv_store.wait_for_broadcast(chain) | peer_inv_store.wait_for_broadcast(chain) | ||||
# Check mempool has MAX_ANCESTORS transactions in it, and descendant and ancestor | # Check mempool has MAX_ANCESTORS transactions in it | ||||
# count and fees should look correct | |||||
mempool = self.nodes[0].getrawmempool(True) | mempool = self.nodes[0].getrawmempool(True) | ||||
assert_equal(len(mempool), MAX_ANCESTORS) | assert_equal(len(mempool), MAX_ANCESTORS) | ||||
descendant_count = 1 | descendant_count = 1 | ||||
descendant_fees = 0 | descendant_fees = 0 | ||||
descendant_size = 0 | descendant_size = 0 | ||||
assert_equal(ancestor_size, sum([mempool[tx]["size"] for tx in mempool])) | ancestor_size = sum([mempool[tx]["size"] for tx in mempool]) | ||||
ancestor_count = MAX_ANCESTORS | ancestor_count = MAX_ANCESTORS | ||||
assert_equal( | ancestor_fees = sum([mempool[tx]["fees"]["base"] for tx in mempool]) | ||||
ancestor_fees, sum([mempool[tx]["fees"]["base"] for tx in mempool]) | |||||
) | |||||
descendants = [] | descendants = [] | ||||
ancestors = list(chain) | ancestors = list(chain) | ||||
for x in reversed(chain): | for x in reversed(chain): | ||||
# Check that getmempoolentry is consistent with getrawmempool | # Check that getmempoolentry is consistent with getrawmempool | ||||
entry = self.nodes[0].getmempoolentry(x) | entry = self.nodes[0].getmempoolentry(x) | ||||
assert_equal(entry, mempool[x]) | assert_equal(entry, mempool[x]) | ||||
# Check that the descendant calculations are correct | |||||
assert_equal(mempool[x]["descendantcount"], descendant_count) | |||||
descendant_fees += mempool[x]["fees"]["base"] | descendant_fees += mempool[x]["fees"]["base"] | ||||
assert_equal(mempool[x]["fees"]["modified"], mempool[x]["fees"]["base"]) | assert_equal(mempool[x]["fees"]["modified"], mempool[x]["fees"]["base"]) | ||||
assert_equal(mempool[x]["fees"]["descendant"], descendant_fees) | |||||
descendant_size += mempool[x]["size"] | descendant_size += mempool[x]["size"] | ||||
assert_equal(mempool[x]["descendantsize"], descendant_size) | |||||
descendant_count += 1 | descendant_count += 1 | ||||
# Check that ancestor calculations are correct | |||||
assert_equal(mempool[x]["ancestorcount"], ancestor_count) | |||||
assert_equal(mempool[x]["fees"]["ancestor"], ancestor_fees) | |||||
assert_equal(mempool[x]["ancestorsize"], ancestor_size) | |||||
ancestor_size -= mempool[x]["size"] | ancestor_size -= mempool[x]["size"] | ||||
ancestor_fees -= mempool[x]["fees"]["base"] | ancestor_fees -= mempool[x]["fees"]["base"] | ||||
ancestor_count -= 1 | ancestor_count -= 1 | ||||
# Check that parent/child list is correct | # Check that parent/child list is correct | ||||
assert_equal(mempool[x]["spentby"], descendants[-1:]) | assert_equal(mempool[x]["spentby"], descendants[-1:]) | ||||
assert_equal(mempool[x]["depends"], ancestors[-2:-1]) | assert_equal(mempool[x]["depends"], ancestors[-2:-1]) | ||||
# Check that getmempooldescendants is correct | # Check that getmempooldescendants is correct | ||||
assert_equal( | assert_equal( | ||||
sorted(descendants), sorted(self.nodes[0].getmempooldescendants(x)) | sorted(descendants), sorted(self.nodes[0].getmempooldescendants(x)) | ||||
) | ) | ||||
# Check getmempooldescendants verbose output is correct | # Check getmempooldescendants verbose output is correct | ||||
for descendant, dinfo in ( | for descendant, dinfo in ( | ||||
self.nodes[0].getmempooldescendants(x, True).items() | self.nodes[0].getmempooldescendants(x, True).items() | ||||
): | ): | ||||
assert_equal(dinfo["depends"], [chain[chain.index(descendant) - 1]]) | assert_equal(dinfo["depends"], [chain[chain.index(descendant) - 1]]) | ||||
if dinfo["descendantcount"] > 1: | |||||
assert_equal(dinfo["spentby"], [chain[chain.index(descendant) + 1]]) | |||||
else: | |||||
assert_equal(dinfo["spentby"], []) | |||||
descendants.append(x) | descendants.append(x) | ||||
# Check that getmempoolancestors is correct | # Check that getmempoolancestors is correct | ||||
ancestors.remove(x) | ancestors.remove(x) | ||||
assert_equal( | assert_equal( | ||||
sorted(ancestors), sorted(self.nodes[0].getmempoolancestors(x)) | sorted(ancestors), sorted(self.nodes[0].getmempoolancestors(x)) | ||||
) | ) | ||||
# Check that getmempoolancestors verbose output is correct | # Check that getmempoolancestors verbose output is correct | ||||
for ancestor, ainfo in self.nodes[0].getmempoolancestors(x, True).items(): | for ancestor, ainfo in self.nodes[0].getmempoolancestors(x, True).items(): | ||||
assert_equal(ainfo["spentby"], [chain[chain.index(ancestor) + 1]]) | assert_equal(ainfo["spentby"], [chain[chain.index(ancestor) + 1]]) | ||||
if ainfo["ancestorcount"] > 1: | |||||
assert_equal(ainfo["depends"], [chain[chain.index(ancestor) - 1]]) | # Check we covered all the ancestors | ||||
else: | assert_equal(ancestor_size, 0) | ||||
assert_equal(ainfo["depends"], []) | assert_equal(ancestor_fees, 0) | ||||
# Check that getmempoolancestors/getmempooldescendants correctly handle | # Check that getmempoolancestors/getmempooldescendants correctly handle | ||||
# verbose=true | # verbose=true | ||||
v_ancestors = self.nodes[0].getmempoolancestors(chain[-1], True) | v_ancestors = self.nodes[0].getmempoolancestors(chain[-1], True) | ||||
assert_equal(len(v_ancestors), len(chain) - 1) | assert_equal(len(v_ancestors), len(chain) - 1) | ||||
for x in v_ancestors.keys(): | for x in v_ancestors.keys(): | ||||
assert_equal(mempool[x], v_ancestors[x]) | assert_equal(mempool[x], v_ancestors[x]) | ||||
assert chain[-1] not in v_ancestors.keys() | assert chain[-1] not in v_ancestors.keys() | ||||
v_descendants = self.nodes[0].getmempooldescendants(chain[0], True) | v_descendants = self.nodes[0].getmempooldescendants(chain[0], True) | ||||
assert_equal(len(v_descendants), len(chain) - 1) | assert_equal(len(v_descendants), len(chain) - 1) | ||||
for x in v_descendants.keys(): | for x in v_descendants.keys(): | ||||
assert_equal(mempool[x], v_descendants[x]) | assert_equal(mempool[x], v_descendants[x]) | ||||
assert chain[0] not in v_descendants.keys() | assert chain[0] not in v_descendants.keys() | ||||
# Check that ancestor modified fees includes fee deltas from | |||||
# prioritisetransaction | |||||
self.nodes[0].prioritisetransaction(txid=chain[0], fee_delta=1000) | |||||
mempool = self.nodes[0].getrawmempool(True) | |||||
ancestor_fees = 0 | |||||
for x in chain: | |||||
ancestor_fees += mempool[x]["fees"]["base"] | |||||
assert_equal( | |||||
mempool[x]["fees"]["ancestor"], ancestor_fees + Decimal("10.00") | |||||
) | |||||
# Undo the prioritisetransaction for later tests | |||||
self.nodes[0].prioritisetransaction(txid=chain[0], fee_delta=-1000) | |||||
# Check that descendant modified fees includes fee deltas from | |||||
# prioritisetransaction | |||||
self.nodes[0].prioritisetransaction(txid=chain[-1], fee_delta=1000) | |||||
mempool = self.nodes[0].getrawmempool(True) | |||||
descendant_fees = 0 | |||||
for x in reversed(chain): | |||||
descendant_fees += mempool[x]["fees"]["base"] | |||||
assert_equal( | |||||
mempool[x]["fees"]["descendant"], descendant_fees + Decimal("10.00") | |||||
) | |||||
# Adding one more transaction on to the chain should fail. | |||||
assert_raises_rpc_error( | |||||
-26, | |||||
"too-long-mempool-chain", | |||||
self.chain_transaction, | |||||
self.nodes[0], | |||||
txid, | |||||
vout, | |||||
value, | |||||
fee, | |||||
1, | |||||
) | |||||
# Check that prioritising a tx before it's added to the mempool works | # Check that prioritising a tx before it's added to the mempool works | ||||
# First clear the mempool by mining a block. | # First clear the mempool by mining a block. | ||||
self.generate(self.nodes[0], 1) | self.generate(self.nodes[0], 1) | ||||
assert_equal(len(self.nodes[0].getrawmempool()), 0) | assert_equal(len(self.nodes[0].getrawmempool()), 0) | ||||
# Prioritise a transaction that has been mined, then add it back to the | # Prioritise a transaction that has been mined, then add it back to the | ||||
# mempool by using invalidateblock. | # mempool by using invalidateblock. | ||||
self.nodes[0].prioritisetransaction(txid=chain[-1], fee_delta=2000) | self.nodes[0].prioritisetransaction(txid=chain[-1], fee_delta=2000) | ||||
self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) | self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) | ||||
# Keep node1's tip synced with node0 | # Keep node1's tip synced with node0 | ||||
self.nodes[1].invalidateblock(self.nodes[1].getbestblockhash()) | self.nodes[1].invalidateblock(self.nodes[1].getbestblockhash()) | ||||
# Now check that the transaction is in the mempool, with the right | # Now check that the transaction is in the mempool, with the right | ||||
# modified fee | # modified fee | ||||
mempool = self.nodes[0].getrawmempool(True) | mempool = self.nodes[0].getrawmempool(True) | ||||
descendant_fees = 0 | |||||
for x in reversed(chain): | for x in reversed(chain): | ||||
descendant_fees += mempool[x]["fees"]["base"] | |||||
if x == chain[-1]: | if x == chain[-1]: | ||||
assert_equal( | assert_equal( | ||||
mempool[x]["fees"]["modified"], | mempool[x]["fees"]["modified"], | ||||
mempool[x]["fees"]["base"] + satoshi_round(20.00), | mempool[x]["fees"]["base"] + satoshi_round(20.00), | ||||
) | ) | ||||
assert_equal( | |||||
mempool[x]["fees"]["descendant"], descendant_fees + satoshi_round(20.00) | |||||
) | |||||
# Check that node1's mempool is as expected (-> custom ancestor limit) | # Check that node1's mempool is as expected | ||||
mempool0 = self.nodes[0].getrawmempool(False) | mempool0 = self.nodes[0].getrawmempool(False) | ||||
mempool1 = self.nodes[1].getrawmempool(False) | mempool1 = self.nodes[1].getrawmempool(False) | ||||
assert_equal(len(mempool1), MAX_ANCESTORS_CUSTOM) | assert_equal(mempool0, mempool1) | ||||
assert set(mempool1).issubset(set(mempool0)) | |||||
for tx in chain[:MAX_ANCESTORS_CUSTOM]: | |||||
assert tx in mempool1 | |||||
# TODO: more detailed check of node1's mempool (fees etc.) | # TODO: more detailed check of node1's mempool (fees etc.) | ||||
# check transaction unbroadcast info (should be false if in both | # check transaction unbroadcast info (should be false if in both | ||||
# mempools) | # mempools) | ||||
mempool = self.nodes[0].getrawmempool(True) | mempool = self.nodes[0].getrawmempool(True) | ||||
for tx in mempool: | for tx in mempool: | ||||
assert_equal(mempool[tx]["unbroadcast"], False) | assert_equal(mempool[tx]["unbroadcast"], False) | ||||
# TODO: test ancestor size limits | |||||
# Now test descendant chain limits | |||||
txid = utxo[1]["txid"] | |||||
value = utxo[1]["amount"] | |||||
vout = utxo[1]["vout"] | |||||
transaction_package = [] | |||||
tx_children = [] | |||||
# First create one parent tx with 10 children | |||||
(txid, sent_value) = self.chain_transaction( | |||||
self.nodes[0], txid, vout, value, fee, 10 | |||||
) | |||||
parent_transaction = txid | |||||
for i in range(10): | |||||
transaction_package.append({"txid": txid, "vout": i, "amount": sent_value}) | |||||
# Sign and send up to MAX_DESCENDANT transactions chained off the | |||||
# parent tx | |||||
for _ in range(MAX_DESCENDANTS - 1): | |||||
utxo = transaction_package.pop(0) | |||||
(txid, sent_value) = self.chain_transaction( | |||||
self.nodes[0], utxo["txid"], utxo["vout"], utxo["amount"], fee, 10 | |||||
) | |||||
if utxo["txid"] is parent_transaction: | |||||
tx_children.append(txid) | |||||
for j in range(10): | |||||
transaction_package.append( | |||||
{"txid": txid, "vout": j, "amount": sent_value} | |||||
) | |||||
mempool = self.nodes[0].getrawmempool(True) | |||||
assert_equal(mempool[parent_transaction]["descendantcount"], MAX_DESCENDANTS) | |||||
assert_equal( | |||||
sorted(mempool[parent_transaction]["spentby"]), sorted(tx_children) | |||||
) | |||||
for child in tx_children: | |||||
assert_equal(mempool[child]["depends"], [parent_transaction]) | |||||
# Sending one more chained transaction will fail | |||||
utxo = transaction_package.pop(0) | |||||
assert_raises_rpc_error( | |||||
-26, | |||||
"too-long-mempool-chain", | |||||
self.chain_transaction, | |||||
self.nodes[0], | |||||
utxo["txid"], | |||||
utxo["vout"], | |||||
utxo["amount"], | |||||
fee, | |||||
10, | |||||
) | |||||
# TODO: check that node1's mempool is as expected | |||||
# TODO: test descendant size limits | |||||
# Test reorg handling | |||||
# First, the basics: | |||||
self.generate(self.nodes[0], 1) | |||||
self.nodes[1].invalidateblock(self.nodes[0].getbestblockhash()) | |||||
self.nodes[1].reconsiderblock(self.nodes[0].getbestblockhash()) | |||||
# Now test the case where node1 has a transaction T in its mempool that | |||||
# depends on transactions A and B which are in a mined block, and the | |||||
# block containing A and B is disconnected, AND B is not accepted back | |||||
# into node1's mempool because its ancestor count is too high. | |||||
# Create 8 transactions, like so: | |||||
# Tx0 -> Tx1 (vout0) | |||||
# \--> Tx2 (vout1) -> Tx3 -> Tx4 -> Tx5 -> Tx6 -> Tx7 | |||||
# | |||||
# Mine them in the next block, then generate a new tx8 that spends | |||||
# Tx1 and Tx7, and add to node1's mempool, then disconnect the | |||||
# last block. | |||||
# Create tx0 with 2 outputs | |||||
utxo = self.nodes[0].listunspent() | |||||
txid = utxo[0]["txid"] | |||||
value = utxo[0]["amount"] | |||||
vout = utxo[0]["vout"] | |||||
send_value = satoshi_round((value - fee) / 2) | |||||
inputs = [{"txid": txid, "vout": vout}] | |||||
outputs = {} | |||||
for _ in range(2): | |||||
outputs[self.nodes[0].getnewaddress()] = send_value | |||||
rawtx = self.nodes[0].createrawtransaction(inputs, outputs) | |||||
signedtx = self.nodes[0].signrawtransactionwithwallet(rawtx) | |||||
txid = self.nodes[0].sendrawtransaction(signedtx["hex"]) | |||||
tx0_id = txid | |||||
value = send_value | |||||
# Create tx1 | |||||
tx1_id, _ = self.chain_transaction(self.nodes[0], tx0_id, 0, value, fee, 1) | |||||
# Create tx2-7 | |||||
vout = 1 | |||||
txid = tx0_id | |||||
for _ in range(6): | |||||
(txid, sent_value) = self.chain_transaction( | |||||
self.nodes[0], txid, vout, value, fee, 1 | |||||
) | |||||
vout = 0 | |||||
value = sent_value | |||||
# Mine these in a block | |||||
self.generate(self.nodes[0], 1) | |||||
# Now generate tx8, with a big fee | |||||
inputs = [{"txid": tx1_id, "vout": 0}, {"txid": txid, "vout": 0}] | |||||
outputs = {self.nodes[0].getnewaddress(): send_value + value - 4 * fee} | |||||
rawtx = self.nodes[0].createrawtransaction(inputs, outputs) | |||||
signedtx = self.nodes[0].signrawtransactionwithwallet(rawtx) | |||||
txid = self.nodes[0].sendrawtransaction(signedtx["hex"]) | |||||
self.sync_mempools() | |||||
# Now try to disconnect the tip on each node... | |||||
self.nodes[1].invalidateblock(self.nodes[1].getbestblockhash()) | |||||
self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) | |||||
self.sync_blocks() | |||||
if __name__ == "__main__": | if __name__ == "__main__": | ||||
MempoolPackagesTest().main() | MempoolPackagesTest().main() |