diff --git a/op-e2e/actions/interop/emitter_contract_test.go b/op-e2e/actions/interop/emitter_contract_test.go index 756e5e84adfe1..bd6add196fae6 100644 --- a/op-e2e/actions/interop/emitter_contract_test.go +++ b/op-e2e/actions/interop/emitter_contract_test.go @@ -113,20 +113,26 @@ func newEmitMessageTx(t helpers.Testing, chain *dsl.Chain, user *userWithKeys, e return tx } +// newExecuteMessageTx creates a new executing message tx based on the given initializing tx. func newExecuteMessageTx(t helpers.Testing, destChain *dsl.Chain, executor *userWithKeys, srcChain *dsl.Chain, srcTx *types.Transaction) *types.Transaction { // Create the id and payload id := idForTx(t, srcTx, srcChain) receipt, err := srcChain.SequencerEngine.EthClient().TransactionReceipt(t.Ctx(), srcTx.Hash()) require.NoError(t, err) payload := stypes.LogToMessagePayload(receipt.Logs[0]) + hash := crypto.Keccak256Hash(payload) // Create the tx to validate the message + return newExecuteMessageTxFromIDAndHash(t, executor, destChain, id, hash) +} + +// newExecuteMessageTxFromIDAndHash creates a new executing message tx for the given id and hash. +func newExecuteMessageTxFromIDAndHash(t helpers.Testing, executor *userWithKeys, destChain *dsl.Chain, id inbox.Identifier, hash common.Hash) *types.Transaction { inboxContract, err := inbox.NewInbox(predeploys.CrossL2InboxAddr, destChain.SequencerEngine.EthClient()) require.NoError(t, err) auth := newL2TxOpts(t, executor.secret, destChain) - tx, err := inboxContract.ValidateMessage(auth, id, crypto.Keccak256Hash(payload)) + tx, err := inboxContract.ValidateMessage(auth, id, hash) require.NoError(t, err) - return tx } diff --git a/op-e2e/actions/interop/interop_test.go b/op-e2e/actions/interop/interop_test.go index 9f4d5dd1031e5..5a25caace329b 100644 --- a/op-e2e/actions/interop/interop_test.go +++ b/op-e2e/actions/interop/interop_test.go @@ -439,3 +439,36 @@ func TestInteropCrossSafeDependencyDelay(gt *testing.T) { require.NoError(t, err) require.Equal(t, chainBSubmittedIn.NumberU64(), source.Number) } + +func TestInteropExecutingMessageOutOfRangeLogIndex(gt *testing.T) { + t := helpers.NewDefaultTesting(gt) + is := dsl.SetupInterop(t) + actors := is.CreateActors() + actors.PrepareChainState(t) + aliceA := setupUser(t, is, actors.ChainA, 0) + + // Execute a fake log on chain A + chainBHead := actors.ChainB.Sequencer.SyncStatus().UnsafeL2 + nonExistentID := inbox.Identifier{ + Origin: aliceA.address, + BlockNumber: big.NewInt(int64(chainBHead.Number)), + LogIndex: common.Big0, + Timestamp: big.NewInt(int64(chainBHead.Time)), + ChainId: actors.ChainB.RollupCfg.L2ChainID, + } + nonExistentHash := crypto.Keccak256Hash([]byte("fake message")) + tx := newExecuteMessageTxFromIDAndHash(t, aliceA, actors.ChainA, nonExistentID, nonExistentHash) + includeTxOnChainBasic(t, actors.ChainA, tx, aliceA.address) + actors.ChainB.Sequencer.ActL2EmptyBlock(t) + + // Sync the system + actors.ChainA.Sequencer.SyncSupervisor(t) + actors.ChainB.Sequencer.SyncSupervisor(t) + actors.Supervisor.ProcessFull(t) + actors.ChainA.Sequencer.ActL2PipelineFull(t) + actors.ChainB.Sequencer.ActL2PipelineFull(t) + + // Assert that chainA's block is not cross-safe but chainB's is. + assertHeads(t, actors.ChainA, 1, 0, 0, 0) + assertHeads(t, actors.ChainB, 1, 0, 1, 0) +} diff --git a/op-supervisor/supervisor/backend/db/logs/db.go b/op-supervisor/supervisor/backend/db/logs/db.go index 7af29fc132a50..51bd8cd330a29 100644 --- a/op-supervisor/supervisor/backend/db/logs/db.go +++ b/op-supervisor/supervisor/backend/db/logs/db.go @@ -346,6 +346,9 @@ func (db *DB) Contains(query types.ContainsQuery) (types.BlockSeal, error) { return types.BlockSeal{}, err } +// findLogInfo returns the hash of the log at the specified block number and log index. +// If the log index is out of range we return an ErrFuture if the block is complete, +// or ErrConflict if it's not. func (db *DB) findLogInfo(blockNum uint64, logIdx uint32) (common.Hash, Iterator, error) { if blockNum == 0 { return common.Hash{}, nil, types.ErrConflict // no logs in block 0 @@ -361,6 +364,10 @@ func (db *DB) findLogInfo(blockNum uint64, logIdx uint32) (common.Hash, Iterator return common.Hash{}, nil, err } if err := iter.NextInitMsg(); err != nil { + // if we get an ErrFuture but have a complete block, then we really have a conflict + if errors.Is(err, types.ErrFuture) && db.lastEntryContext.hasCompleteBlock() { + err = types.ErrConflict + } return common.Hash{}, nil, fmt.Errorf("failed to read initiating message %d, on top of block %d: %w", logIdx, blockNum, err) } if _, x, ok := iter.SealedBlock(); !ok {