diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index f3e2052cfa3..c4acf5d16d2 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -377,6 +377,18 @@ type UpgradeScheduleDeployConfig struct { L1CancunTimeOffset *hexutil.Uint64 `json:"l1CancunTimeOffset,omitempty"` // When Prague activates. Relative to L1 genesis. L1PragueTimeOffset *hexutil.Uint64 `json:"l1PragueTimeOffset,omitempty"` + // When Osaka activates. Relative to L1 genesis. + L1OsakaTimeOffset *hexutil.Uint64 `json:"l1OsakaTimeOffset,omitempty"` + // When BPO1 activates. Relative to L1 genesis. + L1BPO1TimeOffset *hexutil.Uint64 `json:"l1BPO1TimeOffset,omitempty"` + // When BPO2 activates. Relative to L1 genesis. + L1BPO2TimeOffset *hexutil.Uint64 `json:"l1BPO2TimeOffset,omitempty"` + // When BPO3 activates. Relative to L1 genesis. + L1BPO3TimeOffset *hexutil.Uint64 `json:"l1BPO3TimeOffset,omitempty"` + // When BPO4 activates. Relative to L1 genesis. + L1BPO4TimeOffset *hexutil.Uint64 `json:"l1BPO4TimeOffset,omitempty"` + // Blob schedule config. + L1BlobScheduleConfig *params.BlobScheduleConfig `json:"l1BlobScheduleConfig,omitempty"` } var _ ConfigChecker = (*UpgradeScheduleDeployConfig)(nil) diff --git a/op-chain-ops/genesis/genesis.go b/op-chain-ops/genesis/genesis.go index 416c9e6d2a4..306f70df2dd 100644 --- a/op-chain-ops/genesis/genesis.go +++ b/op-chain-ops/genesis/genesis.go @@ -139,6 +139,12 @@ func NewL1Genesis(config *DeployConfig) (*core.Genesis, error) { DevL1DeployConfig: config.DevL1DeployConfig, L1ChainID: eth.ChainIDFromUInt64(config.L1ChainID), L1PragueTimeOffset: (*uint64)(config.L1PragueTimeOffset), + L1OsakaTimeOffset: (*uint64)(config.L1OsakaTimeOffset), + L1BPO1TimeOffset: (*uint64)(config.L1BPO1TimeOffset), + L1BPO2TimeOffset: (*uint64)(config.L1BPO2TimeOffset), + L1BPO3TimeOffset: (*uint64)(config.L1BPO3TimeOffset), + L1BPO4TimeOffset: (*uint64)(config.L1BPO4TimeOffset), + BlobScheduleConfig: config.L1BlobScheduleConfig, }) } @@ -148,6 +154,18 @@ type DevL1DeployConfigMinimal struct { L1ChainID eth.ChainID // When Prague activates. Relative to L1 genesis. L1PragueTimeOffset *uint64 + // When Osaka activates. Relative to L1 genesis. + L1OsakaTimeOffset *uint64 + // When BPO1 activates. Relative to L1 genesis. + L1BPO1TimeOffset *uint64 + // When BPO2 activates. Relative to L1 genesis. + L1BPO2TimeOffset *uint64 + // When BPO3 activates. Relative to L1 genesis. + L1BPO3TimeOffset *uint64 + // When BPO4 activates. Relative to L1 genesis. + L1BPO4TimeOffset *uint64 + // Blob schedule config. + BlobScheduleConfig *params.BlobScheduleConfig } // NewL1GenesisMinimal creates a L1 dev genesis template. @@ -203,6 +221,29 @@ func NewL1GenesisMinimal(config *DevL1DeployConfigMinimal) (*core.Genesis, error pragueTime := uint64(timestamp) + uint64(*config.L1PragueTimeOffset) chainConfig.PragueTime = &pragueTime } + if config.L1OsakaTimeOffset != nil { + osakaTime := uint64(timestamp) + uint64(*config.L1OsakaTimeOffset) + chainConfig.OsakaTime = &osakaTime + } + if config.L1BPO1TimeOffset != nil { + bpo1Time := uint64(timestamp) + uint64(*config.L1BPO1TimeOffset) + chainConfig.BPO1Time = &bpo1Time + } + if config.L1BPO2TimeOffset != nil { + bpo2Time := uint64(timestamp) + uint64(*config.L1BPO2TimeOffset) + chainConfig.BPO2Time = &bpo2Time + } + if config.L1BPO3TimeOffset != nil { + bpo3Time := uint64(timestamp) + uint64(*config.L1BPO3TimeOffset) + chainConfig.BPO3Time = &bpo3Time + } + if config.L1BPO4TimeOffset != nil { + bpo4Time := uint64(timestamp) + uint64(*config.L1BPO4TimeOffset) + chainConfig.BPO4Time = &bpo4Time + } + if config.BlobScheduleConfig != nil { + chainConfig.BlobScheduleConfig = config.BlobScheduleConfig + } // Note: excess-blob-gas, blob-gas-used, withdrawals-hash, requests-hash are set to reasonable defaults for L1 by the ToBlock() function return &core.Genesis{ Config: &chainConfig, diff --git a/op-e2e/actions/helpers/l1_miner.go b/op-e2e/actions/helpers/l1_miner.go index 38697daf40e..18503f81634 100644 --- a/op-e2e/actions/helpers/l1_miner.go +++ b/op-e2e/actions/helpers/l1_miner.go @@ -304,6 +304,17 @@ func (s *L1Miner) ActEmptyBlock(t Testing) *types.Block { return s.ActL1EndBlock(t) } +func (s *L1Miner) ActBuildToOsaka(t Testing) *types.Block { + t.Helper() + require.NotNil(t, s.l1Cfg.Config.OsakaTime, "cannot activate OsakaTime when it is not scheduled") + h := s.L1Chain().CurrentHeader() + for h.Time < *s.l1Cfg.Config.OsakaTime { + h = s.ActEmptyBlock(t).Header() + } + require.True(t, s.l1Cfg.Config.IsOsaka(h.Number, h.Time), "Osaka not active at block", h.Number) + return s.L1Chain().GetBlockByHash(h.Hash()) +} + func (s *L1Miner) Close() error { return s.L1Replica.Close() } diff --git a/op-e2e/actions/helpers/l2_sequencer.go b/op-e2e/actions/helpers/l2_sequencer.go index 4cdc60e10bc..14da99941b4 100644 --- a/op-e2e/actions/helpers/l2_sequencer.go +++ b/op-e2e/actions/helpers/l2_sequencer.go @@ -162,9 +162,9 @@ func (s *L2Sequencer) ActL2ForceAdvanceL1Origin(t Testing) { s.mockL1OriginSelector.originOverride = nextOrigin } -// ActBuildToL1Head builds empty blocks until (incl.) the L1 head becomes the L2 origin +// ActBuildToL1Head builds empty blocks until (incl.) the L1 head becomes the L1 origin of the L2 head func (s *L2Sequencer) ActBuildToL1Head(t Testing) { - for s.engine.UnsafeL2Head().L1Origin.Number < s.syncStatus.L1Head().Number { + for s.L2Unsafe().L1Origin.Number < s.syncStatus.L1Head().Number { s.ActL2PipelineFull(t) s.ActL2EmptyBlock(t) } diff --git a/op-e2e/actions/proofs/l1_blob_parameter_forks_test.go b/op-e2e/actions/proofs/l1_blob_parameter_forks_test.go new file mode 100644 index 00000000000..a1b4685c2ee --- /dev/null +++ b/op-e2e/actions/proofs/l1_blob_parameter_forks_test.go @@ -0,0 +1,141 @@ +package proofs_test + +import ( + "math/big" + "testing" + + batcherFlags "github.com/ethereum-optimism/optimism/op-batcher/flags" + "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" + actionsHelpers "github.com/ethereum-optimism/optimism/op-e2e/actions/helpers" + "github.com/ethereum-optimism/optimism/op-e2e/actions/proofs/helpers" + legacybindings "github.com/ethereum-optimism/optimism/op-e2e/bindings" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/predeploys" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus/misc/eip4844" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" +) + +// Test_ProgramAction_BlobParameterForks tests the blob base fee calculation for different forks. +func Test_ProgramAction_BlobParameterForks(gt *testing.T) { + runBlobParameterForksTest := func(gt *testing.T, testCfg *helpers.TestCfg[any]) { + t := actionsHelpers.NewDefaultTesting(gt) + + // Create test environment with Fusaka activation + env := helpers.NewL2FaultProofEnv(t, testCfg, helpers.NewTestParams(), + helpers.NewBatcherCfg( + func(c *actionsHelpers.BatcherCfg) { + c.DataAvailabilityType = batcherFlags.BlobsType + }, + ), + func(dp *genesis.DeployConfig) { + dp.L1CancunTimeOffset = ptr(hexutil.Uint64(0)) + dp.L1PragueTimeOffset = ptr(hexutil.Uint64(12)) + dp.L1OsakaTimeOffset = ptr(hexutil.Uint64(24)) + dp.L1BPO1TimeOffset = ptr(hexutil.Uint64(36)) + dp.L1BPO2TimeOffset = ptr(hexutil.Uint64(48)) + dp.L1BPO3TimeOffset = ptr(hexutil.Uint64(60)) + dp.L1BPO4TimeOffset = ptr(hexutil.Uint64(72)) + dp.L1BlobScheduleConfig = ¶ms.BlobScheduleConfig{ + Cancun: params.DefaultCancunBlobConfig, + Osaka: params.DefaultOsakaBlobConfig, + Prague: params.DefaultPragueBlobConfig, + BPO1: params.DefaultBPO1BlobConfig, + BPO2: params.DefaultBPO2BlobConfig, + BPO3: params.DefaultBPO3BlobConfig, + BPO4: params.DefaultBPO4BlobConfig, + } + dp.L1GenesisBlockExcessBlobGas = ptr(hexutil.Uint64(1e8)) // Jack up the blob market so we can test the blob fee calculation + }, + ) + + miner, sequencer := env.Miner, env.Sequencer + + // Bind to L1Block contract on L2 + l1BlockContract, err := legacybindings.NewL1Block(predeploys.L1BlockAddr, env.Engine.EthClient()) + require.NoError(t, err) + + atBlockWithHash := func(hash common.Hash) *bind.CallOpts { + return &bind.CallOpts{ + BlockHash: hash, + } + } + + // requireConsistentBlobBaseFeeForFork requires the blob base fee to be consistent between + // the L1 Origin block (computed using the excess blob gas and l1 chain config) + // and the L1 Block contract on L2 (accessed with a contract method call), for a given fork predicate. + requireConsistentBlobBaseFeeForFork := func(t actionsHelpers.StatefulTesting, l2Block eth.L2BlockRef, expectActive bool, label string, isActive func(num *big.Int, time uint64) bool) { + bbfL2, err := l1BlockContract.BlobBaseFee(atBlockWithHash(l2Block.Hash)) + require.NoError(t, err) + + l1Origin := miner.L1Chain().GetHeaderByHash(l2Block.L1Origin.Hash) + if expectActive { + require.True(t, isActive(l1Origin.Number, l1Origin.Time), "%s not active at l1 origin %d, time %d", label, l1Origin.Number, l1Origin.Time) + } else { + require.False(t, isActive(l1Origin.Number, l1Origin.Time), "%s should not be active at l1 origin %d, time %d", label, l1Origin.Number, l1Origin.Time) + } + bbfL1 := eip4844.CalcBlobFee(env.Sd.L1Cfg.Config, l1Origin) + + require.True(t, bbfL2.Cmp(bbfL1) == 0, + "%s: blob base fee does not match, bbfL2=%d, bbfL1=%d, l1BlockNum=%d, l2BlockNum=%d", label, bbfL2, bbfL1, l1Origin.Number, l2Block.Number) + + require.True(t, bbfL2.Cmp(big.NewInt(1)) > 0, + "%s: blob base fee is unrealistically low and doesn't exercise the blob fee calculation", label) + } + + // buildL1ToTime advances L1 with empty blocks until the given fork time. + buildL1ToTime := func(t actionsHelpers.StatefulTesting, forkTime *uint64) { + require.NotNil(t, forkTime, "fork time must be configured") + h := miner.L1Chain().CurrentHeader() + for h.Time < *forkTime { + h = miner.ActEmptyBlock(t).Header() + } + } + + // Iterate through all forks and assert pre/post activation blob fees match expectations + cfg := env.Sd.L1Cfg.Config + forks := []struct { + label string + forkTime *uint64 + isActive func(num *big.Int, time uint64) bool + }{ + {"Prague", cfg.PragueTime, func(num *big.Int, time uint64) bool { return cfg.IsPrague(num, time) }}, + {"Osaka", cfg.OsakaTime, func(num *big.Int, time uint64) bool { return cfg.IsOsaka(num, time) }}, + {"BPO1", cfg.BPO1Time, func(num *big.Int, time uint64) bool { return cfg.IsBPO1(num, time) }}, + {"BPO2", cfg.BPO2Time, func(num *big.Int, time uint64) bool { return cfg.IsBPO2(num, time) }}, + {"BPO3", cfg.BPO3Time, func(num *big.Int, time uint64) bool { return cfg.IsBPO3(num, time) }}, + {"BPO4", cfg.BPO4Time, func(num *big.Int, time uint64) bool { return cfg.IsBPO4(num, time) }}, + } + for _, f := range forks { + // Advance L1 to fork activation + buildL1ToTime(t, f.forkTime) + + // Build an empty L2 block which still has a pre-fork L1 origin, and check blob fee + sequencer.ActL2EmptyBlock(t) + l2Block := sequencer.SyncStatus().UnsafeL2 + requireConsistentBlobBaseFeeForFork(t, l2Block, false, f.label, f.isActive) + + // Advance L2 chain until L1 origin is at/after the fork activation + sequencer.ActL1HeadSignal(t) + sequencer.ActBuildToL1HeadUnsafe(t) + + l2Block = sequencer.L2Unsafe() + require.Greater(t, l2Block.Number, uint64(1)) + requireConsistentBlobBaseFeeForFork(t, l2Block, true, f.label, f.isActive) + } + + // Final sync + env.BatchMineAndSync(t) + + // Run fault proof program + safeL2Head := sequencer.L2Safe() + env.RunFaultProofProgramFromGenesis(t, safeL2Head.Number, testCfg.CheckResult, testCfg.InputParams...) + } + + matrix := helpers.NewMatrix[any]() + matrix.AddDefaultTestCases(nil, helpers.NewForkMatrix(helpers.LatestFork), runBlobParameterForksTest) + matrix.Run(gt) +} diff --git a/op-e2e/actions/proofs/l1_prague_fork_test.go b/op-e2e/actions/proofs/l1_prague_fork_test.go index 532ff8ec834..f200fb3c1b1 100644 --- a/op-e2e/actions/proofs/l1_prague_fork_test.go +++ b/op-e2e/actions/proofs/l1_prague_fork_test.go @@ -7,12 +7,8 @@ import ( "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" actionsHelpers "github.com/ethereum-optimism/optimism/op-e2e/actions/helpers" "github.com/ethereum-optimism/optimism/op-e2e/actions/proofs/helpers" - legacybindings "github.com/ethereum-optimism/optimism/op-e2e/bindings" "github.com/ethereum-optimism/optimism/op-service/eth" - "github.com/ethereum-optimism/optimism/op-service/predeploys" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core/types" "github.com/holiman/uint256" "github.com/stretchr/testify/require" @@ -44,10 +40,7 @@ func Test_ProgramAction_PragueForkAfterGenesis(gt *testing.T) { }, ) - miner, batcher, verifier, sequencer, engine := env.Miner, env.Batcher, env.Sequencer, env.Sequencer, env.Engine - - l1Block, err := legacybindings.NewL1Block(predeploys.L1BlockAddr, engine.EthClient()) - require.NoError(t, err) + miner, batcher, verifier, sequencer, _ := env.Miner, env.Batcher, env.Sequencer, env.Sequencer, env.Engine // utils checkVerifierDerivedToL1Head := func(t actionsHelpers.StatefulTesting) { @@ -89,15 +82,6 @@ func Test_ProgramAction_PragueForkAfterGenesis(gt *testing.T) { checkVerifierDerivedToL1Head(t) } - checkL1BlockBlobBaseFee := func(t actionsHelpers.StatefulTesting, l2Block eth.L2BlockRef) { - l1BlockID := l2Block.L1Origin - l1BlockHeader := miner.L1Chain().GetHeaderByHash(l1BlockID.Hash) - expectedBbf := eip4844.CalcBlobFee(env.Sd.L1Cfg.Config, l1BlockHeader) - bbf, err := l1Block.BlobBaseFee(&bind.CallOpts{BlockHash: l2Block.Hash}) - require.NoError(t, err, "failed to get blob base fee") - require.Equal(t, expectedBbf.Uint64(), bbf.Uint64(), "l1Block blob base fee does not match expectation, l1BlockNum %d, l2BlockNum %d", l1BlockID.Number, l2Block.Number) - } - requireSafeHeadProgression := func(t actionsHelpers.StatefulTesting, safeL2Before, safeL2After eth.L2BlockRef, batchedWithSetCodeTx bool) { if batchedWithSetCodeTx { require.Equal(t, safeL2Before, safeL2After, "safe head should not have changed (SetCode / type 4 batcher tx ignored)") @@ -142,11 +126,10 @@ func Test_ProgramAction_PragueForkAfterGenesis(gt *testing.T) { // Cache safe head before verifier sync safeL2Initial := verifier.SyncStatus().SafeL2 - // Build an empty L2 block which has a pre-prague L1 origin, and check the blob fee is correct + // Build an empty L2 block which has a pre-prague L1 origin sequencer.ActL2EmptyBlock(t) l1OriginHeader := miner.L1Chain().GetHeaderByHash(verifier.SyncStatus().UnsafeL2.L1Origin.Hash) requirePragueStatusOnL1(false, l1OriginHeader) - checkL1BlockBlobBaseFee(t, verifier.SyncStatus().UnsafeL2) // Build L2 unsafe chain and batch it to L1 using either DynamicFee or // EIP-7702 SetCode txs @@ -163,10 +146,9 @@ func Test_ProgramAction_PragueForkAfterGenesis(gt *testing.T) { sequencer.ActBuildToL1Head(t) // Advance L2 chain until L1 origin has Prague active - // Check that the l1 origin is now a Prague block, and that the blob fee is correct + // Check that the l1 origin is now a Prague block l1Origin := miner.L1Chain().GetHeaderByNumber(verifier.SyncStatus().UnsafeL2.L1Origin.Number) requirePragueStatusOnL1(true, l1Origin) - checkL1BlockBlobBaseFee(t, verifier.SyncStatus().UnsafeL2) // Batch and sync again buildUnsafeL2AndSubmit(testCfg.Custom.useSetCodeTx)