Skip to content

feat(sdm): SDM warming-refund accounting, gated by flag#20173

Draft
nonsense wants to merge 7 commits intodevelopfrom
nonsense/sdm/remaining_squashed
Draft

feat(sdm): SDM warming-refund accounting, gated by flag#20173
nonsense wants to merge 7 commits intodevelopfrom
nonsense/sdm/remaining_squashed

Conversation

@nonsense
Copy link
Copy Markdown
Contributor

This PR is introducing SDM - Sequencer-Defined Metering.

Design doc: https://github.com/ethereum-optimism/design-docs/blob/main/protocol/sdm.md
Tracking issue: https://github.com/ethereum-optimism/protocol-team/issues/59

PR is heavy WIP, and ideally will be refactored further so that it is easier for review.


Follow-up from #19954


Per-commit summaries

feat(op-alloy): add post-exec (0x7D) transaction type
Hardened post-exec transaction already included in previous PR.

feat(alloy-op-evm): post-exec block executor and SDM warming inspector
Core consensus change. SDMWarmingInspector tracks first-warmer provenance
and emits refunds.

feat(op-reth): add ConfigurePostExecEvm and post-exec-aware evm config
Plumbing for op-reth

feat(op-reth): inject synthetic post-exec tx in payload builder
Appends the 0x7D tx at block tail when --rollup.sdm-enabled.

feat(op-reth): add debug_replaySDMBlock rpc and post-exec-replay crate
New reth-optimism-post-exec-replay crate re-executes a historical block under Produce and compares the synthesized payload to the embedded 0x7D and the receipt-level refund projection.

feat(op-chain-ops): add Go sdmreplay client
Go driver for debug_replaySDMBlock

test(op-acceptance-tests): add end-to-end SDM coverage
Devstack tests that boot an op-reth sequencer with SDM enabled.

feat(op-reth): add SDM observability metrics
SDM observability


TODO:

  • simplify further and leave only mandatory changes, by extracting nice-to-have functionality and tools for future PRs.

@wiz-0f98cca50a
Copy link
Copy Markdown

wiz-0f98cca50a Bot commented Apr 20, 2026

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities 2 High 1 Medium 4 Low
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations -
SAST Finding SAST Findings 1 Medium
Software Management Finding Software Management Findings -
Total 2 High 2 Medium 4 Low

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try using Wiz Code VS Code Extension.

"--chain=" + chainConfigPath,
"--proofs-history.storage-path=" + proofHistoryDir,
}
initOut, initErr := exec.Command(execPath, initProofsArgs...).CombinedOutput()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium SAST Finding

OS Command Injection (CWE-78)

More Details

OS command injection is a critical vulnerability that allows an attacker to execute arbitrary commands on the system. This can lead to a full system compromise, as the attacker can potentially gain complete control over the application and the underlying system.

The vulnerability arises when user input is used to construct commands or command arguments that are then executed by the application. This can occur when user-supplied data is passed directly to functions that execute OS commands, such as exec.Command, exec.CommandContext, syscall.ForkExec, or syscall.StartProcess.

If an attacker can inject malicious code into these commands, they can potentially execute any command on the system, including installing malware, stealing data, or causing a denial of service. This vulnerability can have severe consequences, including data breaches, system compromise, and unauthorized access to sensitive information.

To avoid this vulnerability, user input should never be used directly in constructing commands or command arguments. Instead, the application should use a hardcoded set of arguments and validate any user input to ensure it does not contain malicious code.

Attribute Value
Impact Medium
Likelihood Medium

Remediation

To remediate this vulnerability, follow these recommendations:

  • Never use user-supplied input directly in constructing commands or command arguments.
  • Validate and sanitize all user input before using it in any context.
  • Use a hardcoded set of arguments for executing OS commands.
  • If filenames are required, use a hash or unique identifier instead of the actual filename.
  • Consider using a native library that implements the required functionality instead of executing OS commands.

Example of safely executing an OS command:

userData := []byte("user data")
// Create a temporary file in the application-specific directory
f, err := ioutil.TempFile("/var/app/restricted", "temp-*.dat")
if err != nil {
  log.Fatal(err)
}

if _, err := f.Write(userData); err != nil {
  log.Fatal(err)
}

if err := f.Close(); err != nil {
  log.Fatal(err)
}

// Pass the full path to the binary and the name of the temporary file
out, err := exec.Command("/bin/cat", f.Name()).Output()
if err != nil {
  log.Fatal(err)
}

Rule ID: WS-I011-GO-00026


To ignore this finding as an exception, reply to this conversation with #wiz_ignore reason

If you'd like to ignore this finding in all future scans, add an exception in the .wiz file (learn more) or create an Ignore Rule (learn more).


To get more details on how to remediate this issue using AI, reply to this conversation with #wiz remediate

@nonsense nonsense added the M-do-not-merge Meta: Do not merge label Apr 20, 2026
Add the canonical post-exec block executor, along with the first
feature riding on it — SDM (Sequencer-Defined Metering) block-level
warming refunds, delivered by the SDMWarmingInspector.

- OpBlockExecutor gains three PostExecModes (Disabled / Produce /
  Verify(payload) / Invalid). Produce accumulates per-tx refund
  entries for the payload builder to append as a synthetic 0x7D tx;
  Verify validates an embedded payload against local replay;
  Disabled is the legacy path (byte-identical to pre-SDM).
- SDMWarmingInspector tracks first-warmer provenance for accounts
  and storage slots, emits exact refund attribution events for every
  re-touch past the EIP-2929 warm threshold, and suppresses claims
  from Deposit and synthetic PostExec tx kinds.
- Verify-mode validations reject duplicate payload indexes, payload
  entries targeting deposits or the 0x7D tx itself, and refunds that
  exceed the tx's raw gas. `apply_pre_execution_changes` debug_asserts
  the Produce hooks are wired so a downstream fork can't silently drop
  refunds.
- Canonical gas settlement credits the sender, debits the beneficiary
  and base-fee recipient by the refunded-gas component of their share,
  and commits the deltas when canonical gas falls below raw gas.
- beneficiary_gas_price can legitimately saturate at zero when a
  legacy tx's gas price equals basefee; inline comment documents the
  consensus-valid zero case.
Extend op-reth's EvmConfig with the post-exec hooks the new
OpBlockExecutor expects. Downstream uses:

- The payload builder asks the executor to Produce + drains refund
  entries via post_exec_executor_for_block + take_post_exec_entries.
- The replay RPC asks for the same Produce mode but on a stripped
  block (0x7D removed) to compare synthesized refunds against the
  embedded payload.

OpEvm auto-wires the SDMWarmingInspector begin/take hooks so callers
don't have to plumb them manually; the alloy-op-evm debug_assert
guards the failure mode if a downstream fork bypasses OpEvm.
Append a type-0x7D post-exec transaction at the tail of the block
when the sequencer builds under --rollup.sdm-enabled. The tx carries
the executor's accumulated refund entries as its RLP payload and
canonicalizes this node's gas accounting with what a verifier will
later independently replay.

- OpBuilderConfig/CLI flag (`--rollup.sdm-enabled`) — off by default.
  When off, the payload path is byte-identical to the pre-feature code.
- try_include_post_exec_tx wraps the executor's refund entries in a
  TxPostExec, executes it, and aborts the payload build with
  PayloadBuilderError::EvmExecutionError on any synthetic-tx
  execution failure. Silently dropping it would yield a payload that
  no honest verifier can reproduce.
- Unit tests pin the abort path (should-not-be-Ok-on-failure), the
  no-entries skip, and the happy-path wrapping of entries.
- custom-node example switches to NoopPayloadServiceBuilder; the
  upstream OpPayloadBuilder is now specialized for OpTransactionSigned
  to carry the post-exec tx and no longer composes with the example's
  custom tx type. Doc comment explains what downstream forks need.
Introduce reth-optimism-post-exec-replay, a new crate that counterfactually
re-executes a historical block under PostExecMode::Produce to derive what
the post-exec payload *would* have been, then compares it to any 0x7D tx
already embedded in the block and to the receipt-level opGasRefund
projection.

- debug_replaySDMBlock JSON-RPC handler exposes the replay result on a
  per-block basis. Compares synthesized refunds vs. embedded payload and
  vs. receipt refunds when requested. Documented as operator/debug
  tooling only — the debug namespace must not be exposed on public RPC,
  each call replays a whole historical block against live state and is
  unbounded in cost.
- Replay detects seven mismatch categories: duplicate payload index,
  payload-index-out-of-range, payload-targets-deposit,
  payload-targets-post-exec, payload-refund-mismatch,
  receipt-refund-mismatch, payload-refund-exceeds-raw-gas.
- strip_post_exec_tx_for_replay preserves the original tx index mapping
  so refund events can be attributed back to source-block positions.
- Semaphore::new(3) caps concurrent replays; per-call wall-clock and
  memory ceilings are a production-hardening gap tracked separately.
Client for driving debug_replaySDMBlock over a range of blocks and
writing a JSONL trail of the replay results, for post-hoc verification
and cross-node consistency checks.

- ReplayRange iterates blocks with ctx cancellation support so long
  ranges can be aborted cleanly.
- DecodePayload RLP-decodes the 0x7D tx input and rejects unknown
  versions in lock-step with the Rust decoder (POST_EXEC_PAYLOAD_VERSION
  = 1). Cross-language drift would let a Go pipeline accept payloads
  the Rust node rejects.
- Unit tests cover the version check on both the current 3-field
  shape and the legacy 2-field fallback, plus an empty-input guard.
- Memory-buffered range mode is fine for devstack tests; streaming
  JSONL for mainnet-scale ranges is tracked as a pre-production item.
Devstack-backed acceptance tests that boot an op-reth sequencer with
--rollup.sdm-enabled, submit a repeated-slot workload, and assert the
full SDM pipeline: sequencer-produced 0x7D tx is present with the
expected refund entries, receipt-level opGasRefund matches the payload,
and debug_replaySDMBlock reproduces the same refunds from state replay.

Also covers SDM-disabled baseline, dedup + refund-cap behavior across
same-slot and many-slot workloads, and a multi-category smoke test.

Mixed-runtime devstack plumbing in op-devstack/sysgo wires the
SDMEnabled flag from node spec to op-reth args.
Producer-side and follower-side metrics so operators can monitor a
canary before flipping SDM on via hardfork.

- optimism_sdm.payload_builder (in reth-optimism-payload-builder):
  * blocks_with_post_exec_tx / blocks_without_post_exec_tx counters
    split on whether the sequencer appended a synthetic 0x7D tx,
  * block_refund_gas / block_refund_entry_count histograms over the
    refund payload size, per block with a 0x7D.

- optimism_sdm.replay (in reth-optimism-post-exec-replay):
  * blocks_total / blocks_with_mismatch_total counters recorded on
    every replay_block() call,
  * per-category mismatch counters for each
    PostExecReplayMismatchKind, so an operator running
    `sdmreplay --fail-on-mismatch` can alert on both which block
    diverged and which rule fired.

Default-instantiated at the call site — handles are cheap and
registry-backed. Verifier-mode metrics in alloy-op-evm are deferred
because that crate is no_std-capable and pulling in reth-metrics
would break the feature gating.
@nonsense nonsense force-pushed the nonsense/sdm/remaining_squashed branch from c251c31 to b51b9bd Compare April 21, 2026 14:00
@nonsense nonsense changed the title feat(sdm): post-exec (0x7D) tx + SDM warming-refund accounting, gated by flag feat(sdm): SDM warming-refund accounting, gated by flag Apr 21, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 21, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 66.3%. Comparing base (64dce2f) to head (b51b9bd).
⚠️ Report is 1 commits behind head on develop.

❗ There is a different number of reports uploaded between BASE (64dce2f) and HEAD (b51b9bd). Click for more details.

HEAD has 12 uploads less than BASE
Flag BASE (64dce2f) HEAD (b51b9bd)
contracts-bedrock-tests 12 0
Additional details and impacted files
@@            Coverage Diff             @@
##           develop   ethereum-optimism/optimism#20173     +/-   ##
==========================================
- Coverage     76.0%    66.3%   -9.7%     
==========================================
  Files          183       55    -128     
  Lines        10536     4035   -6501     
==========================================
- Hits          8012     2677   -5335     
+ Misses        2380     1214   -1166     
  Partials       144      144             
Flag Coverage Δ
cannon-go-tests-64 66.3% <ø> (ø)
contracts-bedrock-tests ?

Flags with carried forward coverage won't be shown. Click here to find out more.
see 128 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

M-do-not-merge Meta: Do not merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants