{
 "number": 34969,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/34969",
 "title": "fuzz: several improvements to scriptpubkeyman harness",
 "author": "brunoerg",
 "author_association": "MEMBER",
 "created_at": "2026-03-31T19:10:58Z",
 "updated_at": "2026-09-17T03:44:53Z",
 "age_days": 169,
 "draft": false,
 "labels": [
  "Fuzzing"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "0b0df80ba02863fd95128822558625a4c2367d15",
 "head_ref": "2026-03-fuzz-spkm-improvements",
 "head_repo": "brunoerg/bitcoin",
 "head_history": [
  {
   "t": "2026-04-13T13:46:44Z",
   "sha": "32b717ec162fbad9cc9a0ec4649a402116db6b12"
  },
  {
   "t": "2026-04-30T18:43:08Z",
   "sha": "3964b4857147bb5c4932f308969532808a9e6937"
  },
  {
   "t": "2026-04-30T20:03:43Z",
   "sha": "5aabf8bf1cc354a97e80ab3363a32023a7be7df2"
  },
  {
   "t": "2026-05-05T14:42:29Z",
   "sha": "40786bb2f8c01fe80f96b9e1dce610d043b800f2"
  },
  {
   "t": "2026-07-15T15:38:36Z",
   "sha": "08499b1dcb79883a6d76d856424f3a9fcd5402d8"
  },
  {
   "t": "2026-08-19T13:03:22Z",
   "sha": "37c2304cc3bb4595a9363aca221f063344674697"
  },
  {
   "t": "2026-08-19T13:04:11Z",
   "sha": "0b0df80ba02863fd95128822558625a4c2367d15"
  }
 ],
 "additions": 71,
 "deletions": 86,
 "changed_files": 3,
 "commit_count": 7,
 "size_bucket": "M",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "pablomartin4btc",
      "url": "https://github.com/bitcoin/bitcoin/pull/34969#pullrequestreview-4899219780"
     }
    ]
   },
   "conflicts": [
    {
     "number": 35444,
     "title": "wallet: make descriptor SPKM mutex non-recursive",
     "author": "w0xlt"
    }
   ]
  }
 },
 "acks_parsed": {
  "pablomartin4btc": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-08-11T01:45:53Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 1,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "andrewtoth",
   "ekzyis",
   "maflcko",
   "pablomartin4btc"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-08-19T13:05:09Z",
  "last_reviewer_activity": "2026-08-11T01:45:53Z",
  "last_reviewer": "pablomartin4btc",
  "author_silent_days": 29,
  "waiting_on_author_days": 0,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [],
  "conflicts": [
   35444
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/wallet/test/fuzz/scriptpubkeyman.cpp"
 ],
 "body": "The scriptpubkeyman harness is slow - I'm getting under 40 exec/s on my personal server. This PR solves the following issues that may be affecting the performance:\n\n- There are redundant `IsMine` calls\n- `ConsumeCoins` always create up to 10'000 coins which may cause slowness.\n- There are some expensive ops in a loop - e.g. One of the slow units I got on my server is about a repeated sequence of `GetNewDestination` calls (see flamegraph below).\n- We're always calling `GetDescriptorString` unconditionally. It looks like an inofensive function but this is extremely costly due to calls to `ToNormalizedString`/`ToPrivateString` functions that touch key operations.\n- Not related to slowness, but there is a bug on harness because we are aborting the execution when we cannot update the wallet descriptor, we can just skip and continue.\n\n![spkm-updated](https://github.com/user-attachments/assets/8220b0f6-b430-43b2-8594-2a7825080c0a)",
 "commits": [
  {
   "sha": "84a334ceda2f243a531ab7774968dfb1399cf8da",
   "date": "2026-08-19T12:26:49Z",
   "message": "fuzz: spkm: remove excessive IsMine calls\n\nAvoid calling IsMine for every single spkm from\nGetScriptPubKeys(). Also, expands `MarkUnusedAddresses`\nwhich covers it. In a later commit, we introduce the\nassertion back in a better way."
  },
  {
   "sha": "13aba9b96938a3108443da1ec9a9397f686a9edc",
   "date": "2026-08-19T12:26:49Z",
   "message": "fuzz: add a num_coins parameter to ConsumeCoins\n\nThis new parameter is used to control the max number of coins\nthis function might create. It allows us to create less than\n10'000 coins and then reduce the cost."
  },
  {
   "sha": "5aca41ac461bc6680d6512fead859e43d6429adb",
   "date": "2026-08-19T12:26:49Z",
   "message": "fuzz: spkm: reduce number of created coins"
  },
  {
   "sha": "ae9779616d19d9029159c256d5c1dd0430e68b35",
   "date": "2026-08-19T12:26:49Z",
   "message": "fuzz: spkm: move expensive ops out of the loop"
  },
  {
   "sha": "5d67ca2c31428b803e89554469a582b13fbf1851",
   "date": "2026-08-19T12:26:49Z",
   "message": "fuzz: spkm: do not call GetDescriptorString unconditionally\n\nGetDescriptorString calls ToNormalizedString or ToPrivateString,\nboth of which rebuild the full descriptor string. This is expensive."
  },
  {
   "sha": "23ed87cab4574f231128d19998ba0fd5f23a5ac2",
   "date": "2026-08-19T12:26:49Z",
   "message": "fuzz: spkm: fix early return on updating the wallet descriptor"
  },
  {
   "sha": "0b0df80ba02863fd95128822558625a4c2367d15",
   "date": "2026-08-19T13:03:55Z",
   "message": "fuzz: spkm: assert the IsMine/GetScriptPubKeys invariant\n\nAssert both directions of the invariant, using the `spks` set that is\nnow computed only once:\n\n- every script from GetScriptPubKeys() is IsMine;\n- every IsMine script is in GetScriptPubKeys().\n\nThe latter is asserted before MarkUnusedAddresses(), which may TopUp()\nand add scripts that are not part of the already computed `spks` set."
  }
 ],
 "timeline": [
  {
   "t": "2026-03-31T21:04:26Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "Getting decent throughput when running on my laptop:\n```bash\n$ FUZZ=scriptpubkeyman ./build_fuzz_nosan/bin/fuzz ../qa-assets/fuzz_corpora/scriptpubkeyman/\n...\n#3326974\tREDUCE cov: 14175 ft: 84867 corp: 5289/37Mb lim: 998620 exec/s: 1319 rss: 262Mb L: 15776/882699 MS: 1 EraseBytes-\n#3327186\tREDUCE cov: 14175 ft: 84867 corp: 5289/37Mb lim: 998620 exec/s: 1319 rss: 262Mb L: 961/882699 MS: 2 ChangeBit-EraseBytes-\n#3328687\tREDUCE cov: 14175 ft: 84867 corp: 5289/37Mb lim: 998620 exec/s: 1319 rss: 262Mb L: 1249/882699 MS: 1 EraseBytes-\n...\n```\n\nHow is your server configured? Do you see a big speedup after applying this patch?"
  },
  {
   "t": "2026-04-01T01:11:42Z",
   "kind": "comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nWhat are the specs? How did you build it? Building with `--preset=libfuzzer` on my Ubuntu machine (AMD Ryzen 9 7900 / 32GB RAM), I get around 20 exec/s. With this PR, on same machine, I could reach similar coverage from master with an avg of 150 exec/s."
  },
  {
   "t": "2026-04-01T03:07:54Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "I'm on a i9-14900 with 96 GB RAM.\n\nI was running with only fuzzer sanitizer. When `--preset=libfuzzer` that uses `undefined,address,fuzzer`. With those I get a lot slower. It started at at 39 but after a few minutes was up to 128 exec/s. I checked out this PR and then it started at 128 and after a few minutes was up to 385 exec/s. Not bad!\n\nRunning with only `fuzzer` sanitizer on this branch however didn't seem to speed up it was around 1100 exec/s."
  },
  {
   "t": "2026-04-01T12:13:36Z",
   "kind": "comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nAh, got it. This is expected, because ASan instruments most memory accesses in instrumented code and tracks heap allocations, while UBSan adds runtime checks for selected undefined behaviors. So operations like GetScriptPubKeys(), ConsumeCoins(), and some string operations, such as GetDescriptorString() can become more expensive under these sanitizers."
  },
  {
   "t": "2026-04-10T10:53:40Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/wallet/test/fuzz/scriptpubkeyman.cpp",
   "commit": "0b0df80ba02863fd95128822558625a4c2367d15",
   "in_reply_to": null,
   "text": "1d826e5b8c49f8ebb8acd84c50861ea65c4129f8: Can you explain this a bit better? Sure, it may be internally covered in `MarkUnusedAddresses`, but the goal of fuzz tests is not only to cover code, but also to assert on results or assumptions"
  },
  {
   "t": "2026-04-13T13:46:44Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "32b717ec162fbad9cc9a0ec4649a402116db6b12"
  },
  {
   "t": "2026-04-13T13:48:21Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "src/wallet/test/fuzz/scriptpubkeyman.cpp",
   "commit": "0b0df80ba02863fd95128822558625a4c2367d15",
   "in_reply_to": 3063766940,
   "text": "I've changed this commit to say that it's removing excessive `IsMine` calls (e.g. calling it for every single spk from `GetScriptPubKeys`. In a later commit, I add the assert back but in a smoothly way."
  },
  {
   "t": "2026-04-30T18:43:08Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "3964b4857147bb5c4932f308969532808a9e6937"
  },
  {
   "t": "2026-04-30T20:03:43Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "5aabf8bf1cc354a97e80ab3363a32023a7be7df2"
  },
  {
   "t": "2026-05-05T14:42:29Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "40786bb2f8c01fe80f96b9e1dce610d043b800f2"
  },
  {
   "t": "2026-05-05T14:42:52Z",
   "kind": "comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "text": "Rebased"
  },
  {
   "t": "2026-06-13T18:25:12Z",
   "kind": "comment",
   "who": "ekzyis",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\nI get around 2000 exec/s on master (4c99ed1076) and 40786bb2f8 with --preset=libfuzzer.\n\nI also fuzzed the scriptpubkeyman target for 10 minutes on both commits, and got a similar amount of runs:\n\n```\n$ tail -1 fuzz-scriptpubkeyman-4c99ed1076.txt\nDone 1191929 runs in 601 second(s)\n$ tail -1 fuzz-scriptpubkeyman-40786bb2f8.txt\nDone 1195257 runs in 601 second(s)\n```\n\nIntel(R) Xeon(R) CPU E3-1275 v6 @ 4.20GHz with 64GB RAM\n\nI thought maybe the latest changes to the harness since your comments changed performance:\n\n```\n$ git log --oneline --since=2026-04-01 master -- src/wallet/test/fuzz/scriptpubkeyman.cpp\nc6f225c757c Merge bitcoin/bitcoin#28333: wallet: Construct ScriptPubKeyMans with all data rather than loaded progressively\nc568624ff29 psbt: Return std::optional from PrecomputePSBTData\nc01c7f068c5 psbt: Remove default constructor\n6538f691357 fuzz: Skip adding descriptor to wallet if it cannot be expanded\n0301c758ea0 wallet migration, fuzz: Migrate hd seed once\ndc4a5d1270f refactor: use PSBTFillOptions for filling and signing\n```\n\nI checked out c6f225c757c and c6f225c757c^. Same performance with around 2000 exec/s:\n\n```\n$ tail -1 fuzz-scriptpubkeyman-5486ef8cc2.txt\nDone 1198053 runs in 601 second(s)\n$ tail -1 fuzz-scriptpubkeyman-c6f225c757.txt\nDone 1124156 runs in 601 second(s)\n```\n\nSo not sure where the low exec/s on your machines are coming from."
  },
  {
   "t": "2026-06-16T11:55:30Z",
   "kind": "comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nRunning from our corpus?"
  },
  {
   "t": "2026-06-26T16:15:17Z",
   "kind": "comment",
   "who": "ekzyis",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\nOh, no. With the scriptpubkeyman corpus from qa-assets, I get about 200\u2013300 exec/s on the same machine. Thanks, this clears up my confusion! Sorry for the noise"
  },
  {
   "t": "2026-07-15T15:38:36Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "08499b1dcb79883a6d76d856424f3a9fcd5402d8"
  },
  {
   "t": "2026-07-15T16:10:36Z",
   "kind": "comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "text": "Rebased"
  },
  {
   "t": "2026-08-10T17:08:20Z",
   "kind": "review_comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "path": "src/wallet/test/fuzz/scriptpubkeyman.cpp",
   "commit": "0b0df80ba02863fd95128822558625a4c2367d15",
   "in_reply_to": null,
   "text": "minor nit: redundant default construction + assignment...\n```suggestion\n            auto tx_to = {*opt_tx_to};\n```"
  },
  {
   "t": "2026-08-11T01:28:30Z",
   "kind": "review_comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "path": "src/wallet/test/fuzz/scriptpubkeyman.cpp",
   "commit": "0b0df80ba02863fd95128822558625a4c2367d15",
   "in_reply_to": 3063766940,
   "text": "Following up on @maflcko's concern about assertions: the assert is restored in the last commit but only for one randomly picked script from `GetScriptPubKeys()`. The original code asserted `IsMine` for *every* script in the set \u2014 a stronger invariant that would catch a bug where some (but not all) returned scripts fail `IsMine`.\n\nAlso, the inverse direction check from the old code is gone entirely: `if (IsMine(random_script)) \u2192 assert(GetScriptPubKeys().contains(script))`. That covered the other direction of the invariant \u2014 a bug where `IsMine` claims ownership of a script that isn't listed in `GetScriptPubKeys()`.\n\nSince `spks` is already computed once outside the loop, the full assertion can be kept cheaply:\n```cpp\nfor (const CScript& spk : spks) {\n    assert(spk_manager->IsMine(spk));\n}\n```\nThe expensive part was calling `GetScriptPubKeys()` repeatedly in the loop \u2014 iterating over the already-computed `spks` is negligible."
  },
  {
   "t": "2026-08-11T01:45:53Z",
   "kind": "review",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "08499b1dcb79883a6d76d856424f3a9fcd5402d8",
   "text": "Concept ACK\n\nA real improvement \u2014 the early return [bug](https://github.com/bitcoin/bitcoin/blob/757aa573c458a1225ec0da89278fc7b0217f46b6/src/wallet/test/fuzz/scriptpubkeyman.cpp#L108-L109) fix and moving expensive ops (`GetDescriptorString`, `GetNewDestination`) out of the hot path are the right calls.\n\nLeft a comment inline on the assertion coverage (not blocking)."
  },
  {
   "t": "2026-08-19T13:03:22Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "37c2304cc3bb4595a9363aca221f063344674697"
  },
  {
   "t": "2026-08-19T13:04:11Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "0b0df80ba02863fd95128822558625a4c2367d15"
  },
  {
   "t": "2026-08-19T13:04:58Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "src/wallet/test/fuzz/scriptpubkeyman.cpp",
   "commit": "0b0df80ba02863fd95128822558625a4c2367d15",
   "in_reply_to": 3063766940,
   "text": "Yes, I agree. Just changed it to full assertion since it's cheap."
  },
  {
   "t": "2026-08-19T13:05:09Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "src/wallet/test/fuzz/scriptpubkeyman.cpp",
   "commit": "0b0df80ba02863fd95128822558625a4c2367d15",
   "in_reply_to": 3751686948,
   "text": "will leave as-is for now."
  }
 ],
 "labels_log": [
  {
   "t": "2026-03-31T19:11:02Z",
   "action": "labeled",
   "label": "Fuzzing",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-29T15:15:13Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-30T19:39:52Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-30T19:58:03Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-30T21:27:29Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-05T13:41:02Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-05T16:01:27Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-14T18:55:58Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-15T16:57:13Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-19T13:05:15Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-19T14:43:26Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 7516,
 "text_tokens_estimate": 1879,
 "changed_paths": [
  "src/test/fuzz/util.cpp",
  "src/test/fuzz/util.h",
  "src/wallet/test/fuzz/scriptpubkeyman.cpp"
 ],
 "files": [
  {
   "path": "src/test/fuzz/util.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/test/fuzz/util.h",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/wallet/test/fuzz/scriptpubkeyman.cpp",
   "add": 68,
   "del": 83
  }
 ],
 "test_lines": 157,
 "git": {
  "head": "0b0df80ba02863fd95128822558625a4c2367d15",
  "head_matches_backup": true,
  "base": "59224b66aa1db43adc61c15ee41b413951f22f80",
  "commits": [
   {
    "sha": "84a334ceda",
    "subject": "fuzz: spkm: remove excessive IsMine calls",
    "files": 1,
    "add": 2,
    "del": 11
   },
   {
    "sha": "13aba9b969",
    "subject": "fuzz: add a num_coins parameter to ConsumeCoins",
    "files": 2,
    "add": 3,
    "del": 3
   },
   {
    "sha": "5aca41ac46",
    "subject": "fuzz: spkm: reduce number of created coins",
    "files": 1,
    "add": 1,
    "del": 1
   },
   {
    "sha": "ae9779616d",
    "subject": "fuzz: spkm: move expensive ops out of the loop",
    "files": 1,
    "add": 52,
    "del": 64
   },
   {
    "sha": "5d67ca2c31",
    "subject": "fuzz: spkm: do not call GetDescriptorString unconditionally",
    "files": 1,
    "add": 5,
    "del": 2
   },
   {
    "sha": "23ed87cab4",
    "subject": "fuzz: spkm: fix early return on updating the wallet descriptor",
    "files": 1,
    "add": 6,
    "del": 8
   },
   {
    "sha": "0b0df80ba0",
    "subject": "fuzz: spkm: assert the IsMine/GetScriptPubKeys invariant",
    "files": 1,
    "add": 5,
    "del": 0
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "1e7fcb291334937b",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}