Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -277,9 +277,7 @@ require (
lukechampine.com/blake3 v1.3.0 // indirect
)

replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101702.1-rc.1

// replace github.com/ethereum/go-ethereum => ../op-geth
replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101702.2-0.20260415133811-2c2badfed10e

// replace github.com/ethereum-optimism/superchain-registry/superchain => ../superchain-registry/superchain
// This release keeps breaking Go builds. Stop that.
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,8 @@ github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.4-0.20251001155152-4eb15ccedf7e h1:iy1vBIzACYUyOVyoADUwvAiq2eOPC0yVsDUdolPwQjk=
github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.4-0.20251001155152-4eb15ccedf7e/go.mod h1:DYj7+vYJ4cIB7zera9mv4LcAynCL5u4YVfoeUu6Wa+w=
github.com/ethereum-optimism/op-geth v1.101702.1-rc.1 h1:2p7pzvmAeZ6xR6pqltf3l6cwCu8HwGR4eWom3v8PwkM=
github.com/ethereum-optimism/op-geth v1.101702.1-rc.1/go.mod h1:HzvOtk7c9KwFaSxRvUBPFHGSjIjomWtw4iSXX6vruQE=
github.com/ethereum-optimism/op-geth v1.101702.2-0.20260415133811-2c2badfed10e h1:FtGf1ae3Q8tzYjJUJLCPmAaG2sc9lWuSFmPi3ErKNyc=
github.com/ethereum-optimism/op-geth v1.101702.2-0.20260415133811-2c2badfed10e/go.mod h1:HzvOtk7c9KwFaSxRvUBPFHGSjIjomWtw4iSXX6vruQE=
github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20260115192958-fb86a23cd30e h1:TO1tUcwbhIrNuea/LCsQJSQ5HDWCHdrzT/5MLC1aIU4=
github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20260115192958-fb86a23cd30e/go.mod h1:NZ816PzLU1TLv1RdAvYAb6KWOj4Zm5aInT0YpDVml2Y=
github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls=
Expand Down
751 changes: 751 additions & 0 deletions op-acceptance-tests/tests/sdm/block_test.go

Large diffs are not rendered by default.

125 changes: 125 additions & 0 deletions op-acceptance-tests/tests/sdm/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package sdm

import (
"context"
"encoding/json"
"fmt"
"math/big"
"strings"

"github.com/ethereum-optimism/optimism/op-devstack/devtest"
"github.com/ethereum-optimism/optimism/op-devstack/dsl"
"github.com/ethereum-optimism/optimism/op-service/txplan"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/lmittmann/w3"
)

// ComputeHeavy: run(uint256 n) loops keccak256 n times (pure computation).
const computeHeavyBin = "6080604052348015600e575f5ffd5b506101908061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c8063a444f5e91461002d575b5f5ffd5b610047600480360381019061004291906100ec565b610049565b005b5f7f66a80b61b29ec044d14c4c8c613e762ba1fb8eeb0c454d1ee00ed6dedaa5b5c590505f5f90505b828110156100b0578160405160200161008b9190610140565b6040516020818303038152906040528051906020012091508080600101915050610072565b505050565b5f5ffd5b5f819050919050565b6100cb816100b9565b81146100d5575f5ffd5b50565b5f813590506100e6816100c2565b92915050565b5f60208284031215610101576101006100b5565b5b5f61010e848285016100d8565b91505092915050565b5f819050919050565b5f819050919050565b61013a61013582610117565b610120565b82525050565b5f61014b8284610129565b6020820191508190509291505056fea264697066735822122013cd314931f1991e7797e220c9553bb73dfef407d4d266dd8b2553907d5bc14364736f6c634300081c0033"

// StateBloat: run(uint256 n) writes n unique SSTORE slots (state growth).
const stateBloatBin = "6080604052348015600e575f5ffd5b5060f28061001b5f395ff3fe6080604052348015600e575f5ffd5b50600436106026575f3560e01c8063a444f5e914602a575b5f5ffd5b60406004803603810190603c91906096565b6042565b005b5f5f90505b8181101560605760018101815580806001019150506047565b5050565b5f5ffd5b5f819050919050565b6078816068565b81146081575f5ffd5b50565b5f813590506090816071565b92915050565b5f6020828403121560a85760a76064565b5b5f60b3848285016084565b9150509291505056fea2646970667358221220fb9ef6750b6ac6ded2dd901595e50b6daefe24726b41a0346f3a36ac6fcf5f8264736f6c634300081c0033"

// SlotTouch: repeatedly touches either one storage slot or many distinct slots.
const slotTouchBin = "6080604052348015600e575f5ffd5b5061010e8061001c5f395ff3fe6080604052348015600e575f5ffd5b50600436106030575f3560e01c80637ebfc845146034578063f1ac3593146045575b5f5ffd5b6043603f366004609e565b6054565b005b60436050366004609e565b6073565b5f5b81811015606f57606681600160b4565b5f556001016056565b5050565b5f5b81811015606f57608581600160b4565b5f82815260016020819052604090912091909155016075565b5f6020828403121560ad575f5ffd5b5035919050565b8082018082111560d257634e487b7160e01b5f52601160045260245ffd5b9291505056fea264697066735822122032537b9a0375aae151d7e212351ad336fe397942ba90c7fb77682efb97e309f564736f6c63430008210033"

var (
funcRun = w3.MustNewFunc("run(uint256)", "")
funcEmitLog = w3.MustNewFunc("emitLog(bytes32[],bytes)", "")
funcHitSameSlot = w3.MustNewFunc("hitSameSlot(uint256)", "")
funcHitManySlots = w3.MustNewFunc("hitManySlots(uint256)", "")
)

// verifyOpReth checks the L2 execution layer client is op-reth by calling
// web3_clientVersion via the L2EthClient's RPC and asserting it contains "reth".
func verifyOpReth(t devtest.T, l2EL *dsl.L2ELNode) string {
rpcClient := l2EL.Escape().L2EthClient().RPC()
var clientVersion string
err := rpcClient.CallContext(context.Background(), &clientVersion, "web3_clientVersion")
t.Require().NoError(err, "web3_clientVersion RPC failed — cannot verify EL client")

lower := strings.ToLower(clientVersion)
t.Require().True(
strings.Contains(lower, "reth"),
"FATAL: Expected op-reth execution client, but got: %q. "+
"This test MUST run on op-reth. "+
"Set DEVSTACK_L2EL_KIND=op-reth or ensure op-reth binary is available.",
clientVersion,
)
t.Require().False(
strings.Contains(lower, "geth"),
"FATAL: Detected op-geth (%q) but this test requires op-reth.", clientVersion,
)

return clientVersion
}

// getOPGasRefund reads the opGasRefund field from a transaction receipt via
// raw JSON RPC. Returns 0 if the field is not present.
func getOPGasRefund(t devtest.T, l2EL *dsl.L2ELNode, txHash common.Hash) uint64 {
rpcClient := l2EL.Escape().L2EthClient().RPC()
var raw json.RawMessage
err := rpcClient.CallContext(context.Background(), &raw, "eth_getTransactionReceipt", txHash)
if err != nil || raw == nil {
return 0
}

var result struct {
OPGasRefund *hexutil.Uint64 `json:"opGasRefund"`
}
if err := json.Unmarshal(raw, &result); err != nil {
return 0
}
if result.OPGasRefund != nil {
return uint64(*result.OPGasRefund)
}
return 0
}

func deployContract(t devtest.T, eoa *dsl.EOA, hexBytecode string) common.Address {
tx := txplan.NewPlannedTx(eoa.Plan(), txplan.WithData(common.FromHex(hexBytecode)))
res, err := tx.Included.Eval(t.Ctx())
t.Require().NoError(err, "failed to deploy contract")
return res.ContractAddress
}

func encodeRun(n uint64) []byte {
data, err := funcRun.EncodeArgs(new(big.Int).SetUint64(n))
if err != nil {
panic(fmt.Sprintf("failed to encode run(%d): %v", n, err))
}
return data
}

func encodeEmitLog(topicCount int, dataLen int) []byte {
topics := make([][32]byte, topicCount)
for i := range topics {
topics[i] = [32]byte{byte(i + 1)}
}
opaqueData := make([]byte, dataLen)
for i := range opaqueData {
opaqueData[i] = byte(i % 256)
}
data, err := funcEmitLog.EncodeArgs(topics, opaqueData)
if err != nil {
panic(fmt.Sprintf("failed to encode emitLog: %v", err))
}
return data
}

func encodeHitSameSlot(n uint64) []byte {
data, err := funcHitSameSlot.EncodeArgs(new(big.Int).SetUint64(n))
if err != nil {
panic(fmt.Sprintf("failed to encode hitSameSlot(%d): %v", n, err))
}
return data
}

func encodeHitManySlots(n uint64) []byte {
data, err := funcHitManySlots.EncodeArgs(new(big.Int).SetUint64(n))
if err != nil {
panic(fmt.Sprintf("failed to encode hitManySlots(%d): %v", n, err))
}
return data
}
38 changes: 38 additions & 0 deletions op-acceptance-tests/tests/sdm/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package sdm

import (
"github.com/ethereum-optimism/optimism/op-devstack/devtest"
"github.com/ethereum-optimism/optimism/op-devstack/dsl"
"github.com/ethereum-optimism/optimism/op-devstack/presets"
"github.com/ethereum-optimism/optimism/op-devstack/sysgo"
)

type sdmRethSystem struct {
L2EL *dsl.L2ELNode
L2Batcher *dsl.L2Batcher
FunderL2 *dsl.Funder
}

func newSDMRethSystem(t devtest.T, sdmEnabled bool) *sdmRethSystem {
runtime := sysgo.NewMixedSingleChainRuntime(t, sysgo.MixedSingleChainPresetConfig{
NodeSpecs: []sysgo.MixedSingleChainNodeSpec{
{
ELKey: "sequencer-op-reth",
CLKey: "sequencer",
ELKind: sysgo.MixedL2ELOpReth,
CLKind: sysgo.MixedL2CLOpNode,
IsSequencer: true,
SDMEnabled: sdmEnabled,
},
},
})
frontends := presets.NewMixedSingleChainFrontends(t, runtime)
frontends.L2Batcher.Stop()

wallet := dsl.NewRandomHDWallet(t, 30)
return &sdmRethSystem{
L2EL: frontends.L2Network.PrimaryEL(),
L2Batcher: frontends.L2Batcher,
FunderL2: dsl.NewFunder(wallet, frontends.FaucetL2, frontends.L2Network.PrimaryEL()),
}
}
32 changes: 32 additions & 0 deletions op-chain-ops/pkg/sdmreplay/jsonl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package sdmreplay

import (
"encoding/json"
"io"
)

// WriteJSONL emits records in stable order: run config, tx rows, block rows, mismatch rows, summary.
func WriteJSONL(w io.Writer, result *RangeResult, summaryOnly bool) error {
enc := json.NewEncoder(w)
if err := enc.Encode(result.RunConfig); err != nil {
return err
}
for _, block := range result.Blocks {
if !summaryOnly {
for _, tx := range block.Txs {
if err := enc.Encode(tx); err != nil {
return err
}
}
}
if err := enc.Encode(block.Block); err != nil {
return err
}
for _, mismatch := range block.Mismatches {
if err := enc.Encode(mismatch); err != nil {
return err
}
}
}
return enc.Encode(result.Summary)
}
85 changes: 85 additions & 0 deletions op-chain-ops/pkg/sdmreplay/payload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package sdmreplay

import (
"fmt"

"github.com/ethereum/go-ethereum/rlp"
)

const SDMTxType = 0x7d

// PostExecPayloadVersion is the only PostExecPayload version the Go decoder accepts.
// Must stay in lock-step with POST_EXEC_PAYLOAD_VERSION in rust/op-alloy, which rejects
// unknown versions at decode time; Go accepting what Rust rejects is a cross-language
// drift hazard on any replay/verifier pipeline sitting between the two.
const PostExecPayloadVersion uint64 = 1

// SDMGasEntry is one per-transaction refund entry inside the SDM portion of a post-exec payload.
type SDMGasEntry struct {
Index uint64 `json:"index"`
GasRefund uint64 `json:"gas_refund"`
}

// PostExecPayload is the decoded RLP payload carried by the synthetic post-exec tx.
// Today this contains the SDM gas refund entries and the L2 block number the payload is anchored to.
// Older payloads may omit BlockNumber.
type PostExecPayload struct {
Version uint64 `json:"version"`
BlockNumber uint64 `json:"block_number,omitempty"`
GasRefundEntries []SDMGasEntry `json:"gas_refund_entries"`
}

// GasRefundForIndex returns the refund for the given block tx index.
func (p *PostExecPayload) GasRefundForIndex(index uint64) (uint64, bool) {
if p == nil {
return 0, false
}
for _, entry := range p.GasRefundEntries {
if entry.Index == index {
return entry.GasRefund, true
}
}
return 0, false
}

// DecodePayload decodes an RLP-encoded post-exec payload from the post-exec tx input.
func DecodePayload(input []byte) (*PostExecPayload, error) {
if len(input) == 0 {
return nil, fmt.Errorf("empty post-exec payload")
}

payload, err := decodePayloadStruct(input)
if err != nil {
return nil, err
}
if payload.Version != PostExecPayloadVersion {
return nil, fmt.Errorf(
"unsupported post-exec payload version %d (expected %d)",
payload.Version, PostExecPayloadVersion,
)
}
return payload, nil
}

// decodePayloadStruct tries the current RLP shape first, then falls back to the legacy
// two-field shape. Version validation is applied by the caller so unknown versions are
// rejected on either path.
func decodePayloadStruct(input []byte) (*PostExecPayload, error) {
var payload PostExecPayload
if err := rlp.DecodeBytes(input, &payload); err == nil {
return &payload, nil
}

// Backward compatibility for older payloads that encoded only version + refund entries.
var legacy struct {
Version uint64
GasRefundEntries []SDMGasEntry
}
if err := rlp.DecodeBytes(input, &legacy); err != nil {
return nil, fmt.Errorf("decode post-exec payload: %w", err)
}
return &PostExecPayload{
Version: legacy.Version,
GasRefundEntries: legacy.GasRefundEntries,
}, nil
}
73 changes: 73 additions & 0 deletions op-chain-ops/pkg/sdmreplay/payload_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package sdmreplay

import (
"strconv"
"testing"

"github.com/ethereum/go-ethereum/rlp"
"github.com/stretchr/testify/require"
)

func TestDecodePayload_AcceptsCurrentShape(t *testing.T) {
payload := PostExecPayload{
Version: PostExecPayloadVersion,
BlockNumber: 42,
GasRefundEntries: []SDMGasEntry{
{Index: 3, GasRefund: 7},
{Index: 5, GasRefund: 11},
},
}
encoded, err := rlp.EncodeToBytes(&payload)
require.NoError(t, err)

decoded, err := DecodePayload(encoded)
require.NoError(t, err)
require.Equal(t, &payload, decoded)
}

func TestDecodePayload_RejectsUnknownVersion(t *testing.T) {
// Any non-1 version must be rejected to stay in lock-step with the Rust decoder in
// rust/op-alloy, which gates on POST_EXEC_PAYLOAD_VERSION. Cross-language divergence
// here would let a Go-based replay pipeline accept payloads the Rust node rejects.
for _, version := range []uint64{0, 2, 99} {
t.Run("version_"+strconv.FormatUint(version, 10), func(t *testing.T) {
payload := PostExecPayload{
Version: version,
BlockNumber: 1,
GasRefundEntries: []SDMGasEntry{{Index: 0, GasRefund: 1}},
}
encoded, err := rlp.EncodeToBytes(&payload)
require.NoError(t, err)

_, err = DecodePayload(encoded)
require.Error(t, err)
require.Contains(t, err.Error(), "unsupported post-exec payload version")
})
}
}

func TestDecodePayload_RejectsUnknownVersionOnLegacyShape(t *testing.T) {
// Same version check on the legacy two-field shape — the fallback decoder must not
// become an escape hatch for payloads whose version is wrong.
legacy := struct {
Version uint64
GasRefundEntries []SDMGasEntry
}{
Version: 7,
GasRefundEntries: []SDMGasEntry{{Index: 0, GasRefund: 1}},
}
encoded, err := rlp.EncodeToBytes(&legacy)
require.NoError(t, err)

_, err = DecodePayload(encoded)
require.Error(t, err)
require.Contains(t, err.Error(), "unsupported post-exec payload version")
}

func TestDecodePayload_EmptyInputRejected(t *testing.T) {
_, err := DecodePayload(nil)
require.Error(t, err)

_, err = DecodePayload([]byte{})
require.Error(t, err)
}
Loading
Loading