Skip to content

Commit 8f7d4b0

Browse files
committed
Merge remote-tracking branch 'origin/develop' into docs/add-2026-06-interop-l2-audit
* origin/develop: fix(op-rbuilder): append trailing SDM PostExec tx in the standard builder (#21332) docs(ai): clarify OP Stack Rust crates are migrated, not vendored (#21469) docs(ai): add branch CI-failure diagnosis to ci-ops (#21468) test(alloy-op-evm): cover included-but-reverted tx warming a later tx (#21466) op-core/types: add DepositTx, PostExecTx and Receipt types (#21356) fix(alloy-op-evm): skip SDM warming rebates for never-cold accesses; harden test suite (#21434) feat(op-reth): gate payload builder on interop failsafe (#21448) docs: add OPCMv2 Cantina audit report (#21458) docs: add notice about Sepolia estimateGas issue (#21456) feat(op-chain-ops): interop bridge/failsafe test tools (check-lagoon, interop-smoke) (#21395) fix(ctb): skip interop L2 genesis upgrade tests under custom gas token (#21444) fix(alloy-op-evm): roll back SDM block-warming for declined op-rbuilder candidates (#21359) # Conflicts: # docs/security-reviews/README.md
2 parents 18c8d1f + cfbc098 commit 8f7d4b0

50 files changed

Lines changed: 3374 additions & 701 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
*.sh text eol=lf
2+
*.pdf binary

docs/ai/ci-ops.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22

33
This document provides guidance for AI agents working with CI/CD operational tasks in the Optimism monorepo.
44

5+
## Diagnosing a CI failure on a feature branch
6+
7+
Before assuming a red check is caused by your change, rule out a failure the branch
8+
**inherited** — especially a long-lived branch that has drifted from `develop`:
9+
10+
1. **Check the diff scope:** `git diff origin/develop...HEAD --name-only` (three dots).
11+
If the failing test exercises code your branch never touches, it almost certainly
12+
isn't your regression.
13+
2. **Check `develop`:** the failure may be a known flake, or a real bug a *later*
14+
`develop` commit already fixed — your branch just predates the fix. Look for an open
15+
flake issue or a recent fix PR touching the failing area.
16+
3. **Rebase onto latest `develop`** before deeper debugging — stale branches miss
17+
upstream fixes, and a rebase often clears failures that were never about your change.
18+
19+
Worked example (#21356): `go-tests-short` failed on `op-deployer` integration tests on a
20+
branch that only added a new `op-core/types` package. The diff touched nothing in
21+
`op-deployer`; the failures were a data-dependent flake already fixed on `develop` by
22+
#21396. Rebasing made CI green with no change to the branch's own work.
23+
524
## TODO Checker Failures
625

726
The repo runs a scheduled CircleCI job every 4 hours that validates TODO comments don't reference closed GitHub issues. When this job fails, issues need to be reopened.

docs/ai/opgeth-decoupling.md

Lines changed: 129 additions & 28 deletions
Large diffs are not rendered by default.

docs/ai/rust-dev.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,22 @@ All Rust code lives under `rust/`. This is a unified Cargo workspace — always
1212

1313
Check `rust/Cargo.toml` for the full workspace member list, dependency versions, and lint configuration. The Rust toolchain version is pinned in `rust/rust-toolchain.toml`.
1414

15+
### Migrated, not vendored
16+
17+
Most of the OP Stack Rust code here was **officially migrated** into the monorepo in coordination with the upstream repository owners — it is **not** a vendored copy. The upstream crates have been deleted or deprecated, so the entire Rust OP Stack is now developed here. This applies to:
18+
19+
- **op-reth** (`rust/op-reth/`)
20+
- **kona-\*** (`rust/kona/`)
21+
- **op-alloy-\*** (`rust/op-alloy/`)
22+
- **alloy-op-\*** (`rust/alloy-op-evm/`, `rust/alloy-op-hardforks/`)
23+
- **op-revm** (`rust/op-revm/`)
24+
25+
These crates are owned and edited directly here — do not look upstream for their source. They still *depend on* generic upstream crates (e.g. reth's engine/provider crates, `alloy`, `revm`), which remain external and pinned in `rust/Cargo.toml`; a change to one of those generic APIs has to go upstream first and then be consumed via a version bump. When changing upstream behavior or API leads to a better overall solution than working around it locally, it is acceptable — and often preferable — to propose that change upstream (a PR to the respective repository); suggest this when it applies.
26+
27+
**Known exception:** `op-alloy-flz` has not been migrated yet and is still an external dependency, tracked by [#21087](https://github.com/ethereum-optimism/optimism/issues/21087).
28+
29+
**Still vendored:** `rust/op-rbuilder/` and `rust/rollup-boost/` are vendored copies, slated for deprecation.
30+
1531
## Build System
1632

1733
Run `just --list` in `rust/` to see all available targets. The key ones:

docs/public-docs/notices/upgrade-19.mdx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,13 @@ If your chain was already configured for `cannon-kona` as part of [Upgrade 18](/
253253

254254
## For app developers
255255

256+
<Warning>
257+
OP Stack Sepolia testnet chains have a bug affecting calls to `eth_estimateGas` when the `gas` parameter is omitted after Karst.
258+
If you use `eth_estimateGas`, providing an explicit `gas` parameter is required.
259+
Only post-Karst Sepolia OP Stack chains are impacted.
260+
This will be fixed on OP Sepolia within the coming days and will be fixed prior to Karst activation on OP Mainnet.
261+
</Warning>
262+
256263
* **EIP-7825 transaction gas limit (Osaka on L2):** L2 transactions are now subject to a per-transaction gas limit of 2^24 = 16,777,216 gas. If your application submits transactions with very high gas limits, verify they remain within the new threshold. Deposit transactions are exempt. Most applications will not be affected.
257264
* **MODEXP gas changes ([EIP-7883](https://eips.ethereum.org/EIPS/eip-7883) + [EIP-7823](https://eips.ethereum.org/EIPS/eip-7823), Osaka on L2):** the gas cost floor for `MODEXP` (`0x05`) increases, and a new input size cap (1024-byte modulus) is enforced. If your contracts call `MODEXP`, update your gas estimates and verify inputs are within the new size limit.
258265
* **P256VERIFY gas change ([EIP-7951](https://eips.ethereum.org/EIPS/eip-7951), Osaka on L2):** the gas cost for `P256VERIFY` (`0x100`) increases from 3,450 to 6,900 gas. If your contracts call `P256VERIFY`, update your gas estimates.
527 KB
Binary file not shown.

docs/security-reviews/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Please see the report for the specific details.
5353
| 2026-03 | Cantina | PolicyEngineStaking Contract | [2026_03-PolicyEngineStaking-Cantina.pdf](./2026_03-PolicyEngineStaking-Cantina.pdf) | 13c74c6d0855caf59b575ccf5fbf74ffe104cb4f | |
5454
| 2026-03 | Spearbit | Kona | [2026_03-Kona-Spearbit.pdf](./2026_03-Kona-Spearbit.pdf) | [86910c9112437bbb849eca28961f813c5450e103](https://github.com/op-rs/kona/tree/86910c9112437bbb849eca28961f813c5450e103/) | |
5555
| 2026-05 | Cantina | Upgrade 19 | [2026_05-U19-Cantina.pdf](./2026_05-U19-Cantina.pdf) | 7cbcb58ffb7322343f0ed6b926f65bea7630ce3e | op-contracts/v7.0.0 |
56+
| 2026-06 | Cantina | OPCMv2: `OPContractsManagerV2.sol`, `OPContractsManagerMigrator.sol`, `OPContractsManagerContainer.sol`, `OPContractsManagerUtils.sol`, `OPContractsManagerUtilsCaller.sol`, and `VerifyOPCM.s.sol` | [2026_06-OPCMv2-Cantina.pdf](./2026_06-OPCMv2-Cantina.pdf) | a00b39720a3f61276d2fa3991abfb672c9dd35ce | |
5657
| 2026-06 | Cantina | Interop L2 Contracts (re-review) | [2026_06-Interop-L2-Contracts.pdf](./2026_06-Interop-L2-Contracts.pdf) | fa9974a2cd38d9e7633a12f9024d52f41de55f86 | |
5758

5859
<!-- Note: in order to reduce dependency on the honesty of repo maintainers, we should include a link to the auditor hosted report whenever possible. -->
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package flashblocks
2+
3+
import (
4+
"fmt"
5+
"math/big"
6+
"slices"
7+
"strings"
8+
"testing"
9+
10+
"github.com/ethereum-optimism/optimism/op-chain-ops/pkg/sdm"
11+
"github.com/ethereum-optimism/optimism/op-devstack/devtest"
12+
"github.com/ethereum-optimism/optimism/op-devstack/presets"
13+
"github.com/ethereum-optimism/optimism/op-devstack/sysgo"
14+
"github.com/ethereum-optimism/optimism/op-service/bigs"
15+
"github.com/ethereum-optimism/optimism/op-service/eth"
16+
"github.com/ethereum-optimism/optimism/op-service/txplan"
17+
)
18+
19+
// phantomMaxGasPerTxn caps op-rbuilder's per-tx gas used. A candidate over the cap is executed
20+
// (warming the slots it touches) then declined — the "executed but not committed" path that leaks
21+
// SDM warming when it isn't rolled back. Sized between the victim's gas and the poisoner's.
22+
const phantomMaxGasPerTxn = 200_000
23+
24+
// The declined "poisoner" call writes a superset of the committed "victim" call's slots, so the
25+
// victim re-touches slots that — in the canonical block — were only warmed by a tx that never
26+
// entered the block.
27+
const (
28+
poisonerSlots = 30 // run(30) ≈ 0.68M gas > cap => executed then declined
29+
victimSlots = 4 // run(4) ≈ 0.11M gas < cap => committed
30+
)
31+
32+
// TestFlashblocksSDMPhantomWarmingDivergence makes the op-rbuilder phantom-warming bug observable
33+
// at the acceptance level (ethereum-optimism/optimism#21354).
34+
//
35+
// SDM block-warming refunds are recorded during EVM execution (before the commit decision) and
36+
// aren't journaled. When op-rbuilder executes a candidate then declines it (here: over
37+
// `--builder.max_gas_per_txn`), the declined candidate's warming is (pre-fix) left behind, and a
38+
// later committed tx touching the same slots claims a "phantom" refund for a tx that never entered
39+
// the block. Commit-only paths (block import, `debug_replaySDMBlock` derivation) never see that
40+
// warmth. This test drives a persistently-declined poisoner alongside committed victims and, per
41+
// victim block, compares the producer-baked SDM payload against the commit-only derivation; they
42+
// must be identical. Pre-fix they diverge and the failure prints the phantom entries.
43+
func TestFlashblocksSDMPhantomWarmingDivergence(gt *testing.T) {
44+
t := devtest.SerialT(gt)
45+
sysgo.SkipOnKonaNode(t, "flashblocks acceptance preset requires user RPC")
46+
sysgo.SkipOnOpGeth(t, "SDM flashblocks require op-reth post-exec support")
47+
48+
// Cap per-tx gas so the poisoner is declined post-execution (the warming-leak path). SDM rides
49+
// Interop; activating it at genesis turns SDM on across op-reth, op-rbuilder, and op-node.
50+
sys := presets.NewSingleChainWithFlashblocks(t,
51+
presets.WithInteropAtGenesis(),
52+
presets.WithOPRBuilderOption(sysgo.OPRBuilderNodeWithExtraArgs(
53+
fmt.Sprintf("--builder.max_gas_per_txn=%d", phantomMaxGasPerTxn),
54+
)),
55+
)
56+
57+
driveViaTestSequencer(t, sys, 2)
58+
59+
// Opt SDM PostExec production in on both the sequencer EL (op-reth fallback) and op-rbuilder;
60+
// the flag is process-local and starts disabled on boot. See flashblocks_sdm_test.go.
61+
setFlashblocksSDMEnabled(t, sys.L2EL.Escape().L2EthClient().RPC(), true)
62+
setFlashblocksSDMEnabled(t, sys.L2OPRBuilder.Escape().L2EthClient().RPC(), true)
63+
64+
rpc := sys.L2EL.Escape().L2EthClient().RPC()
65+
66+
// Deploy the StateBloat contract; run(n) writes slots [0, n).
67+
deployer := sys.FunderL2.NewFundedEOA(eth.OneEther)
68+
stateBloat := flashblocksDeployContract(t, deployer, sdm.StateBloatBin)
69+
70+
// The poisoner: one persistent, top-priority call that always exceeds the gas cap, so op-rbuilder
71+
// re-declines it every block — warming the poisoner's slots before any victim commits. A declined
72+
// candidate isn't marked invalid, so it lingers in the mempool and keeps poisoning blocks. It's
73+
// never included, so we don't wait for it.
74+
poisoner := sys.FunderL2.NewFundedEOA(eth.OneEther)
75+
flashblocksSubmitTxWithoutWait(t, poisoner, poisoner.PendingNonce(),
76+
txplan.WithTo(&stateBloat),
77+
txplan.WithData(sdm.EncodeRun(poisonerSlots)),
78+
txplan.WithGasLimit(1_500_000),
79+
// Highest priority so op-rbuilder processes (and declines) it first in each block.
80+
txplan.WithGasTipCap(big.NewInt(5_000_000_000)),
81+
txplan.WithGasFeeCap(big.NewInt(500_000_000_000)),
82+
)
83+
84+
// The victims: small committed calls that re-touch the poisoner's slots, sent one at a time
85+
// (waiting for each receipt) so each lands in its own block behind the declined poisoner.
86+
const victimCount = 6
87+
victim := sys.FunderL2.NewFundedEOA(eth.OneEther)
88+
victimBlocks := make([]uint64, 0, victimCount)
89+
seen := make(map[uint64]bool)
90+
for i := range victimCount {
91+
ptx := txplan.NewPlannedTx(
92+
victim.Plan(),
93+
txplan.WithNonce(victim.PendingNonce()),
94+
txplan.WithTo(&stateBloat),
95+
txplan.WithData(sdm.EncodeRun(victimSlots)),
96+
txplan.WithGasLimit(400_000),
97+
)
98+
receipt, err := ptx.Included.Eval(t.Ctx())
99+
t.Require().NoError(err, "victim %d must be included", i)
100+
bn := bigs.Uint64Strict(receipt.BlockNumber)
101+
if !seen[bn] {
102+
seen[bn] = true
103+
victimBlocks = append(victimBlocks, bn)
104+
}
105+
}
106+
slices.Sort(victimBlocks)
107+
t.Require().NotEmpty(victimBlocks, "expected at least one victim block")
108+
109+
// Compare producer-baked vs commit-only-derived SDM payload for each victim block.
110+
var divergentBlocks []uint64
111+
var viz strings.Builder
112+
for _, bn := range victimBlocks {
113+
replay, err := sdm.ReplayBlockWithSDM(t.Ctx(), rpc, bn, true)
114+
t.Require().NoError(err, "debug_replaySDMBlock(%d)", bn)
115+
if !replay.PostExecTxPresent {
116+
// No SDM post-exec tx in this block (no committed warm re-touch landed here); skip.
117+
continue
118+
}
119+
120+
producer := entryMap(replay.EmbeddedPayload)
121+
derived := entryMap(&replay.SynthesizedPayload)
122+
if payloadsEqual(producer, derived) {
123+
continue
124+
}
125+
divergentBlocks = append(divergentBlocks, bn)
126+
writeBlockViz(&viz, bn, replay, producer, derived)
127+
}
128+
129+
t.Logger().Info("SDM phantom-warming scan complete",
130+
"victim_blocks", len(victimBlocks),
131+
"divergent_blocks", len(divergentBlocks))
132+
133+
t.Require().Empty(divergentBlocks,
134+
"producer-baked SDM payload diverges from commit-only derivation in %d block(s) — "+
135+
"op-rbuilder credited phantom warming refunds from a declined (uncommitted) candidate "+
136+
"(ethereum-optimism/optimism#21354):\n%s", len(divergentBlocks), viz.String())
137+
}
138+
139+
// entryMap indexes an SDM payload's refund entries by tx index. A nil payload yields an empty map.
140+
func entryMap(p *sdm.PostExecPayload) map[uint64]uint64 {
141+
out := make(map[uint64]uint64)
142+
if p == nil {
143+
return out
144+
}
145+
for _, e := range p.GasRefundEntries {
146+
out[e.Index] = e.GasRefund
147+
}
148+
return out
149+
}
150+
151+
func payloadsEqual(a, b map[uint64]uint64) bool {
152+
if len(a) != len(b) {
153+
return false
154+
}
155+
for idx, v := range a {
156+
if b[idx] != v {
157+
return false
158+
}
159+
}
160+
return true
161+
}
162+
163+
// writeBlockViz renders a per-tx table contrasting the producer-baked refund against the
164+
// commit-only-derived refund, flagging phantom entries (present in the producer payload, absent or
165+
// smaller in derivation).
166+
func writeBlockViz(w *strings.Builder, bn uint64, replay *sdm.ReplaySDMBlock, producer, derived map[uint64]uint64) {
167+
fmt.Fprintf(w, "block %d (hash %s):\n", bn, replay.BlockHash)
168+
fmt.Fprintf(w, " embedded(producer) entries=%d synthesized(derivation) entries=%d mismatches=%d\n",
169+
len(producer), len(derived), len(replay.Mismatches))
170+
fmt.Fprintf(w, " %-9s %-18s %-18s %s\n", "tx_index", "producer_refund", "derived_refund", "")
171+
idxs := unionKeys(producer, derived)
172+
for _, idx := range idxs {
173+
p, pOK := producer[idx]
174+
d, dOK := derived[idx]
175+
flag := ""
176+
if pOK && (!dOK || d < p) {
177+
flag = "<- phantom"
178+
}
179+
fmt.Fprintf(w, " %-9d %-18d %-18d %s\n", idx, p, d, flag)
180+
}
181+
}
182+
183+
func unionKeys(a, b map[uint64]uint64) []uint64 {
184+
set := make(map[uint64]struct{})
185+
for k := range a {
186+
set[k] = struct{}{}
187+
}
188+
for k := range b {
189+
set[k] = struct{}{}
190+
}
191+
out := make([]uint64, 0, len(set))
192+
for k := range set {
193+
out = append(out, k)
194+
}
195+
slices.Sort(out)
196+
return out
197+
}

0 commit comments

Comments
 (0)