diff --git a/op-e2e/actions/proofs/channel_timeout_test.go b/op-e2e/actions/proofs/channel_timeout_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2f3fb555b5d3644a1463e547caeabd0b535b623b --- /dev/null +++ b/op-e2e/actions/proofs/channel_timeout_test.go @@ -0,0 +1,142 @@ +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" +) + +// Run a test that exercises the channel timeout functionality in `op-program`. +// +// Steps: +// 1. Build `NumL2Blocks` empty blocks on L2. +// 2. Buffer the first half of the L2 blocks in the batcher, and submit the frame data. +// 3. Time out the channel by mining `ChannelTimeout + 1` empty blocks on L1. +// 4. Submit the channel frame data across 2 transactions. +// 5. Instruct the sequencer to derive the L2 chain. +// 6. Run the FPP on the safe head. +func runChannelTimeoutTest(gt *testing.T, 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) + + 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) + + // 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) + + // 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()) + + // Time out the channel by mining `ChannelTimeout + 1` empty blocks on L1. + for i := uint64(0); i < tp.ChannelTimeout+1; i++ { + env.miner.ActEmptyBlock(t) + env.miner.ActL1SafeNext(t) + env.miner.ActL1FinalizeNext(t) + } + + // Instruct the sequencer to derive the L2 chain - the channel should now be timed out. + env.sequencer.ActL1HeadSignal(t) + env.sequencer.ActL2PipelineFull(t) + + // Ensure the safe head has still not advanced. + l2SafeHead = env.engine.L2Chain().CurrentSafeBlock() + require.Equal(t, uint64(0), l2SafeHead.Number.Uint64()) + + // Instruct the batcher to submit the blocks to L1 in a new channel, + // submitted across 2 transactions. + for i := 0; i < 2; i++ { + // Buffer half of the L2 chain's blocks. + for j := 0; j < NumL2Blocks/2; j++ { + env.batcher.ActL2BatchBuffer(t) + } + + // Close the channel on the second iteration. + if i == 1 { + env.batcher.ActL2ChannelClose(t) + } + + env.batcher.ActL2BatchSubmit(t) + env.miner.ActL1StartBlock(12)(t) + env.miner.ActL1IncludeTxByHash(env.batcher.LastSubmitted.Hash())(t) + env.miner.ActL1EndBlock(t) + + // Finalize the block with the frame data on L1. + env.miner.ActL1SafeNext(t) + env.miner.ActL1FinalizeNext(t) + } + + // Instruct the sequencer to derive the L2 chain. + env.sequencer.ActL1HeadSignal(t) + env.sequencer.ActL2PipelineFull(t) + + // Ensure the safe head has still advanced to L2 block # NumL2Blocks. + l2SafeHead = env.engine.L2Chain().CurrentSafeBlock() + require.EqualValues(t, NumL2Blocks, l2SafeHead.Number.Uint64()) + + // Run the FPP on L2 block # NumL2Blocks/2. + err := env.RunFaultProofProgram(t, gt, NumL2Blocks/2, inputParams...) + checkResult(gt, err) +} + +func Test_ProgramAction_ChannelTimeout_HonestClaim_Granite(gt *testing.T) { + runChannelTimeoutTest(gt, func(gt *testing.T, err error) { + require.NoError(gt, err, "fault proof program should have succeeded") + }) +} + +func Test_ProgramAction_ChannelTimeout_JunkClaim_Granite(gt *testing.T) { + runChannelTimeoutTest( + gt, + 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") + }, + ) +}