From 3fe0d0348fe12baad0249238e0bd440e601506d5 Mon Sep 17 00:00:00 2001 From: Vladislav Volosnikov Date: Fri, 15 May 2026 07:43:06 -0500 Subject: [PATCH 1/9] feat(bench): wrap SHA3 and ecrecover dispatch in EE-invocation cycle markers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two cycle-marker labels that fire only on EE-driven invocations: - `keccak_execution_environment` around the EVM SHA3 opcode dispatch (including the `len == 0` empty-slice shortcut), so per-execution cycles can be joined 1:1 with the opcode-level SHA3 sample stream. The inner `"keccak"` marker (from `Keccak256Impl::execute`) still fires, so bootloader/intrinsic keccak invocations remain attributable to the existing `"keccak"` label only. - `ecrecover_execution_environment` around the EE-precompile ecrecover dispatch via a new `EcRecoverEEInvocation` wrapper. Intrinsic signature-recovery calls from the bootloader do not go through this path, so the marker fires only for EE-triggered calls — no positional intrinsic-filter heuristic needed in joiner scripts. Both markers are pure observability: the underlying system-function calls are unchanged. Required as STF-side instrumentation for the follow-up benchmarking-pipeline PR (CI workflow, joiner scripts, and docs land separately). Also extract `install_precompile_hook` from `add_precompile_ext` so the `PRECOMPILE_ADDRESSES_LOWS` sanity check stays centralized for any future custom invocation type. Co-Authored-By: Claude Opus 4.7 (1M context) --- evm_interpreter/src/instructions/system.rs | 100 ++++++++++++++------- system_hooks/src/lib.rs | 87 +++++++++++++++--- 2 files changed, 144 insertions(+), 43 deletions(-) diff --git a/evm_interpreter/src/instructions/system.rs b/evm_interpreter/src/instructions/system.rs index c6158591d..cf15ea377 100644 --- a/evm_interpreter/src/instructions/system.rs +++ b/evm_interpreter/src/instructions/system.rs @@ -21,40 +21,78 @@ impl Interpreter<'_, S> { let len = Self::cast_to_usize(&len, EvmError::InvalidOperandOOG.into())?; self.gas.spend_gas_and_native(0, KECCAK256_NATIVE_COST)?; - let hash = if len == 0 { - self.gas.spend_gas(gas_constants::SHA3)?; - Self::EMPTY_SLICE_SHA3 + // Cycle marker wraps the SHA3 dispatch (including the `len == 0` + // constant-lookup shortcut) so it fires exactly once per SHA3 + // opcode invocation, matching the tracer's per-call sample count. + // Otherwise positional pairing in `cycles_per_native_report.py` / + // `join_precompile_samples.py` breaks when some invocations skip + // the inner keccak SF dispatch. The inner `"keccak"` marker (from + // `Keccak256Impl::execute`) still fires, so bootloader/intrinsic + // keccak invocations remain under the `"keccak"` label only. + // Eagerly cast `memory_offset` to a `usize` (Copy, no aliasing) BEFORE + // entering the cycle marker's closure. Otherwise the closure would + // capture `&memory_offset` (a `&U256` borrowed from `self.stack` via + // `pop_2()`) and conflict with the mutable `self.gas` / `self.heap` + // borrows inside the closure. For `len == 0` we never use the offset. + let memory_offset_usize: Option = if len > 0 { + Some(Self::cast_to_usize( + &memory_offset, + EvmError::InvalidOperandOOG.into(), + )?) } else { - let memory_offset = - Self::cast_to_usize(&memory_offset, EvmError::InvalidOperandOOG.into())?; - - self.resize_heap(memory_offset, len)?; - - let allocator = system.get_allocator(); - let input = &self.heap[memory_offset..(memory_offset + len)]; - - let mut dst = U256Builder::default(); - S::SystemFunctions::keccak256(&input, &mut dst, self.gas.resources_mut(), allocator) - .map_err(SystemError::from)?; - - let hash_ruint = dst.build(); - - if Self::PRINT_OPCODES { - use core::fmt::Write; - use zk_ee::logger_log; - use zk_ee::system::logger::Logger; - let mut logger = system.get_logger(); - let input = &self.heap()[memory_offset..(memory_offset + len)]; - let input_iter = input.iter().copied(); - logger_log!(logger, " input: ",); - let _ = logger.log_data(input_iter); - logger_log!(logger, " -> 0x{hash_ruint:0x}"); - } - - // Convert ruint::aliases::U256 to u256::U256 - U256::from(hash_ruint) + None }; + // Use `wrap!` (markers only) rather than `wrap_with_resources!` + // here: the latter expands `$resources.clone()` BEFORE and AFTER the + // inner closure to compute the ergs/native diff, and those borrow + // `self.gas` mutably while the closure ALSO captures `self` (for the + // `spend_gas`/`resize_heap`/`heap` accesses inside) → conflicting + // mutable borrows. `wrap!` doesn't snapshot resources, so the closure + // is the sole borrower. We don't need the per-call ergs/native log + // here — SHA3 gas/native are already captured by `EvmOpcodeStatsTracer`. + let hash = cycle_marker::wrap!("keccak_execution_environment", { + let r: Result = match memory_offset_usize { + None => { + self.gas.spend_gas(gas_constants::SHA3)?; + Ok(Self::EMPTY_SLICE_SHA3) + } + Some(memory_offset) => { + self.resize_heap(memory_offset, len)?; + + let allocator = system.get_allocator(); + let input = &self.heap[memory_offset..(memory_offset + len)]; + + let mut dst = U256Builder::default(); + S::SystemFunctions::keccak256( + &input, + &mut dst, + self.gas.resources_mut(), + allocator, + ) + .map_err(SystemError::from)?; + + let hash_ruint = dst.build(); + + if Self::PRINT_OPCODES { + use core::fmt::Write; + use zk_ee::logger_log; + use zk_ee::system::logger::Logger; + let mut logger = system.get_logger(); + let input = &self.heap()[memory_offset..(memory_offset + len)]; + let input_iter = input.iter().copied(); + logger_log!(logger, " input: ",); + let _ = logger.log_data(input_iter); + logger_log!(logger, " -> 0x{hash_ruint:0x}"); + } + + // Convert ruint::aliases::U256 to u256::U256 + Ok(U256::from(hash_ruint)) + } + }; + r + })?; + self.stack.push(&hash) } diff --git a/system_hooks/src/lib.rs b/system_hooks/src/lib.rs index 0e9d88649..a43944882 100644 --- a/system_hooks/src/lib.rs +++ b/system_hooks/src/lib.rs @@ -125,6 +125,40 @@ where } } +/// EE-triggered ecrecover invocation. Wraps the existing ecrecover dispatch +/// in an outer `"ecrecover_execution_environment"` cycle marker so +/// per-execution cycles can be joined cleanly with the +/// `PrecompileStatsTracer` `ecrecover.samples` (which only sees EE precompile +/// dispatch frames). The bootloader's intrinsic sig-recovery calls do not go +/// through this path, so the new marker fires only for EE-triggered calls. +/// The inner `"ecrecover"` marker (from `EcRecoverImpl::execute`) still +/// fires, preserving backward compatibility for older consumers. +struct EcRecoverEEInvocation(PhantomData); + +impl + SystemFunctionInvocation + for EcRecoverEEInvocation +where + S::IO: IOSubsystemExt, + S: EthereumLikeTypes, +{ + fn invoke + ?Sized, A: core::alloc::Allocator + Clone>( + oracle: &mut ::IOOracle, + logger: &mut S::Logger, + input: &[u8], + output: &mut D, + resources: &mut S::Resources, + allocator: A, + ) -> Result<(), SubsystemError> + { + cycle_marker::wrap_with_resources!("ecrecover_execution_environment", resources, { + >::Secp256k1ECRecover::execute( + input, output, resources, oracle, logger, allocator, + ) + }) + } +} + /// /// Adds EVM precompiles hooks. /// @@ -134,12 +168,16 @@ pub fn add_precompiles( where S::IO: IOSubsystemExt, { - add_precompile_ext::< - _, - _, - >::Secp256k1ECRecover, - Secp256k1ECRecoverErrors, - >(hooks, ECRECOVER_HOOK_ADDRESS_LOW)?; + // EE-frame ecrecover dispatch uses a dedicated invocation wrapper that + // emits the `"ecrecover_execution_environment"` cycle marker around the + // underlying system function call, so per-execution stats can be joined + // without the positional bootloader/intrinsic filter heuristic. Routed + // through `install_precompile_hook` so the address sanity check stays + // in lockstep with `add_precompile` / `add_precompile_ext`. + install_precompile_hook::, Secp256k1ECRecoverErrors>( + hooks, + ECRECOVER_HOOK_ADDRESS_LOW, + )?; add_precompile::<_, _, >::Sha256, Sha256Errors>( hooks, SHA256_HOOK_ADDRESS_LOW, @@ -351,6 +389,36 @@ where ) } +/// Install a precompile hook with a hand-picked invocation type. +/// +/// Centralizes the `PRECOMPILE_ADDRESSES_LOWS` sanity check so any future +/// hook wired through this helper inherits the same defensive guard as +/// `add_precompile`. Used both by `add_precompile_ext` (generic SystemFunctionExt +/// dispatch) and the ecrecover EE dispatch below (which uses a custom +/// invocation type to inject the `ecrecover_execution_environment` cycle +/// marker). +fn install_precompile_hook( + hooks: &mut HooksStorage, + address_low: u16, +) -> Result<(), InternalError> +where + S: EthereumLikeTypes, + S::IO: IOSubsystemExt, + A: Allocator + Clone, + I: SystemFunctionInvocation, + E: Subsystem, +{ + if !PRECOMPILE_ADDRESSES_LOWS.contains(&address_low) { + return Err(internal_error!( + "Attempted to add a precompile that is not in the precompile addresses list" + )); + } + hooks.add_call_hook( + address_low, + SystemCallHook::new(pure_system_function_hook_impl::), + ) +} + fn add_precompile_ext< S: EthereumLikeTypes, A: Allocator + Clone, @@ -363,12 +431,7 @@ fn add_precompile_ext< where S::IO: IOSubsystemExt, { - hooks.add_call_hook( - address_low, - SystemCallHook::new( - pure_system_function_hook_impl::, E, S>, - ), - ) + install_precompile_hook::, E>(hooks, address_low) } /// From d8bb2e50f74fd753b90c5099ddbb322f231ad6fb Mon Sep 17 00:00:00 2001 From: Vladislav Volosnikov Date: Fri, 15 May 2026 07:43:17 -0500 Subject: [PATCH 2/9] =?UTF-8?q?feat(bench):=20add=20da=5Fcommitment=20mark?= =?UTF-8?q?er;=20rename=20verify=5Fand=5Fapply=5Fbatch=20=E2=86=92=20state?= =?UTF-8?q?=5Fcommitment=5Fupdate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap the per-block `write_pubdata` call inside the three proving post-tx-op handlers (single-block, multi-block, sequencing) in a new `da_commitment` cycle marker. For keccak DA this is where the bulk of keccak delegations fire (bytes are absorbed into the `Keccak256CommitmentGenerator` state); for blob DA the call just appends to a buffer and the KZG work shows up under the pre-existing `blob_versioned_hash` marker — both cases are now observable as a single labelled phase. Rename `verify_and_apply_batch` → `state_commitment_update` to reflect what the marker actually wraps (`IOTeardown::update_commitment` — the state-tree merkle commit, which is Blake-heavy and distinct from the DA commit and the per-blob KZG commit). Applies to all four post-tx-op variants so the label string is consistent across them. No state-transition behavior change — pure instrumentation rename. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../block_flow/ethereum/post_tx_op_proving.rs | 4 +-- .../post_tx_op_proving_multiblock_batch.rs | 29 ++++++++++--------- .../post_tx_op_proving_singleblock_batch.rs | 29 +++++++++++++------ .../zk/post_tx_op/post_tx_op_sequencing.rs | 21 +++++++++----- 4 files changed, 51 insertions(+), 32 deletions(-) diff --git a/basic_bootloader/src/bootloader/block_flow/ethereum/post_tx_op_proving.rs b/basic_bootloader/src/bootloader/block_flow/ethereum/post_tx_op_proving.rs index ea0397607..2985faf34 100644 --- a/basic_bootloader/src/bootloader/block_flow/ethereum/post_tx_op_proving.rs +++ b/basic_bootloader/src/bootloader/block_flow/ethereum/post_tx_op_proving.rs @@ -119,9 +119,9 @@ where &initial_state_commitment ); - // // 3. Verify/apply reads and writes + // 3. Verify/apply reads and writes — state-tree merkle commit. let mut updated_state_commitment = initial_state_commitment; - cycle_marker::wrap!("verify_and_apply_batch", { + cycle_marker::wrap!("state_commitment_update", { io.update_commitment( Some(&mut updated_state_commitment), &mut logger, diff --git a/basic_bootloader/src/bootloader/block_flow/zk/post_tx_op/post_tx_op_proving_multiblock_batch.rs b/basic_bootloader/src/bootloader/block_flow/zk/post_tx_op/post_tx_op_proving_multiblock_batch.rs index ef685ffa6..f3893cf59 100644 --- a/basic_bootloader/src/bootloader/block_flow/zk/post_tx_op/post_tx_op_proving_multiblock_batch.rs +++ b/basic_bootloader/src/bootloader/block_flow/zk/post_tx_op/post_tx_op_proving_multiblock_batch.rs @@ -96,17 +96,20 @@ where da_commitment_scheme ); } - write_pubdata( - batch_data - .da_commitment_generator - .as_mut() - .unwrap() - .as_mut(), - result_keeper, - block_hash, - metadata.block_timestamp(), - &mut io, - ); + // See `post_tx_op_proving_singleblock_batch.rs` for the rationale. + cycle_marker::wrap!("da_commitment", { + write_pubdata( + batch_data + .da_commitment_generator + .as_mut() + .unwrap() + .as_mut(), + result_keeper, + block_hash, + metadata.block_timestamp(), + &mut io, + ); + }); io.logs_storage .apply_to_array_vec(&mut batch_data.logs_storage); @@ -142,8 +145,8 @@ where last_block_timestamp, }; - // 3. Verify/apply reads and writes - cycle_marker::wrap!("verify_and_apply_batch", { + // 3. Verify/apply reads and writes — state-tree merkle commit. + cycle_marker::wrap!("state_commitment_update", { IOTeardown::<_>::update_commitment( &mut io, Some(&mut state_commitment), diff --git a/basic_bootloader/src/bootloader/block_flow/zk/post_tx_op/post_tx_op_proving_singleblock_batch.rs b/basic_bootloader/src/bootloader/block_flow/zk/post_tx_op/post_tx_op_proving_singleblock_batch.rs index e3728b873..94d81e6b1 100644 --- a/basic_bootloader/src/bootloader/block_flow/zk/post_tx_op/post_tx_op_proving_singleblock_batch.rs +++ b/basic_bootloader/src/bootloader/block_flow/zk/post_tx_op/post_tx_op_proving_singleblock_batch.rs @@ -88,13 +88,22 @@ where let mut da_commitment_generator = da_commitment_generator_from_scheme(io.da_commitment_scheme.unwrap(), A::default()) .unwrap(); - write_pubdata( - da_commitment_generator.as_mut(), - result_keeper, - block_hash, - metadata.block_timestamp(), - &mut io, - ); + // For keccak DA (`BlobsAndPubdataKeccak256`), `write_pubdata` streams + // bytes through `Keccak256CommitmentGenerator`, which absorbs them + // into the keccak state — this is where the bulk of keccak + // delegations fire on the DA-commit path. For blob DA + // (`BlobsZKsyncOS`) the same call just appends to a buffer (no + // hashing yet); the actual blob KZG work happens in `.finalize()` + // below and is already captured by the `blob_versioned_hash` marker. + cycle_marker::wrap!("da_commitment", { + write_pubdata( + da_commitment_generator.as_mut(), + result_keeper, + block_hash, + metadata.block_timestamp(), + &mut io, + ); + }); let (multichain_root, settlement_layer_chain_id) = read_batch_context_inputs(&mut io); @@ -152,8 +161,10 @@ where chain_state_commitment_before ); - // update state commitment - cycle_marker::wrap!("verify_and_apply_batch", { + // update state commitment — this is the state-tree merkle commit + // (Blake-heavy). Distinct from `da_commitment` (keccak/blob over + // pubdata) and `blob_versioned_hash` (KZG per blob). + cycle_marker::wrap!("state_commitment_update", { IOTeardown::<_>::update_commitment( &mut io, Some(&mut state_commitment), diff --git a/basic_bootloader/src/bootloader/block_flow/zk/post_tx_op/post_tx_op_sequencing.rs b/basic_bootloader/src/bootloader/block_flow/zk/post_tx_op/post_tx_op_sequencing.rs index 087c06118..b0a1ff82b 100644 --- a/basic_bootloader/src/bootloader/block_flow/zk/post_tx_op/post_tx_op_sequencing.rs +++ b/basic_bootloader/src/bootloader/block_flow/zk/post_tx_op/post_tx_op_sequencing.rs @@ -73,15 +73,20 @@ where result_keeper.logs(io.logs_storage.messages_ref_iter()); result_keeper.events(io.events_storage.events_ref_iter()); - write_pubdata( - &mut NopCommitmentGenerator, - result_keeper, - block_hash, - metadata.block_timestamp(), - &mut io, - ); + // Sequencing-mode post-op uses NopCommitmentGenerator (no DA work), + // but we still mark `da_commitment` for parity with the proving + // paths so the bench label set is consistent across STFs. + cycle_marker::wrap!("da_commitment", { + write_pubdata( + &mut NopCommitmentGenerator, + result_keeper, + block_hash, + metadata.block_timestamp(), + &mut io, + ); + }); - cycle_marker::wrap!("verify_and_apply_batch", { + cycle_marker::wrap!("state_commitment_update", { io.update_commitment(None, &mut logger, result_keeper); }); Ok(()) From d98c7ab6c627168e3265eaee8938252877a2fa71 Mon Sep 17 00:00:00 2001 From: Vladislav Volosnikov Date: Fri, 15 May 2026 08:47:17 -0500 Subject: [PATCH 3/9] fix(bench): address PR #656 review feedback on marker placement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `post_tx_op_sequencing.rs`: complete the rename `verify_and_apply_batch` → `state_commitment_update` for the Ethereum-sequencing handler (the 5th post-tx-op site, missed in the prior sweep) and clean up the leftover `// // 3.` double-slash comment artifact, restoring cross-STF parity for the `state_commitment_update` label. - `evm_interpreter/src/instructions/system.rs::sha3`: move `cycle_marker::wrap!("keccak_execution_environment", ...)` to envelop the whole opcode dispatch — including the `pop_2()? / cast_to_usize? / spend_gas_and_native?` prelude — so the marker fires on every dispatch (incl. stack-underflow / invalid-operand / OOG-on-base-cost short-circuits), matching `EvmOpcodeStatsTracer`'s per-dispatch sample count. This was the invariant the inline comment claimed but the code didn't actually uphold; positional pairing in `cycles_per_native_report.py` / `join_precompile_samples.py` now truly matches 1:1. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ethereum/post_tx_op_sequencing.rs | 4 +- evm_interpreter/src/instructions/system.rs | 74 +++++++++---------- 2 files changed, 35 insertions(+), 43 deletions(-) diff --git a/basic_bootloader/src/bootloader/block_flow/ethereum/post_tx_op_sequencing.rs b/basic_bootloader/src/bootloader/block_flow/ethereum/post_tx_op_sequencing.rs index 9a2341f33..3c77410ef 100644 --- a/basic_bootloader/src/bootloader/block_flow/ethereum/post_tx_op_sequencing.rs +++ b/basic_bootloader/src/bootloader/block_flow/ethereum/post_tx_op_sequencing.rs @@ -121,8 +121,8 @@ where // Events result_keeper.events(io.events_iterator()); - // // 3. Verify/apply reads and writes - cycle_marker::wrap!("verify_and_apply_batch", { + // 3. Verify/apply reads and writes + cycle_marker::wrap!("state_commitment_update", { io.update_commitment(None, &mut logger, result_keeper); }); diff --git a/evm_interpreter/src/instructions/system.rs b/evm_interpreter/src/instructions/system.rs index cf15ea377..e7a630999 100644 --- a/evm_interpreter/src/instructions/system.rs +++ b/evm_interpreter/src/instructions/system.rs @@ -16,46 +16,39 @@ impl Interpreter<'_, S> { ]); pub fn sha3(&mut self, system: &mut System) -> InstructionResult { - let (memory_offset, len) = self.stack.pop_2()?; - - let len = Self::cast_to_usize(&len, EvmError::InvalidOperandOOG.into())?; - self.gas.spend_gas_and_native(0, KECCAK256_NATIVE_COST)?; - - // Cycle marker wraps the SHA3 dispatch (including the `len == 0` - // constant-lookup shortcut) so it fires exactly once per SHA3 - // opcode invocation, matching the tracer's per-call sample count. - // Otherwise positional pairing in `cycles_per_native_report.py` / - // `join_precompile_samples.py` breaks when some invocations skip - // the inner keccak SF dispatch. The inner `"keccak"` marker (from - // `Keccak256Impl::execute`) still fires, so bootloader/intrinsic - // keccak invocations remain under the `"keccak"` label only. - // Eagerly cast `memory_offset` to a `usize` (Copy, no aliasing) BEFORE - // entering the cycle marker's closure. Otherwise the closure would - // capture `&memory_offset` (a `&U256` borrowed from `self.stack` via - // `pop_2()`) and conflict with the mutable `self.gas` / `self.heap` - // borrows inside the closure. For `len == 0` we never use the offset. - let memory_offset_usize: Option = if len > 0 { - Some(Self::cast_to_usize( - &memory_offset, - EvmError::InvalidOperandOOG.into(), - )?) - } else { - None - }; + // Wrap the whole dispatch — including the early stack/length/base-cost + // checks that may short-circuit via `?` — so the marker fires once per + // SHA3 opcode dispatch, matching `EvmOpcodeStatsTracer`'s per-dispatch + // sample count. Positional pairing in `cycles_per_native_report.py` / + // `join_precompile_samples.py` relies on this 1:1 correspondence. The + // inner `"keccak"` marker (from `Keccak256Impl::execute`) still fires + // for the system-function call, so bootloader/intrinsic keccak + // invocations remain attributed to `"keccak"` alone. + // + // `wrap!` (markers only) is used rather than `wrap_with_resources!`: + // SHA3 gas/native are already captured by `EvmOpcodeStatsTracer`, so + // the per-call resource diff would be redundant. + cycle_marker::wrap!("keccak_execution_environment", { + let (memory_offset, len) = self.stack.pop_2()?; + let len = Self::cast_to_usize(&len, EvmError::InvalidOperandOOG.into())?; + self.gas.spend_gas_and_native(0, KECCAK256_NATIVE_COST)?; + + // Eagerly cast `memory_offset` to an owned `usize` so the + // `&memory_offset` borrow on `self.stack` ends here and does not + // collide with the final `self.stack.push(&hash)` below. + let memory_offset_usize: Option = if len > 0 { + Some(Self::cast_to_usize( + &memory_offset, + EvmError::InvalidOperandOOG.into(), + )?) + } else { + None + }; - // Use `wrap!` (markers only) rather than `wrap_with_resources!` - // here: the latter expands `$resources.clone()` BEFORE and AFTER the - // inner closure to compute the ergs/native diff, and those borrow - // `self.gas` mutably while the closure ALSO captures `self` (for the - // `spend_gas`/`resize_heap`/`heap` accesses inside) → conflicting - // mutable borrows. `wrap!` doesn't snapshot resources, so the closure - // is the sole borrower. We don't need the per-call ergs/native log - // here — SHA3 gas/native are already captured by `EvmOpcodeStatsTracer`. - let hash = cycle_marker::wrap!("keccak_execution_environment", { - let r: Result = match memory_offset_usize { + let hash = match memory_offset_usize { None => { self.gas.spend_gas(gas_constants::SHA3)?; - Ok(Self::EMPTY_SLICE_SHA3) + Self::EMPTY_SLICE_SHA3 } Some(memory_offset) => { self.resize_heap(memory_offset, len)?; @@ -87,13 +80,12 @@ impl Interpreter<'_, S> { } // Convert ruint::aliases::U256 to u256::U256 - Ok(U256::from(hash_ruint)) + U256::from(hash_ruint) } }; - r - })?; - self.stack.push(&hash) + self.stack.push(&hash) + }) } pub fn address(&mut self) -> InstructionResult { From e46b2d4e14cdebc46536ac7a2b363aad01073dde Mon Sep 17 00:00:00 2001 From: Vladislav Volosnikov Date: Fri, 15 May 2026 07:45:23 -0500 Subject: [PATCH 4/9] feat(cycle_marker): per-execution sample dumps + effective-cycles weighting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds per-execution cycle-sample dumps for both opcode-level and non-opcode (label) markers, plus an effective-cycles formula that weights raw RISC-V cycles by delegation counts so dump consumers see the true prover cost rather than raw cycles alone. - New `OPCODE_CYCLE_SAMPLES_DIR` env var: writes one `.cycles` file per opcode with raw cycles per execution (append-mode). - New `LABEL_CYCLE_SAMPLES_DIR` env var: writes both `