Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
1#!/usr/bin/env python32# Copyright (c) 2014-present The Bitcoin Core developers3# Distributed under the MIT software license, see the accompanying4# file COPYING or http://www.opensource.org/licenses/mit-license.php.5"""Test the -alertnotify, -blocknotify and -walletnotify options."""6import os7import platform9from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE10from test_framework.blocktools import (11create_block,12)13from test_framework.descriptors import descsum_create14from test_framework.extendedkey import ExtendedPrivateKey15from test_framework.test_framework import BitcoinTestFramework16from test_framework.util import (17assert_equal,18)20# Linux allow all characters other than \x0021# Windows disallow control characters (0-31) and /\?%:|"<>22FILE_CHAR_START = 32 if platform.system() == 'Windows' else 123FILE_CHAR_END = 12824FILE_CHARS_DISALLOWED = '/\\?%*:|"<>' if platform.system() == 'Windows' else '/'25UNCONFIRMED_HASH_STRING = 'unconfirmed'27LARGE_WORK_INVALID_CHAIN_WARNING = (28"Warning: Found invalid chain more than 6 blocks longer than our best chain. This could be due to database corruption or consensus incompatibility with peers."29)32def notify_outputname(walletname, txid):33return txid if platform.system() == 'Windows' else f'{walletname}_{txid}'35def shell_escape_posix(arg):36# Identical to ShellEscape() in the C++ code37return "'" + arg.replace("'", "'\"'\"'") + "'"40class NotificationsTest(BitcoinTestFramework):41def set_test_params(self):42self.num_nodes = 243self.setup_clean_chain = True44self.uses_wallet = None45self.noban_tx_relay = True47def setup_network(self):48self.wallet = ''.join(chr(i) for i in range(FILE_CHAR_START, FILE_CHAR_END) if chr(i) not in FILE_CHARS_DISALLOWED)49self.alertnotify_dir = os.path.join(self.options.tmpdir, "alertnotify")50self.alertnotify_file = os.path.join(self.alertnotify_dir, "alertnotify.txt")51self.blocknotify_dir = os.path.join(self.options.tmpdir, "blocknotify")52self.walletnotify_dir = os.path.join(self.options.tmpdir, "walletnotify")53self.shutdownnotify_dir = os.path.join(self.options.tmpdir, "shutdownnotify")54self.shutdownnotify_file = os.path.join(self.shutdownnotify_dir, "shutdownnotify.txt")55os.mkdir(self.alertnotify_dir)56os.mkdir(self.blocknotify_dir)57os.mkdir(self.walletnotify_dir)58os.mkdir(self.shutdownnotify_dir)60if platform.system() == 'Windows':61walletnotify_path = f"\"{os.path.join(self.walletnotify_dir, notify_outputname('%w', '%s'))}\""62else:63walletnotify_path = f"{shell_escape_posix(os.path.join(self.walletnotify_dir, ''))}{notify_outputname('%w', '%s')}"65# -alertnotify and -blocknotify on node0, walletnotify on node166self.extra_args = [[67f"-alertnotify=echo %s >> \"{self.alertnotify_file}\"",68f"-blocknotify=echo > \"{os.path.join(self.blocknotify_dir, '%s')}\"",69f"-shutdownnotify=echo > \"{self.shutdownnotify_file}\"",70], [71f"-walletnotify=echo %h_%b > {walletnotify_path}",72]]73self.wallet_names = [self.default_wallet_name, self.wallet]74super().setup_network()76def run_test(self):77if self.is_wallet_compiled():78# Setup the descriptors to be imported to the wallet79xpriv = ExtendedPrivateKey.generate().to_string()80desc_imports = [{81"desc": descsum_create(f"wpkh({xpriv}/0/*)"),82"timestamp": 0,83"active": True,84"keypool": True,85},{86"desc": descsum_create(f"wpkh({xpriv}/1/*)"),87"timestamp": 0,88"active": True,89"keypool": True,90"internal": True,91}]92# Make the wallets and import the descriptors93# Ensures that node 0 and node 1 share the same wallet for the conflicting transaction tests below.94for i, name in enumerate(self.wallet_names):95self.nodes[i].createwallet(wallet_name=name, blank=True, load_on_startup=True)96self.nodes[i].importdescriptors(desc_imports)98self.log.info("test -blocknotify")99block_count = 10100blocks = self.generatetoaddress(self.nodes[1], block_count, self.nodes[1].getnewaddress() if self.is_wallet_compiled() else ADDRESS_BCRT1_UNSPENDABLE)102# wait at most 10 seconds for expected number of files before reading the content103self.wait_until(lambda: len(os.listdir(self.blocknotify_dir)) == block_count, timeout=10)105# directory content should equal the generated blocks hashes106assert_equal(sorted(blocks), sorted(os.listdir(self.blocknotify_dir)))108if self.is_wallet_compiled():109self.log.info("test -walletnotify")110# wait at most 10 seconds for expected number of files before reading the content111self.wait_until(lambda: len(os.listdir(self.walletnotify_dir)) == block_count, timeout=10)113# directory content should equal the generated transaction hashes114tx_details = list(map(lambda t: (t['txid'], t['blockheight'], t['blockhash']), self.nodes[1].listtransactions("*", block_count)))115self.expect_wallet_notify(tx_details)117self.log.info("test -walletnotify after rescan")118# rescan to force wallet notifications119self.nodes[1].rescanblockchain()120self.wait_until(lambda: len(os.listdir(self.walletnotify_dir)) == block_count, timeout=10)122self.connect_nodes(0, 1)124# directory content should equal the generated transaction hashes125tx_details = list(map(lambda t: (t['txid'], t['blockheight'], t['blockhash']), self.nodes[1].listtransactions("*", block_count)))126self.expect_wallet_notify(tx_details)128# Conflicting transactions tests.129# Generate spends from node 0, and check notifications130# triggered by node 1131self.log.info("test -walletnotify with conflicting transactions")132self.nodes[0].rescanblockchain()133self.generatetoaddress(self.nodes[0], 100, ADDRESS_BCRT1_UNSPENDABLE)135# Generate transaction on node 0, sync mempools, and check for136# notification on node 1.137tx1 = self.nodes[0].sendtoaddress(address=ADDRESS_BCRT1_UNSPENDABLE, amount=1, replaceable=True)138assert_equal(tx1 in self.nodes[0].getrawmempool(), True)139self.sync_mempools()140self.expect_wallet_notify([(tx1, -1, UNCONFIRMED_HASH_STRING)])142# Generate bump transaction, sync mempools, and check for bump1143# notification. In the future, per144# https://github.com/bitcoin/bitcoin/pull/9371, it might be better145# to have notifications for both tx1 and bump1.146bump1 = self.nodes[0].bumpfee(tx1)["txid"]147assert_equal(bump1 in self.nodes[0].getrawmempool(), True)148self.sync_mempools()149self.expect_wallet_notify([(bump1, -1, UNCONFIRMED_HASH_STRING)])151# Add bump1 transaction to new block, checking for a notification152# and the correct number of confirmations.153blockhash1 = self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE)[0]154blockheight1 = self.nodes[0].getblockcount()155self.sync_blocks()156self.expect_wallet_notify([(bump1, blockheight1, blockhash1)])157assert_equal(self.nodes[1].gettransaction(bump1)["confirmations"], 1)159# Generate a second transaction to be bumped.160tx2 = self.nodes[0].sendtoaddress(address=ADDRESS_BCRT1_UNSPENDABLE, amount=1, replaceable=True)161assert_equal(tx2 in self.nodes[0].getrawmempool(), True)162self.sync_mempools()163self.expect_wallet_notify([(tx2, -1, UNCONFIRMED_HASH_STRING)])165# Bump tx2 as bump2 and generate a block on node 0 while166# disconnected, then reconnect and check for notifications on node 1167# about newly confirmed bump2 and newly conflicted tx2.168self.disconnect_nodes(0, 1)169bump2 = self.nodes[0].bumpfee(tx2)["txid"]170blockhash2 = self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE, sync_fun=self.no_op)[0]171blockheight2 = self.nodes[0].getblockcount()172assert_equal(self.nodes[0].gettransaction(bump2)["confirmations"], 1)173assert_equal(tx2 in self.nodes[1].getrawmempool(), True)174self.connect_nodes(0, 1)175self.sync_blocks()176self.expect_wallet_notify([(bump2, blockheight2, blockhash2), (tx2, -1, UNCONFIRMED_HASH_STRING)])177assert_equal(self.nodes[1].gettransaction(bump2)["confirmations"], 1)179if platform.system() != 'Windows':180self.log.info("test -walletnotify replacement metacharacters in wallet name")181self.nodes[1].unloadwallet(self.wallet)182command_marker = os.path.join(self.options.tmpdir, "walletnotify_injected")183# The previous regex replacement expanded `$'` to the command suffix, breaking the shell-escaped wallet name's quote accounting184wallet_name = self.nodes[1].createwallet(f"$'$'; echo Pwned > {os.path.basename(command_marker)}; #")["name"]185txid = self.nodes[0].sendtoaddress(self.nodes[1].get_wallet_rpc(wallet_name).getnewaddress(), 1)186self.sync_mempools()187notify_path = os.path.join(self.walletnotify_dir, notify_outputname(wallet_name, txid))188self.wait_until(lambda: os.path.exists(command_marker) or os.path.exists(notify_path), timeout=10)189assert not os.path.exists(command_marker)190assert os.path.exists(notify_path)192self.log.info("test -alertnotify with large work invalid chain")193# create a bunch of invalid blocks194tip = self.nodes[0].getbestblockhash()195height = self.nodes[0].getblockcount() + 1196block_time = self.nodes[0].getblock(tip)['time'] + 1198invalid_blocks = []199for _ in range(7): # invalid chain must be longer than 6 blocks to trigger warning200block = create_block(int(tip, 16), height=height, ntime=block_time)201# make block invalid by exceeding block subsidy202block.vtx[0].vout[0].nValue += 1203block.hashMerkleRoot = block.calc_merkle_root()204block.solve()205invalid_blocks.append(block)206tip = block.hash_hex207height += 1208block_time += 1210# submit headers of invalid blocks211for invalid_block in invalid_blocks:212self.nodes[0].submitheader(invalid_block.serialize().hex())213# submit invalid blocks in reverse order (tip first, to set m_best_invalid)214for invalid_block in reversed(invalid_blocks):215self.nodes[0].submitblock(invalid_block.serialize().hex())217self.wait_until(lambda: os.path.isfile(self.alertnotify_file), timeout=10)218self.wait_until(self.large_work_invalid_chain_warning_in_alert_file, timeout=10)220self.log.info("test -shutdownnotify")221self.stop_nodes()222self.wait_until(lambda: os.path.isfile(self.shutdownnotify_file), timeout=10)224def large_work_invalid_chain_warning_in_alert_file(self):225with open(self.alertnotify_file, 'r') as f:226alert_text = f.read()227return LARGE_WORK_INVALID_CHAIN_WARNING in alert_text229def expect_wallet_notify(self, tx_details):230self.wait_until(lambda: len(os.listdir(self.walletnotify_dir)) >= len(tx_details), timeout=10)231# Should have no more and no less files than expected232assert_equal(sorted(notify_outputname(self.wallet, tx_id) for tx_id, _, _ in tx_details), sorted(os.listdir(self.walletnotify_dir)))233# Should now verify contents of each file234for tx_id, blockheight, blockhash in tx_details:235fname = os.path.join(self.walletnotify_dir, notify_outputname(self.wallet, tx_id))236# Wait for the cached writes to hit storage237self.wait_until(lambda: os.path.getsize(fname) > 0, timeout=10)238with open(fname, 'rt') as f:239text = f.read()240# Universal newline ensures '\n' on 'nt'241assert_equal(text[-1], '\n')242text = text[:-1]243if platform.system() == 'Windows':244# On Windows, echo as above will append a whitespace245assert_equal(text[-1], ' ')246text = text[:-1]247expected = str(blockheight) + '_' + blockhash248assert_equal(text, expected)250for tx_file in os.listdir(self.walletnotify_dir):
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.