From 756707f6b0da5b78768918be69cda0979147997c Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Mon, 15 Sep 2025 10:49:04 +1000 Subject: [PATCH] op-program: Add hint for fast block hash lookup --- op-program/client/interop/oracle_test.go | 3 ++ op-program/client/l2/fast_canon.go | 6 ++++ op-program/client/l2/fast_canon_test.go | 38 +++++++++++++++++++-- op-program/client/l2/hints.go | 43 ++++++++++++++++++------ op-program/client/l2/oracle.go | 10 ++++-- op-program/client/l2/test/stub_oracle.go | 24 +++++++++++-- op-program/client/l2/types/types.go | 1 + 7 files changed, 108 insertions(+), 17 deletions(-) diff --git a/op-program/client/interop/oracle_test.go b/op-program/client/interop/oracle_test.go index abcfb10e336c0..6acb762150bd2 100644 --- a/op-program/client/interop/oracle_test.go +++ b/op-program/client/interop/oracle_test.go @@ -251,3 +251,6 @@ func (o *OracleHinterStub) HintBlockExecution(parentBlockHash common.Hash, attr func (o *OracleHinterStub) HintWithdrawalsRoot(blockHash common.Hash, chainID eth.ChainID) { } + +func (o *OracleHinterStub) HintBlockHashLookup(blockNumber uint64, headBlockHash common.Hash, l2ChainID eth.ChainID) { +} diff --git a/op-program/client/l2/fast_canon.go b/op-program/client/l2/fast_canon.go index 053c7c1a88de6..db32dfab4582d 100644 --- a/op-program/client/l2/fast_canon.go +++ b/op-program/client/l2/fast_canon.go @@ -5,6 +5,7 @@ import ( "fmt" "math" + l2Types "github.com/ethereum-optimism/optimism/op-program/client/l2/types" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" @@ -33,6 +34,7 @@ type FastCanonicalBlockHeaderOracle struct { ctx *chainContext db ethdb.KeyValueStore cache *simplelru.LRU[uint64, *types.Header] + hinter l2Types.OracleHinter } func NewFastCanonicalBlockHeaderOracle( @@ -54,6 +56,7 @@ func NewFastCanonicalBlockHeaderOracle( fallback: fallback, ctx: ctx, db: db, + hinter: stateOracle.Hinter(), cache: cache, } } @@ -109,6 +112,9 @@ func (o *FastCanonicalBlockHeaderOracle) GetHeaderByNumber(n uint64) *types.Head } func (o *FastCanonicalBlockHeaderOracle) getHistoricalBlockHash(head *types.Header, n uint64) *types.Block { + if o.hinter != nil { + o.hinter.HintBlockHashLookup(n, head.Hash(), eth.ChainIDFromBig(o.config.ChainID)) + } statedb, err := state.New(head.Root, state.NewDatabase(triedb.NewDatabase(rawdb.NewDatabase(o.db), nil), nil)) if err != nil { panic(fmt.Errorf("failed to get state at %v: %w", head.Hash(), err)) diff --git a/op-program/client/l2/fast_canon_test.go b/op-program/client/l2/fast_canon_test.go index 5bd98856a26bf..7b4e819ca68b9 100644 --- a/op-program/client/l2/fast_canon_test.go +++ b/op-program/client/l2/fast_canon_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/ethereum-optimism/optimism/op-program/client/l2/test" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum-optimism/optimism/op-service/testutils" "github.com/ethereum/go-ethereum/common" @@ -20,7 +21,13 @@ func TestFastCanonBlockHeaderOracle_GetHeaderByNumber(t *testing.T) { logger, _ := testlog.CaptureLogger(t, log.LvlInfo) miner, backend := test.NewMiner(t, logger, 0) - stateOracle := &test.KvStateOracle{T: t, Source: backend.TrieDB().Disk()} + chainID := eth.ChainIDFromBig(backend.Config().ChainID) + capturingHinter := &test.CapturingHinter{} + stateOracle := &test.KvStateOracle{ + T: t, + Source: backend.TrieDB().Disk(), + StubHinter: NewPreimageHinter(capturingHinter), + } miner.Mine(t, nil) miner.Mine(t, nil) miner.Mine(t, nil) @@ -50,12 +57,31 @@ func TestFastCanonBlockHeaderOracle_GetHeaderByNumber(t *testing.T) { h := canon.GetHeaderByNumber(3) require.Equal(t, backend.GetBlockByNumber(3).Hash(), h.Hash()) + require.Len(t, capturingHinter.Hints, 0) // No lookups required h = canon.GetHeaderByNumber(2) require.Equal(t, backend.GetBlockByNumber(2).Hash(), h.Hash()) + require.Len(t, capturingHinter.Hints, 1) + require.Equal(t, capturingHinter.Hints[0], BlockHashLookupHint{ + BlockNumber: 2, + HeadBlockHash: head.Hash(), + ChainID: chainID, + }) h = canon.GetHeaderByNumber(1) require.Equal(t, backend.GetBlockByNumber(1).Hash(), h.Hash()) + require.Len(t, capturingHinter.Hints, 2) + require.Equal(t, capturingHinter.Hints[1], BlockHashLookupHint{ + BlockNumber: 1, + HeadBlockHash: head.Hash(), + ChainID: chainID, + }) h = canon.GetHeaderByNumber(0) require.Equal(t, backend.GetBlockByNumber(0).Hash(), h.Hash()) + require.Len(t, capturingHinter.Hints, 3) + require.Equal(t, capturingHinter.Hints[2], BlockHashLookupHint{ + BlockNumber: 0, + HeadBlockHash: head.Hash(), + ChainID: chainID, + }) } func TestFastCanonBlockHeaderOracle_LargeWindow(t *testing.T) { @@ -247,7 +273,12 @@ func TestFastCanonBlockHeaderOracle_SetCanonical(t *testing.T) { func runCanonicalCacheTest(t *testing.T, backend *core.BlockChain, blockNum uint64, expectedNumRequests int) { head := backend.CurrentHeader() tracker := newTrackingBlockByHash(backend.GetBlockByHash) - stateOracle := &test.KvStateOracle{T: t, Source: backend.TrieDB().Disk()} + capturingHinter := &test.CapturingHinter{} + stateOracle := &test.KvStateOracle{ + T: t, + Source: backend.TrieDB().Disk(), + StubHinter: NewPreimageHinter(capturingHinter), + } // Create invalid fallback to assert that it's never used. fatalBlockByHash := func(hash common.Hash) *types.Block { t.Fatalf("Unexpected fallback for block: %v", hash) @@ -261,12 +292,15 @@ func runCanonicalCacheTest(t *testing.T, backend *core.BlockChain, blockNum uint h := canon.GetHeaderByNumber(blockNum) require.Equal(t, expect, h.Hash()) require.Equalf(t, expectedNumRequests, tracker.numRequests, "Unexpected number of requests for block: %v (%d)", expect, blockNum) + require.Len(t, capturingHinter.Hints, expectedNumRequests) // query again and assert that it's cached tracker.numRequests = 0 + capturingHinter.Hints = nil h = canon.GetHeaderByNumber(blockNum) require.Equal(t, expect, h.Hash()) require.Equalf(t, 1, tracker.numRequests, "Unexpected number of requests for block: %v (%d)", expect, blockNum) + require.Len(t, capturingHinter.Hints, 1) } type trackingBlockByHash struct { diff --git a/op-program/client/l2/hints.go b/op-program/client/l2/hints.go index defd8ec829e6e..b691b0bfee650 100644 --- a/op-program/client/l2/hints.go +++ b/op-program/client/l2/hints.go @@ -15,16 +15,17 @@ import ( ) const ( - HintL2BlockHeader = "l2-block-header" - HintL2Transactions = "l2-transactions" - HintL2Receipts = "l2-receipts" - HintL2Code = "l2-code" - HintL2StateNode = "l2-state-node" - HintL2Output = "l2-output" - HintL2BlockData = "l2-block-data" - HintAgreedPrestate = "agreed-pre-state" - HintL2AccountProof = "l2-account-proof" - HintL2PayloadWitness = "l2-payload-witness" + HintL2BlockHeader = "l2-block-header" + HintL2Transactions = "l2-transactions" + HintL2Receipts = "l2-receipts" + HintL2Code = "l2-code" + HintL2StateNode = "l2-state-node" + HintL2Output = "l2-output" + HintL2BlockData = "l2-block-data" + HintAgreedPrestate = "agreed-pre-state" + HintL2AccountProof = "l2-account-proof" + HintL2PayloadWitness = "l2-payload-witness" + HintL2BlockHashLookup = "l2-block-hash-lookup" ) type LegacyBlockHeaderHint common.Hash @@ -185,3 +186,25 @@ func (l PayloadWitnessHint) Hint() string { return HintL2PayloadWitness + " " + hexutil.Encode(marshaled) } + +type BlockHashLookupHint struct { + BlockNumber uint64 + HeadBlockHash common.Hash + ChainID eth.ChainID +} + +func (b BlockHashLookupHint) Hint() string { + hintBytes := make([]byte, 8+32+8) + + binary.BigEndian.PutUint64(hintBytes[0:8], b.BlockNumber) + copy(hintBytes[8:40], b.HeadBlockHash.Bytes()) + binary.BigEndian.PutUint64(hintBytes[40:], eth.EvilChainIDToUInt64(b.ChainID)) + + return HintL2BlockHashLookup + " " + hexutil.Encode(hintBytes) +} + +func (b BlockHashLookupHint) String() string { + return fmt.Sprintf("%v(%v, %v, %v)", HintL2BlockHashLookup, b.BlockNumber, b.HeadBlockHash, b.ChainID) +} + +var _ preimage.Hint = BlockHashLookupHint{} diff --git a/op-program/client/l2/oracle.go b/op-program/client/l2/oracle.go index 9e015243c50db..c490b60911b18 100644 --- a/op-program/client/l2/oracle.go +++ b/op-program/client/l2/oracle.go @@ -26,6 +26,9 @@ type StateOracle interface { // CodeByHash retrieves the contract code pre-image for a given hash. // codeHash should be retrieved from the world state account for a contract. CodeByHash(codeHash common.Hash, chainID eth.ChainID) []byte + + // Hinter provides an optional interface to provide proactive hints. + Hinter() l2Types.OracleHinter } // Oracle defines the high-level API used to retrieve L2 data. @@ -44,9 +47,6 @@ type Oracle interface { TransitionStateByRoot(root common.Hash) *interopTypes.TransitionState ReceiptsByBlockHash(blockHash common.Hash, chainID eth.ChainID) (*types.Block, types.Receipts) - - // Optional interface to provide proactive hints. - Hinter() l2Types.OracleHinter } type PreimageOracleHinter struct { @@ -70,6 +70,10 @@ func (p *PreimageOracleHinter) HintWithdrawalsRoot(blockHash common.Hash, chainI p.hint.Hint(AccountProofHint{BlockHash: blockHash, Address: predeploys.L2ToL1MessagePasserAddr, ChainID: chainID}) } +func (p *PreimageOracleHinter) HintBlockHashLookup(blockNumber uint64, headBlockHash common.Hash, l2ChainID eth.ChainID) { + p.hint.Hint(BlockHashLookupHint{BlockNumber: blockNumber, HeadBlockHash: headBlockHash, ChainID: l2ChainID}) +} + // PreimageOracle implements Oracle using by interfacing with the pure preimage.Oracle // to fetch pre-images to decode into the requested data. type PreimageOracle struct { diff --git a/op-program/client/l2/test/stub_oracle.go b/op-program/client/l2/test/stub_oracle.go index bbe1737a3c9ab..602e434ccf0c8 100644 --- a/op-program/client/l2/test/stub_oracle.go +++ b/op-program/client/l2/test/stub_oracle.go @@ -4,6 +4,7 @@ import ( "encoding/binary" "testing" + preimage "github.com/ethereum-optimism/optimism/op-preimage" interopTypes "github.com/ethereum-optimism/optimism/op-program/client/interop/types" l2Types "github.com/ethereum-optimism/optimism/op-program/client/l2/types" "github.com/ethereum-optimism/optimism/op-service/eth" @@ -104,8 +105,9 @@ func (o StubBlockOracle) ReceiptsByBlockHash(blockHash common.Hash, chainID eth. // KvStateOracle loads data from a source ethdb.KeyValueStore type KvStateOracle struct { - T *testing.T - Source ethdb.KeyValueStore + T *testing.T + Source ethdb.KeyValueStore + StubHinter l2Types.OracleHinter } func NewKvStateOracle(t *testing.T, db ethdb.KeyValueStore) *KvStateOracle { @@ -127,6 +129,10 @@ func (o *KvStateOracle) CodeByHash(hash common.Hash, chainID eth.ChainID) []byte return rawdb.ReadCode(o.Source, hash) } +func (o *KvStateOracle) Hinter() l2Types.OracleHinter { + return o.StubHinter +} + func NewStubStateOracle(t *testing.T) *StubStateOracle { return &StubStateOracle{ t: t, @@ -158,6 +164,10 @@ func (o *StubStateOracle) CodeByHash(hash common.Hash, chainID eth.ChainID) []by return data } +func (o *StubStateOracle) Hinter() l2Types.OracleHinter { + return nil +} + type StubPrecompileOracle struct { t *testing.T Results map[common.Hash]PrecompileResult @@ -183,3 +193,13 @@ func (o *StubPrecompileOracle) Precompile(address common.Address, input []byte, o.Calls++ return result.Result, result.Ok } + +type CapturingHinter struct { + Hints []preimage.Hint +} + +func (c *CapturingHinter) Hint(v preimage.Hint) { + c.Hints = append(c.Hints, v) +} + +var _ preimage.Hinter = (*CapturingHinter)(nil) diff --git a/op-program/client/l2/types/types.go b/op-program/client/l2/types/types.go index f3815638313c3..9823baeae4542 100644 --- a/op-program/client/l2/types/types.go +++ b/op-program/client/l2/types/types.go @@ -11,4 +11,5 @@ import ( type OracleHinter interface { HintBlockExecution(parentBlockHash common.Hash, attr eth.PayloadAttributes, chainID eth.ChainID) HintWithdrawalsRoot(blockHash common.Hash, chainID eth.ChainID) + HintBlockHashLookup(blockNumber uint64, headBlockHash common.Hash, l2ChainID eth.ChainID) }