Skip to content

Commit 0d4cf70

Browse files
committed
mining: add cooldown argument to createNewBlock()
At startup, if the needs to catch up, connected mining clients will receive a flood of new templates as new blocks are connected. Fix this by adding an optional cooldown argument to createNewBlock(). When set to true, block template creation is briefly paused while the best header chain is ahead of the tip. This wait only happens when the best header extends the current tip, to ignore competing branches. Additionally, cooldown waits for isInitialBlockDownload() to latch to false, which happens when there is less than a day of blocks left to sync. When cooldown is false, the default and used by all internal callers, createNewBlock() returns immediately. The argument is optional, because the functional test suite is negatively impacted by this mechanism, and single miner signets could end up stuck if no block was mined for a day. Fixes bitcoin#33994
1 parent eaa1209 commit 0d4cf70

File tree

9 files changed

+122
-11
lines changed

9 files changed

+122
-11
lines changed

src/interfaces/mining.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,15 @@ class Mining
145145
/**
146146
* Construct a new block template.
147147
*
148-
* During node initialization, this will wait until the tip is connected.
149-
*
150148
* @param[in] options options for creating the block
149+
* @param[in] cooldown wait for tip to be connected and IBD to complete.
150+
* If the best header is ahead of the tip, wait for the
151+
* tip to catch up. Recommended, except for regtest and
152+
* signets with only one miner.
151153
* @retval BlockTemplate a block template.
152154
* @retval std::nullptr if the node is shut down.
153155
*/
154-
virtual std::unique_ptr<BlockTemplate> createNewBlock(const node::BlockCreateOptions& options = {}) = 0;
156+
virtual std::unique_ptr<BlockTemplate> createNewBlock(const node::BlockCreateOptions& options = {}, bool cooldown = false) = 0;
155157

156158
/**
157159
* Checks if a given block is valid.

src/ipc/capnp/mining.capnp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ interface Mining $Proxy.wrap("interfaces::Mining") {
1717
isInitialBlockDownload @1 (context :Proxy.Context) -> (result: Bool);
1818
getTip @2 (context :Proxy.Context) -> (result: Common.BlockRef, hasResult: Bool);
1919
waitTipChanged @3 (context :Proxy.Context, currentTip: Data, timeout: Float64) -> (result: Common.BlockRef);
20-
createNewBlock @4 (context :Proxy.Context, options: BlockCreateOptions) -> (result: BlockTemplate);
20+
createNewBlock @4 (context :Proxy.Context, options: BlockCreateOptions, cooldown: Bool) -> (result: BlockTemplate);
2121
checkBlock @5 (context :Proxy.Context, block: Data, options: BlockCheckOptions) -> (reason: Text, debug: Text, result: Bool);
2222
}
2323

src/node/interfaces.cpp

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -967,10 +967,27 @@ class MinerImpl : public Mining
967967
return WaitTipChanged(chainman(), notifications(), current_tip, timeout);
968968
}
969969

970-
std::unique_ptr<BlockTemplate> createNewBlock(const BlockCreateOptions& options) override
970+
std::unique_ptr<BlockTemplate> createNewBlock(const BlockCreateOptions& options, bool cooldown) override
971971
{
972972
// Ensure m_tip_block is set so consumers of BlockTemplate can rely on that.
973-
if (!waitTipChanged(uint256::ZERO, MillisecondsDouble::max())) return {};
973+
std::optional<BlockRef> maybe_tip{waitTipChanged(uint256::ZERO, MillisecondsDouble::max())};
974+
975+
if (!maybe_tip) return {};
976+
977+
if (cooldown) {
978+
// Do not return a template during IBD, because it can have long
979+
// pauses and sometimes takes a while to get started. Although this
980+
// is useful in general, it's gated behind the cooldown argument,
981+
// because on regtest and single miner signets this would wait
982+
// forever if no block was mined in the past day.
983+
while (chainman().IsInitialBlockDownload()) {
984+
maybe_tip = waitTipChanged(maybe_tip->hash, MillisecondsDouble{1000});
985+
if (!maybe_tip) return {};
986+
}
987+
988+
// Also wait during the final catch-up moments after IBD.
989+
if (!CooldownIfHeadersAhead(chainman(), notifications(), *maybe_tip)) return {};
990+
}
974991

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

src/node/miner.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,38 @@ std::optional<BlockRef> GetTip(ChainstateManager& chainman)
452452
return BlockRef{tip->GetBlockHash(), tip->nHeight};
453453
}
454454

455+
bool CooldownIfHeadersAhead(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const BlockRef& last_tip)
456+
{
457+
uint256 last_tip_hash{last_tip.hash};
458+
459+
while (const std::optional<int> remaining = chainman.BlocksAheadOfTip()) {
460+
const int cooldown_seconds = std::clamp(*remaining, 3, 20);
461+
// Use a steady clock so cooldown is independent of mocktime.
462+
const auto cooldown_deadline{SteadyClock::now() + std::chrono::seconds{cooldown_seconds}};
463+
464+
{
465+
WAIT_LOCK(kernel_notifications.m_tip_block_mutex, lock);
466+
kernel_notifications.m_tip_block_cv.wait_until(lock, cooldown_deadline, [&]() EXCLUSIVE_LOCKS_REQUIRED(kernel_notifications.m_tip_block_mutex) {
467+
const auto tip_block = kernel_notifications.TipBlock();
468+
return chainman.m_interrupt || (tip_block && *tip_block != last_tip_hash);
469+
});
470+
if (chainman.m_interrupt) return false;
471+
472+
// If the tip changed during the wait, extend the deadline
473+
const auto tip_block = kernel_notifications.TipBlock();
474+
if (tip_block && *tip_block != last_tip_hash) {
475+
last_tip_hash = *tip_block;
476+
continue;
477+
}
478+
}
479+
480+
// No tip change and the cooldown window has expired.
481+
if (SteadyClock::now() >= cooldown_deadline) break;
482+
}
483+
484+
return true;
485+
}
486+
455487
std::optional<BlockRef> WaitTipChanged(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const uint256& current_tip, MillisecondsDouble& timeout)
456488
{
457489
Assume(timeout >= 0ms); // No internal callers should use a negative timeout

src/node/miner.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,25 @@ std::optional<BlockRef> GetTip(ChainstateManager& chainman);
163163
* Returns the current tip, or nullopt if the node is shutting down. */
164164
std::optional<BlockRef> WaitTipChanged(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const uint256& current_tip, MillisecondsDouble& timeout);
165165

166+
/**
167+
* Wait while the best known header extends the current chain tip AND at least
168+
* one block is being added to the tip every 3 seconds. If the tip is
169+
* sufficiently far behind, allow up to 20 seconds for the next tip update.
170+
*
171+
* It’s not safe to keep waiting, because a malicious miner could announce a
172+
* header and delay revealing the block, causing all other miners using this
173+
* software to stall. At the same time, we need to balance between the default
174+
* waiting time being brief, but not ending the cooldown prematurely when a
175+
* random block is slow to download (or process).
176+
*
177+
* The cooldown only applies to createNewBlock(), which is typically called
178+
* once per connected client. Subsequent templates are provided by waitNext().
179+
*
180+
* @param last_tip tip at the start of the cooldown window.
181+
*
182+
* @returns false if interrupted.
183+
*/
184+
bool CooldownIfHeadersAhead(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const BlockRef& last_tip);
166185
} // namespace node
167186

168187
#endif // BITCOIN_NODE_MINER_H

src/validation.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6299,6 +6299,19 @@ void ChainstateManager::RecalculateBestHeader()
62996299
}
63006300
}
63016301

6302+
std::optional<int> ChainstateManager::BlocksAheadOfTip() const
6303+
{
6304+
LOCK(::cs_main);
6305+
const CBlockIndex* best_header{m_best_header};
6306+
const CBlockIndex* tip{ActiveChain().Tip()};
6307+
// Only consider headers that extend the active tip; ignore competing branches.
6308+
if (best_header && tip && best_header->nChainWork > tip->nChainWork &&
6309+
best_header->GetAncestor(tip->nHeight) == tip) {
6310+
return best_header->nHeight - tip->nHeight;
6311+
}
6312+
return std::nullopt;
6313+
}
6314+
63026315
bool ChainstateManager::ValidatedSnapshotCleanup(Chainstate& validated_cs, Chainstate& unvalidated_cs)
63036316
{
63046317
AssertLockHeld(::cs_main);

src/validation.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,6 +1354,10 @@ class ChainstateManager
13541354
//! header in our block-index not known to be invalid, recalculate it.
13551355
void RecalculateBestHeader() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
13561356

1357+
//! Returns how many blocks the best header is ahead of the current tip,
1358+
//! or nullopt if the best header does not extend the tip.
1359+
std::optional<int> BlocksAheadOfTip() const LOCKS_EXCLUDED(::cs_main);
1360+
13571361
CCheckQueue<CScriptCheck>& GetCheckQueue() { return m_script_check_queue; }
13581362

13591363
~ChainstateManager();

test/functional/interface_ipc_mining.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,20 @@
44
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
55
"""Test the IPC (multiprocess) Mining interface."""
66
import asyncio
7+
import time
78
from contextlib import AsyncExitStack
89
from io import BytesIO
910
from test_framework.blocktools import NULL_OUTPOINT
1011
from test_framework.messages import (
12+
CBlockHeader,
1113
CTransaction,
1214
CTxIn,
1315
CTxOut,
1416
CTxInWitness,
1517
ser_uint256,
1618
COIN,
19+
from_hex,
20+
msg_headers,
1721
)
1822
from test_framework.script import (
1923
CScript,
@@ -22,9 +26,11 @@
2226
from test_framework.test_framework import BitcoinTestFramework
2327
from test_framework.util import (
2428
assert_equal,
29+
assert_greater_than_or_equal,
2530
assert_not_equal
2631
)
2732
from test_framework.wallet import MiniWallet
33+
from test_framework.p2p import P2PInterface
2834
from test_framework.ipc_util import (
2935
destroying,
3036
mining_create_block_template,
@@ -155,18 +161,36 @@ def run_block_template_test(self):
155161

156162
async def async_routine():
157163
ctx, mining = await self.make_mining_ctx()
158-
blockref = await mining.getTip(ctx)
159164

160165
async with AsyncExitStack() as stack:
166+
self.log.debug("createNewBlock() should wait if tip is still updating")
167+
self.disconnect_nodes(0, 1)
168+
node1_block_hash = self.generate(self.nodes[1], 1, sync_fun=self.no_op)[0]
169+
header = from_hex(CBlockHeader(), self.nodes[1].getblockheader(node1_block_hash, False))
170+
header_only_peer = self.nodes[0].add_p2p_connection(P2PInterface())
171+
header_only_peer.send_and_ping(msg_headers([header]))
172+
start = time.time()
173+
async with destroying((await mining.createNewBlock(ctx, self.default_block_create_options, cooldown=True)).result, ctx):
174+
pass
175+
# Lower-bound only: a heavily loaded CI host might still exceed 0.9s
176+
# even without the cooldown, so this can miss regressions but avoids
177+
# spurious failures.
178+
assert_greater_than_or_equal(time.time() - start, 0.9)
179+
180+
header_only_peer.peer_disconnect()
181+
self.connect_nodes(0, 1)
182+
self.sync_all()
183+
161184
self.log.debug("Create a template")
162185
template = await mining_create_block_template(mining, stack, ctx, self.default_block_create_options)
163186

164187
self.log.debug("Test some inspectors of Template")
165188
header = (await template.getBlockHeader(ctx)).result
166189
assert_equal(len(header), block_header_size)
167190
block = await mining_get_block(template, ctx)
168-
assert_equal(ser_uint256(block.hashPrevBlock), blockref.result.hash)
169-
assert len(block.vtx) >= 1
191+
current_tip = self.nodes[0].getbestblockhash()
192+
assert_equal(ser_uint256(block.hashPrevBlock), ser_uint256(int(current_tip, 16)))
193+
assert_greater_than_or_equal(len(block.vtx), 1)
170194
txfees = await template.getTxFees(ctx)
171195
assert_equal(len(txfees.result), 0)
172196
txsigops = await template.getTxSigops(ctx)
@@ -232,7 +256,7 @@ async def async_routine():
232256
current_block_height = self.nodes[0].getchaintips()[0]["height"]
233257
check_opts = self.capnp_modules['mining'].BlockCheckOptions()
234258

235-
async with destroying((await mining.createNewBlock(ctx, self.default_block_create_options)).result, ctx) as template:
259+
async with destroying((await mining.createNewBlock(ctx, self.default_block_create_options, cooldown=True)).result, ctx) as template:
236260
block = await mining_get_block(template, ctx)
237261
balance = self.miniwallet.get_balance()
238262
coinbase = await self.build_coinbase_test(template, ctx, self.miniwallet)

test/functional/test_framework/ipc_util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ async def make_capnp_init_ctx(self):
108108

109109
async def mining_create_block_template(mining, stack, ctx, opts):
110110
"""Call mining.createNewBlock() and return template, then call template.destroy() when stack exits."""
111-
return await stack.enter_async_context(destroying((await mining.createNewBlock(ctx, opts)).result, ctx))
111+
return await stack.enter_async_context(destroying((await mining.createNewBlock(ctx, opts, cooldown=True)).result, ctx))
112112

113113

114114
async def mining_wait_next_template(template, stack, ctx, opts):

0 commit comments

Comments
 (0)