Skip to content

Commit f347d79

Browse files
committed
Merge bitcoin/bitcoin#31283: Add waitNext() to BlockTemplate interface
cadbd41 miner: have waitNext return after 20 min on testnet (Sjors Provoost) d4020f5 Add waitNext() to BlockTemplate interface (Sjors Provoost) Pull request description: This PR introduces `waitNext()`. It waits for either the tip to update or for fees at the top of the mempool to rise sufficiently. It then returns a new template, with which the caller can rinse and repeat. On testnet3 and testnet4 the difficulty drops after 20 minutes, so the second ensures that a new template is returned in that case. Alternative approach to #31003, suggested in bitcoin/bitcoin#31109 (comment) ACKs for top commit: ryanofsky: Code review ACK cadbd41. Main change since last review is adding back a missing `m_interrupt` check in the waitNext loop. Also made various code cleanups in both commits. ismaelsadeeq: Code review ACK cadbd41 vasild: ACK cadbd41 Tree-SHA512: c5a40053723c1c1674449ba1e4675718229a2022c8b0a4853b12a2c9180beb87536a1f99fde969a0ef099bca9ac69ca14ea4f399d277d2db7f556465ce47de95
2 parents aa68ed2 + cadbd41 commit f347d79

File tree

8 files changed

+251
-11
lines changed

8 files changed

+251
-11
lines changed

src/interfaces/mining.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
#include <consensus/amount.h> // for CAmount
99
#include <interfaces/types.h> // for BlockRef
10-
#include <node/types.h> // for BlockCreateOptions
10+
#include <node/types.h> // for BlockCreateOptions, BlockWaitOptions
1111
#include <primitives/block.h> // for CBlock, CBlockHeader
1212
#include <primitives/transaction.h> // for CTransactionRef
1313
#include <stdint.h> // for int64_t
@@ -56,6 +56,19 @@ class BlockTemplate
5656
* @returns if the block was processed, independent of block validity
5757
*/
5858
virtual bool submitSolution(uint32_t version, uint32_t timestamp, uint32_t nonce, CTransactionRef coinbase) = 0;
59+
60+
/**
61+
* Waits for fees in the next block to rise, a new tip or the timeout.
62+
*
63+
* @param[in] options Control the timeout (default forever) and by how much total fees
64+
* for the next block should rise (default infinite).
65+
*
66+
* @returns a new BlockTemplate or nothing if the timeout occurs.
67+
*
68+
* On testnet this will additionally return a template with difficulty 1 if
69+
* the tip is more than 20 minutes old.
70+
*/
71+
virtual std::unique_ptr<BlockTemplate> waitNext(const node::BlockWaitOptions options = {}) = 0;
5972
};
6073

6174
//! Interface giving clients (RPC, Stratum v2 Template Provider in the future)

src/ipc/capnp/mining.capnp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ interface BlockTemplate $Proxy.wrap("interfaces::BlockTemplate") {
3131
getWitnessCommitmentIndex @7 (context: Proxy.Context) -> (result: Int32);
3232
getCoinbaseMerklePath @8 (context: Proxy.Context) -> (result: List(Data));
3333
submitSolution @9 (context: Proxy.Context, version: UInt32, timestamp: UInt32, nonce: UInt32, coinbase :Data) -> (result: Bool);
34+
waitNext @10 (context: Proxy.Context, options: BlockWaitOptions) -> (result: BlockTemplate);
3435
}
3536

3637
struct BlockCreateOptions $Proxy.wrap("node::BlockCreateOptions") {
@@ -39,6 +40,11 @@ struct BlockCreateOptions $Proxy.wrap("node::BlockCreateOptions") {
3940
coinbaseOutputMaxAdditionalSigops @2 :UInt64 $Proxy.name("coinbase_output_max_additional_sigops");
4041
}
4142

43+
struct BlockWaitOptions $Proxy.wrap("node::BlockWaitOptions") {
44+
timeout @0 : Float64 $Proxy.name("timeout");
45+
feeThreshold @1 : Int64 $Proxy.name("fee_threshold");
46+
}
47+
4248
# Note: serialization of the BlockValidationState C++ type is somewhat fragile
4349
# and using the struct can be awkward. It would be good if testBlockValidity
4450
# method were changed to return validity information in a simpler format.

src/node/interfaces.cpp

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ using interfaces::Mining;
8282
using interfaces::Node;
8383
using interfaces::WalletLoader;
8484
using node::BlockAssembler;
85+
using node::BlockWaitOptions;
8586
using util::Join;
8687

8788
namespace node {
@@ -877,7 +878,11 @@ class ChainImpl : public Chain
877878
class BlockTemplateImpl : public BlockTemplate
878879
{
879880
public:
880-
explicit BlockTemplateImpl(std::unique_ptr<CBlockTemplate> block_template, NodeContext& node) : m_block_template(std::move(block_template)), m_node(node)
881+
explicit BlockTemplateImpl(BlockAssembler::Options assemble_options,
882+
std::unique_ptr<CBlockTemplate> block_template,
883+
NodeContext& node) : m_assemble_options(std::move(assemble_options)),
884+
m_block_template(std::move(block_template)),
885+
m_node(node)
881886
{
882887
assert(m_block_template);
883888
}
@@ -942,9 +947,103 @@ class BlockTemplateImpl : public BlockTemplate
942947
return chainman().ProcessNewBlock(block_ptr, /*force_processing=*/true, /*min_pow_checked=*/true, /*new_block=*/nullptr);
943948
}
944949

950+
std::unique_ptr<BlockTemplate> waitNext(BlockWaitOptions options) override
951+
{
952+
// Delay calculating the current template fees, just in case a new block
953+
// comes in before the next tick.
954+
CAmount current_fees = -1;
955+
956+
// Alternate waiting for a new tip and checking if fees have risen.
957+
// The latter check is expensive so we only run it once per second.
958+
auto now{NodeClock::now()};
959+
const auto deadline = now + options.timeout;
960+
const MillisecondsDouble tick{1000};
961+
const bool allow_min_difficulty{chainman().GetParams().GetConsensus().fPowAllowMinDifficultyBlocks};
962+
963+
do {
964+
bool tip_changed{false};
965+
{
966+
WAIT_LOCK(notifications().m_tip_block_mutex, lock);
967+
// Note that wait_until() checks the predicate before waiting
968+
notifications().m_tip_block_cv.wait_until(lock, std::min(now + tick, deadline), [&]() EXCLUSIVE_LOCKS_REQUIRED(notifications().m_tip_block_mutex) {
969+
AssertLockHeld(notifications().m_tip_block_mutex);
970+
const auto tip_block{notifications().TipBlock()};
971+
// We assume tip_block is set, because this is an instance
972+
// method on BlockTemplate and no template could have been
973+
// generated before a tip exists.
974+
tip_changed = Assume(tip_block) && tip_block != m_block_template->block.hashPrevBlock;
975+
return tip_changed || chainman().m_interrupt;
976+
});
977+
}
978+
979+
if (chainman().m_interrupt) return nullptr;
980+
// At this point the tip changed, a full tick went by or we reached
981+
// the deadline.
982+
983+
// Must release m_tip_block_mutex before locking cs_main, to avoid deadlocks.
984+
LOCK(::cs_main);
985+
986+
// On test networks return a minimum difficulty block after 20 minutes
987+
if (!tip_changed && allow_min_difficulty) {
988+
const NodeClock::time_point tip_time{std::chrono::seconds{chainman().ActiveChain().Tip()->GetBlockTime()}};
989+
if (now > tip_time + 20min) {
990+
tip_changed = true;
991+
}
992+
}
993+
994+
/**
995+
* We determine if fees increased compared to the previous template by generating
996+
* a fresh template. There may be more efficient ways to determine how much
997+
* (approximate) fees for the next block increased, perhaps more so after
998+
* Cluster Mempool.
999+
*
1000+
* We'll also create a new template if the tip changed during this iteration.
1001+
*/
1002+
if (options.fee_threshold < MAX_MONEY || tip_changed) {
1003+
auto tmpl{std::make_unique<BlockTemplateImpl>(m_assemble_options,
1004+
BlockAssembler{
1005+
chainman().ActiveChainstate(),
1006+
context()->mempool.get(),
1007+
m_assemble_options}
1008+
.CreateNewBlock(),
1009+
m_node)};
1010+
1011+
// If the tip changed, return the new template regardless of its fees.
1012+
if (tip_changed) return tmpl;
1013+
1014+
// Calculate the original template total fees if we haven't already
1015+
if (current_fees == -1) {
1016+
current_fees = 0;
1017+
for (CAmount fee : m_block_template->vTxFees) {
1018+
// Skip coinbase
1019+
if (fee < 0) continue;
1020+
current_fees += fee;
1021+
}
1022+
}
1023+
1024+
CAmount new_fees = 0;
1025+
for (CAmount fee : tmpl->m_block_template->vTxFees) {
1026+
// Skip coinbase
1027+
if (fee < 0) continue;
1028+
new_fees += fee;
1029+
Assume(options.fee_threshold != MAX_MONEY);
1030+
if (new_fees >= current_fees + options.fee_threshold) return tmpl;
1031+
}
1032+
}
1033+
1034+
now = NodeClock::now();
1035+
} while (now < deadline);
1036+
1037+
return nullptr;
1038+
}
1039+
1040+
const BlockAssembler::Options m_assemble_options;
1041+
9451042
const std::unique_ptr<CBlockTemplate> m_block_template;
9461043

1044+
NodeContext* context() { return &m_node; }
9471045
ChainstateManager& chainman() { return *Assert(m_node.chainman); }
1046+
KernelNotifications& notifications() { return *Assert(m_node.notifications); }
9481047
NodeContext& m_node;
9491048
};
9501049

@@ -991,7 +1090,7 @@ class MinerImpl : public Mining
9911090
{
9921091
BlockAssembler::Options assemble_options{options};
9931092
ApplyArgsManOptions(*Assert(m_node.args), assemble_options);
994-
return std::make_unique<BlockTemplateImpl>(BlockAssembler{chainman().ActiveChainstate(), context()->mempool.get(), assemble_options}.CreateNewBlock(), m_node);
1093+
return std::make_unique<BlockTemplateImpl>(assemble_options, BlockAssembler{chainman().ActiveChainstate(), context()->mempool.get(), assemble_options}.CreateNewBlock(), m_node);
9951094
}
9961095

9971096
NodeContext* context() override { return &m_node; }

src/node/types.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
#ifndef BITCOIN_NODE_TYPES_H
1414
#define BITCOIN_NODE_TYPES_H
1515

16+
#include <consensus/amount.h>
1617
#include <cstddef>
1718
#include <policy/policy.h>
1819
#include <script/script.h>
20+
#include <util/time.h>
1921

2022
namespace node {
2123
enum class TransactionError {
@@ -61,6 +63,28 @@ struct BlockCreateOptions {
6163
*/
6264
CScript coinbase_output_script{CScript() << OP_TRUE};
6365
};
66+
67+
struct BlockWaitOptions {
68+
/**
69+
* How long to wait before returning nullptr instead of a new template.
70+
* Default is to wait forever.
71+
*/
72+
MillisecondsDouble timeout{MillisecondsDouble::max()};
73+
74+
/**
75+
* The wait method will not return a new template unless it has fees at
76+
* least fee_threshold sats higher than the current template, or unless
77+
* the chain tip changes and the previous template is no longer valid.
78+
*
79+
* A caller may not be interested in templates with higher fees, and
80+
* determining whether fee_threshold is reached is also expensive. So as
81+
* an optimization, when fee_threshold is set to MAX_MONEY (default), the
82+
* implementation is able to be much more efficient, skipping expensive
83+
* checks and only returning new templates when the chain tip changes.
84+
*/
85+
CAmount fee_threshold{MAX_MONEY};
86+
};
87+
6488
} // namespace node
6589

6690
#endif // BITCOIN_NODE_TYPES_H

src/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ add_executable(test_bitcoin
9999
streams_tests.cpp
100100
sync_tests.cpp
101101
system_tests.cpp
102+
testnet4_miner_tests.cpp
102103
timeoffsets_tests.cpp
103104
torcontrol_tests.cpp
104105
transaction_tests.cpp

src/test/miner_tests.cpp

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,11 @@ void MinerTestingSetup::TestPackageSelection(const CScript& scriptPubKey, const
180180
tx.vout[0].nValue = 5000000000LL - 1000 - 50000 - feeToUse;
181181
Txid hashLowFeeTx = tx.GetHash();
182182
AddToMempool(tx_mempool, entry.Fee(feeToUse).FromTx(tx));
183-
block_template = mining->createNewBlock(options);
184-
BOOST_REQUIRE(block_template);
183+
184+
// waitNext() should return nullptr because there is no better template
185+
auto should_be_nullptr = block_template->waitNext({.timeout = MillisecondsDouble{0}, .fee_threshold = 1});
186+
BOOST_REQUIRE(should_be_nullptr == nullptr);
187+
185188
block = block_template->getBlock();
186189
// Verify that the free tx and the low fee tx didn't get selected
187190
for (size_t i=0; i<block.vtx.size(); ++i) {
@@ -196,7 +199,9 @@ void MinerTestingSetup::TestPackageSelection(const CScript& scriptPubKey, const
196199
tx.vout[0].nValue -= 2; // Now we should be just over the min relay fee
197200
hashLowFeeTx = tx.GetHash();
198201
AddToMempool(tx_mempool, entry.Fee(feeToUse + 2).FromTx(tx));
199-
block_template = mining->createNewBlock(options);
202+
203+
// waitNext() should return if fees for the new template are at least 1 sat up
204+
block_template = block_template->waitNext({.fee_threshold = 1});
200205
BOOST_REQUIRE(block_template);
201206
block = block_template->getBlock();
202207
BOOST_REQUIRE_EQUAL(block.vtx.size(), 6U);
@@ -671,9 +676,15 @@ BOOST_AUTO_TEST_CASE(CreateNewBlock_validity)
671676
for (const auto& bi : BLOCKINFO) {
672677
const int current_height{mining->getTip()->height};
673678

674-
// Simple block creation, nothing special yet:
675-
block_template = mining->createNewBlock(options);
676-
BOOST_REQUIRE(block_template);
679+
/**
680+
* Simple block creation, nothing special yet.
681+
* If current_height is odd, block_template will have already been
682+
* set at the end of the previous loop.
683+
*/
684+
if (current_height % 2 == 0) {
685+
block_template = mining->createNewBlock(options);
686+
BOOST_REQUIRE(block_template);
687+
}
677688

678689
CBlock block{block_template->getBlock()};
679690
CMutableTransaction txCoinbase(*block.vtx[0]);
@@ -709,8 +720,13 @@ BOOST_AUTO_TEST_CASE(CreateNewBlock_validity)
709720
auto maybe_new_tip{Assert(m_node.chainman)->ActiveChain().Tip()};
710721
BOOST_REQUIRE_EQUAL(maybe_new_tip->GetBlockHash(), block.GetHash());
711722
}
712-
// This just adds coverage
713-
mining->waitTipChanged(block.hashPrevBlock);
723+
if (current_height % 2 == 0) {
724+
block_template = block_template->waitNext();
725+
BOOST_REQUIRE(block_template);
726+
} else {
727+
// This just adds coverage
728+
mining->waitTipChanged(block.hashPrevBlock);
729+
}
714730
}
715731

716732
LOCK(cs_main);

src/test/testnet4_miner_tests.cpp

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) 2025 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <common/system.h>
6+
#include <interfaces/mining.h>
7+
#include <node/miner.h>
8+
#include <util/time.h>
9+
#include <validation.h>
10+
11+
#include <test/util/setup_common.h>
12+
13+
#include <boost/test/unit_test.hpp>
14+
15+
using interfaces::BlockTemplate;
16+
using interfaces::Mining;
17+
using node::BlockAssembler;
18+
using node::BlockWaitOptions;
19+
20+
namespace testnet4_miner_tests {
21+
22+
struct Testnet4MinerTestingSetup : public Testnet4Setup {
23+
std::unique_ptr<Mining> MakeMining()
24+
{
25+
return interfaces::MakeMining(m_node);
26+
}
27+
};
28+
} // namespace testnet4_miner_tests
29+
30+
BOOST_FIXTURE_TEST_SUITE(testnet4_miner_tests, Testnet4MinerTestingSetup)
31+
32+
BOOST_AUTO_TEST_CASE(MiningInterface)
33+
{
34+
auto mining{MakeMining()};
35+
BOOST_REQUIRE(mining);
36+
37+
BlockAssembler::Options options;
38+
std::unique_ptr<BlockTemplate> block_template;
39+
40+
// Set node time a few minutes past the testnet4 genesis block
41+
const int64_t genesis_time{WITH_LOCK(cs_main, return m_node.chainman->ActiveChain().Tip()->GetBlockTime())};
42+
SetMockTime(genesis_time + 3 * 60);
43+
44+
block_template = mining->createNewBlock(options);
45+
BOOST_REQUIRE(block_template);
46+
47+
// The template should use the mocked system time
48+
BOOST_REQUIRE_EQUAL(block_template->getBlockHeader().nTime, genesis_time + 3 * 60);
49+
50+
const BlockWaitOptions wait_options{.timeout = MillisecondsDouble{0}, .fee_threshold = 1};
51+
52+
// waitNext() should return nullptr because there is no better template
53+
auto should_be_nullptr = block_template->waitNext(wait_options);
54+
BOOST_REQUIRE(should_be_nullptr == nullptr);
55+
56+
// This remains the case when exactly 20 minutes have gone by
57+
{
58+
LOCK(cs_main);
59+
SetMockTime(m_node.chainman->ActiveChain().Tip()->GetBlockTime() + 20 * 60);
60+
}
61+
should_be_nullptr = block_template->waitNext(wait_options);
62+
BOOST_REQUIRE(should_be_nullptr == nullptr);
63+
64+
// One second later the difficulty drops and it returns a new template
65+
// Note that we can't test the actual difficulty change, because the
66+
// difficulty is already at 1.
67+
{
68+
LOCK(cs_main);
69+
SetMockTime(m_node.chainman->ActiveChain().Tip()->GetBlockTime() + 20 * 60 + 1);
70+
}
71+
block_template = block_template->waitNext(wait_options);
72+
BOOST_REQUIRE(block_template);
73+
}
74+
75+
BOOST_AUTO_TEST_SUITE_END()

src/test/util/setup_common.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ struct RegTestingSetup : public TestingSetup {
130130
: TestingSetup{ChainType::REGTEST} {}
131131
};
132132

133+
/** Identical to TestingSetup, but chain set to testnet4 */
134+
struct Testnet4Setup : public TestingSetup {
135+
Testnet4Setup()
136+
: TestingSetup{ChainType::TESTNET4} {}
137+
};
138+
133139
class CBlock;
134140
struct CMutableTransaction;
135141
class CScript;

0 commit comments

Comments
 (0)