Skip to content
Merged
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
9 changes: 7 additions & 2 deletions devnet-sdk/shell/env/devnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/ethereum-optimism/optimism/devnet-sdk/controller/surface"
"github.com/ethereum-optimism/optimism/devnet-sdk/descriptors"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/params"
)

Expand Down Expand Up @@ -138,8 +139,12 @@ func fixupDevnetConfig(config *descriptors.DevnetEnvironment) error {
return fmt.Errorf("invalid L1 ID: %s", config.L1.ID)
}
if config.L1.Config == nil {
config.L1.Config = &params.ChainConfig{
ChainID: l1ID,
if l1Config := eth.L1ChainConfigByChainID(eth.ChainIDFromBig(l1ID)); l1Config != nil {
config.L1.Config = l1Config
} else {
config.L1.Config = &params.ChainConfig{
ChainID: l1ID,
}
}
}
for _, l2Chain := range config.L2 {
Expand Down
1 change: 1 addition & 0 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ svm-rs = "0.5.19"

# Go dependencies
"go:github.com/ethereum/go-ethereum/cmd/abigen" = "1.15.10"
"go:github.com/ethereum/go-ethereum/cmd/geth" = "1.16.4" # Osaka release.
"go:gotest.tools/gotestsum" = "1.12.1"
"go:github.com/vektra/mockery/v2" = "2.46.0"
"go:github.com/golangci/golangci-lint/cmd/golangci-lint" = "1.64.8"
Expand Down
14 changes: 12 additions & 2 deletions op-acceptance-tests/tests/interop/loadtest/schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,12 @@ type Spammer interface {
Spam(devtest.T) error
}

type SpammerFunc func(t devtest.T) error

func (s SpammerFunc) Spam(t devtest.T) error {
return s(t)
}

// Schedule schedules a Spammer. It determines how often to spam and when to stop.
type Schedule interface {
Run(devtest.T, Spammer)
Expand Down Expand Up @@ -326,12 +332,16 @@ func setupAIMD(t devtest.T, blockTime time.Duration, aimdOpts ...AIMDOption) *AI
t.Require().NoError(err)
}
aimd := NewAIMD(targetMessagePassesPerBlock, blockTime, aimdOpts...)
ctx, cancel := context.WithCancel(t.Ctx())
var wg sync.WaitGroup
t.Cleanup(wg.Wait)
t.Cleanup(func() {
cancel()
wg.Wait()
})
wg.Add(1)
go func() {
defer wg.Done()
aimd.Start(t.Ctx())
aimd.Start(ctx)
}()
return aimd
}
Expand Down
214 changes: 214 additions & 0 deletions op-acceptance-tests/tests/osaka/osaka_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package osaka

import (
"bytes"
"context"
"crypto/rand"
"fmt"
"math/big"
"os"
"os/exec"
"strings"
"sync"
"testing"
"time"

"github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop/loadtest"
"github.com/ethereum-optimism/optimism/op-batcher/batcher"
"github.com/ethereum-optimism/optimism/op-batcher/flags"
"github.com/ethereum-optimism/optimism/op-chain-ops/devkeys"
"github.com/ethereum-optimism/optimism/op-devstack/devtest"
"github.com/ethereum-optimism/optimism/op-devstack/presets"
"github.com/ethereum-optimism/optimism/op-devstack/stack"
"github.com/ethereum-optimism/optimism/op-devstack/sysgo"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/intentbuilder"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/txinclude"
"github.com/ethereum-optimism/optimism/op-service/txplan"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
)

// configureDevstackEnvVars sets the appropriate env vars to use a mise-installed geth binary for
// the L1 EL. This is useful in Osaka acceptance tests since op-geth does not include full Osaka
// support. This is meant to run before presets.DoMain in a TestMain function. It will log to
// stdout. ResetDevstackEnvVars should be used to reset the environment variables when TestMain
// exits.
//
// Note that this is a no-op if either [sysgo.DevstackL1ELKindVar] or [sysgo.GethExecPathEnvVar]
// are set.
//
// The returned callback resets any modified environment variables.
func configureDevstackEnvVars() func() {
if _, ok := os.LookupEnv(sysgo.DevstackL1ELKindEnvVar); ok {
return func() {}
}
if _, ok := os.LookupEnv(sysgo.GethExecPathEnvVar); ok {
return func() {}
}

cmd := exec.Command("mise", "which", "geth")
buf := bytes.NewBuffer([]byte{})
cmd.Stdout = buf
if err := cmd.Run(); err != nil {
fmt.Printf("Failed to find mise-installed geth: %v\n", err)
return func() {}
}
execPath := strings.TrimSpace(buf.String())
fmt.Println("Found mise-installed geth:", execPath)
_ = os.Setenv(sysgo.GethExecPathEnvVar, execPath)
_ = os.Setenv(sysgo.DevstackL1ELKindEnvVar, "geth")
return func() {
_ = os.Unsetenv(sysgo.GethExecPathEnvVar)
_ = os.Unsetenv(sysgo.DevstackL1ELKindEnvVar)
}
}

func TestMain(m *testing.M) {
resetEnvVars := configureDevstackEnvVars()
defer resetEnvVars()

presets.DoMain(m, stack.MakeCommon(stack.Combine[*sysgo.Orchestrator](
sysgo.DefaultMinimalSystem(&sysgo.DefaultMinimalSystemIDs{}),
sysgo.WithDeployerOptions(func(_ devtest.P, _ devkeys.Keys, builder intentbuilder.Builder) {
_, l1Config := builder.WithL1(sysgo.DefaultL1ID)
l1Config.WithOsakaOffset(0)
l1Config.WithBPO1Offset(0)
l1Config.WithL1BlobSchedule(&params.BlobScheduleConfig{
Cancun: params.DefaultCancunBlobConfig,
Osaka: params.DefaultOsakaBlobConfig,
Prague: params.DefaultPragueBlobConfig,
BPO1: params.DefaultBPO1BlobConfig,
})
}),
sysgo.WithBatcherOption(func(_ stack.L2BatcherID, cfg *batcher.CLIConfig) {
cfg.DataAvailabilityType = flags.BlobsType
}),
)))
}

func TestBatcherUsesNewSidecarFormatAfterOsaka(gt *testing.T) {
t := devtest.SerialT(gt)
sys := presets.NewMinimal(t)
t.Log("Waiting for Osaka to activate")
t.Require().NotNil(sys.L1Network.Escape().ChainConfig().OsakaTime)
sys.L1EL.WaitForTime(*sys.L1Network.Escape().ChainConfig().OsakaTime)
t.Log("Osaka activated")

// 1. Wait for the sequencer to build a block after Osaka is activated. This avoids a race
// condition where the unsafe head has been posted as part of a blob, but has not been
// marked as "safe" yet.
sys.L2EL.WaitForBlock()

// 2. Wait for the batcher to include target in a batch and post it to L1. Because the batch is
// posted after Osaka has activated, it means the batcher must have successfully used the
// new format.
target := sys.L2EL.BlockRefByLabel(eth.Unsafe)
blockTime := time.Duration(sys.L2Chain.Escape().RollupConfig().BlockTime) * time.Second
for range time.Tick(blockTime) {
if sys.L2EL.BlockRefByLabel(eth.Safe).Number >= target.Number {
// If the safe head is ahead of the target height and the target block is part of the
// canonical chain, then the target block is safe.
_, err := sys.L2EL.Escape().EthClient().BlockRefByHash(t.Ctx(), target.Hash)
t.Require().NoError(err)
return
}
}
}

func TestBlobBaseFeeIsCorrectAfterBPOFork(gt *testing.T) {
t := devtest.SerialT(gt)
sys := presets.NewMinimal(t)
t.Log("Waiting for BPO1 to activate")
t.Require().NotNil(sys.L1Network.Escape().ChainConfig().BPO1Time)
sys.L1EL.WaitForTime(*sys.L1Network.Escape().ChainConfig().BPO1Time)
t.Log("BPO1 activated")

sys.L1EL.WaitForBlock()
l1BlockTime := sys.L1EL.EstimateBlockTime()
l1ChainConfig := sys.L1Network.Escape().ChainConfig()

spamBlobs(t, sys) // Raise the blob base fee to make blob parameter changes visible.

// Wait for the blob base fee to rise above 1 so the blob parameter changes will be visible.
for range time.Tick(l1BlockTime) {
info, _, err := sys.L1EL.EthClient().InfoAndTxsByLabel(t.Ctx(), eth.Unsafe)
t.Require().NoError(err)
if calcBlobBaseFee(l1ChainConfig, info).Cmp(big.NewInt(1)) > 0 {
break
}
t.Logf("Waiting for blob base fee to rise above 1")
}

l2UnsafeRef := sys.L2CL.SyncStatus().UnsafeL2

// Get the L1 blob base fee.
l1OriginInfo, err := sys.L1EL.EthClient().InfoByHash(t.Ctx(), l2UnsafeRef.L1Origin.Hash)
t.Require().NoError(err)
l1BlobBaseFee := calcBlobBaseFee(l1ChainConfig, l1OriginInfo)

// Get the L2 blob base fee from the system deposit tx.
info, txs, err := sys.L2EL.Escape().EthClient().InfoAndTxsByHash(t.Ctx(), l2UnsafeRef.Hash)
t.Require().NoError(err)
blockInfo, err := derive.L1BlockInfoFromBytes(sys.L2Chain.Escape().RollupConfig(), info.Time(), txs[0].Data())
t.Require().NoError(err)
l2BlobBaseFee := blockInfo.BlobBaseFee

t.Require().Equal(l1BlobBaseFee, l2BlobBaseFee)
}

func spamBlobs(t devtest.T, sys *presets.Minimal) {
l1BlockTime := sys.L1EL.EstimateBlockTime()
l1ChainConfig := sys.L1Network.Escape().ChainConfig()

eoa := sys.FunderL1.NewFundedEOA(eth.OneEther.Mul(5))
signer := txinclude.NewPkSigner(eoa.Key().Priv(), sys.L1Network.ChainID().ToBig())
l1ETHClient := sys.L1EL.EthClient()
syncEOA := loadtest.NewSyncEOA(txinclude.NewPersistent(signer, struct {
*txinclude.Monitor
*txinclude.Resubmitter
}{
txinclude.NewMonitor(l1ETHClient, l1BlockTime),
txinclude.NewResubmitter(l1ETHClient, l1BlockTime),
}), eoa.Plan())

var blob eth.Blob
_, err := rand.Read(blob[:])
t.Require().NoError(err)
// get the field-elements into a valid range
for i := range 4096 {
blob[32*i] &= 0b0011_1111
}

const maxBlobTxsPerAccountInMempool = 16 // Private policy param in geth.
spammer := loadtest.SpammerFunc(func(t devtest.T) error {
_, err := syncEOA.Include(t, txplan.WithBlobs([]*eth.Blob{&blob}, l1ChainConfig), txplan.WithTo(&common.Address{}))
return err
})
txsPerSlot := min(l1ChainConfig.BlobScheduleConfig.BPO1.Max*3/4, maxBlobTxsPerAccountInMempool)
schedule := loadtest.NewConstant(l1BlockTime, loadtest.WithBaseRPS(uint64(txsPerSlot)))

ctx, cancel := context.WithCancel(t.Ctx())
var wg sync.WaitGroup
t.Cleanup(func() {
cancel()
wg.Wait()
})
wg.Add(1)
go func() {
defer wg.Done()
schedule.Run(t.WithCtx(ctx), spammer)
}()
}

func calcBlobBaseFee(cfg *params.ChainConfig, info eth.BlockInfo) *big.Int {
return eip4844.CalcBlobFee(cfg, &types.Header{
// It's unfortunate that we can't build a proper header from a BlockInfo.
// We do our best to work around deficiencies in the BlockInfo implementation here.
Time: info.Time(),
ExcessBlobGas: info.ExcessBlobGas(),
})
}
3 changes: 3 additions & 0 deletions op-deployer/pkg/deployer/pipeline/seal_l1_dev_genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ func SealL1DevGenesis(env *Env, intent *state.Intent, st *state.State) error {
},
L1ChainID: eth.ChainIDFromUInt64(intent.L1ChainID),
L1PragueTimeOffset: l1DevParams.PragueTimeOffset,
L1OsakaTimeOffset: l1DevParams.OsakaTimeOffset,
L1BPO1TimeOffset: l1DevParams.BPO1TimeOffset,
BlobScheduleConfig: l1DevParams.BlobSchedule,
})
if err != nil {
return fmt.Errorf("failed to create dev L1 genesis template: %w", err)
Expand Down
11 changes: 11 additions & 0 deletions op-deployer/pkg/deployer/state/intent.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/params"

"github.com/ethereum-optimism/optimism/op-chain-ops/addresses"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts"
Expand Down Expand Up @@ -55,6 +56,16 @@ type L1DevGenesisParams struct {
// PragueTimeOffset configures Prague (aka Pectra) to be activated at the given time after L1 dev genesis time.
PragueTimeOffset *uint64 `json:"pragueTimeOffset" toml:"pragueTimeOffset"`

// OsakaTimeOffset configures Osaka (the EL changes in the Fusaka Ethereum fork) to be
// activated at the given time after L1 dev genesis time.
OsakaTimeOffset *uint64 `json:"osakaTimeOffset" toml:"osakaTimeOffset"`

// BPO1TimeOffset configures the BPO1 fork to be activated at the given time after L1 dev
// genesis time.
BPO1TimeOffset *uint64 `json:"bpo1TimeOffset" toml:"bpo1TimeOffset"`

BlobSchedule *params.BlobScheduleConfig `json:"blobSchedule"`

// Prefund is a map of addresses to balances (in wei), to prefund in the L1 dev genesis state.
// This is independent of the "Prefund" functionality that may fund a default 20 test accounts.
Prefund map[common.Address]*hexutil.U256 `json:"prefund" toml:"prefund"`
Expand Down
12 changes: 12 additions & 0 deletions op-devstack/dsl/el.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ func (el *elNode) waitForNextBlock(blocksFromNow uint64) eth.BlockRef {
return newRef
}

// WaitForTime waits until the chain has reached or surpassed the given timestamp.
func (el *elNode) WaitForTime(timestamp uint64) eth.BlockRef {
for range time.Tick(500 * time.Millisecond) {
ref, err := el.inner.EthClient().BlockRefByLabel(el.ctx, eth.Unsafe)
el.require.NoError(err)
if ref.Time >= timestamp {
return ref
}
}
return eth.BlockRef{} // Should never be reached.
}

func (el *elNode) stackEL() stack.ELNode {
return el.inner
}
Expand Down
4 changes: 4 additions & 0 deletions op-devstack/sysgo/engine_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ func (e *engineClient) GetPayloadV4(id engine.PayloadID) (*engine.ExecutionPaylo
return e.getPayload(id, "engine_getPayloadV4")
}

func (e *engineClient) GetPayloadV5(id engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) {
return e.getPayload(id, "engine_getPayloadV5")
}

func (e *engineClient) NewPayloadV2(data engine.ExecutableData) (engine.PayloadStatusV1, error) {
var result engine.PayloadStatusV1
if err := e.inner.CallContext(context.Background(), &result, "engine_newPayloadV2", data); err != nil {
Expand Down
7 changes: 6 additions & 1 deletion op-devstack/sysgo/l1_nodes_subprocess.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,13 @@ func WithL1NodesSubprocess(id stack.L1ELNodeID, clID stack.L1CLNodeID) stack.Opt
args := []string{
"--log.format", "json",
"--datadir", dataDirPath,
"--ws", "--ws.addr", "127.0.0.1", "--ws.port", "0",
"--ws", "--ws.addr", "127.0.0.1", "--ws.port", "0", "--ws.origins", "*", "--ws.api", "admin,debug,eth,net,txpool",
"--authrpc.addr", "127.0.0.1", "--authrpc.port", "0", "--authrpc.jwtsecret", jwtPath,
"--ipcdisable",
"--nodiscover",
"--verbosity", "5",
"--miner.recommit", "2s",
"--gcmode", "archive",
}

l1EL := &ExternalL1Geth{
Expand Down
11 changes: 8 additions & 3 deletions op-e2e/e2eutils/geth/fakepos.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type EngineAPI interface {
ForkchoiceUpdatedV3(engine.ForkchoiceStateV1, *engine.PayloadAttributes) (engine.ForkChoiceResponse, error)
ForkchoiceUpdatedV2(engine.ForkchoiceStateV1, *engine.PayloadAttributes) (engine.ForkChoiceResponse, error)

GetPayloadV5(engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error)
GetPayloadV4(engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error)
GetPayloadV3(engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error)
GetPayloadV2(engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error)
Expand Down Expand Up @@ -157,8 +158,10 @@ func (f *FakePoS) Start() error {
Withdrawals: withdrawals,
}
parentBeaconBlockRoot := f.FakeBeaconBlockRoot(head.Time) // parent beacon block root
isCancun := f.config.IsCancun(new(big.Int).SetUint64(head.Number.Uint64()+1), newBlockTime)
isPrague := f.config.IsPrague(new(big.Int).SetUint64(head.Number.Uint64()+1), newBlockTime)
nextHeight := new(big.Int).SetUint64(head.Number.Uint64() + 1)
isCancun := f.config.IsCancun(nextHeight, newBlockTime)
isPrague := f.config.IsPrague(nextHeight, newBlockTime)
isOsaka := f.config.IsOsaka(nextHeight, newBlockTime)
if isCancun {
attrs.BeaconRoot = &parentBeaconBlockRoot
}
Expand Down Expand Up @@ -192,7 +195,9 @@ func (f *FakePoS) Start() error {
return nil
}
var envelope *engine.ExecutionPayloadEnvelope
if isPrague {
if isOsaka {
envelope, err = f.engineAPI.GetPayloadV5(*res.PayloadID)
} else if isPrague {
envelope, err = f.engineAPI.GetPayloadV4(*res.PayloadID)
} else if isCancun {
envelope, err = f.engineAPI.GetPayloadV3(*res.PayloadID)
Expand Down
Loading