Skip to content

Commit e8b01c4

Browse files
committed
mining: createNewBlock() waits if tip is updating
At startup isInitialBlockDownload() stops returning true once there’s less than a day of blocks left to sync. Connected mining clients will receive a flood of new templates as these last blocks are connected. Fix this by briefly pausing block template creation while the best header chain is ahead of the tip. If no tip update happens for one second, we stop waiting. It’s not safe to keep waiting, because a malicious miner could announce a header and delay revealing the block, causing all other miners using this software to stall. The cooldown only applies to createNewBlock(), which is typically called once per connected client. Subsequent templates are provided by waitNext(). Fixes #33994
1 parent 2bcb3f6 commit e8b01c4

File tree

6 files changed

+98
-5
lines changed

6 files changed

+98
-5
lines changed

src/node/interfaces.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -964,7 +964,13 @@ class MinerImpl : public Mining
964964
std::unique_ptr<BlockTemplate> createNewBlock(const BlockCreateOptions& options) override
965965
{
966966
// Ensure m_tip_block is set so consumers of BlockTemplate can rely on that.
967-
if (!waitTipChanged(uint256::ZERO, MillisecondsDouble::max())) return {};
967+
const std::optional<BlockRef> maybe_tip{waitTipChanged(uint256::ZERO, MillisecondsDouble::max())};
968+
if (!maybe_tip) return {};
969+
970+
// Avoid generating a new template immediately after connecting a block while
971+
// our best known header chain still has more work. This prevents a burst of
972+
// block templates during the final catch-up moments after IBD.
973+
if (!CooldownIfHeadersAhead(chainman(), notifications(), *maybe_tip)) return {};
968974

969975
BlockAssembler::Options assemble_options{options};
970976
ApplyArgsManOptions(*Assert(m_node.args), assemble_options);

src/node/miner.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,42 @@ std::optional<BlockRef> GetTip(ChainstateManager& chainman)
412412
return BlockRef{tip->GetBlockHash(), tip->nHeight};
413413
}
414414

415+
bool CooldownIfHeadersAhead(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const BlockRef& last_tip)
416+
{
417+
constexpr auto COOLDOWN{1s};
418+
// Use a steady clock so cooldown is independent of mocktime.
419+
auto cooldown_deadline{SteadyClock::now() + COOLDOWN};
420+
uint256 last_tip_hash{last_tip.hash};
421+
while (true) {
422+
bool best_header_ahead{false};
423+
{
424+
LOCK(::cs_main);
425+
best_header_ahead = chainman.BestHeaderAheadOfActiveTip();
426+
}
427+
if (!best_header_ahead) break;
428+
429+
WAIT_LOCK(kernel_notifications.m_tip_block_mutex, lock);
430+
kernel_notifications.m_tip_block_cv.wait_until(lock, cooldown_deadline, [&]() EXCLUSIVE_LOCKS_REQUIRED(kernel_notifications.m_tip_block_mutex) {
431+
const auto tip_block = kernel_notifications.TipBlock();
432+
return tip_block && *tip_block != last_tip_hash;
433+
});
434+
if (chainman.m_interrupt) return false;
435+
436+
// If the tip changed during the wait, extend the deadline
437+
const auto tip_block = kernel_notifications.TipBlock();
438+
if (tip_block && *tip_block != last_tip_hash) {
439+
last_tip_hash = *tip_block;
440+
cooldown_deadline = SteadyClock::now() + COOLDOWN;
441+
continue;
442+
}
443+
444+
// No tip change and the cooldown window has expired.
445+
if (SteadyClock::now() >= cooldown_deadline) break;
446+
}
447+
448+
return true;
449+
}
450+
415451
std::optional<BlockRef> WaitTipChanged(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const uint256& current_tip, MillisecondsDouble& timeout)
416452
{
417453
Assume(timeout >= 0ms); // No internal callers should use a negative timeout

src/node/miner.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,15 @@ std::optional<BlockRef> GetTip(ChainstateManager& chainman);
157157
/* Waits for the connected tip to change until timeout has elapsed. During node initialization, this will wait until the tip is connected (regardless of `timeout`).
158158
* Returns the current tip, or nullopt if the node is shutting down. */
159159
std::optional<BlockRef> WaitTipChanged(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const uint256& current_tip, MillisecondsDouble& timeout);
160+
161+
/**
162+
* Pause block template creation when the best header chain is ahead of the tip.
163+
*
164+
* @param last_tip tip at the start of the cooldown window.
165+
*
166+
* @returns false if interrupted.
167+
*/
168+
bool CooldownIfHeadersAhead(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const BlockRef& last_tip);
160169
} // namespace node
161170

162171
#endif // BITCOIN_NODE_MINER_H

src/validation.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6320,6 +6320,16 @@ void ChainstateManager::RecalculateBestHeader()
63206320
}
63216321
}
63226322

6323+
bool ChainstateManager::BestHeaderAheadOfActiveTip() const
6324+
{
6325+
AssertLockHeld(::cs_main);
6326+
const CBlockIndex* best_header{m_best_header};
6327+
const CBlockIndex* tip{ActiveChain().Tip()};
6328+
// Only consider headers that extend the active tip; ignore competing branches.
6329+
return best_header && tip && best_header->nChainWork > tip->nChainWork &&
6330+
best_header->GetAncestor(tip->nHeight) == tip;
6331+
}
6332+
63236333
bool ChainstateManager::ValidatedSnapshotCleanup(Chainstate& validated_cs, Chainstate& unvalidated_cs)
63246334
{
63256335
AssertLockHeld(::cs_main);

src/validation.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,6 +1336,8 @@ class ChainstateManager
13361336
//! header in our block-index not known to be invalid, recalculate it.
13371337
void RecalculateBestHeader() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
13381338

1339+
bool BestHeaderAheadOfActiveTip() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
1340+
13391341
CCheckQueue<CScriptCheck>& GetCheckQueue() { return m_script_check_queue; }
13401342

13411343
~ChainstateManager();

test/functional/interface_ipc.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,28 @@
55
"""Test the IPC (multiprocess) interface."""
66
import asyncio
77
import inspect
8+
import time
89
from contextlib import asynccontextmanager, AsyncExitStack
910
from io import BytesIO
1011
from pathlib import Path
1112
import shutil
12-
from test_framework.messages import (CBlock, CTransaction, ser_uint256, COIN)
13+
from test_framework.messages import (
14+
CBlock,
15+
CBlockHeader,
16+
CTransaction,
17+
COIN,
18+
from_hex,
19+
msg_headers,
20+
ser_uint256,
21+
)
1322
from test_framework.test_framework import BitcoinTestFramework
1423
from test_framework.util import (
1524
assert_equal,
25+
assert_greater_than_or_equal,
1626
assert_not_equal
1727
)
1828
from test_framework.wallet import MiniWallet
29+
from test_framework.p2p import P2PInterface
1930

2031
# Test may be skipped and not have capnp installed
2132
try:
@@ -174,19 +185,38 @@ async def async_routine():
174185
assert_equal(oldblockref.height, newblockref.height)
175186

176187
async with AsyncExitStack() as stack:
177-
self.log.debug("Create a template")
178188
opts = self.capnp_modules['mining'].BlockCreateOptions()
179189
opts.useMempool = True
180190
opts.blockReservedWeight = 4000
181191
opts.coinbaseOutputMaxAdditionalSigops = 0
192+
193+
self.log.debug("createNewBlock() should wait if tip is still updating")
194+
self.disconnect_nodes(0, 1)
195+
node1_block_hash = self.generate(self.nodes[1], 1, sync_fun=self.no_op)[0]
196+
header = from_hex(CBlockHeader(), self.nodes[1].getblockheader(node1_block_hash, False))
197+
header_only_peer = self.nodes[0].add_p2p_connection(P2PInterface())
198+
header_only_peer.send_and_ping(msg_headers([header]))
199+
start = time.time()
200+
async with destroying((await mining.createNewBlock(opts)).result, ctx):
201+
pass
202+
# Lower-bound only: a heavily loaded CI host might still exceed 0.9s
203+
# even without the cooldown, so this can miss regressions but avoids
204+
# spurious failures.
205+
assert_greater_than_or_equal(time.time() - start, 0.9)
206+
header_only_peer.peer_disconnect()
207+
self.connect_nodes(0, 1)
208+
self.sync_all()
209+
210+
self.log.debug("Create a template")
182211
template = await create_block_template(mining, stack, ctx, opts)
183212

184213
self.log.debug("Test some inspectors of Template")
185214
header = (await template.getBlockHeader(ctx)).result
186215
assert_equal(len(header), block_header_size)
187216
block = await self.parse_and_deserialize_block(template, ctx)
188-
assert_equal(ser_uint256(block.hashPrevBlock), newblockref.hash)
189-
assert len(block.vtx) >= 1
217+
current_tip = self.nodes[0].getbestblockhash()
218+
assert_equal(ser_uint256(block.hashPrevBlock), ser_uint256(int(current_tip, 16)))
219+
assert_greater_than_or_equal(len(block.vtx), 1)
190220
txfees = await template.getTxFees(ctx)
191221
assert_equal(len(txfees.result), 0)
192222
txsigops = await template.getTxSigops(ctx)

0 commit comments

Comments
 (0)