diff --git a/op-e2e/actions/interop/proofs_test.go b/op-e2e/actions/interop/proofs_test.go index 9164ef75fcdc0..cc6b88881a86b 100644 --- a/op-e2e/actions/interop/proofs_test.go +++ b/op-e2e/actions/interop/proofs_test.go @@ -443,6 +443,8 @@ func TestInteropFaultProofs_Cycle(gt *testing.T) { } func TestInteropFaultProofs_CascadeInvalidBlock(gt *testing.T) { + // TODO(#14307): Support cascading block invalidations + gt.Skip("TODO(#14307): Support cascading block invalidations") t := helpers.NewDefaultTesting(gt) system := dsl.NewInteropDSL(t) @@ -458,31 +460,39 @@ func TestInteropFaultProofs_CascadeInvalidBlock(gt *testing.T) { emitterContract.Deploy(alice), )) - // Initiating messages on chain A - system.AddL2Block(actors.ChainA, dsl.WithL2BlockTransactions( - emitterContract.EmitMessage(alice, "chainA message"), - )) - chainAInitTx := emitterContract.LastEmittedMessage() - system.AddL2Block(actors.ChainB) - system.SubmitBatchData() + assertHeads(t, actors.ChainA, 1, 0, 1, 0) + assertHeads(t, actors.ChainB, 1, 0, 1, 0) - // Create a message with a conflicting payload on chain B, that also emits an initiating message - system.AddL2Block(actors.ChainB, dsl.WithL2BlockTransactions( - system.InboxContract.Execute(alice, chainAInitTx, dsl.WithPayload([]byte("this message was never emitted"))), - emitterContract.EmitMessage(alice, "chainB message"), - ), dsl.WithL1BlockCrossUnsafe()) - chainBExecTx := system.InboxContract.LastTransaction() - chainBExecTx.CheckIncluded() - chainBInitTx := emitterContract.LastEmittedMessage() - - // Create a message with a valid message on chain A, pointing to the initiating message on B from the same block - // as an invalid message. - system.AddL2Block(actors.ChainA, - dsl.WithL2BlockTransactions(system.InboxContract.Execute(alice, chainBInitTx)), - dsl.WithL1BlockCrossUnsafe(), + // Create initiating and executing messages within the same block + var ( + chainAExecTx *dsl.GeneratedTransaction + chainBExecTx *dsl.GeneratedTransaction + chainBInitTx *dsl.GeneratedTransaction ) - chainAExecTx := system.InboxContract.LastTransaction() - chainAExecTx.CheckIncluded() + { + actors.ChainA.Sequencer.ActL2StartBlock(t) + actors.ChainB.Sequencer.ActL2StartBlock(t) + + chainAInitTx := emitterContract.EmitMessage(alice, "chainA message")(actors.ChainA) + chainAInitTx.Include() + + // Create messages with a conflicting payload on chain B, while also emitting an initiating message + chainBExecTx := system.InboxContract.Execute(alice, chainAInitTx, + dsl.WithPayload([]byte("this message was never emitted")))(actors.ChainB) + chainBExecTx.Include() + chainBInitTx = emitterContract.EmitMessage(alice, "chainB message")(actors.ChainB) + chainBInitTx.Include() + + // Create a message with a valid message on chain A, pointing to the initiating message on B from the same block + // as an invalid message. + chainAExecTx = system.InboxContract.Execute(alice, chainBInitTx)(actors.ChainA) + chainAExecTx.Include() + + actors.ChainA.Sequencer.ActL2EndBlock(t) + actors.ChainB.Sequencer.ActL2EndBlock(t) + } + assertHeads(t, actors.ChainA, 2, 0, 1, 0) + assertHeads(t, actors.ChainB, 2, 0, 1, 0) system.SubmitBatchData(func(opts *dsl.SubmitBatchDataOpts) { opts.SkipCrossSafeUpdate = true @@ -513,9 +523,6 @@ func TestInteropFaultProofs_CascadeInvalidBlock(gt *testing.T) { disputedClaim: optimisticEnd.Marshal(), disputedTraceIndex: consolidateStep, expectValid: false, - // TODO(#14306): Support cascading re-orgs in op-program - skipProgram: true, - skipChallenger: true, }, { name: "Consolidate-ReplaceInvalidBlocks", @@ -523,9 +530,6 @@ func TestInteropFaultProofs_CascadeInvalidBlock(gt *testing.T) { disputedClaim: crossSafeEnd.Marshal(), disputedTraceIndex: consolidateStep, expectValid: true, - // TODO(#14306): Support cascading re-orgs in op-program - skipProgram: true, - skipChallenger: true, }, } runFppAndChallengerTests(gt, system, tests) diff --git a/op-program/client/interop/consolidate.go b/op-program/client/interop/consolidate.go index 141a720524504..60447932df356 100644 --- a/op-program/client/interop/consolidate.go +++ b/op-program/client/interop/consolidate.go @@ -20,6 +20,8 @@ import ( "github.com/ethereum/go-ethereum/log" ) +var ErrInvalidBlockReplacement = errors.New("invalid block replacement error") + // ReceiptsToExecutingMessages returns the executing messages in the receipts indexed by their position in the log. func ReceiptsToExecutingMessages(depset depset.ChainIndexFromID, receipts ethtypes.Receipts) (map[uint32]*supervisortypes.ExecutingMessage, uint32, error) { execMsgs := make(map[uint32]*supervisortypes.ExecutingMessage) @@ -39,17 +41,19 @@ func ReceiptsToExecutingMessages(depset depset.ChainIndexFromID, receipts ethtyp return execMsgs, curr, nil } -func fetchAgreedBlockHashes(oracle l2.Oracle, superRoot *eth.SuperV1) ([]common.Hash, error) { - agreedBlockHashes := make([]common.Hash, len(superRoot.Chains)) - for i, chain := range superRoot.Chains { - output := oracle.OutputByRoot(common.Hash(chain.Output), chain.ChainID) - outputV0, ok := output.(*eth.OutputV0) - if !ok { - return nil, fmt.Errorf("unsupported L2 output version: %d", output.Version()) - } - agreedBlockHashes[i] = common.Hash(outputV0.BlockHash) - } - return agreedBlockHashes, nil +type consolidateState struct { + *types.TransitionState + replacedChains map[eth.ChainID]bool +} + +func (s *consolidateState) isReplaced(chainID eth.ChainID) bool { + return s.replacedChains[chainID] +} + +func (s *consolidateState) setReplaced(transitionStateIndex int, chainID eth.ChainID, outputRoot eth.Bytes32, replacementBlockHash common.Hash) { + s.PendingProgress[transitionStateIndex].OutputRoot = outputRoot + s.PendingProgress[transitionStateIndex].BlockHash = replacementBlockHash + s.replacedChains[chainID] = true } func RunConsolidation( @@ -61,60 +65,111 @@ func RunConsolidation( superRoot *eth.SuperV1, tasks taskExecutor, ) (eth.Bytes32, error) { + consolidateState := consolidateState{ + TransitionState: &types.TransitionState{ + PendingProgress: make([]types.OptimisticBlock, len(transitionState.PendingProgress)), + SuperRoot: transitionState.SuperRoot, + Step: transitionState.Step, + }, + replacedChains: make(map[eth.ChainID]bool), + } + // We will be updating the transition state as blocks are replaced, so make a copy + copy(consolidateState.PendingProgress, transitionState.PendingProgress) + // Use a reference to the transition state so the consolidate oracle has a recent view. + // The TransitionStateByRoot method isn't expected to be used during consolidation, + // but we pass the state for safety in case this changes in the future. + consolidateOracle := NewConsolidateOracle(l2PreimageOracle, consolidateState.TransitionState) + + // Keep consolidating until there are no more invalid blocks to replace +loop: + for { + err := singleRoundConsolidation(logger, bootInfo, l1PreimageOracle, consolidateOracle, &consolidateState, superRoot, tasks) + switch { + case err == nil: + break loop + case errors.Is(err, ErrInvalidBlockReplacement): + continue + default: + return eth.Bytes32{}, err + } + } + + var consolidatedChains []eth.ChainIDAndOutput + for i, chain := range superRoot.Chains { + consolidatedChains = append(consolidatedChains, eth.ChainIDAndOutput{ + ChainID: chain.ChainID, + Output: consolidateState.PendingProgress[i].OutputRoot, + }) + } + consolidatedSuper := ð.SuperV1{ + Timestamp: superRoot.Timestamp + 1, + Chains: consolidatedChains, + } + return eth.SuperRoot(consolidatedSuper), nil +} + +func singleRoundConsolidation( + logger log.Logger, + bootInfo *boot.BootInfoInterop, + l1PreimageOracle l1.Oracle, + l2PreimageOracle *ConsolidateOracle, + consolidateState *consolidateState, + superRoot *eth.SuperV1, + tasks taskExecutor, +) error { // The depset is the same for all chains. So it suffices to use any chain ID depset, err := bootInfo.Configs.DependencySet(superRoot.Chains[0].ChainID) if err != nil { - return eth.Bytes32{}, fmt.Errorf("failed to get dependency set: %w", err) - } - deps, err := newConsolidateCheckDeps(depset, bootInfo, transitionState, superRoot.Chains, l2PreimageOracle) - if err != nil { - return eth.Bytes32{}, fmt.Errorf("failed to create consolidate check deps: %w", err) + return fmt.Errorf("failed to get dependency set: %w", err) } - agreedBlockHashes, err := fetchAgreedBlockHashes(l2PreimageOracle, superRoot) + deps, err := newConsolidateCheckDeps(depset, bootInfo, consolidateState.TransitionState, superRoot.Chains, l2PreimageOracle) if err != nil { - return eth.Bytes32{}, err + return fmt.Errorf("failed to create consolidate check deps: %w", err) } - // TODO(#14306): Handle cascading reorgs - // invalidChains tracks blocks that need to be replaced with a deposits-only block. - // The replacement is done after a first pass on all chains to avoid "contaminating" the caonical block - // oracle in a way that alters the result of hazard checks after a reorg. invalidChains := make(map[eth.ChainID]*ethtypes.Block) for i, chain := range superRoot.Chains { - progress := transitionState.PendingProgress[i] + // Do not check chains that have been replaced with a deposits-only block. + // They are already cross-safe because deposits-only blocks cannot contain executing messages. + if consolidateState.isReplaced(chain.ChainID) { + continue + } + agreedOutput := l2PreimageOracle.OutputByRoot(common.Hash(chain.Output), chain.ChainID) + agreedOutputV0, ok := agreedOutput.(*eth.OutputV0) + if !ok { + return fmt.Errorf("unsupported L2 output version: %d", agreedOutput.Version()) + } + agreedBlockHash := common.Hash(agreedOutputV0.BlockHash) + + progress := consolidateState.PendingProgress[i] // It's possible that the optimistic block is not canonical. // So we use the blockDataByHash hint to trigger a block rebuild to ensure that the block data, including receipts, are available. - _ = l2PreimageOracle.BlockDataByHash(agreedBlockHashes[i], progress.BlockHash, chain.ChainID) + _ = l2PreimageOracle.BlockDataByHash(agreedBlockHash, progress.BlockHash, chain.ChainID) - optimisticBlock, receipts := l2PreimageOracle.ReceiptsByBlockHash(progress.BlockHash, chain.ChainID) - execMsgs, _, err := ReceiptsToExecutingMessages(deps.DependencySet(), receipts) - switch { - case errors.Is(err, supervisortypes.ErrUnknownChain): - invalidChains[chain.ChainID] = optimisticBlock - continue - case err != nil: - return eth.Bytes32{}, err - } + optimisticBlock, _ := l2PreimageOracle.ReceiptsByBlockHash(progress.BlockHash, chain.ChainID) candidate := supervisortypes.BlockSeal{ Hash: progress.BlockHash, Number: optimisticBlock.NumberU64(), Timestamp: optimisticBlock.Time(), } - if err := checkHazards(logger, deps, candidate, chain.ChainID, execMsgs); err != nil { + if err := checkHazards(logger, deps, candidate, chain.ChainID); err != nil { if !isInvalidMessageError(err) { - return eth.Bytes32{}, err + return err } invalidChains[chain.ChainID] = optimisticBlock } } - var consolidatedChains []eth.ChainIDAndOutput + if len(invalidChains) == 0 { + return nil + } + for i, chain := range superRoot.Chains { if optimisticBlock, ok := invalidChains[chain.ChainID]; ok { chainAgreedPrestate := superRoot.Chains[i] - _, outputRoot, err := buildDepositOnlyBlock( + replacementBlockHash, outputRoot, err := buildDepositOnlyBlock( logger, bootInfo, l1PreimageOracle, @@ -122,26 +177,25 @@ func RunConsolidation( chainAgreedPrestate, tasks, optimisticBlock, + // Update the preimage oracle database with the replaced block data + l2PreimageOracle.KeyValueStore(), ) if err != nil { - return eth.Bytes32{}, err + return err } - consolidatedChains = append(consolidatedChains, eth.ChainIDAndOutput{ - ChainID: chain.ChainID, - Output: outputRoot, - }) - } else { - consolidatedChains = append(consolidatedChains, eth.ChainIDAndOutput{ - ChainID: chain.ChainID, - Output: transitionState.PendingProgress[i].OutputRoot, - }) + logger.Info( + "Replaced block", + "chain", chain.ChainID, + "replacedBlock", eth.ToBlockID(optimisticBlock), + "replacementBlockHash", replacementBlockHash, + "outputRoot", outputRoot, + "replacedOutputRoot", superRoot.Chains[i].Output, + ) + superRoot.Chains[i].Output = outputRoot + consolidateState.setReplaced(i, chain.ChainID, outputRoot, replacementBlockHash) } } - consolidatedSuper := ð.SuperV1{ - Timestamp: superRoot.Timestamp + 1, - Chains: consolidatedChains, - } - return eth.SuperRoot(consolidatedSuper), nil + return ErrInvalidBlockReplacement } func isInvalidMessageError(err error) bool { @@ -158,7 +212,7 @@ type ConsolidateCheckDeps interface { cross.UnsafeStartDeps } -func checkHazards(logger log.Logger, deps ConsolidateCheckDeps, candidate supervisortypes.BlockSeal, chainID eth.ChainID, execMsgs map[uint32]*supervisortypes.ExecutingMessage) error { +func checkHazards(logger log.Logger, deps ConsolidateCheckDeps, candidate supervisortypes.BlockSeal, chainID eth.ChainID) error { hazards, err := cross.CrossUnsafeHazards(deps, logger, chainID, candidate) if err != nil { return err @@ -309,6 +363,7 @@ func buildDepositOnlyBlock( chainAgreedPrestate eth.ChainIDAndOutput, tasks taskExecutor, optimisticBlock *ethtypes.Block, + db l2.KeyValueStore, ) (common.Hash, eth.Bytes32, error) { rollupCfg, err := bootInfo.Configs.RollupConfig(chainAgreedPrestate.ChainID) if err != nil { @@ -327,6 +382,7 @@ func buildDepositOnlyBlock( l1PreimageOracle, l2PreimageOracle, optimisticBlock, + db, ) if err != nil { return common.Hash{}, eth.Bytes32{}, err diff --git a/op-program/client/interop/interop.go b/op-program/client/interop/interop.go index 3f5be634a4869..ac9777938065c 100644 --- a/op-program/client/interop/interop.go +++ b/op-program/client/interop/interop.go @@ -53,6 +53,7 @@ type taskExecutor interface { l1Oracle l1.Oracle, l2Oracle l2.Oracle, optimisticBlock *ethtypes.Block, + db l2.KeyValueStore, ) (blockHash common.Hash, outputRoot eth.Bytes32, err error) } @@ -214,6 +215,7 @@ func (t *interopTaskExecutor) BuildDepositOnlyBlock( l1Oracle l1.Oracle, l2Oracle l2.Oracle, optimisticBlock *ethtypes.Block, + db l2.KeyValueStore, ) (common.Hash, eth.Bytes32, error) { return tasks.BuildDepositOnlyBlock( logger, @@ -224,5 +226,6 @@ func (t *interopTaskExecutor) BuildDepositOnlyBlock( agreedL2OutputRoot, l1Oracle, l2Oracle, + db, ) } diff --git a/op-program/client/interop/interop_test.go b/op-program/client/interop/interop_test.go index ba5886ffa55f8..993f81408e46c 100644 --- a/op-program/client/interop/interop_test.go +++ b/op-program/client/interop/interop_test.go @@ -146,19 +146,20 @@ const ( ) func TestDeriveBlockForConsolidateStep(t *testing.T) { - createExecMessage := func(initIncludedIn uint64, config *staticConfigSource) interoptypes.Message { + createExecMessage := func(initIncludedIn uint64, config *staticConfigSource, initChainIndex supervisortypes.ChainIndex) interoptypes.Message { exec := interoptypes.Message{ Identifier: interoptypes.Identifier{ Origin: initiatingMessageOrigin, BlockNumber: initIncludedIn, LogIndex: 0, - Timestamp: initIncludedIn * config.rollupCfgs[chainA].BlockTime, - ChainID: uint256.Int(eth.ChainIDFromBig(config.rollupCfgs[chainA].L2ChainID)), + Timestamp: initIncludedIn * config.rollupCfgs[initChainIndex].BlockTime, + ChainID: uint256.Int(eth.ChainIDFromBig(config.rollupCfgs[initChainIndex].L2ChainID)), }, PayloadHash: initPayloadHash, } return exec } + createInitLog := func() *gethTypes.Log { return &gethTypes.Log{ Address: initiatingMessageOrigin, @@ -179,7 +180,7 @@ func TestDeriveBlockForConsolidateStep(t *testing.T) { testCase: consolidationTestCase{ logBuilderFn: func(includeBlockNumbers map[supervisortypes.ChainIndex]uint64, config *staticConfigSource) map[supervisortypes.ChainIndex][]*gethTypes.Log { init := createInitLog() - exec := createExecMessage(includeBlockNumbers[chainA], config) + exec := createExecMessage(includeBlockNumbers[chainA], config, chainA) return map[supervisortypes.ChainIndex][]*gethTypes.Log{chainA: {init}, chainB: {convertExecutingMessageToLog(t, exec)}} }, }, @@ -189,17 +190,7 @@ func TestDeriveBlockForConsolidateStep(t *testing.T) { testCase: consolidationTestCase{ logBuilderFn: func(includeBlockNumbers map[supervisortypes.ChainIndex]uint64, config *staticConfigSource) map[supervisortypes.ChainIndex][]*gethTypes.Log { init := createInitLog() - initPayloadHash := crypto.Keccak256Hash(initiatingMessageTopic[:]) - execMsg := interoptypes.Message{ - Identifier: interoptypes.Identifier{ - Origin: init.Address, - BlockNumber: includeBlockNumbers[chainB], - LogIndex: 0, - Timestamp: includeBlockNumbers[chainB] * config.rollupCfgs[chainB].BlockTime, - ChainID: uint256.Int(eth.ChainIDFromBig(config.rollupCfgs[chainB].L2ChainID)), - }, - PayloadHash: initPayloadHash, - } + execMsg := createExecMessage(includeBlockNumbers[chainB], config, chainB) exec := convertExecutingMessageToLog(t, execMsg) return map[supervisortypes.ChainIndex][]*gethTypes.Log{chainA: {exec}, chainB: {init}} }, @@ -217,7 +208,7 @@ func TestDeriveBlockForConsolidateStep(t *testing.T) { Address: initiatingMessageOrigin2, Topics: []common.Hash{initiatingMessageTopic}, } - exec := createExecMessage(includeBlockNumbers[chainA], config) + exec := createExecMessage(includeBlockNumbers[chainA], config, chainA) exec.Identifier.Origin = init2.Address exec.Identifier.LogIndex = 1 return map[supervisortypes.ChainIndex][]*gethTypes.Log{ @@ -227,12 +218,30 @@ func TestDeriveBlockForConsolidateStep(t *testing.T) { }, }, }, + { + name: "HappyPathWithValidMessages-IntraBlockCycle", + testCase: consolidationTestCase{ + logBuilderFn: func(includeBlockNumbers map[supervisortypes.ChainIndex]uint64, config *staticConfigSource) map[supervisortypes.ChainIndex][]*gethTypes.Log { + initA := createInitLog() + initB := createInitLog() + + execMsgA := createExecMessage(includeBlockNumbers[chainB], config, chainB) + execA := convertExecutingMessageToLog(t, execMsgA) + execMsgB := createExecMessage(includeBlockNumbers[chainA], config, chainA) + execB := convertExecutingMessageToLog(t, execMsgB) + return map[supervisortypes.ChainIndex][]*gethTypes.Log{ + chainA: {initA, execA}, + chainB: {initB, execB}, + } + }, + }, + }, { name: "ReplaceChainB-UnknownChainID", testCase: consolidationTestCase{ logBuilderFn: func(includeBlockNumbers map[supervisortypes.ChainIndex]uint64, config *staticConfigSource) map[supervisortypes.ChainIndex][]*gethTypes.Log { init := createInitLog() - exec := createExecMessage(includeBlockNumbers[chainA], config) + exec := createExecMessage(includeBlockNumbers[chainA], config, chainA) exec.Identifier.ChainID = uint256.Int(eth.ChainIDFromUInt64(0xdeadbeef)) return map[supervisortypes.ChainIndex][]*gethTypes.Log{chainA: {init}, chainB: {convertExecutingMessageToLog(t, exec)}} }, @@ -253,7 +262,7 @@ func TestDeriveBlockForConsolidateStep(t *testing.T) { Address: initiatingMessageOrigin2, Topics: []common.Hash{initiatingMessageTopic}, } - exec := createExecMessage(includeBlockNumbers[chainA], config) + exec := createExecMessage(includeBlockNumbers[chainA], config, chainA) exec.Identifier.Origin = init2.Address exec.Identifier.LogIndex = 0 return map[supervisortypes.ChainIndex][]*gethTypes.Log{ @@ -271,7 +280,7 @@ func TestDeriveBlockForConsolidateStep(t *testing.T) { testCase: consolidationTestCase{ logBuilderFn: func(includeBlockNumbers map[supervisortypes.ChainIndex]uint64, config *staticConfigSource) map[supervisortypes.ChainIndex][]*gethTypes.Log { init := createInitLog() - execMsg := createExecMessage(includeBlockNumbers[chainA], config) + execMsg := createExecMessage(includeBlockNumbers[chainA], config, chainA) execMsg.PayloadHash = crypto.Keccak256Hash([]byte("invalid hash")) return map[supervisortypes.ChainIndex][]*gethTypes.Log{chainA: {init}, chainB: {convertExecutingMessageToLog(t, execMsg)}} }, @@ -285,7 +294,7 @@ func TestDeriveBlockForConsolidateStep(t *testing.T) { testCase: consolidationTestCase{ logBuilderFn: func(includeBlockNumbers map[supervisortypes.ChainIndex]uint64, config *staticConfigSource) map[supervisortypes.ChainIndex][]*gethTypes.Log { init := createInitLog() - execMsg := createExecMessage(includeBlockNumbers[chainA], config) + execMsg := createExecMessage(includeBlockNumbers[chainA], config, chainA) execMsg.Identifier.Timestamp = execMsg.Identifier.Timestamp - 1 return map[supervisortypes.ChainIndex][]*gethTypes.Log{chainA: {init}, chainB: {convertExecutingMessageToLog(t, execMsg)}} }, @@ -298,7 +307,7 @@ func TestDeriveBlockForConsolidateStep(t *testing.T) { name: "ReplaceBothChains", testCase: consolidationTestCase{ logBuilderFn: func(includeBlockNumbers map[supervisortypes.ChainIndex]uint64, config *staticConfigSource) map[supervisortypes.ChainIndex][]*gethTypes.Log { - invalidExecMsg := createExecMessage(includeBlockNumbers[chainA], config) + invalidExecMsg := createExecMessage(includeBlockNumbers[chainA], config, chainA) invalidExecMsg.PayloadHash = crypto.Keccak256Hash([]byte("invalid hash")) log := convertExecutingMessageToLog(t, invalidExecMsg) return map[supervisortypes.ChainIndex][]*gethTypes.Log{chainA: {log}, chainB: {log}} @@ -308,10 +317,36 @@ func TestDeriveBlockForConsolidateStep(t *testing.T) { }, }, }, + { + name: "ReplaceBothChains-CascadingReorg", + testCase: consolidationTestCase{ + logBuilderFn: func(includeBlockNumbers map[supervisortypes.ChainIndex]uint64, config *staticConfigSource) map[supervisortypes.ChainIndex][]*gethTypes.Log { + initA := createInitLog() + initB := createInitLog() + + execMsgA := createExecMessage(includeBlockNumbers[chainB], config, chainB) + execA := convertExecutingMessageToLog(t, execMsgA) + execMsgB := createExecMessage(includeBlockNumbers[chainA], config, chainA) + execMsgB.PayloadHash = crypto.Keccak256Hash([]byte("invalid hash")) + execB := convertExecutingMessageToLog(t, execMsgB) + + return map[supervisortypes.ChainIndex][]*gethTypes.Log{ + chainA: {initA, execA}, + chainB: {initB, execB}, + } + }, + expectBlockReplacements: func(config *staticConfigSource) []supervisortypes.ChainIndex { + return []supervisortypes.ChainIndex{chainA, chainB} + }, + }, + }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { + if tt.name != "ReplaceBothChains-CascadingReorg" { + t.Skip() + } runConsolidationTestCase(t, tt.testCase) }) } @@ -391,10 +426,16 @@ func runConsolidationTestCase(t *testing.T, testCase consolidationTestCase) { replacedBlockOutputRoot := common.Hash(eth.OutputRoot(replacedBlockOutput)) l2PreimageOracle.Outputs[replacedBlockOutputRoot] = replacedBlockOutput - depositsOnlyBlock, _ := createBlock(rng, configSource.rollupCfgs[chainIndexToReplace], 2, nil) - depositsOnlyOutputRoot := eth.OutputRoot(createOutput(depositsOnlyBlock.Hash())) + depositsOnlyBlock, depositsOnlyBlockReceipts := createBlock(rng, configSource.rollupCfgs[chainIndexToReplace], 2, nil) + depositsOnlyOutput := createOutput(depositsOnlyBlock.Hash()) + depositsOnlyOutputRoot := eth.OutputRoot(depositsOnlyOutput) tasksStub.ExpectBuildDepositOnlyBlock(common.Hash{}, agreedSuperRoot.Chains[chainIndexToReplace].Output, depositsOnlyBlock.Hash(), depositsOnlyOutputRoot) finalRoots[chainIndexToReplace] = depositsOnlyOutputRoot + // stub the preimages in the replacement block + l2PreimageOracle.Blocks[depositsOnlyBlock.Hash()] = depositsOnlyBlock + l2PreimageOracle.BlockData[depositsOnlyBlock.Hash()] = depositsOnlyBlock + l2PreimageOracle.Outputs[common.Hash(depositsOnlyOutputRoot)] = depositsOnlyOutput + l2PreimageOracle.Receipts[depositsOnlyBlock.Hash()] = depositsOnlyBlockReceipts } } expectedClaim := common.Hash(eth.SuperRoot(ð.SuperV1{ @@ -538,6 +579,7 @@ func (t *stubTasks) BuildDepositOnlyBlock( l1Oracle l1.Oracle, l2Oracle l2.Oracle, optimisticBlock *gethTypes.Block, + db l2.KeyValueStore, ) (common.Hash, eth.Bytes32, error) { out := t.Mock.Called( logger, @@ -548,6 +590,7 @@ func (t *stubTasks) BuildDepositOnlyBlock( l1Oracle, l2Oracle, optimisticBlock, + db, ) return out.Get(0).(common.Hash), out.Get(1).(eth.Bytes32), nil } @@ -568,6 +611,7 @@ func (t *stubTasks) ExpectBuildDepositOnlyBlock( mock.Anything, mock.Anything, mock.Anything, + mock.Anything, ).Once().Return(depositOnlyBlockHash, depositOnlyOutputRoot, nil) } diff --git a/op-program/client/interop/oracle.go b/op-program/client/interop/oracle.go new file mode 100644 index 0000000000000..26c18445350b5 --- /dev/null +++ b/op-program/client/interop/oracle.go @@ -0,0 +1,154 @@ +package interop + +import ( + "fmt" + + preimage "github.com/ethereum-optimism/optimism/op-preimage" + interopTypes "github.com/ethereum-optimism/optimism/op-program/client/interop/types" + "github.com/ethereum-optimism/optimism/op-program/client/l2" + l2Types "github.com/ethereum-optimism/optimism/op-program/client/l2/types" + "github.com/ethereum-optimism/optimism/op-program/client/mpt" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb/memorydb" + "github.com/ethereum/go-ethereum/rlp" +) + +// ConsolidateOracle extends another l2.Oracle with consolidated state data. +// The consolidated state data includes data from deposits-only replacement blocks. +type ConsolidateOracle struct { + o l2.Oracle + db l2.KeyValueStore + ts *interopTypes.TransitionState +} + +var _ l2.Oracle = &ConsolidateOracle{} + +func NewConsolidateOracle(oracle l2.Oracle, transitionState *interopTypes.TransitionState) *ConsolidateOracle { + return &ConsolidateOracle{ + o: oracle, + db: memorydb.New(), + ts: transitionState, + } +} + +func (o *ConsolidateOracle) BlockByHash(blockHash common.Hash, chainID eth.ChainID) *types.Block { + block := o.consolidatedBlockByHash(blockHash) + if block != nil { + return block + } + return o.o.BlockByHash(blockHash, chainID) +} + +func (o *ConsolidateOracle) OutputByRoot(root common.Hash, chainID eth.ChainID) eth.Output { + key := preimage.Keccak256Key(root).PreimageKey() + b, err := o.db.Get(key[:]) + if err == nil { + output, err := eth.UnmarshalOutput(b) + if err != nil { + panic(fmt.Errorf("invalid output %s: %w", root, err)) + } + return output + } + return o.o.OutputByRoot(root, chainID) +} + +func (o *ConsolidateOracle) BlockDataByHash(agreedBlockHash, blockHash common.Hash, chainID eth.ChainID) *types.Block { + block := o.consolidatedBlockByHash(blockHash) + if block != nil { + return block + } + return o.o.BlockDataByHash(agreedBlockHash, blockHash, chainID) +} + +func (o *ConsolidateOracle) ReceiptsByBlockHash(blockHash common.Hash, chainID eth.ChainID) (*types.Block, types.Receipts) { + block := o.consolidatedBlockByHash(blockHash) + if block != nil { + opaqueReceipts := mpt.ReadTrie(block.ReceiptHash(), func(key common.Hash) []byte { + k := preimage.Keccak256Key(key).PreimageKey() + b, err := o.db.Get(k[:]) + if err != nil { + panic(fmt.Errorf("missing receipt trie node %s: %w", key, err)) + } + return b + }) + txHashes := make([]common.Hash, len(block.Transactions())) + for i, tx := range block.Transactions() { + txHashes[i] = tx.Hash() + } + receipts, err := eth.DecodeRawReceipts(eth.ToBlockID(block), opaqueReceipts, txHashes) + if err != nil { + panic(fmt.Errorf("failed to decode receipts for block %v: %w", block.Hash(), err)) + } + return block, receipts + } + + return o.o.ReceiptsByBlockHash(blockHash, chainID) +} + +func (o *ConsolidateOracle) NodeByHash(nodeHash common.Hash, chainID eth.ChainID) []byte { + node, err := o.db.Get(nodeHash[:]) + if err == nil { + return node + } + return o.o.NodeByHash(nodeHash, chainID) +} + +func (o *ConsolidateOracle) CodeByHash(codeHash common.Hash, chainID eth.ChainID) []byte { + code, err := o.db.Get(codeHash[:]) + if err == nil { + return code + } + return o.o.CodeByHash(codeHash, chainID) +} + +func (o *ConsolidateOracle) Hinter() l2Types.OracleHinter { + return o.o.Hinter() +} + +func (o *ConsolidateOracle) TransitionStateByRoot(root common.Hash) *interopTypes.TransitionState { + return o.ts +} + +func (o *ConsolidateOracle) headerByBlockHash(blockHash common.Hash) *types.Header { + blockHashKey := preimage.Keccak256Key(blockHash).PreimageKey() + headerRlp, err := o.db.Get(blockHashKey[:]) + if err != nil { + return nil + } + var header types.Header + if err := rlp.DecodeBytes(headerRlp, &header); err != nil { + panic(fmt.Errorf("invalid block header %s: %w", blockHash, err)) + } + return &header +} + +func (o *ConsolidateOracle) loadTransactions(txHash common.Hash) []*types.Transaction { + opaqueTxs := mpt.ReadTrie(txHash, func(key common.Hash) []byte { + k := preimage.Keccak256Key(key).PreimageKey() + b, err := o.db.Get(k[:]) + if err != nil { + panic(fmt.Errorf("missing tx trie node %s", key)) + } + return b + }) + txs, err := eth.DecodeTransactions(opaqueTxs) + if err != nil { + panic(fmt.Errorf("failed to decode list of txs: %w", err)) + } + return txs +} + +func (o *ConsolidateOracle) consolidatedBlockByHash(blockHash common.Hash) *types.Block { + header := o.headerByBlockHash(blockHash) + if header == nil { + return nil + } + txs := o.loadTransactions(header.TxHash) + return types.NewBlockWithHeader(header).WithBody(types.Body{Transactions: txs}) +} + +func (o *ConsolidateOracle) KeyValueStore() l2.KeyValueStore { + return o.db +} diff --git a/op-program/client/interop/oracle_test.go b/op-program/client/interop/oracle_test.go new file mode 100644 index 0000000000000..abcfb10e336c0 --- /dev/null +++ b/op-program/client/interop/oracle_test.go @@ -0,0 +1,253 @@ +package interop + +import ( + "math/rand" + "testing" + + preimage "github.com/ethereum-optimism/optimism/op-preimage" + interopTypes "github.com/ethereum-optimism/optimism/op-program/client/interop/types" + "github.com/ethereum-optimism/optimism/op-program/client/l2" + l2Types "github.com/ethereum-optimism/optimism/op-program/client/l2/types" + "github.com/ethereum-optimism/optimism/op-program/client/mpt" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/testutils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + gethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/require" + + "github.com/stretchr/testify/mock" +) + +func TestConsolidateOracle_NoConsolidatedData(t *testing.T) { + chainID := uint64(48294) + rng := rand.New(rand.NewSource(1)) + + t.Run("BlockByHash", func(t *testing.T) { + mock := new(OracleMock) + oracle := NewConsolidateOracle(mock, &interopTypes.TransitionState{}) + block, _ := testutils.RandomBlock(rng, 1) + mock.On("BlockByHash", block.Hash(), eth.ChainIDFromUInt64(chainID)).Return(block) + actual := oracle.BlockByHash(block.Hash(), eth.ChainIDFromUInt64(chainID)) + require.Equal(t, block, actual) + mock.AssertExpectations(t) + }) + t.Run("OutputByRoot", func(t *testing.T) { + mock := new(OracleMock) + oracle := NewConsolidateOracle(mock, &interopTypes.TransitionState{}) + root := common.Hash{0xaa} + output := testutils.RandomOutputV0(rng) + mock.On("OutputByRoot", root, eth.ChainIDFromUInt64(chainID)).Return(output) + actual := oracle.OutputByRoot(root, eth.ChainIDFromUInt64(chainID)) + require.Equal(t, output, actual) + mock.AssertExpectations(t) + }) + t.Run("BlockDataByHash", func(t *testing.T) { + mock := new(OracleMock) + oracle := NewConsolidateOracle(mock, &interopTypes.TransitionState{}) + block, _ := testutils.RandomBlock(rng, 1) + mock.On("BlockDataByHash", block.Hash(), block.Hash(), eth.ChainIDFromUInt64(chainID)).Return(block) + actual := oracle.BlockDataByHash(block.Hash(), block.Hash(), eth.ChainIDFromUInt64(chainID)) + require.Equal(t, block, actual) + mock.AssertExpectations(t) + }) + t.Run("ReceiptsByBlockHash", func(t *testing.T) { + mock := new(OracleMock) + oracle := NewConsolidateOracle(mock, &interopTypes.TransitionState{}) + block, receipts := testutils.RandomBlock(rng, 1) + mock.On("ReceiptsByBlockHash", block.Hash(), eth.ChainIDFromUInt64(chainID)).Return(block, types.Receipts(receipts)) + actual, actualReceipts := oracle.ReceiptsByBlockHash(block.Hash(), eth.ChainIDFromUInt64(chainID)) + require.Equal(t, block, actual) + require.Equal(t, types.Receipts(receipts), actualReceipts) + mock.AssertExpectations(t) + }) + t.Run("NodeByHash", func(t *testing.T) { + mock := new(OracleMock) + oracle := NewConsolidateOracle(mock, &interopTypes.TransitionState{}) + node := []byte{12, 3, 4} + hash := common.Hash{0xaa} + mock.On("NodeByHash", hash, eth.ChainIDFromUInt64(chainID)).Return(node) + actual := oracle.NodeByHash(hash, eth.ChainIDFromUInt64(chainID)) + require.Equal(t, node, actual) + mock.AssertExpectations(t) + }) + t.Run("CodeByHash", func(t *testing.T) { + mock := new(OracleMock) + oracle := NewConsolidateOracle(mock, &interopTypes.TransitionState{}) + code := []byte{12, 3, 4} + hash := common.Hash{0xaa} + mock.On("CodeByHash", hash, eth.ChainIDFromUInt64(chainID)).Return(code) + actual := oracle.CodeByHash(hash, eth.ChainIDFromUInt64(chainID)) + require.Equal(t, code, actual) + mock.AssertExpectations(t) + }) + t.Run("TransitionStateByRoot", func(t *testing.T) { + mock := new(OracleMock) + ts := &interopTypes.TransitionState{SuperRoot: []byte{0xbb}} + oracle := NewConsolidateOracle(mock, ts) + root := common.Hash{0xaa} + actual := oracle.TransitionStateByRoot(root) + require.Equal(t, ts, actual) + mock.AssertExpectations(t) + }) + t.Run("Hinter", func(t *testing.T) { + mock := new(OracleMock) + oracle := NewConsolidateOracle(mock, &interopTypes.TransitionState{}) + mock.On("Hinter").Return(new(OracleHinterStub)) + actual := oracle.Hinter() + require.Equal(t, new(OracleHinterStub), actual) + mock.AssertExpectations(t) + }) +} + +func TestConsolidateOracle_WithConsolidatedData(t *testing.T) { + chainID := eth.ChainIDFromUInt64(48294) + rng := rand.New(rand.NewSource(1)) + block, receipts := testutils.RandomBlock(rng, 1) + mock := new(OracleMock) + defer mock.AssertExpectations(t) + + t.Run("BlockByHash", func(t *testing.T) { + oracle := NewConsolidateOracle(mock, &interopTypes.TransitionState{}) + db := oracle.KeyValueStore() + storeBlock(t, db, block, receipts) + + actual := oracle.BlockByHash(block.Hash(), chainID) + require.Equal(t, block.Hash(), actual.Hash()) + + require.Equal(t, len(block.Transactions()), len(actual.Transactions())) + for i := range block.Transactions() { + require.Equal(t, block.Transactions()[i].Hash(), actual.Transactions()[i].Hash()) + } + }) + t.Run("ReceiptsByBlockHash", func(t *testing.T) { + oracle := NewConsolidateOracle(mock, &interopTypes.TransitionState{}) + db := oracle.KeyValueStore() + storeBlock(t, db, block, receipts) + + actual, actualReceipts := oracle.ReceiptsByBlockHash(block.Hash(), chainID) + require.Equal(t, block.Hash(), actual.Hash()) + require.Equal(t, len(receipts), len(actualReceipts)) + for i := range receipts { + // compare only consensus fields + a, err := receipts[i].MarshalBinary() + require.NoError(t, err) + b, err := actualReceipts[i].MarshalBinary() + require.NoError(t, err) + require.Equal(t, a, b) + } + }) + t.Run("OutputByRoot", func(t *testing.T) { + oracle := NewConsolidateOracle(mock, &interopTypes.TransitionState{}) + db := oracle.KeyValueStore() + key := common.Hash{0xaa} + dbKey := preimage.Keccak256Key(key).PreimageKey() + output := testutils.RandomOutputV0(rng).Marshal() + require.NoError(t, db.Put(dbKey[:], output)) + + actual := oracle.OutputByRoot(key, chainID) + require.Equal(t, output, actual.Marshal()) + }) + t.Run("NodeByHash", func(t *testing.T) { + oracle := NewConsolidateOracle(mock, &interopTypes.TransitionState{}) + db := oracle.KeyValueStore() + key := common.Hash{0xaa} + require.NoError(t, db.Put(key[:], []byte{1, 2, 3})) + storeBlock(t, db, block, receipts) + + actual := oracle.NodeByHash(key, chainID) + require.Equal(t, []byte{1, 2, 3}, actual) + }) + t.Run("CodeByHash", func(t *testing.T) { + oracle := NewConsolidateOracle(mock, &interopTypes.TransitionState{}) + db := oracle.KeyValueStore() + key := common.Hash{0xaa} + require.NoError(t, db.Put(key[:], []byte{1, 2, 3})) + storeBlock(t, db, block, receipts) + + actual := oracle.CodeByHash(key, chainID) + require.Equal(t, []byte{1, 2, 3}, actual) + }) +} + +func storeBlock(t *testing.T, kv l2.KeyValueStore, block *types.Block, receipts types.Receipts) { + opaqueRcpts, err := eth.EncodeReceipts(receipts) + require.NoError(t, err) + _, nodes := mpt.WriteTrie(opaqueRcpts) + for _, node := range nodes { + key := preimage.Keccak256Key(crypto.Keccak256Hash(node)).PreimageKey() + require.NoError(t, kv.Put(key[:], node)) + } + + opaqueTxs, err := eth.EncodeTransactions(block.Transactions()) + require.NoError(t, err) + _, txsNodes := mpt.WriteTrie(opaqueTxs) + for _, p := range txsNodes { + key := preimage.Keccak256Key(crypto.Keccak256Hash(p)).PreimageKey() + require.NoError(t, kv.Put(key[:], p)) + } + + headerRlp, err := rlp.EncodeToBytes(block.Header()) + require.NoError(t, err) + key := preimage.Keccak256Key(block.Hash()).PreimageKey() + require.NoError(t, kv.Put(key[:], headerRlp)) +} + +type OracleMock struct { + mock.Mock +} + +var _ l2.Oracle = &OracleMock{} + +func (o *OracleMock) BlockByHash(blockHash common.Hash, chainID eth.ChainID) *gethTypes.Block { + args := o.Called(blockHash, chainID) + return args.Get(0).(*gethTypes.Block) +} + +func (o *OracleMock) OutputByRoot(root common.Hash, chainID eth.ChainID) eth.Output { + args := o.Called(root, chainID) + return args.Get(0).(eth.Output) +} + +func (o *OracleMock) BlockDataByHash(agreedBlockHash, blockHash common.Hash, chainID eth.ChainID) *gethTypes.Block { + args := o.Called(agreedBlockHash, blockHash, chainID) + return args.Get(0).(*gethTypes.Block) +} + +func (o *OracleMock) TransitionStateByRoot(root common.Hash) *interopTypes.TransitionState { + args := o.Called(root) + return args.Get(0).(*interopTypes.TransitionState) +} + +func (o *OracleMock) ReceiptsByBlockHash(blockHash common.Hash, chainID eth.ChainID) (*gethTypes.Block, gethTypes.Receipts) { + args := o.Called(blockHash, chainID) + return args.Get(0).(*gethTypes.Block), args.Get(1).(gethTypes.Receipts) +} + +func (o *OracleMock) NodeByHash(nodeHash common.Hash, chainID eth.ChainID) []byte { + args := o.Called(nodeHash, chainID) + return args.Get(0).([]byte) +} + +func (o *OracleMock) CodeByHash(codeHash common.Hash, chainID eth.ChainID) []byte { + args := o.Called(codeHash, chainID) + return args.Get(0).([]byte) +} + +func (o *OracleMock) Hinter() l2Types.OracleHinter { + args := o.Called() + return args.Get(0).(l2Types.OracleHinter) +} + +type OracleHinterStub struct { +} + +var _ l2Types.OracleHinter = &OracleHinterStub{} + +func (o *OracleHinterStub) HintBlockExecution(parentBlockHash common.Hash, attr eth.PayloadAttributes, chainID eth.ChainID) { +} + +func (o *OracleHinterStub) HintWithdrawalsRoot(blockHash common.Hash, chainID eth.ChainID) { +} diff --git a/op-program/client/tasks/deposits_block.go b/op-program/client/tasks/deposits_block.go index 4c3fd0c99c64c..fceaaf485a928 100644 --- a/op-program/client/tasks/deposits_block.go +++ b/op-program/client/tasks/deposits_block.go @@ -6,12 +6,14 @@ import ( "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup/interop/managed" + preimage "github.com/ethereum-optimism/optimism/op-preimage" "github.com/ethereum-optimism/optimism/op-program/client/l1" "github.com/ethereum-optimism/optimism/op-program/client/l2" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb/memorydb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" @@ -20,6 +22,7 @@ import ( // BuildDepositOnlyBlock builds a deposits-only block replacement for the specified optimistic block and returns the block hash and output root // for the new block. // The specified l2OutputRoot must be the output root of the optimistic block's parent. +// The provided l2.KeyValueStore is used to store state diff that's applied to the deposits-only block, which also includes the output root and any transaction and receipt trie nodes of the new block. func BuildDepositOnlyBlock( logger log.Logger, cfg *rollup.Config, @@ -29,8 +32,9 @@ func BuildDepositOnlyBlock( agreedL2OutputRoot eth.Bytes32, l1Oracle l1.Oracle, l2Oracle l2.Oracle, + db l2.KeyValueStore, ) (common.Hash, eth.Bytes32, error) { - engineBackend, err := l2.NewOracleBackedL2Chain(logger, l2Oracle, l1Oracle, l2Cfg, common.Hash(agreedL2OutputRoot), memorydb.New()) + engineBackend, err := l2.NewOracleBackedL2Chain(logger, l2Oracle, l1Oracle, l2Cfg, common.Hash(agreedL2OutputRoot), db) if err != nil { return common.Hash{}, eth.Bytes32{}, fmt.Errorf("failed to create oracle-backed L2 chain: %w", err) } @@ -81,11 +85,21 @@ func BuildDepositOnlyBlock( return common.Hash{}, eth.Bytes32{}, fmt.Errorf("failed to update forkchoice state (no build): %w", eth.ForkchoiceUpdateErr(result.PayloadStatus)) } - blockHash, outputRoot, err := l2Source.L2OutputRoot(uint64(payload.ExecutionPayload.BlockNumber)) + if err := storeBlockData(payload.ExecutionPayload.BlockHash, db, engineBackend); err != nil { + return common.Hash{}, eth.Bytes32{}, fmt.Errorf("failed to write tx/receipts trie nodes: %w", err) + } + output, err := l2Source.L2OutputAtBlockHash(payload.ExecutionPayload.BlockHash) if err != nil { - return common.Hash{}, eth.Bytes32{}, fmt.Errorf("failed to get L2 output root: %w", err) + return common.Hash{}, eth.Bytes32{}, fmt.Errorf("failed to get L2 output: %w", err) } - return blockHash, outputRoot, nil + marshaledOutput := output.Marshal() + outputRoot := eth.Bytes32(crypto.Keccak256Hash(marshaledOutput)) + outputRootKey := preimage.Keccak256Key(outputRoot).PreimageKey() + if err := db.Put(outputRootKey[:], marshaledOutput); err != nil { + return common.Hash{}, eth.Bytes32{}, fmt.Errorf("failed to store L2 output: %w", err) + } + + return payload.ExecutionPayload.BlockHash, outputRoot, nil } func getL2Output(logger log.Logger, cfg *rollup.Config, l2Cfg *params.ChainConfig, l2Oracle l2.Oracle, l1Oracle l1.Oracle, block *types.Block) (*eth.OutputV0, error) { diff --git a/op-program/client/tasks/derive.go b/op-program/client/tasks/derive.go index 2c2d24ec78314..db251f50e2ff4 100644 --- a/op-program/client/tasks/derive.go +++ b/op-program/client/tasks/derive.go @@ -68,7 +68,7 @@ func RunDerivation( logger.Info("Derivation complete", "head", result) if options.StoreBlockData { - if err := storeBlockData(result, db, engineBackend); err != nil { + if err := storeBlockData(result.Hash, db, engineBackend); err != nil { return DerivationResult{}, fmt.Errorf("failed to write trie nodes: %w", err) } logger.Info("Trie nodes written") @@ -88,16 +88,16 @@ func loadOutputRoot(l2ClaimBlockNum uint64, head eth.L2BlockRef, src L2Source) ( }, nil } -func storeBlockData(derived eth.L2BlockRef, db l2.KeyValueStore, backend engineapi.CachingEngineBackend) error { - block := backend.GetBlockByHash(derived.Hash) +func storeBlockData(derivedBlockHash common.Hash, db l2.KeyValueStore, backend engineapi.CachingEngineBackend) error { + block := backend.GetBlockByHash(derivedBlockHash) if block == nil { - return fmt.Errorf("derived block %v is missing", derived.Hash) + return fmt.Errorf("derived block %v is missing", derivedBlockHash) } headerRLP, err := rlp.EncodeToBytes(block.Header()) if err != nil { return fmt.Errorf("failed to encode block header: %w", err) } - blockHashKey := preimage.Keccak256Key(derived.Hash).PreimageKey() + blockHashKey := preimage.Keccak256Key(derivedBlockHash).PreimageKey() if err := db.Put(blockHashKey[:], headerRLP); err != nil { return fmt.Errorf("failed to store block header: %w", err) }