diff --git a/op-program/client/l1/cache_test.go b/op-program/client/l1/cache_test.go index 015192e693188..b50eeb47b0d3c 100644 --- a/op-program/client/l1/cache_test.go +++ b/op-program/client/l1/cache_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/ethereum-optimism/optimism/op-node/testutils" + "github.com/ethereum-optimism/optimism/op-program/client/l1/test" "github.com/stretchr/testify/require" ) @@ -13,37 +14,37 @@ var _ Oracle = (*CachingOracle)(nil) func TestCachingOracle_HeaderByBlockHash(t *testing.T) { rng := rand.New(rand.NewSource(1)) - stub := newStubOracle(t) + stub := test.NewStubOracle(t) oracle := NewCachingOracle(stub) block := testutils.RandomBlockInfo(rng) // Initial call retrieves from the stub - stub.blocks[block.Hash()] = block + stub.Blocks[block.Hash()] = block result := oracle.HeaderByBlockHash(block.Hash()) require.Equal(t, block, result) // Later calls should retrieve from cache - delete(stub.blocks, block.Hash()) + delete(stub.Blocks, block.Hash()) result = oracle.HeaderByBlockHash(block.Hash()) require.Equal(t, block, result) } func TestCachingOracle_TransactionsByBlockHash(t *testing.T) { rng := rand.New(rand.NewSource(1)) - stub := newStubOracle(t) + stub := test.NewStubOracle(t) oracle := NewCachingOracle(stub) block, _ := testutils.RandomBlock(rng, 3) // Initial call retrieves from the stub - stub.blocks[block.Hash()] = block - stub.txs[block.Hash()] = block.Transactions() + stub.Blocks[block.Hash()] = block + stub.Txs[block.Hash()] = block.Transactions() actualBlock, actualTxs := oracle.TransactionsByBlockHash(block.Hash()) require.Equal(t, block, actualBlock) require.Equal(t, block.Transactions(), actualTxs) // Later calls should retrieve from cache - delete(stub.blocks, block.Hash()) - delete(stub.txs, block.Hash()) + delete(stub.Blocks, block.Hash()) + delete(stub.Txs, block.Hash()) actualBlock, actualTxs = oracle.TransactionsByBlockHash(block.Hash()) require.Equal(t, block, actualBlock) require.Equal(t, block.Transactions(), actualTxs) @@ -51,20 +52,20 @@ func TestCachingOracle_TransactionsByBlockHash(t *testing.T) { func TestCachingOracle_ReceiptsByBlockHash(t *testing.T) { rng := rand.New(rand.NewSource(1)) - stub := newStubOracle(t) + stub := test.NewStubOracle(t) oracle := NewCachingOracle(stub) block, rcpts := testutils.RandomBlock(rng, 3) // Initial call retrieves from the stub - stub.blocks[block.Hash()] = block - stub.rcpts[block.Hash()] = rcpts + stub.Blocks[block.Hash()] = block + stub.Rcpts[block.Hash()] = rcpts actualBlock, actualRcpts := oracle.ReceiptsByBlockHash(block.Hash()) require.Equal(t, block, actualBlock) require.EqualValues(t, rcpts, actualRcpts) // Later calls should retrieve from cache - delete(stub.blocks, block.Hash()) - delete(stub.rcpts, block.Hash()) + delete(stub.Blocks, block.Hash()) + delete(stub.Rcpts, block.Hash()) actualBlock, actualRcpts = oracle.ReceiptsByBlockHash(block.Hash()) require.Equal(t, block, actualBlock) require.EqualValues(t, rcpts, actualRcpts) diff --git a/op-program/client/l1/client_test.go b/op-program/client/l1/client_test.go index 858cd5bb85d3e..b1130508a8e02 100644 --- a/op-program/client/l1/client_test.go +++ b/op-program/client/l1/client_test.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum-optimism/optimism/op-node/sources" "github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testutils" + "github.com/ethereum-optimism/optimism/op-program/client/l1/test" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -25,7 +26,7 @@ func TestInfoByHash(t *testing.T) { client, oracle := newClient(t) hash := common.HexToHash("0xAABBCC") expected := &sources.HeaderInfo{} - oracle.blocks[hash] = expected + oracle.Blocks[hash] = expected info, err := client.InfoByHash(context.Background(), hash) require.NoError(t, err) @@ -36,7 +37,7 @@ func TestL1BlockRefByHash(t *testing.T) { client, oracle := newClient(t) hash := common.HexToHash("0xAABBCC") header := &sources.HeaderInfo{} - oracle.blocks[hash] = header + oracle.Blocks[hash] = header expected := eth.InfoToL1BlockRef(header) ref, err := client.L1BlockRefByHash(context.Background(), hash) @@ -51,8 +52,8 @@ func TestFetchReceipts(t *testing.T) { expectedReceipts := types.Receipts{ &types.Receipt{}, } - oracle.blocks[hash] = expectedInfo - oracle.rcpts[hash] = expectedReceipts + oracle.Blocks[hash] = expectedInfo + oracle.Rcpts[hash] = expectedReceipts info, rcpts, err := client.FetchReceipts(context.Background(), hash) require.NoError(t, err) @@ -67,8 +68,8 @@ func TestInfoAndTxsByHash(t *testing.T) { expectedTxs := types.Transactions{ &types.Transaction{}, } - oracle.blocks[hash] = expectedInfo - oracle.txs[hash] = expectedTxs + oracle.Blocks[hash] = expectedInfo + oracle.Txs[hash] = expectedTxs info, txs, err := client.InfoAndTxsByHash(context.Background(), hash) require.NoError(t, err) @@ -120,7 +121,7 @@ func TestL1BlockRefByNumber(t *testing.T) { t.Run("ParentOfHead", func(t *testing.T) { client, oracle := newClient(t) parent := blockNum(head.NumberU64() - 1) - oracle.blocks[parent.Hash()] = parent + oracle.Blocks[parent.Hash()] = parent ref, err := client.L1BlockRefByNumber(context.Background(), parent.NumberU64()) require.NoError(t, err) @@ -132,7 +133,7 @@ func TestL1BlockRefByNumber(t *testing.T) { blocks := []eth.BlockInfo{block} for i := 0; i < 10; i++ { block = blockNum(block.NumberU64() - 1) - oracle.blocks[block.Hash()] = block + oracle.Blocks[block.Hash()] = block blocks = append(blocks, block) } @@ -144,9 +145,9 @@ func TestL1BlockRefByNumber(t *testing.T) { }) } -func newClient(t *testing.T) (*OracleL1Client, *stubOracle) { - stub := newStubOracle(t) - stub.blocks[head.Hash()] = head +func newClient(t *testing.T) (*OracleL1Client, *test.StubOracle) { + stub := test.NewStubOracle(t) + stub.Blocks[head.Hash()] = head client := NewOracleL1Client(testlog.Logger(t, log.LvlDebug), stub, head.Hash()) return client, stub } diff --git a/op-program/client/l1/stub_oracle_test.go b/op-program/client/l1/stub_oracle_test.go deleted file mode 100644 index 4e87d4c13d345..0000000000000 --- a/op-program/client/l1/stub_oracle_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package l1 - -import ( - "testing" - - "github.com/ethereum-optimism/optimism/op-node/eth" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" -) - -type stubOracle struct { - t *testing.T - - // blocks maps block hash to eth.BlockInfo - blocks map[common.Hash]eth.BlockInfo - - // txs maps block hash to transactions - txs map[common.Hash]types.Transactions - - // rcpts maps Block hash to receipts - rcpts map[common.Hash]types.Receipts -} - -func newStubOracle(t *testing.T) *stubOracle { - return &stubOracle{ - t: t, - blocks: make(map[common.Hash]eth.BlockInfo), - txs: make(map[common.Hash]types.Transactions), - rcpts: make(map[common.Hash]types.Receipts), - } -} -func (o stubOracle) HeaderByBlockHash(blockHash common.Hash) eth.BlockInfo { - info, ok := o.blocks[blockHash] - if !ok { - o.t.Fatalf("unknown block %s", blockHash) - } - return info -} - -func (o stubOracle) TransactionsByBlockHash(blockHash common.Hash) (eth.BlockInfo, types.Transactions) { - txs, ok := o.txs[blockHash] - if !ok { - o.t.Fatalf("unknown txs %s", blockHash) - } - return o.HeaderByBlockHash(blockHash), txs -} - -func (o stubOracle) ReceiptsByBlockHash(blockHash common.Hash) (eth.BlockInfo, types.Receipts) { - rcpts, ok := o.rcpts[blockHash] - if !ok { - o.t.Fatalf("unknown rcpts %s", blockHash) - } - return o.HeaderByBlockHash(blockHash), rcpts -} diff --git a/op-program/client/l1/test/stub_oracle.go b/op-program/client/l1/test/stub_oracle.go new file mode 100644 index 0000000000000..1ec03d945bc69 --- /dev/null +++ b/op-program/client/l1/test/stub_oracle.go @@ -0,0 +1,54 @@ +package test + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-node/eth" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +type StubOracle struct { + t *testing.T + + // Blocks maps block hash to eth.BlockInfo + Blocks map[common.Hash]eth.BlockInfo + + // Txs maps block hash to transactions + Txs map[common.Hash]types.Transactions + + // Rcpts maps Block hash to receipts + Rcpts map[common.Hash]types.Receipts +} + +func NewStubOracle(t *testing.T) *StubOracle { + return &StubOracle{ + t: t, + Blocks: make(map[common.Hash]eth.BlockInfo), + Txs: make(map[common.Hash]types.Transactions), + Rcpts: make(map[common.Hash]types.Receipts), + } +} +func (o StubOracle) HeaderByBlockHash(blockHash common.Hash) eth.BlockInfo { + info, ok := o.Blocks[blockHash] + if !ok { + o.t.Fatalf("unknown block %s", blockHash) + } + return info +} + +func (o StubOracle) TransactionsByBlockHash(blockHash common.Hash) (eth.BlockInfo, types.Transactions) { + txs, ok := o.Txs[blockHash] + if !ok { + o.t.Fatalf("unknown txs %s", blockHash) + } + return o.HeaderByBlockHash(blockHash), txs +} + +func (o StubOracle) ReceiptsByBlockHash(blockHash common.Hash) (eth.BlockInfo, types.Receipts) { + rcpts, ok := o.Rcpts[blockHash] + if !ok { + o.t.Fatalf("unknown rcpts %s", blockHash) + } + return o.HeaderByBlockHash(blockHash), rcpts +} diff --git a/op-program/host/l1/fetcher.go b/op-program/host/l1/fetcher.go index 46e8846c43f81..af91e9e1a7d3f 100644 --- a/op-program/host/l1/fetcher.go +++ b/op-program/host/l1/fetcher.go @@ -11,7 +11,7 @@ import ( ) type Source interface { - InfoByHash(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, error) + HeaderByHash(ctx context.Context, blockHash common.Hash) (*types.Header, 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) } @@ -31,15 +31,20 @@ func NewFetchingL1Oracle(ctx context.Context, logger log.Logger, source Source) } func (o *FetchingL1Oracle) HeaderByBlockHash(blockHash common.Hash) eth.BlockInfo { + header := o.RawHeaderByBlockHash(blockHash) + return eth.HeaderBlockInfo(header) +} + +func (o *FetchingL1Oracle) RawHeaderByBlockHash(blockHash common.Hash) *types.Header { o.logger.Trace("HeaderByBlockHash", "hash", blockHash) - info, err := o.source.InfoByHash(o.ctx, blockHash) + header, err := o.source.HeaderByHash(o.ctx, blockHash) if err != nil { panic(fmt.Errorf("retrieve block %s: %w", blockHash, err)) } - if info == nil { + if header == nil { panic(fmt.Errorf("unknown block: %s", blockHash)) } - return info + return header } func (o *FetchingL1Oracle) TransactionsByBlockHash(blockHash common.Hash) (eth.BlockInfo, types.Transactions) { diff --git a/op-program/host/prefetcher/prefetcher.go b/op-program/host/prefetcher/prefetcher.go new file mode 100644 index 0000000000000..2f3565a03cff6 --- /dev/null +++ b/op-program/host/prefetcher/prefetcher.go @@ -0,0 +1,128 @@ +package prefetcher + +import ( + "errors" + "fmt" + "strings" + + "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" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" +) + +type Prefetcher struct { + l1Fetcher l1.Oracle + l2Fetcher l2.Oracle + lastHint *preimage.Hint + kvStore kvstore.KV +} + +func NewPrefetcher(l1Fetcher l1.Oracle, l2Fetcher l2.Oracle, kvStore kvstore.KV) *Prefetcher { + return &Prefetcher{ + l1Fetcher: l1Fetcher, + l2Fetcher: l2Fetcher, + kvStore: kvStore, + } +} + +func (p *Prefetcher) Hint(hint string) error { + hintType, hashStr, found := strings.Cut(hint, " ") + if !found { + return fmt.Errorf("unsupported hint: %s", hint) + } + println(hintType, hashStr) + hash := common.HexToHash(hashStr) + if hash == (common.Hash{}) { + return fmt.Errorf("invalid hash: %s", hashStr) + } + var parsedHint preimage.Hint + switch hintType { + case "l1-block-header": + parsedHint = l1.BlockHeaderHint(hash) + case "l1-transactions": + parsedHint = l1.TransactionsHint(hash) + case "l1-receipts": + parsedHint = l1.ReceiptsHint(hash) + case "l2-block-header": + parsedHint = l2.BlockHeaderHint(hash) + case "l2-transactions": + parsedHint = l2.TransactionsHint(hash) + case "l2-code": + parsedHint = l2.CodeHint(hash) + case "l2-state-node": + parsedHint = l2.StateNodeHint(hash) + default: + p.lastHint = nil + return nil + } + p.lastHint = &parsedHint + return nil +} + +func (p *Prefetcher) GetPreimage(key common.Hash) ([]byte, error) { + pre, err := p.kvStore.Get(key) + if errors.Is(err, kvstore.ErrNotFound) && p.lastHint != nil { + hint := *p.lastHint + p.lastHint = nil + if err := p.prefetch(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(hint preimage.Hint) error { + switch v := hint.(type) { + case l1.BlockHeaderHint: + hash := common.Hash(v) + header := p.l1Fetcher.HeaderByBlockHash(hash) + data, err := rlp.EncodeToBytes(header) + if err != nil { + return fmt.Errorf("marshall header: %w", err) + } + + return p.kvStore.Put(preimage.Keccak256Key(hash).PreimageKey(), data) + case l1.TransactionsHint: + hash := common.Hash(v) + _, txs := p.l1Fetcher.TransactionsByBlockHash(hash) + opaqueTxs, err := eth.EncodeTransactions(txs) + if err != nil { + return err + } + return p.storeTrieNodes(opaqueTxs) + case l1.ReceiptsHint: + hash := common.Hash(v) + _, rcpts := p.l1Fetcher.ReceiptsByBlockHash(hash) + opaqueRcpts, err := eth.EncodeReceipts(rcpts) + if err != nil { + return err + } + return p.storeTrieNodes(opaqueRcpts) + + case l2.BlockHeaderHint: + case l2.TransactionsHint: + case l2.StateNodeHint: + case l2.CodeHint: + } + return fmt.Errorf("unknown hint type: %v", hint.Hint()) +} + +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 +} diff --git a/op-program/host/prefetcher/prefetcher_test.go b/op-program/host/prefetcher/prefetcher_test.go new file mode 100644 index 0000000000000..875e3eaece952 --- /dev/null +++ b/op-program/host/prefetcher/prefetcher_test.go @@ -0,0 +1,169 @@ +package prefetcher + +import ( + "fmt" + "math/rand" + "testing" + + "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" + l1test "github.com/ethereum-optimism/optimism/op-program/client/l1/test" + "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" + "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" +) + +func TestNoHint(t *testing.T) { + t.Run("NotFound", func(t *testing.T) { + prefetcher, _, _, _ := createPrefetcher(t) + res, err := prefetcher.GetPreimage(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(hash) + require.NoError(t, err) + require.Equal(t, res, data) + }) +} + +func TestFetchL1BlockHeader(t *testing.T) { + rng := rand.New(rand.NewSource(123)) + header := testutils.RandomHeader(rng) + hash := header.Hash() + key := preimage.Keccak256Key(hash).PreimageKey() + pre, err := rlp.EncodeToBytes(header) + require.NoError(t, err) + + t.Run("AlreadyKnown", func(t *testing.T) { + prefetcher, _, _, kv := createPrefetcher(t) + require.NoError(t, kv.Put(key, pre)) + + require.NoError(t, prefetcher.Hint(l1.BlockHeaderHint(hash).Hint())) + result, err := prefetcher.GetPreimage(key) + require.NoError(t, err) + require.Equal(t, pre, result) + }) + + t.Run("Unknown", func(t *testing.T) { + prefetcher, l1Oracle, _, _ := createPrefetcher(t) + l1Oracle.Blocks[hash] = eth.HeaderBlockInfo(header) + require.NoError(t, prefetcher.Hint(l1.BlockHeaderHint(hash).Hint())) + result, err := prefetcher.GetPreimage(key) + require.NoError(t, err) + require.Equal(t, pre, result) + }) +} + +func TestFetchL1Transactions(t *testing.T) { + rng := rand.New(rand.NewSource(123)) + block, _ := testutils.RandomBlock(rng, 10) + hash := block.Hash() + headerRlp, err := rlp.EncodeToBytes(block.Header()) + require.NoError(t, err) + + t.Run("AlreadyKnown", func(t *testing.T) { + prefetcher, _, _, kv := createPrefetcher(t) + + 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)) + } + require.NoError(t, kv.Put(preimage.Keccak256Key(hash).PreimageKey(), headerRlp)) + + 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, l1Oracle, _, _ := createPrefetcher(t) + l1Oracle.Blocks[hash] = block + l1Oracle.Txs[hash] = block.Transactions() + + 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 TestHint(t *testing.T) { + prefetcher, _, _, _ := createPrefetcher(t) + hash := common.Hash{0xad} + + t.Run("NoSpace", func(t *testing.T) { + require.Error(t, prefetcher.Hint("l1-block")) + }) + + t.Run("UnknownType", func(t *testing.T) { + // Unknown hint types are ignored and no pre-fetching will be done + var previousHint preimage.Hint = l1.BlockHeaderHint(hash) + prefetcher.lastHint = &previousHint + require.NoError(t, prefetcher.Hint("unknown "+hash.Hex())) + require.Nil(t, prefetcher.lastHint) + }) + + validHints := []preimage.Hint{ + l1.BlockHeaderHint(hash), + l1.TransactionsHint(hash), + l1.ReceiptsHint(hash), + l2.BlockHeaderHint(hash), + l2.TransactionsHint(hash), + l2.StateNodeHint(hash), + l2.CodeHint(hash), + } + for _, hint := range validHints { + hint := hint + t.Run(fmt.Sprintf("valid_%s", hint.Hint()), func(t *testing.T) { + require.NoError(t, prefetcher.Hint(hint.Hint())) + require.NotNil(t, *prefetcher.lastHint) + require.Equal(t, hint, *prefetcher.lastHint) + }) + } +} + +func createPrefetcher(t *testing.T) (*Prefetcher, *l1test.StubOracle, l2.Oracle, kvstore.KV) { + kv := kvstore.NewMemKV() + l1Oracle := l1test.NewStubOracle(t) + prefetcher := NewPrefetcher(l1Oracle, nil, kv) + return prefetcher, l1Oracle, nil, kv +} + +func asOracleFn(t *testing.T, prefetcher *Prefetcher) preimage.OracleFn { + return func(key preimage.Key) []byte { + pre, err := prefetcher.GetPreimage(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()) + } +}