diff --git a/eth/protocols/eth/broadcast.go b/eth/protocols/eth/broadcast.go deleted file mode 100644 index 3045303f22..0000000000 --- a/eth/protocols/eth/broadcast.go +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright 2020 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 eth - -import ( - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" -) - -const ( - // This is the target size for the packs of transactions or announcements. A - // pack can get larger than this if a single transactions exceeds this size. - maxTxPacketSize = 100 * 1024 -) - -// blockPropagation is a block propagation event, waiting for its turn in the -// broadcast queue. -type blockPropagation struct { - block *types.Block - td *big.Int -} - -// broadcastBlocks is a write loop that multiplexes blocks and block announcements -// to the remote peer. The goal is to have an async writer that does not lock up -// node internals and at the same time rate limits queued data. -func (p *Peer) broadcastBlocks() { - for { - select { - case prop := <-p.queuedBlocks: - if err := p.SendNewBlock(prop.block, prop.td); err != nil { - return - } - p.Log().Trace("Propagated block", "number", prop.block.Number(), "hash", prop.block.Hash(), "td", prop.td) - - case block := <-p.queuedBlockAnns: - if err := p.SendNewBlockHashes([]common.Hash{block.Hash()}, []uint64{block.NumberU64()}); err != nil { - return - } - p.Log().Trace("Announced block", "number", block.Number(), "hash", block.Hash()) - - case <-p.term: - return - } - } -} - -// broadcastTransactions is a write loop that schedules transaction broadcasts -// to the remote peer. The goal is to have an async writer that does not lock up -// node internals and at the same time rate limits queued data. -func (p *Peer) broadcastTransactions() { - var ( - queue []common.Hash // Queue of hashes to broadcast as full transactions - done chan struct{} // Non-nil if background broadcaster is running - fail = make(chan error, 1) // Channel used to receive network error - failed bool // Flag whether a send failed, discard everything onward - ) - for { - // If there's no in-flight broadcast running, check if a new one is needed - if done == nil && len(queue) > 0 { - // Pile transaction until we reach our allowed network limit - var ( - hashesCount uint64 - txs []*types.Transaction - size common.StorageSize - ) - for i := 0; i < len(queue) && size < maxTxPacketSize; i++ { - if tx := p.txpool.Get(queue[i]); tx != nil { - txs = append(txs, tx) - size += common.StorageSize(tx.Size()) - } - hashesCount++ - } - queue = queue[:copy(queue, queue[hashesCount:])] - - // If there's anything available to transfer, fire up an async writer - if len(txs) > 0 { - done = make(chan struct{}) - go func() { - if err := p.SendTransactions(txs); err != nil { - fail <- err - return - } - close(done) - p.Log().Trace("Sent transactions", "count", len(txs)) - }() - } - } - // Transfer goroutine may or may not have been started, listen for events - select { - case hashes := <-p.txBroadcast: - // If the connection failed, discard all transaction events - if failed { - continue - } - // New batch of transactions to be broadcast, queue them (with cap) - queue = append(queue, hashes...) - if len(queue) > maxQueuedTxs { - // Fancy copy and resize to ensure buffer doesn't grow indefinitely - queue = queue[:copy(queue, queue[len(queue)-maxQueuedTxs:])] - } - - case <-done: - done = nil - - case <-fail: - failed = true - - case <-p.term: - return - } - } -} - -// announceTransactions is a write loop that schedules transaction broadcasts -// to the remote peer. The goal is to have an async writer that does not lock up -// node internals and at the same time rate limits queued data. -func (p *Peer) announceTransactions() { - var ( - queue []common.Hash // Queue of hashes to announce as transaction stubs - done chan struct{} // Non-nil if background announcer is running - fail = make(chan error, 1) // Channel used to receive network error - failed bool // Flag whether a send failed, discard everything onward - ) - for { - // If there's no in-flight announce running, check if a new one is needed - if done == nil && len(queue) > 0 { - // Pile transaction hashes until we reach our allowed network limit - var ( - count int - pending []common.Hash - pendingTypes []byte - pendingSizes []uint32 - size common.StorageSize - ) - for count = 0; count < len(queue) && size < maxTxPacketSize; count++ { - if tx := p.txpool.Get(queue[count]); tx != nil { - pending = append(pending, queue[count]) - pendingTypes = append(pendingTypes, tx.Type()) - pendingSizes = append(pendingSizes, uint32(tx.Size())) - size += common.HashLength - } - } - // Shift and trim queue - queue = queue[:copy(queue, queue[count:])] - - // If there's anything available to transfer, fire up an async writer - if len(pending) > 0 { - done = make(chan struct{}) - go func() { - if p.version >= ETH68 { - if err := p.sendPooledTransactionHashes68(pending, pendingTypes, pendingSizes); err != nil { - fail <- err - return - } - } else { - if err := p.sendPooledTransactionHashes66(pending); err != nil { - fail <- err - return - } - } - close(done) - p.Log().Trace("Sent transaction announcements", "count", len(pending)) - }() - } - } - // Transfer goroutine may or may not have been started, listen for events - select { - case hashes := <-p.txAnnounce: - // If the connection failed, discard all transaction events - if failed { - continue - } - // New batch of transactions to be broadcast, queue them (with cap) - queue = append(queue, hashes...) - if len(queue) > maxQueuedTxAnns { - // Fancy copy and resize to ensure buffer doesn't grow indefinitely - queue = queue[:copy(queue, queue[len(queue)-maxQueuedTxAnns:])] - } - - case <-done: - done = nil - - case <-fail: - failed = true - - case <-p.term: - return - } - } -} diff --git a/eth/protocols/eth/discovery.go b/eth/protocols/eth/discovery.go deleted file mode 100644 index a7bdd47daf..0000000000 --- a/eth/protocols/eth/discovery.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2020 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 eth - -import ( - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/forkid" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/rlp" -) - -// enrEntry is the ENR entry which advertises `eth` protocol on the discovery. -type enrEntry struct { - ForkID forkid.ID // Fork identifier per EIP-2124 - - // Ignore additional fields (for forward compatibility). - Rest []rlp.RawValue `rlp:"tail"` -} - -// ENRKey implements enr.Entry. -func (e enrEntry) ENRKey() string { - return "eth" -} - -// StartENRUpdater starts the `eth` ENR updater loop, which listens for chain -// head events and updates the requested node record whenever a fork is passed. -func StartENRUpdater(chain *core.BlockChain, ln *enode.LocalNode) { - var newHead = make(chan core.ChainHeadEvent, 10) - sub := chain.SubscribeChainHeadEvent(newHead) - - go func() { - defer sub.Unsubscribe() - for { - select { - case <-newHead: - ln.Set(currentENREntry(chain)) - case <-sub.Err(): - // Would be nice to sync with Stop, but there is no - // good way to do that. - return - } - } - }() -} - -// currentENREntry constructs an `eth` ENR entry based on the current state of the chain. -func currentENREntry(chain *core.BlockChain) *enrEntry { - head := chain.CurrentHeader() - return &enrEntry{ - ForkID: forkid.NewID(chain.Config(), chain.Genesis(), head.Number.Uint64(), head.Time), - } -} diff --git a/eth/protocols/eth/dispatcher.go b/eth/protocols/eth/dispatcher.go deleted file mode 100644 index ae98820cd6..0000000000 --- a/eth/protocols/eth/dispatcher.go +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright 2021 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 eth - -import ( - "errors" - "fmt" - "time" - - "github.com/ethereum/go-ethereum/p2p" -) - -var ( - // errDisconnected is returned if a request is attempted to be made to a peer - // that was already closed. - errDisconnected = errors.New("disconnected") - - // errDanglingResponse is returned if a response arrives with a request id - // which does not match to any existing pending requests. - errDanglingResponse = errors.New("response to non-existent request") - - // errMismatchingResponseType is returned if the remote peer sent a different - // packet type as a response to a request than what the local node expected. - errMismatchingResponseType = errors.New("mismatching response type") -) - -// Request is a pending request to allow tracking it and delivering a response -// back to the requester on their chosen channel. -type Request struct { - peer *Peer // Peer to which this request belongs for untracking - id uint64 // Request ID to match up replies to - - sink chan *Response // Channel to deliver the response on - cancel chan struct{} // Channel to cancel requests ahead of time - - code uint64 // Message code of the request packet - want uint64 // Message code of the response packet - data interface{} // Data content of the request packet - - Peer string // Demultiplexer if cross-peer requests are batched together - Sent time.Time // Timestamp when the request was sent -} - -// Close aborts an in-flight request. Although there's no way to notify the -// remote peer about the cancellation, this method notifies the dispatcher to -// discard any late responses. -func (r *Request) Close() error { - if r.peer == nil { // Tests mock out the dispatcher, skip internal cancellation - return nil - } - cancelOp := &cancel{ - id: r.id, - fail: make(chan error), - } - select { - case r.peer.reqCancel <- cancelOp: - if err := <-cancelOp.fail; err != nil { - return err - } - close(r.cancel) - return nil - case <-r.peer.term: - return errDisconnected - } -} - -// request is a wrapper around a client Request that has an error channel to -// signal on if sending the request already failed on a network level. -type request struct { - req *Request - fail chan error -} - -// cancel is a maintenance type on the dispatcher to stop tracking a pending -// request. -type cancel struct { - id uint64 // Request ID to stop tracking - fail chan error -} - -// Response is a reply packet to a previously created request. It is delivered -// on the channel assigned by the requester subsystem and contains the original -// request embedded to allow uniquely matching it caller side. -type Response struct { - id uint64 // Request ID to match up this reply to - recv time.Time // Timestamp when the request was received - code uint64 // Response packet type to cross validate with request - - Req *Request // Original request to cross-reference with - Res interface{} // Remote response for the request query - Meta interface{} // Metadata generated locally on the receiver thread - Time time.Duration // Time it took for the request to be served - Done chan error // Channel to signal message handling to the reader -} - -// response is a wrapper around a remote Response that has an error channel to -// signal on if processing the response failed. -type response struct { - res *Response - fail chan error -} - -// dispatchRequest schedules the request to the dispatcher for tracking and -// network serialization, blocking until it's successfully sent. -// -// The returned Request must either be closed before discarding it, or the reply -// must be waited for and the Response's Done channel signalled. -func (p *Peer) dispatchRequest(req *Request) error { - reqOp := &request{ - req: req, - fail: make(chan error), - } - req.cancel = make(chan struct{}) - req.peer = p - req.Peer = p.id - - select { - case p.reqDispatch <- reqOp: - return <-reqOp.fail - case <-p.term: - return errDisconnected - } -} - -// dispatchRequest fulfils a pending request and delivers it to the requested -// sink. -func (p *Peer) dispatchResponse(res *Response, metadata func() interface{}) error { - resOp := &response{ - res: res, - fail: make(chan error), - } - res.recv = time.Now() - res.Done = make(chan error) - - select { - case p.resDispatch <- resOp: - // Ensure the response is accepted by the dispatcher - if err := <-resOp.fail; err != nil { - return nil - } - // Request was accepted, run any postprocessing step to generate metadata - // on the receiver thread, not the sink thread - if metadata != nil { - res.Meta = metadata() - } - // Deliver the filled out response and wait until it's handled. This - // path is a bit funky as Go's select has no order, so if a response - // arrives to an already cancelled request, there's a 50-50% changes - // of picking on channel or the other. To avoid such cases delivering - // the packet upstream, check for cancellation first and only after - // block on delivery. - select { - case <-res.Req.cancel: - return nil // Request cancelled, silently discard response - default: - // Request not yet cancelled, attempt to deliver it, but do watch - // for fresh cancellations too - select { - case res.Req.sink <- res: - return <-res.Done // Response delivered, return any errors - case <-res.Req.cancel: - return nil // Request cancelled, silently discard response - } - } - - case <-p.term: - return errDisconnected - } -} - -// dispatcher is a loop that accepts requests from higher layer packages, pushes -// it to the network and tracks and dispatches the responses back to the original -// requester. -func (p *Peer) dispatcher() { - pending := make(map[uint64]*Request) - - for { - select { - case reqOp := <-p.reqDispatch: - req := reqOp.req - req.Sent = time.Now() - - requestTracker.Track(p.id, p.version, req.code, req.want, req.id) - err := p2p.Send(p.rw, req.code, req.data) - reqOp.fail <- err - - if err == nil { - pending[req.id] = req - } - - case cancelOp := <-p.reqCancel: - // Retrieve the pending request to cancel and short circuit if it - // has already been serviced and is not available anymore - req := pending[cancelOp.id] - if req == nil { - cancelOp.fail <- nil - continue - } - // Stop tracking the request - delete(pending, cancelOp.id) - cancelOp.fail <- nil - - case resOp := <-p.resDispatch: - res := resOp.res - res.Req = pending[res.id] - - // Independent if the request exists or not, track this packet - requestTracker.Fulfil(p.id, p.version, res.code, res.id) - - switch { - case res.Req == nil: - // Response arrived with an untracked ID. Since even cancelled - // requests are tracked until fulfillment, a dangling response - // means the remote peer implements the protocol badly. - resOp.fail <- errDanglingResponse - - case res.Req.want != res.code: - // Response arrived, but it's a different packet type than the - // one expected by the requester. Either the local code is bad, - // or the remote peer send junk. In neither cases can we handle - // the packet. - resOp.fail <- fmt.Errorf("%w: have %d, want %d", errMismatchingResponseType, res.code, res.Req.want) - - default: - // All dispatcher checks passed and the response was initialized - // with the matching request. Signal to the delivery routine that - // it can wait for a handler response and dispatch the data. - res.Time = res.recv.Sub(res.Req.Sent) - resOp.fail <- nil - - // Stop tracking the request, the response dispatcher will deliver - delete(pending, res.id) - } - - case <-p.term: - return - } - } -} diff --git a/eth/protocols/eth/handler.go b/eth/protocols/eth/handler.go deleted file mode 100644 index 42d0412a12..0000000000 --- a/eth/protocols/eth/handler.go +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright 2020 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 eth - -import ( - "fmt" - "math/big" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/enr" - "github.com/ethereum/go-ethereum/params" -) - -const ( - // softResponseLimit is the target maximum size of replies to data retrievals. - softResponseLimit = 2 * 1024 * 1024 - - // maxHeadersServe is the maximum number of block headers to serve. This number - // is there to limit the number of disk lookups. - maxHeadersServe = 1024 - - // maxBodiesServe is the maximum number of block bodies to serve. This number - // is mostly there to limit the number of disk lookups. With 24KB block sizes - // nowadays, the practical limit will always be softResponseLimit. - maxBodiesServe = 1024 - - // maxReceiptsServe is the maximum number of block receipts to serve. This - // number is mostly there to limit the number of disk lookups. With block - // containing 200+ transactions nowadays, the practical limit will always - // be softResponseLimit. - maxReceiptsServe = 1024 -) - -// Handler is a callback to invoke from an outside runner after the boilerplate -// exchanges have passed. -type Handler func(peer *Peer) error - -// Backend defines the data retrieval methods to serve remote requests and the -// callback methods to invoke on remote deliveries. -type Backend interface { - // Chain retrieves the blockchain object to serve data. - Chain() *core.BlockChain - - // TxPool retrieves the transaction pool object to serve data. - TxPool() TxPool - - // AcceptTxs retrieves whether transaction processing is enabled on the node - // or if inbound transactions should simply be dropped. - AcceptTxs() bool - - // RunPeer is invoked when a peer joins on the `eth` protocol. The handler - // should do any peer maintenance work, handshakes and validations. If all - // is passed, control should be given back to the `handler` to process the - // inbound messages going forward. - RunPeer(peer *Peer, handler Handler) error - - // PeerInfo retrieves all known `eth` information about a peer. - PeerInfo(id enode.ID) interface{} - - // Handle is a callback to be invoked when a data packet is received from - // the remote peer. Only packets not consumed by the protocol handler will - // be forwarded to the backend. - Handle(peer *Peer, packet Packet) error -} - -// TxPool defines the methods needed by the protocol handler to serve transactions. -type TxPool interface { - // Get retrieves the transaction from the local txpool with the given hash. - Get(hash common.Hash) *types.Transaction -} - -// MakeProtocols constructs the P2P protocol definitions for `eth`. -func MakeProtocols(backend Backend, network uint64, dnsdisc enode.Iterator) []p2p.Protocol { - protocols := make([]p2p.Protocol, 0, len(ProtocolVersions)) - for _, version := range ProtocolVersions { - // Blob transactions require eth/68 announcements, disable everything else - if version <= ETH67 && backend.Chain().Config().CancunTime != nil { - continue - } - version := version // Closure - - protocols = append(protocols, p2p.Protocol{ - Name: ProtocolName, - Version: version, - Length: protocolLengths[version], - Run: func(p *p2p.Peer, rw p2p.MsgReadWriter) error { - peer := NewPeer(version, p, rw, backend.TxPool()) - defer peer.Close() - - return backend.RunPeer(peer, func(peer *Peer) error { - return Handle(backend, peer) - }) - }, - NodeInfo: func() interface{} { - return nodeInfo(backend.Chain(), network) - }, - PeerInfo: func(id enode.ID) interface{} { - return backend.PeerInfo(id) - }, - Attributes: []enr.Entry{currentENREntry(backend.Chain())}, - DialCandidates: dnsdisc, - }) - } - return protocols -} - -// NodeInfo represents a short summary of the `eth` sub-protocol metadata -// known about the host peer. -type NodeInfo struct { - Network uint64 `json:"network"` // Ethereum network ID (1=Mainnet, Goerli=5) - Difficulty *big.Int `json:"difficulty"` // Total difficulty of the host's blockchain - Genesis common.Hash `json:"genesis"` // SHA3 hash of the host's genesis block - Config *params.ChainConfig `json:"config"` // Chain configuration for the fork rules - Head common.Hash `json:"head"` // Hex hash of the host's best owned block -} - -// nodeInfo retrieves some `eth` protocol metadata about the running host node. -func nodeInfo(chain *core.BlockChain, network uint64) *NodeInfo { - head := chain.CurrentBlock() - hash := head.Hash() - - return &NodeInfo{ - Network: network, - Difficulty: chain.GetTd(hash, head.Number.Uint64()), - Genesis: chain.Genesis().Hash(), - Config: chain.Config(), - Head: hash, - } -} - -// Handle is invoked whenever an `eth` connection is made that successfully passes -// the protocol handshake. This method will keep processing messages until the -// connection is torn down. -func Handle(backend Backend, peer *Peer) error { - for { - if err := handleMessage(backend, peer); err != nil { - peer.Log().Debug("Message handling failed in `eth`", "err", err) - return err - } - } -} - -type msgHandler func(backend Backend, msg Decoder, peer *Peer) error -type Decoder interface { - Decode(val interface{}) error - Time() time.Time -} - -var eth67 = map[uint64]msgHandler{ - NewBlockHashesMsg: handleNewBlockhashes, - NewBlockMsg: handleNewBlock, - TransactionsMsg: handleTransactions, - NewPooledTransactionHashesMsg: handleNewPooledTransactionHashes67, - GetBlockHeadersMsg: handleGetBlockHeaders, - BlockHeadersMsg: handleBlockHeaders, - GetBlockBodiesMsg: handleGetBlockBodies, - BlockBodiesMsg: handleBlockBodies, - GetReceiptsMsg: handleGetReceipts, - ReceiptsMsg: handleReceipts, - GetPooledTransactionsMsg: handleGetPooledTransactions, - PooledTransactionsMsg: handlePooledTransactions, -} - -var eth68 = map[uint64]msgHandler{ - NewBlockHashesMsg: handleNewBlockhashes, - NewBlockMsg: handleNewBlock, - TransactionsMsg: handleTransactions, - NewPooledTransactionHashesMsg: handleNewPooledTransactionHashes68, - GetBlockHeadersMsg: handleGetBlockHeaders, - BlockHeadersMsg: handleBlockHeaders, - GetBlockBodiesMsg: handleGetBlockBodies, - BlockBodiesMsg: handleBlockBodies, - GetReceiptsMsg: handleGetReceipts, - ReceiptsMsg: handleReceipts, - GetPooledTransactionsMsg: handleGetPooledTransactions, - PooledTransactionsMsg: handlePooledTransactions, -} - -// handleMessage is invoked whenever an inbound message is received from a remote -// peer. The remote connection is torn down upon returning any error. -func handleMessage(backend Backend, peer *Peer) error { - // Read the next message from the remote peer, and ensure it's fully consumed - msg, err := peer.rw.ReadMsg() - if err != nil { - return err - } - if msg.Size > maxMessageSize { - return fmt.Errorf("%w: %v > %v", errMsgTooLarge, msg.Size, maxMessageSize) - } - defer msg.Discard() - - var handlers = eth67 - if peer.Version() >= ETH68 { - handlers = eth68 - } - // Track the amount of time it takes to serve the request and run the handler - if metrics.Enabled { - h := fmt.Sprintf("%s/%s/%d/%#02x", p2p.HandleHistName, ProtocolName, peer.Version(), msg.Code) - defer func(start time.Time) { - sampler := func() metrics.Sample { - return metrics.ResettingSample( - metrics.NewExpDecaySample(1028, 0.015), - ) - } - metrics.GetOrRegisterHistogramLazy(h, nil, sampler).Update(time.Since(start).Microseconds()) - }(time.Now()) - } - if handler := handlers[msg.Code]; handler != nil { - return handler(backend, msg, peer) - } - return fmt.Errorf("%w: %v", errInvalidMsgCode, msg.Code) -} diff --git a/eth/protocols/eth/handler_test.go b/eth/protocols/eth/handler_test.go deleted file mode 100644 index 41e18bfb3e..0000000000 --- a/eth/protocols/eth/handler_test.go +++ /dev/null @@ -1,504 +0,0 @@ -// Copyright 2020 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 eth - -import ( - "math" - "math/big" - "math/rand" - "testing" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/consensus" - "github.com/ethereum/go-ethereum/consensus/beacon" - "github.com/ethereum/go-ethereum/consensus/ethash" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/txpool" - "github.com/ethereum/go-ethereum/core/txpool/legacypool" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/params" -) - -var ( - // testKey is a private key to use for funding a tester account. - testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") - - // testAddr is the Ethereum address of the tester account. - testAddr = crypto.PubkeyToAddress(testKey.PublicKey) -) - -func u64(val uint64) *uint64 { return &val } - -// testBackend is a mock implementation of the live Ethereum message handler. Its -// purpose is to allow testing the request/reply workflows and wire serialization -// in the `eth` protocol without actually doing any data processing. -type testBackend struct { - db ethdb.Database - chain *core.BlockChain - txpool *txpool.TxPool -} - -// newTestBackend creates an empty chain and wraps it into a mock backend. -func newTestBackend(blocks int) *testBackend { - return newTestBackendWithGenerator(blocks, false, nil) -} - -// newTestBackend creates a chain with a number of explicitly defined blocks and -// wraps it into a mock backend. -func newTestBackendWithGenerator(blocks int, shanghai bool, generator func(int, *core.BlockGen)) *testBackend { - var ( - // Create a database pre-initialize with a genesis block - db = rawdb.NewMemoryDatabase() - config = params.TestChainConfig - engine consensus.Engine = ethash.NewFaker() - ) - - if shanghai { - config = ¶ms.ChainConfig{ - ChainID: big.NewInt(1), - HomesteadBlock: big.NewInt(0), - DAOForkBlock: nil, - DAOForkSupport: true, - EIP150Block: big.NewInt(0), - EIP155Block: big.NewInt(0), - EIP158Block: big.NewInt(0), - ByzantiumBlock: big.NewInt(0), - ConstantinopleBlock: big.NewInt(0), - PetersburgBlock: big.NewInt(0), - IstanbulBlock: big.NewInt(0), - MuirGlacierBlock: big.NewInt(0), - BerlinBlock: big.NewInt(0), - LondonBlock: big.NewInt(0), - ArrowGlacierBlock: big.NewInt(0), - GrayGlacierBlock: big.NewInt(0), - MergeNetsplitBlock: big.NewInt(0), - ShanghaiTime: u64(0), - TerminalTotalDifficulty: big.NewInt(0), - TerminalTotalDifficultyPassed: true, - Ethash: new(params.EthashConfig), - } - engine = beacon.NewFaker() - } - - gspec := &core.Genesis{ - Config: config, - Alloc: core.GenesisAlloc{testAddr: {Balance: big.NewInt(100_000_000_000_000_000)}}, - } - chain, _ := core.NewBlockChain(db, nil, gspec, nil, engine, vm.Config{}, nil, nil) - - _, bs, _ := core.GenerateChainWithGenesis(gspec, engine, blocks, generator) - if _, err := chain.InsertChain(bs); err != nil { - panic(err) - } - for _, block := range bs { - chain.TrieDB().Commit(block.Root(), false) - } - txconfig := legacypool.DefaultConfig - txconfig.Journal = "" // Don't litter the disk with test journals - - pool := legacypool.New(txconfig, chain) - txpool, _ := txpool.New(new(big.Int).SetUint64(txconfig.PriceLimit), chain, []txpool.SubPool{pool}) - - return &testBackend{ - db: db, - chain: chain, - txpool: txpool, - } -} - -// close tears down the transaction pool and chain behind the mock backend. -func (b *testBackend) close() { - b.txpool.Close() - b.chain.Stop() -} - -func (b *testBackend) Chain() *core.BlockChain { return b.chain } -func (b *testBackend) TxPool() TxPool { return b.txpool } - -func (b *testBackend) RunPeer(peer *Peer, handler Handler) error { - // Normally the backend would do peer maintenance and handshakes. All that - // is omitted and we will just give control back to the handler. - return handler(peer) -} -func (b *testBackend) PeerInfo(enode.ID) interface{} { panic("not implemented") } - -func (b *testBackend) AcceptTxs() bool { - panic("data processing tests should be done in the handler package") -} -func (b *testBackend) Handle(*Peer, Packet) error { - panic("data processing tests should be done in the handler package") -} - -// Tests that block headers can be retrieved from a remote chain based on user queries. -func TestGetBlockHeaders67(t *testing.T) { testGetBlockHeaders(t, ETH67) } -func TestGetBlockHeaders68(t *testing.T) { testGetBlockHeaders(t, ETH68) } - -func testGetBlockHeaders(t *testing.T, protocol uint) { - t.Parallel() - - backend := newTestBackend(maxHeadersServe + 15) - defer backend.close() - - peer, _ := newTestPeer("peer", protocol, backend) - defer peer.close() - - // Create a "random" unknown hash for testing - var unknown common.Hash - for i := range unknown { - unknown[i] = byte(i) - } - getHashes := func(from, limit uint64) (hashes []common.Hash) { - for i := uint64(0); i < limit; i++ { - hashes = append(hashes, backend.chain.GetCanonicalHash(from-1-i)) - } - return hashes - } - // Create a batch of tests for various scenarios - limit := uint64(maxHeadersServe) - tests := []struct { - query *GetBlockHeadersRequest // The query to execute for header retrieval - expect []common.Hash // The hashes of the block whose headers are expected - }{ - // A single random block should be retrievable by hash - { - &GetBlockHeadersRequest{Origin: HashOrNumber{Hash: backend.chain.GetBlockByNumber(limit / 2).Hash()}, Amount: 1}, - []common.Hash{backend.chain.GetBlockByNumber(limit / 2).Hash()}, - }, - // A single random block should be retrievable by number - { - &GetBlockHeadersRequest{Origin: HashOrNumber{Number: limit / 2}, Amount: 1}, - []common.Hash{backend.chain.GetBlockByNumber(limit / 2).Hash()}, - }, - // Multiple headers should be retrievable in both directions - { - &GetBlockHeadersRequest{Origin: HashOrNumber{Number: limit / 2}, Amount: 3}, - []common.Hash{ - backend.chain.GetBlockByNumber(limit / 2).Hash(), - backend.chain.GetBlockByNumber(limit/2 + 1).Hash(), - backend.chain.GetBlockByNumber(limit/2 + 2).Hash(), - }, - }, { - &GetBlockHeadersRequest{Origin: HashOrNumber{Number: limit / 2}, Amount: 3, Reverse: true}, - []common.Hash{ - backend.chain.GetBlockByNumber(limit / 2).Hash(), - backend.chain.GetBlockByNumber(limit/2 - 1).Hash(), - backend.chain.GetBlockByNumber(limit/2 - 2).Hash(), - }, - }, - // Multiple headers with skip lists should be retrievable - { - &GetBlockHeadersRequest{Origin: HashOrNumber{Number: limit / 2}, Skip: 3, Amount: 3}, - []common.Hash{ - backend.chain.GetBlockByNumber(limit / 2).Hash(), - backend.chain.GetBlockByNumber(limit/2 + 4).Hash(), - backend.chain.GetBlockByNumber(limit/2 + 8).Hash(), - }, - }, { - &GetBlockHeadersRequest{Origin: HashOrNumber{Number: limit / 2}, Skip: 3, Amount: 3, Reverse: true}, - []common.Hash{ - backend.chain.GetBlockByNumber(limit / 2).Hash(), - backend.chain.GetBlockByNumber(limit/2 - 4).Hash(), - backend.chain.GetBlockByNumber(limit/2 - 8).Hash(), - }, - }, - // The chain endpoints should be retrievable - { - &GetBlockHeadersRequest{Origin: HashOrNumber{Number: 0}, Amount: 1}, - []common.Hash{backend.chain.GetBlockByNumber(0).Hash()}, - }, - { - &GetBlockHeadersRequest{Origin: HashOrNumber{Number: backend.chain.CurrentBlock().Number.Uint64()}, Amount: 1}, - []common.Hash{backend.chain.CurrentBlock().Hash()}, - }, - { // If the peer requests a bit into the future, we deliver what we have - &GetBlockHeadersRequest{Origin: HashOrNumber{Number: backend.chain.CurrentBlock().Number.Uint64()}, Amount: 10}, - []common.Hash{backend.chain.CurrentBlock().Hash()}, - }, - // Ensure protocol limits are honored - { - &GetBlockHeadersRequest{Origin: HashOrNumber{Number: backend.chain.CurrentBlock().Number.Uint64() - 1}, Amount: limit + 10, Reverse: true}, - getHashes(backend.chain.CurrentBlock().Number.Uint64(), limit), - }, - // Check that requesting more than available is handled gracefully - { - &GetBlockHeadersRequest{Origin: HashOrNumber{Number: backend.chain.CurrentBlock().Number.Uint64() - 4}, Skip: 3, Amount: 3}, - []common.Hash{ - backend.chain.GetBlockByNumber(backend.chain.CurrentBlock().Number.Uint64() - 4).Hash(), - backend.chain.GetBlockByNumber(backend.chain.CurrentBlock().Number.Uint64()).Hash(), - }, - }, { - &GetBlockHeadersRequest{Origin: HashOrNumber{Number: 4}, Skip: 3, Amount: 3, Reverse: true}, - []common.Hash{ - backend.chain.GetBlockByNumber(4).Hash(), - backend.chain.GetBlockByNumber(0).Hash(), - }, - }, - // Check that requesting more than available is handled gracefully, even if mid skip - { - &GetBlockHeadersRequest{Origin: HashOrNumber{Number: backend.chain.CurrentBlock().Number.Uint64() - 4}, Skip: 2, Amount: 3}, - []common.Hash{ - backend.chain.GetBlockByNumber(backend.chain.CurrentBlock().Number.Uint64() - 4).Hash(), - backend.chain.GetBlockByNumber(backend.chain.CurrentBlock().Number.Uint64() - 1).Hash(), - }, - }, { - &GetBlockHeadersRequest{Origin: HashOrNumber{Number: 4}, Skip: 2, Amount: 3, Reverse: true}, - []common.Hash{ - backend.chain.GetBlockByNumber(4).Hash(), - backend.chain.GetBlockByNumber(1).Hash(), - }, - }, - // Check a corner case where requesting more can iterate past the endpoints - { - &GetBlockHeadersRequest{Origin: HashOrNumber{Number: 2}, Amount: 5, Reverse: true}, - []common.Hash{ - backend.chain.GetBlockByNumber(2).Hash(), - backend.chain.GetBlockByNumber(1).Hash(), - backend.chain.GetBlockByNumber(0).Hash(), - }, - }, - // Check a corner case where skipping overflow loops back into the chain start - { - &GetBlockHeadersRequest{Origin: HashOrNumber{Hash: backend.chain.GetBlockByNumber(3).Hash()}, Amount: 2, Reverse: false, Skip: math.MaxUint64 - 1}, - []common.Hash{ - backend.chain.GetBlockByNumber(3).Hash(), - }, - }, - // Check a corner case where skipping overflow loops back to the same header - { - &GetBlockHeadersRequest{Origin: HashOrNumber{Hash: backend.chain.GetBlockByNumber(1).Hash()}, Amount: 2, Reverse: false, Skip: math.MaxUint64}, - []common.Hash{ - backend.chain.GetBlockByNumber(1).Hash(), - }, - }, - // Check that non existing headers aren't returned - { - &GetBlockHeadersRequest{Origin: HashOrNumber{Hash: unknown}, Amount: 1}, - []common.Hash{}, - }, { - &GetBlockHeadersRequest{Origin: HashOrNumber{Number: backend.chain.CurrentBlock().Number.Uint64() + 1}, Amount: 1}, - []common.Hash{}, - }, - } - // Run each of the tests and verify the results against the chain - for i, tt := range tests { - // Collect the headers to expect in the response - var headers []*types.Header - for _, hash := range tt.expect { - headers = append(headers, backend.chain.GetBlockByHash(hash).Header()) - } - // Send the hash request and verify the response - p2p.Send(peer.app, GetBlockHeadersMsg, &GetBlockHeadersPacket{ - RequestId: 123, - GetBlockHeadersRequest: tt.query, - }) - if err := p2p.ExpectMsg(peer.app, BlockHeadersMsg, &BlockHeadersPacket{ - RequestId: 123, - BlockHeadersRequest: headers, - }); err != nil { - t.Errorf("test %d: headers mismatch: %v", i, err) - } - // If the test used number origins, repeat with hashes as the too - if tt.query.Origin.Hash == (common.Hash{}) { - if origin := backend.chain.GetBlockByNumber(tt.query.Origin.Number); origin != nil { - tt.query.Origin.Hash, tt.query.Origin.Number = origin.Hash(), 0 - - p2p.Send(peer.app, GetBlockHeadersMsg, &GetBlockHeadersPacket{ - RequestId: 456, - GetBlockHeadersRequest: tt.query, - }) - expected := &BlockHeadersPacket{RequestId: 456, BlockHeadersRequest: headers} - if err := p2p.ExpectMsg(peer.app, BlockHeadersMsg, expected); err != nil { - t.Errorf("test %d by hash: headers mismatch: %v", i, err) - } - } - } - } -} - -// Tests that block contents can be retrieved from a remote chain based on their hashes. -func TestGetBlockBodies67(t *testing.T) { testGetBlockBodies(t, ETH67) } -func TestGetBlockBodies68(t *testing.T) { testGetBlockBodies(t, ETH68) } - -func testGetBlockBodies(t *testing.T, protocol uint) { - t.Parallel() - - gen := func(n int, g *core.BlockGen) { - if n%2 == 0 { - w := &types.Withdrawal{ - Address: common.Address{0xaa}, - Amount: 42, - } - g.AddWithdrawal(w) - } - } - - backend := newTestBackendWithGenerator(maxBodiesServe+15, true, gen) - defer backend.close() - - peer, _ := newTestPeer("peer", protocol, backend) - defer peer.close() - - // Create a batch of tests for various scenarios - limit := maxBodiesServe - tests := []struct { - random int // Number of blocks to fetch randomly from the chain - explicit []common.Hash // Explicitly requested blocks - available []bool // Availability of explicitly requested blocks - expected int // Total number of existing blocks to expect - }{ - {1, nil, nil, 1}, // A single random block should be retrievable - {10, nil, nil, 10}, // Multiple random blocks should be retrievable - {limit, nil, nil, limit}, // The maximum possible blocks should be retrievable - {limit + 1, nil, nil, limit}, // No more than the possible block count should be returned - {0, []common.Hash{backend.chain.Genesis().Hash()}, []bool{true}, 1}, // The genesis block should be retrievable - {0, []common.Hash{backend.chain.CurrentBlock().Hash()}, []bool{true}, 1}, // The chains head block should be retrievable - {0, []common.Hash{{}}, []bool{false}, 0}, // A non existent block should not be returned - - // Existing and non-existing blocks interleaved should not cause problems - {0, []common.Hash{ - {}, - backend.chain.GetBlockByNumber(1).Hash(), - {}, - backend.chain.GetBlockByNumber(10).Hash(), - {}, - backend.chain.GetBlockByNumber(100).Hash(), - {}, - }, []bool{false, true, false, true, false, true, false}, 3}, - } - // Run each of the tests and verify the results against the chain - for i, tt := range tests { - // Collect the hashes to request, and the response to expect - var ( - hashes []common.Hash - bodies []*BlockBody - seen = make(map[int64]bool) - ) - for j := 0; j < tt.random; j++ { - for { - num := rand.Int63n(int64(backend.chain.CurrentBlock().Number.Uint64())) - if !seen[num] { - seen[num] = true - - block := backend.chain.GetBlockByNumber(uint64(num)) - hashes = append(hashes, block.Hash()) - if len(bodies) < tt.expected { - bodies = append(bodies, &BlockBody{Transactions: block.Transactions(), Uncles: block.Uncles(), Withdrawals: block.Withdrawals()}) - } - break - } - } - } - for j, hash := range tt.explicit { - hashes = append(hashes, hash) - if tt.available[j] && len(bodies) < tt.expected { - block := backend.chain.GetBlockByHash(hash) - bodies = append(bodies, &BlockBody{Transactions: block.Transactions(), Uncles: block.Uncles(), Withdrawals: block.Withdrawals()}) - } - } - - // Send the hash request and verify the response - p2p.Send(peer.app, GetBlockBodiesMsg, &GetBlockBodiesPacket{ - RequestId: 123, - GetBlockBodiesRequest: hashes, - }) - if err := p2p.ExpectMsg(peer.app, BlockBodiesMsg, &BlockBodiesPacket{ - RequestId: 123, - BlockBodiesResponse: bodies, - }); err != nil { - t.Fatalf("test %d: bodies mismatch: %v", i, err) - } - } -} - -// Tests that the transaction receipts can be retrieved based on hashes. -func TestGetBlockReceipts67(t *testing.T) { testGetBlockReceipts(t, ETH67) } -func TestGetBlockReceipts68(t *testing.T) { testGetBlockReceipts(t, ETH68) } - -func testGetBlockReceipts(t *testing.T, protocol uint) { - t.Parallel() - - // Define three accounts to simulate transactions with - acc1Key, _ := crypto.HexToECDSA("8a1f9a8f95be41cd7ccb6168179afb4504aefe388d1e14474d32c45c72ce7b7a") - acc2Key, _ := crypto.HexToECDSA("49a7b37aa6f6645917e7b807e9d1c00d4fa71f18343b0d4122a4d2df64dd6fee") - acc1Addr := crypto.PubkeyToAddress(acc1Key.PublicKey) - acc2Addr := crypto.PubkeyToAddress(acc2Key.PublicKey) - - signer := types.HomesteadSigner{} - // Create a chain generator with some simple transactions (blatantly stolen from @fjl/chain_markets_test) - generator := func(i int, block *core.BlockGen) { - switch i { - case 0: - // In block 1, the test bank sends account #1 some ether. - tx, _ := types.SignTx(types.NewTransaction(block.TxNonce(testAddr), acc1Addr, big.NewInt(10_000_000_000_000_000), params.TxGas, block.BaseFee(), nil), signer, testKey) - block.AddTx(tx) - case 1: - // In block 2, the test bank sends some more ether to account #1. - // acc1Addr passes it on to account #2. - tx1, _ := types.SignTx(types.NewTransaction(block.TxNonce(testAddr), acc1Addr, big.NewInt(1_000_000_000_000_000), params.TxGas, block.BaseFee(), nil), signer, testKey) - tx2, _ := types.SignTx(types.NewTransaction(block.TxNonce(acc1Addr), acc2Addr, big.NewInt(1_000_000_000_000_000), params.TxGas, block.BaseFee(), nil), signer, acc1Key) - block.AddTx(tx1) - block.AddTx(tx2) - case 2: - // Block 3 is empty but was mined by account #2. - block.SetCoinbase(acc2Addr) - block.SetExtra([]byte("yeehaw")) - case 3: - // Block 4 includes blocks 2 and 3 as uncle headers (with modified extra data). - b2 := block.PrevBlock(1).Header() - b2.Extra = []byte("foo") - block.AddUncle(b2) - b3 := block.PrevBlock(2).Header() - b3.Extra = []byte("foo") - block.AddUncle(b3) - } - } - // Assemble the test environment - backend := newTestBackendWithGenerator(4, false, generator) - defer backend.close() - - peer, _ := newTestPeer("peer", protocol, backend) - defer peer.close() - - // Collect the hashes to request, and the response to expect - var ( - hashes []common.Hash - receipts [][]*types.Receipt - ) - for i := uint64(0); i <= backend.chain.CurrentBlock().Number.Uint64(); i++ { - block := backend.chain.GetBlockByNumber(i) - - hashes = append(hashes, block.Hash()) - receipts = append(receipts, backend.chain.GetReceiptsByHash(block.Hash())) - } - // Send the hash request and verify the response - p2p.Send(peer.app, GetReceiptsMsg, &GetReceiptsPacket{ - RequestId: 123, - GetReceiptsRequest: hashes, - }) - if err := p2p.ExpectMsg(peer.app, ReceiptsMsg, &ReceiptsPacket{ - RequestId: 123, - ReceiptsResponse: receipts, - }); err != nil { - t.Errorf("receipts mismatch: %v", err) - } -} diff --git a/eth/protocols/eth/handlers.go b/eth/protocols/eth/handlers.go deleted file mode 100644 index 069e92dadf..0000000000 --- a/eth/protocols/eth/handlers.go +++ /dev/null @@ -1,501 +0,0 @@ -// Copyright 2021 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 eth - -import ( - "encoding/json" - "fmt" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/trie" -) - -func handleGetBlockHeaders(backend Backend, msg Decoder, peer *Peer) error { - // Decode the complex header query - var query GetBlockHeadersPacket - if err := msg.Decode(&query); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - response := ServiceGetBlockHeadersQuery(backend.Chain(), query.GetBlockHeadersRequest, peer) - return peer.ReplyBlockHeadersRLP(query.RequestId, response) -} - -// ServiceGetBlockHeadersQuery assembles the response to a header query. It is -// exposed to allow external packages to test protocol behavior. -func ServiceGetBlockHeadersQuery(chain *core.BlockChain, query *GetBlockHeadersRequest, peer *Peer) []rlp.RawValue { - if query.Skip == 0 { - // The fast path: when the request is for a contiguous segment of headers. - return serviceContiguousBlockHeaderQuery(chain, query) - } else { - return serviceNonContiguousBlockHeaderQuery(chain, query, peer) - } -} - -func serviceNonContiguousBlockHeaderQuery(chain *core.BlockChain, query *GetBlockHeadersRequest, peer *Peer) []rlp.RawValue { - hashMode := query.Origin.Hash != (common.Hash{}) - first := true - maxNonCanonical := uint64(100) - - // Gather headers until the fetch or network limits is reached - var ( - bytes common.StorageSize - headers []rlp.RawValue - unknown bool - lookups int - ) - for !unknown && len(headers) < int(query.Amount) && bytes < softResponseLimit && - len(headers) < maxHeadersServe && lookups < 2*maxHeadersServe { - lookups++ - // Retrieve the next header satisfying the query - var origin *types.Header - if hashMode { - if first { - first = false - origin = chain.GetHeaderByHash(query.Origin.Hash) - if origin != nil { - query.Origin.Number = origin.Number.Uint64() - } - } else { - origin = chain.GetHeader(query.Origin.Hash, query.Origin.Number) - } - } else { - origin = chain.GetHeaderByNumber(query.Origin.Number) - } - if origin == nil { - break - } - if rlpData, err := rlp.EncodeToBytes(origin); err != nil { - log.Crit("Unable to encode our own headers", "err", err) - } else { - headers = append(headers, rlp.RawValue(rlpData)) - bytes += common.StorageSize(len(rlpData)) - } - // Advance to the next header of the query - switch { - case hashMode && query.Reverse: - // Hash based traversal towards the genesis block - ancestor := query.Skip + 1 - if ancestor == 0 { - unknown = true - } else { - query.Origin.Hash, query.Origin.Number = chain.GetAncestor(query.Origin.Hash, query.Origin.Number, ancestor, &maxNonCanonical) - unknown = (query.Origin.Hash == common.Hash{}) - } - case hashMode && !query.Reverse: - // Hash based traversal towards the leaf block - var ( - current = origin.Number.Uint64() - next = current + query.Skip + 1 - ) - if next <= current { - infos, _ := json.MarshalIndent(peer.Peer.Info(), "", " ") - peer.Log().Warn("GetBlockHeaders skip overflow attack", "current", current, "skip", query.Skip, "next", next, "attacker", infos) - unknown = true - } else { - if header := chain.GetHeaderByNumber(next); header != nil { - nextHash := header.Hash() - expOldHash, _ := chain.GetAncestor(nextHash, next, query.Skip+1, &maxNonCanonical) - if expOldHash == query.Origin.Hash { - query.Origin.Hash, query.Origin.Number = nextHash, next - } else { - unknown = true - } - } else { - unknown = true - } - } - case query.Reverse: - // Number based traversal towards the genesis block - if query.Origin.Number >= query.Skip+1 { - query.Origin.Number -= query.Skip + 1 - } else { - unknown = true - } - - case !query.Reverse: - // Number based traversal towards the leaf block - query.Origin.Number += query.Skip + 1 - } - } - return headers -} - -func serviceContiguousBlockHeaderQuery(chain *core.BlockChain, query *GetBlockHeadersRequest) []rlp.RawValue { - count := query.Amount - if count > maxHeadersServe { - count = maxHeadersServe - } - if query.Origin.Hash == (common.Hash{}) { - // Number mode, just return the canon chain segment. The backend - // delivers in [N, N-1, N-2..] descending order, so we need to - // accommodate for that. - from := query.Origin.Number - if !query.Reverse { - from = from + count - 1 - } - headers := chain.GetHeadersFrom(from, count) - if !query.Reverse { - for i, j := 0, len(headers)-1; i < j; i, j = i+1, j-1 { - headers[i], headers[j] = headers[j], headers[i] - } - } - return headers - } - // Hash mode. - var ( - headers []rlp.RawValue - hash = query.Origin.Hash - header = chain.GetHeaderByHash(hash) - ) - if header != nil { - rlpData, _ := rlp.EncodeToBytes(header) - headers = append(headers, rlpData) - } else { - // We don't even have the origin header - return headers - } - num := header.Number.Uint64() - if !query.Reverse { - // Theoretically, we are tasked to deliver header by hash H, and onwards. - // However, if H is not canon, we will be unable to deliver any descendants of - // H. - if canonHash := chain.GetCanonicalHash(num); canonHash != hash { - // Not canon, we can't deliver descendants - return headers - } - descendants := chain.GetHeadersFrom(num+count-1, count-1) - for i, j := 0, len(descendants)-1; i < j; i, j = i+1, j-1 { - descendants[i], descendants[j] = descendants[j], descendants[i] - } - headers = append(headers, descendants...) - return headers - } - { // Last mode: deliver ancestors of H - for i := uint64(1); header != nil && i < count; i++ { - header = chain.GetHeaderByHash(header.ParentHash) - if header == nil { - break - } - rlpData, _ := rlp.EncodeToBytes(header) - headers = append(headers, rlpData) - } - return headers - } -} - -func handleGetBlockBodies(backend Backend, msg Decoder, peer *Peer) error { - // Decode the block body retrieval message - var query GetBlockBodiesPacket - if err := msg.Decode(&query); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - response := ServiceGetBlockBodiesQuery(backend.Chain(), query.GetBlockBodiesRequest) - return peer.ReplyBlockBodiesRLP(query.RequestId, response) -} - -// ServiceGetBlockBodiesQuery assembles the response to a body query. It is -// exposed to allow external packages to test protocol behavior. -func ServiceGetBlockBodiesQuery(chain *core.BlockChain, query GetBlockBodiesRequest) []rlp.RawValue { - // Gather blocks until the fetch or network limits is reached - var ( - bytes int - bodies []rlp.RawValue - ) - for lookups, hash := range query { - if bytes >= softResponseLimit || len(bodies) >= maxBodiesServe || - lookups >= 2*maxBodiesServe { - break - } - if data := chain.GetBodyRLP(hash); len(data) != 0 { - bodies = append(bodies, data) - bytes += len(data) - } - } - return bodies -} - -func handleGetReceipts(backend Backend, msg Decoder, peer *Peer) error { - // Decode the block receipts retrieval message - var query GetReceiptsPacket - if err := msg.Decode(&query); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - response := ServiceGetReceiptsQuery(backend.Chain(), query.GetReceiptsRequest) - return peer.ReplyReceiptsRLP(query.RequestId, response) -} - -// ServiceGetReceiptsQuery assembles the response to a receipt query. It is -// exposed to allow external packages to test protocol behavior. -func ServiceGetReceiptsQuery(chain *core.BlockChain, query GetReceiptsRequest) []rlp.RawValue { - // Gather state data until the fetch or network limits is reached - var ( - bytes int - receipts []rlp.RawValue - ) - for lookups, hash := range query { - if bytes >= softResponseLimit || len(receipts) >= maxReceiptsServe || - lookups >= 2*maxReceiptsServe { - break - } - // Retrieve the requested block's receipts - results := chain.GetReceiptsByHash(hash) - if results == nil { - if header := chain.GetHeaderByHash(hash); header == nil || header.ReceiptHash != types.EmptyRootHash { - continue - } - } - // If known, encode and queue for response packet - if encoded, err := rlp.EncodeToBytes(results); err != nil { - log.Error("Failed to encode receipt", "err", err) - } else { - receipts = append(receipts, encoded) - bytes += len(encoded) - } - } - return receipts -} - -func handleNewBlockhashes(backend Backend, msg Decoder, peer *Peer) error { - // A batch of new block announcements just arrived - ann := new(NewBlockHashesPacket) - if err := msg.Decode(ann); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - // Mark the hashes as present at the remote node - for _, block := range *ann { - peer.markBlock(block.Hash) - } - // Deliver them all to the backend for queuing - return backend.Handle(peer, ann) -} - -func handleNewBlock(backend Backend, msg Decoder, peer *Peer) error { - // Retrieve and decode the propagated block - ann := new(NewBlockPacket) - if err := msg.Decode(ann); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - if err := ann.sanityCheck(); err != nil { - return err - } - if hash := types.CalcUncleHash(ann.Block.Uncles()); hash != ann.Block.UncleHash() { - log.Warn("Propagated block has invalid uncles", "have", hash, "exp", ann.Block.UncleHash()) - return nil // TODO(karalabe): return error eventually, but wait a few releases - } - if hash := types.DeriveSha(ann.Block.Transactions(), trie.NewStackTrie(nil)); hash != ann.Block.TxHash() { - log.Warn("Propagated block has invalid body", "have", hash, "exp", ann.Block.TxHash()) - return nil // TODO(karalabe): return error eventually, but wait a few releases - } - ann.Block.ReceivedAt = msg.Time() - ann.Block.ReceivedFrom = peer - - // Mark the peer as owning the block - peer.markBlock(ann.Block.Hash()) - - return backend.Handle(peer, ann) -} - -func handleBlockHeaders(backend Backend, msg Decoder, peer *Peer) error { - // A batch of headers arrived to one of our previous requests - res := new(BlockHeadersPacket) - if err := msg.Decode(res); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - metadata := func() interface{} { - hashes := make([]common.Hash, len(res.BlockHeadersRequest)) - for i, header := range res.BlockHeadersRequest { - hashes[i] = header.Hash() - } - return hashes - } - return peer.dispatchResponse(&Response{ - id: res.RequestId, - code: BlockHeadersMsg, - Res: &res.BlockHeadersRequest, - }, metadata) -} - -func handleBlockBodies(backend Backend, msg Decoder, peer *Peer) error { - // A batch of block bodies arrived to one of our previous requests - res := new(BlockBodiesPacket) - if err := msg.Decode(res); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - metadata := func() interface{} { - var ( - txsHashes = make([]common.Hash, len(res.BlockBodiesResponse)) - uncleHashes = make([]common.Hash, len(res.BlockBodiesResponse)) - withdrawalHashes = make([]common.Hash, len(res.BlockBodiesResponse)) - ) - hasher := trie.NewStackTrie(nil) - for i, body := range res.BlockBodiesResponse { - txsHashes[i] = types.DeriveSha(types.Transactions(body.Transactions), hasher) - uncleHashes[i] = types.CalcUncleHash(body.Uncles) - if body.Withdrawals != nil { - withdrawalHashes[i] = types.DeriveSha(types.Withdrawals(body.Withdrawals), hasher) - } - } - return [][]common.Hash{txsHashes, uncleHashes, withdrawalHashes} - } - return peer.dispatchResponse(&Response{ - id: res.RequestId, - code: BlockBodiesMsg, - Res: &res.BlockBodiesResponse, - }, metadata) -} - -func handleReceipts(backend Backend, msg Decoder, peer *Peer) error { - // A batch of receipts arrived to one of our previous requests - res := new(ReceiptsPacket) - if err := msg.Decode(res); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - metadata := func() interface{} { - hasher := trie.NewStackTrie(nil) - hashes := make([]common.Hash, len(res.ReceiptsResponse)) - for i, receipt := range res.ReceiptsResponse { - hashes[i] = types.DeriveSha(types.Receipts(receipt), hasher) - } - return hashes - } - return peer.dispatchResponse(&Response{ - id: res.RequestId, - code: ReceiptsMsg, - Res: &res.ReceiptsResponse, - }, metadata) -} - -func handleNewPooledTransactionHashes67(backend Backend, msg Decoder, peer *Peer) error { - // New transaction announcement arrived, make sure we have - // a valid and fresh chain to handle them - if !backend.AcceptTxs() { - return nil - } - ann := new(NewPooledTransactionHashesPacket67) - if err := msg.Decode(ann); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - // Schedule all the unknown hashes for retrieval - for _, hash := range *ann { - peer.markTransaction(hash) - } - return backend.Handle(peer, ann) -} - -func handleNewPooledTransactionHashes68(backend Backend, msg Decoder, peer *Peer) error { - // New transaction announcement arrived, make sure we have - // a valid and fresh chain to handle them - if !backend.AcceptTxs() { - return nil - } - ann := new(NewPooledTransactionHashesPacket68) - if err := msg.Decode(ann); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - if len(ann.Hashes) != len(ann.Types) || len(ann.Hashes) != len(ann.Sizes) { - return fmt.Errorf("%w: message %v: invalid len of fields: %v %v %v", errDecode, msg, len(ann.Hashes), len(ann.Types), len(ann.Sizes)) - } - // Schedule all the unknown hashes for retrieval - for _, hash := range ann.Hashes { - peer.markTransaction(hash) - } - return backend.Handle(peer, ann) -} - -func handleGetPooledTransactions(backend Backend, msg Decoder, peer *Peer) error { - // Decode the pooled transactions retrieval message - var query GetPooledTransactionsPacket - if err := msg.Decode(&query); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - hashes, txs := answerGetPooledTransactions(backend, query.GetPooledTransactionsRequest) - return peer.ReplyPooledTransactionsRLP(query.RequestId, hashes, txs) -} - -func answerGetPooledTransactions(backend Backend, query GetPooledTransactionsRequest) ([]common.Hash, []rlp.RawValue) { - // Gather transactions until the fetch or network limits is reached - var ( - bytes int - hashes []common.Hash - txs []rlp.RawValue - ) - for _, hash := range query { - if bytes >= softResponseLimit { - break - } - // Retrieve the requested transaction, skipping if unknown to us - tx := backend.TxPool().Get(hash) - if tx == nil { - continue - } - // If known, encode and queue for response packet - if encoded, err := rlp.EncodeToBytes(tx); err != nil { - log.Error("Failed to encode transaction", "err", err) - } else { - hashes = append(hashes, hash) - txs = append(txs, encoded) - bytes += len(encoded) - } - } - return hashes, txs -} - -func handleTransactions(backend Backend, msg Decoder, peer *Peer) error { - // Transactions arrived, make sure we have a valid and fresh chain to handle them - if !backend.AcceptTxs() { - return nil - } - // Transactions can be processed, parse all of them and deliver to the pool - var txs TransactionsPacket - if err := msg.Decode(&txs); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - for i, tx := range txs { - // Validate and mark the remote transaction - if tx == nil { - return fmt.Errorf("%w: transaction %d is nil", errDecode, i) - } - peer.markTransaction(tx.Hash()) - } - return backend.Handle(peer, &txs) -} - -func handlePooledTransactions(backend Backend, msg Decoder, peer *Peer) error { - // Transactions arrived, make sure we have a valid and fresh chain to handle them - if !backend.AcceptTxs() { - return nil - } - // Transactions can be processed, parse all of them and deliver to the pool - var txs PooledTransactionsPacket - if err := msg.Decode(&txs); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - for i, tx := range txs.PooledTransactionsResponse { - // Validate and mark the remote transaction - if tx == nil { - return fmt.Errorf("%w: transaction %d is nil", errDecode, i) - } - peer.markTransaction(tx.Hash()) - } - requestTracker.Fulfil(peer.id, peer.version, PooledTransactionsMsg, txs.RequestId) - - return backend.Handle(peer, &txs.PooledTransactionsResponse) -} diff --git a/eth/protocols/eth/handshake.go b/eth/protocols/eth/handshake.go deleted file mode 100644 index ea16a85b1e..0000000000 --- a/eth/protocols/eth/handshake.go +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright 2020 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 eth - -import ( - "errors" - "fmt" - "math/big" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/forkid" - "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/p2p" -) - -const ( - // handshakeTimeout is the maximum allowed time for the `eth` handshake to - // complete before dropping the connection.= as malicious. - handshakeTimeout = 5 * time.Second -) - -// Handshake executes the eth protocol handshake, negotiating version number, -// network IDs, difficulties, head and genesis blocks. -func (p *Peer) Handshake(network uint64, td *big.Int, head common.Hash, genesis common.Hash, forkID forkid.ID, forkFilter forkid.Filter) error { - // Send out own handshake in a new thread - errc := make(chan error, 2) - - var status StatusPacket // safe to read after two values have been received from errc - - go func() { - errc <- p2p.Send(p.rw, StatusMsg, &StatusPacket{ - ProtocolVersion: uint32(p.version), - NetworkID: network, - TD: td, - Head: head, - Genesis: genesis, - ForkID: forkID, - }) - }() - go func() { - errc <- p.readStatus(network, &status, genesis, forkFilter) - }() - timeout := time.NewTimer(handshakeTimeout) - defer timeout.Stop() - for i := 0; i < 2; i++ { - select { - case err := <-errc: - if err != nil { - markError(p, err) - return err - } - case <-timeout.C: - markError(p, p2p.DiscReadTimeout) - return p2p.DiscReadTimeout - } - } - p.td, p.head = status.TD, status.Head - - // TD at mainnet block #7753254 is 76 bits. If it becomes 100 million times - // larger, it will still fit within 100 bits - if tdlen := p.td.BitLen(); tdlen > 100 { - return fmt.Errorf("too large total difficulty: bitlen %d", tdlen) - } - return nil -} - -// readStatus reads the remote handshake message. -func (p *Peer) readStatus(network uint64, status *StatusPacket, genesis common.Hash, forkFilter forkid.Filter) error { - msg, err := p.rw.ReadMsg() - if err != nil { - return err - } - if msg.Code != StatusMsg { - return fmt.Errorf("%w: first msg has code %x (!= %x)", errNoStatusMsg, msg.Code, StatusMsg) - } - if msg.Size > maxMessageSize { - return fmt.Errorf("%w: %v > %v", errMsgTooLarge, msg.Size, maxMessageSize) - } - // Decode the handshake and make sure everything matches - if err := msg.Decode(&status); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - if status.NetworkID != network { - return fmt.Errorf("%w: %d (!= %d)", errNetworkIDMismatch, status.NetworkID, network) - } - if uint(status.ProtocolVersion) != p.version { - return fmt.Errorf("%w: %d (!= %d)", errProtocolVersionMismatch, status.ProtocolVersion, p.version) - } - if status.Genesis != genesis { - return fmt.Errorf("%w: %x (!= %x)", errGenesisMismatch, status.Genesis, genesis) - } - if err := forkFilter(status.ForkID); err != nil { - return fmt.Errorf("%w: %v", errForkIDRejected, err) - } - return nil -} - -// markError registers the error with the corresponding metric. -func markError(p *Peer, err error) { - if !metrics.Enabled { - return - } - m := meters.get(p.Inbound()) - switch errors.Unwrap(err) { - case errNetworkIDMismatch: - m.networkIDMismatch.Mark(1) - case errProtocolVersionMismatch: - m.protocolVersionMismatch.Mark(1) - case errGenesisMismatch: - m.genesisMismatch.Mark(1) - case errForkIDRejected: - m.forkidRejected.Mark(1) - case p2p.DiscReadTimeout: - m.timeoutError.Mark(1) - default: - m.peerError.Mark(1) - } -} diff --git a/eth/protocols/eth/handshake_test.go b/eth/protocols/eth/handshake_test.go deleted file mode 100644 index d96cfc8165..0000000000 --- a/eth/protocols/eth/handshake_test.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2020 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 eth - -import ( - "errors" - "testing" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/forkid" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" -) - -// Tests that handshake failures are detected and reported correctly. -func TestHandshake67(t *testing.T) { testHandshake(t, ETH67) } -func TestHandshake68(t *testing.T) { testHandshake(t, ETH68) } - -func testHandshake(t *testing.T, protocol uint) { - t.Parallel() - - // Create a test backend only to have some valid genesis chain - backend := newTestBackend(3) - defer backend.close() - - var ( - genesis = backend.chain.Genesis() - head = backend.chain.CurrentBlock() - td = backend.chain.GetTd(head.Hash(), head.Number.Uint64()) - forkID = forkid.NewID(backend.chain.Config(), backend.chain.Genesis(), backend.chain.CurrentHeader().Number.Uint64(), backend.chain.CurrentHeader().Time) - ) - tests := []struct { - code uint64 - data interface{} - want error - }{ - { - code: TransactionsMsg, data: []interface{}{}, - want: errNoStatusMsg, - }, - { - code: StatusMsg, data: StatusPacket{10, 1, td, head.Hash(), genesis.Hash(), forkID}, - want: errProtocolVersionMismatch, - }, - { - code: StatusMsg, data: StatusPacket{uint32(protocol), 999, td, head.Hash(), genesis.Hash(), forkID}, - want: errNetworkIDMismatch, - }, - { - code: StatusMsg, data: StatusPacket{uint32(protocol), 1, td, head.Hash(), common.Hash{3}, forkID}, - want: errGenesisMismatch, - }, - { - code: StatusMsg, data: StatusPacket{uint32(protocol), 1, td, head.Hash(), genesis.Hash(), forkid.ID{Hash: [4]byte{0x00, 0x01, 0x02, 0x03}}}, - want: errForkIDRejected, - }, - } - for i, test := range tests { - // Create the two peers to shake with each other - app, net := p2p.MsgPipe() - defer app.Close() - defer net.Close() - - peer := NewPeer(protocol, p2p.NewPeer(enode.ID{}, "peer", nil), net, nil) - defer peer.Close() - - // Send the junk test with one peer, check the handshake failure - go p2p.Send(app, test.code, test.data) - - err := peer.Handshake(1, td, head.Hash(), genesis.Hash(), forkID, forkid.NewFilter(backend.chain)) - if err == nil { - t.Errorf("test %d: protocol returned nil error, want %q", i, test.want) - } else if !errors.Is(err, test.want) { - t.Errorf("test %d: wrong error: got %q, want %q", i, err, test.want) - } - } -} diff --git a/eth/protocols/eth/metrics.go b/eth/protocols/eth/metrics.go deleted file mode 100644 index 5e0aee39f8..0000000000 --- a/eth/protocols/eth/metrics.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2023 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 eth - -import "github.com/ethereum/go-ethereum/metrics" - -// meters stores ingress and egress handshake meters. -var meters bidirectionalMeters - -// bidirectionalMeters stores ingress and egress handshake meters. -type bidirectionalMeters struct { - ingress *hsMeters - egress *hsMeters -} - -// get returns the corresponding meter depending if ingress or egress is -// desired. -func (h *bidirectionalMeters) get(ingress bool) *hsMeters { - if ingress { - return h.ingress - } - return h.egress -} - -// hsMeters is a collection of meters which track metrics related to the -// eth subprotocol handshake. -type hsMeters struct { - // peerError measures the number of errors related to incorrect peer - // behaviour, such as invalid message code, size, encoding, etc. - peerError metrics.Meter - - // timeoutError measures the number of timeouts. - timeoutError metrics.Meter - - // networkIDMismatch measures the number of network id mismatch errors. - networkIDMismatch metrics.Meter - - // protocolVersionMismatch measures the number of differing protocol - // versions. - protocolVersionMismatch metrics.Meter - - // genesisMismatch measures the number of differing genesises. - genesisMismatch metrics.Meter - - // forkidRejected measures the number of differing forkids. - forkidRejected metrics.Meter -} - -// newHandshakeMeters registers and returns handshake meters for the given -// base. -func newHandshakeMeters(base string) *hsMeters { - return &hsMeters{ - peerError: metrics.NewRegisteredMeter(base+"error/peer", nil), - timeoutError: metrics.NewRegisteredMeter(base+"error/timeout", nil), - networkIDMismatch: metrics.NewRegisteredMeter(base+"error/network", nil), - protocolVersionMismatch: metrics.NewRegisteredMeter(base+"error/version", nil), - genesisMismatch: metrics.NewRegisteredMeter(base+"error/genesis", nil), - forkidRejected: metrics.NewRegisteredMeter(base+"error/forkid", nil), - } -} - -func init() { - meters = bidirectionalMeters{ - ingress: newHandshakeMeters("eth/protocols/eth/ingress/handshake/"), - egress: newHandshakeMeters("eth/protocols/eth/egress/handshake/"), - } -} diff --git a/eth/protocols/eth/peer.go b/eth/protocols/eth/peer.go deleted file mode 100644 index 98ad22a8cf..0000000000 --- a/eth/protocols/eth/peer.go +++ /dev/null @@ -1,505 +0,0 @@ -// Copyright 2020 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 eth - -import ( - "math/big" - "math/rand" - "sync" - - mapset "github.com/deckarep/golang-set/v2" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/rlp" -) - -const ( - // maxKnownTxs is the maximum transactions hashes to keep in the known list - // before starting to randomly evict them. - maxKnownTxs = 32768 - - // maxKnownBlocks is the maximum block hashes to keep in the known list - // before starting to randomly evict them. - maxKnownBlocks = 1024 - - // maxQueuedTxs is the maximum number of transactions to queue up before dropping - // older broadcasts. - maxQueuedTxs = 4096 - - // maxQueuedTxAnns is the maximum number of transaction announcements to queue up - // before dropping older announcements. - maxQueuedTxAnns = 4096 - - // maxQueuedBlocks is the maximum number of block propagations to queue up before - // dropping broadcasts. There's not much point in queueing stale blocks, so a few - // that might cover uncles should be enough. - maxQueuedBlocks = 4 - - // maxQueuedBlockAnns is the maximum number of block announcements to queue up before - // dropping broadcasts. Similarly to block propagations, there's no point to queue - // above some healthy uncle limit, so use that. - maxQueuedBlockAnns = 4 -) - -// max is a helper function which returns the larger of the two given integers. -func max(a, b int) int { - if a > b { - return a - } - return b -} - -// Peer is a collection of relevant information we have about a `eth` peer. -type Peer struct { - id string // Unique ID for the peer, cached - - *p2p.Peer // The embedded P2P package peer - rw p2p.MsgReadWriter // Input/output streams for snap - version uint // Protocol version negotiated - - head common.Hash // Latest advertised head block hash - td *big.Int // Latest advertised head block total difficulty - - knownBlocks *knownCache // Set of block hashes known to be known by this peer - queuedBlocks chan *blockPropagation // Queue of blocks to broadcast to the peer - queuedBlockAnns chan *types.Block // Queue of blocks to announce to the peer - - txpool TxPool // Transaction pool used by the broadcasters for liveness checks - knownTxs *knownCache // Set of transaction hashes known to be known by this peer - txBroadcast chan []common.Hash // Channel used to queue transaction propagation requests - txAnnounce chan []common.Hash // Channel used to queue transaction announcement requests - - reqDispatch chan *request // Dispatch channel to send requests and track then until fulfillment - reqCancel chan *cancel // Dispatch channel to cancel pending requests and untrack them - resDispatch chan *response // Dispatch channel to fulfil pending requests and untrack them - - term chan struct{} // Termination channel to stop the broadcasters - lock sync.RWMutex // Mutex protecting the internal fields -} - -// NewPeer create a wrapper for a network connection and negotiated protocol -// version. -func NewPeer(version uint, p *p2p.Peer, rw p2p.MsgReadWriter, txpool TxPool) *Peer { - peer := &Peer{ - id: p.ID().String(), - Peer: p, - rw: rw, - version: version, - knownTxs: newKnownCache(maxKnownTxs), - knownBlocks: newKnownCache(maxKnownBlocks), - queuedBlocks: make(chan *blockPropagation, maxQueuedBlocks), - queuedBlockAnns: make(chan *types.Block, maxQueuedBlockAnns), - txBroadcast: make(chan []common.Hash), - txAnnounce: make(chan []common.Hash), - reqDispatch: make(chan *request), - reqCancel: make(chan *cancel), - resDispatch: make(chan *response), - txpool: txpool, - term: make(chan struct{}), - } - // Start up all the broadcasters - go peer.broadcastBlocks() - go peer.broadcastTransactions() - go peer.announceTransactions() - go peer.dispatcher() - - return peer -} - -// Close signals the broadcast goroutine to terminate. Only ever call this if -// you created the peer yourself via NewPeer. Otherwise let whoever created it -// clean it up! -func (p *Peer) Close() { - close(p.term) -} - -// ID retrieves the peer's unique identifier. -func (p *Peer) ID() string { - return p.id -} - -// Version retrieves the peer's negotiated `eth` protocol version. -func (p *Peer) Version() uint { - return p.version -} - -// Head retrieves the current head hash and total difficulty of the peer. -func (p *Peer) Head() (hash common.Hash, td *big.Int) { - p.lock.RLock() - defer p.lock.RUnlock() - - copy(hash[:], p.head[:]) - return hash, new(big.Int).Set(p.td) -} - -// SetHead updates the head hash and total difficulty of the peer. -func (p *Peer) SetHead(hash common.Hash, td *big.Int) { - p.lock.Lock() - defer p.lock.Unlock() - - copy(p.head[:], hash[:]) - p.td.Set(td) -} - -// KnownBlock returns whether peer is known to already have a block. -func (p *Peer) KnownBlock(hash common.Hash) bool { - return p.knownBlocks.Contains(hash) -} - -// KnownTransaction returns whether peer is known to already have a transaction. -func (p *Peer) KnownTransaction(hash common.Hash) bool { - return p.knownTxs.Contains(hash) -} - -// markBlock marks a block as known for the peer, ensuring that the block will -// never be propagated to this particular peer. -func (p *Peer) markBlock(hash common.Hash) { - // If we reached the memory allowance, drop a previously known block hash - p.knownBlocks.Add(hash) -} - -// markTransaction marks a transaction as known for the peer, ensuring that it -// will never be propagated to this particular peer. -func (p *Peer) markTransaction(hash common.Hash) { - // If we reached the memory allowance, drop a previously known transaction hash - p.knownTxs.Add(hash) -} - -// SendTransactions sends transactions to the peer and includes the hashes -// in its transaction hash set for future reference. -// -// This method is a helper used by the async transaction sender. Don't call it -// directly as the queueing (memory) and transmission (bandwidth) costs should -// not be managed directly. -// -// The reasons this is public is to allow packages using this protocol to write -// tests that directly send messages without having to do the async queueing. -func (p *Peer) SendTransactions(txs types.Transactions) error { - // Mark all the transactions as known, but ensure we don't overflow our limits - for _, tx := range txs { - p.knownTxs.Add(tx.Hash()) - } - return p2p.Send(p.rw, TransactionsMsg, txs) -} - -// AsyncSendTransactions queues a list of transactions (by hash) to eventually -// propagate to a remote peer. The number of pending sends are capped (new ones -// will force old sends to be dropped) -func (p *Peer) AsyncSendTransactions(hashes []common.Hash) { - select { - case p.txBroadcast <- hashes: - // Mark all the transactions as known, but ensure we don't overflow our limits - p.knownTxs.Add(hashes...) - case <-p.term: - p.Log().Debug("Dropping transaction propagation", "count", len(hashes)) - } -} - -// sendPooledTransactionHashes66 sends transaction hashes to the peer and includes -// them in its transaction hash set for future reference. -// -// This method is a helper used by the async transaction announcer. Don't call it -// directly as the queueing (memory) and transmission (bandwidth) costs should -// not be managed directly. -func (p *Peer) sendPooledTransactionHashes66(hashes []common.Hash) error { - // Mark all the transactions as known, but ensure we don't overflow our limits - p.knownTxs.Add(hashes...) - return p2p.Send(p.rw, NewPooledTransactionHashesMsg, NewPooledTransactionHashesPacket67(hashes)) -} - -// sendPooledTransactionHashes68 sends transaction hashes (tagged with their type -// and size) to the peer and includes them in its transaction hash set for future -// reference. -// -// This method is a helper used by the async transaction announcer. Don't call it -// directly as the queueing (memory) and transmission (bandwidth) costs should -// not be managed directly. -func (p *Peer) sendPooledTransactionHashes68(hashes []common.Hash, types []byte, sizes []uint32) error { - // Mark all the transactions as known, but ensure we don't overflow our limits - p.knownTxs.Add(hashes...) - return p2p.Send(p.rw, NewPooledTransactionHashesMsg, NewPooledTransactionHashesPacket68{Types: types, Sizes: sizes, Hashes: hashes}) -} - -// AsyncSendPooledTransactionHashes queues a list of transactions hashes to eventually -// announce to a remote peer. The number of pending sends are capped (new ones -// will force old sends to be dropped) -func (p *Peer) AsyncSendPooledTransactionHashes(hashes []common.Hash) { - select { - case p.txAnnounce <- hashes: - // Mark all the transactions as known, but ensure we don't overflow our limits - p.knownTxs.Add(hashes...) - case <-p.term: - p.Log().Debug("Dropping transaction announcement", "count", len(hashes)) - } -} - -// ReplyPooledTransactionsRLP is the response to RequestTxs. -func (p *Peer) ReplyPooledTransactionsRLP(id uint64, hashes []common.Hash, txs []rlp.RawValue) error { - // Mark all the transactions as known, but ensure we don't overflow our limits - p.knownTxs.Add(hashes...) - - // Not packed into PooledTransactionsResponse to avoid RLP decoding - return p2p.Send(p.rw, PooledTransactionsMsg, &PooledTransactionsRLPPacket{ - RequestId: id, - PooledTransactionsRLPResponse: txs, - }) -} - -// SendNewBlockHashes announces the availability of a number of blocks through -// a hash notification. -func (p *Peer) SendNewBlockHashes(hashes []common.Hash, numbers []uint64) error { - // Mark all the block hashes as known, but ensure we don't overflow our limits - p.knownBlocks.Add(hashes...) - - request := make(NewBlockHashesPacket, len(hashes)) - for i := 0; i < len(hashes); i++ { - request[i].Hash = hashes[i] - request[i].Number = numbers[i] - } - return p2p.Send(p.rw, NewBlockHashesMsg, request) -} - -// AsyncSendNewBlockHash queues the availability of a block for propagation to a -// remote peer. If the peer's broadcast queue is full, the event is silently -// dropped. -func (p *Peer) AsyncSendNewBlockHash(block *types.Block) { - select { - case p.queuedBlockAnns <- block: - // Mark all the block hash as known, but ensure we don't overflow our limits - p.knownBlocks.Add(block.Hash()) - default: - p.Log().Debug("Dropping block announcement", "number", block.NumberU64(), "hash", block.Hash()) - } -} - -// SendNewBlock propagates an entire block to a remote peer. -func (p *Peer) SendNewBlock(block *types.Block, td *big.Int) error { - // Mark all the block hash as known, but ensure we don't overflow our limits - p.knownBlocks.Add(block.Hash()) - return p2p.Send(p.rw, NewBlockMsg, &NewBlockPacket{ - Block: block, - TD: td, - }) -} - -// AsyncSendNewBlock queues an entire block for propagation to a remote peer. If -// the peer's broadcast queue is full, the event is silently dropped. -func (p *Peer) AsyncSendNewBlock(block *types.Block, td *big.Int) { - select { - case p.queuedBlocks <- &blockPropagation{block: block, td: td}: - // Mark all the block hash as known, but ensure we don't overflow our limits - p.knownBlocks.Add(block.Hash()) - default: - p.Log().Debug("Dropping block propagation", "number", block.NumberU64(), "hash", block.Hash()) - } -} - -// ReplyBlockHeadersRLP is the response to GetBlockHeaders. -func (p *Peer) ReplyBlockHeadersRLP(id uint64, headers []rlp.RawValue) error { - return p2p.Send(p.rw, BlockHeadersMsg, &BlockHeadersRLPPacket{ - RequestId: id, - BlockHeadersRLPResponse: headers, - }) -} - -// ReplyBlockBodiesRLP is the response to GetBlockBodies. -func (p *Peer) ReplyBlockBodiesRLP(id uint64, bodies []rlp.RawValue) error { - // Not packed into BlockBodiesResponse to avoid RLP decoding - return p2p.Send(p.rw, BlockBodiesMsg, &BlockBodiesRLPPacket{ - RequestId: id, - BlockBodiesRLPResponse: bodies, - }) -} - -// ReplyReceiptsRLP is the response to GetReceipts. -func (p *Peer) ReplyReceiptsRLP(id uint64, receipts []rlp.RawValue) error { - return p2p.Send(p.rw, ReceiptsMsg, &ReceiptsRLPPacket{ - RequestId: id, - ReceiptsRLPResponse: receipts, - }) -} - -// RequestOneHeader is a wrapper around the header query functions to fetch a -// single header. It is used solely by the fetcher. -func (p *Peer) RequestOneHeader(hash common.Hash, sink chan *Response) (*Request, error) { - p.Log().Debug("Fetching single header", "hash", hash) - id := rand.Uint64() - - req := &Request{ - id: id, - sink: sink, - code: GetBlockHeadersMsg, - want: BlockHeadersMsg, - data: &GetBlockHeadersPacket{ - RequestId: id, - GetBlockHeadersRequest: &GetBlockHeadersRequest{ - Origin: HashOrNumber{Hash: hash}, - Amount: uint64(1), - Skip: uint64(0), - Reverse: false, - }, - }, - } - if err := p.dispatchRequest(req); err != nil { - return nil, err - } - return req, nil -} - -// RequestHeadersByHash fetches a batch of blocks' headers corresponding to the -// specified header query, based on the hash of an origin block. -func (p *Peer) RequestHeadersByHash(origin common.Hash, amount int, skip int, reverse bool, sink chan *Response) (*Request, error) { - p.Log().Debug("Fetching batch of headers", "count", amount, "fromhash", origin, "skip", skip, "reverse", reverse) - id := rand.Uint64() - - req := &Request{ - id: id, - sink: sink, - code: GetBlockHeadersMsg, - want: BlockHeadersMsg, - data: &GetBlockHeadersPacket{ - RequestId: id, - GetBlockHeadersRequest: &GetBlockHeadersRequest{ - Origin: HashOrNumber{Hash: origin}, - Amount: uint64(amount), - Skip: uint64(skip), - Reverse: reverse, - }, - }, - } - if err := p.dispatchRequest(req); err != nil { - return nil, err - } - return req, nil -} - -// RequestHeadersByNumber fetches a batch of blocks' headers corresponding to the -// specified header query, based on the number of an origin block. -func (p *Peer) RequestHeadersByNumber(origin uint64, amount int, skip int, reverse bool, sink chan *Response) (*Request, error) { - p.Log().Debug("Fetching batch of headers", "count", amount, "fromnum", origin, "skip", skip, "reverse", reverse) - id := rand.Uint64() - - req := &Request{ - id: id, - sink: sink, - code: GetBlockHeadersMsg, - want: BlockHeadersMsg, - data: &GetBlockHeadersPacket{ - RequestId: id, - GetBlockHeadersRequest: &GetBlockHeadersRequest{ - Origin: HashOrNumber{Number: origin}, - Amount: uint64(amount), - Skip: uint64(skip), - Reverse: reverse, - }, - }, - } - if err := p.dispatchRequest(req); err != nil { - return nil, err - } - return req, nil -} - -// RequestBodies fetches a batch of blocks' bodies corresponding to the hashes -// specified. -func (p *Peer) RequestBodies(hashes []common.Hash, sink chan *Response) (*Request, error) { - p.Log().Debug("Fetching batch of block bodies", "count", len(hashes)) - id := rand.Uint64() - - req := &Request{ - id: id, - sink: sink, - code: GetBlockBodiesMsg, - want: BlockBodiesMsg, - data: &GetBlockBodiesPacket{ - RequestId: id, - GetBlockBodiesRequest: hashes, - }, - } - if err := p.dispatchRequest(req); err != nil { - return nil, err - } - return req, nil -} - -// RequestReceipts fetches a batch of transaction receipts from a remote node. -func (p *Peer) RequestReceipts(hashes []common.Hash, sink chan *Response) (*Request, error) { - p.Log().Debug("Fetching batch of receipts", "count", len(hashes)) - id := rand.Uint64() - - req := &Request{ - id: id, - sink: sink, - code: GetReceiptsMsg, - want: ReceiptsMsg, - data: &GetReceiptsPacket{ - RequestId: id, - GetReceiptsRequest: hashes, - }, - } - if err := p.dispatchRequest(req); err != nil { - return nil, err - } - return req, nil -} - -// RequestTxs fetches a batch of transactions from a remote node. -func (p *Peer) RequestTxs(hashes []common.Hash) error { - p.Log().Debug("Fetching batch of transactions", "count", len(hashes)) - id := rand.Uint64() - - requestTracker.Track(p.id, p.version, GetPooledTransactionsMsg, PooledTransactionsMsg, id) - return p2p.Send(p.rw, GetPooledTransactionsMsg, &GetPooledTransactionsPacket{ - RequestId: id, - GetPooledTransactionsRequest: hashes, - }) -} - -// knownCache is a cache for known hashes. -type knownCache struct { - hashes mapset.Set[common.Hash] - max int -} - -// newKnownCache creates a new knownCache with a max capacity. -func newKnownCache(max int) *knownCache { - return &knownCache{ - max: max, - hashes: mapset.NewSet[common.Hash](), - } -} - -// Add adds a list of elements to the set. -func (k *knownCache) Add(hashes ...common.Hash) { - for k.hashes.Cardinality() > max(0, k.max-len(hashes)) { - k.hashes.Pop() - } - for _, hash := range hashes { - k.hashes.Add(hash) - } -} - -// Contains returns whether the given item is in the set. -func (k *knownCache) Contains(hash common.Hash) bool { - return k.hashes.Contains(hash) -} - -// Cardinality returns the number of elements in the set. -func (k *knownCache) Cardinality() int { - return k.hashes.Cardinality() -} diff --git a/eth/protocols/eth/peer_test.go b/eth/protocols/eth/peer_test.go deleted file mode 100644 index efbbbc6fff..0000000000 --- a/eth/protocols/eth/peer_test.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2020 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 . - -// This file contains some shares testing functionality, common to multiple -// different files and modules being tested. - -package eth - -import ( - "crypto/rand" - "testing" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" -) - -// testPeer is a simulated peer to allow testing direct network calls. -type testPeer struct { - *Peer - - net p2p.MsgReadWriter // Network layer reader/writer to simulate remote messaging - app *p2p.MsgPipeRW // Application layer reader/writer to simulate the local side -} - -// newTestPeer creates a new peer registered at the given data backend. -func newTestPeer(name string, version uint, backend Backend) (*testPeer, <-chan error) { - // Create a message pipe to communicate through - app, net := p2p.MsgPipe() - - // Start the peer on a new thread - var id enode.ID - rand.Read(id[:]) - - peer := NewPeer(version, p2p.NewPeer(id, name, nil), net, backend.TxPool()) - errc := make(chan error, 1) - go func() { - defer app.Close() - - errc <- backend.RunPeer(peer, func(peer *Peer) error { - return Handle(backend, peer) - }) - }() - return &testPeer{app: app, net: net, Peer: peer}, errc -} - -// close terminates the local side of the peer, notifying the remote protocol -// manager of termination. -func (p *testPeer) close() { - p.Peer.Close() - p.app.Close() -} - -func TestPeerSet(t *testing.T) { - size := 5 - s := newKnownCache(size) - - // add 10 items - for i := 0; i < size*2; i++ { - s.Add(common.Hash{byte(i)}) - } - - if s.Cardinality() != size { - t.Fatalf("wrong size, expected %d but found %d", size, s.Cardinality()) - } - - vals := []common.Hash{} - for i := 10; i < 20; i++ { - vals = append(vals, common.Hash{byte(i)}) - } - - // add item in batch - s.Add(vals...) - if s.Cardinality() < size { - t.Fatalf("bad size") - } -} diff --git a/eth/protocols/eth/protocol.go b/eth/protocols/eth/protocol.go deleted file mode 100644 index 0f44f83de1..0000000000 --- a/eth/protocols/eth/protocol.go +++ /dev/null @@ -1,364 +0,0 @@ -// Copyright 2020 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 eth - -import ( - "errors" - "fmt" - "io" - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/forkid" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rlp" -) - -// Constants to match up protocol versions and messages -const ( - ETH67 = 67 - ETH68 = 68 -) - -// ProtocolName is the official short name of the `eth` protocol used during -// devp2p capability negotiation. -const ProtocolName = "eth" - -// ProtocolVersions are the supported versions of the `eth` protocol (first -// is primary). -var ProtocolVersions = []uint{ETH68, ETH67} - -// protocolLengths are the number of implemented message corresponding to -// different protocol versions. -var protocolLengths = map[uint]uint64{ETH68: 17, ETH67: 17} - -// maxMessageSize is the maximum cap on the size of a protocol message. -const maxMessageSize = 10 * 1024 * 1024 - -const ( - StatusMsg = 0x00 - NewBlockHashesMsg = 0x01 - TransactionsMsg = 0x02 - GetBlockHeadersMsg = 0x03 - BlockHeadersMsg = 0x04 - GetBlockBodiesMsg = 0x05 - BlockBodiesMsg = 0x06 - NewBlockMsg = 0x07 - NewPooledTransactionHashesMsg = 0x08 - GetPooledTransactionsMsg = 0x09 - PooledTransactionsMsg = 0x0a - GetReceiptsMsg = 0x0f - ReceiptsMsg = 0x10 -) - -var ( - errNoStatusMsg = errors.New("no status message") - errMsgTooLarge = errors.New("message too long") - errDecode = errors.New("invalid message") - errInvalidMsgCode = errors.New("invalid message code") - errProtocolVersionMismatch = errors.New("protocol version mismatch") - errNetworkIDMismatch = errors.New("network ID mismatch") - errGenesisMismatch = errors.New("genesis mismatch") - errForkIDRejected = errors.New("fork ID rejected") -) - -// Packet represents a p2p message in the `eth` protocol. -type Packet interface { - Name() string // Name returns a string corresponding to the message type. - Kind() byte // Kind returns the message type. -} - -// StatusPacket is the network packet for the status message. -type StatusPacket struct { - ProtocolVersion uint32 - NetworkID uint64 - TD *big.Int - Head common.Hash - Genesis common.Hash - ForkID forkid.ID -} - -// NewBlockHashesPacket is the network packet for the block announcements. -type NewBlockHashesPacket []struct { - Hash common.Hash // Hash of one particular block being announced - Number uint64 // Number of one particular block being announced -} - -// Unpack retrieves the block hashes and numbers from the announcement packet -// and returns them in a split flat format that's more consistent with the -// internal data structures. -func (p *NewBlockHashesPacket) Unpack() ([]common.Hash, []uint64) { - var ( - hashes = make([]common.Hash, len(*p)) - numbers = make([]uint64, len(*p)) - ) - for i, body := range *p { - hashes[i], numbers[i] = body.Hash, body.Number - } - return hashes, numbers -} - -// TransactionsPacket is the network packet for broadcasting new transactions. -type TransactionsPacket []*types.Transaction - -// GetBlockHeadersRequest represents a block header query. -type GetBlockHeadersRequest struct { - Origin HashOrNumber // Block from which to retrieve headers - Amount uint64 // Maximum number of headers to retrieve - Skip uint64 // Blocks to skip between consecutive headers - Reverse bool // Query direction (false = rising towards latest, true = falling towards genesis) -} - -// GetBlockHeadersPacket represents a block header query with request ID wrapping. -type GetBlockHeadersPacket struct { - RequestId uint64 - *GetBlockHeadersRequest -} - -// HashOrNumber is a combined field for specifying an origin block. -type HashOrNumber struct { - Hash common.Hash // Block hash from which to retrieve headers (excludes Number) - Number uint64 // Block hash from which to retrieve headers (excludes Hash) -} - -// EncodeRLP is a specialized encoder for HashOrNumber to encode only one of the -// two contained union fields. -func (hn *HashOrNumber) EncodeRLP(w io.Writer) error { - if hn.Hash == (common.Hash{}) { - return rlp.Encode(w, hn.Number) - } - if hn.Number != 0 { - return fmt.Errorf("both origin hash (%x) and number (%d) provided", hn.Hash, hn.Number) - } - return rlp.Encode(w, hn.Hash) -} - -// DecodeRLP is a specialized decoder for HashOrNumber to decode the contents -// into either a block hash or a block number. -func (hn *HashOrNumber) DecodeRLP(s *rlp.Stream) error { - _, size, err := s.Kind() - switch { - case err != nil: - return err - case size == 32: - hn.Number = 0 - return s.Decode(&hn.Hash) - case size <= 8: - hn.Hash = common.Hash{} - return s.Decode(&hn.Number) - default: - return fmt.Errorf("invalid input size %d for origin", size) - } -} - -// BlockHeadersRequest represents a block header response. -type BlockHeadersRequest []*types.Header - -// BlockHeadersPacket represents a block header response over with request ID wrapping. -type BlockHeadersPacket struct { - RequestId uint64 - BlockHeadersRequest -} - -// BlockHeadersRLPResponse represents a block header response, to use when we already -// have the headers rlp encoded. -type BlockHeadersRLPResponse []rlp.RawValue - -// BlockHeadersRLPPacket represents a block header response with request ID wrapping. -type BlockHeadersRLPPacket struct { - RequestId uint64 - BlockHeadersRLPResponse -} - -// NewBlockPacket is the network packet for the block propagation message. -type NewBlockPacket struct { - Block *types.Block - TD *big.Int -} - -// sanityCheck verifies that the values are reasonable, as a DoS protection -func (request *NewBlockPacket) sanityCheck() error { - if err := request.Block.SanityCheck(); err != nil { - return err - } - //TD at mainnet block #7753254 is 76 bits. If it becomes 100 million times - // larger, it will still fit within 100 bits - if tdlen := request.TD.BitLen(); tdlen > 100 { - return fmt.Errorf("too large block TD: bitlen %d", tdlen) - } - return nil -} - -// GetBlockBodiesRequest represents a block body query. -type GetBlockBodiesRequest []common.Hash - -// GetBlockBodiesPacket represents a block body query with request ID wrapping. -type GetBlockBodiesPacket struct { - RequestId uint64 - GetBlockBodiesRequest -} - -// BlockBodiesResponse is the network packet for block content distribution. -type BlockBodiesResponse []*BlockBody - -// BlockBodiesPacket is the network packet for block content distribution with -// request ID wrapping. -type BlockBodiesPacket struct { - RequestId uint64 - BlockBodiesResponse -} - -// BlockBodiesRLPResponse is used for replying to block body requests, in cases -// where we already have them RLP-encoded, and thus can avoid the decode-encode -// roundtrip. -type BlockBodiesRLPResponse []rlp.RawValue - -// BlockBodiesRLPPacket is the BlockBodiesRLPResponse with request ID wrapping. -type BlockBodiesRLPPacket struct { - RequestId uint64 - BlockBodiesRLPResponse -} - -// BlockBody represents the data content of a single block. -type BlockBody struct { - Transactions []*types.Transaction // Transactions contained within a block - Uncles []*types.Header // Uncles contained within a block - Withdrawals []*types.Withdrawal `rlp:"optional"` // Withdrawals contained within a block -} - -// Unpack retrieves the transactions and uncles from the range packet and returns -// them in a split flat format that's more consistent with the internal data structures. -func (p *BlockBodiesResponse) Unpack() ([][]*types.Transaction, [][]*types.Header, [][]*types.Withdrawal) { - // TODO(matt): add support for withdrawals to fetchers - var ( - txset = make([][]*types.Transaction, len(*p)) - uncleset = make([][]*types.Header, len(*p)) - withdrawalset = make([][]*types.Withdrawal, len(*p)) - ) - for i, body := range *p { - txset[i], uncleset[i], withdrawalset[i] = body.Transactions, body.Uncles, body.Withdrawals - } - return txset, uncleset, withdrawalset -} - -// GetReceiptsRequest represents a block receipts query. -type GetReceiptsRequest []common.Hash - -// GetReceiptsPacket represents a block receipts query with request ID wrapping. -type GetReceiptsPacket struct { - RequestId uint64 - GetReceiptsRequest -} - -// ReceiptsResponse is the network packet for block receipts distribution. -type ReceiptsResponse [][]*types.Receipt - -// ReceiptsPacket is the network packet for block receipts distribution with -// request ID wrapping. -type ReceiptsPacket struct { - RequestId uint64 - ReceiptsResponse -} - -// ReceiptsRLPResponse is used for receipts, when we already have it encoded -type ReceiptsRLPResponse []rlp.RawValue - -// ReceiptsRLPPacket is ReceiptsRLPResponse with request ID wrapping. -type ReceiptsRLPPacket struct { - RequestId uint64 - ReceiptsRLPResponse -} - -// NewPooledTransactionHashesPacket67 represents a transaction announcement packet on eth/67. -type NewPooledTransactionHashesPacket67 []common.Hash - -// NewPooledTransactionHashesPacket68 represents a transaction announcement packet on eth/68 and newer. -type NewPooledTransactionHashesPacket68 struct { - Types []byte - Sizes []uint32 - Hashes []common.Hash -} - -// GetPooledTransactionsRequest represents a transaction query. -type GetPooledTransactionsRequest []common.Hash - -// GetPooledTransactionsPacket represents a transaction query with request ID wrapping. -type GetPooledTransactionsPacket struct { - RequestId uint64 - GetPooledTransactionsRequest -} - -// PooledTransactionsResponse is the network packet for transaction distribution. -type PooledTransactionsResponse []*types.Transaction - -// PooledTransactionsPacket is the network packet for transaction distribution -// with request ID wrapping. -type PooledTransactionsPacket struct { - RequestId uint64 - PooledTransactionsResponse -} - -// PooledTransactionsRLPResponse is the network packet for transaction distribution, used -// in the cases we already have them in rlp-encoded form -type PooledTransactionsRLPResponse []rlp.RawValue - -// PooledTransactionsRLPPacket is PooledTransactionsRLPResponse with request ID wrapping. -type PooledTransactionsRLPPacket struct { - RequestId uint64 - PooledTransactionsRLPResponse -} - -func (*StatusPacket) Name() string { return "Status" } -func (*StatusPacket) Kind() byte { return StatusMsg } - -func (*NewBlockHashesPacket) Name() string { return "NewBlockHashes" } -func (*NewBlockHashesPacket) Kind() byte { return NewBlockHashesMsg } - -func (*TransactionsPacket) Name() string { return "Transactions" } -func (*TransactionsPacket) Kind() byte { return TransactionsMsg } - -func (*GetBlockHeadersRequest) Name() string { return "GetBlockHeaders" } -func (*GetBlockHeadersRequest) Kind() byte { return GetBlockHeadersMsg } - -func (*BlockHeadersRequest) Name() string { return "BlockHeaders" } -func (*BlockHeadersRequest) Kind() byte { return BlockHeadersMsg } - -func (*GetBlockBodiesRequest) Name() string { return "GetBlockBodies" } -func (*GetBlockBodiesRequest) Kind() byte { return GetBlockBodiesMsg } - -func (*BlockBodiesResponse) Name() string { return "BlockBodies" } -func (*BlockBodiesResponse) Kind() byte { return BlockBodiesMsg } - -func (*NewBlockPacket) Name() string { return "NewBlock" } -func (*NewBlockPacket) Kind() byte { return NewBlockMsg } - -func (*NewPooledTransactionHashesPacket67) Name() string { return "NewPooledTransactionHashes" } -func (*NewPooledTransactionHashesPacket67) Kind() byte { return NewPooledTransactionHashesMsg } -func (*NewPooledTransactionHashesPacket68) Name() string { return "NewPooledTransactionHashes" } -func (*NewPooledTransactionHashesPacket68) Kind() byte { return NewPooledTransactionHashesMsg } - -func (*GetPooledTransactionsRequest) Name() string { return "GetPooledTransactions" } -func (*GetPooledTransactionsRequest) Kind() byte { return GetPooledTransactionsMsg } - -func (*PooledTransactionsResponse) Name() string { return "PooledTransactions" } -func (*PooledTransactionsResponse) Kind() byte { return PooledTransactionsMsg } - -func (*GetReceiptsRequest) Name() string { return "GetReceipts" } -func (*GetReceiptsRequest) Kind() byte { return GetReceiptsMsg } - -func (*ReceiptsResponse) Name() string { return "Receipts" } -func (*ReceiptsResponse) Kind() byte { return ReceiptsMsg } diff --git a/eth/protocols/eth/protocol_test.go b/eth/protocols/eth/protocol_test.go deleted file mode 100644 index bc2545dea2..0000000000 --- a/eth/protocols/eth/protocol_test.go +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright 2020 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 eth - -import ( - "bytes" - "math/big" - "testing" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rlp" -) - -// Tests that the custom union field encoder and decoder works correctly. -func TestGetBlockHeadersDataEncodeDecode(t *testing.T) { - // Create a "random" hash for testing - var hash common.Hash - for i := range hash { - hash[i] = byte(i) - } - // Assemble some table driven tests - tests := []struct { - packet *GetBlockHeadersRequest - fail bool - }{ - // Providing the origin as either a hash or a number should both work - {fail: false, packet: &GetBlockHeadersRequest{Origin: HashOrNumber{Number: 314}}}, - {fail: false, packet: &GetBlockHeadersRequest{Origin: HashOrNumber{Hash: hash}}}, - - // Providing arbitrary query field should also work - {fail: false, packet: &GetBlockHeadersRequest{Origin: HashOrNumber{Number: 314}, Amount: 314, Skip: 1, Reverse: true}}, - {fail: false, packet: &GetBlockHeadersRequest{Origin: HashOrNumber{Hash: hash}, Amount: 314, Skip: 1, Reverse: true}}, - - // Providing both the origin hash and origin number must fail - {fail: true, packet: &GetBlockHeadersRequest{Origin: HashOrNumber{Hash: hash, Number: 314}}}, - } - // Iterate over each of the tests and try to encode and then decode - for i, tt := range tests { - bytes, err := rlp.EncodeToBytes(tt.packet) - if err != nil && !tt.fail { - t.Fatalf("test %d: failed to encode packet: %v", i, err) - } else if err == nil && tt.fail { - t.Fatalf("test %d: encode should have failed", i) - } - if !tt.fail { - packet := new(GetBlockHeadersRequest) - if err := rlp.DecodeBytes(bytes, packet); err != nil { - t.Fatalf("test %d: failed to decode packet: %v", i, err) - } - if packet.Origin.Hash != tt.packet.Origin.Hash || packet.Origin.Number != tt.packet.Origin.Number || packet.Amount != tt.packet.Amount || - packet.Skip != tt.packet.Skip || packet.Reverse != tt.packet.Reverse { - t.Fatalf("test %d: encode decode mismatch: have %+v, want %+v", i, packet, tt.packet) - } - } - } -} - -// TestEmptyMessages tests encoding of empty messages. -func TestEmptyMessages(t *testing.T) { - // All empty messages encodes to the same format - want := common.FromHex("c4820457c0") - - for i, msg := range []interface{}{ - // Headers - GetBlockHeadersPacket{1111, nil}, - BlockHeadersPacket{1111, nil}, - // Bodies - GetBlockBodiesPacket{1111, nil}, - BlockBodiesPacket{1111, nil}, - BlockBodiesRLPPacket{1111, nil}, - // Receipts - GetReceiptsPacket{1111, nil}, - ReceiptsPacket{1111, nil}, - // Transactions - GetPooledTransactionsPacket{1111, nil}, - PooledTransactionsPacket{1111, nil}, - PooledTransactionsRLPPacket{1111, nil}, - - // Headers - BlockHeadersPacket{1111, BlockHeadersRequest([]*types.Header{})}, - // Bodies - GetBlockBodiesPacket{1111, GetBlockBodiesRequest([]common.Hash{})}, - BlockBodiesPacket{1111, BlockBodiesResponse([]*BlockBody{})}, - BlockBodiesRLPPacket{1111, BlockBodiesRLPResponse([]rlp.RawValue{})}, - // Receipts - GetReceiptsPacket{1111, GetReceiptsRequest([]common.Hash{})}, - ReceiptsPacket{1111, ReceiptsResponse([][]*types.Receipt{})}, - // Transactions - GetPooledTransactionsPacket{1111, GetPooledTransactionsRequest([]common.Hash{})}, - PooledTransactionsPacket{1111, PooledTransactionsResponse([]*types.Transaction{})}, - PooledTransactionsRLPPacket{1111, PooledTransactionsRLPResponse([]rlp.RawValue{})}, - } { - if have, _ := rlp.EncodeToBytes(msg); !bytes.Equal(have, want) { - t.Errorf("test %d, type %T, have\n\t%x\nwant\n\t%x", i, msg, have, want) - } - } -} - -// TestMessages tests the encoding of all messages. -func TestMessages(t *testing.T) { - // Some basic structs used during testing - var ( - header *types.Header - blockBody *BlockBody - blockBodyRlp rlp.RawValue - txs []*types.Transaction - txRlps []rlp.RawValue - hashes []common.Hash - receipts []*types.Receipt - receiptsRlp rlp.RawValue - - err error - ) - header = &types.Header{ - Difficulty: big.NewInt(2222), - Number: big.NewInt(3333), - GasLimit: 4444, - GasUsed: 5555, - Time: 6666, - Extra: []byte{0x77, 0x88}, - } - // Init the transactions, taken from a different test - { - for _, hexrlp := range []string{ - "f867088504a817c8088302e2489435353535353535353535353535353535353535358202008025a064b1702d9298fee62dfeccc57d322a463ad55ca201256d01f62b45b2e1c21c12a064b1702d9298fee62dfeccc57d322a463ad55ca201256d01f62b45b2e1c21c10", - "f867098504a817c809830334509435353535353535353535353535353535353535358202d98025a052f8f61201b2b11a78d6e866abc9c3db2ae8631fa656bfe5cb53668255367afba052f8f61201b2b11a78d6e866abc9c3db2ae8631fa656bfe5cb53668255367afb", - } { - var tx *types.Transaction - rlpdata := common.FromHex(hexrlp) - if err := rlp.DecodeBytes(rlpdata, &tx); err != nil { - t.Fatal(err) - } - txs = append(txs, tx) - txRlps = append(txRlps, rlpdata) - } - } - // init the block body data, both object and rlp form - blockBody = &BlockBody{ - Transactions: txs, - Uncles: []*types.Header{header}, - } - blockBodyRlp, err = rlp.EncodeToBytes(blockBody) - if err != nil { - t.Fatal(err) - } - - hashes = []common.Hash{ - common.HexToHash("deadc0de"), - common.HexToHash("feedbeef"), - } - // init the receipts - { - receipts = []*types.Receipt{ - { - Status: types.ReceiptStatusFailed, - CumulativeGasUsed: 1, - Logs: []*types.Log{ - { - Address: common.BytesToAddress([]byte{0x11}), - Topics: []common.Hash{common.HexToHash("dead"), common.HexToHash("beef")}, - Data: []byte{0x01, 0x00, 0xff}, - }, - }, - TxHash: hashes[0], - ContractAddress: common.BytesToAddress([]byte{0x01, 0x11, 0x11}), - GasUsed: 111111, - }, - } - rlpData, err := rlp.EncodeToBytes(receipts) - if err != nil { - t.Fatal(err) - } - receiptsRlp = rlpData - } - - for i, tc := range []struct { - message interface{} - want []byte - }{ - { - GetBlockHeadersPacket{1111, &GetBlockHeadersRequest{HashOrNumber{hashes[0], 0}, 5, 5, false}}, - common.FromHex("e8820457e4a000000000000000000000000000000000000000000000000000000000deadc0de050580"), - }, - { - GetBlockHeadersPacket{1111, &GetBlockHeadersRequest{HashOrNumber{common.Hash{}, 9999}, 5, 5, false}}, - common.FromHex("ca820457c682270f050580"), - }, - { - BlockHeadersPacket{1111, BlockHeadersRequest{header}}, - common.FromHex("f90202820457f901fcf901f9a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008208ae820d0582115c8215b3821a0a827788a00000000000000000000000000000000000000000000000000000000000000000880000000000000000"), - }, - { - GetBlockBodiesPacket{1111, GetBlockBodiesRequest(hashes)}, - common.FromHex("f847820457f842a000000000000000000000000000000000000000000000000000000000deadc0dea000000000000000000000000000000000000000000000000000000000feedbeef"), - }, - { - BlockBodiesPacket{1111, BlockBodiesResponse([]*BlockBody{blockBody})}, - common.FromHex("f902dc820457f902d6f902d3f8d2f867088504a817c8088302e2489435353535353535353535353535353535353535358202008025a064b1702d9298fee62dfeccc57d322a463ad55ca201256d01f62b45b2e1c21c12a064b1702d9298fee62dfeccc57d322a463ad55ca201256d01f62b45b2e1c21c10f867098504a817c809830334509435353535353535353535353535353535353535358202d98025a052f8f61201b2b11a78d6e866abc9c3db2ae8631fa656bfe5cb53668255367afba052f8f61201b2b11a78d6e866abc9c3db2ae8631fa656bfe5cb53668255367afbf901fcf901f9a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008208ae820d0582115c8215b3821a0a827788a00000000000000000000000000000000000000000000000000000000000000000880000000000000000"), - }, - { // Identical to non-rlp-shortcut version - BlockBodiesRLPPacket{1111, BlockBodiesRLPResponse([]rlp.RawValue{blockBodyRlp})}, - common.FromHex("f902dc820457f902d6f902d3f8d2f867088504a817c8088302e2489435353535353535353535353535353535353535358202008025a064b1702d9298fee62dfeccc57d322a463ad55ca201256d01f62b45b2e1c21c12a064b1702d9298fee62dfeccc57d322a463ad55ca201256d01f62b45b2e1c21c10f867098504a817c809830334509435353535353535353535353535353535353535358202d98025a052f8f61201b2b11a78d6e866abc9c3db2ae8631fa656bfe5cb53668255367afba052f8f61201b2b11a78d6e866abc9c3db2ae8631fa656bfe5cb53668255367afbf901fcf901f9a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008208ae820d0582115c8215b3821a0a827788a00000000000000000000000000000000000000000000000000000000000000000880000000000000000"), - }, - { - GetReceiptsPacket{1111, GetReceiptsRequest(hashes)}, - common.FromHex("f847820457f842a000000000000000000000000000000000000000000000000000000000deadc0dea000000000000000000000000000000000000000000000000000000000feedbeef"), - }, - { - ReceiptsPacket{1111, ReceiptsResponse([][]*types.Receipt{receipts})}, - common.FromHex("f90172820457f9016cf90169f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff"), - }, - { - ReceiptsRLPPacket{1111, ReceiptsRLPResponse([]rlp.RawValue{receiptsRlp})}, - common.FromHex("f90172820457f9016cf90169f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff"), - }, - { - GetPooledTransactionsPacket{1111, GetPooledTransactionsRequest(hashes)}, - common.FromHex("f847820457f842a000000000000000000000000000000000000000000000000000000000deadc0dea000000000000000000000000000000000000000000000000000000000feedbeef"), - }, - { - PooledTransactionsPacket{1111, PooledTransactionsResponse(txs)}, - common.FromHex("f8d7820457f8d2f867088504a817c8088302e2489435353535353535353535353535353535353535358202008025a064b1702d9298fee62dfeccc57d322a463ad55ca201256d01f62b45b2e1c21c12a064b1702d9298fee62dfeccc57d322a463ad55ca201256d01f62b45b2e1c21c10f867098504a817c809830334509435353535353535353535353535353535353535358202d98025a052f8f61201b2b11a78d6e866abc9c3db2ae8631fa656bfe5cb53668255367afba052f8f61201b2b11a78d6e866abc9c3db2ae8631fa656bfe5cb53668255367afb"), - }, - { - PooledTransactionsRLPPacket{1111, PooledTransactionsRLPResponse(txRlps)}, - common.FromHex("f8d7820457f8d2f867088504a817c8088302e2489435353535353535353535353535353535353535358202008025a064b1702d9298fee62dfeccc57d322a463ad55ca201256d01f62b45b2e1c21c12a064b1702d9298fee62dfeccc57d322a463ad55ca201256d01f62b45b2e1c21c10f867098504a817c809830334509435353535353535353535353535353535353535358202d98025a052f8f61201b2b11a78d6e866abc9c3db2ae8631fa656bfe5cb53668255367afba052f8f61201b2b11a78d6e866abc9c3db2ae8631fa656bfe5cb53668255367afb"), - }, - } { - if have, _ := rlp.EncodeToBytes(tc.message); !bytes.Equal(have, tc.want) { - t.Errorf("test %d, type %T, have\n\t%x\nwant\n\t%x", i, tc.message, have, tc.want) - } - } -} diff --git a/eth/protocols/eth/tracker.go b/eth/protocols/eth/tracker.go deleted file mode 100644 index 324fd22839..0000000000 --- a/eth/protocols/eth/tracker.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2021 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 eth - -import ( - "time" - - "github.com/ethereum/go-ethereum/p2p/tracker" -) - -// requestTracker is a singleton tracker for eth/66 and newer request times. -var requestTracker = tracker.New(ProtocolName, 5*time.Minute) diff --git a/eth/protocols/snap/discovery.go b/eth/protocols/snap/discovery.go deleted file mode 100644 index 684ec7e632..0000000000 --- a/eth/protocols/snap/discovery.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2020 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 snap - -import ( - "github.com/ethereum/go-ethereum/rlp" -) - -// enrEntry is the ENR entry which advertises `snap` protocol on the discovery. -type enrEntry struct { - // Ignore additional fields (for forward compatibility). - Rest []rlp.RawValue `rlp:"tail"` -} - -// ENRKey implements enr.Entry. -func (e enrEntry) ENRKey() string { - return "snap" -} diff --git a/eth/protocols/snap/handler.go b/eth/protocols/snap/handler.go deleted file mode 100644 index bd7ce9e715..0000000000 --- a/eth/protocols/snap/handler.go +++ /dev/null @@ -1,577 +0,0 @@ -// Copyright 2020 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 snap - -import ( - "bytes" - "fmt" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/enr" - "github.com/ethereum/go-ethereum/trie" - "github.com/ethereum/go-ethereum/trie/trienode" -) - -const ( - // softResponseLimit is the target maximum size of replies to data retrievals. - softResponseLimit = 2 * 1024 * 1024 - - // maxCodeLookups is the maximum number of bytecodes to serve. This number is - // there to limit the number of disk lookups. - maxCodeLookups = 1024 - - // stateLookupSlack defines the ratio by how much a state response can exceed - // the requested limit in order to try and avoid breaking up contracts into - // multiple packages and proving them. - stateLookupSlack = 0.1 - - // maxTrieNodeLookups is the maximum number of state trie nodes to serve. This - // number is there to limit the number of disk lookups. - maxTrieNodeLookups = 1024 - - // maxTrieNodeTimeSpent is the maximum time we should spend on looking up trie nodes. - // If we spend too much time, then it's a fairly high chance of timing out - // at the remote side, which means all the work is in vain. - maxTrieNodeTimeSpent = 5 * time.Second -) - -// Handler is a callback to invoke from an outside runner after the boilerplate -// exchanges have passed. -type Handler func(peer *Peer) error - -// Backend defines the data retrieval methods to serve remote requests and the -// callback methods to invoke on remote deliveries. -type Backend interface { - // Chain retrieves the blockchain object to serve data. - Chain() *core.BlockChain - - // RunPeer is invoked when a peer joins on the `eth` protocol. The handler - // should do any peer maintenance work, handshakes and validations. If all - // is passed, control should be given back to the `handler` to process the - // inbound messages going forward. - RunPeer(peer *Peer, handler Handler) error - - // PeerInfo retrieves all known `snap` information about a peer. - PeerInfo(id enode.ID) interface{} - - // Handle is a callback to be invoked when a data packet is received from - // the remote peer. Only packets not consumed by the protocol handler will - // be forwarded to the backend. - Handle(peer *Peer, packet Packet) error -} - -// MakeProtocols constructs the P2P protocol definitions for `snap`. -func MakeProtocols(backend Backend, dnsdisc enode.Iterator) []p2p.Protocol { - // Filter the discovery iterator for nodes advertising snap support. - dnsdisc = enode.Filter(dnsdisc, func(n *enode.Node) bool { - var snap enrEntry - return n.Load(&snap) == nil - }) - - protocols := make([]p2p.Protocol, len(ProtocolVersions)) - for i, version := range ProtocolVersions { - version := version // Closure - - protocols[i] = p2p.Protocol{ - Name: ProtocolName, - Version: version, - Length: protocolLengths[version], - Run: func(p *p2p.Peer, rw p2p.MsgReadWriter) error { - return backend.RunPeer(NewPeer(version, p, rw), func(peer *Peer) error { - return Handle(backend, peer) - }) - }, - NodeInfo: func() interface{} { - return nodeInfo(backend.Chain()) - }, - PeerInfo: func(id enode.ID) interface{} { - return backend.PeerInfo(id) - }, - Attributes: []enr.Entry{&enrEntry{}}, - DialCandidates: dnsdisc, - } - } - return protocols -} - -// Handle is the callback invoked to manage the life cycle of a `snap` peer. -// When this function terminates, the peer is disconnected. -func Handle(backend Backend, peer *Peer) error { - for { - if err := HandleMessage(backend, peer); err != nil { - peer.Log().Debug("Message handling failed in `snap`", "err", err) - return err - } - } -} - -// HandleMessage is invoked whenever an inbound message is received from a -// remote peer on the `snap` protocol. The remote connection is torn down upon -// returning any error. -func HandleMessage(backend Backend, peer *Peer) error { - // Read the next message from the remote peer, and ensure it's fully consumed - msg, err := peer.rw.ReadMsg() - if err != nil { - return err - } - if msg.Size > maxMessageSize { - return fmt.Errorf("%w: %v > %v", errMsgTooLarge, msg.Size, maxMessageSize) - } - defer msg.Discard() - start := time.Now() - // Track the amount of time it takes to serve the request and run the handler - if metrics.Enabled { - h := fmt.Sprintf("%s/%s/%d/%#02x", p2p.HandleHistName, ProtocolName, peer.Version(), msg.Code) - defer func(start time.Time) { - sampler := func() metrics.Sample { - return metrics.ResettingSample( - metrics.NewExpDecaySample(1028, 0.015), - ) - } - metrics.GetOrRegisterHistogramLazy(h, nil, sampler).Update(time.Since(start).Microseconds()) - }(start) - } - // Handle the message depending on its contents - switch { - case msg.Code == GetAccountRangeMsg: - // Decode the account retrieval request - var req GetAccountRangePacket - if err := msg.Decode(&req); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - // Service the request, potentially returning nothing in case of errors - accounts, proofs := ServiceGetAccountRangeQuery(backend.Chain(), &req) - - // Send back anything accumulated (or empty in case of errors) - return p2p.Send(peer.rw, AccountRangeMsg, &AccountRangePacket{ - ID: req.ID, - Accounts: accounts, - Proof: proofs, - }) - - case msg.Code == AccountRangeMsg: - // A range of accounts arrived to one of our previous requests - res := new(AccountRangePacket) - if err := msg.Decode(res); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - // Ensure the range is monotonically increasing - for i := 1; i < len(res.Accounts); i++ { - if bytes.Compare(res.Accounts[i-1].Hash[:], res.Accounts[i].Hash[:]) >= 0 { - return fmt.Errorf("accounts not monotonically increasing: #%d [%x] vs #%d [%x]", i-1, res.Accounts[i-1].Hash[:], i, res.Accounts[i].Hash[:]) - } - } - requestTracker.Fulfil(peer.id, peer.version, AccountRangeMsg, res.ID) - - return backend.Handle(peer, res) - - case msg.Code == GetStorageRangesMsg: - // Decode the storage retrieval request - var req GetStorageRangesPacket - if err := msg.Decode(&req); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - // Service the request, potentially returning nothing in case of errors - slots, proofs := ServiceGetStorageRangesQuery(backend.Chain(), &req) - - // Send back anything accumulated (or empty in case of errors) - return p2p.Send(peer.rw, StorageRangesMsg, &StorageRangesPacket{ - ID: req.ID, - Slots: slots, - Proof: proofs, - }) - - case msg.Code == StorageRangesMsg: - // A range of storage slots arrived to one of our previous requests - res := new(StorageRangesPacket) - if err := msg.Decode(res); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - // Ensure the ranges are monotonically increasing - for i, slots := range res.Slots { - for j := 1; j < len(slots); j++ { - if bytes.Compare(slots[j-1].Hash[:], slots[j].Hash[:]) >= 0 { - return fmt.Errorf("storage slots not monotonically increasing for account #%d: #%d [%x] vs #%d [%x]", i, j-1, slots[j-1].Hash[:], j, slots[j].Hash[:]) - } - } - } - requestTracker.Fulfil(peer.id, peer.version, StorageRangesMsg, res.ID) - - return backend.Handle(peer, res) - - case msg.Code == GetByteCodesMsg: - // Decode bytecode retrieval request - var req GetByteCodesPacket - if err := msg.Decode(&req); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - // Service the request, potentially returning nothing in case of errors - codes := ServiceGetByteCodesQuery(backend.Chain(), &req) - - // Send back anything accumulated (or empty in case of errors) - return p2p.Send(peer.rw, ByteCodesMsg, &ByteCodesPacket{ - ID: req.ID, - Codes: codes, - }) - - case msg.Code == ByteCodesMsg: - // A batch of byte codes arrived to one of our previous requests - res := new(ByteCodesPacket) - if err := msg.Decode(res); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - requestTracker.Fulfil(peer.id, peer.version, ByteCodesMsg, res.ID) - - return backend.Handle(peer, res) - - case msg.Code == GetTrieNodesMsg: - // Decode trie node retrieval request - var req GetTrieNodesPacket - if err := msg.Decode(&req); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - // Service the request, potentially returning nothing in case of errors - nodes, err := ServiceGetTrieNodesQuery(backend.Chain(), &req, start) - if err != nil { - return err - } - // Send back anything accumulated (or empty in case of errors) - return p2p.Send(peer.rw, TrieNodesMsg, &TrieNodesPacket{ - ID: req.ID, - Nodes: nodes, - }) - - case msg.Code == TrieNodesMsg: - // A batch of trie nodes arrived to one of our previous requests - res := new(TrieNodesPacket) - if err := msg.Decode(res); err != nil { - return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) - } - requestTracker.Fulfil(peer.id, peer.version, TrieNodesMsg, res.ID) - - return backend.Handle(peer, res) - - default: - return fmt.Errorf("%w: %v", errInvalidMsgCode, msg.Code) - } -} - -// ServiceGetAccountRangeQuery assembles the response to an account range query. -// It is exposed to allow external packages to test protocol behavior. -func ServiceGetAccountRangeQuery(chain *core.BlockChain, req *GetAccountRangePacket) ([]*AccountData, [][]byte) { - if req.Bytes > softResponseLimit { - req.Bytes = softResponseLimit - } - // Retrieve the requested state and bail out if non existent - tr, err := trie.New(trie.StateTrieID(req.Root), chain.TrieDB()) - if err != nil { - return nil, nil - } - it, err := chain.Snapshots().AccountIterator(req.Root, req.Origin) - if err != nil { - return nil, nil - } - // Iterate over the requested range and pile accounts up - var ( - accounts []*AccountData - size uint64 - last common.Hash - ) - for it.Next() { - hash, account := it.Hash(), common.CopyBytes(it.Account()) - - // Track the returned interval for the Merkle proofs - last = hash - - // Assemble the reply item - size += uint64(common.HashLength + len(account)) - accounts = append(accounts, &AccountData{ - Hash: hash, - Body: account, - }) - // If we've exceeded the request threshold, abort - if bytes.Compare(hash[:], req.Limit[:]) >= 0 { - break - } - if size > req.Bytes { - break - } - } - it.Release() - - // Generate the Merkle proofs for the first and last account - proof := trienode.NewProofSet() - if err := tr.Prove(req.Origin[:], proof); err != nil { - log.Warn("Failed to prove account range", "origin", req.Origin, "err", err) - return nil, nil - } - if last != (common.Hash{}) { - if err := tr.Prove(last[:], proof); err != nil { - log.Warn("Failed to prove account range", "last", last, "err", err) - return nil, nil - } - } - var proofs [][]byte - for _, blob := range proof.List() { - proofs = append(proofs, blob) - } - return accounts, proofs -} - -func ServiceGetStorageRangesQuery(chain *core.BlockChain, req *GetStorageRangesPacket) ([][]*StorageData, [][]byte) { - if req.Bytes > softResponseLimit { - req.Bytes = softResponseLimit - } - // TODO(karalabe): Do we want to enforce > 0 accounts and 1 account if origin is set? - // TODO(karalabe): - Logging locally is not ideal as remote faults annoy the local user - // TODO(karalabe): - Dropping the remote peer is less flexible wrt client bugs (slow is better than non-functional) - - // Calculate the hard limit at which to abort, even if mid storage trie - hardLimit := uint64(float64(req.Bytes) * (1 + stateLookupSlack)) - - // Retrieve storage ranges until the packet limit is reached - var ( - slots [][]*StorageData - proofs [][]byte - size uint64 - ) - for _, account := range req.Accounts { - // If we've exceeded the requested data limit, abort without opening - // a new storage range (that we'd need to prove due to exceeded size) - if size >= req.Bytes { - break - } - // The first account might start from a different origin and end sooner - var origin common.Hash - if len(req.Origin) > 0 { - origin, req.Origin = common.BytesToHash(req.Origin), nil - } - var limit = common.MaxHash - if len(req.Limit) > 0 { - limit, req.Limit = common.BytesToHash(req.Limit), nil - } - // Retrieve the requested state and bail out if non existent - it, err := chain.Snapshots().StorageIterator(req.Root, account, origin) - if err != nil { - return nil, nil - } - // Iterate over the requested range and pile slots up - var ( - storage []*StorageData - last common.Hash - abort bool - ) - for it.Next() { - if size >= hardLimit { - abort = true - break - } - hash, slot := it.Hash(), common.CopyBytes(it.Slot()) - - // Track the returned interval for the Merkle proofs - last = hash - - // Assemble the reply item - size += uint64(common.HashLength + len(slot)) - storage = append(storage, &StorageData{ - Hash: hash, - Body: slot, - }) - // If we've exceeded the request threshold, abort - if bytes.Compare(hash[:], limit[:]) >= 0 { - break - } - } - if len(storage) > 0 { - slots = append(slots, storage) - } - it.Release() - - // Generate the Merkle proofs for the first and last storage slot, but - // only if the response was capped. If the entire storage trie included - // in the response, no need for any proofs. - if origin != (common.Hash{}) || (abort && len(storage) > 0) { - // Request started at a non-zero hash or was capped prematurely, add - // the endpoint Merkle proofs - accTrie, err := trie.NewStateTrie(trie.StateTrieID(req.Root), chain.TrieDB()) - if err != nil { - return nil, nil - } - acc, err := accTrie.GetAccountByHash(account) - if err != nil || acc == nil { - return nil, nil - } - id := trie.StorageTrieID(req.Root, account, acc.Root) - stTrie, err := trie.NewStateTrie(id, chain.TrieDB()) - if err != nil { - return nil, nil - } - proof := trienode.NewProofSet() - if err := stTrie.Prove(origin[:], proof); err != nil { - log.Warn("Failed to prove storage range", "origin", req.Origin, "err", err) - return nil, nil - } - if last != (common.Hash{}) { - if err := stTrie.Prove(last[:], proof); err != nil { - log.Warn("Failed to prove storage range", "last", last, "err", err) - return nil, nil - } - } - for _, blob := range proof.List() { - proofs = append(proofs, blob) - } - // Proof terminates the reply as proofs are only added if a node - // refuses to serve more data (exception when a contract fetch is - // finishing, but that's that). - break - } - } - return slots, proofs -} - -// ServiceGetByteCodesQuery assembles the response to a byte codes query. -// It is exposed to allow external packages to test protocol behavior. -func ServiceGetByteCodesQuery(chain *core.BlockChain, req *GetByteCodesPacket) [][]byte { - if req.Bytes > softResponseLimit { - req.Bytes = softResponseLimit - } - if len(req.Hashes) > maxCodeLookups { - req.Hashes = req.Hashes[:maxCodeLookups] - } - // Retrieve bytecodes until the packet size limit is reached - var ( - codes [][]byte - bytes uint64 - ) - for _, hash := range req.Hashes { - if hash == types.EmptyCodeHash { - // Peers should not request the empty code, but if they do, at - // least sent them back a correct response without db lookups - codes = append(codes, []byte{}) - } else if blob, err := chain.ContractCodeWithPrefix(hash); err == nil { - codes = append(codes, blob) - bytes += uint64(len(blob)) - } - if bytes > req.Bytes { - break - } - } - return codes -} - -// ServiceGetTrieNodesQuery assembles the response to a trie nodes query. -// It is exposed to allow external packages to test protocol behavior. -func ServiceGetTrieNodesQuery(chain *core.BlockChain, req *GetTrieNodesPacket, start time.Time) ([][]byte, error) { - if req.Bytes > softResponseLimit { - req.Bytes = softResponseLimit - } - // Make sure we have the state associated with the request - triedb := chain.TrieDB() - - accTrie, err := trie.NewStateTrie(trie.StateTrieID(req.Root), triedb) - if err != nil { - // We don't have the requested state available, bail out - return nil, nil - } - // The 'snap' might be nil, in which case we cannot serve storage slots. - snap := chain.Snapshots().Snapshot(req.Root) - // Retrieve trie nodes until the packet size limit is reached - var ( - nodes [][]byte - bytes uint64 - loads int // Trie hash expansions to count database reads - ) - for _, pathset := range req.Paths { - switch len(pathset) { - case 0: - // Ensure we penalize invalid requests - return nil, fmt.Errorf("%w: zero-item pathset requested", errBadRequest) - - case 1: - // If we're only retrieving an account trie node, fetch it directly - blob, resolved, err := accTrie.GetNode(pathset[0]) - loads += resolved // always account database reads, even for failures - if err != nil { - break - } - nodes = append(nodes, blob) - bytes += uint64(len(blob)) - - default: - var stRoot common.Hash - // Storage slots requested, open the storage trie and retrieve from there - if snap == nil { - // We don't have the requested state snapshotted yet (or it is stale), - // but can look up the account via the trie instead. - account, err := accTrie.GetAccountByHash(common.BytesToHash(pathset[0])) - loads += 8 // We don't know the exact cost of lookup, this is an estimate - if err != nil || account == nil { - break - } - stRoot = account.Root - } else { - account, err := snap.Account(common.BytesToHash(pathset[0])) - loads++ // always account database reads, even for failures - if err != nil || account == nil { - break - } - stRoot = common.BytesToHash(account.Root) - } - id := trie.StorageTrieID(req.Root, common.BytesToHash(pathset[0]), stRoot) - stTrie, err := trie.NewStateTrie(id, triedb) - loads++ // always account database reads, even for failures - if err != nil { - break - } - for _, path := range pathset[1:] { - blob, resolved, err := stTrie.GetNode(path) - loads += resolved // always account database reads, even for failures - if err != nil { - break - } - nodes = append(nodes, blob) - bytes += uint64(len(blob)) - - // Sanity check limits to avoid DoS on the store trie loads - if bytes > req.Bytes || loads > maxTrieNodeLookups || time.Since(start) > maxTrieNodeTimeSpent { - break - } - } - } - // Abort request processing if we've exceeded our limits - if bytes > req.Bytes || loads > maxTrieNodeLookups || time.Since(start) > maxTrieNodeTimeSpent { - break - } - } - return nodes, nil -} - -// NodeInfo represents a short summary of the `snap` sub-protocol metadata -// known about the host peer. -type NodeInfo struct{} - -// nodeInfo retrieves some `snap` protocol metadata about the running host node. -func nodeInfo(chain *core.BlockChain) *NodeInfo { - return &NodeInfo{} -} diff --git a/eth/protocols/snap/handler_fuzzing_test.go b/eth/protocols/snap/handler_fuzzing_test.go deleted file mode 100644 index daed7ed44a..0000000000 --- a/eth/protocols/snap/handler_fuzzing_test.go +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright 2021 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 snap - -import ( - "bytes" - "encoding/binary" - "fmt" - "math/big" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/consensus/ethash" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rlp" - fuzz "github.com/google/gofuzz" -) - -func FuzzARange(f *testing.F) { - f.Fuzz(func(t *testing.T, data []byte) { - doFuzz(data, &GetAccountRangePacket{}, GetAccountRangeMsg) - }) -} - -func FuzzSRange(f *testing.F) { - f.Fuzz(func(t *testing.T, data []byte) { - doFuzz(data, &GetStorageRangesPacket{}, GetStorageRangesMsg) - }) -} - -func FuzzByteCodes(f *testing.F) { - f.Fuzz(func(t *testing.T, data []byte) { - doFuzz(data, &GetByteCodesPacket{}, GetByteCodesMsg) - }) -} - -func FuzzTrieNodes(f *testing.F) { - f.Fuzz(func(t *testing.T, data []byte) { - doFuzz(data, &GetTrieNodesPacket{}, GetTrieNodesMsg) - }) -} - -func doFuzz(input []byte, obj interface{}, code int) { - bc := getChain() - defer bc.Stop() - fuzz.NewFromGoFuzz(input).Fuzz(obj) - var data []byte - switch p := obj.(type) { - case *GetTrieNodesPacket: - p.Root = trieRoot - data, _ = rlp.EncodeToBytes(obj) - default: - data, _ = rlp.EncodeToBytes(obj) - } - cli := &dummyRW{ - code: uint64(code), - data: data, - } - peer := NewFakePeer(65, "gazonk01", cli) - err := HandleMessage(&dummyBackend{bc}, peer) - switch { - case err == nil && cli.writeCount != 1: - panic(fmt.Sprintf("Expected 1 response, got %d", cli.writeCount)) - case err != nil && cli.writeCount != 0: - panic(fmt.Sprintf("Expected 0 response, got %d", cli.writeCount)) - } -} - -var trieRoot common.Hash - -func getChain() *core.BlockChain { - ga := make(core.GenesisAlloc, 1000) - var a = make([]byte, 20) - var mkStorage = func(k, v int) (common.Hash, common.Hash) { - var kB = make([]byte, 32) - var vB = make([]byte, 32) - binary.LittleEndian.PutUint64(kB, uint64(k)) - binary.LittleEndian.PutUint64(vB, uint64(v)) - return common.BytesToHash(kB), common.BytesToHash(vB) - } - storage := make(map[common.Hash]common.Hash) - for i := 0; i < 10; i++ { - k, v := mkStorage(i, i) - storage[k] = v - } - for i := 0; i < 1000; i++ { - binary.LittleEndian.PutUint64(a, uint64(i+0xff)) - acc := core.GenesisAccount{Balance: big.NewInt(int64(i))} - if i%2 == 1 { - acc.Storage = storage - } - ga[common.BytesToAddress(a)] = acc - } - gspec := &core.Genesis{ - Config: params.TestChainConfig, - Alloc: ga, - } - _, blocks, _ := core.GenerateChainWithGenesis(gspec, ethash.NewFaker(), 2, func(i int, gen *core.BlockGen) {}) - cacheConf := &core.CacheConfig{ - TrieCleanLimit: 0, - TrieDirtyLimit: 0, - TrieTimeLimit: 5 * time.Minute, - TrieCleanNoPrefetch: true, - SnapshotLimit: 100, - SnapshotWait: true, - } - trieRoot = blocks[len(blocks)-1].Root() - bc, _ := core.NewBlockChain(rawdb.NewMemoryDatabase(), cacheConf, gspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil) - if _, err := bc.InsertChain(blocks); err != nil { - panic(err) - } - return bc -} - -type dummyBackend struct { - chain *core.BlockChain -} - -func (d *dummyBackend) Chain() *core.BlockChain { return d.chain } -func (d *dummyBackend) RunPeer(*Peer, Handler) error { return nil } -func (d *dummyBackend) PeerInfo(enode.ID) interface{} { return "Foo" } -func (d *dummyBackend) Handle(*Peer, Packet) error { return nil } - -type dummyRW struct { - code uint64 - data []byte - writeCount int -} - -func (d *dummyRW) ReadMsg() (p2p.Msg, error) { - return p2p.Msg{ - Code: d.code, - Payload: bytes.NewReader(d.data), - ReceivedAt: time.Now(), - Size: uint32(len(d.data)), - }, nil -} - -func (d *dummyRW) WriteMsg(msg p2p.Msg) error { - d.writeCount++ - return nil -} diff --git a/eth/protocols/snap/metrics.go b/eth/protocols/snap/metrics.go deleted file mode 100644 index a7d071953f..0000000000 --- a/eth/protocols/snap/metrics.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2023 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 snap - -import ( - metrics "github.com/ethereum/go-ethereum/metrics" -) - -var ( - ingressRegistrationErrorName = "eth/protocols/snap/ingress/registration/error" - egressRegistrationErrorName = "eth/protocols/snap/egress/registration/error" - - IngressRegistrationErrorMeter = metrics.NewRegisteredMeter(ingressRegistrationErrorName, nil) - EgressRegistrationErrorMeter = metrics.NewRegisteredMeter(egressRegistrationErrorName, nil) - - // deletionGauge is the metric to track how many trie node deletions - // are performed in total during the sync process. - deletionGauge = metrics.NewRegisteredGauge("eth/protocols/snap/sync/delete", nil) - - // lookupGauge is the metric to track how many trie node lookups are - // performed to determine if node needs to be deleted. - lookupGauge = metrics.NewRegisteredGauge("eth/protocols/snap/sync/lookup", nil) - - // boundaryAccountNodesGauge is the metric to track how many boundary trie - // nodes in account trie are met. - boundaryAccountNodesGauge = metrics.NewRegisteredGauge("eth/protocols/snap/sync/boundary/account", nil) - - // boundaryAccountNodesGauge is the metric to track how many boundary trie - // nodes in storage tries are met. - boundaryStorageNodesGauge = metrics.NewRegisteredGauge("eth/protocols/snap/sync/boundary/storage", nil) - - // smallStorageGauge is the metric to track how many storages are small enough - // to retrieved in one or two request. - smallStorageGauge = metrics.NewRegisteredGauge("eth/protocols/snap/sync/storage/small", nil) - - // largeStorageGauge is the metric to track how many storages are large enough - // to retrieved concurrently. - largeStorageGauge = metrics.NewRegisteredGauge("eth/protocols/snap/sync/storage/large", nil) - - // skipStorageHealingGauge is the metric to track how many storages are retrieved - // in multiple requests but healing is not necessary. - skipStorageHealingGauge = metrics.NewRegisteredGauge("eth/protocols/snap/sync/storage/noheal", nil) -) diff --git a/eth/protocols/snap/peer.go b/eth/protocols/snap/peer.go deleted file mode 100644 index 3db6e22cbd..0000000000 --- a/eth/protocols/snap/peer.go +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright 2020 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 snap - -import ( - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/p2p" -) - -// Peer is a collection of relevant information we have about a `snap` peer. -type Peer struct { - id string // Unique ID for the peer, cached - - *p2p.Peer // The embedded P2P package peer - rw p2p.MsgReadWriter // Input/output streams for snap - version uint // Protocol version negotiated - - logger log.Logger // Contextual logger with the peer id injected -} - -// NewPeer create a wrapper for a network connection and negotiated protocol -// version. -func NewPeer(version uint, p *p2p.Peer, rw p2p.MsgReadWriter) *Peer { - id := p.ID().String() - return &Peer{ - id: id, - Peer: p, - rw: rw, - version: version, - logger: log.New("peer", id[:8]), - } -} - -// NewFakePeer create a fake snap peer without a backing p2p peer, for testing purposes. -func NewFakePeer(version uint, id string, rw p2p.MsgReadWriter) *Peer { - return &Peer{ - id: id, - rw: rw, - version: version, - logger: log.New("peer", id[:8]), - } -} - -// ID retrieves the peer's unique identifier. -func (p *Peer) ID() string { - return p.id -} - -// Version retrieves the peer's negotiated `snap` protocol version. -func (p *Peer) Version() uint { - return p.version -} - -// Log overrides the P2P logger with the higher level one containing only the id. -func (p *Peer) Log() log.Logger { - return p.logger -} - -// RequestAccountRange fetches a batch of accounts rooted in a specific account -// trie, starting with the origin. -func (p *Peer) RequestAccountRange(id uint64, root common.Hash, origin, limit common.Hash, bytes uint64) error { - p.logger.Trace("Fetching range of accounts", "reqid", id, "root", root, "origin", origin, "limit", limit, "bytes", common.StorageSize(bytes)) - - requestTracker.Track(p.id, p.version, GetAccountRangeMsg, AccountRangeMsg, id) - return p2p.Send(p.rw, GetAccountRangeMsg, &GetAccountRangePacket{ - ID: id, - Root: root, - Origin: origin, - Limit: limit, - Bytes: bytes, - }) -} - -// RequestStorageRanges fetches a batch of storage slots belonging to one or more -// accounts. If slots from only one account is requested, an origin marker may also -// be used to retrieve from there. -func (p *Peer) RequestStorageRanges(id uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, bytes uint64) error { - if len(accounts) == 1 && origin != nil { - p.logger.Trace("Fetching range of large storage slots", "reqid", id, "root", root, "account", accounts[0], "origin", common.BytesToHash(origin), "limit", common.BytesToHash(limit), "bytes", common.StorageSize(bytes)) - } else { - p.logger.Trace("Fetching ranges of small storage slots", "reqid", id, "root", root, "accounts", len(accounts), "first", accounts[0], "bytes", common.StorageSize(bytes)) - } - requestTracker.Track(p.id, p.version, GetStorageRangesMsg, StorageRangesMsg, id) - return p2p.Send(p.rw, GetStorageRangesMsg, &GetStorageRangesPacket{ - ID: id, - Root: root, - Accounts: accounts, - Origin: origin, - Limit: limit, - Bytes: bytes, - }) -} - -// RequestByteCodes fetches a batch of bytecodes by hash. -func (p *Peer) RequestByteCodes(id uint64, hashes []common.Hash, bytes uint64) error { - p.logger.Trace("Fetching set of byte codes", "reqid", id, "hashes", len(hashes), "bytes", common.StorageSize(bytes)) - - requestTracker.Track(p.id, p.version, GetByteCodesMsg, ByteCodesMsg, id) - return p2p.Send(p.rw, GetByteCodesMsg, &GetByteCodesPacket{ - ID: id, - Hashes: hashes, - Bytes: bytes, - }) -} - -// RequestTrieNodes fetches a batch of account or storage trie nodes rooted in -// a specific state trie. -func (p *Peer) RequestTrieNodes(id uint64, root common.Hash, paths []TrieNodePathSet, bytes uint64) error { - p.logger.Trace("Fetching set of trie nodes", "reqid", id, "root", root, "pathsets", len(paths), "bytes", common.StorageSize(bytes)) - - requestTracker.Track(p.id, p.version, GetTrieNodesMsg, TrieNodesMsg, id) - return p2p.Send(p.rw, GetTrieNodesMsg, &GetTrieNodesPacket{ - ID: id, - Root: root, - Paths: paths, - Bytes: bytes, - }) -} diff --git a/eth/protocols/snap/protocol.go b/eth/protocols/snap/protocol.go deleted file mode 100644 index 0db206b081..0000000000 --- a/eth/protocols/snap/protocol.go +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright 2020 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 snap - -import ( - "errors" - "fmt" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rlp" -) - -// Constants to match up protocol versions and messages -const ( - SNAP1 = 1 -) - -// ProtocolName is the official short name of the `snap` protocol used during -// devp2p capability negotiation. -const ProtocolName = "snap" - -// ProtocolVersions are the supported versions of the `snap` protocol (first -// is primary). -var ProtocolVersions = []uint{SNAP1} - -// protocolLengths are the number of implemented message corresponding to -// different protocol versions. -var protocolLengths = map[uint]uint64{SNAP1: 8} - -// maxMessageSize is the maximum cap on the size of a protocol message. -const maxMessageSize = 10 * 1024 * 1024 - -const ( - GetAccountRangeMsg = 0x00 - AccountRangeMsg = 0x01 - GetStorageRangesMsg = 0x02 - StorageRangesMsg = 0x03 - GetByteCodesMsg = 0x04 - ByteCodesMsg = 0x05 - GetTrieNodesMsg = 0x06 - TrieNodesMsg = 0x07 -) - -var ( - errMsgTooLarge = errors.New("message too long") - errDecode = errors.New("invalid message") - errInvalidMsgCode = errors.New("invalid message code") - errBadRequest = errors.New("bad request") -) - -// Packet represents a p2p message in the `snap` protocol. -type Packet interface { - Name() string // Name returns a string corresponding to the message type. - Kind() byte // Kind returns the message type. -} - -// GetAccountRangePacket represents an account query. -type GetAccountRangePacket struct { - ID uint64 // Request ID to match up responses with - Root common.Hash // Root hash of the account trie to serve - Origin common.Hash // Hash of the first account to retrieve - Limit common.Hash // Hash of the last account to retrieve - Bytes uint64 // Soft limit at which to stop returning data -} - -// AccountRangePacket represents an account query response. -type AccountRangePacket struct { - ID uint64 // ID of the request this is a response for - Accounts []*AccountData // List of consecutive accounts from the trie - Proof [][]byte // List of trie nodes proving the account range -} - -// AccountData represents a single account in a query response. -type AccountData struct { - Hash common.Hash // Hash of the account - Body rlp.RawValue // Account body in slim format -} - -// Unpack retrieves the accounts from the range packet and converts from slim -// wire representation to consensus format. The returned data is RLP encoded -// since it's expected to be serialized to disk without further interpretation. -// -// Note, this method does a round of RLP decoding and reencoding, so only use it -// once and cache the results if need be. Ideally discard the packet afterwards -// to not double the memory use. -func (p *AccountRangePacket) Unpack() ([]common.Hash, [][]byte, error) { - var ( - hashes = make([]common.Hash, len(p.Accounts)) - accounts = make([][]byte, len(p.Accounts)) - ) - for i, acc := range p.Accounts { - val, err := types.FullAccountRLP(acc.Body) - if err != nil { - return nil, nil, fmt.Errorf("invalid account %x: %v", acc.Body, err) - } - hashes[i], accounts[i] = acc.Hash, val - } - return hashes, accounts, nil -} - -// GetStorageRangesPacket represents an storage slot query. -type GetStorageRangesPacket struct { - ID uint64 // Request ID to match up responses with - Root common.Hash // Root hash of the account trie to serve - Accounts []common.Hash // Account hashes of the storage tries to serve - Origin []byte // Hash of the first storage slot to retrieve (large contract mode) - Limit []byte // Hash of the last storage slot to retrieve (large contract mode) - Bytes uint64 // Soft limit at which to stop returning data -} - -// StorageRangesPacket represents a storage slot query response. -type StorageRangesPacket struct { - ID uint64 // ID of the request this is a response for - Slots [][]*StorageData // Lists of consecutive storage slots for the requested accounts - Proof [][]byte // Merkle proofs for the *last* slot range, if it's incomplete -} - -// StorageData represents a single storage slot in a query response. -type StorageData struct { - Hash common.Hash // Hash of the storage slot - Body []byte // Data content of the slot -} - -// Unpack retrieves the storage slots from the range packet and returns them in -// a split flat format that's more consistent with the internal data structures. -func (p *StorageRangesPacket) Unpack() ([][]common.Hash, [][][]byte) { - var ( - hashset = make([][]common.Hash, len(p.Slots)) - slotset = make([][][]byte, len(p.Slots)) - ) - for i, slots := range p.Slots { - hashset[i] = make([]common.Hash, len(slots)) - slotset[i] = make([][]byte, len(slots)) - for j, slot := range slots { - hashset[i][j] = slot.Hash - slotset[i][j] = slot.Body - } - } - return hashset, slotset -} - -// GetByteCodesPacket represents a contract bytecode query. -type GetByteCodesPacket struct { - ID uint64 // Request ID to match up responses with - Hashes []common.Hash // Code hashes to retrieve the code for - Bytes uint64 // Soft limit at which to stop returning data -} - -// ByteCodesPacket represents a contract bytecode query response. -type ByteCodesPacket struct { - ID uint64 // ID of the request this is a response for - Codes [][]byte // Requested contract bytecodes -} - -// GetTrieNodesPacket represents a state trie node query. -type GetTrieNodesPacket struct { - ID uint64 // Request ID to match up responses with - Root common.Hash // Root hash of the account trie to serve - Paths []TrieNodePathSet // Trie node hashes to retrieve the nodes for - Bytes uint64 // Soft limit at which to stop returning data -} - -// TrieNodePathSet is a list of trie node paths to retrieve. A naive way to -// represent trie nodes would be a simple list of `account || storage` path -// segments concatenated, but that would be very wasteful on the network. -// -// Instead, this array special cases the first element as the path in the -// account trie and the remaining elements as paths in the storage trie. To -// address an account node, the slice should have a length of 1 consisting -// of only the account path. There's no need to be able to address both an -// account node and a storage node in the same request as it cannot happen -// that a slot is accessed before the account path is fully expanded. -type TrieNodePathSet [][]byte - -// TrieNodesPacket represents a state trie node query response. -type TrieNodesPacket struct { - ID uint64 // ID of the request this is a response for - Nodes [][]byte // Requested state trie nodes -} - -func (*GetAccountRangePacket) Name() string { return "GetAccountRange" } -func (*GetAccountRangePacket) Kind() byte { return GetAccountRangeMsg } - -func (*AccountRangePacket) Name() string { return "AccountRange" } -func (*AccountRangePacket) Kind() byte { return AccountRangeMsg } - -func (*GetStorageRangesPacket) Name() string { return "GetStorageRanges" } -func (*GetStorageRangesPacket) Kind() byte { return GetStorageRangesMsg } - -func (*StorageRangesPacket) Name() string { return "StorageRanges" } -func (*StorageRangesPacket) Kind() byte { return StorageRangesMsg } - -func (*GetByteCodesPacket) Name() string { return "GetByteCodes" } -func (*GetByteCodesPacket) Kind() byte { return GetByteCodesMsg } - -func (*ByteCodesPacket) Name() string { return "ByteCodes" } -func (*ByteCodesPacket) Kind() byte { return ByteCodesMsg } - -func (*GetTrieNodesPacket) Name() string { return "GetTrieNodes" } -func (*GetTrieNodesPacket) Kind() byte { return GetTrieNodesMsg } - -func (*TrieNodesPacket) Name() string { return "TrieNodes" } -func (*TrieNodesPacket) Kind() byte { return TrieNodesMsg } diff --git a/eth/protocols/snap/range.go b/eth/protocols/snap/range.go deleted file mode 100644 index 8c98c71d50..0000000000 --- a/eth/protocols/snap/range.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2021 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 snap - -import ( - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/holiman/uint256" -) - -// hashRange is a utility to handle ranges of hashes, Split up the -// hash-space into sections, and 'walk' over the sections -type hashRange struct { - current *uint256.Int - step *uint256.Int -} - -// newHashRange creates a new hashRange, initiated at the start position, -// and with the step set to fill the desired 'num' chunks -func newHashRange(start common.Hash, num uint64) *hashRange { - left := new(big.Int).Sub(hashSpace, start.Big()) - step := new(big.Int).Div( - new(big.Int).Add(left, new(big.Int).SetUint64(num-1)), - new(big.Int).SetUint64(num), - ) - step256 := new(uint256.Int) - step256.SetFromBig(step) - - return &hashRange{ - current: new(uint256.Int).SetBytes32(start[:]), - step: step256, - } -} - -// Next pushes the hash range to the next interval. -func (r *hashRange) Next() bool { - next, overflow := new(uint256.Int).AddOverflow(r.current, r.step) - if overflow { - return false - } - r.current = next - return true -} - -// Start returns the first hash in the current interval. -func (r *hashRange) Start() common.Hash { - return r.current.Bytes32() -} - -// End returns the last hash in the current interval. -func (r *hashRange) End() common.Hash { - // If the end overflows (non divisible range), return a shorter interval - next, overflow := new(uint256.Int).AddOverflow(r.current, r.step) - if overflow { - return common.MaxHash - } - return next.SubUint64(next, 1).Bytes32() -} - -// incHash returns the next hash, in lexicographical order (a.k.a plus one) -func incHash(h common.Hash) common.Hash { - var a uint256.Int - a.SetBytes32(h[:]) - a.AddUint64(&a, 1) - return common.Hash(a.Bytes32()) -} diff --git a/eth/protocols/snap/range_test.go b/eth/protocols/snap/range_test.go deleted file mode 100644 index ea643f1361..0000000000 --- a/eth/protocols/snap/range_test.go +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright 2021 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 snap - -import ( - "testing" - - "github.com/ethereum/go-ethereum/common" -) - -// Tests that given a starting hash and a density, the hash ranger can correctly -// split up the remaining hash space into a fixed number of chunks. -func TestHashRanges(t *testing.T) { - tests := []struct { - head common.Hash - chunks uint64 - starts []common.Hash - ends []common.Hash - }{ - // Simple test case to split the entire hash range into 4 chunks - { - head: common.Hash{}, - chunks: 4, - starts: []common.Hash{ - {}, - common.HexToHash("0x4000000000000000000000000000000000000000000000000000000000000000"), - common.HexToHash("0x8000000000000000000000000000000000000000000000000000000000000000"), - common.HexToHash("0xc000000000000000000000000000000000000000000000000000000000000000"), - }, - ends: []common.Hash{ - common.HexToHash("0x3fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - common.HexToHash("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - common.HexToHash("0xbfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - common.MaxHash, - }, - }, - // Split a divisible part of the hash range up into 2 chunks - { - head: common.HexToHash("0x2000000000000000000000000000000000000000000000000000000000000000"), - chunks: 2, - starts: []common.Hash{ - {}, - common.HexToHash("0x9000000000000000000000000000000000000000000000000000000000000000"), - }, - ends: []common.Hash{ - common.HexToHash("0x8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - common.MaxHash, - }, - }, - // Split the entire hash range into a non divisible 3 chunks - { - head: common.Hash{}, - chunks: 3, - starts: []common.Hash{ - {}, - common.HexToHash("0x5555555555555555555555555555555555555555555555555555555555555556"), - common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac"), - }, - ends: []common.Hash{ - common.HexToHash("0x5555555555555555555555555555555555555555555555555555555555555555"), - common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab"), - common.MaxHash, - }, - }, - // Split a part of hash range into a non divisible 3 chunks - { - head: common.HexToHash("0x2000000000000000000000000000000000000000000000000000000000000000"), - chunks: 3, - starts: []common.Hash{ - {}, - common.HexToHash("0x6aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab"), - common.HexToHash("0xb555555555555555555555555555555555555555555555555555555555555556"), - }, - ends: []common.Hash{ - common.HexToHash("0x6aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), - common.HexToHash("0xb555555555555555555555555555555555555555555555555555555555555555"), - common.MaxHash, - }, - }, - // Split a part of hash range into a non divisible 3 chunks, but with a - // meaningful space size for manual verification. - // - The head being 0xff...f0, we have 14 hashes left in the space - // - Chunking up 14 into 3 pieces is 4.(6), but we need the ceil of 5 to avoid a micro-last-chunk - // - Since the range is not divisible, the last interval will be shorter, capped at 0xff...f - // - The chunk ranges thus needs to be [..0, ..5], [..6, ..b], [..c, ..f] - { - head: common.HexToHash("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0"), - chunks: 3, - starts: []common.Hash{ - {}, - common.HexToHash("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6"), - common.HexToHash("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc"), - }, - ends: []common.Hash{ - common.HexToHash("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5"), - common.HexToHash("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffb"), - common.MaxHash, - }, - }, - } - for i, tt := range tests { - r := newHashRange(tt.head, tt.chunks) - - var ( - starts = []common.Hash{{}} - ends = []common.Hash{r.End()} - ) - for r.Next() { - starts = append(starts, r.Start()) - ends = append(ends, r.End()) - } - if len(starts) != len(tt.starts) { - t.Errorf("test %d: starts count mismatch: have %d, want %d", i, len(starts), len(tt.starts)) - } - for j := 0; j < len(starts) && j < len(tt.starts); j++ { - if starts[j] != tt.starts[j] { - t.Errorf("test %d, start %d: hash mismatch: have %x, want %x", i, j, starts[j], tt.starts[j]) - } - } - if len(ends) != len(tt.ends) { - t.Errorf("test %d: ends count mismatch: have %d, want %d", i, len(ends), len(tt.ends)) - } - for j := 0; j < len(ends) && j < len(tt.ends); j++ { - if ends[j] != tt.ends[j] { - t.Errorf("test %d, end %d: hash mismatch: have %x, want %x", i, j, ends[j], tt.ends[j]) - } - } - } -} diff --git a/eth/protocols/snap/sort_test.go b/eth/protocols/snap/sort_test.go deleted file mode 100644 index be0a8c5706..0000000000 --- a/eth/protocols/snap/sort_test.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2022 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 snap - -import ( - "bytes" - "fmt" - "testing" - - "github.com/ethereum/go-ethereum/common" -) - -func hexToNibbles(s string) []byte { - if len(s) >= 2 && s[0] == '0' && s[1] == 'x' { - s = s[2:] - } - var s2 []byte - for _, ch := range []byte(s) { - s2 = append(s2, '0') - s2 = append(s2, ch) - } - return common.Hex2Bytes(string(s2)) -} - -func TestRequestSorting(t *testing.T) { - // - Path 0x9 -> {0x19} - // - Path 0x99 -> {0x0099} - // - Path 0x01234567890123456789012345678901012345678901234567890123456789019 -> {0x0123456789012345678901234567890101234567890123456789012345678901, 0x19} - // - Path 0x012345678901234567890123456789010123456789012345678901234567890199 -> {0x0123456789012345678901234567890101234567890123456789012345678901, 0x0099} - var f = func(path string) string { - data := hexToNibbles(path) - return string(data) - } - var ( - hashes []common.Hash - paths []string - ) - for _, x := range []string{ - "0x9", - "0x012345678901234567890123456789010123456789012345678901234567890195", - "0x012345678901234567890123456789010123456789012345678901234567890197", - "0x012345678901234567890123456789010123456789012345678901234567890196", - "0x99", - "0x012345678901234567890123456789010123456789012345678901234567890199", - "0x01234567890123456789012345678901012345678901234567890123456789019", - "0x0123456789012345678901234567890101234567890123456789012345678901", - "0x01234567890123456789012345678901012345678901234567890123456789010", - "0x01234567890123456789012345678901012345678901234567890123456789011", - } { - paths = append(paths, f(x)) - hashes = append(hashes, common.Hash{}) - } - _, _, syncPaths, pathsets := sortByAccountPath(paths, hashes) - { - var b = new(bytes.Buffer) - for i := 0; i < len(syncPaths); i++ { - fmt.Fprintf(b, "\n%d. paths %x", i, syncPaths[i]) - } - want := ` -0. paths [0099] -1. paths [0123456789012345678901234567890101234567890123456789012345678901 00] -2. paths [0123456789012345678901234567890101234567890123456789012345678901 0095] -3. paths [0123456789012345678901234567890101234567890123456789012345678901 0096] -4. paths [0123456789012345678901234567890101234567890123456789012345678901 0097] -5. paths [0123456789012345678901234567890101234567890123456789012345678901 0099] -6. paths [0123456789012345678901234567890101234567890123456789012345678901 10] -7. paths [0123456789012345678901234567890101234567890123456789012345678901 11] -8. paths [0123456789012345678901234567890101234567890123456789012345678901 19] -9. paths [19]` - if have := b.String(); have != want { - t.Errorf("have:%v\nwant:%v\n", have, want) - } - } - { - var b = new(bytes.Buffer) - for i := 0; i < len(pathsets); i++ { - fmt.Fprintf(b, "\n%d. pathset %x", i, pathsets[i]) - } - want := ` -0. pathset [0099] -1. pathset [0123456789012345678901234567890101234567890123456789012345678901 00 0095 0096 0097 0099 10 11 19] -2. pathset [19]` - if have := b.String(); have != want { - t.Errorf("have:%v\nwant:%v\n", have, want) - } - } -} diff --git a/eth/protocols/snap/sync.go b/eth/protocols/snap/sync.go deleted file mode 100644 index 887a50775d..0000000000 --- a/eth/protocols/snap/sync.go +++ /dev/null @@ -1,3210 +0,0 @@ -// Copyright 2020 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 snap - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - gomath "math" - "math/big" - "math/rand" - "sort" - "sync" - "sync/atomic" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/math" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/p2p/msgrate" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/trie" - "github.com/ethereum/go-ethereum/trie/trienode" - "golang.org/x/crypto/sha3" -) - -const ( - // minRequestSize is the minimum number of bytes to request from a remote peer. - // This number is used as the low cap for account and storage range requests. - // Bytecode and trienode are limited inherently by item count (1). - minRequestSize = 64 * 1024 - - // maxRequestSize is the maximum number of bytes to request from a remote peer. - // This number is used as the high cap for account and storage range requests. - // Bytecode and trienode are limited more explicitly by the caps below. - maxRequestSize = 512 * 1024 - - // maxCodeRequestCount is the maximum number of bytecode blobs to request in a - // single query. If this number is too low, we're not filling responses fully - // and waste round trip times. If it's too high, we're capping responses and - // waste bandwidth. - // - // Deployed bytecodes are currently capped at 24KB, so the minimum request - // size should be maxRequestSize / 24K. Assuming that most contracts do not - // come close to that, requesting 4x should be a good approximation. - maxCodeRequestCount = maxRequestSize / (24 * 1024) * 4 - - // maxTrieRequestCount is the maximum number of trie node blobs to request in - // a single query. If this number is too low, we're not filling responses fully - // and waste round trip times. If it's too high, we're capping responses and - // waste bandwidth. - maxTrieRequestCount = maxRequestSize / 512 - - // trienodeHealRateMeasurementImpact is the impact a single measurement has on - // the local node's trienode processing capacity. A value closer to 0 reacts - // slower to sudden changes, but it is also more stable against temporary hiccups. - trienodeHealRateMeasurementImpact = 0.005 - - // minTrienodeHealThrottle is the minimum divisor for throttling trie node - // heal requests to avoid overloading the local node and excessively expanding - // the state trie breadth wise. - minTrienodeHealThrottle = 1 - - // maxTrienodeHealThrottle is the maximum divisor for throttling trie node - // heal requests to avoid overloading the local node and exessively expanding - // the state trie bedth wise. - maxTrienodeHealThrottle = maxTrieRequestCount - - // trienodeHealThrottleIncrease is the multiplier for the throttle when the - // rate of arriving data is higher than the rate of processing it. - trienodeHealThrottleIncrease = 1.33 - - // trienodeHealThrottleDecrease is the divisor for the throttle when the - // rate of arriving data is lower than the rate of processing it. - trienodeHealThrottleDecrease = 1.25 -) - -var ( - // accountConcurrency is the number of chunks to split the account trie into - // to allow concurrent retrievals. - accountConcurrency = 16 - - // storageConcurrency is the number of chunks to split the a large contract - // storage trie into to allow concurrent retrievals. - storageConcurrency = 16 -) - -// ErrCancelled is returned from snap syncing if the operation was prematurely -// terminated. -var ErrCancelled = errors.New("sync cancelled") - -// accountRequest tracks a pending account range request to ensure responses are -// to actual requests and to validate any security constraints. -// -// Concurrency note: account requests and responses are handled concurrently from -// the main runloop to allow Merkle proof verifications on the peer's thread and -// to drop on invalid response. The request struct must contain all the data to -// construct the response without accessing runloop internals (i.e. task). That -// is only included to allow the runloop to match a response to the task being -// synced without having yet another set of maps. -type accountRequest struct { - peer string // Peer to which this request is assigned - id uint64 // Request ID of this request - time time.Time // Timestamp when the request was sent - - deliver chan *accountResponse // Channel to deliver successful response on - revert chan *accountRequest // Channel to deliver request failure on - cancel chan struct{} // Channel to track sync cancellation - timeout *time.Timer // Timer to track delivery timeout - stale chan struct{} // Channel to signal the request was dropped - - origin common.Hash // First account requested to allow continuation checks - limit common.Hash // Last account requested to allow non-overlapping chunking - - task *accountTask // Task which this request is filling (only access fields through the runloop!!) -} - -// accountResponse is an already Merkle-verified remote response to an account -// range request. It contains the subtrie for the requested account range and -// the database that's going to be filled with the internal nodes on commit. -type accountResponse struct { - task *accountTask // Task which this request is filling - - hashes []common.Hash // Account hashes in the returned range - accounts []*types.StateAccount // Expanded accounts in the returned range - - cont bool // Whether the account range has a continuation -} - -// bytecodeRequest tracks a pending bytecode request to ensure responses are to -// actual requests and to validate any security constraints. -// -// Concurrency note: bytecode requests and responses are handled concurrently from -// the main runloop to allow Keccak256 hash verifications on the peer's thread and -// to drop on invalid response. The request struct must contain all the data to -// construct the response without accessing runloop internals (i.e. task). That -// is only included to allow the runloop to match a response to the task being -// synced without having yet another set of maps. -type bytecodeRequest struct { - peer string // Peer to which this request is assigned - id uint64 // Request ID of this request - time time.Time // Timestamp when the request was sent - - deliver chan *bytecodeResponse // Channel to deliver successful response on - revert chan *bytecodeRequest // Channel to deliver request failure on - cancel chan struct{} // Channel to track sync cancellation - timeout *time.Timer // Timer to track delivery timeout - stale chan struct{} // Channel to signal the request was dropped - - hashes []common.Hash // Bytecode hashes to validate responses - task *accountTask // Task which this request is filling (only access fields through the runloop!!) -} - -// bytecodeResponse is an already verified remote response to a bytecode request. -type bytecodeResponse struct { - task *accountTask // Task which this request is filling - - hashes []common.Hash // Hashes of the bytecode to avoid double hashing - codes [][]byte // Actual bytecodes to store into the database (nil = missing) -} - -// storageRequest tracks a pending storage ranges request to ensure responses are -// to actual requests and to validate any security constraints. -// -// Concurrency note: storage requests and responses are handled concurrently from -// the main runloop to allow Merkle proof verifications on the peer's thread and -// to drop on invalid response. The request struct must contain all the data to -// construct the response without accessing runloop internals (i.e. tasks). That -// is only included to allow the runloop to match a response to the task being -// synced without having yet another set of maps. -type storageRequest struct { - peer string // Peer to which this request is assigned - id uint64 // Request ID of this request - time time.Time // Timestamp when the request was sent - - deliver chan *storageResponse // Channel to deliver successful response on - revert chan *storageRequest // Channel to deliver request failure on - cancel chan struct{} // Channel to track sync cancellation - timeout *time.Timer // Timer to track delivery timeout - stale chan struct{} // Channel to signal the request was dropped - - accounts []common.Hash // Account hashes to validate responses - roots []common.Hash // Storage roots to validate responses - - origin common.Hash // First storage slot requested to allow continuation checks - limit common.Hash // Last storage slot requested to allow non-overlapping chunking - - mainTask *accountTask // Task which this response belongs to (only access fields through the runloop!!) - subTask *storageTask // Task which this response is filling (only access fields through the runloop!!) -} - -// storageResponse is an already Merkle-verified remote response to a storage -// range request. It contains the subtries for the requested storage ranges and -// the databases that's going to be filled with the internal nodes on commit. -type storageResponse struct { - mainTask *accountTask // Task which this response belongs to - subTask *storageTask // Task which this response is filling - - accounts []common.Hash // Account hashes requested, may be only partially filled - roots []common.Hash // Storage roots requested, may be only partially filled - - hashes [][]common.Hash // Storage slot hashes in the returned range - slots [][][]byte // Storage slot values in the returned range - - cont bool // Whether the last storage range has a continuation -} - -// trienodeHealRequest tracks a pending state trie request to ensure responses -// are to actual requests and to validate any security constraints. -// -// Concurrency note: trie node requests and responses are handled concurrently from -// the main runloop to allow Keccak256 hash verifications on the peer's thread and -// to drop on invalid response. The request struct must contain all the data to -// construct the response without accessing runloop internals (i.e. task). That -// is only included to allow the runloop to match a response to the task being -// synced without having yet another set of maps. -type trienodeHealRequest struct { - peer string // Peer to which this request is assigned - id uint64 // Request ID of this request - time time.Time // Timestamp when the request was sent - - deliver chan *trienodeHealResponse // Channel to deliver successful response on - revert chan *trienodeHealRequest // Channel to deliver request failure on - cancel chan struct{} // Channel to track sync cancellation - timeout *time.Timer // Timer to track delivery timeout - stale chan struct{} // Channel to signal the request was dropped - - paths []string // Trie node paths for identifying trie node - hashes []common.Hash // Trie node hashes to validate responses - - task *healTask // Task which this request is filling (only access fields through the runloop!!) -} - -// trienodeHealResponse is an already verified remote response to a trie node request. -type trienodeHealResponse struct { - task *healTask // Task which this request is filling - - paths []string // Paths of the trie nodes - hashes []common.Hash // Hashes of the trie nodes to avoid double hashing - nodes [][]byte // Actual trie nodes to store into the database (nil = missing) -} - -// bytecodeHealRequest tracks a pending bytecode request to ensure responses are to -// actual requests and to validate any security constraints. -// -// Concurrency note: bytecode requests and responses are handled concurrently from -// the main runloop to allow Keccak256 hash verifications on the peer's thread and -// to drop on invalid response. The request struct must contain all the data to -// construct the response without accessing runloop internals (i.e. task). That -// is only included to allow the runloop to match a response to the task being -// synced without having yet another set of maps. -type bytecodeHealRequest struct { - peer string // Peer to which this request is assigned - id uint64 // Request ID of this request - time time.Time // Timestamp when the request was sent - - deliver chan *bytecodeHealResponse // Channel to deliver successful response on - revert chan *bytecodeHealRequest // Channel to deliver request failure on - cancel chan struct{} // Channel to track sync cancellation - timeout *time.Timer // Timer to track delivery timeout - stale chan struct{} // Channel to signal the request was dropped - - hashes []common.Hash // Bytecode hashes to validate responses - task *healTask // Task which this request is filling (only access fields through the runloop!!) -} - -// bytecodeHealResponse is an already verified remote response to a bytecode request. -type bytecodeHealResponse struct { - task *healTask // Task which this request is filling - - hashes []common.Hash // Hashes of the bytecode to avoid double hashing - codes [][]byte // Actual bytecodes to store into the database (nil = missing) -} - -// accountTask represents the sync task for a chunk of the account snapshot. -type accountTask struct { - // These fields get serialized to leveldb on shutdown - Next common.Hash // Next account to sync in this interval - Last common.Hash // Last account to sync in this interval - SubTasks map[common.Hash][]*storageTask // Storage intervals needing fetching for large contracts - - // These fields are internals used during runtime - req *accountRequest // Pending request to fill this task - res *accountResponse // Validate response filling this task - pend int // Number of pending subtasks for this round - - needCode []bool // Flags whether the filling accounts need code retrieval - needState []bool // Flags whether the filling accounts need storage retrieval - needHeal []bool // Flags whether the filling accounts's state was chunked and need healing - - codeTasks map[common.Hash]struct{} // Code hashes that need retrieval - stateTasks map[common.Hash]common.Hash // Account hashes->roots that need full state retrieval - - genBatch ethdb.Batch // Batch used by the node generator - genTrie *trie.StackTrie // Node generator from storage slots - - done bool // Flag whether the task can be removed -} - -// storageTask represents the sync task for a chunk of the storage snapshot. -type storageTask struct { - Next common.Hash // Next account to sync in this interval - Last common.Hash // Last account to sync in this interval - - // These fields are internals used during runtime - root common.Hash // Storage root hash for this instance - req *storageRequest // Pending request to fill this task - - genBatch ethdb.Batch // Batch used by the node generator - genTrie *trie.StackTrie // Node generator from storage slots - - done bool // Flag whether the task can be removed -} - -// healTask represents the sync task for healing the snap-synced chunk boundaries. -type healTask struct { - scheduler *trie.Sync // State trie sync scheduler defining the tasks - - trieTasks map[string]common.Hash // Set of trie node tasks currently queued for retrieval, indexed by node path - codeTasks map[common.Hash]struct{} // Set of byte code tasks currently queued for retrieval, indexed by code hash -} - -// SyncProgress is a database entry to allow suspending and resuming a snapshot state -// sync. Opposed to full and fast sync, there is no way to restart a suspended -// snap sync without prior knowledge of the suspension point. -type SyncProgress struct { - Tasks []*accountTask // The suspended account tasks (contract tasks within) - - // Status report during syncing phase - AccountSynced uint64 // Number of accounts downloaded - AccountBytes common.StorageSize // Number of account trie bytes persisted to disk - BytecodeSynced uint64 // Number of bytecodes downloaded - BytecodeBytes common.StorageSize // Number of bytecode bytes downloaded - StorageSynced uint64 // Number of storage slots downloaded - StorageBytes common.StorageSize // Number of storage trie bytes persisted to disk - - // Status report during healing phase - TrienodeHealSynced uint64 // Number of state trie nodes downloaded - TrienodeHealBytes common.StorageSize // Number of state trie bytes persisted to disk - BytecodeHealSynced uint64 // Number of bytecodes downloaded - BytecodeHealBytes common.StorageSize // Number of bytecodes persisted to disk -} - -// SyncPending is analogous to SyncProgress, but it's used to report on pending -// ephemeral sync progress that doesn't get persisted into the database. -type SyncPending struct { - TrienodeHeal uint64 // Number of state trie nodes pending - BytecodeHeal uint64 // Number of bytecodes pending -} - -// SyncPeer abstracts out the methods required for a peer to be synced against -// with the goal of allowing the construction of mock peers without the full -// blown networking. -type SyncPeer interface { - // ID retrieves the peer's unique identifier. - ID() string - - // RequestAccountRange fetches a batch of accounts rooted in a specific account - // trie, starting with the origin. - RequestAccountRange(id uint64, root, origin, limit common.Hash, bytes uint64) error - - // RequestStorageRanges fetches a batch of storage slots belonging to one or - // more accounts. If slots from only one account is requested, an origin marker - // may also be used to retrieve from there. - RequestStorageRanges(id uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, bytes uint64) error - - // RequestByteCodes fetches a batch of bytecodes by hash. - RequestByteCodes(id uint64, hashes []common.Hash, bytes uint64) error - - // RequestTrieNodes fetches a batch of account or storage trie nodes rooted in - // a specific state trie. - RequestTrieNodes(id uint64, root common.Hash, paths []TrieNodePathSet, bytes uint64) error - - // Log retrieves the peer's own contextual logger. - Log() log.Logger -} - -// Syncer is an Ethereum account and storage trie syncer based on snapshots and -// the snap protocol. It's purpose is to download all the accounts and storage -// slots from remote peers and reassemble chunks of the state trie, on top of -// which a state sync can be run to fix any gaps / overlaps. -// -// Every network request has a variety of failure events: -// - The peer disconnects after task assignment, failing to send the request -// - The peer disconnects after sending the request, before delivering on it -// - The peer remains connected, but does not deliver a response in time -// - The peer delivers a stale response after a previous timeout -// - The peer delivers a refusal to serve the requested state -type Syncer struct { - db ethdb.KeyValueStore // Database to store the trie nodes into (and dedup) - scheme string // Node scheme used in node database - - root common.Hash // Current state trie root being synced - tasks []*accountTask // Current account task set being synced - snapped bool // Flag to signal that snap phase is done - healer *healTask // Current state healing task being executed - update chan struct{} // Notification channel for possible sync progression - - peers map[string]SyncPeer // Currently active peers to download from - peerJoin *event.Feed // Event feed to react to peers joining - peerDrop *event.Feed // Event feed to react to peers dropping - rates *msgrate.Trackers // Message throughput rates for peers - - // Request tracking during syncing phase - statelessPeers map[string]struct{} // Peers that failed to deliver state data - accountIdlers map[string]struct{} // Peers that aren't serving account requests - bytecodeIdlers map[string]struct{} // Peers that aren't serving bytecode requests - storageIdlers map[string]struct{} // Peers that aren't serving storage requests - - accountReqs map[uint64]*accountRequest // Account requests currently running - bytecodeReqs map[uint64]*bytecodeRequest // Bytecode requests currently running - storageReqs map[uint64]*storageRequest // Storage requests currently running - - accountSynced uint64 // Number of accounts downloaded - accountBytes common.StorageSize // Number of account trie bytes persisted to disk - bytecodeSynced uint64 // Number of bytecodes downloaded - bytecodeBytes common.StorageSize // Number of bytecode bytes downloaded - storageSynced uint64 // Number of storage slots downloaded - storageBytes common.StorageSize // Number of storage trie bytes persisted to disk - - extProgress *SyncProgress // progress that can be exposed to external caller. - - // Request tracking during healing phase - trienodeHealIdlers map[string]struct{} // Peers that aren't serving trie node requests - bytecodeHealIdlers map[string]struct{} // Peers that aren't serving bytecode requests - - trienodeHealReqs map[uint64]*trienodeHealRequest // Trie node requests currently running - bytecodeHealReqs map[uint64]*bytecodeHealRequest // Bytecode requests currently running - - trienodeHealRate float64 // Average heal rate for processing trie node data - trienodeHealPend atomic.Uint64 // Number of trie nodes currently pending for processing - trienodeHealThrottle float64 // Divisor for throttling the amount of trienode heal data requested - trienodeHealThrottled time.Time // Timestamp the last time the throttle was updated - - trienodeHealSynced uint64 // Number of state trie nodes downloaded - trienodeHealBytes common.StorageSize // Number of state trie bytes persisted to disk - trienodeHealDups uint64 // Number of state trie nodes already processed - trienodeHealNops uint64 // Number of state trie nodes not requested - bytecodeHealSynced uint64 // Number of bytecodes downloaded - bytecodeHealBytes common.StorageSize // Number of bytecodes persisted to disk - bytecodeHealDups uint64 // Number of bytecodes already processed - bytecodeHealNops uint64 // Number of bytecodes not requested - - stateWriter ethdb.Batch // Shared batch writer used for persisting raw states - accountHealed uint64 // Number of accounts downloaded during the healing stage - accountHealedBytes common.StorageSize // Number of raw account bytes persisted to disk during the healing stage - storageHealed uint64 // Number of storage slots downloaded during the healing stage - storageHealedBytes common.StorageSize // Number of raw storage bytes persisted to disk during the healing stage - - startTime time.Time // Time instance when snapshot sync started - logTime time.Time // Time instance when status was last reported - - pend sync.WaitGroup // Tracks network request goroutines for graceful shutdown - lock sync.RWMutex // Protects fields that can change outside of sync (peers, reqs, root) -} - -// NewSyncer creates a new snapshot syncer to download the Ethereum state over the -// snap protocol. -func NewSyncer(db ethdb.KeyValueStore, scheme string) *Syncer { - return &Syncer{ - db: db, - scheme: scheme, - - peers: make(map[string]SyncPeer), - peerJoin: new(event.Feed), - peerDrop: new(event.Feed), - rates: msgrate.NewTrackers(log.New("proto", "snap")), - update: make(chan struct{}, 1), - - accountIdlers: make(map[string]struct{}), - storageIdlers: make(map[string]struct{}), - bytecodeIdlers: make(map[string]struct{}), - - accountReqs: make(map[uint64]*accountRequest), - storageReqs: make(map[uint64]*storageRequest), - bytecodeReqs: make(map[uint64]*bytecodeRequest), - - trienodeHealIdlers: make(map[string]struct{}), - bytecodeHealIdlers: make(map[string]struct{}), - - trienodeHealReqs: make(map[uint64]*trienodeHealRequest), - bytecodeHealReqs: make(map[uint64]*bytecodeHealRequest), - trienodeHealThrottle: maxTrienodeHealThrottle, // Tune downward instead of insta-filling with junk - stateWriter: db.NewBatch(), - - extProgress: new(SyncProgress), - } -} - -// Register injects a new data source into the syncer's peerset. -func (s *Syncer) Register(peer SyncPeer) error { - // Make sure the peer is not registered yet - id := peer.ID() - - s.lock.Lock() - if _, ok := s.peers[id]; ok { - log.Error("Snap peer already registered", "id", id) - - s.lock.Unlock() - return errors.New("already registered") - } - s.peers[id] = peer - s.rates.Track(id, msgrate.NewTracker(s.rates.MeanCapacities(), s.rates.MedianRoundTrip())) - - // Mark the peer as idle, even if no sync is running - s.accountIdlers[id] = struct{}{} - s.storageIdlers[id] = struct{}{} - s.bytecodeIdlers[id] = struct{}{} - s.trienodeHealIdlers[id] = struct{}{} - s.bytecodeHealIdlers[id] = struct{}{} - s.lock.Unlock() - - // Notify any active syncs that a new peer can be assigned data - s.peerJoin.Send(id) - return nil -} - -// Unregister injects a new data source into the syncer's peerset. -func (s *Syncer) Unregister(id string) error { - // Remove all traces of the peer from the registry - s.lock.Lock() - if _, ok := s.peers[id]; !ok { - log.Error("Snap peer not registered", "id", id) - - s.lock.Unlock() - return errors.New("not registered") - } - delete(s.peers, id) - s.rates.Untrack(id) - - // Remove status markers, even if no sync is running - delete(s.statelessPeers, id) - - delete(s.accountIdlers, id) - delete(s.storageIdlers, id) - delete(s.bytecodeIdlers, id) - delete(s.trienodeHealIdlers, id) - delete(s.bytecodeHealIdlers, id) - s.lock.Unlock() - - // Notify any active syncs that pending requests need to be reverted - s.peerDrop.Send(id) - return nil -} - -// Sync starts (or resumes a previous) sync cycle to iterate over a state trie -// with the given root and reconstruct the nodes based on the snapshot leaves. -// Previously downloaded segments will not be redownloaded of fixed, rather any -// errors will be healed after the leaves are fully accumulated. -func (s *Syncer) Sync(root common.Hash, cancel chan struct{}) error { - // Move the trie root from any previous value, revert stateless markers for - // any peers and initialize the syncer if it was not yet run - s.lock.Lock() - s.root = root - s.healer = &healTask{ - scheduler: state.NewStateSync(root, s.db, s.onHealState, s.scheme), - trieTasks: make(map[string]common.Hash), - codeTasks: make(map[common.Hash]struct{}), - } - s.statelessPeers = make(map[string]struct{}) - s.lock.Unlock() - - if s.startTime == (time.Time{}) { - s.startTime = time.Now() - } - // Retrieve the previous sync status from LevelDB and abort if already synced - s.loadSyncStatus() - if len(s.tasks) == 0 && s.healer.scheduler.Pending() == 0 { - log.Debug("Snapshot sync already completed") - return nil - } - defer func() { // Persist any progress, independent of failure - for _, task := range s.tasks { - s.forwardAccountTask(task) - } - s.cleanAccountTasks() - s.saveSyncStatus() - }() - - log.Debug("Starting snapshot sync cycle", "root", root) - - // Flush out the last committed raw states - defer func() { - if s.stateWriter.ValueSize() > 0 { - s.stateWriter.Write() - s.stateWriter.Reset() - } - }() - defer s.report(true) - // commit any trie- and bytecode-healing data. - defer s.commitHealer(true) - - // Whether sync completed or not, disregard any future packets - defer func() { - log.Debug("Terminating snapshot sync cycle", "root", root) - s.lock.Lock() - s.accountReqs = make(map[uint64]*accountRequest) - s.storageReqs = make(map[uint64]*storageRequest) - s.bytecodeReqs = make(map[uint64]*bytecodeRequest) - s.trienodeHealReqs = make(map[uint64]*trienodeHealRequest) - s.bytecodeHealReqs = make(map[uint64]*bytecodeHealRequest) - s.lock.Unlock() - }() - // Keep scheduling sync tasks - peerJoin := make(chan string, 16) - peerJoinSub := s.peerJoin.Subscribe(peerJoin) - defer peerJoinSub.Unsubscribe() - - peerDrop := make(chan string, 16) - peerDropSub := s.peerDrop.Subscribe(peerDrop) - defer peerDropSub.Unsubscribe() - - // Create a set of unique channels for this sync cycle. We need these to be - // ephemeral so a data race doesn't accidentally deliver something stale on - // a persistent channel across syncs (yup, this happened) - var ( - accountReqFails = make(chan *accountRequest) - storageReqFails = make(chan *storageRequest) - bytecodeReqFails = make(chan *bytecodeRequest) - accountResps = make(chan *accountResponse) - storageResps = make(chan *storageResponse) - bytecodeResps = make(chan *bytecodeResponse) - trienodeHealReqFails = make(chan *trienodeHealRequest) - bytecodeHealReqFails = make(chan *bytecodeHealRequest) - trienodeHealResps = make(chan *trienodeHealResponse) - bytecodeHealResps = make(chan *bytecodeHealResponse) - ) - for { - // Remove all completed tasks and terminate sync if everything's done - s.cleanStorageTasks() - s.cleanAccountTasks() - if len(s.tasks) == 0 && s.healer.scheduler.Pending() == 0 { - return nil - } - // Assign all the data retrieval tasks to any free peers - s.assignAccountTasks(accountResps, accountReqFails, cancel) - s.assignBytecodeTasks(bytecodeResps, bytecodeReqFails, cancel) - s.assignStorageTasks(storageResps, storageReqFails, cancel) - - if len(s.tasks) == 0 { - // Sync phase done, run heal phase - s.assignTrienodeHealTasks(trienodeHealResps, trienodeHealReqFails, cancel) - s.assignBytecodeHealTasks(bytecodeHealResps, bytecodeHealReqFails, cancel) - } - // Update sync progress - s.lock.Lock() - s.extProgress = &SyncProgress{ - AccountSynced: s.accountSynced, - AccountBytes: s.accountBytes, - BytecodeSynced: s.bytecodeSynced, - BytecodeBytes: s.bytecodeBytes, - StorageSynced: s.storageSynced, - StorageBytes: s.storageBytes, - TrienodeHealSynced: s.trienodeHealSynced, - TrienodeHealBytes: s.trienodeHealBytes, - BytecodeHealSynced: s.bytecodeHealSynced, - BytecodeHealBytes: s.bytecodeHealBytes, - } - s.lock.Unlock() - // Wait for something to happen - select { - case <-s.update: - // Something happened (new peer, delivery, timeout), recheck tasks - case <-peerJoin: - // A new peer joined, try to schedule it new tasks - case id := <-peerDrop: - s.revertRequests(id) - case <-cancel: - return ErrCancelled - - case req := <-accountReqFails: - s.revertAccountRequest(req) - case req := <-bytecodeReqFails: - s.revertBytecodeRequest(req) - case req := <-storageReqFails: - s.revertStorageRequest(req) - case req := <-trienodeHealReqFails: - s.revertTrienodeHealRequest(req) - case req := <-bytecodeHealReqFails: - s.revertBytecodeHealRequest(req) - - case res := <-accountResps: - s.processAccountResponse(res) - case res := <-bytecodeResps: - s.processBytecodeResponse(res) - case res := <-storageResps: - s.processStorageResponse(res) - case res := <-trienodeHealResps: - s.processTrienodeHealResponse(res) - case res := <-bytecodeHealResps: - s.processBytecodeHealResponse(res) - } - // Report stats if something meaningful happened - s.report(false) - } -} - -// cleanPath is used to remove the dangling nodes in the stackTrie. -func (s *Syncer) cleanPath(batch ethdb.Batch, owner common.Hash, path []byte) { - if owner == (common.Hash{}) && rawdb.ExistsAccountTrieNode(s.db, path) { - rawdb.DeleteAccountTrieNode(batch, path) - deletionGauge.Inc(1) - } - if owner != (common.Hash{}) && rawdb.ExistsStorageTrieNode(s.db, owner, path) { - rawdb.DeleteStorageTrieNode(batch, owner, path) - deletionGauge.Inc(1) - } - lookupGauge.Inc(1) -} - -// loadSyncStatus retrieves a previously aborted sync status from the database, -// or generates a fresh one if none is available. -func (s *Syncer) loadSyncStatus() { - var progress SyncProgress - - if status := rawdb.ReadSnapshotSyncStatus(s.db); status != nil { - if err := json.Unmarshal(status, &progress); err != nil { - log.Error("Failed to decode snap sync status", "err", err) - } else { - for _, task := range progress.Tasks { - log.Debug("Scheduled account sync task", "from", task.Next, "last", task.Last) - } - s.tasks = progress.Tasks - for _, task := range s.tasks { - task := task // closure for task.genBatch in the stacktrie writer callback - - task.genBatch = ethdb.HookedBatch{ - Batch: s.db.NewBatch(), - OnPut: func(key []byte, value []byte) { - s.accountBytes += common.StorageSize(len(key) + len(value)) - }, - } - options := trie.NewStackTrieOptions() - options = options.WithWriter(func(path []byte, hash common.Hash, blob []byte) { - rawdb.WriteTrieNode(task.genBatch, common.Hash{}, path, hash, blob, s.scheme) - }) - if s.scheme == rawdb.PathScheme { - // Configure the dangling node cleaner and also filter out boundary nodes - // only in the context of the path scheme. Deletion is forbidden in the - // hash scheme, as it can disrupt state completeness. - options = options.WithCleaner(func(path []byte) { - s.cleanPath(task.genBatch, common.Hash{}, path) - }) - // Skip the left boundary if it's not the first range. - // Skip the right boundary if it's not the last range. - options = options.WithSkipBoundary(task.Next != (common.Hash{}), task.Last != common.MaxHash, boundaryAccountNodesGauge) - } - task.genTrie = trie.NewStackTrie(options) - for accountHash, subtasks := range task.SubTasks { - for _, subtask := range subtasks { - subtask := subtask // closure for subtask.genBatch in the stacktrie writer callback - - subtask.genBatch = ethdb.HookedBatch{ - Batch: s.db.NewBatch(), - OnPut: func(key []byte, value []byte) { - s.storageBytes += common.StorageSize(len(key) + len(value)) - }, - } - owner := accountHash // local assignment for stacktrie writer closure - options := trie.NewStackTrieOptions() - options = options.WithWriter(func(path []byte, hash common.Hash, blob []byte) { - rawdb.WriteTrieNode(subtask.genBatch, owner, path, hash, blob, s.scheme) - }) - if s.scheme == rawdb.PathScheme { - // Configure the dangling node cleaner and also filter out boundary nodes - // only in the context of the path scheme. Deletion is forbidden in the - // hash scheme, as it can disrupt state completeness. - options = options.WithCleaner(func(path []byte) { - s.cleanPath(subtask.genBatch, owner, path) - }) - // Skip the left boundary if it's not the first range. - // Skip the right boundary if it's not the last range. - options = options.WithSkipBoundary(subtask.Next != common.Hash{}, subtask.Last != common.MaxHash, boundaryStorageNodesGauge) - } - subtask.genTrie = trie.NewStackTrie(options) - } - } - } - s.lock.Lock() - defer s.lock.Unlock() - - s.snapped = len(s.tasks) == 0 - - s.accountSynced = progress.AccountSynced - s.accountBytes = progress.AccountBytes - s.bytecodeSynced = progress.BytecodeSynced - s.bytecodeBytes = progress.BytecodeBytes - s.storageSynced = progress.StorageSynced - s.storageBytes = progress.StorageBytes - - s.trienodeHealSynced = progress.TrienodeHealSynced - s.trienodeHealBytes = progress.TrienodeHealBytes - s.bytecodeHealSynced = progress.BytecodeHealSynced - s.bytecodeHealBytes = progress.BytecodeHealBytes - return - } - } - // Either we've failed to decode the previous state, or there was none. - // Start a fresh sync by chunking up the account range and scheduling - // them for retrieval. - s.tasks = nil - s.accountSynced, s.accountBytes = 0, 0 - s.bytecodeSynced, s.bytecodeBytes = 0, 0 - s.storageSynced, s.storageBytes = 0, 0 - s.trienodeHealSynced, s.trienodeHealBytes = 0, 0 - s.bytecodeHealSynced, s.bytecodeHealBytes = 0, 0 - - var next common.Hash - step := new(big.Int).Sub( - new(big.Int).Div( - new(big.Int).Exp(common.Big2, common.Big256, nil), - big.NewInt(int64(accountConcurrency)), - ), common.Big1, - ) - for i := 0; i < accountConcurrency; i++ { - last := common.BigToHash(new(big.Int).Add(next.Big(), step)) - if i == accountConcurrency-1 { - // Make sure we don't overflow if the step is not a proper divisor - last = common.MaxHash - } - batch := ethdb.HookedBatch{ - Batch: s.db.NewBatch(), - OnPut: func(key []byte, value []byte) { - s.accountBytes += common.StorageSize(len(key) + len(value)) - }, - } - options := trie.NewStackTrieOptions() - options = options.WithWriter(func(path []byte, hash common.Hash, blob []byte) { - rawdb.WriteTrieNode(batch, common.Hash{}, path, hash, blob, s.scheme) - }) - if s.scheme == rawdb.PathScheme { - // Configure the dangling node cleaner and also filter out boundary nodes - // only in the context of the path scheme. Deletion is forbidden in the - // hash scheme, as it can disrupt state completeness. - options = options.WithCleaner(func(path []byte) { - s.cleanPath(batch, common.Hash{}, path) - }) - // Skip the left boundary if it's not the first range. - // Skip the right boundary if it's not the last range. - options = options.WithSkipBoundary(next != common.Hash{}, last != common.MaxHash, boundaryAccountNodesGauge) - } - s.tasks = append(s.tasks, &accountTask{ - Next: next, - Last: last, - SubTasks: make(map[common.Hash][]*storageTask), - genBatch: batch, - genTrie: trie.NewStackTrie(options), - }) - log.Debug("Created account sync task", "from", next, "last", last) - next = common.BigToHash(new(big.Int).Add(last.Big(), common.Big1)) - } -} - -// saveSyncStatus marshals the remaining sync tasks into leveldb. -func (s *Syncer) saveSyncStatus() { - // Serialize any partial progress to disk before spinning down - for _, task := range s.tasks { - if err := task.genBatch.Write(); err != nil { - log.Error("Failed to persist account slots", "err", err) - } - for _, subtasks := range task.SubTasks { - for _, subtask := range subtasks { - if err := subtask.genBatch.Write(); err != nil { - log.Error("Failed to persist storage slots", "err", err) - } - } - } - } - // Store the actual progress markers - progress := &SyncProgress{ - Tasks: s.tasks, - AccountSynced: s.accountSynced, - AccountBytes: s.accountBytes, - BytecodeSynced: s.bytecodeSynced, - BytecodeBytes: s.bytecodeBytes, - StorageSynced: s.storageSynced, - StorageBytes: s.storageBytes, - TrienodeHealSynced: s.trienodeHealSynced, - TrienodeHealBytes: s.trienodeHealBytes, - BytecodeHealSynced: s.bytecodeHealSynced, - BytecodeHealBytes: s.bytecodeHealBytes, - } - status, err := json.Marshal(progress) - if err != nil { - panic(err) // This can only fail during implementation - } - rawdb.WriteSnapshotSyncStatus(s.db, status) -} - -// Progress returns the snap sync status statistics. -func (s *Syncer) Progress() (*SyncProgress, *SyncPending) { - s.lock.Lock() - defer s.lock.Unlock() - pending := new(SyncPending) - if s.healer != nil { - pending.TrienodeHeal = uint64(len(s.healer.trieTasks)) - pending.BytecodeHeal = uint64(len(s.healer.codeTasks)) - } - return s.extProgress, pending -} - -// cleanAccountTasks removes account range retrieval tasks that have already been -// completed. -func (s *Syncer) cleanAccountTasks() { - // If the sync was already done before, don't even bother - if len(s.tasks) == 0 { - return - } - // Sync wasn't finished previously, check for any task that can be finalized - for i := 0; i < len(s.tasks); i++ { - if s.tasks[i].done { - s.tasks = append(s.tasks[:i], s.tasks[i+1:]...) - i-- - } - } - // If everything was just finalized just, generate the account trie and start heal - if len(s.tasks) == 0 { - s.lock.Lock() - s.snapped = true - s.lock.Unlock() - - // Push the final sync report - s.reportSyncProgress(true) - } -} - -// cleanStorageTasks iterates over all the account tasks and storage sub-tasks -// within, cleaning any that have been completed. -func (s *Syncer) cleanStorageTasks() { - for _, task := range s.tasks { - for account, subtasks := range task.SubTasks { - // Remove storage range retrieval tasks that completed - for j := 0; j < len(subtasks); j++ { - if subtasks[j].done { - subtasks = append(subtasks[:j], subtasks[j+1:]...) - j-- - } - } - if len(subtasks) > 0 { - task.SubTasks[account] = subtasks - continue - } - // If all storage chunks are done, mark the account as done too - for j, hash := range task.res.hashes { - if hash == account { - task.needState[j] = false - } - } - delete(task.SubTasks, account) - task.pend-- - - // If this was the last pending task, forward the account task - if task.pend == 0 { - s.forwardAccountTask(task) - } - } - } -} - -// assignAccountTasks attempts to match idle peers to pending account range -// retrievals. -func (s *Syncer) assignAccountTasks(success chan *accountResponse, fail chan *accountRequest, cancel chan struct{}) { - s.lock.Lock() - defer s.lock.Unlock() - - // Sort the peers by download capacity to use faster ones if many available - idlers := &capacitySort{ - ids: make([]string, 0, len(s.accountIdlers)), - caps: make([]int, 0, len(s.accountIdlers)), - } - targetTTL := s.rates.TargetTimeout() - for id := range s.accountIdlers { - if _, ok := s.statelessPeers[id]; ok { - continue - } - idlers.ids = append(idlers.ids, id) - idlers.caps = append(idlers.caps, s.rates.Capacity(id, AccountRangeMsg, targetTTL)) - } - if len(idlers.ids) == 0 { - return - } - sort.Sort(sort.Reverse(idlers)) - - // Iterate over all the tasks and try to find a pending one - for _, task := range s.tasks { - // Skip any tasks already filling - if task.req != nil || task.res != nil { - continue - } - // Task pending retrieval, try to find an idle peer. If no such peer - // exists, we probably assigned tasks for all (or they are stateless). - // Abort the entire assignment mechanism. - if len(idlers.ids) == 0 { - return - } - var ( - idle = idlers.ids[0] - peer = s.peers[idle] - cap = idlers.caps[0] - ) - idlers.ids, idlers.caps = idlers.ids[1:], idlers.caps[1:] - - // Matched a pending task to an idle peer, allocate a unique request id - var reqid uint64 - for { - reqid = uint64(rand.Int63()) - if reqid == 0 { - continue - } - if _, ok := s.accountReqs[reqid]; ok { - continue - } - break - } - // Generate the network query and send it to the peer - req := &accountRequest{ - peer: idle, - id: reqid, - time: time.Now(), - deliver: success, - revert: fail, - cancel: cancel, - stale: make(chan struct{}), - origin: task.Next, - limit: task.Last, - task: task, - } - req.timeout = time.AfterFunc(s.rates.TargetTimeout(), func() { - peer.Log().Debug("Account range request timed out", "reqid", reqid) - s.rates.Update(idle, AccountRangeMsg, 0, 0) - s.scheduleRevertAccountRequest(req) - }) - s.accountReqs[reqid] = req - delete(s.accountIdlers, idle) - - s.pend.Add(1) - go func(root common.Hash) { - defer s.pend.Done() - - // Attempt to send the remote request and revert if it fails - if cap > maxRequestSize { - cap = maxRequestSize - } - if cap < minRequestSize { // Don't bother with peers below a bare minimum performance - cap = minRequestSize - } - if err := peer.RequestAccountRange(reqid, root, req.origin, req.limit, uint64(cap)); err != nil { - peer.Log().Debug("Failed to request account range", "err", err) - s.scheduleRevertAccountRequest(req) - } - }(s.root) - - // Inject the request into the task to block further assignments - task.req = req - } -} - -// assignBytecodeTasks attempts to match idle peers to pending code retrievals. -func (s *Syncer) assignBytecodeTasks(success chan *bytecodeResponse, fail chan *bytecodeRequest, cancel chan struct{}) { - s.lock.Lock() - defer s.lock.Unlock() - - // Sort the peers by download capacity to use faster ones if many available - idlers := &capacitySort{ - ids: make([]string, 0, len(s.bytecodeIdlers)), - caps: make([]int, 0, len(s.bytecodeIdlers)), - } - targetTTL := s.rates.TargetTimeout() - for id := range s.bytecodeIdlers { - if _, ok := s.statelessPeers[id]; ok { - continue - } - idlers.ids = append(idlers.ids, id) - idlers.caps = append(idlers.caps, s.rates.Capacity(id, ByteCodesMsg, targetTTL)) - } - if len(idlers.ids) == 0 { - return - } - sort.Sort(sort.Reverse(idlers)) - - // Iterate over all the tasks and try to find a pending one - for _, task := range s.tasks { - // Skip any tasks not in the bytecode retrieval phase - if task.res == nil { - continue - } - // Skip tasks that are already retrieving (or done with) all codes - if len(task.codeTasks) == 0 { - continue - } - // Task pending retrieval, try to find an idle peer. If no such peer - // exists, we probably assigned tasks for all (or they are stateless). - // Abort the entire assignment mechanism. - if len(idlers.ids) == 0 { - return - } - var ( - idle = idlers.ids[0] - peer = s.peers[idle] - cap = idlers.caps[0] - ) - idlers.ids, idlers.caps = idlers.ids[1:], idlers.caps[1:] - - // Matched a pending task to an idle peer, allocate a unique request id - var reqid uint64 - for { - reqid = uint64(rand.Int63()) - if reqid == 0 { - continue - } - if _, ok := s.bytecodeReqs[reqid]; ok { - continue - } - break - } - // Generate the network query and send it to the peer - if cap > maxCodeRequestCount { - cap = maxCodeRequestCount - } - hashes := make([]common.Hash, 0, cap) - for hash := range task.codeTasks { - delete(task.codeTasks, hash) - hashes = append(hashes, hash) - if len(hashes) >= cap { - break - } - } - req := &bytecodeRequest{ - peer: idle, - id: reqid, - time: time.Now(), - deliver: success, - revert: fail, - cancel: cancel, - stale: make(chan struct{}), - hashes: hashes, - task: task, - } - req.timeout = time.AfterFunc(s.rates.TargetTimeout(), func() { - peer.Log().Debug("Bytecode request timed out", "reqid", reqid) - s.rates.Update(idle, ByteCodesMsg, 0, 0) - s.scheduleRevertBytecodeRequest(req) - }) - s.bytecodeReqs[reqid] = req - delete(s.bytecodeIdlers, idle) - - s.pend.Add(1) - go func() { - defer s.pend.Done() - - // Attempt to send the remote request and revert if it fails - if err := peer.RequestByteCodes(reqid, hashes, maxRequestSize); err != nil { - log.Debug("Failed to request bytecodes", "err", err) - s.scheduleRevertBytecodeRequest(req) - } - }() - } -} - -// assignStorageTasks attempts to match idle peers to pending storage range -// retrievals. -func (s *Syncer) assignStorageTasks(success chan *storageResponse, fail chan *storageRequest, cancel chan struct{}) { - s.lock.Lock() - defer s.lock.Unlock() - - // Sort the peers by download capacity to use faster ones if many available - idlers := &capacitySort{ - ids: make([]string, 0, len(s.storageIdlers)), - caps: make([]int, 0, len(s.storageIdlers)), - } - targetTTL := s.rates.TargetTimeout() - for id := range s.storageIdlers { - if _, ok := s.statelessPeers[id]; ok { - continue - } - idlers.ids = append(idlers.ids, id) - idlers.caps = append(idlers.caps, s.rates.Capacity(id, StorageRangesMsg, targetTTL)) - } - if len(idlers.ids) == 0 { - return - } - sort.Sort(sort.Reverse(idlers)) - - // Iterate over all the tasks and try to find a pending one - for _, task := range s.tasks { - // Skip any tasks not in the storage retrieval phase - if task.res == nil { - continue - } - // Skip tasks that are already retrieving (or done with) all small states - if len(task.SubTasks) == 0 && len(task.stateTasks) == 0 { - continue - } - // Task pending retrieval, try to find an idle peer. If no such peer - // exists, we probably assigned tasks for all (or they are stateless). - // Abort the entire assignment mechanism. - if len(idlers.ids) == 0 { - return - } - var ( - idle = idlers.ids[0] - peer = s.peers[idle] - cap = idlers.caps[0] - ) - idlers.ids, idlers.caps = idlers.ids[1:], idlers.caps[1:] - - // Matched a pending task to an idle peer, allocate a unique request id - var reqid uint64 - for { - reqid = uint64(rand.Int63()) - if reqid == 0 { - continue - } - if _, ok := s.storageReqs[reqid]; ok { - continue - } - break - } - // Generate the network query and send it to the peer. If there are - // large contract tasks pending, complete those before diving into - // even more new contracts. - if cap > maxRequestSize { - cap = maxRequestSize - } - if cap < minRequestSize { // Don't bother with peers below a bare minimum performance - cap = minRequestSize - } - storageSets := cap / 1024 - - var ( - accounts = make([]common.Hash, 0, storageSets) - roots = make([]common.Hash, 0, storageSets) - subtask *storageTask - ) - for account, subtasks := range task.SubTasks { - for _, st := range subtasks { - // Skip any subtasks already filling - if st.req != nil { - continue - } - // Found an incomplete storage chunk, schedule it - accounts = append(accounts, account) - roots = append(roots, st.root) - subtask = st - break // Large contract chunks are downloaded individually - } - if subtask != nil { - break // Large contract chunks are downloaded individually - } - } - if subtask == nil { - // No large contract required retrieval, but small ones available - for account, root := range task.stateTasks { - delete(task.stateTasks, account) - - accounts = append(accounts, account) - roots = append(roots, root) - - if len(accounts) >= storageSets { - break - } - } - } - // If nothing was found, it means this task is actually already fully - // retrieving, but large contracts are hard to detect. Skip to the next. - if len(accounts) == 0 { - continue - } - req := &storageRequest{ - peer: idle, - id: reqid, - time: time.Now(), - deliver: success, - revert: fail, - cancel: cancel, - stale: make(chan struct{}), - accounts: accounts, - roots: roots, - mainTask: task, - subTask: subtask, - } - if subtask != nil { - req.origin = subtask.Next - req.limit = subtask.Last - } - req.timeout = time.AfterFunc(s.rates.TargetTimeout(), func() { - peer.Log().Debug("Storage request timed out", "reqid", reqid) - s.rates.Update(idle, StorageRangesMsg, 0, 0) - s.scheduleRevertStorageRequest(req) - }) - s.storageReqs[reqid] = req - delete(s.storageIdlers, idle) - - s.pend.Add(1) - go func(root common.Hash) { - defer s.pend.Done() - - // Attempt to send the remote request and revert if it fails - var origin, limit []byte - if subtask != nil { - origin, limit = req.origin[:], req.limit[:] - } - if err := peer.RequestStorageRanges(reqid, root, accounts, origin, limit, uint64(cap)); err != nil { - log.Debug("Failed to request storage", "err", err) - s.scheduleRevertStorageRequest(req) - } - }(s.root) - - // Inject the request into the subtask to block further assignments - if subtask != nil { - subtask.req = req - } - } -} - -// assignTrienodeHealTasks attempts to match idle peers to trie node requests to -// heal any trie errors caused by the snap sync's chunked retrieval model. -func (s *Syncer) assignTrienodeHealTasks(success chan *trienodeHealResponse, fail chan *trienodeHealRequest, cancel chan struct{}) { - s.lock.Lock() - defer s.lock.Unlock() - - // Sort the peers by download capacity to use faster ones if many available - idlers := &capacitySort{ - ids: make([]string, 0, len(s.trienodeHealIdlers)), - caps: make([]int, 0, len(s.trienodeHealIdlers)), - } - targetTTL := s.rates.TargetTimeout() - for id := range s.trienodeHealIdlers { - if _, ok := s.statelessPeers[id]; ok { - continue - } - idlers.ids = append(idlers.ids, id) - idlers.caps = append(idlers.caps, s.rates.Capacity(id, TrieNodesMsg, targetTTL)) - } - if len(idlers.ids) == 0 { - return - } - sort.Sort(sort.Reverse(idlers)) - - // Iterate over pending tasks and try to find a peer to retrieve with - for len(s.healer.trieTasks) > 0 || s.healer.scheduler.Pending() > 0 { - // If there are not enough trie tasks queued to fully assign, fill the - // queue from the state sync scheduler. The trie synced schedules these - // together with bytecodes, so we need to queue them combined. - var ( - have = len(s.healer.trieTasks) + len(s.healer.codeTasks) - want = maxTrieRequestCount + maxCodeRequestCount - ) - if have < want { - paths, hashes, codes := s.healer.scheduler.Missing(want - have) - for i, path := range paths { - s.healer.trieTasks[path] = hashes[i] - } - for _, hash := range codes { - s.healer.codeTasks[hash] = struct{}{} - } - } - // If all the heal tasks are bytecodes or already downloading, bail - if len(s.healer.trieTasks) == 0 { - return - } - // Task pending retrieval, try to find an idle peer. If no such peer - // exists, we probably assigned tasks for all (or they are stateless). - // Abort the entire assignment mechanism. - if len(idlers.ids) == 0 { - return - } - var ( - idle = idlers.ids[0] - peer = s.peers[idle] - cap = idlers.caps[0] - ) - idlers.ids, idlers.caps = idlers.ids[1:], idlers.caps[1:] - - // Matched a pending task to an idle peer, allocate a unique request id - var reqid uint64 - for { - reqid = uint64(rand.Int63()) - if reqid == 0 { - continue - } - if _, ok := s.trienodeHealReqs[reqid]; ok { - continue - } - break - } - // Generate the network query and send it to the peer - if cap > maxTrieRequestCount { - cap = maxTrieRequestCount - } - cap = int(float64(cap) / s.trienodeHealThrottle) - if cap <= 0 { - cap = 1 - } - var ( - hashes = make([]common.Hash, 0, cap) - paths = make([]string, 0, cap) - pathsets = make([]TrieNodePathSet, 0, cap) - ) - for path, hash := range s.healer.trieTasks { - delete(s.healer.trieTasks, path) - - paths = append(paths, path) - hashes = append(hashes, hash) - if len(paths) >= cap { - break - } - } - // Group requests by account hash - paths, hashes, _, pathsets = sortByAccountPath(paths, hashes) - req := &trienodeHealRequest{ - peer: idle, - id: reqid, - time: time.Now(), - deliver: success, - revert: fail, - cancel: cancel, - stale: make(chan struct{}), - paths: paths, - hashes: hashes, - task: s.healer, - } - req.timeout = time.AfterFunc(s.rates.TargetTimeout(), func() { - peer.Log().Debug("Trienode heal request timed out", "reqid", reqid) - s.rates.Update(idle, TrieNodesMsg, 0, 0) - s.scheduleRevertTrienodeHealRequest(req) - }) - s.trienodeHealReqs[reqid] = req - delete(s.trienodeHealIdlers, idle) - - s.pend.Add(1) - go func(root common.Hash) { - defer s.pend.Done() - - // Attempt to send the remote request and revert if it fails - if err := peer.RequestTrieNodes(reqid, root, pathsets, maxRequestSize); err != nil { - log.Debug("Failed to request trienode healers", "err", err) - s.scheduleRevertTrienodeHealRequest(req) - } - }(s.root) - } -} - -// assignBytecodeHealTasks attempts to match idle peers to bytecode requests to -// heal any trie errors caused by the snap sync's chunked retrieval model. -func (s *Syncer) assignBytecodeHealTasks(success chan *bytecodeHealResponse, fail chan *bytecodeHealRequest, cancel chan struct{}) { - s.lock.Lock() - defer s.lock.Unlock() - - // Sort the peers by download capacity to use faster ones if many available - idlers := &capacitySort{ - ids: make([]string, 0, len(s.bytecodeHealIdlers)), - caps: make([]int, 0, len(s.bytecodeHealIdlers)), - } - targetTTL := s.rates.TargetTimeout() - for id := range s.bytecodeHealIdlers { - if _, ok := s.statelessPeers[id]; ok { - continue - } - idlers.ids = append(idlers.ids, id) - idlers.caps = append(idlers.caps, s.rates.Capacity(id, ByteCodesMsg, targetTTL)) - } - if len(idlers.ids) == 0 { - return - } - sort.Sort(sort.Reverse(idlers)) - - // Iterate over pending tasks and try to find a peer to retrieve with - for len(s.healer.codeTasks) > 0 || s.healer.scheduler.Pending() > 0 { - // If there are not enough trie tasks queued to fully assign, fill the - // queue from the state sync scheduler. The trie synced schedules these - // together with trie nodes, so we need to queue them combined. - var ( - have = len(s.healer.trieTasks) + len(s.healer.codeTasks) - want = maxTrieRequestCount + maxCodeRequestCount - ) - if have < want { - paths, hashes, codes := s.healer.scheduler.Missing(want - have) - for i, path := range paths { - s.healer.trieTasks[path] = hashes[i] - } - for _, hash := range codes { - s.healer.codeTasks[hash] = struct{}{} - } - } - // If all the heal tasks are trienodes or already downloading, bail - if len(s.healer.codeTasks) == 0 { - return - } - // Task pending retrieval, try to find an idle peer. If no such peer - // exists, we probably assigned tasks for all (or they are stateless). - // Abort the entire assignment mechanism. - if len(idlers.ids) == 0 { - return - } - var ( - idle = idlers.ids[0] - peer = s.peers[idle] - cap = idlers.caps[0] - ) - idlers.ids, idlers.caps = idlers.ids[1:], idlers.caps[1:] - - // Matched a pending task to an idle peer, allocate a unique request id - var reqid uint64 - for { - reqid = uint64(rand.Int63()) - if reqid == 0 { - continue - } - if _, ok := s.bytecodeHealReqs[reqid]; ok { - continue - } - break - } - // Generate the network query and send it to the peer - if cap > maxCodeRequestCount { - cap = maxCodeRequestCount - } - hashes := make([]common.Hash, 0, cap) - for hash := range s.healer.codeTasks { - delete(s.healer.codeTasks, hash) - - hashes = append(hashes, hash) - if len(hashes) >= cap { - break - } - } - req := &bytecodeHealRequest{ - peer: idle, - id: reqid, - time: time.Now(), - deliver: success, - revert: fail, - cancel: cancel, - stale: make(chan struct{}), - hashes: hashes, - task: s.healer, - } - req.timeout = time.AfterFunc(s.rates.TargetTimeout(), func() { - peer.Log().Debug("Bytecode heal request timed out", "reqid", reqid) - s.rates.Update(idle, ByteCodesMsg, 0, 0) - s.scheduleRevertBytecodeHealRequest(req) - }) - s.bytecodeHealReqs[reqid] = req - delete(s.bytecodeHealIdlers, idle) - - s.pend.Add(1) - go func() { - defer s.pend.Done() - - // Attempt to send the remote request and revert if it fails - if err := peer.RequestByteCodes(reqid, hashes, maxRequestSize); err != nil { - log.Debug("Failed to request bytecode healers", "err", err) - s.scheduleRevertBytecodeHealRequest(req) - } - }() - } -} - -// revertRequests locates all the currently pending requests from a particular -// peer and reverts them, rescheduling for others to fulfill. -func (s *Syncer) revertRequests(peer string) { - // Gather the requests first, revertals need the lock too - s.lock.Lock() - var accountReqs []*accountRequest - for _, req := range s.accountReqs { - if req.peer == peer { - accountReqs = append(accountReqs, req) - } - } - var bytecodeReqs []*bytecodeRequest - for _, req := range s.bytecodeReqs { - if req.peer == peer { - bytecodeReqs = append(bytecodeReqs, req) - } - } - var storageReqs []*storageRequest - for _, req := range s.storageReqs { - if req.peer == peer { - storageReqs = append(storageReqs, req) - } - } - var trienodeHealReqs []*trienodeHealRequest - for _, req := range s.trienodeHealReqs { - if req.peer == peer { - trienodeHealReqs = append(trienodeHealReqs, req) - } - } - var bytecodeHealReqs []*bytecodeHealRequest - for _, req := range s.bytecodeHealReqs { - if req.peer == peer { - bytecodeHealReqs = append(bytecodeHealReqs, req) - } - } - s.lock.Unlock() - - // Revert all the requests matching the peer - for _, req := range accountReqs { - s.revertAccountRequest(req) - } - for _, req := range bytecodeReqs { - s.revertBytecodeRequest(req) - } - for _, req := range storageReqs { - s.revertStorageRequest(req) - } - for _, req := range trienodeHealReqs { - s.revertTrienodeHealRequest(req) - } - for _, req := range bytecodeHealReqs { - s.revertBytecodeHealRequest(req) - } -} - -// scheduleRevertAccountRequest asks the event loop to clean up an account range -// request and return all failed retrieval tasks to the scheduler for reassignment. -func (s *Syncer) scheduleRevertAccountRequest(req *accountRequest) { - select { - case req.revert <- req: - // Sync event loop notified - case <-req.cancel: - // Sync cycle got cancelled - case <-req.stale: - // Request already reverted - } -} - -// revertAccountRequest cleans up an account range request and returns all failed -// retrieval tasks to the scheduler for reassignment. -// -// Note, this needs to run on the event runloop thread to reschedule to idle peers. -// On peer threads, use scheduleRevertAccountRequest. -func (s *Syncer) revertAccountRequest(req *accountRequest) { - log.Debug("Reverting account request", "peer", req.peer, "reqid", req.id) - select { - case <-req.stale: - log.Trace("Account request already reverted", "peer", req.peer, "reqid", req.id) - return - default: - } - close(req.stale) - - // Remove the request from the tracked set - s.lock.Lock() - delete(s.accountReqs, req.id) - s.lock.Unlock() - - // If there's a timeout timer still running, abort it and mark the account - // task as not-pending, ready for rescheduling - req.timeout.Stop() - if req.task.req == req { - req.task.req = nil - } -} - -// scheduleRevertBytecodeRequest asks the event loop to clean up a bytecode request -// and return all failed retrieval tasks to the scheduler for reassignment. -func (s *Syncer) scheduleRevertBytecodeRequest(req *bytecodeRequest) { - select { - case req.revert <- req: - // Sync event loop notified - case <-req.cancel: - // Sync cycle got cancelled - case <-req.stale: - // Request already reverted - } -} - -// revertBytecodeRequest cleans up a bytecode request and returns all failed -// retrieval tasks to the scheduler for reassignment. -// -// Note, this needs to run on the event runloop thread to reschedule to idle peers. -// On peer threads, use scheduleRevertBytecodeRequest. -func (s *Syncer) revertBytecodeRequest(req *bytecodeRequest) { - log.Debug("Reverting bytecode request", "peer", req.peer) - select { - case <-req.stale: - log.Trace("Bytecode request already reverted", "peer", req.peer, "reqid", req.id) - return - default: - } - close(req.stale) - - // Remove the request from the tracked set - s.lock.Lock() - delete(s.bytecodeReqs, req.id) - s.lock.Unlock() - - // If there's a timeout timer still running, abort it and mark the code - // retrievals as not-pending, ready for rescheduling - req.timeout.Stop() - for _, hash := range req.hashes { - req.task.codeTasks[hash] = struct{}{} - } -} - -// scheduleRevertStorageRequest asks the event loop to clean up a storage range -// request and return all failed retrieval tasks to the scheduler for reassignment. -func (s *Syncer) scheduleRevertStorageRequest(req *storageRequest) { - select { - case req.revert <- req: - // Sync event loop notified - case <-req.cancel: - // Sync cycle got cancelled - case <-req.stale: - // Request already reverted - } -} - -// revertStorageRequest cleans up a storage range request and returns all failed -// retrieval tasks to the scheduler for reassignment. -// -// Note, this needs to run on the event runloop thread to reschedule to idle peers. -// On peer threads, use scheduleRevertStorageRequest. -func (s *Syncer) revertStorageRequest(req *storageRequest) { - log.Debug("Reverting storage request", "peer", req.peer) - select { - case <-req.stale: - log.Trace("Storage request already reverted", "peer", req.peer, "reqid", req.id) - return - default: - } - close(req.stale) - - // Remove the request from the tracked set - s.lock.Lock() - delete(s.storageReqs, req.id) - s.lock.Unlock() - - // If there's a timeout timer still running, abort it and mark the storage - // task as not-pending, ready for rescheduling - req.timeout.Stop() - if req.subTask != nil { - req.subTask.req = nil - } else { - for i, account := range req.accounts { - req.mainTask.stateTasks[account] = req.roots[i] - } - } -} - -// scheduleRevertTrienodeHealRequest asks the event loop to clean up a trienode heal -// request and return all failed retrieval tasks to the scheduler for reassignment. -func (s *Syncer) scheduleRevertTrienodeHealRequest(req *trienodeHealRequest) { - select { - case req.revert <- req: - // Sync event loop notified - case <-req.cancel: - // Sync cycle got cancelled - case <-req.stale: - // Request already reverted - } -} - -// revertTrienodeHealRequest cleans up a trienode heal request and returns all -// failed retrieval tasks to the scheduler for reassignment. -// -// Note, this needs to run on the event runloop thread to reschedule to idle peers. -// On peer threads, use scheduleRevertTrienodeHealRequest. -func (s *Syncer) revertTrienodeHealRequest(req *trienodeHealRequest) { - log.Debug("Reverting trienode heal request", "peer", req.peer) - select { - case <-req.stale: - log.Trace("Trienode heal request already reverted", "peer", req.peer, "reqid", req.id) - return - default: - } - close(req.stale) - - // Remove the request from the tracked set - s.lock.Lock() - delete(s.trienodeHealReqs, req.id) - s.lock.Unlock() - - // If there's a timeout timer still running, abort it and mark the trie node - // retrievals as not-pending, ready for rescheduling - req.timeout.Stop() - for i, path := range req.paths { - req.task.trieTasks[path] = req.hashes[i] - } -} - -// scheduleRevertBytecodeHealRequest asks the event loop to clean up a bytecode heal -// request and return all failed retrieval tasks to the scheduler for reassignment. -func (s *Syncer) scheduleRevertBytecodeHealRequest(req *bytecodeHealRequest) { - select { - case req.revert <- req: - // Sync event loop notified - case <-req.cancel: - // Sync cycle got cancelled - case <-req.stale: - // Request already reverted - } -} - -// revertBytecodeHealRequest cleans up a bytecode heal request and returns all -// failed retrieval tasks to the scheduler for reassignment. -// -// Note, this needs to run on the event runloop thread to reschedule to idle peers. -// On peer threads, use scheduleRevertBytecodeHealRequest. -func (s *Syncer) revertBytecodeHealRequest(req *bytecodeHealRequest) { - log.Debug("Reverting bytecode heal request", "peer", req.peer) - select { - case <-req.stale: - log.Trace("Bytecode heal request already reverted", "peer", req.peer, "reqid", req.id) - return - default: - } - close(req.stale) - - // Remove the request from the tracked set - s.lock.Lock() - delete(s.bytecodeHealReqs, req.id) - s.lock.Unlock() - - // If there's a timeout timer still running, abort it and mark the code - // retrievals as not-pending, ready for rescheduling - req.timeout.Stop() - for _, hash := range req.hashes { - req.task.codeTasks[hash] = struct{}{} - } -} - -// processAccountResponse integrates an already validated account range response -// into the account tasks. -func (s *Syncer) processAccountResponse(res *accountResponse) { - // Switch the task from pending to filling - res.task.req = nil - res.task.res = res - - // Ensure that the response doesn't overflow into the subsequent task - last := res.task.Last.Big() - for i, hash := range res.hashes { - // Mark the range complete if the last is already included. - // Keep iteration to delete the extra states if exists. - cmp := hash.Big().Cmp(last) - if cmp == 0 { - res.cont = false - continue - } - if cmp > 0 { - // Chunk overflown, cut off excess - res.hashes = res.hashes[:i] - res.accounts = res.accounts[:i] - res.cont = false // Mark range completed - break - } - } - // Iterate over all the accounts and assemble which ones need further sub- - // filling before the entire account range can be persisted. - res.task.needCode = make([]bool, len(res.accounts)) - res.task.needState = make([]bool, len(res.accounts)) - res.task.needHeal = make([]bool, len(res.accounts)) - - res.task.codeTasks = make(map[common.Hash]struct{}) - res.task.stateTasks = make(map[common.Hash]common.Hash) - - resumed := make(map[common.Hash]struct{}) - - res.task.pend = 0 - for i, account := range res.accounts { - // Check if the account is a contract with an unknown code - if !bytes.Equal(account.CodeHash, types.EmptyCodeHash.Bytes()) { - if !rawdb.HasCodeWithPrefix(s.db, common.BytesToHash(account.CodeHash)) { - res.task.codeTasks[common.BytesToHash(account.CodeHash)] = struct{}{} - res.task.needCode[i] = true - res.task.pend++ - } - } - // Check if the account is a contract with an unknown storage trie - if account.Root != types.EmptyRootHash { - if !rawdb.HasTrieNode(s.db, res.hashes[i], nil, account.Root, s.scheme) { - // If there was a previous large state retrieval in progress, - // don't restart it from scratch. This happens if a sync cycle - // is interrupted and resumed later. However, *do* update the - // previous root hash. - if subtasks, ok := res.task.SubTasks[res.hashes[i]]; ok { - log.Debug("Resuming large storage retrieval", "account", res.hashes[i], "root", account.Root) - for _, subtask := range subtasks { - subtask.root = account.Root - } - res.task.needHeal[i] = true - resumed[res.hashes[i]] = struct{}{} - } else { - res.task.stateTasks[res.hashes[i]] = account.Root - } - res.task.needState[i] = true - res.task.pend++ - } - } - } - // Delete any subtasks that have been aborted but not resumed. This may undo - // some progress if a new peer gives us less accounts than an old one, but for - // now we have to live with that. - for hash := range res.task.SubTasks { - if _, ok := resumed[hash]; !ok { - log.Debug("Aborting suspended storage retrieval", "account", hash) - delete(res.task.SubTasks, hash) - } - } - // If the account range contained no contracts, or all have been fully filled - // beforehand, short circuit storage filling and forward to the next task - if res.task.pend == 0 { - s.forwardAccountTask(res.task) - return - } - // Some accounts are incomplete, leave as is for the storage and contract - // task assigners to pick up and fill -} - -// processBytecodeResponse integrates an already validated bytecode response -// into the account tasks. -func (s *Syncer) processBytecodeResponse(res *bytecodeResponse) { - batch := s.db.NewBatch() - - var ( - codes uint64 - ) - for i, hash := range res.hashes { - code := res.codes[i] - - // If the bytecode was not delivered, reschedule it - if code == nil { - res.task.codeTasks[hash] = struct{}{} - continue - } - // Code was delivered, mark it not needed any more - for j, account := range res.task.res.accounts { - if res.task.needCode[j] && hash == common.BytesToHash(account.CodeHash) { - res.task.needCode[j] = false - res.task.pend-- - } - } - // Push the bytecode into a database batch - codes++ - rawdb.WriteCode(batch, hash, code) - } - bytes := common.StorageSize(batch.ValueSize()) - if err := batch.Write(); err != nil { - log.Crit("Failed to persist bytecodes", "err", err) - } - s.bytecodeSynced += codes - s.bytecodeBytes += bytes - - log.Debug("Persisted set of bytecodes", "count", codes, "bytes", bytes) - - // If this delivery completed the last pending task, forward the account task - // to the next chunk - if res.task.pend == 0 { - s.forwardAccountTask(res.task) - return - } - // Some accounts are still incomplete, leave as is for the storage and contract - // task assigners to pick up and fill. -} - -// processStorageResponse integrates an already validated storage response -// into the account tasks. -func (s *Syncer) processStorageResponse(res *storageResponse) { - // Switch the subtask from pending to idle - if res.subTask != nil { - res.subTask.req = nil - } - batch := ethdb.HookedBatch{ - Batch: s.db.NewBatch(), - OnPut: func(key []byte, value []byte) { - s.storageBytes += common.StorageSize(len(key) + len(value)) - }, - } - var ( - slots int - oldStorageBytes = s.storageBytes - ) - // Iterate over all the accounts and reconstruct their storage tries from the - // delivered slots - for i, account := range res.accounts { - // If the account was not delivered, reschedule it - if i >= len(res.hashes) { - res.mainTask.stateTasks[account] = res.roots[i] - continue - } - // State was delivered, if complete mark as not needed any more, otherwise - // mark the account as needing healing - for j, hash := range res.mainTask.res.hashes { - if account != hash { - continue - } - acc := res.mainTask.res.accounts[j] - - // If the packet contains multiple contract storage slots, all - // but the last are surely complete. The last contract may be - // chunked, so check it's continuation flag. - if res.subTask == nil && res.mainTask.needState[j] && (i < len(res.hashes)-1 || !res.cont) { - res.mainTask.needState[j] = false - res.mainTask.pend-- - smallStorageGauge.Inc(1) - } - // If the last contract was chunked, mark it as needing healing - // to avoid writing it out to disk prematurely. - if res.subTask == nil && !res.mainTask.needHeal[j] && i == len(res.hashes)-1 && res.cont { - res.mainTask.needHeal[j] = true - } - // If the last contract was chunked, we need to switch to large - // contract handling mode - if res.subTask == nil && i == len(res.hashes)-1 && res.cont { - // If we haven't yet started a large-contract retrieval, create - // the subtasks for it within the main account task - if tasks, ok := res.mainTask.SubTasks[account]; !ok { - var ( - keys = res.hashes[i] - chunks = uint64(storageConcurrency) - lastKey common.Hash - ) - if len(keys) > 0 { - lastKey = keys[len(keys)-1] - } - // If the number of slots remaining is low, decrease the - // number of chunks. Somewhere on the order of 10-15K slots - // fit into a packet of 500KB. A key/slot pair is maximum 64 - // bytes, so pessimistically maxRequestSize/64 = 8K. - // - // Chunk so that at least 2 packets are needed to fill a task. - if estimate, err := estimateRemainingSlots(len(keys), lastKey); err == nil { - if n := estimate / (2 * (maxRequestSize / 64)); n+1 < chunks { - chunks = n + 1 - } - log.Debug("Chunked large contract", "initiators", len(keys), "tail", lastKey, "remaining", estimate, "chunks", chunks) - } else { - log.Debug("Chunked large contract", "initiators", len(keys), "tail", lastKey, "chunks", chunks) - } - r := newHashRange(lastKey, chunks) - if chunks == 1 { - smallStorageGauge.Inc(1) - } else { - largeStorageGauge.Inc(1) - } - // Our first task is the one that was just filled by this response. - batch := ethdb.HookedBatch{ - Batch: s.db.NewBatch(), - OnPut: func(key []byte, value []byte) { - s.storageBytes += common.StorageSize(len(key) + len(value)) - }, - } - owner := account // local assignment for stacktrie writer closure - options := trie.NewStackTrieOptions() - options = options.WithWriter(func(path []byte, hash common.Hash, blob []byte) { - rawdb.WriteTrieNode(batch, owner, path, hash, blob, s.scheme) - }) - if s.scheme == rawdb.PathScheme { - options = options.WithCleaner(func(path []byte) { - s.cleanPath(batch, owner, path) - }) - // Keep the left boundary as it's the first range. - // Skip the right boundary if it's not the last range. - options = options.WithSkipBoundary(false, r.End() != common.MaxHash, boundaryStorageNodesGauge) - } - tasks = append(tasks, &storageTask{ - Next: common.Hash{}, - Last: r.End(), - root: acc.Root, - genBatch: batch, - genTrie: trie.NewStackTrie(options), - }) - for r.Next() { - batch := ethdb.HookedBatch{ - Batch: s.db.NewBatch(), - OnPut: func(key []byte, value []byte) { - s.storageBytes += common.StorageSize(len(key) + len(value)) - }, - } - options := trie.NewStackTrieOptions() - options = options.WithWriter(func(path []byte, hash common.Hash, blob []byte) { - rawdb.WriteTrieNode(batch, owner, path, hash, blob, s.scheme) - }) - if s.scheme == rawdb.PathScheme { - // Configure the dangling node cleaner and also filter out boundary nodes - // only in the context of the path scheme. Deletion is forbidden in the - // hash scheme, as it can disrupt state completeness. - options = options.WithCleaner(func(path []byte) { - s.cleanPath(batch, owner, path) - }) - // Skip the left boundary as it's not the first range - // Skip the right boundary if it's not the last range. - options = options.WithSkipBoundary(true, r.End() != common.MaxHash, boundaryStorageNodesGauge) - } - tasks = append(tasks, &storageTask{ - Next: r.Start(), - Last: r.End(), - root: acc.Root, - genBatch: batch, - genTrie: trie.NewStackTrie(options), - }) - } - for _, task := range tasks { - log.Debug("Created storage sync task", "account", account, "root", acc.Root, "from", task.Next, "last", task.Last) - } - res.mainTask.SubTasks[account] = tasks - - // Since we've just created the sub-tasks, this response - // is surely for the first one (zero origin) - res.subTask = tasks[0] - } - } - // If we're in large contract delivery mode, forward the subtask - if res.subTask != nil { - // Ensure the response doesn't overflow into the subsequent task - last := res.subTask.Last.Big() - // Find the first overflowing key. While at it, mark res as complete - // if we find the range to include or pass the 'last' - index := sort.Search(len(res.hashes[i]), func(k int) bool { - cmp := res.hashes[i][k].Big().Cmp(last) - if cmp >= 0 { - res.cont = false - } - return cmp > 0 - }) - if index >= 0 { - // cut off excess - res.hashes[i] = res.hashes[i][:index] - res.slots[i] = res.slots[i][:index] - } - // Forward the relevant storage chunk (even if created just now) - if res.cont { - res.subTask.Next = incHash(res.hashes[i][len(res.hashes[i])-1]) - } else { - res.subTask.done = true - } - } - } - // Iterate over all the complete contracts, reconstruct the trie nodes and - // push them to disk. If the contract is chunked, the trie nodes will be - // reconstructed later. - slots += len(res.hashes[i]) - - if i < len(res.hashes)-1 || res.subTask == nil { - // no need to make local reassignment of account: this closure does not outlive the loop - options := trie.NewStackTrieOptions() - options = options.WithWriter(func(path []byte, hash common.Hash, blob []byte) { - rawdb.WriteTrieNode(batch, account, path, hash, blob, s.scheme) - }) - if s.scheme == rawdb.PathScheme { - // Configure the dangling node cleaner only in the context of the - // path scheme. Deletion is forbidden in the hash scheme, as it can - // disrupt state completeness. - // - // Notably, boundary nodes can be also kept because the whole storage - // trie is complete. - options = options.WithCleaner(func(path []byte) { - s.cleanPath(batch, account, path) - }) - } - tr := trie.NewStackTrie(options) - for j := 0; j < len(res.hashes[i]); j++ { - tr.Update(res.hashes[i][j][:], res.slots[i][j]) - } - tr.Commit() - } - // Persist the received storage segments. These flat state maybe - // outdated during the sync, but it can be fixed later during the - // snapshot generation. - for j := 0; j < len(res.hashes[i]); j++ { - rawdb.WriteStorageSnapshot(batch, account, res.hashes[i][j], res.slots[i][j]) - - // If we're storing large contracts, generate the trie nodes - // on the fly to not trash the gluing points - if i == len(res.hashes)-1 && res.subTask != nil { - res.subTask.genTrie.Update(res.hashes[i][j][:], res.slots[i][j]) - } - } - } - // Large contracts could have generated new trie nodes, flush them to disk - if res.subTask != nil { - if res.subTask.done { - root := res.subTask.genTrie.Commit() - if err := res.subTask.genBatch.Write(); err != nil { - log.Error("Failed to persist stack slots", "err", err) - } - res.subTask.genBatch.Reset() - - // If the chunk's root is an overflown but full delivery, - // clear the heal request. - accountHash := res.accounts[len(res.accounts)-1] - if root == res.subTask.root && rawdb.HasStorageTrieNode(s.db, accountHash, nil, root) { - for i, account := range res.mainTask.res.hashes { - if account == accountHash { - res.mainTask.needHeal[i] = false - skipStorageHealingGauge.Inc(1) - } - } - } - } - if res.subTask.genBatch.ValueSize() > ethdb.IdealBatchSize { - if err := res.subTask.genBatch.Write(); err != nil { - log.Error("Failed to persist stack slots", "err", err) - } - res.subTask.genBatch.Reset() - } - } - // Flush anything written just now and update the stats - if err := batch.Write(); err != nil { - log.Crit("Failed to persist storage slots", "err", err) - } - s.storageSynced += uint64(slots) - - log.Debug("Persisted set of storage slots", "accounts", len(res.hashes), "slots", slots, "bytes", s.storageBytes-oldStorageBytes) - - // If this delivery completed the last pending task, forward the account task - // to the next chunk - if res.mainTask.pend == 0 { - s.forwardAccountTask(res.mainTask) - return - } - // Some accounts are still incomplete, leave as is for the storage and contract - // task assigners to pick up and fill. -} - -// processTrienodeHealResponse integrates an already validated trienode response -// into the healer tasks. -func (s *Syncer) processTrienodeHealResponse(res *trienodeHealResponse) { - var ( - start = time.Now() - fills int - ) - for i, hash := range res.hashes { - node := res.nodes[i] - - // If the trie node was not delivered, reschedule it - if node == nil { - res.task.trieTasks[res.paths[i]] = res.hashes[i] - continue - } - fills++ - - // Push the trie node into the state syncer - s.trienodeHealSynced++ - s.trienodeHealBytes += common.StorageSize(len(node)) - - err := s.healer.scheduler.ProcessNode(trie.NodeSyncResult{Path: res.paths[i], Data: node}) - switch err { - case nil: - case trie.ErrAlreadyProcessed: - s.trienodeHealDups++ - case trie.ErrNotRequested: - s.trienodeHealNops++ - default: - log.Error("Invalid trienode processed", "hash", hash, "err", err) - } - } - s.commitHealer(false) - - // Calculate the processing rate of one filled trie node - rate := float64(fills) / (float64(time.Since(start)) / float64(time.Second)) - - // Update the currently measured trienode queueing and processing throughput. - // - // The processing rate needs to be updated uniformly independent if we've - // processed 1x100 trie nodes or 100x1 to keep the rate consistent even in - // the face of varying network packets. As such, we cannot just measure the - // time it took to process N trie nodes and update once, we need one update - // per trie node. - // - // Naively, that would be: - // - // for i:=0; i time.Second { - // Periodically adjust the trie node throttler - if float64(pending) > 2*s.trienodeHealRate { - s.trienodeHealThrottle *= trienodeHealThrottleIncrease - } else { - s.trienodeHealThrottle /= trienodeHealThrottleDecrease - } - if s.trienodeHealThrottle > maxTrienodeHealThrottle { - s.trienodeHealThrottle = maxTrienodeHealThrottle - } else if s.trienodeHealThrottle < minTrienodeHealThrottle { - s.trienodeHealThrottle = minTrienodeHealThrottle - } - s.trienodeHealThrottled = time.Now() - - log.Debug("Updated trie node heal throttler", "rate", s.trienodeHealRate, "pending", pending, "throttle", s.trienodeHealThrottle) - } -} - -func (s *Syncer) commitHealer(force bool) { - if !force && s.healer.scheduler.MemSize() < ethdb.IdealBatchSize { - return - } - batch := s.db.NewBatch() - if err := s.healer.scheduler.Commit(batch); err != nil { - log.Error("Failed to commit healing data", "err", err) - } - if err := batch.Write(); err != nil { - log.Crit("Failed to persist healing data", "err", err) - } - log.Debug("Persisted set of healing data", "type", "trienodes", "bytes", common.StorageSize(batch.ValueSize())) -} - -// processBytecodeHealResponse integrates an already validated bytecode response -// into the healer tasks. -func (s *Syncer) processBytecodeHealResponse(res *bytecodeHealResponse) { - for i, hash := range res.hashes { - node := res.codes[i] - - // If the trie node was not delivered, reschedule it - if node == nil { - res.task.codeTasks[hash] = struct{}{} - continue - } - // Push the trie node into the state syncer - s.bytecodeHealSynced++ - s.bytecodeHealBytes += common.StorageSize(len(node)) - - err := s.healer.scheduler.ProcessCode(trie.CodeSyncResult{Hash: hash, Data: node}) - switch err { - case nil: - case trie.ErrAlreadyProcessed: - s.bytecodeHealDups++ - case trie.ErrNotRequested: - s.bytecodeHealNops++ - default: - log.Error("Invalid bytecode processed", "hash", hash, "err", err) - } - } - s.commitHealer(false) -} - -// forwardAccountTask takes a filled account task and persists anything available -// into the database, after which it forwards the next account marker so that the -// task's next chunk may be filled. -func (s *Syncer) forwardAccountTask(task *accountTask) { - // Remove any pending delivery - res := task.res - if res == nil { - return // nothing to forward - } - task.res = nil - - // Persist the received account segments. These flat state maybe - // outdated during the sync, but it can be fixed later during the - // snapshot generation. - oldAccountBytes := s.accountBytes - - batch := ethdb.HookedBatch{ - Batch: s.db.NewBatch(), - OnPut: func(key []byte, value []byte) { - s.accountBytes += common.StorageSize(len(key) + len(value)) - }, - } - for i, hash := range res.hashes { - if task.needCode[i] || task.needState[i] { - break - } - slim := types.SlimAccountRLP(*res.accounts[i]) - rawdb.WriteAccountSnapshot(batch, hash, slim) - - // If the task is complete, drop it into the stack trie to generate - // account trie nodes for it - if !task.needHeal[i] { - full, err := types.FullAccountRLP(slim) // TODO(karalabe): Slim parsing can be omitted - if err != nil { - panic(err) // Really shouldn't ever happen - } - task.genTrie.Update(hash[:], full) - } - } - // Flush anything written just now and update the stats - if err := batch.Write(); err != nil { - log.Crit("Failed to persist accounts", "err", err) - } - s.accountSynced += uint64(len(res.accounts)) - - // Task filling persisted, push it the chunk marker forward to the first - // account still missing data. - for i, hash := range res.hashes { - if task.needCode[i] || task.needState[i] { - return - } - task.Next = incHash(hash) - } - // All accounts marked as complete, track if the entire task is done - task.done = !res.cont - - // Stack trie could have generated trie nodes, push them to disk (we need to - // flush after finalizing task.done. It's fine even if we crash and lose this - // write as it will only cause more data to be downloaded during heal. - if task.done { - task.genTrie.Commit() - } - if task.genBatch.ValueSize() > ethdb.IdealBatchSize || task.done { - if err := task.genBatch.Write(); err != nil { - log.Error("Failed to persist stack account", "err", err) - } - task.genBatch.Reset() - } - log.Debug("Persisted range of accounts", "accounts", len(res.accounts), "bytes", s.accountBytes-oldAccountBytes) -} - -// OnAccounts is a callback method to invoke when a range of accounts are -// received from a remote peer. -func (s *Syncer) OnAccounts(peer SyncPeer, id uint64, hashes []common.Hash, accounts [][]byte, proof [][]byte) error { - size := common.StorageSize(len(hashes) * common.HashLength) - for _, account := range accounts { - size += common.StorageSize(len(account)) - } - for _, node := range proof { - size += common.StorageSize(len(node)) - } - logger := peer.Log().New("reqid", id) - logger.Trace("Delivering range of accounts", "hashes", len(hashes), "accounts", len(accounts), "proofs", len(proof), "bytes", size) - - // Whether or not the response is valid, we can mark the peer as idle and - // notify the scheduler to assign a new task. If the response is invalid, - // we'll drop the peer in a bit. - defer func() { - s.lock.Lock() - defer s.lock.Unlock() - if _, ok := s.peers[peer.ID()]; ok { - s.accountIdlers[peer.ID()] = struct{}{} - } - select { - case s.update <- struct{}{}: - default: - } - }() - s.lock.Lock() - // Ensure the response is for a valid request - req, ok := s.accountReqs[id] - if !ok { - // Request stale, perhaps the peer timed out but came through in the end - logger.Warn("Unexpected account range packet") - s.lock.Unlock() - return nil - } - delete(s.accountReqs, id) - s.rates.Update(peer.ID(), AccountRangeMsg, time.Since(req.time), int(size)) - - // Clean up the request timeout timer, we'll see how to proceed further based - // on the actual delivered content - if !req.timeout.Stop() { - // The timeout is already triggered, and this request will be reverted+rescheduled - s.lock.Unlock() - return nil - } - // Response is valid, but check if peer is signalling that it does not have - // the requested data. For account range queries that means the state being - // retrieved was either already pruned remotely, or the peer is not yet - // synced to our head. - if len(hashes) == 0 && len(accounts) == 0 && len(proof) == 0 { - logger.Debug("Peer rejected account range request", "root", s.root) - s.statelessPeers[peer.ID()] = struct{}{} - s.lock.Unlock() - - // Signal this request as failed, and ready for rescheduling - s.scheduleRevertAccountRequest(req) - return nil - } - root := s.root - s.lock.Unlock() - - // Reconstruct a partial trie from the response and verify it - keys := make([][]byte, len(hashes)) - for i, key := range hashes { - keys[i] = common.CopyBytes(key[:]) - } - nodes := make(trienode.ProofList, len(proof)) - for i, node := range proof { - nodes[i] = node - } - cont, err := trie.VerifyRangeProof(root, req.origin[:], keys, accounts, nodes.Set()) - if err != nil { - logger.Warn("Account range failed proof", "err", err) - // Signal this request as failed, and ready for rescheduling - s.scheduleRevertAccountRequest(req) - return err - } - accs := make([]*types.StateAccount, len(accounts)) - for i, account := range accounts { - acc := new(types.StateAccount) - if err := rlp.DecodeBytes(account, acc); err != nil { - panic(err) // We created these blobs, we must be able to decode them - } - accs[i] = acc - } - response := &accountResponse{ - task: req.task, - hashes: hashes, - accounts: accs, - cont: cont, - } - select { - case req.deliver <- response: - case <-req.cancel: - case <-req.stale: - } - return nil -} - -// OnByteCodes is a callback method to invoke when a batch of contract -// bytes codes are received from a remote peer. -func (s *Syncer) OnByteCodes(peer SyncPeer, id uint64, bytecodes [][]byte) error { - s.lock.RLock() - syncing := !s.snapped - s.lock.RUnlock() - - if syncing { - return s.onByteCodes(peer, id, bytecodes) - } - return s.onHealByteCodes(peer, id, bytecodes) -} - -// onByteCodes is a callback method to invoke when a batch of contract -// bytes codes are received from a remote peer in the syncing phase. -func (s *Syncer) onByteCodes(peer SyncPeer, id uint64, bytecodes [][]byte) error { - var size common.StorageSize - for _, code := range bytecodes { - size += common.StorageSize(len(code)) - } - logger := peer.Log().New("reqid", id) - logger.Trace("Delivering set of bytecodes", "bytecodes", len(bytecodes), "bytes", size) - - // Whether or not the response is valid, we can mark the peer as idle and - // notify the scheduler to assign a new task. If the response is invalid, - // we'll drop the peer in a bit. - defer func() { - s.lock.Lock() - defer s.lock.Unlock() - if _, ok := s.peers[peer.ID()]; ok { - s.bytecodeIdlers[peer.ID()] = struct{}{} - } - select { - case s.update <- struct{}{}: - default: - } - }() - s.lock.Lock() - // Ensure the response is for a valid request - req, ok := s.bytecodeReqs[id] - if !ok { - // Request stale, perhaps the peer timed out but came through in the end - logger.Warn("Unexpected bytecode packet") - s.lock.Unlock() - return nil - } - delete(s.bytecodeReqs, id) - s.rates.Update(peer.ID(), ByteCodesMsg, time.Since(req.time), len(bytecodes)) - - // Clean up the request timeout timer, we'll see how to proceed further based - // on the actual delivered content - if !req.timeout.Stop() { - // The timeout is already triggered, and this request will be reverted+rescheduled - s.lock.Unlock() - return nil - } - - // Response is valid, but check if peer is signalling that it does not have - // the requested data. For bytecode range queries that means the peer is not - // yet synced. - if len(bytecodes) == 0 { - logger.Debug("Peer rejected bytecode request") - s.statelessPeers[peer.ID()] = struct{}{} - s.lock.Unlock() - - // Signal this request as failed, and ready for rescheduling - s.scheduleRevertBytecodeRequest(req) - return nil - } - s.lock.Unlock() - - // Cross reference the requested bytecodes with the response to find gaps - // that the serving node is missing - hasher := sha3.NewLegacyKeccak256().(crypto.KeccakState) - hash := make([]byte, 32) - - codes := make([][]byte, len(req.hashes)) - for i, j := 0, 0; i < len(bytecodes); i++ { - // Find the next hash that we've been served, leaving misses with nils - hasher.Reset() - hasher.Write(bytecodes[i]) - hasher.Read(hash) - - for j < len(req.hashes) && !bytes.Equal(hash, req.hashes[j][:]) { - j++ - } - if j < len(req.hashes) { - codes[j] = bytecodes[i] - j++ - continue - } - // We've either ran out of hashes, or got unrequested data - logger.Warn("Unexpected bytecodes", "count", len(bytecodes)-i) - // Signal this request as failed, and ready for rescheduling - s.scheduleRevertBytecodeRequest(req) - return errors.New("unexpected bytecode") - } - // Response validated, send it to the scheduler for filling - response := &bytecodeResponse{ - task: req.task, - hashes: req.hashes, - codes: codes, - } - select { - case req.deliver <- response: - case <-req.cancel: - case <-req.stale: - } - return nil -} - -// OnStorage is a callback method to invoke when ranges of storage slots -// are received from a remote peer. -func (s *Syncer) OnStorage(peer SyncPeer, id uint64, hashes [][]common.Hash, slots [][][]byte, proof [][]byte) error { - // Gather some trace stats to aid in debugging issues - var ( - hashCount int - slotCount int - size common.StorageSize - ) - for _, hashset := range hashes { - size += common.StorageSize(common.HashLength * len(hashset)) - hashCount += len(hashset) - } - for _, slotset := range slots { - for _, slot := range slotset { - size += common.StorageSize(len(slot)) - } - slotCount += len(slotset) - } - for _, node := range proof { - size += common.StorageSize(len(node)) - } - logger := peer.Log().New("reqid", id) - logger.Trace("Delivering ranges of storage slots", "accounts", len(hashes), "hashes", hashCount, "slots", slotCount, "proofs", len(proof), "size", size) - - // Whether or not the response is valid, we can mark the peer as idle and - // notify the scheduler to assign a new task. If the response is invalid, - // we'll drop the peer in a bit. - defer func() { - s.lock.Lock() - defer s.lock.Unlock() - if _, ok := s.peers[peer.ID()]; ok { - s.storageIdlers[peer.ID()] = struct{}{} - } - select { - case s.update <- struct{}{}: - default: - } - }() - s.lock.Lock() - // Ensure the response is for a valid request - req, ok := s.storageReqs[id] - if !ok { - // Request stale, perhaps the peer timed out but came through in the end - logger.Warn("Unexpected storage ranges packet") - s.lock.Unlock() - return nil - } - delete(s.storageReqs, id) - s.rates.Update(peer.ID(), StorageRangesMsg, time.Since(req.time), int(size)) - - // Clean up the request timeout timer, we'll see how to proceed further based - // on the actual delivered content - if !req.timeout.Stop() { - // The timeout is already triggered, and this request will be reverted+rescheduled - s.lock.Unlock() - return nil - } - - // Reject the response if the hash sets and slot sets don't match, or if the - // peer sent more data than requested. - if len(hashes) != len(slots) { - s.lock.Unlock() - s.scheduleRevertStorageRequest(req) // reschedule request - logger.Warn("Hash and slot set size mismatch", "hashset", len(hashes), "slotset", len(slots)) - return errors.New("hash and slot set size mismatch") - } - if len(hashes) > len(req.accounts) { - s.lock.Unlock() - s.scheduleRevertStorageRequest(req) // reschedule request - logger.Warn("Hash set larger than requested", "hashset", len(hashes), "requested", len(req.accounts)) - return errors.New("hash set larger than requested") - } - // Response is valid, but check if peer is signalling that it does not have - // the requested data. For storage range queries that means the state being - // retrieved was either already pruned remotely, or the peer is not yet - // synced to our head. - if len(hashes) == 0 && len(proof) == 0 { - logger.Debug("Peer rejected storage request") - s.statelessPeers[peer.ID()] = struct{}{} - s.lock.Unlock() - s.scheduleRevertStorageRequest(req) // reschedule request - return nil - } - s.lock.Unlock() - - // Reconstruct the partial tries from the response and verify them - var cont bool - - // If a proof was attached while the response is empty, it indicates that the - // requested range specified with 'origin' is empty. Construct an empty state - // response locally to finalize the range. - if len(hashes) == 0 && len(proof) > 0 { - hashes = append(hashes, []common.Hash{}) - slots = append(slots, [][]byte{}) - } - for i := 0; i < len(hashes); i++ { - // Convert the keys and proofs into an internal format - keys := make([][]byte, len(hashes[i])) - for j, key := range hashes[i] { - keys[j] = common.CopyBytes(key[:]) - } - nodes := make(trienode.ProofList, 0, len(proof)) - if i == len(hashes)-1 { - for _, node := range proof { - nodes = append(nodes, node) - } - } - var err error - if len(nodes) == 0 { - // No proof has been attached, the response must cover the entire key - // space and hash to the origin root. - _, err = trie.VerifyRangeProof(req.roots[i], nil, keys, slots[i], nil) - if err != nil { - s.scheduleRevertStorageRequest(req) // reschedule request - logger.Warn("Storage slots failed proof", "err", err) - return err - } - } else { - // A proof was attached, the response is only partial, check that the - // returned data is indeed part of the storage trie - proofdb := nodes.Set() - - cont, err = trie.VerifyRangeProof(req.roots[i], req.origin[:], keys, slots[i], proofdb) - if err != nil { - s.scheduleRevertStorageRequest(req) // reschedule request - logger.Warn("Storage range failed proof", "err", err) - return err - } - } - } - // Partial tries reconstructed, send them to the scheduler for storage filling - response := &storageResponse{ - mainTask: req.mainTask, - subTask: req.subTask, - accounts: req.accounts, - roots: req.roots, - hashes: hashes, - slots: slots, - cont: cont, - } - select { - case req.deliver <- response: - case <-req.cancel: - case <-req.stale: - } - return nil -} - -// OnTrieNodes is a callback method to invoke when a batch of trie nodes -// are received from a remote peer. -func (s *Syncer) OnTrieNodes(peer SyncPeer, id uint64, trienodes [][]byte) error { - var size common.StorageSize - for _, node := range trienodes { - size += common.StorageSize(len(node)) - } - logger := peer.Log().New("reqid", id) - logger.Trace("Delivering set of healing trienodes", "trienodes", len(trienodes), "bytes", size) - - // Whether or not the response is valid, we can mark the peer as idle and - // notify the scheduler to assign a new task. If the response is invalid, - // we'll drop the peer in a bit. - defer func() { - s.lock.Lock() - defer s.lock.Unlock() - if _, ok := s.peers[peer.ID()]; ok { - s.trienodeHealIdlers[peer.ID()] = struct{}{} - } - select { - case s.update <- struct{}{}: - default: - } - }() - s.lock.Lock() - // Ensure the response is for a valid request - req, ok := s.trienodeHealReqs[id] - if !ok { - // Request stale, perhaps the peer timed out but came through in the end - logger.Warn("Unexpected trienode heal packet") - s.lock.Unlock() - return nil - } - delete(s.trienodeHealReqs, id) - s.rates.Update(peer.ID(), TrieNodesMsg, time.Since(req.time), len(trienodes)) - - // Clean up the request timeout timer, we'll see how to proceed further based - // on the actual delivered content - if !req.timeout.Stop() { - // The timeout is already triggered, and this request will be reverted+rescheduled - s.lock.Unlock() - return nil - } - - // Response is valid, but check if peer is signalling that it does not have - // the requested data. For bytecode range queries that means the peer is not - // yet synced. - if len(trienodes) == 0 { - logger.Debug("Peer rejected trienode heal request") - s.statelessPeers[peer.ID()] = struct{}{} - s.lock.Unlock() - - // Signal this request as failed, and ready for rescheduling - s.scheduleRevertTrienodeHealRequest(req) - return nil - } - s.lock.Unlock() - - // Cross reference the requested trienodes with the response to find gaps - // that the serving node is missing - var ( - hasher = sha3.NewLegacyKeccak256().(crypto.KeccakState) - hash = make([]byte, 32) - nodes = make([][]byte, len(req.hashes)) - fills uint64 - ) - for i, j := 0, 0; i < len(trienodes); i++ { - // Find the next hash that we've been served, leaving misses with nils - hasher.Reset() - hasher.Write(trienodes[i]) - hasher.Read(hash) - - for j < len(req.hashes) && !bytes.Equal(hash, req.hashes[j][:]) { - j++ - } - if j < len(req.hashes) { - nodes[j] = trienodes[i] - fills++ - j++ - continue - } - // We've either ran out of hashes, or got unrequested data - logger.Warn("Unexpected healing trienodes", "count", len(trienodes)-i) - - // Signal this request as failed, and ready for rescheduling - s.scheduleRevertTrienodeHealRequest(req) - return errors.New("unexpected healing trienode") - } - // Response validated, send it to the scheduler for filling - s.trienodeHealPend.Add(fills) - defer func() { - s.trienodeHealPend.Add(^(fills - 1)) - }() - response := &trienodeHealResponse{ - paths: req.paths, - task: req.task, - hashes: req.hashes, - nodes: nodes, - } - select { - case req.deliver <- response: - case <-req.cancel: - case <-req.stale: - } - return nil -} - -// onHealByteCodes is a callback method to invoke when a batch of contract -// bytes codes are received from a remote peer in the healing phase. -func (s *Syncer) onHealByteCodes(peer SyncPeer, id uint64, bytecodes [][]byte) error { - var size common.StorageSize - for _, code := range bytecodes { - size += common.StorageSize(len(code)) - } - logger := peer.Log().New("reqid", id) - logger.Trace("Delivering set of healing bytecodes", "bytecodes", len(bytecodes), "bytes", size) - - // Whether or not the response is valid, we can mark the peer as idle and - // notify the scheduler to assign a new task. If the response is invalid, - // we'll drop the peer in a bit. - defer func() { - s.lock.Lock() - defer s.lock.Unlock() - if _, ok := s.peers[peer.ID()]; ok { - s.bytecodeHealIdlers[peer.ID()] = struct{}{} - } - select { - case s.update <- struct{}{}: - default: - } - }() - s.lock.Lock() - // Ensure the response is for a valid request - req, ok := s.bytecodeHealReqs[id] - if !ok { - // Request stale, perhaps the peer timed out but came through in the end - logger.Warn("Unexpected bytecode heal packet") - s.lock.Unlock() - return nil - } - delete(s.bytecodeHealReqs, id) - s.rates.Update(peer.ID(), ByteCodesMsg, time.Since(req.time), len(bytecodes)) - - // Clean up the request timeout timer, we'll see how to proceed further based - // on the actual delivered content - if !req.timeout.Stop() { - // The timeout is already triggered, and this request will be reverted+rescheduled - s.lock.Unlock() - return nil - } - - // Response is valid, but check if peer is signalling that it does not have - // the requested data. For bytecode range queries that means the peer is not - // yet synced. - if len(bytecodes) == 0 { - logger.Debug("Peer rejected bytecode heal request") - s.statelessPeers[peer.ID()] = struct{}{} - s.lock.Unlock() - - // Signal this request as failed, and ready for rescheduling - s.scheduleRevertBytecodeHealRequest(req) - return nil - } - s.lock.Unlock() - - // Cross reference the requested bytecodes with the response to find gaps - // that the serving node is missing - hasher := sha3.NewLegacyKeccak256().(crypto.KeccakState) - hash := make([]byte, 32) - - codes := make([][]byte, len(req.hashes)) - for i, j := 0, 0; i < len(bytecodes); i++ { - // Find the next hash that we've been served, leaving misses with nils - hasher.Reset() - hasher.Write(bytecodes[i]) - hasher.Read(hash) - - for j < len(req.hashes) && !bytes.Equal(hash, req.hashes[j][:]) { - j++ - } - if j < len(req.hashes) { - codes[j] = bytecodes[i] - j++ - continue - } - // We've either ran out of hashes, or got unrequested data - logger.Warn("Unexpected healing bytecodes", "count", len(bytecodes)-i) - // Signal this request as failed, and ready for rescheduling - s.scheduleRevertBytecodeHealRequest(req) - return errors.New("unexpected healing bytecode") - } - // Response validated, send it to the scheduler for filling - response := &bytecodeHealResponse{ - task: req.task, - hashes: req.hashes, - codes: codes, - } - select { - case req.deliver <- response: - case <-req.cancel: - case <-req.stale: - } - return nil -} - -// onHealState is a callback method to invoke when a flat state(account -// or storage slot) is downloaded during the healing stage. The flat states -// can be persisted blindly and can be fixed later in the generation stage. -// Note it's not concurrent safe, please handle the concurrent issue outside. -func (s *Syncer) onHealState(paths [][]byte, value []byte) error { - if len(paths) == 1 { - var account types.StateAccount - if err := rlp.DecodeBytes(value, &account); err != nil { - return nil // Returning the error here would drop the remote peer - } - blob := types.SlimAccountRLP(account) - rawdb.WriteAccountSnapshot(s.stateWriter, common.BytesToHash(paths[0]), blob) - s.accountHealed += 1 - s.accountHealedBytes += common.StorageSize(1 + common.HashLength + len(blob)) - } - if len(paths) == 2 { - rawdb.WriteStorageSnapshot(s.stateWriter, common.BytesToHash(paths[0]), common.BytesToHash(paths[1]), value) - s.storageHealed += 1 - s.storageHealedBytes += common.StorageSize(1 + 2*common.HashLength + len(value)) - } - if s.stateWriter.ValueSize() > ethdb.IdealBatchSize { - s.stateWriter.Write() // It's fine to ignore the error here - s.stateWriter.Reset() - } - return nil -} - -// hashSpace is the total size of the 256 bit hash space for accounts. -var hashSpace = new(big.Int).Exp(common.Big2, common.Big256, nil) - -// report calculates various status reports and provides it to the user. -func (s *Syncer) report(force bool) { - if len(s.tasks) > 0 { - s.reportSyncProgress(force) - return - } - s.reportHealProgress(force) -} - -// reportSyncProgress calculates various status reports and provides it to the user. -func (s *Syncer) reportSyncProgress(force bool) { - // Don't report all the events, just occasionally - if !force && time.Since(s.logTime) < 8*time.Second { - return - } - // Don't report anything until we have a meaningful progress - synced := s.accountBytes + s.bytecodeBytes + s.storageBytes - if synced == 0 { - return - } - accountGaps := new(big.Int) - for _, task := range s.tasks { - accountGaps.Add(accountGaps, new(big.Int).Sub(task.Last.Big(), task.Next.Big())) - } - accountFills := new(big.Int).Sub(hashSpace, accountGaps) - if accountFills.BitLen() == 0 { - return - } - s.logTime = time.Now() - estBytes := float64(new(big.Int).Div( - new(big.Int).Mul(new(big.Int).SetUint64(uint64(synced)), hashSpace), - accountFills, - ).Uint64()) - // Don't report anything until we have a meaningful progress - if estBytes < 1.0 { - return - } - elapsed := time.Since(s.startTime) - estTime := elapsed / time.Duration(synced) * time.Duration(estBytes) - - // Create a mega progress report - var ( - progress = fmt.Sprintf("%.2f%%", float64(synced)*100/estBytes) - accounts = fmt.Sprintf("%v@%v", log.FormatLogfmtUint64(s.accountSynced), s.accountBytes.TerminalString()) - storage = fmt.Sprintf("%v@%v", log.FormatLogfmtUint64(s.storageSynced), s.storageBytes.TerminalString()) - bytecode = fmt.Sprintf("%v@%v", log.FormatLogfmtUint64(s.bytecodeSynced), s.bytecodeBytes.TerminalString()) - ) - log.Info("Syncing: state download in progress", "synced", progress, "state", synced, - "accounts", accounts, "slots", storage, "codes", bytecode, "eta", common.PrettyDuration(estTime-elapsed)) -} - -// reportHealProgress calculates various status reports and provides it to the user. -func (s *Syncer) reportHealProgress(force bool) { - // Don't report all the events, just occasionally - if !force && time.Since(s.logTime) < 8*time.Second { - return - } - s.logTime = time.Now() - - // Create a mega progress report - var ( - trienode = fmt.Sprintf("%v@%v", log.FormatLogfmtUint64(s.trienodeHealSynced), s.trienodeHealBytes.TerminalString()) - bytecode = fmt.Sprintf("%v@%v", log.FormatLogfmtUint64(s.bytecodeHealSynced), s.bytecodeHealBytes.TerminalString()) - accounts = fmt.Sprintf("%v@%v", log.FormatLogfmtUint64(s.accountHealed), s.accountHealedBytes.TerminalString()) - storage = fmt.Sprintf("%v@%v", log.FormatLogfmtUint64(s.storageHealed), s.storageHealedBytes.TerminalString()) - ) - log.Info("Syncing: state healing in progress", "accounts", accounts, "slots", storage, - "codes", bytecode, "nodes", trienode, "pending", s.healer.scheduler.Pending()) -} - -// estimateRemainingSlots tries to determine roughly how many slots are left in -// a contract storage, based on the number of keys and the last hash. This method -// assumes that the hashes are lexicographically ordered and evenly distributed. -func estimateRemainingSlots(hashes int, last common.Hash) (uint64, error) { - if last == (common.Hash{}) { - return 0, errors.New("last hash empty") - } - space := new(big.Int).Mul(math.MaxBig256, big.NewInt(int64(hashes))) - space.Div(space, last.Big()) - if !space.IsUint64() { - // Gigantic address space probably due to too few or malicious slots - return 0, errors.New("too few slots for estimation") - } - return space.Uint64() - uint64(hashes), nil -} - -// capacitySort implements the Sort interface, allowing sorting by peer message -// throughput. Note, callers should use sort.Reverse to get the desired effect -// of highest capacity being at the front. -type capacitySort struct { - ids []string - caps []int -} - -func (s *capacitySort) Len() int { - return len(s.ids) -} - -func (s *capacitySort) Less(i, j int) bool { - return s.caps[i] < s.caps[j] -} - -func (s *capacitySort) Swap(i, j int) { - s.ids[i], s.ids[j] = s.ids[j], s.ids[i] - s.caps[i], s.caps[j] = s.caps[j], s.caps[i] -} - -// healRequestSort implements the Sort interface, allowing sorting trienode -// heal requests, which is a prerequisite for merging storage-requests. -type healRequestSort struct { - paths []string - hashes []common.Hash - syncPaths []trie.SyncPath -} - -func (t *healRequestSort) Len() int { - return len(t.hashes) -} - -func (t *healRequestSort) Less(i, j int) bool { - a := t.syncPaths[i] - b := t.syncPaths[j] - switch bytes.Compare(a[0], b[0]) { - case -1: - return true - case 1: - return false - } - // identical first part - if len(a) < len(b) { - return true - } - if len(b) < len(a) { - return false - } - if len(a) == 2 { - return bytes.Compare(a[1], b[1]) < 0 - } - return false -} - -func (t *healRequestSort) Swap(i, j int) { - t.paths[i], t.paths[j] = t.paths[j], t.paths[i] - t.hashes[i], t.hashes[j] = t.hashes[j], t.hashes[i] - t.syncPaths[i], t.syncPaths[j] = t.syncPaths[j], t.syncPaths[i] -} - -// Merge merges the pathsets, so that several storage requests concerning the -// same account are merged into one, to reduce bandwidth. -// OBS: This operation is moot if t has not first been sorted. -func (t *healRequestSort) Merge() []TrieNodePathSet { - var result []TrieNodePathSet - for _, path := range t.syncPaths { - pathset := TrieNodePathSet(path) - if len(path) == 1 { - // It's an account reference. - result = append(result, pathset) - } else { - // It's a storage reference. - end := len(result) - 1 - if len(result) == 0 || !bytes.Equal(pathset[0], result[end][0]) { - // The account doesn't match last, create a new entry. - result = append(result, pathset) - } else { - // It's the same account as the previous one, add to the storage - // paths of that request. - result[end] = append(result[end], pathset[1]) - } - } - } - return result -} - -// sortByAccountPath takes hashes and paths, and sorts them. After that, it generates -// the TrieNodePaths and merges paths which belongs to the same account path. -func sortByAccountPath(paths []string, hashes []common.Hash) ([]string, []common.Hash, []trie.SyncPath, []TrieNodePathSet) { - var syncPaths []trie.SyncPath - for _, path := range paths { - syncPaths = append(syncPaths, trie.NewSyncPath([]byte(path))) - } - n := &healRequestSort{paths, hashes, syncPaths} - sort.Sort(n) - pathsets := n.Merge() - return n.paths, n.hashes, n.syncPaths, pathsets -} diff --git a/eth/protocols/snap/sync_test.go b/eth/protocols/snap/sync_test.go deleted file mode 100644 index 5d4099a814..0000000000 --- a/eth/protocols/snap/sync_test.go +++ /dev/null @@ -1,1974 +0,0 @@ -// Copyright 2021 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 snap - -import ( - "bytes" - "crypto/rand" - "encoding/binary" - "fmt" - "math/big" - mrand "math/rand" - "sync" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/trie" - "github.com/ethereum/go-ethereum/trie/testutil" - "github.com/ethereum/go-ethereum/trie/triedb/pathdb" - "github.com/ethereum/go-ethereum/trie/trienode" - "golang.org/x/crypto/sha3" - "golang.org/x/exp/slices" -) - -func TestHashing(t *testing.T) { - t.Parallel() - - var bytecodes = make([][]byte, 10) - for i := 0; i < len(bytecodes); i++ { - buf := make([]byte, 100) - rand.Read(buf) - bytecodes[i] = buf - } - var want, got string - var old = func() { - hasher := sha3.NewLegacyKeccak256() - for i := 0; i < len(bytecodes); i++ { - hasher.Reset() - hasher.Write(bytecodes[i]) - hash := hasher.Sum(nil) - got = fmt.Sprintf("%v\n%v", got, hash) - } - } - var new = func() { - hasher := sha3.NewLegacyKeccak256().(crypto.KeccakState) - var hash = make([]byte, 32) - for i := 0; i < len(bytecodes); i++ { - hasher.Reset() - hasher.Write(bytecodes[i]) - hasher.Read(hash) - want = fmt.Sprintf("%v\n%v", want, hash) - } - } - old() - new() - if want != got { - t.Errorf("want\n%v\ngot\n%v\n", want, got) - } -} - -func BenchmarkHashing(b *testing.B) { - var bytecodes = make([][]byte, 10000) - for i := 0; i < len(bytecodes); i++ { - buf := make([]byte, 100) - rand.Read(buf) - bytecodes[i] = buf - } - var old = func() { - hasher := sha3.NewLegacyKeccak256() - for i := 0; i < len(bytecodes); i++ { - hasher.Reset() - hasher.Write(bytecodes[i]) - hasher.Sum(nil) - } - } - var new = func() { - hasher := sha3.NewLegacyKeccak256().(crypto.KeccakState) - var hash = make([]byte, 32) - for i := 0; i < len(bytecodes); i++ { - hasher.Reset() - hasher.Write(bytecodes[i]) - hasher.Read(hash) - } - } - b.Run("old", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - old() - } - }) - b.Run("new", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - new() - } - }) -} - -type ( - accountHandlerFunc func(t *testPeer, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap uint64) error - storageHandlerFunc func(t *testPeer, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max uint64) error - trieHandlerFunc func(t *testPeer, requestId uint64, root common.Hash, paths []TrieNodePathSet, cap uint64) error - codeHandlerFunc func(t *testPeer, id uint64, hashes []common.Hash, max uint64) error -) - -type testPeer struct { - id string - test *testing.T - remote *Syncer - logger log.Logger - accountTrie *trie.Trie - accountValues []*kv - storageTries map[common.Hash]*trie.Trie - storageValues map[common.Hash][]*kv - - accountRequestHandler accountHandlerFunc - storageRequestHandler storageHandlerFunc - trieRequestHandler trieHandlerFunc - codeRequestHandler codeHandlerFunc - term func() - - // counters - nAccountRequests int - nStorageRequests int - nBytecodeRequests int - nTrienodeRequests int -} - -func newTestPeer(id string, t *testing.T, term func()) *testPeer { - peer := &testPeer{ - id: id, - test: t, - logger: log.New("id", id), - accountRequestHandler: defaultAccountRequestHandler, - trieRequestHandler: defaultTrieRequestHandler, - storageRequestHandler: defaultStorageRequestHandler, - codeRequestHandler: defaultCodeRequestHandler, - term: term, - } - //stderrHandler := log.StreamHandler(os.Stderr, log.TerminalFormat(true)) - //peer.logger.SetHandler(stderrHandler) - return peer -} - -func (t *testPeer) setStorageTries(tries map[common.Hash]*trie.Trie) { - t.storageTries = make(map[common.Hash]*trie.Trie) - for root, trie := range tries { - t.storageTries[root] = trie.Copy() - } -} - -func (t *testPeer) ID() string { return t.id } -func (t *testPeer) Log() log.Logger { return t.logger } - -func (t *testPeer) Stats() string { - return fmt.Sprintf(`Account requests: %d -Storage requests: %d -Bytecode requests: %d -Trienode requests: %d -`, t.nAccountRequests, t.nStorageRequests, t.nBytecodeRequests, t.nTrienodeRequests) -} - -func (t *testPeer) RequestAccountRange(id uint64, root, origin, limit common.Hash, bytes uint64) error { - t.logger.Trace("Fetching range of accounts", "reqid", id, "root", root, "origin", origin, "limit", limit, "bytes", common.StorageSize(bytes)) - t.nAccountRequests++ - go t.accountRequestHandler(t, id, root, origin, limit, bytes) - return nil -} - -func (t *testPeer) RequestTrieNodes(id uint64, root common.Hash, paths []TrieNodePathSet, bytes uint64) error { - t.logger.Trace("Fetching set of trie nodes", "reqid", id, "root", root, "pathsets", len(paths), "bytes", common.StorageSize(bytes)) - t.nTrienodeRequests++ - go t.trieRequestHandler(t, id, root, paths, bytes) - return nil -} - -func (t *testPeer) RequestStorageRanges(id uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, bytes uint64) error { - t.nStorageRequests++ - if len(accounts) == 1 && origin != nil { - t.logger.Trace("Fetching range of large storage slots", "reqid", id, "root", root, "account", accounts[0], "origin", common.BytesToHash(origin), "limit", common.BytesToHash(limit), "bytes", common.StorageSize(bytes)) - } else { - t.logger.Trace("Fetching ranges of small storage slots", "reqid", id, "root", root, "accounts", len(accounts), "first", accounts[0], "bytes", common.StorageSize(bytes)) - } - go t.storageRequestHandler(t, id, root, accounts, origin, limit, bytes) - return nil -} - -func (t *testPeer) RequestByteCodes(id uint64, hashes []common.Hash, bytes uint64) error { - t.nBytecodeRequests++ - t.logger.Trace("Fetching set of byte codes", "reqid", id, "hashes", len(hashes), "bytes", common.StorageSize(bytes)) - go t.codeRequestHandler(t, id, hashes, bytes) - return nil -} - -// defaultTrieRequestHandler is a well-behaving handler for trie healing requests -func defaultTrieRequestHandler(t *testPeer, requestId uint64, root common.Hash, paths []TrieNodePathSet, cap uint64) error { - // Pass the response - var nodes [][]byte - for _, pathset := range paths { - switch len(pathset) { - case 1: - blob, _, err := t.accountTrie.GetNode(pathset[0]) - if err != nil { - t.logger.Info("Error handling req", "error", err) - break - } - nodes = append(nodes, blob) - default: - account := t.storageTries[(common.BytesToHash(pathset[0]))] - for _, path := range pathset[1:] { - blob, _, err := account.GetNode(path) - if err != nil { - t.logger.Info("Error handling req", "error", err) - break - } - nodes = append(nodes, blob) - } - } - } - t.remote.OnTrieNodes(t, requestId, nodes) - return nil -} - -// defaultAccountRequestHandler is a well-behaving handler for AccountRangeRequests -func defaultAccountRequestHandler(t *testPeer, id uint64, root common.Hash, origin common.Hash, limit common.Hash, cap uint64) error { - keys, vals, proofs := createAccountRequestResponse(t, root, origin, limit, cap) - if err := t.remote.OnAccounts(t, id, keys, vals, proofs); err != nil { - t.test.Errorf("Remote side rejected our delivery: %v", err) - t.term() - return err - } - return nil -} - -func createAccountRequestResponse(t *testPeer, root common.Hash, origin common.Hash, limit common.Hash, cap uint64) (keys []common.Hash, vals [][]byte, proofs [][]byte) { - var size uint64 - if limit == (common.Hash{}) { - limit = common.MaxHash - } - for _, entry := range t.accountValues { - if size > cap { - break - } - if bytes.Compare(origin[:], entry.k) <= 0 { - keys = append(keys, common.BytesToHash(entry.k)) - vals = append(vals, entry.v) - size += uint64(32 + len(entry.v)) - } - // If we've exceeded the request threshold, abort - if bytes.Compare(entry.k, limit[:]) >= 0 { - break - } - } - // Unless we send the entire trie, we need to supply proofs - // Actually, we need to supply proofs either way! This seems to be an implementation - // quirk in go-ethereum - proof := trienode.NewProofSet() - if err := t.accountTrie.Prove(origin[:], proof); err != nil { - t.logger.Error("Could not prove inexistence of origin", "origin", origin, "error", err) - } - if len(keys) > 0 { - lastK := (keys[len(keys)-1])[:] - if err := t.accountTrie.Prove(lastK, proof); err != nil { - t.logger.Error("Could not prove last item", "error", err) - } - } - for _, blob := range proof.List() { - proofs = append(proofs, blob) - } - return keys, vals, proofs -} - -// defaultStorageRequestHandler is a well-behaving storage request handler -func defaultStorageRequestHandler(t *testPeer, requestId uint64, root common.Hash, accounts []common.Hash, bOrigin, bLimit []byte, max uint64) error { - hashes, slots, proofs := createStorageRequestResponse(t, root, accounts, bOrigin, bLimit, max) - if err := t.remote.OnStorage(t, requestId, hashes, slots, proofs); err != nil { - t.test.Errorf("Remote side rejected our delivery: %v", err) - t.term() - } - return nil -} - -func defaultCodeRequestHandler(t *testPeer, id uint64, hashes []common.Hash, max uint64) error { - var bytecodes [][]byte - for _, h := range hashes { - bytecodes = append(bytecodes, getCodeByHash(h)) - } - if err := t.remote.OnByteCodes(t, id, bytecodes); err != nil { - t.test.Errorf("Remote side rejected our delivery: %v", err) - t.term() - } - return nil -} - -func createStorageRequestResponse(t *testPeer, root common.Hash, accounts []common.Hash, origin, limit []byte, max uint64) (hashes [][]common.Hash, slots [][][]byte, proofs [][]byte) { - var size uint64 - for _, account := range accounts { - // The first account might start from a different origin and end sooner - var originHash common.Hash - if len(origin) > 0 { - originHash = common.BytesToHash(origin) - } - var limitHash = common.MaxHash - if len(limit) > 0 { - limitHash = common.BytesToHash(limit) - } - var ( - keys []common.Hash - vals [][]byte - abort bool - ) - for _, entry := range t.storageValues[account] { - if size >= max { - abort = true - break - } - if bytes.Compare(entry.k, originHash[:]) < 0 { - continue - } - keys = append(keys, common.BytesToHash(entry.k)) - vals = append(vals, entry.v) - size += uint64(32 + len(entry.v)) - if bytes.Compare(entry.k, limitHash[:]) >= 0 { - break - } - } - if len(keys) > 0 { - hashes = append(hashes, keys) - slots = append(slots, vals) - } - // Generate the Merkle proofs for the first and last storage slot, but - // only if the response was capped. If the entire storage trie included - // in the response, no need for any proofs. - if originHash != (common.Hash{}) || (abort && len(keys) > 0) { - // If we're aborting, we need to prove the first and last item - // This terminates the response (and thus the loop) - proof := trienode.NewProofSet() - stTrie := t.storageTries[account] - - // Here's a potential gotcha: when constructing the proof, we cannot - // use the 'origin' slice directly, but must use the full 32-byte - // hash form. - if err := stTrie.Prove(originHash[:], proof); err != nil { - t.logger.Error("Could not prove inexistence of origin", "origin", originHash, "error", err) - } - if len(keys) > 0 { - lastK := (keys[len(keys)-1])[:] - if err := stTrie.Prove(lastK, proof); err != nil { - t.logger.Error("Could not prove last item", "error", err) - } - } - for _, blob := range proof.List() { - proofs = append(proofs, blob) - } - break - } - } - return hashes, slots, proofs -} - -// createStorageRequestResponseAlwaysProve tests a cornercase, where the peer always -// supplies the proof for the last account, even if it is 'complete'. -func createStorageRequestResponseAlwaysProve(t *testPeer, root common.Hash, accounts []common.Hash, bOrigin, bLimit []byte, max uint64) (hashes [][]common.Hash, slots [][][]byte, proofs [][]byte) { - var size uint64 - max = max * 3 / 4 - - var origin common.Hash - if len(bOrigin) > 0 { - origin = common.BytesToHash(bOrigin) - } - var exit bool - for i, account := range accounts { - var keys []common.Hash - var vals [][]byte - for _, entry := range t.storageValues[account] { - if bytes.Compare(entry.k, origin[:]) < 0 { - exit = true - } - keys = append(keys, common.BytesToHash(entry.k)) - vals = append(vals, entry.v) - size += uint64(32 + len(entry.v)) - if size > max { - exit = true - } - } - if i == len(accounts)-1 { - exit = true - } - hashes = append(hashes, keys) - slots = append(slots, vals) - - if exit { - // If we're aborting, we need to prove the first and last item - // This terminates the response (and thus the loop) - proof := trienode.NewProofSet() - stTrie := t.storageTries[account] - - // Here's a potential gotcha: when constructing the proof, we cannot - // use the 'origin' slice directly, but must use the full 32-byte - // hash form. - if err := stTrie.Prove(origin[:], proof); err != nil { - t.logger.Error("Could not prove inexistence of origin", "origin", origin, - "error", err) - } - if len(keys) > 0 { - lastK := (keys[len(keys)-1])[:] - if err := stTrie.Prove(lastK, proof); err != nil { - t.logger.Error("Could not prove last item", "error", err) - } - } - for _, blob := range proof.List() { - proofs = append(proofs, blob) - } - break - } - } - return hashes, slots, proofs -} - -// emptyRequestAccountRangeFn is a rejects AccountRangeRequests -func emptyRequestAccountRangeFn(t *testPeer, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap uint64) error { - t.remote.OnAccounts(t, requestId, nil, nil, nil) - return nil -} - -func nonResponsiveRequestAccountRangeFn(t *testPeer, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap uint64) error { - return nil -} - -func emptyTrieRequestHandler(t *testPeer, requestId uint64, root common.Hash, paths []TrieNodePathSet, cap uint64) error { - t.remote.OnTrieNodes(t, requestId, nil) - return nil -} - -func nonResponsiveTrieRequestHandler(t *testPeer, requestId uint64, root common.Hash, paths []TrieNodePathSet, cap uint64) error { - return nil -} - -func emptyStorageRequestHandler(t *testPeer, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max uint64) error { - t.remote.OnStorage(t, requestId, nil, nil, nil) - return nil -} - -func nonResponsiveStorageRequestHandler(t *testPeer, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max uint64) error { - return nil -} - -func proofHappyStorageRequestHandler(t *testPeer, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max uint64) error { - hashes, slots, proofs := createStorageRequestResponseAlwaysProve(t, root, accounts, origin, limit, max) - if err := t.remote.OnStorage(t, requestId, hashes, slots, proofs); err != nil { - t.test.Errorf("Remote side rejected our delivery: %v", err) - t.term() - } - return nil -} - -//func emptyCodeRequestHandler(t *testPeer, id uint64, hashes []common.Hash, max uint64) error { -// var bytecodes [][]byte -// t.remote.OnByteCodes(t, id, bytecodes) -// return nil -//} - -func corruptCodeRequestHandler(t *testPeer, id uint64, hashes []common.Hash, max uint64) error { - var bytecodes [][]byte - for _, h := range hashes { - // Send back the hashes - bytecodes = append(bytecodes, h[:]) - } - if err := t.remote.OnByteCodes(t, id, bytecodes); err != nil { - t.logger.Info("remote error on delivery (as expected)", "error", err) - // Mimic the real-life handler, which drops a peer on errors - t.remote.Unregister(t.id) - } - return nil -} - -func cappedCodeRequestHandler(t *testPeer, id uint64, hashes []common.Hash, max uint64) error { - var bytecodes [][]byte - for _, h := range hashes[:1] { - bytecodes = append(bytecodes, getCodeByHash(h)) - } - // Missing bytecode can be retrieved again, no error expected - if err := t.remote.OnByteCodes(t, id, bytecodes); err != nil { - t.test.Errorf("Remote side rejected our delivery: %v", err) - t.term() - } - return nil -} - -// starvingStorageRequestHandler is somewhat well-behaving storage handler, but it caps the returned results to be very small -func starvingStorageRequestHandler(t *testPeer, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max uint64) error { - return defaultStorageRequestHandler(t, requestId, root, accounts, origin, limit, 500) -} - -func starvingAccountRequestHandler(t *testPeer, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap uint64) error { - return defaultAccountRequestHandler(t, requestId, root, origin, limit, 500) -} - -//func misdeliveringAccountRequestHandler(t *testPeer, requestId uint64, root common.Hash, origin common.Hash, cap uint64) error { -// return defaultAccountRequestHandler(t, requestId-1, root, origin, 500) -//} - -func corruptAccountRequestHandler(t *testPeer, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap uint64) error { - hashes, accounts, proofs := createAccountRequestResponse(t, root, origin, limit, cap) - if len(proofs) > 0 { - proofs = proofs[1:] - } - if err := t.remote.OnAccounts(t, requestId, hashes, accounts, proofs); err != nil { - t.logger.Info("remote error on delivery (as expected)", "error", err) - // Mimic the real-life handler, which drops a peer on errors - t.remote.Unregister(t.id) - } - return nil -} - -// corruptStorageRequestHandler doesn't provide good proofs -func corruptStorageRequestHandler(t *testPeer, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max uint64) error { - hashes, slots, proofs := createStorageRequestResponse(t, root, accounts, origin, limit, max) - if len(proofs) > 0 { - proofs = proofs[1:] - } - if err := t.remote.OnStorage(t, requestId, hashes, slots, proofs); err != nil { - t.logger.Info("remote error on delivery (as expected)", "error", err) - // Mimic the real-life handler, which drops a peer on errors - t.remote.Unregister(t.id) - } - return nil -} - -func noProofStorageRequestHandler(t *testPeer, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max uint64) error { - hashes, slots, _ := createStorageRequestResponse(t, root, accounts, origin, limit, max) - if err := t.remote.OnStorage(t, requestId, hashes, slots, nil); err != nil { - t.logger.Info("remote error on delivery (as expected)", "error", err) - // Mimic the real-life handler, which drops a peer on errors - t.remote.Unregister(t.id) - } - return nil -} - -// TestSyncBloatedProof tests a scenario where we provide only _one_ value, but -// also ship the entire trie inside the proof. If the attack is successful, -// the remote side does not do any follow-up requests -func TestSyncBloatedProof(t *testing.T) { - t.Parallel() - - testSyncBloatedProof(t, rawdb.HashScheme) - testSyncBloatedProof(t, rawdb.PathScheme) -} - -func testSyncBloatedProof(t *testing.T, scheme string) { - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme) - source := newTestPeer("source", t, term) - source.accountTrie = sourceAccountTrie.Copy() - source.accountValues = elems - - source.accountRequestHandler = func(t *testPeer, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap uint64) error { - var ( - proofs [][]byte - keys []common.Hash - vals [][]byte - ) - // The values - for _, entry := range t.accountValues { - if bytes.Compare(entry.k, origin[:]) < 0 { - continue - } - if bytes.Compare(entry.k, limit[:]) > 0 { - continue - } - keys = append(keys, common.BytesToHash(entry.k)) - vals = append(vals, entry.v) - } - // The proofs - proof := trienode.NewProofSet() - if err := t.accountTrie.Prove(origin[:], proof); err != nil { - t.logger.Error("Could not prove origin", "origin", origin, "error", err) - t.logger.Error("Could not prove origin", "origin", origin, "error", err) - } - // The bloat: add proof of every single element - for _, entry := range t.accountValues { - if err := t.accountTrie.Prove(entry.k, proof); err != nil { - t.logger.Error("Could not prove item", "error", err) - } - } - // And remove one item from the elements - if len(keys) > 2 { - keys = append(keys[:1], keys[2:]...) - vals = append(vals[:1], vals[2:]...) - } - for _, blob := range proof.List() { - proofs = append(proofs, blob) - } - if err := t.remote.OnAccounts(t, requestId, keys, vals, proofs); err != nil { - t.logger.Info("remote error on delivery (as expected)", "error", err) - t.term() - // This is actually correct, signal to exit the test successfully - } - return nil - } - syncer := setupSyncer(nodeScheme, source) - if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err == nil { - t.Fatal("No error returned from incomplete/cancelled sync") - } -} - -func setupSyncer(scheme string, peers ...*testPeer) *Syncer { - stateDb := rawdb.NewMemoryDatabase() - syncer := NewSyncer(stateDb, scheme) - for _, peer := range peers { - syncer.Register(peer) - peer.remote = syncer - } - return syncer -} - -// TestSync tests a basic sync with one peer -func TestSync(t *testing.T) { - t.Parallel() - - testSync(t, rawdb.HashScheme) - testSync(t, rawdb.PathScheme) -} - -func testSync(t *testing.T, scheme string) { - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme) - - mkSource := func(name string) *testPeer { - source := newTestPeer(name, t, term) - source.accountTrie = sourceAccountTrie.Copy() - source.accountValues = elems - return source - } - syncer := setupSyncer(nodeScheme, mkSource("source")) - if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { - t.Fatalf("sync failed: %v", err) - } - verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) -} - -// TestSyncTinyTriePanic tests a basic sync with one peer, and a tiny trie. This caused a -// panic within the prover -func TestSyncTinyTriePanic(t *testing.T) { - t.Parallel() - - testSyncTinyTriePanic(t, rawdb.HashScheme) - testSyncTinyTriePanic(t, rawdb.PathScheme) -} - -func testSyncTinyTriePanic(t *testing.T, scheme string) { - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(1, scheme) - - mkSource := func(name string) *testPeer { - source := newTestPeer(name, t, term) - source.accountTrie = sourceAccountTrie.Copy() - source.accountValues = elems - return source - } - syncer := setupSyncer(nodeScheme, mkSource("source")) - done := checkStall(t, term) - if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { - t.Fatalf("sync failed: %v", err) - } - close(done) - verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) -} - -// TestMultiSync tests a basic sync with multiple peers -func TestMultiSync(t *testing.T) { - t.Parallel() - - testMultiSync(t, rawdb.HashScheme) - testMultiSync(t, rawdb.PathScheme) -} - -func testMultiSync(t *testing.T, scheme string) { - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme) - - mkSource := func(name string) *testPeer { - source := newTestPeer(name, t, term) - source.accountTrie = sourceAccountTrie.Copy() - source.accountValues = elems - return source - } - syncer := setupSyncer(nodeScheme, mkSource("sourceA"), mkSource("sourceB")) - done := checkStall(t, term) - if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { - t.Fatalf("sync failed: %v", err) - } - close(done) - verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) -} - -// TestSyncWithStorage tests basic sync using accounts + storage + code -func TestSyncWithStorage(t *testing.T) { - t.Parallel() - - testSyncWithStorage(t, rawdb.HashScheme) - testSyncWithStorage(t, rawdb.PathScheme) -} - -func testSyncWithStorage(t *testing.T, scheme string) { - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 3, 3000, true, false, false) - - mkSource := func(name string) *testPeer { - source := newTestPeer(name, t, term) - source.accountTrie = sourceAccountTrie.Copy() - source.accountValues = elems - source.setStorageTries(storageTries) - source.storageValues = storageElems - return source - } - syncer := setupSyncer(scheme, mkSource("sourceA")) - done := checkStall(t, term) - if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { - t.Fatalf("sync failed: %v", err) - } - close(done) - verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) -} - -// TestMultiSyncManyUseless contains one good peer, and many which doesn't return anything valuable at all -func TestMultiSyncManyUseless(t *testing.T) { - t.Parallel() - - testMultiSyncManyUseless(t, rawdb.HashScheme) - testMultiSyncManyUseless(t, rawdb.PathScheme) -} - -func testMultiSyncManyUseless(t *testing.T, scheme string) { - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false) - - mkSource := func(name string, noAccount, noStorage, noTrieNode bool) *testPeer { - source := newTestPeer(name, t, term) - source.accountTrie = sourceAccountTrie.Copy() - source.accountValues = elems - source.setStorageTries(storageTries) - source.storageValues = storageElems - - if !noAccount { - source.accountRequestHandler = emptyRequestAccountRangeFn - } - if !noStorage { - source.storageRequestHandler = emptyStorageRequestHandler - } - if !noTrieNode { - source.trieRequestHandler = emptyTrieRequestHandler - } - return source - } - - syncer := setupSyncer( - scheme, - mkSource("full", true, true, true), - mkSource("noAccounts", false, true, true), - mkSource("noStorage", true, false, true), - mkSource("noTrie", true, true, false), - ) - done := checkStall(t, term) - if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { - t.Fatalf("sync failed: %v", err) - } - close(done) - verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) -} - -// TestMultiSyncManyUseless contains one good peer, and many which doesn't return anything valuable at all -func TestMultiSyncManyUselessWithLowTimeout(t *testing.T) { - t.Parallel() - - testMultiSyncManyUselessWithLowTimeout(t, rawdb.HashScheme) - testMultiSyncManyUselessWithLowTimeout(t, rawdb.PathScheme) -} - -func testMultiSyncManyUselessWithLowTimeout(t *testing.T, scheme string) { - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false) - - mkSource := func(name string, noAccount, noStorage, noTrieNode bool) *testPeer { - source := newTestPeer(name, t, term) - source.accountTrie = sourceAccountTrie.Copy() - source.accountValues = elems - source.setStorageTries(storageTries) - source.storageValues = storageElems - - if !noAccount { - source.accountRequestHandler = emptyRequestAccountRangeFn - } - if !noStorage { - source.storageRequestHandler = emptyStorageRequestHandler - } - if !noTrieNode { - source.trieRequestHandler = emptyTrieRequestHandler - } - return source - } - - syncer := setupSyncer( - scheme, - mkSource("full", true, true, true), - mkSource("noAccounts", false, true, true), - mkSource("noStorage", true, false, true), - mkSource("noTrie", true, true, false), - ) - // We're setting the timeout to very low, to increase the chance of the timeout - // being triggered. This was previously a cause of panic, when a response - // arrived simultaneously as a timeout was triggered. - syncer.rates.OverrideTTLLimit = time.Millisecond - - done := checkStall(t, term) - if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { - t.Fatalf("sync failed: %v", err) - } - close(done) - verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) -} - -// TestMultiSyncManyUnresponsive contains one good peer, and many which doesn't respond at all -func TestMultiSyncManyUnresponsive(t *testing.T) { - t.Parallel() - - testMultiSyncManyUnresponsive(t, rawdb.HashScheme) - testMultiSyncManyUnresponsive(t, rawdb.PathScheme) -} - -func testMultiSyncManyUnresponsive(t *testing.T, scheme string) { - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false) - - mkSource := func(name string, noAccount, noStorage, noTrieNode bool) *testPeer { - source := newTestPeer(name, t, term) - source.accountTrie = sourceAccountTrie.Copy() - source.accountValues = elems - source.setStorageTries(storageTries) - source.storageValues = storageElems - - if !noAccount { - source.accountRequestHandler = nonResponsiveRequestAccountRangeFn - } - if !noStorage { - source.storageRequestHandler = nonResponsiveStorageRequestHandler - } - if !noTrieNode { - source.trieRequestHandler = nonResponsiveTrieRequestHandler - } - return source - } - - syncer := setupSyncer( - scheme, - mkSource("full", true, true, true), - mkSource("noAccounts", false, true, true), - mkSource("noStorage", true, false, true), - mkSource("noTrie", true, true, false), - ) - // We're setting the timeout to very low, to make the test run a bit faster - syncer.rates.OverrideTTLLimit = time.Millisecond - - done := checkStall(t, term) - if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { - t.Fatalf("sync failed: %v", err) - } - close(done) - verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) -} - -func checkStall(t *testing.T, term func()) chan struct{} { - testDone := make(chan struct{}) - go func() { - select { - case <-time.After(time.Minute): // TODO(karalabe): Make tests smaller, this is too much - t.Log("Sync stalled") - term() - case <-testDone: - return - } - }() - return testDone -} - -// TestSyncBoundaryAccountTrie tests sync against a few normal peers, but the -// account trie has a few boundary elements. -func TestSyncBoundaryAccountTrie(t *testing.T) { - t.Parallel() - - testSyncBoundaryAccountTrie(t, rawdb.HashScheme) - testSyncBoundaryAccountTrie(t, rawdb.PathScheme) -} - -func testSyncBoundaryAccountTrie(t *testing.T, scheme string) { - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - nodeScheme, sourceAccountTrie, elems := makeBoundaryAccountTrie(scheme, 3000) - - mkSource := func(name string) *testPeer { - source := newTestPeer(name, t, term) - source.accountTrie = sourceAccountTrie.Copy() - source.accountValues = elems - return source - } - syncer := setupSyncer( - nodeScheme, - mkSource("peer-a"), - mkSource("peer-b"), - ) - done := checkStall(t, term) - if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { - t.Fatalf("sync failed: %v", err) - } - close(done) - verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) -} - -// TestSyncNoStorageAndOneCappedPeer tests sync using accounts and no storage, where one peer is -// consistently returning very small results -func TestSyncNoStorageAndOneCappedPeer(t *testing.T) { - t.Parallel() - - testSyncNoStorageAndOneCappedPeer(t, rawdb.HashScheme) - testSyncNoStorageAndOneCappedPeer(t, rawdb.PathScheme) -} - -func testSyncNoStorageAndOneCappedPeer(t *testing.T, scheme string) { - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(3000, scheme) - - mkSource := func(name string, slow bool) *testPeer { - source := newTestPeer(name, t, term) - source.accountTrie = sourceAccountTrie.Copy() - source.accountValues = elems - - if slow { - source.accountRequestHandler = starvingAccountRequestHandler - } - return source - } - - syncer := setupSyncer( - nodeScheme, - mkSource("nice-a", false), - mkSource("nice-b", false), - mkSource("nice-c", false), - mkSource("capped", true), - ) - done := checkStall(t, term) - if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { - t.Fatalf("sync failed: %v", err) - } - close(done) - verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) -} - -// TestSyncNoStorageAndOneCodeCorruptPeer has one peer which doesn't deliver -// code requests properly. -func TestSyncNoStorageAndOneCodeCorruptPeer(t *testing.T) { - t.Parallel() - - testSyncNoStorageAndOneCodeCorruptPeer(t, rawdb.HashScheme) - testSyncNoStorageAndOneCodeCorruptPeer(t, rawdb.PathScheme) -} - -func testSyncNoStorageAndOneCodeCorruptPeer(t *testing.T, scheme string) { - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(3000, scheme) - - mkSource := func(name string, codeFn codeHandlerFunc) *testPeer { - source := newTestPeer(name, t, term) - source.accountTrie = sourceAccountTrie.Copy() - source.accountValues = elems - source.codeRequestHandler = codeFn - return source - } - // One is capped, one is corrupt. If we don't use a capped one, there's a 50% - // chance that the full set of codes requested are sent only to the - // non-corrupt peer, which delivers everything in one go, and makes the - // test moot - syncer := setupSyncer( - nodeScheme, - mkSource("capped", cappedCodeRequestHandler), - mkSource("corrupt", corruptCodeRequestHandler), - ) - done := checkStall(t, term) - if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { - t.Fatalf("sync failed: %v", err) - } - close(done) - verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) -} - -func TestSyncNoStorageAndOneAccountCorruptPeer(t *testing.T) { - t.Parallel() - - testSyncNoStorageAndOneAccountCorruptPeer(t, rawdb.HashScheme) - testSyncNoStorageAndOneAccountCorruptPeer(t, rawdb.PathScheme) -} - -func testSyncNoStorageAndOneAccountCorruptPeer(t *testing.T, scheme string) { - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(3000, scheme) - - mkSource := func(name string, accFn accountHandlerFunc) *testPeer { - source := newTestPeer(name, t, term) - source.accountTrie = sourceAccountTrie.Copy() - source.accountValues = elems - source.accountRequestHandler = accFn - return source - } - // One is capped, one is corrupt. If we don't use a capped one, there's a 50% - // chance that the full set of codes requested are sent only to the - // non-corrupt peer, which delivers everything in one go, and makes the - // test moot - syncer := setupSyncer( - nodeScheme, - mkSource("capped", defaultAccountRequestHandler), - mkSource("corrupt", corruptAccountRequestHandler), - ) - done := checkStall(t, term) - if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { - t.Fatalf("sync failed: %v", err) - } - close(done) - verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) -} - -// TestSyncNoStorageAndOneCodeCappedPeer has one peer which delivers code hashes -// one by one -func TestSyncNoStorageAndOneCodeCappedPeer(t *testing.T) { - t.Parallel() - - testSyncNoStorageAndOneCodeCappedPeer(t, rawdb.HashScheme) - testSyncNoStorageAndOneCodeCappedPeer(t, rawdb.PathScheme) -} - -func testSyncNoStorageAndOneCodeCappedPeer(t *testing.T, scheme string) { - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(3000, scheme) - - mkSource := func(name string, codeFn codeHandlerFunc) *testPeer { - source := newTestPeer(name, t, term) - source.accountTrie = sourceAccountTrie.Copy() - source.accountValues = elems - source.codeRequestHandler = codeFn - return source - } - // Count how many times it's invoked. Remember, there are only 8 unique hashes, - // so it shouldn't be more than that - var counter int - syncer := setupSyncer( - nodeScheme, - mkSource("capped", func(t *testPeer, id uint64, hashes []common.Hash, max uint64) error { - counter++ - return cappedCodeRequestHandler(t, id, hashes, max) - }), - ) - done := checkStall(t, term) - if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { - t.Fatalf("sync failed: %v", err) - } - close(done) - - // There are only 8 unique hashes, and 3K accounts. However, the code - // deduplication is per request batch. If it were a perfect global dedup, - // we would expect only 8 requests. If there were no dedup, there would be - // 3k requests. - // We expect somewhere below 100 requests for these 8 unique hashes. But - // the number can be flaky, so don't limit it so strictly. - if threshold := 100; counter > threshold { - t.Logf("Error, expected < %d invocations, got %d", threshold, counter) - } - verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) -} - -// TestSyncBoundaryStorageTrie tests sync against a few normal peers, but the -// storage trie has a few boundary elements. -func TestSyncBoundaryStorageTrie(t *testing.T) { - t.Parallel() - - testSyncBoundaryStorageTrie(t, rawdb.HashScheme) - testSyncBoundaryStorageTrie(t, rawdb.PathScheme) -} - -func testSyncBoundaryStorageTrie(t *testing.T, scheme string) { - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 10, 1000, false, true, false) - - mkSource := func(name string) *testPeer { - source := newTestPeer(name, t, term) - source.accountTrie = sourceAccountTrie.Copy() - source.accountValues = elems - source.setStorageTries(storageTries) - source.storageValues = storageElems - return source - } - syncer := setupSyncer( - scheme, - mkSource("peer-a"), - mkSource("peer-b"), - ) - done := checkStall(t, term) - if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { - t.Fatalf("sync failed: %v", err) - } - close(done) - verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) -} - -// TestSyncWithStorageAndOneCappedPeer tests sync using accounts + storage, where one peer is -// consistently returning very small results -func TestSyncWithStorageAndOneCappedPeer(t *testing.T) { - t.Parallel() - - testSyncWithStorageAndOneCappedPeer(t, rawdb.HashScheme) - testSyncWithStorageAndOneCappedPeer(t, rawdb.PathScheme) -} - -func testSyncWithStorageAndOneCappedPeer(t *testing.T, scheme string) { - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 300, 1000, false, false, false) - - mkSource := func(name string, slow bool) *testPeer { - source := newTestPeer(name, t, term) - source.accountTrie = sourceAccountTrie.Copy() - source.accountValues = elems - source.setStorageTries(storageTries) - source.storageValues = storageElems - - if slow { - source.storageRequestHandler = starvingStorageRequestHandler - } - return source - } - - syncer := setupSyncer( - scheme, - mkSource("nice-a", false), - mkSource("slow", true), - ) - done := checkStall(t, term) - if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { - t.Fatalf("sync failed: %v", err) - } - close(done) - verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) -} - -// TestSyncWithStorageAndCorruptPeer tests sync using accounts + storage, where one peer is -// sometimes sending bad proofs -func TestSyncWithStorageAndCorruptPeer(t *testing.T) { - t.Parallel() - - testSyncWithStorageAndCorruptPeer(t, rawdb.HashScheme) - testSyncWithStorageAndCorruptPeer(t, rawdb.PathScheme) -} - -func testSyncWithStorageAndCorruptPeer(t *testing.T, scheme string) { - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false) - - mkSource := func(name string, handler storageHandlerFunc) *testPeer { - source := newTestPeer(name, t, term) - source.accountTrie = sourceAccountTrie.Copy() - source.accountValues = elems - source.setStorageTries(storageTries) - source.storageValues = storageElems - source.storageRequestHandler = handler - return source - } - - syncer := setupSyncer( - scheme, - mkSource("nice-a", defaultStorageRequestHandler), - mkSource("nice-b", defaultStorageRequestHandler), - mkSource("nice-c", defaultStorageRequestHandler), - mkSource("corrupt", corruptStorageRequestHandler), - ) - done := checkStall(t, term) - if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { - t.Fatalf("sync failed: %v", err) - } - close(done) - verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) -} - -func TestSyncWithStorageAndNonProvingPeer(t *testing.T) { - t.Parallel() - - testSyncWithStorageAndNonProvingPeer(t, rawdb.HashScheme) - testSyncWithStorageAndNonProvingPeer(t, rawdb.PathScheme) -} - -func testSyncWithStorageAndNonProvingPeer(t *testing.T, scheme string) { - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false) - - mkSource := func(name string, handler storageHandlerFunc) *testPeer { - source := newTestPeer(name, t, term) - source.accountTrie = sourceAccountTrie.Copy() - source.accountValues = elems - source.setStorageTries(storageTries) - source.storageValues = storageElems - source.storageRequestHandler = handler - return source - } - syncer := setupSyncer( - scheme, - mkSource("nice-a", defaultStorageRequestHandler), - mkSource("nice-b", defaultStorageRequestHandler), - mkSource("nice-c", defaultStorageRequestHandler), - mkSource("corrupt", noProofStorageRequestHandler), - ) - done := checkStall(t, term) - if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { - t.Fatalf("sync failed: %v", err) - } - close(done) - verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) -} - -// TestSyncWithStorage tests basic sync using accounts + storage + code, against -// a peer who insists on delivering full storage sets _and_ proofs. This triggered -// an error, where the recipient erroneously clipped the boundary nodes, but -// did not mark the account for healing. -func TestSyncWithStorageMisbehavingProve(t *testing.T) { - t.Parallel() - - testSyncWithStorageMisbehavingProve(t, rawdb.HashScheme) - testSyncWithStorageMisbehavingProve(t, rawdb.PathScheme) -} - -func testSyncWithStorageMisbehavingProve(t *testing.T, scheme string) { - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorageWithUniqueStorage(scheme, 10, 30, false) - - mkSource := func(name string) *testPeer { - source := newTestPeer(name, t, term) - source.accountTrie = sourceAccountTrie.Copy() - source.accountValues = elems - source.setStorageTries(storageTries) - source.storageValues = storageElems - source.storageRequestHandler = proofHappyStorageRequestHandler - return source - } - syncer := setupSyncer(nodeScheme, mkSource("sourceA")) - if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { - t.Fatalf("sync failed: %v", err) - } - verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) -} - -// TestSyncWithUnevenStorage tests sync where the storage trie is not even -// and with a few empty ranges. -func TestSyncWithUnevenStorage(t *testing.T) { - t.Parallel() - - testSyncWithUnevenStorage(t, rawdb.HashScheme) - testSyncWithUnevenStorage(t, rawdb.PathScheme) -} - -func testSyncWithUnevenStorage(t *testing.T, scheme string) { - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - accountTrie, accounts, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 3, 256, false, false, true) - - mkSource := func(name string) *testPeer { - source := newTestPeer(name, t, term) - source.accountTrie = accountTrie.Copy() - source.accountValues = accounts - source.setStorageTries(storageTries) - source.storageValues = storageElems - source.storageRequestHandler = func(t *testPeer, reqId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max uint64) error { - return defaultStorageRequestHandler(t, reqId, root, accounts, origin, limit, 128) // retrieve storage in large mode - } - return source - } - syncer := setupSyncer(scheme, mkSource("source")) - if err := syncer.Sync(accountTrie.Hash(), cancel); err != nil { - t.Fatalf("sync failed: %v", err) - } - verifyTrie(scheme, syncer.db, accountTrie.Hash(), t) -} - -type kv struct { - k, v []byte -} - -func (k *kv) cmp(other *kv) int { - return bytes.Compare(k.k, other.k) -} - -func key32(i uint64) []byte { - key := make([]byte, 32) - binary.LittleEndian.PutUint64(key, i) - return key -} - -var ( - codehashes = []common.Hash{ - crypto.Keccak256Hash([]byte{0}), - crypto.Keccak256Hash([]byte{1}), - crypto.Keccak256Hash([]byte{2}), - crypto.Keccak256Hash([]byte{3}), - crypto.Keccak256Hash([]byte{4}), - crypto.Keccak256Hash([]byte{5}), - crypto.Keccak256Hash([]byte{6}), - crypto.Keccak256Hash([]byte{7}), - } -) - -// getCodeHash returns a pseudo-random code hash -func getCodeHash(i uint64) []byte { - h := codehashes[int(i)%len(codehashes)] - return common.CopyBytes(h[:]) -} - -// getCodeByHash convenience function to lookup the code from the code hash -func getCodeByHash(hash common.Hash) []byte { - if hash == types.EmptyCodeHash { - return nil - } - for i, h := range codehashes { - if h == hash { - return []byte{byte(i)} - } - } - return nil -} - -// makeAccountTrieNoStorage spits out a trie, along with the leafs -func makeAccountTrieNoStorage(n int, scheme string) (string, *trie.Trie, []*kv) { - var ( - db = trie.NewDatabase(rawdb.NewMemoryDatabase(), newDbConfig(scheme)) - accTrie = trie.NewEmpty(db) - entries []*kv - ) - for i := uint64(1); i <= uint64(n); i++ { - value, _ := rlp.EncodeToBytes(&types.StateAccount{ - Nonce: i, - Balance: big.NewInt(int64(i)), - Root: types.EmptyRootHash, - CodeHash: getCodeHash(i), - }) - key := key32(i) - elem := &kv{key, value} - accTrie.MustUpdate(elem.k, elem.v) - entries = append(entries, elem) - } - slices.SortFunc(entries, (*kv).cmp) - - // Commit the state changes into db and re-create the trie - // for accessing later. - root, nodes, _ := accTrie.Commit(false) - db.Update(root, types.EmptyRootHash, 0, trienode.NewWithNodeSet(nodes), nil) - - accTrie, _ = trie.New(trie.StateTrieID(root), db) - return db.Scheme(), accTrie, entries -} - -// makeBoundaryAccountTrie constructs an account trie. Instead of filling -// accounts normally, this function will fill a few accounts which have -// boundary hash. -func makeBoundaryAccountTrie(scheme string, n int) (string, *trie.Trie, []*kv) { - var ( - entries []*kv - boundaries []common.Hash - - db = trie.NewDatabase(rawdb.NewMemoryDatabase(), newDbConfig(scheme)) - accTrie = trie.NewEmpty(db) - ) - // Initialize boundaries - var next common.Hash - step := new(big.Int).Sub( - new(big.Int).Div( - new(big.Int).Exp(common.Big2, common.Big256, nil), - big.NewInt(int64(accountConcurrency)), - ), common.Big1, - ) - for i := 0; i < accountConcurrency; i++ { - last := common.BigToHash(new(big.Int).Add(next.Big(), step)) - if i == accountConcurrency-1 { - last = common.MaxHash - } - boundaries = append(boundaries, last) - next = common.BigToHash(new(big.Int).Add(last.Big(), common.Big1)) - } - // Fill boundary accounts - for i := 0; i < len(boundaries); i++ { - value, _ := rlp.EncodeToBytes(&types.StateAccount{ - Nonce: uint64(0), - Balance: big.NewInt(int64(i)), - Root: types.EmptyRootHash, - CodeHash: getCodeHash(uint64(i)), - }) - elem := &kv{boundaries[i].Bytes(), value} - accTrie.MustUpdate(elem.k, elem.v) - entries = append(entries, elem) - } - // Fill other accounts if required - for i := uint64(1); i <= uint64(n); i++ { - value, _ := rlp.EncodeToBytes(&types.StateAccount{ - Nonce: i, - Balance: big.NewInt(int64(i)), - Root: types.EmptyRootHash, - CodeHash: getCodeHash(i), - }) - elem := &kv{key32(i), value} - accTrie.MustUpdate(elem.k, elem.v) - entries = append(entries, elem) - } - slices.SortFunc(entries, (*kv).cmp) - - // Commit the state changes into db and re-create the trie - // for accessing later. - root, nodes, _ := accTrie.Commit(false) - db.Update(root, types.EmptyRootHash, 0, trienode.NewWithNodeSet(nodes), nil) - - accTrie, _ = trie.New(trie.StateTrieID(root), db) - return db.Scheme(), accTrie, entries -} - -// makeAccountTrieWithStorageWithUniqueStorage creates an account trie where each accounts -// has a unique storage set. -func makeAccountTrieWithStorageWithUniqueStorage(scheme string, accounts, slots int, code bool) (string, *trie.Trie, []*kv, map[common.Hash]*trie.Trie, map[common.Hash][]*kv) { - var ( - db = trie.NewDatabase(rawdb.NewMemoryDatabase(), newDbConfig(scheme)) - accTrie = trie.NewEmpty(db) - entries []*kv - storageRoots = make(map[common.Hash]common.Hash) - storageTries = make(map[common.Hash]*trie.Trie) - storageEntries = make(map[common.Hash][]*kv) - nodes = trienode.NewMergedNodeSet() - ) - // Create n accounts in the trie - for i := uint64(1); i <= uint64(accounts); i++ { - key := key32(i) - codehash := types.EmptyCodeHash.Bytes() - if code { - codehash = getCodeHash(i) - } - // Create a storage trie - stRoot, stNodes, stEntries := makeStorageTrieWithSeed(common.BytesToHash(key), uint64(slots), i, db) - nodes.Merge(stNodes) - - value, _ := rlp.EncodeToBytes(&types.StateAccount{ - Nonce: i, - Balance: big.NewInt(int64(i)), - Root: stRoot, - CodeHash: codehash, - }) - elem := &kv{key, value} - accTrie.MustUpdate(elem.k, elem.v) - entries = append(entries, elem) - - storageRoots[common.BytesToHash(key)] = stRoot - storageEntries[common.BytesToHash(key)] = stEntries - } - slices.SortFunc(entries, (*kv).cmp) - - // Commit account trie - root, set, _ := accTrie.Commit(true) - nodes.Merge(set) - - // Commit gathered dirty nodes into database - db.Update(root, types.EmptyRootHash, 0, nodes, nil) - - // Re-create tries with new root - accTrie, _ = trie.New(trie.StateTrieID(root), db) - for i := uint64(1); i <= uint64(accounts); i++ { - key := key32(i) - id := trie.StorageTrieID(root, common.BytesToHash(key), storageRoots[common.BytesToHash(key)]) - trie, _ := trie.New(id, db) - storageTries[common.BytesToHash(key)] = trie - } - return db.Scheme(), accTrie, entries, storageTries, storageEntries -} - -// makeAccountTrieWithStorage spits out a trie, along with the leafs -func makeAccountTrieWithStorage(scheme string, accounts, slots int, code, boundary bool, uneven bool) (*trie.Trie, []*kv, map[common.Hash]*trie.Trie, map[common.Hash][]*kv) { - var ( - db = trie.NewDatabase(rawdb.NewMemoryDatabase(), newDbConfig(scheme)) - accTrie = trie.NewEmpty(db) - entries []*kv - storageRoots = make(map[common.Hash]common.Hash) - storageTries = make(map[common.Hash]*trie.Trie) - storageEntries = make(map[common.Hash][]*kv) - nodes = trienode.NewMergedNodeSet() - ) - // Create n accounts in the trie - for i := uint64(1); i <= uint64(accounts); i++ { - key := key32(i) - codehash := types.EmptyCodeHash.Bytes() - if code { - codehash = getCodeHash(i) - } - // Make a storage trie - var ( - stRoot common.Hash - stNodes *trienode.NodeSet - stEntries []*kv - ) - if boundary { - stRoot, stNodes, stEntries = makeBoundaryStorageTrie(common.BytesToHash(key), slots, db) - } else if uneven { - stRoot, stNodes, stEntries = makeUnevenStorageTrie(common.BytesToHash(key), slots, db) - } else { - stRoot, stNodes, stEntries = makeStorageTrieWithSeed(common.BytesToHash(key), uint64(slots), 0, db) - } - nodes.Merge(stNodes) - - value, _ := rlp.EncodeToBytes(&types.StateAccount{ - Nonce: i, - Balance: big.NewInt(int64(i)), - Root: stRoot, - CodeHash: codehash, - }) - elem := &kv{key, value} - accTrie.MustUpdate(elem.k, elem.v) - entries = append(entries, elem) - - // we reuse the same one for all accounts - storageRoots[common.BytesToHash(key)] = stRoot - storageEntries[common.BytesToHash(key)] = stEntries - } - slices.SortFunc(entries, (*kv).cmp) - - // Commit account trie - root, set, _ := accTrie.Commit(true) - nodes.Merge(set) - - // Commit gathered dirty nodes into database - db.Update(root, types.EmptyRootHash, 0, nodes, nil) - - // Re-create tries with new root - accTrie, err := trie.New(trie.StateTrieID(root), db) - if err != nil { - panic(err) - } - for i := uint64(1); i <= uint64(accounts); i++ { - key := key32(i) - id := trie.StorageTrieID(root, common.BytesToHash(key), storageRoots[common.BytesToHash(key)]) - trie, err := trie.New(id, db) - if err != nil { - panic(err) - } - storageTries[common.BytesToHash(key)] = trie - } - return accTrie, entries, storageTries, storageEntries -} - -// makeStorageTrieWithSeed fills a storage trie with n items, returning the -// not-yet-committed trie and the sorted entries. The seeds can be used to ensure -// that tries are unique. -func makeStorageTrieWithSeed(owner common.Hash, n, seed uint64, db *trie.Database) (common.Hash, *trienode.NodeSet, []*kv) { - trie, _ := trie.New(trie.StorageTrieID(types.EmptyRootHash, owner, types.EmptyRootHash), db) - var entries []*kv - for i := uint64(1); i <= n; i++ { - // store 'x' at slot 'x' - slotValue := key32(i + seed) - rlpSlotValue, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(slotValue[:])) - - slotKey := key32(i) - key := crypto.Keccak256Hash(slotKey[:]) - - elem := &kv{key[:], rlpSlotValue} - trie.MustUpdate(elem.k, elem.v) - entries = append(entries, elem) - } - slices.SortFunc(entries, (*kv).cmp) - root, nodes, _ := trie.Commit(false) - return root, nodes, entries -} - -// makeBoundaryStorageTrie constructs a storage trie. Instead of filling -// storage slots normally, this function will fill a few slots which have -// boundary hash. -func makeBoundaryStorageTrie(owner common.Hash, n int, db *trie.Database) (common.Hash, *trienode.NodeSet, []*kv) { - var ( - entries []*kv - boundaries []common.Hash - trie, _ = trie.New(trie.StorageTrieID(types.EmptyRootHash, owner, types.EmptyRootHash), db) - ) - // Initialize boundaries - var next common.Hash - step := new(big.Int).Sub( - new(big.Int).Div( - new(big.Int).Exp(common.Big2, common.Big256, nil), - big.NewInt(int64(accountConcurrency)), - ), common.Big1, - ) - for i := 0; i < accountConcurrency; i++ { - last := common.BigToHash(new(big.Int).Add(next.Big(), step)) - if i == accountConcurrency-1 { - last = common.MaxHash - } - boundaries = append(boundaries, last) - next = common.BigToHash(new(big.Int).Add(last.Big(), common.Big1)) - } - // Fill boundary slots - for i := 0; i < len(boundaries); i++ { - key := boundaries[i] - val := []byte{0xde, 0xad, 0xbe, 0xef} - - elem := &kv{key[:], val} - trie.MustUpdate(elem.k, elem.v) - entries = append(entries, elem) - } - // Fill other slots if required - for i := uint64(1); i <= uint64(n); i++ { - slotKey := key32(i) - key := crypto.Keccak256Hash(slotKey[:]) - - slotValue := key32(i) - rlpSlotValue, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(slotValue[:])) - - elem := &kv{key[:], rlpSlotValue} - trie.MustUpdate(elem.k, elem.v) - entries = append(entries, elem) - } - slices.SortFunc(entries, (*kv).cmp) - root, nodes, _ := trie.Commit(false) - return root, nodes, entries -} - -// makeUnevenStorageTrie constructs a storage tries will states distributed in -// different range unevenly. -func makeUnevenStorageTrie(owner common.Hash, slots int, db *trie.Database) (common.Hash, *trienode.NodeSet, []*kv) { - var ( - entries []*kv - tr, _ = trie.New(trie.StorageTrieID(types.EmptyRootHash, owner, types.EmptyRootHash), db) - chosen = make(map[byte]struct{}) - ) - for i := 0; i < 3; i++ { - var n int - for { - n = mrand.Intn(15) // the last range is set empty deliberately - if _, ok := chosen[byte(n)]; ok { - continue - } - chosen[byte(n)] = struct{}{} - break - } - for j := 0; j < slots/3; j++ { - key := append([]byte{byte(n)}, testutil.RandBytes(31)...) - val, _ := rlp.EncodeToBytes(testutil.RandBytes(32)) - - elem := &kv{key, val} - tr.MustUpdate(elem.k, elem.v) - entries = append(entries, elem) - } - } - slices.SortFunc(entries, (*kv).cmp) - root, nodes, _ := tr.Commit(false) - return root, nodes, entries -} - -func verifyTrie(scheme string, db ethdb.KeyValueStore, root common.Hash, t *testing.T) { - t.Helper() - triedb := trie.NewDatabase(rawdb.NewDatabase(db), newDbConfig(scheme)) - accTrie, err := trie.New(trie.StateTrieID(root), triedb) - if err != nil { - t.Fatal(err) - } - accounts, slots := 0, 0 - accIt := trie.NewIterator(accTrie.MustNodeIterator(nil)) - for accIt.Next() { - var acc struct { - Nonce uint64 - Balance *big.Int - Root common.Hash - CodeHash []byte - } - if err := rlp.DecodeBytes(accIt.Value, &acc); err != nil { - log.Crit("Invalid account encountered during snapshot creation", "err", err) - } - accounts++ - if acc.Root != types.EmptyRootHash { - id := trie.StorageTrieID(root, common.BytesToHash(accIt.Key), acc.Root) - storeTrie, err := trie.NewStateTrie(id, triedb) - if err != nil { - t.Fatal(err) - } - storeIt := trie.NewIterator(storeTrie.MustNodeIterator(nil)) - for storeIt.Next() { - slots++ - } - if err := storeIt.Err; err != nil { - t.Fatal(err) - } - } - } - if err := accIt.Err; err != nil { - t.Fatal(err) - } - t.Logf("accounts: %d, slots: %d", accounts, slots) -} - -// TestSyncAccountPerformance tests how efficient the snap algo is at minimizing -// state healing -func TestSyncAccountPerformance(t *testing.T) { - t.Parallel() - - testSyncAccountPerformance(t, rawdb.HashScheme) - testSyncAccountPerformance(t, rawdb.PathScheme) -} - -func testSyncAccountPerformance(t *testing.T, scheme string) { - // Set the account concurrency to 1. This _should_ result in the - // range root to become correct, and there should be no healing needed - defer func(old int) { accountConcurrency = old }(accountConcurrency) - accountConcurrency = 1 - - var ( - once sync.Once - cancel = make(chan struct{}) - term = func() { - once.Do(func() { - close(cancel) - }) - } - ) - nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme) - - mkSource := func(name string) *testPeer { - source := newTestPeer(name, t, term) - source.accountTrie = sourceAccountTrie.Copy() - source.accountValues = elems - return source - } - src := mkSource("source") - syncer := setupSyncer(nodeScheme, src) - if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { - t.Fatalf("sync failed: %v", err) - } - verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) - // The trie root will always be requested, since it is added when the snap - // sync cycle starts. When popping the queue, we do not look it up again. - // Doing so would bring this number down to zero in this artificial testcase, - // but only add extra IO for no reason in practice. - if have, want := src.nTrienodeRequests, 1; have != want { - fmt.Print(src.Stats()) - t.Errorf("trie node heal requests wrong, want %d, have %d", want, have) - } -} - -func TestSlotEstimation(t *testing.T) { - for i, tc := range []struct { - last common.Hash - count int - want uint64 - }{ - { - // Half the space - common.HexToHash("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - 100, - 100, - }, - { - // 1 / 16th - common.HexToHash("0x0fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - 100, - 1500, - }, - { - // Bit more than 1 / 16th - common.HexToHash("0x1000000000000000000000000000000000000000000000000000000000000000"), - 100, - 1499, - }, - { - // Almost everything - common.HexToHash("0xF000000000000000000000000000000000000000000000000000000000000000"), - 100, - 6, - }, - { - // Almost nothing -- should lead to error - common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001"), - 1, - 0, - }, - { - // Nothing -- should lead to error - common.Hash{}, - 100, - 0, - }, - } { - have, _ := estimateRemainingSlots(tc.count, tc.last) - if want := tc.want; have != want { - t.Errorf("test %d: have %d want %d", i, have, want) - } - } -} - -func newDbConfig(scheme string) *trie.Config { - if scheme == rawdb.HashScheme { - return &trie.Config{} - } - return &trie.Config{PathDB: pathdb.Defaults} -} diff --git a/eth/protocols/snap/tracker.go b/eth/protocols/snap/tracker.go deleted file mode 100644 index 2cf59cc23a..0000000000 --- a/eth/protocols/snap/tracker.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2021 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 snap - -import ( - "time" - - "github.com/ethereum/go-ethereum/p2p/tracker" -) - -// requestTracker is a singleton tracker for request times. -var requestTracker = tracker.New(ProtocolName, time.Minute)