From 118abf4c305c01e6359a8588327a0b011ca52944 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Sat, 20 Sep 2025 21:32:41 +0200 Subject: [PATCH 1/7] test: add block 2016 to mock mainnet The next commit requires an additional mainnet block which changes the difficulty. Also fix a few minor mistakes in the test (suite): - rename the create_coinbase retarger_period argument to halving_period. Before bitcoin#31583 this was hardcoded for regtest where these values are the same. - drop unused fees argument from mine helper Finally the CPU miner instructions for generating the alternative mainnet chain are expanded. Github-Pull: #33446 Rebased-From: 4c3c1f42cf705e039751395799240da33ca969bd --- test/functional/data/README.md | 14 +++++++++++--- test/functional/data/mainnet_alt.json | 6 ++++-- test/functional/mining_mainnet.py | 19 +++++++++++++------ test/functional/test_framework/blocktools.py | 4 ++-- 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/test/functional/data/README.md b/test/functional/data/README.md index bb03422f95f..956394e385c 100644 --- a/test/functional/data/README.md +++ b/test/functional/data/README.md @@ -11,9 +11,10 @@ The alternate mainnet chain was generated as follows: - restart node with a faketime 2 minutes later ```sh -for i in {1..2015} +for i in {1..2016} do - faketime "`date -d @"$(( 1231006505 + $i * 120 ))" +'%Y-%m-%d %H:%M:%S'`" \ + t=$(( 1231006505 + $i * 120 )) + faketime "`date -d @$t +'%Y-%m-%d %H:%M:%S'`" \ bitcoind -connect=0 -nocheckpoints -stopatheight=$i done ``` @@ -21,7 +22,9 @@ done The CPU miner is kept running as follows: ```sh -./minerd --coinbase-addr 1NQpH6Nf8QtR2HphLRcvuVqfhXBXsiWn8r --no-stratum --algo sha256d --no-longpoll --scantime 3 --retry-pause 1 +./minerd -u ... -p ... -o http://127.0.0.1:8332 --no-stratum \ + --coinbase-addr 1NQpH6Nf8QtR2HphLRcvuVqfhXBXsiWn8r \ + --algo sha256d --no-longpoll --scantime 3 --retry-pause 1 ``` The payout address is derived from first BIP32 test vector master key: @@ -40,3 +43,8 @@ The timestamp was not kept constant because at difficulty 1 it's not sufficient to only grind the nonce. Grinding the extra_nonce or version field instead would have required additional (stratum) software. It would also make it more complicated to reconstruct the blocks in this test. + +The `getblocktemplate` RPC code needs to be patched to ignore not being connected +to any peers, and to ignore the IBD status check. + +On macOS use `faketime "@$t"` instead. diff --git a/test/functional/data/mainnet_alt.json b/test/functional/data/mainnet_alt.json index a4a072d2c5b..96821a36f41 100644 --- a/test/functional/data/mainnet_alt.json +++ b/test/functional/data/mainnet_alt.json @@ -2014,7 +2014,8 @@ 1231247971, 1231248071, 1231248198, - 1231248322 + 1231248322, + 1231248621 ], "nonces": [ 2345621585, @@ -4031,6 +4032,7 @@ 3658502865, 2519048297, 1915965760, - 1183846025 + 1183846025, + 2713372123 ] } diff --git a/test/functional/mining_mainnet.py b/test/functional/mining_mainnet.py index c2757b61574..c58c4784b4d 100755 --- a/test/functional/mining_mainnet.py +++ b/test/functional/mining_mainnet.py @@ -54,15 +54,15 @@ class MiningMainnetTest(BitcoinTestFramework): self.add_wallet_options(parser) - def mine(self, height, prev_hash, blocks, node, fees=0): + def mine(self, height, prev_hash, blocks, node): self.log.debug(f"height={height}") block = CBlock() block.nVersion = 0x20000000 block.hashPrevBlock = int(prev_hash, 16) block.nTime = blocks['timestamps'][height - 1] - block.nBits = DIFF_1_N_BITS + block.nBits = DIFF_1_N_BITS if height < 2016 else DIFF_4_N_BITS block.nNonce = blocks['nonces'][height - 1] - block.vtx = [create_coinbase(height=height, script_pubkey=bytes.fromhex(COINBASE_SCRIPT_PUBKEY), retarget_period=2016)] + block.vtx = [create_coinbase(height=height, script_pubkey=bytes.fromhex(COINBASE_SCRIPT_PUBKEY), halving_period=210000)] block.hashMerkleRoot = block.calc_merkle_root() block.rehash() block_hex = block.serialize(with_witness=False).hex() @@ -81,12 +81,15 @@ class MiningMainnetTest(BitcoinTestFramework): self.log.info("Load alternative mainnet blocks") path = os.path.join(os.path.dirname(os.path.realpath(__file__)), self.options.datafile) prev_hash = node.getbestblockhash() + blocks = None with open(path, encoding='utf-8') as f: blocks = json.load(f) n_blocks = len(blocks['timestamps']) - assert_equal(n_blocks, 2015) - for i in range(2015): - prev_hash = self.mine(i + 1, prev_hash, blocks, node) + assert_equal(n_blocks, 2016) + + # Mine up to the last block of the first retarget period + for i in range(2015): + prev_hash = self.mine(i + 1, prev_hash, blocks, node) assert_equal(node.getblockcount(), 2015) @@ -101,5 +104,9 @@ class MiningMainnetTest(BitcoinTestFramework): assert_equal(mining_info['next']['bits'], nbits_str(DIFF_4_N_BITS)) assert_equal(mining_info['next']['target'], target_str(DIFF_4_TARGET)) + # Mine first block of the second retarget period + height = 2016 + prev_hash = self.mine(height, prev_hash, blocks, node) + assert_equal(node.getblockcount(), height) if __name__ == '__main__': MiningMainnetTest(__file__).main() diff --git a/test/functional/test_framework/blocktools.py b/test/functional/test_framework/blocktools.py index 38600bc005a..49e2518887f 100644 --- a/test/functional/test_framework/blocktools.py +++ b/test/functional/test_framework/blocktools.py @@ -143,7 +143,7 @@ def script_BIP34_coinbase_height(height): return CScript([CScriptNum(height)]) -def create_coinbase(height, pubkey=None, *, script_pubkey=None, extra_output_script=None, fees=0, nValue=50, retarget_period=REGTEST_RETARGET_PERIOD): +def create_coinbase(height, pubkey=None, *, script_pubkey=None, extra_output_script=None, fees=0, nValue=50, halving_period=REGTEST_RETARGET_PERIOD): """Create a coinbase transaction. If pubkey is passed in, the coinbase output will be a P2PK output; @@ -156,7 +156,7 @@ def create_coinbase(height, pubkey=None, *, script_pubkey=None, extra_output_scr coinbaseoutput = CTxOut() coinbaseoutput.nValue = nValue * COIN if nValue == 50: - halvings = int(height / retarget_period) + halvings = int(height / halving_period) coinbaseoutput.nValue >>= halvings coinbaseoutput.nValue += fees if pubkey is not None: From 22ab141243eeb4a929e589ef70a6f54a5aaed3ba Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Sat, 20 Sep 2025 21:33:13 +0200 Subject: [PATCH 2/7] rpc: fix getblock(header) returns target for tip A target field was added to the getblock and getblockheader RPC calls in bitcoin#31583, but it mistakingly always used the tip value. Because regtest does not have difficulty adjustment, a test is added for mainnet instead. Github-Pull: #33446 Rebased-From: bf7996cbc3becf329d8b1cd2f1007fec9b3a3188 --- src/rpc/blockchain.cpp | 2 +- test/functional/mining_mainnet.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 8cbca51ccbf..edda17d3697 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -164,7 +164,7 @@ UniValue blockheaderToJSON(const CBlockIndex& tip, const CBlockIndex& blockindex result.pushKV("mediantime", blockindex.GetMedianTimePast()); result.pushKV("nonce", blockindex.nNonce); result.pushKV("bits", strprintf("%08x", blockindex.nBits)); - result.pushKV("target", GetTarget(tip, pow_limit).GetHex()); + result.pushKV("target", GetTarget(blockindex, pow_limit).GetHex()); result.pushKV("difficulty", GetDifficulty(blockindex)); result.pushKV("chainwork", blockindex.nChainWork.GetHex()); result.pushKV("nTx", blockindex.nTx); diff --git a/test/functional/mining_mainnet.py b/test/functional/mining_mainnet.py index c58c4784b4d..456381af55a 100755 --- a/test/functional/mining_mainnet.py +++ b/test/functional/mining_mainnet.py @@ -108,5 +108,17 @@ class MiningMainnetTest(BitcoinTestFramework): height = 2016 prev_hash = self.mine(height, prev_hash, blocks, node) assert_equal(node.getblockcount(), height) + + mining_info = node.getmininginfo() + assert_equal(mining_info['difficulty'], 4) + + self.log.info("getblock RPC should show historical target") + block_info = node.getblock(node.getblockhash(1)) + + assert_equal(block_info['difficulty'], 1) + assert_equal(block_info['bits'], nbits_str(DIFF_1_N_BITS)) + assert_equal(block_info['target'], target_str(DIFF_1_TARGET)) + + if __name__ == '__main__': MiningMainnetTest(__file__).main() From 9d9baafc6f9357179e57fdcc6cf2ce36d65dd16d Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Fri, 26 Sep 2025 19:25:26 +0200 Subject: [PATCH 3/7] doc: rpc: fix case typo in `finalizepsbt` help (final_scriptwitness) Github-Pull: #33484 Rebased-From: ff05bebcc4262966b117082a67dc4c63a3f67d2d --- src/rpc/rawtransaction.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 421656152cb..77e8fd49e11 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -1494,7 +1494,7 @@ static RPCHelpMan finalizepsbt() return RPCHelpMan{"finalizepsbt", "Finalize the inputs of a PSBT. If the transaction is fully signed, it will produce a\n" "network serialized transaction which can be broadcast with sendrawtransaction. Otherwise a PSBT will be\n" - "created which has the final_scriptSig and final_scriptWitness fields filled for inputs that are complete.\n" + "created which has the final_scriptSig and final_scriptwitness fields filled for inputs that are complete.\n" "Implements the Finalizer and Extractor roles.\n", { {"psbt", RPCArg::Type::STR, RPCArg::Optional::NO, "A base64 string of a PSBT"}, From 6f23ead4a2d97e245f4fc1824b1dd956dc06cc42 Mon Sep 17 00:00:00 2001 From: Greg Sanders Date: Mon, 29 Sep 2025 15:47:07 -0400 Subject: [PATCH 4/7] fuzz: don't bypass_limits for most mempool harnesses Using bypass_limits=true is essentially fuzzing part of a reorg only, and results in TRUC invariants unable to be checked. Remove most instances of bypassing limits, leaving one harness able to do so. Github-Pull: #33504 Rebased-From: bbe8e9063c15dc230553e0cbf16d603f5ad0e4cf --- src/test/fuzz/package_eval.cpp | 2 +- src/test/fuzz/tx_pool.cpp | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/test/fuzz/package_eval.cpp b/src/test/fuzz/package_eval.cpp index 8e3d84a9e63..37b18a59414 100644 --- a/src/test/fuzz/package_eval.cpp +++ b/src/test/fuzz/package_eval.cpp @@ -324,7 +324,7 @@ FUZZ_TARGET(ephemeral_package_eval, .init = initialize_tx_pool) return ProcessNewPackage(chainstate, tx_pool, txs, /*test_accept=*/single_submit, /*client_maxfeerate=*/{})); const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, txs.back(), GetTime(), - /*bypass_limits=*/fuzzed_data_provider.ConsumeBool(), /*test_accept=*/!single_submit)); + /*bypass_limits=*/false, /*test_accept=*/!single_submit)); if (!single_submit && result_package.m_state.GetResult() != PackageValidationResult::PCKG_POLICY) { // We don't know anything about the validity since transactions were randomly generated, so diff --git a/src/test/fuzz/tx_pool.cpp b/src/test/fuzz/tx_pool.cpp index a697ee9d838..98feadf516e 100644 --- a/src/test/fuzz/tx_pool.cpp +++ b/src/test/fuzz/tx_pool.cpp @@ -295,7 +295,6 @@ FUZZ_TARGET(tx_pool_standard, .init = initialize_tx_pool) std::set added; auto txr = std::make_shared(removed, added); node.validation_signals->RegisterSharedValidationInterface(txr); - const bool bypass_limits = fuzzed_data_provider.ConsumeBool(); // Make sure ProcessNewPackage on one transaction works. // The result is not guaranteed to be the same as what is returned by ATMP. @@ -310,7 +309,7 @@ FUZZ_TARGET(tx_pool_standard, .init = initialize_tx_pool) it->second.m_result_type == MempoolAcceptResult::ResultType::INVALID); } - const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, tx, GetTime(), bypass_limits, /*test_accept=*/false)); + const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, tx, GetTime(), /*bypass_limits=*/false, /*test_accept=*/false)); const bool accepted = res.m_result_type == MempoolAcceptResult::ResultType::VALID; node.validation_signals->SyncWithValidationInterfaceQueue(); node.validation_signals->UnregisterSharedValidationInterface(txr); @@ -393,6 +392,9 @@ FUZZ_TARGET(tx_pool, .init = initialize_tx_pool) chainstate.SetMempool(&tx_pool); + // If we ever bypass limits, do not do TRUC invariants checks + bool ever_bypassed_limits{false}; + LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 300) { const auto mut_tx = ConsumeTransaction(fuzzed_data_provider, txids); @@ -411,13 +413,17 @@ FUZZ_TARGET(tx_pool, .init = initialize_tx_pool) tx_pool.PrioritiseTransaction(txid.ToUint256(), delta); } + const bool bypass_limits{fuzzed_data_provider.ConsumeBool()}; + ever_bypassed_limits |= bypass_limits; + const auto tx = MakeTransactionRef(mut_tx); - const bool bypass_limits = fuzzed_data_provider.ConsumeBool(); const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, tx, GetTime(), bypass_limits, /*test_accept=*/false)); const bool accepted = res.m_result_type == MempoolAcceptResult::ResultType::VALID; if (accepted) { txids.push_back(tx->GetHash()); - CheckMempoolTRUCInvariants(tx_pool); + if (!ever_bypassed_limits) { + CheckMempoolTRUCInvariants(tx_pool); + } } } Finish(fuzzed_data_provider, tx_pool, chainstate); From 666aec7d49506c587ecbbcd71f6e8f1e7bb4e4cd Mon Sep 17 00:00:00 2001 From: Greg Sanders Date: Fri, 26 Sep 2025 14:47:47 -0400 Subject: [PATCH 5/7] Mempool: Do not enforce TRUC checks on reorg Not enforcing TRUC topology on reorg was the intended behavior, but the appropriate bypass argument was not checked. This mistake means we could potentially invalidate a long chain of perfectly incentive-compatible transactions that were made historically, including subsequent non-TRUC transactions, all of which may have been very high feerate. Lastly, it wastes CPU cycles doing topology checks since this behavior cannot actually enforce the topology in general for the reorg setting. Github-Pull: #33504 Rebased-From: 26e71c237d9d2197824b547f55ee3a0a60149f92 --- src/validation.cpp | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/validation.cpp b/src/validation.cpp index fde064458dc..85504d1e290 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1025,26 +1025,28 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) // Even though just checking direct mempool parents for inheritance would be sufficient, we // check using the full ancestor set here because it's more convenient to use what we have // already calculated. - if (const auto err{SingleTRUCChecks(ws.m_ptx, ws.m_ancestors, ws.m_conflicts, ws.m_vsize)}) { - // Single transaction contexts only. - if (args.m_allow_sibling_eviction && err->second != nullptr) { - // We should only be considering where replacement is considered valid as well. - Assume(args.m_allow_replacement); + if (!args.m_bypass_limits) { + if (const auto err{SingleTRUCChecks(ws.m_ptx, ws.m_ancestors, ws.m_conflicts, ws.m_vsize)}) { + // Single transaction contexts only. + if (args.m_allow_sibling_eviction && err->second != nullptr) { + // We should only be considering where replacement is considered valid as well. + Assume(args.m_allow_replacement); - // Potential sibling eviction. Add the sibling to our list of mempool conflicts to be - // included in RBF checks. - ws.m_conflicts.insert(err->second->GetHash()); - // Adding the sibling to m_iters_conflicting here means that it doesn't count towards - // RBF Carve Out above. This is correct, since removing to-be-replaced transactions from - // the descendant count is done separately in SingleTRUCChecks for TRUC transactions. - ws.m_iters_conflicting.insert(m_pool.GetIter(err->second->GetHash()).value()); - ws.m_sibling_eviction = true; - // The sibling will be treated as part of the to-be-replaced set in ReplacementChecks. - // Note that we are not checking whether it opts in to replaceability via BIP125 or TRUC - // (which is normally done in PreChecks). However, the only way a TRUC transaction can - // have a non-TRUC and non-BIP125 descendant is due to a reorg. - } else { - return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "TRUC-violation", err->first); + // Potential sibling eviction. Add the sibling to our list of mempool conflicts to be + // included in RBF checks. + ws.m_conflicts.insert(err->second->GetHash()); + // Adding the sibling to m_iters_conflicting here means that it doesn't count towards + // RBF Carve Out above. This is correct, since removing to-be-replaced transactions from + // the descendant count is done separately in SingleTRUCChecks for TRUC transactions. + ws.m_iters_conflicting.insert(m_pool.GetIter(err->second->GetHash()).value()); + ws.m_sibling_eviction = true; + // The sibling will be treated as part of the to-be-replaced set in ReplacementChecks. + // Note that we are not checking whether it opts in to replaceability via BIP125 or TRUC + // (which is normally done in PreChecks). However, the only way a TRUC transaction can + // have a non-TRUC and non-BIP125 descendant is due to a reorg. + } else { + return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "TRUC-violation", err->first); + } } } From a8bb76b61f49e1abd681f21a754f970eef206ced Mon Sep 17 00:00:00 2001 From: Greg Sanders Date: Fri, 26 Sep 2025 14:49:06 -0400 Subject: [PATCH 6/7] test: add more TRUC reorg coverge Github-Pull: #33504 Rebased-From: 06df14ba75be5f48cf9c417424900ace17d1cf4d --- test/functional/mempool_truc.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/test/functional/mempool_truc.py b/test/functional/mempool_truc.py index 8850ba80028..d095033a847 100755 --- a/test/functional/mempool_truc.py +++ b/test/functional/mempool_truc.py @@ -164,23 +164,36 @@ class MempoolTRUC(BitcoinTestFramework): def test_truc_reorg(self): node = self.nodes[0] self.log.info("Test that, during a reorg, TRUC rules are not enforced") - tx_v2_block = self.wallet.send_self_transfer(from_node=node, version=2) - tx_v3_block = self.wallet.send_self_transfer(from_node=node, version=3) - tx_v3_block2 = self.wallet.send_self_transfer(from_node=node, version=3) - self.check_mempool([tx_v3_block["txid"], tx_v2_block["txid"], tx_v3_block2["txid"]]) + self.check_mempool([]) + + # Testing 2<-3 versions allowed + tx_v2_block = self.wallet.create_self_transfer(version=2) + + # Testing 3<-2 versions allowed + tx_v3_block = self.wallet.create_self_transfer(version=3) + + # Testing overly-large child size + tx_v3_block2 = self.wallet.create_self_transfer(version=3) + + # Also create a linear chain of 3 TRUC transactions that will be directly mined, followed by one v2 in-mempool after block is made + tx_chain_1 = self.wallet.create_self_transfer(version=3) + tx_chain_2 = self.wallet.create_self_transfer(utxo_to_spend=tx_chain_1["new_utxo"], version=3) + tx_chain_3 = self.wallet.create_self_transfer(utxo_to_spend=tx_chain_2["new_utxo"], version=3) + + tx_to_mine = [tx_v3_block["hex"], tx_v2_block["hex"], tx_v3_block2["hex"], tx_chain_1["hex"], tx_chain_2["hex"], tx_chain_3["hex"]] + block = self.generateblock(node, output="raw(42)", transactions=tx_to_mine) - block = self.generate(node, 1) self.check_mempool([]) tx_v2_from_v3 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v3_block["new_utxo"], version=2) tx_v3_from_v2 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v2_block["new_utxo"], version=3) tx_v3_child_large = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v3_block2["new_utxo"], target_vsize=1250, version=3) assert_greater_than(node.getmempoolentry(tx_v3_child_large["txid"])["vsize"], TRUC_CHILD_MAX_VSIZE) - self.check_mempool([tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"]]) - node.invalidateblock(block[0]) - self.check_mempool([tx_v3_block["txid"], tx_v2_block["txid"], tx_v3_block2["txid"], tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"]]) - # This is needed because generate() will create the exact same block again. - node.reconsiderblock(block[0]) + tx_chain_4 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_chain_3["new_utxo"], version=2) + self.check_mempool([tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"], tx_chain_4["txid"]]) + # Reorg should have all block transactions re-accepted, ignoring TRUC enforcement + node.invalidateblock(block["hash"]) + self.check_mempool([tx_v3_block["txid"], tx_v2_block["txid"], tx_v3_block2["txid"], tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"], tx_chain_1["txid"], tx_chain_2["txid"], tx_chain_3["txid"], tx_chain_4["txid"]]) @cleanup(extra_args=["-limitdescendantsize=10", "-datacarriersize=40000"]) def test_nondefault_package_limits(self): From 2d7ebd2d913ea63c1a23fefa0a09ee06fb069161 Mon Sep 17 00:00:00 2001 From: fanquake Date: Wed, 24 Sep 2025 10:35:55 -0400 Subject: [PATCH 7/7] doc: update release notes for 29.x --- doc/release-notes.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/doc/release-notes.md b/doc/release-notes.md index 0325d3a3e28..8a79e99ad27 100644 --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -1,6 +1,6 @@ -Bitcoin Core version 29.2rc1 is now available from: +Bitcoin Core version 29.2rc2 is now available from: - + This release includes various bug fixes and performance improvements, as well as updated translations. @@ -43,6 +43,14 @@ Notable changes - #33296 net: check for empty header before calling FillBlock - #33395 net: do not apply whitelist permissions to onion inbounds +### Mempool + +- #33504 mempool: Do not enforce TRUC checks on reorg + +### RPC + +- #33446 rpc: fix getblock(header) returns target for tip + ### CI - #32999 ci: Use APT_LLVM_V in msan task @@ -50,6 +58,10 @@ Notable changes - #33258 ci: use LLVM 21 - #33364 ci: always use tag for LLVM checkout +### Doc + +- #33484 doc: rpc: fix case typo in `finalizepsbt` help + ### Misc - #33310 trace: Workaround GCC bug compiling with old systemtap @@ -67,6 +79,8 @@ Thanks to everyone who directly contributed to this release: - Luke Dashjr - MarcoFalke - Martin Zumsande +- Sebastian Falbesoner +- Sjors Provoost - Vasil Dimov As well as to everyone that helped with translations on