Commit 3e60c7e1 authored by Inphi's avatar Inphi Committed by GitHub

Revert "Revert: "op-program: Boot program using output root"" (#6544)

* Revert "Revert "op-program: Boot program using output root""

This reverts commit 40e952d8.

* fix(ctb): Differential test for Ouptut V0 only

* ci: Add op-node to contracts-bedrock check-changed
parent b2d75bf0
...@@ -322,7 +322,7 @@ jobs: ...@@ -322,7 +322,7 @@ jobs:
steps: steps:
- checkout - checkout
- check-changed: - check-changed:
patterns: contracts-bedrock patterns: contracts-bedrock,op-node
- run: - run:
name: print forge version name: print forge version
command: forge --version command: forge --version
...@@ -347,7 +347,7 @@ jobs: ...@@ -347,7 +347,7 @@ jobs:
steps: steps:
- checkout - checkout
- check-changed: - check-changed:
patterns: contracts-bedrock patterns: contracts-bedrock,op-node
- run: - run:
name: print forge version name: print forge version
command: forge --version command: forge --version
...@@ -371,7 +371,7 @@ jobs: ...@@ -371,7 +371,7 @@ jobs:
keys: keys:
- pnpm-packages-v2-{{ checksum "pnpm-lock.yaml" }} - pnpm-packages-v2-{{ checksum "pnpm-lock.yaml" }}
- check-changed: - check-changed:
patterns: contracts-bedrock patterns: contracts-bedrock,op-node
- run: - run:
name: build contracts name: build contracts
command: pnpm build command: pnpm build
......
...@@ -54,6 +54,7 @@ type L2API interface { ...@@ -54,6 +54,7 @@ type L2API interface {
InfoByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, error) InfoByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, error)
// GetProof returns a proof of the account, it may return a nil result without error if the address was not found. // GetProof returns a proof of the account, it may return a nil result without error if the address was not found.
GetProof(ctx context.Context, address common.Address, storage []common.Hash, blockTag string) (*eth.AccountResult, error) GetProof(ctx context.Context, address common.Address, storage []common.Hash, blockTag string) (*eth.AccountResult, error)
OutputV0AtBlock(ctx context.Context, blockHash common.Hash) (*eth.OutputV0, error)
} }
func NewL2Verifier(t Testing, log log.Logger, l1 derive.L1Fetcher, eng L2API, cfg *rollup.Config, syncCfg *sync.Config) *L2Verifier { func NewL2Verifier(t Testing, log log.Logger, l1 derive.L1Fetcher, eng L2API, cfg *rollup.Config, syncCfg *sync.Config) *L2Verifier {
......
...@@ -86,7 +86,10 @@ func testVerifyL2OutputRootEmptyBlock(t *testing.T, detached bool) { ...@@ -86,7 +86,10 @@ func testVerifyL2OutputRootEmptyBlock(t *testing.T, detached bool) {
require.NoError(t, waitForSafeHead(ctx, receipt.BlockNumber.Uint64(), rollupClient)) 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) 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=====") t.Log("=====Stopping batch submitter=====")
err = sys.BatchSubmitter.Stop(ctx) err = sys.BatchSubmitter.Stop(ctx)
...@@ -136,6 +139,7 @@ func testVerifyL2OutputRootEmptyBlock(t *testing.T, detached bool) { ...@@ -136,6 +139,7 @@ func testVerifyL2OutputRootEmptyBlock(t *testing.T, detached bool) {
testFaultProofProgramScenario(t, ctx, sys, &FaultProofProgramTestScenario{ testFaultProofProgramScenario(t, ctx, sys, &FaultProofProgramTestScenario{
L1Head: l1Head, L1Head: l1Head,
L2Head: l2Head, L2Head: l2Head,
L2OutputRoot: common.Hash(l2OutputRoot),
L2Claim: common.Hash(l2Claim), L2Claim: common.Hash(l2Claim),
L2ClaimBlockNumber: l2ClaimBlockNumber, L2ClaimBlockNumber: l2ClaimBlockNumber,
Detached: detached, Detached: detached,
...@@ -181,9 +185,12 @@ func testVerifyL2OutputRoot(t *testing.T, detached bool) { ...@@ -181,9 +185,12 @@ func testVerifyL2OutputRoot(t *testing.T, detached bool) {
}) })
t.Log("Capture current L2 head as agreed starting point") 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") 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") t.Log("Sending transactions to modify existing state, within challenged period")
SendDepositTx(t, cfg, l1Client, l2Seq, opts, func(l2Opts *DepositTxOpts) { SendDepositTx(t, cfg, l1Client, l2Seq, opts, func(l2Opts *DepositTxOpts) {
...@@ -214,6 +221,7 @@ func testVerifyL2OutputRoot(t *testing.T, detached bool) { ...@@ -214,6 +221,7 @@ func testVerifyL2OutputRoot(t *testing.T, detached bool) {
testFaultProofProgramScenario(t, ctx, sys, &FaultProofProgramTestScenario{ testFaultProofProgramScenario(t, ctx, sys, &FaultProofProgramTestScenario{
L1Head: l1Head, L1Head: l1Head,
L2Head: l2Head, L2Head: l2Head,
L2OutputRoot: common.Hash(l2OutputRoot),
L2Claim: common.Hash(l2Claim), L2Claim: common.Hash(l2Claim),
L2ClaimBlockNumber: l2ClaimBlockNumber, L2ClaimBlockNumber: l2ClaimBlockNumber,
Detached: detached, Detached: detached,
...@@ -223,6 +231,7 @@ func testVerifyL2OutputRoot(t *testing.T, detached bool) { ...@@ -223,6 +231,7 @@ func testVerifyL2OutputRoot(t *testing.T, detached bool) {
type FaultProofProgramTestScenario struct { type FaultProofProgramTestScenario struct {
L1Head common.Hash L1Head common.Hash
L2Head common.Hash L2Head common.Hash
L2OutputRoot common.Hash
L2Claim common.Hash L2Claim common.Hash
L2ClaimBlockNumber uint64 L2ClaimBlockNumber uint64
Detached bool Detached bool
...@@ -231,7 +240,7 @@ type FaultProofProgramTestScenario struct { ...@@ -231,7 +240,7 @@ type FaultProofProgramTestScenario struct {
// testFaultProofProgramScenario runs the fault proof program in several contexts, given a test scenario. // 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) { func testFaultProofProgramScenario(t *testing.T, ctx context.Context, sys *System, s *FaultProofProgramTestScenario) {
preimageDir := t.TempDir() 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.L1URL = sys.NodeEndpoint("l1")
fppConfig.L2URL = sys.NodeEndpoint("sequencer") fppConfig.L2URL = sys.NodeEndpoint("sequencer")
fppConfig.DataDir = preimageDir fppConfig.DataDir = preimageDir
......
package eth package eth
import ( import (
"errors"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
) )
type OutputResponse struct { type OutputResponse struct {
...@@ -12,3 +15,70 @@ type OutputResponse struct { ...@@ -12,3 +15,70 @@ type OutputResponse struct {
StateRoot common.Hash `json:"stateRoot"` StateRoot common.Hash `json:"stateRoot"`
Status *SyncStatus `json:"syncStatus"` 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
}
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)
}
...@@ -4,12 +4,10 @@ import ( ...@@ -4,12 +4,10 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/version" "github.com/ethereum-optimism/optimism/op-node/version"
...@@ -20,6 +18,7 @@ type l2EthClient interface { ...@@ -20,6 +18,7 @@ type l2EthClient interface {
// GetProof returns a proof of the account, it may return a nil result without error if the address was not found. // GetProof returns a proof of the account, it may return a nil result without error if the address was not found.
// Optionally keys of the account storage trie can be specified to include with corresponding values in the proof. // Optionally keys of the account storage trie can be specified to include with corresponding values in the proof.
GetProof(ctx context.Context, address common.Address, storage []common.Hash, blockTag string) (*eth.AccountResult, error) GetProof(ctx context.Context, address common.Address, storage []common.Hash, blockTag string) (*eth.AccountResult, error)
OutputV0AtBlock(ctx context.Context, blockHash common.Hash) (*eth.OutputV0, error)
} }
type driverClient interface { type driverClient interface {
...@@ -99,40 +98,16 @@ func (n *nodeAPI) OutputAtBlock(ctx context.Context, number hexutil.Uint64) (*et ...@@ -99,40 +98,16 @@ func (n *nodeAPI) OutputAtBlock(ctx context.Context, number hexutil.Uint64) (*et
return nil, fmt.Errorf("failed to get L2 block ref with sync status: %w", err) return nil, fmt.Errorf("failed to get L2 block ref with sync status: %w", err)
} }
head, err := n.client.InfoByHash(ctx, ref.Hash) output, err := n.client.OutputV0AtBlock(ctx, ref.Hash)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get L2 block by hash %s: %w", ref, err) return nil, fmt.Errorf("failed to get L2 output at block %s: %w", ref, err)
} }
if head == nil {
return nil, ethereum.NotFound
}
proof, err := n.client.GetProof(ctx, predeploys.L2ToL1MessagePasserAddr, []common.Hash{}, ref.Hash.String())
if err != nil {
return nil, fmt.Errorf("failed to get contract proof at block %s: %w", ref, err)
}
if proof == nil {
return nil, fmt.Errorf("proof %w", ethereum.NotFound)
}
// make sure that the proof (including storage hash) that we retrieved is correct by verifying it against the state-root
if err := proof.Verify(head.Root()); err != nil {
n.log.Error("invalid withdrawal root detected in block", "stateRoot", head.Root(), "blocknum", number, "msg", err)
return nil, fmt.Errorf("invalid withdrawal root hash, state root was %s: %w", head.Root(), err)
}
var l2OutputRootVersion eth.Bytes32 // it's zero for now
l2OutputRoot, err := rollup.ComputeL2OutputRootV0(head, proof.StorageHash)
if err != nil {
n.log.Error("Error computing L2 output root, nil ptr passed to hashing function")
return nil, err
}
return &eth.OutputResponse{ return &eth.OutputResponse{
Version: l2OutputRootVersion, Version: output.Version(),
OutputRoot: l2OutputRoot, OutputRoot: eth.OutputRoot(output),
BlockRef: ref, BlockRef: ref,
WithdrawalStorageRoot: proof.StorageHash, WithdrawalStorageRoot: common.Hash(output.MessagePasserStorageRoot),
StateRoot: head.Root(), StateRoot: common.Hash(output.StateRoot),
Status: status, Status: status,
}, nil }, nil
} }
......
...@@ -16,7 +16,6 @@ import ( ...@@ -16,7 +16,6 @@ import (
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/metrics" "github.com/ethereum-optimism/optimism/op-node/metrics"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
...@@ -84,17 +83,6 @@ func TestOutputAtBlock(t *testing.T) { ...@@ -84,17 +83,6 @@ func TestOutputAtBlock(t *testing.T) {
} }
l2Client := &testutils.MockL2Client{} l2Client := &testutils.MockL2Client{}
info := &testutils.MockBlockInfo{
InfoHash: header.Hash(),
InfoParentHash: header.ParentHash,
InfoCoinbase: header.Coinbase,
InfoRoot: header.Root,
InfoNum: header.Number.Uint64(),
InfoTime: header.Time,
InfoMixDigest: header.MixDigest,
InfoBaseFee: header.BaseFee,
InfoReceiptRoot: header.ReceiptHash,
}
ref := eth.L2BlockRef{ ref := eth.L2BlockRef{
Hash: header.Hash(), Hash: header.Hash(),
Number: header.Number.Uint64(), Number: header.Number.Uint64(),
...@@ -103,8 +91,12 @@ func TestOutputAtBlock(t *testing.T) { ...@@ -103,8 +91,12 @@ func TestOutputAtBlock(t *testing.T) {
L1Origin: eth.BlockID{}, L1Origin: eth.BlockID{},
SequenceNumber: 0, SequenceNumber: 0,
} }
l2Client.ExpectInfoByHash(common.HexToHash("0x8512bee03061475e4b069171f7b406097184f16b22c3f5c97c0abfc49591c524"), info, nil) output := &eth.OutputV0{
l2Client.ExpectGetProof(predeploys.L2ToL1MessagePasserAddr, []common.Hash{}, "0x8512bee03061475e4b069171f7b406097184f16b22c3f5c97c0abfc49591c524", &result, nil) StateRoot: eth.Bytes32(header.Root),
BlockHash: ref.Hash,
MessagePasserStorageRoot: eth.Bytes32(result.StorageHash),
}
l2Client.ExpectOutputV0AtBlock(common.HexToHash("0x8512bee03061475e4b069171f7b406097184f16b22c3f5c97c0abfc49591c524"), output, nil)
drClient := &mockDriverClient{} drClient := &mockDriverClient{}
status := randomSyncStatus(rand.New(rand.NewSource(123))) status := randomSyncStatus(rand.New(rand.NewSource(123)))
......
...@@ -5,32 +5,33 @@ import ( ...@@ -5,32 +5,33 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-node/eth" "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. // ComputeL2OutputRoot computes the L2 output root by hashing an output root proof.
func ComputeL2OutputRoot(proofElements *bindings.TypesOutputRootProof) (eth.Bytes32, error) { func ComputeL2OutputRoot(proofElements *bindings.TypesOutputRootProof) (eth.Bytes32, error) {
if proofElements == nil { if proofElements == nil {
return eth.Bytes32{}, NilProof return eth.Bytes32{}, ErrNilProof
} }
digest := crypto.Keccak256Hash( if eth.Bytes32(proofElements.Version) != eth.OutputVersionV0 {
proofElements.Version[:], return eth.Bytes32{}, errors.New("unsupported output root version")
proofElements.StateRoot[:], }
proofElements.MessagePasserStorageRoot[:], l2Output := eth.OutputV0{
proofElements.LatestBlockhash[:], StateRoot: eth.Bytes32(proofElements.StateRoot),
) MessagePasserStorageRoot: proofElements.MessagePasserStorageRoot,
return eth.Bytes32(digest), nil BlockHash: proofElements.LatestBlockhash,
}
return eth.OutputRoot(&l2Output), nil
} }
func ComputeL2OutputRootV0(block eth.BlockInfo, storageRoot [32]byte) (eth.Bytes32, error) { func ComputeL2OutputRootV0(block eth.BlockInfo, storageRoot [32]byte) (eth.Bytes32, error) {
var l2OutputRootVersion eth.Bytes32 // it's zero for now stateRoot := block.Root()
return ComputeL2OutputRoot(&bindings.TypesOutputRootProof{ l2Output := eth.OutputV0{
Version: l2OutputRootVersion, StateRoot: eth.Bytes32(stateRoot),
StateRoot: block.Root(),
MessagePasserStorageRoot: storageRoot, MessagePasserStorageRoot: storageRoot,
LatestBlockhash: block.Hash(), BlockHash: block.Hash(),
}) }
return eth.OutputRoot(&l2Output), nil
} }
...@@ -10,6 +10,7 @@ import ( ...@@ -10,6 +10,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-node/client" "github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
...@@ -165,3 +166,31 @@ func (s *L2Client) SystemConfigByL2Hash(ctx context.Context, hash common.Hash) ( ...@@ -165,3 +166,31 @@ func (s *L2Client) SystemConfigByL2Hash(ctx context.Context, hash common.Hash) (
s.systemConfigsCache.Add(hash, cfg) s.systemConfigsCache.Add(hash, cfg)
return cfg, nil return cfg, nil
} }
func (s *L2Client) OutputV0AtBlock(ctx context.Context, blockHash common.Hash) (*eth.OutputV0, error) {
head, err := s.InfoByHash(ctx, blockHash)
if err != nil {
return nil, fmt.Errorf("failed to get L2 block by hash: %w", err)
}
if head == nil {
return nil, ethereum.NotFound
}
proof, err := s.GetProof(ctx, predeploys.L2ToL1MessagePasserAddr, []common.Hash{}, blockHash.String())
if err != nil {
return nil, fmt.Errorf("failed to get contract proof at block %s: %w", blockHash, err)
}
if proof == nil {
return nil, fmt.Errorf("proof %w", ethereum.NotFound)
}
// make sure that the proof (including storage hash) that we retrieved is correct by verifying it against the state-root
if err := proof.Verify(head.Root()); err != nil {
return nil, fmt.Errorf("invalid withdrawal root hash, state root was %s: %w", head.Root(), err)
}
stateRoot := head.Root()
return &eth.OutputV0{
StateRoot: eth.Bytes32(stateRoot),
MessagePasserStorageRoot: eth.Bytes32(proof.StorageHash),
BlockHash: blockHash,
}, nil
}
...@@ -43,3 +43,11 @@ func (m *MockL2Client) SystemConfigByL2Hash(ctx context.Context, hash common.Has ...@@ -43,3 +43,11 @@ func (m *MockL2Client) SystemConfigByL2Hash(ctx context.Context, hash common.Has
func (m *MockL2Client) ExpectSystemConfigByL2Hash(hash common.Hash, cfg eth.SystemConfig, err error) { func (m *MockL2Client) ExpectSystemConfigByL2Hash(hash common.Hash, cfg eth.SystemConfig, err error) {
m.Mock.On("SystemConfigByL2Hash", hash).Once().Return(cfg, &err) m.Mock.On("SystemConfigByL2Hash", hash).Once().Return(cfg, &err)
} }
func (m *MockL2Client) OutputV0AtBlock(ctx context.Context, blockHash common.Hash) (*eth.OutputV0, error) {
return m.Mock.MethodCalled("OutputV0AtBlock", blockHash).Get(0).(*eth.OutputV0), nil
}
func (m *MockL2Client) ExpectOutputV0AtBlock(blockHash common.Hash, output *eth.OutputV0, err error) {
m.Mock.On("OutputV0AtBlock", blockHash).Once().Return(output, &err)
}
...@@ -270,3 +270,11 @@ func RandomOutputResponse(rng *rand.Rand) *eth.OutputResponse { ...@@ -270,3 +270,11 @@ func RandomOutputResponse(rng *rand.Rand) *eth.OutputResponse {
}, },
} }
} }
func RandomOutputV0(rng *rand.Rand) *eth.OutputV0 {
return &eth.OutputV0{
StateRoot: eth.Bytes32(RandomHash(rng)),
MessagePasserStorageRoot: eth.Bytes32(RandomHash(rng)),
BlockHash: RandomHash(rng),
}
}
...@@ -12,7 +12,7 @@ import ( ...@@ -12,7 +12,7 @@ import (
const ( const (
L1HeadLocalIndex preimage.LocalIndexKey = iota + 1 L1HeadLocalIndex preimage.LocalIndexKey = iota + 1
L2HeadLocalIndex L2OutputRootLocalIndex
L2ClaimLocalIndex L2ClaimLocalIndex
L2ClaimBlockNumberLocalIndex L2ClaimBlockNumberLocalIndex
L2ChainConfigLocalIndex L2ChainConfigLocalIndex
...@@ -21,7 +21,7 @@ const ( ...@@ -21,7 +21,7 @@ const (
type BootInfo struct { type BootInfo struct {
L1Head common.Hash L1Head common.Hash
L2Head common.Hash L2OutputRoot common.Hash
L2Claim common.Hash L2Claim common.Hash
L2ClaimBlockNumber uint64 L2ClaimBlockNumber uint64
L2ChainConfig *params.ChainConfig L2ChainConfig *params.ChainConfig
...@@ -42,7 +42,7 @@ func NewBootstrapClient(r oracleClient) *BootstrapClient { ...@@ -42,7 +42,7 @@ func NewBootstrapClient(r oracleClient) *BootstrapClient {
func (br *BootstrapClient) BootInfo() *BootInfo { func (br *BootstrapClient) BootInfo() *BootInfo {
l1Head := common.BytesToHash(br.r.Get(L1HeadLocalIndex)) 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)) l2Claim := common.BytesToHash(br.r.Get(L2ClaimLocalIndex))
l2ClaimBlockNumber := binary.BigEndian.Uint64(br.r.Get(L2ClaimBlockNumberLocalIndex)) l2ClaimBlockNumber := binary.BigEndian.Uint64(br.r.Get(L2ClaimBlockNumberLocalIndex))
l2ChainConfig := new(params.ChainConfig) l2ChainConfig := new(params.ChainConfig)
...@@ -58,7 +58,7 @@ func (br *BootstrapClient) BootInfo() *BootInfo { ...@@ -58,7 +58,7 @@ func (br *BootstrapClient) BootInfo() *BootInfo {
return &BootInfo{ return &BootInfo{
L1Head: l1Head, L1Head: l1Head,
L2Head: l2Head, L2OutputRoot: l2OutputRoot,
L2Claim: l2Claim, L2Claim: l2Claim,
L2ClaimBlockNumber: l2ClaimBlockNumber, L2ClaimBlockNumber: l2ClaimBlockNumber,
L2ChainConfig: l2ChainConfig, L2ChainConfig: l2ChainConfig,
......
...@@ -15,7 +15,7 @@ import ( ...@@ -15,7 +15,7 @@ import (
func TestBootstrapClient(t *testing.T) { func TestBootstrapClient(t *testing.T) {
bootInfo := &BootInfo{ bootInfo := &BootInfo{
L1Head: common.HexToHash("0x1111"), L1Head: common.HexToHash("0x1111"),
L2Head: common.HexToHash("0x2222"), L2OutputRoot: common.HexToHash("0x2222"),
L2Claim: common.HexToHash("0x3333"), L2Claim: common.HexToHash("0x3333"),
L2ClaimBlockNumber: 1, L2ClaimBlockNumber: 1,
L2ChainConfig: params.GoerliChainConfig, L2ChainConfig: params.GoerliChainConfig,
...@@ -34,8 +34,8 @@ func (o *mockBoostrapOracle) Get(key preimage.Key) []byte { ...@@ -34,8 +34,8 @@ func (o *mockBoostrapOracle) Get(key preimage.Key) []byte {
switch key.PreimageKey() { switch key.PreimageKey() {
case L1HeadLocalIndex.PreimageKey(): case L1HeadLocalIndex.PreimageKey():
return o.b.L1Head[:] return o.b.L1Head[:]
case L2HeadLocalIndex.PreimageKey(): case L2OutputRootLocalIndex.PreimageKey():
return o.b.L2Head[:] return o.b.L2OutputRoot[:]
case L2ClaimLocalIndex.PreimageKey(): case L2ClaimLocalIndex.PreimageKey():
return o.b.L2Claim[:] return o.b.L2Claim[:]
case L2ClaimBlockNumberLocalIndex.PreimageKey(): case L2ClaimBlockNumberLocalIndex.PreimageKey():
......
package l2 package l2
import ( import (
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/hashicorp/golang-lru/v2/simplelru" "github.com/hashicorp/golang-lru/v2/simplelru"
...@@ -13,21 +14,24 @@ const nodeCacheSize = 100_000 ...@@ -13,21 +14,24 @@ const nodeCacheSize = 100_000
const codeCacheSize = 10_000 const codeCacheSize = 10_000
type CachingOracle struct { type CachingOracle struct {
oracle Oracle oracle Oracle
blocks *simplelru.LRU[common.Hash, *types.Block] blocks *simplelru.LRU[common.Hash, *types.Block]
nodes *simplelru.LRU[common.Hash, []byte] nodes *simplelru.LRU[common.Hash, []byte]
codes *simplelru.LRU[common.Hash, []byte] codes *simplelru.LRU[common.Hash, []byte]
outputs *simplelru.LRU[common.Hash, eth.Output]
} }
func NewCachingOracle(oracle Oracle) *CachingOracle { func NewCachingOracle(oracle Oracle) *CachingOracle {
blockLRU, _ := simplelru.NewLRU[common.Hash, *types.Block](blockCacheSize, nil) blockLRU, _ := simplelru.NewLRU[common.Hash, *types.Block](blockCacheSize, nil)
nodeLRU, _ := simplelru.NewLRU[common.Hash, []byte](nodeCacheSize, nil) nodeLRU, _ := simplelru.NewLRU[common.Hash, []byte](nodeCacheSize, nil)
codeLRU, _ := simplelru.NewLRU[common.Hash, []byte](codeCacheSize, nil) codeLRU, _ := simplelru.NewLRU[common.Hash, []byte](codeCacheSize, nil)
outputLRU, _ := simplelru.NewLRU[common.Hash, eth.Output](codeCacheSize, nil)
return &CachingOracle{ return &CachingOracle{
oracle: oracle, oracle: oracle,
blocks: blockLRU, blocks: blockLRU,
nodes: nodeLRU, nodes: nodeLRU,
codes: codeLRU, codes: codeLRU,
outputs: outputLRU,
} }
} }
...@@ -60,3 +64,13 @@ func (o *CachingOracle) BlockByHash(blockHash common.Hash) *types.Block { ...@@ -60,3 +64,13 @@ func (o *CachingOracle) BlockByHash(blockHash common.Hash) *types.Block {
o.blocks.Add(blockHash, block) o.blocks.Add(blockHash, block)
return block return block
} }
func (o *CachingOracle) OutputByRoot(root common.Hash) eth.Output {
output, ok := o.outputs.Get(root)
if ok {
return output
}
output = o.oracle.OutputByRoot(root)
o.outputs.Add(root, output)
return output
}
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"math/rand" "math/rand"
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/testutils" "github.com/ethereum-optimism/optimism/op-node/testutils"
"github.com/ethereum-optimism/optimism/op-program/client/l2/test" "github.com/ethereum-optimism/optimism/op-program/client/l2/test"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -66,3 +67,22 @@ func TestCodeByHash(t *testing.T) { ...@@ -66,3 +67,22 @@ func TestCodeByHash(t *testing.T) {
actual = oracle.CodeByHash(hash) actual = oracle.CodeByHash(hash)
require.Equal(t, node, actual) require.Equal(t, node, actual)
} }
func TestOutputByRoot(t *testing.T) {
stub, _ := test.NewStubOracle(t)
oracle := NewCachingOracle(stub)
rng := rand.New(rand.NewSource(1))
output := testutils.RandomOutputV0(rng)
// Initial call retrieves from the stub
root := common.Hash(eth.OutputRoot(output))
stub.Outputs[root] = output
actual := oracle.OutputByRoot(root)
require.Equal(t, output, actual)
// Later calls should retrieve from cache
delete(stub.Outputs, root)
actual = oracle.OutputByRoot(root)
require.Equal(t, output, actual)
}
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-program/client/l2/engineapi" "github.com/ethereum-optimism/optimism/op-program/client/l2/engineapi"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus"
...@@ -39,8 +40,13 @@ type OracleBackedL2Chain struct { ...@@ -39,8 +40,13 @@ type OracleBackedL2Chain struct {
var _ engineapi.EngineBackend = (*OracleBackedL2Chain)(nil) var _ engineapi.EngineBackend = (*OracleBackedL2Chain)(nil)
func NewOracleBackedL2Chain(logger log.Logger, oracle Oracle, chainCfg *params.ChainConfig, l2Head common.Hash) (*OracleBackedL2Chain, error) { func NewOracleBackedL2Chain(logger log.Logger, oracle Oracle, chainCfg *params.ChainConfig, l2OutputRoot common.Hash) (*OracleBackedL2Chain, error) {
head := oracle.BlockByHash(l2Head) output := oracle.OutputByRoot(l2OutputRoot)
outputV0, ok := output.(*eth.OutputV0)
if !ok {
return nil, fmt.Errorf("unsupported L2 output version: %d", output.Version())
}
head := oracle.BlockByHash(outputV0.BlockHash)
logger.Info("Loaded L2 head", "hash", head.Hash(), "number", head.Number()) logger.Info("Loaded L2 head", "hash", head.Hash(), "number", head.Number())
return &OracleBackedL2Chain{ return &OracleBackedL2Chain{
log: logger, log: logger,
......
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-program/client/l2/engineapi" "github.com/ethereum-optimism/optimism/op-program/client/l2/engineapi"
"github.com/ethereum-optimism/optimism/op-program/client/l2/engineapi/test" "github.com/ethereum-optimism/optimism/op-program/client/l2/engineapi/test"
...@@ -199,7 +200,8 @@ func setupOracleBackedChainWithLowerHead(t *testing.T, blockCount int, headBlock ...@@ -199,7 +200,8 @@ func setupOracleBackedChainWithLowerHead(t *testing.T, blockCount int, headBlock
logger := testlog.Logger(t, log.LvlDebug) logger := testlog.Logger(t, log.LvlDebug)
chainCfg, blocks, oracle := setupOracle(t, blockCount, headBlockNumber) chainCfg, blocks, oracle := setupOracle(t, blockCount, headBlockNumber)
head := blocks[headBlockNumber].Hash() head := blocks[headBlockNumber].Hash()
chain, err := NewOracleBackedL2Chain(logger, oracle, chainCfg, head) stubOutput := eth.OutputV0{BlockHash: head}
chain, err := NewOracleBackedL2Chain(logger, oracle, chainCfg, common.Hash(eth.OutputRoot(&stubOutput)))
require.NoError(t, err) require.NoError(t, err)
return blocks, chain return blocks, chain
} }
...@@ -232,7 +234,12 @@ func setupOracle(t *testing.T, blockCount int, headBlockNumber int) (*params.Cha ...@@ -232,7 +234,12 @@ func setupOracle(t *testing.T, blockCount int, headBlockNumber int) (*params.Cha
genesisBlock := l2Genesis.MustCommit(db) genesisBlock := l2Genesis.MustCommit(db)
blocks, _ := core.GenerateChain(chainCfg, genesisBlock, consensus, db, blockCount, func(i int, gen *core.BlockGen) {}) blocks, _ := core.GenerateChain(chainCfg, genesisBlock, consensus, db, blockCount, func(i int, gen *core.BlockGen) {})
blocks = append([]*types.Block{genesisBlock}, blocks...) blocks = append([]*types.Block{genesisBlock}, blocks...)
oracle := l2test.NewStubOracleWithBlocks(t, blocks[:headBlockNumber+1], db)
var outputs []eth.Output
for _, block := range blocks {
outputs = append(outputs, &eth.OutputV0{BlockHash: block.Hash()})
}
oracle := l2test.NewStubOracleWithBlocks(t, blocks[:headBlockNumber+1], outputs, db)
return chainCfg, blocks, oracle return chainCfg, blocks, oracle
} }
......
...@@ -11,6 +11,7 @@ const ( ...@@ -11,6 +11,7 @@ const (
HintL2Transactions = "l2-transactions" HintL2Transactions = "l2-transactions"
HintL2Code = "l2-code" HintL2Code = "l2-code"
HintL2StateNode = "l2-state-node" HintL2StateNode = "l2-state-node"
HintL2Output = "l2-output"
) )
type BlockHeaderHint common.Hash type BlockHeaderHint common.Hash
...@@ -44,3 +45,11 @@ var _ preimage.Hint = StateNodeHint{} ...@@ -44,3 +45,11 @@ var _ preimage.Hint = StateNodeHint{}
func (l StateNodeHint) Hint() string { func (l StateNodeHint) Hint() string {
return HintL2StateNode + " " + (common.Hash)(l).String() return HintL2StateNode + " " + (common.Hash)(l).String()
} }
type L2OutputHint common.Hash
var _ preimage.Hint = L2OutputHint{}
func (l L2OutputHint) Hint() string {
return HintL2Output + " " + (common.Hash)(l).String()
}
...@@ -32,6 +32,8 @@ type Oracle interface { ...@@ -32,6 +32,8 @@ type Oracle interface {
// BlockByHash retrieves the block with the given hash. // BlockByHash retrieves the block with the given hash.
BlockByHash(blockHash common.Hash) *types.Block BlockByHash(blockHash common.Hash) *types.Block
OutputByRoot(root common.Hash) eth.Output
} }
// PreimageOracle implements Oracle using by interfacing with the pure preimage.Oracle // PreimageOracle implements Oracle using by interfacing with the pure preimage.Oracle
...@@ -85,3 +87,13 @@ func (p *PreimageOracle) CodeByHash(codeHash common.Hash) []byte { ...@@ -85,3 +87,13 @@ func (p *PreimageOracle) CodeByHash(codeHash common.Hash) []byte {
p.hint.Hint(CodeHint(codeHash)) p.hint.Hint(CodeHint(codeHash))
return p.oracle.Get(preimage.Keccak256Key(codeHash)) return p.oracle.Get(preimage.Keccak256Key(codeHash))
} }
func (p *PreimageOracle) OutputByRoot(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("invalid L2 output data for root %s: %w", l2OutputRoot, err))
}
return output
}
...@@ -122,3 +122,21 @@ func TestPreimageOracleCodeByHash(t *testing.T) { ...@@ -122,3 +122,21 @@ func TestPreimageOracleCodeByHash(t *testing.T) {
}) })
} }
} }
func TestPreimageOracleOutputByRoot(t *testing.T) {
rng := rand.New(rand.NewSource(123))
for i := 0; i < 10; i++ {
t.Run(fmt.Sprintf("output_%d", i), func(t *testing.T) {
po, hints, preimages := mockPreimageOracle(t)
output := testutils.RandomOutputV0(rng)
h := common.Hash(eth.OutputRoot(output))
preimages[preimage.Keccak256Key(h).PreimageKey()] = output.Marshal()
hints.On("hint", L2OutputHint(h).Hint()).Once().Return()
gotOutput := po.OutputByRoot(h)
hints.AssertExpectations(t)
require.Equal(t, hexutil.Bytes(output.Marshal()), hexutil.Bytes(gotOutput.Marshal()), "output matches")
})
}
}
...@@ -3,6 +3,7 @@ package test ...@@ -3,6 +3,7 @@ package test
import ( import (
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
...@@ -16,8 +17,9 @@ type stateOracle interface { ...@@ -16,8 +17,9 @@ type stateOracle interface {
} }
type StubBlockOracle struct { type StubBlockOracle struct {
t *testing.T t *testing.T
Blocks map[common.Hash]*types.Block Blocks map[common.Hash]*types.Block
Outputs map[common.Hash]eth.Output
stateOracle stateOracle
} }
...@@ -26,18 +28,25 @@ func NewStubOracle(t *testing.T) (*StubBlockOracle, *StubStateOracle) { ...@@ -26,18 +28,25 @@ func NewStubOracle(t *testing.T) (*StubBlockOracle, *StubStateOracle) {
blockOracle := StubBlockOracle{ blockOracle := StubBlockOracle{
t: t, t: t,
Blocks: make(map[common.Hash]*types.Block), Blocks: make(map[common.Hash]*types.Block),
Outputs: make(map[common.Hash]eth.Output),
stateOracle: stateOracle, stateOracle: stateOracle,
} }
return &blockOracle, stateOracle return &blockOracle, stateOracle
} }
func NewStubOracleWithBlocks(t *testing.T, chain []*types.Block, db ethdb.Database) *StubBlockOracle { func NewStubOracleWithBlocks(t *testing.T, chain []*types.Block, outputs []eth.Output, db ethdb.Database) *StubBlockOracle {
blocks := make(map[common.Hash]*types.Block, len(chain)) blocks := make(map[common.Hash]*types.Block, len(chain))
for _, block := range chain { for _, block := range chain {
blocks[block.Hash()] = block blocks[block.Hash()] = block
} }
o := make(map[common.Hash]eth.Output, len(outputs))
for _, output := range outputs {
o[common.Hash(eth.OutputRoot(output))] = output
}
return &StubBlockOracle{ return &StubBlockOracle{
t: t,
Blocks: blocks, Blocks: blocks,
Outputs: o,
stateOracle: &KvStateOracle{t: t, Source: db}, stateOracle: &KvStateOracle{t: t, Source: db},
} }
} }
...@@ -50,6 +59,14 @@ func (o StubBlockOracle) BlockByHash(blockHash common.Hash) *types.Block { ...@@ -50,6 +59,14 @@ func (o StubBlockOracle) BlockByHash(blockHash common.Hash) *types.Block {
return block return block
} }
func (o StubBlockOracle) OutputByRoot(root common.Hash) eth.Output {
output, ok := o.Outputs[root]
if !ok {
o.t.Fatalf("requested unknown output root %s", root)
}
return output
}
// KvStateOracle loads data from a source ethdb.KeyValueStore // KvStateOracle loads data from a source ethdb.KeyValueStore
type KvStateOracle struct { type KvStateOracle struct {
t *testing.T t *testing.T
......
...@@ -53,7 +53,7 @@ func RunProgram(logger log.Logger, preimageOracle io.ReadWriter, preimageHinter ...@@ -53,7 +53,7 @@ func RunProgram(logger log.Logger, preimageOracle io.ReadWriter, preimageHinter
bootInfo.RollupConfig, bootInfo.RollupConfig,
bootInfo.L2ChainConfig, bootInfo.L2ChainConfig,
bootInfo.L1Head, bootInfo.L1Head,
bootInfo.L2Head, bootInfo.L2OutputRoot,
bootInfo.L2Claim, bootInfo.L2Claim,
bootInfo.L2ClaimBlockNumber, bootInfo.L2ClaimBlockNumber,
l1PreimageOracle, l1PreimageOracle,
...@@ -62,9 +62,9 @@ func RunProgram(logger log.Logger, preimageOracle io.ReadWriter, preimageHinter ...@@ -62,9 +62,9 @@ func RunProgram(logger log.Logger, preimageOracle io.ReadWriter, preimageHinter
} }
// runDerivation executes the L2 state transition, given a minimal interface to retrieve data. // 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) l1Source := l1.NewOracleL1Client(logger, l1Oracle, l1Head)
engineBackend, err := l2.NewOracleBackedL2Chain(logger, l2Oracle, l2Cfg, l2Head) engineBackend, err := l2.NewOracleBackedL2Chain(logger, l2Oracle, l2Cfg, l2OutputRoot)
if err != nil { if err != nil {
return fmt.Errorf("failed to create oracle-backed L2 chain: %w", err) return fmt.Errorf("failed to create oracle-backed L2 chain: %w", err)
} }
......
...@@ -20,6 +20,7 @@ var ( ...@@ -20,6 +20,7 @@ var (
l1HeadValue = common.HexToHash("0x111111").Hex() l1HeadValue = common.HexToHash("0x111111").Hex()
l2HeadValue = common.HexToHash("0x222222").Hex() l2HeadValue = common.HexToHash("0x222222").Hex()
l2ClaimValue = common.HexToHash("0x333333").Hex() l2ClaimValue = common.HexToHash("0x333333").Hex()
l2OutputRoot = common.HexToHash("0x444444").Hex()
l2ClaimBlockNumber = uint64(1203) l2ClaimBlockNumber = uint64(1203)
// Note: This is actually the L1 goerli genesis config. Just using it as an arbitrary, valid genesis config // Note: This is actually the L1 goerli genesis config. Just using it as an arbitrary, valid genesis config
l2Genesis = core.DefaultGoerliGenesisBlock() l2Genesis = core.DefaultGoerliGenesisBlock()
...@@ -48,6 +49,7 @@ func TestDefaultCLIOptionsMatchDefaultConfig(t *testing.T) { ...@@ -48,6 +49,7 @@ func TestDefaultCLIOptionsMatchDefaultConfig(t *testing.T) {
config.OPGoerliChainConfig, config.OPGoerliChainConfig,
common.HexToHash(l1HeadValue), common.HexToHash(l1HeadValue),
common.HexToHash(l2HeadValue), common.HexToHash(l2HeadValue),
common.HexToHash(l2OutputRoot),
common.HexToHash(l2ClaimValue), common.HexToHash(l2ClaimValue),
l2ClaimBlockNumber) l2ClaimBlockNumber)
require.Equal(t, defaultCfg, cfg) require.Equal(t, defaultCfg, cfg)
...@@ -131,6 +133,21 @@ func TestL2Head(t *testing.T) { ...@@ -131,6 +133,21 @@ func TestL2Head(t *testing.T) {
}) })
} }
func TestL2OutputRoot(t *testing.T) {
t.Run("Required", func(t *testing.T) {
verifyArgsInvalid(t, "flag l2.outputroot is required", addRequiredArgsExcept("--l2.outputroot"))
})
t.Run("Valid", func(t *testing.T) {
cfg := configForArgs(t, replaceRequiredArg("--l2.outputroot", l2OutputRoot))
require.Equal(t, common.HexToHash(l2OutputRoot), cfg.L2OutputRoot)
})
t.Run("Invalid", func(t *testing.T) {
verifyArgsInvalid(t, config.ErrInvalidL2OutputRoot.Error(), replaceRequiredArg("--l2.outputroot", "something"))
})
}
func TestL1Head(t *testing.T) { func TestL1Head(t *testing.T) {
t.Run("Required", func(t *testing.T) { t.Run("Required", func(t *testing.T) {
verifyArgsInvalid(t, "flag l1.head is required", addRequiredArgsExcept("--l1.head")) verifyArgsInvalid(t, "flag l1.head is required", addRequiredArgsExcept("--l1.head"))
...@@ -302,6 +319,7 @@ func requiredArgs() map[string]string { ...@@ -302,6 +319,7 @@ func requiredArgs() map[string]string {
"--network": "goerli", "--network": "goerli",
"--l1.head": l1HeadValue, "--l1.head": l1HeadValue,
"--l2.head": l2HeadValue, "--l2.head": l2HeadValue,
"--l2.outputroot": l2OutputRoot,
"--l2.claim": l2ClaimValue, "--l2.claim": l2ClaimValue,
"--l2.blocknumber": strconv.FormatUint(l2ClaimBlockNumber, 10), "--l2.blocknumber": strconv.FormatUint(l2ClaimBlockNumber, 10),
} }
......
...@@ -22,6 +22,7 @@ var ( ...@@ -22,6 +22,7 @@ var (
ErrMissingL2Genesis = errors.New("missing l2 genesis") ErrMissingL2Genesis = errors.New("missing l2 genesis")
ErrInvalidL1Head = errors.New("invalid l1 head") ErrInvalidL1Head = errors.New("invalid l1 head")
ErrInvalidL2Head = errors.New("invalid l2 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") ErrL1AndL2Inconsistent = errors.New("l1 and l2 options must be specified together or both omitted")
ErrInvalidL2Claim = errors.New("invalid l2 claim") ErrInvalidL2Claim = errors.New("invalid l2 claim")
ErrInvalidL2ClaimBlock = errors.New("invalid l2 claim block number") ErrInvalidL2ClaimBlock = errors.New("invalid l2 claim block number")
...@@ -41,9 +42,12 @@ type Config struct { ...@@ -41,9 +42,12 @@ type Config struct {
L1TrustRPC bool L1TrustRPC bool
L1RPCKind sources.RPCProviderKind 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 by searching the oracle for the l2 output root
L2Head common.Hash 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 is the claimed L2 output root to verify
L2Claim common.Hash L2Claim common.Hash
// L2ClaimBlockNumber is the block number the claimed L2 output root is from // L2ClaimBlockNumber is the block number the claimed L2 output root is from
...@@ -73,6 +77,9 @@ func (c *Config) Check() error { ...@@ -73,6 +77,9 @@ func (c *Config) Check() error {
if c.L2Head == (common.Hash{}) { if c.L2Head == (common.Hash{}) {
return ErrInvalidL2Head return ErrInvalidL2Head
} }
if c.L2OutputRoot == (common.Hash{}) {
return ErrInvalidL2OutputRoot
}
if c.L2Claim == (common.Hash{}) { if c.L2Claim == (common.Hash{}) {
return ErrInvalidL2Claim return ErrInvalidL2Claim
} }
...@@ -99,12 +106,21 @@ func (c *Config) FetchingEnabled() bool { ...@@ -99,12 +106,21 @@ func (c *Config) FetchingEnabled() bool {
} }
// NewConfig creates a Config with all optional values set to the CLI default value // 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{ return &Config{
Rollup: rollupCfg, Rollup: rollupCfg,
L2ChainConfig: l2Genesis, L2ChainConfig: l2Genesis,
L1Head: l1Head, L1Head: l1Head,
L2Head: l2Head, L2Head: l2Head,
L2OutputRoot: l2OutputRoot,
L2Claim: l2Claim, L2Claim: l2Claim,
L2ClaimBlockNumber: l2ClaimBlockNum, L2ClaimBlockNumber: l2ClaimBlockNum,
L1RPCKind: sources.RPCKindBasic, L1RPCKind: sources.RPCKindBasic,
...@@ -123,6 +139,10 @@ func NewConfigFromCLI(log log.Logger, ctx *cli.Context) (*Config, error) { ...@@ -123,6 +139,10 @@ func NewConfigFromCLI(log log.Logger, ctx *cli.Context) (*Config, error) {
if l2Head == (common.Hash{}) { if l2Head == (common.Hash{}) {
return nil, ErrInvalidL2Head return nil, ErrInvalidL2Head
} }
l2OutputRoot := common.HexToHash(ctx.String(flags.L2OutputRoot.Name))
if l2OutputRoot == (common.Hash{}) {
return nil, ErrInvalidL2OutputRoot
}
l2Claim := common.HexToHash(ctx.String(flags.L2Claim.Name)) l2Claim := common.HexToHash(ctx.String(flags.L2Claim.Name))
if l2Claim == (common.Hash{}) { if l2Claim == (common.Hash{}) {
return nil, ErrInvalidL2Claim return nil, ErrInvalidL2Claim
...@@ -152,6 +172,7 @@ func NewConfigFromCLI(log log.Logger, ctx *cli.Context) (*Config, error) { ...@@ -152,6 +172,7 @@ func NewConfigFromCLI(log log.Logger, ctx *cli.Context) (*Config, error) {
L2URL: ctx.String(flags.L2NodeAddr.Name), L2URL: ctx.String(flags.L2NodeAddr.Name),
L2ChainConfig: l2ChainConfig, L2ChainConfig: l2ChainConfig,
L2Head: l2Head, L2Head: l2Head,
L2OutputRoot: l2OutputRoot,
L2Claim: l2Claim, L2Claim: l2Claim,
L2ClaimBlockNumber: l2ClaimBlockNum, L2ClaimBlockNumber: l2ClaimBlockNum,
L1Head: l1Head, L1Head: l1Head,
......
...@@ -16,6 +16,7 @@ var ( ...@@ -16,6 +16,7 @@ var (
validL1Head = common.Hash{0xaa} validL1Head = common.Hash{0xaa}
validL2Head = common.Hash{0xbb} validL2Head = common.Hash{0xbb}
validL2Claim = common.Hash{0xcc} validL2Claim = common.Hash{0xcc}
validL2OutputRoot = common.Hash{0xdd}
validL2ClaimBlockNum = uint64(15) validL2ClaimBlockNum = uint64(15)
) )
...@@ -55,6 +56,13 @@ func TestL2HeadRequired(t *testing.T) { ...@@ -55,6 +56,13 @@ func TestL2HeadRequired(t *testing.T) {
require.ErrorIs(t, err, ErrInvalidL2Head) require.ErrorIs(t, err, ErrInvalidL2Head)
} }
func TestL2OutputRootRequired(t *testing.T) {
config := validConfig()
config.L2OutputRoot = common.Hash{}
err := config.Check()
require.ErrorIs(t, err, ErrInvalidL2OutputRoot)
}
func TestL2ClaimRequired(t *testing.T) { func TestL2ClaimRequired(t *testing.T) {
config := validConfig() config := validConfig()
config.L2Claim = common.Hash{} config.L2Claim = common.Hash{}
...@@ -151,7 +159,7 @@ func TestRejectExecAndServerMode(t *testing.T) { ...@@ -151,7 +159,7 @@ func TestRejectExecAndServerMode(t *testing.T) {
} }
func validConfig() *Config { func validConfig() *Config {
cfg := NewConfig(validRollupConfig, validL2Genesis, validL1Head, validL2Head, validL2Claim, validL2ClaimBlockNum) cfg := NewConfig(validRollupConfig, validL2Genesis, validL1Head, validL2Head, validL2OutputRoot, validL2Claim, validL2ClaimBlockNum)
cfg.DataDir = "/tmp/configTest" cfg.DataDir = "/tmp/configTest"
return cfg return cfg
} }
...@@ -47,9 +47,14 @@ var ( ...@@ -47,9 +47,14 @@ var (
} }
L2Head = &cli.StringFlag{ L2Head = &cli.StringFlag{
Name: "l2.head", Name: "l2.head",
Usage: "Hash of the agreed L2 block to start derivation from", Usage: "Hash of the L2 block at l2.outputroot",
EnvVars: prefixEnvVars("L2_HEAD"), EnvVars: prefixEnvVars("L2_HEAD"),
} }
L2OutputRoot = &cli.StringFlag{
Name: "l2.outputroot",
Usage: "Agreed L2 Output Root to start derivation from",
EnvVars: prefixEnvVars("L2_OUTPUT_ROOT"),
}
L2Claim = &cli.StringFlag{ L2Claim = &cli.StringFlag{
Name: "l2.claim", Name: "l2.claim",
Usage: "Claimed L2 output root to validate", Usage: "Claimed L2 output root to validate",
...@@ -103,6 +108,7 @@ var Flags []cli.Flag ...@@ -103,6 +108,7 @@ var Flags []cli.Flag
var requiredFlags = []cli.Flag{ var requiredFlags = []cli.Flag{
L1Head, L1Head,
L2Head, L2Head,
L2OutputRoot,
L2Claim, L2Claim,
L2BlockNumber, L2BlockNumber,
} }
......
...@@ -26,7 +26,7 @@ import ( ...@@ -26,7 +26,7 @@ import (
) )
type L2Source struct { type L2Source struct {
*sources.L2Client *L2Client
*sources.DebugClient *sources.DebugClient
} }
...@@ -205,7 +205,7 @@ func makePrefetcher(ctx context.Context, logger log.Logger, kv kvstore.KV, cfg * ...@@ -205,7 +205,7 @@ func makePrefetcher(ctx context.Context, logger log.Logger, kv kvstore.KV, cfg *
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create L1 client: %w", err) return nil, fmt.Errorf("failed to create L1 client: %w", err)
} }
l2Cl, err := sources.NewL2Client(l2RPC, logger, nil, l2ClCfg) l2Cl, err := NewL2Client(l2RPC, logger, nil, &L2ClientConfig{L2ClientConfig: l2ClCfg, L2Head: cfg.L2Head})
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create L2 client: %w", err) return nil, fmt.Errorf("failed to create L2 client: %w", err)
} }
......
...@@ -23,7 +23,8 @@ func TestServerMode(t *testing.T) { ...@@ -23,7 +23,8 @@ func TestServerMode(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
l1Head := common.Hash{0x11} 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.DataDir = dir
cfg.ServerMode = true cfg.ServerMode = true
...@@ -43,7 +44,8 @@ func TestServerMode(t *testing.T) { ...@@ -43,7 +44,8 @@ func TestServerMode(t *testing.T) {
hClient := preimage.NewHintWriter(hintClient) hClient := preimage.NewHintWriter(hintClient)
l1PreimageOracle := l1.NewPreimageOracle(pClient, hClient) 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 // Should exit when a preimage is unavailable
require.Panics(t, func() { require.Panics(t, func() {
......
...@@ -19,7 +19,7 @@ func NewLocalPreimageSource(config *config.Config) *LocalPreimageSource { ...@@ -19,7 +19,7 @@ func NewLocalPreimageSource(config *config.Config) *LocalPreimageSource {
var ( var (
l1HeadKey = client.L1HeadLocalIndex.PreimageKey() l1HeadKey = client.L1HeadLocalIndex.PreimageKey()
l2HeadKey = client.L2HeadLocalIndex.PreimageKey() l2OutputRootKey = client.L2OutputRootLocalIndex.PreimageKey()
l2ClaimKey = client.L2ClaimLocalIndex.PreimageKey() l2ClaimKey = client.L2ClaimLocalIndex.PreimageKey()
l2ClaimBlockNumberKey = client.L2ClaimBlockNumberLocalIndex.PreimageKey() l2ClaimBlockNumberKey = client.L2ClaimBlockNumberLocalIndex.PreimageKey()
l2ChainConfigKey = client.L2ChainConfigLocalIndex.PreimageKey() l2ChainConfigKey = client.L2ChainConfigLocalIndex.PreimageKey()
...@@ -30,8 +30,8 @@ func (s *LocalPreimageSource) Get(key common.Hash) ([]byte, error) { ...@@ -30,8 +30,8 @@ func (s *LocalPreimageSource) Get(key common.Hash) ([]byte, error) {
switch [32]byte(key) { switch [32]byte(key) {
case l1HeadKey: case l1HeadKey:
return s.config.L1Head.Bytes(), nil return s.config.L1Head.Bytes(), nil
case l2HeadKey: case l2OutputRootKey:
return s.config.L2Head.Bytes(), nil return s.config.L2OutputRoot.Bytes(), nil
case l2ClaimKey: case l2ClaimKey:
return s.config.L2Claim.Bytes(), nil return s.config.L2Claim.Bytes(), nil
case l2ClaimBlockNumberKey: case l2ClaimBlockNumberKey:
......
...@@ -17,7 +17,7 @@ func TestLocalPreimageSource(t *testing.T) { ...@@ -17,7 +17,7 @@ func TestLocalPreimageSource(t *testing.T) {
cfg := &config.Config{ cfg := &config.Config{
Rollup: &chaincfg.Goerli, Rollup: &chaincfg.Goerli,
L1Head: common.HexToHash("0x1111"), L1Head: common.HexToHash("0x1111"),
L2Head: common.HexToHash("0x2222"), L2OutputRoot: common.HexToHash("0x2222"),
L2Claim: common.HexToHash("0x3333"), L2Claim: common.HexToHash("0x3333"),
L2ClaimBlockNumber: 1234, L2ClaimBlockNumber: 1234,
L2ChainConfig: params.GoerliChainConfig, L2ChainConfig: params.GoerliChainConfig,
...@@ -29,7 +29,7 @@ func TestLocalPreimageSource(t *testing.T) { ...@@ -29,7 +29,7 @@ func TestLocalPreimageSource(t *testing.T) {
expected []byte expected []byte
}{ }{
{"L1Head", l1HeadKey, cfg.L1Head.Bytes()}, {"L1Head", l1HeadKey, cfg.L1Head.Bytes()},
{"L2Head", l2HeadKey, cfg.L2Head.Bytes()}, {"L2OutputRoot", l2OutputRootKey, cfg.L2OutputRoot.Bytes()},
{"L2Claim", l2ClaimKey, cfg.L2Claim.Bytes()}, {"L2Claim", l2ClaimKey, cfg.L2Claim.Bytes()},
{"L2ClaimBlockNumber", l2ClaimBlockNumberKey, binary.BigEndian.AppendUint64(nil, cfg.L2ClaimBlockNumber)}, {"L2ClaimBlockNumber", l2ClaimBlockNumberKey, binary.BigEndian.AppendUint64(nil, cfg.L2ClaimBlockNumber)},
{"Rollup", rollupKey, asJson(t, cfg.Rollup)}, {"Rollup", rollupKey, asJson(t, cfg.Rollup)},
......
package host
import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-node/sources/caching"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
type L2Client struct {
*sources.L2Client
// l2Head is the L2 block hash that we use to fetch L2 output
l2Head common.Hash
}
type L2ClientConfig struct {
*sources.L2ClientConfig
L2Head common.Hash
}
func NewL2Client(client client.RPC, log log.Logger, metrics caching.Metrics, config *L2ClientConfig) (*L2Client, error) {
l2Client, err := sources.NewL2Client(client, log, metrics, config.L2ClientConfig)
if err != nil {
return nil, err
}
return &L2Client{
L2Client: l2Client,
l2Head: config.L2Head,
}, nil
}
func (s *L2Client) OutputByRoot(ctx context.Context, l2OutputRoot common.Hash) (eth.Output, error) {
output, err := s.OutputV0AtBlock(ctx, s.l2Head)
if err != nil {
return nil, err
}
if eth.OutputRoot(output) != eth.Bytes32(l2OutputRoot) {
// For fault proofs, we only reference outputs at the l2 head at boot time
// The caller shouldn't be requesting outputs at any other block
return nil, fmt.Errorf("unknown output root")
}
return output, nil
}
...@@ -29,6 +29,7 @@ type L2Source interface { ...@@ -29,6 +29,7 @@ type L2Source interface {
InfoAndTxsByHash(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, types.Transactions, error) InfoAndTxsByHash(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, types.Transactions, error)
NodeByHash(ctx context.Context, hash common.Hash) ([]byte, error) NodeByHash(ctx context.Context, hash common.Hash) ([]byte, error)
CodeByHash(ctx context.Context, hash common.Hash) ([]byte, error) CodeByHash(ctx context.Context, hash common.Hash) ([]byte, error)
OutputByRoot(ctx context.Context, root common.Hash) (eth.Output, error)
} }
type Prefetcher struct { type Prefetcher struct {
...@@ -124,6 +125,12 @@ func (p *Prefetcher) prefetch(ctx context.Context, hint string) error { ...@@ -124,6 +125,12 @@ func (p *Prefetcher) prefetch(ctx context.Context, hint string) error {
return fmt.Errorf("failed to fetch L2 contract code %s: %w", hash, err) return fmt.Errorf("failed to fetch L2 contract code %s: %w", hash, err)
} }
return p.kvStore.Put(preimage.Keccak256Key(hash).PreimageKey(), code) return p.kvStore.Put(preimage.Keccak256Key(hash).PreimageKey(), code)
case l2.HintL2Output:
output, err := p.l2Fetcher.OutputByRoot(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())
} }
return fmt.Errorf("unknown hint type: %v", hintType) return fmt.Errorf("unknown hint type: %v", hintType)
} }
......
...@@ -286,6 +286,15 @@ type l2Client struct { ...@@ -286,6 +286,15 @@ type l2Client struct {
*testutils.MockDebugClient *testutils.MockDebugClient
} }
func (m *l2Client) OutputByRoot(ctx context.Context, root common.Hash) (eth.Output, error) {
out := m.Mock.MethodCalled("OutputByRoot", root)
return out[0].(eth.Output), *out[1].(*error)
}
func (m *l2Client) ExpectOutputByRoot(root common.Hash, output eth.Output, err error) {
m.Mock.On("OutputByRoot", root).Once().Return(output, &err)
}
func createPrefetcher(t *testing.T) (*Prefetcher, *testutils.MockL1Source, *l2Client, kvstore.KV) { func createPrefetcher(t *testing.T) (*Prefetcher, *testutils.MockL1Source, *l2Client, kvstore.KV) {
logger := testlog.Logger(t, log.LvlDebug) logger := testlog.Logger(t, log.LvlDebug)
kv := kvstore.NewMemKV() kv := kvstore.NewMemKV()
......
...@@ -125,6 +125,20 @@ func (s *RetryingL2Source) CodeByHash(ctx context.Context, hash common.Hash) ([] ...@@ -125,6 +125,20 @@ func (s *RetryingL2Source) CodeByHash(ctx context.Context, hash common.Hash) ([]
return code, err return code, err
} }
func (s *RetryingL2Source) OutputByRoot(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.OutputByRoot(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
}
func NewRetryingL2Source(logger log.Logger, source L2Source) *RetryingL2Source { func NewRetryingL2Source(logger log.Logger, source L2Source) *RetryingL2Source {
return &RetryingL2Source{ return &RetryingL2Source{
logger: logger, logger: logger,
......
...@@ -119,6 +119,8 @@ func TestRetryingL2Source(t *testing.T) { ...@@ -119,6 +119,8 @@ func TestRetryingL2Source(t *testing.T) {
&types.Transaction{}, &types.Transaction{},
} }
data := []byte{1, 2, 3, 4, 5} data := []byte{1, 2, 3, 4, 5}
output := &eth.OutputV0{}
wrongOutput := &eth.OutputV0{BlockHash: common.Hash{0x99}}
t.Run("InfoAndTxsByHash Success", func(t *testing.T) { t.Run("InfoAndTxsByHash Success", func(t *testing.T) {
source, mock := createL2Source(t) source, mock := createL2Source(t)
...@@ -187,6 +189,28 @@ func TestRetryingL2Source(t *testing.T) { ...@@ -187,6 +189,28 @@ func TestRetryingL2Source(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, data, actual) require.Equal(t, data, actual)
}) })
t.Run("OutputByRoot Success", func(t *testing.T) {
source, mock := createL2Source(t)
defer mock.AssertExpectations(t)
mock.ExpectOutputByRoot(hash, output, nil)
actualOutput, err := source.OutputByRoot(ctx, hash)
require.NoError(t, err)
require.Equal(t, output, actualOutput)
})
t.Run("OutputByRoot Error", func(t *testing.T) {
source, mock := createL2Source(t)
defer mock.AssertExpectations(t)
expectedErr := errors.New("boom")
mock.ExpectOutputByRoot(hash, wrongOutput, expectedErr)
mock.ExpectOutputByRoot(hash, output, nil)
actualOutput, err := source.OutputByRoot(ctx, hash)
require.NoError(t, err)
require.Equal(t, output, actualOutput)
})
} }
func createL2Source(t *testing.T) (*RetryingL2Source, *MockL2Source) { func createL2Source(t *testing.T) (*RetryingL2Source, *MockL2Source) {
...@@ -217,6 +241,11 @@ func (m *MockL2Source) CodeByHash(ctx context.Context, hash common.Hash) ([]byte ...@@ -217,6 +241,11 @@ func (m *MockL2Source) CodeByHash(ctx context.Context, hash common.Hash) ([]byte
return out[0].([]byte), *out[1].(*error) return out[0].([]byte), *out[1].(*error)
} }
func (m *MockL2Source) OutputByRoot(ctx context.Context, root common.Hash) (eth.Output, error) {
out := m.Mock.MethodCalled("OutputByRoot", root)
return out[0].(eth.Output), *out[1].(*error)
}
func (m *MockL2Source) ExpectInfoAndTxsByHash(blockHash common.Hash, info eth.BlockInfo, txs types.Transactions, err error) { func (m *MockL2Source) ExpectInfoAndTxsByHash(blockHash common.Hash, info eth.BlockInfo, txs types.Transactions, err error) {
m.Mock.On("InfoAndTxsByHash", blockHash).Once().Return(info, txs, &err) m.Mock.On("InfoAndTxsByHash", blockHash).Once().Return(info, txs, &err)
} }
...@@ -229,4 +258,8 @@ func (m *MockL2Source) ExpectCodeByHash(hash common.Hash, code []byte, err error ...@@ -229,4 +258,8 @@ func (m *MockL2Source) ExpectCodeByHash(hash common.Hash, code []byte, err error
m.Mock.On("CodeByHash", hash).Once().Return(code, &err) m.Mock.On("CodeByHash", hash).Once().Return(code, &err)
} }
func (m *MockL2Source) ExpectOutputByRoot(root common.Hash, output eth.Output, err error) {
m.Mock.On("OutputByRoot", root).Once().Return(output, &err)
}
var _ L2Source = (*MockL2Source)(nil) var _ L2Source = (*MockL2Source)(nil)
...@@ -100,7 +100,31 @@ func Run(l1RpcUrl string, l2RpcUrl string, l2OracleAddr common.Address) error { ...@@ -100,7 +100,31 @@ func Run(l1RpcUrl string, l2RpcUrl string, l2OracleAddr common.Address) error {
if err != nil { if err != nil {
return fmt.Errorf("retrieve agreed l2 block: %w", err) 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")
}
// Find an output that differs from what is being claimed
var agreedOutput bindings.TypesOutputProposal
for {
agreedOutput, err = outputOracle.GetL2Output(callOpts, agreedOutputIndex)
if err != nil {
return fmt.Errorf("retrieve agreed output: %w", err)
}
if agreedOutput.OutputRoot != output.OutputRoot {
break
}
fmt.Printf("Output at %v equals output at finalized block. Continuing search...\n", agreedOutput.L2BlockNumber)
agreedOutputIndex.Sub(agreedOutputIndex, big.NewInt(1))
if agreedOutputIndex.Int64() < 0 {
return fmt.Errorf("failed to find an output different from finalized block output")
}
}
l2BlockAtOutput, err := l2Client.BlockByNumber(ctx, agreedOutput.L2BlockNumber)
if err != nil {
return fmt.Errorf("retrieve agreed block: %w", err)
}
l2Head := l2BlockAtOutput.Hash()
temp, err := os.MkdirTemp("", "oracledata") temp, err := os.MkdirTemp("", "oracledata")
if err != nil { if err != nil {
...@@ -120,6 +144,7 @@ func Run(l1RpcUrl string, l2RpcUrl string, l2OracleAddr common.Address) error { ...@@ -120,6 +144,7 @@ func Run(l1RpcUrl string, l2RpcUrl string, l2OracleAddr common.Address) error {
"--datadir", temp, "--datadir", temp,
"--l1.head", l1Head.Hex(), "--l1.head", l1Head.Hex(),
"--l2.head", l2Head.Hex(), "--l2.head", l2Head.Hex(),
"--l2.outputroot", common.Bytes2Hex(agreedOutput.OutputRoot[:]),
"--l2.claim", l2Claim.Hex(), "--l2.claim", l2Claim.Hex(),
"--l2.blocknumber", l2BlockNumber.String(), "--l2.blocknumber", l2BlockNumber.String(),
} }
......
...@@ -89,22 +89,22 @@ contract Hashing_hashWithdrawal_Test is CommonTest { ...@@ -89,22 +89,22 @@ contract Hashing_hashWithdrawal_Test is CommonTest {
contract Hashing_hashOutputRootProof_Test is CommonTest { contract Hashing_hashOutputRootProof_Test is CommonTest {
/// @notice Tests that hashOutputRootProof returns the correct hash in a simple case. /// @notice Tests that hashOutputRootProof returns the correct hash in a simple case.
function testDiff_hashOutputRootProof_succeeds( function testDiff_hashOutputRootProof_succeeds(
bytes32 _version,
bytes32 _stateRoot, bytes32 _stateRoot,
bytes32 _messagePasserStorageRoot, bytes32 _messagePasserStorageRoot,
bytes32 _latestBlockhash bytes32 _latestBlockhash
) external { ) external {
bytes32 version = 0;
assertEq( assertEq(
Hashing.hashOutputRootProof( Hashing.hashOutputRootProof(
Types.OutputRootProof({ Types.OutputRootProof({
version: _version, version: version,
stateRoot: _stateRoot, stateRoot: _stateRoot,
messagePasserStorageRoot: _messagePasserStorageRoot, messagePasserStorageRoot: _messagePasserStorageRoot,
latestBlockhash: _latestBlockhash latestBlockhash: _latestBlockhash
}) })
), ),
ffi.hashOutputRootProof( ffi.hashOutputRootProof(
_version, version,
_stateRoot, _stateRoot,
_messagePasserStorageRoot, _messagePasserStorageRoot,
_latestBlockhash _latestBlockhash
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment