From e9ba6ac07d3af2bc42bcb5d412282df21523cee0 Mon Sep 17 00:00:00 2001 From: clabby Date: Thu, 12 Sep 2024 21:12:25 -0400 Subject: [PATCH] feat: Add garbage frame tests for `op-program` (#11896) * feat: Add channel timeout tests for `op-program` * typo * assert error * fix comment * feat: Add garbage frame tests for `op-program` * assert error * fix comment --- op-e2e/actions/garbage_channel_out.go | 27 +++- op-e2e/actions/l2_batcher.go | 68 +++------ op-e2e/actions/l2_batcher_test.go | 4 +- op-e2e/actions/proofs/garbage_channel_test.go | 144 ++++++++++++++++++ 4 files changed, 188 insertions(+), 55 deletions(-) create mode 100644 op-e2e/actions/proofs/garbage_channel_test.go diff --git a/op-e2e/actions/garbage_channel_out.go b/op-e2e/actions/garbage_channel_out.go index a24cca7c2c03..d5373755f8fe 100644 --- a/op-e2e/actions/garbage_channel_out.go +++ b/op-e2e/actions/garbage_channel_out.go @@ -36,10 +36,29 @@ var GarbageKinds = []GarbageKind{ MALFORM_RLP, } +func (gk GarbageKind) String() string { + switch gk { + case STRIP_VERSION: + return "STRIP_VERSION" + case RANDOM: + return "RANDOM" + case TRUNCATE_END: + return "TRUNCATE_END" + case DIRTY_APPEND: + return "DIRTY_APPEND" + case INVALID_COMPRESSION: + return "INVALID_COMPRESSION" + case MALFORM_RLP: + return "MALFORM_RLP" + default: + return "UNKNOWN" + } +} + // GarbageChannelCfg is the configuration for a `GarbageChannelOut` type GarbageChannelCfg struct { - useInvalidCompression bool - malformRLP bool + UseInvalidCompression bool + MalformRLP bool } // Writer is the interface shared between `zlib.Writer` and `gzip.Writer` @@ -109,7 +128,7 @@ func NewGarbageChannelOut(cfg *GarbageChannelCfg) (*GarbageChannelOut, error) { // Optionally use zlib or gzip compression var compress Writer - if cfg.useInvalidCompression { + if cfg.UseInvalidCompression { compress, err = gzip.NewWriterLevel(&c.buf, gzip.BestCompression) } else { compress, err = zlib.NewWriterLevel(&c.buf, zlib.BestCompression) @@ -152,7 +171,7 @@ func (co *GarbageChannelOut) AddBlock(rollupCfg *rollup.Config, block *types.Blo if err := rlp.Encode(&buf, batch); err != nil { return err } - if co.cfg.malformRLP { + if co.cfg.MalformRLP { // Malform the RLP by incrementing the length prefix by 1. bufBytes := buf.Bytes() bufBytes[0] += 1 diff --git a/op-e2e/actions/l2_batcher.go b/op-e2e/actions/l2_batcher.go index 42fe7c8b2bcd..75d46f3418ca 100644 --- a/op-e2e/actions/l2_batcher.go +++ b/op-e2e/actions/l2_batcher.go @@ -230,12 +230,11 @@ func (s *L2Batcher) ActL2ChannelClose(t Testing) { require.NoError(t, s.l2ChannelOut.Close(), "must close channel before submitting it") } -// ActL2BatchSubmit constructs a batch tx from previous buffered L2 blocks, and submits it to L1 -func (s *L2Batcher) ActL2BatchSubmit(t Testing, txOpts ...func(tx *types.DynamicFeeTx)) { +func (s *L2Batcher) ReadNextOutputFrame(t Testing) []byte { // Don't run this action if there's no data to submit if s.l2ChannelOut == nil { t.InvalidAction("need to buffer data first, cannot batch submit with empty buffer") - return + return nil } // Collect the output frame data := new(bytes.Buffer) @@ -249,7 +248,15 @@ func (s *L2Batcher) ActL2BatchSubmit(t Testing, txOpts ...func(tx *types.Dynamic t.Fatalf("failed to output channel data to frame: %v", err) } - payload := data.Bytes() + return data.Bytes() +} + +// ActL2BatchSubmit constructs a batch tx from previous buffered L2 blocks, and submits it to L1 +func (s *L2Batcher) ActL2BatchSubmit(t Testing, txOpts ...func(tx *types.DynamicFeeTx)) { + s.ActL2BatchSubmitRaw(t, s.ReadNextOutputFrame(t), txOpts...) +} + +func (s *L2Batcher) ActL2BatchSubmitRaw(t Testing, payload []byte, txOpts ...func(tx *types.DynamicFeeTx)) { if s.l2BatcherCfg.UseAltDA { comm, err := s.l2BatcherCfg.AltDA.SetInput(t.Ctx(), payload) require.NoError(t, err, "failed to set input for altda") @@ -401,27 +408,14 @@ func (s *L2Batcher) ActL2BatchSubmitMultiBlob(t Testing, numBlobs int) { // batch inbox. This *should* cause the batch inbox to reject the blocks // encoded within the frame, even if the blocks themselves are valid. func (s *L2Batcher) ActL2BatchSubmitGarbage(t Testing, kind GarbageKind) { - // Don't run this action if there's no data to submit - if s.l2ChannelOut == nil { - t.InvalidAction("need to buffer data first, cannot batch submit with empty buffer") - return - } - - // Collect the output frame - data := new(bytes.Buffer) - data.WriteByte(derive.DerivationVersion0) - - // subtract one, to account for the version byte - if _, err := s.l2ChannelOut.OutputFrame(data, s.l2BatcherCfg.MaxL1TxSize-1); err == io.EOF { - s.l2ChannelOut = nil - s.l2Submitting = false - } else if err != nil { - s.l2Submitting = false - t.Fatalf("failed to output channel data to frame: %v", err) - } - - outputFrame := data.Bytes() + outputFrame := s.ReadNextOutputFrame(t) + s.ActL2BatchSubmitGarbageRaw(t, outputFrame, kind) +} +// ActL2BatchSubmitGarbageRaw constructs a malformed channel frame from `outputFrame` and submits it to the +// batch inbox. This *should* cause the batch inbox to reject the blocks +// encoded within the frame, even if the blocks themselves are valid. +func (s *L2Batcher) ActL2BatchSubmitGarbageRaw(t Testing, outputFrame []byte, kind GarbageKind) { // Malform the output frame switch kind { // Strip the derivation version byte from the output frame @@ -453,31 +447,7 @@ func (s *L2Batcher) ActL2BatchSubmitGarbage(t Testing, kind GarbageKind) { t.Fatalf("Unexpected garbage kind: %v", kind) } - nonce, err := s.l1.PendingNonceAt(t.Ctx(), s.batcherAddr) - require.NoError(t, err, "need batcher nonce") - - gasTipCap := big.NewInt(2 * params.GWei) - pendingHeader, err := s.l1.HeaderByNumber(t.Ctx(), big.NewInt(-1)) - require.NoError(t, err, "need l1 pending header for gas price estimation") - gasFeeCap := new(big.Int).Add(gasTipCap, new(big.Int).Mul(pendingHeader.BaseFee, big.NewInt(2))) - - rawTx := &types.DynamicFeeTx{ - ChainID: s.rollupCfg.L1ChainID, - Nonce: nonce, - To: &s.rollupCfg.BatchInboxAddress, - GasTipCap: gasTipCap, - GasFeeCap: gasFeeCap, - Data: outputFrame, - } - gas, err := core.IntrinsicGas(rawTx.Data, nil, false, true, true, false) - require.NoError(t, err, "need to compute intrinsic gas") - rawTx.Gas = gas - - tx, err := types.SignNewTx(s.l2BatcherCfg.BatcherKey, s.l1Signer, rawTx) - require.NoError(t, err, "need to sign tx") - - err = s.l1.SendTransaction(t.Ctx(), tx) - require.NoError(t, err, "need to send tx") + s.ActL2BatchSubmitRaw(t, outputFrame) } func (s *L2Batcher) ActBufferAll(t Testing) { diff --git a/op-e2e/actions/l2_batcher_test.go b/op-e2e/actions/l2_batcher_test.go index 88dd393a38df..0b9140020b0b 100644 --- a/op-e2e/actions/l2_batcher_test.go +++ b/op-e2e/actions/l2_batcher_test.go @@ -317,8 +317,8 @@ func GarbageBatch(gt *testing.T, deltaTimeOffset *hexutil.Uint64) { // If the garbage kind is `INVALID_COMPRESSION` or `MALFORM_RLP`, use the `actions` packages // modified `ChannelOut`. batcherCfg.GarbageCfg = &GarbageChannelCfg{ - useInvalidCompression: garbageKind == INVALID_COMPRESSION, - malformRLP: garbageKind == MALFORM_RLP, + UseInvalidCompression: garbageKind == INVALID_COMPRESSION, + MalformRLP: garbageKind == MALFORM_RLP, } } diff --git a/op-e2e/actions/proofs/garbage_channel_test.go b/op-e2e/actions/proofs/garbage_channel_test.go new file mode 100644 index 000000000000..c8b44dd45f1c --- /dev/null +++ b/op-e2e/actions/proofs/garbage_channel_test.go @@ -0,0 +1,144 @@ +package proofs + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-e2e/actions" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils" + "github.com/ethereum-optimism/optimism/op-program/client/claim" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stretchr/testify/require" +) + +// garbageKinds is a list of garbage kinds to test. We don't use `INVALID_COMPRESSION` and `MALFORM_RLP` because +// they submit malformed frames always, and this test models a valid channel with a single invalid frame in the +// middle. +var garbageKinds = []actions.GarbageKind{ + actions.STRIP_VERSION, + actions.RANDOM, + actions.TRUNCATE_END, + actions.DIRTY_APPEND, +} + +// Run a test that submits garbage channel data in the middle of a channel. +// +// channel format ([]Frame): +// [f[0 - correct] f_x[1 - bad frame] f[1 - correct]] +func runGarbageChannelTest(gt *testing.T, garbageKind actions.GarbageKind, checkResult func(gt *testing.T, err error), inputParams ...FixtureInputParam) { + t := actions.NewDefaultTesting(gt) + tp := NewTestParams(func(tp *e2eutils.TestParams) { + // Set the channel timeout to 10 blocks, 12x lower than the sequencing window. + tp.ChannelTimeout = 10 + }) + dp := NewDeployParams(t, func(dp *e2eutils.DeployParams) { + genesisBlock := hexutil.Uint64(0) + + // Enable Cancun on L1 & Granite on L2 at genesis + dp.DeployConfig.L1CancunTimeOffset = &genesisBlock + dp.DeployConfig.L2GenesisRegolithTimeOffset = &genesisBlock + dp.DeployConfig.L2GenesisCanyonTimeOffset = &genesisBlock + dp.DeployConfig.L2GenesisDeltaTimeOffset = &genesisBlock + dp.DeployConfig.L2GenesisEcotoneTimeOffset = &genesisBlock + dp.DeployConfig.L2GenesisFjordTimeOffset = &genesisBlock + dp.DeployConfig.L2GenesisGraniteTimeOffset = &genesisBlock + }) + bCfg := NewBatcherCfg() + env := NewL2FaultProofEnv(t, tp, dp, bCfg) + + includeBatchTx := func(env *L2FaultProofEnv) { + // Instruct the batcher to submit the first channel frame to L1, and include the transaction. + env.miner.ActL1StartBlock(12)(t) + env.miner.ActL1IncludeTxByHash(env.batcher.LastSubmitted.Hash())(t) + env.miner.ActL1EndBlock(t) + + // Finalize the block with the first channel frame on L1. + env.miner.ActL1SafeNext(t) + env.miner.ActL1FinalizeNext(t) + + // Instruct the sequencer to derive the L2 chain from the data on L1 that the batcher just posted. + env.sequencer.ActL1HeadSignal(t) + env.sequencer.ActL2PipelineFull(t) + } + + const NumL2Blocks = 10 + + // Build NumL2Blocks empty blocks on L2 + for i := 0; i < NumL2Blocks; i++ { + env.sequencer.ActL2StartBlock(t) + env.sequencer.ActL2EndBlock(t) + } + + // Buffer the first half of L2 blocks in the batcher, and submit it. + for i := 0; i < NumL2Blocks/2; i++ { + env.batcher.ActL2BatchBuffer(t) + } + env.batcher.ActL2BatchSubmit(t) + + // Include the batcher transaction. + includeBatchTx(env) + + // Ensure that the safe head has not advanced - the channel is incomplete. + l2SafeHead := env.engine.L2Chain().CurrentSafeBlock() + require.Equal(t, uint64(0), l2SafeHead.Number.Uint64()) + + // Buffer the second half of L2 blocks in the batcher. + for i := 0; i < NumL2Blocks/2; i++ { + env.batcher.ActL2BatchBuffer(t) + } + env.batcher.ActL2ChannelClose(t) + expectedSecondFrame := env.batcher.ReadNextOutputFrame(t) + + // Submit a garbage frame, modified from the expected second frame. + env.batcher.ActL2BatchSubmitGarbageRaw(t, expectedSecondFrame, garbageKind) + // Include the garbage second frame tx + includeBatchTx(env) + + // Ensure that the safe head has not advanced - the channel is incomplete. + l2SafeHead = env.engine.L2Chain().CurrentSafeBlock() + require.Equal(t, uint64(0), l2SafeHead.Number.Uint64()) + + // Submit the correct second frame. + env.batcher.ActL2BatchSubmitRaw(t, expectedSecondFrame) + // Include the corract second frame tx. + includeBatchTx(env) + + // Ensure that the safe head has advanced - the channel is complete. + l2SafeHead = env.engine.L2Chain().CurrentSafeBlock() + require.Equal(t, uint64(NumL2Blocks), l2SafeHead.Number.Uint64()) + + // Run the FPP on L2 block # NumL2Blocks. + err := env.RunFaultProofProgram(t, gt, NumL2Blocks, inputParams...) + checkResult(gt, err) +} + +func Test_ProgramAction_GarbageChannel_HonestClaim_Granite(gt *testing.T) { + for _, garbageKind := range garbageKinds { + gt.Run(garbageKind.String(), func(t *testing.T) { + runGarbageChannelTest( + t, + garbageKind, + func(gt *testing.T, err error) { + require.NoError(gt, err, "fault proof program should not have failed") + }, + ) + }) + } +} + +func Test_ProgramAction_GarbageChannel_JunkClaim_Granite(gt *testing.T) { + for _, garbageKind := range garbageKinds { + gt.Run(garbageKind.String(), func(t *testing.T) { + runGarbageChannelTest( + t, + garbageKind, + func(gt *testing.T, err error) { + require.ErrorIs(gt, err, claim.ErrClaimNotValid, "fault proof program should have failed") + }, + func(f *FixtureInputs) { + f.L2Claim = common.HexToHash("0xdeadbeef") + }, + ) + }) + } +}