diff --git a/ethclient/lightclient/chain.go b/ethclient/lightclient/blocks.go similarity index 56% rename from ethclient/lightclient/chain.go rename to ethclient/lightclient/blocks.go index ace30bfbc5..800d337de9 100644 --- a/ethclient/lightclient/chain.go +++ b/ethclient/lightclient/blocks.go @@ -21,241 +21,15 @@ import ( "encoding/json" "errors" "math/big" - "sync" - "time" "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/beacon/light/request" btypes "github.com/ethereum/go-ethereum/beacon/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/lru" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rpc" ) -const recentCanonicalLength = 256 - -type canonicalChainFields struct { - chainLock sync.Mutex - head, finality *btypes.ExecutionHeader - recent map[uint64]common.Hash // nil while head == nil - recentTail uint64 // if recent != nil then recent hashes are available from recentTail to head - tailFetchCh chan struct{} - finalized *lru.Cache[uint64, common.Hash] // finalized but not recent hashes - requests *requestMap[uint64, common.Hash] // requested; neither recent nor cached finalized -} - -func (c *Client) initCanonicalChain() { - c.finalized = lru.NewCache[uint64, common.Hash](10000) - c.requests = newRequestMap[uint64, common.Hash](nil) - c.tailFetchCh = make(chan struct{}) - go c.tailFetcher() -} - -func (c *Client) closeCanonicalChain() { - c.requests.close() -} - -// Process implements request.Module in order to get notified about new heads. -func (c *Client) Process(requester request.Requester, events []request.Event) { - if finality, ok := c.headTracker.ValidatedFinality(); ok { - finalized := finality.Finalized.PayloadHeader - c.setFinality(finalized) - c.addPayloadHeader(finalized) - } - if optimistic, ok := c.headTracker.ValidatedOptimistic(); ok { - head := optimistic.Attested.PayloadHeader - c.addPayloadHeader(head) - if c.setHead(head) { - go c.processNewHead(head.BlockNumber(), head.BlockHash()) - } - } -} - -func (c *Client) tailFetcher() { //TODO stop - for { - c.chainLock.Lock() - var ( - tailNum uint64 - tailHash common.Hash - ) - if c.recent != nil { - tailNum, tailHash = c.recentTail, c.recent[c.recentTail] - } - needTail := tailNum - for _, reqNum := range c.requests.allKeys() { - if reqNum < needTail { - needTail = reqNum - } - } - c.chainLock.Unlock() - if needTail < tailNum { //TODO check recentCanonicalLength - log.Debug("Fetching tail headers", "have", tailNum, "need", needTail) - ctx, _ := context.WithTimeout(context.Background(), time.Second*10) - //TODO parallel fetch by number - if header, err := c.getHeader(ctx, tailHash); err == nil { - c.addRecentTail(header) - } - } else { - <-c.tailFetchCh - } - } -} - -func (c *Client) getHash(ctx context.Context, number uint64) (common.Hash, error) { - if hash, ok := c.getCachedHash(number); ok { - return hash, nil - } - req := c.requests.request(number) - select { - case c.tailFetchCh <- struct{}{}: - default: - } - defer req.release() - return req.getResult(ctx) -} - -func (c *Client) getCachedHash(number uint64) (common.Hash, bool) { - c.chainLock.Lock() - hash, ok := c.recent[number] - c.chainLock.Unlock() - if ok { - return hash, true - } - return c.finalized.Get(number) -} - -func (c *Client) setHead(head *btypes.ExecutionHeader) bool { - c.chainLock.Lock() - defer c.chainLock.Unlock() - - headNum, headHash := head.BlockNumber(), head.BlockHash() - if c.head != nil && c.head.BlockHash() == headHash { - 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 - c.recent = make(map[uint64]common.Hash) - if headNum > 0 { - c.recent[headNum-1] = head.ParentHash() - c.recentTail = headNum - 1 - } else { - c.recentTail = 0 - } - } - 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]) - } - delete(c.recent, c.recentTail) - c.recentTail++ - } - c.requests.tryDeliver(headNum, headHash) - log.Debug("SetHead", "recentTail", c.recentTail, "head", headNum) - return true -} - -func (c *Client) setFinality(finality *btypes.ExecutionHeader) { - c.chainLock.Lock() - defer c.chainLock.Unlock() - - c.finality = finality - finalNum := finality.BlockNumber() - if finalNum < c.recentTail { - c.finalized.Add(finalNum, finality.BlockHash()) - } - c.requests.tryDeliver(finalNum, finality.BlockHash()) -} - -func (c *Client) addRecentTail(tail *types.Header) bool { - c.chainLock.Lock() - defer c.chainLock.Unlock() - - if c.recent == nil || tail.Number.Uint64() != c.recentTail || c.recent[c.recentTail] != tail.Hash() { - return false - } - if c.recentTail > 0 { - c.recentTail-- - c.recent[c.recentTail] = tail.ParentHash - c.requests.tryDeliver(c.recentTail, tail.ParentHash) - } - return true -} - -func (c *Client) getHead() *btypes.ExecutionHeader { - c.chainLock.Lock() - defer c.chainLock.Unlock() - - return c.head -} - -func (c *Client) getFinality() *btypes.ExecutionHeader { - c.chainLock.Lock() - defer c.chainLock.Unlock() - - return c.finality -} - -func (c *Client) resolveBlockNumber(number *big.Int) (uint64, *btypes.ExecutionHeader, error) { - if !number.IsInt64() { - return 0, nil, errors.New("Invalid block number") - } - num := number.Int64() - if num < 0 { - switch rpc.BlockNumber(num) { - case rpc.SafeBlockNumber, rpc.FinalizedBlockNumber: - if header := c.getFinality(); header != nil { - return header.BlockNumber(), header, nil - } - return 0, nil, errors.New("Finalized block unknown") - case rpc.LatestBlockNumber, rpc.PendingBlockNumber: - if header := c.getHead(); header != nil { - return header.BlockNumber(), header, nil - } - return 0, nil, errors.New("Head block unknown") - default: - return 0, nil, errors.New("Invalid block number") - } - } - return uint64(num), nil, nil -} - -func (c *Client) blockNumberToHash(ctx context.Context, number *big.Int) (common.Hash, error) { - num, pheader, err := c.resolveBlockNumber(number) - if err != nil { - return common.Hash{}, err - } - if pheader != nil { - return pheader.BlockHash(), nil - } - return c.getHash(ctx, num) -} - -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))) - } - hash := *blockNrOrHash.BlockHash - if blockNrOrHash.RequireCanonical { - header, err := c.getHeader(ctx, hash) - if err != nil { - return common.Hash{}, err - } - chash, err := c.getHash(ctx, header.Number.Uint64()) - if err != nil { - return common.Hash{}, err - } - if chash != hash { - return common.Hash{}, errors.New("hash is not currently canonical") - } - } - return hash, nil -} - type blocksAndHeadersFields struct { headerCache *lru.Cache[common.Hash, *types.Header] headerRequests *requestMap[common.Hash, *types.Header] @@ -277,41 +51,6 @@ func (c *Client) closeBlocksAndHeaders() { c.blockRequests.close() } -func (c *Client) requestHeader(ctx context.Context, hash common.Hash) (*types.Header, error) { - var header *types.Header - log.Debug("Starting RPC request", "type", "eth_getBlockByHash", "hash", hash, "full", false) - err := c.client.CallContext(ctx, &header, "eth_getBlockByHash", hash, false) - if err == nil && header == nil { - err = ethereum.NotFound - } - if err == nil && header.Hash() != hash { - header, err = nil, errors.New("header hash does not match") - } - log.Debug("Finished RPC request", "type", "eth_getBlockByHash", "hash", hash, "full", false, "error", err) - return header, err -} - -func (c *Client) requestBlock(ctx context.Context, hash common.Hash) (*types.Block, error) { - var ( - raw json.RawMessage - block *types.Block - ) - log.Debug("Starting RPC request", "type", "eth_getBlockByHash", "hash", hash, "full", true) - err := c.client.CallContext(ctx, &raw, "eth_getBlockByHash", hash, true) - log.Debug("Finished RPC request", "type", "eth_getBlockByHash", "hash", hash, "full", true, "error", err) - if err != nil { - return nil, err - } - block, err = decodeBlock(raw) // returns ethereum.NotFound if block not found - if err != nil { - return nil, err - } - if block.Hash() != hash { - return nil, errors.New("block hash does not match") - } - return block, nil -} - func (c *Client) getHeader(ctx context.Context, hash common.Hash) (*types.Header, error) { if header, ok := c.headerCache.Get(hash); ok { return header, nil @@ -347,6 +86,10 @@ func (c *Client) getPayloadHeader(hash common.Hash) *btypes.ExecutionHeader { return pheader } +func (c *Client) addPayloadHeader(header *btypes.ExecutionHeader) { + c.payloadHeaderCache.Add(header.BlockHash(), header) +} + func (c *Client) getBlock(ctx context.Context, hash common.Hash) (*types.Block, error) { if block, ok := c.blockCache.Get(hash); ok { return block, nil @@ -364,7 +107,81 @@ func (c *Client) getBlock(ctx context.Context, hash common.Hash) (*types.Block, return block, err } +func (c *Client) requestHeader(ctx context.Context, hash common.Hash) (*types.Header, error) { + var header *types.Header + log.Debug("Starting RPC request", "type", "eth_getBlockByHash", "hash", hash, "full", false) + err := c.client.CallContext(ctx, &header, "eth_getBlockByHash", hash, false) + if err == nil && header == nil { + err = ethereum.NotFound + } + if err == nil && header.Hash() != hash { + header, err = nil, errors.New("header hash does not match") + } + log.Debug("Finished RPC request", "type", "eth_getBlockByHash", "hash", hash, "full", false, "error", err) + return header, err +} + +func (c *Client) requestBlock(ctx context.Context, hash common.Hash) (*types.Block, error) { + var ( + raw json.RawMessage + block *types.Block + ) + log.Debug("Starting RPC request", "type", "eth_getBlockByHash", "hash", hash, "full", true) + err := c.client.CallContext(ctx, &raw, "eth_getBlockByHash", hash, true) + log.Debug("Finished RPC request", "type", "eth_getBlockByHash", "hash", hash, "full", true, "error", err) + if err != nil { + return nil, err + } + block, err = decodeBlock(raw) // returns ethereum.NotFound if block not found + if err != nil { + return nil, err + } + if block.Hash() != hash { + return nil, errors.New("block hash does not match") + } + return block, nil +} + //TODO de-duplicate json block decoding +func decodeBlock(raw json.RawMessage) (*types.Block, error) { + // Decode header and transactions. + var head *types.Header + if err := json.Unmarshal(raw, &head); err != nil { + return nil, err + } + // When the block is not found, the API returns JSON null. + if head == nil { + return nil, ethereum.NotFound + } + + var body rpcBlock + if err := json.Unmarshal(raw, &body); err != nil { + return nil, err + } + // Quick-verify transaction and uncle lists. This mostly helps with debugging the server. + if head.UncleHash == types.EmptyUncleHash && len(body.UncleHashes) > 0 { + return nil, errors.New("server returned non-empty uncle list but block header indicates no uncles") + } + if head.UncleHash != types.EmptyUncleHash && len(body.UncleHashes) == 0 { + return nil, errors.New("server returned empty uncle list but block header indicates uncles") + } + if head.TxHash == types.EmptyTxsHash && len(body.Transactions) > 0 { + return nil, errors.New("server returned non-empty transaction list but block header indicates no transactions") + } + if head.TxHash != types.EmptyTxsHash && len(body.Transactions) == 0 { + return nil, errors.New("server returned empty transaction list but block header indicates transactions") + } + // Fill the sender cache of transactions in the block. + txs := make([]*types.Transaction, len(body.Transactions)) + for i, tx := range body.Transactions { + if tx.From != nil { + setSenderFromServer(tx.tx, *tx.From, body.Hash) + } + txs[i] = tx.tx + } + return types.NewBlockWithHeader(head).WithBody(types.Body{Transactions: txs, Withdrawals: body.Withdrawals}), nil +} + type rpcBlock struct { Hash common.Hash `json:"hash"` Transactions []rpcTransaction `json:"transactions"` @@ -426,46 +243,3 @@ func (s *senderFromServer) Hash(tx *types.Transaction) common.Hash { func (s *senderFromServer) SignatureValues(tx *types.Transaction, sig []byte) (R, S, V *big.Int, err error) { panic("can't sign with senderFromServer") } - -func decodeBlock(raw json.RawMessage) (*types.Block, error) { - // Decode header and transactions. - var head *types.Header - if err := json.Unmarshal(raw, &head); err != nil { - return nil, err - } - // When the block is not found, the API returns JSON null. - if head == nil { - return nil, ethereum.NotFound - } - - var body rpcBlock - if err := json.Unmarshal(raw, &body); err != nil { - return nil, err - } - // Quick-verify transaction and uncle lists. This mostly helps with debugging the server. - if head.UncleHash == types.EmptyUncleHash && len(body.UncleHashes) > 0 { - return nil, errors.New("server returned non-empty uncle list but block header indicates no uncles") - } - if head.UncleHash != types.EmptyUncleHash && len(body.UncleHashes) == 0 { - return nil, errors.New("server returned empty uncle list but block header indicates uncles") - } - if head.TxHash == types.EmptyTxsHash && len(body.Transactions) > 0 { - return nil, errors.New("server returned non-empty transaction list but block header indicates no transactions") - } - if head.TxHash != types.EmptyTxsHash && len(body.Transactions) == 0 { - return nil, errors.New("server returned empty transaction list but block header indicates transactions") - } - // Fill the sender cache of transactions in the block. - txs := make([]*types.Transaction, len(body.Transactions)) - for i, tx := range body.Transactions { - if tx.From != nil { - setSenderFromServer(tx.tx, *tx.From, body.Hash) - } - txs[i] = tx.tx - } - return types.NewBlockWithHeader(head).WithBody(types.Body{Transactions: txs, Withdrawals: body.Withdrawals}), nil -} - -func (c *Client) addPayloadHeader(header *btypes.ExecutionHeader) { - c.payloadHeaderCache.Add(header.BlockHash(), header) -} diff --git a/ethclient/lightclient/canonical.go b/ethclient/lightclient/canonical.go new file mode 100644 index 0000000000..77ab47429b --- /dev/null +++ b/ethclient/lightclient/canonical.go @@ -0,0 +1,238 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package lightclient + +import ( + "context" + "errors" + "math/big" + "sync" + "time" + + btypes "github.com/ethereum/go-ethereum/beacon/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/lru" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" +) + +const recentCanonicalLength = 256 + +type canonicalChainFields struct { + chainLock sync.Mutex + head, finality *btypes.ExecutionHeader + recent map[uint64]common.Hash // nil while head == nil + recentTail uint64 // if recent != nil then recent hashes are available from recentTail to head + tailFetchCh chan struct{} + finalized *lru.Cache[uint64, common.Hash] // finalized but not recent hashes + requests *requestMap[uint64, common.Hash] // requested; neither recent nor cached finalized +} + +func (c *Client) initCanonicalChain() { + c.finalized = lru.NewCache[uint64, common.Hash](10000) + c.requests = newRequestMap[uint64, common.Hash](nil) + c.tailFetchCh = make(chan struct{}) + go c.tailFetcher() +} + +func (c *Client) closeCanonicalChain() { + c.requests.close() +} + +func (c *Client) setHead(head *btypes.ExecutionHeader) bool { + c.chainLock.Lock() + defer c.chainLock.Unlock() + + headNum, headHash := head.BlockNumber(), head.BlockHash() + if c.head != nil && c.head.BlockHash() == headHash { + 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 + c.recent = make(map[uint64]common.Hash) + if headNum > 0 { + c.recent[headNum-1] = head.ParentHash() + c.recentTail = headNum - 1 + } else { + c.recentTail = 0 + } + } + 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]) + } + delete(c.recent, c.recentTail) + c.recentTail++ + } + c.requests.tryDeliver(headNum, headHash) + log.Debug("SetHead", "recentTail", c.recentTail, "head", headNum) + return true +} + +func (c *Client) setFinality(finality *btypes.ExecutionHeader) { + c.chainLock.Lock() + defer c.chainLock.Unlock() + + c.finality = finality + finalNum := finality.BlockNumber() + if finalNum < c.recentTail { + c.finalized.Add(finalNum, finality.BlockHash()) + } + c.requests.tryDeliver(finalNum, finality.BlockHash()) +} + +func (c *Client) getHead() *btypes.ExecutionHeader { + c.chainLock.Lock() + defer c.chainLock.Unlock() + + return c.head +} + +func (c *Client) getFinality() *btypes.ExecutionHeader { + c.chainLock.Lock() + defer c.chainLock.Unlock() + + return c.finality +} + +func (c *Client) addRecentTail(tail *types.Header) bool { + c.chainLock.Lock() + defer c.chainLock.Unlock() + + if c.recent == nil || tail.Number.Uint64() != c.recentTail || c.recent[c.recentTail] != tail.Hash() { + return false + } + if c.recentTail > 0 { + c.recentTail-- + c.recent[c.recentTail] = tail.ParentHash + c.requests.tryDeliver(c.recentTail, tail.ParentHash) + } + return true +} + +func (c *Client) tailFetcher() { //TODO stop + for { + c.chainLock.Lock() + var ( + tailNum uint64 + tailHash common.Hash + ) + if c.recent != nil { + tailNum, tailHash = c.recentTail, c.recent[c.recentTail] + } + needTail := tailNum + for _, reqNum := range c.requests.allKeys() { + if reqNum < needTail { + needTail = reqNum + } + } + c.chainLock.Unlock() + if needTail < tailNum { //TODO check recentCanonicalLength + log.Debug("Fetching tail headers", "have", tailNum, "need", needTail) + ctx, _ := context.WithTimeout(context.Background(), time.Second*10) + //TODO parallel fetch by number + if header, err := c.getHeader(ctx, tailHash); err == nil { + c.addRecentTail(header) + } + } else { + <-c.tailFetchCh + } + } +} + +func (c *Client) getHash(ctx context.Context, number uint64) (common.Hash, error) { + if hash, ok := c.getCachedHash(number); ok { + return hash, nil + } + req := c.requests.request(number) + select { + case c.tailFetchCh <- struct{}{}: + default: + } + defer req.release() + return req.getResult(ctx) +} + +func (c *Client) getCachedHash(number uint64) (common.Hash, bool) { + c.chainLock.Lock() + hash, ok := c.recent[number] + c.chainLock.Unlock() + if ok { + return hash, true + } + return c.finalized.Get(number) +} + +func (c *Client) resolveBlockNumber(number *big.Int) (uint64, *btypes.ExecutionHeader, error) { + if !number.IsInt64() { + return 0, nil, errors.New("Invalid block number") + } + num := number.Int64() + if num < 0 { + switch rpc.BlockNumber(num) { + case rpc.SafeBlockNumber, rpc.FinalizedBlockNumber: + if header := c.getFinality(); header != nil { + return header.BlockNumber(), header, nil + } + return 0, nil, errors.New("Finalized block unknown") + case rpc.LatestBlockNumber, rpc.PendingBlockNumber: + if header := c.getHead(); header != nil { + return header.BlockNumber(), header, nil + } + return 0, nil, errors.New("Head block unknown") + default: + return 0, nil, errors.New("Invalid block number") + } + } + return uint64(num), nil, nil +} + +func (c *Client) blockNumberToHash(ctx context.Context, number *big.Int) (common.Hash, error) { + num, pheader, err := c.resolveBlockNumber(number) + if err != nil { + return common.Hash{}, err + } + if pheader != nil { + return pheader.BlockHash(), nil + } + return c.getHash(ctx, num) +} + +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))) + } + hash := *blockNrOrHash.BlockHash + if blockNrOrHash.RequireCanonical { + header, err := c.getHeader(ctx, hash) + if err != nil { + return common.Hash{}, err + } + chash, err := c.getHash(ctx, header.Number.Uint64()) + if err != nil { + return common.Hash{}, err + } + if chash != hash { + return common.Hash{}, errors.New("hash is not currently canonical") + } + } + return hash, nil +} diff --git a/ethclient/lightclient/lightclient.go b/ethclient/lightclient/lightclient.go index dae6814c13..1eb45d50b3 100644 --- a/ethclient/lightclient/lightclient.go +++ b/ethclient/lightclient/lightclient.go @@ -80,7 +80,7 @@ func NewClient(clConfig config.LightClientConfig, elConfig *params.ChainConfig, client.scheduler.RegisterModule(checkpointInit, "checkpointInit") client.scheduler.RegisterModule(forwardSync, "forwardSync") client.scheduler.RegisterModule(headSync, "headSync") - client.scheduler.RegisterModule(client, "canonicalChain") + client.scheduler.RegisterModule((*schedulerModule)(client), "lightClient") return client } @@ -100,6 +100,25 @@ func (c *Client) Close() { c.scheduler.Stop() } +type schedulerModule Client + +// Process implements request.Module in order to get notified about new heads. +func (s *schedulerModule) Process(requester request.Requester, events []request.Event) { + c := (*Client)(s) + if finality, ok := c.headTracker.ValidatedFinality(); ok { + finalized := finality.Finalized.PayloadHeader + c.setFinality(finalized) + c.addPayloadHeader(finalized) + } + if optimistic, ok := c.headTracker.ValidatedOptimistic(); ok { + head := optimistic.Attested.PayloadHeader + c.addPayloadHeader(head) + if c.setHead(head) { + go c.processNewHead(head.BlockNumber(), head.BlockHash()) + } + } +} + // ChainReader interface func (c *Client) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { diff --git a/ethclient/lightclient/state.go b/ethclient/lightclient/state.go index 7d129de949..93eba62541 100644 --- a/ethclient/lightclient/state.go +++ b/ethclient/lightclient/state.go @@ -35,6 +35,13 @@ import ( "github.com/holiman/uint256" ) +type lightStateFields struct { + proofCache *lru.Cache[proofRequest, *gethclient.AccountResult] + proofRequests *requestMap[proofRequest, *gethclient.AccountResult] + codeCache *lru.Cache[codeRequest, []byte] + codeRequests *requestMap[codeRequest, []byte] +} + type proofRequest struct { blockNumber uint64 address common.Address @@ -46,13 +53,6 @@ type codeRequest struct { address common.Address } -type lightStateFields struct { - proofCache *lru.Cache[proofRequest, *gethclient.AccountResult] - proofRequests *requestMap[proofRequest, *gethclient.AccountResult] - codeCache *lru.Cache[codeRequest, []byte] - codeRequests *requestMap[codeRequest, []byte] -} - func (c *Client) initLightState() { c.proofCache = lru.NewCache[proofRequest, *gethclient.AccountResult](100) c.codeCache = lru.NewCache[codeRequest, []byte](10) @@ -65,135 +65,6 @@ func (c *Client) closeLightState() { c.codeRequests.close() } -func (c *Client) fetchProof(ctx context.Context, req proofRequest) (*gethclient.AccountResult, error) { - if proof, ok := c.proofCache.Get(req); ok { - return proof, nil - } - request := c.proofRequests.request(req) - proof, err := request.getResult(ctx) - if err == nil { - c.proofCache.Add(req, proof) // cached before validation; remove and retry if invalid - } - request.release() - return proof, err -} - -func (c *Client) requestProof(ctx context.Context, req proofRequest) (*gethclient.AccountResult, error) { - type storageResult struct { - Key string `json:"key"` - Value *hexutil.Big `json:"value"` - Proof []string `json:"proof"` - } - - type accountResult struct { - Address common.Address `json:"address"` - AccountProof []string `json:"accountProof"` - Balance *hexutil.Big `json:"balance"` - CodeHash common.Hash `json:"codeHash"` - Nonce hexutil.Uint64 `json:"nonce"` - StorageHash common.Hash `json:"storageHash"` - StorageProof []storageResult `json:"storageProof"` - } - - var storageKeys []string - if len(req.storageKeys) > 0 { - storageKeys = strings.Split(req.storageKeys, ",") - } - log.Debug("Starting RPC request", "type", "eth_getProof", "blockNumber", req.blockNumber, "address", req.address, "storageKeys", len(storageKeys)) - var res accountResult - err := c.client.CallContext(ctx, &res, "eth_getProof", req.address, storageKeys, hexutil.EncodeUint64(req.blockNumber)) - log.Debug("Finished RPC request", "type", "eth_getProof", "blockNumber", req.blockNumber, "address", req.address, "storageKeys", len(storageKeys), "error", err) - var proof *gethclient.AccountResult - if err == nil { //TODO de-duplicate - // Turn hexutils back to normal datatypes - storageResults := make([]gethclient.StorageResult, 0, len(res.StorageProof)) - for _, st := range res.StorageProof { - storageResults = append(storageResults, gethclient.StorageResult{ - Key: st.Key, - Value: st.Value.ToInt(), - Proof: st.Proof, - }) - } - proof = &gethclient.AccountResult{ - Address: res.Address, - AccountProof: res.AccountProof, - Balance: res.Balance.ToInt(), - Nonce: uint64(res.Nonce), - CodeHash: res.CodeHash, - StorageHash: res.StorageHash, - StorageProof: storageResults, - } - } - return proof, err -} - -func (c *Client) fetchCode(ctx context.Context, req codeRequest) ([]byte, error) { - if code, ok := c.codeCache.Get(req); ok { - return code, nil - } - request := c.codeRequests.request(req) - code, err := request.getResult(ctx) - if err == nil { - c.codeCache.Add(req, code) // cached before validation; remove and retry if invalid - } - request.release() - return code, err -} - -func (c *Client) requestCode(ctx context.Context, req codeRequest) ([]byte, error) { - var code hexutil.Bytes - log.Debug("Starting RPC request", "type", "eth_getCode", "blockNumber", req.blockNumber, "address", req.address) - err := c.client.CallContext(ctx, &code, "eth_getCode", req.address, hexutil.EncodeUint64(req.blockNumber)) - log.Debug("Finished RPC request", "type", "eth_getCode", "blockNumber", req.blockNumber, "address", req.address, "error", err) - return code, err -} - -// proofReader implements ethdb.KeyValueReader. -type proofReader map[string][]byte - -func (p proofReader) Has(key []byte) (bool, error) { - _, ok := p[string(key)] - return ok, nil -} - -func (p proofReader) Get(key []byte) ([]byte, error) { - if value, ok := p[string(key)]; ok { - return value, nil - } - return nil, errors.New("not found") -} - -func makeProofReader(proof []string) (proofReader, error) { - pr := make(proofReader) - for _, s := range proof { - node, err := hexutil.Decode(s) - if err != nil { - return nil, err - } - pr[string(crypto.Keccak256(node))] = node - } - return pr, nil -} - -func stValueBytes(value *big.Int) ([]byte, error) { - if value == nil { - return nil, errors.New("storage value is nil") - } - switch value.Sign() { - case -1: - return nil, errors.New("negative storage value") - case 1: - if value.BitLen() > 256 { - return nil, errors.New("storage value bigger than uint256") - } - stv := make([]byte, 32) - value.FillBytes(stv) - return stv, nil - default: - return nil, nil - } -} - func (c *Client) getProof(ctx context.Context, blockNumber *big.Int, account common.Address, storageKeys []string, getCode bool) (*gethclient.AccountResult, []byte, error) { proof, code, retry, err := c.getProofOnce(ctx, blockNumber, account, storageKeys, getCode) if retry { @@ -351,3 +222,132 @@ func (c *Client) validateProof(proof *gethclient.AccountResult, stateRoot common } return nil } + +// proofReader implements ethdb.KeyValueReader. +type proofReader map[string][]byte + +func makeProofReader(proof []string) (proofReader, error) { + pr := make(proofReader) + for _, s := range proof { + node, err := hexutil.Decode(s) + if err != nil { + return nil, err + } + pr[string(crypto.Keccak256(node))] = node + } + return pr, nil +} + +func (p proofReader) Has(key []byte) (bool, error) { + _, ok := p[string(key)] + return ok, nil +} + +func (p proofReader) Get(key []byte) ([]byte, error) { + if value, ok := p[string(key)]; ok { + return value, nil + } + return nil, errors.New("not found") +} + +func stValueBytes(value *big.Int) ([]byte, error) { + if value == nil { + return nil, errors.New("storage value is nil") + } + switch value.Sign() { + case -1: + return nil, errors.New("negative storage value") + case 1: + if value.BitLen() > 256 { + return nil, errors.New("storage value bigger than uint256") + } + stv := make([]byte, 32) + value.FillBytes(stv) + return stv, nil + default: + return nil, nil + } +} + +func (c *Client) fetchProof(ctx context.Context, req proofRequest) (*gethclient.AccountResult, error) { + if proof, ok := c.proofCache.Get(req); ok { + return proof, nil + } + request := c.proofRequests.request(req) + proof, err := request.getResult(ctx) + if err == nil { + c.proofCache.Add(req, proof) // cached before validation; remove and retry if invalid + } + request.release() + return proof, err +} + +func (c *Client) fetchCode(ctx context.Context, req codeRequest) ([]byte, error) { + if code, ok := c.codeCache.Get(req); ok { + return code, nil + } + request := c.codeRequests.request(req) + code, err := request.getResult(ctx) + if err == nil { + c.codeCache.Add(req, code) // cached before validation; remove and retry if invalid + } + request.release() + return code, err +} + +func (c *Client) requestProof(ctx context.Context, req proofRequest) (*gethclient.AccountResult, error) { + type storageResult struct { + Key string `json:"key"` + Value *hexutil.Big `json:"value"` + Proof []string `json:"proof"` + } + + type accountResult struct { + Address common.Address `json:"address"` + AccountProof []string `json:"accountProof"` + Balance *hexutil.Big `json:"balance"` + CodeHash common.Hash `json:"codeHash"` + Nonce hexutil.Uint64 `json:"nonce"` + StorageHash common.Hash `json:"storageHash"` + StorageProof []storageResult `json:"storageProof"` + } + + var storageKeys []string + if len(req.storageKeys) > 0 { + storageKeys = strings.Split(req.storageKeys, ",") + } + log.Debug("Starting RPC request", "type", "eth_getProof", "blockNumber", req.blockNumber, "address", req.address, "storageKeys", len(storageKeys)) + var res accountResult + err := c.client.CallContext(ctx, &res, "eth_getProof", req.address, storageKeys, hexutil.EncodeUint64(req.blockNumber)) + log.Debug("Finished RPC request", "type", "eth_getProof", "blockNumber", req.blockNumber, "address", req.address, "storageKeys", len(storageKeys), "error", err) + var proof *gethclient.AccountResult + if err == nil { //TODO de-duplicate + // Turn hexutils back to normal datatypes + storageResults := make([]gethclient.StorageResult, 0, len(res.StorageProof)) + for _, st := range res.StorageProof { + storageResults = append(storageResults, gethclient.StorageResult{ + Key: st.Key, + Value: st.Value.ToInt(), + Proof: st.Proof, + }) + } + proof = &gethclient.AccountResult{ + Address: res.Address, + AccountProof: res.AccountProof, + Balance: res.Balance.ToInt(), + Nonce: uint64(res.Nonce), + CodeHash: res.CodeHash, + StorageHash: res.StorageHash, + StorageProof: storageResults, + } + } + return proof, err +} + +func (c *Client) requestCode(ctx context.Context, req codeRequest) ([]byte, error) { + var code hexutil.Bytes + log.Debug("Starting RPC request", "type", "eth_getCode", "blockNumber", req.blockNumber, "address", req.address) + err := c.client.CallContext(ctx, &code, "eth_getCode", req.address, hexutil.EncodeUint64(req.blockNumber)) + log.Debug("Finished RPC request", "type", "eth_getCode", "blockNumber", req.blockNumber, "address", req.address, "error", err) + return code, err +} diff --git a/ethclient/lightclient/transactions.go b/ethclient/lightclient/transactions.go index 2014849a96..c2124ed42e 100644 --- a/ethclient/lightclient/transactions.go +++ b/ethclient/lightclient/transactions.go @@ -317,48 +317,6 @@ func (c *Client) sendTransaction(ctx context.Context, tx *types.Transaction) err return nil } -func (c *Client) markTxAsSeen(sender common.Address, tx *types.Transaction) { - senderTxs := c.sentTxs[sender] - if senderTxs == nil { - senderTxs = make(map[common.Hash]sentTx) - c.sentTxs[sender] = senderTxs - } - senderTxs[tx.Hash()] = sentTx{nonce: tx.Nonce(), lastSeen: c.headCounter} -} - -func (c *Client) txAndReceiptsNewHead(number uint64, hash common.Hash) { - c.sentTxLock.Lock() - if number > c.lastHeadNumber { - c.lastHeadNumber = number - c.headCounter++ - c.discardOldTxs() - } - c.sentTxLock.Unlock() -} - -func (c *Client) discardOldTxs() { - for sender, senderTxs := range c.sentTxs { - for txHash, sentTx := range senderTxs { - if sentTx.lastSeen+maxTxAge < c.headCounter { - delete(senderTxs, txHash) - } - } - if len(senderTxs) == 0 { - delete(c.sentTxs, sender) - } - } -} - -func (c *Client) allSenders() []common.Address { - c.sentTxLock.Lock() - allSenders := make([]common.Address, 0, len(c.sentTxs)) - for sender := range c.sentTxs { - allSenders = append(allSenders, sender) - } - c.sentTxLock.Unlock() - return allSenders -} - func (c *Client) nonceAndPendingTxs(ctx context.Context, head *btypes.ExecutionHeader, sender common.Address) (uint64, types.Transactions, error) { proof, err := c.fetchProof(ctx, proofRequest{blockNumber: head.BlockNumber(), address: sender, storageKeys: ""}) if err != nil { @@ -411,3 +369,45 @@ func (c *Client) nonceAndPendingTxs(ctx context.Context, head *btypes.ExecutionH c.sentTxLock.Unlock() return proof.Nonce, pendingList, nil } + +func (c *Client) allSenders() []common.Address { + c.sentTxLock.Lock() + allSenders := make([]common.Address, 0, len(c.sentTxs)) + for sender := range c.sentTxs { + allSenders = append(allSenders, sender) + } + c.sentTxLock.Unlock() + return allSenders +} + +func (c *Client) txAndReceiptsNewHead(number uint64, hash common.Hash) { + c.sentTxLock.Lock() + if number > c.lastHeadNumber { + c.lastHeadNumber = number + c.headCounter++ + c.discardOldTxs() + } + c.sentTxLock.Unlock() +} + +func (c *Client) discardOldTxs() { + for sender, senderTxs := range c.sentTxs { + for txHash, sentTx := range senderTxs { + if sentTx.lastSeen+maxTxAge < c.headCounter { + delete(senderTxs, txHash) + } + } + if len(senderTxs) == 0 { + delete(c.sentTxs, sender) + } + } +} + +func (c *Client) markTxAsSeen(sender common.Address, tx *types.Transaction) { + senderTxs := c.sentTxs[sender] + if senderTxs == nil { + senderTxs = make(map[common.Hash]sentTx) + c.sentTxs[sender] = senderTxs + } + senderTxs[tx.Hash()] = sentTx{nonce: tx.Nonce(), lastSeen: c.headCounter} +}