{
 "number": 34360,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/34360",
 "title": "bench: add WalletBalanceManySpent for high-history wallet scenario",
 "author": "w0xlt",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-01-21T03:15:17Z",
 "updated_at": "2026-09-09T14:37:18Z",
 "age_days": 239,
 "draft": true,
 "labels": [
  "Tests",
  "Needs rebase",
  "CI failed"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "7971f410b38fb4dc55712e57cf1ac29a6d52be88",
 "head_ref": "wallet-balance-many-spent-bench-3",
 "head_repo": "w0xlt/bitcoin",
 "head_history": [
  {
   "t": "2026-05-22T00:26:46Z",
   "sha": "058e97c1593d1edc840cfccedd066722e6c09651"
  },
  {
   "t": "2026-05-22T01:10:39Z",
   "sha": "622265f7fba2d0c08ae77091526dc36f5faa30d9"
  },
  {
   "t": "2026-05-22T02:03:40Z",
   "sha": "5defb96252e0b955d1a3863f78a87ca064d9071a"
  },
  {
   "t": "2026-08-18T22:48:11Z",
   "sha": "7971f410b38fb4dc55712e57cf1ac29a6d52be88"
  }
 ],
 "additions": 237,
 "deletions": 64,
 "changed_files": 3,
 "commit_count": 2,
 "size_bucket": "M",
 "mergeable_state": "dirty",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "stale_ack": [
     {
      "login": "l0rinc",
      "url": "https://github.com/bitcoin/bitcoin/pull/34360#issuecomment-4519980069"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36181,
     "title": "ci: Upgrade IWYU to 0.27 compatible with Clang 23",
     "author": "hebasto"
    },
    {
     "number": 35511,
     "title": "RFC: consensus: Make `CAmount` a class",
     "author": "hodlinator"
    }
   ]
  }
 },
 "acks_parsed": {
  "l0rinc": {
   "kind": "ack",
   "hash": "5defb96252e0b955d1a3863f78a87ca064d9071a",
   "t": "2026-05-22T15:20:14Z",
   "stale": true
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 1,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 1,
  "distinct_reviewers": [
   "fanquake",
   "l0rinc",
   "sedited"
  ]
 },
 "signals": {
  "needs_rebase": true,
  "ci_failed": true,
  "mergeable_state": "dirty",
  "last_author_activity": "2026-08-18T22:48:41Z",
  "last_reviewer_activity": "2026-08-10T19:34:39Z",
  "last_reviewer": "sedited",
  "author_silent_days": 29,
  "waiting_on_author_days": 0,
  "days_since_update": 8
 },
 "refs": {
  "mentioned": [
   27865
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 27865,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "wallet: Track no-longer-spendable TXOs separately"
   }
  ],
  "conflicts": [
   36181,
   35511
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/bench/wallet_balance.cpp",
  "src/bench/wallet_bench_util.h"
 ],
 "body": "This PR introduces a new benchmark that measures `GetBalance()` performance on wallets with extensive transaction history where the majority of outputs have already been spent.\n\nIt was created to test #27865, which separates spent TXOs from spendable ones.\nThis pattern mirrors real-world usage in high-activity wallets such as Lightning nodes, exchanges, hot wallets, and payment processors.\n\n**Setup:**\n- Creates 500 blocks \u00d7 100 coinbase outputs = 50,000 wallet TXOs\n- Matures all coinbases with COINBASE_MATURITY additional blocks\n- Spends 49,950 outputs in batched transactions (confirmed in blocks)\n- Consolidation outputs go to a burn script to avoid inflating wallet TXO count\n\nTo keep the setup time reasonable, the benchmark uses the fast \u201cfake block\u201d approach, similar to `wallet_create_tx.cpp`.",
 "commits": [
  {
   "sha": "f61c644ac0c40730347627208e8e61e56cd37707",
   "date": "2026-08-18T22:08:26Z",
   "message": "bench: share wallet fake block generation\n\nCo-authored-by: L\u0151rinc <pap.lorinc@gmail.com>"
  },
  {
   "sha": "7971f410b38fb4dc55712e57cf1ac29a6d52be88",
   "date": "2026-08-18T22:09:20Z",
   "message": "bench: add WalletBalanceManySpent\n\nCo-authored-by: L\u0151rinc <pap.lorinc@gmail.com>"
  }
 ],
 "timeline": [
  {
   "t": "2026-03-03T14:13:41Z",
   "kind": "comment",
   "who": "fanquake",
   "assoc": "MEMBER",
   "text": "cc @achow101 given this is inspired by #27865."
  },
  {
   "t": "2026-05-14T10:44:18Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "@w0xlt can you rebase this?"
  },
  {
   "t": "2026-05-15T10:44:59Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": null,
   "text": "For clarity on what the unnamed primitive arguments mean, please consider adding name hints:\n```suggestion\n        GenerateFakeBlock(params, test_setup->m_node, wallet, burn_script, /*num_coinbase_outputs=*/1, /*total_coinbase_value=*/50 * COIN, block_txs);\n```\nor even better, we could extract these as suggested in https://github.com/bitcoin/bitcoin/pull/34360/changes#r3247900786\nand use them as\n```suggestion\n        GenerateFakeBlock(params, test_setup->m_node, wallet, burn_script, FILLER_OUTPUTS, FILLER_VALUE, block_txs);\n```"
  },
  {
   "t": "2026-05-15T10:59:09Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": null,
   "text": "we could make the benchmark more stable and avoid:\n[quoted text omitted]\n\nby specifying the minimum iteration count, something like:\n```suggestion\n    bench.minEpochIterations(10).run([&] {\n```"
  },
  {
   "t": "2026-05-15T11:24:31Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": null,
   "text": "Not sure how to interpret this, why would we want to run a benchmark with `0`?\nThe description of the PR claims we have 50k total outputs, we could reserve and assert that for clarity:\n```C++\nconstexpr size_t TOTAL_WALLET_OUTPUTS{NUM_BLOCKS * OUTPUTS_PER_BLOCK};\n...\nstd::vector<COutPoint> outputs_to_spend;\noutputs_to_spend.reserve(TOTAL_WALLET_OUTPUTS);\n...\nconstexpr size_t KEEP_UNSPENT{50};\nassert(outputs_to_spend.size() == TOTAL_WALLET_OUTPUTS);\nconst size_t num_to_spend{outputs_to_spend.size() - KEEP_UNSPENT};\n```"
  },
  {
   "t": "2026-05-15T11:26:54Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": null,
   "text": "nit: this basically duplicates the logic of the previous loop - alternatively it may be simpler to understand it if we reused the logic:\n```suggestion\n        CAmount total_value{0};\n        for (size_t j{i}; j < batch_end; ++j) {\n            spend_tx.vin.emplace_back(outputs_to_spend[j]);\n            total_value += OUTPUT_VALUE;\n        }\n\n        // Send to burn script so consolidation outputs don't add to wallet TXOs\n```"
  },
  {
   "t": "2026-05-15T11:37:18Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": null,
   "text": "these intertwined constant definitions kinda' break the abstraction for me: the point of defining them is that we shouldn't care about their exact value when we're using them.\nIf you agree, please consider extracting the constant initializations either at the beginning or as fields (and maybe use brace init to avoid narrowing and weird type conversions).\nThat would separate the concerns: when we're interested in the configuration of the benchmark (the values used) we have that concern in one dedicated place and when we're interested in the algorithm (where we want to avoid knowing the implementation details) we can just rely on the config names:\n```C++\nstatic void WalletBalanceManySpent(benchmark::Bench& bench)\n{\n    constexpr int NUM_BLOCKS{500};\n    constexpr int OUTPUTS_PER_BLOCK{100};\n    constexpr CAmount OUTPUT_VALUE{50 * COIN / OUTPUTS_PER_BLOCK};\n    constexpr size_t TOTAL_WALLET_OUTPUTS{NUM_BLOCKS * OUTPUTS_PER_BLOCK};\n    constexpr size_t KEEP_UNSPENT{50};\n    constexpr size_t BATCH_SIZE{100};\n    constexpr CAmount FEE{1000};\n    constexpr size_t TXS_PER_BLOCK{10};\n    constexpr int FILLER_OUTPUTS{1};\n    constexpr CAmount FILLER_VALUE{50 * COIN};\n...\n```"
  },
  {
   "t": "2026-05-15T11:38:51Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": null,
   "text": "nit: these comments lock in the values of the configuration options above. Code comments are meant to add extra context that the code can't easily explain. Here, it's partially duplicating the code.\nPlease consider deduplicating the values from their meanings:\n```C++\n    // Target: many spent TXOs, few unspent TXOs.\n```\nand\n```C++\n    // Phase 1: Create the configured wallet-owned output history.\n```\nand\n```C++\n    // Keep the configured small remainder unspent, and spend the rest.\n```\netc."
  },
  {
   "t": "2026-05-15T11:41:15Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": null,
   "text": "This would still pass if setup accidentally left more spendable wallet outputs than intended.\nSince the benchmark claims only the configured remainder stays unspent, could we assert the exact trusted balance?\n```C++\nCAmount expected_balance{0};\nfor (size_t i{0}; i < KEEP_UNSPENT; ++i) {\n    expected_balance += OUTPUT_VALUE;\n}\n...\nassert(bal.m_mine_trusted == expected_balance);\nassert(bal.m_mine_immature == 0);\n```"
  },
  {
   "t": "2026-05-15T11:45:38Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": null,
   "text": "Why is this needed exactly? Wouldn't a `bench.warmup(1)` achieve the same? That would also allow us to define `bal` inside:\n```C++\nbench.warmup(1).minEpochIterations(128).run([&] {\n    const auto bal{GetBalance(wallet)};\n    assert(bal.m_mine_trusted == expected_balance);\n    assert(bal.m_mine_immature == 0);\n});\n```\n\nThough it kinda' begs the question: don't we want to measure the uncached state at all? Is caching referring to an internal state, i.e. the first iteration being different from the rest?"
  },
  {
   "t": "2026-05-15T11:58:34Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": null,
   "text": "Is this really needed here? It walks [mapWallet](https://github.com/bitcoin/bitcoin/blob/cad5f5604582eac95bcfbf08cca8af1c186c36eb/src/wallet/wallet.cpp#L989) and invalidates per-transaction caches, while this confirmed-output balance path iterates [wallet.GetTXOs()](https://github.com/bitcoin/bitcoin/blob/cd7865b0ce17568945c7f72db0248489cf284787/src/wallet/receive.cpp#L252), checks spend status, and reads each remaining output value.\nThe benchmarks indicate the performance is also similar (except for the extra iteration):\n\n[quoted text omitted]\n|               ns/op |                op/s |    err% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------:|:----------\n|        2,927,937.88 |              341.54 |    0.3% |     11.01 | `WalletBalanceManySpent`\n\n[quoted text omitted]\n|               ns/op |                op/s |    err% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------:|:----------\n|        2,897,842.24 |              345.08 |    0.2% |     11.01 | `WalletBalanceManySpent`"
  },
  {
   "t": "2026-05-15T12:06:31Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "7971f410b38fb4dc55712e57cf1ac29a6d52be88",
   "in_reply_to": null,
   "text": "My understanding is that balance alone does not prove that spent wallet outputs remain in history, consider adding a sanity check here (should probably be done via a helper method to make it less noisy):\n```C++\nsize_t owned_outputs{0}, spent_outputs{0};\n{\n    LOCK(wallet.cs_wallet);\n    for (const auto& [outpoint, _] : wallet.GetTXOs()) {\n        ++owned_outputs;\n        spent_outputs += wallet.IsSpent(outpoint);\n    }\n}\nassert(owned_outputs == TOTAL_WALLET_OUTPUTS);\nassert(spent_outputs == TOTAL_WALLET_OUTPUTS - KEEP_UNSPENT);\n```"
  },
  {
   "t": "2026-05-15T12:18:21Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": null,
   "text": "We could use `NodeClockContext` here instead, like the adjacent wallet balance benchmark, so the mocked clock is restored automatically when setup leaves scope (see https://github.com/bitcoin/bitcoin/pull/34858/changes#diff-242a3d315a6189295f7169824284383659b1d83ac65241351a930348d5b8ee82L10-R39):\n```suggestion\n    NodeClockContext clock_ctx{params.GenesisBlock().Time()};\n```\n\ncc: @maflcko"
  },
  {
   "t": "2026-05-15T12:23:39Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "7971f410b38fb4dc55712e57cf1ac29a6d52be88",
   "in_reply_to": null,
   "text": "We could avoid copying the 50k COutput entries (and avoid resizing) before the spend phase by iterating the existing `AvailableCoins` result directly:\n```suggestion\n    std::vector<COutPoint> outputs_to_spend;\n    outputs_to_spend.reserve(TOTAL_WALLET_OUTPUTS);\n    {\n        LOCK(wallet.cs_wallet);\n        for (const auto& [_, coins] : AvailableCoins(wallet).coins) {\n            for (const auto& coin : coins) {\n                outputs_to_spend.push_back(coin.outpoint);\n            }\n        }\n    }\n```"
  },
  {
   "t": "2026-05-15T12:27:47Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": null,
   "text": "It seems to me we could simplify this continuous-slice copy:\n```suggestion\n        const size_t batch_end{std::min(i + TXS_PER_BLOCK, spending_txs.size())};\n        std::vector<CTransactionRef> block_txs{spending_txs.begin() + i, spending_txs.begin() + batch_end};\n```\n\n----\n\nSimilarly for\n```C++\n    // Add any extra transactions\n    for (const auto& tx : extra_txs) {\n        block.vtx.push_back(tx);\n    }\n```\nwe can probably use `vector::insert` for that append instead of spelling out an equivalent `push_back` loop in the helper:\n```C++\n    block.vtx.insert(block.vtx.end(), extra_txs.begin(), extra_txs.end());\n```"
  },
  {
   "t": "2026-05-15T12:36:26Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/bench/wallet_bench_util.h",
   "commit": "7971f410b38fb4dc55712e57cf1ac29a6d52be88",
   "in_reply_to": null,
   "text": "The original code calls `ActiveTip()` under `cs_main`, but then dereferences the returned `CBlockIndex*` after the lock is released.\nThat is probably harmless for these immutable-looking fields in this benchmark, but the helper only needs a snapshot, so it seems clearer to copy the hash/time/height while holding the lock and avoid carrying the tip pointer outside the protected region.\n\nThis also avoids the ternary, which my IDE parses poorly:\n```suggestion\ninline TipBlock GetTip(const CChainParams& params, const node::NodeContext& context)\n{\n    {\n        LOCK(::cs_main);\n        if (const auto* tip{context.chainman->ActiveTip()}) {\n            return {tip->GetBlockHash(), tip->GetBlockTime(), tip->nHeight};\n        }\n    }\n    return {params.GenesisBlock().GetHash(), params.GenesisBlock().GetBlockTime(), 0};\n}\n```"
  },
  {
   "t": "2026-05-15T12:42:28Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/bench/wallet_bench_util.h",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": null,
   "text": "Do we need to reread the active tip here or could we do it in the previous locked scope instead?\n\n```suggestion\n    CBlockIndex* pindex{nullptr};\n    {\n        LOCK(::cs_main);\n        pindex = context.chainman->m_blockman.AddToBlockIndex(block, context.chainman->m_best_header);\n        context.chainman->ActiveChain().SetTip(*pindex);\n    }\n```"
  },
  {
   "t": "2026-05-15T12:51:28Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/bench/wallet_bench_util.h",
   "commit": "7971f410b38fb4dc55712e57cf1ac29a6d52be88",
   "in_reply_to": null,
   "text": "This may be slightly bigger unrelated change (generalizing wallet_create_tx.cpp to use our newly introduced methods), but maybe the `coinbase_outputs` should be exposed as a single param:\n```suggestion\ninline void GenerateFakeBlock(const CChainParams& params,\n                              const node::NodeContext& context,\n                              CWallet& wallet,\n                              const std::vector<CTxOut>& coinbase_outputs,\n                              const std::vector<CTransactionRef>& extra_txs = {})\n```\nadd a helper delegating to this with the old signature:\n```C++\ninline void GenerateFakeBlock(const CChainParams& params,\n                              const node::NodeContext& context,\n                              CWallet& wallet,\n                              const CScript& coinbase_script,\n                              int num_coinbase_outputs,\n                              CAmount total_coinbase_value = 50 * COIN,\n                              const std::vector<CTransactionRef>& extra_txs = {})\n{\n    std::vector<CTxOut> coinbase_outputs;\n    coinbase_outputs.reserve(num_coinbase_outputs);\n    const CAmount per_output{total_coinbase_value / num_coinbase_outputs};\n    for (int i{0}; i < num_coinbase_outputs; ++i) {\n        coinbase_outputs.emplace_back(per_output, coinbase_script);\n    }\n    GenerateFakeBlock(params, context, wallet, coinbase_outputs, extra_txs);\n}\n```\nwhich would enable us to call it from the benchmark's `generateFakeBlock` and avoid a lot of duplication between `wallet_create_tx.cpp` and `wallet_bench_util.h`."
  },
  {
   "t": "2026-05-15T12:53:26Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/bench/wallet_bench_util.h",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": null,
   "text": "nit: we could initialize the fields so static analysis in my IDE stops complaining:\n```suggestion\n    uint256 prev_block_hash{};\n    int64_t prev_block_time{0};\n    int tip_height{0};\n```\n\nJust resolve if you don't think it's important."
  },
  {
   "t": "2026-05-15T13:13:43Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "CHANGES_REQUESTED",
   "commit": "42805a16091d9c8da52f7dc4e98b596de4f7d67a",
   "text": "I might have gone a bit overboard again, I left comments documenting my review journey, please see the individual explanations - feel free to pick and choose between them.\n\nFor convenience here's my local patch against your rebased branch:\nLocal pending changes\n\n```patch\ndiff --git a/src/bench/wallet_balance.cpp b/src/bench/wallet_balance.cpp\nindex ee8244501a..7ce15529d6 100644\n--- a/src/bench/wallet_balance.cpp\n+++ b/src/bench/wallet_balance.cpp\n@@ -18,6 +18,7 @@\n #include <wallet/wallet.h>\n\n #include <cassert>\n+#include <cstddef>\n #include <memory>\n #include <optional>\n #include <string>\n@@ -72,15 +73,26 @@ BENCHMARK(WalletBalanceWatch);\n\n static void WalletBalanceManySpent(benchmark::Bench& bench)\n {\n+    constexpr int NUM_BLOCKS{500};\n+    constexpr int OUTPUTS_PER_BLOCK{100};\n+    constexpr CAmount OUTPUT_VALUE{50 * COIN / OUTPUTS_PER_BLOCK};\n+    constexpr size_t TOTAL_WALLET_OUTPUTS{NUM_BLOCKS * OUTPUTS_PER_BLOCK};\n+    constexpr size_t KEEP_UNSPENT{50};\n+    constexpr size_t BATCH_SIZE{100};\n+    constexpr CAmount FEE{1000};\n+    constexpr size_t TXS_PER_BLOCK{10};\n+    constexpr int FILLER_OUTPUTS{1};\n+    constexpr CAmount FILLER_VALUE{50 * COIN};\n+\n     // Benchmark GetBalance with many spent TXOs and few unspent TXOs.\n     // This scenario benefits from optimizations that separate definitely-spent\n     // outputs from potentially-spendable ones (such as m_unusable_txos).\n     //\n-    // Target: ~50,000 spent TXOs, ~50 unspent TXOs\n+    // Target: many spent TXOs, few unspent TXOs\n     const auto test_setup = MakeNoLogFileContext<const TestingSetup>();\n     const auto& params = test_setup->m_node.chainman->GetParams();\n\n-    SetMockTime(params.GenesisBlock().nTime);\n+    NodeClockContext clock_ctx{params.GenesisBlock().Time()};\n     CWallet wallet{test_setup->m_node.chain.get(), \"\", CreateMockableWalletDatabase()};\n     {\n         LOCK(wallet.cs_wallet);\n@@ -91,55 +103,51 @@ static void WalletBalanceManySpent(benchmark::Bench& bench)\n\n     const auto dest = getNewDestination(wallet, OutputType::BECH32);\n     const CScript wallet_script = GetScriptForDestination(dest);\n-    // Burn script for outputs we don't want in the wallet\n+    // OP_TRUE keeps the output spendable without adding it to the wallet\n     const CScript burn_script{CScript() << OP_TRUE};\n\n-    // Phase 1: Create many UTXOs (100 outputs per block * 500 blocks = 50,000 TXOs)\n-    constexpr int NUM_BLOCKS = 500;\n-    constexpr int OUTPUTS_PER_BLOCK = 100;\n-    constexpr CAmount OUTPUT_VALUE = 50 * COIN / OUTPUTS_PER_BLOCK;\n-\n+    // Phase 1: Create the configured wallet-owned output history\n     for (int i = 0; i < NUM_BLOCKS; ++i) {\n         GenerateFakeBlock(params, test_setup->m_node, wallet, wallet_script, OUTPUTS_PER_BLOCK);\n     }\n\n     // Phase 2: Add COINBASE_MATURITY blocks to mature all coinbase outputs\n-    // This ensures all 50,000 TXOs are spendable\n     for (int i = 0; i < COINBASE_MATURITY; ++i) {\n-        GenerateFakeBlock(params, test_setup->m_node, wallet, burn_script, 1);\n+        GenerateFakeBlock(params, test_setup->m_node, wallet, burn_script, FILLER_OUTPUTS);\n     }\n\n     // Phase 3: Spend most UTXOs by creating spending transactions\n     // Get all available coins and spend them in batches\n     std::vector<COutPoint> outputs_to_spend;\n+    outputs_to_spend.reserve(TOTAL_WALLET_OUTPUTS);\n     {\n         LOCK(wallet.cs_wallet);\n-        auto available = AvailableCoins(wallet);\n-        for (const auto& coin : available.All()) {\n-            outputs_to_spend.push_back(coin.outpoint);\n+        for (const auto& [_, coins] : AvailableCoins(wallet).coins) {\n+            for (const auto& coin : coins) {\n+                outputs_to_spend.push_back(coin.outpoint);\n+            }\n         }\n     }\n\n-    // Keep ~50 outputs unspent, spend the rest\n-    constexpr int KEEP_UNSPENT = 50;\n-    int num_to_spend = std::max(0, static_cast<int>(outputs_to_spend.size()) - KEEP_UNSPENT);\n+    // Keep the configured small remainder unspent, and spend the rest\n+    assert(outputs_to_spend.size() == TOTAL_WALLET_OUTPUTS);\n+    const size_t num_to_spend{outputs_to_spend.size() - KEEP_UNSPENT};\n\n-    // Create spending transactions in batches of 100 inputs each\n-    constexpr int BATCH_SIZE = 100;\n-    constexpr CAmount BENCHMARK_FEE = 1000;\n+    // Create spending transactions in configured-size batches\n     std::vector<CTransactionRef> spending_txs;\n\n-    for (int i = 0; i < num_to_spend; i += BATCH_SIZE) {\n+    for (size_t i{0}; i < num_to_spend; i += BATCH_SIZE) {\n         CMutableTransaction spend_tx;\n-        int batch_end = std::min(i + BATCH_SIZE, num_to_spend);\n+        const size_t batch_end{std::min(i + BATCH_SIZE, num_to_spend)};\n\n-        for (int j = i; j < batch_end; ++j) {\n+        CAmount total_value{0};\n+        for (size_t j{i}; j < batch_end; ++j) {\n             spend_tx.vin.emplace_back(outputs_to_spend[j]);\n+            total_value += OUTPUT_VALUE;\n         }\n\n         // Send to burn script so consolidation outputs don't add to wallet TXOs\n-        CAmount total_value = (batch_end - i) * OUTPUT_VALUE;\n-        spend_tx.vout.emplace_back(total_value - BENCHMARK_FEE, burn_script);\n+        spend_tx.vout.emplace_back(total_value - FEE, burn_script);\n\n         spending_txs.push_back(MakeTransactionRef(std::move(spend_tx)));\n     }\n@@ -147,22 +155,33 @@ static void WalletBalanceManySpent(benchmark::Bench& bench)\n     // Phase 4: Confirm all spending transactions in new blocks\n     // Include multiple spending txs per block for efficiency\n     // Coinbase outputs go to burn script to avoid adding wallet TXOs\n-    constexpr int TXS_PER_BLOCK = 10;\n-    for (size_t i = 0; i < spending_txs.size(); i += TXS_PER_BLOCK) {\n-        std::vector<CTransactionRef> block_txs;\n-        for (size_t j = i; j < std::min(i + TXS_PER_BLOCK, spending_txs.size()); ++j) {\n-            block_txs.push_back(spending_txs[j]);\n+    for (size_t i{0}; i < spending_txs.size(); i += TXS_PER_BLOCK) {\n+        const size_t batch_end{std::min(i + TXS_PER_BLOCK, spending_txs.size())};\n+        std::vector<CTransactionRef> block_txs{spending_txs.begin() + i, spending_txs.begin() + batch_end};\n+        GenerateFakeBlock(params, test_setup->m_node, wallet, burn_script, FILLER_OUTPUTS, FILLER_VALUE, block_txs);\n+    }\n+\n+    // Balance alone does not prove that spent wallet outputs remain in history.\n+    size_t owned_outputs{0}, spent_outputs{0};\n+    {\n+        LOCK(wallet.cs_wallet);\n+        for (const auto& [outpoint, _] : wallet.GetTXOs()) {\n+            ++owned_outputs;\n+            spent_outputs += wallet.IsSpent(outpoint);\n         }\n-        GenerateFakeBlock(params, test_setup->m_node, wallet, burn_script, 1, 50 * COIN, block_txs);\n     }\n+    assert(owned_outputs == TOTAL_WALLET_OUTPUTS);\n+    assert(spent_outputs == TOTAL_WALLET_OUTPUTS - KEEP_UNSPENT);\n\n-    // Phase 5: Run the benchmark\n-    auto bal = GetBalance(wallet); // Cache\n+    CAmount expected_balance{0};\n+    for (size_t i{0}; i < KEEP_UNSPENT; ++i) {\n+        expected_balance += OUTPUT_VALUE;\n+    }\n\n-    bench.run([&] {\n-        wallet.MarkDirty();\n-        bal = GetBalance(wallet);\n-        assert(bal.m_mine_trusted > 0);\n+    bench.minEpochIterations(10).run([&] {\n+        const auto bal{GetBalance(wallet)};\n+        assert(bal.m_mine_trusted == expected_balance);\n+        assert(bal.m_mine_immature == 0);\n     });\n }\n\ndiff --git a/src/bench/wallet_bench_util.h b/src/bench/wallet_bench_util.h\nindex 7c7ebbb1d5..edc1bc15e3 100644\n--- a/src/bench/wallet_bench_util.h\n+++ b/src/bench/wallet_bench_util.h\n@@ -19,20 +19,26 @@\n #include <versionbits.h>\n #include <wallet/wallet.h>\n\n+#include <vector>\n+\n namespace wallet {\n\n struct TipBlock\n {\n-    uint256 prev_block_hash;\n-    int64_t prev_block_time;\n-    int tip_height;\n+    uint256 prev_block_hash{};\n+    int64_t prev_block_time{0};\n+    int tip_height{0};\n };\n\n inline TipBlock GetTip(const CChainParams& params, const node::NodeContext& context)\n {\n-    auto tip = WITH_LOCK(::cs_main, return context.chainman->ActiveTip());\n-    return (tip) ? TipBlock{tip->GetBlockHash(), tip->GetBlockTime(), tip->nHeight} :\n-           TipBlock{params.GenesisBlock().GetHash(), params.GenesisBlock().GetBlockTime(), 0};\n+    {\n+        LOCK(::cs_main);\n+        if (const auto* tip{context.chainman->ActiveTip()}) {\n+            return {tip->GetBlockHash(), tip->GetBlockTime(), tip->nHeight};\n+        }\n+    }\n+    return {params.GenesisBlock().GetHash(), params.GenesisBlock().GetBlockTime(), 0};\n }\n\n /**\n@@ -42,9 +48,7 @@ inline TipBlock GetTip(const CChainParams& params, const node::NodeContext& cont\n inline void GenerateFakeBlock(const CChainParams& params,\n                               const node::NodeContext& context,\n                               CWallet& wallet,\n-                              const CScript& coinbase_script,\n-                              int num_coinbase_outputs,\n-                              CAmount total_coinbase_value = 50 * COIN,\n+                              const std::vector<CTxOut>& coinbase_outputs,\n                               const std::vector<CTransactionRef>& extra_txs = {})\n {\n     TipBlock tip{GetTip(params, context)};\n@@ -54,19 +58,11 @@ inline void GenerateFakeBlock(const CChainParams& params,\n     coinbase_tx.vin.resize(1);\n     coinbase_tx.vin[0].prevout.SetNull();\n     coinbase_tx.vin[0].scriptSig = CScript() << ++tip.tip_height << OP_0;\n-\n-    // Create coinbase outputs\n-    CAmount per_output = total_coinbase_value / num_coinbase_outputs;\n-    for (int i = 0; i < num_coinbase_outputs; ++i) {\n-        coinbase_tx.vout.emplace_back(per_output, coinbase_script);\n-    }\n+    coinbase_tx.vout = coinbase_outputs;\n\n     block.vtx = {MakeTransactionRef(std::move(coinbase_tx))};\n\n-    // Add any extra transactions\n-    for (const auto& tx : extra_txs) {\n-        block.vtx.push_back(tx);\n-    }\n+    block.vtx.insert(block.vtx.end(), extra_txs.begin(), extra_txs.end());\n\n     block.nVersion = VERSIONBITS_LAST_OLD_BLOCK_VERSION;\n     block.hashPrevBlock = tip.prev_block_hash;\n@@ -75,16 +71,33 @@ inline void GenerateFakeBlock(const CChainParams& params,\n     block.nBits = params.GenesisBlock().nBits;\n     block.nNonce = 0;\n\n+    CBlockIndex* pindex{nullptr};\n     {\n         LOCK(::cs_main);\n-        CBlockIndex* pindex{context.chainman->m_blockman.AddToBlockIndex(block, context.chainman->m_best_header)};\n+        pindex = context.chainman->m_blockman.AddToBlockIndex(block, context.chainman->m_best_header);\n         context.chainman->ActiveChain().SetTip(*pindex);\n     }\n\n-    const auto* pindex = WITH_LOCK(::cs_main, return context.chainman->ActiveChain().Tip());\n     wallet.blockConnected(kernel::ChainstateRole{}, kernel::MakeBlockInfo(pindex, &block));\n }\n\n+inline void GenerateFakeBlock(const CChainParams& params,\n+                              const node::NodeContext& context,\n+                              CWallet& wallet,\n+                              const CScript& coinbase_script,\n+                              int num_coinbase_outputs,\n+                              CAmount total_coinbase_value = 50 * COIN,\n+                              const std::vector<CTransactionRef>& extra_txs = {})\n+{\n+    std::vector<CTxOut> coinbase_outputs;\n+    coinbase_outputs.reserve(num_coinbase_outputs);\n+    const CAmount per_output{total_coinbase_value / num_coinbase_outputs};\n+    for (int i{0}; i < num_coinbase_outputs; ++i) {\n+        coinbase_outputs.emplace_back(per_output, coinbase_script);\n+    }\n+    GenerateFakeBlock(params, context, wallet, coinbase_outputs, extra_txs);\n+}\n+\n } // namespace wallet\n\n #endif // BITCOIN_BENCH_WALLET_BENCH_UTIL_H\ndiff --git a/src/bench/wallet_create_tx.cpp b/src/bench/wallet_create_tx.cpp\nindex 11125e26fe..309c950671 100644\n--- a/src/bench/wallet_create_tx.cpp\n+++ b/src/bench/wallet_create_tx.cpp\n@@ -4,28 +4,20 @@\n\n #include <addresstype.h>\n #include <bench/bench.h>\n-#include <chain.h>\n+#include <bench/wallet_bench_util.h>\n #include <chainparams.h>\n #include <consensus/amount.h>\n #include <consensus/consensus.h>\n-#include <consensus/merkle.h>\n #include <interfaces/chain.h>\n-#include <kernel/chain.h>\n-#include <kernel/types.h>\n-#include <node/blockstorage.h>\n #include <outputtype.h>\n #include <policy/feerate.h>\n-#include <primitives/block.h>\n #include <primitives/transaction.h>\n #include <script/script.h>\n #include <sync.h>\n #include <test/util/setup_common.h>\n #include <test/util/time.h>\n-#include <uint256.h>\n #include <util/result.h>\n #include <util/time.h>\n-#include <validation.h>\n-#include <versionbits.h>\n #include <wallet/coincontrol.h>\n #include <wallet/coinselection.h>\n #include <wallet/spend.h>\n@@ -41,70 +33,28 @@\n #include <utility>\n #include <vector>\n\n-using kernel::ChainstateRole;\n using wallet::CWallet;\n using wallet::CreateMockableWalletDatabase;\n using wallet::WALLET_FLAG_DESCRIPTORS;\n\n-struct TipBlock\n-{\n-    uint256 prev_block_hash;\n-    int64_t prev_block_time;\n-    int tip_height;\n-};\n-\n-TipBlock getTip(const CChainParams& params, const node::NodeContext& context)\n-{\n-    auto tip = WITH_LOCK(::cs_main, return context.chainman->ActiveTip());\n-    return (tip) ? TipBlock{tip->GetBlockHash(), tip->GetBlockTime(), tip->nHeight} :\n-           TipBlock{params.GenesisBlock().GetHash(), params.GenesisBlock().GetBlockTime(), 0};\n-}\n-\n void generateFakeBlock(const CChainParams& params,\n                        const node::NodeContext& context,\n                        CWallet& wallet,\n                        const CScript& coinbase_out_script)\n {\n-    TipBlock tip{getTip(params, context)};\n-\n-    // Create block\n-    CBlock block;\n-    CMutableTransaction coinbase_tx;\n-    coinbase_tx.vin.resize(1);\n-    coinbase_tx.vin[0].prevout.SetNull();\n-    coinbase_tx.vout.resize(2);\n-    coinbase_tx.vout[0].scriptPubKey = coinbase_out_script;\n-    coinbase_tx.vout[0].nValue = 48 * COIN;\n-    coinbase_tx.vin[0].scriptSig = CScript() << ++tip.tip_height << OP_0;\n-    coinbase_tx.vout[1].scriptPubKey = coinbase_out_script; // extra output\n-    coinbase_tx.vout[1].nValue = 1 * COIN;\n+    constexpr int NON_WALLET_OUTPUTS{50};\n+    std::vector<CTxOut> coinbase_outputs;\n+    coinbase_outputs.reserve(2 + NON_WALLET_OUTPUTS);\n+    coinbase_outputs.emplace_back(48 * COIN, coinbase_out_script);\n+    coinbase_outputs.emplace_back(1 * COIN, coinbase_out_script);\n\n     // Fill the coinbase with outputs that don't belong to the wallet in order to benchmark\n     // AvailableCoins' behavior with unnecessary TXOs\n-    for (int i = 0; i < 50; ++i) {\n-        coinbase_tx.vout.emplace_back(1 * COIN / 50, CScript(OP_TRUE));\n-    }\n-\n-    block.vtx = {MakeTransactionRef(std::move(coinbase_tx))};\n-\n-    block.nVersion = VERSIONBITS_LAST_OLD_BLOCK_VERSION;\n-    block.hashPrevBlock = tip.prev_block_hash;\n-    block.hashMerkleRoot = BlockMerkleRoot(block);\n-    block.nTime = ++tip.prev_block_time;\n-    block.nBits = params.GenesisBlock().nBits;\n-    block.nNonce = 0;\n-\n-    {\n-        LOCK(::cs_main);\n-        // Add it to the index\n-        CBlockIndex* pindex{context.chainman->m_blockman.AddToBlockIndex(block, context.chainman->m_best_header)};\n-        // add it to the chain\n-        context.chainman->ActiveChain().SetTip(*pindex);\n+    for (int i{0}; i < NON_WALLET_OUTPUTS; ++i) {\n+        coinbase_outputs.emplace_back(1 * COIN / NON_WALLET_OUTPUTS, CScript(OP_TRUE));\n     }\n\n-    // notify wallet\n-    const auto& pindex = WITH_LOCK(::cs_main, return context.chainman->ActiveChain().Tip());\n-    wallet.blockConnected(ChainstateRole{}, kernel::MakeBlockInfo(pindex, &block));\n+    wallet::GenerateFakeBlock(params, context, wallet, coinbase_outputs);\n }\n\n struct PreSelectInputs {\n```"
  },
  {
   "t": "2026-05-22T00:26:46Z",
   "kind": "force_push",
   "who": "w0xlt",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651"
  },
  {
   "t": "2026-05-22T01:10:39Z",
   "kind": "force_push",
   "who": "w0xlt",
   "commit": "622265f7fba2d0c08ae77091526dc36f5faa30d9"
  },
  {
   "t": "2026-05-22T02:03:40Z",
   "kind": "force_push",
   "who": "w0xlt",
   "commit": "5defb96252e0b955d1a3863f78a87ca064d9071a"
  },
  {
   "t": "2026-05-22T02:06:02Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/bench/wallet_bench_util.h",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": 3248295979,
   "text": "Thanks. Done"
  },
  {
   "t": "2026-05-22T02:06:09Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/bench/wallet_bench_util.h",
   "commit": "7971f410b38fb4dc55712e57cf1ac29a6d52be88",
   "in_reply_to": 3248286281,
   "text": "Thanks. Done"
  },
  {
   "t": "2026-05-22T02:06:16Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/bench/wallet_bench_util.h",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": 3248238869,
   "text": "Thanks. Done"
  },
  {
   "t": "2026-05-22T02:06:23Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/bench/wallet_bench_util.h",
   "commit": "7971f410b38fb4dc55712e57cf1ac29a6d52be88",
   "in_reply_to": 3248206850,
   "text": "Thanks. Done"
  },
  {
   "t": "2026-05-22T02:06:30Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": 3248159567,
   "text": "Thanks. Done"
  },
  {
   "t": "2026-05-22T02:06:36Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": 3247900786,
   "text": "Thanks. Done"
  },
  {
   "t": "2026-05-22T02:06:57Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": 3247910588,
   "text": "Thanks. Done"
  },
  {
   "t": "2026-05-22T02:07:04Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": 3247948540,
   "text": "Thanks. Done"
  },
  {
   "t": "2026-05-22T02:07:26Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": 3248011187,
   "text": "Thanks. Done"
  },
  {
   "t": "2026-05-22T02:07:36Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "7971f410b38fb4dc55712e57cf1ac29a6d52be88",
   "in_reply_to": 3248047902,
   "text": "Thanks. Done"
  },
  {
   "t": "2026-05-22T02:07:46Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": 3248108170,
   "text": "Thanks. Done"
  },
  {
   "t": "2026-05-22T02:07:52Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "7971f410b38fb4dc55712e57cf1ac29a6d52be88",
   "in_reply_to": 3248135213,
   "text": "Thanks. Done"
  },
  {
   "t": "2026-05-22T02:07:59Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": 3247924666,
   "text": "Thanks. Done"
  },
  {
   "t": "2026-05-22T02:08:05Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": 3247847480,
   "text": "Thanks. Done"
  },
  {
   "t": "2026-05-22T02:08:12Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": 3247835552,
   "text": "Thanks. Done"
  },
  {
   "t": "2026-05-22T02:08:21Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": 3247693415,
   "text": "Thanks. Done"
  },
  {
   "t": "2026-05-22T02:08:28Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": 3247612327,
   "text": "Thanks. Done"
  },
  {
   "t": "2026-05-22T02:10:26Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "@l0rinc Thanks for the thorough review. I split the changes into two commits to make them easier to review, and added you as a co-author on both since many of the improvements came from your suggestions."
  },
  {
   "t": "2026-05-22T02:17:32Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "I've re-run this benchmark to validate bitcoin/bitcoin#27865, which changes how wallet TXOs are retained for balance calculation.\n\nCommand used:\n\n```bash\nbuild/bin/bench_bitcoin -filter=WalletBalanceManySpent -min-time=10000\n```\n\nResults:\n\n| branch | median | notes |\n| -- | --: | -- |\n| current master | 8,861,448 ns/op | higher variance in this run, ~17% error |\n| #27865  | 2,973 ns/op | lower variance in this run, ~0.8% error |\n\nThis is about a **2,981x speedup** with #27865 for this workload.\n\nThe benchmark's purpose is to model a wallet with many spent TXOs and only a few unspent TXOs, then measure `GetBalance()`. On current master, the balance path still has to deal with the large spent-output history.\n\nWith #27865, the balance path only sees the relevant unspent outputs, which is the performance improvement this benchmark is intended to demonstrate."
  },
  {
   "t": "2026-05-22T02:22:24Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\n@sedited Done."
  },
  {
   "t": "2026-05-22T15:15:14Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "058e97c1593d1edc840cfccedd066722e6c09651",
   "in_reply_to": 3247693415,
   "text": "Does `minEpochIterations(128)` addresses benchmark stability better than the suggested 10?"
  },
  {
   "t": "2026-05-22T15:17:26Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/bench/wallet_balance.cpp",
   "commit": "7971f410b38fb4dc55712e57cf1ac29a6d52be88",
   "in_reply_to": null,
   "text": "nit: Is this meant to support running the benchmark on top of #27865?\nOn this PR\u2019s current base the `else` branch should be dead code, so a short comment explaining the forward-compatibility case would help."
  },
  {
   "t": "2026-05-22T15:20:14Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "ACK 5defb96252e0b955d1a3863f78a87ca064d9071a\n\nThanks for taking the suggestions. I see you've made it forward-compatible - I'm usually more strict about adding dead code, but in this case I personally don't mind. If you touch this again, either remove it and let the other PR extend the benchmark (it likely needs other changes anyway) or add a comment explaining it. I'm also fine with just adding a note to the PR's description."
  },
  {
   "t": "2026-08-10T19:34:39Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "Ping @w0xlt for a rebase."
  },
  {
   "t": "2026-08-18T22:48:11Z",
   "kind": "force_push",
   "who": "w0xlt",
   "commit": "7971f410b38fb4dc55712e57cf1ac29a6d52be88"
  },
  {
   "t": "2026-08-18T22:48:41Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "Rebased. Thanks @sedited"
  }
 ],
 "labels_log": [
  {
   "t": "2026-01-21T03:15:22Z",
   "action": "labeled",
   "label": "Tests",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-09T23:03:38Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-22T01:11:06Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-22T02:01:09Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-22T04:13:53Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-15T14:29:51Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-18T23:25:29Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-19T00:13:34Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-09T11:51:04Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-08-31T08:31:49Z",
   "kind": "convert_to_draft",
   "who": "DrahtBot"
  }
 ],
 "text_chars": 29319,
 "text_tokens_estimate": 7329,
 "changed_paths": [],
 "files": [],
 "test_lines": null,
 "git": null,
 "input_hash": "ad72125758f4af67",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}