#35641 kernel: Add script evaluation tracer

ReviewabilityReviewsAgreementSize
#35641 sedited kernel: Add script evaluation tracerWaiting on author1 (+1)Mild549 + 111 tests
  • Expose step-by-step script execution snapshots to libbitcoinkernel consumers
  • Enables external script debuggers, execution visualizers, and deeper test inspection
  • Worthwhile to review now as nothing invalidates review
  • Broad concept support and extensive test verification
  • Tested ACK across thousands of script spends compared against an external interpreter (fametrano)
  • Unaddressed objection: mutex locking causes contention during parallel verification (fametrano)
  • Answered objection without follow-up: inactive branch opcodes reported as skipped (w0xlt)
  • Concept approval highlighting debugging and testing benefits (furszy, theStack)
  • 549 lines added or modified outside tests
  • 111 lines added or modified in tests
  • 2 lines removed in total
  • 9 files, 1 commits
Kernel (libbitcoinkernel)#14 of 18
P3 · new feature
  • P3 because it adds an optional debugging API rather than core validation or chainstate logic
  • Useful for downstream debuggers and tooling, but remains an opt-in extension
Reviewability: ReadyStalePaused
Agreement: StrongPositivePositive w/ caveatsNeutralMildDisputedBlockedCrickets
Reviews: current code-review ACKs, then (+stale ACKs) and -NACKs; greener = more ACKs.
Size: lines added or modified outside tests, then in tests; greener = smaller.

Goal

Adds an optional compile-time script execution tracer exposed through the libbitcoinkernel C API and C++ wrapper. When compiled with `-DENABLE_SCRIPT_TRACE=ON`, hooks in `EvalScript` emit execution state snapshots (stack, altstack, opcode position, execution flag, op count, sigversion, tapleaf hash, codeseparator position, and error code) at execution start, each instruction step, and completion. When the CMake option is disabled (default), the trace hooks compile to no-ops.

Problem: External consumers of libbitcoinkernel cannot observe intermediate script execution states, preventing them from implementing step-by-step script debuggers or execution visualizers against Bitcoin Core's interpreter. Additionally, testing and fuzzing harnesses cannot easily verify internal interpreter behavior when later errors shadow earlier states.

Evidence of importance:

  • The author demonstrated the use case by building a proof-of-concept debugger against rust-bitcoinkernel (PR description: 'The initial sketch for this work was prepared on rust-bitcoinkernel... That branch also includes a very rudimentary script debugger showcasing the use case for this patch')
  • The author cites PR #33961 as an example where test bugs occurred because 'error conditions shadow actual behaviour', which intermediate trace inspection could prevent

Scope: The feature is gated behind the CMake flag `-DENABLE_SCRIPT_TRACE=ON`. In default builds, the macro hooks in `src/script/interpreter.cpp` expand to `static_assert(true)`.

Risk of a mistake: medium (validation, consensus). A mistake in the macro hooks or state references in EvalScript could alter script evaluation behavior or crash validation if compiled with tracing enabled.

Changed since the previous assessment: No substantial architectural or API changes; the update represents a rebase and minor implementation polish.

Reviewability

Paused · Waiting on author

Open objection from fametrano with no author reply for 7 days; review would repeat what has been asked.

Waiting on (from the thread read):

  • author: respond to fametrano's concern regarding ScriptTraceGetCallback mutex contention during parallel evaluation 2026-10-25

Agreement

Mild

objection that needs a change was answered, no reply from the objector (w0xlt: tracer incorrectly reports control-flow opcodes inside inactive branches as skipped even though they are executed by the); objection that needs an answer has none yet (fametrano)

Thread notes: Added Tested ACK and mutex contention objection from fametrano. The PR is now waiting on the author to respond to fametrano's proposed atomic check to avoid lock contention.

Objections, suggestions, and questions:

ReviewerTypeHarm, request, or questionStatusMust beAuthor repliedQuote
alexanderwiederin (member)question, interfacewhether multi-threaded applications are supported and desiredanswered · author explained thread safety and that multithreading is not the main use caseansweredyes2026-07-06: 'Does this support multi-threaded applications? If not, do we want it to?'
Status from: 2026-07-06: sedited: 'It should be thread-safe, but I don't think this is particularly interesting to run in a multi-threaded context.'
alexanderwiederin (member)question, maintenancewhether a CI job is needed to run with ENABLE_SCRIPT_TRACE=ONanswered · author replied unsure where to put it and questioned if worthwhile yetansweredyes2026-07-10: 'Do we need a CI job to run with `ENABLE_SCRIPT_TRACE=ON`?'
Status from: 2026-07-10: sedited: 'I wasn't sure where to put such a job, so I didn't introduce it here.'
alexanderwiederin (member)suggestion, interfacedefine what each error code meansanswered · author replied preferring to expose the error list in a follow-up PRansweredyes2026-07-10: 'I think we have to define what each code means. What do you think?'
Status from: 2026-07-10: sedited: 'I think we should leave this here - it's fairly straight forward to get the error code header from somewhere. I'll try t'
alexanderwiederin (member)suggestion, approachsnapshot the callback in ScriptTraceScope's constructor to take mutex acquisition and std::function copy out of per-opcode pathfixed · fix pushed · author implemented callback snapshotting in constructoransweredyes2026-07-10: '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.'
Status from: 2026-07-13: sedited: 'Done.'
elmeriniemela (none)question, correctnesswhether it is expected that a trace can be invalid while the script error code is OKanswered · author explained this is expected because EvalScript does not enforce final stack conditionsansweredyes2026-07-24: 'one weirdness that I found is that a trace can be invalid, but the related script error code is still OK.'
Status from: 2026-07-25: sedited: 'What you are seeing is the intended behaviour here. Your LLM seems to already have inferred the correct scenario from th'
haanhvu (none)suggestion, maintenanceadd a comment that user data is destroyed in any case to avoid problems like double freeanswered · author declined, viewing it as self-explanatoryansweredyes2026-07-24: 'Should we clearly comment that user data is destroyed in any case, whether tracing enabled/succeeds or not, to avoid problems like double free?'
Status from: 2026-08-28: sedited: 'I don't think this needs an extra comment. To me it seems self-explanatory.'
haanhvu (none)objection, safetyBOOST_CHECK_EQUAL on vector size allows test execution to continue and access out-of-bounds indices, causing undefined behaviorfixed · fix pushed · author fixed by making boost size check a hard requirementansweredyes2026-07-29: '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, '
Status from: 2026-08-28: sedited: 'made the boost size check a hard requirement.'
haanhvu (none)suggestion, correctnessverify that unregistering the tracer actually stops tracingfixed · fix pushed · author added verification that unregistering worksansweredyes2026-07-30: 'Should we verify unregistering actually stops tracing?'
Status from: 2026-08-28: sedited: 'added a quick verification that unregistering the tracer actually works.'
haanhvu (none)suggestion, stylestd::move state into m_state to avoid an extra copy of ScriptTraceFrameanswered · author declined as not mattering for a testansweredyes2026-08-01: '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?'
Status from: 2026-08-28: sedited: 'I don't think this matters in a test. Besides, the new type is hardly worth the move now.'
haanhvu (none)suggestion, correctnessextend tests to cover callback replacement contractfixed · fix pushed · author added test verification that re-registering the tracer worksansweredyes2026-08-01: 'Should we extend the test to cover this callback replacement contract?'
Status from: 2026-08-28: sedited: 'added a verification that re-registering the tracer works.'
stringintech (contributor)suggestion, interfacemake frame opaque with getters instead of a public structfixed · fix pushed · author wrapped the frame into nested opaque types and gettersansweredyes2026-08-11: 'would it make sense to make the frame opaque like other `btck_*` types, with getters, instead of a public struct'
Status from: 2026-08-28: sedited: 'wrapped the frame into nested opaque types and getters.'
stringintech (contributor)suggestion, interfaceuse exhaustive switch for kind and sig_version conversionsfixed · fix pushed · author switched to exhaustive enum conversionsansweredyes2026-08-11: 'Perhaps it is better to use an exhaustive switch for `kind` and `sig_version` as well, in both directions'
Status from: 2026-08-28: sedited: 'use exhaustive enum conversions.'
stringintech (contributor)suggestion, approachdrop the closure for f_exec and use a fixed dummy in begin and end framesfixed · fix pushed · author dropped f_exec preloading closure and documented dummy values in begin and end framesansweredyes2026-08-11: 'can we drop the closure and give it a fixed dummy in begin and end frames?'
Status from: 2026-08-28: sedited: 'dropped f_exec pre-loading with a lambda. As suggested, the begin and end frames now contain dummy values'
stringintech (contributor)objection, correctnessnull error pointer causes successful evaluation to report SCRIPT_ERR_UNKNOWN_ERROR in trace end framesfixed · fix pushed · author fixed by adding assertion for passing in erroransweredyes2026-08-11: '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 i'
Status from: 2026-08-28: sedited: 'added the suggested assertion for passing in the error.'
haanhvu (none)suggestion, correctnessverify other fields besides m_kind and m_script_error in testsanswered · author declined making tests exhaustiveansweredyes2026-08-14: 'Should we also verify other fields besides `m_kind` and `m_script_error` to see if the deep-copy conversion works fully correctly?'
Status from: 2026-08-28: sedited: '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.'
alexanderwiederin (member)objection, correctnesstest suite fails when compiled with ENABLE_SCRIPT_TRACE=ON due to callback pushing into destroyed vector in subsequent testsfixed · fix pushed · author fixed by unsetting the callback after trace tests completechangedyes2026-08-31: 'Add ScriptTraceUnsetCallback() to avoid the next test case push into a destroyed vector. Test suite currently does not pass with the feature flag.'
Status from: 2026-08-31: sedited: 'unsetting the callback after the trace tests are complete.'
alexanderwiederin (member)suggestion, interfaceadd BITCOINKERNEL_WARN_UNUSED_RESULT to kernel functionfixed · fix pushed · author added BITCOINKERNEL_WARN_UNUSED_RESULT annotationansweredyes2026-08-31: 'This needs a `BITCOINKERNEL_WARN_UNUSED_RESULT`'
Status from: 2026-10-06: sedited: 'added suggested `BITCOINKERNEL_WARN_UNUSED_RESULT`'
w0xlt (contributor)objection, correctnessEvalScript triggers an assertion failure when tracing is enabled if called with a null serror pointer (e.g. in CheckSignetBlockSolution)fixed · fix pushed · author pushed fix making error code optional instead of assertingchangedyes2026-09-01: 'The tracing code assumes serror is non-null whenever a callback is registered, but EvalScript permits nullptr... This triggers the assertion assert(!m_callback || error);'
Status from: 2026-09-24: sedited: 'making the error code "optional" by encoding `-1` as a nullopt. This gets rid of the assertion'
w0xlt (contributor)objection, correctnessoversized scripts are rejected before ScriptTraceScope is instantiated, preventing BEGIN and END trace frames from being emittedanswered · author declined changing interpreter and noted kernel lib catches the length checkansweredyes2026-09-01: 'EvalScript rejects oversized scripts before constructing ScriptTraceScope, so a registered callback receives neither BEGIN nor END.'
Status from: 2026-09-24: sedited: 'I don't want to change anything in the interpreter in this pull request. When running through the kernel lib's script ve'
w0xlt (contributor)question, stylewhether function-like macros are needed instead of expanding ifdefs at call sitesanswered · author declined changing, preferring function macros to minimize interpreter line changesansweredyes2026-09-01: 'Is there a specific reason these need to be function-like macros?'
Status from: 2026-09-24: sedited: 'I'd like to keep the lines changed in the interpreter to an absolute minimum. My preference style-wise is the function m'
w0xlt (contributor)objection, correctnesstracer incorrectly reports control-flow opcodes inside inactive branches as skipped even though they are executed by the interpreteranswered · author declined changing fExec meaning, added docs clarification and a test casechangedyes2026-09-24: 'The tracer uses `fExec` to indicates whether the current conditional branch is active. But the interpreter needs to process control-flow opcodes even inside inactive branches... the tracer incorrectly'
Status from: 2026-09-25: sedited: 'added a sentence in the docs that conditional opcodes are indeed processed when fexec is set. I don't think it is a good'
alexanderwiederin (member)suggestion, interfaceadd BITCOINKERNEL_WARN_UNUSED_RESULT annotations to kernel trace header functionsfixed · fix pushed · author added suggested BITCOINKERNEL_WARN_UNUSED_RESULT annotationsansweredyes2026-10-05: 'Missing some `BITCOINKERNEL_WARN_UNUSED_RESULT` I think.'
Status from: 2026-10-06: sedited: 'added suggested `BITCOINKERNEL_WARN_UNUSED_RESULT`'
alexanderwiederin (member)suggestion, interfaceadd BITCOINKERNEL_WARN_UNUSED_RESULT to kernel functionfixed · fix pushed · author added BITCOINKERNEL_WARN_UNUSED_RESULT annotationansweredyes2026-10-05: 'This needs a `BITCOINKERNEL_WARN_UNUSED_RESULT`'
Status from: 2026-10-06: sedited: 'added suggested `BITCOINKERNEL_WARN_UNUSED_RESULT`'
alexanderwiederin (member)suggestion, interfaceadd BITCOINKERNEL_WARN_UNUSED_RESULT to kernel functionfixed · fix pushed · author added BITCOINKERNEL_WARN_UNUSED_RESULT annotationansweredyes2026-10-05: 'This needs a `BITCOINKERNEL_WARN_UNUSED_RESULT`'
Status from: 2026-10-06: sedited: 'added suggested `BITCOINKERNEL_WARN_UNUSED_RESULT`'
fametrano (contributor)objection, approachScriptTraceGetCallback locks g_script_trace_mutex on every EvalScript call even with no tracer registered, causing mutex contention during parallel script verificationopenansweredno2026-10-25: 'ScriptTraceGetCallback() still locks g_script_trace_mutex on every EvalScript call, so parallel script checks contend on it.'

Support:

  • furszy (member) (Concept ACK, 2026-07-02): haven't checked the implementation. But this is something that would benefit testing greatly.
  • w0xlt (contributor) (Concept ACK, 2026-07-02): Strong Concept ACK [not substantive]
  • theStack (member) (Concept ACK, 2026-07-08): Concept ACK [not substantive]
  • alexanderwiederin (member) (ACK, 2026-07-15): ACK 114e48e968a087f74c1ab611ac2a31a9266812e1 [not substantive]
  • elmeriniemela (none) (2026-07-22) [tested]: tested with a clanker... Added python bindings... Made a visualizer using the traces... FWIW, found no issues
  • haanhvu (none) (Approach ACK, 2026-08-14): Approach ACK [not substantive]
  • fametrano (contributor) (Tested ACK, 2026-10-25) [both: C API, ScriptTraceScope, ScriptTraceGetCallback(), test_kernel]: I drove the tracer through the C API over 32,535 spends: legacy, P2SH, P2WSH and tapscript, valid and invalid, with branches not taken, OP_CODESEPARATOR, OP_SUCCESSx and scripts that fail on their last opcode. I compared each frame with a step trace of a script interpreter I maintain. All 158,683 st

Participants:

  • furszy (member, support, 1 statements 2026-07-02 to 2026-07-02): Concept ACK noting it would benefit testing greatly
  • w0xlt (contributor, objection, 5 statements 2026-07-02 to 2026-09-24): Strong Concept ACK, but objected to assertion on null serror, missing frames on oversized scripts, and incorrect exec reporting for control flow
  • alexanderwiederin (member, objection, 15 statements 2026-07-06 to 2026-10-05): ACKed earlier revision, but pointed out test suite failures with ENABLE_SCRIPT_TRACE=ON and missing BITCOINKERNEL_WARN_UNUSED_RESULT annotations
  • theStack (member, support, 1 statements 2026-07-08 to 2026-07-08): Concept ACK
  • elmeriniemela (none, support, 2 statements 2026-07-22 to 2026-07-24): tested with python bindings and visualizer, finding no issues
  • haanhvu (none, objection, 7 statements 2026-07-24 to 2026-08-14): Approach ACK, but raised out-of-bounds UB concern on test assertions which was resolved
  • stringintech (contributor, objection, 4 statements 2026-08-11 to 2026-08-11): Suggested opaque types, exhaustive enums, and asserting non-null error pointer in trace scope
  • fametrano (contributor, objection, 1 statements 2026-10-25 to 2026-10-25): Tested ACK after extensive verification, but raised objection to mutex contention in EvalScript when tracing is enabled without a registered callback

Files

111 lines under test/bench/ci.

  • src/kernel/bitcoinkernel.cpp +146/-1
  • src/kernel/bitcoinkernel.h +135/-0
  • src/script/trace.h +118/-0
  • src/test/kernel/test_kernel.cpp +107/-0
  • src/kernel/bitcoinkernel_wrapper.h +96/-0
  • src/script/trace.cpp +40/-0
  • src/script/interpreter.cpp +8/-1
  • src/kernel/CMakeLists.txt +6/-0
  • src/test/kernel/CMakeLists.txt +4/-0

Card

The short text the ranking pass compares across PRs.

Adds an optional compile-time script execution tracing framework exposed via the libbitcoinkernel C API and C++ wrapper. External libbitcoinkernel consumers currently have no way to inspect intermediate script interpreter states, preventing the development of external script debuggers or fine-grained execution analyzers. The author provides a working prototype debugger in rust-bitcoinkernel to demonstrate practical utility and notes that intermediate frames can help catch interpreter bugs shadowed by later failure codes in tests. The feature is disabled by default via CMake and has no dependencies.

Data

dossier JSON · extract JSON · model ledger, generated 2026-10-26T01:21, confidence high, input hash 677a530429c17e5e
thread read through 2026-10-25 (54 statements), head 0161ebdbf6
labels: Validation

Categories

Kernel (libbitcoinkernel)

P3 · new feature

Exposes intermediate script execution states to libbitcoinkernel consumers for debuggers and visualizers. The PR adds an optional compile-time API that downstream tools (such as rust-bitcoinkernel) can build on, but it is an opt-in debugging extension rather than core block validation or chainstate functionality, making it worthwhile but deferrable.

Membership: Adds C API functions and C++ wrapper headers in src/kernel/ and kernel test coverage for script execution tracing.

Factors: security/stability 0, bug 0, performance 0, user value 2, leverage 1