From b03b318454051560ae7a46ec70b9f10243c7ddd1 Mon Sep 17 00:00:00 2001 From: Josh Klopfenstein Date: Thu, 18 Sep 2025 14:22:48 -0700 Subject: [PATCH 1/4] op-service/txplan: support blob txs --- op-service/txplan/txplan.go | 52 ++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/op-service/txplan/txplan.go b/op-service/txplan/txplan.go index 07d802ad8a8..d4151c03dfc 100644 --- a/op-service/txplan/txplan.go +++ b/op-service/txplan/txplan.go @@ -8,6 +8,7 @@ import ( "math/big" "github.com/ethereum-optimism/optimism/op-service/retry" + "github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/holiman/uint256" "github.com/ethereum/go-ethereum" @@ -54,6 +55,9 @@ type PlannedTx struct { Value plan.Lazy[*big.Int] AccessList plan.Lazy[types.AccessList] // resolves to nil if not an attribute AuthList plan.Lazy[[]types.SetCodeAuthorization] // resolves to nil if not a 7702 tx + BlobFeeCap plan.Lazy[*uint256.Int] // resolves to nil if not a blob tx + BlobHashes plan.Lazy[[]common.Hash] // resolves to nil if not a blob tx + Sidecar plan.Lazy[*types.BlobTxSidecar] // resolves to nil if not a blob tx } func (ptx *PlannedTx) String() string { @@ -384,6 +388,29 @@ func WithChainID(cl ChainID) Option { } } +func WithBlobs(blobs []*eth.Blob, config *params.ChainConfig) Option { + return func(tx *PlannedTx) { + tx.Type.Set(types.BlobTxType) + tx.BlobFeeCap.DependOn(&tx.AgainstBlock) + tx.BlobFeeCap.Fn(func(_ context.Context) (*uint256.Int, error) { + return uint256.MustFromBig(tx.AgainstBlock.Value().BlobBaseFee(config)), nil + }) + var blobHashes []common.Hash + tx.Sidecar.Fn(func(_ context.Context) (*types.BlobTxSidecar, error) { + sidecar, hashes, err := txmgr.MakeSidecar(blobs, true) + if err != nil { + return nil, fmt.Errorf("make blob tx sidecar: %w", err) + } + blobHashes = hashes + return sidecar, nil + }) + tx.BlobHashes.DependOn(&tx.Sidecar) + tx.BlobHashes.Fn(func(_ context.Context) ([]common.Hash, error) { + return blobHashes, nil + }) + } +} + func (tx *PlannedTx) Defaults() { tx.Type.Set(types.DynamicFeeTxType) tx.To.Set(nil) @@ -421,6 +448,10 @@ func (tx *PlannedTx) Defaults() { return crypto.PubkeyToAddress(tx.Priv.Value().PublicKey), nil }) + tx.BlobFeeCap.Set(nil) + tx.BlobHashes.Set(nil) + tx.Sidecar.Set(nil) + // Automatically build tx from the individual attributes tx.Unsigned.DependOn( &tx.Sender, @@ -435,6 +466,9 @@ func (tx *PlannedTx) Defaults() { &tx.Value, &tx.AccessList, &tx.AuthList, + &tx.BlobFeeCap, + &tx.BlobHashes, + &tx.Sidecar, ) tx.Unsigned.Fn(func(ctx context.Context) (types.TxData, error) { chainID := tx.ChainID.Value() @@ -501,7 +535,23 @@ func (tx *PlannedTx) Defaults() { S: nil, }, nil case types.BlobTxType: - return nil, errors.New("blob tx not supported") + return &types.BlobTx{ + ChainID: uint256.MustFromBig(chainID.ToBig()), + Nonce: tx.Nonce.Value(), + GasTipCap: uint256.MustFromBig(tx.GasTipCap.Value()), + GasFeeCap: uint256.MustFromBig(tx.GasFeeCap.Value()), + Gas: tx.Gas.Value(), + To: *tx.To.Value(), + Value: uint256.MustFromBig(tx.Value.Value()), + Data: tx.Data.Value(), + AccessList: tx.AccessList.Value(), + BlobFeeCap: tx.BlobFeeCap.Value(), + BlobHashes: tx.BlobHashes.Value(), + Sidecar: tx.Sidecar.Value(), + V: nil, + R: nil, + S: nil, + }, nil case types.DepositTxType: return nil, errors.New("deposit tx not supported") default: From 0a6ae70baaa8df8906082d97f2362e942bca3398 Mon Sep 17 00:00:00 2001 From: Josh Klopfenstein Date: Thu, 18 Sep 2025 14:22:48 -0700 Subject: [PATCH 2/4] op-service/txinclude: handle future nonce gaps This can happen when we get mempool errors like "nonce too high", which itself can occur when we hit "account limit exceeded" errors in the blob pool. --- op-service/txinclude/nonce_manager.go | 6 +++++- op-service/txinclude/nonce_manager_test.go | 24 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/op-service/txinclude/nonce_manager.go b/op-service/txinclude/nonce_manager.go index 3b1eb64d51c..1bf546721a5 100644 --- a/op-service/txinclude/nonce_manager.go +++ b/op-service/txinclude/nonce_manager.go @@ -35,10 +35,14 @@ func (nm *nonceManager) Next() uint64 { return nonce } -// InsertGap inserts a nonce gap. It is a no-op if nonce is already a gap. +// InsertGap inserts a nonce gap. It is a no-op if nonce is already a gap or if it is ahead of the +// current nonce. func (nm *nonceManager) InsertGap(nonce uint64) { nm.mu.Lock() defer nm.mu.Unlock() + if nonce >= nm.nextNonce { + return + } i, exists := slices.BinarySearch(nm.gaps, nonce) if exists { return diff --git a/op-service/txinclude/nonce_manager_test.go b/op-service/txinclude/nonce_manager_test.go index 386859fe9b0..611f10f8d25 100644 --- a/op-service/txinclude/nonce_manager_test.go +++ b/op-service/txinclude/nonce_manager_test.go @@ -87,4 +87,28 @@ func TestNonceManagerInsertGap(t *testing.T) { require.Equal(t, uint64(30), nm.Next()) require.Equal(t, uint64(100), nm.Next()) }) + + t.Run("future gap is a no-op", func(t *testing.T) { + nm := newNonceManager(20) + + nm.InsertGap(21) + + require.Equal(t, uint64(20), nm.Next()) + require.Equal(t, uint64(21), nm.Next()) + require.Equal(t, uint64(22), nm.Next()) + }) + + t.Run("handles multiple future gaps", func(t *testing.T) { + nm := newNonceManager(20) + + nm.InsertGap(21) + nm.InsertGap(22) + nm.InsertGap(23) + + require.Equal(t, uint64(20), nm.Next()) + require.Equal(t, uint64(21), nm.Next()) + require.Equal(t, uint64(22), nm.Next()) + require.Equal(t, uint64(23), nm.Next()) + require.Equal(t, uint64(24), nm.Next()) + }) } From 24bc9fb66d0e6cf2a56f4d1149acdfe473e4aeeb Mon Sep 17 00:00:00 2001 From: Josh Klopfenstein Date: Thu, 18 Sep 2025 14:22:48 -0700 Subject: [PATCH 3/4] sysgo: add Osaka activation test --- mise.toml | 1 + .../tests/interop/loadtest/schedule.go | 14 +- op-acceptance-tests/tests/osaka/osaka_test.go | 214 ++++++++++++++++++ .../deployer/pipeline/seal_l1_dev_genesis.go | 3 + op-deployer/pkg/deployer/state/intent.go | 11 + op-devstack/dsl/el.go | 12 + op-devstack/sysgo/engine_client.go | 4 + op-devstack/sysgo/l1_nodes_subprocess.go | 7 +- op-e2e/e2eutils/geth/fakepos.go | 11 +- op-e2e/e2eutils/intentbuilder/builder.go | 22 ++ op-e2e/e2eutils/intentbuilder/builder_test.go | 6 + 11 files changed, 299 insertions(+), 6 deletions(-) create mode 100644 op-acceptance-tests/tests/osaka/osaka_test.go diff --git a/mise.toml b/mise.toml index 76ca9d78a5c..6b8d56fd773 100644 --- a/mise.toml +++ b/mise.toml @@ -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" diff --git a/op-acceptance-tests/tests/interop/loadtest/schedule.go b/op-acceptance-tests/tests/interop/loadtest/schedule.go index 9420ba4119e..00aa0160229 100644 --- a/op-acceptance-tests/tests/interop/loadtest/schedule.go +++ b/op-acceptance-tests/tests/interop/loadtest/schedule.go @@ -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) @@ -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 } diff --git a/op-acceptance-tests/tests/osaka/osaka_test.go b/op-acceptance-tests/tests/osaka/osaka_test.go new file mode 100644 index 00000000000..eabd454749b --- /dev/null +++ b/op-acceptance-tests/tests/osaka/osaka_test.go @@ -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(¶ms.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(), + }) +} diff --git a/op-deployer/pkg/deployer/pipeline/seal_l1_dev_genesis.go b/op-deployer/pkg/deployer/pipeline/seal_l1_dev_genesis.go index dec3e742a16..d267b841b13 100644 --- a/op-deployer/pkg/deployer/pipeline/seal_l1_dev_genesis.go +++ b/op-deployer/pkg/deployer/pipeline/seal_l1_dev_genesis.go @@ -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) diff --git a/op-deployer/pkg/deployer/state/intent.go b/op-deployer/pkg/deployer/state/intent.go index e12c356a212..8c331e9f642 100644 --- a/op-deployer/pkg/deployer/state/intent.go +++ b/op-deployer/pkg/deployer/state/intent.go @@ -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" @@ -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"` diff --git a/op-devstack/dsl/el.go b/op-devstack/dsl/el.go index 715c60e3087..b7e4d97251a 100644 --- a/op-devstack/dsl/el.go +++ b/op-devstack/dsl/el.go @@ -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 } diff --git a/op-devstack/sysgo/engine_client.go b/op-devstack/sysgo/engine_client.go index 225765b88a7..d825cd6c5df 100644 --- a/op-devstack/sysgo/engine_client.go +++ b/op-devstack/sysgo/engine_client.go @@ -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 { diff --git a/op-devstack/sysgo/l1_nodes_subprocess.go b/op-devstack/sysgo/l1_nodes_subprocess.go index 0edbc09af35..90ce7a4cfcf 100644 --- a/op-devstack/sysgo/l1_nodes_subprocess.go +++ b/op-devstack/sysgo/l1_nodes_subprocess.go @@ -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{ diff --git a/op-e2e/e2eutils/geth/fakepos.go b/op-e2e/e2eutils/geth/fakepos.go index 6784fa7fea7..b84c9216a73 100644 --- a/op-e2e/e2eutils/geth/fakepos.go +++ b/op-e2e/e2eutils/geth/fakepos.go @@ -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) @@ -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 } @@ -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) diff --git a/op-e2e/e2eutils/intentbuilder/builder.go b/op-e2e/e2eutils/intentbuilder/builder.go index 448cb72e1c4..be8d4d8c8ea 100644 --- a/op-e2e/e2eutils/intentbuilder/builder.go +++ b/op-e2e/e2eutils/intentbuilder/builder.go @@ -10,6 +10,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-chain-ops/devkeys" @@ -28,6 +29,9 @@ type L1Configurator interface { WithGasLimit(v uint64) L1Configurator WithExcessBlobGas(v uint64) L1Configurator WithPragueOffset(v uint64) L1Configurator + WithOsakaOffset(v uint64) L1Configurator + WithBPO1Offset(v uint64) L1Configurator + WithL1BlobSchedule(schedule *params.BlobScheduleConfig) L1Configurator WithPrefundedAccount(addr common.Address, amount uint256.Int) L1Configurator } @@ -303,6 +307,24 @@ func (c *l1Configurator) WithPragueOffset(v uint64) L1Configurator { return c } +func (c *l1Configurator) WithOsakaOffset(v uint64) L1Configurator { + c.initL1DevGenesisParams() + c.builder.intent.L1DevGenesisParams.OsakaTimeOffset = &v + return c +} + +func (c *l1Configurator) WithBPO1Offset(v uint64) L1Configurator { + c.initL1DevGenesisParams() + c.builder.intent.L1DevGenesisParams.BPO1TimeOffset = &v + return c +} + +func (c *l1Configurator) WithL1BlobSchedule(schedule *params.BlobScheduleConfig) L1Configurator { + c.initL1DevGenesisParams() + c.builder.intent.L1DevGenesisParams.BlobSchedule = schedule + return c +} + func (c *l1Configurator) WithPrefundedAccount(addr common.Address, amount uint256.Int) L1Configurator { c.initL1DevGenesisParams() c.builder.intent.L1DevGenesisParams.Prefund[addr] = (*hexutil.U256)(&amount) diff --git a/op-e2e/e2eutils/intentbuilder/builder_test.go b/op-e2e/e2eutils/intentbuilder/builder_test.go index ecf7e19d307..d98eec5ce38 100644 --- a/op-e2e/e2eutils/intentbuilder/builder_test.go +++ b/op-e2e/e2eutils/intentbuilder/builder_test.go @@ -36,6 +36,8 @@ func TestBuilder(t *testing.T) { // Configure L1 pragueOffset := uint64(100) + osakaOffset := uint64(200) + bpo1Offset := uint64(300) alice := common.HexToAddress("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") aliceFunds := uint256.NewInt(10000) l1Params := state.L1DevGenesisParams{ @@ -45,6 +47,8 @@ func TestBuilder(t *testing.T) { ExcessBlobGas: 123, }, PragueTimeOffset: &pragueOffset, + OsakaTimeOffset: &osakaOffset, + BPO1TimeOffset: &bpo1Offset, Prefund: map[common.Address]*hexutil.U256{ alice: (*hexutil.U256)(aliceFunds), }, @@ -55,6 +59,8 @@ func TestBuilder(t *testing.T) { l1Config.WithGasLimit(l1Params.BlockParams.GasLimit) l1Config.WithExcessBlobGas(l1Params.BlockParams.ExcessBlobGas) l1Config.WithPragueOffset(*l1Params.PragueTimeOffset) + l1Config.WithOsakaOffset(*l1Params.OsakaTimeOffset) + l1Config.WithBPO1Offset(*l1Params.BPO1TimeOffset) l1Config.WithPrefundedAccount(alice, *aliceFunds) // Configure L2 From 9f3c38c17464bc09c074300f3fbc186f00f21bb4 Mon Sep 17 00:00:00 2001 From: Josh Klopfenstein Date: Sun, 5 Oct 2025 17:06:57 -0700 Subject: [PATCH 4/4] sysext: infer L1 config when possible --- devnet-sdk/shell/env/devnet.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/devnet-sdk/shell/env/devnet.go b/devnet-sdk/shell/env/devnet.go index e766a0032ef..2f1d3baf6eb 100644 --- a/devnet-sdk/shell/env/devnet.go +++ b/devnet-sdk/shell/env/devnet.go @@ -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" ) @@ -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 = ¶ms.ChainConfig{ - ChainID: l1ID, + if l1Config := eth.L1ChainConfigByChainID(eth.ChainIDFromBig(l1ID)); l1Config != nil { + config.L1.Config = l1Config + } else { + config.L1.Config = ¶ms.ChainConfig{ + ChainID: l1ID, + } } } for _, l2Chain := range config.L2 {