{
 "number": 35641,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35641",
 "title": "kernel: Add script evaluation tracer",
 "author": "sedited",
 "author_association": "MEMBER",
 "created_at": "2026-07-02T15:00:17Z",
 "updated_at": "2026-09-16T22:11:20Z",
 "age_days": 77,
 "draft": false,
 "labels": [
  "Validation"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "97462c683d49792a58ba954723a205cda86a5e0b",
 "head_ref": "kernel_script_tracer",
 "head_repo": "sedited/bitcoin",
 "head_history": [
  {
   "t": "2026-07-02T20:02:42Z",
   "sha": "1a4287439d40960e600964afe4e9746516be003e"
  },
  {
   "t": "2026-07-02T20:19:14Z",
   "sha": "31a5c9869b8897d858227751afb3d2e1d94d57da"
  },
  {
   "t": "2026-07-08T15:26:41Z",
   "sha": "2df591d0b8497b5b1fb48b762990b829acd5f843"
  },
  {
   "t": "2026-07-13T20:19:38Z",
   "sha": "a131d018ca854179c8c763ef593b8c8d24a6f774"
  },
  {
   "t": "2026-07-14T10:46:39Z",
   "sha": "114e48e968a087f74c1ab611ac2a31a9266812e1"
  },
  {
   "t": "2026-08-28T20:39:48Z",
   "sha": "bc4484237c2c47c328a80bb56b7b64bd8d75c0c7"
  },
  {
   "t": "2026-08-28T20:46:44Z",
   "sha": "20fb03490d1f38dde67f058acd68e816595d9222"
  },
  {
   "t": "2026-08-31T15:45:51Z",
   "sha": "97462c683d49792a58ba954723a205cda86a5e0b"
  }
 ],
 "additions": 617,
 "deletions": 2,
 "changed_files": 9,
 "commit_count": 1,
 "size_bucket": "L",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "furszy",
      "url": "https://github.com/bitcoin/bitcoin/pull/35641#issuecomment-4867757550"
     },
     {
      "login": "w0xlt",
      "url": "https://github.com/bitcoin/bitcoin/pull/35641#issuecomment-4869011143"
     },
     {
      "login": "theStack",
      "url": "https://github.com/bitcoin/bitcoin/pull/35641#issuecomment-4918882553"
     }
    ],
    "approach_ack": [
     {
      "login": "haanhvu",
      "url": "https://github.com/bitcoin/bitcoin/pull/35641#pullrequestreview-4772166308"
     }
    ],
    "stale_ack": [
     {
      "login": "alexanderwiederin",
      "url": "https://github.com/bitcoin/bitcoin/pull/35641#pullrequestreview-4701828768"
     }
    ]
   },
   "conflicts": [
    {
     "number": 35511,
     "title": "RFC: consensus: Make `CAmount` a class",
     "author": "hodlinator"
    },
    {
     "number": 34374,
     "title": "kernel: use struct-based logging and simplify logging interface",
     "author": "stickies-v"
    }
   ]
  }
 },
 "acks_parsed": {
  "furszy": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-07-02T16:04:03Z",
   "stale": false
  },
  "theStack": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-07-08T20:22:44Z",
   "stale": false
  },
  "alexanderwiederin": {
   "kind": "ack",
   "hash": "114e48e968a087f74c1ab611ac2a31a9266812e1",
   "t": "2026-07-15T07:20:05Z",
   "stale": true
  },
  "haanhvu": {
   "kind": "approach_ack",
   "hash": null,
   "t": "2026-08-14T10:07:58Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 1,
  "concept_ack": 2,
  "approach_ack": 1,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "alexanderwiederin",
   "elmeriniemela",
   "furszy",
   "haanhvu",
   "stringintech",
   "theStack",
   "w0xlt"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-08-31T15:46:00Z",
  "last_reviewer_activity": "2026-09-01T20:37:18Z",
  "last_reviewer": "w0xlt",
  "author_silent_days": 17,
  "waiting_on_author_days": 15,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [
   0,
   1
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 0,
    "type": null,
    "state": null,
    "merged": null,
    "merged_at": null,
    "title": null
   },
   {
    "number": 1,
    "type": "issue",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "JSON-RPC support for mobile devices (\\\"ultra-lightweight\\\" clients)"
   }
  ],
  "conflicts": [
   35511,
   34374
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/kernel/bitcoinkernel.cpp",
  "src/kernel/bitcoinkernel.h",
  "src/script/trace.cpp",
  "src/script/trace.h",
  "src/test/kernel/test_kernel.cpp"
 ],
 "body": "This adds a callback hook to the kernel library C header for getting the current script evaluation state on each script evaluation step. This can be used for debugging scripts and gives external applications a view into how Bitcoin Core does script evaluation.\n\nThe initial sketch for this work was prepared on rust-bitcoinkernel: https://github.com/sedited/rust-bitcoinkernel/tree/feature_script_debug . That branch also includes a very rudimentary script debugger showcasing the use case for this patch. Clankers seems to do a decent job at coding up prototypes for more featureful debuggers, which seems like a fun project to hack on if anybody else wants to give it a try.\n\nThis feature is gated behind the ENABLE_SCRIPT_TRACE flag. To compile with the feature enabled, pass `-DENABLE_SCRIPT_TRACE=ON`. If the feature is not manually enabled, the trace hooks are stubbed out.\n\nThe traces are divided into separate frames. RAII is used to guarantee that every script evaluation has at least its start and end state captured. Each instruction generates a unique frame as well.\n\nThe ScriptError in the VerifyScript invocation run when calling script_pubkey_verify is added in order to be able to surface the error code in the trace frames.\n\nAnother use case could also be using the frames to ensure that the script interpreter is actually doing what we think it is doing during the script tests. This might prevent unit test bugs in the future, i.e. when [error conditions shadow actual behaviour](https://github.com/bitcoin/bitcoin/pull/33961), and could also be interesting for fuzzing the script interpreter.",
 "commits": [
  {
   "sha": "97462c683d49792a58ba954723a205cda86a5e0b",
   "date": "2026-08-31T15:35:21Z",
   "message": "kernel: Add script tracer\n\nThis adds a callback hook to the kernel library C header for getting the\ncurrent script execution state on each script evaluation step. This can\nbe used for debugging scripts and gives external applications a view\ninto how Bitcoin Core does script evaluation.\n\nThis feature is gated behind the ENABLE_SCRIPT_TRACE flag. To compile\nwith the feature enabled, pass `-DENABLE_SCRIPT_TRACE=ON`. If the feature\nis not manually enabled, the trace hooks are stubbed out.\n\nThe traces are divided into separate frames. RAII is used to guarantee\nthat every script execution has at least its start and end state\ncaptured. Each instruction generates a unique frame as well.\n\nThe ScriptError in the VerifyScript invocation run when calling\nscript_pubkey_verify is added in order to be able to surface the error\ncode in the trace frames."
  }
 ],
 "timeline": [
  {
   "t": "2026-07-02T16:04:03Z",
   "kind": "comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "text": "Concept ACK, haven't checked the implementation. But this is something that would benefit testing greatly."
  },
  {
   "t": "2026-07-02T17:57:29Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "Strong Concept ACK"
  },
  {
   "t": "2026-07-02T20:02:42Z",
   "kind": "force_push",
   "who": "sedited",
   "commit": "1a4287439d40960e600964afe4e9746516be003e"
  },
  {
   "t": "2026-07-02T20:19:14Z",
   "kind": "force_push",
   "who": "sedited",
   "commit": "31a5c9869b8897d858227751afb3d2e1d94d57da"
  },
  {
   "t": "2026-07-06T09:31:56Z",
   "kind": "comment",
   "who": "alexanderwiederin",
   "assoc": "MEMBER",
   "text": "Does this support multi-threaded applications? If not, do we want it to?"
  },
  {
   "t": "2026-07-06T09:37:01Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nIt should be thread-safe, but I don't think this is particularly interesting to run in a multi-threaded context. I was thinking of adding an id to the trace frames, but didn't because I felt like the use case is pretty slim."
  },
  {
   "t": "2026-07-08T07:38:48Z",
   "kind": "review_comment",
   "who": "alexanderwiederin",
   "assoc": "MEMBER",
   "path": "src/kernel/bitcoinkernel.h",
   "commit": "97462c683d49792a58ba954723a205cda86a5e0b",
   "in_reply_to": null,
   "text": "I suggest we add something like: \"The callback fires once per instruction, _after_ the opcode is decoded and _before_ it is dispatched/executed.\""
  },
  {
   "t": "2026-07-08T07:42:30Z",
   "kind": "review_comment",
   "who": "alexanderwiederin",
   "assoc": "MEMBER",
   "path": "src/kernel/bitcoinkernel.h",
   "commit": "31a5c9869b8897d858227751afb3d2e1d94d57da",
   "in_reply_to": null,
   "text": "Is this accurate? Couldn't a thread that copied the callback still begin an invocation?\n\nI think it's safe, but the doc comment overpromises if I am not wrong."
  },
  {
   "t": "2026-07-08T13:46:46Z",
   "kind": "review_comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "path": "src/kernel/bitcoinkernel.h",
   "commit": "31a5c9869b8897d858227751afb3d2e1d94d57da",
   "in_reply_to": 3542121809,
   "text": "Mmh, not sure how to reword this. Can you suggest something?"
  },
  {
   "t": "2026-07-08T14:20:15Z",
   "kind": "review_comment",
   "who": "alexanderwiederin",
   "assoc": "MEMBER",
   "path": "src/kernel/bitcoinkernel.h",
   "commit": "31a5c9869b8897d858227751afb3d2e1d94d57da",
   "in_reply_to": 3542121809,
   "text": "Maybe:\n\"Unregistration is not synchronized with callback execution. Pending invocations on other threads may still run to completion after this returns, but no invocations begin thereafter.\"\n\nWhat do you think?"
  },
  {
   "t": "2026-07-08T15:26:41Z",
   "kind": "force_push",
   "who": "sedited",
   "commit": "2df591d0b8497b5b1fb48b762990b829acd5f843"
  },
  {
   "t": "2026-07-08T15:26:48Z",
   "kind": "review_comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "path": "src/kernel/bitcoinkernel.h",
   "commit": "31a5c9869b8897d858227751afb3d2e1d94d57da",
   "in_reply_to": 3542121809,
   "text": "Taken."
  },
  {
   "t": "2026-07-08T15:26:57Z",
   "kind": "review_comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "path": "src/kernel/bitcoinkernel.h",
   "commit": "97462c683d49792a58ba954723a205cda86a5e0b",
   "in_reply_to": 3542101901,
   "text": "Taken."
  },
  {
   "t": "2026-07-08T20:22:44Z",
   "kind": "comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "text": "Concept ACK"
  },
  {
   "t": "2026-07-10T15:23:49Z",
   "kind": "review_comment",
   "who": "alexanderwiederin",
   "assoc": "MEMBER",
   "path": "src/kernel/bitcoinkernel.h",
   "commit": "114e48e968a087f74c1ab611ac2a31a9266812e1",
   "in_reply_to": null,
   "text": "I think we have to define what each code means. What do you think?"
  },
  {
   "t": "2026-07-10T15:36:10Z",
   "kind": "review",
   "who": "alexanderwiederin",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "2df591d0b8497b5b1fb48b762990b829acd5f843",
   "text": "Do we need a CI job to run with `ENABLE_SCRIPT_TRACE=ON`?"
  },
  {
   "t": "2026-07-10T15:40:03Z",
   "kind": "review_comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "path": "src/kernel/bitcoinkernel.h",
   "commit": "114e48e968a087f74c1ab611ac2a31a9266812e1",
   "in_reply_to": 3560046335,
   "text": "I think we should leave this here - it's fairly straight forward to get the error code header from somewhere. I'll try to open another pull request for exposing the entire error list in the kernel header, since we'd also want to wire it through the script verification function."
  },
  {
   "t": "2026-07-10T15:42:23Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nI wasn't sure where to put such a job, so I didn't introduce it here. Maybe we can add a kernel-lib-only job that sets it, but not sure if that is worthwhile enough yet."
  },
  {
   "t": "2026-07-10T16:14:53Z",
   "kind": "review_comment",
   "who": "alexanderwiederin",
   "assoc": "MEMBER",
   "path": "src/script/trace.cpp",
   "commit": "2df591d0b8497b5b1fb48b762990b829acd5f843",
   "in_reply_to": null,
   "text": "Could `ScriptTraceScope` snapshot the callback in its constructor? That takes both the mutex acquisition and the std::function copy out of the per-opcode path."
  },
  {
   "t": "2026-07-13T20:19:38Z",
   "kind": "force_push",
   "who": "sedited",
   "commit": "a131d018ca854179c8c763ef593b8c8d24a6f774"
  },
  {
   "t": "2026-07-13T20:19:54Z",
   "kind": "review_comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "path": "src/script/trace.cpp",
   "commit": "2df591d0b8497b5b1fb48b762990b829acd5f843",
   "in_reply_to": 3560371508,
   "text": "Done."
  },
  {
   "t": "2026-07-14T10:32:40Z",
   "kind": "comment",
   "who": "alexanderwiederin",
   "assoc": "MEMBER",
   "text": "Thanks! Can we rebase, so we can pull in the changes to rust-bitcoinkernel without reverting commits?"
  },
  {
   "t": "2026-07-14T10:46:39Z",
   "kind": "force_push",
   "who": "sedited",
   "commit": "114e48e968a087f74c1ab611ac2a31a9266812e1"
  },
  {
   "t": "2026-07-14T10:47:00Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nDone."
  },
  {
   "t": "2026-07-15T07:20:05Z",
   "kind": "review",
   "who": "alexanderwiederin",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "114e48e968a087f74c1ab611ac2a31a9266812e1",
   "text": "ACK 114e48e968a087f74c1ab611ac2a31a9266812e1"
  },
  {
   "t": "2026-07-22T09:44:23Z",
   "kind": "comment",
   "who": "elmeriniemela",
   "assoc": "NONE",
   "text": "I had some fun and tested this with a clanker.\n\nAdded python bindings: https://github.com/elmeriniemela/pybitcoinkernel/commit/a3017b277fa1f8ca877218869049f8a35233d69e#diff-203e11f27272eb568dbda09906ba8c8aa6718c4816d32310c279cd1e521e35e8\n\nMade a visualizer using the traces: https://eniemela.fi/bitcoin/tx/8e2ee13d2a19951c2777bb3a54f0cb69a2f76dae8baa954cd86149ed1138cb6c\n\nFWIW, found no issues :)"
  },
  {
   "t": "2026-07-24T10:10:25Z",
   "kind": "comment",
   "who": "elmeriniemela",
   "assoc": "NONE",
   "text": "I kept playing with this, and one weirdness that I found is that a trace can be invalid, but the related script error code is still OK. My clanker added a condition for this + its own logic to figure out what the real error is: https://github.com/elmeriniemela/pybitcoinkernel/blob/f67613ce47dbf142ba1d6f9746153d9e3e39878d/src/pybitcoinkernel/debugger.py#L809\n\nI can reach the if condition above with this command:\n```sh\n$ python scripts/manual_debug.py 02000000013f7cebd65c27431a90bba7f796914fe8cc2ddfc3f2cbd6f7e5f2fc854534da95000000006b483045022100de1ac3bcdfb0332207c4a91f3832bd2c2915840165f876ab47c5f8996b971c3602201c6c053d750fadde599e6f5c4e1963df0f01fc0d97815e8157e3d59fe09ca30d012103699b464d1d8bc9e47d4fb1cdaa89a1c5783d68363c4dbc4b524ed3d857148617feffffff02836d3c01000000001976a914fc25d6d5c94003bf5b0c7b640a248e2c637fcfb088ac7ada8202000000001976a914fbed3d9b11183209a57999d54d59f67c019e756c88ac6acb0700 --spent 00 0\ntransaction script verification: INVALID (1 input(s))\n\n########## input 0 ##########\nscript verification: INVALID  (error: OK)\n\n=== script #0: input script (scriptSig) \u00b7 BASE \u00b7 107 bytes ===\n    483045022100de1ac3bcdfb0332207c4a91f3832bd2c2915840165f876ab47c5f8996b971c3602201c6c053d750fadde599e6f5c4e1963df0f01fc0d97815e8157e3d59fe09ca30d012103699b464d1d8bc9e47d4fb1cdaa89a1c5783d68363c4dbc4b524ed3d857148617\n  #0000  OP_PUSHBYTES_72        Push the next 72 bytes onto the stack.\n         stack: []\n  #0001  OP_PUSHBYTES_33        Push the next 33 bytes onto the stack.\n         stack: [3045022100de1ac3bcdfb0332207c4a9...(72 bytes)]\n  result: [3045022100de1ac3bcdfb0332207c4a9...(72 bytes), 03699b464d1d8bc9e47d4fb1cdaa89a1...(33 bytes)]  -> OK\n\n=== script #1: output script (scriptPubkey) \u00b7 BASE \u00b7 1 bytes ===\n    00\n  #0000  OP_0                   Push an empty byte vector (represents false / zero).\n         stack: [3045022100de1ac3bcdfb0332207c4a9...(72 bytes), 03699b464d1d8bc9e47d4fb1cdaa89a1...(33 bytes)]\n  result: [3045022100de1ac3bcdfb0332207c4a9...(72 bytes), 03699b464d1d8bc9e47d4fb1cdaa89a1...(33 bytes), 0x]  -> OK\n\n  note: scripts evaluated without error, but verification failed (top of stack is false).\n```\n\nI'm not competent enough to say if this is how it should be or not (still learning about bitcoin), but I just thought it might be useful to let you know."
  },
  {
   "t": "2026-07-24T10:31:39Z",
   "kind": "review_comment",
   "who": "haanhvu",
   "assoc": "NONE",
   "path": "src/kernel/bitcoinkernel.cpp",
   "commit": "97462c683d49792a58ba954723a205cda86a5e0b",
   "in_reply_to": null,
   "text": "Should we clearly comment that user data is destroyed in any case, whether tracing enabled/succeeds or not, to avoid problems like double free?"
  },
  {
   "t": "2026-07-25T14:26:49Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nWhat you are seeing is the intended behaviour here. Your LLM seems to already have inferred the correct scenario from the context. The verification of the full script fails, because the remaining stack element (OP_0) is false. `VerifyScript` (which is what we call here), checks this after script evaluation script/interpreter.cpp:2092. The tracer however is limited to the `EvalScript` function, which does not enforce this final stack condition. This should be communicated by exposing the full script verification error codes when calling `btck_script_pubkey_verify`, but as I said here https://github.com/bitcoin/bitcoin/pull/35641#discussion_r3560152977 , I'd prefer doing that in a separate pull request."
  },
  {
   "t": "2026-07-29T15:11:48Z",
   "kind": "review_comment",
   "who": "haanhvu",
   "assoc": "NONE",
   "path": "src/test/kernel/test_kernel.cpp",
   "commit": "114e48e968a087f74c1ab611ac2a31a9266812e1",
   "in_reply_to": null,
   "text": "`BOOST_CHECK_EQUAL(states.size(), 11)` is non-fatal, but the test checks `states[0]` through `states[10]` after this. So if the size check fails, the test still continues and may access out-of-bound items, resulting in an undefined behavior instead of a clean test failure. Would it make sense to use `BOOST_REQUIRE_EQUAL(states.size(), 11)` instead?"
  },
  {
   "t": "2026-07-30T10:36:04Z",
   "kind": "review_comment",
   "who": "haanhvu",
   "assoc": "NONE",
   "path": "src/test/kernel/test_kernel.cpp",
   "commit": "97462c683d49792a58ba954723a205cda86a5e0b",
   "in_reply_to": null,
   "text": "Should we verify unregistering actually stops tracing?\n\nSomething like:\n```\n// Before unregistering\nconst auto frame_count = states.size();\n\n// After unregistering\nBOOST_CHECK(legacy_spent_script_pubkey.Verify(...));\nBOOST_CHECK_EQUAL(states.size(), frame_count);\n```"
  },
  {
   "t": "2026-08-01T08:59:57Z",
   "kind": "review_comment",
   "who": "haanhvu",
   "assoc": "NONE",
   "path": "src/test/kernel/test_kernel.cpp",
   "commit": "114e48e968a087f74c1ab611ac2a31a9266812e1",
   "in_reply_to": null,
   "text": "Nit: `state` is already passed by value here, so `m_state.emplace_back(state)` performs an extra copy of the `ScriptTraceFrame`. Would it make sense to move it instead? `m_state.emplace_back(std::move(state));`"
  },
  {
   "t": "2026-08-01T10:14:07Z",
   "kind": "review_comment",
   "who": "haanhvu",
   "assoc": "NONE",
   "path": "src/kernel/bitcoinkernel.h",
   "commit": "97462c683d49792a58ba954723a205cda86a5e0b",
   "in_reply_to": null,
   "text": "Should we extend the test to cover this callback replacement contract? For example, register tracer A, then register tracer B, perform a new script evaluation, and verify that only tracer B receives frames."
  },
  {
   "t": "2026-08-11T13:20:21Z",
   "kind": "review_comment",
   "who": "stringintech",
   "assoc": "CONTRIBUTOR",
   "path": "src/kernel/bitcoinkernel.h",
   "commit": "114e48e968a087f74c1ab611ac2a31a9266812e1",
   "in_reply_to": null,
   "text": "I noticed kernel is zero-copy where possible for frames (e.g. the stack item bytes are borrowed; only the pointer and size arrays are built per frame), and then the C++ wrapper deep-copies every frame unconditionally. If an owning copy is what consumers need anyway, why not do it in the library instead?\n\nAlso, would it make sense to make the frame opaque like other `btck_*` types, with getters, instead of a public struct (perhaps an opaque pointer to a `shared_ptr`, so copying a frame is just a refcount bump)? The C++ wrapper could then use the existing `View`/`Handle` pattern."
  },
  {
   "t": "2026-08-11T13:20:21Z",
   "kind": "review_comment",
   "who": "stringintech",
   "assoc": "CONTRIBUTOR",
   "path": "src/kernel/bitcoinkernel.cpp",
   "commit": "114e48e968a087f74c1ab611ac2a31a9266812e1",
   "in_reply_to": null,
   "text": "Perhaps it is better to use an exhaustive switch for `kind` and `sig_version` as well, in both directions (`bitcoinkernel.cpp` and the C++ wrapper), like the existing enum conversions in the kernel?"
  },
  {
   "t": "2026-08-11T13:20:21Z",
   "kind": "review_comment",
   "who": "stringintech",
   "assoc": "CONTRIBUTOR",
   "path": "src/script/trace.h",
   "commit": "114e48e968a087f74c1ab611ac2a31a9266812e1",
   "in_reply_to": null,
   "text": "My understanding is that `f_exec` is only meaningful in step frames, where it says whether the current opcode is about to be evaluated. If so, can we drop the closure and give it a fixed dummy in begin and end frames?"
  },
  {
   "t": "2026-08-11T13:20:21Z",
   "kind": "review_comment",
   "who": "stringintech",
   "assoc": "CONTRIBUTOR",
   "path": "src/script/trace.h",
   "commit": "114e48e968a087f74c1ab611ac2a31a9266812e1",
   "in_reply_to": null,
   "text": "`ScriptTraceScope` learns the outcome only from `m_error`. `set_error` in `EvalScript` also communicates through the returned bool, but the end frame only has access to the error object, so a null `m_error` makes a successful eval report `SCRIPT_ERR_UNKNOWN_ERROR`. The kernel path always passes `&error`, but in case any non-kernel callers want to use the tracer, would it make sense to assert it in the ctor?\n\nSuggested change\n\n```diff\n@@\n         m_error{error}\n     {\n+        assert(!m_callback || error);\n         Emit(ScriptTraceFrameKind::Begin, m_exec_fn(), /*opcode=*/0, SCRIPT_ERR_OK);\n     }\n```"
  },
  {
   "t": "2026-08-14T09:22:36Z",
   "kind": "review_comment",
   "who": "haanhvu",
   "assoc": "NONE",
   "path": "src/test/kernel/test_kernel.cpp",
   "commit": "97462c683d49792a58ba954723a205cda86a5e0b",
   "in_reply_to": null,
   "text": "Should we also verify other fields besides `m_kind` and `m_script_error` to see if the deep-copy conversion works fully correctly? For example, checking at least one STEP frame with a non-empty stack and verifying that stack items are copied correctly would help catch pointer/size conversion bugs (if there's any)."
  },
  {
   "t": "2026-08-14T10:07:58Z",
   "kind": "review",
   "who": "haanhvu",
   "assoc": "NONE",
   "state": "COMMENTED",
   "commit": "114e48e968a087f74c1ab611ac2a31a9266812e1",
   "text": "Approach ACK"
  },
  {
   "t": "2026-08-28T20:39:48Z",
   "kind": "force_push",
   "who": "sedited",
   "commit": "bc4484237c2c47c328a80bb56b7b64bd8d75c0c7"
  },
  {
   "t": "2026-08-28T20:39:53Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "Thank you for the review @stringintech and @haanhvu,\n\nRebased and updated 114e48e968a087f74c1ab611ac2a31a9266812e1 -> 20fb03490d1f38dde67f058acd68e816595d9222 ([kernel_script_tracer_0](https://github.com/sedited/bitcoin/tree/kernel_script_tracer_0) -> [kernel_script_tracer_1](https://github.com/sedited/bitcoin/tree/kernel_script_tracer_1), [compare](https://github.com/sedited/bitcoin/compare/kernel_script_tracer_0..kernel_script_tracer_1))\n\n* Addressed @stringintech's [comment](https://github.com/bitcoin/bitcoin/pull/35641#discussion_r3758299206), wrapped the frame into nested opaque types and getters.\n* Addressed @stringintech's [comment](https://github.com/bitcoin/bitcoin/pull/35641#discussion_r3758299213), use exhaustive enum conversions.\n* Addressed @stringintech's [comment](https://github.com/bitcoin/bitcoin/pull/35641#discussion_r3758299219), dropped f_exec pre-loading with a lambda. As suggested, the begin and end frames now contain dummy values that are documented as such.\n* Addressed @stringintech's [comment](https://github.com/bitcoin/bitcoin/pull/35641#discussion_r3758299224), added the suggested assertion for passing in the error.\n* Addressed @haanhvu's [comment](https://github.com/bitcoin/bitcoin/pull/35641#discussion_r3675592714), made the boost size check a hard requirement.\n* Addressed @haanhvu's [comment](https://github.com/bitcoin/bitcoin/pull/35641#discussion_r3681990206), added a quick verification that unregistering the tracer actually works.\n* Addressed @haanhvu's [comment](https://github.com/bitcoin/bitcoin/pull/35641/changes#r3695400629), added a verification that re-registering the tracer works."
  },
  {
   "t": "2026-08-28T20:46:44Z",
   "kind": "force_push",
   "who": "sedited",
   "commit": "20fb03490d1f38dde67f058acd68e816595d9222"
  },
  {
   "t": "2026-08-28T20:53:03Z",
   "kind": "review_comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "path": "src/kernel/bitcoinkernel.cpp",
   "commit": "97462c683d49792a58ba954723a205cda86a5e0b",
   "in_reply_to": 3644706556,
   "text": "I don't think this needs an extra comment. To me it seems self-explanatory."
  },
  {
   "t": "2026-08-28T20:54:01Z",
   "kind": "review_comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "path": "src/test/kernel/test_kernel.cpp",
   "commit": "114e48e968a087f74c1ab611ac2a31a9266812e1",
   "in_reply_to": 3695293748,
   "text": "I don't think this matters in a test. Besides, the new type is hardly worth the move now."
  },
  {
   "t": "2026-08-28T20:54:21Z",
   "kind": "review_comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "path": "src/test/kernel/test_kernel.cpp",
   "commit": "97462c683d49792a58ba954723a205cda86a5e0b",
   "in_reply_to": 3782561309,
   "text": "I'm not sure if this is still relevant after the last push. In any case, I don't want the tests here to be exhaustive."
  },
  {
   "t": "2026-08-31T08:23:05Z",
   "kind": "review_comment",
   "who": "alexanderwiederin",
   "assoc": "MEMBER",
   "path": "src/kernel/bitcoinkernel.h",
   "commit": "97462c683d49792a58ba954723a205cda86a5e0b",
   "in_reply_to": null,
   "text": "Needs a `///@}`"
  },
  {
   "t": "2026-08-31T10:39:25Z",
   "kind": "review_comment",
   "who": "alexanderwiederin",
   "assoc": "MEMBER",
   "path": "src/test/kernel/test_kernel.cpp",
   "commit": "97462c683d49792a58ba954723a205cda86a5e0b",
   "in_reply_to": null,
   "text": "Add `ScriptTraceUnsetCallback()` to avoid the next test case push into a destroyed vector. Test suite currently does not pass with the feature flag."
  },
  {
   "t": "2026-08-31T15:45:51Z",
   "kind": "force_push",
   "who": "sedited",
   "commit": "97462c683d49792a58ba954723a205cda86a5e0b"
  },
  {
   "t": "2026-08-31T15:46:00Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "Updated 20fb03490d1f38dde67f058acd68e816595d9222 -> 97462c683d49792a58ba954723a205cda86a5e0b ([kernel_script_tracer_1](https://github.com/sedited/bitcoin/tree/kernel_script_tracer_1) -> [kernel_script_tracer_2](https://github.com/sedited/bitcoin/tree/kernel_script_tracer_2), [compare](https://github.com/sedited/bitcoin/compare/kernel_script_tracer_1..kernel_script_tracer_2))\n\n* Addressed @alexanderwiederin's [comment](https://github.com/bitcoin/bitcoin/pull/35641#discussion_r3892814321), added close tag for new doxygen section.\n* Addressed @alexanderwiederin's [comment](https://github.com/bitcoin/bitcoin/pull/35641#discussion_r3893679920), unsetting the callback after the trace tests are complete."
  },
  {
   "t": "2026-09-01T19:40:13Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "The tracing code assumes `serror` is non-null whenever a callback is registered, but `EvalScript` permits `nullptr` for example, `CheckSignetBlockSolution()` calls `VerifyScript()` without an error pointer.\n\nThis triggers the assertion `assert(!m_callback || error);` when tracing is enabled.\n\nSuggestion:\n\n```diff\ndiff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp\nindex 45e7f48cd0..a7d2ce7440 100644\n--- a/src/script/interpreter.cpp\n+++ b/src/script/interpreter.cpp\n@@ -417,6 +417,12 @@ static bool EvalChecksig(const valtype& sig, const valtype& pubkey, CScript::con\n\n bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript& script, script_verify_flags flags, const BaseSignatureChecker& checker, SigVersion sigversion, ScriptExecutionData& execdata, ScriptError* serror)\n {\n+#ifdef ENABLE_SCRIPT_TRACE\n+    // END frames report the result even when the caller does not request it.\n+    ScriptError trace_error{SCRIPT_ERR_UNKNOWN_ERROR};\n+    if (serror == nullptr) serror = &trace_error;\n+#endif\n+\n     static const CScriptNum bnZero(0);\n     static const CScriptNum bnOne(1);\n     // static const CScriptNum bnFalse(0);\ndiff --git a/src/test/kernel/test_kernel.cpp b/src/test/kernel/test_kernel.cpp\nindex 5b97321367..cbb7464197 100644\n--- a/src/test/kernel/test_kernel.cpp\n+++ b/src/test/kernel/test_kernel.cpp\n@@ -463,6 +463,22 @@ BOOST_AUTO_TEST_CASE(btck_script_trace_tests)\n         status));\n     BOOST_CHECK_EQUAL(states.size(), 22);\n     ScriptTraceUnsetCallback();\n+\n+    // Script errors are optional for internal EvalScript callers. Exercise the\n+    // signet validation path, which intentionally does not request one.\n+    states.clear();\n+    ChainParams signet_params{ChainType::SIGNET};\n+    Block signet_block{hex_string_to_byte_vec(\n+        \"00000020f61eee3b63a380a477a063af32b2bbc97c9ff9f01f2c4225e973988108000000f575c83235984e7dc4afc1f30944c170462e84437ab6f2d52e16878a79e4678bd1914d5fae77031eccf4070001010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025151feffffff0200f2052a010000001600149243f727dd5343293eb83174324019ec16c2630f0000000000000000776a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf94c4fecc7daa2490047304402205e423a8754336ca99dbe16509b877ef1bf98d008836c725005b3c787c41ebe46022047246e4467ad7cc7f1ad98662afcaf14c115e0095a227c7b05c5182591c23e7e01000120000000000000000000000000000000000000000000000000000000000000000000000000\")};\n+    BlockValidationState validation_state;\n+    ScriptTraceSetCallback(std::make_unique<ScriptTracer>(states));\n+    const bool valid{signet_block.Check(signet_params.GetConsensusParams(), BlockCheckFlags::ALL, validation_state)};\n+    ScriptTraceUnsetCallback();\n+\n+    BOOST_CHECK(valid);\n+    BOOST_REQUIRE(!states.empty());\n+    BOOST_CHECK(states.back().m_kind == ScriptTraceFrameKind::END);\n+    BOOST_CHECK_EQUAL(states.back().m_script_error, 0);\n #endif\n }\n```"
  },
  {
   "t": "2026-09-01T20:05:12Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "`EvalScript` rejects oversized scripts before constructing `ScriptTraceScope`, so a registered callback receives neither `BEGIN` nor `END`.\nMoving the size check below scope construction produces:\n\n```\ncreate scope \u2192 BEGIN\nsize check \u2192 set SCRIPT_ERR_SCRIPT_SIZE \u2192 return\nscope destructor \u2192 END with SCRIPT_ERR_SCRIPT_SIZE\n```\n\nNon-tracing behavior remains the same.\n\nSuggestion:\n\n```diff\ndiff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp\nindex 45e7f48cd0..1116ffd9c2 100644\n--- a/src/script/interpreter.cpp\n+++ b/src/script/interpreter.cpp\n@@ -436,9 +436,6 @@ bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript&\n     ConditionStack vfExec;\n     std::vector<valtype> altstack;\n     set_error(serror, SCRIPT_ERR_UNKNOWN_ERROR);\n-    if ((sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0) && script.size() > MAX_SCRIPT_SIZE) {\n-        return set_error(serror, SCRIPT_ERR_SCRIPT_SIZE);\n-    }\n     int nOpCount = 0;\n     bool fRequireMinimal = (flags & SCRIPT_VERIFY_MINIMALDATA) != 0;\n     uint32_t opcode_pos = 0;\n@@ -447,6 +444,10 @@ bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript&\n\n     SCRIPT_TRACE_SCOPE(stack, script, opcode_pos, altstack, nOpCount, sigversion, execdata.m_tapleaf_hash_init ? execdata.m_tapleaf_hash.data() : nullptr, execdata.m_codeseparator_pos, serror);\n\n+    if ((sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0) && script.size() > MAX_SCRIPT_SIZE) {\n+        return set_error(serror, SCRIPT_ERR_SCRIPT_SIZE);\n+    }\n+\n     try\n     {\n         for (; pc < pend; ++opcode_pos) {\ndiff --git a/src/test/kernel/test_kernel.cpp b/src/test/kernel/test_kernel.cpp\nindex 5b97321367..999199bb3b 100644\n--- a/src/test/kernel/test_kernel.cpp\n+++ b/src/test/kernel/test_kernel.cpp\n@@ -4,6 +4,7 @@\n\n #include <kernel/bitcoinkernel.h>\n #include <kernel/bitcoinkernel_wrapper.h>\n+#include <script/script_error.h>\n #include <util/byte_units.h>\n #include <util/fs.h>\n\n@@ -463,6 +464,23 @@ BOOST_AUTO_TEST_CASE(btck_script_trace_tests)\n         status));\n     BOOST_CHECK_EQUAL(states.size(), 22);\n     ScriptTraceUnsetCallback();\n+\n+    std::vector<ScriptTracer::Snapshot> oversized_states;\n+    ScriptPubkey oversized_script_pubkey{std::vector<std::byte>(10'001)};\n+    ScriptTraceSetCallback(std::make_unique<ScriptTracer>(oversized_states));\n+    BOOST_CHECK(!oversized_script_pubkey.Verify(\n+        /*amount=*/0,\n+        /*tx_to=*/legacy_spending_tx,\n+        /*precomputed_txdata=*/nullptr,\n+        /*input_index=*/0,\n+        VERIFY_ALL_PRE_TAPROOT,\n+        status));\n+    ScriptTraceUnsetCallback();\n+\n+    BOOST_REQUIRE(oversized_states.size() >= 2);\n+    BOOST_CHECK(oversized_states[oversized_states.size() - 2].m_kind == ScriptTraceFrameKind::BEGIN);\n+    BOOST_CHECK(oversized_states.back().m_kind == ScriptTraceFrameKind::END);\n+    BOOST_CHECK_EQUAL(oversized_states.back().m_script_error, SCRIPT_ERR_SCRIPT_SIZE);\n #endif\n }\n ```"
  },
  {
   "t": "2026-09-01T20:37:18Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "Is there a specific reason these need to be function-like macros? Expanding them at their sole call site makes the object lifetime and `Step()` call explicit and easier to debug, while the `#ifdefs` still prevent argument evaluation when tracing is disabled.\n\nThe trade-off is some additional preprocessor noise and repetition if more step sites are added, but with only one scope and one `Step()` call, this seems worthwhile.\n\nPossible version without the tracing helper macros:\n\n```diff\ndiff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp\nindex 45e7f48cd0..f23673007f 100644\n--- a/src/script/interpreter.cpp\n+++ b/src/script/interpreter.cpp\n@@ -445,7 +445,19 @@ bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript&\n     execdata.m_codeseparator_pos = 0xFFFFFFFFUL;\n     execdata.m_codeseparator_pos_init = true;\n\n-    SCRIPT_TRACE_SCOPE(stack, script, opcode_pos, altstack, nOpCount, sigversion, execdata.m_tapleaf_hash_init ? execdata.m_tapleaf_hash.data() : nullptr, execdata.m_codeseparator_pos, serror);\n+#ifdef ENABLE_SCRIPT_TRACE\n+    ScriptTraceScope script_trace_scope{\n+        stack,\n+        script,\n+        opcode_pos,\n+        altstack,\n+        nOpCount,\n+        static_cast<uint8_t>(sigversion),\n+        execdata.m_tapleaf_hash_init ? execdata.m_tapleaf_hash.data() : nullptr,\n+        execdata.m_codeseparator_pos,\n+        serror,\n+    };\n+#endif\n\n     try\n     {\n@@ -459,7 +471,9 @@ bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript&\n                 return set_error(serror, SCRIPT_ERR_BAD_OPCODE);\n             }\n\n-            SCRIPT_TRACE_STEP(fExec, opcode);\n+#ifdef ENABLE_SCRIPT_TRACE\n+            script_trace_scope.Step(fExec, static_cast<uint8_t>(opcode));\n+#endif\n\n             if (vchPushValue.size() > MAX_SCRIPT_ELEMENT_SIZE)\n                 return set_error(serror, SCRIPT_ERR_PUSH_SIZE);\ndiff --git a/src/script/trace.h b/src/script/trace.h\nindex 7cd63fffdd..1f1e6aec8e 100644\n--- a/src/script/trace.h\n+++ b/src/script/trace.h\n@@ -104,15 +104,6 @@ private:\n     }\n };\n\n-#define SCRIPT_TRACE_SCOPE(stack, script, opcode_pos, altstack, nOpCount, sigversion, tapleaf_hash, codeseparator_pos, error) \\\n-    ScriptTraceScope script_trace_scope { stack, script, opcode_pos, altstack, nOpCount, static_cast<uint8_t>(sigversion), tapleaf_hash, codeseparator_pos, error }\n-\n-#define SCRIPT_TRACE_STEP(fExec, opcode) \\\n-    script_trace_scope.Step(fExec, static_cast<uint8_t>(opcode))\n-\n-#else\n-#define SCRIPT_TRACE_SCOPE(stack, script, opcode_pos, altstack, nOpCount, sigversion, tapleaf_hash, codeseparator_pos, error) static_assert(true)\n-#define SCRIPT_TRACE_STEP(fExec, opcode) static_assert(true)\n #endif // ENABLE_SCRIPT_TRACE\n\n #endif // BITCOIN_SCRIPT_TRACE_H\n```"
  }
 ],
 "labels_log": [
  {
   "t": "2026-07-02T15:00:22Z",
   "action": "labeled",
   "label": "Validation",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-02T20:19:46Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-02T21:20:35Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-11T21:12:35Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-28T20:47:14Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-28T22:05:01Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-28T22:22:44Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-07-02T20:20:55Z",
   "kind": "ready_for_review",
   "who": "sedited"
  },
  {
   "t": "2026-07-04T10:16:40Z",
   "kind": "renamed",
   "who": "sedited",
   "from": "kernel: Add script execution tracer",
   "to": "kernel: Add script evaluation tracer"
  }
 ],
 "text_chars": 22551,
 "text_tokens_estimate": 5637,
 "changed_paths": [
  "src/kernel/CMakeLists.txt",
  "src/kernel/bitcoinkernel.cpp",
  "src/kernel/bitcoinkernel.h",
  "src/kernel/bitcoinkernel_wrapper.h",
  "src/script/interpreter.cpp",
  "src/script/trace.cpp",
  "src/script/trace.h",
  "src/test/kernel/CMakeLists.txt",
  "src/test/kernel/test_kernel.cpp"
 ],
 "files": [
  {
   "path": "src/kernel/CMakeLists.txt",
   "add": 6,
   "del": 0
  },
  {
   "path": "src/kernel/bitcoinkernel.cpp",
   "add": 144,
   "del": 1
  },
  {
   "path": "src/kernel/bitcoinkernel.h",
   "add": 134,
   "del": 0
  },
  {
   "path": "src/kernel/bitcoinkernel_wrapper.h",
   "add": 89,
   "del": 0
  },
  {
   "path": "src/script/interpreter.cpp",
   "add": 8,
   "del": 1
  },
  {
   "path": "src/script/trace.cpp",
   "add": 40,
   "del": 0
  },
  {
   "path": "src/script/trace.h",
   "add": 118,
   "del": 0
  },
  {
   "path": "src/test/kernel/CMakeLists.txt",
   "add": 4,
   "del": 0
  },
  {
   "path": "src/test/kernel/test_kernel.cpp",
   "add": 74,
   "del": 0
  }
 ],
 "test_lines": 78,
 "git": {
  "head": "97462c683d49792a58ba954723a205cda86a5e0b",
  "head_matches_backup": true,
  "base": "ea7d459ac08b05521021e3d273625424e1b9b19e",
  "commits": [
   {
    "sha": "97462c683d",
    "subject": "kernel: Add script tracer",
    "files": 9,
    "add": 617,
    "del": 2
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "eb3a3c209c06dae6",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}