|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# Copyright (c) 2020 The Bitcoin Core developers |
| 3 | +# Distributed under the MIT software license, see the accompanying |
| 4 | +# file COPYING or http://www.opensource.org/licenses/mit-license.php. |
| 5 | +"""Tests that a mempool transaction expires after a given timeout and that its |
| 6 | +children are removed as well. |
| 7 | +
|
| 8 | +Both the default expiry timeout defined by DEFAULT_MEMPOOL_EXPIRY and a user |
| 9 | +definable expiry timeout via the '-mempoolexpiry=<n>' command line argument |
| 10 | +(<n> is the timeout in hours) are tested. |
| 11 | +""" |
| 12 | + |
| 13 | +from datetime import timedelta |
| 14 | + |
| 15 | +from test_framework.test_framework import BitcoinTestFramework |
| 16 | +from test_framework.util import ( |
| 17 | + assert_equal, |
| 18 | + assert_raises_rpc_error, |
| 19 | + find_vout_for_address, |
| 20 | +) |
| 21 | + |
| 22 | +DEFAULT_MEMPOOL_EXPIRY = 336 # hours |
| 23 | +CUSTOM_MEMPOOL_EXPIRY = 10 # hours |
| 24 | + |
| 25 | + |
| 26 | +class MempoolExpiryTest(BitcoinTestFramework): |
| 27 | + def set_test_params(self): |
| 28 | + self.num_nodes = 1 |
| 29 | + |
| 30 | + def skip_test_if_missing_module(self): |
| 31 | + self.skip_if_no_wallet() |
| 32 | + |
| 33 | + def test_transaction_expiry(self, timeout): |
| 34 | + """Tests that a transaction expires after the expiry timeout and its |
| 35 | + children are removed as well.""" |
| 36 | + node = self.nodes[0] |
| 37 | + |
| 38 | + # Send a parent transaction that will expire. |
| 39 | + parent_address = node.getnewaddress() |
| 40 | + parent_txid = node.sendtoaddress(parent_address, 1.0) |
| 41 | + |
| 42 | + # Set the mocktime to the arrival time of the parent transaction. |
| 43 | + entry_time = node.getmempoolentry(parent_txid)['time'] |
| 44 | + node.setmocktime(entry_time) |
| 45 | + |
| 46 | + # Create child transaction spending the parent transaction |
| 47 | + vout = find_vout_for_address(node, parent_txid, parent_address) |
| 48 | + inputs = [{'txid': parent_txid, 'vout': vout}] |
| 49 | + outputs = {node.getnewaddress(): 0.99} |
| 50 | + child_raw = node.createrawtransaction(inputs, outputs) |
| 51 | + child_signed = node.signrawtransactionwithwallet(child_raw)['hex'] |
| 52 | + |
| 53 | + # Let half of the timeout elapse and broadcast the child transaction. |
| 54 | + half_expiry_time = entry_time + int(60 * 60 * timeout/2) |
| 55 | + node.setmocktime(half_expiry_time) |
| 56 | + child_txid = node.sendrawtransaction(child_signed) |
| 57 | + self.log.info('Broadcast child transaction after {} hours.'.format( |
| 58 | + timedelta(seconds=(half_expiry_time-entry_time)))) |
| 59 | + |
| 60 | + # Let most of the timeout elapse and check that the parent tx is still |
| 61 | + # in the mempool. |
| 62 | + nearly_expiry_time = entry_time + 60 * 60 * timeout - 5 |
| 63 | + node.setmocktime(nearly_expiry_time) |
| 64 | + # Expiry of mempool transactions is only checked when a new transaction |
| 65 | + # is added to the to the mempool. |
| 66 | + node.sendtoaddress(node.getnewaddress(), 1.0) |
| 67 | + self.log.info('Test parent tx not expired after {} hours.'.format( |
| 68 | + timedelta(seconds=(nearly_expiry_time-entry_time)))) |
| 69 | + assert_equal(entry_time, node.getmempoolentry(parent_txid)['time']) |
| 70 | + |
| 71 | + # Transaction should be evicted from the mempool after the expiry time |
| 72 | + # has passed. |
| 73 | + expiry_time = entry_time + 60 * 60 * timeout + 5 |
| 74 | + node.setmocktime(expiry_time) |
| 75 | + # Expiry of mempool transactions is only checked when a new transaction |
| 76 | + # is added to the to the mempool. |
| 77 | + node.sendtoaddress(node.getnewaddress(), 1.0) |
| 78 | + self.log.info('Test parent tx expiry after {} hours.'.format( |
| 79 | + timedelta(seconds=(expiry_time-entry_time)))) |
| 80 | + assert_raises_rpc_error(-5, 'Transaction not in mempool', |
| 81 | + node.getmempoolentry, parent_txid) |
| 82 | + |
| 83 | + # The child transaction should be removed from the mempool as well. |
| 84 | + self.log.info('Test child tx is evicted as well.') |
| 85 | + assert_raises_rpc_error(-5, 'Transaction not in mempool', |
| 86 | + node.getmempoolentry, child_txid) |
| 87 | + |
| 88 | + def run_test(self): |
| 89 | + self.log.info('Test default mempool expiry timeout of %d hours.' % |
| 90 | + DEFAULT_MEMPOOL_EXPIRY) |
| 91 | + self.test_transaction_expiry(DEFAULT_MEMPOOL_EXPIRY) |
| 92 | + |
| 93 | + self.log.info('Test custom mempool expiry timeout of %d hours.' % |
| 94 | + CUSTOM_MEMPOOL_EXPIRY) |
| 95 | + self.restart_node(0, ['-mempoolexpiry=%d' % CUSTOM_MEMPOOL_EXPIRY]) |
| 96 | + self.test_transaction_expiry(CUSTOM_MEMPOOL_EXPIRY) |
| 97 | + |
| 98 | + |
| 99 | +if __name__ == '__main__': |
| 100 | + MempoolExpiryTest().main() |
0 commit comments