|
7 | 7 | Test mempool update of transaction descendants/ancestors information (count, size)
|
8 | 8 | when transactions have been re-added from a disconnected block to the mempool.
|
9 | 9 | """
|
| 10 | +from decimal import Decimal |
10 | 11 | from math import ceil
|
11 | 12 | import time
|
12 | 13 |
|
| 14 | +from test_framework.blocktools import ( |
| 15 | + create_block, |
| 16 | + create_coinbase, |
| 17 | +) |
13 | 18 | from test_framework.test_framework import BitcoinTestFramework
|
14 | 19 | from test_framework.util import assert_equal
|
15 | 20 | from test_framework.wallet import MiniWallet
|
16 | 21 |
|
| 22 | +MAX_DISCONNECTED_TX_POOL_BYTES = 20_000_000 |
17 | 23 |
|
18 | 24 | class MempoolUpdateFromBlockTest(BitcoinTestFramework):
|
19 | 25 | def set_test_params(self):
|
20 | 26 | self.num_nodes = 1
|
21 |
| - self.extra_args = [['-limitdescendantsize=1000', '-limitancestorsize=1000', '-limitancestorcount=100']] |
| 27 | + self.extra_args = [['-limitdescendantsize=1000', '-limitancestorsize=1000', '-limitancestorcount=100', '-datacarriersize=100000']] |
| 28 | + |
| 29 | + def create_empty_fork(self, fork_length): |
| 30 | + ''' |
| 31 | + Creates a fork using first node's chaintip as the starting point. |
| 32 | + Returns a list of blocks to submit in order. |
| 33 | + ''' |
| 34 | + tip = int(self.nodes[0].getbestblockhash(), 16) |
| 35 | + height = self.nodes[0].getblockcount() |
| 36 | + block_time = self.nodes[0].getblock(self.nodes[0].getbestblockhash())['time'] + 1 |
| 37 | + |
| 38 | + blocks = [] |
| 39 | + for _ in range(fork_length): |
| 40 | + block = create_block(tip, create_coinbase(height + 1), block_time) |
| 41 | + block.solve() |
| 42 | + blocks.append(block) |
| 43 | + tip = block.sha256 |
| 44 | + block_time += 1 |
| 45 | + height += 1 |
| 46 | + |
| 47 | + return blocks |
22 | 48 |
|
23 | 49 | def transaction_graph_test(self, size, n_tx_to_mine=None, fee=100_000):
|
24 | 50 | """Create an acyclic tournament (a type of directed graph) of transactions and use it for testing.
|
@@ -97,10 +123,66 @@ def transaction_graph_test(self, size, n_tx_to_mine=None, fee=100_000):
|
97 | 123 | assert_equal(entry['ancestorcount'], k + 1)
|
98 | 124 | assert_equal(entry['ancestorsize'], sum(tx_size[0:(k + 1)]))
|
99 | 125 |
|
| 126 | + self.generate(self.nodes[0], 1) |
| 127 | + assert_equal(self.nodes[0].getrawmempool(), []) |
| 128 | + wallet.rescan_utxos() |
| 129 | + |
| 130 | + def test_max_disconnect_pool_bytes(self): |
| 131 | + self.log.info('Creating independent transactions to test MAX_DISCONNECTED_TX_POOL_BYTES limit during reorg') |
| 132 | + |
| 133 | + # Generate coins for the hundreds of transactions we will make |
| 134 | + parent_target_vsize = 100_000 |
| 135 | + wallet = MiniWallet(self.nodes[0]) |
| 136 | + self.generate(wallet, (MAX_DISCONNECTED_TX_POOL_BYTES // parent_target_vsize) + 100) |
| 137 | + |
| 138 | + assert_equal(self.nodes[0].getrawmempool(), []) |
| 139 | + |
| 140 | + # Set up empty fork blocks ahead of time, needs to be longer than full fork made later |
| 141 | + fork_blocks = self.create_empty_fork(fork_length=60) |
| 142 | + |
| 143 | + large_std_txs = [] |
| 144 | + # Add children to ensure they're recursively removed if disconnectpool trimming of parent occurs |
| 145 | + small_child_txs = [] |
| 146 | + aggregate_serialized_size = 0 |
| 147 | + while aggregate_serialized_size < MAX_DISCONNECTED_TX_POOL_BYTES: |
| 148 | + # Mine parents in FIFO order via fee ordering |
| 149 | + large_std_txs.append(wallet.create_self_transfer(target_vsize=parent_target_vsize, fee=Decimal("0.00400000") - (Decimal("0.00001000") * len(large_std_txs)))) |
| 150 | + small_child_txs.append(wallet.create_self_transfer(utxo_to_spend=large_std_txs[-1]['new_utxo'])) |
| 151 | + # Slight underestimate of dynamic cost, so we'll be over during reorg |
| 152 | + aggregate_serialized_size += len(large_std_txs[-1]["tx"].serialize()) |
| 153 | + |
| 154 | + for large_std_tx in large_std_txs: |
| 155 | + self.nodes[0].sendrawtransaction(large_std_tx["hex"]) |
| 156 | + |
| 157 | + assert_equal(self.nodes[0].getmempoolinfo()["size"], len(large_std_txs)) |
| 158 | + |
| 159 | + # Mine non-empty chain that will be reorged shortly |
| 160 | + self.generate(self.nodes[0], len(fork_blocks) - 1) |
| 161 | + assert_equal(self.nodes[0].getrawmempool(), []) |
| 162 | + |
| 163 | + # Stick children in mempool, evicted with parent potentially |
| 164 | + for small_child_tx in small_child_txs: |
| 165 | + self.nodes[0].sendrawtransaction(small_child_tx["hex"]) |
| 166 | + |
| 167 | + assert_equal(self.nodes[0].getmempoolinfo()["size"], len(small_child_txs)) |
| 168 | + |
| 169 | + # Reorg back before the first block in the series, should drop something |
| 170 | + # but not all, and any time parent is dropped, child is also removed |
| 171 | + for block in fork_blocks: |
| 172 | + self.nodes[0].submitblock(block.serialize().hex()) |
| 173 | + mempool = self.nodes[0].getrawmempool() |
| 174 | + expected_parent_count = len(large_std_txs) - 2 |
| 175 | + assert_equal(len(mempool), expected_parent_count * 2) |
| 176 | + |
| 177 | + # The txns at the end of the list, or most recently confirmed, should have been trimmed |
| 178 | + assert_equal([tx["txid"] in mempool for tx in large_std_txs], [tx["txid"] in mempool for tx in small_child_txs]) |
| 179 | + assert_equal([tx["txid"] in mempool for tx in large_std_txs], [True] * expected_parent_count + [False] * 2) |
| 180 | + |
100 | 181 | def run_test(self):
|
101 | 182 | # Use batch size limited by DEFAULT_ANCESTOR_LIMIT = 25 to not fire "too many unconfirmed parents" error.
|
102 | 183 | self.transaction_graph_test(size=100, n_tx_to_mine=[25, 50, 75])
|
103 | 184 |
|
| 185 | + self.test_max_disconnect_pool_bytes() |
104 | 186 |
|
105 | 187 | if __name__ == '__main__':
|
106 | 188 | MempoolUpdateFromBlockTest(__file__).main()
|
0 commit comments