/ test / functional / mempool_persist.py
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()