feature_notifications.py
1 #!/usr/bin/env python3 2 # Copyright (c) 2014-2022 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 the -alertnotify, -blocknotify and -walletnotify options.""" 6 import os 7 import platform 8 9 from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE 10 from test_framework.descriptors import descsum_create 11 from test_framework.test_framework import BitcoinTestFramework 12 from test_framework.util import ( 13 assert_equal, 14 ) 15 16 # Linux allow all characters other than \x00 17 # Windows disallow control characters (0-31) and /\?%:|"<> 18 FILE_CHAR_START = 32 if platform.system() == 'Windows' else 1 19 FILE_CHAR_END = 128 20 FILE_CHARS_DISALLOWED = '/\\?%*:|"<>' if platform.system() == 'Windows' else '/' 21 UNCONFIRMED_HASH_STRING = 'unconfirmed' 22 23 def notify_outputname(walletname, txid): 24 return txid if platform.system() == 'Windows' else f'{walletname}_{txid}' 25 26 27 class NotificationsTest(BitcoinTestFramework): 28 def add_options(self, parser): 29 self.add_wallet_options(parser) 30 31 def set_test_params(self): 32 self.num_nodes = 2 33 self.setup_clean_chain = True 34 35 def setup_network(self): 36 self.wallet = ''.join(chr(i) for i in range(FILE_CHAR_START, FILE_CHAR_END) if chr(i) not in FILE_CHARS_DISALLOWED) 37 self.alertnotify_dir = os.path.join(self.options.tmpdir, "alertnotify") 38 self.blocknotify_dir = os.path.join(self.options.tmpdir, "blocknotify") 39 self.walletnotify_dir = os.path.join(self.options.tmpdir, "walletnotify") 40 self.shutdownnotify_dir = os.path.join(self.options.tmpdir, "shutdownnotify") 41 self.shutdownnotify_file = os.path.join(self.shutdownnotify_dir, "shutdownnotify.txt") 42 os.mkdir(self.alertnotify_dir) 43 os.mkdir(self.blocknotify_dir) 44 os.mkdir(self.walletnotify_dir) 45 os.mkdir(self.shutdownnotify_dir) 46 47 # -alertnotify and -blocknotify on node0, walletnotify on node1 48 self.extra_args = [[ 49 f"-alertnotify=echo > {os.path.join(self.alertnotify_dir, '%s')}", 50 f"-blocknotify=echo > {os.path.join(self.blocknotify_dir, '%s')}", 51 f"-shutdownnotify=echo > {self.shutdownnotify_file}", 52 ], [ 53 f"-walletnotify=echo %h_%b > {os.path.join(self.walletnotify_dir, notify_outputname('%w', '%s'))}", 54 ]] 55 self.wallet_names = [self.default_wallet_name, self.wallet] 56 super().setup_network() 57 58 def run_test(self): 59 if self.is_wallet_compiled(): 60 # Setup the descriptors to be imported to the wallet 61 seed = "cTdGmKFWpbvpKQ7ejrdzqYT2hhjyb3GPHnLAK7wdi5Em67YLwSm9" 62 xpriv = "tprv8ZgxMBicQKsPfHCsTwkiM1KT56RXbGGTqvc2hgqzycpwbHqqpcajQeMRZoBD35kW4RtyCemu6j34Ku5DEspmgjKdt2qe4SvRch5Kk8B8A2v" 63 desc_imports = [{ 64 "desc": descsum_create(f"wpkh({xpriv}/0/*)"), 65 "timestamp": 0, 66 "active": True, 67 "keypool": True, 68 },{ 69 "desc": descsum_create(f"wpkh({xpriv}/1/*)"), 70 "timestamp": 0, 71 "active": True, 72 "keypool": True, 73 "internal": True, 74 }] 75 # Make the wallets and import the descriptors 76 # Ensures that node 0 and node 1 share the same wallet for the conflicting transaction tests below. 77 for i, name in enumerate(self.wallet_names): 78 self.nodes[i].createwallet(wallet_name=name, descriptors=self.options.descriptors, blank=True, load_on_startup=True) 79 if self.options.descriptors: 80 self.nodes[i].importdescriptors(desc_imports) 81 else: 82 self.nodes[i].sethdseed(True, seed) 83 84 self.log.info("test -blocknotify") 85 block_count = 10 86 blocks = self.generatetoaddress(self.nodes[1], block_count, self.nodes[1].getnewaddress() if self.is_wallet_compiled() else ADDRESS_BCRT1_UNSPENDABLE) 87 88 # wait at most 10 seconds for expected number of files before reading the content 89 self.wait_until(lambda: len(os.listdir(self.blocknotify_dir)) == block_count, timeout=10) 90 91 # directory content should equal the generated blocks hashes 92 assert_equal(sorted(blocks), sorted(os.listdir(self.blocknotify_dir))) 93 94 if self.is_wallet_compiled(): 95 self.log.info("test -walletnotify") 96 # wait at most 10 seconds for expected number of files before reading the content 97 self.wait_until(lambda: len(os.listdir(self.walletnotify_dir)) == block_count, timeout=10) 98 99 # directory content should equal the generated transaction hashes 100 tx_details = list(map(lambda t: (t['txid'], t['blockheight'], t['blockhash']), self.nodes[1].listtransactions("*", block_count))) 101 self.expect_wallet_notify(tx_details) 102 103 self.log.info("test -walletnotify after rescan") 104 # rescan to force wallet notifications 105 self.nodes[1].rescanblockchain() 106 self.wait_until(lambda: len(os.listdir(self.walletnotify_dir)) == block_count, timeout=10) 107 108 self.connect_nodes(0, 1) 109 110 # directory content should equal the generated transaction hashes 111 tx_details = list(map(lambda t: (t['txid'], t['blockheight'], t['blockhash']), self.nodes[1].listtransactions("*", block_count))) 112 self.expect_wallet_notify(tx_details) 113 114 # Conflicting transactions tests. 115 # Generate spends from node 0, and check notifications 116 # triggered by node 1 117 self.log.info("test -walletnotify with conflicting transactions") 118 self.nodes[0].rescanblockchain() 119 self.generatetoaddress(self.nodes[0], 100, ADDRESS_BCRT1_UNSPENDABLE) 120 121 # Generate transaction on node 0, sync mempools, and check for 122 # notification on node 1. 123 tx1 = self.nodes[0].sendtoaddress(address=ADDRESS_BCRT1_UNSPENDABLE, amount=1, replaceable=True) 124 assert_equal(tx1 in self.nodes[0].getrawmempool(), True) 125 self.sync_mempools() 126 self.expect_wallet_notify([(tx1, -1, UNCONFIRMED_HASH_STRING)]) 127 128 # Generate bump transaction, sync mempools, and check for bump1 129 # notification. In the future, per 130 # https://github.com/bitcoin/bitcoin/pull/9371, it might be better 131 # to have notifications for both tx1 and bump1. 132 bump1 = self.nodes[0].bumpfee(tx1)["txid"] 133 assert_equal(bump1 in self.nodes[0].getrawmempool(), True) 134 self.sync_mempools() 135 self.expect_wallet_notify([(bump1, -1, UNCONFIRMED_HASH_STRING)]) 136 137 # Add bump1 transaction to new block, checking for a notification 138 # and the correct number of confirmations. 139 blockhash1 = self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE)[0] 140 blockheight1 = self.nodes[0].getblockcount() 141 self.sync_blocks() 142 self.expect_wallet_notify([(bump1, blockheight1, blockhash1)]) 143 assert_equal(self.nodes[1].gettransaction(bump1)["confirmations"], 1) 144 145 # Generate a second transaction to be bumped. 146 tx2 = self.nodes[0].sendtoaddress(address=ADDRESS_BCRT1_UNSPENDABLE, amount=1, replaceable=True) 147 assert_equal(tx2 in self.nodes[0].getrawmempool(), True) 148 self.sync_mempools() 149 self.expect_wallet_notify([(tx2, -1, UNCONFIRMED_HASH_STRING)]) 150 151 # Bump tx2 as bump2 and generate a block on node 0 while 152 # disconnected, then reconnect and check for notifications on node 1 153 # about newly confirmed bump2 and newly conflicted tx2. 154 self.disconnect_nodes(0, 1) 155 bump2 = self.nodes[0].bumpfee(tx2)["txid"] 156 blockhash2 = self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE, sync_fun=self.no_op)[0] 157 blockheight2 = self.nodes[0].getblockcount() 158 assert_equal(self.nodes[0].gettransaction(bump2)["confirmations"], 1) 159 assert_equal(tx2 in self.nodes[1].getrawmempool(), True) 160 self.connect_nodes(0, 1) 161 self.sync_blocks() 162 self.expect_wallet_notify([(bump2, blockheight2, blockhash2), (tx2, -1, UNCONFIRMED_HASH_STRING)]) 163 assert_equal(self.nodes[1].gettransaction(bump2)["confirmations"], 1) 164 165 # TODO: add test for `-alertnotify` large fork notifications 166 167 self.log.info("test -shutdownnotify") 168 self.stop_nodes() 169 self.wait_until(lambda: os.path.isfile(self.shutdownnotify_file), timeout=10) 170 171 def expect_wallet_notify(self, tx_details): 172 self.wait_until(lambda: len(os.listdir(self.walletnotify_dir)) >= len(tx_details), timeout=10) 173 # Should have no more and no less files than expected 174 assert_equal(sorted(notify_outputname(self.wallet, tx_id) for tx_id, _, _ in tx_details), sorted(os.listdir(self.walletnotify_dir))) 175 # Should now verify contents of each file 176 for tx_id, blockheight, blockhash in tx_details: 177 fname = os.path.join(self.walletnotify_dir, notify_outputname(self.wallet, tx_id)) 178 # Wait for the cached writes to hit storage 179 self.wait_until(lambda: os.path.getsize(fname) > 0, timeout=10) 180 with open(fname, 'rt', encoding='utf-8') as f: 181 text = f.read() 182 # Universal newline ensures '\n' on 'nt' 183 assert_equal(text[-1], '\n') 184 text = text[:-1] 185 if platform.system() == 'Windows': 186 # On Windows, echo as above will append a whitespace 187 assert_equal(text[-1], ' ') 188 text = text[:-1] 189 expected = str(blockheight) + '_' + blockhash 190 assert_equal(text, expected) 191 192 for tx_file in os.listdir(self.walletnotify_dir): 193 os.remove(os.path.join(self.walletnotify_dir, tx_file)) 194 195 196 if __name__ == '__main__': 197 NotificationsTest().main()