diff --git a/devnet-sdk/shell/env/devnet.go b/devnet-sdk/shell/env/devnet.go index e766a0032ef7f..2f1d3baf6eb88 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 { diff --git a/mise.toml b/mise.toml index 76ca9d78a5cad..6b8d56fd77366 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 9420ba4119e6f..00aa016022938 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 0000000000000..eabd454749b8c --- /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 dec3e742a1668..d267b841b1378 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 e12c356a21248..8c331e9f64296 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 715c60e30874c..b7e4d97251a72 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 225765b88a7c2..d825cd6c5df52 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 0edbc09af35c9..90ce7a4cfcfe7 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 6784fa7fea7f8..b84c9216a7337 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 448cb72e1c453..be8d4d8c8eae2 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 ecf7e19d30738..d98eec5ce387f 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 diff --git a/op-service/txinclude/nonce_manager.go b/op-service/txinclude/nonce_manager.go index 3b1eb64d51c15..1bf546721a5c7 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 386859fe9b0d5..611f10f8d2570 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()) + }) } diff --git a/op-service/txplan/txplan.go b/op-service/txplan/txplan.go index 07d802ad8a88e..d4151c03dfc1c 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: