Skip to content

Commit

Permalink
feat: Add garbage frame tests for op-program (#11896)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
clabby authored Sep 13, 2024
1 parent deb9bac commit e9ba6ac
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 55 deletions.
27 changes: 23 additions & 4 deletions op-e2e/actions/garbage_channel_out.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
68 changes: 19 additions & 49 deletions op-e2e/actions/l2_batcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions op-e2e/actions/l2_batcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down
144 changes: 144 additions & 0 deletions op-e2e/actions/proofs/garbage_channel_test.go
Original file line number Diff line number Diff line change
@@ -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")
},
)
})
}
}

0 comments on commit e9ba6ac

Please sign in to comment.