diff --git a/ethclient/lightclient/blocks.go b/ethclient/lightclient/blocks.go index 8b2e0360ef..55f8d62a4d 100644 --- a/ethclient/lightclient/blocks.go +++ b/ethclient/lightclient/blocks.go @@ -54,6 +54,7 @@ func (c *Client) closeBlocksAndHeaders() { c.blockRequests.close() } +// getHeader returns the header with the given block hash. func (c *Client) getHeader(ctx context.Context, hash common.Hash) (*types.Header, error) { if header, ok := c.headerCache.Get(hash); ok { return header, nil @@ -84,15 +85,25 @@ func (c *Client) getHeader(ctx context.Context, hash common.Hash) (*types.Header return header, err } +// getPayloadHeader returns the payload header with the given block hash. +// Note that types.Header and the CL execution payload header (btypes.ExecutionHeader) +// both contain the information relevant to the light client but they are not +// interchangeable. Payload headers are received from head/finality updates and +// are cached but they are not obtainable later on demand as they are not directly +// chained to each other by hash reference. If reverse syncing is required then +// EL headers should be used. func (c *Client) getPayloadHeader(hash common.Hash) *btypes.ExecutionHeader { pheader, _ := c.payloadHeaderCache.Get(hash) return pheader } +// addPayloadHeader caches the given payload header. func (c *Client) addPayloadHeader(header *btypes.ExecutionHeader) { c.payloadHeaderCache.Add(header.BlockHash(), header) } +// getBlock returns the block with the given hash. If the block has been retrieved +// from the server then the transaction inclusion positions are also cached. func (c *Client) getBlock(ctx context.Context, hash common.Hash) (*types.Block, error) { if block, ok := c.blockCache.Get(hash); ok { return block, nil @@ -149,7 +160,8 @@ func (c *Client) requestBlock(ctx context.Context, hash common.Hash) (*types.Blo return block, nil } -//TODO de-duplicate json block decoding +// decodeBlock decodes a JSON encoded block. +//TODO de-duplicate func decodeBlock(raw json.RawMessage) (*types.Block, error) { // Decode header and transactions. var head *types.Header diff --git a/ethclient/lightclient/canonical.go b/ethclient/lightclient/canonical.go index 4c04f41272..790b340e49 100644 --- a/ethclient/lightclient/canonical.go +++ b/ethclient/lightclient/canonical.go @@ -31,6 +31,8 @@ import ( "github.com/ethereum/go-ethereum/rpc" ) +// recentCanonicalLength specifies the maximum number of most recent number to hash +// associations stored in the recent map const recentCanonicalLength = 256 // canonicalChainFields defines Client fields related to canonical chain tracking. @@ -40,13 +42,13 @@ type canonicalChainFields struct { recent map[uint64]common.Hash // nil while head == nil recentTail uint64 // if recent != nil then recent hashes are available from recentTail to head tailFetchCh, closeCh chan struct{} - finalized *lru.Cache[uint64, common.Hash] // finalized but not recent hashes - requests *requestMap[uint64, common.Hash] // requested; neither recent nor cached finalized + cache *lru.Cache[uint64, common.Hash] // older than recentTail + requests *requestMap[uint64, common.Hash] // requested; neither recent nor cached } // initCanonicalChain initializes the structures related to canonical chain tracking. func (c *Client) initCanonicalChain() { - c.finalized = lru.NewCache[uint64, common.Hash](10000) + c.cache = lru.NewCache[uint64, common.Hash](10000) c.requests = newRequestMap[uint64, common.Hash](nil) c.tailFetchCh = make(chan struct{}) c.closeCh = make(chan struct{}) @@ -59,6 +61,8 @@ func (c *Client) closeCanonicalChain() { close(c.closeCh) } +// setHead sets a new head for the canonical chain. It also updates the recent +// hash associations and takes care of state cache invalidation if necessary. func (c *Client) setHead(head *btypes.ExecutionHeader) bool { c.chainLock.Lock() defer c.chainLock.Unlock() @@ -68,9 +72,15 @@ func (c *Client) setHead(head *btypes.ExecutionHeader) bool { return false } if c.recent == nil || c.head == nil || c.head.BlockNumber()+1 != headNum || c.head.BlockHash() != head.ParentHash() { - // initialize recent canonical hash map when first head is added or when - // it is not a descendant of the previous head + // new head is not a descendant of the previous one; everything that was + // not finalized should be invalidated. + if c.finality == nil || c.recentTail > c.finality.BlockNumber()+1 { + // purge cache if the previous contents were not all finalized + c.cache.Purge() + } + // state proofs are cached by number c.clearStateCache() + // initialize recent canonical hash map c.recent = make(map[uint64]common.Hash) if headNum > 0 { c.recent[headNum-1] = head.ParentHash() @@ -82,9 +92,7 @@ func (c *Client) setHead(head *btypes.ExecutionHeader) bool { c.head = head c.recent[headNum] = headHash for headNum >= c.recentTail+recentCanonicalLength { - if c.finality != nil && c.recentTail <= c.finality.BlockNumber() { - c.finalized.Add(c.recentTail, c.recent[c.recentTail]) - } + c.cache.Add(c.recentTail, c.recent[c.recentTail]) delete(c.recent, c.recentTail) c.recentTail++ } @@ -93,6 +101,7 @@ func (c *Client) setHead(head *btypes.ExecutionHeader) bool { return true } +// setFinality sets a new finality slot. func (c *Client) setFinality(finality *btypes.ExecutionHeader) { c.chainLock.Lock() defer c.chainLock.Unlock() @@ -100,11 +109,12 @@ func (c *Client) setFinality(finality *btypes.ExecutionHeader) { c.finality = finality finalNum := finality.BlockNumber() if finalNum < c.recentTail { - c.finalized.Add(finalNum, finality.BlockHash()) + c.cache.Add(finalNum, finality.BlockHash()) } c.requests.tryDeliver(finalNum, finality.BlockHash()) } +// getHead returns the current local canonical chain head. func (c *Client) getHead() *btypes.ExecutionHeader { c.chainLock.Lock() defer c.chainLock.Unlock() @@ -112,6 +122,7 @@ func (c *Client) getHead() *btypes.ExecutionHeader { return c.head } +// getFinality returns the current finality header func (c *Client) getFinality() *btypes.ExecutionHeader { c.chainLock.Lock() defer c.chainLock.Unlock() @@ -119,6 +130,8 @@ func (c *Client) getFinality() *btypes.ExecutionHeader { return c.finality } +// addRecentTail tries to add the given header to the tail of the recent canonical +// section and returns true if successful. func (c *Client) addRecentTail(tail *types.Header) bool { c.chainLock.Lock() defer c.chainLock.Unlock() @@ -136,6 +149,7 @@ func (c *Client) addRecentTail(tail *types.Header) bool { return true } +// tailFetcher reverse syncs canonical hashes until all requested items are present. func (c *Client) tailFetcher() { for { c.chainLock.Lock() @@ -173,6 +187,7 @@ func (c *Client) tailFetcher() { } } +// getHash returns the requested number -> hash association. func (c *Client) getHash(ctx context.Context, number uint64) (common.Hash, error) { if hash, ok := c.getCachedHash(number); ok { return hash, nil @@ -186,6 +201,8 @@ func (c *Client) getHash(ctx context.Context, number uint64) (common.Hash, error return req.waitForResult(ctx) } +// getCachedHash returns the requested number -> hash association if it is already +// in memory cache. func (c *Client) getCachedHash(number uint64) (common.Hash, bool) { c.chainLock.Lock() hash, ok := c.recent[number] @@ -193,9 +210,11 @@ func (c *Client) getCachedHash(number uint64) (common.Hash, bool) { if ok { return hash, true } - return c.finalized.Get(number) + return c.cache.Get(number) } +// resolveBlockNumber resolves an RPC block number into uint64 and also returns +// the payload header in case of special (negative) RPC numbers. func (c *Client) resolveBlockNumber(number *big.Int) (uint64, *btypes.ExecutionHeader, error) { if !number.IsInt64() { return 0, nil, errors.New("Invalid block number") @@ -220,6 +239,10 @@ func (c *Client) resolveBlockNumber(number *big.Int) (uint64, *btypes.ExecutionH return uint64(num), nil, nil } +// blockNumberToHash returns the canonical hash belonging to the given RPC block +// number. Note that requesting older canonical hashes might trigger a reverse +// header sync process which might take a very long time depending on the age +// of the specified block. func (c *Client) blockNumberToHash(ctx context.Context, number *big.Int) (common.Hash, error) { num, pheader, err := c.resolveBlockNumber(number) if err != nil { @@ -231,6 +254,7 @@ func (c *Client) blockNumberToHash(ctx context.Context, number *big.Int) (common return c.getHash(ctx, num) } +// blockNumberOrHashToHash resolves rpc.BlockNumberOrHash into a block hash. func (c *Client) blockNumberOrHashToHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (common.Hash, error) { if blockNrOrHash.BlockNumber != nil { return c.blockNumberToHash(ctx, big.NewInt(int64(*blockNrOrHash.BlockNumber))) diff --git a/ethclient/lightclient/transactions.go b/ethclient/lightclient/transactions.go index ceb22ea2cb..7c70ce715c 100644 --- a/ethclient/lightclient/transactions.go +++ b/ethclient/lightclient/transactions.go @@ -370,6 +370,7 @@ func (c *Client) sendTransaction(ctx context.Context, tx *types.Transaction) err return c.TrackTransaction(tx) } +// TrackTransaction adds a transaction to the set of tracked transactions. func (c *Client) TrackTransaction(tx *types.Transaction) error { sender, err := types.Sender(c.signer, tx) if err != nil {