0diff --git a/src/net_processing.cpp b/src/net_processing.cpp
1index f2768efe19..3f0f328840 100644
2--- a/src/net_processing.cpp
3+++ b/src/net_processing.cpp
4@@ -2960,8 +2960,6 @@ void PeerManagerImpl::ProcessHeadersMessage(CNode& pfrom, Peer& peer,
5 return;
6 }
7
8- const CBlockIndex *pindexLast = nullptr;
9-
10 // We'll set already_validated_work to true if these headers are
11 // successfully processed as part of a low-work headers sync in progress
12 // (either in PRESYNC or REDOWNLOAD phase).
13@@ -3046,11 +3044,9 @@ void PeerManagerImpl::ProcessHeadersMessage(CNode& pfrom, Peer& peer,
14 // something new (if these headers are valid).
15 bool received_new_header{last_received_header == nullptr};
16
17- // Now process all the headers and return the block validation state.
18- BlockValidationState state{m_chainman.ProcessNewBlockHeaders(headers,
19- /*min_pow_checked=*/true,
20- &pindexLast)};
21- if (state.IsInvalid()) {
22+ auto headers_processed{m_chainman.ProcessNewBlockHeaders(headers, /*min_pow_checked=*/true)};
23+ if (!headers_processed) {
24+ const auto& state{headers_processed.error()};
25 if (!pfrom.IsInboundConn() && state.GetResult() == BlockValidationResult::BLOCK_CACHED_INVALID) {
26 // Warn user if outgoing peers send us headers of blocks that we previously marked as invalid.
27 LogWarning("%s (received from peer=%i). "
28@@ -3061,9 +3057,10 @@ void PeerManagerImpl::ProcessHeadersMessage(CNode& pfrom, Peer& peer,
29 MaybePunishNodeForBlock(pfrom.GetId(), state, via_compact_block, "invalid header received");
30 return;
31 }
32+ const CBlockIndex* pindexLast{headers_processed.value()};
33 assert(pindexLast);
34
35- if (state.IsValid() && received_new_header) {
36+ if (received_new_header) {
37 LogBlockHeader(*pindexLast, pfrom, /*via_compact_block=*/false);
38 }
39
40@@ -4554,15 +4551,14 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
41 }
42 }
43
44- const CBlockIndex *pindex = nullptr;
45- BlockValidationState state{m_chainman.ProcessNewBlockHeaders({{cmpctblock.header}}, /*min_pow_checked=*/true, &pindex)};
46- if (state.IsInvalid()) {
47- MaybePunishNodeForBlock(pfrom.GetId(), state, /*via_compact_block=*/true, "invalid header via cmpctblock");
48+ auto headers_processed{m_chainman.ProcessNewBlockHeaders({{cmpctblock.header}}, /*min_pow_checked=*/true)};
49+ if (!headers_processed) {
50+ MaybePunishNodeForBlock(pfrom.GetId(), headers_processed.error(), /*via_compact_block=*/true, "invalid header via cmpctblock");
51 return;
52 }
53
54 // If AcceptBlockHeader returned true, it set pindex
55- Assert(pindex);
56+ const CBlockIndex* pindex{Assert(headers_processed.value())};
57 if (received_new_header) {
58 LogBlockHeader(*pindex, pfrom, /*via_compact_block=*/true);
59 }
60diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp
61index e979ccc9a4..fe3c827d15 100644
62--- a/src/rpc/mining.cpp
63+++ b/src/rpc/mining.cpp
64@@ -1123,12 +1123,15 @@ static RPCHelpMan submitheader()
65 }
66 }
67
68- BlockValidationState state{chainman.ProcessNewBlockHeaders({{h}}, /*min_pow_checked=*/true)};
69- if (state.IsValid()) return UniValue::VNULL;
70- if (state.IsError()) {
71- throw JSONRPCError(RPC_VERIFY_ERROR, state.ToString());
72+ if (auto headers_processed{chainman.ProcessNewBlockHeaders({{h}}, /*min_pow_checked=*/true)}) {
73+ return UniValue::VNULL;
74+ } else {
75+ const auto& state{headers_processed.error()};
76+ if (state.IsError()) {
77+ throw JSONRPCError(RPC_VERIFY_ERROR, state.ToString());
78+ }
79+ throw JSONRPCError(RPC_VERIFY_ERROR, state.GetRejectReason());
80 }
81- throw JSONRPCError(RPC_VERIFY_ERROR, state.GetRejectReason());
82 },
83 };
84 }
85diff --git a/src/test/blockfilter_index_tests.cpp b/src/test/blockfilter_index_tests.cpp
86index 0fef42eb88..3ec5f35258 100644
87--- a/src/test/blockfilter_index_tests.cpp
88+++ b/src/test/blockfilter_index_tests.cpp
89@@ -103,7 +103,9 @@ bool BuildChainTestingSetup::BuildChain(const CBlockIndex* pindex,
90 for (auto& block : chain) {
91 block = std::make_shared<CBlock>(CreateBlock(pindex, no_txns, coinbase_script_pub_key));
92
93- if (!Assert(m_node.chainman)->ProcessNewBlockHeaders({{*block}}, true, &pindex).IsValid()) {
94+ if (auto headers_processed{Assert(m_node.chainman)->ProcessNewBlockHeaders({{*block}}, true)}) {
95+ pindex = *headers_processed;
96+ } else {
97 return false;
98 }
99 }
100diff --git a/src/test/fuzz/utxo_snapshot.cpp b/src/test/fuzz/utxo_snapshot.cpp
101index 197ae9c871..db4b0165f2 100644
102--- a/src/test/fuzz/utxo_snapshot.cpp
103+++ b/src/test/fuzz/utxo_snapshot.cpp
104@@ -88,8 +88,7 @@ void initialize_chain()
105 if constexpr (INVALID) {
106 auto& chainman{*setup->m_node.chainman};
107 for (const auto& block : chain) {
108- auto result{chainman.ProcessNewBlockHeaders({{*block}}, true)};
109- Assert(result.IsValid());
110+ Assert(chainman.ProcessNewBlockHeaders({{*block}}, true));
111 const auto* index{WITH_LOCK(::cs_main, return chainman.m_blockman.LookupBlockIndex(block->GetHash()))};
112 Assert(index);
113 }
114@@ -169,8 +168,7 @@ void utxo_snapshot_fuzz(FuzzBufferType buffer)
115 // Consume the bool, but skip the code for the INVALID fuzz target
116 if constexpr (!INVALID) {
117 for (const auto& block : *g_chain) {
118- auto result{chainman.ProcessNewBlockHeaders({{*block}}, true)};
119- Assert(result.IsValid());
120+ Assert(chainman.ProcessNewBlockHeaders({{*block}}, true));
121 const auto* index{WITH_LOCK(::cs_main, return chainman.m_blockman.LookupBlockIndex(block->GetHash()))};
122 Assert(index);
123 }
124diff --git a/src/test/validation_block_tests.cpp b/src/test/validation_block_tests.cpp
125index 85ed77a9ab..ca2624859f 100644
126--- a/src/test/validation_block_tests.cpp
127+++ b/src/test/validation_block_tests.cpp
128@@ -104,7 +104,7 @@ std::shared_ptr<CBlock> MinerTestingSetup::FinalizeBlock(std::shared_ptr<CBlock>
129
130 // submit block header, so that miner can get the block height from the
131 // global state and the node has the topology of the chain
132- BOOST_CHECK(Assert(m_node.chainman)->ProcessNewBlockHeaders({{*pblock}}, true).IsValid());
133+ BOOST_CHECK(Assert(m_node.chainman)->ProcessNewBlockHeaders({{*pblock}}, true));
134
135 return pblock;
136 }
137diff --git a/src/validation.cpp b/src/validation.cpp
138index b323b5c06e..4c123c1cc7 100644
139--- a/src/validation.cpp
140+++ b/src/validation.cpp
141@@ -4300,31 +4300,27 @@ bool ChainstateManager::AcceptBlockHeader(const CBlockHeader& block, BlockValida
142 }
143
144 // Exposed wrapper for AcceptBlockHeader
145-BlockValidationState ChainstateManager::ProcessNewBlockHeaders(std::span<const CBlockHeader> headers, bool min_pow_checked, const CBlockIndex** ppindex)
146+util::Expected<CBlockIndex*, BlockValidationState> ChainstateManager::ProcessNewBlockHeaders(std::span<const CBlockHeader> headers, bool min_pow_checked)
147 {
148 AssertLockNotHeld(cs_main);
149 Assume(!headers.empty());
150 BlockValidationState state;
151+ CBlockIndex* pindex{nullptr};
152 {
153 LOCK(cs_main);
154 for (const CBlockHeader& header : headers) {
155- CBlockIndex *pindex = nullptr; // Use a temp pindex instead of ppindex to avoid a const_cast
156 bool accepted{AcceptBlockHeader(header, state, &pindex, min_pow_checked)};
157 CheckBlockIndex();
158
159 if (!accepted) {
160 if (state.IsValid()) NONFATAL_UNREACHABLE();
161- return state;
162- }
163-
164- if (ppindex) {
165- *ppindex = pindex;
166+ return util::Unexpected{state};
167 }
168 }
169 }
170 if (NotifyHeaderTip()) {
171- if (IsInitialBlockDownload() && ppindex && *ppindex) {
172- const CBlockIndex& last_accepted{**ppindex};
173+ if (IsInitialBlockDownload() && pindex) {
174+ const CBlockIndex& last_accepted{*pindex};
175 int64_t blocks_left{(NodeClock::now() - last_accepted.Time()) / GetConsensus().PowTargetSpacing()};
176 blocks_left = std::max<int64_t>(0, blocks_left);
177 const double progress{100.0 * last_accepted.nHeight / (last_accepted.nHeight + blocks_left)};
178@@ -4333,7 +4329,7 @@ BlockValidationState ChainstateManager::ProcessNewBlockHeaders(std::span<const C
179 }
180
181 if (!state.IsValid()) NONFATAL_UNREACHABLE();
182- return state;
183+ return pindex;
184 }
185
186 void ChainstateManager::ReportHeadersPresync(int64_t height, int64_t timestamp)
187diff --git a/src/validation.h b/src/validation.h
188index 85f252f6ab..0d755a40d6 100644
189--- a/src/validation.h
190+++ b/src/validation.h
191@@ -1239,12 +1239,10 @@ public:
192 *
193 * [@param](/bitcoin-bitcoin/contributor/param/)[in] headers The block headers themselves
194 * [@param](/bitcoin-bitcoin/contributor/param/)[in] min_pow_checked True if proof-of-work anti-DoS checks have been done by caller for headers chain
195- * [@param](/bitcoin-bitcoin/contributor/param/)[out] ppindex If set, the pointer will be set to point to the last new block index object for the given headers
196- * [@returns](/bitcoin-bitcoin/contributor/returns/) BlockValidationState indicating the result. IsValid() returns true if all headers
197- * were accepted. On failure, IsInvalid() is true and the state contains the specific
198- * validation failure reason.
199+ * [@returns](/bitcoin-bitcoin/contributor/returns/) The last new block index object for the given headers if all headers were accepted.
200+ * Otherwise, BlockValidationState containing the validation failure reason.
201 */
202- BlockValidationState ProcessNewBlockHeaders(std::span<const CBlockHeader> headers, bool min_pow_checked, const CBlockIndex** ppindex = nullptr) LOCKS_EXCLUDED(cs_main);
203+ util::Expected<CBlockIndex*, BlockValidationState> ProcessNewBlockHeaders(std::span<const CBlockHeader> headers, bool min_pow_checked) LOCKS_EXCLUDED(cs_main);
204
205 /**
206 * Sufficiently validate a block for disk storage (and store on disk).