From e9dc16194b47921ea6c5453e71c5c97b9232ba8c Mon Sep 17 00:00:00 2001 From: midnight Date: Sun, 16 Feb 2025 16:25:19 -0800 Subject: [PATCH 001/292] correct the linearize-data script (minor) this clips off unwanted final data in event of reorg and fixes the file open mode to prevent the truncation of files reopened small patch being passed around which I did not write but thought would be helpful to the community at large --- contrib/linearize/linearize-data.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/contrib/linearize/linearize-data.py b/contrib/linearize/linearize-data.py index 73f54cd4885e..0d7818e9e4aa 100755 --- a/contrib/linearize/linearize-data.py +++ b/contrib/linearize/linearize-data.py @@ -157,6 +157,7 @@ def __init__(self, settings, blkindex, blkmap): def writeBlock(self, inhdr, blk_hdr, rawblock): blockSizeOnDisk = len(inhdr) + len(blk_hdr) + len(rawblock) if not self.fileOutput and ((self.outsz + blockSizeOnDisk) > self.maxOutSz): + os.ftruncate(self.outF.fileno(), self.outF.tell()) self.outF.close() if self.setFileTime: os.utime(self.outFname, (int(time.time()), self.highTS)) @@ -170,6 +171,7 @@ def writeBlock(self, inhdr, blk_hdr, rawblock): print("New month " + blkDate.strftime("%Y-%m") + " @ " + self.hash_str) self.lastDate = blkDate if self.outF: + os.ftruncate(self.outF.fileno(), self.outF.tell()) self.outF.close() if self.setFileTime: os.utime(self.outFname, (int(time.time()), self.highTS)) @@ -184,7 +186,10 @@ def writeBlock(self, inhdr, blk_hdr, rawblock): else: self.outFname = os.path.join(self.settings['output'], "blk%05d.dat" % self.outFn) print("Output file " + self.outFname) - self.outF = open(self.outFname, "wb") + try: + self.outF = open(self.outFname, "xb+") + except FileExistsError: + self.outF = open(self.outFname, "rb+") self.outF.write(inhdr) self.outF.write(blk_hdr) From a1ecda2609965f623ec808bc870ff6d7f151a419 Mon Sep 17 00:00:00 2001 From: naiyoma Date: Wed, 25 Feb 2026 15:41:04 +0300 Subject: [PATCH 002/292] test: delete redundant addr relay assertion The assertion checks that the first addr message is not relayed due to m_getaddr_sent being set to true. But we now send a self-announcement as the first message, which resets the flag to false, meaning subsequent addr messages are relayed. --- test/functional/p2p_addr_relay.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/test/functional/p2p_addr_relay.py b/test/functional/p2p_addr_relay.py index 65b21c0d5059..43df1ad63193 100755 --- a/test/functional/p2p_addr_relay.py +++ b/test/functional/p2p_addr_relay.py @@ -187,13 +187,6 @@ def relay_tests(self): full_outbound_peer = self.nodes[0].add_outbound_p2p_connection(AddrReceiver(), p2p_idx=0, connection_type="outbound-full-relay") msg = self.setup_addr_msg(2) self.send_addr_msg(full_outbound_peer, msg, [inbound_peer]) - self.log.info('Check that the first addr message received from an outbound peer is not relayed') - # Currently, there is a flag that prevents the first addr message received - # from a new outbound peer to be relayed to others. Originally meant to prevent - # large GETADDR responses from being relayed, it now typically affects the self-announcement - # of the outbound peer which is often sent before the GETADDR response. - assert_equal(inbound_peer.num_ipv4_received, 0) - self.log.info('Check that subsequent addr messages sent from an outbound peer are relayed') msg2 = self.setup_addr_msg(2) self.send_addr_msg(full_outbound_peer, msg2, [inbound_peer]) From c8ee319fffa1b7674fad3a797cd6d5110435a74c Mon Sep 17 00:00:00 2001 From: naiyoma Date: Tue, 24 Feb 2026 12:49:46 +0300 Subject: [PATCH 003/292] net: remove m_getaddr_sent The flag was intended to track whether we sent a GETADDR to a peer, but it became redundant after the initial self-announcement sets it to false, even though we still expect the actual getaddr response. Co-authored-by: Martin Zumsande --- src/net_processing.cpp | 6 +----- test/functional/p2p_addr_relay.py | 3 +-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 28d7d2c13ab6..e01ee24ae6aa 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -362,8 +362,6 @@ struct Peer { * This field must correlate with whether m_addr_known has been * initialized.*/ std::atomic_bool m_addr_relay_enabled{false}; - /** Whether a getaddr request to this peer is outstanding. */ - bool m_getaddr_sent GUARDED_BY(NetEventsInterface::g_msgproc_mutex){false}; /** Guards address sending timers. */ mutable Mutex m_addr_send_times_mutex; /** Time point to send the next ADDR message to this peer. */ @@ -3761,7 +3759,6 @@ void PeerManagerImpl::ProcessMessage(Peer& peer, CNode& pfrom, const std::string // potentially leaking addr information and we do not want to // indicate to the peer that we will participate in addr relay. MakeAndPushMessage(pfrom, NetMsgType::GETADDR); - peer.m_getaddr_sent = true; // When requesting a getaddr, accept an additional MAX_ADDR_TO_SEND addresses in response // (bypassing the MAX_ADDR_PROCESSING_TOKEN_BUCKET limit). peer.m_addr_token_bucket += MAX_ADDR_TO_SEND; @@ -5685,7 +5682,7 @@ void PeerManagerImpl::ProcessAddrs(std::string_view msg_type, CNode& pfrom, Peer } ++num_proc; const bool reachable{g_reachable_nets.Contains(addr)}; - if (addr.nTime > current_time - 10min && !peer.m_getaddr_sent && vAddr.size() <= 10 && addr.IsRoutable()) { + if (addr.nTime > current_time - 10min && vAddr.size() <= 10 && addr.IsRoutable()) { // Relay to a limited number of other nodes RelayAddress(pfrom.GetId(), addr, reachable); } @@ -5700,7 +5697,6 @@ void PeerManagerImpl::ProcessAddrs(std::string_view msg_type, CNode& pfrom, Peer vAddr.size(), num_proc, num_rate_limit, pfrom.GetId()); m_addrman.Add(vAddrOk, pfrom.addr, /*time_penalty=*/2h); - if (vAddr.size() < 1000) peer.m_getaddr_sent = false; // AddrFetch: Require multiple addresses to avoid disconnecting on self-announcements if (pfrom.IsAddrFetchConn() && vAddr.size() > 1) { diff --git a/test/functional/p2p_addr_relay.py b/test/functional/p2p_addr_relay.py index 43df1ad63193..3c53552ca8ef 100755 --- a/test/functional/p2p_addr_relay.py +++ b/test/functional/p2p_addr_relay.py @@ -185,8 +185,7 @@ def relay_tests(self): inbound_peer.send_and_ping(msg_addr()) full_outbound_peer = self.nodes[0].add_outbound_p2p_connection(AddrReceiver(), p2p_idx=0, connection_type="outbound-full-relay") - msg = self.setup_addr_msg(2) - self.send_addr_msg(full_outbound_peer, msg, [inbound_peer]) + self.log.info('Check that subsequent addr messages sent from an outbound peer are relayed') msg2 = self.setup_addr_msg(2) self.send_addr_msg(full_outbound_peer, msg2, [inbound_peer]) From e78a2a0d00cf67637eae4caf4db1735f110aff14 Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Sun, 7 Jun 2026 01:51:27 +0200 Subject: [PATCH 004/292] test: refactor: simplify tx vin/vout creation in txvalidationcache_tests.cpp --- src/test/txvalidationcache_tests.cpp | 74 +++++++++------------------- 1 file changed, 22 insertions(+), 52 deletions(-) diff --git a/src/test/txvalidationcache_tests.cpp b/src/test/txvalidationcache_tests.cpp index d695e08f5f50..5ace00880a60 100644 --- a/src/test/txvalidationcache_tests.cpp +++ b/src/test/txvalidationcache_tests.cpp @@ -49,12 +49,8 @@ BOOST_FIXTURE_TEST_CASE(tx_mempool_block_doublespend, Dersig100Setup) for (int i = 0; i < 2; i++) { spends[i].version = 1; - spends[i].vin.resize(1); - spends[i].vin[0].prevout.hash = m_coinbase_txns[0]->GetHash(); - spends[i].vin[0].prevout.n = 0; - spends[i].vout.resize(1); - spends[i].vout[0].nValue = 11*CENT; - spends[i].vout[0].scriptPubKey = scriptPubKey; + spends[i].vin = {CTxIn{m_coinbase_txns[0]->GetHash(), 0}}; + spends[i].vout = {CTxOut{11*CENT, scriptPubKey}}; // Sign: std::vector vchSig; @@ -184,18 +180,13 @@ BOOST_FIXTURE_TEST_CASE(checkinputs_test, Dersig100Setup) CMutableTransaction spend_tx; spend_tx.version = 1; - spend_tx.vin.resize(1); - spend_tx.vin[0].prevout.hash = m_coinbase_txns[0]->GetHash(); - spend_tx.vin[0].prevout.n = 0; - spend_tx.vout.resize(4); - spend_tx.vout[0].nValue = 11*CENT; - spend_tx.vout[0].scriptPubKey = p2sh_scriptPubKey; - spend_tx.vout[1].nValue = 11*CENT; - spend_tx.vout[1].scriptPubKey = p2wpkh_scriptPubKey; - spend_tx.vout[2].nValue = 11*CENT; - spend_tx.vout[2].scriptPubKey = CScript() << OP_CHECKLOCKTIMEVERIFY << OP_DROP << ToByteVector(coinbaseKey.GetPubKey()) << OP_CHECKSIG; - spend_tx.vout[3].nValue = 11*CENT; - spend_tx.vout[3].scriptPubKey = CScript() << OP_CHECKSEQUENCEVERIFY << OP_DROP << ToByteVector(coinbaseKey.GetPubKey()) << OP_CHECKSIG; + spend_tx.vin = {CTxIn{m_coinbase_txns[0]->GetHash(), 0}}; + spend_tx.vout = { + CTxOut{11*CENT, p2sh_scriptPubKey}, + CTxOut{11*CENT, p2wpkh_scriptPubKey}, + CTxOut{11*CENT, CScript() << OP_CHECKLOCKTIMEVERIFY << OP_DROP << ToByteVector(coinbaseKey.GetPubKey()) << OP_CHECKSIG}, + CTxOut{11*CENT, CScript() << OP_CHECKSEQUENCEVERIFY << OP_DROP << ToByteVector(coinbaseKey.GetPubKey()) << OP_CHECKSIG}, + }; // Sign, with a non-DER signature { @@ -246,12 +237,8 @@ BOOST_FIXTURE_TEST_CASE(checkinputs_test, Dersig100Setup) { CMutableTransaction invalid_under_p2sh_tx; invalid_under_p2sh_tx.version = 1; - invalid_under_p2sh_tx.vin.resize(1); - invalid_under_p2sh_tx.vin[0].prevout.hash = spend_tx.GetHash(); - invalid_under_p2sh_tx.vin[0].prevout.n = 0; - invalid_under_p2sh_tx.vout.resize(1); - invalid_under_p2sh_tx.vout[0].nValue = 11*CENT; - invalid_under_p2sh_tx.vout[0].scriptPubKey = p2pk_scriptPubKey; + invalid_under_p2sh_tx.vin = {CTxIn{spend_tx.GetHash(), 0}}; + invalid_under_p2sh_tx.vout = {CTxOut{11*CENT, p2pk_scriptPubKey}}; std::vector vchSig2(p2pk_scriptPubKey.begin(), p2pk_scriptPubKey.end()); invalid_under_p2sh_tx.vin[0].scriptSig << vchSig2; @@ -263,13 +250,8 @@ BOOST_FIXTURE_TEST_CASE(checkinputs_test, Dersig100Setup) CMutableTransaction invalid_with_cltv_tx; invalid_with_cltv_tx.version = 1; invalid_with_cltv_tx.nLockTime = 100; - invalid_with_cltv_tx.vin.resize(1); - invalid_with_cltv_tx.vin[0].prevout.hash = spend_tx.GetHash(); - invalid_with_cltv_tx.vin[0].prevout.n = 2; - invalid_with_cltv_tx.vin[0].nSequence = 0; - invalid_with_cltv_tx.vout.resize(1); - invalid_with_cltv_tx.vout[0].nValue = 11*CENT; - invalid_with_cltv_tx.vout[0].scriptPubKey = p2pk_scriptPubKey; + invalid_with_cltv_tx.vin = {CTxIn{spend_tx.GetHash(), 2, {}, /*nSequenceIn=*/0}}; + invalid_with_cltv_tx.vout = {CTxOut{11*CENT, p2pk_scriptPubKey}}; // Sign std::vector vchSig; @@ -291,13 +273,8 @@ BOOST_FIXTURE_TEST_CASE(checkinputs_test, Dersig100Setup) { CMutableTransaction invalid_with_csv_tx; invalid_with_csv_tx.version = 2; - invalid_with_csv_tx.vin.resize(1); - invalid_with_csv_tx.vin[0].prevout.hash = spend_tx.GetHash(); - invalid_with_csv_tx.vin[0].prevout.n = 3; - invalid_with_csv_tx.vin[0].nSequence = 100; - invalid_with_csv_tx.vout.resize(1); - invalid_with_csv_tx.vout[0].nValue = 11*CENT; - invalid_with_csv_tx.vout[0].scriptPubKey = p2pk_scriptPubKey; + invalid_with_csv_tx.vin = {CTxIn{spend_tx.GetHash(), 3, {}, /*nSequenceIn=*/100}}; + invalid_with_csv_tx.vout = {CTxOut{11*CENT, p2pk_scriptPubKey}}; // Sign std::vector vchSig; @@ -322,12 +299,8 @@ BOOST_FIXTURE_TEST_CASE(checkinputs_test, Dersig100Setup) { CMutableTransaction valid_with_witness_tx; valid_with_witness_tx.version = 1; - valid_with_witness_tx.vin.resize(1); - valid_with_witness_tx.vin[0].prevout.hash = spend_tx.GetHash(); - valid_with_witness_tx.vin[0].prevout.n = 1; - valid_with_witness_tx.vout.resize(1); - valid_with_witness_tx.vout[0].nValue = 11*CENT; - valid_with_witness_tx.vout[0].scriptPubKey = p2pk_scriptPubKey; + valid_with_witness_tx.vin = {CTxIn{spend_tx.GetHash(), 1}}; + valid_with_witness_tx.vout = {CTxOut{11*CENT, p2pk_scriptPubKey}}; // Sign SignatureData sigdata; @@ -347,14 +320,11 @@ BOOST_FIXTURE_TEST_CASE(checkinputs_test, Dersig100Setup) CMutableTransaction tx; tx.version = 1; - tx.vin.resize(2); - tx.vin[0].prevout.hash = spend_tx.GetHash(); - tx.vin[0].prevout.n = 0; - tx.vin[1].prevout.hash = spend_tx.GetHash(); - tx.vin[1].prevout.n = 1; - tx.vout.resize(1); - tx.vout[0].nValue = 22*CENT; - tx.vout[0].scriptPubKey = p2pk_scriptPubKey; + tx.vin = { + CTxIn{spend_tx.GetHash(), 0}, + CTxIn{spend_tx.GetHash(), 1}, + }; + tx.vout = {CTxOut{22*CENT, p2pk_scriptPubKey}}; // Sign for (int i = 0; i < 2; ++i) { From 198b36bc85d54792af82ea9b9e58554608573cb6 Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Sun, 7 Jun 2026 01:58:34 +0200 Subject: [PATCH 005/292] test: respect "TAPROOT requires WITNESS" rule in `ValidateCheckInputsForAllFlags` This is preparatory for the next commit, which introduces Taproot spends that would fail without this rule being enforced. While touching the function, also remove an outdated comment that refers to a parameter that doesn't exist anymore (`upgraded_nop` was removed in commit 01013f5d2fbe3fa86565c927bf7bb8ec0f525073, PR #10699). --- src/test/txvalidationcache_tests.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/txvalidationcache_tests.cpp b/src/test/txvalidationcache_tests.cpp index 5ace00880a60..dd6360279437 100644 --- a/src/test/txvalidationcache_tests.cpp +++ b/src/test/txvalidationcache_tests.cpp @@ -114,8 +114,6 @@ BOOST_FIXTURE_TEST_CASE(tx_mempool_block_doublespend, Dersig100Setup) // CHECKLOCKTIMEVERIFY (or CHECKSEQUENCEVERIFY), but the script does contain // OP_CHECKLOCKTIMEVERIFY (or OP_CHECKSEQUENCEVERIFY), then script execution // should fail. -// Capture this interaction with the upgraded_nop argument: set it when evaluating -// any script flag that is implemented as an upgraded NOP code. static void ValidateCheckInputsForAllFlags(const CTransaction &tx, script_verify_flags failing_flags, bool add_to_cache, CCoinsViewCache& active_coins_tip, ValidationCache& validation_cache) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { PrecomputedTransactionData txdata; @@ -134,6 +132,10 @@ static void ValidateCheckInputsForAllFlags(const CTransaction &tx, script_verify // script/interpreter.cpp test_flags |= SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_WITNESS; } + if ((test_flags & SCRIPT_VERIFY_TAPROOT)) { + // TAPROOT requires WITNESS + test_flags |= SCRIPT_VERIFY_WITNESS; + } if ((test_flags & SCRIPT_VERIFY_WITNESS)) { // WITNESS requires P2SH test_flags |= SCRIPT_VERIFY_P2SH; From 3ba1bbfa3f85fae32c0b6614db9bb1a1b2db8ce7 Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Sun, 7 Jun 2026 03:16:33 +0200 Subject: [PATCH 006/292] test: exercise Schnorr signature cache in txvalidationcache_tests.cpp --- src/test/txvalidationcache_tests.cpp | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/test/txvalidationcache_tests.cpp b/src/test/txvalidationcache_tests.cpp index dd6360279437..1995c10ed349 100644 --- a/src/test/txvalidationcache_tests.cpp +++ b/src/test/txvalidationcache_tests.cpp @@ -170,6 +170,7 @@ BOOST_FIXTURE_TEST_CASE(checkinputs_test, Dersig100Setup) CScript p2sh_scriptPubKey = GetScriptForDestination(ScriptHash(p2pk_scriptPubKey)); CScript p2pkh_scriptPubKey = GetScriptForDestination(PKHash(coinbaseKey.GetPubKey())); CScript p2wpkh_scriptPubKey = GetScriptForDestination(WitnessV0KeyHash(coinbaseKey.GetPubKey())); + CScript p2tr_scriptPubKey = GetScriptForDestination(WitnessV1Taproot(XOnlyPubKey(coinbaseKey.GetPubKey()))); FillableSigningProvider keystore; BOOST_CHECK(keystore.AddKey(coinbaseKey)); @@ -188,6 +189,7 @@ BOOST_FIXTURE_TEST_CASE(checkinputs_test, Dersig100Setup) CTxOut{11*CENT, p2wpkh_scriptPubKey}, CTxOut{11*CENT, CScript() << OP_CHECKLOCKTIMEVERIFY << OP_DROP << ToByteVector(coinbaseKey.GetPubKey()) << OP_CHECKSIG}, CTxOut{11*CENT, CScript() << OP_CHECKSEQUENCEVERIFY << OP_DROP << ToByteVector(coinbaseKey.GetPubKey()) << OP_CHECKSIG}, + CTxOut{11*CENT, p2tr_scriptPubKey}, }; // Sign, with a non-DER signature @@ -317,6 +319,32 @@ BOOST_FIXTURE_TEST_CASE(checkinputs_test, Dersig100Setup) ValidateCheckInputsForAllFlags(CTransaction(valid_with_witness_tx), SCRIPT_VERIFY_WITNESS, true, m_node.chainman->ActiveChainstate().CoinsTip(), m_node.chainman->m_validation_cache); } + // Test a Taproot (witness v1) key-path spend, to exercise the Schnorr branch of the signature cache. + { + CMutableTransaction tr_tx; + tr_tx.vin = {CTxIn{spend_tx.GetHash(), 4}}; + tr_tx.vout = {CTxOut{11*CENT, p2pk_scriptPubKey}}; + + // Sign P2TR output for key-path spending (i.e. add Schnorr signature to witness stack) + FlatSigningProvider tr_keystore; + tr_keystore.keys.emplace(coinbaseKey.GetPubKey().GetID(), coinbaseKey); + const std::map coins{ + {tr_tx.vin[0].prevout, Coin(spend_tx.vout[4], /*nHeightIn=*/0, /*fCoinBaseIn=*/false)} + }; + std::map input_errors; + BOOST_REQUIRE(SignTransaction(tr_tx, &tr_keystore, coins, {.sighash_type = SIGHASH_DEFAULT}, input_errors)); + auto& witness_stack = tr_tx.vin[0].scriptWitness.stack; + BOOST_REQUIRE(witness_stack.size() == 1 && witness_stack[0].size() == 64); + + // Invalidate signature; an invalid Taproot key-path spend is only invalid if SCRIPT_VERIFY_TAPROOT is set + witness_stack[0][63] ^= 0x01; // damage signature + ValidateCheckInputsForAllFlags(CTransaction(tr_tx), SCRIPT_VERIFY_TAPROOT, true, m_node.chainman->ActiveChainstate().CoinsTip(), m_node.chainman->m_validation_cache); + witness_stack[0][63] ^= 0x01; // repair signature + + // A valid Taproot key-path spend is valid under all flags + ValidateCheckInputsForAllFlags(CTransaction(tr_tx), 0, true, m_node.chainman->ActiveChainstate().CoinsTip(), m_node.chainman->m_validation_cache); + } + { // Test a transaction with multiple inputs. CMutableTransaction tx; From d5e64b01e1bfb3c693d4940f5ede8a321be36c9d Mon Sep 17 00:00:00 2001 From: Guillermo Fernandes <39845628+fernandguil@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:07:09 -0400 Subject: [PATCH 007/292] doc: note -blocknotify is not run during IBD/reindex in help text The -blocknotify command is suppressed for blocks connected during initial block download and reindexing (it only fires once the node is in the post-init sync state). Mention this in the option's help output. Suggested in review of #35518. --- src/init.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/init.cpp b/src/init.cpp index 407ee0fefffe..1e0a0fb6b2b4 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -499,7 +499,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc) ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-fastprune", "Use smaller block files and lower minimum prune height for testing purposes", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); #if HAVE_SYSTEM - argsman.AddArg("-blocknotify=", "Execute command when the best block changes (%s in cmd is replaced by block hash)", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); + argsman.AddArg("-blocknotify=", "Execute command when the best block changes (%s in cmd is replaced by block hash). Not run for blocks connected during initial block download or reindexing.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); #endif argsman.AddArg("-blockreconstructionextratxn=", strprintf("Extra transactions to keep in memory for compact block reconstructions (default: %u)", DEFAULT_BLOCK_RECONSTRUCTION_EXTRA_TXN), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-blocksonly", strprintf("Whether to reject transactions from network peers. Disables automatic broadcast and rebroadcast of transactions, unless the source peer has the 'forcerelay' permission. RPC transactions are not affected. (default: %u)", DEFAULT_BLOCKSONLY), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); From 7e973cce52caeadaa9dcd4b5b6b7f53372e0aa0e Mon Sep 17 00:00:00 2001 From: Hennadii Stepanov <32963518+hebasto@users.noreply.github.com> Date: Mon, 29 Jun 2026 14:49:56 +0100 Subject: [PATCH 008/292] depends: Make tarball creation from local directory reproducible This guarantees `$(package)_sha256_hash` reproducibility regardless of the default behavior of `$(build_TAR)`. Fixes caching on some systems, for example, on FreeBSD. --- depends/funcs.mk | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/depends/funcs.mk b/depends/funcs.mk index f6221ef18f8c..85e28792358f 100644 --- a/depends/funcs.mk +++ b/depends/funcs.mk @@ -47,7 +47,11 @@ endef define fetch_local_dir_sha256 if ! [ -f $($(1)_source) ] || [ -n "$$(find $($(1)_local_dir) -newer $($(1)_source) | head -n1)" ]; then \ mkdir -p $(dir $($(1)_source)) && \ - $(build_TAR) -c -f $($(1)_source) -C $($(1)_local_dir) . && \ + ( \ + cd $($(1)_local_dir) && \ + find . -print0 | TZ=UTC xargs -0r $(build_TOUCH) && \ + find . | LC_ALL=C sort | $(build_TAR) --no-recursion -c -f $($(1)_source) -T - \ + ) && \ rm -f $($(1)_fetched); \ fi && \ if ! [ -f $($(1)_fetched) ] || [ -n "$$(find $($(1)_source) -newer $($(1)_fetched))" ]; then \ From 6e60c362bc1e373a284911381e2a513f57f5f26b Mon Sep 17 00:00:00 2001 From: Musa Haruna Date: Fri, 24 Jul 2026 15:35:44 +0100 Subject: [PATCH 009/292] test: add P2SH sigop counting coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the TODO in `test_witness_sigops()` by adding coverage for sigop accounting in P2SH spends. Co-authored-by: Lőrinc --- test/functional/p2p_segwit.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/functional/p2p_segwit.py b/test/functional/p2p_segwit.py index 92ebe21ad7d8..92b69e49970f 100755 --- a/test/functional/p2p_segwit.py +++ b/test/functional/p2p_segwit.py @@ -29,6 +29,7 @@ MSG_WTX, NODE_NETWORK, NODE_WITNESS, + WITNESS_SCALE_FACTOR, msg_no_witness_block, msg_getdata, msg_headers, @@ -1868,6 +1869,7 @@ def test_witness_sigops(self): # sig ops outputs = (MAX_SIGOP_COST // sigops_per_script) + 2 extra_sigops_available = MAX_SIGOP_COST % sigops_per_script + p2sh_outputs = MAX_SIGOP_COST // (sigops_per_script * WITNESS_SCALE_FACTOR) + 1 # We chose the number of checkmultisigs/checksigs to make this work: assert extra_sigops_available < 100 # steer clear of MAX_OPS_PER_SCRIPT @@ -1891,6 +1893,7 @@ def test_witness_sigops(self): tx.vout.append(CTxOut(split_value, script_pubkey)) tx.vout[-2].scriptPubKey = script_pubkey_toomany tx.vout[-1].scriptPubKey = script_pubkey_justright + tx.vout += [CTxOut(0, script_to_p2sh_script(witness_script)) for _ in range(p2sh_outputs)] block_1 = self.build_next_block() self.update_witness_block_with_transactions(block_1, [tx]) @@ -1919,7 +1922,7 @@ def test_witness_sigops(self): tx2.vout.append(CTxOut(0, script_pubkey_checksigs)) tx2.vin.pop() tx2.wit.vtxinwit.pop() - tx2.vout[0].nValue -= tx.vout[-2].nValue + tx2.vout[0].nValue -= tx.vout[outputs - 2].nValue block_3 = self.build_next_block() self.update_witness_block_with_transactions(block_3, [tx2]) test_witness_block(self.nodes[0], self.test_node, block_3, accepted=False, reason='bad-blk-sigops') @@ -1945,7 +1948,12 @@ def test_witness_sigops(self): self.update_witness_block_with_transactions(block_5, [tx2]) test_witness_block(self.nodes[0], self.test_node, block_5, accepted=True) - # TODO: test p2sh sigop counting + p2sh_tx = CTransaction() + p2sh_tx.vin = [CTxIn(COutPoint(tx.txid_int, outputs + i), CScript([witness_script])) for i in range(p2sh_outputs)] + p2sh_tx.vout.append(CTxOut(0, CScript([OP_TRUE]))) + block_6 = self.build_next_block() + self.update_witness_block_with_transactions(block_6, [p2sh_tx]) + test_witness_block(self.nodes[0], self.test_node, block_6, accepted=False, reason='bad-blk-sigops') # Cleanup and prep for next test self.utxo.pop(0) From bb8bca630a8ad772a15433d7a57cee4868d7d7cb Mon Sep 17 00:00:00 2001 From: Larry Ruane Date: Mon, 14 Aug 2023 22:52:43 -0600 Subject: [PATCH 010/292] change m_last_block_announcement type from int64_t to NodeClock::time_point This non-functional change makes it clear that this variable is a time_point (point-in-time) rather than a duration. (An int64_t can be either). This variable's use is modified in the following commits, so we may as well modernize its type. This type is also mockable, which a later test commit takes advantage of. --- src/net_processing.cpp | 37 +++++++++++++++++------------- src/net_processing.h | 2 +- src/test/denialofservice_tests.cpp | 2 +- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/net_processing.cpp b/src/net_processing.cpp index dfbeac0706dd..2d9c43409197 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -495,7 +495,7 @@ struct CNodeState { ChainSyncTimeoutState m_chain_sync; //! Time of last new block announcement - int64_t m_last_block_announcement{0}; + NodeClock::time_point m_last_block_announcement{NodeClock::epoch}; }; class PeerManagerImpl final : public PeerManager @@ -547,7 +547,7 @@ class PeerManagerImpl final : public PeerManager m_best_block_time = time; }; void UnitTestMisbehaving(NodeId peer_id) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex) { Misbehaving(*Assert(GetPeerRef(peer_id)), ""); }; - void UpdateLastBlockAnnounceTime(NodeId node, int64_t time_in_seconds) override; + void UpdateLastBlockAnnounceTime(NodeId node, NodeClock::time_point time) override; ServiceFlags GetDesirableServiceFlags(ServiceFlags services) const override; private: @@ -1620,11 +1620,11 @@ void PeerManagerImpl::PushNodeVersion(CNode& pnode, const Peer& peer) my_tx_relay, pnode.GetId()); } -void PeerManagerImpl::UpdateLastBlockAnnounceTime(NodeId node, int64_t time_in_seconds) +void PeerManagerImpl::UpdateLastBlockAnnounceTime(NodeId node, NodeClock::time_point time) { LOCK(cs_main); CNodeState *state = State(node); - if (state) state->m_last_block_announcement = time_in_seconds; + if (state) state->m_last_block_announcement = time; } void PeerManagerImpl::InitializeNode(const CNode& node, ServiceFlags our_services) @@ -2972,7 +2972,7 @@ void PeerManagerImpl::UpdatePeerStateForReceivedHeaders(CNode& pfrom, Peer& peer // are still present, however, as belt-and-suspenders. if (received_new_header && last_header.nChainWork > m_chainman.ActiveChain().Tip()->nChainWork) { - nodestate->m_last_block_announcement = GetTime(); + nodestate->m_last_block_announcement = NodeClock::now(); } // If we're in IBD, we want outbound peers that will serve us a useful @@ -4654,7 +4654,7 @@ void PeerManagerImpl::ProcessMessage(Peer& peer, CNode& pfrom, const std::string // If this was a new header with more work than our tip, update the // peer's last block announcement time if (received_new_header && pindex->nChainWork > m_chainman.ActiveChain().Tip()->nChainWork) { - nodestate->m_last_block_announcement = GetTime(); + nodestate->m_last_block_announcement = NodeClock::now(); } if (pindex->nStatus & BLOCK_HAVE_DATA) // Nothing to do here @@ -5393,13 +5393,16 @@ void PeerManagerImpl::EvictExtraOutboundPeers(NodeClock::time_point now) // Check whether we have too many outbound-full-relay peers if (m_connman.GetExtraFullOutboundCount() > 0) { // If we have more outbound-full-relay peers than we target, disconnect one. - // Pick the outbound-full-relay peer that least recently announced + // Pick the outbound-full-relay peer that least-recently announced // us a new block, with ties broken by choosing the more recent - // connection (higher node id) + // connection (higher node id). // Protect peers from eviction if we don't have another connection // to their network, counting both outbound-full-relay and manual peers. - NodeId worst_peer = -1; - int64_t oldest_block_announcement = std::numeric_limits::max(); + struct WorstPeer { + NodeId node; + NodeClock::time_point oldest_block_announcement; + }; + std::optional worst_peer; m_connman.ForEachNode([&](CNode* pnode) EXCLUSIVE_LOCKS_REQUIRED(::cs_main, m_connman.GetNodesMutex()) { AssertLockHeld(::cs_main); @@ -5414,13 +5417,14 @@ void PeerManagerImpl::EvictExtraOutboundPeers(NodeClock::time_point now) // If this is the only connection on a particular network that is // OUTBOUND_FULL_RELAY or MANUAL, protect it. if (!m_connman.MultipleManualOrFullOutboundConns(pnode->addr.GetNetwork())) return; - if (state->m_last_block_announcement < oldest_block_announcement || (state->m_last_block_announcement == oldest_block_announcement && pnode->GetId() > worst_peer)) { - worst_peer = pnode->GetId(); - oldest_block_announcement = state->m_last_block_announcement; + if (!worst_peer.has_value() || + (state->m_last_block_announcement < (*worst_peer).oldest_block_announcement) || + ((state->m_last_block_announcement == (*worst_peer).oldest_block_announcement) && pnode->GetId() > (*worst_peer).node)) { + worst_peer = WorstPeer{pnode->GetId(), state->m_last_block_announcement}; } }); - if (worst_peer != -1) { - bool disconnected = m_connman.ForNode(worst_peer, [&](CNode* pnode) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { + if (worst_peer.has_value()) { + bool disconnected = m_connman.ForNode((*worst_peer).node, [&](CNode* pnode) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { AssertLockHeld(::cs_main); // Only disconnect a peer that has been connected to us for @@ -5430,7 +5434,8 @@ void PeerManagerImpl::EvictExtraOutboundPeers(NodeClock::time_point now) // block from. CNodeState &state = *State(pnode->GetId()); if (now - pnode->m_connected > MINIMUM_CONNECT_TIME && state.vBlocksInFlight.empty()) { - LogDebug(BCLog::NET, "disconnecting extra outbound peer=%d (last block announcement received at time %d)\n", pnode->GetId(), oldest_block_announcement); + LogDebug(BCLog::NET, "disconnecting extra outbound peer=%d (last block announcement received at time %d)\n", + pnode->GetId(), TicksSinceEpoch((*worst_peer).oldest_block_announcement)); pnode->fDisconnect = true; return true; } else { diff --git a/src/net_processing.h b/src/net_processing.h index f29adc0157f9..eb6f22071a0d 100644 --- a/src/net_processing.h +++ b/src/net_processing.h @@ -169,7 +169,7 @@ class PeerManager : public CValidationInterface, public NetEventsInterface virtual void CheckForStaleTipAndEvictPeers() = 0; /** This function is used for testing the stale tip eviction logic, see denialofservice_tests.cpp */ - virtual void UpdateLastBlockAnnounceTime(NodeId node, int64_t time_in_seconds) = 0; + virtual void UpdateLastBlockAnnounceTime(NodeId node, NodeClock::time_point time) = 0; /** * Gets the set of service flags which are "desirable" for a given peer. diff --git a/src/test/denialofservice_tests.cpp b/src/test/denialofservice_tests.cpp index 1a003c00a9ec..7aa2fd54d02e 100644 --- a/src/test/denialofservice_tests.cpp +++ b/src/test/denialofservice_tests.cpp @@ -200,7 +200,7 @@ BOOST_FIXTURE_TEST_CASE(stale_tip_peer_management, OutboundTest) // Update the last announced block time for the last // peer, and check that the next newest node gets evicted. - peerLogic->UpdateLastBlockAnnounceTime(vNodes.back()->GetId(), GetTime()); + peerLogic->UpdateLastBlockAnnounceTime(vNodes.back()->GetId(), NodeClock::now()); peerLogic->CheckForStaleTipAndEvictPeers(); for (int i = 0; i < max_outbound_full_relay - 1; ++i) { From 26a25162a48af567d819a3c59478ae924e4d21ce Mon Sep 17 00:00:00 2001 From: Larry Ruane Date: Mon, 14 Aug 2023 23:20:29 -0600 Subject: [PATCH 011/292] net: add m_last_block_announcement to CNodeStateStats Copy its value from CNodeState. Unused until the next commit. --- src/net_processing.cpp | 1 + src/net_processing.h | 1 + 2 files changed, 2 insertions(+) diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 2d9c43409197..ba9f99525cee 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -1836,6 +1836,7 @@ bool PeerManagerImpl::GetNodeStateStats(NodeId nodeid, CNodeStateStats& stats) c if (queue.pindex) stats.vHeightInFlight.push_back(queue.pindex->nHeight); } + stats.m_last_block_announcement = state->m_last_block_announcement; } PeerRef peer = GetPeerRef(nodeid); diff --git a/src/net_processing.h b/src/net_processing.h index eb6f22071a0d..c2b20237f4a7 100644 --- a/src/net_processing.h +++ b/src/net_processing.h @@ -67,6 +67,7 @@ struct CNodeStateStats { ServiceFlags their_services; int64_t presync_height{-1}; std::chrono::seconds time_offset{0}; + NodeClock::time_point m_last_block_announcement; }; struct PeerManagerInfo { From 7d3d04e925c70c0bb1df25a01feace9a2e3f009f Mon Sep 17 00:00:00 2001 From: Larry Ruane Date: Mon, 14 Aug 2023 23:57:38 -0600 Subject: [PATCH 012/292] rpc: add last_block_announcement to the getpeerinfo output This is the most recent time that this peer was the first to notify our node of a new block (one that we didn't already know about), or zero if this peer has never been the first to notify us of a new block. This can be used to evaluate the quality (performance) of this peer, similar to the existing `last_block` field. This timestamp already exists internally and is used for stale-tip eviction logic; this PR exposes it at the RPC layer. This will also be used (in a later commit) to increase stale-tip test coverage. --- src/rpc/net.cpp | 2 ++ test/functional/rpc_net.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp index ba1080ed11f0..b7cb8e0c8f7a 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -155,6 +155,7 @@ static RPCMethod getpeerinfo() {RPCResult::Type::NUM_TIME, "lastrecv", "The " + UNIX_EPOCH_TIME + " of the last receive"}, {RPCResult::Type::NUM_TIME, "last_transaction", "The " + UNIX_EPOCH_TIME + " of the last valid transaction received from this peer"}, {RPCResult::Type::NUM_TIME, "last_block", "The " + UNIX_EPOCH_TIME + " of the last block received from this peer"}, + {RPCResult::Type::NUM_TIME, "last_block_announcement", "The " + UNIX_EPOCH_TIME + " this peer was first to announce a block"}, {RPCResult::Type::NUM, "bytessent", "The total bytes sent"}, {RPCResult::Type::NUM, "bytesrecv", "The total bytes received"}, {RPCResult::Type::NUM_TIME, "conntime", "The " + UNIX_EPOCH_TIME + " of the connection"}, @@ -252,6 +253,7 @@ static RPCMethod getpeerinfo() obj.pushKV("lastrecv", TicksSinceEpoch(stats.m_last_recv)); obj.pushKV("last_transaction", count_seconds(stats.m_last_tx_time)); obj.pushKV("last_block", count_seconds(stats.m_last_block_time)); + obj.pushKV("last_block_announcement", TicksSinceEpoch(statestats.m_last_block_announcement)); obj.pushKV("bytessent", stats.nSendBytes); obj.pushKV("bytesrecv", stats.nRecvBytes); obj.pushKV("conntime", TicksSinceEpoch(stats.m_connected)); diff --git a/test/functional/rpc_net.py b/test/functional/rpc_net.py index 81cf4da78dbe..4387994a3c39 100755 --- a/test/functional/rpc_net.py +++ b/test/functional/rpc_net.py @@ -107,7 +107,10 @@ def test_getpeerinfo(self): time_now = int(time.time()) peer_info = [x.getpeerinfo() for x in self.nodes] # Verify last_block and last_transaction keys/values. - for node, peer, field in product(range(self.num_nodes), range(2), ['last_block', 'last_transaction']): + for node, peer, field in product(range(self.num_nodes), range(2), + ['last_block', + 'last_block_announcement', + 'last_transaction']): assert field in peer_info[node][peer].keys() if peer_info[node][peer][field] != 0: assert_approx(peer_info[node][peer][field], time_now, vspan=60) @@ -161,6 +164,7 @@ def test_getpeerinfo(self): "inbound": True, "inflight": [], "last_block": 0, + "last_block_announcement": 0, "last_transaction": 0, "lastrecv": 0 if not self.options.v2transport else no_version_peer_conntime, "lastsend": 0 if not self.options.v2transport else no_version_peer_conntime, From 5d5397d841085d024643927d952aa19bf36a8236 Mon Sep 17 00:00:00 2001 From: Larry Ruane Date: Mon, 14 Aug 2023 23:59:54 -0600 Subject: [PATCH 013/292] test: add functional test for block announcement time tracking Co-authored-by: naiyoma --- test/functional/p2p_block_times.py | 105 +++++++++++++++++++++++++++++ test/functional/test_runner.py | 1 + 2 files changed, 106 insertions(+) create mode 100755 test/functional/p2p_block_times.py diff --git a/test/functional/p2p_block_times.py b/test/functional/p2p_block_times.py new file mode 100755 index 000000000000..cc053246749e --- /dev/null +++ b/test/functional/p2p_block_times.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# Copyright (c) The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +"""Test block announcement time tracking + +The bitcoind client records, for each peer, the most recent time that +this peer announced a block that the client wasn't already aware of. This +timestamp, CNodeState::m_last_block_announcement, is available in the +`last_block_announcement` field of each peer's `getpeerinfo` result. The +value zero means that this peer has never been the first to announce +a block to us. Blocks are announced using either the "headers" or +"cmpctblock" messages. + +This timestamp is used when the "potential stale tip" condition occurs: +When a new block hasn't been seen for a longer-than-expected amount of +time (currently 30 minutes, see TipMayBeStale()), the client, suspecting +that there may be new blocks that its peers are not announcing, will +add an extra outbound peer and disconnect (evict) the peer that has +least recently been the first to announce a new block to us. (If there +is a tie, it will disconnect the most recently-added of those peers.) + +This test verifies that this timestamp is being set correctly, see +https://github.com/bitcoin/bitcoin/pull/26172. + +The test announces blocks to the node from a test framework peer and +verifies that the field updates only when the peer is first to announce +a block extending the tip. +""" + +import time +from test_framework.blocktools import ( + create_block, + create_coinbase, +) +from test_framework.messages import ( + CBlockHeader, + msg_headers, +) +from test_framework.p2p import P2PDataStore +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + + +class P2PBlockTimes(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + + def run_test(self): + node = self.nodes[0] + cur_time = int(time.time()) + node.setmocktime(cur_time) + + # Generate one block to exit IBD + self.generate(node, 1) + + self.log.info("Create a full-outbound test framework peer") + peer = node.add_outbound_p2p_connection(P2PDataStore(), p2p_idx=0) + + self.log.info("Test framework peer generates a new block at height 2") + tip = int(node.getbestblockhash(), 16) + block = create_block(tip, create_coinbase(2)) + block.solve() + + self.log.info("Check that last_block_announcement is initially zero") + peerinfo = node.getpeerinfo()[0] + assert_equal(peerinfo['last_block_announcement'], 0) + + self.log.info("Test framework peer sends node the new block") + peer.send_blocks_and_test([block], node, success=True) + + self.log.info("Verify peerinfo block timestamps") + peerinfo = node.getpeerinfo()[0] + assert_equal(peerinfo['last_block'], cur_time) + assert_equal(peerinfo['last_block_announcement'], cur_time) + + self.log.info("Sending a block announcement with no new blocks") + node.setmocktime(cur_time + 1) + headers_message = msg_headers() + headers_message.headers = [CBlockHeader(block)] + peer.send_and_ping(headers_message) + + self.log.info("Verify that block announcement time isn't updated") + peerinfo = node.getpeerinfo()[0] + assert_equal(peerinfo['last_block_announcement'], cur_time) + + # Receiving a second block at height 2 will not be accepted as the tip, + # because its chainwork is not greater than that of the earlier height 2 block. + self.log.info("Create a second block at height 2 (will be stale)") + block2 = create_block(tip, create_coinbase(2), ntime=block.nTime + 1) + block2.solve() + headers_message2 = msg_headers() + headers_message2.headers = [CBlockHeader(block2)] + self.log.info("Test framework peer sends node the new (stale) block") + peer.send_and_ping(headers_message2) + + self.log.info("Verify that block announcement time isn't updated") + peerinfo = node.getpeerinfo()[0] + assert_equal(peerinfo['last_block_announcement'], cur_time) + + +if __name__ == '__main__': + P2PBlockTimes(__file__).main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 5ad5bbbde91b..0ee7f7b7e7cc 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -198,6 +198,7 @@ 'mempool_reorg.py', 'p2p_block_sync.py --v1transport', 'p2p_block_sync.py --v2transport', + 'p2p_block_times.py', 'wallet_createwallet.py --usecli', 'wallet_createwallet.py', 'wallet_reindex.py', From 13ce46ba1a79c99882cc1ac768db1df1f25de098 Mon Sep 17 00:00:00 2001 From: Larry Ruane Date: Tue, 15 Aug 2023 00:28:44 -0600 Subject: [PATCH 014/292] doc: add release note for 27052 The `getpeerinfo` RPC now returns `last_block_announcement`. --- doc/release-notes-27052.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 doc/release-notes-27052.md diff --git a/doc/release-notes-27052.md b/doc/release-notes-27052.md new file mode 100644 index 000000000000..37393c6fc910 --- /dev/null +++ b/doc/release-notes-27052.md @@ -0,0 +1,8 @@ +JSON-RPC +-------- + +The `getpeerinfo` RPC now returns an additional result field, +`last_block_announcement`, which indicates the most recent time +this peer was the first to announce a new block to the local node. +This timestamp, previously internal only, is used by the stale-tip +eviction logic. From 1194918a5d9c4aad1f7a8b9ab2c29666f89c4052 Mon Sep 17 00:00:00 2001 From: Hennadii Stepanov <32963518+hebasto@users.noreply.github.com> Date: Sun, 26 Jul 2026 16:42:48 +0100 Subject: [PATCH 015/292] scripted-diff: Use C.UTF-8 locale in all shell scripts There is no reason to avoid it nowadays. -BEGIN VERIFY SCRIPT- sed -i "s/\/export LC_ALL=C.UTF-8/g" \ $( git grep -l "^export LC_ALL=C$" -- ':!src/ipc/libmultiprocess' ':!src/minisketch' ':!src/secp256k1' ) -END VERIFY SCRIPT- --- ci/lint/01_install.sh | 2 +- ci/lint/06_script.sh | 2 +- cmake/script/macos_zip.sh | 2 +- contrib/devtools/check-deps.sh | 2 +- contrib/devtools/gen-bitcoin-conf.sh | 2 +- contrib/macdeploy/detached-sig-create.sh | 2 +- contrib/qos/tc.sh | 2 +- contrib/verify-commits/gpg.sh | 2 +- contrib/windeploy/detached-sig-create.sh | 2 +- src/qt/res/animation/makespinner.sh | 2 +- test/lint/commit-script-check.sh | 2 +- test/lint/git-subtree-check.sh | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ci/lint/01_install.sh b/ci/lint/01_install.sh index 9c2020a9a15d..ba04b8966b63 100755 --- a/ci/lint/01_install.sh +++ b/ci/lint/01_install.sh @@ -4,7 +4,7 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -export LC_ALL=C +export LC_ALL=C.UTF-8 set -o errexit -o pipefail -o xtrace diff --git a/ci/lint/06_script.sh b/ci/lint/06_script.sh index a0f2dc88bc47..cb4b3bd69c63 100755 --- a/ci/lint/06_script.sh +++ b/ci/lint/06_script.sh @@ -4,7 +4,7 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -export LC_ALL=C +export LC_ALL=C.UTF-8 set -o errexit -o pipefail -o xtrace diff --git a/cmake/script/macos_zip.sh b/cmake/script/macos_zip.sh index cc51699dc938..3ccb780f85d7 100755 --- a/cmake/script/macos_zip.sh +++ b/cmake/script/macos_zip.sh @@ -3,7 +3,7 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or https://opensource.org/license/mit/. -export LC_ALL=C +export LC_ALL=C.UTF-8 if [ -n "$SOURCE_DATE_EPOCH" ]; then find . -exec touch -d "@$SOURCE_DATE_EPOCH" {} + diff --git a/contrib/devtools/check-deps.sh b/contrib/devtools/check-deps.sh index 0ae817254d78..5b36b8189661 100755 --- a/contrib/devtools/check-deps.sh +++ b/contrib/devtools/check-deps.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -export LC_ALL=C +export LC_ALL=C.UTF-8 set -Eeuo pipefail # Declare paths to libraries diff --git a/contrib/devtools/gen-bitcoin-conf.sh b/contrib/devtools/gen-bitcoin-conf.sh index 056182524b6f..88e36eceba72 100755 --- a/contrib/devtools/gen-bitcoin-conf.sh +++ b/contrib/devtools/gen-bitcoin-conf.sh @@ -3,7 +3,7 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -export LC_ALL=C +export LC_ALL=C.UTF-8 TOPDIR=${TOPDIR:-$(git rev-parse --show-toplevel)} BUILDDIR=${BUILDDIR:-$TOPDIR/build} BINDIR=${BINDIR:-$BUILDDIR/bin} diff --git a/contrib/macdeploy/detached-sig-create.sh b/contrib/macdeploy/detached-sig-create.sh index efb0d2472074..da458dd96d06 100755 --- a/contrib/macdeploy/detached-sig-create.sh +++ b/contrib/macdeploy/detached-sig-create.sh @@ -3,7 +3,7 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -export LC_ALL=C +export LC_ALL=C.UTF-8 set -e SIGNAPPLE=signapple diff --git a/contrib/qos/tc.sh b/contrib/qos/tc.sh index 4e4237084e1d..45f52ed30aa0 100755 --- a/contrib/qos/tc.sh +++ b/contrib/qos/tc.sh @@ -4,7 +4,7 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -export LC_ALL=C +export LC_ALL=C.UTF-8 #network interface on which to limit traffic IF="eth0" #limit of the network interface in question diff --git a/contrib/verify-commits/gpg.sh b/contrib/verify-commits/gpg.sh index bcb117f19a05..bed61aae927b 100755 --- a/contrib/verify-commits/gpg.sh +++ b/contrib/verify-commits/gpg.sh @@ -3,7 +3,7 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -export LC_ALL=C +export LC_ALL=C.UTF-8 INPUT=$(cat /dev/stdin) if [ "$BITCOIN_VERIFY_COMMITS_ALLOW_SHA1" = 1 ]; then printf '%s\n' "$INPUT" | gpg --trust-model always "$@" 2>/dev/null diff --git a/contrib/windeploy/detached-sig-create.sh b/contrib/windeploy/detached-sig-create.sh index f417b04662d6..fa3095f7be89 100755 --- a/contrib/windeploy/detached-sig-create.sh +++ b/contrib/windeploy/detached-sig-create.sh @@ -3,7 +3,7 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -export LC_ALL=C +export LC_ALL=C.UTF-8 if [ -z "$OSSLSIGNCODE" ]; then OSSLSIGNCODE=osslsigncode fi diff --git a/src/qt/res/animation/makespinner.sh b/src/qt/res/animation/makespinner.sh index ff7b191ba9a6..3664b3acc00e 100755 --- a/src/qt/res/animation/makespinner.sh +++ b/src/qt/res/animation/makespinner.sh @@ -4,7 +4,7 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -export LC_ALL=C +export LC_ALL=C.UTF-8 FRAMEDIR=$(dirname "$0") for i in {0..35} do diff --git a/test/lint/commit-script-check.sh b/test/lint/commit-script-check.sh index 08fed2196448..d0fc4d3375e6 100755 --- a/test/lint/commit-script-check.sh +++ b/test/lint/commit-script-check.sh @@ -11,7 +11,7 @@ # The resulting script should exactly transform the previous commit into the current # one. Any remaining diff signals an error. -export LC_ALL=C +export LC_ALL=C.UTF-8 if test -z "$1"; then echo "Usage: $0 ..." exit 1 diff --git a/test/lint/git-subtree-check.sh b/test/lint/git-subtree-check.sh index 534fadbad710..246474bb8571 100755 --- a/test/lint/git-subtree-check.sh +++ b/test/lint/git-subtree-check.sh @@ -3,7 +3,7 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -export LC_ALL=C +export LC_ALL=C.UTF-8 check_remote=0 while getopts "?hr" opt; do From 982ee649381f234507e8286f9ca55239a69c1ad7 Mon Sep 17 00:00:00 2001 From: Hennadii Stepanov <32963518+hebasto@users.noreply.github.com> Date: Sun, 26 Jul 2026 16:43:31 +0100 Subject: [PATCH 016/292] lint: Skip `libmultiprocess` subtree in `lint-shell-locale.py` --- test/lint/lint-shell-locale.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lint/lint-shell-locale.py b/test/lint/lint-shell-locale.py index 80b1d4d0f143..69b808489c5d 100755 --- a/test/lint/lint-shell-locale.py +++ b/test/lint/lint-shell-locale.py @@ -38,7 +38,7 @@ def main(): exit_code = 0 shell_files = get_shell_files_list() for file_path in shell_files: - if re.search('src/(secp256k1|minisketch)/', file_path): + if re.search('src/(ipc/libmultiprocess|secp256k1|minisketch)/', file_path): continue with open(file_path, 'r') as file_obj: From 47632ab2a3876825576b3f07e6f80e799e6ed58f Mon Sep 17 00:00:00 2001 From: Henry Romp <151henry151@gmail.com> Date: Sun, 26 Jul 2026 19:43:10 -0400 Subject: [PATCH 017/292] test: add coverage for feebumper uncomputable cluster error path --- test/functional/wallet_bumpfee.py | 74 +++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py index b07425c1a26f..5d0844a9cdf8 100755 --- a/test/functional/wallet_bumpfee.py +++ b/test/functional/wallet_bumpfee.py @@ -19,6 +19,7 @@ COINBASE_MATURITY, ) from test_framework.messages import ( + COIN, MAX_BIP125_RBF_SEQUENCE, MAX_SEQUENCE_NONFINAL, ) @@ -114,6 +115,7 @@ def run_test(self): # Context independent tests test_feerate_checks_replaced_outputs(self, rbf_node, peer_node) test_bumpfee_with_feerate_ignores_walletincrementalrelayfee(self, rbf_node, peer_node) + test_bumpfee_uncomputable_cluster(self, rbf_node, dest_address) def test_invalid_parameters(self, rbf_node, peer_node, dest_address): self.log.info('Test invalid parameters') @@ -829,5 +831,77 @@ def test_bumpfee_with_feerate_ignores_walletincrementalrelayfee(self, rbf_node, self.clear_mempool() +def test_bumpfee_uncomputable_cluster(self, rbf_node, dest_address): + self.log.info('Test that bumpfee fails when unconfirmed UTXOs depend on an enormous cluster') + # CheckFeeRate() can only fail to compute a combined bump fee when + # CTxMemPool::GatherClusters() hits its 500 transaction limit. Clusters are capped + # at 64 transactions, so we need the tx we bump to spend from 8 of them. + # + # That tx can't be in the mempool once they're full, or it would merge all 8 into + # one and get rejected. We can't create it then either, since coin selection runs + # into the same MiniMiner limit. So we broadcast it while the clusters are still + # single transactions and evict it afterwards by raising -minrelaytxfee. It stays + # in the wallet, which is all bumpfee needs. + NUM_CLUSTERS = 8 # 8 * 64 = 512, above the 500 gather limit + CLUSTER_SIZE = 64 + SEED_PAYMENT = COIN // 100 # paid to rbf_node by every seed + SEED_FEE = 20_000 # high enough to survive the -minrelaytxfee bump below + ORIGINAL_TX_FEE = 2000 # ~3 sat/vB, low enough to get evicted by it + + # Keep the peer disconnected so we don't relay txs to it. + self.disconnect_nodes(0, 1) + + # Fund a MiniWallet on rbf_node so the cluster transactions end up in its mempool, + # which is the one the bump fee calculation looks at. The tag keeps these coins + # separate from the default MiniWallet other tests use on this node. get_utxo() + # skips immature coinbases, so mine COINBASE_MATURITY blocks on top. + miniwallet = MiniWallet(rbf_node, tag_name="feebumper_cluster") + self.generatetoaddress(rbf_node, COINBASE_MATURITY + NUM_CLUSTERS, + miniwallet.get_address(), sync_fun=self.no_op) + miniwallet.rescan_utxos() + + # Each seed pays rbf_node and keeps a MiniWallet output we grow the cluster from. + rbf_spk = bytes.fromhex(rbf_node.getaddressinfo(rbf_node.getnewaddress())["scriptPubKey"]) + seeds = [miniwallet.send_to(from_node=rbf_node, scriptPubKey=rbf_spk, + amount=SEED_PAYMENT, fee=SEED_FEE) + for _ in range(NUM_CLUSTERS)] + + # Broadcast the tx we'll bump while every cluster is still a single transaction. + inputs = [{"txid": seed["txid"], "vout": seed["sent_vout"]} for seed in seeds] + outputs = {dest_address: Decimal(NUM_CLUSTERS * SEED_PAYMENT - ORIGINAL_TX_FEE) / COIN} + signed = rbf_node.signrawtransactionwithwallet(rbf_node.createrawtransaction(inputs, outputs)) + original_txid = rbf_node.sendrawtransaction(signed["hex"]) + + # Bumping -minrelaytxfee to 10 sat/vB drops it on mempool reload and rejects the + # resubmission the wallet does on startup. The seeds pay much more, so they stay. + # Note the bump below uses NORMAL (100 sat/vB); if that ever ends up under the + # -minrelaytxfee here, CheckFeeRate() bails on its mempool minimum check instead. + self.restart_node(1, ["-minrelaytxfee=0.0001"] + self.extra_args[1]) + rbf_node.walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT) + assert_equal(set(rbf_node.getrawmempool()), {seed["txid"] for seed in seeds}) + assert_equal(rbf_node.gettransaction(original_txid)["confirmations"], 0) + + # Grow each seed into a full cluster. MiniWallet's default 300 sat/vB keeps these + # above the raised -minrelaytxfee. + for seed in seeds: + miniwallet.send_self_transfer_chain(from_node=rbf_node, + chain_length=CLUSTER_SIZE - 1, + utxo_to_spend=miniwallet.get_utxo(txid=seed["txid"])) + # Make sure the setup worked, so the check below can't pass for the wrong reason. + assert_equal(len(rbf_node.getrawmempool()), NUM_CLUSTERS * CLUSTER_SIZE) + + assert_raises_rpc_error(-4, + "Failed to calculate bump fees, because unconfirmed UTXOs depend on an enormous cluster of unconfirmed transactions.", + rbf_node.bumpfee, original_txid, {"fee_rate": NORMAL}) + + # Restore initial state + rbf_node.abandontransaction(original_txid) + self.generate(rbf_node, 1, sync_fun=self.no_op) + self.restart_node(1, self.extra_args[1]) + rbf_node.walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT) + self.connect_nodes(1, 0) + self.sync_all() + + if __name__ == "__main__": BumpFeeTest(__file__).main() From 975229580f742135fd84c61b68f99acd7c7a4621 Mon Sep 17 00:00:00 2001 From: Ruslan Kasheparov Date: Wed, 1 Jul 2026 10:11:46 +0200 Subject: [PATCH 018/292] rpc: Fix inaccuracies in RPC help docs --- src/rpc/blockchain.cpp | 2 +- src/rpc/fees.cpp | 2 +- src/rpc/mempool.cpp | 6 +++--- src/rpc/net.cpp | 6 +++--- src/wallet/rpc/coins.cpp | 2 +- src/wallet/rpc/transactions.cpp | 4 ++-- src/wallet/rpc/wallet.cpp | 6 +++--- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index f3e1330b4494..af6287aced9e 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1190,7 +1190,7 @@ static RPCMethod gettxout() "gettxout", "Returns details about an unspent transaction output.\n", { - {"txid", RPCArg::Type::STR, RPCArg::Optional::NO, "The transaction id"}, + {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"}, {"n", RPCArg::Type::NUM, RPCArg::Optional::NO, "vout number"}, {"include_mempool", RPCArg::Type::BOOL, RPCArg::Default{true}, "Whether to include the mempool. Note that an unspent output that is spent in the mempool won't appear."}, }, diff --git a/src/rpc/fees.cpp b/src/rpc/fees.cpp index 450af530d941..cc7b99a3b508 100644 --- a/src/rpc/fees.cpp +++ b/src/rpc/fees.cpp @@ -117,7 +117,7 @@ static std::vector FeeEstimateHorizonDoc(bool elide = false) {RPCResult::Type::OBJ, "fail", /*optional=*/true, "information about the highest range of feerates to fail to meet the threshold", FeeRateBucketDoc(/*elide=*/true)}, {RPCResult::Type::ARR, "errors", /*optional=*/true, "Errors encountered during processing (if there are any)", { - {RPCResult::Type::STR, "error", ""}, + {RPCResult::Type::STR, "", "error"}, }}, }; return elide ? ElideGroup(std::move(fields)) : fields; diff --git a/src/rpc/mempool.cpp b/src/rpc/mempool.cpp index c262332942fc..3c4914bdf779 100644 --- a/src/rpc/mempool.cpp +++ b/src/rpc/mempool.cpp @@ -439,7 +439,7 @@ static std::vector ClusterDescription() RPCResult{RPCResult::Type::ARR, "chunks", "chunks in this cluster (in mining order)", {RPCResult{RPCResult::Type::OBJ, "chunk", "", { - RPCResult{RPCResult::Type::NUM, "chunkfee", "fees of the transactions in this chunk"}, + RPCResult{RPCResult::Type::STR_AMOUNT, "chunkfee", "fees of the transactions in this chunk"}, RPCResult{RPCResult::Type::NUM, "chunkweight", "sigops-adjusted weight of all transactions in this chunk"}, RPCResult{RPCResult::Type::ARR, "txs", "transactions in this chunk in mining order", {RPCResult{RPCResult::Type::STR_HEX, "txid", "transaction id"}}}, @@ -650,7 +650,7 @@ static RPCMethod getmempoolfeeratediagram() RPCResult::Type::OBJ, "", "", { {RPCResult::Type::NUM, "weight", "cumulative sigops-adjusted weight"}, - {RPCResult::Type::NUM, "fee", "cumulative fee"} + {RPCResult::Type::STR_AMOUNT, "fee", "cumulative fee"} } } } @@ -1114,7 +1114,7 @@ static RPCMethod getmempoolinfo() {RPCResult::Type::NUM, "maxmempool", "Maximum memory usage for the mempool"}, {RPCResult::Type::STR_AMOUNT, "mempoolminfee", "Minimum fee rate in " + CURRENCY_UNIT + "/kvB for tx to be accepted. Is the maximum of minrelaytxfee and minimum mempool fee"}, {RPCResult::Type::STR_AMOUNT, "minrelaytxfee", "Current minimum relay fee for transactions"}, - {RPCResult::Type::NUM, "incrementalrelayfee", "minimum fee rate increment for mempool limiting or replacement in " + CURRENCY_UNIT + "/kvB"}, + {RPCResult::Type::STR_AMOUNT, "incrementalrelayfee", "minimum fee rate increment for mempool limiting or replacement in " + CURRENCY_UNIT + "/kvB"}, {RPCResult::Type::NUM, "unbroadcastcount", "Current number of transactions that haven't passed initial broadcast yet"}, {RPCResult::Type::BOOL, "permitbaremultisig", "True if the mempool accepts transactions with bare multisig outputs"}, {RPCResult::Type::NUM, "maxdatacarriersize", "Maximum number of bytes that can be used by OP_RETURN outputs in the mempool"}, diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp index 2d20ff161063..bcab3e9e6aba 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -181,7 +181,7 @@ static RPCMethod getpeerinfo() { {RPCResult::Type::STR, "permission_type", Join(NET_PERMISSIONS_DOC, ",\n") + ".\n"}, }}, - {RPCResult::Type::NUM, "minfeefilter", "The minimum fee rate for transactions this peer accepts"}, + {RPCResult::Type::STR_AMOUNT, "minfeefilter", "The minimum fee rate for transactions this peer accepts"}, {RPCResult::Type::OBJ_DYN, "bytessent_per_msg", "", { {RPCResult::Type::NUM, "msg", "The total bytes sent aggregated by message type\n" @@ -684,8 +684,8 @@ static RPCMethod getnetworkinfo() {RPCResult::Type::BOOL, "proxy_randomize_credentials", "Whether randomized credentials are used"}, }}, }}, - {RPCResult::Type::NUM, "relayfee", "minimum relay fee rate for transactions in " + CURRENCY_UNIT + "/kvB"}, - {RPCResult::Type::NUM, "incrementalfee", "minimum fee rate increment for mempool limiting or replacement in " + CURRENCY_UNIT + "/kvB"}, + {RPCResult::Type::STR_AMOUNT, "relayfee", "minimum relay fee rate for transactions in " + CURRENCY_UNIT + "/kvB"}, + {RPCResult::Type::STR_AMOUNT, "incrementalfee", "minimum fee rate increment for mempool limiting or replacement in " + CURRENCY_UNIT + "/kvB"}, {RPCResult::Type::ARR, "localaddresses", "list of local addresses", { {RPCResult::Type::OBJ, "", "", diff --git a/src/wallet/rpc/coins.cpp b/src/wallet/rpc/coins.cpp index e38cb17851d3..bda31cdb828a 100644 --- a/src/wallet/rpc/coins.cpp +++ b/src/wallet/rpc/coins.cpp @@ -490,7 +490,7 @@ RPCMethod listunspent() {RPCResult::Type::NUM, "vout", "the vout value"}, {RPCResult::Type::STR, "address", /*optional=*/true, "the bitcoin address"}, {RPCResult::Type::STR, "label", /*optional=*/true, "The associated label, or \"\" for the default label"}, - {RPCResult::Type::STR, "scriptPubKey", "the output script"}, + {RPCResult::Type::STR_HEX, "scriptPubKey", "the output script"}, {RPCResult::Type::STR_AMOUNT, "amount", "the transaction output amount in " + CURRENCY_UNIT}, {RPCResult::Type::NUM, "confirmations", "The number of confirmations"}, {RPCResult::Type::NUM, "ancestorcount", /*optional=*/true, "The number of in-mempool ancestor transactions, including this one (if transaction is in the mempool)"}, diff --git a/src/wallet/rpc/transactions.cpp b/src/wallet/rpc/transactions.cpp index 480a346f3c7c..a2c456f562e0 100644 --- a/src/wallet/rpc/transactions.cpp +++ b/src/wallet/rpc/transactions.cpp @@ -411,7 +411,7 @@ static std::vector TransactionDescriptionString() {RPCResult::Type::STR, "comment", /*optional=*/true, "If a comment is associated with the transaction, only present if not empty."}, {RPCResult::Type::STR, "bip125-replaceable", /*optional=*/true, "(\"yes|no|unknown\") (DEPRECATED) Whether this transaction signals BIP125 replaceability or has an unconfirmed ancestor signaling BIP125 replaceability.\n" "May be unknown for unconfirmed transactions not in the mempool because their unconfirmed ancestors are unknown."}, - {RPCResult::Type::ARR, "parent_descs", /*optional=*/true, "Only if 'category' is 'received'. List of parent descriptors for the output script of this coin.", { + {RPCResult::Type::ARR, "parent_descs", /*optional=*/true, "Only if 'category' is 'receive'. List of parent descriptors for the output script of this coin.", { {RPCResult::Type::STR, "desc", "The descriptor string."}, }}, }; @@ -711,7 +711,7 @@ RPCMethod gettransaction() {RPCResult::Type::STR_AMOUNT, "fee", /*optional=*/true, "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the \n" "'send' category of transactions."}, {RPCResult::Type::BOOL, "abandoned", "'true' if the transaction has been abandoned (inputs are respendable)."}, - {RPCResult::Type::ARR, "parent_descs", /*optional=*/true, "Only if 'category' is 'received'. List of parent descriptors for the output script of this coin.", { + {RPCResult::Type::ARR, "parent_descs", /*optional=*/true, "Only if 'category' is 'receive'. List of parent descriptors for the output script of this coin.", { {RPCResult::Type::STR, "desc", "The descriptor string."}, }}, }}, diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp index ed3c8bfd8e4a..8abc06a70802 100644 --- a/src/wallet/rpc/wallet.cpp +++ b/src/wallet/rpc/wallet.cpp @@ -48,7 +48,7 @@ static RPCMethod getwalletinfo() {RPCResult::Type::STR, "format", "the database format (only sqlite)"}, {RPCResult::Type::NUM, "txcount", "the total number of transactions in the wallet"}, {RPCResult::Type::NUM, "keypoolsize", "how many new keys are pre-generated (only counts external keys)"}, - {RPCResult::Type::NUM, "keypoolsize_hd_internal", /*optional=*/true, "how many new keys are pre-generated for internal use (used for change outputs, only appears if the wallet is using this feature, otherwise external keys are used)"}, + {RPCResult::Type::NUM, "keypoolsize_hd_internal", "how many new keys are pre-generated for internal use (used for change outputs; 0 if external keys are used for change)"}, {RPCResult::Type::NUM_TIME, "unlocked_until", /*optional=*/true, "the " + UNIX_EPOCH_TIME + " until which the wallet is unlocked for transfers, or 0 if the wallet is locked (only present for passphrase-encrypted wallets)"}, {RPCResult::Type::BOOL, "private_keys_enabled", "false if privatekeys are disabled for this wallet (enforced watch-only wallet)"}, {RPCResult::Type::BOOL, "avoid_reuse", "whether this wallet tracks clean/dirty coins in terms of reuse"}, @@ -149,7 +149,7 @@ static RPCMethod listwalletdir() {RPCResult::Type::OBJ, "", "", { {RPCResult::Type::STR, "name", "The wallet name"}, - {RPCResult::Type::ARR, "warnings", /*optional=*/true, "Warning messages, if any, related to loading the wallet.", + {RPCResult::Type::ARR, "warnings", "Warning messages related to loading the wallet (may be empty).", { {RPCResult::Type::STR, "", ""}, }}, @@ -495,7 +495,7 @@ RPCMethod simulaterawtransaction() "simulaterawtransaction", "Calculate the balance change resulting in the signing and broadcasting of the given transaction(s).\n", { - {"rawtxs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "An array of hex strings of raw transactions.\n", + {"rawtxs", RPCArg::Type::ARR, RPCArg::Optional::NO, "An array of hex strings of raw transactions.\n", { {"rawtx", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, ""}, }, From 8976ba50a2b9cfa8f5b7898bd84f31be25270d5e Mon Sep 17 00:00:00 2001 From: Ruslan Kasheparov Date: Wed, 1 Jul 2026 10:15:15 +0200 Subject: [PATCH 019/292] rpc: require connman in getnetworkinfo --- src/rpc/net.cpp | 60 ++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp index bcab3e9e6aba..83b9ff738047 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -717,40 +717,34 @@ static RPCMethod getnetworkinfo() obj.pushKV("subversion", strSubVersion); obj.pushKV("protocolversion",PROTOCOL_VERSION); NodeContext& node = EnsureAnyNodeContext(request.context); - if (node.connman) { - ServiceFlags services = node.connman->GetLocalServices(); - obj.pushKV("localservices", strprintf("%016x", services)); - obj.pushKV("localservicesnames", GetServicesNames(services)); - } - if (node.peerman) { - auto peerman_info{node.peerman->GetInfo()}; - obj.pushKV("localrelay", !peerman_info.ignores_incoming_txs); - obj.pushKV("timeoffset", Ticks(peerman_info.median_outbound_time_offset)); - obj.pushKV("tx_send_rate", peerman_info.tx_send_rate); - auto buckjson = [&](const auto& buckinfo) { - UniValue b{UniValue::VOBJ}; - b.pushKV("backlog", buckinfo.backlog_count); - b.pushKV("count_tok", buckinfo.count_bucket); - b.pushKV("size_tok", buckinfo.size_bucket); - return b; - }; - UniValue invbuckets{UniValue::VOBJ}; - invbuckets.pushKV("inbound", buckjson(peerman_info.inbound_bucket)); - invbuckets.pushKV("outbound", buckjson(peerman_info.outbound_bucket)); - obj.pushKV("inv_buckets", invbuckets); - } - if (node.connman) { - obj.pushKV("networkactive", node.connman->GetNetworkActive()); - obj.pushKV("connections", node.connman->GetNodeCount(ConnectionDirection::Both)); - obj.pushKV("connections_in", node.connman->GetNodeCount(ConnectionDirection::In)); - obj.pushKV("connections_out", node.connman->GetNodeCount(ConnectionDirection::Out)); - } + CConnman& connman = EnsureConnman(node); + ServiceFlags services = connman.GetLocalServices(); + obj.pushKV("localservices", strprintf("%016x", services)); + obj.pushKV("localservicesnames", GetServicesNames(services)); + auto peerman_info{EnsurePeerman(node).GetInfo()}; + obj.pushKV("localrelay", !peerman_info.ignores_incoming_txs); + obj.pushKV("timeoffset", Ticks(peerman_info.median_outbound_time_offset)); + obj.pushKV("tx_send_rate", peerman_info.tx_send_rate); + auto buckjson = [&](const auto& buckinfo) { + UniValue b{UniValue::VOBJ}; + b.pushKV("backlog", buckinfo.backlog_count); + b.pushKV("count_tok", buckinfo.count_bucket); + b.pushKV("size_tok", buckinfo.size_bucket); + return b; + }; + UniValue invbuckets{UniValue::VOBJ}; + invbuckets.pushKV("inbound", buckjson(peerman_info.inbound_bucket)); + invbuckets.pushKV("outbound", buckjson(peerman_info.outbound_bucket)); + obj.pushKV("inv_buckets", invbuckets); + obj.pushKV("networkactive", connman.GetNetworkActive()); + obj.pushKV("connections", connman.GetNodeCount(ConnectionDirection::Both)); + obj.pushKV("connections_in", connman.GetNodeCount(ConnectionDirection::In)); + obj.pushKV("connections_out", connman.GetNodeCount(ConnectionDirection::Out)); obj.pushKV("networks", GetNetworksInfo()); - if (node.mempool) { - // Those fields can be deprecated, to be replaced by the getmempoolinfo fields - obj.pushKV("relayfee", ValueFromAmount(node.mempool->m_opts.min_relay_feerate.GetFeePerK())); - obj.pushKV("incrementalfee", ValueFromAmount(node.mempool->m_opts.incremental_relay_feerate.GetFeePerK())); - } + const CTxMemPool& mempool = EnsureAnyMemPool(request.context); + // Those fields can be deprecated, to be replaced by the getmempoolinfo fields + obj.pushKV("relayfee", ValueFromAmount(mempool.m_opts.min_relay_feerate.GetFeePerK())); + obj.pushKV("incrementalfee", ValueFromAmount(mempool.m_opts.incremental_relay_feerate.GetFeePerK())); UniValue localAddresses(UniValue::VARR); { LOCK(g_maplocalhost_mutex); From adbff4d442864061171c693d6f57b51c9d8180c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Thu, 30 Jul 2026 12:16:19 -0400 Subject: [PATCH 020/292] init: don't suggest -reindex-chainstate for recovery on a pruned node --- src/init.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/init.cpp b/src/init.cpp index f22302ff21ad..47ea1966f6ab 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1894,7 +1894,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) bool do_retry{HasTestOption(args, "reindex_after_failure_noninteractive_yes") || uiInterface.ThreadSafeQuestion( error + Untranslated(".\n\n") + _("Do you want to rebuild the databases now?"), - error.original + ".\nPlease restart with -reindex or -reindex-chainstate to recover.", + error.original + (args.GetIntArg("-prune", 0) ? ".\nPlease restart with -reindex to recover." : ".\nPlease restart with -reindex or -reindex-chainstate to recover."), CClientUIInterface::MSG_ERROR | CClientUIInterface::BTN_ABORT)}; if (!do_retry) { return false; From 330022993fb96b3b776e562f1de6696d381e6524 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Wed, 15 Jul 2026 13:23:51 -0400 Subject: [PATCH 021/292] coins: filter coinbase txid from parallel input fetching A non-segwit invalid block could spend its own coinbase output. In that case we would want to skip fetching the coinbase prevout since it would already be in the CoinsViewOverlay's cache and would cause block validation to revert to synchronous fetching. Co-authored-by: Pieter Wuille --- src/coins.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/coins.cpp b/src/coins.cpp index 7bb05f68c1a6..39c9c12dbee8 100644 --- a/src/coins.cpp +++ b/src/coins.cpp @@ -372,12 +372,13 @@ CCoinsViewCache::ResetGuard CoinsViewOverlay::StartFetching(const CBlock& block Assert(m_inputs.empty()); Assert(m_input_head.load(std::memory_order_relaxed) == 0); Assert(m_input_tail == 0); - if (const auto workers_count{m_thread_pool->WorkersCount()}; workers_count > 0) { + if (const auto workers_count{m_thread_pool->WorkersCount()}; workers_count > 0 && block.vtx.size() > 1) { // Loop through the block inputs and set their prevouts in the queue. // Filter inputs that spend outputs created earlier in the same block. These outputs will be created // directly in the cache from the tx that creates them, so they will not be requested from a base view. std::unordered_set earlier_txids; earlier_txids.reserve(block.vtx.size()); + earlier_txids.emplace(block.vtx[0]->GetHash()); for (const auto& tx : block.vtx | std::views::drop(1)) { for (const auto& input : tx->vin) { if (!earlier_txids.contains(input.prevout.hash)) m_inputs.emplace_back(input.prevout); From 2ffaa6e6a7db239306309a859b7e8aed478a810d Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Wed, 15 Jul 2026 13:24:11 -0400 Subject: [PATCH 022/292] coins: delete Sync and SetBackend on CoinsViewOverlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neither is called in production code, and both would write to or swap the base view, which is unsafe while workers are still fetching. Hide the non-virtual base class methods with deleted ones. Also fix a doubled comment and reuse the existing overlay pointer in the coins_view fuzz target. Co-authored-by: Lőrinc --- src/coins.h | 4 ++++ src/test/fuzz/coins_view.cpp | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/coins.h b/src/coins.h index 6e8643ccc252..5bf4c07e7afa 100644 --- a/src/coins.h +++ b/src/coins.h @@ -733,6 +733,10 @@ class CoinsViewOverlay : public CCoinsViewCache CCoinsViewCache::Flush(reallocate_cache); } + //! Swapping the backend or writing through to it with Sync() is not supported while fetching. + void SetBackend(CCoinsView&) = delete; + void Sync() = delete; + //! Verify that all parallel fetched input prevouts have been consumed. bool AllInputsConsumed() const noexcept { return m_input_tail == m_inputs.size(); } }; diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp index 5e1b08ade52f..343eb993f0dc 100644 --- a/src/test/fuzz/coins_view.cpp +++ b/src/test/fuzz/coins_view.cpp @@ -204,7 +204,7 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& co coins_view_cache.Uncache(random_out_point); }, [&] { - if (overlay) return; // // CoinsViewOverlay::SetBackend() is never called in production code + if (overlay) return; // CoinsViewOverlay::SetBackend() is never called in production code const bool use_original_backend{fuzzed_data_provider.ConsumeBool()}; if (use_original_backend && backend_coins_view != original_backend) { // FRESH flags valid against the empty backend may be invalid @@ -382,7 +382,7 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& co // If HaveCoin on the backend is true, it must also be on the cache if the coin wasn't spent. std::optional coin_in_backend; bool exists_using_have_coin_in_backend; - if (dynamic_cast(&coins_view_cache)) { + if (overlay) { // PeekCoin does not mutate cacheCoins, so async workers can keep running. coin_in_backend = backend_coins_view->PeekCoin(random_out_point); exists_using_have_coin_in_backend = coin_in_backend.has_value(); From d552c52b081c4c0853086bcdce38d67fd02f2c67 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Wed, 15 Jul 2026 13:24:24 -0400 Subject: [PATCH 023/292] coins: log error reason when prevout fetch submission fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lőrinc --- src/coins.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coins.cpp b/src/coins.cpp index 39c9c12dbee8..83eefb519583 100644 --- a/src/coins.cpp +++ b/src/coins.cpp @@ -396,7 +396,7 @@ CCoinsViewCache::ResetGuard CoinsViewOverlay::StartFetching(const CBlock& block // Submit can fail if a shared owner of the thread pool outside of this class calls Stop() or // Interrupt() on a different thread after we call WorkersCount() above. In that case parallel // fetching will not make progress, so we clear the inputs to fall back to single threaded fetching. - LogWarning("Failed to submit prevout fetch tasks; falling back to single-threaded fetching for this block."); + LogWarning("Failed to submit prevout fetch tasks (%s); falling back to single-threaded fetching for this block.", SubmitErrorString(futures.error())); m_inputs.clear(); StopFetching(); // Assert nothing changed if we failed to start tasks. } From 5292386b785a0a133368b5429a2363cf53e29e00 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Wed, 15 Jul 2026 13:24:57 -0400 Subject: [PATCH 024/292] doc: improve CoinsViewOverlay documentation Document the single main thread requirement, clarify how FetchCoinFromBase consumes fetched inputs, and note why Reset must stop fetching. Also explain a no-op StartFetching call in the unit tests. Co-authored-by: Ryan Ofsky --- src/coins.h | 30 +++++++++++++++++------------ src/test/coinsviewoverlay_tests.cpp | 2 ++ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/coins.h b/src/coins.h index 5bf4c07e7afa..15f1f34c4bd6 100644 --- a/src/coins.h +++ b/src/coins.h @@ -556,12 +556,16 @@ class CCoinsViewCache : public CCoinsViewBacked }; /** - * CCoinsViewCache subclass that asynchronously fetches most block input prevouts in parallel during ConnectBlock without - * mutating the base cache. + * CCoinsViewCache subclass that asynchronously fetches most block input prevouts in parallel during ConnectBlock + * without mutating the base cache. This is achieved by fetching coins from the base view using PeekCoin() instead of + * GetCoin(), so intermediate CCoinsViewCache layers are not filled. * - * Only used in ConnectBlock to pass as an ephemeral view that can be reset if the block is invalid. - * It provides the same interface as CCoinsViewCache. - * It adds an additional StartFetching method to provide the block. + * Used during ConnectBlock() as an ephemeral, resettable top-level view that is flushed only on success, so invalid + * blocks don't pollute the underlying cache. + * + * While this class uses threads internally to fetch coins, externally it is only safe to call its methods from a + * single "main" thread. It assumes StartFetching, StopFetching, FetchCoinFromBase, Flush and Reset will all only be + * called from the main thread. * * When a block is passed to StartFetching, the inputs of the block are flattened into a vector of InputToFetch * objects. StartFetching then submits worker tasks to a ThreadPool and keeps the returned futures alive until fetching @@ -571,17 +575,16 @@ class CCoinsViewCache : public CCoinsViewBacked * m_inputs vector at a time. Workers race to claim inputs, so they may fetch elements in any order. If the fetched * index is greater than or equal to the size of m_inputs, no more inputs can be fetched and false is returned. * - * The worker claims the InputToFetch at this index, fetches the coin from the base cache and moves it into the + * The worker claims the InputToFetch at this index, fetches the coin with base->PeekCoin() and moves it into the * InputToFetch object. The ready flag is then set with a release memory order. This allows the ready flag to be * used as a memory fence, guaranteeing the coin being written to the object will have happened before another * thread tests the flag with an acquire memory order. - * This assumes all base->PeekCoin() paths are safe for concurrent readers and do not mutate lower cache layers. + * This assumes all base->PeekCoin() paths are safe for concurrent readers. * - * When a coin is requested from the cache on the main thread and is not already in cacheCoins map, FetchCoinFromBase - * checks whether the next unconsumed entry in m_inputs has the requested outpoint. On a match, m_input_tail is advanced - * and the entry's ready flag is waited on with an acquire memory order until a worker has finished fetching it. The - * coin is then moved out and returned. Since the main thread is the only consumer of validation results, it blocks - * on the specific input it needs rather than racing workers for other inputs. + * The main thread is the only consumer of the fetched coins. FetchCoinFromBase is called when a coin is requested on + * the main thread and is not already in the cache. It checks whether the next unconsumed entry in m_inputs has the + * requested outpoint. On a match, m_input_tail is advanced and the entry's ready flag is waited on with an acquire + * memory order until a worker has finished fetching it. The coin is then moved out and returned. * * StopFetching() is called in Flush() and in Reset() (the per-block teardown) so workers stop before the block they * reference goes away. It stops fetching by moving m_input_head to the end of m_inputs (so workers quickly exit), @@ -705,6 +708,9 @@ class CoinsViewOverlay : public CCoinsViewCache std::vector> m_futures{}; protected: + //! StopFetching must be called here for two reasons: InputToFetch objects hold references to the + //! block's outpoints, so they must not outlive the block being connected; and when connecting a + //! block fails, workers must not keep fetching inputs for the block that was abandoned. void Reset() noexcept override { StopFetching(); diff --git a/src/test/coinsviewoverlay_tests.cpp b/src/test/coinsviewoverlay_tests.cpp index d6403752b0e6..e6ea22175989 100644 --- a/src/test/coinsviewoverlay_tests.cpp +++ b/src/test/coinsviewoverlay_tests.cpp @@ -203,6 +203,8 @@ BOOST_AUTO_TEST_CASE(access_non_input_coins) main_cache.EmplaceCoinInternalDANGER(COutPoint{outpoint}, std::move(coin)); CoinsViewOverlay view{&main_cache, MakeStartedThreadPool()}; + // The block has no non-coinbase transactions, so this fetches nothing and only creates the + // reset guard. All lookups below use the fallback path. const auto reset_guard{view.StartFetching(block)}; // Non-input fallback hit. From 8e4b7ab7258aa8497ef90847e495e81d984b99d4 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Wed, 15 Jul 2026 13:25:06 -0400 Subject: [PATCH 025/292] fuzz: use per-level fetch scopes in coinscache_sim Keep one StartFetching guard per cache level instead of a single guard for the top level, so overlays continue fetching while new cache levels are added on top. Tear the guards down top down, since resetting a lower cache while an upper overlay's workers read through it would cause a data race. Co-authored-by: Ryan Ofsky --- src/test/fuzz/coinscache_sim.cpp | 34 +++++++++++++++++++------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/test/fuzz/coinscache_sim.cpp b/src/test/fuzz/coinscache_sim.cpp index 3e5d29a2c249..f78625039054 100644 --- a/src/test/fuzz/coinscache_sim.cpp +++ b/src/test/fuzz/coinscache_sim.cpp @@ -226,8 +226,8 @@ FUZZ_TARGET(coinscache_sim, .init = [] { static auto setup{MakeNoLogFileContext< CoinsViewBottom bottom; /** Real CCoinsViewCache objects. */ std::vector> caches; - /** Long-lived StartFetching guard (nullptr unless corresponding level is a CoinsViewOverlay). */ - std::unique_ptr overlay_fetch_scope; + /** Long-lived StartFetching guards, parallel to `caches` (entries are nullptr unless corresponding level is a CoinsViewOverlay). */ + std::vector> fetch_scopes; /** Simulated cache data (sim_caches[0] matches bottom, sim_caches[i+1] matches caches[i]). */ CacheLevel sim_caches[MAX_CACHES + 1]; /** Current height in the simulation. */ @@ -265,6 +265,12 @@ FUZZ_TARGET(coinscache_sim, .init = [] { static auto setup{MakeNoLogFileContext< } }; + /** Helper creating a fetch scope for the top cache (which must be a CoinsViewOverlay). */ + const auto make_fetch_scope{[&] { + auto& overlay{static_cast(*caches.back())}; + return std::make_unique(overlay, data.block); + }}; + // Main simulation loop: read commands from the fuzzer input, and apply them // to both the real cache stack and the simulation. FuzzedDataProvider provider(buffer.data(), buffer.size()); @@ -275,8 +281,10 @@ FUZZ_TARGET(coinscache_sim, .init = [] { static auto setup{MakeNoLogFileContext< // Make sure there is always at least one CCoinsViewCache. if (caches.empty()) { caches.emplace_back(new CCoinsViewCache(&bottom, /*deterministic=*/true)); + fetch_scopes.emplace_back(); sim_caches[caches.size()].Wipe(); } + assert(caches.size() == fetch_scopes.size()); // Execute command. CallOneOf( @@ -403,17 +411,13 @@ FUZZ_TARGET(coinscache_sim, .init = [] { static auto setup{MakeNoLogFileContext< [&]() { // Add a cache level (if not already at the max). if (caches.size() != MAX_CACHES) { - if (overlay_fetch_scope) { - overlay_fetch_scope.reset(); - sim_caches[caches.size()].Wipe(); - } // Apply to real caches. if (provider.ConsumeBool()) { caches.emplace_back(new CCoinsViewCache(&*caches.back(), /*deterministic=*/true)); + fetch_scopes.emplace_back(); } else { caches.emplace_back(new CoinsViewOverlay(&*caches.back(), g_thread_pool, /*deterministic=*/true)); - auto& overlay{static_cast(*caches.back())}; - overlay_fetch_scope = std::make_unique(overlay, data.block); + fetch_scopes.emplace_back(make_fetch_scope()); } // Apply to simulation data. sim_caches[caches.size()].Wipe(); @@ -423,7 +427,7 @@ FUZZ_TARGET(coinscache_sim, .init = [] { static auto setup{MakeNoLogFileContext< [&]() { // Remove a cache level. // Apply to real caches (this reduces caches.size(), implicitly doing the same on the simulation data). caches.back()->SanityCheck(); - overlay_fetch_scope.reset(); + fetch_scopes.pop_back(); caches.pop_back(); }, @@ -440,7 +444,7 @@ FUZZ_TARGET(coinscache_sim, .init = [] { static auto setup{MakeNoLogFileContext< }, [&]() { // Sync. - if (overlay_fetch_scope) return; // CoinsViewOverlay::Sync() is never called in production + if (fetch_scopes.back()) return; // CoinsViewOverlay::Sync() is never called in production // Apply to simulation data (note that in our simulation, syncing and flushing is the same thing). flush(); // Apply to real caches. @@ -450,10 +454,9 @@ FUZZ_TARGET(coinscache_sim, .init = [] { static auto setup{MakeNoLogFileContext< [&]() { // Reset. sim_caches[caches.size()].Wipe(); // Apply to real caches. Optionally start fetching again. - if (overlay_fetch_scope && provider.ConsumeBool()) { - overlay_fetch_scope.reset(); - auto& overlay{static_cast(*caches.back())}; - overlay_fetch_scope = std::make_unique(overlay, data.block); + if (fetch_scopes.back() && provider.ConsumeBool()) { + fetch_scopes.back().reset(); // Stop fetching before starting again. + fetch_scopes.back() = make_fetch_scope(); } else { (void)caches.back()->CreateResetGuard(); } @@ -514,4 +517,7 @@ FUZZ_TARGET(coinscache_sim, .init = [] { static auto setup{MakeNoLogFileContext< assert(realcoin->nHeight == sim->second); } } + + // Tear down the fetch scopes top down. Otherwise lower level could reset while upper level is reading from it. + while (!fetch_scopes.empty()) fetch_scopes.pop_back(); } From 0b3bb071036ac00649901b7a806e3882b61ddfde Mon Sep 17 00:00:00 2001 From: azuchi Date: Mon, 27 Jul 2026 10:51:31 +0900 Subject: [PATCH 026/292] test: add coverage for untested descriptor parse error paths Coverage measurement of master (unit and functional tests combined) shows that a number of error branches in descriptor.cpp are never exercised by any test. Add CheckUnparsable vectors for each reachable one: - musig(): unterminated expression, invalid participant key, trailing garbage after a participant key, invalid derivation path element, and participants with multipath derivations of mismatched lengths - context restrictions: multi() inside tr(), multi_a() at top level, and addr()/tr()/rawtr()/raw() inside sh() - taptree structure errors: exceeding the 128 nesting level limit, missing '}' after a right branch, missing ',' after a left branch, and trailing garbage after a script expression or the internal key - rawtr() with an invalid key A positive check verifies that a taptree of exactly 128 nesting levels still parses and expands, so the limit is exercised on both sides. Since CheckUnparsable asserts on the exact error message and each targeted branch produces a distinct one, a passing vector proves the corresponding branch executed. --- src/test/descriptor_tests.cpp | 50 +++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/test/descriptor_tests.cpp b/src/test/descriptor_tests.cpp index e22bcb21b255..106486f6c61f 100644 --- a/src/test/descriptor_tests.cpp +++ b/src/test/descriptor_tests.cpp @@ -1286,6 +1286,56 @@ BOOST_AUTO_TEST_CASE(descriptor_test) // Fuzzer crash test cases CheckUnparsable("pk(musig(dd}uue/00/)k(", "pk(musig(dd}uue/00/)k(", "'pk(musig(dd}uue/00/)k(' is not a valid descriptor function"); CheckUnparsable("tr(musig(tuus(oldepk(gg)ggggfgg)<,z(((((((((((((((((((((st)", "tr(musig(tuus(oldepk(gg)ggggfgg)<,z(((((((((((((((((((((st)","tr(): Too many ')' in musig() expression"); + + // Unterminated musig() expression + CheckUnparsable("tr(musig(00)", "tr(musig(00)", "tr(): Invalid musig() expression"); + // Invalid musig() participant key + CheckUnparsable("tr(musig(00))", "tr(musig(00))", "tr(): musig(): Pubkey '00' is invalid"); + // Garbage after a musig() participant key. The '}' closes the level opened by the '(' of + // musig( and the '{' re-opens it, so the final ')' stays inside the expression span. + CheckUnparsable("tr(musig(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1}{))", "tr(musig(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd}{))", "tr(): musig(): expected ',', got '}'"); + // Invalid musig() derivation path element + CheckUnparsable("tr(musig(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y)/4294967296)", "tr(musig(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y)/4294967296)", "tr(): musig(): Key path value '4294967296' is not a valid uint32"); + // musig() participants with multipath derivations of mismatched lengths + CheckUnparsable("tr(musig(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/<0;1>,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/<0;1;2>))", "tr(musig(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/<0;1>,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/<0;1;2>))", "tr(): musig(): Multipath derivation paths have mismatched lengths"); + // multi() is not allowed inside tr() + CheckUnparsable("tr(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1,multi(1,Kx9HCDjGiwFcgVNhTrS5z5NeZdD6veeam61eDxLDCkGWujvL4Gnn))", "tr(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,multi(1,032707170c71d8f75e4ca4e3fce870b9409dcaf12b051d3bcadff74747fa7619c0))", "Can only have multi/sortedmulti at top level, in sh(), or in wsh()"); + // multi_a() is not allowed at top level + CheckUnparsable("multi_a(1,L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)", "multi_a(1,03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)", "Can only have multi_a/sortedmulti_a inside tr()"); + // addr() is not allowed inside sh() + CheckUnparsable("sh(addr(asdf))", "sh(addr(asdf))", "Can only have addr() at top level"); + // Garbage after the tr() internal key + CheckUnparsable("tr(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1}(x))", "tr(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd}(x))", "tr: expected ',', got '}'"); + // Too many taptree nesting levels + CheckUnparsable("tr(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1," + std::string(129, '{') + ")", "tr(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd," + std::string(129, '{') + ")", "tr() supports at most 128 nesting levels"); + // A taptree of exactly 128 nesting levels is valid, so the limit above is not off by one + { + std::string tree = "pk(032707170c71d8f75e4ca4e3fce870b9409dcaf12b051d3bcadff74747fa7619c0)"; + for (int i = 0; i < 128; ++i) { + tree = "{" + tree + ",pk(02aa27e5eb2c185e87cd1dbc3e0efc9cb1175235e0259df1713424941c3cb40402)}"; + } + FlatSigningProvider tree_keys; + std::string tree_error; + auto parsed = Parse("tr(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd," + tree + ")", tree_keys, tree_error); + BOOST_CHECK_MESSAGE(!parsed.empty(), tree_error); + std::vector scripts; + FlatSigningProvider provider; + BOOST_CHECK(parsed.at(0)->Expand(0, tree_keys, scripts, provider)); + } + // Missing '}' after the right branch of a taptree pair + CheckUnparsable("tr(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1,{pk(Kx9HCDjGiwFcgVNhTrS5z5NeZdD6veeam61eDxLDCkGWujvL4Gnn),pk(L4o2kDvXXDRH2VS9uBnouScLduWt4dZnM25se7kvEjJeQ285en2A),pk(L4o2kDvXXDRH2VS9uBnouScLduWt4dZnM25se7kvEjJeQ285en2A)})", "tr(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,{pk(032707170c71d8f75e4ca4e3fce870b9409dcaf12b051d3bcadff74747fa7619c0),pk(02aa27e5eb2c185e87cd1dbc3e0efc9cb1175235e0259df1713424941c3cb40402),pk(02aa27e5eb2c185e87cd1dbc3e0efc9cb1175235e0259df1713424941c3cb40402)})", "tr(): expected '}' after script expression"); + // Missing ',' after the left branch of a taptree pair + CheckUnparsable("tr(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1,{pk(Kx9HCDjGiwFcgVNhTrS5z5NeZdD6veeam61eDxLDCkGWujvL4Gnn)})", "tr(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,{pk(032707170c71d8f75e4ca4e3fce870b9409dcaf12b051d3bcadff74747fa7619c0)})", "tr(): expected ',' after script expression"); + // Garbage after a taptree script expression + CheckUnparsable("tr(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1,pk(Kx9HCDjGiwFcgVNhTrS5z5NeZdD6veeam61eDxLDCkGWujvL4Gnn),pk(L4o2kDvXXDRH2VS9uBnouScLduWt4dZnM25se7kvEjJeQ285en2A))", "tr(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,pk(032707170c71d8f75e4ca4e3fce870b9409dcaf12b051d3bcadff74747fa7619c0),pk(02aa27e5eb2c185e87cd1dbc3e0efc9cb1175235e0259df1713424941c3cb40402))", "tr(): expected ')' after script expression"); + // tr() is not allowed inside sh() + CheckUnparsable("sh(tr(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1))", "sh(tr(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))", "Can only have tr at top level"); + // Invalid rawtr() key + CheckUnparsable("rawtr(00)", "rawtr(00)", "rawtr(): Pubkey '00' is invalid"); + // rawtr() is not allowed inside sh() + CheckUnparsable("sh(rawtr(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1))", "sh(rawtr(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))", "Can only have rawtr at top level"); + // raw() is not allowed inside sh() + CheckUnparsable("sh(raw(00))", "sh(raw(00))", "Can only have raw() at top level"); } BOOST_AUTO_TEST_CASE(descriptor_literal_null_byte) From a31eb8bfa41262528cc4c5a1f9cd46c826597552 Mon Sep 17 00:00:00 2001 From: Hennadii Stepanov <32963518+hebasto@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:43:08 +0100 Subject: [PATCH 027/292] ci, iwyu: Request IPC file generation explicitly Fixes a part of https://github.com/bitcoin/bitcoin/issues/35361. --- ci/test/00_setup_env_native_iwyu.sh | 5 ++++- ci/test/03_test_script.sh | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ci/test/00_setup_env_native_iwyu.sh b/ci/test/00_setup_env_native_iwyu.sh index 14c367bb31cd..e3b71b69cd18 100755 --- a/ci/test/00_setup_env_native_iwyu.sh +++ b/ci/test/00_setup_env_native_iwyu.sh @@ -17,7 +17,10 @@ export RUN_FUNCTIONAL_TESTS=false export RUN_FUZZ_TESTS=false export RUN_CHECK_DEPS=false export RUN_IWYU=true -export GOAL="codegen" +# Adding non-codegen targets to the build goal is a workaround +# for https://gitlab.kitware.com/cmake/cmake/-/work_items/27862 +# and https://github.com/bitcoin-core/libmultiprocess/issues/284. +export GOAL="codegen mp_headers mptest_headers bitcoin_ipc_headers bitcoin_ipc_test_headers bitcoin_ipc_fuzz_headers" export BITCOIN_CONFIG="\ --preset dev-mode -DBUILD_GUI=OFF \ -DCMAKE_C_COMPILER=clang-${IWYU_LLVM_V} \ diff --git a/ci/test/03_test_script.sh b/ci/test/03_test_script.sh index 834473b1ada9..dad3fab82840 100755 --- a/ci/test/03_test_script.sh +++ b/ci/test/03_test_script.sh @@ -123,7 +123,7 @@ cmake -S "$BASE_ROOT_DIR" -B "$BASE_BUILD_DIR" "${CMAKE_ARGS[@]}" || ( false ) -if [[ "${GOAL}" != all && "${GOAL}" != codegen ]]; then +if [[ "${GOAL}" != all && "${GOAL}" != *codegen* ]]; then GOAL="all ${GOAL}" fi From bd3a38f10ab7ef44a0f8e8626edf022831aa7e74 Mon Sep 17 00:00:00 2001 From: ViniciusCestarii Date: Tue, 4 Aug 2026 16:46:38 -0300 Subject: [PATCH 028/292] ipc, refactor: return std::optional from checkSpawned() --- src/ipc/interfaces.cpp | 7 ++++--- src/ipc/process.cpp | 26 +++++++++++++------------- src/ipc/process.h | 7 ++++--- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/ipc/interfaces.cpp b/src/ipc/interfaces.cpp index 40cddb4b662d..5747e702f673 100644 --- a/src/ipc/interfaces.cpp +++ b/src/ipc/interfaces.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -74,12 +75,12 @@ class IpcImpl : public interfaces::Ipc bool startSpawnedProcess(int argc, char* argv[], int& exit_status) override { exit_status = EXIT_FAILURE; - mp::SocketId socket{mp::SocketError}; - if (!m_process->checkSpawned(argc, argv, socket)) { + std::optional socket{m_process->checkSpawned(argc, argv)}; + if (!socket) { return false; } IgnoreCtrlC(strprintf("[%s] SIGINT received — waiting for parent to shut down.\n", m_exe_name)); - m_protocol->serve(m_init, [&] { return m_protocol->makeStream(socket); } ); + m_protocol->serve(m_init, [&] { return m_protocol->makeStream(*socket); } ); exit_status = EXIT_SUCCESS; return true; } diff --git a/src/ipc/process.cpp b/src/ipc/process.cpp index a9aa47aedbc9..50cafd8e8b31 100644 --- a/src/ipc/process.cpp +++ b/src/ipc/process.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -42,26 +43,25 @@ class ProcessImpl : public Process }); } int waitSpawned(mp::ProcessId pid) override { return mp::WaitProcess(pid); } - bool checkSpawned(int argc, char* argv[], mp::SocketId& socket) override + std::optional checkSpawned(int argc, char* argv[]) override { - // If this process was not started with a single -ipcfd argument, it is - // not a process spawned by the spawn() call above, so return false and - // do not try to serve requests. + // If this process was not started with a single -ipcfd argument, it + // is not a process spawned by the spawn() call above, so return + // std::nullopt and do not try to serve requests. if (argc != 3 || strcmp(argv[1], "-ipcfd") != 0) { - return false; + return std::nullopt; } - // If a single -ipcfd argument was provided, return true and get the - // file descriptor so Protocol::serve() can be called to handle - // requests from the parent process. The -ipcfd argument is not valid - // in combination with other arguments because the parent process - // should be able to control the child process through the IPC protocol - // without passing information out of band. + // If a single -ipcfd argument was provided, return the socket id so + // Protocol::serve() can be called to handle requests from the parent + // process. The -ipcfd argument is not valid in combination with + // other arguments because the parent process should be able to control + // the child process through the IPC protocol without passing + // information out of band. try { - socket = mp::StartSpawned(argv[2]); + return mp::StartSpawned(argv[2]); } catch (const std::exception& e) { throw std::runtime_error(strprintf("Invalid -ipcfd number '%s' (%s)", argv[2], e.what())); } - return true; } mp::SocketId connect(const fs::path& data_dir, const std::string& dest_exe_name, diff --git a/src/ipc/process.h b/src/ipc/process.h index ac597cb042da..5f791b16d21a 100644 --- a/src/ipc/process.h +++ b/src/ipc/process.h @@ -9,6 +9,7 @@ #include #include +#include #include namespace ipc { @@ -31,9 +32,9 @@ class Process virtual int waitSpawned(mp::ProcessId pid) = 0; //! Parse command line and determine if current process is a spawned child - //! process. If so, return true and a socket id for communicating - //! with the parent process. - virtual bool checkSpawned(int argc, char* argv[], mp::SocketId& socket) = 0; + //! process. If so, return a socket id for communicating with the parent + //! process, otherwise return std::nullopt. + virtual std::optional checkSpawned(int argc, char* argv[]) = 0; //! Canonicalize and connect to address, returning socket id. virtual mp::SocketId connect(const fs::path& data_dir, From 8bca7555f8328851bff0bf50d3bbdaa57aaac4b1 Mon Sep 17 00:00:00 2001 From: ViniciusCestarii Date: Tue, 4 Aug 2026 16:51:56 -0300 Subject: [PATCH 029/292] ipc, refactor: rename arg -ipcfd to -ipcchild --- src/bitcoin.cpp | 2 +- src/ipc/process.cpp | 12 ++++++------ test/lint/check-doc.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/bitcoin.cpp b/src/bitcoin.cpp index 827f6cf101e2..1ad16b8dbd66 100644 --- a/src/bitcoin.cpp +++ b/src/bitcoin.cpp @@ -169,7 +169,7 @@ bool UseMultiprocess(const CommandLine& cmd) // If any -ipc* options are set these need to be processed by a // multiprocess-capable binary. - return args.IsArgSet("-ipcbind") || args.IsArgSet("-ipcconnect") || args.IsArgSet("-ipcfd"); + return args.IsArgSet("-ipcbind") || args.IsArgSet("-ipcconnect") || args.IsArgSet("-ipcchild"); } //! Execute the specified bitcoind, bitcoin-qt or other command line in `args` diff --git a/src/ipc/process.cpp b/src/ipc/process.cpp index 50cafd8e8b31..5f6f5c46bdea 100644 --- a/src/ipc/process.cpp +++ b/src/ipc/process.cpp @@ -39,28 +39,28 @@ class ProcessImpl : public Process fs::path path = argv0_path; path.remove_filename(); path /= fs::PathFromString(new_exe_name); - return std::vector{fs::PathToString(path), "-ipcfd", std::move(connect_info)}; + return std::vector{fs::PathToString(path), "-ipcchild", std::move(connect_info)}; }); } int waitSpawned(mp::ProcessId pid) override { return mp::WaitProcess(pid); } std::optional checkSpawned(int argc, char* argv[]) override { - // If this process was not started with a single -ipcfd argument, it + // If this process was not started with a single -ipcchild argument, it // is not a process spawned by the spawn() call above, so return // std::nullopt and do not try to serve requests. - if (argc != 3 || strcmp(argv[1], "-ipcfd") != 0) { + if (argc != 3 || strcmp(argv[1], "-ipcchild") != 0) { return std::nullopt; } - // If a single -ipcfd argument was provided, return the socket id so + // If a single -ipcchild argument was provided, return the socket id so // Protocol::serve() can be called to handle requests from the parent - // process. The -ipcfd argument is not valid in combination with + // process. The -ipcchild argument is not valid in combination with // other arguments because the parent process should be able to control // the child process through the IPC protocol without passing // information out of band. try { return mp::StartSpawned(argv[2]); } catch (const std::exception& e) { - throw std::runtime_error(strprintf("Invalid -ipcfd number '%s' (%s)", argv[2], e.what())); + throw std::runtime_error(strprintf("Invalid -ipcchild value '%s' (%s)", argv[2], e.what())); } } mp::SocketId connect(const fs::path& data_dir, diff --git a/test/lint/check-doc.py b/test/lint/check-doc.py index 4dc605b1d76f..43f6c3de9572 100755 --- a/test/lint/check-doc.py +++ b/test/lint/check-doc.py @@ -21,7 +21,7 @@ CMD_GREP_WALLET_HIDDEN_ARGS = r"git grep --function-context 'void DummyWalletInit::AddWalletOptions' -- {}".format(CMD_ROOT_DIR) CMD_GREP_DOCS = r"git grep --perl-regexp '{}' {}".format(REGEX_DOC, CMD_ROOT_DIR) # list unsupported, deprecated and duplicate args as they need no documentation -SET_DOC_OPTIONAL = set(['-h', '-?', '-dbcrashratio', '-forcecompactdb', '-ipcconnect', '-ipcfd']) +SET_DOC_OPTIONAL = set(['-h', '-?', '-dbcrashratio', '-forcecompactdb', '-ipcconnect', '-ipcchild']) def lint_missing_argument_documentation(): From d180b891a2904fb787ac5fa1ed66f56c98496698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Thu, 23 Jul 2026 19:06:18 -0700 Subject: [PATCH 030/292] test: add mixed P2SH/witness sigop accounting Add test covering transactions containing both P2SH and witness inputs when enforcing the block sigop limit. --- test/functional/p2p_segwit.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/functional/p2p_segwit.py b/test/functional/p2p_segwit.py index 92b69e49970f..0917e1dc2a72 100755 --- a/test/functional/p2p_segwit.py +++ b/test/functional/p2p_segwit.py @@ -1955,6 +1955,14 @@ def test_witness_sigops(self): self.update_witness_block_with_transactions(block_6, [p2sh_tx]) test_witness_block(self.nodes[0], self.test_node, block_6, accepted=False, reason='bad-blk-sigops') + # Add witness data to verify that the transaction's P2SH sigops are still counted. + p2sh_tx.vin.append(CTxIn(COutPoint(tx.txid_int, outputs - 2), b"")) + p2sh_tx.wit.vtxinwit = [CTxInWitness() for _ in p2sh_tx.vin] + p2sh_tx.wit.vtxinwit[-1].scriptWitness.stack = [witness_script_toomany] + block_7 = self.build_next_block() + self.update_witness_block_with_transactions(block_7, [p2sh_tx]) + test_witness_block(self.nodes[0], self.test_node, block_7, accepted=False, reason='bad-blk-sigops') + # Cleanup and prep for next test self.utxo.pop(0) self.utxo.append(UTXO(tx2.txid_int, 0, tx2.vout[0].nValue)) From 525163c2413ed595a6b9adbc682cc182339c0600 Mon Sep 17 00:00:00 2001 From: Novo Date: Fri, 10 Jul 2026 18:12:16 +0200 Subject: [PATCH 031/292] wallet/tests: pin rescan behavior Add unit tests locking in currently untested rescan behavior, so that the upcoming ChainScanner refactor can be reviewed against them. Co-authored-by: pablomartin4btc --- src/wallet/test/wallet_tests.cpp | 407 +++++++++++++++++++++++++++++++ 1 file changed, 407 insertions(+) diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 9a0531ca7d1b..99cddf439a8c 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -10,8 +10,13 @@ #include #include +#include +#include +#include +#include #include #include +#include #include #include #include @@ -21,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -213,6 +219,106 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) } } +BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_reorged_block, TestChain100Setup) +{ + BOOST_REQUIRE(InitBlockFilterIndex([&]{ return interfaces::MakeChain(m_node); }, BlockFilterType::BASIC, 1_MiB, /*f_memory=*/true)); + BlockFilterIndex& filter_index{*Assert(GetBlockFilterIndex(BlockFilterType::BASIC))}; + BOOST_REQUIRE(filter_index.Init()); + filter_index.Sync(); + + // Reorg the tip out of the active chain: invalidate it, then mine a + // longer replacement branch paying a script unrelated to the wallets + // below. + CBlockIndex* stale_block = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip()); + const uint256 stale_hash{stale_block->GetBlockHash()}; + const int stale_height{stale_block->nHeight}; + BlockValidationState state; + BOOST_REQUIRE(m_node.chainman->ActiveChainstate().InvalidateBlock(state, stale_block)); + const CScript replacement_script{GetScriptForRawPubKey(GenerateRandomKey().GetPubKey())}; + CreateAndProcessBlock({}, replacement_script); + CreateAndProcessBlock({}, replacement_script); + BOOST_REQUIRE(filter_index.BlockUntilSyncedToCurrentChain()); + { + LOCK(Assert(m_node.chainman)->GetMutex()); + BOOST_REQUIRE(!m_node.chainman->ActiveChain().Contains(*stale_block)); + BOOST_REQUIRE_EQUAL(m_node.chainman->ActiveChain().Height(), stale_height + 1); + } + + { + BlockFilter filter; + BOOST_REQUIRE(filter_index.LookupFilter(stale_block, filter)); + } + + // Test wallet whose scripts do not match the stale block's filter. + { + CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); + { + LOCK(wallet.cs_wallet); + LOCK(Assert(m_node.chainman)->GetMutex()); + wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); + } + WalletRescanReserver reserver(wallet); + reserver.reserve(); + CWallet::ScanResult result = wallet.ScanForWalletTransactions(stale_hash, stale_height, /*max_height=*/{}, reserver, /*save_progress=*/false); + BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); + BOOST_CHECK(result.last_failed_block.IsNull()); + BOOST_CHECK_EQUAL(result.last_scanned_block, stale_hash); + BOOST_CHECK_EQUAL(*result.last_scanned_height, stale_height); + } + + // Test wallet whose scripts do match the stale block's filter. + { + CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); + { + LOCK(wallet.cs_wallet); + LOCK(Assert(m_node.chainman)->GetMutex()); + wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); + } + AddKey(wallet, coinbaseKey); // the stale block's coinbase pays coinbaseKey + WalletRescanReserver reserver(wallet); + reserver.reserve(); + CWallet::ScanResult result = wallet.ScanForWalletTransactions(stale_hash, stale_height, /*max_height=*/{}, reserver, /*save_progress=*/false); + BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE); + BOOST_CHECK_EQUAL(result.last_failed_block, stale_hash); + BOOST_CHECK(result.last_scanned_block.IsNull()); + BOOST_CHECK(!result.last_scanned_height); + BOOST_CHECK(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.empty())); + } + + // Prune the stale block's file — the block is now not active AND unreadable. + int file_number; + { + LOCK(cs_main); + file_number = stale_block->GetBlockPos().nFile; + Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number); + } + m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number}); + + { + CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); + { + LOCK(wallet.cs_wallet); + LOCK(Assert(m_node.chainman)->GetMutex()); + wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); + } + AddKey(wallet, coinbaseKey); + WalletRescanReserver reserver(wallet); + reserver.reserve(); + CWallet::ScanResult result = wallet.ScanForWalletTransactions(stale_hash, stale_height, /*max_height=*/{}, reserver, /*save_progress=*/false); + BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE); + BOOST_CHECK_EQUAL(result.last_failed_block, stale_hash); + BOOST_CHECK(result.last_scanned_block.IsNull()); + BOOST_CHECK(!result.last_scanned_height); + BOOST_CHECK(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.empty())); + } + + filter_index.Stop(); + BOOST_REQUIRE(DestroyBlockFilterIndex(BlockFilterType::BASIC)); +} + BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_abort, TestChain100Setup) { CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); @@ -243,6 +349,307 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_abort, TestChain100Setup) BOOST_CHECK(result.last_failed_block.IsNull()); } +BOOST_FIXTURE_TEST_CASE(wallet_rescan_reserver, TestingSetup) +{ + CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); + + // No scan in progress: accessors report idle state. + BOOST_CHECK(!wallet.IsScanning()); + BOOST_CHECK(wallet.ScanningDuration() == SteadyClock::duration{}); + BOOST_CHECK_EQUAL(wallet.ScanningProgress(), 0.0); + + { + WalletRescanReserver first_reserver(wallet); + BOOST_CHECK(first_reserver.reserve()); + BOOST_CHECK(first_reserver.isReserved()); + BOOST_CHECK(wallet.IsScanning()); + BOOST_CHECK(!wallet.IsScanningWithPassphrase()); + BOOST_CHECK_EQUAL(wallet.ScanningProgress(), 0.0); + + // Only one reservation can be held at a time. + WalletRescanReserver second_reserver(wallet); + BOOST_CHECK(!second_reserver.reserve()); + BOOST_CHECK(!second_reserver.isReserved()); + } + // Destroying the reserver (RAII) clears the scanning state. + BOOST_CHECK(!wallet.IsScanning()); + + { + WalletRescanReserver passphrase_reserver(wallet); + BOOST_CHECK(passphrase_reserver.reserve(/*with_passphrase=*/true)); + BOOST_CHECK(wallet.IsScanningWithPassphrase()); + } + BOOST_CHECK(!wallet.IsScanningWithPassphrase()); +} + +BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_bounded, TestChain100Setup) +{ + uint256 genesis_hash, max_hash, tip_hash; + int max_height, tip_height; + { + LOCK(Assert(m_node.chainman)->GetMutex()); + genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash(); + tip_height = m_node.chainman->ActiveChain().Height(); + tip_hash = m_node.chainman->ActiveChain().Tip()->GetBlockHash(); + max_height = tip_height - 2; + max_hash = m_node.chainman->ActiveChain()[max_height]->GetBlockHash(); + } + + // A scan with max_height set stops exactly at max_height and does not + // sync any blocks beyond it. + { + CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); + { + LOCK(wallet.cs_wallet); + wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + wallet.SetLastBlockProcessed(tip_height, tip_hash); + } + AddKey(wallet, coinbaseKey); + WalletRescanReserver reserver(wallet); + reserver.reserve(); + CWallet::ScanResult result = wallet.ScanForWalletTransactions(genesis_hash, /*start_height=*/0, max_height, reserver, /*save_progress=*/false); + BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); + BOOST_CHECK(result.last_failed_block.IsNull()); + BOOST_CHECK_EQUAL(result.last_scanned_block, max_hash); + BOOST_CHECK_EQUAL(*result.last_scanned_height, max_height); + // One coinbase per block from height 1 through max_height. + BOOST_CHECK_EQUAL(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.size()), static_cast(max_height)); + } + + // A single-block range (start == max_height == tip) scans exactly that + // block. + { + CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); + { + LOCK(wallet.cs_wallet); + wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + wallet.SetLastBlockProcessed(tip_height, tip_hash); + } + AddKey(wallet, coinbaseKey); + WalletRescanReserver reserver(wallet); + reserver.reserve(); + CWallet::ScanResult result = wallet.ScanForWalletTransactions(tip_hash, tip_height, tip_height, reserver, /*save_progress=*/false); + BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); + BOOST_CHECK(result.last_failed_block.IsNull()); + BOOST_CHECK_EQUAL(result.last_scanned_block, tip_hash); + BOOST_CHECK_EQUAL(*result.last_scanned_height, tip_height); + BOOST_CHECK_EQUAL(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.size()), 1U); + } +} + +BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_tip_extension, TestChain100Setup) +{ + CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); + uint256 genesis_hash; + int start_tip_height{0}; + { + LOCK(wallet.cs_wallet); + LOCK(Assert(m_node.chainman)->GetMutex()); + wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + start_tip_height = m_node.chainman->ActiveChain().Height(); + wallet.SetLastBlockProcessed(start_tip_height, m_node.chainman->ActiveChain().Tip()->GetBlockHash()); + genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash(); + } + AddKey(wallet, coinbaseKey); + + // Connect a block while the scan is running (the handler fires on the + // scanning thread as the scan starts) and advance the wallet's tip, as + // the blockConnected notification would. The scan must pick up the new + // tip instead of stopping at the height it started with. + uint256 new_tip_hash; + bool extended{false}; + auto handler = wallet.ShowProgress.connect([&](const std::string&, int) { + if (extended) return; + extended = true; + CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); + LOCK(wallet.cs_wallet); + LOCK(Assert(m_node.chainman)->GetMutex()); + const CBlockIndex* new_tip = m_node.chainman->ActiveChain().Tip(); + new_tip_hash = new_tip->GetBlockHash(); + wallet.SetLastBlockProcessed(new_tip->nHeight, new_tip_hash); + }); + + WalletRescanReserver reserver(wallet); + reserver.reserve(); + CWallet::ScanResult result = wallet.ScanForWalletTransactions(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); + handler.disconnect(); + BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); + BOOST_CHECK_EQUAL(result.last_scanned_block, new_tip_hash); + BOOST_CHECK_EQUAL(*result.last_scanned_height, start_tip_height + 1); +} + +BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_no_progress_saved, TestChain100Setup) +{ + CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); + uint256 genesis_hash, tip_hash; + int max_height; + { + LOCK(wallet.cs_wallet); + LOCK(Assert(m_node.chainman)->GetMutex()); + wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + tip_hash = m_node.chainman->ActiveChain().Tip()->GetBlockHash(); + wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), tip_hash); + genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash(); + max_height = m_node.chainman->ActiveChain().Height() - 2; + } + AddKey(wallet, coinbaseKey); + + WalletRescanReserver reserver(wallet); + // Advance the clock on every call so that every scanned block would be + // eligible for a progress write if save_progress were set. + std::chrono::steady_clock::time_point fake_time; + reserver.setNow([&] { fake_time += 60s; return fake_time; }); + reserver.reserve(); + + CWallet::ScanResult result = wallet.ScanForWalletTransactions(genesis_hash, /*start_height=*/0, max_height, reserver, /*save_progress=*/false); + BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); + + // With save_progress=false the scan must not touch the wallet's best + // block record: it still points at the tip written when the descriptor + // was added, not at any block the scan visited. + CBlockLocator locator; + BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator)); + BOOST_CHECK(!locator.IsNull()); + BOOST_CHECK_EQUAL(locator.vHave.front(), tip_hash); +} + +BOOST_FIXTURE_TEST_CASE(rescan_from_time, TestChain100Setup) +{ + // Cap last block file size, and mine new block in a new block file. + CBlockIndex* old_tip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip()); + WITH_LOCK(::cs_main, m_node.chainman->m_blockman.GetBlockFileInfo(old_tip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE); + CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); + CBlockIndex* new_tip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip()); + + // Prune the older block file. + int file_number; + { + LOCK(cs_main); + file_number = old_tip->GetBlockPos().nFile; + Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number); + } + m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number}); + + CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); + { + LOCK(wallet.cs_wallet); + LOCK(Assert(m_node.chainman)->GetMutex()); + wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); + } + AddKey(wallet, coinbaseKey); + WalletRescanReserver reserver(wallet); + reserver.reserve(); + + // Blocks before the prune point cannot be read: the returned timestamp + // is moved past the last unreadable block, telling the caller from when + // the rescan is actually complete. + const int64_t genesis_time{WITH_LOCK(::cs_main, return m_node.chainman->ActiveChain().Genesis()->GetBlockTime())}; + BOOST_CHECK_EQUAL(wallet.RescanFromTime(genesis_time, reserver), + WITH_LOCK(::cs_main, return old_tip->GetBlockTimeMax()) + TIMESTAMP_WINDOW + 1); + + bool scan_logged{false}; + DebugLogHelper scan_check{"Rescan started from block", [&](const std::string* s) { + if (s) scan_logged = true; + return false; + }}; + // A timestamp past the tip requires no scanning and is returned unchanged. + const int64_t future_time{WITH_LOCK(::cs_main, return new_tip->GetBlockTimeMax()) + TIMESTAMP_WINDOW + 1}; + BOOST_CHECK(!scan_logged); + BOOST_CHECK_EQUAL(wallet.RescanFromTime(future_time, reserver), future_time); +} + +BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_missing_filter, TestChain100Setup) +{ + // Enable the block filter index but do not sync it: no filters are + // available, so the scan must inspect every block rather than treat + // the missing filters as misses and skip blocks. + BOOST_REQUIRE(InitBlockFilterIndex([&]{ return interfaces::MakeChain(m_node); }, BlockFilterType::BASIC, 1_MiB, /*f_memory=*/true)); + BlockFilterIndex& filter_index{*Assert(GetBlockFilterIndex(BlockFilterType::BASIC))}; + BOOST_REQUIRE(filter_index.Init()); + + { + CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); + uint256 genesis_hash, tip_hash; + int tip_height; + { + LOCK(wallet.cs_wallet); + LOCK(Assert(m_node.chainman)->GetMutex()); + wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash(); + tip_height = m_node.chainman->ActiveChain().Height(); + auto tip{m_node.chainman->ActiveChain().Tip()}; + tip_hash = tip->GetBlockHash(); + wallet.SetLastBlockProcessed(tip_height, tip_hash); + BlockFilter filter; + BOOST_REQUIRE(!filter_index.LookupFilter(tip, filter)); + } + AddKey(wallet, coinbaseKey); + WalletRescanReserver reserver(wallet); + reserver.reserve(); + bool fast_scan_logged{false}; + DebugLogHelper scan_check{"fast variant using block filters", [&](const std::string* s) { + if (s) fast_scan_logged = true; + return false; + }}; + CWallet::ScanResult result = wallet.ScanForWalletTransactions(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); + BOOST_REQUIRE(fast_scan_logged); + BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); + BOOST_CHECK(result.last_failed_block.IsNull()); + BOOST_CHECK_EQUAL(result.last_scanned_block, tip_hash); + BOOST_CHECK_EQUAL(*result.last_scanned_height, tip_height); + // One coinbase per block from height 1 through the tip. + BOOST_CHECK_EQUAL(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.size()), static_cast(tip_height)); + } + + filter_index.Stop(); + BOOST_REQUIRE(DestroyBlockFilterIndex(BlockFilterType::BASIC)); +} + +//! Test the rescan that loading a wallet performs when the wallet is behind +//! the chain tip: it scans from the wallet's recorded best block - a +//! mid-chain start - with cs_wallet held. +BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_attach_chain, TestChain100Setup) +{ + // Do not wait for sqlite to flush data to disk to improve performance + m_args.ForceSetArg("-unsafesqlitesync", "1"); + + // Create a wallet owning the coinbases, and unload it at the current tip. + WalletContext context; + context.args = &m_args; + context.chain = m_node.chain.get(); + auto wallet = TestCreateWallet(context); + AddKey(*wallet, coinbaseKey); + TestUnloadWallet(std::move(wallet)); + + // Extend the chain while the wallet is not loaded. + constexpr int NEW_BLOCKS{5}; + for (int i = 0; i < NEW_BLOCKS; ++i) { + CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); + } + + int tip_height; + uint256 tip_hash; + { + LOCK(Assert(m_node.chainman)->GetMutex()); + tip_height = m_node.chainman->ActiveChain().Height(); + tip_hash = m_node.chainman->ActiveChain().Tip()->GetBlockHash(); + } + + // Loading the wallet must rescan the extension from the recorded best + // block and find its coinbases. + wallet = TestLoadWallet(context); + { + LOCK(wallet->cs_wallet); + BOOST_CHECK_EQUAL(wallet->GetLastBlockHeight(), tip_height); + BOOST_CHECK_EQUAL(wallet->GetLastBlockHash(), tip_hash); + // The extension's coinbases plus the one of the recorded best block: + // the load rescan starts mid-chain, at that block inclusive. + BOOST_CHECK_EQUAL(wallet->mapWallet.size(), static_cast(NEW_BLOCKS + 1)); + } + TestUnloadWallet(std::move(wallet)); +} + // This test verifies that wallet settings can be added and removed // concurrently, ensuring no race conditions occur during either process. BOOST_FIXTURE_TEST_CASE(write_wallet_settings_concurrently, TestingSetup) From db9185a44464b241dec7a20fdedd7142bc728b1e Mon Sep 17 00:00:00 2001 From: Novo Date: Mon, 3 Nov 2025 05:47:02 +0100 Subject: [PATCH 032/292] wallet: introduce ChainScanner as a CWallet member Move scan state atomics (abort, scanning, passphrase, start time, progress) into ChainScanner and expose it via Scanner(). All callers use Scanner().Scan() directly. The newly added `m_scanner` is an incomplete type so CWallet's constructor and destructor is moved into wallet.cpp where the type is complete. This change introduces a new circular dependency of the form "wallet/scan -> wallet/wallet -> wallet/scan" which is added to `EXPECTED_CIRCULAR_DEPENDENCIES`. --- src/qt/test/wallettests.cpp | 5 +- src/wallet/CMakeLists.txt | 1 + src/wallet/interfaces.cpp | 3 +- src/wallet/rpc/backup.cpp | 3 +- src/wallet/rpc/encrypt.cpp | 7 +- src/wallet/rpc/transactions.cpp | 15 +- src/wallet/rpc/wallet.cpp | 7 +- src/wallet/scan.cpp | 241 ++++++++++++++++++ src/wallet/scan.h | 89 +++++++ src/wallet/test/util.cpp | 5 +- src/wallet/test/wallet_tests.cpp | 85 ++++--- src/wallet/wallet.cpp | 267 +++----------------- src/wallet/wallet.h | 85 +------ test/functional/wallet_importdescriptors.py | 2 +- test/lint/lint-circular-dependencies.py | 1 + 15 files changed, 454 insertions(+), 362 deletions(-) create mode 100644 src/wallet/scan.cpp create mode 100644 src/wallet/scan.h diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp index a2983c23979b..1c1199302a2e 100644 --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -179,8 +180,8 @@ void SyncUpWallet(const std::shared_ptr& wallet, interfaces::Node& node { WalletRescanReserver reserver(*wallet); reserver.reserve(); - CWallet::ScanResult result = wallet->ScanForWalletTransactions(Params().GetConsensus().hashGenesisBlock, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); - QCOMPARE(result.status, CWallet::ScanResult::SUCCESS); + wallet::ScanResult result = wallet->Scanner().Scan(Params().GetConsensus().hashGenesisBlock, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); + QCOMPARE(result.status, wallet::ScanResult::SUCCESS); QCOMPARE(result.last_scanned_block, WITH_LOCK(node.context()->chainman->GetMutex(), return node.context()->chainman->ActiveChain().Tip()->GetBlockHash())); QVERIFY(result.last_failed_block.IsNull()); } diff --git a/src/wallet/CMakeLists.txt b/src/wallet/CMakeLists.txt index 040569f7d653..b6d573bedd3d 100644 --- a/src/wallet/CMakeLists.txt +++ b/src/wallet/CMakeLists.txt @@ -27,6 +27,7 @@ add_library(bitcoin_wallet STATIC EXCLUDE_FROM_ALL rpc/transactions.cpp rpc/util.cpp rpc/wallet.cpp + scan.cpp scriptpubkeyman.cpp spend.cpp sqlite.cpp diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 0237c795e1b3..999ff8172256 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include @@ -149,7 +150,7 @@ class WalletImpl : public Wallet { return m_wallet->ChangeWalletPassphrase(old_wallet_passphrase, new_wallet_passphrase); } - void abortRescan() override { m_wallet->AbortRescan(); } + void abortRescan() override { m_wallet->Scanner().Abort(); } bool backupWallet(const std::string& filename) override { return m_wallet->BackupWallet(filename); } std::string getWalletName() override { return m_wallet->GetName(); } util::Result getNewDestination(const OutputType type, const std::string& label) override diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index 9c58e0fe3f79..889bf91af86f 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -431,7 +432,7 @@ RPCMethod importdescriptors() int64_t scanned_time = pwallet->RescanFromTime(lowest_timestamp, reserver); pwallet->ResubmitWalletTransactions(node::TxBroadcast::MEMPOOL_NO_BROADCAST, /*force=*/true); - if (pwallet->IsAbortingRescan()) { + if (pwallet->Scanner().IsAborting()) { throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted by user."); } diff --git a/src/wallet/rpc/encrypt.cpp b/src/wallet/rpc/encrypt.cpp index 49490d57b945..d78ce53d320b 100644 --- a/src/wallet/rpc/encrypt.cpp +++ b/src/wallet/rpc/encrypt.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -138,7 +139,7 @@ RPCMethod walletpassphrasechange() throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: running with an unencrypted wallet, but walletpassphrasechange was called."); } - if (pwallet->IsScanningWithPassphrase()) { + if (pwallet->Scanner().IsScanningWithPassphrase()) { throw JSONRPCError(RPC_WALLET_ERROR, "Error: the wallet is currently being used to rescan the blockchain for related transactions. Please call `abortrescan` before changing the passphrase."); } @@ -203,7 +204,7 @@ RPCMethod walletlock() throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: running with an unencrypted wallet, but walletlock was called."); } - if (pwallet->IsScanningWithPassphrase()) { + if (pwallet->Scanner().IsScanningWithPassphrase()) { throw JSONRPCError(RPC_WALLET_ERROR, "Error: the wallet is currently being used to rescan the blockchain for related transactions. Please call `abortrescan` before locking the wallet."); } @@ -260,7 +261,7 @@ RPCMethod encryptwallet() throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: running with an encrypted wallet, but encryptwallet was called."); } - if (pwallet->IsScanningWithPassphrase()) { + if (pwallet->Scanner().IsScanningWithPassphrase()) { throw JSONRPCError(RPC_WALLET_ERROR, "Error: the wallet is currently being used to rescan the blockchain for related transactions. Please call `abortrescan` before encrypting the wallet."); } diff --git a/src/wallet/rpc/transactions.cpp b/src/wallet/rpc/transactions.cpp index 77af932ea680..0cce5b7956b1 100644 --- a/src/wallet/rpc/transactions.cpp +++ b/src/wallet/rpc/transactions.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include using interfaces::FoundBlock; @@ -908,14 +909,14 @@ RPCMethod rescanblockchain() CHECK_NONFATAL(pwallet->chain().findAncestorByHeight(pwallet->GetLastBlockHash(), start_height, FoundBlock().hash(start_block))); } - CWallet::ScanResult result = - pwallet->ScanForWalletTransactions(start_block, start_height, stop_height, reserver, /*save_progress=*/false); + ScanResult result = + pwallet->Scanner().Scan(start_block, start_height, stop_height, reserver, /*save_progress=*/false); switch (result.status) { - case CWallet::ScanResult::SUCCESS: + case ScanResult::SUCCESS: break; - case CWallet::ScanResult::FAILURE: + case ScanResult::FAILURE: throw JSONRPCError(RPC_MISC_ERROR, "Rescan failed. Potentially corrupted data files."); - case CWallet::ScanResult::USER_ABORT: + case ScanResult::USER_ABORT: throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted."); } // no default case, so the compiler can warn about missing cases UniValue response(UniValue::VOBJ); @@ -946,8 +947,8 @@ RPCMethod abortrescan() std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); if (!pwallet) return UniValue::VNULL; - if (!pwallet->IsScanning() || pwallet->IsAbortingRescan()) return false; - pwallet->AbortRescan(); + if (!pwallet->Scanner().IsScanning() || pwallet->Scanner().IsAborting()) return false; + pwallet->Scanner().Abort(); return true; }, }; diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp index ed3c8bfd8e4a..4585369d595f 100644 --- a/src/wallet/rpc/wallet.cpp +++ b/src/wallet/rpc/wallet.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -100,10 +101,10 @@ static RPCMethod getwalletinfo() } obj.pushKV("private_keys_enabled", !pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)); obj.pushKV("avoid_reuse", pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE)); - if (pwallet->IsScanning()) { + if (pwallet->Scanner().IsScanning()) { UniValue scanning(UniValue::VOBJ); - scanning.pushKV("duration", Ticks(pwallet->ScanningDuration())); - scanning.pushKV("progress", pwallet->ScanningProgress()); + scanning.pushKV("duration", Ticks(pwallet->Scanner().ScanningDuration())); + scanning.pushKV("progress", pwallet->Scanner().ScanningProgress()); obj.pushKV("scanning", std::move(scanning)); } else { obj.pushKV("scanning", false); diff --git a/src/wallet/scan.cpp b/src/wallet/scan.cpp new file mode 100644 index 000000000000..2259ba4bfffb --- /dev/null +++ b/src/wallet/scan.cpp @@ -0,0 +1,241 @@ +// Copyright (c) 2026-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include +#include +#include + +using interfaces::FoundBlock; + +namespace wallet { + +bool ChainScanner::TryReserve(bool with_passphrase) { + if (m_scanning.exchange(true)) return false; + // Discard any abort request left over from previous reservation, so + // that an abort requested while the reservation is held always applies + // to abort this rescan, even if it arrives before the scan loop starts. + m_abort = false; + m_scanning_with_passphrase = with_passphrase; + m_scanning_start = SteadyClock::now(); + m_scanning_progress = 0; + return true; +} + +void ChainScanner::Release() { + m_scanning = false; + m_scanning_with_passphrase = false; +} + +namespace { +class FastWalletRescanFilter +{ +public: + FastWalletRescanFilter(const CWallet& wallet) : m_wallet(wallet) + { + // create initial filter with scripts from all ScriptPubKeyMans + for (auto spkm : m_wallet.GetAllScriptPubKeyMans()) { + auto desc_spkm{dynamic_cast(spkm)}; + assert(desc_spkm != nullptr); + AddScriptPubKeys(desc_spkm); + // save each range descriptor's end for possible future filter updates + if (desc_spkm->IsHDEnabled()) { + m_last_range_ends.emplace(desc_spkm->GetID(), desc_spkm->GetEndRange()); + } + } + } + + void UpdateIfNeeded() + { + // repopulate filter with new scripts if top-up has happened since last iteration + for (const auto& [desc_spkm_id, last_range_end] : m_last_range_ends) { + auto desc_spkm{dynamic_cast(m_wallet.GetScriptPubKeyMan(desc_spkm_id))}; + assert(desc_spkm != nullptr); + int32_t current_range_end{desc_spkm->GetEndRange()}; + if (current_range_end > last_range_end) { + AddScriptPubKeys(desc_spkm, last_range_end); + m_last_range_ends.at(desc_spkm->GetID()) = current_range_end; + } + } + } + + std::optional MatchesBlock(const uint256& block_hash) const + { + return m_wallet.chain().blockFilterMatchesAny(BlockFilterType::BASIC, block_hash, m_filter_set); + } + +private: + const CWallet& m_wallet; + /** Map for keeping track of each range descriptor's last seen end range. + * This information is used to detect whether new addresses were derived + * (that is, if the current end range is larger than the saved end range) + * after processing a block and hence a filter set update is needed to + * take possible keypool top-ups into account. + */ + std::map m_last_range_ends; + GCSFilter::ElementSet m_filter_set; + + void AddScriptPubKeys(const DescriptorScriptPubKeyMan* desc_spkm, int32_t last_range_end = 0) + { + for (const auto& script_pub_key : desc_spkm->GetScriptPubKeys(last_range_end)) { + m_filter_set.emplace(script_pub_key.begin(), script_pub_key.end()); + } + } +}; +} // namespace + +ScanResult ChainScanner::Scan(const uint256& start_block, int start_height, std::optional max_height, + const WalletRescanReserver& reserver, bool save_progress) { + constexpr auto INTERVAL_TIME{60s}; + auto current_time{reserver.now()}; + auto start_time{reserver.now()}; + + assert(reserver.isReserved()); + auto& chain = m_wallet.chain(); + + std::unique_ptr fast_rescan_filter; + if (chain.hasBlockFilterIndex(BlockFilterType::BASIC)) fast_rescan_filter = std::make_unique(m_wallet); + + m_wallet.WalletLogPrintf("Rescan started from block %s... (%s)\n", start_block.ToString(), + fast_rescan_filter ? "fast variant using block filters" : "slow variant inspecting all blocks"); + + // show rescan progress in GUI as dialog or on splashscreen, if rescan required on startup (e.g. due to corruption) + m_wallet.ShowProgress(strprintf("[%s] %s", m_wallet.DisplayName(), _("Rescanning…")), 0); + uint256 tip_hash = WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHash()); + uint256 end_hash = tip_hash; + if (max_height) chain.findAncestorByHeight(tip_hash, *max_height, FoundBlock().hash(end_hash)); + + ScanResult result; + uint256 block_hash = start_block; + double progress_begin = chain.guessVerificationProgress(block_hash); + double progress_end = chain.guessVerificationProgress(end_hash); + double progress_current = progress_begin; + int block_height = start_height; + while (!m_abort && !chain.shutdownRequested()) { + if (progress_end - progress_begin > 0.0) { + m_scanning_progress = (progress_current - progress_begin) / (progress_end - progress_begin); + } else { // avoid divide-by-zero for single block scan range (i.e. start and stop hashes are equal) + m_scanning_progress = 0; + } + if (block_height % 100 == 0 && progress_end - progress_begin > 0.0) { + m_wallet.ShowProgress(strprintf("[%s] %s", m_wallet.DisplayName(), _("Rescanning…")), std::max(1, std::min(99, (int)(m_scanning_progress.load() * 100)))); + } + + bool next_interval = reserver.now() >= current_time + INTERVAL_TIME; + if (next_interval) { + current_time = reserver.now(); + m_wallet.WalletLogPrintf("Still rescanning. At block %d. Progress=%f\n", block_height, progress_current); + } + + bool fetch_block{true}; + if (fast_rescan_filter) { + fast_rescan_filter->UpdateIfNeeded(); + auto matches_block{fast_rescan_filter->MatchesBlock(block_hash)}; + if (matches_block.has_value()) { + if (*matches_block) { + LogDebug(BCLog::SCAN, "Fast rescan: inspect block %d [%s] (filter matched)\n", block_height, block_hash.ToString()); + } else { + result.last_scanned_block = block_hash; + result.last_scanned_height = block_height; + fetch_block = false; + } + } else { + LogDebug(BCLog::SCAN, "Fast rescan: inspect block %d [%s] (WARNING: block filter not found!)\n", block_height, block_hash.ToString()); + } + } + + // Find next block separately from reading data above, because reading + // is slow and there might be a reorg while it is read. + bool block_still_active = false; + bool next_block = false; + uint256 next_block_hash; + chain.findBlock(block_hash, FoundBlock().inActiveChain(block_still_active).nextBlock(FoundBlock().inActiveChain(next_block).hash(next_block_hash))); + + if (fetch_block) { + // Read block data and locator if needed (the locator is usually null unless we need to save progress) + CBlock block; + CBlockLocator loc; + // Find block + FoundBlock found_block{FoundBlock().data(block)}; + if (save_progress && next_interval) found_block.locator(loc); + chain.findBlock(block_hash, found_block); + + if (!block.IsNull()) { + LOCK(m_wallet.cs_wallet); + if (!block_still_active) { + // Abort scan if current block is no longer active, to prevent + // marking transactions as coming from the wrong block. + result.last_failed_block = block_hash; + result.status = ScanResult::FAILURE; + break; + } + for (size_t posInBlock = 0; posInBlock < block.vtx.size(); ++posInBlock) { + m_wallet.SyncTransaction(block.vtx[posInBlock], TxStateConfirmed{block_hash, block_height, static_cast(posInBlock)}, /*rescanning_old_block=*/true); + } + // scan succeeded, record block as most recent successfully scanned + result.last_scanned_block = block_hash; + result.last_scanned_height = block_height; + + if (!loc.IsNull()) { + m_wallet.WalletLogPrintf("Saving scan progress %d.\n", block_height); + WalletBatch batch(m_wallet.GetDatabase()); + batch.WriteBestBlock(loc); + } + } else { + // could not scan block, keep scanning but record this block as the most recent failure + result.last_failed_block = block_hash; + result.status = ScanResult::FAILURE; + } + } + if (max_height && block_height >= *max_height) { + break; + } + // If rescanning was triggered with cs_wallet permanently locked (AttachChain), additional blocks that were connected during the rescan + // aren't processed here but will be processed with the pending blockConnected notifications after the lock is released. + // If rescanning without a permanent cs_wallet lock, additional blocks that were added during the rescan will be re-processed if + // the notification was processed and the last block height was updated. + if (block_height >= WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHeight())) { + break; + } + + { + if (!next_block) { + // break successfully when rescan has reached the tip, or + // previous block is no longer on the chain due to a reorg + break; + } + + // increment block and verification progress + block_hash = next_block_hash; + ++block_height; + progress_current = chain.guessVerificationProgress(block_hash); + + // handle updated tip hash + const uint256 prev_tip_hash = tip_hash; + tip_hash = WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHash()); + if (!max_height && prev_tip_hash != tip_hash) { + // in case the tip has changed, update progress max + progress_end = chain.guessVerificationProgress(tip_hash); + } + } + } + if (!max_height) { + m_wallet.WalletLogPrintf("Scanning current mempool transactions.\n"); + WITH_LOCK(m_wallet.cs_wallet, chain.requestMempoolTransactions(m_wallet)); + } + m_wallet.ShowProgress(strprintf("[%s] %s", m_wallet.DisplayName(), _("Rescanning…")), 100); // hide progress dialog in GUI + if (m_abort) { + m_wallet.WalletLogPrintf("Rescan aborted at block %d. Progress=%f\n", block_height, progress_current); + result.status = ScanResult::USER_ABORT; + } else if (chain.shutdownRequested()) { + m_wallet.WalletLogPrintf("Rescan interrupted by shutdown request at block %d. Progress=%f\n", block_height, progress_current); + result.status = ScanResult::USER_ABORT; + } else { + m_wallet.WalletLogPrintf("Rescan completed in %15dms\n", Ticks(reserver.now() - start_time)); + } + return result; +} +} diff --git a/src/wallet/scan.h b/src/wallet/scan.h new file mode 100644 index 000000000000..a6f7be491cb9 --- /dev/null +++ b/src/wallet/scan.h @@ -0,0 +1,89 @@ +// Copyright (c) 2026-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_WALLET_SCAN_H +#define BITCOIN_WALLET_SCAN_H + +#include +#include + +#include +#include + +namespace wallet { +class CWallet; +class WalletRescanReserver; + +/** Result of a wallet scan */ +struct ScanResult { + enum { SUCCESS, FAILURE, USER_ABORT } status = SUCCESS; + + //! Hash and height of most recent block that was successfully scanned. + //! Unset if no blocks were scanned due to read errors or the chain + //! being empty. + uint256 last_scanned_block; + std::optional last_scanned_height; + + //! Height of the most recent block that could not be scanned due to + //! read errors or pruning. Will be set if status is FAILURE, unset if + //! status is SUCCESS, and may or may not be set if status is + //! USER_ABORT. + uint256 last_failed_block; +}; + +class ChainScanner { +private: + CWallet& m_wallet; + + std::atomic m_abort{false}; + std::atomic m_scanning{false}; + std::atomic m_scanning_with_passphrase{false}; + std::atomic m_scanning_start{SteadyClock::time_point{}}; + std::atomic m_scanning_progress{0}; + + //! Only WalletRescanReserver may reserve and release scans, so that + //! reservations are always managed RAII-style. + friend class WalletRescanReserver; + bool TryReserve(bool with_passphrase = false); + void Release(); + +public: + explicit ChainScanner(CWallet& wallet) : m_wallet(wallet) {} + + void Abort() { m_abort = true; } + bool IsAborting() const { return m_abort; } + bool IsScanning() const { return m_scanning; } + bool IsScanningWithPassphrase() const { return m_scanning_with_passphrase; } + SteadyClock::duration ScanningDuration() const { return m_scanning ? SteadyClock::now() - m_scanning_start.load() : SteadyClock::duration{}; } + double ScanningProgress() const { return m_scanning ? m_scanning_progress.load() : 0; } + + /** + * Scan the block chain (starting in start_block) for transactions + * from or to us. If max_height is not set, the + * mempool will be scanned as well. + * + * @param[in] start_block Scan starting block. If block is not on the active + * chain, the scan will return SUCCESS immediately. + * @param[in] start_height Height of start_block + * @param[in] max_height Optional max scanning height. If unset there is + * no maximum and scanning can continue to the tip + * + * @return ScanResult returning scan information and indicating success or + * failure. Return status will be set to SUCCESS if scan was + * successful. FAILURE if a complete rescan was not possible (due to + * pruning or corruption). USER_ABORT if the rescan was aborted before + * it could complete. + * + * @pre Caller needs to make sure start_block (and the optional stop_block) are on + * the main chain after the addition of any new keys you want to detect + * transactions for. + */ + ScanResult Scan(const uint256& start_block, int start_height, std::optional max_height, + const WalletRescanReserver& reserver, bool save_progress); + +}; + +} // namespace wallet + +#endif // BITCOIN_WALLET_SCAN_H diff --git a/src/wallet/test/util.cpp b/src/wallet/test/util.cpp index 43cfd40fabdc..3a4e4556e841 100644 --- a/src/wallet/test/util.cpp +++ b/src/wallet/test/util.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -40,8 +41,8 @@ std::unique_ptr CreateSyncedWallet(interfaces::Chain& chain, CChain& cc } WalletRescanReserver reserver(*wallet); reserver.reserve(); - CWallet::ScanResult result = wallet->ScanForWalletTransactions(cchain.Genesis()->GetBlockHash(), /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); - assert(result.status == CWallet::ScanResult::SUCCESS); + ScanResult result = wallet->Scanner().Scan(cchain.Genesis()->GetBlockHash(), /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); + assert(result.status == ScanResult::SUCCESS); assert(result.last_scanned_block == cchain.Tip()->GetBlockHash()); assert(*result.last_scanned_height == cchain.Height()); assert(result.last_failed_block.IsNull()); diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 99cddf439a8c..cde9ab9331bc 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -2,6 +2,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include #include #include @@ -104,7 +105,7 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); CBlockIndex* newTip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip()); - // Verify ScanForWalletTransactions fails to read an unknown start block. + // Verify Scan fails to read an unknown start block. { CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); { @@ -116,15 +117,15 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) AddKey(wallet, coinbaseKey); WalletRescanReserver reserver(wallet); reserver.reserve(); - CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/{}, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); - BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE); + ScanResult result = wallet.Scanner().Scan(/*start_block=*/{}, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); + BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE); BOOST_CHECK(result.last_failed_block.IsNull()); BOOST_CHECK(result.last_scanned_block.IsNull()); BOOST_CHECK(!result.last_scanned_height); BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 0); } - // Verify ScanForWalletTransactions picks up transactions in both the old + // Verify Scan picks up transactions in both the old // and new block files. { CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); @@ -146,8 +147,8 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) BOOST_CHECK(!locator.IsNull() && locator.vHave.front() == newTip->GetBlockHash()); } - CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/true); - BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); + ScanResult result = wallet.Scanner().Scan(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/true); + BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS); BOOST_CHECK(result.last_failed_block.IsNull()); BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash()); BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight); @@ -169,7 +170,7 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) } m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number}); - // Verify ScanForWalletTransactions only picks transactions in the new block + // Verify Scan only picks transactions in the new block // file. { CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); @@ -182,8 +183,8 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) AddKey(wallet, coinbaseKey); WalletRescanReserver reserver(wallet); reserver.reserve(); - CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/false); - BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE); + ScanResult result = wallet.Scanner().Scan(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/false); + BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE); BOOST_CHECK_EQUAL(result.last_failed_block, oldTip->GetBlockHash()); BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash()); BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight); @@ -198,7 +199,7 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) } m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number}); - // Verify ScanForWalletTransactions scans no blocks. + // Verify Scan scans no blocks. { CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); { @@ -210,8 +211,8 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) AddKey(wallet, coinbaseKey); WalletRescanReserver reserver(wallet); reserver.reserve(); - CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/false); - BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE); + ScanResult result = wallet.Scanner().Scan(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/false); + BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE); BOOST_CHECK_EQUAL(result.last_failed_block, newTip->GetBlockHash()); BOOST_CHECK(result.last_scanned_block.IsNull()); BOOST_CHECK(!result.last_scanned_height); @@ -260,8 +261,8 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_reorged_block, TestChain100 } WalletRescanReserver reserver(wallet); reserver.reserve(); - CWallet::ScanResult result = wallet.ScanForWalletTransactions(stale_hash, stale_height, /*max_height=*/{}, reserver, /*save_progress=*/false); - BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); + ScanResult result = wallet.Scanner().Scan(stale_hash, stale_height, /*max_height=*/{}, reserver, /*save_progress=*/false); + BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS); BOOST_CHECK(result.last_failed_block.IsNull()); BOOST_CHECK_EQUAL(result.last_scanned_block, stale_hash); BOOST_CHECK_EQUAL(*result.last_scanned_height, stale_height); @@ -279,8 +280,8 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_reorged_block, TestChain100 AddKey(wallet, coinbaseKey); // the stale block's coinbase pays coinbaseKey WalletRescanReserver reserver(wallet); reserver.reserve(); - CWallet::ScanResult result = wallet.ScanForWalletTransactions(stale_hash, stale_height, /*max_height=*/{}, reserver, /*save_progress=*/false); - BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE); + ScanResult result = wallet.Scanner().Scan(stale_hash, stale_height, /*max_height=*/{}, reserver, /*save_progress=*/false); + BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE); BOOST_CHECK_EQUAL(result.last_failed_block, stale_hash); BOOST_CHECK(result.last_scanned_block.IsNull()); BOOST_CHECK(!result.last_scanned_height); @@ -307,8 +308,8 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_reorged_block, TestChain100 AddKey(wallet, coinbaseKey); WalletRescanReserver reserver(wallet); reserver.reserve(); - CWallet::ScanResult result = wallet.ScanForWalletTransactions(stale_hash, stale_height, /*max_height=*/{}, reserver, /*save_progress=*/false); - BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE); + ScanResult result = wallet.Scanner().Scan(stale_hash, stale_height, /*max_height=*/{}, reserver, /*save_progress=*/false); + BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE); BOOST_CHECK_EQUAL(result.last_failed_block, stale_hash); BOOST_CHECK(result.last_scanned_block.IsNull()); BOOST_CHECK(!result.last_scanned_height); @@ -333,17 +334,17 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_abort, TestChain100Setup) // An abort requested while no rescan is held is stale and must // not cancel a later scan. - wallet.AbortRescan(); + wallet.Scanner().Abort(); WalletRescanReserver reserver(wallet); BOOST_CHECK(reserver.reserve()); - BOOST_CHECK(!wallet.IsAbortingRescan()); + BOOST_CHECK(!wallet.Scanner().IsAborting()); // An abort requested after the reservation but before the scan starts // (e.g. while importdescriptors is still deriving keys) must cancel the // scan. - wallet.AbortRescan(); - CWallet::ScanResult result = wallet.ScanForWalletTransactions(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); - BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::USER_ABORT); + wallet.Scanner().Abort(); + ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); + BOOST_CHECK_EQUAL(result.status, ScanResult::USER_ABORT); BOOST_CHECK(result.last_scanned_block.IsNull()); BOOST_CHECK(!result.last_scanned_height); BOOST_CHECK(result.last_failed_block.IsNull()); @@ -354,17 +355,17 @@ BOOST_FIXTURE_TEST_CASE(wallet_rescan_reserver, TestingSetup) CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); // No scan in progress: accessors report idle state. - BOOST_CHECK(!wallet.IsScanning()); - BOOST_CHECK(wallet.ScanningDuration() == SteadyClock::duration{}); - BOOST_CHECK_EQUAL(wallet.ScanningProgress(), 0.0); + BOOST_CHECK(!wallet.Scanner().IsScanning()); + BOOST_CHECK(wallet.Scanner().ScanningDuration() == SteadyClock::duration{}); + BOOST_CHECK_EQUAL(wallet.Scanner().ScanningProgress(), 0.0); { WalletRescanReserver first_reserver(wallet); BOOST_CHECK(first_reserver.reserve()); BOOST_CHECK(first_reserver.isReserved()); - BOOST_CHECK(wallet.IsScanning()); - BOOST_CHECK(!wallet.IsScanningWithPassphrase()); - BOOST_CHECK_EQUAL(wallet.ScanningProgress(), 0.0); + BOOST_CHECK(wallet.Scanner().IsScanning()); + BOOST_CHECK(!wallet.Scanner().IsScanningWithPassphrase()); + BOOST_CHECK_EQUAL(wallet.Scanner().ScanningProgress(), 0.0); // Only one reservation can be held at a time. WalletRescanReserver second_reserver(wallet); @@ -372,14 +373,14 @@ BOOST_FIXTURE_TEST_CASE(wallet_rescan_reserver, TestingSetup) BOOST_CHECK(!second_reserver.isReserved()); } // Destroying the reserver (RAII) clears the scanning state. - BOOST_CHECK(!wallet.IsScanning()); + BOOST_CHECK(!wallet.Scanner().IsScanning()); { WalletRescanReserver passphrase_reserver(wallet); BOOST_CHECK(passphrase_reserver.reserve(/*with_passphrase=*/true)); - BOOST_CHECK(wallet.IsScanningWithPassphrase()); + BOOST_CHECK(wallet.Scanner().IsScanningWithPassphrase()); } - BOOST_CHECK(!wallet.IsScanningWithPassphrase()); + BOOST_CHECK(!wallet.Scanner().IsScanningWithPassphrase()); } BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_bounded, TestChain100Setup) @@ -407,8 +408,8 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_bounded, TestChain100Setup) AddKey(wallet, coinbaseKey); WalletRescanReserver reserver(wallet); reserver.reserve(); - CWallet::ScanResult result = wallet.ScanForWalletTransactions(genesis_hash, /*start_height=*/0, max_height, reserver, /*save_progress=*/false); - BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); + ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, max_height, reserver, /*save_progress=*/false); + BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS); BOOST_CHECK(result.last_failed_block.IsNull()); BOOST_CHECK_EQUAL(result.last_scanned_block, max_hash); BOOST_CHECK_EQUAL(*result.last_scanned_height, max_height); @@ -428,8 +429,8 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_bounded, TestChain100Setup) AddKey(wallet, coinbaseKey); WalletRescanReserver reserver(wallet); reserver.reserve(); - CWallet::ScanResult result = wallet.ScanForWalletTransactions(tip_hash, tip_height, tip_height, reserver, /*save_progress=*/false); - BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); + ScanResult result = wallet.Scanner().Scan(tip_hash, tip_height, tip_height, reserver, /*save_progress=*/false); + BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS); BOOST_CHECK(result.last_failed_block.IsNull()); BOOST_CHECK_EQUAL(result.last_scanned_block, tip_hash); BOOST_CHECK_EQUAL(*result.last_scanned_height, tip_height); @@ -471,9 +472,9 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_tip_extension, TestChain100 WalletRescanReserver reserver(wallet); reserver.reserve(); - CWallet::ScanResult result = wallet.ScanForWalletTransactions(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); + ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); handler.disconnect(); - BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); + BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS); BOOST_CHECK_EQUAL(result.last_scanned_block, new_tip_hash); BOOST_CHECK_EQUAL(*result.last_scanned_height, start_tip_height + 1); } @@ -501,8 +502,8 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_no_progress_saved, TestChai reserver.setNow([&] { fake_time += 60s; return fake_time; }); reserver.reserve(); - CWallet::ScanResult result = wallet.ScanForWalletTransactions(genesis_hash, /*start_height=*/0, max_height, reserver, /*save_progress=*/false); - BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); + ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, max_height, reserver, /*save_progress=*/false); + BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS); // With save_progress=false the scan must not touch the wallet's best // block record: it still points at the tip written when the descriptor @@ -592,9 +593,9 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_missing_filter, TestChain10 if (s) fast_scan_logged = true; return false; }}; - CWallet::ScanResult result = wallet.ScanForWalletTransactions(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); + ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); BOOST_REQUIRE(fast_scan_logged); - BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); + BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS); BOOST_CHECK(result.last_failed_block.IsNull()); BOOST_CHECK_EQUAL(result.last_scanned_block, tip_hash); BOOST_CHECK_EQUAL(*result.last_scanned_height, tip_height); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index be618efbd6d7..862e31835abf 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -66,6 +66,7 @@ #include #include #include +#include #include #include #include @@ -312,61 +313,6 @@ std::shared_ptr LoadWalletInternal(WalletContext& context, const std::s return nullptr; } } - -class FastWalletRescanFilter -{ -public: - FastWalletRescanFilter(const CWallet& wallet) : m_wallet(wallet) - { - // create initial filter with scripts from all ScriptPubKeyMans - for (auto spkm : m_wallet.GetAllScriptPubKeyMans()) { - auto desc_spkm{dynamic_cast(spkm)}; - assert(desc_spkm != nullptr); - AddScriptPubKeys(desc_spkm); - // save each range descriptor's end for possible future filter updates - if (desc_spkm->IsHDEnabled()) { - m_last_range_ends.emplace(desc_spkm->GetID(), desc_spkm->GetEndRange()); - } - } - } - - void UpdateIfNeeded() - { - // repopulate filter with new scripts if top-up has happened since last iteration - for (const auto& [desc_spkm_id, last_range_end] : m_last_range_ends) { - auto desc_spkm{dynamic_cast(m_wallet.GetScriptPubKeyMan(desc_spkm_id))}; - assert(desc_spkm != nullptr); - int32_t current_range_end{desc_spkm->GetEndRange()}; - if (current_range_end > last_range_end) { - AddScriptPubKeys(desc_spkm, last_range_end); - m_last_range_ends.at(desc_spkm->GetID()) = current_range_end; - } - } - } - - std::optional MatchesBlock(const uint256& block_hash) const - { - return m_wallet.chain().blockFilterMatchesAny(BlockFilterType::BASIC, block_hash, m_filter_set); - } - -private: - const CWallet& m_wallet; - /** Map for keeping track of each range descriptor's last seen end range. - * This information is used to detect whether new addresses were derived - * (that is, if the current end range is larger than the saved end range) - * after processing a block and hence a filter set update is needed to - * take possible keypool top-ups into account. - */ - std::map m_last_range_ends; - GCSFilter::ElementSet m_filter_set; - - void AddScriptPubKeys(const DescriptorScriptPubKeyMan* desc_spkm, int32_t last_range_end = 0) - { - for (const auto& script_pub_key : desc_spkm->GetScriptPubKeys(last_range_end)) { - m_filter_set.emplace(script_pub_key.begin(), script_pub_key.end()); - } - } -}; } // namespace std::shared_ptr LoadWallet(WalletContext& context, const std::string& name, std::optional load_on_start, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector& warnings) @@ -533,6 +479,42 @@ std::shared_ptr RestoreWallet(WalletContext& context, const fs::path& b return wallet; } +CWallet::CWallet(interfaces::Chain* chain, const std::string& name, std::unique_ptr database) + : m_chain(chain), + m_name(name), + m_database(std::move(database)), + m_scanner(std::make_unique(*this)) +{ +} + +CWallet::~CWallet() +{ + // Should not have slots connected at this point. + assert(NotifyUnload.empty()); +} + +ChainScanner& CWallet::Scanner() { return *m_scanner; } +const ChainScanner& CWallet::Scanner() const { return *m_scanner; } + +bool WalletRescanReserver::reserve(bool with_passphrase) { + assert(!m_could_reserve); + if (!m_wallet.Scanner().TryReserve(with_passphrase)) { + return false; + } + m_could_reserve = true; + return true; +} + +bool WalletRescanReserver::isReserved() const { + return (m_could_reserve && m_wallet.Scanner().IsScanning()); +} + +WalletRescanReserver::~WalletRescanReserver() { + if (m_could_reserve) { + m_wallet.Scanner().Release(); + } +} + /** @defgroup mapWallet * * @{ @@ -1843,7 +1825,7 @@ int64_t CWallet::RescanFromTime(int64_t startTime, const WalletRescanReserver& r if (start) { // TODO: this should take into account failure by ScanResult::USER_ABORT - ScanResult result = ScanForWalletTransactions(start_block, start_height, /*max_height=*/{}, reserver, /*save_progress=*/false); + ScanResult result = Scanner().Scan(start_block, start_height, /*max_height=*/{}, reserver, /*save_progress=*/false); if (result.status == ScanResult::FAILURE) { int64_t time_max; CHECK_NONFATAL(chain().findBlock(result.last_failed_block, FoundBlock().maxTime(time_max))); @@ -1853,177 +1835,6 @@ int64_t CWallet::RescanFromTime(int64_t startTime, const WalletRescanReserver& r return startTime; } -/** - * Scan the block chain (starting in start_block) for transactions - * from or to us. If max_height is not set, the - * mempool will be scanned as well. - * - * @param[in] start_block Scan starting block. If block is not on the active - * chain, the scan will return SUCCESS immediately. - * @param[in] start_height Height of start_block - * @param[in] max_height Optional max scanning height. If unset there is - * no maximum and scanning can continue to the tip - * - * @return ScanResult returning scan information and indicating success or - * failure. Return status will be set to SUCCESS if scan was - * successful. FAILURE if a complete rescan was not possible (due to - * pruning or corruption). USER_ABORT if the rescan was aborted before - * it could complete. - * - * @pre Caller needs to make sure start_block (and the optional stop_block) are on - * the main chain after to the addition of any new keys you want to detect - * transactions for. - */ -CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_block, int start_height, std::optional max_height, const WalletRescanReserver& reserver, const bool save_progress) -{ - constexpr auto INTERVAL_TIME{60s}; - auto current_time{reserver.now()}; - auto start_time{reserver.now()}; - - assert(reserver.isReserved()); - - uint256 block_hash = start_block; - ScanResult result; - - std::unique_ptr fast_rescan_filter; - if (chain().hasBlockFilterIndex(BlockFilterType::BASIC)) fast_rescan_filter = std::make_unique(*this); - - WalletLogPrintf("Rescan started from block %s... (%s)\n", start_block.ToString(), - fast_rescan_filter ? "fast variant using block filters" : "slow variant inspecting all blocks"); - - ShowProgress(strprintf("[%s] %s", DisplayName(), _("Rescanning…")), 0); // show rescan progress in GUI as dialog or on splashscreen, if rescan required on startup (e.g. due to corruption) - uint256 tip_hash = WITH_LOCK(cs_wallet, return GetLastBlockHash()); - uint256 end_hash = tip_hash; - if (max_height) chain().findAncestorByHeight(tip_hash, *max_height, FoundBlock().hash(end_hash)); - double progress_begin = chain().guessVerificationProgress(block_hash); - double progress_end = chain().guessVerificationProgress(end_hash); - double progress_current = progress_begin; - int block_height = start_height; - while (!fAbortRescan && !chain().shutdownRequested()) { - if (progress_end - progress_begin > 0.0) { - m_scanning_progress = (progress_current - progress_begin) / (progress_end - progress_begin); - } else { // avoid divide-by-zero for single block scan range (i.e. start and stop hashes are equal) - m_scanning_progress = 0; - } - if (block_height % 100 == 0 && progress_end - progress_begin > 0.0) { - ShowProgress(strprintf("[%s] %s", DisplayName(), _("Rescanning…")), std::max(1, std::min(99, (int)(m_scanning_progress * 100)))); - } - - bool next_interval = reserver.now() >= current_time + INTERVAL_TIME; - if (next_interval) { - current_time = reserver.now(); - WalletLogPrintf("Still rescanning. At block %d. Progress=%f\n", block_height, progress_current); - } - - bool fetch_block{true}; - if (fast_rescan_filter) { - fast_rescan_filter->UpdateIfNeeded(); - auto matches_block{fast_rescan_filter->MatchesBlock(block_hash)}; - if (matches_block.has_value()) { - if (*matches_block) { - LogDebug(BCLog::SCAN, "Fast rescan: inspect block %d [%s] (filter matched)\n", block_height, block_hash.ToString()); - } else { - result.last_scanned_block = block_hash; - result.last_scanned_height = block_height; - fetch_block = false; - } - } else { - LogDebug(BCLog::SCAN, "Fast rescan: inspect block %d [%s] (WARNING: block filter not found!)\n", block_height, block_hash.ToString()); - } - } - - // Find next block separately from reading data above, because reading - // is slow and there might be a reorg while it is read. - bool block_still_active = false; - bool next_block = false; - uint256 next_block_hash; - chain().findBlock(block_hash, FoundBlock().inActiveChain(block_still_active).nextBlock(FoundBlock().inActiveChain(next_block).hash(next_block_hash))); - - if (fetch_block) { - // Read block data and locator if needed (the locator is usually null unless we need to save progress) - CBlock block; - CBlockLocator loc; - // Find block - FoundBlock found_block{FoundBlock().data(block)}; - if (save_progress && next_interval) found_block.locator(loc); - chain().findBlock(block_hash, found_block); - - if (!block.IsNull()) { - LOCK(cs_wallet); - if (!block_still_active) { - // Abort scan if current block is no longer active, to prevent - // marking transactions as coming from the wrong block. - result.last_failed_block = block_hash; - result.status = ScanResult::FAILURE; - break; - } - for (size_t posInBlock = 0; posInBlock < block.vtx.size(); ++posInBlock) { - SyncTransaction(block.vtx[posInBlock], TxStateConfirmed{block_hash, block_height, static_cast(posInBlock)}, /*rescanning_old_block=*/true); - } - // scan succeeded, record block as most recent successfully scanned - result.last_scanned_block = block_hash; - result.last_scanned_height = block_height; - - if (!loc.IsNull()) { - WalletLogPrintf("Saving scan progress %d.\n", block_height); - WalletBatch batch(GetDatabase()); - batch.WriteBestBlock(loc); - } - } else { - // could not scan block, keep scanning but record this block as the most recent failure - result.last_failed_block = block_hash; - result.status = ScanResult::FAILURE; - } - } - if (max_height && block_height >= *max_height) { - break; - } - // If rescanning was triggered with cs_wallet permanently locked (AttachChain), additional blocks that were connected during the rescan - // aren't processed here but will be processed with the pending blockConnected notifications after the lock is released. - // If rescanning without a permanent cs_wallet lock, additional blocks that were added during the rescan will be re-processed if - // the notification was processed and the last block height was updated. - if (block_height >= WITH_LOCK(cs_wallet, return GetLastBlockHeight())) { - break; - } - - { - if (!next_block) { - // break successfully when rescan has reached the tip, or - // previous block is no longer on the chain due to a reorg - break; - } - - // increment block and verification progress - block_hash = next_block_hash; - ++block_height; - progress_current = chain().guessVerificationProgress(block_hash); - - // handle updated tip hash - const uint256 prev_tip_hash = tip_hash; - tip_hash = WITH_LOCK(cs_wallet, return GetLastBlockHash()); - if (!max_height && prev_tip_hash != tip_hash) { - // in case the tip has changed, update progress max - progress_end = chain().guessVerificationProgress(tip_hash); - } - } - } - if (!max_height) { - WalletLogPrintf("Scanning current mempool transactions.\n"); - WITH_LOCK(cs_wallet, chain().requestMempoolTransactions(*this)); - } - ShowProgress(strprintf("[%s] %s", DisplayName(), _("Rescanning…")), 100); // hide progress dialog in GUI - if (fAbortRescan) { - WalletLogPrintf("Rescan aborted at block %d. Progress=%f\n", block_height, progress_current); - result.status = ScanResult::USER_ABORT; - } else if (chain().shutdownRequested()) { - WalletLogPrintf("Rescan interrupted by shutdown request at block %d. Progress=%f\n", block_height, progress_current); - result.status = ScanResult::USER_ABORT; - } else { - WalletLogPrintf("Rescan completed in %15dms\n", Ticks(reserver.now() - start_time)); - } - return result; -} - bool CWallet::SubmitTxMemoryPoolAndRelay(CWalletTx& wtx, std::string& err_string, node::TxBroadcast broadcast_method) const @@ -3303,7 +3114,7 @@ bool CWallet::AttachChain(const std::shared_ptr& walletInstance, interf error = _("Failed to acquire rescan reserver during wallet initialization"); return false; } - ScanResult scan_res = walletInstance->ScanForWalletTransactions(chain.getBlockHash(rescan_height), rescan_height, /*max_height=*/{}, reserver, /*save_progress=*/true); + ScanResult scan_res = walletInstance->Scanner().Scan(chain.getBlockHash(rescan_height), rescan_height, /*max_height=*/{}, reserver, /*save_progress=*/true); if (ScanResult::SUCCESS != scan_res.status) { error = _("Failed to rescan the wallet during initialization"); return false; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 2d1f3b6e8f25..79d282500e25 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -83,6 +83,7 @@ using LoadWalletFn = std::function wall struct bilingual_str; namespace wallet { +class ChainScanner; struct WalletContext; //! Explicitly delete the wallet. @@ -302,7 +303,8 @@ struct CRecipient bool fSubtractFeeFromAmount; }; -class WalletRescanReserver; //forward declarations for ScanForWalletTransactions/RescanFromTime +class WalletRescanReserver; + /** * A CWallet maintains a set of transactions and balances, and provides the ability to create new transactions. */ @@ -313,12 +315,8 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati bool Unlock(const CKeyingMaterial& vMasterKeyIn); - std::atomic fAbortRescan{false}; - std::atomic fScanningWallet{false}; // controlled by WalletRescanReserver - std::atomic m_scanning_with_passphrase{false}; - std::atomic m_scanning_start{SteadyClock::time_point{}}; - std::atomic m_scanning_progress{0}; friend class WalletRescanReserver; + friend class ChainScanner; /** The next scheduled rebroadcast of wallet transactions. */ NodeClock::time_point m_next_resend{GetDefaultNextResend()}; @@ -397,6 +395,8 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati /** Internal database handle. */ std::unique_ptr m_database; + std::unique_ptr m_scanner; + /** * The following is used to keep track of how far behind the wallet is * from the chain sync, and to allow clients to block on us being caught up. @@ -473,18 +473,8 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati unsigned int nMasterKeyMaxID = 0; /** Construct wallet with specified name and database implementation. */ - CWallet(interfaces::Chain* chain, const std::string& name, std::unique_ptr database) - : m_chain(chain), - m_name(name), - m_database(std::move(database)) - { - } - - ~CWallet() - { - // Should not have slots connected at this point. - assert(NotifyUnload.empty()); - } + CWallet(interfaces::Chain* chain, const std::string& name, std::unique_ptr database); + ~CWallet(); bool IsLocked() const override; bool Lock(); @@ -576,15 +566,8 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati bool UnlockAllCoins() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void ListLockedCoins(std::vector& vOutpts) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); - /* - * Rescan abort properties - */ - void AbortRescan() { fAbortRescan = true; } - bool IsAbortingRescan() const { return fAbortRescan; } - bool IsScanning() const { return fScanningWallet; } - bool IsScanningWithPassphrase() const { return m_scanning_with_passphrase; } - SteadyClock::duration ScanningDuration() const { return fScanningWallet ? SteadyClock::now() - m_scanning_start.load() : SteadyClock::duration{}; } - double ScanningProgress() const { return fScanningWallet ? (double) m_scanning_progress : 0; } + ChainScanner& Scanner(); + const ChainScanner& Scanner() const; //! Upgrade DescriptorCaches void UpgradeDescriptorCache() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); @@ -636,23 +619,6 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati void blockDisconnected(const interfaces::BlockInfo& block) override; void updatedBlockTip() override; int64_t RescanFromTime(int64_t startTime, const WalletRescanReserver& reserver); - - struct ScanResult { - enum { SUCCESS, FAILURE, USER_ABORT } status = SUCCESS; - - //! Hash and height of most recent block that was successfully scanned. - //! Unset if no blocks were scanned due to read errors or the chain - //! being empty. - uint256 last_scanned_block; - std::optional last_scanned_height; - - //! Height of the most recent block that could not be scanned due to - //! read errors or pruning. Will be set if status is FAILURE, unset if - //! status is SUCCESS, and may or may not be set if status is - //! USER_ABORT. - uint256 last_failed_block; - }; - ScanResult ScanForWalletTransactions(const uint256& start_block, int start_height, std::optional max_height, const WalletRescanReserver& reserver, bool save_progress); void transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason) override; /** Set the next time this wallet should resend transactions to 12-36 hours from now, ~1 day on average. */ void SetNextResend() { m_next_resend = GetDefaultNextResend(); } @@ -1098,39 +1064,14 @@ class WalletRescanReserver public: explicit WalletRescanReserver(CWallet& w) : m_wallet(w) {} - bool reserve(bool with_passphrase = false) - { - assert(!m_could_reserve); - if (m_wallet.fScanningWallet.exchange(true)) { - return false; - } - // Discard any abort request left over from previous reservation, so - // that an abort requested while the reservation is held always applies - // to abort this rescan, even if it arrives before the scan loop starts. - m_wallet.fAbortRescan = false; - m_wallet.m_scanning_with_passphrase.exchange(with_passphrase); - m_wallet.m_scanning_start = SteadyClock::now(); - m_wallet.m_scanning_progress = 0; - m_could_reserve = true; - return true; - } - - bool isReserved() const - { - return (m_could_reserve && m_wallet.fScanningWallet); - } + bool reserve(bool with_passphrase = false); + bool isReserved() const; Clock::time_point now() const { return m_now ? m_now() : Clock::now(); }; void setNow(NowFn now) { m_now = std::move(now); } - ~WalletRescanReserver() - { - if (m_could_reserve) { - m_wallet.fScanningWallet = false; - m_wallet.m_scanning_with_passphrase = false; - } - } + ~WalletRescanReserver(); }; //! Add wallet name to persistent configuration so it will be loaded on startup. diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py index f60484d84cd7..77248925f3ba 100755 --- a/test/functional/wallet_importdescriptors.py +++ b/test/functional/wallet_importdescriptors.py @@ -184,7 +184,7 @@ def import_after_barrier(wallet, descriptors): importing = thread.submit(w_import.importdescriptors, descriptor) - # Keep trying because an abort before ScanForWalletTransactions starts + # Keep trying because an abort before wallet transaction scan starts # is reset when the scan loop begins. abort_succeeded = False abort_deadline = time.time() + 30 * self.options.timeout_factor diff --git a/test/lint/lint-circular-dependencies.py b/test/lint/lint-circular-dependencies.py index 8114b6dd5eac..28adcc93e529 100755 --- a/test/lint/lint-circular-dependencies.py +++ b/test/lint/lint-circular-dependencies.py @@ -19,6 +19,7 @@ "qt/recentrequeststablemodel -> qt/walletmodel -> qt/recentrequeststablemodel", "qt/sendcoinsdialog -> qt/walletmodel -> qt/sendcoinsdialog", "qt/transactiontablemodel -> qt/walletmodel -> qt/transactiontablemodel", + "wallet/scan -> wallet/wallet -> wallet/scan", "wallet/wallet -> wallet/walletdb -> wallet/wallet", "kernel/coinstats -> validation -> kernel/coinstats", "versionbits -> versionbits_impl -> versionbits", From 0274e80130af2cc58847b5d8f3628fa97a6f4b79 Mon Sep 17 00:00:00 2001 From: Novo Date: Tue, 26 May 2026 19:07:01 +0200 Subject: [PATCH 033/292] wallet/scan: move RescanFromTime to ChainScanner as ScanFromTime Callers now reach this via Scanner().ScanFromTime() rather than a CWallet member function, keeping all scan logic in ChainScanner. --- src/wallet/rpc/backup.cpp | 2 +- src/wallet/scan.cpp | 24 ++++++++++++++++++++++++ src/wallet/scan.h | 7 +++++++ src/wallet/test/wallet_tests.cpp | 4 ++-- src/wallet/wallet.cpp | 30 ------------------------------ src/wallet/wallet.h | 1 - 6 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index 889bf91af86f..0ffa694811be 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -429,7 +429,7 @@ RPCMethod importdescriptors() // Rescan the blockchain using the lowest timestamp if (rescan) { - int64_t scanned_time = pwallet->RescanFromTime(lowest_timestamp, reserver); + int64_t scanned_time = pwallet->Scanner().ScanFromTime(lowest_timestamp, reserver); pwallet->ResubmitWalletTransactions(node::TxBroadcast::MEMPOOL_NO_BROADCAST, /*force=*/true); if (pwallet->Scanner().IsAborting()) { diff --git a/src/wallet/scan.cpp b/src/wallet/scan.cpp index 2259ba4bfffb..114251598016 100644 --- a/src/wallet/scan.cpp +++ b/src/wallet/scan.cpp @@ -2,10 +2,12 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include #include #include #include #include +#include #include #include @@ -13,6 +15,28 @@ using interfaces::FoundBlock; namespace wallet { +int64_t ChainScanner::ScanFromTime(int64_t startTime, const WalletRescanReserver& reserver) +{ + // Find starting block. May be null if nCreateTime is greater than the + // highest blockchain timestamp, in which case there is nothing that needs + // to be scanned. + int start_height = 0; + uint256 start_block; + bool start = m_wallet.chain().findFirstBlockWithTimeAndHeight(startTime - TIMESTAMP_WINDOW, 0, FoundBlock().hash(start_block).height(start_height)); + m_wallet.WalletLogPrintf("%s: Rescanning last %i blocks\n", __func__, start ? WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHeight()) - start_height + 1 : 0); + + if (start) { + // TODO: this should take into account failure by ScanResult::USER_ABORT + ScanResult result = Scan(start_block, start_height, /*max_height=*/{}, reserver, /*save_progress=*/false); + if (result.status == ScanResult::FAILURE) { + int64_t time_max; + CHECK_NONFATAL(m_wallet.chain().findBlock(result.last_failed_block, FoundBlock().maxTime(time_max))); + return time_max + TIMESTAMP_WINDOW + 1; + } + } + return startTime; +} + bool ChainScanner::TryReserve(bool with_passphrase) { if (m_scanning.exchange(true)) return false; // Discard any abort request left over from previous reservation, so diff --git a/src/wallet/scan.h b/src/wallet/scan.h index a6f7be491cb9..1875ed1d2736 100644 --- a/src/wallet/scan.h +++ b/src/wallet/scan.h @@ -58,6 +58,13 @@ class ChainScanner { SteadyClock::duration ScanningDuration() const { return m_scanning ? SteadyClock::now() - m_scanning_start.load() : SteadyClock::duration{}; } double ScanningProgress() const { return m_scanning ? m_scanning_progress.load() : 0; } + /** Scan active chain for relevant transactions after importing keys. Should + * be called whenever new keys are added to the wallet, with the oldest key + * creation time. + * @return Earliest timestamp that could be successfully scanned from. Timestamp + * returned will be higher than startTime if relevant blocks could not be read. */ + int64_t ScanFromTime(int64_t startTime, const WalletRescanReserver& reserver); + /** * Scan the block chain (starting in start_block) for transactions * from or to us. If max_height is not set, the diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index cde9ab9331bc..2d9de620a774 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -546,7 +546,7 @@ BOOST_FIXTURE_TEST_CASE(rescan_from_time, TestChain100Setup) // is moved past the last unreadable block, telling the caller from when // the rescan is actually complete. const int64_t genesis_time{WITH_LOCK(::cs_main, return m_node.chainman->ActiveChain().Genesis()->GetBlockTime())}; - BOOST_CHECK_EQUAL(wallet.RescanFromTime(genesis_time, reserver), + BOOST_CHECK_EQUAL(wallet.Scanner().ScanFromTime(genesis_time, reserver), WITH_LOCK(::cs_main, return old_tip->GetBlockTimeMax()) + TIMESTAMP_WINDOW + 1); bool scan_logged{false}; @@ -557,7 +557,7 @@ BOOST_FIXTURE_TEST_CASE(rescan_from_time, TestChain100Setup) // A timestamp past the tip requires no scanning and is returned unchanged. const int64_t future_time{WITH_LOCK(::cs_main, return new_tip->GetBlockTimeMax()) + TIMESTAMP_WINDOW + 1}; BOOST_CHECK(!scan_logged); - BOOST_CHECK_EQUAL(wallet.RescanFromTime(future_time, reserver), future_time); + BOOST_CHECK_EQUAL(wallet.Scanner().ScanFromTime(future_time, reserver), future_time); } BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_missing_filter, TestChain100Setup) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 862e31835abf..d822bb5bd178 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1805,36 +1805,6 @@ void CWallet::MaybeUpdateBirthTime(int64_t time) } } -/** - * Scan active chain for relevant transactions after importing keys. This should - * be called whenever new keys are added to the wallet, with the oldest key - * creation time. - * - * @return Earliest timestamp that could be successfully scanned from. Timestamp - * returned will be higher than startTime if relevant blocks could not be read. - */ -int64_t CWallet::RescanFromTime(int64_t startTime, const WalletRescanReserver& reserver) -{ - // Find starting block. May be null if nCreateTime is greater than the - // highest blockchain timestamp, in which case there is nothing that needs - // to be scanned. - int start_height = 0; - uint256 start_block; - bool start = chain().findFirstBlockWithTimeAndHeight(startTime - TIMESTAMP_WINDOW, 0, FoundBlock().hash(start_block).height(start_height)); - WalletLogPrintf("%s: Rescanning last %i blocks\n", __func__, start ? WITH_LOCK(cs_wallet, return GetLastBlockHeight()) - start_height + 1 : 0); - - if (start) { - // TODO: this should take into account failure by ScanResult::USER_ABORT - ScanResult result = Scanner().Scan(start_block, start_height, /*max_height=*/{}, reserver, /*save_progress=*/false); - if (result.status == ScanResult::FAILURE) { - int64_t time_max; - CHECK_NONFATAL(chain().findBlock(result.last_failed_block, FoundBlock().maxTime(time_max))); - return time_max + TIMESTAMP_WINDOW + 1; - } - } - return startTime; -} - bool CWallet::SubmitTxMemoryPoolAndRelay(CWalletTx& wtx, std::string& err_string, node::TxBroadcast broadcast_method) const diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 79d282500e25..ce5670ca85f1 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -618,7 +618,6 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati void blockConnected(const kernel::ChainstateRole& role, const interfaces::BlockInfo& block) override; void blockDisconnected(const interfaces::BlockInfo& block) override; void updatedBlockTip() override; - int64_t RescanFromTime(int64_t startTime, const WalletRescanReserver& reserver); void transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason) override; /** Set the next time this wallet should resend transactions to 12-36 hours from now, ~1 day on average. */ void SetNextResend() { m_next_resend = GetDefaultNextResend(); } From 72a37031d325e9f50bc95ef7496fc495b755962e Mon Sep 17 00:00:00 2001 From: Novo Date: Tue, 26 May 2026 19:07:51 +0200 Subject: [PATCH 034/292] wallet/scan: move WalletRescanReserver to scan files --- src/wallet/scan.cpp | 19 +++++++++++++++++++ src/wallet/scan.h | 24 +++++++++++++++++++++++- src/wallet/wallet.cpp | 19 ------------------- src/wallet/wallet.h | 23 ----------------------- 4 files changed, 42 insertions(+), 43 deletions(-) diff --git a/src/wallet/scan.cpp b/src/wallet/scan.cpp index 114251598016..b7ab8b16175e 100644 --- a/src/wallet/scan.cpp +++ b/src/wallet/scan.cpp @@ -37,6 +37,25 @@ int64_t ChainScanner::ScanFromTime(int64_t startTime, const WalletRescanReserver return startTime; } +bool WalletRescanReserver::reserve(bool with_passphrase) { + assert(!m_could_reserve); + if (!m_wallet.Scanner().TryReserve(with_passphrase)) { + return false; + } + m_could_reserve = true; + return true; +} + +bool WalletRescanReserver::isReserved() const { + return (m_could_reserve && m_wallet.Scanner().IsScanning()); +} + +WalletRescanReserver::~WalletRescanReserver() { + if (m_could_reserve) { + m_wallet.Scanner().Release(); + } +} + bool ChainScanner::TryReserve(bool with_passphrase) { if (m_scanning.exchange(true)) return false; // Discard any abort request left over from previous reservation, so diff --git a/src/wallet/scan.h b/src/wallet/scan.h index 1875ed1d2736..cb07da71d3b4 100644 --- a/src/wallet/scan.h +++ b/src/wallet/scan.h @@ -9,11 +9,11 @@ #include #include +#include #include namespace wallet { class CWallet; -class WalletRescanReserver; /** Result of a wallet scan */ struct ScanResult { @@ -32,6 +32,28 @@ struct ScanResult { uint256 last_failed_block; }; +/** RAII object to check and reserve a wallet rescan */ +class WalletRescanReserver +{ +private: + using Clock = std::chrono::steady_clock; + using NowFn = std::function; + CWallet& m_wallet; + bool m_could_reserve{false}; + NowFn m_now; +public: + explicit WalletRescanReserver(CWallet& w) : m_wallet(w) {} + + bool reserve(bool with_passphrase = false); + bool isReserved() const; + + Clock::time_point now() const { return m_now ? m_now() : Clock::now(); }; + + void setNow(NowFn now) { m_now = std::move(now); } + + ~WalletRescanReserver(); +}; + class ChainScanner { private: CWallet& m_wallet; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index d822bb5bd178..2f89d03a6009 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -496,25 +496,6 @@ CWallet::~CWallet() ChainScanner& CWallet::Scanner() { return *m_scanner; } const ChainScanner& CWallet::Scanner() const { return *m_scanner; } -bool WalletRescanReserver::reserve(bool with_passphrase) { - assert(!m_could_reserve); - if (!m_wallet.Scanner().TryReserve(with_passphrase)) { - return false; - } - m_could_reserve = true; - return true; -} - -bool WalletRescanReserver::isReserved() const { - return (m_could_reserve && m_wallet.Scanner().IsScanning()); -} - -WalletRescanReserver::~WalletRescanReserver() { - if (m_could_reserve) { - m_wallet.Scanner().Release(); - } -} - /** @defgroup mapWallet * * @{ diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index ce5670ca85f1..a9ca58a158e9 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -303,7 +303,6 @@ struct CRecipient bool fSubtractFeeFromAmount; }; -class WalletRescanReserver; /** * A CWallet maintains a set of transactions and balances, and provides the ability to create new transactions. @@ -315,7 +314,6 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati bool Unlock(const CKeyingMaterial& vMasterKeyIn); - friend class WalletRescanReserver; friend class ChainScanner; /** The next scheduled rebroadcast of wallet transactions. */ @@ -1051,27 +1049,6 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati */ void MaybeResendWalletTxs(WalletContext& context); -/** RAII object to check and reserve a wallet rescan */ -class WalletRescanReserver -{ -private: - using Clock = std::chrono::steady_clock; - using NowFn = std::function; - CWallet& m_wallet; - bool m_could_reserve{false}; - NowFn m_now; -public: - explicit WalletRescanReserver(CWallet& w) : m_wallet(w) {} - - bool reserve(bool with_passphrase = false); - bool isReserved() const; - - Clock::time_point now() const { return m_now ? m_now() : Clock::now(); }; - - void setNow(NowFn now) { m_now = std::move(now); } - - ~WalletRescanReserver(); -}; //! Add wallet name to persistent configuration so it will be loaded on startup. bool AddWalletSetting(interfaces::Chain& chain, const std::string& wallet_name); From 4535dc0c7885ad678074699627b363f382f29906 Mon Sep 17 00:00:00 2001 From: Novo Date: Tue, 26 May 2026 19:08:49 +0200 Subject: [PATCH 035/292] wallet/scan: extract block filter matching to ShouldFetchBlock --- src/wallet/scan.cpp | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/wallet/scan.cpp b/src/wallet/scan.cpp index b7ab8b16175e..9f6afbf88449 100644 --- a/src/wallet/scan.cpp +++ b/src/wallet/scan.cpp @@ -128,6 +128,21 @@ class FastWalletRescanFilter } } }; + +static bool ShouldFetchBlock(const FastWalletRescanFilter& filter, const uint256& block_hash, int block_height) { + auto matches_block{filter.MatchesBlock(block_hash)}; + if (matches_block.has_value()) { + if (*matches_block) { + LogDebug(BCLog::SCAN, "Fast rescan: inspect block %d [%s] (filter matched)\n", block_height, block_hash.ToString()); + return true; + } else { + return false; + } + } else { + LogDebug(BCLog::SCAN, "Fast rescan: inspect block %d [%s] (WARNING: block filter not found!)\n", block_height, block_hash.ToString()); + return true; + } +} } // namespace ScanResult ChainScanner::Scan(const uint256& start_block, int start_height, std::optional max_height, @@ -176,18 +191,7 @@ ScanResult ChainScanner::Scan(const uint256& start_block, int start_height, std: bool fetch_block{true}; if (fast_rescan_filter) { fast_rescan_filter->UpdateIfNeeded(); - auto matches_block{fast_rescan_filter->MatchesBlock(block_hash)}; - if (matches_block.has_value()) { - if (*matches_block) { - LogDebug(BCLog::SCAN, "Fast rescan: inspect block %d [%s] (filter matched)\n", block_height, block_hash.ToString()); - } else { - result.last_scanned_block = block_hash; - result.last_scanned_height = block_height; - fetch_block = false; - } - } else { - LogDebug(BCLog::SCAN, "Fast rescan: inspect block %d [%s] (WARNING: block filter not found!)\n", block_height, block_hash.ToString()); - } + fetch_block = ShouldFetchBlock(*fast_rescan_filter, block_hash, block_height); } // Find next block separately from reading data above, because reading @@ -232,7 +236,11 @@ ScanResult ChainScanner::Scan(const uint256& start_block, int start_height, std: result.last_failed_block = block_hash; result.status = ScanResult::FAILURE; } + } else { + result.last_scanned_block = block_hash; + result.last_scanned_height = block_height; } + if (max_height && block_height >= *max_height) { break; } From 3655beb8f1531f780fffcbf2f1018d893b87e98e Mon Sep 17 00:00:00 2001 From: Novo Date: Tue, 26 May 2026 19:11:56 +0200 Subject: [PATCH 036/292] wallet/scan: extract block scanning logic to ScanBlock A slight behavior change is introduced here. Previously, if a block could not be read and the block is not active, the scan will continue but record this block as the most recent failure. After this commit, the scan will abort with this block as the most recent failure. This happens because the `Scan()` function now checks if the block is active before trying to read the block from disk. The scan process already ignores reorged blocks, so this should have no effect on the wallet balance. --- src/wallet/scan.cpp | 81 +++++++++++++++++++++++++-------------------- src/wallet/scan.h | 2 ++ 2 files changed, 47 insertions(+), 36 deletions(-) diff --git a/src/wallet/scan.cpp b/src/wallet/scan.cpp index 9f6afbf88449..6214770b0526 100644 --- a/src/wallet/scan.cpp +++ b/src/wallet/scan.cpp @@ -145,6 +145,37 @@ static bool ShouldFetchBlock(const FastWalletRescanFilter& filter, const uint256 } } // namespace +bool ChainScanner::ScanBlock(const uint256& block_hash, int block_height, bool save_progress) { + // Read block data and locator if needed (the locator is usually null unless we need to save progress) + CBlock block; + CBlockLocator loc; + // Find block + FoundBlock found_block{FoundBlock().data(block)}; + if (save_progress) found_block.locator(loc); + m_wallet.chain().findBlock(block_hash, found_block); + + if (block.IsNull()) return false; + + { + // cs_wallet is a RecursiveMutex; ScanBlock may be called + // with cs_wallet already held as in AttachChain or without it. + LOCK(m_wallet.cs_wallet); + for (size_t posInBlock = 0; posInBlock < block.vtx.size(); ++posInBlock) { + m_wallet.SyncTransaction( + block.vtx[posInBlock], TxStateConfirmed{block_hash, block_height, + static_cast(posInBlock)}, + /*rescanning_old_block=*/true); + } + + if (!loc.IsNull()) { + m_wallet.WalletLogPrintf("Saving scan progress %d.\n", block_height); + WalletBatch batch(m_wallet.GetDatabase()); + batch.WriteBestBlock(loc); + } + } + return true; +} + ScanResult ChainScanner::Scan(const uint256& start_block, int start_height, std::optional max_height, const WalletRescanReserver& reserver, bool save_progress) { constexpr auto INTERVAL_TIME{60s}; @@ -201,44 +232,22 @@ ScanResult ChainScanner::Scan(const uint256& start_block, int start_height, std: uint256 next_block_hash; chain.findBlock(block_hash, FoundBlock().inActiveChain(block_still_active).nextBlock(FoundBlock().inActiveChain(next_block).hash(next_block_hash))); - if (fetch_block) { - // Read block data and locator if needed (the locator is usually null unless we need to save progress) - CBlock block; - CBlockLocator loc; - // Find block - FoundBlock found_block{FoundBlock().data(block)}; - if (save_progress && next_interval) found_block.locator(loc); - chain.findBlock(block_hash, found_block); - - if (!block.IsNull()) { - LOCK(m_wallet.cs_wallet); - if (!block_still_active) { - // Abort scan if current block is no longer active, to prevent - // marking transactions as coming from the wrong block. - result.last_failed_block = block_hash; - result.status = ScanResult::FAILURE; - break; - } - for (size_t posInBlock = 0; posInBlock < block.vtx.size(); ++posInBlock) { - m_wallet.SyncTransaction(block.vtx[posInBlock], TxStateConfirmed{block_hash, block_height, static_cast(posInBlock)}, /*rescanning_old_block=*/true); - } - // scan succeeded, record block as most recent successfully scanned - result.last_scanned_block = block_hash; - result.last_scanned_height = block_height; - - if (!loc.IsNull()) { - m_wallet.WalletLogPrintf("Saving scan progress %d.\n", block_height); - WalletBatch batch(m_wallet.GetDatabase()); - batch.WriteBestBlock(loc); - } - } else { - // could not scan block, keep scanning but record this block as the most recent failure - result.last_failed_block = block_hash; - result.status = ScanResult::FAILURE; - } - } else { + if (fetch_block && !block_still_active) { + // Abort scan if current block is no longer active, to prevent + // marking transactions as coming from the wrong block. + result.last_failed_block = block_hash; + result.status = ScanResult::FAILURE; + break; + } + if (!fetch_block || ScanBlock(block_hash, block_height, save_progress && next_interval)) { + // scanned the block, or skipped it via the filter: record it as + // the most recent successfully scanned block result.last_scanned_block = block_hash; result.last_scanned_height = block_height; + } else { + // could not scan block, keep scanning but record this block as the most recent failure + result.last_failed_block = block_hash; + result.status = ScanResult::FAILURE; } if (max_height && block_height >= *max_height) { diff --git a/src/wallet/scan.h b/src/wallet/scan.h index cb07da71d3b4..07dc57ba46ac 100644 --- a/src/wallet/scan.h +++ b/src/wallet/scan.h @@ -64,6 +64,8 @@ class ChainScanner { std::atomic m_scanning_start{SteadyClock::time_point{}}; std::atomic m_scanning_progress{0}; + bool ScanBlock(const uint256& block_hash, int block_height, bool save_progress); + //! Only WalletRescanReserver may reserve and release scans, so that //! reservations are always managed RAII-style. friend class WalletRescanReserver; From 062aa3ce56d72c03569743aa4142bcec2400bf8f Mon Sep 17 00:00:00 2001 From: Novo Date: Tue, 26 May 2026 19:15:57 +0200 Subject: [PATCH 037/292] wallet/scan: extract QueueNextBlock Dequeue the current block at the top of the Scan loop and extract the lookup of its chain position and the queueing of its active-chain successor into QueueNextBlock. Since the current block is now dequeued at the top of the loop, update progress_current there as well so the reported progress keeps referring to the block being processed, as before. --- src/wallet/scan.cpp | 86 +++++++++++++++++++++++++-------------------- src/wallet/scan.h | 4 +++ 2 files changed, 52 insertions(+), 38 deletions(-) diff --git a/src/wallet/scan.cpp b/src/wallet/scan.cpp index 6214770b0526..950717442834 100644 --- a/src/wallet/scan.cpp +++ b/src/wallet/scan.cpp @@ -145,6 +145,23 @@ static bool ShouldFetchBlock(const FastWalletRescanFilter& filter, const uint256 } } // namespace +bool ChainScanner::QueueNextBlock(const uint256& block_hash, int block_height, std::optional>& next_block, std::optional max_height) { + bool block_still_active = false; + bool has_next_block = false; + uint256 next_block_hash; + m_wallet.chain().findBlock(block_hash, FoundBlock().inActiveChain(block_still_active).nextBlock(FoundBlock().inActiveChain(has_next_block).hash(next_block_hash))); + + // Queue the next block if it exists and is within range. Whether the scan + // has caught up with the wallet's tip is checked after the current block + // is processed, so blocks connected while it was being processed are not + // missed. + if (has_next_block && (!max_height || block_height < *max_height)) { + next_block = {{next_block_hash, block_height + 1}}; + } + + return block_still_active; +} + bool ChainScanner::ScanBlock(const uint256& block_hash, int block_height, bool save_progress) { // Read block data and locator if needed (the locator is usually null unless we need to save progress) CBlock block; @@ -198,12 +215,23 @@ ScanResult ChainScanner::Scan(const uint256& start_block, int start_height, std: if (max_height) chain.findAncestorByHeight(tip_hash, *max_height, FoundBlock().hash(end_hash)); ScanResult result; - uint256 block_hash = start_block; - double progress_begin = chain.guessVerificationProgress(block_hash); + double progress_begin = chain.guessVerificationProgress(start_block); double progress_end = chain.guessVerificationProgress(end_hash); double progress_current = progress_begin; + std::optional> next_block = {{start_block, start_height}}; int block_height = start_height; while (!m_abort && !chain.shutdownRequested()) { + if (!next_block) break; + + const uint256 block_hash = next_block->first; + block_height = next_block->second; + next_block.reset(); + // Look up the current block's position separately from reading its + // data below, because reading is slow and there might be a reorg + // while it is read. + const bool block_still_active = QueueNextBlock(block_hash, block_height, next_block, max_height); + + progress_current = chain.guessVerificationProgress(block_hash); if (progress_end - progress_begin > 0.0) { m_scanning_progress = (progress_current - progress_begin) / (progress_end - progress_begin); } else { // avoid divide-by-zero for single block scan range (i.e. start and stop hashes are equal) @@ -225,16 +253,13 @@ ScanResult ChainScanner::Scan(const uint256& start_block, int start_height, std: fetch_block = ShouldFetchBlock(*fast_rescan_filter, block_hash, block_height); } - // Find next block separately from reading data above, because reading - // is slow and there might be a reorg while it is read. - bool block_still_active = false; - bool next_block = false; - uint256 next_block_hash; - chain.findBlock(block_hash, FoundBlock().inActiveChain(block_still_active).nextBlock(FoundBlock().inActiveChain(next_block).hash(next_block_hash))); - if (fetch_block && !block_still_active) { - // Abort scan if current block is no longer active, to prevent - // marking transactions as coming from the wrong block. + // Abort scan if a block that needs to be inspected is no longer + // active, to prevent marking transactions as coming from the + // wrong block. A block skipped by the filter can stay skipped: + // it has no successor in the active chain, so the scan ends + // successfully at the reorg point and the replacement blocks are + // handled by blockConnected notifications. result.last_failed_block = block_hash; result.status = ScanResult::FAILURE; break; @@ -249,37 +274,22 @@ ScanResult ChainScanner::Scan(const uint256& start_block, int start_height, std: result.last_failed_block = block_hash; result.status = ScanResult::FAILURE; } - - if (max_height && block_height >= *max_height) { - break; - } - // If rescanning was triggered with cs_wallet permanently locked (AttachChain), additional blocks that were connected during the rescan - // aren't processed here but will be processed with the pending blockConnected notifications after the lock is released. - // If rescanning without a permanent cs_wallet lock, additional blocks that were added during the rescan will be re-processed if - // the notification was processed and the last block height was updated. + // Stop scanning once the wallet's tip is reached, re-reading the height + // after the block was processed so a tip extension that happened + // meanwhile is picked up. If scanning with cs_wallet locked (AttachChain), + // blocks connected during rescan are handled after scanning is complete + // via blockConnected notifications. Without the lock, newly added blocks + // are re-processed here if the notifications were handled and the last + // block height was updated. if (block_height >= WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHeight())) { break; } - { - if (!next_block) { - // break successfully when rescan has reached the tip, or - // previous block is no longer on the chain due to a reorg - break; - } - - // increment block and verification progress - block_hash = next_block_hash; - ++block_height; - progress_current = chain.guessVerificationProgress(block_hash); - - // handle updated tip hash - const uint256 prev_tip_hash = tip_hash; - tip_hash = WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHash()); - if (!max_height && prev_tip_hash != tip_hash) { - // in case the tip has changed, update progress max - progress_end = chain.guessVerificationProgress(tip_hash); - } + const uint256 prev_tip_hash = tip_hash; + tip_hash = WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHash()); + if (!max_height && prev_tip_hash != tip_hash) { + // in case the tip has changed, update progress max + progress_end = chain.guessVerificationProgress(tip_hash); } } if (!max_height) { diff --git a/src/wallet/scan.h b/src/wallet/scan.h index 07dc57ba46ac..b6ad1eab1022 100644 --- a/src/wallet/scan.h +++ b/src/wallet/scan.h @@ -64,6 +64,10 @@ class ChainScanner { std::atomic m_scanning_start{SteadyClock::time_point{}}; std::atomic m_scanning_progress{0}; + //! Locate block_hash in the chain, queueing its active-chain successor + //! into next_block if it exists and is within the scan range. Returns + //! whether the block itself is still in the active chain. + bool QueueNextBlock(const uint256& block_hash, int block_height, std::optional>& next_block, std::optional max_height); bool ScanBlock(const uint256& block_hash, int block_height, bool save_progress); //! Only WalletRescanReserver may reserve and release scans, so that From e67f75cf7d3e5a8f161d1525695c8653d225a01f Mon Sep 17 00:00:00 2001 From: Novo Date: Tue, 26 May 2026 22:42:53 +0200 Subject: [PATCH 038/292] wallet/scan: extract progress tracking helpers from `ChainScanner::Scan` --- src/wallet/scan.cpp | 52 ++++++++++++++++++++++++++++----------------- src/wallet/scan.h | 12 +++++++++++ 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/wallet/scan.cpp b/src/wallet/scan.cpp index 950717442834..8abb43cb8872 100644 --- a/src/wallet/scan.cpp +++ b/src/wallet/scan.cpp @@ -162,6 +162,28 @@ bool ChainScanner::QueueNextBlock(const uint256& block_hash, int block_height, s return block_still_active; } +void ChainScanner::UpdateProgress(const LoopState& state, double progress_current, int block_height) { + m_scanning_progress = 0; + double progress_diff = state.progress_end - state.progress_begin; + + // avoid divide-by-zero for single block scan range (i.e. start and stop hashes are equal) + if (progress_diff <= 0.0) return; + m_scanning_progress = (progress_current - state.progress_begin) / progress_diff; + + if (block_height % 100 == 0) { + m_wallet.ShowProgress(strprintf("[%s] %s", m_wallet.DisplayName(), _("Rescanning…")), + std::max(1, std::min(99, (int)(m_scanning_progress.load() * 100)))); + } +} + +void ChainScanner::UpdateTipIfChanged(LoopState& state) { + const uint256 new_tip = WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHash()); + if (new_tip != state.tip_hash) { + state.tip_hash = new_tip; + state.progress_end = m_wallet.chain().guessVerificationProgress(state.tip_hash); + } +} + bool ChainScanner::ScanBlock(const uint256& block_hash, int block_height, bool save_progress) { // Read block data and locator if needed (the locator is usually null unless we need to save progress) CBlock block; @@ -210,14 +232,15 @@ ScanResult ChainScanner::Scan(const uint256& start_block, int start_height, std: // show rescan progress in GUI as dialog or on splashscreen, if rescan required on startup (e.g. due to corruption) m_wallet.ShowProgress(strprintf("[%s] %s", m_wallet.DisplayName(), _("Rescanning…")), 0); - uint256 tip_hash = WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHash()); - uint256 end_hash = tip_hash; - if (max_height) chain.findAncestorByHeight(tip_hash, *max_height, FoundBlock().hash(end_hash)); ScanResult result; - double progress_begin = chain.guessVerificationProgress(start_block); - double progress_end = chain.guessVerificationProgress(end_hash); - double progress_current = progress_begin; + LoopState state; + state.tip_hash = WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHash()); + uint256 end_hash = state.tip_hash; + if (max_height) chain.findAncestorByHeight(state.tip_hash, *max_height, FoundBlock().hash(end_hash)); + state.progress_begin = chain.guessVerificationProgress(start_block); + state.progress_end = chain.guessVerificationProgress(end_hash); + double progress_current = state.progress_begin; std::optional> next_block = {{start_block, start_height}}; int block_height = start_height; while (!m_abort && !chain.shutdownRequested()) { @@ -232,14 +255,7 @@ ScanResult ChainScanner::Scan(const uint256& start_block, int start_height, std: const bool block_still_active = QueueNextBlock(block_hash, block_height, next_block, max_height); progress_current = chain.guessVerificationProgress(block_hash); - if (progress_end - progress_begin > 0.0) { - m_scanning_progress = (progress_current - progress_begin) / (progress_end - progress_begin); - } else { // avoid divide-by-zero for single block scan range (i.e. start and stop hashes are equal) - m_scanning_progress = 0; - } - if (block_height % 100 == 0 && progress_end - progress_begin > 0.0) { - m_wallet.ShowProgress(strprintf("[%s] %s", m_wallet.DisplayName(), _("Rescanning…")), std::max(1, std::min(99, (int)(m_scanning_progress.load() * 100)))); - } + UpdateProgress(state, progress_current, block_height); bool next_interval = reserver.now() >= current_time + INTERVAL_TIME; if (next_interval) { @@ -274,6 +290,7 @@ ScanResult ChainScanner::Scan(const uint256& start_block, int start_height, std: result.last_failed_block = block_hash; result.status = ScanResult::FAILURE; } + // Stop scanning once the wallet's tip is reached, re-reading the height // after the block was processed so a tip extension that happened // meanwhile is picked up. If scanning with cs_wallet locked (AttachChain), @@ -285,12 +302,7 @@ ScanResult ChainScanner::Scan(const uint256& start_block, int start_height, std: break; } - const uint256 prev_tip_hash = tip_hash; - tip_hash = WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHash()); - if (!max_height && prev_tip_hash != tip_hash) { - // in case the tip has changed, update progress max - progress_end = chain.guessVerificationProgress(tip_hash); - } + if (!max_height) UpdateTipIfChanged(state); } if (!max_height) { m_wallet.WalletLogPrintf("Scanning current mempool transactions.\n"); diff --git a/src/wallet/scan.h b/src/wallet/scan.h index b6ad1eab1022..7d3e91ede950 100644 --- a/src/wallet/scan.h +++ b/src/wallet/scan.h @@ -64,11 +64,23 @@ class ChainScanner { std::atomic m_scanning_start{SteadyClock::time_point{}}; std::atomic m_scanning_progress{0}; + //! Progress window and tip tracked across Scan loop iterations. The + //! current block's progress is a plain local in Scan; only the window + //! bounds are shared with the helpers, and UpdateTipIfChanged is the + //! sole mutator. + struct LoopState { + double progress_begin{0}; + double progress_end{0}; + uint256 tip_hash; + }; + //! Locate block_hash in the chain, queueing its active-chain successor //! into next_block if it exists and is within the scan range. Returns //! whether the block itself is still in the active chain. bool QueueNextBlock(const uint256& block_hash, int block_height, std::optional>& next_block, std::optional max_height); bool ScanBlock(const uint256& block_hash, int block_height, bool save_progress); + void UpdateProgress(const LoopState& state, double progress_current, int block_height); + void UpdateTipIfChanged(LoopState& state); //! Only WalletRescanReserver may reserve and release scans, so that //! reservations are always managed RAII-style. From 1c557a380288df661e13e88f5d9c7c27b9bbbefb Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Fri, 7 Aug 2026 15:12:29 -0700 Subject: [PATCH 039/292] wallet: Add WriteTxMetadata to write just the tx record When writing a tx to the wallet, we don't always need to rewrite all of the wtx variants. Most writes can write the single tx record since they are only updating metadata stored in that record. --- src/wallet/wallet.cpp | 12 ++++++------ src/wallet/walletdb.cpp | 5 +++++ src/wallet/walletdb.h | 3 +++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index eb40aea0cd6c..30449a1195da 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -934,7 +934,7 @@ DBErrors CWallet::ReorderTransactions() nOrderPos = nOrderPosNext++; nOrderPosOffsets.push_back(nOrderPos); - if (!batch.WriteTx(*pwtx)) + if (!batch.WriteTxMetadata(*pwtx)) return DBErrors::LOAD_FAIL; } else @@ -952,7 +952,7 @@ DBErrors CWallet::ReorderTransactions() continue; // Since we're changing the order, write it back - if (!batch.WriteTx(*pwtx)) + if (!batch.WriteTxMetadata(*pwtx)) return DBErrors::LOAD_FAIL; } } @@ -1004,7 +1004,7 @@ bool CWallet::MarkReplaced(const Txid& originalHash, const Txid& newHash) WalletBatch batch(GetDatabase()); bool success = true; - if (!batch.WriteTx(wtx)) { + if (!batch.WriteTxMetadata(wtx)) { WalletLogPrintf("%s: Updating batch tx %s failed\n", __func__, wtx.GetHash().ToString()); success = false; } @@ -1099,7 +1099,7 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const TxState& state, const desc_tx->m_state = inactive_state; // Break caches since we have changed the state desc_tx->MarkDirty(); - batch.WriteTx(*desc_tx); + batch.WriteTxMetadata(*desc_tx); MarkInputsDirty(desc_tx->GetTx()); for (unsigned int i = 0; i < desc_tx->GetTx()->vout.size(); ++i) { COutPoint outpoint(desc_tx->GetHash(), i); @@ -1387,7 +1387,7 @@ void CWallet::RecursiveUpdateTxState(WalletBatch* batch, const Txid& tx_hash, co TxUpdate update_state = try_updating_state(wtx); if (update_state != TxUpdate::UNCHANGED) { wtx.MarkDirty(); - if (batch) batch->WriteTx(wtx); + if (batch) batch->WriteTxMetadata(wtx); // Iterate over all its outputs, and update those tx states as well (if applicable) for (unsigned int i = 0; i < wtx.GetTx()->vout.size(); ++i) { std::pair range = mapTxSpends.equal_range(COutPoint(now, i)); @@ -4051,7 +4051,7 @@ util::Result CWallet::ApplyMigrationData(WalletBatch& local_wallet_batch, return util::Error{strprintf(_("Error: Transaction %s in wallet cannot be identified to belong to migrated wallets"), wtx->GetHash().GetHex())}; } // Rewrite the transaction so that anything that may have changed about it in memory also persists to disk - local_wallet_batch.WriteTx(*wtx); + local_wallet_batch.WriteTxMetadata(*wtx); } // Do the removes diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index a71a60c4a824..a0096d19c5fa 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -118,6 +118,11 @@ bool WalletBatch::WriteWtxVariant(const Txid& txid, const CTransactionRef& tx) return WriteIC(std::make_pair(DBKeys::WTX_VARIANT, std::make_pair(txid, tx->GetWitnessHash())), TX_WITH_WITNESS(tx)); } +bool WalletBatch::WriteTxMetadata(const CWalletTx& wtx) +{ + return WriteIC(std::make_pair(DBKeys::TX, wtx.GetHash()), wtx); +} + bool WalletBatch::WriteKeyMetadata(const CKeyMetadata& meta, const CPubKey& pubkey, const bool overwrite) { return WriteIC(std::make_pair(DBKeys::KEYMETA, pubkey), meta, overwrite); diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index f5804ab95ba1..6e0bb2f7b406 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -232,7 +232,10 @@ class WalletBatch bool WriteTx(const CWalletTx& wtx); bool EraseTx(Txid hash); + // Write a single witness variant of CWalletTx (single wtxvariant record) bool WriteWtxVariant(const Txid& txid, const CTransactionRef& tx); + // Write only the canonical witness tx and all of the tx metadata (single tx record) + bool WriteTxMetadata(const CWalletTx& wtx); bool WriteKeyMetadata(const CKeyMetadata& meta, const CPubKey& pubkey, bool overwrite); bool WriteKey(const CPubKey& vchPubKey, const CPrivKey& vchPrivKey, const CKeyMetadata &keyMeta); From cc577de954671378268bab2a78714869b4f7388d Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Wed, 12 Aug 2026 10:01:59 -0300 Subject: [PATCH 040/292] net: align v2 message type validation with v1 range BIP324 specifies the 13-byte long-form message type encoding as "an ASCII message type (as in the v1 P2P protocol)", but V2Transport::GetMessageType() accepted bytes up to 0x7F, while for V1 it only accepts printable ASCII (0x20-0x7E). This changes V2 to match V1 on it and add test coverage. --- src/net.cpp | 5 +++-- src/test/net_tests.cpp | 11 ++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/net.cpp b/src/net.cpp index e890e291c519..468aaf924a98 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1440,8 +1440,9 @@ std::optional V2Transport::GetMessageType(std::span& size_t msg_type_len{0}; while (msg_type_len < CMessageHeader::MESSAGE_TYPE_SIZE && contents[msg_type_len] != 0) { - // Verify that message type bytes before the first 0x00 are in range. - if (contents[msg_type_len] < ' ' || contents[msg_type_len] > 0x7F) { + // Verify that message type bytes before the first 0x00 are in range. BIP324 specifies the + // long message type encoding as "an ASCII message type (as in the v1 P2P protocol)". + if (contents[msg_type_len] < ' ' || contents[msg_type_len] > 0x7E) { return {}; } ++msg_type_len; diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp index 523115ee44a6..93b02ea8d708 100644 --- a/src/test/net_tests.cpp +++ b/src/test/net_tests.cpp @@ -1400,9 +1400,14 @@ BOOST_AUTO_TEST_CASE(v2transport_test) tester.SendMessage(uint8_t(4), msg_data_1); // cmpctblock short id tester.SendMessage(0, {}); // Invalidly encoded message tester.SendMessage("tx", msg_data_2); // 12-character encoded message type + // Message type containing 0x7F (DEL): outside the printable-ASCII range + // accepted by the v1 transport, so must be rejected here as well. + tester.SendMessage(std::string{"t\x7f"}, msg_data_2); + // Message type containing 0x7E ('~'): at the boundary, still valid. + tester.SendMessage("t~", msg_data_2); ret = tester.Interact(); BOOST_REQUIRE(ret); - BOOST_REQUIRE(ret->size() == 3); + BOOST_REQUIRE(ret->size() == 5); BOOST_REQUIRE((*ret)[0]); BOOST_CHECK((*ret)[0]->m_type == "cmpctblock"); BOOST_CHECK(std::ranges::equal((*ret)[0]->m_recv, MakeByteSpan(msg_data_1))); @@ -1410,6 +1415,10 @@ BOOST_AUTO_TEST_CASE(v2transport_test) BOOST_REQUIRE((*ret)[2]); BOOST_CHECK((*ret)[2]->m_type == "tx"); BOOST_CHECK(std::ranges::equal((*ret)[2]->m_recv, MakeByteSpan(msg_data_2))); + BOOST_CHECK(!(*ret)[3]); + BOOST_REQUIRE((*ret)[4]); + BOOST_CHECK((*ret)[4]->m_type == "t~"); + BOOST_CHECK(std::ranges::equal((*ret)[4]->m_recv, MakeByteSpan(msg_data_2))); // Then send a message with a bit error, expecting failure. It's possible this failure does // not occur immediately (when the length descriptor was modified), but it should come From 94cb6594573876230840f0c8a563896c8700921b Mon Sep 17 00:00:00 2001 From: Hennadii Stepanov <32963518+hebasto@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:49:04 +0100 Subject: [PATCH 041/292] depends: Update `boost` package to 1.92.0 From the Boost.MultiIndex library Release Notes: > Fixed a performance issue with hashed indices when rehashing at very large container sizes. --- depends/packages/boost.mk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/packages/boost.mk b/depends/packages/boost.mk index 81d5ef5f9584..902c018ff1b2 100644 --- a/depends/packages/boost.mk +++ b/depends/packages/boost.mk @@ -1,8 +1,8 @@ package=boost -$(package)_version = 1.91.0-1 +$(package)_version = 1.92.0 $(package)_download_path = https://github.com/boostorg/boost/releases/download/boost-$($(package)_version) $(package)_file_name = boost-$($(package)_version)-cmake.tar.gz -$(package)_sha256_hash = 8a82bd11a720c70923806c36ee5c26dbd2d630c1eaa1d8fad9a7bd5529908a26 +$(package)_sha256_hash = f51707c27359a0df0cac1beada86de31bb5eed5e8285592dadec384df99c2984 $(package)_build_subdir = build define $(package)_set_vars From 4200f8163a5560e3ba9511adf2332fc564ab1e06 Mon Sep 17 00:00:00 2001 From: joaonevess Date: Sat, 16 May 2026 00:13:00 -0700 Subject: [PATCH 042/292] policy: fix negative CFeeRate::ToString() formatting --- src/policy/feerate.cpp | 12 ++++++++++-- src/test/amount_tests.cpp | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/policy/feerate.cpp b/src/policy/feerate.cpp index f62835acbcf2..7cc07099daa2 100644 --- a/src/policy/feerate.cpp +++ b/src/policy/feerate.cpp @@ -7,6 +7,7 @@ #include #include +#include CFeeRate::CFeeRate(const CAmount& nFeePaid, int32_t virtual_bytes) { @@ -29,9 +30,16 @@ CAmount CFeeRate::GetFee(int32_t virtual_bytes) const std::string CFeeRate::ToString(FeeRateFormat fee_rate_format) const { const CAmount feerate_per_kvb{GetFeePerK()}; + const auto format_feerate = [](const CAmount fee_rate, const CAmount divisor, const int decimals, const std::string& currency_unit, const std::string& size_unit) { + Assert(divisor > 0); + const char* sign{fee_rate < 0 ? "-" : ""}; + const CAmount quotient{std::abs(fee_rate / divisor)}; + const CAmount remainder{std::abs(fee_rate % divisor)}; + return strprintf("%s%d.%0*d %s/%s", sign, quotient, decimals, remainder, currency_unit, size_unit); + }; switch (fee_rate_format) { - case FeeRateFormat::BTC_KVB: return strprintf("%d.%08d %s/kvB", feerate_per_kvb / COIN, feerate_per_kvb % COIN, CURRENCY_UNIT); - case FeeRateFormat::SAT_VB: return strprintf("%d.%03d %s/vB", feerate_per_kvb / 1000, feerate_per_kvb % 1000, CURRENCY_ATOM); + case FeeRateFormat::BTC_KVB: return format_feerate(feerate_per_kvb, COIN, /*decimals=*/8, CURRENCY_UNIT, "kvB"); + case FeeRateFormat::SAT_VB: return format_feerate(feerate_per_kvb, 1000, /*decimals=*/3, CURRENCY_ATOM, "vB"); } // no default case, so the compiler can warn about missing cases assert(false); } diff --git a/src/test/amount_tests.cpp b/src/test/amount_tests.cpp index e1630b41948d..322e048f9cbe 100644 --- a/src/test/amount_tests.cpp +++ b/src/test/amount_tests.cpp @@ -140,6 +140,24 @@ BOOST_AUTO_TEST_CASE(ToStringTest) BOOST_CHECK_EQUAL(feeRate.ToString(), "0.00000001 BTC/kvB"); BOOST_CHECK_EQUAL(feeRate.ToString(FeeRateFormat::BTC_KVB), "0.00000001 BTC/kvB"); BOOST_CHECK_EQUAL(feeRate.ToString(FeeRateFormat::SAT_VB), "0.001 sat/vB"); + + feeRate = CFeeRate(0); + BOOST_CHECK_EQUAL(feeRate.ToString(), "0.00000000 BTC/kvB"); + BOOST_CHECK_EQUAL(feeRate.ToString(FeeRateFormat::BTC_KVB), "0.00000000 BTC/kvB"); + BOOST_CHECK_EQUAL(feeRate.ToString(FeeRateFormat::SAT_VB), "0.000 sat/vB"); + + feeRate = CFeeRate(-1); + BOOST_CHECK_EQUAL(feeRate.ToString(), "-0.00000001 BTC/kvB"); + BOOST_CHECK_EQUAL(feeRate.ToString(FeeRateFormat::BTC_KVB), "-0.00000001 BTC/kvB"); + BOOST_CHECK_EQUAL(feeRate.ToString(FeeRateFormat::SAT_VB), "-0.001 sat/vB"); + + feeRate = CFeeRate(-1000); + BOOST_CHECK_EQUAL(feeRate.ToString(), "-0.00001000 BTC/kvB"); + BOOST_CHECK_EQUAL(feeRate.ToString(FeeRateFormat::SAT_VB), "-1.000 sat/vB"); + + feeRate = CFeeRate(-COIN - 1); + BOOST_CHECK_EQUAL(feeRate.ToString(), "-1.00000001 BTC/kvB"); + BOOST_CHECK_EQUAL(feeRate.ToString(FeeRateFormat::SAT_VB), "-100000.001 sat/vB"); } BOOST_AUTO_TEST_SUITE_END() From d29b22d07897058eb69c862ecea8b00325229de2 Mon Sep 17 00:00:00 2001 From: Hao Xu Date: Sun, 26 Jul 2026 19:05:17 +0800 Subject: [PATCH 043/292] fuzz: reset connman state in p2p targets Clear ConnmanTestMsg state at the start of each input in cmpctblock and p2p_handshake so sticky CConnman state does not leak between fuzz inputs. --- src/test/fuzz/cmpctblock.cpp | 3 ++- src/test/fuzz/p2p_handshake.cpp | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/fuzz/cmpctblock.cpp b/src/test/fuzz/cmpctblock.cpp index e2aea86a89e8..b5dc4fee5b9a 100644 --- a/src/test/fuzz/cmpctblock.cpp +++ b/src/test/fuzz/cmpctblock.cpp @@ -167,6 +167,8 @@ FUZZ_TARGET(cmpctblock, .init = initialize_cmpctblock) FakeSteadyClock steady_clock; auto setup = g_setup; + auto& connman = *static_cast(setup->m_node.connman.get()); + connman.Reset(); auto& mempool = *setup->m_node.mempool; auto& chainman = static_cast(*setup->m_node.chainman); chainman.ResetIbd(); @@ -174,7 +176,6 @@ FUZZ_TARGET(cmpctblock, .init = initialize_cmpctblock) const size_t initial_index_size{WITH_LOCK(chainman.GetMutex(), return chainman.BlockIndex().size())}; AddrMan addrman{*setup->m_node.netgroupman, /*deterministic=*/true, /*consistency_check_ratio=*/0}; - auto& connman = *static_cast(setup->m_node.connman.get()); auto peerman = PeerManager::make(connman, addrman, /*banman=*/nullptr, chainman, mempool, *setup->m_node.warnings, diff --git a/src/test/fuzz/p2p_handshake.cpp b/src/test/fuzz/p2p_handshake.cpp index a71a61b85c96..05a86a01629f 100644 --- a/src/test/fuzz/p2p_handshake.cpp +++ b/src/test/fuzz/p2p_handshake.cpp @@ -40,6 +40,7 @@ FUZZ_TARGET(p2p_handshake, .init = ::initialize) auto& node{g_setup->m_node}; auto& connman{static_cast(*node.connman)}; + connman.Reset(); auto& chainman{static_cast(*node.chainman)}; FakeNodeClock clock{1610000000s}; // any time to successfully reset ibd FakeSteadyClock steady_clock; From e014e5bb61c117f0230a092439e4bbc98cd383f5 Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Mon, 3 Aug 2026 01:02:10 +0200 Subject: [PATCH 044/292] miner: Enforce murch-zawy rule (BIP54) --- src/node/miner.cpp | 7 +++++++ test/functional/mining_basic.py | 37 +++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/node/miner.cpp b/src/node/miner.cpp index 06611a4a2f42..e9d3a3035296 100644 --- a/src/node/miner.cpp +++ b/src/node/miner.cpp @@ -64,6 +64,13 @@ int64_t GetMinimumTime(const CBlockIndex* pindexPrev, const int64_t difficulty_a if (height % difficulty_adjustment_interval == 0) { min_time = std::max(min_time, pindexPrev->GetBlockTime() - MAX_TIMEWARP); } + // Account for the BIP54 Murch-Zawy rule on all networks: the last block of + // a difficulty adjustment period may not be earlier than its first block. + if (height % difficulty_adjustment_interval == difficulty_adjustment_interval - 1) { + const int first_height{height - static_cast(difficulty_adjustment_interval) + 1}; + const CBlockIndex* first_block{Assert(pindexPrev->GetAncestor(first_height))}; + min_time = std::max(min_time, first_block->GetBlockTime()); + } return min_time; } diff --git a/test/functional/mining_basic.py b/test/functional/mining_basic.py index 7060a3cba62b..d1e5d61a7982 100755 --- a/test/functional/mining_basic.py +++ b/test/functional/mining_basic.py @@ -14,6 +14,7 @@ from decimal import Decimal from test_framework.blocktools import ( + create_block, create_coinbase, get_witness_script, NORMAL_GBT_REQUEST_PARAMS, @@ -252,6 +253,41 @@ def test_timewarp(self): bad_block.solve() node.submitheader(hexdata=CBlockHeader(bad_block).serialize().hex()) + def test_murch_zawy_mintime(self): + self.log.info("Test that GetMinimumTime accounts for the Murch-Zawy rule (BIP54)") + node = self.nodes[0] + + self.log.info("Mine the first block of a retarget period two hours in the future") + blockchain_info = node.getblockchaininfo() + n = DIFFICULTY_ADJUSTMENT_INTERVAL - blockchain_info['blocks'] % DIFFICULTY_ADJUSTMENT_INTERVAL - 1 + t = blockchain_info['time'] + for _ in range(n): + t += 600 + node.setmocktime(t) + self.generate(self.wallet, 1, sync_fun=self.no_op) + node.setmocktime(t + MAX_FUTURE_BLOCK_TIME) + self.generate(self.wallet, 1, sync_fun=self.no_op) + first_block_time = node.getblock(node.getbestblockhash())['time'] + assert_equal(first_block_time, t + MAX_FUTURE_BLOCK_TIME) + + self.log.info("Mine to the end of the period with timestamps held back") + node.setmocktime(t) + self.generate(self.wallet, DIFFICULTY_ADJUSTMENT_INTERVAL - 2, sync_fun=self.no_op) + assert_greater_than(first_block_time, node.getblock(node.getbestblockhash())['time']) + + self.log.info("The template for the last block of the period is adjusted to its first block's time") + tmpl = node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS) + assert_equal(tmpl['mintime'], first_block_time) + assert_equal(tmpl['curtime'], first_block_time) + + block = create_block(tmpl=tmpl) + block.solve() + node.submitheader(hexdata=CBlockHeader(block).serialize().hex()) + + self.log.info("The node mines a valid block at the end of the period despite its early wall clock") + self.generate(self.wallet, 1, sync_fun=self.no_op) + assert_equal(node.getblock(node.getbestblockhash())['time'], first_block_time) + def test_pruning(self): self.log.info("Test that submitblock stores previously pruned block") prune_node = self.nodes[2] @@ -528,6 +564,7 @@ def chain_tip(b_hash, *, status='headers-only', branchlen=1): self.test_blockmintxfee_parameter() self.test_block_max_weight() self.test_timewarp() + self.test_murch_zawy_mintime() self.test_pruning() self.test_height_in_locktime() From 05e7d573184079720e11d933d24b9c9228477a14 Mon Sep 17 00:00:00 2001 From: ViniciusCestarii Date: Tue, 4 Aug 2026 17:26:11 -0300 Subject: [PATCH 045/292] ipc, test: add checkSpawned() unit tests --- src/ipc/test/ipc_tests.cpp | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/ipc/test/ipc_tests.cpp b/src/ipc/test/ipc_tests.cpp index e353a7ee7c17..34dfb74e3d34 100644 --- a/src/ipc/test/ipc_tests.cpp +++ b/src/ipc/test/ipc_tests.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -230,4 +231,44 @@ BOOST_AUTO_TEST_CASE(parse_address_test) check_address("invalid", "invalid", "Unrecognized address 'invalid'"); } +// Test command line parsing in ipc::Process::checkSpawned(). +BOOST_AUTO_TEST_CASE(check_spawned_test) +{ + std::unique_ptr process{ipc::MakeProcess()}; + char arg0[]{"bitcoin-node"}; + char arg_spawn[]{"-ipcchild"}; + char arg_invalid[]{"invalid"}; + char arg_other[]{"-ipcbind=unix"}; + + // no -ipcchild arg. + { + char* argv[]{arg0}; + BOOST_CHECK(!process->checkSpawned(1, argv)); + } + { + char* argv[]{arg0, arg_other}; + BOOST_CHECK(!process->checkSpawned(2, argv)); + } + { + char* argv[]{arg0, arg_other, arg_invalid}; + BOOST_CHECK(!process->checkSpawned(3, argv)); + } + // -ipcchild without a value. + { + char* argv[]{arg0, arg_spawn}; + BOOST_CHECK(!process->checkSpawned(2, argv)); + } + // -ipcchild combined with other arguments. + { + char* argv[]{arg0, arg_spawn, arg_invalid, arg_other}; + BOOST_CHECK(!process->checkSpawned(4, argv)); + } + // -ipcchild with a value that is not a valid way of connecting to the + // parent process. + { + char* argv[]{arg0, arg_spawn, arg_invalid}; + BOOST_CHECK_THROW(process->checkSpawned(3, argv), std::runtime_error); + } +} + BOOST_AUTO_TEST_SUITE_END() From 1883cecb4d05788a02d673b1c7541d93fb9af2c7 Mon Sep 17 00:00:00 2001 From: Hodlinator <172445034+hodlinator@users.noreply.github.com> Date: Wed, 19 Aug 2026 09:41:04 +0200 Subject: [PATCH 046/292] test: Characterize lagging-clock headers presync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The node currently continues low-work headers presync and requests more headers when its clock is more than `MAX_FUTURE_BLOCK_TIME` behind the chain-start MTP. Record this behavior before the follow-up rejects the invalid elapsed-time calculation. The unit test covers HeadersSyncState() behavior while the functional test covers net_processing.cpp behavior. Co-authored-by: Lőrinc --- src/test/headers_sync_chainwork_tests.cpp | 13 +++++++++++++ .../p2p_headers_sync_with_minchainwork.py | 8 ++++++++ 2 files changed, 21 insertions(+) diff --git a/src/test/headers_sync_chainwork_tests.cpp b/src/test/headers_sync_chainwork_tests.cpp index bba612f8b46b..e18b6f4a027d 100644 --- a/src/test/headers_sync_chainwork_tests.cpp +++ b/src/test/headers_sync_chainwork_tests.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -252,4 +253,16 @@ BOOST_AUTO_TEST_CASE(too_little_work) /*exp_locator_hash=*/std::nullopt); } +BOOST_AUTO_TEST_CASE(system_clock_lagging_behind_chain_start) +{ + FakeNodeClock clock{(chain_start.GetBlockTime() - MAX_FUTURE_BLOCK_TIME) * 1s}; + BOOST_CHECK_NO_THROW(CreateState()); + + clock -= 1s; + // TODO: Fix - Being more than MAX_FUTURE_BLOCK_TIME behind the starting + // block leads HeadersSyncState() to compute a negative max_seconds_since_start + // which leads to very high HeadersSyncState::m_max_commitments. + BOOST_CHECK_NO_THROW(CreateState()); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/functional/p2p_headers_sync_with_minchainwork.py b/test/functional/p2p_headers_sync_with_minchainwork.py index 1dc38faadb2b..cd46a82c2a13 100755 --- a/test/functional/p2p_headers_sync_with_minchainwork.py +++ b/test/functional/p2p_headers_sync_with_minchainwork.py @@ -15,6 +15,7 @@ ) from test_framework.blocktools import ( + MAX_FUTURE_BLOCK_TIME, NORMAL_GBT_REQUEST_PARAMS, create_block, ) @@ -144,6 +145,13 @@ def test_peerinfo_includes_headers_presync_height(self): # getpeerinfo should show a sync in progress assert_equal(node.getpeerinfo()[0]['presynced_headers'], 2000) + self.log.info("Test whether a lagging clock aborts low-work headers sync") + node.disconnect_p2ps() + node.setmocktime(node.getblockheader(node.getblockhash(0))['mediantime'] - MAX_FUTURE_BLOCK_TIME - 1) + p2p = node.add_p2p_connection(P2PInterface()) + p2p.send_without_ping(headers_message) + p2p.wait_for_getheaders(timeout=30, block_hash=hashPrevBlock) # TODO: A negative elapsed interval should trigger fatal shutdown. + def test_large_reorgs_can_succeed(self): self.log.info("Test that a 2000+ block reorg, starting from a point that is more than 2000 blocks before a locator entry, can succeed") From ff3e2e4ebdcef21f1c3a81ae08e737adb7b78a78 Mon Sep 17 00:00:00 2001 From: Hodlinator <172445034+hodlinator@users.noreply.github.com> Date: Wed, 19 Aug 2026 09:41:54 +0200 Subject: [PATCH 047/292] net: Trigger process abort when behind start block MTP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We should not proceed syncing headers from peers when the local system clock is incorrectly set. A node with a system clock set too far back will typically fail early during startup when the chainstate detects the tip to be too far in the future. This means that in practice we don't expect the failure to ever happen in net_processing.cpp. An exception is thrown from HeadersSyncState() in order to only compute the error condition once. An alternative would be to compute it a second time in TryLowWorkHeadersSync() to guard against calling HeadersSyncState(), and have an assert inside HeadersSyncState(). We shut down the process so possible resource leaks due to the exception should not be an issue, although none have been spotted. Throwing an exception also keeps the unit test straightforward. Co-authored-by: Lőrinc --- src/headerssync.cpp | 12 ++++++++++-- src/headerssync.h | 10 +++++++++- src/net_processing.cpp | 15 +++++++++++++-- src/test/headers_sync_chainwork_tests.cpp | 5 +---- .../p2p_headers_sync_with_minchainwork.py | 6 +++++- 5 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/headerssync.cpp b/src/headerssync.cpp index 633ffef53adb..ef605735435c 100644 --- a/src/headerssync.cpp +++ b/src/headerssync.cpp @@ -38,8 +38,16 @@ HeadersSyncState::HeadersSyncState(NodeId id, // exceeds this bound, because it's not possible for a consensus-valid // chain to be longer than this (at the current time -- in the future we // could try again, if necessary, to sync a longer chain). - const auto max_seconds_since_start{(Ticks(NodeClock::now() - NodeSeconds{std::chrono::seconds{chain_start.GetMedianTimePast()}})) - + MAX_FUTURE_BLOCK_TIME}; + const auto now{NodeClock::now()}; + const int64_t max_seconds_since_start{Ticks(now - NodeSeconds{std::chrono::seconds{chain_start.GetMedianTimePast()}}) + + MAX_FUTURE_BLOCK_TIME}; + if (max_seconds_since_start < 0) { + throw SystemClockError{strprintf( + "System clock is more than %d minutes behind chain start MTP (%s vs %s).", + MAX_FUTURE_BLOCK_TIME / 60, + FormatISO8601DateTime(TicksSinceEpoch(now)), + FormatISO8601DateTime(chain_start.GetMedianTimePast()))}; + } m_max_commitments = 6 * max_seconds_since_start / m_params.commitment_period; LogDebug(BCLog::NET, "Initial headers sync started with peer=%d: height=%i, max_commitments=%i, min_work=%s\n", m_id, m_current_height, m_max_commitments, m_minimum_required_work.ToString()); diff --git a/src/headerssync.h b/src/headerssync.h index 6d720874411d..65364bb069f0 100644 --- a/src/headerssync.h +++ b/src/headerssync.h @@ -15,6 +15,7 @@ #include #include +#include #include // A compressed CBlockHeader, which leaves out the prevhash @@ -99,8 +100,13 @@ struct CompressedHeader { * sync (temporary, per-peer storage). */ -class HeadersSyncState { +class HeadersSyncState +{ public: + struct SystemClockError : std::runtime_error { + using std::runtime_error::runtime_error; + }; + ~HeadersSyncState() = default; enum class State { @@ -135,6 +141,8 @@ class HeadersSyncState { * consensus_params: parameters needed for difficulty adjustment validation * chain_start: best known fork point that the peer's headers branch from * minimum_required_work: amount of chain work required to accept the chain + * + * @throws SystemClockError if system clock is too far behind chain_start MTP. */ HeadersSyncState(NodeId id, const Consensus::Params& consensus_params, const HeadersSyncParams& params, const CBlockIndex& chain_start, diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 501b14eaee40..d76f6ffddfba 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -3030,8 +3030,19 @@ bool PeerManagerImpl::TryLowWorkHeadersSync(Peer& peer, CNode& pfrom, const CBlo // of headers is known, some header in this set must be new, so // advancing to the first unknown header would be a small effect. LOCK(peer.m_headers_sync_mutex); - peer.m_headers_sync.reset(new HeadersSyncState(peer.m_id, m_chainparams.GetConsensus(), - m_chainparams.HeadersSync(), chain_start_header, minimum_chain_work)); + try { + peer.m_headers_sync.reset(new HeadersSyncState(peer.m_id, m_chainparams.GetConsensus(), + m_chainparams.HeadersSync(), chain_start_header, minimum_chain_work)); + } catch (const HeadersSyncState::SystemClockError& e) { + // Typically we would expect the chain state loading logic to + // already have verified that the tip of the locally stored + // chain is <= system clock + MAX_FUTURE_BLOCK_TIME. Getting + // here is really unexpected. + const auto msg{strprintf("Failure when attempting to initiate headers sync: %s", e.what())}; + std::cerr << msg << std::endl; + LogError("%s", msg); + std::abort(); + } // Now a HeadersSyncState object for tracking this synchronization // is created, process the headers using it as normal. Failures are diff --git a/src/test/headers_sync_chainwork_tests.cpp b/src/test/headers_sync_chainwork_tests.cpp index e18b6f4a027d..4385dd7d8ad1 100644 --- a/src/test/headers_sync_chainwork_tests.cpp +++ b/src/test/headers_sync_chainwork_tests.cpp @@ -259,10 +259,7 @@ BOOST_AUTO_TEST_CASE(system_clock_lagging_behind_chain_start) BOOST_CHECK_NO_THROW(CreateState()); clock -= 1s; - // TODO: Fix - Being more than MAX_FUTURE_BLOCK_TIME behind the starting - // block leads HeadersSyncState() to compute a negative max_seconds_since_start - // which leads to very high HeadersSyncState::m_max_commitments. - BOOST_CHECK_NO_THROW(CreateState()); + BOOST_CHECK_THROW(CreateState(), HeadersSyncState::SystemClockError); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/functional/p2p_headers_sync_with_minchainwork.py b/test/functional/p2p_headers_sync_with_minchainwork.py index cd46a82c2a13..b1efe09cd9b0 100755 --- a/test/functional/p2p_headers_sync_with_minchainwork.py +++ b/test/functional/p2p_headers_sync_with_minchainwork.py @@ -22,6 +22,7 @@ from test_framework.util import assert_equal +import re import time NODE1_BLOCKS_REQUIRED = 15 @@ -150,7 +151,10 @@ def test_peerinfo_includes_headers_presync_height(self): node.setmocktime(node.getblockheader(node.getblockhash(0))['mediantime'] - MAX_FUTURE_BLOCK_TIME - 1) p2p = node.add_p2p_connection(P2PInterface()) p2p.send_without_ping(headers_message) - p2p.wait_for_getheaders(timeout=30, block_hash=hashPrevBlock) # TODO: A negative elapsed interval should trigger fatal shutdown. + node.wait_until_stopped(expect_error=True, expected_ret_code=[-6, # Unix + 3, # Windows native + 0xC0000409], # Windows cross builds + expected_stderr=re.compile("Failure when attempting to initiate headers sync: System clock")) def test_large_reorgs_can_succeed(self): self.log.info("Test that a 2000+ block reorg, starting from a point that is more than 2000 blocks before a locator entry, can succeed") From e5be0dc35e882b686155e5a484990d8e03286717 Mon Sep 17 00:00:00 2001 From: Hodlinator <172445034+hodlinator@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:27:39 +0200 Subject: [PATCH 048/292] refactor: Make HTTPResponse a struct since all fields are public --- src/httpserver.cpp | 30 +++++++++++++++--------------- src/httpserver.h | 12 ++++-------- src/test/httpserver_tests.cpp | 6 +++--- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index fad1db51becd..f6a849f5b2eb 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -370,11 +370,11 @@ std::string HTTPHeaders::Stringify() const std::string HTTPResponse::StringifyHeaders() const { return strprintf("HTTP/%d.%d %d %s\r\n%s", - m_version.major, - m_version.minor, - m_status, - HTTPStatusReasonString(m_status), - m_headers.Stringify()); + version.major, + version.minor, + status, + HTTPStatusReasonString(status), + headers.Stringify()); } bool HTTPRequest::LoadControlData(LineReader& reader) @@ -539,13 +539,13 @@ void HTTPRequest::WriteReply(HTTPStatusCode status, std::span r HTTPResponse res; // Some response headers are determined in advance and stored in the request - res.m_headers = std::move(m_response_headers); + res.headers = std::move(m_response_headers); // Response version matches request version - res.m_version = m_version; + res.version = m_version; // Add response code - res.m_status = status; + res.status = status; // See libevent evhttp_response_needs_body() // Response headers are different if no body is needed @@ -561,7 +561,7 @@ void HTTPRequest::WriteReply(HTTPStatusCode status, std::span r if (m_version.minor == 0) { auto connection_header{m_headers.FindFirst("Connection")}; if (connection_header && ToLower(connection_header.value()) == "keep-alive") { - res.m_headers.Write("Connection", "keep-alive"); + res.headers.Write("Connection", "keep-alive"); keep_alive = true; // HTTP/1.0 connections are closed by default so EOF is sufficient // to indicate end of the body. Adding Content-Length a special case. @@ -572,7 +572,7 @@ void HTTPRequest::WriteReply(HTTPStatusCode status, std::span r // HTTP/1.1 if (m_version.minor >= 1) { const int64_t now_seconds{TicksSinceEpoch(NodeClock::now())}; - res.m_headers.Write("Date", FormatRFC1123DateTime(now_seconds)); + res.headers.Write("Date", FormatRFC1123DateTime(now_seconds)); // HTTP/1.1 connections are kept alive by default and always require Content-Length. if (needs_body) needs_content_length = true; @@ -583,20 +583,20 @@ void HTTPRequest::WriteReply(HTTPStatusCode status, std::span r } if (needs_content_length) { - res.m_headers.Write("Content-Length", util::ToString(reply_body.size())); + res.headers.Write("Content-Length", util::ToString(reply_body.size())); } - if (needs_body && !res.m_headers.FindFirst("Content-Type")) { + if (needs_body && !res.headers.FindFirst("Content-Type")) { // Default type from libevent evhttp_new_object() - res.m_headers.Write("Content-Type", "text/html; charset=ISO-8859-1"); + res.headers.Write("Content-Type", "text/html; charset=ISO-8859-1"); } auto connection_header{m_headers.FindFirst("Connection")}; if (connection_header && ToLower(connection_header.value()) == "close") { // Might not exist already but we need to replace it, not append to it - res.m_headers.RemoveAll("Connection"); + res.headers.RemoveAll("Connection"); - res.m_headers.Write("Connection", "close"); + res.headers.Write("Connection", "close"); keep_alive = false; } diff --git a/src/httpserver.h b/src/httpserver.h index a0e3c2dcbf63..4e1d1a9a716a 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -137,14 +137,10 @@ struct HTTPVersion { /// @} }; - -class HTTPResponse -{ -public: - HTTPVersion m_version; - - HTTPStatusCode m_status{HTTP_INTERNAL_SERVER_ERROR}; - HTTPHeaders m_headers; +struct HTTPResponse { + HTTPVersion version; + HTTPStatusCode status{HTTP_INTERNAL_SERVER_ERROR}; + HTTPHeaders headers; std::string StringifyHeaders() const; }; diff --git a/src/test/httpserver_tests.cpp b/src/test/httpserver_tests.cpp index 96d6c387676a..2b0d172e1ab7 100644 --- a/src/test/httpserver_tests.cpp +++ b/src/test/httpserver_tests.cpp @@ -188,9 +188,9 @@ BOOST_AUTO_TEST_CASE(http_response_tests) // Response points to headers which already exist because some of them // are set before we even know what the response will be. HTTPResponse res; - res.m_version = {.major = 1, .minor = 1}; - res.m_status = HTTP_OK; - res.m_headers = std::move(headers); + res.version = {.major = 1, .minor = 1}; + res.status = HTTP_OK; + res.headers = std::move(headers); BOOST_CHECK_EQUAL( res.StringifyHeaders(), "HTTP/1.1 200 OK\r\n" From 558e26e66e0256d57f26d4c511dfb41c12efd698 Mon Sep 17 00:00:00 2001 From: ViniciusCestarii Date: Thu, 20 Aug 2026 10:18:19 -0300 Subject: [PATCH 049/292] test: cover OP_SUCCESSx bypassing the initial stack element size limit --- test/functional/feature_taproot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/functional/feature_taproot.py b/test/functional/feature_taproot.py index a0c11fd6b811..188d2011424b 100755 --- a/test/functional/feature_taproot.py +++ b/test/functional/feature_taproot.py @@ -1206,6 +1206,7 @@ def hashtype(_): add_spender(spenders, "opsuccess/bigpush", standard=False, tap=tap, leaf="bigpush_success", failure={"leaf": "bigpush_nop"}, **ERR_PUSH_SIZE) add_spender(spenders, "opsuccess/1001push", standard=False, tap=tap, leaf="1001push_success", failure={"leaf": "1001push_nop"}, **ERR_STACK_SIZE) add_spender(spenders, "opsuccess/1001inputs", standard=False, tap=tap, leaf="bare_success", inputs=[b'']*1001, failure={"leaf": "bare_nop"}, **ERR_STACK_SIZE) + add_spender(spenders, "opsuccess/bigstackelem", standard=False, tap=tap, leaf="bare_success", inputs=[random.randbytes(MAX_SCRIPT_ELEMENT_SIZE+1)], failure={"leaf": "bare_nop"}, **ERR_PUSH_SIZE) # Non-OP_SUCCESSx (verify that those aren't accidentally treated as OP_SUCCESSx) for opval in range(0, 0x100): From 747cff842481153357199bf9a81b5a4d82ea91fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Wed, 19 Aug 2026 22:05:42 -0700 Subject: [PATCH 050/292] rpc: avoid quadratic output lookups `ParseOutputs` iterates a `UniValue` object's keys and looks up each value by key. Each lookup scans the key vector from the beginning, making the lookup work quadratic. Walk the parallel key and value vectors together to avoid repeated scans. This preserves output order and validation behavior. --- src/rpc/rawtransaction_util.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/rpc/rawtransaction_util.cpp b/src/rpc/rawtransaction_util.cpp index c90ead5ac554..1c98de577914 100644 --- a/src/rpc/rawtransaction_util.cpp +++ b/src/rpc/rawtransaction_util.cpp @@ -105,19 +105,23 @@ std::vector> ParseOutputs(const UniValue& out std::set destinations; std::vector> parsed_outputs; bool has_data{false}; - for (const std::string& name_ : outputs.getKeys()) { + const auto& keys{outputs.getKeys()}; + const auto& values{outputs.getValues()}; + for (size_t i{0}; i < keys.size(); ++i) { + const auto& name_{keys[i]}; + const auto& value{values[i]}; if (name_ == "data") { if (has_data) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, duplicate key: data"); } has_data = true; - std::vector data = ParseHexV(outputs[name_].getValStr(), "Data"); + std::vector data = ParseHexV(value.getValStr(), "Data"); CTxDestination destination{CNoDestination{CScript() << OP_RETURN << data}}; CAmount amount{0}; parsed_outputs.emplace_back(destination, amount); } else { CTxDestination destination{DecodeDestination(name_)}; - CAmount amount{AmountFromValue(outputs[name_])}; + CAmount amount{AmountFromValue(value)}; if (!IsValidDestination(destination)) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid Bitcoin address: ") + name_); } From 3bb87bc61b489911cf36e50f74ac8b6ee201b82b Mon Sep 17 00:00:00 2001 From: ViniciusCestarii Date: Thu, 20 Aug 2026 15:58:04 -0300 Subject: [PATCH 051/292] test: cover OP_FROMALTSTACK must pop the altstack https://bitcoincore.space/src/script/interpreter.cpp#3951 --- src/test/data/script_tests.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/data/script_tests.json b/src/test/data/script_tests.json index b88c64154728..d29c152b60a9 100644 --- a/src/test/data/script_tests.json +++ b/src/test/data/script_tests.json @@ -1042,6 +1042,7 @@ ["NOP", "TOALTSTACK 1", "P2SH,STRICTENC", "INVALID_STACK_OPERATION", "but, hey, more is always better, right?"], ["1", "FROMALTSTACK", "P2SH,STRICTENC", "INVALID_ALTSTACK_OPERATION"], +["1", "2 TOALTSTACK FROMALTSTACK FROMALTSTACK", "P2SH,STRICTENC", "INVALID_ALTSTACK_OPERATION", "FROMALTSTACK must pop the altstack"], ["1", "2DROP 1", "P2SH,STRICTENC", "INVALID_STACK_OPERATION"], ["1", "2DUP", "P2SH,STRICTENC", "INVALID_STACK_OPERATION"], ["1 1", "3DUP", "P2SH,STRICTENC", "INVALID_STACK_OPERATION"], From 331bf798819ca4f2815450ba50d5920e2d2c2aa6 Mon Sep 17 00:00:00 2001 From: ViniciusCestarii Date: Thu, 20 Aug 2026 19:06:44 -0300 Subject: [PATCH 052/292] test: cover OP_WITHIN must pop all 3 elements https://bitcoincore.space/src/script/interpreter.cpp#4084 --- src/test/data/script_tests.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/data/script_tests.json b/src/test/data/script_tests.json index d29c152b60a9..ea0ed8d7cbf4 100644 --- a/src/test/data/script_tests.json +++ b/src/test/data/script_tests.json @@ -221,6 +221,7 @@ ["11 -100 100", "WITHIN", "P2SH,STRICTENC", "OK"], ["-2147483647 -100 100", "WITHIN NOT", "P2SH,STRICTENC", "OK"], ["2147483647 -100 100", "WITHIN NOT", "P2SH,STRICTENC", "OK"], +["0 0 1", "WITHIN DEPTH 1 EQUALVERIFY", "P2SH,STRICTENC", "OK", "WITHIN must pop all 3 elements"], ["2147483647 2147483647 SUB", "0 EQUAL", "P2SH,STRICTENC", "OK"], ["2147483647 DUP ADD", "4294967294 EQUAL", "P2SH,STRICTENC", "OK", ">32 bit EQUAL is valid"], From 86c7fb910d674c757c4829cd006b3732d3a5e78b Mon Sep 17 00:00:00 2001 From: ViniciusCestarii Date: Thu, 20 Aug 2026 19:47:32 -0300 Subject: [PATCH 053/292] test: cover OP_16 does not count towards the opcode limit https://bitcoincore.space/src/script/interpreter.cpp#3883 --- src/test/data/script_tests.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/data/script_tests.json b/src/test/data/script_tests.json index ea0ed8d7cbf4..1b0d7c6906db 100644 --- a/src/test/data/script_tests.json +++ b/src/test/data/script_tests.json @@ -333,6 +333,10 @@ "0x616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161", "P2SH,STRICTENC", "OK", "201 opcodes executed. 0x61 is NOP"], +["1", +"0x60606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060", +"P2SH,STRICTENC", "OK", +"202 OP_16 do not count towards the 201 opcode limit."], ["1 2 3 4 5 0x6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f", "1 2 3 4 5 0x6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f", "P2SH,STRICTENC", "OK", From 4a12773f269742d2c655beb1b3f5ffe98e9beadb Mon Sep 17 00:00:00 2001 From: ViniciusCestarii Date: Fri, 21 Aug 2026 11:17:25 -0300 Subject: [PATCH 054/292] test: cover DERSIG rejects a non-compound signature type https://bitcoincore.space/src/script/interpreter.cpp#3808 --- src/test/data/script_tests.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/data/script_tests.json b/src/test/data/script_tests.json index 1b0d7c6906db..2eb02c40ca81 100644 --- a/src/test/data/script_tests.json +++ b/src/test/data/script_tests.json @@ -1292,6 +1292,7 @@ ["0x17 0x3014020002107777777777777777777777777777777701", "0 CHECKSIG NOT", "DERSIG", "SIG_DER", "Zero-length R is incorrectly encoded for DERSIG"], ["0x17 0x3014021077777777777777777777777777777777020001", "0 CHECKSIG NOT", "DERSIG", "SIG_DER", "Zero-length S is incorrectly encoded for DERSIG"], ["0x27 0x302402107777777777777777777777777777777702108777777777777777777777777777777701", "0 CHECKSIG NOT", "DERSIG", "SIG_DER", "Negative S is incorrectly encoded for DERSIG"], +["0x27 0x312402107777777777777777777777777777777702107777777777777777777777777777777701", "0 CHECKSIG NOT", "DERSIG", "SIG_DER", "Non-compound type is incorrectly encoded for DERSIG"], ["Some basic segwit checks"], [["00", 0.00000000 ], "", "0 0x206e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d", "P2SH,WITNESS", "EVAL_FALSE", "Invalid witness script"], From d564b0255f7ec984b1c788910a113a2533dd4d6a Mon Sep 17 00:00:00 2001 From: Hennadii Stepanov <32963518+hebasto@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:13:12 +0100 Subject: [PATCH 055/292] iwyu: Add temporary mapping to work around upstream bug --- contrib/devtools/iwyu/bitcoin.core.imp | 4 ++++ src/script/signingprovider.cpp | 1 + src/script/signingprovider.h | 1 - 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/contrib/devtools/iwyu/bitcoin.core.imp b/contrib/devtools/iwyu/bitcoin.core.imp index 9b471cb82f23..ca7e27508962 100644 --- a/contrib/devtools/iwyu/bitcoin.core.imp +++ b/contrib/devtools/iwyu/bitcoin.core.imp @@ -5,4 +5,8 @@ { "include": [ "", "private", "", "public" ] }, { "include": [ "", "private", "", "public" ] }, { "include": [ "", "private", "", "public" ] }, + + # Workaround for IWYU issue. + # See: https://github.com/include-what-you-use/include-what-you-use/issues/2084. + { "symbol": ["std::tuple", "private", "", "public"] }, ] diff --git a/src/script/signingprovider.cpp b/src/script/signingprovider.cpp index 2b5d6e2f696a..6e2e6c9c6e57 100644 --- a/src/script/signingprovider.cpp +++ b/src/script/signingprovider.cpp @@ -13,6 +13,7 @@ #include #include +#include const SigningProvider& DUMMY_SIGNING_PROVIDER = SigningProvider(); diff --git a/src/script/signingprovider.h b/src/script/signingprovider.h index ef9141a1cc65..cf2ad5a8e85a 100644 --- a/src/script/signingprovider.h +++ b/src/script/signingprovider.h @@ -25,7 +25,6 @@ #include #include #include -#include #include class MuSig2SecNonce; From b3ff9c4d683fdcd0530b7c76c408b4a6e9e0830e Mon Sep 17 00:00:00 2001 From: Hennadii Stepanov <32963518+hebasto@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:13:42 +0100 Subject: [PATCH 056/292] iwyu: Fix warnings in `src/interfaces` and treat them as errors --- ci/test/03_test_script.sh | 1 + src/interfaces/chain.h | 8 +++----- src/interfaces/node.h | 11 +++-------- src/interfaces/wallet.h | 23 ++++++++++------------- 4 files changed, 17 insertions(+), 26 deletions(-) diff --git a/ci/test/03_test_script.sh b/ci/test/03_test_script.sh index f89ba657156f..cffd87af9c73 100755 --- a/ci/test/03_test_script.sh +++ b/ci/test/03_test_script.sh @@ -249,6 +249,7 @@ if [[ "${RUN_IWYU}" == true ]]; then -Xiwyu --max_line_length=160 \ -Xiwyu --check_also='*/common/types\.h' \ -Xiwyu --check_also='*/consensus/*\.h' \ + -Xiwyu --check_also='*/interfaces/*\.h' \ -Xiwyu --check_also='*/primitives/transaction_identifier\.h' \ 2>&1 || true } | tee /tmp/iwyu_ci.out diff --git a/src/interfaces/chain.h b/src/interfaces/chain.h index 4b63d885a977..d713ee7c0a84 100644 --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -7,12 +7,13 @@ #include #include +#include #include // IWYU pragma: export -#include #include #include #include #include +#include #include #include @@ -23,9 +24,7 @@ #include #include -class ArgsManager; class CBlock; -class CBlockUndo; class CFeeRate; class CRPCCommand; class CScheduler; @@ -40,12 +39,11 @@ struct ChainstateRole; } // namespace kernel namespace node { struct NodeContext; +enum class TxBroadcast : uint8_t; } // namespace node namespace interfaces { - class Handler; -class Wallet; //! Helper for findBlock to selectively return pieces of block data. If block is //! found, data will be returned by setting specified output variables. If block diff --git a/src/interfaces/node.h b/src/interfaces/node.h index 8b8402ece163..260d9557e17f 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -11,35 +11,30 @@ #include #include #include -#include +#include +#include #include #include #include #include #include +#include #include #include #include #include #include -class BanMan; class CFeeRate; -class CNodeStats; class Coin; class UniValue; -class Proxy; enum class SynchronizationState; struct CNodeStateStats; -struct bilingual_str; namespace node { enum class TransactionError; struct NodeContext; } // namespace node -namespace wallet { -class CCoinControl; -} // namespace wallet namespace interfaces { class Handler; diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index d34fc62aca8b..9d3c7ff47ba6 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -10,37 +10,34 @@ #include #include #include -#include -#include -#include