mempool_persist.py
1 #!/usr/bin/env python3 2 # Copyright (c) 2014-present 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 """Test mempool persistence. 6 7 By default, bitcoind will dump mempool on shutdown and 8 then reload it on startup. This can be overridden with 9 the -persistmempool=0 command line option. 10 11 Test is as follows: 12 13 - start node0, node1 and node2. node1 has -persistmempool=0 14 - create 5 transactions on node2 to its own address. Note that these 15 are not sent to node0 or node1 addresses because we don't want 16 them to be saved in the wallet. 17 - check that node0 and node1 have 5 transactions in their mempools 18 - shutdown all nodes. 19 - startup node0. Verify that it still has 5 transactions 20 in its mempool. Shutdown node0. This tests that by default the 21 mempool is persistent. 22 - startup node1. Verify that its mempool is empty. Shutdown node1. 23 This tests that with -persistmempool=0, the mempool is not 24 dumped to disk when the node is shut down. 25 - Restart node0 with -persistmempool=0. Verify that its mempool is 26 empty. Shutdown node0. This tests that with -persistmempool=0, 27 the mempool is not loaded from disk on start up. 28 - Restart node0 with -persistmempool. Verify that it has 5 29 transactions in its mempool. This tests that -persistmempool=0 30 does not overwrite a previously valid mempool stored on disk. 31 - Remove node0 mempool.dat and verify savemempool RPC recreates it 32 and verify that node1 can load it and has 5 transactions in its 33 mempool. 34 - Verify that savemempool throws when the RPC is called if 35 node1 can't write to disk. 36 37 """ 38 from decimal import Decimal 39 import os 40 import time 41 42 from test_framework.p2p import P2PTxInvStore 43 from test_framework.test_framework import BitcoinTestFramework 44 from test_framework.util import ( 45 assert_equal, 46 assert_greater_than_or_equal, 47 assert_raises_rpc_error, 48 ) 49 from test_framework.wallet import MiniWallet, COIN 50 51 52 class MempoolPersistTest(BitcoinTestFramework): 53 def set_test_params(self): 54 self.num_nodes = 3 55 self.extra_args = [[], ["-persistmempool=0"], []] 56 self.uses_wallet = None 57 58 def run_test(self): 59 self.mini_wallet = MiniWallet(self.nodes[2]) 60 if self.is_wallet_compiled(): 61 self.nodes[2].createwallet( 62 wallet_name="watch", 63 disable_private_keys=True, 64 load_on_startup=False, 65 ) 66 wallet_watch = self.nodes[2].get_wallet_rpc("watch") 67 assert_equal([{'success': True}], wallet_watch.importdescriptors([{'desc': self.mini_wallet.get_descriptor(), 'timestamp': 0}])) 68 69 self.log.debug("Send 5 transactions from node2 (to its own address)") 70 tx_creation_time_lower = int(time.time()) 71 for _ in range(5): 72 last_txid = self.mini_wallet.send_self_transfer(from_node=self.nodes[2])["txid"] 73 if self.is_wallet_compiled(): 74 self.nodes[2].syncwithvalidationinterfacequeue() # Flush mempool to wallet 75 node2_balance = wallet_watch.getbalance() 76 self.sync_all() 77 tx_creation_time_higher = int(time.time()) 78 79 self.log.debug("Verify that node0 and node1 have 5 transactions in their mempools") 80 assert_equal(len(self.nodes[0].getrawmempool()), 5) 81 assert_equal(len(self.nodes[1].getrawmempool()), 5) 82 83 total_fee_old = self.nodes[0].getmempoolinfo()['total_fee'] 84 85 self.log.debug("Prioritize a transaction on node0") 86 fees = self.nodes[0].getmempoolentry(txid=last_txid)['fees'] 87 assert_equal(fees['base'], fees['modified']) 88 self.nodes[0].prioritisetransaction(txid=last_txid, fee_delta=1000) 89 fees = self.nodes[0].getmempoolentry(txid=last_txid)['fees'] 90 assert_equal(fees['base'] + Decimal('0.00001000'), fees['modified']) 91 92 self.log.info('Check the total base fee is unchanged after prioritisetransaction') 93 assert_equal(total_fee_old, self.nodes[0].getmempoolinfo()['total_fee']) 94 assert_equal(total_fee_old, sum(v['fees']['base'] for k, v in self.nodes[0].getrawmempool(verbose=True).items())) 95 96 last_entry = self.nodes[0].getmempoolentry(txid=last_txid) 97 tx_creation_time = last_entry['time'] 98 assert_greater_than_or_equal(tx_creation_time, tx_creation_time_lower) 99 assert_greater_than_or_equal(tx_creation_time_higher, tx_creation_time) 100 101 # disconnect nodes & make a txn that remains in the unbroadcast set. 102 self.disconnect_nodes(0, 1) 103 assert_equal(len(self.nodes[0].getpeerinfo()), 0) 104 assert_equal(len(self.nodes[0].p2ps), 0) 105 self.mini_wallet.send_self_transfer(from_node=self.nodes[0]) 106 107 # Test persistence of prioritisation for transactions not in the mempool. 108 # Create a tx and prioritise but don't submit until after the restart. 109 tx_prioritised_not_submitted = self.mini_wallet.create_self_transfer() 110 self.nodes[0].prioritisetransaction(txid=tx_prioritised_not_submitted['txid'], fee_delta=9999) 111 112 self.log.debug("Stop-start the nodes. Verify that node0 has the transactions in its mempool and node1 does not. Verify that node2 calculates its balance correctly after loading wallet transactions.") 113 self.stop_nodes() 114 # Give this node a head-start, so we can be "extra-sure" that it didn't load anything later 115 # Also don't store the mempool, to keep the datadir clean 116 self.start_node(1, extra_args=["-persistmempool=0"]) 117 self.start_node(0) 118 self.start_node(2) 119 assert self.nodes[0].getmempoolinfo()["loaded"] # start_node is blocking on the mempool being loaded 120 assert self.nodes[2].getmempoolinfo()["loaded"] 121 assert_equal(len(self.nodes[0].getrawmempool()), 6) 122 assert_equal(len(self.nodes[2].getrawmempool()), 5) 123 # The others have loaded their mempool. If node_1 loaded anything, we'd probably notice by now: 124 assert_equal(len(self.nodes[1].getrawmempool()), 0) 125 126 self.log.debug('Verify prioritization is loaded correctly') 127 fees = self.nodes[0].getmempoolentry(txid=last_txid)['fees'] 128 assert_equal(fees['base'] + Decimal('0.00001000'), fees['modified']) 129 130 self.log.debug('Verify all fields are loaded correctly') 131 new_entry = self.nodes[0].getmempoolentry(txid=last_txid) 132 assert_equal({**last_entry, "clusterid": None}, {**new_entry, "clusterid": None}) 133 self.nodes[0].sendrawtransaction(tx_prioritised_not_submitted['hex']) 134 entry_prioritised_before_restart = self.nodes[0].getmempoolentry(txid=tx_prioritised_not_submitted['txid']) 135 assert_equal(entry_prioritised_before_restart['fees']['base'] + Decimal('0.00009999'), entry_prioritised_before_restart['fees']['modified']) 136 137 # Verify accounting of mempool transactions after restart is correct 138 if self.is_wallet_compiled(): 139 self.nodes[2].loadwallet("watch") 140 wallet_watch = self.nodes[2].get_wallet_rpc("watch") 141 self.nodes[2].syncwithvalidationinterfacequeue() # Flush mempool to wallet 142 assert_equal(node2_balance, wallet_watch.getbalance()) 143 144 mempooldat0 = os.path.join(self.nodes[0].chain_path, 'mempool.dat') 145 mempooldat1 = os.path.join(self.nodes[1].chain_path, 'mempool.dat') 146 147 self.log.debug("Force -persistmempool=0 node1 to savemempool to disk via RPC") 148 assert not os.path.exists(mempooldat1) 149 result1 = self.nodes[1].savemempool() 150 assert os.path.isfile(mempooldat1) 151 assert_equal(result1['filename'], mempooldat1) 152 os.remove(mempooldat1) 153 154 self.log.debug("Stop-start node0 with -persistmempool=0. Verify that it doesn't load its mempool.dat file.") 155 self.stop_nodes() 156 self.start_node(0, extra_args=["-persistmempool=0"]) 157 assert self.nodes[0].getmempoolinfo()["loaded"] 158 assert_equal(len(self.nodes[0].getrawmempool()), 0) 159 160 self.log.debug("Import mempool at runtime to node0.") 161 assert_equal({}, self.nodes[0].importmempool(mempooldat0)) 162 assert_equal(len(self.nodes[0].getrawmempool()), 7) 163 fees = self.nodes[0].getmempoolentry(txid=last_txid)["fees"] 164 assert_equal(fees["base"], fees["modified"]) 165 assert_equal({}, self.nodes[0].importmempool(mempooldat0, {"apply_fee_delta_priority": True, "apply_unbroadcast_set": True})) 166 assert_equal(2, self.nodes[0].getmempoolinfo()["unbroadcastcount"]) 167 fees = self.nodes[0].getmempoolentry(txid=last_txid)["fees"] 168 assert_equal(fees["base"] + Decimal("0.00001000"), fees["modified"]) 169 170 self.log.debug("Stop-start node0. Verify that it has the transactions in its mempool.") 171 self.stop_nodes() 172 self.start_node(0) 173 assert self.nodes[0].getmempoolinfo()["loaded"] 174 assert_equal(len(self.nodes[0].getrawmempool()), 7) 175 176 self.log.debug("Remove the mempool.dat file. Verify that savemempool to disk via RPC re-creates it") 177 os.remove(mempooldat0) 178 result0 = self.nodes[0].savemempool() 179 assert os.path.isfile(mempooldat0) 180 assert_equal(result0['filename'], mempooldat0) 181 182 self.log.debug("Stop nodes, make node1 use mempool.dat from node0. Verify it has 7 transactions") 183 os.rename(mempooldat0, mempooldat1) 184 self.stop_nodes() 185 self.start_node(1, extra_args=["-persistmempool"]) 186 assert self.nodes[1].getmempoolinfo()["loaded"] 187 assert_equal(len(self.nodes[1].getrawmempool()), 7) 188 189 self.log.debug("Prevent bitcoind from writing mempool.dat to disk. Verify that `savemempool` fails") 190 # to test the exception we are creating a tmp folder called mempool.dat.new 191 # which is an implementation detail that could change and break this test 192 mempooldotnew1 = mempooldat1 + '.new' 193 os.mkdir(mempooldotnew1) 194 assert_raises_rpc_error(-1, "Unable to dump mempool to disk", self.nodes[1].savemempool) 195 os.rmdir(mempooldotnew1) 196 197 self.test_importmempool_union() 198 self.test_persist_unbroadcast() 199 200 def test_persist_unbroadcast(self): 201 node0 = self.nodes[0] 202 self.start_node(0) 203 self.start_node(2) 204 205 # clear out mempool 206 self.generate(node0, 1, sync_fun=self.no_op) 207 208 # ensure node0 doesn't have any connections 209 # make a transaction that will remain in the unbroadcast set 210 assert_equal(len(node0.getpeerinfo()), 0) 211 assert_equal(len(node0.p2ps), 0) 212 self.mini_wallet.send_self_transfer(from_node=node0) 213 214 # shutdown, then startup with wallet disabled 215 self.restart_node(0, extra_args=["-disablewallet"]) 216 217 # check that txn gets broadcast due to unbroadcast logic 218 conn = node0.add_p2p_connection(P2PTxInvStore()) 219 node0.mockscheduler(16 * 60) # 15 min + 1 for buffer 220 self.wait_until(lambda: len(conn.get_invs()) == 1) 221 222 def test_importmempool_union(self): 223 self.log.debug("Submit different transactions to node0 and node1's mempools") 224 self.start_node(0) 225 self.start_node(2) 226 tx_node0 = self.mini_wallet.send_self_transfer(from_node=self.nodes[0]) 227 tx_node1 = self.mini_wallet.send_self_transfer(from_node=self.nodes[1]) 228 tx_node01 = self.mini_wallet.create_self_transfer() 229 tx_node01_secret = self.mini_wallet.create_self_transfer() 230 self.nodes[0].prioritisetransaction(tx_node01["txid"], 0, COIN) 231 self.nodes[0].prioritisetransaction(tx_node01_secret["txid"], 0, 2 * COIN) 232 self.nodes[1].prioritisetransaction(tx_node01_secret["txid"], 0, 3 * COIN) 233 self.nodes[0].sendrawtransaction(tx_node01["hex"]) 234 self.nodes[1].sendrawtransaction(tx_node01["hex"]) 235 assert tx_node0["txid"] in self.nodes[0].getrawmempool() 236 assert tx_node0["txid"] not in self.nodes[1].getrawmempool() 237 assert tx_node1["txid"] not in self.nodes[0].getrawmempool() 238 assert tx_node1["txid"] in self.nodes[1].getrawmempool() 239 assert tx_node01["txid"] in self.nodes[0].getrawmempool() 240 assert tx_node01["txid"] in self.nodes[1].getrawmempool() 241 assert tx_node01_secret["txid"] not in self.nodes[0].getrawmempool() 242 assert tx_node01_secret["txid"] not in self.nodes[1].getrawmempool() 243 244 self.log.debug("Check that importmempool can add txns without replacing the entire mempool") 245 mempooldat0 = str(self.nodes[0].chain_path / "mempool.dat") 246 result0 = self.nodes[0].savemempool() 247 assert_equal(mempooldat0, result0["filename"]) 248 assert_equal({}, self.nodes[1].importmempool(mempooldat0, {"apply_fee_delta_priority": True})) 249 # All transactions should be in node1's mempool now. 250 assert tx_node0["txid"] in self.nodes[1].getrawmempool() 251 assert tx_node1["txid"] in self.nodes[1].getrawmempool() 252 assert tx_node1["txid"] not in self.nodes[0].getrawmempool() 253 # For transactions that already existed, priority should be changed 254 entry_node01 = self.nodes[1].getmempoolentry(tx_node01["txid"]) 255 assert_equal(entry_node01["fees"]["base"] + 1, entry_node01["fees"]["modified"]) 256 # Deltas for not-yet-submitted transactions should be applied as well (prioritisation is stackable). 257 self.nodes[1].sendrawtransaction(tx_node01_secret["hex"]) 258 entry_node01_secret = self.nodes[1].getmempoolentry(tx_node01_secret["txid"]) 259 assert_equal(entry_node01_secret["fees"]["base"] + 5, entry_node01_secret["fees"]["modified"]) 260 self.stop_nodes() 261 262 263 if __name__ == "__main__": 264 MempoolPersistTest(__file__).main()