From d5911445c28bb066208374c60c53ff390c7a91b4 Mon Sep 17 00:00:00 2001 From: Dense Date: Fri, 19 Sep 2025 02:14:02 +0800 Subject: [PATCH 01/14] add Fusaka activation test --- op-e2e/actions/proofs/l1_fusaka_fork_test.go | 62 ++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 op-e2e/actions/proofs/l1_fusaka_fork_test.go diff --git a/op-e2e/actions/proofs/l1_fusaka_fork_test.go b/op-e2e/actions/proofs/l1_fusaka_fork_test.go new file mode 100644 index 00000000000..4ec8742d46e --- /dev/null +++ b/op-e2e/actions/proofs/l1_fusaka_fork_test.go @@ -0,0 +1,62 @@ +package proofs_test + +import ( + "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" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +func Test_ProgramAction_FusakaForkAfterGenesis(gt *testing.T) { + runL1FusakaTest := 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.CalldataType + }, + ), + func(dp *genesis.DeployConfig) { + // TODO: When Fusaka is implemented, change this to dp.L1FusakaTimeOffset + dp.L1PragueTimeOffset = ptr(hexutil.Uint64(24)) // Activate at second L1 block + }, + ) + + miner, batcher, verifier, sequencer := env.Miner, env.Batcher, env.Sequencer, env.Sequencer + + // Start nodes + sequencer.ActL2PipelineFull(t) + verifier.ActL2PipelineFull(t) + + // Build L1 blocks to trigger Fusaka activation + miner.ActEmptyBlock(t) // block 1 + miner.ActEmptyBlock(t) // block 2 - Fusaka activates here + + // TODO: When Fusaka is implemented, add proper fork validation: + // require.True(t, env.Sd.L1Cfg.Config.IsFusaka(block.Number, block.Time)) + + // Build some L2 blocks and batch them + sequencer.ActL1HeadSignal(t) + sequencer.ActBuildToL1Head(t) + + batcher.ActSubmitAll(t) + miner.ActL1IncludeTx(batcher.BatcherAddr)(t) + + // Sync and verify + verifier.ActL1HeadSignal(t) + verifier.ActL2PipelineFull(t) + + // Run fault proof program + safeL2Head := verifier.SyncStatus().SafeL2 + env.RunFaultProofProgramFromGenesis(t, safeL2Head.Number, testCfg.CheckResult, testCfg.InputParams...) + } + + matrix := helpers.NewMatrix[any]() + defer matrix.Run(gt) + matrix.AddDefaultTestCases(nil, helpers.NewForkMatrix(helpers.Holocene, helpers.LatestFork), runL1FusakaTest) +} From 20865f25eb5ca48ba681dc4a6f1795cb68fa562c Mon Sep 17 00:00:00 2001 From: Dense Date: Tue, 23 Sep 2025 23:48:43 +0800 Subject: [PATCH 02/14] updated the test to add L1 blob base fee assertions --- op-e2e/actions/proofs/l1_fusaka_fork_test.go | 44 ++++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/op-e2e/actions/proofs/l1_fusaka_fork_test.go b/op-e2e/actions/proofs/l1_fusaka_fork_test.go index 4ec8742d46e..05e955a2e17 100644 --- a/op-e2e/actions/proofs/l1_fusaka_fork_test.go +++ b/op-e2e/actions/proofs/l1_fusaka_fork_test.go @@ -7,7 +7,13 @@ 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/stretchr/testify/require" ) func Test_ProgramAction_FusakaForkAfterGenesis(gt *testing.T) { @@ -27,7 +33,22 @@ func Test_ProgramAction_FusakaForkAfterGenesis(gt *testing.T) { }, ) - miner, batcher, verifier, sequencer := env.Miner, env.Batcher, env.Sequencer, env.Sequencer + 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) + + // Helper function to check L1 blob base fee consistency between L1 and L2 + checkL1BlockBlobBaseFee := func(t actionsHelpers.StatefulTesting, l2Block eth.L2BlockRef) { + l1BlockID := l2Block.L1Origin + l1BlockHeader := miner.L1Chain().GetHeaderByHash(l1BlockID.Hash) + expectedBbf := eth.CalcBlobFeeDefault(l1BlockHeader) + upstreamExpectedBbf := eip4844.CalcBlobFee(env.Sd.L1Cfg.Config, l1BlockHeader) + require.Equal(t, expectedBbf.Uint64(), upstreamExpectedBbf.Uint64(), "expected blob base fee should match upstream calculation") + 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) + } // Start nodes sequencer.ActL2PipelineFull(t) @@ -40,14 +61,29 @@ func Test_ProgramAction_FusakaForkAfterGenesis(gt *testing.T) { // TODO: When Fusaka is implemented, add proper fork validation: // require.True(t, env.Sd.L1Cfg.Config.IsFusaka(block.Number, block.Time)) - // Build some L2 blocks and batch them + // Build an empty L2 block which has a pre-Fusaka L1 origin, and check the blob fee is correct + sequencer.ActL2EmptyBlock(t) + // TODO: When Fusaka is implemented, add fork status validation for pre-Fusaka L1 origin + checkL1BlockBlobBaseFee(t, verifier.SyncStatus().UnsafeL2) + + // Build L2 unsafe chain and batch it to L1 sequencer.ActL1HeadSignal(t) sequencer.ActBuildToL1Head(t) - batcher.ActSubmitAll(t) miner.ActL1IncludeTx(batcher.BatcherAddr)(t) - // Sync and verify + // Sync verifier + verifier.ActL1HeadSignal(t) + verifier.ActL2PipelineFull(t) + + // Advance L2 chain until L1 origin has Fusaka active + sequencer.ActBuildToL1Head(t) + + // Check that the L1 origin is now a Fusaka block, and that the blob fee is correct + // TODO: When Fusaka is implemented, add fork status validation for Fusaka-active L1 origin + checkL1BlockBlobBaseFee(t, verifier.SyncStatus().UnsafeL2) + + // Final sync verifier.ActL1HeadSignal(t) verifier.ActL2PipelineFull(t) From 8f4f259caad47c25c0c923671e95fb2579aff148 Mon Sep 17 00:00:00 2001 From: geoknee Date: Wed, 24 Sep 2025 17:59:18 +0100 Subject: [PATCH 03/14] fixes --- op-chain-ops/genesis/config.go | 2 ++ op-e2e/actions/proofs/l1_fusaka_fork_test.go | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index f3e2052cfa3..7ae6eca9441 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -377,6 +377,8 @@ type UpgradeScheduleDeployConfig struct { L1CancunTimeOffset *hexutil.Uint64 `json:"l1CancunTimeOffset,omitempty"` // When Prague activates. Relative to L1 genesis. L1PragueTimeOffset *hexutil.Uint64 `json:"l1PragueTimeOffset,omitempty"` + // When Fusaka activates. Relative to L1 genesis. + L1FusakaTimeOffset *hexutil.Uint64 `json:"l1FusakaTimeOffset,omitempty"` } var _ ConfigChecker = (*UpgradeScheduleDeployConfig)(nil) diff --git a/op-e2e/actions/proofs/l1_fusaka_fork_test.go b/op-e2e/actions/proofs/l1_fusaka_fork_test.go index 05e955a2e17..224f8eb344e 100644 --- a/op-e2e/actions/proofs/l1_fusaka_fork_test.go +++ b/op-e2e/actions/proofs/l1_fusaka_fork_test.go @@ -28,8 +28,7 @@ func Test_ProgramAction_FusakaForkAfterGenesis(gt *testing.T) { }, ), func(dp *genesis.DeployConfig) { - // TODO: When Fusaka is implemented, change this to dp.L1FusakaTimeOffset - dp.L1PragueTimeOffset = ptr(hexutil.Uint64(24)) // Activate at second L1 block + dp.L1FusakaTimeOffset = ptr(hexutil.Uint64(24)) }, ) @@ -58,8 +57,8 @@ func Test_ProgramAction_FusakaForkAfterGenesis(gt *testing.T) { miner.ActEmptyBlock(t) // block 1 miner.ActEmptyBlock(t) // block 2 - Fusaka activates here - // TODO: When Fusaka is implemented, add proper fork validation: - // require.True(t, env.Sd.L1Cfg.Config.IsFusaka(block.Number, block.Time)) + block := miner.L1Chain().CurrentBlock() + require.True(t, env.Sd.L1Cfg.Config.IsOsaka(block.Number, block.Time)) // Build an empty L2 block which has a pre-Fusaka L1 origin, and check the blob fee is correct sequencer.ActL2EmptyBlock(t) From ad87c6f45ab339d4929d579edc14bfcd7170c332 Mon Sep 17 00:00:00 2001 From: geoknee Date: Wed, 24 Sep 2025 18:30:58 +0100 Subject: [PATCH 04/14] wire up Fusaka on L1 and improve test --- op-chain-ops/genesis/config.go | 2 +- op-chain-ops/genesis/genesis.go | 7 +++ ...aka_fork_test.go => l1_osaka_fork_test.go} | 52 +++++++------------ 3 files changed, 26 insertions(+), 35 deletions(-) rename op-e2e/actions/proofs/{l1_fusaka_fork_test.go => l1_osaka_fork_test.go} (51%) diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index 7ae6eca9441..9df158dc4f0 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -378,7 +378,7 @@ type UpgradeScheduleDeployConfig struct { // When Prague activates. Relative to L1 genesis. L1PragueTimeOffset *hexutil.Uint64 `json:"l1PragueTimeOffset,omitempty"` // When Fusaka activates. Relative to L1 genesis. - L1FusakaTimeOffset *hexutil.Uint64 `json:"l1FusakaTimeOffset,omitempty"` + L1OsakaTimeOffset *hexutil.Uint64 `json:"l1FusakaTimeOffset,omitempty"` } var _ ConfigChecker = (*UpgradeScheduleDeployConfig)(nil) diff --git a/op-chain-ops/genesis/genesis.go b/op-chain-ops/genesis/genesis.go index 416c9e6d2a4..fb7c17db5db 100644 --- a/op-chain-ops/genesis/genesis.go +++ b/op-chain-ops/genesis/genesis.go @@ -139,6 +139,7 @@ func NewL1Genesis(config *DeployConfig) (*core.Genesis, error) { DevL1DeployConfig: config.DevL1DeployConfig, L1ChainID: eth.ChainIDFromUInt64(config.L1ChainID), L1PragueTimeOffset: (*uint64)(config.L1PragueTimeOffset), + L1OsakaTimeOffset: (*uint64)(config.L1OsakaTimeOffset), }) } @@ -148,6 +149,8 @@ type DevL1DeployConfigMinimal struct { L1ChainID eth.ChainID // When Prague activates. Relative to L1 genesis. L1PragueTimeOffset *uint64 + // When Fusaka activates. Relative to L1 genesis. + L1OsakaTimeOffset *uint64 } // NewL1GenesisMinimal creates a L1 dev genesis template. @@ -203,6 +206,10 @@ 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 + } // 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/proofs/l1_fusaka_fork_test.go b/op-e2e/actions/proofs/l1_osaka_fork_test.go similarity index 51% rename from op-e2e/actions/proofs/l1_fusaka_fork_test.go rename to op-e2e/actions/proofs/l1_osaka_fork_test.go index 224f8eb344e..f1aad1248d7 100644 --- a/op-e2e/actions/proofs/l1_fusaka_fork_test.go +++ b/op-e2e/actions/proofs/l1_osaka_fork_test.go @@ -7,16 +7,11 @@ 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/stretchr/testify/require" ) -func Test_ProgramAction_FusakaForkAfterGenesis(gt *testing.T) { +func Test_ProgramAction_OsakaForkAfterGenesis(gt *testing.T) { runL1FusakaTest := func(gt *testing.T, testCfg *helpers.TestCfg[any]) { t := actionsHelpers.NewDefaultTesting(gt) @@ -28,26 +23,17 @@ func Test_ProgramAction_FusakaForkAfterGenesis(gt *testing.T) { }, ), func(dp *genesis.DeployConfig) { - dp.L1FusakaTimeOffset = ptr(hexutil.Uint64(24)) + dp.L1OsakaTimeOffset = ptr(hexutil.Uint64(24)) }, ) - 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, _, verifier, sequencer, _ := env.Miner, env.Batcher, env.Sequencer, env.Sequencer, env.Engine // Helper function to check L1 blob base fee consistency between L1 and L2 - checkL1BlockBlobBaseFee := func(t actionsHelpers.StatefulTesting, l2Block eth.L2BlockRef) { - l1BlockID := l2Block.L1Origin - l1BlockHeader := miner.L1Chain().GetHeaderByHash(l1BlockID.Hash) - expectedBbf := eth.CalcBlobFeeDefault(l1BlockHeader) - upstreamExpectedBbf := eip4844.CalcBlobFee(env.Sd.L1Cfg.Config, l1BlockHeader) - require.Equal(t, expectedBbf.Uint64(), upstreamExpectedBbf.Uint64(), "expected blob base fee should match upstream calculation") - 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) - } + // checkL1BlockBlobBaseFee := func(t actionsHelpers.StatefulTesting, l2Block *types.Block, l1BlockID eth.BlockID) { + // // TODO get the blob fee via L1 RPC, and also via L2 RPC if possible. + // // Also query the L1Block contract and check these value are all consistent. + // } // Start nodes sequencer.ActL2PipelineFull(t) @@ -58,33 +44,31 @@ func Test_ProgramAction_FusakaForkAfterGenesis(gt *testing.T) { miner.ActEmptyBlock(t) // block 2 - Fusaka activates here block := miner.L1Chain().CurrentBlock() - require.True(t, env.Sd.L1Cfg.Config.IsOsaka(block.Number, block.Time)) + require.True(t, env.Sd.L1Cfg.Config.IsOsaka(block.Number, block.Time), "Osaka not active") // Build an empty L2 block which has a pre-Fusaka L1 origin, and check the blob fee is correct sequencer.ActL2EmptyBlock(t) - // TODO: When Fusaka is implemented, add fork status validation for pre-Fusaka L1 origin - checkL1BlockBlobBaseFee(t, verifier.SyncStatus().UnsafeL2) // Build L2 unsafe chain and batch it to L1 sequencer.ActL1HeadSignal(t) sequencer.ActBuildToL1Head(t) - batcher.ActSubmitAll(t) - miner.ActL1IncludeTx(batcher.BatcherAddr)(t) - - // Sync verifier - verifier.ActL1HeadSignal(t) - verifier.ActL2PipelineFull(t) + env.BatchMineAndSync(t) // Advance L2 chain until L1 origin has Fusaka active sequencer.ActBuildToL1Head(t) // Check that the L1 origin is now a Fusaka block, and that the blob fee is correct - // TODO: When Fusaka is implemented, add fork status validation for Fusaka-active L1 origin - checkL1BlockBlobBaseFee(t, verifier.SyncStatus().UnsafeL2) + l2Block := verifier.SyncStatus().UnsafeL2 + + l1BlockHeader := miner.L1Chain().GetHeaderByHash(l2Block.L1Origin.Hash) + require.True(t, env.Sd.L1Cfg.Config.IsOsaka(l1BlockHeader.Number, l1BlockHeader.Time), "Osaka not active at l1 origin") + + // checkL1BlockBlobBaseFee(t, fullL2Block, l2Block.L1Origin) // TODO // Final sync - verifier.ActL1HeadSignal(t) - verifier.ActL2PipelineFull(t) + sequencer.ActL1HeadSignal(t) + sequencer.ActBuildToL1Head(t) + env.BatchMineAndSync(t) // Run fault proof program safeL2Head := verifier.SyncStatus().SafeL2 From 6349fd0fa52f5bba30aad7006d7d2da7e2fb329d Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 25 Sep 2025 14:56:28 +0100 Subject: [PATCH 05/14] give test teeth --- op-e2e/actions/helpers/l1_miner.go | 11 +++ op-e2e/actions/helpers/l2_sequencer.go | 4 +- op-e2e/actions/proofs/l1_osaka_fork_test.go | 84 +++++++++++++-------- 3 files changed, 66 insertions(+), 33 deletions(-) diff --git a/op-e2e/actions/helpers/l1_miner.go b/op-e2e/actions/helpers/l1_miner.go index b97416cf51c..ee15951f755 100644 --- a/op-e2e/actions/helpers/l1_miner.go +++ b/op-e2e/actions/helpers/l1_miner.go @@ -266,6 +266,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 23be8eaf8c9..5e5613c77b2 100644 --- a/op-e2e/actions/helpers/l2_sequencer.go +++ b/op-e2e/actions/helpers/l2_sequencer.go @@ -160,9 +160,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_osaka_fork_test.go b/op-e2e/actions/proofs/l1_osaka_fork_test.go index f1aad1248d7..07eac8aaf9a 100644 --- a/op-e2e/actions/proofs/l1_osaka_fork_test.go +++ b/op-e2e/actions/proofs/l1_osaka_fork_test.go @@ -1,13 +1,20 @@ 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/stretchr/testify/require" ) @@ -19,63 +26,78 @@ func Test_ProgramAction_OsakaForkAfterGenesis(gt *testing.T) { env := helpers.NewL2FaultProofEnv(t, testCfg, helpers.NewTestParams(), helpers.NewBatcherCfg( func(c *actionsHelpers.BatcherCfg) { - c.DataAvailabilityType = batcherFlags.CalldataType + c.DataAvailabilityType = batcherFlags.BlobsType }, ), func(dp *genesis.DeployConfig) { dp.L1OsakaTimeOffset = ptr(hexutil.Uint64(24)) + // TODO add the BPO forks + dp.L1GenesisBlockExcessBlobGas = ptr(hexutil.Uint64(1e8)) // Jack up the blob market so we can test the blob fee calculation }, ) - miner, _, verifier, sequencer, _ := env.Miner, env.Batcher, env.Sequencer, env.Sequencer, env.Engine + miner, _, _, sequencer, _ := env.Miner, env.Batcher, env.Sequencer, env.Sequencer, env.Engine - // Helper function to check L1 blob base fee consistency between L1 and L2 - // checkL1BlockBlobBaseFee := func(t actionsHelpers.StatefulTesting, l2Block *types.Block, l1BlockID eth.BlockID) { - // // TODO get the blob fee via L1 RPC, and also via L2 RPC if possible. - // // Also query the L1Block contract and check these value are all consistent. - // } + // Bind to L1Block contract on L2 + l1BlockContract, err := legacybindings.NewL1Block(predeploys.L1BlockAddr, env.Engine.EthClient()) + require.NoError(t, err) - // Start nodes - sequencer.ActL2PipelineFull(t) - verifier.ActL2PipelineFull(t) + atBlockWithHash := func(hash common.Hash) *bind.CallOpts { + return &bind.CallOpts{ + BlockHash: hash, + } + } - // Build L1 blocks to trigger Fusaka activation - miner.ActEmptyBlock(t) // block 1 - miner.ActEmptyBlock(t) // block 2 - Fusaka activates here + // requireConsistentBlobBaseFee 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 (acessed with a contract method call). + requireConsistentBlobBaseFee := func(t actionsHelpers.StatefulTesting, l2Block eth.L2BlockRef, expectL1OriginToBeFusaka bool) { + bbfL2, err := l1BlockContract.BlobBaseFee(atBlockWithHash(l2Block.Hash)) + require.NoError(t, err) + + l1Origin := miner.L1Chain().GetHeaderByHash(l2Block.L1Origin.Hash) + if expectL1OriginToBeFusaka { + require.True(t, env.Sd.L1Cfg.Config.IsOsaka(l1Origin.Number, l1Origin.Time), "Osaka not active at l1 origin %d, time %d", l1Origin.Number, l1Origin.Time) + } else { + require.False(t, env.Sd.L1Cfg.Config.IsOsaka(l1Origin.Number, l1Origin.Time), "Osaka should not be active at l1 origin %d, time %d", l1Origin.Number, l1Origin.Time) + } + bbfL1 := eip4844.CalcBlobFee(env.Sd.L1Cfg.Config, l1Origin) + + require.True(t, bbfL2.Cmp(bbfL1) == 0, + "blob base fee does not match, bbfL2=%d, bbfL1=%d, l1BlockNum=%d, l2BlockNum=%d", bbfL2, bbfL1, l1Origin.Number, l2Block.Number) - block := miner.L1Chain().CurrentBlock() - require.True(t, env.Sd.L1Cfg.Config.IsOsaka(block.Number, block.Time), "Osaka not active") + require.True(t, bbfL2.Cmp(big.NewInt(1)) > 0, + "blob base fee is unrealistically low and doesn't exercise the blob fee calculation") + } + + // Build L1 blocks to trigger Fusaka activation + l1Block := miner.ActBuildToOsaka(t) + require.Equal(t, uint64(2), l1Block.Number().Uint64()) // Build an empty L2 block which has a pre-Fusaka L1 origin, and check the blob fee is correct sequencer.ActL2EmptyBlock(t) + l2Block := sequencer.SyncStatus().UnsafeL2 + require.Equal(t, uint64(1), l2Block.Number) + requireConsistentBlobBaseFee(t, l2Block, false) - // Build L2 unsafe chain and batch it to L1 + // Advance L2 chain until L1 origin has Fusaka activ sequencer.ActL1HeadSignal(t) - sequencer.ActBuildToL1Head(t) - env.BatchMineAndSync(t) - - // Advance L2 chain until L1 origin has Fusaka active - sequencer.ActBuildToL1Head(t) + sequencer.ActBuildToL1HeadUnsafe(t) // Check that the L1 origin is now a Fusaka block, and that the blob fee is correct - l2Block := verifier.SyncStatus().UnsafeL2 - - l1BlockHeader := miner.L1Chain().GetHeaderByHash(l2Block.L1Origin.Hash) - require.True(t, env.Sd.L1Cfg.Config.IsOsaka(l1BlockHeader.Number, l1BlockHeader.Time), "Osaka not active at l1 origin") - - // checkL1BlockBlobBaseFee(t, fullL2Block, l2Block.L1Origin) // TODO + l2Block = sequencer.L2Unsafe() + require.Greater(t, l2Block.Number, uint64(1)) + requireConsistentBlobBaseFee(t, l2Block, true) // Final sync - sequencer.ActL1HeadSignal(t) - sequencer.ActBuildToL1Head(t) env.BatchMineAndSync(t) // Run fault proof program - safeL2Head := verifier.SyncStatus().SafeL2 + safeL2Head := sequencer.L2Safe() env.RunFaultProofProgramFromGenesis(t, safeL2Head.Number, testCfg.CheckResult, testCfg.InputParams...) } matrix := helpers.NewMatrix[any]() defer matrix.Run(gt) - matrix.AddDefaultTestCases(nil, helpers.NewForkMatrix(helpers.Holocene, helpers.LatestFork), runL1FusakaTest) + matrix.AddDefaultTestCases(nil, helpers.NewForkMatrix(helpers.LatestFork), runL1FusakaTest) } From 4eece3233ff6793244065c90fdc093415144b182 Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 25 Sep 2025 15:34:23 +0100 Subject: [PATCH 06/14] improve prague fork test --- op-e2e/actions/proofs/l1_prague_fork_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/op-e2e/actions/proofs/l1_prague_fork_test.go b/op-e2e/actions/proofs/l1_prague_fork_test.go index d9fcaf47aa5..f4436d4acf3 100644 --- a/op-e2e/actions/proofs/l1_prague_fork_test.go +++ b/op-e2e/actions/proofs/l1_prague_fork_test.go @@ -40,7 +40,9 @@ func Test_ProgramAction_PragueForkAfterGenesis(gt *testing.T) { }, ), func(dp *genesis.DeployConfig) { - dp.L1PragueTimeOffset = ptr(hexutil.Uint64(24)) // Activate at second l1 block + dp.L1PragueTimeOffset = ptr(hexutil.Uint64(24)) // Activate at second l1 block + dp.L1GenesisBlockExcessBlobGas = ptr(hexutil.Uint64(1e8)) // Jack up the blob market so we can test the blob fee calculation + }, ) @@ -98,6 +100,8 @@ func Test_ProgramAction_PragueForkAfterGenesis(gt *testing.T) { 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) + require.Greater(t, bbf.Uint64(), uint64(1), + "blob base fee is unrealistically low and doesn't exercise the blob fee calculation") } requireSafeHeadProgression := func(t actionsHelpers.StatefulTesting, safeL2Before, safeL2After eth.L2BlockRef, batchedWithSetCodeTx bool) { From 93271d98e36a1d58196a9195148a3f5ab7c50d9c Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 25 Sep 2025 15:40:01 +0100 Subject: [PATCH 07/14] activate prague at genesis and osaka after --- op-e2e/actions/proofs/l1_osaka_fork_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/op-e2e/actions/proofs/l1_osaka_fork_test.go b/op-e2e/actions/proofs/l1_osaka_fork_test.go index 07eac8aaf9a..eb310d305fa 100644 --- a/op-e2e/actions/proofs/l1_osaka_fork_test.go +++ b/op-e2e/actions/proofs/l1_osaka_fork_test.go @@ -30,13 +30,14 @@ func Test_ProgramAction_OsakaForkAfterGenesis(gt *testing.T) { }, ), func(dp *genesis.DeployConfig) { + dp.L1PragueTimeOffset = ptr(hexutil.Uint64(0)) dp.L1OsakaTimeOffset = ptr(hexutil.Uint64(24)) // TODO add the BPO forks dp.L1GenesisBlockExcessBlobGas = ptr(hexutil.Uint64(1e8)) // Jack up the blob market so we can test the blob fee calculation }, ) - miner, _, _, sequencer, _ := env.Miner, env.Batcher, env.Sequencer, env.Sequencer, env.Engine + miner, sequencer := env.Miner, env.Sequencer // Bind to L1Block contract on L2 l1BlockContract, err := legacybindings.NewL1Block(predeploys.L1BlockAddr, env.Engine.EthClient()) @@ -71,6 +72,8 @@ func Test_ProgramAction_OsakaForkAfterGenesis(gt *testing.T) { } // Build L1 blocks to trigger Fusaka activation + // TODO in the current version of op-geth, the blob parameters don't change between Prague and Osaka. + // So this test is no useful until we can activate different blob parameters. l1Block := miner.ActBuildToOsaka(t) require.Equal(t, uint64(2), l1Block.Number().Uint64()) From ba6d77179d81a2424964f4193b3cc61969694e74 Mon Sep 17 00:00:00 2001 From: geoknee Date: Tue, 30 Sep 2025 12:11:39 +0100 Subject: [PATCH 08/14] updated op-geth to v1.101603.0-synctest.0.0.20250930110811-5eee1eab50e6 pairs with https://github.com/ethereum-optimism/op-geth/pull/690 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e0d618bf953..2d0d61f5d87 100644 --- a/go.mod +++ b/go.mod @@ -303,7 +303,7 @@ require ( lukechampine.com/blake3 v1.3.0 // indirect ) -replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101603.0-rc.1 +replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101603.0-synctest.0.0.20250930110811-5eee1eab50e6 // replace github.com/ethereum/go-ethereum => ../op-geth diff --git a/go.sum b/go.sum index 31b6013fc15..256a5e7898d 100644 --- a/go.sum +++ b/go.sum @@ -230,8 +230,8 @@ github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3 h1:RWHKLhCrQThMfch+QJ1Z8veEq5ZO3DfIhZ7xgRP9WTc= github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3/go.mod h1:QziizLAiF0KqyLdNJYD7O5cpDlaFMNZzlxYNcWsJUxs= -github.com/ethereum-optimism/op-geth v1.101603.0-rc.1 h1:gTAlQu6QwaZyG2hW6fDhLkXeftVNHC5RCl4P0eBXH8U= -github.com/ethereum-optimism/op-geth v1.101603.0-rc.1/go.mod h1:Ct2QjqZ2UKgvvgKLLYzoh/DBicJZB8DXsv45DgEjcco= +github.com/ethereum-optimism/op-geth v1.101603.0-synctest.0.0.20250930110811-5eee1eab50e6 h1:D6FjDhVwyLL9Sf5UEAwmcq5F7jUomx9gRUeJ/60pFWo= +github.com/ethereum-optimism/op-geth v1.101603.0-synctest.0.0.20250930110811-5eee1eab50e6/go.mod h1:Ct2QjqZ2UKgvvgKLLYzoh/DBicJZB8DXsv45DgEjcco= github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20250603144016-9c45ca7d4508 h1:A/3QVFt+Aa9ozpPVXxUTLui8honBjSusAaiCVRbafgs= github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20250603144016-9c45ca7d4508/go.mod h1:NZ816PzLU1TLv1RdAvYAb6KWOj4Zm5aInT0YpDVml2Y= github.com/ethereum/c-kzg-4844/v2 v2.1.0 h1:gQropX9YFBhl3g4HYhwE70zq3IHFRgbbNPw0Shwzf5w= From 7e2df560c5f765e4af75930843248c255d2b3778 Mon Sep 17 00:00:00 2001 From: geoknee Date: Tue, 30 Sep 2025 15:58:24 +0100 Subject: [PATCH 09/14] extend test to cover BPO forks --- op-chain-ops/genesis/config.go | 10 +++ op-chain-ops/genesis/genesis.go | 34 +++++++++ op-e2e/actions/proofs/l1_osaka_fork_test.go | 78 +++++++++++++++++---- 3 files changed, 109 insertions(+), 13 deletions(-) diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index 9df158dc4f0..be2732ba9de 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -379,6 +379,16 @@ type UpgradeScheduleDeployConfig struct { L1PragueTimeOffset *hexutil.Uint64 `json:"l1PragueTimeOffset,omitempty"` // When Fusaka activates. Relative to L1 genesis. L1OsakaTimeOffset *hexutil.Uint64 `json:"l1FusakaTimeOffset,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 fb7c17db5db..6c7659fd7c6 100644 --- a/op-chain-ops/genesis/genesis.go +++ b/op-chain-ops/genesis/genesis.go @@ -140,6 +140,11 @@ func NewL1Genesis(config *DeployConfig) (*core.Genesis, error) { 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, }) } @@ -151,6 +156,16 @@ type DevL1DeployConfigMinimal struct { L1PragueTimeOffset *uint64 // When Fusaka 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. @@ -210,6 +225,25 @@ func NewL1GenesisMinimal(config *DevL1DeployConfigMinimal) (*core.Genesis, error 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/proofs/l1_osaka_fork_test.go b/op-e2e/actions/proofs/l1_osaka_fork_test.go index eb310d305fa..07fb240b8be 100644 --- a/op-e2e/actions/proofs/l1_osaka_fork_test.go +++ b/op-e2e/actions/proofs/l1_osaka_fork_test.go @@ -15,6 +15,7 @@ import ( "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" ) @@ -32,7 +33,19 @@ func Test_ProgramAction_OsakaForkAfterGenesis(gt *testing.T) { func(dp *genesis.DeployConfig) { dp.L1PragueTimeOffset = ptr(hexutil.Uint64(0)) dp.L1OsakaTimeOffset = ptr(hexutil.Uint64(24)) - // TODO add the BPO forks + 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 }, ) @@ -49,31 +62,40 @@ func Test_ProgramAction_OsakaForkAfterGenesis(gt *testing.T) { } } - // requireConsistentBlobBaseFee requires the blob base fee to be consistent between + // 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 (acessed with a contract method call). - requireConsistentBlobBaseFee := func(t actionsHelpers.StatefulTesting, l2Block eth.L2BlockRef, expectL1OriginToBeFusaka bool) { + // 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 expectL1OriginToBeFusaka { - require.True(t, env.Sd.L1Cfg.Config.IsOsaka(l1Origin.Number, l1Origin.Time), "Osaka not active at l1 origin %d, time %d", l1Origin.Number, l1Origin.Time) + 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, env.Sd.L1Cfg.Config.IsOsaka(l1Origin.Number, l1Origin.Time), "Osaka should not be active at l1 origin %d, time %d", l1Origin.Number, l1Origin.Time) + 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, - "blob base fee does not match, bbfL2=%d, bbfL1=%d, l1BlockNum=%d, l2BlockNum=%d", bbfL2, bbfL1, l1Origin.Number, l2Block.Number) + "%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, - "blob base fee is unrealistically low and doesn't exercise the blob fee calculation") + "%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() + } } // Build L1 blocks to trigger Fusaka activation - // TODO in the current version of op-geth, the blob parameters don't change between Prague and Osaka. - // So this test is no useful until we can activate different blob parameters. + // The blob parameters don't change between Prague and Osaka. + // So this test is only useful when we hit the BPO forks below l1Block := miner.ActBuildToOsaka(t) require.Equal(t, uint64(2), l1Block.Number().Uint64()) @@ -81,7 +103,7 @@ func Test_ProgramAction_OsakaForkAfterGenesis(gt *testing.T) { sequencer.ActL2EmptyBlock(t) l2Block := sequencer.SyncStatus().UnsafeL2 require.Equal(t, uint64(1), l2Block.Number) - requireConsistentBlobBaseFee(t, l2Block, false) + requireConsistentBlobBaseFeeForFork(t, l2Block, false, "Osaka", func(num *big.Int, time uint64) bool { return env.Sd.L1Cfg.Config.IsOsaka(num, time) }) // Advance L2 chain until L1 origin has Fusaka activ sequencer.ActL1HeadSignal(t) @@ -90,7 +112,37 @@ func Test_ProgramAction_OsakaForkAfterGenesis(gt *testing.T) { // Check that the L1 origin is now a Fusaka block, and that the blob fee is correct l2Block = sequencer.L2Unsafe() require.Greater(t, l2Block.Number, uint64(1)) - requireConsistentBlobBaseFee(t, l2Block, true) + requireConsistentBlobBaseFeeForFork(t, l2Block, true, "Osaka", func(num *big.Int, time uint64) bool { return env.Sd.L1Cfg.Config.IsOsaka(num, time) }) + + // Now iterate through BPO 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 + }{ + {"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) From 0aa5c1a15d9bdf47a60a99ae602ae47eb57906cd Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 2 Oct 2025 10:54:53 +0100 Subject: [PATCH 10/14] typo fix --- op-chain-ops/genesis/genesis.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/op-chain-ops/genesis/genesis.go b/op-chain-ops/genesis/genesis.go index 6c7659fd7c6..306f70df2dd 100644 --- a/op-chain-ops/genesis/genesis.go +++ b/op-chain-ops/genesis/genesis.go @@ -154,7 +154,7 @@ type DevL1DeployConfigMinimal struct { L1ChainID eth.ChainID // When Prague activates. Relative to L1 genesis. L1PragueTimeOffset *uint64 - // When Fusaka activates. Relative to L1 genesis. + // When Osaka activates. Relative to L1 genesis. L1OsakaTimeOffset *uint64 // When BPO1 activates. Relative to L1 genesis. L1BPO1TimeOffset *uint64 From 417dedfcb05f24cd67c9992db008948b248d7eb4 Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 2 Oct 2025 10:56:09 +0100 Subject: [PATCH 11/14] typo fix --- op-chain-ops/genesis/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index be2732ba9de..c4acf5d16d2 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -377,8 +377,8 @@ type UpgradeScheduleDeployConfig struct { L1CancunTimeOffset *hexutil.Uint64 `json:"l1CancunTimeOffset,omitempty"` // When Prague activates. Relative to L1 genesis. L1PragueTimeOffset *hexutil.Uint64 `json:"l1PragueTimeOffset,omitempty"` - // When Fusaka activates. Relative to L1 genesis. - L1OsakaTimeOffset *hexutil.Uint64 `json:"l1FusakaTimeOffset,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. From cc37affc2167c7333688cae611656941cfeecfd3 Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 2 Oct 2025 11:11:13 +0100 Subject: [PATCH 12/14] dedupe tests --- op-e2e/actions/proofs/l1_osaka_fork_test.go | 35 +++++--------------- op-e2e/actions/proofs/l1_prague_fork_test.go | 31 +++-------------- 2 files changed, 13 insertions(+), 53 deletions(-) diff --git a/op-e2e/actions/proofs/l1_osaka_fork_test.go b/op-e2e/actions/proofs/l1_osaka_fork_test.go index 07fb240b8be..49365f16090 100644 --- a/op-e2e/actions/proofs/l1_osaka_fork_test.go +++ b/op-e2e/actions/proofs/l1_osaka_fork_test.go @@ -19,8 +19,9 @@ import ( "github.com/stretchr/testify/require" ) -func Test_ProgramAction_OsakaForkAfterGenesis(gt *testing.T) { - runL1FusakaTest := func(gt *testing.T, testCfg *helpers.TestCfg[any]) { +// 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 @@ -31,7 +32,8 @@ func Test_ProgramAction_OsakaForkAfterGenesis(gt *testing.T) { }, ), func(dp *genesis.DeployConfig) { - dp.L1PragueTimeOffset = ptr(hexutil.Uint64(0)) + 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)) @@ -93,34 +95,15 @@ func Test_ProgramAction_OsakaForkAfterGenesis(gt *testing.T) { } } - // Build L1 blocks to trigger Fusaka activation - // The blob parameters don't change between Prague and Osaka. - // So this test is only useful when we hit the BPO forks below - l1Block := miner.ActBuildToOsaka(t) - require.Equal(t, uint64(2), l1Block.Number().Uint64()) - - // Build an empty L2 block which has a pre-Fusaka L1 origin, and check the blob fee is correct - sequencer.ActL2EmptyBlock(t) - l2Block := sequencer.SyncStatus().UnsafeL2 - require.Equal(t, uint64(1), l2Block.Number) - requireConsistentBlobBaseFeeForFork(t, l2Block, false, "Osaka", func(num *big.Int, time uint64) bool { return env.Sd.L1Cfg.Config.IsOsaka(num, time) }) - - // Advance L2 chain until L1 origin has Fusaka activ - sequencer.ActL1HeadSignal(t) - sequencer.ActBuildToL1HeadUnsafe(t) - - // Check that the L1 origin is now a Fusaka block, and that the blob fee is correct - l2Block = sequencer.L2Unsafe() - require.Greater(t, l2Block.Number, uint64(1)) - requireConsistentBlobBaseFeeForFork(t, l2Block, true, "Osaka", func(num *big.Int, time uint64) bool { return env.Sd.L1Cfg.Config.IsOsaka(num, time) }) - - // Now iterate through BPO forks and assert pre/post activation blob fees match expectations + // 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) }}, @@ -154,5 +137,5 @@ func Test_ProgramAction_OsakaForkAfterGenesis(gt *testing.T) { matrix := helpers.NewMatrix[any]() defer matrix.Run(gt) - matrix.AddDefaultTestCases(nil, helpers.NewForkMatrix(helpers.LatestFork), runL1FusakaTest) + matrix.AddDefaultTestCases(nil, helpers.NewForkMatrix(helpers.LatestFork), runBlobParameterForksTest) } diff --git a/op-e2e/actions/proofs/l1_prague_fork_test.go b/op-e2e/actions/proofs/l1_prague_fork_test.go index f4436d4acf3..95f7f575205 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" @@ -40,16 +36,12 @@ func Test_ProgramAction_PragueForkAfterGenesis(gt *testing.T) { }, ), func(dp *genesis.DeployConfig) { - dp.L1PragueTimeOffset = ptr(hexutil.Uint64(24)) // Activate at second l1 block - dp.L1GenesisBlockExcessBlobGas = ptr(hexutil.Uint64(1e8)) // Jack up the blob market so we can test the blob fee calculation + dp.L1PragueTimeOffset = ptr(hexutil.Uint64(24)) // Activate at second l1 block }, ) - 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) { @@ -91,19 +83,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 := eth.CalcBlobFeeDefault(l1BlockHeader) - upstreamExpectedBbf := eip4844.CalcBlobFee(env.Sd.L1Cfg.Config, l1BlockHeader) - require.Equal(t, expectedBbf.Uint64(), upstreamExpectedBbf.Uint64(), "expected blob base fee should match upstream calculation") - 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) - require.Greater(t, bbf.Uint64(), uint64(1), - "blob base fee is unrealistically low and doesn't exercise the blob fee calculation") - } - 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)") @@ -148,11 +127,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 @@ -169,10 +147,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) From 39e491a92aa1b04037e1d6e9b7ab4953e14ffe13 Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 2 Oct 2025 11:12:12 +0100 Subject: [PATCH 13/14] rename file --- .../{l1_osaka_fork_test.go => l1_blob_parameter_forks_test.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename op-e2e/actions/proofs/{l1_osaka_fork_test.go => l1_blob_parameter_forks_test.go} (100%) diff --git a/op-e2e/actions/proofs/l1_osaka_fork_test.go b/op-e2e/actions/proofs/l1_blob_parameter_forks_test.go similarity index 100% rename from op-e2e/actions/proofs/l1_osaka_fork_test.go rename to op-e2e/actions/proofs/l1_blob_parameter_forks_test.go From 28a78827d77d980ecec09a1e8fe7fffeee20e7bb Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 2 Oct 2025 13:04:38 +0100 Subject: [PATCH 14/14] respond to review --- op-e2e/actions/proofs/l1_blob_parameter_forks_test.go | 2 +- op-e2e/actions/proofs/l1_prague_fork_test.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/op-e2e/actions/proofs/l1_blob_parameter_forks_test.go b/op-e2e/actions/proofs/l1_blob_parameter_forks_test.go index 49365f16090..a1b4685c2ee 100644 --- a/op-e2e/actions/proofs/l1_blob_parameter_forks_test.go +++ b/op-e2e/actions/proofs/l1_blob_parameter_forks_test.go @@ -136,6 +136,6 @@ func Test_ProgramAction_BlobParameterForks(gt *testing.T) { } matrix := helpers.NewMatrix[any]() - defer matrix.Run(gt) 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 95f7f575205..f200fb3c1b1 100644 --- a/op-e2e/actions/proofs/l1_prague_fork_test.go +++ b/op-e2e/actions/proofs/l1_prague_fork_test.go @@ -37,7 +37,6 @@ func Test_ProgramAction_PragueForkAfterGenesis(gt *testing.T) { ), func(dp *genesis.DeployConfig) { dp.L1PragueTimeOffset = ptr(hexutil.Uint64(24)) // Activate at second l1 block - }, )