diff --git a/op-e2e/actions/helpers/l2_batcher.go b/op-e2e/actions/helpers/l2_batcher.go index 3e67166d92f6e..833572e69714c 100644 --- a/op-e2e/actions/helpers/l2_batcher.go +++ b/op-e2e/actions/helpers/l2_batcher.go @@ -528,3 +528,49 @@ func (s *L2Batcher) ActSubmitAllMultiBlobs(t Testing, numBlobs int) { s.ActL2ChannelClose(t) s.ActL2BatchSubmitMultiBlob(t, numBlobs) } + +// ActSubmitSetCodeTx submits a SetCodeTx to the batch inbox. This models a malicious +// batcher and is only used to tests the derivation pipeline follows spec and ignores +// the SetCodeTx. +func (s *L2Batcher) ActSubmitSetCodeTx(t Testing) { + chainId := *uint256.MustFromBig(s.rollupCfg.L1ChainID) + + nonce, err := s.l1.PendingNonceAt(t.Ctx(), s.BatcherAddr) + require.NoError(t, err, "need batcher nonce") + + tx, err := PrepareSignedSetCodeTx(chainId, s.l2BatcherCfg.BatcherKey, s.l1Signer, nonce, s.rollupCfg.BatchInboxAddress, s.ReadNextOutputFrame(t)) + require.NoError(t, err, "need to sign tx") + + t.Log("submitting EIP 7702 Set Code Batcher Transaction...") + err = s.l1.SendTransaction(t.Ctx(), tx) + require.NoError(t, err, "need to send tx") + s.LastSubmitted = tx +} + +func PrepareSignedSetCodeTx(chainId uint256.Int, privateKey *ecdsa.PrivateKey, signer types.Signer, nonce uint64, to common.Address, data []byte) (*types.Transaction, error) { + + setCodeAuthorization := types.SetCodeAuthorization{ + ChainID: chainId, + Address: common.HexToAddress("0xab"), // arbitrary nonzero address + Nonce: nonce, + } + + signedAuth, err := types.SignSetCode(privateKey, setCodeAuthorization) + if err != nil { + return nil, err + } + + txData := &types.SetCodeTx{ + ChainID: &chainId, + Nonce: nonce, + To: to, + Value: uint256.NewInt(0), + Data: data, + AccessList: types.AccessList{}, + AuthList: []types.SetCodeAuthorization{signedAuth}, + Gas: 1_000_000, + GasFeeCap: uint256.NewInt(1_000_000_000), + } + + return types.SignNewTx(privateKey, signer, txData) +} diff --git a/op-e2e/actions/proofs/helpers/matrix.go b/op-e2e/actions/proofs/helpers/matrix.go index c679f03584163..44612f7c708b7 100644 --- a/op-e2e/actions/proofs/helpers/matrix.go +++ b/op-e2e/actions/proofs/helpers/matrix.go @@ -81,15 +81,24 @@ func (ts *TestMatrix[cfg]) AddDefaultTestCases( testCfg cfg, forkMatrix ForkMatrix, runTest RunTest[cfg], +) *TestMatrix[cfg] { + return ts.AddDefaultTestCasesWithName("", testCfg, forkMatrix, runTest) +} + +func (ts *TestMatrix[cfg]) AddDefaultTestCasesWithName( + name string, + testCfg cfg, + forkMatrix ForkMatrix, + runTest RunTest[cfg], ) *TestMatrix[cfg] { return ts.AddTestCase( - "HonestClaim", + "HonestClaim-"+name, testCfg, forkMatrix, runTest, ExpectNoError(), ).AddTestCase( - "JunkClaim", + "JunkClaim-"+name, testCfg, forkMatrix, runTest, diff --git a/op-e2e/actions/proofs/l1_prague_fork_test.go b/op-e2e/actions/proofs/l1_prague_fork_test.go new file mode 100644 index 0000000000000..1ec028a69bd19 --- /dev/null +++ b/op-e2e/actions/proofs/l1_prague_fork_test.go @@ -0,0 +1,186 @@ +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" + 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" +) + +func TestPragueForkAfterGenesis(gt *testing.T) { + type testCase struct { + name string + useSetCodeTx bool + } + + dynamiceFeeCase := testCase{ + name: "dynamicFeeTx", useSetCodeTx: false, + } + setCodeCase := testCase{ + name: "setCodeTx", useSetCodeTx: true, + } + + runL1PragueTest := func(gt *testing.T, testCfg *helpers.TestCfg[testCase]) { + t := actionsHelpers.NewDefaultTesting(gt) + env := helpers.NewL2FaultProofEnv(t, testCfg, helpers.NewTestParams(), + helpers.NewBatcherCfg( + func(c *actionsHelpers.BatcherCfg) { + c.DataAvailabilityType = batcherFlags.CalldataType + }, + ), + func(dp *genesis.DeployConfig) { + 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) + + // utils + checkVerifierDerivedToL1Head := func(t actionsHelpers.StatefulTesting) { + l1Head := miner.L1Chain().CurrentBlock() + currentL1 := verifier.SyncStatus().CurrentL1 + require.Equal(t, l1Head.Number.Int64(), int64(currentL1.Number), "verifier should derive up to and including the L1 head") + require.Equal(t, l1Head.Hash(), currentL1.Hash, "verifier should derive up to and including the L1 head") + } + + buildUnsafeL2AndSubmit := func(useSetCode bool) { + sequencer.ActL1HeadSignal(t) + sequencer.ActBuildToL1Head(t) + + miner.ActL1StartBlock(12)(t) + if useSetCode { + batcher.ActBufferAll(t) + batcher.ActL2ChannelClose(t) + batcher.ActSubmitSetCodeTx(t) + } else { + batcher.ActSubmitAll(t) + } + miner.ActL1IncludeTx(batcher.BatcherAddr)(t) + miner.ActL1EndBlock(t) + } + + requirePragueStatusOnL1 := func(active bool, block *types.Header) { + if active { + require.True(t, env.Sd.L1Cfg.Config.IsPrague(block.Number, block.Time), "Prague should be active at block", block.Number.Uint64()) + require.NotNil(t, block.RequestsHash, "Prague header requests hash should be non-nil") + } else { + require.False(t, env.Sd.L1Cfg.Config.IsPrague(block.Number, block.Time), "Prague should not be active yet at block", block.Number.Uint64()) + require.Nil(t, block.RequestsHash, "Prague header requests hash should be nil") + } + } + + syncVerifierAndCheck := func(t actionsHelpers.StatefulTesting) { + verifier.ActL1HeadSignal(t) + verifier.ActL2PipelineFull(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) + } + + 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)") + require.Equal(t, safeL2Before.L1Origin.Number, safeL2After.Number, "l1 origin of l2 safe should not have changed (SetCode / type 4 batcher tx ignored)") + } else { + require.Greater(t, safeL2After.Number, safeL2Before.Number, "safe head should have progressed (DynamicFee / type 2 batcher tx derived from)") + require.Equal(t, verifier.SyncStatus().UnsafeL2.Number, safeL2After.Number, "safe head should equal unsafe head (DynamicFee / type 2 batcher tx derived from)") + require.Greater(t, safeL2After.L1Origin.Number, safeL2Before.L1Origin.Number, "l1 origin of l2 safe should have progressed (DynamicFee / type 2 batcher tx tx derived from)") + } + } + + // Check initially Prague is not activated + requirePragueStatusOnL1(false, miner.L1Chain().CurrentBlock()) + + // Start op-nodes + sequencer.ActL2PipelineFull(t) + verifier.ActL2PipelineFull(t) + + // Build L1 blocks, crossing the fork boundary + miner.ActEmptyBlock(t) // block 1 + miner.ActEmptyBlock(t) // Prague activates here (block 2) + + // Here's a block with a type 4 deposit transaction, sent to the OptimismPortal + miner.ActL1StartBlock(12)(t) // block 3 + tx, err := actionsHelpers.PrepareSignedSetCodeTx( + *uint256.MustFromBig(env.Sd.L1Cfg.Config.ChainID), + env.Dp.Secrets.Alice, + env.Alice.L1.Signer(), + env.Alice.L1.PendingNonce(t), // nonce + env.Sd.DeploymentsL1.OptimismPortalProxy, + []byte{}) + require.NoError(t, err, "failed to prepare set code tx") + err = miner.EthClient().SendTransaction(t.Ctx(), tx) + require.NoError(t, err, "failed to send set code tx") + miner.ActL1IncludeTx(env.Alice.Address())(t) + miner.ActL1EndBlock(t) + + // Check that Prague is active on L1 + requirePragueStatusOnL1(true, miner.L1Chain().CurrentBlock()) + + // 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 + 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 + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md + buildUnsafeL2AndSubmit(testCfg.Custom.useSetCodeTx) + + // Check verifier derived from Prague L1 blocks + syncVerifierAndCheck(t) + + // Check safe head did or did not change, + // depending on tx type used by batcher: + safeL2AfterFirstBatch := verifier.SyncStatus().SafeL2 + requireSafeHeadProgression(t, safeL2Initial, safeL2AfterFirstBatch, testCfg.Custom.useSetCodeTx) + + 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 + 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) + syncVerifierAndCheck(t) + safeL2AfterSecondBatch := verifier.SyncStatus().SafeL2 + requireSafeHeadProgression(t, safeL2AfterFirstBatch, safeL2AfterSecondBatch, testCfg.Custom.useSetCodeTx) + + env.RunFaultProofProgram(t, safeL2AfterSecondBatch.Number, testCfg.CheckResult, testCfg.InputParams...) + } + + matrix := helpers.NewMatrix[testCase]() + defer matrix.Run(gt) + matrix. + AddDefaultTestCasesWithName(dynamiceFeeCase.name, dynamiceFeeCase, helpers.NewForkMatrix(helpers.Holocene, helpers.LatestFork), runL1PragueTest). + AddDefaultTestCasesWithName(setCodeCase.name, setCodeCase, helpers.NewForkMatrix(helpers.Holocene, helpers.LatestFork), runL1PragueTest) +}