1 #!/usr/bin/env python3
2 # Copyright (c) 2017-2022 The Limenka 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 that the wallet resends transactions periodically."""
6 import time
7 8 from decimal import Decimal
9 10 from test_framework.blocktools import (
11 create_block,
12 create_coinbase,
13 )
14 from test_framework.messages import DEFAULT_MEMPOOL_EXPIRY_HOURS
15 from test_framework.p2p import P2PTxInvStore
16 from test_framework.test_framework import LimenkaTestFramework
17 from test_framework.util import (
18 assert_equal,
19 assert_raises_rpc_error,
20 get_fee,
21 try_rpc,
22 )
23 24 class ResendWalletTransactionsTest(LimenkaTestFramework):
25 def add_options(self, parser):
26 self.add_wallet_options(parser)
27 28 def set_test_params(self):
29 self.num_nodes = 1
30 31 def skip_test_if_missing_module(self):
32 self.skip_if_no_wallet()
33 34 def run_test(self):
35 node = self.nodes[0] # alias
36 37 peer_first = node.add_p2p_connection(P2PTxInvStore())
38 39 self.log.info("Create a new transaction and wait until it's broadcast")
40 parent_utxo, indep_utxo = node.listunspent()[:2]
41 addr = node.getnewaddress()
42 txid = node.send(outputs=[{addr: 1}], inputs=[parent_utxo])["txid"]
43 44 # Can take a few seconds due to transaction trickling
45 peer_first.wait_for_broadcast([txid])
46 47 # Add a second peer since txs aren't rebroadcast to the same peer (see m_tx_inventory_known_filter)
48 peer_second = node.add_p2p_connection(P2PTxInvStore())
49 50 self.log.info("Create a block")
51 # Create and submit a block without the transaction.
52 # Transactions are only rebroadcast if there has been a block at least five minutes
53 # after the last time we tried to broadcast. Use mocktime and give an extra minute to be sure.
54 block_time = int(time.time()) + 6 * 60
55 node.setmocktime(block_time)
56 block = create_block(int(node.getbestblockhash(), 16), create_coinbase(node.getblockcount() + 1), block_time)
57 block.solve()
58 node.submitblock(block.serialize().hex())
59 60 # Set correct m_best_block_time, which is used in ResubmitWalletTransactions
61 node.syncwithvalidationinterfacequeue()
62 now = int(time.time())
63 64 # Transaction should not be rebroadcast within first 12 hours
65 # Leave 2 mins for buffer
66 twelve_hrs = 12 * 60 * 60
67 two_min = 2 * 60
68 node.setmocktime(now + twelve_hrs - two_min)
69 node.mockscheduler(60) # Tell scheduler to call MaybeResendWalletTxs now
70 assert_equal(int(txid, 16) in peer_second.get_invs(), False)
71 72 self.log.info("Bump time & check that transaction is rebroadcast")
73 # Transaction should be rebroadcast approximately 24 hours in the future,
74 # but can range from 12-36. So bump 36 hours to be sure.
75 with node.assert_debug_log(['resubmit 1 unconfirmed transactions']):
76 node.setmocktime(now + 36 * 60 * 60)
77 # Tell scheduler to call MaybeResendWalletTxs now.
78 node.mockscheduler(60)
79 # Give some time for trickle to occur
80 node.setmocktime(now + 36 * 60 * 60 + 600)
81 peer_second.wait_for_broadcast([txid])
82 83 self.log.info("Chain of unconfirmed not-in-mempool txs are rebroadcast")
84 # This tests that the node broadcasts the parent transaction before the child transaction.
85 # To test that scenario, we need a method to reliably get a child transaction placed
86 # in mapWallet positioned before the parent. We cannot predict the position in mapWallet,
87 # but we can observe it using listreceivedbyaddress and other related RPCs.
88 #
89 # So we will create the child transaction, use listreceivedbyaddress to see what the
90 # ordering of mapWallet is, if the child is not before the parent, we will create a new
91 # child (via bumpfee) and remove the old child (via removeprunedfunds) until we get the
92 # ordering of child before parent.
93 child_inputs = [{"txid": txid, "vout": 0}]
94 child_txid = node.sendall(recipients=[addr], inputs=child_inputs)["txid"]
95 # Get the child tx's info for manual bumping
96 child_tx_info = node.gettransaction(txid=child_txid, verbose=True)
97 child_output_value = child_tx_info["decoded"]["vout"][0]["value"]
98 # Include an additional 1 vbyte buffer to handle when we have a smaller signature
99 additional_child_fee = get_fee(child_tx_info["decoded"]["vsize"] + 1, Decimal(0.00001100))
100 while True:
101 txids = node.listreceivedbyaddress(minconf=0, address_filter=addr)[0]["txids"]
102 if txids == [child_txid, txid]:
103 break
104 # Manually bump the tx
105 # The inputs and the output address stay the same, just changing the amount for the new fee
106 child_output_value -= additional_child_fee
107 bumped_raw = node.createrawtransaction(inputs=child_inputs, outputs=[{addr: child_output_value}])
108 bumped = node.signrawtransactionwithwallet(bumped_raw)
109 bumped_txid = node.decoderawtransaction(bumped["hex"])["txid"]
110 # Sometimes we will get a signature that is a little bit shorter than we expect which causes the
111 # feerate to be a bit higher, then the followup to be a bit lower. This results in a replacement
112 # that can't be broadcast. We can just skip that and keep grinding.
113 if try_rpc(-26, "insufficient fee, rejecting replacement", node.sendrawtransaction, bumped["hex"]):
114 continue
115 # The scheduler queue creates a copy of the added tx after
116 # send/bumpfee and re-adds it to the wallet (undoing the next
117 # removeprunedfunds). So empty the scheduler queue:
118 node.syncwithvalidationinterfacequeue()
119 node.removeprunedfunds(child_txid)
120 child_txid = bumped_txid
121 entry_time = node.getmempoolentry(child_txid)["time"]
122 123 block_time = entry_time + 6 * 60
124 node.setmocktime(block_time)
125 block = create_block(int(node.getbestblockhash(), 16), create_coinbase(node.getblockcount() + 1), block_time)
126 block.solve()
127 node.submitblock(block.serialize().hex())
128 # Set correct m_best_block_time, which is used in ResubmitWalletTransactions
129 node.syncwithvalidationinterfacequeue()
130 131 evict_time = block_time + 60 * 60 * DEFAULT_MEMPOOL_EXPIRY_HOURS + 5
132 # Flush out currently scheduled resubmit attempt now so that there can't be one right between eviction and check.
133 with node.assert_debug_log(['resubmit 2 unconfirmed transactions']):
134 node.setmocktime(evict_time)
135 node.mockscheduler(60)
136 137 # Evict these txs from the mempool
138 indep_send = node.send(outputs=[{node.getnewaddress(): 1}], inputs=[indep_utxo])
139 node.getmempoolentry(indep_send["txid"])
140 assert_raises_rpc_error(-5, "Transaction not in mempool", node.getmempoolentry, txid)
141 assert_raises_rpc_error(-5, "Transaction not in mempool", node.getmempoolentry, child_txid)
142 143 # Rebroadcast and check that parent and child are both in the mempool
144 with node.assert_debug_log(['resubmit 2 unconfirmed transactions']):
145 node.setmocktime(evict_time + 36 * 60 * 60) # 36 hrs is the upper limit of the resend timer
146 node.mockscheduler(60)
147 node.getmempoolentry(txid)
148 node.getmempoolentry(child_txid)
149 150 151 if __name__ == '__main__':
152 ResendWalletTransactionsTest(__file__).main()
153