From ef80de057aa8f93e52b975e2f0210412940c824e Mon Sep 17 00:00:00 2001 From: Zsolt Felfoldi Date: Sun, 26 May 2024 00:33:42 +0200 Subject: [PATCH] ethclient/lightclient: add transaction and receipt functions --- cmd/bltest/main.go | 12 +- cmd/geth/config.go | 3 +- ethclient/lightclient/chain.go | 44 ++++-- ethclient/lightclient/lightclient.go | 40 ++++-- ethclient/lightclient/transactions.go | 194 ++++++++++++++++++++++++++ 5 files changed, 270 insertions(+), 23 deletions(-) create mode 100644 ethclient/lightclient/transactions.go diff --git a/cmd/bltest/main.go b/cmd/bltest/main.go index 152abdc2de..b38d7aa0fc 100644 --- a/cmd/bltest/main.go +++ b/cmd/bltest/main.go @@ -30,6 +30,7 @@ import ( "github.com/ethereum/go-ethereum/beacon/config" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient/lightclient" "github.com/ethereum/go-ethereum/ethdb/memorydb" @@ -110,7 +111,11 @@ func sync(ctx *cli.Context) error { if err != nil { utils.Fatalf("Could not create RPC client: %v", err) } - client := lightclient.NewClient(config.MakeLightClientConfig(ctx), memorydb.New(), rpcClient) + genesis := utils.MakeGenesis(ctx) + if genesis == nil { + genesis = core.DefaultGenesisBlock() + } + client := lightclient.NewClient(config.MakeLightClientConfig(ctx), genesis.Config, memorydb.New(), rpcClient) client.Start() headCh := make(chan *types.Header, 1) @@ -140,6 +145,11 @@ loop: } else { log.Error("TransactionCount", "hash", head.Hash(), "error", err) } + if receipts, err := client.BlockReceipts(ctx, rpc.BlockNumberOrHashWithHash(head.Hash(), false)); err == nil { + log.Info("BlockReceipts", "hash", head.Hash(), "len(receipts)", len(receipts)) + } else { + log.Error("BlockReceipts", "hash", head.Hash(), "error", err) + } testState := func(addr common.Address) { if balance, err := client.BalanceAt(ctx, addr, big.NewInt(int64(rpc.LatestBlockNumber))); err == nil { log.Info("BalanceAt ", "address", addr, "balance", balance) diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 522e5e22f2..775e4f446d 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -32,6 +32,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/scwallet" "github.com/ethereum/go-ethereum/accounts/usbwallet" "github.com/ethereum/go-ethereum/beacon/blsync" + "github.com/ethereum/go-ethereum/beacon/config" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -227,7 +228,7 @@ func makeFullNode(ctx *cli.Context) *node.Node { // Start blsync mode. srv := rpc.NewServer() srv.RegisterName("engine", catalyst.NewConsensusAPI(eth)) - blsyncer := blsync.NewClient(ctx) + blsyncer := blsync.NewClient(config.MakeLightClientConfig(ctx)) blsyncer.SetEngineRPC(rpc.DialInProc(srv)) stack.RegisterLifecycle(blsyncer) } else { diff --git a/ethclient/lightclient/chain.go b/ethclient/lightclient/chain.go index a0bceb39a2..8b93a7d04c 100644 --- a/ethclient/lightclient/chain.go +++ b/ethclient/lightclient/chain.go @@ -48,7 +48,7 @@ type canonicalChain struct { 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 finalized + requests *requestMap[uint64, common.Hash] // requested; neither recent nor cached finalized } func newCanonicalChain(headTracker *light.HeadTracker, blocksAndHeaders *blocksAndHeaders, newHeadCb func(common.Hash)) *canonicalChain { @@ -111,17 +111,10 @@ func (c *canonicalChain) tailFetcher() { //TODO stop } func (c *canonicalChain) getHash(ctx context.Context, number uint64) (common.Hash, error) { - c.lock.Lock() - if hash, ok := c.recent[number]; ok { - c.lock.Unlock() - return hash, nil - } - if hash, ok := c.finalized.Get(number); ok { - c.lock.Unlock() + if hash, ok := c.getCachedHash(number); ok { return hash, nil } req := c.requests.request(number) - c.lock.Unlock() select { case c.tailFetchCh <- struct{}{}: default: @@ -130,6 +123,16 @@ func (c *canonicalChain) getHash(ctx context.Context, number uint64) (common.Has return req.getResult(ctx) } +func (c *canonicalChain) getCachedHash(number uint64) (common.Hash, bool) { + c.lock.Lock() + hash, ok := c.recent[number] + c.lock.Unlock() + if ok { + return hash, true + } + return c.finalized.Get(number) +} + func (c *canonicalChain) setHead(head *btypes.ExecutionHeader) bool { c.lock.Lock() defer c.lock.Unlock() @@ -139,6 +142,8 @@ func (c *canonicalChain) 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 c.recent = make(map[uint64]common.Hash) if headNum > 0 { c.recent[headNum-1] = head.ParentHash() @@ -237,6 +242,27 @@ func (c *canonicalChain) blockNumberToHash(ctx context.Context, number *big.Int) return c.getHash(ctx, num) } +func (c *canonicalChain) 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.blocksAndHeaders.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 blocksAndHeaders struct { client *rpc.Client headerCache *lru.Cache[common.Hash, *types.Header] diff --git a/ethclient/lightclient/lightclient.go b/ethclient/lightclient/lightclient.go index c482c2cfd2..39c1dd0b40 100644 --- a/ethclient/lightclient/lightclient.go +++ b/ethclient/lightclient/lightclient.go @@ -33,14 +33,17 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" ) type Client struct { - config config.LightClientConfig + clConfig config.LightClientConfig + elConfig *params.ChainConfig scheduler *request.Scheduler canonicalChain *canonicalChain blocksAndHeaders *blocksAndHeaders + txAndReceipts *txAndReceipts state *lightState headSubLock ssync.Mutex headSubs map[*headSub]struct{} @@ -48,27 +51,30 @@ type Client struct { headFetchCounter int } -func NewClient(config config.LightClientConfig, db ethdb.KeyValueStore, rpcClient *rpc.Client) *Client { +func NewClient(clConfig config.LightClientConfig, elConfig *params.ChainConfig, db ethdb.KeyValueStore, rpcClient *rpc.Client) *Client { // create data structures var ( - committeeChain = light.NewCommitteeChain(db, config.ChainConfig, config.SignerThreshold, config.EnforceTime) - headTracker = light.NewHeadTracker(committeeChain, config.SignerThreshold) + committeeChain = light.NewCommitteeChain(db, clConfig.ChainConfig, clConfig.SignerThreshold, clConfig.EnforceTime) + headTracker = light.NewHeadTracker(committeeChain, clConfig.SignerThreshold) ) // set up scheduler and sync modules //chainHeadFeed := new(event.Feed) scheduler := request.NewScheduler() - blocksAndHeaders := newBlocksAndHeaders(rpcClient) client := &Client{ - config: config, - scheduler: scheduler, - blocksAndHeaders: blocksAndHeaders, - headSubs: make(map[*headSub]struct{}), + clConfig: clConfig, + elConfig: elConfig, + scheduler: scheduler, + headSubs: make(map[*headSub]struct{}), } + blocksAndHeaders := newBlocksAndHeaders(rpcClient) canonicalChain := newCanonicalChain(headTracker, blocksAndHeaders, client.newHead) + txAndReceipts := newTxAndReceipts(rpcClient, canonicalChain, blocksAndHeaders, elConfig) + client.blocksAndHeaders = blocksAndHeaders + client.txAndReceipts = txAndReceipts client.canonicalChain = canonicalChain client.state = newLightState(rpcClient, canonicalChain, blocksAndHeaders) - checkpointInit := sync.NewCheckpointInit(committeeChain, config.Checkpoint) + checkpointInit := sync.NewCheckpointInit(committeeChain, clConfig.Checkpoint) forwardSync := sync.NewForwardUpdateSync(committeeChain) headSync := sync.NewHeadSync(headTracker, committeeChain) scheduler.RegisterTarget(headTracker) @@ -80,10 +86,12 @@ func NewClient(config config.LightClientConfig, db ethdb.KeyValueStore, rpcClien return client } +//TODO return ethereum.NotFound error properly + func (c *Client) Start() { c.scheduler.Start() - for _, url := range c.config.ApiUrls { - beaconApi := api.NewBeaconLightApi(url, c.config.CustomHeader) + for _, url := range c.clConfig.ApiUrls { + beaconApi := api.NewBeaconLightApi(url, c.clConfig.CustomHeader) c.scheduler.RegisterServer(request.NewServer(api.NewApiServer(beaconApi), &mclock.System{})) } } @@ -224,3 +232,11 @@ func (c *Client) NonceAt(ctx context.Context, account common.Address, blockNumbe } return proof.Nonce, nil } + +func (c *Client) BlockReceipts(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) ([]*types.Receipt, error) { + hash, err := c.canonicalChain.blockNumberOrHashToHash(ctx, blockNrOrHash) + if err != nil { + return nil, err + } + return c.txAndReceipts.getBlockReceipts(ctx, hash) +} diff --git a/ethclient/lightclient/transactions.go b/ethclient/lightclient/transactions.go new file mode 100644 index 0000000000..32194c146e --- /dev/null +++ b/ethclient/lightclient/transactions.go @@ -0,0 +1,194 @@ +// 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 ( + "bytes" + "context" + "encoding/json" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/lru" + "github.com/ethereum/go-ethereum/consensus/misc/eip4844" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/trie" +) + +type txAndReceipts struct { + client *rpc.Client + canonicalChain *canonicalChain + blocksAndHeaders *blocksAndHeaders + elConfig *params.ChainConfig + receiptsCache *lru.Cache[common.Hash, types.Receipts] + receiptsRequests *requestMap[common.Hash, types.Receipts] + txPosCache *lru.Cache[common.Hash, txInBlock] +} + +type txInBlock struct { + blockNumber uint64 + blockHash common.Hash // only considered valid if the block is canonical + index uint +} + +func newTxAndReceipts(client *rpc.Client, canonicalChain *canonicalChain, blocksAndHeaders *blocksAndHeaders, elConfig *params.ChainConfig) *txAndReceipts { + t := &txAndReceipts{ + client: client, + canonicalChain: canonicalChain, + blocksAndHeaders: blocksAndHeaders, + elConfig: elConfig, + receiptsCache: lru.NewCache[common.Hash, types.Receipts](10), + txPosCache: lru.NewCache[common.Hash, txInBlock](10000), + } + t.receiptsRequests = newRequestMap[common.Hash, types.Receipts](t.requestBlockReceipts) + return t +} + +func (t *txAndReceipts) getTxByHash(ctx context.Context, txHash common.Hash) (tx *types.Transaction, isPending bool, err error) { + if pos, ok := t.txPosCache.Get(txHash); ok { + if hash, ok := t.canonicalChain.getCachedHash(pos.blockNumber); ok && hash == pos.blockHash { + if block, ok := t.blocksAndHeaders.blockCache.Get(pos.blockHash); ok { + return block.Transactions()[pos.index], false, nil //TODO index range check + } + } + } + return t.requestTxByHash(ctx, txHash) +} + +func (t *txAndReceipts) getReceiptByTxHash(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { + if pos, ok := t.txPosCache.Get(txHash); ok { + if hash, ok := t.canonicalChain.getCachedHash(pos.blockNumber); ok && hash == pos.blockHash { + if receipts, ok := t.receiptsCache.Get(pos.blockHash); ok { + return receipts[pos.index], nil //TODO index range check + } + } + } + return t.requestReceiptByTxHash(ctx, txHash) + //TODO validate position +} + +func (t *txAndReceipts) getBlockReceipts(ctx context.Context, blockHash common.Hash) (types.Receipts, error) { + if receipts, ok := t.receiptsCache.Get(blockHash); ok { + return receipts, nil + } + request := t.receiptsRequests.request(blockHash) + block, err := t.blocksAndHeaders.getBlock(ctx, blockHash) + if err != nil { + return nil, err + } + receipts, err := request.getResult(ctx) + if err == nil { + receipts, err = t.validateBlockReceipts(block, receipts) + } + if err == nil { + t.receiptsCache.Add(blockHash, receipts) + } + request.release() + return receipts, err +} + +func (t *txAndReceipts) validateBlockReceipts(block *types.Block, receipts types.Receipts) (types.Receipts, error) { + // verify consensus fields agains receipts hash in block + var hash common.Hash + if len(receipts) == 0 { + hash = types.EmptyReceiptsHash + } else { + hash = types.DeriveSha(receipts, trie.NewStackTrie(nil)) + } + if hash != block.ReceiptHash() { + return nil, errors.New("invalid receipts hash") + } + // copy verified consensus fields + newReceipts := make(types.Receipts, len(receipts)) + for i, receipt := range receipts { + enc, err := receipt.MarshalBinary() + if err != nil { + return nil, err + } + newReceipt := &types.Receipt{} + if err := newReceipt.UnmarshalBinary(enc); err != nil { + return nil, err + } + newReceipts[i] = newReceipt + } + // derive non-consensus fields again + var blobGasPrice *big.Int + if block.ExcessBlobGas() != nil { + blobGasPrice = eip4844.CalcBlobFee(*block.ExcessBlobGas()) + } + if err := newReceipts.DeriveFields(t.elConfig, block.Hash(), block.NumberU64(), block.Time(), block.BaseFee(), blobGasPrice, block.Transactions()); err != nil { + return nil, err + } + // compare the JSON encoding of received and derived versions + jsonReceived, err := json.Marshal(receipts) + if err != nil { + return nil, err + } + jsonDerived, err := json.Marshal(receipts) + if err != nil { + return nil, err + } + if !bytes.Equal(jsonReceived, jsonDerived) { + return nil, errors.New("received and derived receipts do not match") + } + return newReceipts, nil +} + +func (t *txAndReceipts) requestTxByHash(ctx context.Context, hash common.Hash) (tx *types.Transaction, isPending bool, err error) { + var json *rpcTransaction + err = t.client.CallContext(ctx, &json, "eth_getTransactionByHash", hash) + if err != nil { + return nil, false, err + } else if json == nil { + return nil, false, ethereum.NotFound + } else if _, r, _ := json.tx.RawSignatureValues(); r == nil { + return nil, false, errors.New("server returned transaction without signature") + } + if json.From != nil && json.BlockHash != nil { + setSenderFromServer(json.tx, *json.From, *json.BlockHash) + } + return json.tx, json.BlockNumber == nil, nil +} + +func (t *txAndReceipts) requestReceiptByTxHash(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { + var r *types.Receipt + err := t.client.CallContext(ctx, &r, "eth_getTransactionReceipt", txHash) + if err == nil && r == nil { + return nil, ethereum.NotFound + } + return r, err +} + +func (t *txAndReceipts) requestBlockReceipts(ctx context.Context, blockHash common.Hash) (types.Receipts, error) { + var r []*types.Receipt + err := t.client.CallContext(ctx, &r, "eth_getBlockReceipts", blockHash) + if err == nil && r == nil { + return nil, ethereum.NotFound + } + return types.Receipts(r), err +} + +func (t *txAndReceipts) addBlockTxs(block *types.Block) { + blockNumber, blockHash := block.NumberU64(), block.Hash() + for i, tx := range block.Transactions() { + t.txPosCache.Add(tx.Hash(), txInBlock{blockNumber: blockNumber, blockHash: blockHash, index: uint(i)}) + } +}