From e9ba6ac07d3af2bc42bcb5d412282df21523cee0 Mon Sep 17 00:00:00 2001
From: clabby <ben@clab.by>
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 a24cca7c2..d5373755f 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 42fe7c8b2..75d46f341 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 88dd393a3..0b9140020 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 000000000..c8b44dd45
--- /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")
+				},
+			)
+		})
+	}
+}
-- 
2.23.0