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. The two line-number columns refer to before and after.

src/node/miner.cpp CHANGED
@@ -28,6 +28,7 @@
28
28
  #include <validation.h>
29
29
 
30
30
  #include <algorithm>
31
+ #include <map>
31
32
  #include <utility>
32
33
  #include <numeric>
33
34
 
@@ -81,6 +82,8 @@ static BlockAssembler::Options ClampOptions(BlockAssembler::Options options)
81
82
  // Apply DEFAULT_BLOCK_RESERVED_WEIGHT when the caller left it unset.
82
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);
83
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);
84
87
  // Limit weight to between block_reserved_weight and MAX_BLOCK_WEIGHT for sanity:
85
88
  // block_reserved_weight can safely exceed -blockmaxweight, but the rest of the block template will be empty.
86
89
  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
101
  void ApplyArgsManOptions(const ArgsManager& args, BlockAssembler::Options& options)
99
102
  {
100
103
  // Block resource limits
101
- options.nBlockMaxWeight = args.GetIntArg("-blockmaxweight", options.nBlockMaxWeight);
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);
102
114
  if (const auto blockmintxfee{args.GetArg("-blockmintxfee")}) {
103
115
  if (const auto parsed{ParseMoney(*blockmintxfee)}) options.blockMinFeeRate = CFeeRate{*parsed};
104
116
  }
@@ -111,6 +123,8 @@ void ApplyArgsManOptions(const ArgsManager& args, BlockAssembler::Options& optio
111
123
  void BlockAssembler::resetBlock()
112
124
  {
113
125
  // Reserve space for fixed-size block header, txs count, and coinbase tx.
126
+ m_in_block.clear();
127
+ nBlockSize = DEFAULT_BLOCK_RESERVED_SIZE;
114
128
  nBlockWeight = *Assert(m_options.block_reserved_weight);
115
129
  nBlockSigOpsCost = m_options.coinbase_output_max_additional_sigops;
116
130
 
@@ -149,9 +163,8 @@ std::unique_ptr<CBlockTemplate> BlockAssembler::CreateNewBlock()
149
163
 
150
164
  if (m_mempool) {
151
165
  LOCK(m_mempool->cs);
152
- m_mempool->StartBlockBuilding();
166
+ addPriorityTxs();
153
167
  addChunks();
154
- m_mempool->StopBlockBuilding();
155
168
  }
156
169
 
157
170
  const auto time_1{SteadyClock::now()};
@@ -236,8 +249,9 @@ std::unique_ptr<CBlockTemplate> BlockAssembler::CreateNewBlock()
236
249
  return std::move(pblocktemplate);
237
250
  }
238
251
 
239
- bool BlockAssembler::TestChunkBlockLimits(FeePerWeight chunk_feerate, int64_t chunk_sigops_cost) const
252
+ bool BlockAssembler::TestChunkBlockLimits(FeePerWeight chunk_feerate, int64_t chunk_sigops_cost, uint64_t chunk_size) const
240
253
  {
254
+ if (nBlockSize + chunk_size >= m_options.nBlockMaxSize) return false;
241
255
  if (nBlockWeight + chunk_feerate.size >= m_options.nBlockMaxWeight) {
242
256
  return false;
243
257
  }
@@ -261,6 +275,8 @@ bool BlockAssembler::TestChunkTransactions(const std::vector<CTxMemPoolEntryRef>
261
275
 
262
276
  void BlockAssembler::AddToBlock(const CTxMemPoolEntry& entry)
263
277
  {
278
+ Assert(m_in_block.insert(&entry).second);
279
+ nBlockSize += GetSerializeSize(TX_WITH_WITNESS(entry.GetTx()));
264
280
  pblocktemplate->block.vtx.emplace_back(entry.GetSharedTx());
265
281
  pblocktemplate->vTxFees.push_back(entry.GetFee());
266
282
  pblocktemplate->vTxSigOpsCost.push_back(entry.GetSigOpCost());
@@ -276,60 +292,135 @@ void BlockAssembler::AddToBlock(const CTxMemPoolEntry& entry)
276
292
  }
277
293
  }
278
294
 
279
- void BlockAssembler::addChunks()
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)
280
299
  {
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;
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
+ }
291
312
 
292
- // This fills selected_transactions
293
- chunk_feerate = m_mempool->GetBlockBuilderChunk(selected_transactions);
294
- FeePerVSize chunk_feerate_vsize = ToFeePerVSize(chunk_feerate);
313
+ struct ResidualEntry : TxGraph::Ref {
314
+ const CTxMemPoolEntry* entry;
315
+ explicit ResidualEntry(const CTxMemPoolEntry* tx_entry) : entry{tx_entry} {}
316
+ };
317
+ } // namespace
295
318
 
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;
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;
301
347
  }
302
-
303
- int64_t chunk_sig_ops = 0;
304
- for (const auto& tx : selected_transactions) {
305
- chunk_sig_ops += tx.get().GetSigOpCost();
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);
306
361
  }
362
+ }
363
+ }
307
364
 
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;
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);
318
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;
319
418
  } 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);
419
+ builder->Include();
420
+ consecutive_failed = 0;
421
+ for (const auto& entry : entries) AddToBlock(entry);
422
+ pblocktemplate->m_package_feerates.emplace_back(feerate);
328
423
  }
329
-
330
- selected_transactions.clear();
331
- chunk_feerate = m_mempool->GetBlockBuilderChunk(selected_transactions);
332
- chunk_feerate_vsize = ToFeePerVSize(chunk_feerate);
333
424
  }
334
425
  }
335
426