What changed, and why it matters
This commit introduces a new internal helper class, util::Expected, that gives Bitcoin Core a cleaner way to return either a successful result or an error message from a function. It is a pure code-quality/refactoring change: it replaces the existing std::optional<std::string> pattern used by FetchBlock with the new Expected<void, std::string> type. There is no change to network behavior, RPC behavior, or consensus rules, and no security bug is fixed or introduced.
No security action required. Treat as normal code-review/refactoring commit.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch adds src/util/expected.h, a lightweight std::expected-like template built on std::variant, plus a unit-test file and CMake wiring. It refactors PeerManagerImpl::FetchBlock and its callers in net_processing.cpp/h and rpc/blockchain.cpp to return util::Expected
Changed components
src/util/expected.h (new utility)src/util/expected.cpp (new translation unit)src/test/util_expected_tests.cpp (new tests)src/net_processing.cpp / .h (FetchBlock signature refactor)src/rpc/blockchain.cpp (getblockfrompeer caller refactor)src/kernel/CMakeLists.txtsrc/test/CMakeLists.txtInspect captured patch +181 / −14
diff --git a/src/kernel/CMakeLists.txt b/src/kernel/CMakeLists.txt
index 6b9565e8..85011f44 100644
--- a/src/kernel/CMakeLists.txt
+++ b/src/kernel/CMakeLists.txt
@@ -63,6 +63,7 @@ add_library(bitcoinkernel
../uint256.cpp
../util/chaintype.cpp
../util/check.cpp
+ ../util/expected.cpp
../util/feefrac.cpp
../util/fs.cpp
../util/fs_helpers.cpp
diff --git a/src/net_processing.cpp b/src/net_processing.cpp
index 9cab2461..8da34575 100644
--- a/src/net_processing.cpp
+++ b/src/net_processing.cpp
@@ -530,7 +530,7 @@ public:
/** Implement PeerManager */
void StartScheduledTasks(CScheduler& scheduler) override;
void CheckForStaleTipAndEvictPeers() override;
- std::optional<std::string> FetchBlock(NodeId peer_id, const CBlockIndex& block_index) override
+ util::Expected<void, std::string> FetchBlock(NodeId peer_id, const CBlockIndex& block_index) override
EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
bool GetNodeStateStats(NodeId nodeid, CNodeStateStats& stats) const override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
std::vector<node::TxOrphanage::OrphanInfo> GetOrphanTransactions() override EXCLUSIVE_LOCKS_REQUIRED(!m_tx_download_mutex);
@@ -1844,16 +1844,16 @@ bool PeerManagerImpl::BlockRequestAllowed(const CBlockIndex* pindex)
(GetBlockProofEquivalentTime(*m_chainman.m_best_header, *pindex, *m_chainman.m_best_header, m_chainparams.GetConsensus()) < STALE_RELAY_AGE_LIMIT);
}
-std::optional<std::string> PeerManagerImpl::FetchBlock(NodeId peer_id, const CBlockIndex& block_index)
+util::Expected<void, std::string> PeerManagerImpl::FetchBlock(NodeId peer_id, const CBlockIndex& block_index)
{
- if (m_chainman.m_blockman.LoadingBlocks()) return "Loading blocks ...";
+ if (m_chainman.m_blockman.LoadingBlocks()) return util::Unexpected{"Loading blocks ..."};
// Ensure this peer exists and hasn't been disconnected
PeerRef peer = GetPeerRef(peer_id);
- if (peer == nullptr) return "Peer does not exist";
+ if (peer == nullptr) return util::Unexpected{"Peer does not exist"};
// Ignore pre-segwit peers
- if (!CanServeWitnesses(*peer)) return "Pre-SegWit peer";
+ if (!CanServeWitnesses(*peer)) return util::Unexpected{"Pre-SegWit peer"};
LOCK(cs_main);
@@ -1861,7 +1861,7 @@ std::optional<std::string> PeerManagerImpl::FetchBlock(NodeId peer_id, const CBl
RemoveBlockRequest(block_index.GetBlockHash(), std::nullopt);
// Mark block as in-flight
- if (!BlockRequested(peer_id, block_index)) return "Already requested from this peer";
+ if (!BlockRequested(peer_id, block_index)) return util::Unexpected{"Already requested from this peer"};
// Construct message to request the block
const uint256& hash{block_index.GetBlockHash()};
@@ -1873,11 +1873,11 @@ std::optional<std::string> PeerManagerImpl::FetchBlock(NodeId peer_id, const CBl
return true;
});
- if (!success) return "Peer not fully connected";
+ if (!success) return util::Unexpected{"Peer not fully connected"};
LogDebug(BCLog::NET, "Requesting block %s from peer=%d\n",
hash.ToString(), peer_id);
- return std::nullopt;
+ return {};
}
std::unique_ptr<PeerManager> PeerManager::make(CConnman& connman, AddrMan& addrman,
diff --git a/src/net_processing.h b/src/net_processing.h
index 6eb4a5e1..cf75e8f6 100644
--- a/src/net_processing.h
+++ b/src/net_processing.h
@@ -11,6 +11,7 @@
#include <node/txorphanage.h>
#include <protocol.h>
#include <threadsafety.h>
+#include <util/expected.h>
#include <validationinterface.h>
#include <atomic>
@@ -101,9 +102,8 @@ public:
*
* @param[in] peer_id The peer id
* @param[in] block_index The blockindex
- * @returns std::nullopt if a request was successfully made, otherwise an error message
*/
- virtual std::optional<std::string> FetchBlock(NodeId peer_id, const CBlockIndex& block_index) = 0;
+ virtual util::Expected<void, std::string> FetchBlock(NodeId peer_id, const CBlockIndex& block_index) = 0;
/** Begin running background tasks, should only be called once */
virtual void StartScheduledTasks(CScheduler& scheduler) = 0;
diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp
index ff8cb119..8f283ff4 100644
--- a/src/rpc/blockchain.cpp
+++ b/src/rpc/blockchain.cpp
@@ -536,8 +536,8 @@ static RPCHelpMan getblockfrompeer()
throw JSONRPCError(RPC_MISC_ERROR, "Block already downloaded");
}
- if (const auto err{peerman.FetchBlock(peer_id, *index)}) {
- throw JSONRPCError(RPC_MISC_ERROR, err.value());
+ if (const auto res{peerman.FetchBlock(peer_id, *index)}; !res) {
+ throw JSONRPCError(RPC_MISC_ERROR, res.error());
}
return UniValue::VOBJ;
},
diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt
index 8d27e462..83cb989a 100644
--- a/src/test/CMakeLists.txt
+++ b/src/test/CMakeLists.txt
@@ -119,6 +119,7 @@ add_executable(test_bitcoin
txvalidationcache_tests.cpp
uint256_tests.cpp
util_check_tests.cpp
+ util_expected_tests.cpp
util_string_tests.cpp
util_tests.cpp
util_threadnames_tests.cpp
diff --git a/src/test/util_expected_tests.cpp b/src/test/util_expected_tests.cpp
new file mode 100644
index 00000000..61a6d2e0
--- /dev/null
+++ b/src/test/util_expected_tests.cpp
@@ -0,0 +1,68 @@
+// Copyright (c) The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or https://opensource.org/license/mit.
+
+#include <test/util/setup_common.h>
+#include <util/expected.h>
+
+#include <boost/test/unit_test.hpp>
+
+using namespace util;
+
+BOOST_AUTO_TEST_SUITE(util_expected_tests)
+
+BOOST_AUTO_TEST_CASE(expected_value)
+{
+ struct Obj {
+ int x;
+ };
+ Expected<Obj, int> e{};
+ BOOST_CHECK(e.value().x == 0);
+
+ e = Obj{42};
+
+ BOOST_CHECK(e.has_value());
+ BOOST_CHECK(static_cast<bool>(e));
+ BOOST_CHECK(e.value().x == 42);
+ BOOST_CHECK((*e).x == 42);
+ BOOST_CHECK(e->x == 42);
+
+ // modify value
+ e.value().x += 1;
+ (*e).x += 1;
+ e->x += 1;
+
+ const auto& read{e};
+ BOOST_CHECK(read.value().x == 45);
+ BOOST_CHECK((*read).x == 45);
+ BOOST_CHECK(read->x == 45);
+}
+
+BOOST_AUTO_TEST_CASE(expected_value_or)
+{
+ Expected<std::unique_ptr<int>, int> no_copy{std::make_unique<int>(1)};
+ const int one{*std::move(no_copy).value_or(std::make_unique<int>(2))};
+ BOOST_CHECK_EQUAL(one, 1);
+
+ const Expected<std::string, int> const_val{Unexpected{-1}};
+ BOOST_CHECK_EQUAL(const_val.value_or("fallback"), "fallback");
+}
+
+BOOST_AUTO_TEST_CASE(expected_error)
+{
+ Expected<void, std::string> e{};
+ BOOST_CHECK(e.has_value());
+
+ e = Unexpected{"fail"};
+ BOOST_CHECK(!e.has_value());
+ BOOST_CHECK(!static_cast<bool>(e));
+ BOOST_CHECK(e.error() == "fail");
+
+ // modify error
+ e.error() += "1";
+
+ const auto& read{e};
+ BOOST_CHECK(read.error() == "fail1");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/util/expected.cpp b/src/util/expected.cpp
new file mode 100644
index 00000000..f339f999
--- /dev/null
+++ b/src/util/expected.cpp
@@ -0,0 +1,5 @@
+// Copyright (c) The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or https://opensource.org/license/mit.
+
+#include <util/expected.h>
diff --git a/src/util/expected.h b/src/util/expected.h
new file mode 100644
index 00000000..0e7256f8
--- /dev/null
+++ b/src/util/expected.h
@@ -0,0 +1,92 @@
+// Copyright (c) The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or https://opensource.org/license/mit.
+
+#ifndef BITCOIN_UTIL_EXPECTED_H
+#define BITCOIN_UTIL_EXPECTED_H
+
+#include <attributes.h>
+
+#include <cassert>
+#include <type_traits>
+#include <utility>
+#include <variant>
+
+namespace util {
+
+/// The util::Unexpected class represents an unexpected value stored in
+/// util::Expected.
+template <class E>
+class Unexpected
+{
+public:
+ constexpr explicit Unexpected(E e) : err(std::move(e)) {}
+ E err;
+};
+
+/// The util::Expected class provides a standard way for low-level functions to
+/// return either error values or result values.
+///
+/// It provides a smaller version of std::expected from C++23. Missing features
+/// can be added, if needed.
+template <class T, class E>
+class Expected
+{
+private:
+ using ValueType = std::conditional_t<std::is_same_v<T, void>, std::monostate, T>;
+ std::variant<ValueType, E> m_data;
+
+public:
+ constexpr Expected() : m_data{std::in_place_index_t<0>{}, ValueType{}} {}
+ constexpr Expected(ValueType v) : m_data{std::in_place_index_t<0>{}, std::move(v)} {}
+ template <class Err>
+ constexpr Expected(Unexpected<Err> u) : m_data{std::in_place_index_t<1>{}, std::move(u.err)}
+ {
+ }
+
+ constexpr bool has_value() const noexcept { return m_data.index() == 0; }
+ constexpr explicit operator bool() const noexcept { return has_value(); }
+
+ constexpr const ValueType& value() const LIFETIMEBOUND
+ {
+ assert(has_value());
+ return std::get<0>(m_data);
+ }
+ constexpr ValueType& value() LIFETIMEBOUND
+ {
+ assert(has_value());
+ return std::get<0>(m_data);
+ }
+
+ template <class U>
+ ValueType value_or(U&& default_value) const&
+ {
+ return has_value() ? value() : std::forward<U>(default_value);
+ }
+ template <class U>
+ ValueType value_or(U&& default_value) &&
+ {
+ return has_value() ? std::move(value()) : std::forward<U>(default_value);
+ }
+
+ constexpr const E& error() const LIFETIMEBOUND
+ {
+ assert(!has_value());
+ return std::get<1>(m_data);
+ }
+ constexpr E& error() LIFETIMEBOUND
+ {
+ assert(!has_value());
+ return std::get<1>(m_data);
+ }
+
+ constexpr ValueType& operator*() LIFETIMEBOUND { return value(); }
+ constexpr const ValueType& operator*() const LIFETIMEBOUND { return value(); }
+
+ constexpr ValueType* operator->() LIFETIMEBOUND { return &value(); }
+ constexpr const ValueType* operator->() const LIFETIMEBOUND { return &value(); }
+};
+
+} // namespace util
+
+#endif // BITCOIN_UTIL_EXPECTED_H
diff --git a/src/util/result.h b/src/util/result.h
index 122a7638..fa774fb3 100644
--- a/src/util/result.h
+++ b/src/util/result.h
@@ -21,8 +21,8 @@ struct Error {
//!
//! It is intended for high-level functions that need to report error strings to
//! end users. Lower-level functions that don't need this error-reporting and
-//! only need error-handling should avoid util::Result and instead use standard
-//! classes like std::optional, std::variant, and std::tuple, or custom structs
+//! that only need error-handling should avoid util::Result and instead use
+//! util::Expected, std::optional, std::variant, or custom structs
//! and enum types to return function results.
//!
//! Usage examples can be found in \example ../test/result_tests.cpp, but in
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.