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
329 changes: 329 additions & 0 deletions op-acceptance-tests/tests/osaka_on_l2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
package tests

import (
"context"
"math/big"
"sync"
"testing"
"time"

"github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop/loadtest"
"github.com/ethereum-optimism/optimism/op-core/forks"
"github.com/ethereum-optimism/optimism/op-core/predeploys"
"github.com/ethereum-optimism/optimism/op-devstack/devtest"
"github.com/ethereum-optimism/optimism/op-devstack/presets"
"github.com/ethereum-optimism/optimism/op-devstack/sysgo"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/txplan"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/rpc"
)

var modexpPrecompile = common.HexToAddress("0x0000000000000000000000000000000000000005")
var p256VerifyPrecompile = common.HexToAddress("0x0000000000000000000000000000000000000100")

// buildModExpInput constructs input data for the MODEXP precompile (address 0x05).
// Format: <Bsize (32 bytes)> <Esize (32 bytes)> <Msize (32 bytes)> <B> <E> <M>
func buildModExpInput(base, exp, mod []byte) []byte {
input := make([]byte, 0, 96+len(base)+len(exp)+len(mod))
input = append(input, common.LeftPadBytes(new(big.Int).SetInt64(int64(len(base))).Bytes(), 32)...)
input = append(input, common.LeftPadBytes(new(big.Int).SetInt64(int64(len(exp))).Bytes(), 32)...)
input = append(input, common.LeftPadBytes(new(big.Int).SetInt64(int64(len(mod))).Bytes(), 32)...)
input = append(input, base...)
input = append(input, exp...)
input = append(input, mod...)
return input
}

func TestEIP7823UpperBoundModExp(gt *testing.T) {
t := devtest.ParallelT(gt)

karstOffset := uint64(3)
sys := presets.NewMinimal(t, presets.WithDeployerOptions(sysgo.WithKarstAtOffset(&karstOffset)))

activationBlock := sys.L2Chain.AwaitActivation(t, forks.Karst)
t.Require().Greater(activationBlock.Number, uint64(0), "karst must not activate at genesis")
preForkBlockNum := activationBlock.Number - 1
postForkBlockNum := activationBlock.Number + 1
sys.L2EL.WaitForBlockNumber(postForkBlockNum)

l2Client := sys.L2EL.EthClient()

// Modexp input exceeding EIP-7823 limits: modulus length is 1025 bytes (limit is 1024)
oversizeMod := make([]byte, 1025)
oversizeMod[1024] = 5
exceedingLimitInput := buildModExpInput([]byte{2}, []byte{3}, oversizeMod)

// Pre-fork: oversized modexp input should succeed (EIP-7823 not yet active)
result, err := l2Client.Call(t.Ctx(), ethereum.CallMsg{
To: &modexpPrecompile,
Data: exceedingLimitInput,
}, rpc.BlockNumber(preForkBlockNum))
t.Require().NoError(err)
t.Require().Len(result, 1025, "pre-fork: modexp with oversized input should return 1025-byte result")

// Post-fork: oversized modexp input should fail (EIP-7823 enforced)
result, err = l2Client.Call(t.Ctx(), ethereum.CallMsg{
To: &modexpPrecompile,
Data: exceedingLimitInput,
}, rpc.BlockNumber(postForkBlockNum))
t.Require().Error(err)
t.Require().Empty(result, "post-fork: modexp with oversized input should return empty result due to EIP-7823")

// Post-fork: within-limit modexp input should still succeed
result, err = l2Client.Call(t.Ctx(), ethereum.CallMsg{
To: &modexpPrecompile,
Data: buildModExpInput([]byte{2}, []byte{3}, []byte{5}),
}, rpc.BlockNumber(postForkBlockNum))
t.Require().NoError(err)
t.Require().Equal([]byte{3}, result, "2^3 mod 5 should equal 3")
}

func TestEIP7883ModExpGasCostIncrease(gt *testing.T) {
t := devtest.ParallelT(gt)

karstOffset := uint64(3)
sys := presets.NewMinimal(t, presets.WithDeployerOptions(sysgo.WithKarstAtOffset(&karstOffset)))

activationBlock := sys.L2Chain.AwaitActivation(t, forks.Karst)
t.Require().Greater(activationBlock.Number, uint64(0), "karst must not activate at genesis")
preForkBlockNum := activationBlock.Number - 1
postForkBlockNum := activationBlock.Number + 1
sys.L2EL.WaitForBlockNumber(postForkBlockNum)

l2Client := sys.L2EL.EthClient()

// Call modexp with empty calldata. The precompile pads missing bytes with
// zeros, giving Bsize=0, Esize=0, Msize=0. This hits exactly the gas floor:
// EIP-2565 (pre-Karst): max(200, floor(0*0/3)) = 200 gas
// EIP-7883 (post-Karst): max(500, floor(0*0)) = 500 gas
// Empty calldata also avoids EIP-7623 calldata cost inflation, so intrinsic
// gas is just 21,000 and we can precisely control execution gas via Gas limit.

// Pre-fork: 21,000 + 300 execution gas is enough for 200-gas floor.
_, err := l2Client.Call(t.Ctx(), ethereum.CallMsg{
To: &modexpPrecompile,
Gas: 21_300,
}, rpc.BlockNumber(preForkBlockNum))
t.Require().NoError(err, "pre-fork: modexp should succeed with 300 execution gas (floor is 200)")

// Post-fork: 21,000 + 300 execution gas is NOT enough for 500-gas floor.
_, err = l2Client.Call(t.Ctx(), ethereum.CallMsg{
To: &modexpPrecompile,
Gas: 21_300,
}, rpc.BlockNumber(postForkBlockNum))
t.Require().Error(err, "post-fork: modexp should fail with 300 execution gas (floor is 500)")

// Post-fork: 21,000 + 600 execution gas is enough for 500-gas floor.
_, err = l2Client.Call(t.Ctx(), ethereum.CallMsg{
To: &modexpPrecompile,
Gas: 21_600,
}, rpc.BlockNumber(postForkBlockNum))
t.Require().NoError(err, "post-fork: modexp should succeed with 600 execution gas (floor is 500)")
}

func TestEIP7825TxGasLimitCap(gt *testing.T) {
t := devtest.ParallelT(gt)
testCases := map[string]struct {
opt sysgo.DeployerOption
expectErr bool
}{
"pre-karst": {
opt: sysgo.WithJovianAtGenesis,
},
"post-karst": {
opt: sysgo.WithKarstAtGenesis,
expectErr: true,
},
}
// EIP-7825 caps transaction gas at 2^24 = 16,777,216.
// This is a tx validity rule enforced at the txpool/block level, not by the
// EVM, so eth_call and eth_simulateV1 don't enforce it. We must send a real
// transaction and verify the RPC rejects it.
const maxTxGas = 1 << 24
for name, testCase := range testCases {
t.Run(name, func(t devtest.T) {
t.Parallel()
sys := presets.NewMinimal(t, presets.WithDeployerOptions(testCase.opt))

eoa := sys.FunderL2.NewFundedEOA(eth.OneEther)

planWithGasLimit := func(gas uint64) txplan.Option {
return txplan.Combine(
eoa.Plan(),
txplan.WithGasLimit(gas),
txplan.WithTo(&common.Address{}),
)
}

_, err := txplan.NewPlannedTx(planWithGasLimit(maxTxGas)).Success.Eval(t.Ctx())
t.Require().NoError(err, "tx with gas at 2^24 should succeed")

tx := txplan.NewPlannedTx(planWithGasLimit(maxTxGas + 1))
if testCase.expectErr {
_, err := tx.Included.Eval(t.Ctx())
t.Require().Error(err, "tx with gas above 2^24 should be rejected")
} else {
_, err := tx.Success.Eval(t.Ctx())
t.Require().NoError(err, "tx with gas above 2^24 should succeed")
}
})
}
}

func TestEIP7951P256VerifyGasCostIncrease(gt *testing.T) {
t := devtest.ParallelT(gt)

karstOffset := uint64(3)
sys := presets.NewMinimal(t, presets.WithDeployerOptions(sysgo.WithKarstAtOffset(&karstOffset)))

activationBlock := sys.L2Chain.AwaitActivation(t, forks.Karst)
t.Require().Greater(activationBlock.Number, uint64(0), "karst must not activate at genesis")
preForkBlockNum := activationBlock.Number - 1
postForkBlockNum := activationBlock.Number + 1
sys.L2EL.WaitForBlockNumber(postForkBlockNum)

l2Client := sys.L2EL.EthClient()

// Call P256VERIFY with empty calldata. The precompile charges its full gas
// cost regardless of input length, then returns empty (input != 160 bytes).
// Empty calldata avoids EIP-7623 calldata cost inflation, so intrinsic gas
// is just 21,000 and we can precisely control execution gas via gas limit.
// RIP-7212 (pre-Karst): P256VERIFY costs 3,450 gas
// EIP-7951 (post-Karst): P256VERIFY costs 6,900 gas

// Pre-fork: 21,000 + 3,500 execution gas is enough for 3,450-gas precompile.
_, err := l2Client.Call(t.Ctx(), ethereum.CallMsg{
To: &p256VerifyPrecompile,
Gas: 24_500,
}, rpc.BlockNumber(preForkBlockNum))
t.Require().NoError(err, "pre-fork: P256VERIFY should succeed with 3,500 execution gas (cost is 3,450)")

// Post-fork: 21,000 + 3,500 execution gas is NOT enough for 6,900-gas precompile.
_, err = l2Client.Call(t.Ctx(), ethereum.CallMsg{
To: &p256VerifyPrecompile,
Gas: 24_500,
}, rpc.BlockNumber(postForkBlockNum))
t.Require().Error(err, "post-fork: P256VERIFY should fail with 3,500 execution gas (cost is 6,900)")

// Post-fork: 21,000 + 7,000 execution gas is enough for 6,900-gas precompile.
_, err = l2Client.Call(t.Ctx(), ethereum.CallMsg{
To: &p256VerifyPrecompile,
Gas: 28_000,
}, rpc.BlockNumber(postForkBlockNum))
t.Require().NoError(err, "post-fork: P256VERIFY should succeed with 7,000 execution gas (cost is 6,900)")
}

func TestEIP7939CLZ(gt *testing.T) {
t := devtest.ParallelT(gt)

karstOffset := uint64(3)
sys := presets.NewMinimal(t, presets.WithDeployerOptions(sysgo.WithKarstAtOffset(&karstOffset)))

activationBlock := sys.L2Chain.AwaitActivation(t, forks.Karst)
t.Require().Greater(activationBlock.Number, uint64(0), "karst must not activate at genesis")
preForkBlockNum := activationBlock.Number - 1
postForkBlockNum := activationBlock.Number + 1
sys.L2EL.WaitForBlockNumber(postForkBlockNum)

l2Client := sys.L2EL.EthClient()

// EVM init code that computes CLZ(1) and returns the 32-byte result.
// CLZ(1) = 255 because 1 has 255 leading zero bits in a uint256.
clzCode := []byte{
byte(vm.PUSH1), 1, // stack: [1]
byte(vm.CLZ), // stack: [255] (1 has 255 leading zeros)
byte(vm.PUSH1), 0, // stack: [0, 255]
byte(vm.MSTORE), // mem[0:32] = 255
byte(vm.PUSH1), 32, // stack: [32]
byte(vm.PUSH1), 0, // stack: [0, 32]
byte(vm.RETURN), // return mem[0:32]
}

// Pre-fork: CLZ opcode (0x1e) is not yet valid, so execution should fail.
_, err := l2Client.Call(t.Ctx(), ethereum.CallMsg{
Data: clzCode,
}, rpc.BlockNumber(preForkBlockNum))
t.Require().Error(err, "pre-fork: CLZ opcode should not be available")

// Post-fork: CLZ opcode is valid, execution should succeed.
result, err := l2Client.Call(t.Ctx(), ethereum.CallMsg{
Data: clzCode,
}, rpc.BlockNumber(postForkBlockNum))
t.Require().NoError(err, "post-fork: CLZ opcode should be available")
expected := common.LeftPadBytes([]byte{0xff}, 32) // 255 as uint256
t.Require().Equal(expected, result, "CLZ(1) should equal 255")
}

func TestEIP7934BlockSizeLimitDisabled(gt *testing.T) {
t := devtest.ParallelT(gt)

// EIP-7934 limits RLP-encoded block size to 10 MiB on L1. OP Stack
// chains must NOT enforce this limit. We prove it by building a single
// block whose transaction data alone exceeds 10 MiB.
//
// EIP-7623 inflates zero-byte calldata cost to 10 gas/byte, so packing
// 12 MB into one block requires ~120M gas.
sys := presets.NewMinimal(t, presets.WithDeployerOptions(
sysgo.WithKarstAtGenesis,
sysgo.WithL2GasLimit(200_000_000),
))

spamTxs(sys)

// Find a block whose total transaction data exceeds 10 MiB.
l2Client := sys.L2EL.EthClient()
l2BlockTime := time.Duration(sys.L2Chain.Escape().RollupConfig().BlockTime) * time.Second
for {
select {
case <-time.After(l2BlockTime):
_, blockTxs, err := l2Client.InfoAndTxsByLabel(t.Ctx(), eth.Unsafe)
t.Require().NoError(err)

var totalTxSize int
for _, tx := range blockTxs {
bin, err := tx.MarshalBinary()
t.Require().NoError(err)
totalTxSize += len(bin)
}

if totalTxSize > 10_000_000 {
return
}
case <-t.Ctx().Done():
t.Require().NoError(t.Ctx().Err())
}
}
}

func spamTxs(sys *presets.Minimal) {
l2BlockTime := time.Duration(sys.L2Chain.Escape().RollupConfig().BlockTime) * time.Second
eoas := loadtest.FundEOAs(sys.T, eth.HundredEther, 50, l2BlockTime, sys.L2EL, sys.Wallet, sys.FaucetL2)
eoasRR := loadtest.NewRoundRobin(eoas)
spammer := loadtest.SpammerFunc(func(t devtest.T) error {
// Max tx size in op-geth and op-reth mempools is 128 kB per tx.
// We leave an 8 kB buffer for tx data outside the calldata.
const calldataSize = 120 * 1024
_, err := eoasRR.Get().Include(t,
txplan.WithTo(&predeploys.L1BlockAddr),
txplan.WithData(make([]byte, calldataSize)),
txplan.WithGasLimit(1_250_000),
)
return err
})
schedule := loadtest.NewBurst(l2BlockTime, loadtest.WithBaseRPS(50))

ctx, cancel := context.WithCancel(sys.T.Ctx())
var wg sync.WaitGroup
wg.Add(1)
sys.T.Cleanup(func() {
cancel()
wg.Wait()
})
go func() {
defer wg.Done()
schedule.Run(sys.T.WithCtx(ctx), spammer)
}()
}
1 change: 1 addition & 0 deletions op-chain-ops/genesis/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,7 @@ func (d *DeployConfig) RollupConfig(l1StartBlock *eth.BlockRef, l2GenesisBlockHa
PectraBlobScheduleTime: d.PectraBlobScheduleTime(l1StartTime),
IsthmusTime: d.IsthmusTime(l1StartTime),
JovianTime: d.JovianTime(l1StartTime),
KarstTime: d.KarstTime(l1StartTime),
InteropTime: d.InteropTime(l1StartTime),
ProtocolVersionsAddress: d.ProtocolVersionsProxy,
AltDAConfig: altDA,
Expand Down
22 changes: 22 additions & 0 deletions op-devstack/sysgo/deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,34 @@ func WithDefaultBPOBlobSchedule(_ devtest.T, _ devkeys.Keys, builder intentbuild
})
}

func WithKarstAtOffset(offset *uint64) DeployerOption {
return func(p devtest.T, _ devkeys.Keys, builder intentbuilder.Builder) {
for _, l2Cfg := range builder.L2s() {
l2Cfg.WithForkAtOffset(opforks.Karst, offset)
}
}
}

func WithKarstAtGenesis(p devtest.T, _ devkeys.Keys, builder intentbuilder.Builder) {
for _, l2Cfg := range builder.L2s() {
l2Cfg.WithForkAtGenesis(opforks.Karst)
}
}

func WithJovianAtGenesis(p devtest.T, _ devkeys.Keys, builder intentbuilder.Builder) {
for _, l2Cfg := range builder.L2s() {
l2Cfg.WithForkAtGenesis(opforks.Jovian)
}
}

func WithL2GasLimit(gasLimit uint64) DeployerOption {
return func(_ devtest.T, _ devkeys.Keys, builder intentbuilder.Builder) {
for _, l2Cfg := range builder.L2s() {
l2Cfg.WithGasLimit(gasLimit)
}
}
}

func WithEcotoneAtGenesis(p devtest.T, _ devkeys.Keys, builder intentbuilder.Builder) {
for _, l2Cfg := range builder.L2s() {
l2Cfg.WithForkAtGenesis(opforks.Ecotone)
Expand Down
5 changes: 5 additions & 0 deletions op-e2e/e2eutils/intentbuilder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type L2Configurator interface {
L2HardforkConfigurator
WithPrefundedAccount(addr common.Address, amount uint256.Int) L2Configurator
WithDAFootprintGasScalar(scalar uint16)
WithGasLimit(v uint64)
}

type ContractsConfigurator interface {
Expand Down Expand Up @@ -551,6 +552,10 @@ func (c *l2Configurator) WithPrefundedAccount(addr common.Address, amount uint25
return c
}

func (c *l2Configurator) WithGasLimit(v uint64) {
c.builder.intent.Chains[c.chainIndex].GasLimit = v
}

func (c *l2Configurator) WithAdditionalDisputeGames(games []state.AdditionalDisputeGame) {
chain := c.builder.intent.Chains[c.chainIndex]
if chain.AdditionalDisputeGames == nil {
Expand Down
Loading
Loading