diff --git a/ci/test/01_base_install.sh b/ci/test/01_base_install.sh index 8b10ba10baac..54dafbe8fd40 100755 --- a/ci/test/01_base_install.sh +++ b/ci/test/01_base_install.sh @@ -89,7 +89,10 @@ fi if [[ "${RUN_IWYU}" == true ]]; then ${CI_RETRY_EXE} git clone --depth=1 https://github.com/include-what-you-use/include-what-you-use -b clang_"${IWYU_LLVM_V}" /include-what-you-use - (cd /include-what-you-use && patch -p1 < /ci_container_base/ci/test/01_iwyu.patch) + pushd /include-what-you-use + patch -p1 < /ci_container_base/ci/test/01_iwyu.patch + patch -p1 < /ci_container_base/ci/test/02_iwyu_hash.patch + popd cmake -B /iwyu-build/ -G 'Unix Makefiles' -DCMAKE_PREFIX_PATH=/usr/lib/llvm-"${IWYU_LLVM_V}" -S /include-what-you-use make -C /iwyu-build/ install "$MAKEJOBS" fi diff --git a/ci/test/02_iwyu_hash.patch b/ci/test/02_iwyu_hash.patch new file mode 100644 index 000000000000..12df7a04ed5c --- /dev/null +++ b/ci/test/02_iwyu_hash.patch @@ -0,0 +1,44 @@ +Map std::hash to its providing standard headers. +Backport of https://github.com/include-what-you-use/include-what-you-use/pull/2013 +(commit 52f85e1f4d990f55fc6556d543eb051d79364a16) to the clang_22 release +branch. Drop once the upstream fix lands in the IWYU branch tracked by +ci/test/00_setup_env_native_iwyu.sh. + +--- a/std_symbol_map.inc ++++ b/std_symbol_map.inc +@@ -1054,12 +1054,27 @@ + { "std::has_unique_object_representations_v", kPrivate, "", kPublic }, + { "std::has_virtual_destructor", kPrivate, "", kPublic }, + { "std::has_virtual_destructor_v", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, + { "std::hash>", kPrivate, "", kPublic }, + { "std::hash, :0>>", kPrivate, "", kPublic }, + { "std::hash, :0>>", kPrivate, "", kPublic }, + { "std::hash, :0>>", kPrivate, "", kPublic }, + { "std::hash, :0>>", kPrivate, "", kPublic }, + { "std::hash, :0>>", kPrivate, "", kPublic }, ++{ "std::hash>", kPrivate, "", kPublic }, + { "std::hash>", kPrivate, "", kPublic }, + { "std::hash", kPrivate, "", kPublic }, + { "std::hash", kPrivate, "", kPublic }, +@@ -1069,6 +1084,7 @@ + { "std::hash>", kPrivate, "", kPublic }, + { "std::hash", kPrivate, "", kPublic }, + { "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, + { "std::hash", kPrivate, "", kPublic }, + { "std::hash", kPrivate, "", kPublic }, + { "std::hash", kPrivate, "", kPublic }, diff --git a/ci/test_imagefile b/ci/test_imagefile index 908e9a0f5ab4..dac6b5531564 100644 --- a/ci/test_imagefile +++ b/ci/test_imagefile @@ -16,7 +16,8 @@ ENV BASE_ROOT_DIR=${BASE_ROOT_DIR} # Make retry available in PATH, needed for CI_RETRY_EXE COPY ./ci/retry/retry /usr/bin/retry -COPY ./ci/test/00_setup_env.sh ./${FILE_ENV} ./ci/test/01_base_install.sh ./ci/test/01_iwyu.patch /ci_container_base/ci/test/ +COPY ./ci/test/00_setup_env.sh ./${FILE_ENV} ./ci/test/01_base_install.sh /ci_container_base/ci/test/ +COPY ./ci/test/*.patch /ci_container_base/ci/test/ # Bash is required, so install it when missing RUN sh -c "bash -c 'true' || ( apk update && apk add --no-cache bash )" diff --git a/src/crypto/hmac_sha256.cpp b/src/crypto/hmac_sha256.cpp index a95ef70849b5..0796bbeb3271 100644 --- a/src/crypto/hmac_sha256.cpp +++ b/src/crypto/hmac_sha256.cpp @@ -5,6 +5,7 @@ #include #include +#include #include @@ -26,6 +27,8 @@ CHMAC_SHA256::CHMAC_SHA256(const unsigned char* key, size_t keylen) for (int n = 0; n < 64; n++) rkey[n] ^= 0x5c ^ 0x36; inner.Write(rkey, 64); + + memory_cleanse(rkey, sizeof(rkey)); } void CHMAC_SHA256::Finalize(unsigned char hash[OUTPUT_SIZE]) @@ -33,4 +36,5 @@ void CHMAC_SHA256::Finalize(unsigned char hash[OUTPUT_SIZE]) unsigned char temp[32]; inner.Finalize(temp); outer.Write(temp, 32).Finalize(hash); + memory_cleanse(temp, sizeof(temp)); } diff --git a/src/crypto/hmac_sha512.cpp b/src/crypto/hmac_sha512.cpp index f37e709d13cf..0a9d1041a67d 100644 --- a/src/crypto/hmac_sha512.cpp +++ b/src/crypto/hmac_sha512.cpp @@ -5,6 +5,7 @@ #include #include +#include #include @@ -26,6 +27,8 @@ CHMAC_SHA512::CHMAC_SHA512(const unsigned char* key, size_t keylen) for (int n = 0; n < 128; n++) rkey[n] ^= 0x5c ^ 0x36; inner.Write(rkey, 128); + + memory_cleanse(rkey, sizeof(rkey)); } void CHMAC_SHA512::Finalize(unsigned char hash[OUTPUT_SIZE]) @@ -33,4 +36,5 @@ void CHMAC_SHA512::Finalize(unsigned char hash[OUTPUT_SIZE]) unsigned char temp[64]; inner.Finalize(temp); outer.Write(temp, 64).Finalize(hash); + memory_cleanse(temp, sizeof(temp)); } diff --git a/src/hash.h b/src/hash.h index 34486af64a1d..b671761fbb6e 100644 --- a/src/hash.h +++ b/src/hash.h @@ -13,12 +13,20 @@ #include #include #include +#include #include #include #include -typedef uint256 ChainCode; +/** A BIP32 chain code. Cleansed on destruction. */ +class ChainCode : public base_blob<256> { +public: + constexpr ChainCode() = default; + constexpr explicit ChainCode(std::span vch) : base_blob<256>(vch) {} + constexpr explicit ChainCode(const base_blob<256>& b) : base_blob<256>(b) {} + ~ChainCode() { memory_cleanse(data(), size()); } +}; /** A hasher class for Bitcoin's 256-bit hash (double SHA-256). */ class CHash256 { diff --git a/src/musig.cpp b/src/musig.cpp index d211790c43b2..d187ad001338 100644 --- a/src/musig.cpp +++ b/src/musig.cpp @@ -11,10 +11,7 @@ //! MuSig2 chaincode as defined by BIP 328 using namespace util::hex_literals; -constexpr uint256 MUSIG_CHAINCODE{ - // Use immediate lambda to work around GCC-14 bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=117966 - []() consteval { return uint256{"868087ca02a6f974c4598924c36b57762d32cb45717167e300622c7167e38965"_hex_u8}; }(), -}; +const ChainCode MUSIG_CHAINCODE{"868087ca02a6f974c4598924c36b57762d32cb45717167e300622c7167e38965"_hex_u8}; static bool GetMuSig2KeyAggCache(const std::vector& pubkeys, secp256k1_musig_keyagg_cache& keyagg_cache) { diff --git a/src/net.cpp b/src/net.cpp index 6fb541180a68..b22b503997b0 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -296,7 +297,7 @@ bool AddLocal(const CService& addr_, int nScore) const auto [it, is_newly_added] = mapLocalHost.emplace(addr, LocalServiceInfo()); LocalServiceInfo &info = it->second; if (is_newly_added || nScore >= info.nScore) { - info.nScore = nScore + (is_newly_added ? 0 : 1); + info.nScore = SaturatingAdd(nScore, is_newly_added ? 0 : 1); info.nPort = addr.GetPort(); } } @@ -325,7 +326,7 @@ bool SeenLocal(const CService& addr) LOCK(g_maplocalhost_mutex); const auto it = mapLocalHost.find(addr); if (it == mapLocalHost.end()) return false; - ++it->second.nScore; + it->second.nScore = SaturatingAdd(it->second.nScore, 1); return true; } diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index 8cc93fb46c81..b060108c1863 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -486,7 +486,9 @@ bool BlockManager::LoadBlockIndex(const std::optional& snapshot_blockha pindex->m_chain_tx_count = pindex->pprev->m_chain_tx_count + pindex->nTx; } else { pindex->m_chain_tx_count = 0; - m_blocks_unlinked.insert(std::make_pair(pindex->pprev, pindex)); + if (pindex->nStatus & BLOCK_HAVE_DATA) { + m_blocks_unlinked.insert(std::make_pair(pindex->pprev, pindex)); + } } } else { pindex->m_chain_tx_count = pindex->nTx; diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index 34bb9571c153..3a7735e149b6 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -403,4 +403,16 @@ struct CMutableTransaction typedef std::shared_ptr CTransactionRef; template static inline CTransactionRef MakeTransactionRef(Tx&& txIn) { return std::make_shared(std::forward(txIn)); } +namespace std { +/** Disable default std::hash for CTransactionRef to prevent accidentally + * comparing by pointer. Use CTransactionRefHash or provide a custom + * hasher. */ +template <> +struct hash { + hash() = delete; + // Belt-and-suspenders, already implied by the above. + size_t operator()(const CTransactionRef&) const = delete; +}; +} // namespace std + #endif // BITCOIN_PRIMITIVES_TRANSACTION_H diff --git a/src/pubkey.h b/src/pubkey.h index 02ad7371a796..0391609ebcc3 100644 --- a/src/pubkey.h +++ b/src/pubkey.h @@ -27,8 +27,6 @@ class CKeyID : public uint160 explicit CKeyID(const uint160& in) : uint160(in) {} }; -typedef uint256 ChainCode; - /** An encapsulated public key. */ class CPubKey { diff --git a/src/test/fuzz/key.cpp b/src/test/fuzz/key.cpp index e4bedff8b512..19101ae81c2d 100644 --- a/src/test/fuzz/key.cpp +++ b/src/test/fuzz/key.cpp @@ -85,7 +85,7 @@ FUZZ_TARGET(key, .init = initialize_key) { CKey child_key; ChainCode child_chaincode; - const bool ok = key.Derive(child_key, child_chaincode, 0, random_uint256); + const bool ok = key.Derive(child_key, child_chaincode, 0, ChainCode{random_uint256}); assert(ok); assert(child_key.IsValid()); assert(!(child_key == key)); @@ -275,7 +275,7 @@ FUZZ_TARGET(key, .init = initialize_key) { CPubKey child_pubkey; ChainCode child_chaincode; - const bool ok = pubkey.Derive(child_pubkey, child_chaincode, 0, random_uint256); + const bool ok = pubkey.Derive(child_pubkey, child_chaincode, 0, ChainCode{random_uint256}); assert(ok); assert(child_pubkey != pubkey); assert(child_pubkey.IsCompressed()); diff --git a/src/test/fuzz/util/check_globals.cpp b/src/test/fuzz/util/check_globals.cpp index 6e7901dd98f3..1c29906932c5 100644 --- a/src/test/fuzz/util/check_globals.cpp +++ b/src/test/fuzz/util/check_globals.cpp @@ -19,6 +19,7 @@ struct CheckGlobalsImpl { g_seeded_g_prng_zero = false; g_used_system_time = false; SetMockTime(0s); + MockableSteadyClock::ClearMockTime(); } ~CheckGlobalsImpl() { diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp index 32801d97b8bd..7168d3dcaec2 100644 --- a/src/test/net_tests.cpp +++ b/src/test/net_tests.cpp @@ -802,6 +802,41 @@ BOOST_AUTO_TEST_CASE(LocalAddress_BasicLifecycle) BOOST_CHECK(!IsLocal(addr)); } +BOOST_AUTO_TEST_CASE(LocalAddress_nScore_Overflow) +{ + g_reachable_nets.Add(NET_IPV4); + const CService addr{UtilBuildAddress(0x002, 0x001, 0x001, 0x001), 1000}; // 2.1.1.1:1000 + + const auto get_score = [](const CService& service) -> int { + LOCK(g_maplocalhost_mutex); + const auto it = mapLocalHost.find(service); + return it != mapLocalHost.end() ? it->second.nScore : 0; + }; + + const int initial_score = 1000; + BOOST_REQUIRE(AddLocal(addr, initial_score)); + BOOST_REQUIRE(IsLocal(addr)); + BOOST_CHECK_EQUAL(get_score(addr), initial_score); + + // SeenLocal should increment nScore by 1. + BOOST_CHECK(SeenLocal(addr)); + BOOST_CHECK_EQUAL(get_score(addr), initial_score + 1); + + // AddLocal() saturates nScore when updating an existing entry at INT_MAX. + BOOST_REQUIRE(AddLocal(addr, std::numeric_limits::max())); + BOOST_CHECK_EQUAL(get_score(addr), std::numeric_limits::max()); + + BOOST_CHECK(AddLocal(addr, std::numeric_limits::max())); + BOOST_CHECK_EQUAL(get_score(addr), std::numeric_limits::max()); + + // SeenLocal() also saturates at INT_MAX. + BOOST_CHECK(SeenLocal(addr)); + BOOST_CHECK_EQUAL(get_score(addr), std::numeric_limits::max()); + + RemoveLocal(addr); + BOOST_CHECK(!IsLocal(addr)); +} + BOOST_AUTO_TEST_CASE(initial_advertise_from_version_message) { LOCK(NetEventsInterface::g_msgproc_mutex); diff --git a/test/functional/feature_prune_stale_fork.py b/test/functional/feature_prune_stale_fork.py new file mode 100755 index 000000000000..5badca0a7cfc --- /dev/null +++ b/test/functional/feature_prune_stale_fork.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# 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. +"""Test node restart with a pruned stale-fork block whose parent has no transactions.""" + +from test_framework.blocktools import create_empty_fork +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, assert_raises_rpc_error + + +class FeaturePruneStaleForkTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.extra_args = [["-prune=1", "-fastprune"]] + + def run_test(self): + node = self.nodes[0] + + self.log.info("Create a 2-block stale fork: parent has no transactions, child has transactions") + [side_parent, side_child] = create_empty_fork(node, 2) + + node.submitheader(side_parent.serialize().hex()) + node.submitblock(side_child.serialize().hex()) + assert_equal(node.getblockheader(side_parent.hash_hex)["nTx"], 0) + assert_equal(node.getblockheader(side_child.hash_hex)["nTx"], 1) + + self.log.info("Advance and prune so the stale-fork child's block data is removed from disk") + self.generate(node, 500) + node.pruneblockchain(node.getblockcount() - 100) + assert_raises_rpc_error(-1, "Block not available (pruned data)", node.getblock, side_child.hash_hex) + + self.log.info("Restart and mine; node must reload cleanly after the stale-fork child was pruned") + self.restart_node(0) + self.generate(node, 1) + + +if __name__ == '__main__': + FeaturePruneStaleForkTest(__file__).main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 545c50726282..4f59f0efee72 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -371,6 +371,7 @@ 'wallet_startup.py', 'p2p_private_broadcast_retry_v1.py', 'feature_remove_pruned_files_on_startup.py', + 'feature_prune_stale_fork.py', 'p2p_i2p_ports.py', 'p2p_i2p_sessions.py', 'feature_presegwit_node_upgrade.py',