diff --git a/op-e2e/actions/helpers/l1_miner.go b/op-e2e/actions/helpers/l1_miner.go index 5152b7f8b4507..d05f36474488d 100644 --- a/op-e2e/actions/helpers/l1_miner.go +++ b/op-e2e/actions/helpers/l1_miner.go @@ -166,7 +166,7 @@ func (s *L1Miner) ActL1IncludeTxByHash(txHash common.Hash) Action { } } -func (s *L1Miner) IncludeTx(t Testing, tx *types.Transaction) { +func (s *L1Miner) IncludeTx(t Testing, tx *types.Transaction) *types.Receipt { from, err := s.l1Signer.Sender(tx) require.NoError(t, err) s.log.Info("including tx", "nonce", tx.Nonce(), "from", from, "to", tx.To()) @@ -175,7 +175,7 @@ func (s *L1Miner) IncludeTx(t Testing, tx *types.Transaction) { } if tx.Gas() > uint64(*s.L1GasPool) { t.InvalidAction("action takes too much gas: %d, only have %d", tx.Gas(), uint64(*s.L1GasPool)) - return + return nil } s.l1BuildingState.SetTxContext(tx.Hash(), len(s.L1Transactions)) blockCtx := core.NewEVMBlockContext(s.l1BuildingHeader, s.l1Chain, nil, s.l1Cfg.Config, s.l1BuildingState) @@ -196,6 +196,7 @@ func (s *L1Miner) IncludeTx(t Testing, tx *types.Transaction) { } *s.l1BuildingHeader.BlobGasUsed += receipt.BlobGasUsed } + return receipt } func (s *L1Miner) ActL1SetFeeRecipient(coinbase common.Address) { diff --git a/op-e2e/actions/interop/dsl/dsl.go b/op-e2e/actions/interop/dsl/dsl.go index a8cfabad189bb..dd46bff928174 100644 --- a/op-e2e/actions/interop/dsl/dsl.go +++ b/op-e2e/actions/interop/dsl/dsl.go @@ -175,6 +175,12 @@ type SubmitBatchDataOpts struct { SkipCrossSafeUpdate bool } +func WithSkipCrossSafeUpdate() func(*SubmitBatchDataOpts) { + return func(o *SubmitBatchDataOpts) { + o.SkipCrossSafeUpdate = true + } +} + // SubmitBatchData submits batch data to L1 and processes the new L1 blocks, advancing the safe heads. // By default, submits all batch data for all chains. func (d *InteropDSL) SubmitBatchData(optionalArgs ...func(*SubmitBatchDataOpts)) { @@ -250,6 +256,12 @@ type AdvanceL1Opts struct { TxInclusion []helpers.Action } +func WithActIncludeTx(includeTxAction helpers.Action) func(*AdvanceL1Opts) { + return func(o *AdvanceL1Opts) { + o.TxInclusion = append(o.TxInclusion, includeTxAction) + } +} + // AdvanceL1 adds a new L1 block with the specified transactions and ensures it is processed by the specified chains // and the supervisor. func (d *InteropDSL) AdvanceL1(optionalArgs ...func(*AdvanceL1Opts)) { @@ -332,3 +344,17 @@ func (d *InteropDSL) AdvanceSafeHeads(optionalArgs ...func(*AdvanceSafeHeadsOpts }) } } + +// AdvanceL2ToLastBlockOfOrigin advances the chain to the last block of the epoch at the specified L1 origin. +func (d *InteropDSL) AdvanceL2ToLastBlockOfOrigin(chain *Chain, l1OriginHeight uint64) { + const l1BlockTime = uint64(12) + require.Equal(d.t, l1BlockTime%chain.RollupCfg.BlockTime, uint64(0), "L2 block time must be a multiple of L1 block time") + endOfEpoch := (l1BlockTime/chain.RollupCfg.BlockTime)*(l1OriginHeight+1) - 1 + require.LessOrEqual(d.t, chain.Sequencer.L2Unsafe().Number, endOfEpoch, "end of epoch is in the future") + for { + if n := chain.Sequencer.L2Unsafe().Number; n == endOfEpoch { + break + } + d.AddL2Block(chain) + } +} diff --git a/op-e2e/actions/interop/dsl/dsl_user.go b/op-e2e/actions/interop/dsl/dsl_user.go index ec17e8060cca0..57268a6bd4e72 100644 --- a/op-e2e/actions/interop/dsl/dsl_user.go +++ b/op-e2e/actions/interop/dsl/dsl_user.go @@ -18,10 +18,10 @@ type DSLUser struct { keys devkeys.Keys } -func (u *DSLUser) TransactOpts(chain *Chain) (*bind.TransactOpts, common.Address) { - privKey, err := u.keys.Secret(devkeys.ChainUserKeys(chain.ChainID.ToBig())(u.index)) +func (u *DSLUser) TransactOpts(chainID *big.Int) (*bind.TransactOpts, common.Address) { + privKey, err := u.keys.Secret(devkeys.ChainUserKeys(chainID)(u.index)) require.NoError(u.t, err) - opts, err := bind.NewKeyedTransactorWithChainID(privKey, chain.ChainID.ToBig()) + opts, err := bind.NewKeyedTransactorWithChainID(privKey, chainID) require.NoError(u.t, err) opts.GasTipCap = big.NewInt(params.GWei) diff --git a/op-e2e/actions/interop/dsl/emitter.go b/op-e2e/actions/interop/dsl/emitter.go index d840fb74bbfcc..62ffc0c7fe33c 100644 --- a/op-e2e/actions/interop/dsl/emitter.go +++ b/op-e2e/actions/interop/dsl/emitter.go @@ -24,7 +24,7 @@ func NewEmitterContract(t helpers.Testing) *EmitterContract { func (c *EmitterContract) Deploy(user *DSLUser) TransactionCreator { return func(chain *Chain) *GeneratedTransaction { - opts, from := user.TransactOpts(chain) + opts, from := user.TransactOpts(chain.ChainID.ToBig()) emitContract, tx, _, err := emit.DeployEmit(opts, chain.SequencerEngine.EthClient()) require.NoError(c.t, err) c.addressByChain[chain.ChainID] = emitContract @@ -34,7 +34,7 @@ func (c *EmitterContract) Deploy(user *DSLUser) TransactionCreator { func (c *EmitterContract) EmitMessage(user *DSLUser, message string) TransactionCreator { return func(chain *Chain) *GeneratedTransaction { - opts, from := user.TransactOpts(chain) + opts, from := user.TransactOpts(chain.ChainID.ToBig()) address, ok := c.addressByChain[chain.ChainID] require.Truef(c.t, ok, "not deployed on chain %d", chain.ChainID) bindings, err := emit.NewEmitTransactor(address, chain.SequencerEngine.EthClient()) diff --git a/op-e2e/actions/interop/dsl/inbox.go b/op-e2e/actions/interop/dsl/inbox.go index 1ebd221e7594b..17a10596f1ad4 100644 --- a/op-e2e/actions/interop/dsl/inbox.go +++ b/op-e2e/actions/interop/dsl/inbox.go @@ -39,13 +39,12 @@ func WithPayload(payload []byte) func(opts *ExecuteOpts) { } } -func WithPendingMessage(emitter *EmitterContract, chain *Chain, logIndex int, msg string) func(opts *ExecuteOpts) { +func WithPendingMessage(emitter *EmitterContract, chain *Chain, number uint64, logIndex int, msg string) func(opts *ExecuteOpts) { return func(opts *ExecuteOpts) { - head := chain.Sequencer.L2Unsafe() - blockTime := chain.RollupCfg.TimestampForBlock(head.Number) + blockTime := chain.RollupCfg.TimestampForBlock(number) id := inbox.Identifier{ Origin: emitter.Address(chain), - BlockNumber: big.NewInt(int64(head.Number)), + BlockNumber: big.NewInt(int64(number)), LogIndex: big.NewInt(int64(logIndex)), Timestamp: big.NewInt(int64(blockTime)), ChainId: chain.RollupCfg.L2ChainID, @@ -82,7 +81,7 @@ func (i *InboxContract) Execute(user *DSLUser, initTx *GeneratedTransaction, arg } else { payload = initTx.MessagePayload() } - txOpts, from := user.TransactOpts(chain) + txOpts, from := user.TransactOpts(chain.ChainID.ToBig()) contract, err := inbox.NewInbox(predeploys.CrossL2InboxAddr, chain.SequencerEngine.EthClient()) require.NoError(i.t, err) tx, err := contract.ValidateMessage(txOpts, ident, crypto.Keccak256Hash(payload)) diff --git a/op-e2e/actions/interop/dsl/message.go b/op-e2e/actions/interop/dsl/message.go index d564081a36b7e..0edaa0c483357 100644 --- a/op-e2e/actions/interop/dsl/message.go +++ b/op-e2e/actions/interop/dsl/message.go @@ -13,6 +13,7 @@ type Message struct { message string emitter *EmitterContract inbox *InboxContract + l1Miner *helpers.L1Miner initTx *GeneratedTransaction execTx *GeneratedTransaction @@ -25,6 +26,7 @@ func NewMessage(dsl *InteropDSL, chain *Chain, emitter *EmitterContract, message chain: chain, emitter: emitter, inbox: dsl.InboxContract, + l1Miner: dsl.Actors.L1Miner, message: message, } } @@ -36,6 +38,22 @@ func (m *Message) Emit() *Message { return m } +// EmitDeposit emits a message via a user deposit transaction. +func (m *Message) EmitDeposit(l1User *DSLUser) *Message { + emitAction := m.emitter.EmitMessage(m.user, m.message) + m.initTx = emitAction(m.chain) + opts, _ := m.user.TransactOpts(m.chain.ChainID.ToBig()) + m.initTx.IncludeDepositOK(l1User, opts, m.l1Miner) + return m +} + +// ActEmitDeposit returns an action that emits a message via a user deposit transaction. +func (m *Message) ActEmitDeposit(l1User *DSLUser) helpers.Action { + return func(t helpers.Testing) { + m.EmitDeposit(l1User) + } +} + func (m *Message) ExecuteOn(target *Chain, execOpts ...func(*ExecuteOpts)) *Message { require.NotNil(m.t, m.initTx, "message must be emitted before it can be executed") execAction := m.inbox.Execute(m.user, m.initTx, execOpts...) @@ -44,6 +62,17 @@ func (m *Message) ExecuteOn(target *Chain, execOpts ...func(*ExecuteOpts)) *Mess return m } +// ExecutePendingOn executes a message that may not have been emitted yet. +func (m *Message) ExecutePendingOn(target *Chain, pendingMessageBlockNumber uint64, execOpts ...func(*ExecuteOpts)) *Message { + var opts []func(*ExecuteOpts) + opts = append(opts, WithPendingMessage(m.emitter, m.chain, pendingMessageBlockNumber, 0, m.message)) + opts = append(opts, execOpts...) + execAction := m.inbox.Execute(m.user, nil, opts...) + m.execTx = execAction(target) + m.execTx.IncludeOK() + return m +} + func (m *Message) CheckEmitted() { require.NotNil(m.t, m.initTx, "message must be emitted before it can be checked") m.initTx.CheckIncluded() diff --git a/op-e2e/actions/interop/dsl/transactions.go b/op-e2e/actions/interop/dsl/transactions.go index 658f5ee57657e..7cdc2de7f3241 100644 --- a/op-e2e/actions/interop/dsl/transactions.go +++ b/op-e2e/actions/interop/dsl/transactions.go @@ -4,9 +4,11 @@ import ( "math/big" "github.com/ethereum-optimism/optimism/op-e2e/actions/helpers" + "github.com/ethereum-optimism/optimism/op-e2e/bindingspreview" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/interop/contracts/bindings/inbox" stypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/require" @@ -47,6 +49,24 @@ func (m *GeneratedTransaction) IncludeOK() { require.Equal(m.t, types.ReceiptStatusSuccessful, rcpt.Status) } +// IncludeDepositOK includes the GeneratedTransaction via a user deposit transaction. +func (m *GeneratedTransaction) IncludeDepositOK(l1User *DSLUser, depositTxOpts *bind.TransactOpts, l1Miner *helpers.L1Miner) { + optimismPortal2, err := bindingspreview.NewOptimismPortal2(m.chain.RollupCfg.DepositContractAddress, l1Miner.EthClient()) + require.NoError(m.t, err) + + l1Opts, _ := l1User.TransactOpts(l1Miner.L1Chain().Config().ChainID) + l1Opts.Value = depositTxOpts.Value + + to := m.tx.To() + min, err := optimismPortal2.MinimumGasLimit(&bind.CallOpts{}, uint64(len(m.tx.Data()))) + require.NoError(m.t, err) + gas := max(m.tx.Gas(), min) + tx, err := optimismPortal2.DepositTransaction(l1Opts, *to, m.tx.Value(), gas, to == nil, m.tx.Data()) + require.NoError(m.t, err, "failed to create deposit tx") + rcpt := l1Miner.IncludeTx(m.t, tx) + require.Equal(m.t, types.ReceiptStatusSuccessful, rcpt.Status, "deposit tx failed") +} + func (m *GeneratedTransaction) Identifier() inbox.Identifier { require.NotZero(m.t, len(m.rcpt.Logs), "Transaction did not include any logs to reference") @@ -71,8 +91,8 @@ func (m *GeneratedTransaction) MessagePayload() []byte { func (m *GeneratedTransaction) CheckIncluded() { rcpt, err := m.chain.SequencerEngine.EthClient().TransactionReceipt(m.t.Ctx(), m.tx.Hash()) - require.NoError(m.t, err) - require.NotNil(m.t, rcpt) + require.NoError(m.t, err, "Transaction should have been included") + require.NotNil(m.t, rcpt, "No receipt found") } func (m *GeneratedTransaction) CheckNotIncluded() { diff --git a/op-e2e/actions/interop/proofs_test.go b/op-e2e/actions/interop/proofs_test.go index 4eca84a3605af..35c85a24f882e 100644 --- a/op-e2e/actions/interop/proofs_test.go +++ b/op-e2e/actions/interop/proofs_test.go @@ -1132,6 +1132,138 @@ func TestInteropFaultProofs_VariedBlockTimes_FasterChainB(gt *testing.T) { runFppAndChallengerTests(gt, system, tests) } +func TestInteropFaultProofs_DepositMessage(gt *testing.T) { + t := helpers.NewDefaultTesting(gt) + + system := dsl.NewInteropDSL(t) + actors := system.Actors + emitter := system.DeployEmitterContracts() + + // Advance L1 a couple times to avoid deposit gas metering issues near genesis + system.AdvanceL1() + system.AdvanceL1() + + l1User := system.CreateUser() + depositMessage := dsl.NewMessage(system, actors.ChainA, emitter, "hello") + system.AdvanceL1( + dsl.WithActIncludeTx( + depositMessage.ActEmitDeposit(l1User))) + + // As such, the next block timestamp across both chains will contain a user-deposit message and an executing message + system.AdvanceL2ToLastBlockOfOrigin(actors.ChainA, 2) + system.AdvanceL2ToLastBlockOfOrigin(actors.ChainB, 2) + + actors.ChainA.Sequencer.ActL2StartBlock(t) + actors.ChainB.Sequencer.ActL2StartBlock(t) + // The pending block on chain A will contain the user deposit + depositMessage.ExecutePendingOn(actors.ChainB, actors.ChainA.Sequencer.L2Unsafe().Number+1) + actors.ChainA.Sequencer.ActL2EndBlock(t) + actors.ChainB.Sequencer.ActL2EndBlock(t) + system.SubmitBatchData(dsl.WithSkipCrossSafeUpdate()) + + endTimestamp := actors.ChainB.Sequencer.L2Unsafe().Time + startTimestamp := endTimestamp - 1 + preConsolidation := system.Outputs.TransitionState(startTimestamp, consolidateStep, + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainA, endTimestamp), + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainB, endTimestamp), + ).Marshal() + + system.ProcessCrossSafe() + depositMessage.CheckExecuted() + assertUserDepositEmitted(t, system.Actors.ChainA, nil, emitter) + crossSafeEnd := system.Outputs.SuperRoot(endTimestamp) + + tests := []*transitionTest{ + { + name: "Consolidate", + agreedClaim: preConsolidation, + disputedClaim: crossSafeEnd.Marshal(), + disputedTraceIndex: consolidateStep, + expectValid: true, + }, + { + name: "Consolidate-InvalidNoChange", + agreedClaim: preConsolidation, + disputedClaim: preConsolidation, + disputedTraceIndex: consolidateStep, + expectValid: false, + }, + } + runFppAndChallengerTests(gt, system, tests) +} + +func TestInteropFaultProofs_DepositMessage_InvalidExecution(gt *testing.T) { + t := helpers.NewDefaultTesting(gt) + + system := dsl.NewInteropDSL(t) + actors := system.Actors + emitter := system.DeployEmitterContracts() + + // Advance L1 a couple times to avoid deposit gas metering issues near genesis + system.AdvanceL1() + system.AdvanceL1() + + l1User := system.CreateUser() + depositMessage := dsl.NewMessage(system, actors.ChainA, emitter, "hello") + system.AdvanceL1( + dsl.WithActIncludeTx( + depositMessage.ActEmitDeposit(l1User))) + + // As such, the next block timestamp across both chains will contain a user-deposit message and an executing message + system.AdvanceL2ToLastBlockOfOrigin(actors.ChainA, 2) + system.AdvanceL2ToLastBlockOfOrigin(actors.ChainB, 2) + + actors.ChainA.Sequencer.ActL2StartBlock(t) + actors.ChainB.Sequencer.ActL2StartBlock(t) + // The pending block on chain A will contain the user deposit + depositMessage.ExecutePendingOn(actors.ChainB, + actors.ChainA.Sequencer.L2Unsafe().Number+1, + dsl.WithPayload([]byte("this message was never emitted")), + ) + actors.ChainA.Sequencer.ActL2EndBlock(t) + actors.ChainB.Sequencer.ActL2EndBlock(t) + system.SubmitBatchData(dsl.WithSkipCrossSafeUpdate()) + + endTimestamp := actors.ChainB.Sequencer.L2Unsafe().Time + startTimestamp := endTimestamp - 1 + optimisticEnd := system.Outputs.SuperRoot(endTimestamp) + + preConsolidation := system.Outputs.TransitionState(startTimestamp, consolidateStep, + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainA, endTimestamp), + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainB, endTimestamp), + ).Marshal() + + system.ProcessCrossSafe() + depositMessage.CheckNotExecuted() + assertUserDepositEmitted(t, system.Actors.ChainA, nil, emitter) + crossSafeEnd := system.Outputs.SuperRoot(endTimestamp) + + tests := []*transitionTest{ + { + name: "Consolidate", + agreedClaim: preConsolidation, + disputedClaim: crossSafeEnd.Marshal(), + disputedTraceIndex: consolidateStep, + expectValid: true, + }, + { + name: "Consolidate-InvalidNoChange", + agreedClaim: preConsolidation, + disputedClaim: preConsolidation, + disputedTraceIndex: consolidateStep, + expectValid: false, + }, + } + tests = append(tests, &transitionTest{ + name: "Consolidate-ExpectInvalidPendingBlock", + agreedClaim: preConsolidation, + disputedClaim: optimisticEnd.Marshal(), + disputedTraceIndex: consolidateStep, + expectValid: false, + }) + runFppAndChallengerTests(gt, system, tests) +} + func runFppAndChallengerTests(gt *testing.T, system *dsl.InteropDSL, tests []*transitionTest) { for _, test := range tests { test := test @@ -1248,6 +1380,15 @@ func assertTime(t helpers.Testing, chain *dsl.Chain, unsafe, crossUnsafe, localS require.Equal(t, start+safe, status.SafeL2.Time, "Safe") } +func assertUserDepositEmitted(t helpers.Testing, chain *dsl.Chain, number *big.Int, emitter *dsl.EmitterContract) { + block, err := chain.SequencerEngine.EthClient().BlockByNumber(t.Ctx(), number) + require.NoError(t, err) + require.GreaterOrEqual(t, len(block.Transactions()), 3) // l1-attrs + user-deposit + l1-attrs (end deposit contxt) + [txs] + userDepositTx := block.Transactions()[1] + require.NotNil(t, userDepositTx.To()) + require.Equal(t, emitter.Address(chain), *userDepositTx.To()) +} + type transitionTest struct { name string agreedClaim []byte @@ -1337,7 +1478,8 @@ func (c *cyclicDependencyInvalidCase) Setup(t helpers.StatefulTesting, system *d alice := system.CreateUser() // Create an exec message for chain B without including it - pendingExecBOpts := dsl.WithPendingMessage(emitter, actors.ChainB, 0, "message from B") + pendingBlockNumber := actors.ChainB.Sequencer.L2Unsafe().Number + 1 + pendingExecBOpts := dsl.WithPendingMessage(emitter, actors.ChainB, pendingBlockNumber, 0, "message from B") // Exec(A) -> Exec(B) -> Exec(A) actExecA := system.InboxContract.Execute(alice, nil, pendingExecBOpts)