From 9c858501a6dfa33e57c85e3a108d1e2a45947e64 Mon Sep 17 00:00:00 2001 From: inphi <mlaw2501@gmail.com> Date: Mon, 24 Jul 2023 15:42:03 -0400 Subject: [PATCH] op-program: Boot program using output root The output root replaces the L2 head hash at boot as the agreed upon point to run the fault proof program. The L2 head is still necessary for derivation but is redundant as it's contained in the l2 output. --- op-e2e/system_fpp_test.go | 17 ++++-- op-node/eth/output.go | 70 ++++++++++++++++++++++++ op-node/eth/output_test.go | 26 +++++++++ op-node/rollup/output_root.go | 33 +++++------ op-node/sources/l1_client.go | 7 +++ op-node/testutils/mock_l1.go | 9 +++ op-program/client/boot.go | 8 +-- op-program/client/boot_test.go | 6 +- op-program/client/l1/cache.go | 29 +++++++--- op-program/client/l1/cache_test.go | 27 +++++++++ op-program/client/l1/client.go | 4 ++ op-program/client/l1/hints.go | 9 +++ op-program/client/l1/oracle.go | 13 +++++ op-program/client/l1/test/stub_oracle.go | 20 +++++-- op-program/client/program.go | 13 ++++- op-program/host/cmd/main_test.go | 15 +++-- op-program/host/config/config.go | 28 ++++++++-- op-program/host/config/config_test.go | 3 +- op-program/host/flags/flags.go | 6 ++ op-program/host/host_test.go | 6 +- op-program/host/kvstore/local.go | 6 +- op-program/host/kvstore/local_test.go | 4 +- op-program/host/prefetcher/prefetcher.go | 7 +++ op-program/host/prefetcher/retry.go | 14 +++++ op-program/verify/cmd/goerli.go | 13 +++++ 25 files changed, 333 insertions(+), 60 deletions(-) create mode 100644 op-node/eth/output_test.go diff --git a/op-e2e/system_fpp_test.go b/op-e2e/system_fpp_test.go index 78ada4d7c..74f3f5d3c 100644 --- a/op-e2e/system_fpp_test.go +++ b/op-e2e/system_fpp_test.go @@ -86,7 +86,10 @@ func testVerifyL2OutputRootEmptyBlock(t *testing.T, detached bool) { require.NoError(t, waitForSafeHead(ctx, receipt.BlockNumber.Uint64(), rollupClient)) t.Logf("Capture current L2 head as agreed starting point. l2Head=%x l2BlockNumber=%v", receipt.BlockHash, receipt.BlockNumber) - l2Head := receipt.BlockHash + agreedL2Output, err := rollupClient.OutputAtBlock(ctx, receipt.BlockNumber.Uint64()) + require.NoError(t, err, "could not retrieve l2 agreed block") + l2Head := agreedL2Output.BlockRef.Hash + l2OutputRoot := agreedL2Output.OutputRoot t.Log("=====Stopping batch submitter=====") err = sys.BatchSubmitter.Stop(ctx) @@ -136,6 +139,7 @@ func testVerifyL2OutputRootEmptyBlock(t *testing.T, detached bool) { testFaultProofProgramScenario(t, ctx, sys, &FaultProofProgramTestScenario{ L1Head: l1Head, L2Head: l2Head, + L2OutputRoot: common.Hash(l2OutputRoot), L2Claim: common.Hash(l2Claim), L2ClaimBlockNumber: l2ClaimBlockNumber, Detached: detached, @@ -181,9 +185,12 @@ func testVerifyL2OutputRoot(t *testing.T, detached bool) { }) t.Log("Capture current L2 head as agreed starting point") - l2AgreedBlock, err := l2Seq.BlockByNumber(ctx, nil) + latestBlock, err := l2Seq.BlockByNumber(ctx, nil) + require.NoError(t, err) + agreedL2Output, err := rollupClient.OutputAtBlock(ctx, latestBlock.NumberU64()) require.NoError(t, err, "could not retrieve l2 agreed block") - l2Head := l2AgreedBlock.Hash() + l2Head := agreedL2Output.BlockRef.Hash + l2OutputRoot := agreedL2Output.OutputRoot t.Log("Sending transactions to modify existing state, within challenged period") SendDepositTx(t, cfg, l1Client, l2Seq, opts, func(l2Opts *DepositTxOpts) { @@ -214,6 +221,7 @@ func testVerifyL2OutputRoot(t *testing.T, detached bool) { testFaultProofProgramScenario(t, ctx, sys, &FaultProofProgramTestScenario{ L1Head: l1Head, L2Head: l2Head, + L2OutputRoot: common.Hash(l2OutputRoot), L2Claim: common.Hash(l2Claim), L2ClaimBlockNumber: l2ClaimBlockNumber, Detached: detached, @@ -223,6 +231,7 @@ func testVerifyL2OutputRoot(t *testing.T, detached bool) { type FaultProofProgramTestScenario struct { L1Head common.Hash L2Head common.Hash + L2OutputRoot common.Hash L2Claim common.Hash L2ClaimBlockNumber uint64 Detached bool @@ -231,7 +240,7 @@ type FaultProofProgramTestScenario struct { // testFaultProofProgramScenario runs the fault proof program in several contexts, given a test scenario. func testFaultProofProgramScenario(t *testing.T, ctx context.Context, sys *System, s *FaultProofProgramTestScenario) { preimageDir := t.TempDir() - fppConfig := oppconf.NewConfig(sys.RollupConfig, sys.L2GenesisCfg.Config, s.L1Head, s.L2Head, common.Hash(s.L2Claim), s.L2ClaimBlockNumber) + fppConfig := oppconf.NewConfig(sys.RollupConfig, sys.L2GenesisCfg.Config, s.L1Head, s.L2Head, s.L2OutputRoot, common.Hash(s.L2Claim), s.L2ClaimBlockNumber) fppConfig.L1URL = sys.NodeEndpoint("l1") fppConfig.L2URL = sys.NodeEndpoint("sequencer") fppConfig.DataDir = preimageDir diff --git a/op-node/eth/output.go b/op-node/eth/output.go index 9594775d4..d7c972395 100644 --- a/op-node/eth/output.go +++ b/op-node/eth/output.go @@ -1,7 +1,10 @@ package eth import ( + "errors" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" ) type OutputResponse struct { @@ -12,3 +15,70 @@ type OutputResponse struct { StateRoot common.Hash `json:"stateRoot"` Status *SyncStatus `json:"syncStatus"` } + +var ( + ErrInvalidOutput = errors.New("invalid output") + ErrInvalidOutputVersion = errors.New("invalid output version") + + OutputVersionV0 = Bytes32{} +) + +type Output interface { + // Version returns the version of the L2 output + Version() Bytes32 + + // Marshal a L2 output into a byte slice for hashing + Marshal() []byte +} + +type OutputV0 struct { + StateRoot Bytes32 + MessagePasserStorageRoot Bytes32 + BlockHash common.Hash +} + +func (o *OutputV0) Version() Bytes32 { + return OutputVersionV0 +} + +func (o *OutputV0) Marshal() []byte { + var buf [128]byte + version := o.Version() + copy(buf[:32], version[:]) + copy(buf[32:], o.StateRoot[:]) + copy(buf[64:], o.MessagePasserStorageRoot[:]) + copy(buf[96:], o.BlockHash[:]) + return buf[:] +} + +// OutputRoot returns the keccak256 hash of the marshaled L2 output +func OutputRoot(output Output) Bytes32 { + marshaled := output.Marshal() + return Bytes32(crypto.Keccak256Hash(marshaled)) +} + +func UnmarshalOutput(data []byte) (Output, error) { + if len(data) < 32 { + return nil, ErrInvalidOutput + } + var ver Bytes32 + copy(ver[:], data[:32]) + switch ver { + case OutputVersionV0: + return unmarshalOutputV0(data) + default: + return nil, ErrInvalidOutputVersion + } +} + +func unmarshalOutputV0(data []byte) (*OutputV0, error) { + if len(data) < 128 { + return nil, ErrInvalidOutput + } + var output OutputV0 + // data[:32] is the version + copy(output.StateRoot[:], data[32:64]) + copy(output.MessagePasserStorageRoot[:], data[64:96]) + copy(output.BlockHash[:], data[96:128]) + return &output, nil +} diff --git a/op-node/eth/output_test.go b/op-node/eth/output_test.go new file mode 100644 index 000000000..e53d0a063 --- /dev/null +++ b/op-node/eth/output_test.go @@ -0,0 +1,26 @@ +package eth + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestOutputV0Codec(t *testing.T) { + output := OutputV0{ + StateRoot: Bytes32{1, 2, 3}, + MessagePasserStorageRoot: Bytes32{4, 5, 6}, + BlockHash: common.Hash{7, 8, 9}, + } + marshaled := output.Marshal() + unmarshaled, err := UnmarshalOutput(marshaled) + require.NoError(t, err) + unmarshaledV0 := unmarshaled.(*OutputV0) + require.Equal(t, output, *unmarshaledV0) + + _, err = UnmarshalOutput([]byte{0: 0xA, 32: 0xA}) + require.ErrorIs(t, err, ErrInvalidOutputVersion) + _, err = UnmarshalOutput([]byte{64: 0xA}) + require.ErrorIs(t, err, ErrInvalidOutput) +} diff --git a/op-node/rollup/output_root.go b/op-node/rollup/output_root.go index 0ab222848..74b2420bf 100644 --- a/op-node/rollup/output_root.go +++ b/op-node/rollup/output_root.go @@ -5,32 +5,33 @@ import ( "github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-node/eth" - "github.com/ethereum/go-ethereum/crypto" ) -var NilProof = errors.New("Output root proof is nil") +var ErrNilProof = errors.New("output root proof is nil") // ComputeL2OutputRoot computes the L2 output root by hashing an output root proof. func ComputeL2OutputRoot(proofElements *bindings.TypesOutputRootProof) (eth.Bytes32, error) { if proofElements == nil { - return eth.Bytes32{}, NilProof + return eth.Bytes32{}, ErrNilProof } - digest := crypto.Keccak256Hash( - proofElements.Version[:], - proofElements.StateRoot[:], - proofElements.MessagePasserStorageRoot[:], - proofElements.LatestBlockhash[:], - ) - return eth.Bytes32(digest), nil + if proofElements.Version != [32]byte{} { + return eth.Bytes32{}, errors.New("unsupported output root version") + } + l2Output := eth.OutputV0{ + StateRoot: eth.Bytes32(proofElements.StateRoot), + MessagePasserStorageRoot: proofElements.MessagePasserStorageRoot, + BlockHash: proofElements.LatestBlockhash, + } + return eth.OutputRoot(&l2Output), nil } func ComputeL2OutputRootV0(block eth.BlockInfo, storageRoot [32]byte) (eth.Bytes32, error) { - var l2OutputRootVersion eth.Bytes32 // it's zero for now - return ComputeL2OutputRoot(&bindings.TypesOutputRootProof{ - Version: l2OutputRootVersion, - StateRoot: block.Root(), + stateRoot := block.Root() + l2Output := eth.OutputV0{ + StateRoot: eth.Bytes32(stateRoot), MessagePasserStorageRoot: storageRoot, - LatestBlockhash: block.Hash(), - }) + BlockHash: block.Hash(), + } + return eth.OutputRoot(&l2Output), nil } diff --git a/op-node/sources/l1_client.go b/op-node/sources/l1_client.go index d3d1d7438..9cc0764f9 100644 --- a/op-node/sources/l1_client.go +++ b/op-node/sources/l1_client.go @@ -115,3 +115,10 @@ func (s *L1Client) L1BlockRefByHash(ctx context.Context, hash common.Hash) (eth. s.l1BlockRefsCache.Add(ref.Hash, ref) return ref, nil } + +func (s *L1Client) L2OutputByRoot(ctx context.Context, l2OutputRoot common.Hash) (eth.Output, error) { + // TODO(inphi): Fetch Output from preset. Or directly from the oracle + //return s.OutputByRoot(ctx, l2OutputRoot) + var output eth.Output + return output, nil +} diff --git a/op-node/testutils/mock_l1.go b/op-node/testutils/mock_l1.go index 79543d494..7e0b653d9 100644 --- a/op-node/testutils/mock_l1.go +++ b/op-node/testutils/mock_l1.go @@ -37,3 +37,12 @@ func (m *MockL1Source) L1BlockRefByHash(ctx context.Context, hash common.Hash) ( func (m *MockL1Source) ExpectL1BlockRefByHash(hash common.Hash, ref eth.L1BlockRef, err error) { m.Mock.On("L1BlockRefByHash", hash).Once().Return(ref, &err) } + +func (m *MockL1Source) L2OutputByRoot(ctx context.Context, root common.Hash) (eth.Output, error) { + out := m.Mock.MethodCalled("L2OutputByRoot", root) + return out[0].(eth.Output), *out[1].(*error) +} + +func (m *MockL1Source) ExpectL2OutputByRoot(root common.Hash, output eth.Output, err error) { + m.Mock.On("L2OutputByRoot", root).Once().Return(output, &err) +} diff --git a/op-program/client/boot.go b/op-program/client/boot.go index 84f54c405..836195162 100644 --- a/op-program/client/boot.go +++ b/op-program/client/boot.go @@ -12,7 +12,7 @@ import ( const ( L1HeadLocalIndex preimage.LocalIndexKey = iota + 1 - L2HeadLocalIndex + L2OutputRootLocalIndex L2ClaimLocalIndex L2ClaimBlockNumberLocalIndex L2ChainConfigLocalIndex @@ -21,7 +21,7 @@ const ( type BootInfo struct { L1Head common.Hash - L2Head common.Hash + L2OutputRoot common.Hash L2Claim common.Hash L2ClaimBlockNumber uint64 L2ChainConfig *params.ChainConfig @@ -42,7 +42,7 @@ func NewBootstrapClient(r oracleClient) *BootstrapClient { func (br *BootstrapClient) BootInfo() *BootInfo { l1Head := common.BytesToHash(br.r.Get(L1HeadLocalIndex)) - l2Head := common.BytesToHash(br.r.Get(L2HeadLocalIndex)) + l2OutputRoot := common.BytesToHash(br.r.Get(L2OutputRootLocalIndex)) l2Claim := common.BytesToHash(br.r.Get(L2ClaimLocalIndex)) l2ClaimBlockNumber := binary.BigEndian.Uint64(br.r.Get(L2ClaimBlockNumberLocalIndex)) l2ChainConfig := new(params.ChainConfig) @@ -58,7 +58,7 @@ func (br *BootstrapClient) BootInfo() *BootInfo { return &BootInfo{ L1Head: l1Head, - L2Head: l2Head, + L2OutputRoot: l2OutputRoot, L2Claim: l2Claim, L2ClaimBlockNumber: l2ClaimBlockNumber, L2ChainConfig: l2ChainConfig, diff --git a/op-program/client/boot_test.go b/op-program/client/boot_test.go index 284b38456..d58a850b0 100644 --- a/op-program/client/boot_test.go +++ b/op-program/client/boot_test.go @@ -15,7 +15,7 @@ import ( func TestBootstrapClient(t *testing.T) { bootInfo := &BootInfo{ L1Head: common.HexToHash("0x1111"), - L2Head: common.HexToHash("0x2222"), + L2OutputRoot: common.HexToHash("0x2222"), L2Claim: common.HexToHash("0x3333"), L2ClaimBlockNumber: 1, L2ChainConfig: params.GoerliChainConfig, @@ -34,8 +34,8 @@ func (o *mockBoostrapOracle) Get(key preimage.Key) []byte { switch key.PreimageKey() { case L1HeadLocalIndex.PreimageKey(): return o.b.L1Head[:] - case L2HeadLocalIndex.PreimageKey(): - return o.b.L2Head[:] + case L2OutputRootLocalIndex.PreimageKey(): + return o.b.L2OutputRoot[:] case L2ClaimLocalIndex.PreimageKey(): return o.b.L2Claim[:] case L2ClaimBlockNumberLocalIndex.PreimageKey(): diff --git a/op-program/client/l1/cache.go b/op-program/client/l1/cache.go index 862331707..163c5ef30 100644 --- a/op-program/client/l1/cache.go +++ b/op-program/client/l1/cache.go @@ -12,21 +12,24 @@ const cacheSize = 2000 // CachingOracle is an implementation of Oracle that delegates to another implementation, adding caching of all results type CachingOracle struct { - oracle Oracle - blocks *simplelru.LRU[common.Hash, eth.BlockInfo] - txs *simplelru.LRU[common.Hash, types.Transactions] - rcpts *simplelru.LRU[common.Hash, types.Receipts] + oracle Oracle + blocks *simplelru.LRU[common.Hash, eth.BlockInfo] + txs *simplelru.LRU[common.Hash, types.Transactions] + rcpts *simplelru.LRU[common.Hash, types.Receipts] + outputs *simplelru.LRU[common.Hash, eth.Output] } func NewCachingOracle(oracle Oracle) *CachingOracle { blockLRU, _ := simplelru.NewLRU[common.Hash, eth.BlockInfo](cacheSize, nil) txsLRU, _ := simplelru.NewLRU[common.Hash, types.Transactions](cacheSize, nil) rcptsLRU, _ := simplelru.NewLRU[common.Hash, types.Receipts](cacheSize, nil) + outputsLRU, _ := simplelru.NewLRU[common.Hash, eth.Output](cacheSize, nil) return &CachingOracle{ - oracle: oracle, - blocks: blockLRU, - txs: txsLRU, - rcpts: rcptsLRU, + oracle: oracle, + blocks: blockLRU, + txs: txsLRU, + rcpts: rcptsLRU, + outputs: outputsLRU, } } @@ -61,3 +64,13 @@ func (o *CachingOracle) ReceiptsByBlockHash(blockHash common.Hash) (eth.BlockInf o.rcpts.Add(blockHash, rcpts) return block, rcpts } + +func (o *CachingOracle) L2OutputByRoot(l2OutputRoot common.Hash) eth.Output { + output, ok := o.outputs.Get(l2OutputRoot) + if ok { + return output + } + output = o.oracle.L2OutputByRoot(l2OutputRoot) + o.outputs.Add(l2OutputRoot, output) + return output +} diff --git a/op-program/client/l1/cache_test.go b/op-program/client/l1/cache_test.go index b2b20b64f..09f32f032 100644 --- a/op-program/client/l1/cache_test.go +++ b/op-program/client/l1/cache_test.go @@ -7,6 +7,7 @@ import ( "github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/testutils" "github.com/ethereum-optimism/optimism/op-program/client/l1/test" + "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" ) @@ -71,3 +72,29 @@ func TestCachingOracle_ReceiptsByBlockHash(t *testing.T) { require.Equal(t, eth.BlockToInfo(block), actualBlock) require.EqualValues(t, rcpts, actualRcpts) } + +func TestCachingOracle_L2OutputByRoot(t *testing.T) { + rng := rand.New(rand.NewSource(1)) + stub := test.NewStubOracle(t) + oracle := NewCachingOracle(stub) + block, _ := testutils.RandomBlock(rng, 3) + + var storageRoot [32]byte + rng.Read(storageRoot[:]) + l2Output := ð.OutputV0{ + StateRoot: eth.Bytes32(block.Root()), + MessagePasserStorageRoot: storageRoot, + BlockHash: block.Hash(), + } + l2OutputRoot := common.Hash(eth.OutputRoot(l2Output)) + + // Initial call retrieves from the stub + stub.L2Outputs[l2OutputRoot] = l2Output + result := oracle.L2OutputByRoot(l2OutputRoot) + require.Equal(t, l2Output.Marshal(), result) + + // Later calls should retrieve from cache + delete(stub.L2Outputs, l2OutputRoot) + result = oracle.L2OutputByRoot(l2OutputRoot) + require.Equal(t, l2Output.Marshal(), result) +} diff --git a/op-program/client/l1/client.go b/op-program/client/l1/client.go index 6d878d928..0395112b2 100644 --- a/op-program/client/l1/client.go +++ b/op-program/client/l1/client.go @@ -80,3 +80,7 @@ func (o *OracleL1Client) InfoAndTxsByHash(ctx context.Context, hash common.Hash) info, txs := o.oracle.TransactionsByBlockHash(hash) return info, txs, nil } + +func (o *OracleL1Client) L2OutputByRoot(ctx context.Context, root common.Hash) (eth.Output, error) { + return o.oracle.L2OutputByRoot(root), nil +} diff --git a/op-program/client/l1/hints.go b/op-program/client/l1/hints.go index 1383f6a29..b9421870e 100644 --- a/op-program/client/l1/hints.go +++ b/op-program/client/l1/hints.go @@ -10,6 +10,7 @@ const ( HintL1BlockHeader = "l1-block-header" HintL1Transactions = "l1-transactions" HintL1Receipts = "l1-receipts" + HintL2Output = "l1-l2-output" ) type BlockHeaderHint common.Hash @@ -35,3 +36,11 @@ var _ preimage.Hint = ReceiptsHint{} func (l ReceiptsHint) Hint() string { return HintL1Receipts + " " + (common.Hash)(l).String() } + +type L2OutputHint common.Hash + +var _ preimage.Hint = L2OutputHint{} + +func (l L2OutputHint) Hint() string { + return HintL2Output + " " + (common.Hash)(l).String() +} diff --git a/op-program/client/l1/oracle.go b/op-program/client/l1/oracle.go index 3b68e7a2f..2ec8ed1ab 100644 --- a/op-program/client/l1/oracle.go +++ b/op-program/client/l1/oracle.go @@ -21,6 +21,9 @@ type Oracle interface { // ReceiptsByBlockHash retrieves the receipts from the block with the given hash. ReceiptsByBlockHash(blockHash common.Hash) (eth.BlockInfo, types.Receipts) + + // L2OutputByRoot retrieves the L2 output for the given L2 output root. + L2OutputByRoot(l2OutputRoot common.Hash) eth.Output } // PreimageOracle implements Oracle using by interfacing with the pure preimage.Oracle @@ -86,3 +89,13 @@ func (p *PreimageOracle) ReceiptsByBlockHash(blockHash common.Hash) (eth.BlockIn return info, receipts } + +func (p *PreimageOracle) L2OutputByRoot(l2OutputRoot common.Hash) eth.Output { + p.hint.Hint(L2OutputHint(l2OutputRoot)) + data := p.oracle.Get(preimage.Keccak256Key(l2OutputRoot)) + output, err := eth.UnmarshalOutput(data) + if err != nil { + panic(fmt.Errorf("invalidd L2 output data for root %s: %w", l2OutputRoot, err)) + } + return output +} diff --git a/op-program/client/l1/test/stub_oracle.go b/op-program/client/l1/test/stub_oracle.go index 1ec03d945..e1f23823b 100644 --- a/op-program/client/l1/test/stub_oracle.go +++ b/op-program/client/l1/test/stub_oracle.go @@ -19,14 +19,18 @@ type StubOracle struct { // Rcpts maps Block hash to receipts Rcpts map[common.Hash]types.Receipts + + // L2Outputs maps L2 output roots to L2 outputs + L2Outputs map[common.Hash]eth.Output } func NewStubOracle(t *testing.T) *StubOracle { return &StubOracle{ - t: t, - Blocks: make(map[common.Hash]eth.BlockInfo), - Txs: make(map[common.Hash]types.Transactions), - Rcpts: make(map[common.Hash]types.Receipts), + t: t, + Blocks: make(map[common.Hash]eth.BlockInfo), + Txs: make(map[common.Hash]types.Transactions), + Rcpts: make(map[common.Hash]types.Receipts), + L2Outputs: make(map[common.Hash]eth.Output), } } func (o StubOracle) HeaderByBlockHash(blockHash common.Hash) eth.BlockInfo { @@ -52,3 +56,11 @@ func (o StubOracle) ReceiptsByBlockHash(blockHash common.Hash) (eth.BlockInfo, t } return o.HeaderByBlockHash(blockHash), rcpts } + +func (o StubOracle) L2OutputByRoot(l2OutputRoot common.Hash) eth.Output { + output, ok := o.L2Outputs[l2OutputRoot] + if !ok { + o.t.Fatalf("unknown output %s", l2OutputRoot) + } + return output +} diff --git a/op-program/client/program.go b/op-program/client/program.go index 5ed19cad4..8684ace0f 100644 --- a/op-program/client/program.go +++ b/op-program/client/program.go @@ -53,7 +53,7 @@ func RunProgram(logger log.Logger, preimageOracle io.ReadWriter, preimageHinter bootInfo.RollupConfig, bootInfo.L2ChainConfig, bootInfo.L1Head, - bootInfo.L2Head, + bootInfo.L2OutputRoot, bootInfo.L2Claim, bootInfo.L2ClaimBlockNumber, l1PreimageOracle, @@ -62,8 +62,17 @@ func RunProgram(logger log.Logger, preimageOracle io.ReadWriter, preimageHinter } // runDerivation executes the L2 state transition, given a minimal interface to retrieve data. -func runDerivation(logger log.Logger, cfg *rollup.Config, l2Cfg *params.ChainConfig, l1Head common.Hash, l2Head common.Hash, l2Claim common.Hash, l2ClaimBlockNum uint64, l1Oracle l1.Oracle, l2Oracle l2.Oracle) error { +func runDerivation(logger log.Logger, cfg *rollup.Config, l2Cfg *params.ChainConfig, l1Head common.Hash, l2OutputRoot common.Hash, l2Claim common.Hash, l2ClaimBlockNum uint64, l1Oracle l1.Oracle, l2Oracle l2.Oracle) error { l1Source := l1.NewOracleL1Client(logger, l1Oracle, l1Head) + output, err := l1Source.L2OutputByRoot(context.Background(), l2OutputRoot) + if err != nil { + return fmt.Errorf("failed to find L2 output for %s: %w", l2OutputRoot, err) + } + outputV0, ok := output.(*eth.OutputV0) + if !ok { + return fmt.Errorf("unsupported L2 output version: %d", output.Version()) + } + l2Head := outputV0.BlockHash engineBackend, err := l2.NewOracleBackedL2Chain(logger, l2Oracle, l2Cfg, l2Head) if err != nil { return fmt.Errorf("failed to create oracle-backed L2 chain: %w", err) diff --git a/op-program/host/cmd/main_test.go b/op-program/host/cmd/main_test.go index 93991bc02..dc54340f0 100644 --- a/op-program/host/cmd/main_test.go +++ b/op-program/host/cmd/main_test.go @@ -20,10 +20,12 @@ var ( l1HeadValue = common.HexToHash("0x111111").Hex() l2HeadValue = common.HexToHash("0x222222").Hex() l2ClaimValue = common.HexToHash("0x333333").Hex() + l2OutputRoot = common.HexToHash("0x444444").Hex() l2ClaimBlockNumber = uint64(1203) // Note: This is actually the L1 goerli genesis config. Just using it as an arbitrary, valid genesis config - l2Genesis = core.DefaultGoerliGenesisBlock() - l2GenesisConfig = l2Genesis.Config + l2Genesis = core.DefaultGoerliGenesisBlock() + l2GenesisConfig = l2Genesis.Config + l2OutputOracleAddress = common.HexToAddress("0x1234567890123456789012345678901234567890").Hex() ) func TestLogLevel(t *testing.T) { @@ -48,6 +50,7 @@ func TestDefaultCLIOptionsMatchDefaultConfig(t *testing.T) { config.OPGoerliChainConfig, common.HexToHash(l1HeadValue), common.HexToHash(l2HeadValue), + common.HexToHash(l2OutputRoot), common.HexToHash(l2ClaimValue), l2ClaimBlockNumber) require.Equal(t, defaultCfg, cfg) @@ -121,14 +124,14 @@ func TestL2Genesis(t *testing.T) { }) } -func TestL2Head(t *testing.T) { +func TestL2OutputRoot(t *testing.T) { t.Run("Required", func(t *testing.T) { - verifyArgsInvalid(t, "flag l2.head is required", addRequiredArgsExcept("--l2.head")) + verifyArgsInvalid(t, "flag l2.outputroot is required", addRequiredArgsExcept("--l2.head")) }) t.Run("Valid", func(t *testing.T) { - cfg := configForArgs(t, replaceRequiredArg("--l2.head", l2HeadValue)) - require.Equal(t, common.HexToHash(l2HeadValue), cfg.L2Head) + cfg := configForArgs(t, replaceRequiredArg("--l2.outputroot", l2HeadValue)) + require.Equal(t, common.HexToHash(l2HeadValue), cfg.L2OutputRoot) }) t.Run("Invalid", func(t *testing.T) { diff --git a/op-program/host/config/config.go b/op-program/host/config/config.go index c13d8198e..07be7e9b5 100644 --- a/op-program/host/config/config.go +++ b/op-program/host/config/config.go @@ -22,6 +22,7 @@ var ( ErrMissingL2Genesis = errors.New("missing l2 genesis") ErrInvalidL1Head = errors.New("invalid l1 head") ErrInvalidL2Head = errors.New("invalid l2 head") + ErrInvalidL2OutputRoot = errors.New("invalid l2 output root") ErrL1AndL2Inconsistent = errors.New("l1 and l2 options must be specified together or both omitted") ErrInvalidL2Claim = errors.New("invalid l2 claim") ErrInvalidL2ClaimBlock = errors.New("invalid l2 claim block number") @@ -41,9 +42,12 @@ type Config struct { L1TrustRPC bool L1RPCKind sources.RPCProviderKind - // L2Head is the agreed L2 block to start derivation from + // L2Head is the l2 block hash contained in the L2 Output referenced by the L2OutputRoot + // TODO(inphi): This can be made optional with hardcoded rollup configs and output oracle addresses L2Head common.Hash - L2URL string + // L2OutputRoot is the agreed L2 output root to start derivation from + L2OutputRoot common.Hash + L2URL string // L2Claim is the claimed L2 output root to verify L2Claim common.Hash // L2ClaimBlockNumber is the block number the claimed L2 output root is from @@ -70,8 +74,8 @@ func (c *Config) Check() error { if c.L1Head == (common.Hash{}) { return ErrInvalidL1Head } - if c.L2Head == (common.Hash{}) { - return ErrInvalidL2Head + if c.L2OutputRoot == (common.Hash{}) { + return ErrInvalidL2OutputRoot } if c.L2Claim == (common.Hash{}) { return ErrInvalidL2Claim @@ -99,12 +103,21 @@ func (c *Config) FetchingEnabled() bool { } // NewConfig creates a Config with all optional values set to the CLI default value -func NewConfig(rollupCfg *rollup.Config, l2Genesis *params.ChainConfig, l1Head common.Hash, l2Head common.Hash, l2Claim common.Hash, l2ClaimBlockNum uint64) *Config { +func NewConfig( + rollupCfg *rollup.Config, + l2Genesis *params.ChainConfig, + l1Head common.Hash, + l2Head common.Hash, + l2OutputRoot common.Hash, + l2Claim common.Hash, + l2ClaimBlockNum uint64, +) *Config { return &Config{ Rollup: rollupCfg, L2ChainConfig: l2Genesis, L1Head: l1Head, L2Head: l2Head, + L2OutputRoot: l2OutputRoot, L2Claim: l2Claim, L2ClaimBlockNumber: l2ClaimBlockNum, L1RPCKind: sources.RPCKindBasic, @@ -123,6 +136,10 @@ func NewConfigFromCLI(log log.Logger, ctx *cli.Context) (*Config, error) { if l2Head == (common.Hash{}) { return nil, ErrInvalidL2Head } + l2OutputRoot := common.HexToHash(ctx.String(flags.L2Head.Name)) + if l2OutputRoot == (common.Hash{}) { + return nil, ErrInvalidL2OutputRoot + } l2Claim := common.HexToHash(ctx.String(flags.L2Claim.Name)) if l2Claim == (common.Hash{}) { return nil, ErrInvalidL2Claim @@ -152,6 +169,7 @@ func NewConfigFromCLI(log log.Logger, ctx *cli.Context) (*Config, error) { L2URL: ctx.String(flags.L2NodeAddr.Name), L2ChainConfig: l2ChainConfig, L2Head: l2Head, + L2OutputRoot: l2OutputRoot, L2Claim: l2Claim, L2ClaimBlockNumber: l2ClaimBlockNum, L1Head: l1Head, diff --git a/op-program/host/config/config_test.go b/op-program/host/config/config_test.go index 024f6cdd8..ca520148b 100644 --- a/op-program/host/config/config_test.go +++ b/op-program/host/config/config_test.go @@ -16,6 +16,7 @@ var ( validL1Head = common.Hash{0xaa} validL2Head = common.Hash{0xbb} validL2Claim = common.Hash{0xcc} + validL2OutputRoot = common.Hash{0xdd} validL2ClaimBlockNum = uint64(15) ) @@ -151,7 +152,7 @@ func TestRejectExecAndServerMode(t *testing.T) { } func validConfig() *Config { - cfg := NewConfig(validRollupConfig, validL2Genesis, validL1Head, validL2Head, validL2Claim, validL2ClaimBlockNum) + cfg := NewConfig(validRollupConfig, validL2Genesis, validL1Head, validL2Head, validL2OutputRoot, validL2Claim, validL2ClaimBlockNum) cfg.DataDir = "/tmp/configTest" return cfg } diff --git a/op-program/host/flags/flags.go b/op-program/host/flags/flags.go index b334d3dd1..0b28f6d37 100644 --- a/op-program/host/flags/flags.go +++ b/op-program/host/flags/flags.go @@ -50,6 +50,11 @@ var ( Usage: "Hash of the agreed L2 block to start derivation from", EnvVars: prefixEnvVars("L2_HEAD"), } + L2OutputRoot = &cli.StringFlag{ + Name: "l2.outputroot", + Usage: "L2 Output Root at l2.head", + EnvVars: prefixEnvVars("L2_OUTPUT_ROOT"), + } L2Claim = &cli.StringFlag{ Name: "l2.claim", Usage: "Claimed L2 output root to validate", @@ -103,6 +108,7 @@ var Flags []cli.Flag var requiredFlags = []cli.Flag{ L1Head, L2Head, + L2OutputRoot, L2Claim, L2BlockNumber, } diff --git a/op-program/host/host_test.go b/op-program/host/host_test.go index 5d90d1373..207417487 100644 --- a/op-program/host/host_test.go +++ b/op-program/host/host_test.go @@ -23,7 +23,8 @@ func TestServerMode(t *testing.T) { dir := t.TempDir() l1Head := common.Hash{0x11} - cfg := config.NewConfig(&chaincfg.Goerli, config.OPGoerliChainConfig, l1Head, common.Hash{0x22}, common.Hash{0x33}, 1000) + l2OutputRoot := common.Hash{0x33} + cfg := config.NewConfig(&chaincfg.Goerli, config.OPGoerliChainConfig, l1Head, common.Hash{0x22}, l2OutputRoot, common.Hash{0x44}, 1000) cfg.DataDir = dir cfg.ServerMode = true @@ -43,7 +44,8 @@ func TestServerMode(t *testing.T) { hClient := preimage.NewHintWriter(hintClient) l1PreimageOracle := l1.NewPreimageOracle(pClient, hClient) - require.Equal(t, l1Head.Bytes(), pClient.Get(client.L1HeadLocalIndex), "Should get preimages") + require.Equal(t, l1Head.Bytes(), pClient.Get(client.L1HeadLocalIndex), "Should get l1 head preimages") + require.Equal(t, l2OutputRoot.Bytes(), pClient.Get(client.L2OutputRootLocalIndex), "Should get l2 output root preimages") // Should exit when a preimage is unavailable require.Panics(t, func() { diff --git a/op-program/host/kvstore/local.go b/op-program/host/kvstore/local.go index ce234e642..22104579a 100644 --- a/op-program/host/kvstore/local.go +++ b/op-program/host/kvstore/local.go @@ -19,7 +19,7 @@ func NewLocalPreimageSource(config *config.Config) *LocalPreimageSource { var ( l1HeadKey = client.L1HeadLocalIndex.PreimageKey() - l2HeadKey = client.L2HeadLocalIndex.PreimageKey() + l2OutputRootKey = client.L2OutputRootLocalIndex.PreimageKey() l2ClaimKey = client.L2ClaimLocalIndex.PreimageKey() l2ClaimBlockNumberKey = client.L2ClaimBlockNumberLocalIndex.PreimageKey() l2ChainConfigKey = client.L2ChainConfigLocalIndex.PreimageKey() @@ -30,8 +30,8 @@ func (s *LocalPreimageSource) Get(key common.Hash) ([]byte, error) { switch [32]byte(key) { case l1HeadKey: return s.config.L1Head.Bytes(), nil - case l2HeadKey: - return s.config.L2Head.Bytes(), nil + case l2OutputRootKey: + return s.config.L2OutputRoot.Bytes(), nil case l2ClaimKey: return s.config.L2Claim.Bytes(), nil case l2ClaimBlockNumberKey: diff --git a/op-program/host/kvstore/local_test.go b/op-program/host/kvstore/local_test.go index 11d8a4245..ad9766364 100644 --- a/op-program/host/kvstore/local_test.go +++ b/op-program/host/kvstore/local_test.go @@ -17,7 +17,7 @@ func TestLocalPreimageSource(t *testing.T) { cfg := &config.Config{ Rollup: &chaincfg.Goerli, L1Head: common.HexToHash("0x1111"), - L2Head: common.HexToHash("0x2222"), + L2OutputRoot: common.HexToHash("0x2222"), L2Claim: common.HexToHash("0x3333"), L2ClaimBlockNumber: 1234, L2ChainConfig: params.GoerliChainConfig, @@ -29,7 +29,7 @@ func TestLocalPreimageSource(t *testing.T) { expected []byte }{ {"L1Head", l1HeadKey, cfg.L1Head.Bytes()}, - {"L2Head", l2HeadKey, cfg.L2Head.Bytes()}, + {"L2OutputRoot", l2OutputRootKey, cfg.L2OutputRoot.Bytes()}, {"L2Claim", l2ClaimKey, cfg.L2Claim.Bytes()}, {"L2ClaimBlockNumber", l2ClaimBlockNumberKey, binary.BigEndian.AppendUint64(nil, cfg.L2ClaimBlockNumber)}, {"Rollup", rollupKey, asJson(t, cfg.Rollup)}, diff --git a/op-program/host/prefetcher/prefetcher.go b/op-program/host/prefetcher/prefetcher.go index 0f4abc0ff..205f05340 100644 --- a/op-program/host/prefetcher/prefetcher.go +++ b/op-program/host/prefetcher/prefetcher.go @@ -23,6 +23,7 @@ type L1Source interface { InfoByHash(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, error) InfoAndTxsByHash(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, types.Transactions, error) FetchReceipts(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, types.Receipts, error) + L2OutputByRoot(ctx context.Context, l2OutputRoot common.Hash) (eth.Output, error) } type L2Source interface { @@ -98,6 +99,12 @@ func (p *Prefetcher) prefetch(ctx context.Context, hint string) error { return fmt.Errorf("failed to fetch L1 block %s receipts: %w", hash, err) } return p.storeReceipts(receipts) + case l1.HintL2Output: + output, err := p.l1Fetcher.L2OutputByRoot(ctx, hash) + if err != nil { + return fmt.Errorf("failed to fetch L2 output root %s: %w", hash, err) + } + return p.kvStore.Put(preimage.Keccak256Key(hash).PreimageKey(), output.Marshal()) case l2.HintL2BlockHeader: header, txs, err := p.l2Fetcher.InfoAndTxsByHash(ctx, hash) if err != nil { diff --git a/op-program/host/prefetcher/retry.go b/op-program/host/prefetcher/retry.go index dad6ebad1..b461675b6 100644 --- a/op-program/host/prefetcher/retry.go +++ b/op-program/host/prefetcher/retry.go @@ -73,6 +73,20 @@ func (s *RetryingL1Source) FetchReceipts(ctx context.Context, blockHash common.H return info, rcpts, err } +func (s *RetryingL1Source) L2OutputByRoot(ctx context.Context, root common.Hash) (eth.Output, error) { + var output eth.Output + err := backoff.DoCtx(ctx, maxAttempts, s.strategy, func() error { + o, err := s.source.L2OutputByRoot(ctx, root) + if err != nil { + s.logger.Warn("Failed to fetch l2 output", "root", root, "err", err) + return err + } + output = o + return nil + }) + return output, err +} + var _ L1Source = (*RetryingL1Source)(nil) type RetryingL2Source struct { diff --git a/op-program/verify/cmd/goerli.go b/op-program/verify/cmd/goerli.go index 3725553ce..098f44bff 100644 --- a/op-program/verify/cmd/goerli.go +++ b/op-program/verify/cmd/goerli.go @@ -101,6 +101,18 @@ func Run(l1RpcUrl string, l2RpcUrl string, l2OracleAddr common.Address) error { return fmt.Errorf("retrieve agreed l2 block: %w", err) } l2Head := l2AgreedBlock.Hash() + agreedOutputIndex, err := outputOracle.GetL2OutputIndexAfter(callOpts, l2AgreedBlock.Number()) + if err != nil { + return fmt.Errorf("failed to output index after agreed block") + } + agreedOutput, err := outputOracle.GetL2Output(callOpts, agreedOutputIndex) + if err != nil { + return fmt.Errorf("retrieve agreed output: %w", err) + } + if agreedOutput.OutputRoot == output.OutputRoot { + // TODO(inphi): Don't return here but keep searching preceeding blocks for a different output + return fmt.Errorf("agreed output is the same as the output claim") + } temp, err := os.MkdirTemp("", "oracledata") if err != nil { @@ -120,6 +132,7 @@ func Run(l1RpcUrl string, l2RpcUrl string, l2OracleAddr common.Address) error { "--datadir", temp, "--l1.head", l1Head.Hex(), "--l2.head", l2Head.Hex(), + "--l2.outputroot", common.Bytes2Hex(agreedOutput.OutputRoot[:]), "--l2.claim", l2Claim.Hex(), "--l2.blocknumber", l2BlockNumber.String(), } -- 2.23.0