diff --git a/op-node/sources/debug_client.go b/op-node/sources/debug_client.go new file mode 100644 index 000000000000..db5a782281f1 --- /dev/null +++ b/op-node/sources/debug_client.go @@ -0,0 +1,49 @@ +package sources + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/rawdb" +) + +type DebugClient struct { + callContext CallContextFn +} + +func NewDebugClient(callContext CallContextFn) *DebugClient { + return &DebugClient{callContext} +} + +func (o *DebugClient) NodeByHash(ctx context.Context, hash common.Hash) ([]byte, error) { + // MPT nodes are stored as the hash of the node (with no prefix) + node, err := o.dbGet(ctx, hash[:]) + if err != nil { + return nil, fmt.Errorf("failed to retrieve state MPT node: %w", err) + } + return node, nil +} + +func (o *DebugClient) CodeByHash(ctx context.Context, hash common.Hash) ([]byte, error) { + // First try retrieving with the new code prefix + code, err := o.dbGet(ctx, append(append(make([]byte, 0), rawdb.CodePrefix...), hash[:]...)) + if err != nil { + // Fallback to the legacy un-prefixed version + code, err = o.dbGet(ctx, hash[:]) + if err != nil { + return nil, fmt.Errorf("failed to retrieve contract code, using new and legacy keys, with codehash %s: %w", hash, err) + } + } + return code, nil +} + +func (o *DebugClient) dbGet(ctx context.Context, key []byte) ([]byte, error) { + var node hexutil.Bytes + err := o.callContext(ctx, &node, "debug_dbGet", hexutil.Encode(key)) + if err != nil { + return nil, fmt.Errorf("fetch error %x: %w", key, err) + } + return node, nil +} diff --git a/op-node/testutils/mock_debug_client.go b/op-node/testutils/mock_debug_client.go new file mode 100644 index 000000000000..57e13fb2e97f --- /dev/null +++ b/op-node/testutils/mock_debug_client.go @@ -0,0 +1,30 @@ +package testutils + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/mock" +) + +type MockDebugClient struct { + mock.Mock +} + +func (m *MockDebugClient) ExpectNodeByHash(hash common.Hash, res []byte, err error) { + m.Mock.On("NodeByHash", hash).Once().Return(res, &err) +} + +func (m *MockDebugClient) NodeByHash(ctx context.Context, hash common.Hash) ([]byte, error) { + out := m.Mock.MethodCalled("NodeByHash", hash) + return out[0].([]byte), *out[1].(*error) +} + +func (m *MockDebugClient) ExpectCodeByHash(hash common.Hash, res []byte, err error) { + m.Mock.On("CodeByHash", hash).Once().Return(res, &err) +} + +func (m *MockDebugClient) CodeByHash(ctx context.Context, hash common.Hash) ([]byte, error) { + out := m.Mock.MethodCalled("CodeByHash", hash) + return out[0].([]byte), *out[1].(*error) +} diff --git a/op-program/client/l1/hints.go b/op-program/client/l1/hints.go index 4de32b00c082..c388807d9be2 100644 --- a/op-program/client/l1/hints.go +++ b/op-program/client/l1/hints.go @@ -6,12 +6,18 @@ import ( "github.com/ethereum-optimism/optimism/op-program/preimage" ) +const ( + HintL1BlockHeader = "l1-block-header" + HintL1Transactions = "l1-transactions" + HintL1Receipts = "l1-receipts" +) + type BlockHeaderHint common.Hash var _ preimage.Hint = BlockHeaderHint{} func (l BlockHeaderHint) Hint() string { - return "l1-block-header " + (common.Hash)(l).String() + return HintL1BlockHeader + " " + (common.Hash)(l).String() } type TransactionsHint common.Hash @@ -19,7 +25,7 @@ type TransactionsHint common.Hash var _ preimage.Hint = TransactionsHint{} func (l TransactionsHint) Hint() string { - return "l1-transactions " + (common.Hash)(l).String() + return HintL1Transactions + " " + (common.Hash)(l).String() } type ReceiptsHint common.Hash @@ -27,5 +33,5 @@ type ReceiptsHint common.Hash var _ preimage.Hint = ReceiptsHint{} func (l ReceiptsHint) Hint() string { - return "l1-receipts " + (common.Hash)(l).String() + return HintL1Receipts + " " + (common.Hash)(l).String() } diff --git a/op-program/client/l2/hints.go b/op-program/client/l2/hints.go index 07fcfde66a70..5602d3884be2 100644 --- a/op-program/client/l2/hints.go +++ b/op-program/client/l2/hints.go @@ -6,12 +6,19 @@ import ( "github.com/ethereum-optimism/optimism/op-program/preimage" ) +const ( + HintL2BlockHeader = "l2-block-header" + HintL2Transactions = "l2-transactions" + HintL2Code = "l2-code" + HintL2StateNode = "l2-state-node" +) + type BlockHeaderHint common.Hash var _ preimage.Hint = BlockHeaderHint{} func (l BlockHeaderHint) Hint() string { - return "l2-block-header " + (common.Hash)(l).String() + return HintL2BlockHeader + " " + (common.Hash)(l).String() } type TransactionsHint common.Hash @@ -19,7 +26,7 @@ type TransactionsHint common.Hash var _ preimage.Hint = TransactionsHint{} func (l TransactionsHint) Hint() string { - return "l2-transactions " + (common.Hash)(l).String() + return HintL2Transactions + " " + (common.Hash)(l).String() } type CodeHint common.Hash @@ -27,7 +34,7 @@ type CodeHint common.Hash var _ preimage.Hint = CodeHint{} func (l CodeHint) Hint() string { - return "l2-code " + (common.Hash)(l).String() + return HintL2Code + " " + (common.Hash)(l).String() } type StateNodeHint common.Hash @@ -35,5 +42,5 @@ type StateNodeHint common.Hash var _ preimage.Hint = StateNodeHint{} func (l StateNodeHint) Hint() string { - return "l2-state-node " + (common.Hash)(l).String() + return HintL2StateNode + " " + (common.Hash)(l).String() } diff --git a/op-program/host/cmd/main.go b/op-program/host/cmd/main.go index ce62bb90868f..32268ad8f259 100644 --- a/op-program/host/cmd/main.go +++ b/op-program/host/cmd/main.go @@ -6,17 +6,20 @@ import ( "fmt" "io" "os" - "time" "github.com/ethereum-optimism/optimism/op-node/chaincfg" + "github.com/ethereum-optimism/optimism/op-node/client" "github.com/ethereum-optimism/optimism/op-node/eth" - "github.com/ethereum-optimism/optimism/op-node/rollup/derive" + "github.com/ethereum-optimism/optimism/op-node/sources" cldr "github.com/ethereum-optimism/optimism/op-program/client/driver" "github.com/ethereum-optimism/optimism/op-program/host/config" "github.com/ethereum-optimism/optimism/op-program/host/flags" + "github.com/ethereum-optimism/optimism/op-program/host/kvstore" "github.com/ethereum-optimism/optimism/op-program/host/l1" "github.com/ethereum-optimism/optimism/op-program/host/l2" + "github.com/ethereum-optimism/optimism/op-program/host/prefetcher" "github.com/ethereum-optimism/optimism/op-program/host/version" + "github.com/ethereum-optimism/optimism/op-program/preimage" oplog "github.com/ethereum-optimism/optimism/op-service/log" "github.com/ethereum/go-ethereum/log" "github.com/urfave/cli" @@ -96,6 +99,11 @@ func setupLogging(ctx *cli.Context) (log.Logger, error) { return logger, nil } +type L2Source struct { + *sources.L2Client + *sources.DebugClient +} + // FaultProofProgram is the programmatic entry-point for the fault proof program func FaultProofProgram(logger log.Logger, cfg *config.Config) error { cfg.Rollup.LogDescription(logger, chaincfg.L2ChainIDToNetworkName) @@ -104,27 +112,49 @@ func FaultProofProgram(logger log.Logger, cfg *config.Config) error { } ctx := context.Background() + kv := kvstore.NewMemKV() + logger.Info("Connecting to L1 node", "l1", cfg.L1URL) - l1Source, err := l1.NewFetchingL1(ctx, logger, cfg) + l1RPC, err := client.NewRPC(ctx, logger, cfg.L1URL) if err != nil { - return fmt.Errorf("connect l1 oracle: %w", err) + return fmt.Errorf("failed to setup L1 RPC: %w", err) } logger.Info("Connecting to L2 node", "l2", cfg.L2URL) - l2Source, err := l2.NewFetchingEngine(ctx, logger, cfg) + l2RPC, err := client.NewRPC(ctx, logger, cfg.L2URL) + if err != nil { + return fmt.Errorf("failed to setup L2 RPC: %w", err) + } + + l1ClCfg := sources.L1ClientDefaultConfig(cfg.Rollup, cfg.L1TrustRPC, cfg.L1RPCKind) + l2ClCfg := sources.L2ClientDefaultConfig(cfg.Rollup, true) + l1Cl, err := sources.NewL1Client(l1RPC, logger, nil, l1ClCfg) + if err != nil { + return fmt.Errorf("failed to create L1 client: %w", err) + } + l2Cl, err := sources.NewL2Client(l2RPC, logger, nil, l2ClCfg) + if err != nil { + return fmt.Errorf("failed to create L2 client: %w", err) + } + l2DebugCl := &L2Source{L2Client: l2Cl, DebugClient: sources.NewDebugClient(l2RPC.CallContext)} + + logger.Info("Setting up pre-fetcher") + prefetch := prefetcher.NewPrefetcher(l1Cl, l2DebugCl, kv) + preimageOracle := asOracleFn(ctx, prefetch) + hinter := asHinter(prefetch) + l1Source := l1.NewSource(logger, preimageOracle, hinter, cfg.L1Head) + + logger.Info("Connecting to L2 node", "l2", cfg.L2URL) + l2Source, err := l2.NewEngine(logger, preimageOracle, hinter, cfg) if err != nil { return fmt.Errorf("connect l2 oracle: %w", err) } + logger.Info("Starting derivation") d := cldr.NewDriver(logger, cfg.Rollup, l1Source, l2Source) for { if err = d.Step(ctx); errors.Is(err, io.EOF) { break - } else if cfg.FetchingEnabled() && errors.Is(err, derive.ErrTemporary) { - // When in fetching mode, recover from temporary errors to allow us to keep fetching data - // TODO(CLI-3780) Ideally the retry would happen in the fetcher so this is not needed - logger.Warn("Temporary error in pipeline", "err", err) - time.Sleep(5 * time.Second) } else if err != nil { return err } @@ -135,3 +165,22 @@ func FaultProofProgram(logger log.Logger, cfg *config.Config) error { } return nil } + +func asOracleFn(ctx context.Context, prefetcher *prefetcher.Prefetcher) preimage.OracleFn { + return func(key preimage.Key) []byte { + pre, err := prefetcher.GetPreimage(ctx, key.PreimageKey()) + if err != nil { + panic(fmt.Errorf("preimage unavailable for key %v: %w", key, err)) + } + return pre + } +} + +func asHinter(prefetcher *prefetcher.Prefetcher) preimage.HinterFn { + return func(v preimage.Hint) { + err := prefetcher.Hint(v.Hint()) + if err != nil { + panic(fmt.Errorf("hint rejected %v: %w", v, err)) + } + } +} diff --git a/op-program/host/l1/l1.go b/op-program/host/l1/l1.go index 7dbe1789bf90..daecfaba1c13 100644 --- a/op-program/host/l1/l1.go +++ b/op-program/host/l1/l1.go @@ -8,10 +8,12 @@ import ( "github.com/ethereum-optimism/optimism/op-node/sources" cll1 "github.com/ethereum-optimism/optimism/op-program/client/l1" "github.com/ethereum-optimism/optimism/op-program/host/config" + "github.com/ethereum-optimism/optimism/op-program/preimage" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" ) -func NewFetchingL1(ctx context.Context, logger log.Logger, cfg *config.Config) (derive.L1Fetcher, error) { +func NewFetchingOracle(ctx context.Context, logger log.Logger, cfg *config.Config) (cll1.Oracle, error) { rpc, err := client.NewRPC(ctx, logger, cfg.L1URL) if err != nil { return nil, err @@ -21,6 +23,10 @@ func NewFetchingL1(ctx context.Context, logger log.Logger, cfg *config.Config) ( if err != nil { return nil, err } - oracle := cll1.NewCachingOracle(NewFetchingL1Oracle(ctx, logger, source)) - return cll1.NewOracleL1Client(logger, oracle, cfg.L1Head), err + return NewFetchingL1Oracle(ctx, logger, source), nil +} + +func NewSource(logger log.Logger, oracle preimage.Oracle, hint preimage.Hinter, l1Head common.Hash) derive.L1Fetcher { + l1Oracle := cll1.NewCachingOracle(cll1.NewPreimageOracle(oracle, hint)) + return cll1.NewOracleL1Client(logger, l1Oracle, l1Head) } diff --git a/op-program/host/l2/l2.go b/op-program/host/l2/l2.go index d00eb2afe83d..59db93746db0 100644 --- a/op-program/host/l2/l2.go +++ b/op-program/host/l2/l2.go @@ -8,22 +8,18 @@ import ( cll2 "github.com/ethereum-optimism/optimism/op-program/client/l2" "github.com/ethereum-optimism/optimism/op-program/host/config" + "github.com/ethereum-optimism/optimism/op-program/preimage" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" ) -func NewFetchingEngine(ctx context.Context, logger log.Logger, cfg *config.Config) (*cll2.OracleEngine, error) { +func NewEngine(logger log.Logger, pre preimage.Oracle, hint preimage.Hinter, cfg *config.Config) (*cll2.OracleEngine, error) { + oracle := cll2.NewCachingOracle(cll2.NewPreimageOracle(pre, hint)) genesis, err := loadL2Genesis(cfg) if err != nil { return nil, err } - fetcher, err := NewFetchingL2Oracle(ctx, logger, cfg.L2URL, cfg.L2Head) - if err != nil { - return nil, fmt.Errorf("connect l2 oracle: %w", err) - } - oracle := cll2.NewCachingOracle(fetcher) - engineBackend, err := cll2.NewOracleBackedL2Chain(logger, oracle, genesis, cfg.L2Head) if err != nil { return nil, fmt.Errorf("create l2 chain: %w", err) @@ -31,6 +27,14 @@ func NewFetchingEngine(ctx context.Context, logger log.Logger, cfg *config.Confi return cll2.NewOracleEngine(cfg.Rollup, logger, engineBackend), nil } +func NewFetchingOracle(ctx context.Context, logger log.Logger, cfg *config.Config) (cll2.Oracle, error) { + oracle, err := NewFetchingL2Oracle(ctx, logger, cfg.L2URL, cfg.L2Head) + if err != nil { + return nil, fmt.Errorf("connect l2 oracle: %w", err) + } + return oracle, nil +} + func loadL2Genesis(cfg *config.Config) (*params.ChainConfig, error) { data, err := os.ReadFile(cfg.L2GenesisPath) if err != nil { diff --git a/op-program/host/prefetcher/prefetcher.go b/op-program/host/prefetcher/prefetcher.go new file mode 100644 index 000000000000..346dbca78113 --- /dev/null +++ b/op-program/host/prefetcher/prefetcher.go @@ -0,0 +1,164 @@ +package prefetcher + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/ethereum-optimism/optimism/op-node/eth" + "github.com/ethereum-optimism/optimism/op-program/client/l1" + "github.com/ethereum-optimism/optimism/op-program/client/l2" + "github.com/ethereum-optimism/optimism/op-program/client/mpt" + "github.com/ethereum-optimism/optimism/op-program/host/kvstore" + "github.com/ethereum-optimism/optimism/op-program/preimage" +) + +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) +} + +type L2Source interface { + InfoAndTxsByHash(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, types.Transactions, error) + NodeByHash(ctx context.Context, hash common.Hash) ([]byte, error) + CodeByHash(ctx context.Context, hash common.Hash) ([]byte, error) +} + +type Prefetcher struct { + l1Fetcher L1Source + l2Fetcher L2Source + lastHint string + kvStore kvstore.KV +} + +func NewPrefetcher(l1Fetcher L1Source, l2Fetcher L2Source, kvStore kvstore.KV) *Prefetcher { + return &Prefetcher{ + l1Fetcher: l1Fetcher, + l2Fetcher: l2Fetcher, + kvStore: kvStore, + } +} + +func (p *Prefetcher) Hint(hint string) error { + p.lastHint = hint + return nil +} + +func (p *Prefetcher) GetPreimage(ctx context.Context, key common.Hash) ([]byte, error) { + pre, err := p.kvStore.Get(key) + if errors.Is(err, kvstore.ErrNotFound) && p.lastHint != "" { + hint := p.lastHint + p.lastHint = "" + if err := p.prefetch(ctx, hint); err != nil { + return nil, fmt.Errorf("prefetch failed: %w", err) + } + // Should now be available + return p.kvStore.Get(key) + } + return pre, err +} + +func (p *Prefetcher) prefetch(ctx context.Context, hint string) error { + hintType, hash, err := parseHint(hint) + if err != nil { + return err + } + switch hintType { + case l1.HintL1BlockHeader: + header, err := p.l1Fetcher.InfoByHash(ctx, hash) + if err != nil { + return fmt.Errorf("failed to fetch L1 block %s header: %w", hash, err) + } + data, err := header.HeaderRLP() + if err != nil { + return fmt.Errorf("marshall header: %w", err) + } + return p.kvStore.Put(preimage.Keccak256Key(hash).PreimageKey(), data) + case l1.HintL1Transactions: + _, txs, err := p.l1Fetcher.InfoAndTxsByHash(ctx, hash) + if err != nil { + return fmt.Errorf("failed to fetch L1 block %s txs: %w", hash, err) + } + return p.storeTransactions(txs) + case l1.HintL1Receipts: + _, receipts, err := p.l1Fetcher.FetchReceipts(ctx, hash) + if err != nil { + return fmt.Errorf("failed to fetch L1 block %s receipts: %w", hash, err) + } + return p.storeReceipts(receipts) + case l2.HintL2BlockHeader: + header, txs, err := p.l2Fetcher.InfoAndTxsByHash(ctx, hash) + if err != nil { + return fmt.Errorf("failed to fetch L2 block %s: %w", hash, err) + } + data, err := header.HeaderRLP() + if err != nil { + return fmt.Errorf("failed to encode header to RLP: %w", err) + } + err = p.kvStore.Put(preimage.Keccak256Key(hash).PreimageKey(), data) + if err != nil { + return err + } + return p.storeTransactions(txs) + case l2.HintL2StateNode: + node, err := p.l2Fetcher.NodeByHash(ctx, hash) + if err != nil { + return fmt.Errorf("failed to fetch L2 state node %s: %w", hash, err) + } + return p.kvStore.Put(preimage.Keccak256Key(hash).PreimageKey(), node) + case l2.HintL2Code: + code, err := p.l2Fetcher.CodeByHash(ctx, hash) + if err != nil { + return fmt.Errorf("failed to fetch L2 contract code %s: %w", hash, err) + } + return p.kvStore.Put(preimage.Keccak256Key(hash).PreimageKey(), code) + } + return fmt.Errorf("unknown hint type: %v", hintType) +} + +func (p *Prefetcher) storeReceipts(receipts types.Receipts) error { + opaqueReceipts, err := eth.EncodeReceipts(receipts) + if err != nil { + return err + } + return p.storeTrieNodes(opaqueReceipts) +} + +func (p *Prefetcher) storeTransactions(txs types.Transactions) error { + opaqueTxs, err := eth.EncodeTransactions(txs) + if err != nil { + return err + } + return p.storeTrieNodes(opaqueTxs) +} + +func (p *Prefetcher) storeTrieNodes(values []hexutil.Bytes) error { + _, nodes := mpt.WriteTrie(values) + for _, node := range nodes { + err := p.kvStore.Put(preimage.Keccak256Key(crypto.Keccak256Hash(node)).PreimageKey(), node) + if err != nil { + return fmt.Errorf("failed to store node: %w", err) + } + } + return nil +} + +// parseHint parses a hint string in wire protocol. Returns the hint type, requested hash and error (if any). +func parseHint(hint string) (string, common.Hash, error) { + hintType, hashStr, found := strings.Cut(hint, " ") + if !found { + return "", common.Hash{}, fmt.Errorf("unsupported hint: %s", hint) + } + hash := common.HexToHash(hashStr) + if hash == (common.Hash{}) { + return "", common.Hash{}, fmt.Errorf("invalid hash: %s", hashStr) + } + return hintType, hash, nil +} diff --git a/op-program/host/prefetcher/prefetcher_test.go b/op-program/host/prefetcher/prefetcher_test.go new file mode 100644 index 000000000000..18545fcc1372 --- /dev/null +++ b/op-program/host/prefetcher/prefetcher_test.go @@ -0,0 +1,333 @@ +package prefetcher + +import ( + "context" + "math/rand" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/require" + + "github.com/ethereum-optimism/optimism/op-node/eth" + "github.com/ethereum-optimism/optimism/op-node/testutils" + "github.com/ethereum-optimism/optimism/op-program/client/l1" + "github.com/ethereum-optimism/optimism/op-program/client/l2" + "github.com/ethereum-optimism/optimism/op-program/client/mpt" + "github.com/ethereum-optimism/optimism/op-program/host/kvstore" + "github.com/ethereum-optimism/optimism/op-program/preimage" +) + +func TestNoHint(t *testing.T) { + t.Run("NotFound", func(t *testing.T) { + prefetcher, _, _, _ := createPrefetcher(t) + res, err := prefetcher.GetPreimage(context.Background(), common.Hash{0xab}) + require.ErrorIs(t, err, kvstore.ErrNotFound) + require.Nil(t, res) + }) + + t.Run("Exists", func(t *testing.T) { + prefetcher, _, _, kv := createPrefetcher(t) + data := []byte{1, 2, 3} + hash := crypto.Keccak256Hash(data) + require.NoError(t, kv.Put(hash, data)) + + res, err := prefetcher.GetPreimage(context.Background(), hash) + require.NoError(t, err) + require.Equal(t, res, data) + }) +} + +func TestFetchL1BlockHeader(t *testing.T) { + rng := rand.New(rand.NewSource(123)) + block, rcpts := testutils.RandomBlock(rng, 2) + hash := block.Hash() + key := preimage.Keccak256Key(hash).PreimageKey() + pre, err := rlp.EncodeToBytes(block.Header()) + require.NoError(t, err) + + t.Run("AlreadyKnown", func(t *testing.T) { + prefetcher, _, _, kv := createPrefetcher(t) + storeBlock(t, kv, block, rcpts) + + oracle := l1.NewPreimageOracle(asOracleFn(t, prefetcher), asHinter(t, prefetcher)) + result := oracle.HeaderByBlockHash(hash) + require.Equal(t, eth.HeaderBlockInfo(block.Header()), result) + }) + + t.Run("Unknown", func(t *testing.T) { + prefetcher, l1Cl, _, _ := createPrefetcher(t) + l1Cl.ExpectInfoByHash(hash, eth.HeaderBlockInfo(block.Header()), nil) + defer l1Cl.AssertExpectations(t) + + require.NoError(t, prefetcher.Hint(l1.BlockHeaderHint(hash).Hint())) + result, err := prefetcher.GetPreimage(context.Background(), key) + require.NoError(t, err) + require.Equal(t, pre, result) + }) +} + +func TestFetchL1Transactions(t *testing.T) { + rng := rand.New(rand.NewSource(123)) + block, rcpts := testutils.RandomBlock(rng, 10) + hash := block.Hash() + + t.Run("AlreadyKnown", func(t *testing.T) { + prefetcher, _, _, kv := createPrefetcher(t) + + storeBlock(t, kv, block, rcpts) + + // Check the data is available (note the oracle does not know about the block, only the kvstore does) + oracle := l1.NewPreimageOracle(asOracleFn(t, prefetcher), asHinter(t, prefetcher)) + header, txs := oracle.TransactionsByBlockHash(hash) + require.EqualValues(t, hash, header.Hash()) + assertTransactionsEqual(t, block.Transactions(), txs) + }) + + t.Run("Unknown", func(t *testing.T) { + prefetcher, l1Cl, _, _ := createPrefetcher(t) + l1Cl.ExpectInfoByHash(hash, eth.BlockToInfo(block), nil) + l1Cl.ExpectInfoAndTxsByHash(hash, eth.BlockToInfo(block), block.Transactions(), nil) + defer l1Cl.AssertExpectations(t) + + oracle := l1.NewPreimageOracle(asOracleFn(t, prefetcher), asHinter(t, prefetcher)) + header, txs := oracle.TransactionsByBlockHash(hash) + require.EqualValues(t, hash, header.Hash()) + assertTransactionsEqual(t, block.Transactions(), txs) + }) +} + +func TestFetchL1Receipts(t *testing.T) { + rng := rand.New(rand.NewSource(123)) + block, receipts := testutils.RandomBlock(rng, 10) + hash := block.Hash() + + t.Run("AlreadyKnown", func(t *testing.T) { + prefetcher, _, _, kv := createPrefetcher(t) + storeBlock(t, kv, block, receipts) + + // Check the data is available (note the oracle does not know about the block, only the kvstore does) + oracle := l1.NewPreimageOracle(asOracleFn(t, prefetcher), asHinter(t, prefetcher)) + header, actualReceipts := oracle.ReceiptsByBlockHash(hash) + require.EqualValues(t, hash, header.Hash()) + assertReceiptsEqual(t, receipts, actualReceipts) + }) + + t.Run("Unknown", func(t *testing.T) { + prefetcher, l1Cl, _, _ := createPrefetcher(t) + l1Cl.ExpectInfoByHash(hash, eth.BlockToInfo(block), nil) + l1Cl.ExpectInfoAndTxsByHash(hash, eth.BlockToInfo(block), block.Transactions(), nil) + l1Cl.ExpectFetchReceipts(hash, eth.BlockToInfo(block), receipts, nil) + defer l1Cl.AssertExpectations(t) + + oracle := l1.NewPreimageOracle(asOracleFn(t, prefetcher), asHinter(t, prefetcher)) + header, actualReceipts := oracle.ReceiptsByBlockHash(hash) + require.EqualValues(t, hash, header.Hash()) + assertReceiptsEqual(t, receipts, actualReceipts) + }) +} + +func TestFetchL2Block(t *testing.T) { + rng := rand.New(rand.NewSource(123)) + block, rcpts := testutils.RandomBlock(rng, 10) + hash := block.Hash() + + t.Run("AlreadyKnown", func(t *testing.T) { + prefetcher, _, _, kv := createPrefetcher(t) + storeBlock(t, kv, block, rcpts) + + oracle := l2.NewPreimageOracle(asOracleFn(t, prefetcher), asHinter(t, prefetcher)) + result := oracle.BlockByHash(hash) + require.EqualValues(t, block.Header(), result.Header()) + assertTransactionsEqual(t, block.Transactions(), result.Transactions()) + }) + + t.Run("Unknown", func(t *testing.T) { + prefetcher, _, l2Cl, _ := createPrefetcher(t) + l2Cl.ExpectInfoAndTxsByHash(hash, eth.BlockToInfo(block), block.Transactions(), nil) + defer l2Cl.MockL2Client.AssertExpectations(t) + + oracle := l2.NewPreimageOracle(asOracleFn(t, prefetcher), asHinter(t, prefetcher)) + result := oracle.BlockByHash(hash) + require.EqualValues(t, block.Header(), result.Header()) + assertTransactionsEqual(t, block.Transactions(), result.Transactions()) + }) +} + +func TestFetchL2Node(t *testing.T) { + rng := rand.New(rand.NewSource(123)) + node := testutils.RandomData(rng, 30) + hash := crypto.Keccak256Hash(node) + key := preimage.Keccak256Key(hash).PreimageKey() + + t.Run("AlreadyKnown", func(t *testing.T) { + prefetcher, _, _, kv := createPrefetcher(t) + require.NoError(t, kv.Put(key, node)) + + oracle := l2.NewPreimageOracle(asOracleFn(t, prefetcher), asHinter(t, prefetcher)) + result := oracle.NodeByHash(hash) + require.EqualValues(t, node, result) + }) + + t.Run("Unknown", func(t *testing.T) { + prefetcher, _, l2Cl, _ := createPrefetcher(t) + l2Cl.ExpectNodeByHash(hash, node, nil) + defer l2Cl.MockDebugClient.AssertExpectations(t) + + oracle := l2.NewPreimageOracle(asOracleFn(t, prefetcher), asHinter(t, prefetcher)) + result := oracle.NodeByHash(hash) + require.EqualValues(t, node, result) + }) +} + +func TestFetchL2Code(t *testing.T) { + rng := rand.New(rand.NewSource(123)) + code := testutils.RandomData(rng, 30) + hash := crypto.Keccak256Hash(code) + key := preimage.Keccak256Key(hash).PreimageKey() + + t.Run("AlreadyKnown", func(t *testing.T) { + prefetcher, _, _, kv := createPrefetcher(t) + require.NoError(t, kv.Put(key, code)) + + oracle := l2.NewPreimageOracle(asOracleFn(t, prefetcher), asHinter(t, prefetcher)) + result := oracle.CodeByHash(hash) + require.EqualValues(t, code, result) + }) + + t.Run("Unknown", func(t *testing.T) { + prefetcher, _, l2Cl, _ := createPrefetcher(t) + l2Cl.ExpectCodeByHash(hash, code, nil) + defer l2Cl.MockDebugClient.AssertExpectations(t) + + oracle := l2.NewPreimageOracle(asOracleFn(t, prefetcher), asHinter(t, prefetcher)) + result := oracle.CodeByHash(hash) + require.EqualValues(t, code, result) + }) +} + +func TestBadHints(t *testing.T) { + prefetcher, _, _, kv := createPrefetcher(t) + hash := common.Hash{0xad} + + t.Run("NoSpace", func(t *testing.T) { + // Accept the hint + require.NoError(t, prefetcher.Hint(l1.HintL1BlockHeader)) + + // But it will fail to prefetch when the pre-image isn't available + pre, err := prefetcher.GetPreimage(context.Background(), hash) + require.ErrorContains(t, err, "unsupported hint") + require.Nil(t, pre) + }) + + t.Run("InvalidHash", func(t *testing.T) { + // Accept the hint + require.NoError(t, prefetcher.Hint(l1.HintL1BlockHeader+" asdfsadf")) + + // But it will fail to prefetch when the pre-image isn't available + pre, err := prefetcher.GetPreimage(context.Background(), hash) + require.ErrorContains(t, err, "invalid hash") + require.Nil(t, pre) + }) + + t.Run("UnknownType", func(t *testing.T) { + // Accept the hint + require.NoError(t, prefetcher.Hint("unknown "+hash.Hex())) + + // But it will fail to prefetch when the pre-image isn't available + pre, err := prefetcher.GetPreimage(context.Background(), hash) + require.ErrorContains(t, err, "unknown hint type") + require.Nil(t, pre) + }) + + // Should not return hint errors if the preimage is already available + t.Run("KeyExists", func(t *testing.T) { + // Prepopulate the requested preimage + value := []byte{1, 2, 3, 4} + require.NoError(t, kv.Put(hash, value)) + + // Hint is invalid + require.NoError(t, prefetcher.Hint("asdfsadf")) + // But fetching the key fails because prefetching isn't required + pre, err := prefetcher.GetPreimage(context.Background(), hash) + require.NoError(t, err) + require.Equal(t, value, pre) + }) +} + +type l2Client struct { + *testutils.MockL2Client + *testutils.MockDebugClient +} + +func createPrefetcher(t *testing.T) (*Prefetcher, *testutils.MockL1Source, *l2Client, kvstore.KV) { + kv := kvstore.NewMemKV() + + l1Source := new(testutils.MockL1Source) + l2Source := &l2Client{ + MockL2Client: new(testutils.MockL2Client), + MockDebugClient: new(testutils.MockDebugClient), + } + + prefetcher := NewPrefetcher(l1Source, l2Source, kv) + return prefetcher, l1Source, l2Source, kv +} + +func storeBlock(t *testing.T, kv kvstore.KV, block *types.Block, receipts types.Receipts) { + // Pre-store receipts + opaqueRcpts, err := eth.EncodeReceipts(receipts) + require.NoError(t, err) + _, nodes := mpt.WriteTrie(opaqueRcpts) + for _, p := range nodes { + require.NoError(t, kv.Put(preimage.Keccak256Key(crypto.Keccak256Hash(p)).PreimageKey(), p)) + } + + // Pre-store transactions + opaqueTxs, err := eth.EncodeTransactions(block.Transactions()) + require.NoError(t, err) + _, txsNodes := mpt.WriteTrie(opaqueTxs) + for _, p := range txsNodes { + require.NoError(t, kv.Put(preimage.Keccak256Key(crypto.Keccak256Hash(p)).PreimageKey(), p)) + } + + // Pre-store block + headerRlp, err := rlp.EncodeToBytes(block.Header()) + require.NoError(t, err) + require.NoError(t, kv.Put(preimage.Keccak256Key(block.Hash()).PreimageKey(), headerRlp)) +} + +func asOracleFn(t *testing.T, prefetcher *Prefetcher) preimage.OracleFn { + return func(key preimage.Key) []byte { + pre, err := prefetcher.GetPreimage(context.Background(), key.PreimageKey()) + require.NoError(t, err) + return pre + } +} + +func asHinter(t *testing.T, prefetcher *Prefetcher) preimage.HinterFn { + return func(v preimage.Hint) { + err := prefetcher.Hint(v.Hint()) + require.NoError(t, err) + } +} + +func assertTransactionsEqual(t *testing.T, blockTx types.Transactions, txs types.Transactions) { + require.Equal(t, len(blockTx), len(txs)) + for i, tx := range txs { + require.Equal(t, blockTx[i].Hash(), tx.Hash()) + } +} + +func assertReceiptsEqual(t *testing.T, expectedRcpt types.Receipts, actualRcpt types.Receipts) { + require.Equal(t, len(expectedRcpt), len(actualRcpt)) + for i, rcpt := range actualRcpt { + // Make a copy of each to zero out fields we expect to be different + expected := *expectedRcpt[i] + actual := *rcpt + expected.ContractAddress = common.Address{} + actual.ContractAddress = common.Address{} + require.Equal(t, expected, actual) + } +}