Skip to content
Bitcoin Roots

v31.1-roots.1 · Commit 4: [PATCH 04/17] feat(mining): preserve Roots priority and template policies

src/node/miner.cpp

Before: src/node/miner.cpp · After: src/node/miner.cpp

File metadata
index 7ea8d10b73064724ab4369aca08cc7e6e6342cd8..1d1fd69ac866973b5026488e77c30f12a29fec9c 100644

Download original patch · Release notes

Diff rows 1–248 of 248. Line numbers refer to the original before/after files.

+ Added− RemovedUnmarked lines are context.

Scroll the code area horizontally to read long lines. Before is on the left; after is on the right. Unified view fits narrow screens more easily.

src/node/miner.cpp CHANGED
@@ -28,6 +28,7 @@
28
  #include <validation.h>
29
 
30
  #include <algorithm>
 
31
  #include <utility>
32
  #include <numeric>
33
 
@@ -81,6 +82,8 @@ static BlockAssembler::Options ClampOptions(BlockAssembler::Options options)
81
  // Apply DEFAULT_BLOCK_RESERVED_WEIGHT when the caller left it unset.
82
  options.block_reserved_weight = std::clamp<size_t>(options.block_reserved_weight.value_or(DEFAULT_BLOCK_RESERVED_WEIGHT), MINIMUM_BLOCK_RESERVED_WEIGHT, MAX_BLOCK_WEIGHT);
83
  options.coinbase_output_max_additional_sigops = std::clamp<size_t>(options.coinbase_output_max_additional_sigops, 0, MAX_BLOCK_SIGOPS_COST);
 
 
84
  // Limit weight to between block_reserved_weight and MAX_BLOCK_WEIGHT for sanity:
85
  // block_reserved_weight can safely exceed -blockmaxweight, but the rest of the block template will be empty.
86
  options.nBlockMaxWeight = std::clamp<size_t>(options.nBlockMaxWeight, *options.block_reserved_weight, MAX_BLOCK_WEIGHT);
@@ -98,7 +101,16 @@ BlockAssembler::BlockAssembler(Chainstate& chainstate, const CTxMemPool* mempool
98
  void ApplyArgsManOptions(const ArgsManager& args, BlockAssembler::Options& options)
99
  {
100
  // Block resource limits
101
- options.nBlockMaxWeight = args.GetIntArg("-blockmaxweight", options.nBlockMaxWeight);
 
 
 
 
 
 
 
 
 
102
  if (const auto blockmintxfee{args.GetArg("-blockmintxfee")}) {
103
  if (const auto parsed{ParseMoney(*blockmintxfee)}) options.blockMinFeeRate = CFeeRate{*parsed};
104
  }
@@ -111,6 +123,8 @@ void ApplyArgsManOptions(const ArgsManager& args, BlockAssembler::Options& optio
111
  void BlockAssembler::resetBlock()
112
  {
113
  // Reserve space for fixed-size block header, txs count, and coinbase tx.
 
 
114
  nBlockWeight = *Assert(m_options.block_reserved_weight);
115
  nBlockSigOpsCost = m_options.coinbase_output_max_additional_sigops;
116
 
@@ -149,9 +163,8 @@ std::unique_ptr<CBlockTemplate> BlockAssembler::CreateNewBlock()
149
 
150
  if (m_mempool) {
151
  LOCK(m_mempool->cs);
152
- m_mempool->StartBlockBuilding();
153
  addChunks();
154
- m_mempool->StopBlockBuilding();
155
  }
156
 
157
  const auto time_1{SteadyClock::now()};
@@ -236,8 +249,9 @@ std::unique_ptr<CBlockTemplate> BlockAssembler::CreateNewBlock()
236
  return std::move(pblocktemplate);
237
  }
238
 
239
- bool BlockAssembler::TestChunkBlockLimits(FeePerWeight chunk_feerate, int64_t chunk_sigops_cost) const
240
  {
 
241
  if (nBlockWeight + chunk_feerate.size >= m_options.nBlockMaxWeight) {
242
  return false;
243
  }
@@ -261,6 +275,8 @@ bool BlockAssembler::TestChunkTransactions(const std::vector<CTxMemPoolEntryRef>
261
 
262
  void BlockAssembler::AddToBlock(const CTxMemPoolEntry& entry)
263
  {
 
 
264
  pblocktemplate->block.vtx.emplace_back(entry.GetSharedTx());
265
  pblocktemplate->vTxFees.push_back(entry.GetFee());
266
  pblocktemplate->vTxSigOpsCost.push_back(entry.GetSigOpCost());
@@ -276,60 +292,135 @@ void BlockAssembler::AddToBlock(const CTxMemPoolEntry& entry)
276
  }
277
  }
278
 
279
- void BlockAssembler::addChunks()
 
 
 
280
  {
281
- // Limit the number of attempts to add transactions to the block when it is
282
- // close to full; this is just a simple heuristic to finish quickly if the
283
- // mempool has a lot of entries.
284
- const int64_t MAX_CONSECUTIVE_FAILURES = 1000;
285
- constexpr int32_t BLOCK_FULL_ENOUGH_WEIGHT_DELTA = 4000;
286
- int64_t nConsecutiveFailed = 0;
287
-
288
- std::vector<CTxMemPoolEntry::CTxMemPoolEntryRef> selected_transactions;
289
- selected_transactions.reserve(MAX_CLUSTER_COUNT_LIMIT);
290
- FeePerWeight chunk_feerate;
 
 
291
 
292
- // This fills selected_transactions
293
- chunk_feerate = m_mempool->GetBlockBuilderChunk(selected_transactions);
294
- FeePerVSize chunk_feerate_vsize = ToFeePerVSize(chunk_feerate);
 
 
295
 
296
- while (selected_transactions.size() > 0) {
297
- // Check to see if min fee rate is still respected.
298
- if (chunk_feerate_vsize << m_options.blockMinFeeRate.GetFeePerVSize()) {
299
- // Everything else we might consider has a lower feerate
300
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  }
302
-
303
- int64_t chunk_sig_ops = 0;
304
- for (const auto& tx : selected_transactions) {
305
- chunk_sig_ops += tx.get().GetSigOpCost();
 
 
 
 
 
 
 
 
 
306
  }
 
 
307
 
308
- // Check to see if this chunk will fit.
309
- if (!TestChunkBlockLimits(chunk_feerate, chunk_sig_ops) || !TestChunkTransactions(selected_transactions)) {
310
- // This chunk won't fit, so we skip it and will try the next best one.
311
- m_mempool->SkipBuilderChunk();
312
- ++nConsecutiveFailed;
313
-
314
- if (nConsecutiveFailed > MAX_CONSECUTIVE_FAILURES && nBlockWeight +
315
- BLOCK_FULL_ENOUGH_WEIGHT_DELTA > m_options.nBlockMaxWeight) {
316
- // Give up if we're close to full and haven't succeeded in a while
317
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  } else {
320
- m_mempool->IncludeBuilderChunk();
321
-
322
- // This chunk will fit, so add it to the block.
323
- nConsecutiveFailed = 0;
324
- for (const auto& tx : selected_transactions) {
325
- AddToBlock(tx);
326
- }
327
- pblocktemplate->m_package_feerates.emplace_back(chunk_feerate_vsize);
328
  }
329
-
330
- selected_transactions.clear();
331
- chunk_feerate = m_mempool->GetBlockBuilderChunk(selected_transactions);
332
- chunk_feerate_vsize = ToFeePerVSize(chunk_feerate);
333
  }
334
  }
335
 
 
28
  #include <validation.h>
29
 
30
  #include <algorithm>
31
+ #include <map>
32
  #include <utility>
33
  #include <numeric>
34
 
 
82
  // Apply DEFAULT_BLOCK_RESERVED_WEIGHT when the caller left it unset.
83
  options.block_reserved_weight = std::clamp<size_t>(options.block_reserved_weight.value_or(DEFAULT_BLOCK_RESERVED_WEIGHT), MINIMUM_BLOCK_RESERVED_WEIGHT, MAX_BLOCK_WEIGHT);
84
  options.coinbase_output_max_additional_sigops = std::clamp<size_t>(options.coinbase_output_max_additional_sigops, 0, MAX_BLOCK_SIGOPS_COST);
85
+ options.nBlockMaxSize = std::clamp<size_t>(options.nBlockMaxSize, DEFAULT_BLOCK_RESERVED_SIZE, MAX_BLOCK_SERIALIZED_SIZE);
86
+ options.nBlockPrioritySize = std::min(options.nBlockPrioritySize, options.nBlockMaxSize);
87
  // Limit weight to between block_reserved_weight and MAX_BLOCK_WEIGHT for sanity:
88
  // block_reserved_weight can safely exceed -blockmaxweight, but the rest of the block template will be empty.
89
  options.nBlockMaxWeight = std::clamp<size_t>(options.nBlockMaxWeight, *options.block_reserved_weight, MAX_BLOCK_WEIGHT);
 
101
  void ApplyArgsManOptions(const ArgsManager& args, BlockAssembler::Options& options)
102
  {
103
  // Block resource limits
104
+ const bool weight_set{args.IsArgSet("-blockmaxweight")};
105
+ if (weight_set) {
106
+ options.nBlockMaxWeight = args.GetIntArg("-blockmaxweight", DEFAULT_BLOCK_MAX_WEIGHT);
107
+ options.nBlockMaxSize = MAX_BLOCK_SERIALIZED_SIZE;
108
+ }
109
+ if (args.IsArgSet("-blockmaxsize")) {
110
+ options.nBlockMaxSize = args.GetIntArg("-blockmaxsize", MAX_BLOCK_SERIALIZED_SIZE);
111
+ if (!weight_set) options.nBlockMaxWeight = MAX_BLOCK_WEIGHT;
112
+ }
113
+ options.nBlockPrioritySize = args.GetIntArg("-blockprioritysize", options.nBlockPrioritySize);
114
  if (const auto blockmintxfee{args.GetArg("-blockmintxfee")}) {
115
  if (const auto parsed{ParseMoney(*blockmintxfee)}) options.blockMinFeeRate = CFeeRate{*parsed};
116
  }
 
123
  void BlockAssembler::resetBlock()
124
  {
125
  // Reserve space for fixed-size block header, txs count, and coinbase tx.
126
+ m_in_block.clear();
127
+ nBlockSize = DEFAULT_BLOCK_RESERVED_SIZE;
128
  nBlockWeight = *Assert(m_options.block_reserved_weight);
129
  nBlockSigOpsCost = m_options.coinbase_output_max_additional_sigops;
130
 
 
163
 
164
  if (m_mempool) {
165
  LOCK(m_mempool->cs);
166
+ addPriorityTxs();
167
  addChunks();
 
168
  }
169
 
170
  const auto time_1{SteadyClock::now()};
 
249
  return std::move(pblocktemplate);
250
  }
251
 
252
+ bool BlockAssembler::TestChunkBlockLimits(FeePerWeight chunk_feerate, int64_t chunk_sigops_cost, uint64_t chunk_size) const
253
  {
254
+ if (nBlockSize + chunk_size >= m_options.nBlockMaxSize) return false;
255
  if (nBlockWeight + chunk_feerate.size >= m_options.nBlockMaxWeight) {
256
  return false;
257
  }
 
275
 
276
  void BlockAssembler::AddToBlock(const CTxMemPoolEntry& entry)
277
  {
278
+ Assert(m_in_block.insert(&entry).second);
279
+ nBlockSize += GetSerializeSize(TX_WITH_WITNESS(entry.GetTx()));
280
  pblocktemplate->block.vtx.emplace_back(entry.GetSharedTx());
281
  pblocktemplate->vTxFees.push_back(entry.GetFee());
282
  pblocktemplate->vTxSigOpsCost.push_back(entry.GetSigOpCost());
 
292
  }
293
  }
294
 
295
+ namespace {
296
+ // Recompute from the active UTXO set, so confirmed former mempool parents age
297
+ // correctly after a tip change without maintaining an ancestor/priority cache.
298
+ double CoinAgePriority(const CTxMemPoolEntry& entry, const CCoinsViewCache& coins, int height)
299
  {
300
+ unsigned int modified_size = entry.GetTxSize();
301
+ double coin_age{0};
302
+ for (const auto& input : entry.GetTx().vin) {
303
+ const unsigned int offset = 41U + std::min<unsigned int>(110, input.scriptSig.size());
304
+ if (modified_size > offset) modified_size -= offset;
305
+ const Coin& coin = coins.AccessCoin(input.prevout);
306
+ if (!coin.IsSpent() && coin.nHeight <= height) {
307
+ coin_age += double(coin.out.nValue) * (height - coin.nHeight);
308
+ }
309
+ }
310
+ return modified_size ? coin_age / modified_size : 0;
311
+ }
312
 
313
+ struct ResidualEntry : TxGraph::Ref {
314
+ const CTxMemPoolEntry* entry;
315
+ explicit ResidualEntry(const CTxMemPoolEntry* tx_entry) : entry{tx_entry} {}
316
+ };
317
+ } // namespace
318
 
319
+ void BlockAssembler::addPriorityTxs()
320
+ {
321
+ if (m_options.nBlockPrioritySize == 0) return;
322
+ using Candidate = std::pair<double, const CTxMemPoolEntry*>;
323
+ // Roots priority ties use actual individual fee/vsize, then descending txid.
324
+ const auto compare = [](const Candidate& a, const Candidate& b) {
325
+ if (a.first != b.first) return a.first < b.first;
326
+ const FeePerVSize a_fee{a.second->GetFee(), a.second->GetTxSize()};
327
+ const FeePerVSize b_fee{b.second->GetFee(), b.second->GetTxSize()};
328
+ if (const auto fee_order = FeeRateCompare(a_fee, b_fee); fee_order != 0) return fee_order < 0;
329
+ return a.second->GetTx().GetHash() < b.second->GetTx().GetHash();
330
+ };
331
+ std::vector<Candidate> candidates;
332
+ std::map<const CTxMemPoolEntry*, double> waiting;
333
+ candidates.reserve(m_mempool->mapTx.size());
334
+ for (const auto& entry : m_mempool->mapTx) {
335
+ candidates.emplace_back(CoinAgePriority(entry, m_chainstate.CoinsTip(), nHeight), &entry);
336
+ }
337
+ std::make_heap(candidates.begin(), candidates.end(), compare);
338
+ while (!candidates.empty()) {
339
+ const auto [priority, entry] = candidates.front();
340
+ std::pop_heap(candidates.begin(), candidates.end(), compare);
341
+ candidates.pop_back();
342
+ Assert(!m_in_block.contains(entry));
343
+ const auto parents = m_mempool->GetParents(*entry);
344
+ if (std::ranges::any_of(parents, [&](const auto& parent) { return !m_in_block.contains(&parent.get()); })) {
345
+ waiting.emplace(entry, priority);
346
+ continue;
347
  }
348
+ const uint64_t size = GetSerializeSize(TX_WITH_WITNESS(entry->GetTx()));
349
+ if (!TestChunkBlockLimits({entry->GetModifiedFee(), static_cast<int32_t>(entry->GetAdjustedWeight())}, entry->GetSigOpCost(), size) ||
350
+ !IsFinalTx(entry->GetTx(), nHeight, m_lock_time_cutoff)) continue;
351
+ AddToBlock(*entry);
352
+ // Preserve Roots' threshold behavior: the final selected transaction can
353
+ // cross the area boundary or be the first at/below the priority threshold.
354
+ if (nBlockSize >= m_options.nBlockPrioritySize || priority <= MINIMUM_TX_PRIORITY) break;
355
+ for (const auto& child : m_mempool->GetChildren(*entry)) {
356
+ auto it = waiting.find(&child.get());
357
+ if (it == waiting.end()) continue;
358
+ candidates.emplace_back(it->second, it->first);
359
+ std::push_heap(candidates.begin(), candidates.end(), compare);
360
+ waiting.erase(it);
361
  }
362
+ }
363
+ }
364
 
365
+ void BlockAssembler::addChunks()
366
+ {
367
+ // Native builders forbid graph mutation while alive. Construct the residual
368
+ // before its builder and destroy the builder before its refs/graph. No live
369
+ // mempool graph mutation, staging, admission or fee update occurs here.
370
+ // The residual is a subset of the admitted graph, retaining L8's aggregate
371
+ // signed fee/weight budgets and cluster bounds. Storage/work is O(mempool)
372
+ // plus native bounded cluster optimization; the disabled/empty-prefix path
373
+ // uses the existing graph directly without copying it.
374
+ std::unique_ptr<TxGraph> residual;
375
+ std::map<Txid, ResidualEntry> refs;
376
+ if (!m_in_block.empty()) {
377
+ residual = MakeTxGraph(m_mempool->m_opts.limits.cluster_count,
378
+ m_mempool->m_opts.limits.cluster_size_vbytes * WITNESS_SCALE_FACTOR,
379
+ ACCEPTABLE_COST, [](const TxGraph::Ref& a, const TxGraph::Ref& b) {
380
+ return static_cast<const ResidualEntry&>(a).entry->GetTx().GetHash() <=>
381
+ static_cast<const ResidualEntry&>(b).entry->GetTx().GetHash();
382
+ });
383
+ for (const auto& entry : m_mempool->mapTx) {
384
+ if (m_in_block.contains(&entry)) continue;
385
+ auto [it, inserted] = refs.try_emplace(entry.GetTx().GetHash(), &entry);
386
+ Assert(inserted);
387
+ residual->AddTransaction(it->second, {entry.GetModifiedFee(), static_cast<int32_t>(entry.GetAdjustedWeight())});
388
+ }
389
+ for (auto& [txid, ref] : refs) {
390
+ for (const auto& parent : m_mempool->GetParents(*ref.entry)) {
391
+ auto it = refs.find(parent.get().GetTx().GetHash());
392
+ if (it != refs.end()) residual->AddDependency(it->second, ref);
393
  }
394
+ }
395
+ }
396
+ auto builder = residual ? residual->GetBlockBuilder() : m_mempool->MakeBlockBuilder();
397
+ const int64_t MAX_CONSECUTIVE_FAILURES = 1000;
398
+ constexpr int32_t BLOCK_FULL_ENOUGH_WEIGHT_DELTA = 4000;
399
+ int64_t consecutive_failed{0};
400
+ while (auto chunk = builder->GetCurrentChunk()) {
401
+ const auto& [chunk_refs, chunk_feerate] = *chunk;
402
+ const FeePerVSize feerate = ToFeePerVSize(chunk_feerate);
403
+ if (feerate << m_options.blockMinFeeRate.GetFeePerVSize()) return;
404
+ std::vector<CTxMemPoolEntryRef> entries;
405
+ int64_t sigops{0};
406
+ uint64_t size{0};
407
+ for (const auto* ref : chunk_refs) {
408
+ const auto& entry = residual ? *static_cast<const ResidualEntry*>(ref)->entry :
409
+ static_cast<const CTxMemPoolEntry&>(*ref);
410
+ entries.emplace_back(entry);
411
+ sigops += entry.GetSigOpCost();
412
+ size += GetSerializeSize(TX_WITH_WITNESS(entry.GetTx()));
413
+ }
414
+ if (!TestChunkBlockLimits(chunk_feerate, sigops, size) || !TestChunkTransactions(entries)) {
415
+ builder->Skip();
416
+ if (++consecutive_failed > MAX_CONSECUTIVE_FAILURES &&
417
+ nBlockWeight + BLOCK_FULL_ENOUGH_WEIGHT_DELTA > m_options.nBlockMaxWeight) return;
418
  } else {
419
+ builder->Include();
420
+ consecutive_failed = 0;
421
+ for (const auto& entry : entries) AddToBlock(entry);
422
+ pblocktemplate->m_package_feerates.emplace_back(feerate);
 
 
 
 
423
  }
 
 
 
 
424
  }
425
  }
426