mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-03-24 11:58:11 +00:00
This changes the p2p protocol handlers to delay message decoding. It's the first part of a larger change that will delay decoding all the way through message processing. For responses, we delay the decoding until it is confirmed that the response matches an active request and does not exceed its limits. In order to make this work, all messages have been changed to use rlp.RawList instead of a slice of the decoded item type. For block bodies specifically, the decoding has been delayed all the way until after verification of the response hash. The role of p2p/tracker.Tracker changes significantly in this PR. The Tracker's original purpose was to maintain metrics about requests and responses in the peer-to-peer protocols. Each protocol maintained a single global Tracker instance. As of this change, the Tracker is now always active (regardless of metrics collection), and there is a separate instance of it for each peer. Whenever a response arrives, it is first verified that a request exists for it in the tracker. The tracker is also the place where limits are kept.
414 lines
13 KiB
Go
414 lines
13 KiB
Go
// 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 <http://www.gnu.org/licenses/>.
|
|
|
|
package eth
|
|
|
|
import (
|
|
"math/rand"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
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/p2p/tracker"
|
|
"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
|
|
|
|
// 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
|
|
)
|
|
|
|
// Peer is a collection of relevant information we have about a `eth` peer.
|
|
type Peer struct {
|
|
*p2p.Peer // The embedded P2P package peer
|
|
|
|
id string // Unique ID for the peer, cached
|
|
|
|
rw p2p.MsgReadWriter // Input/output streams for snap
|
|
version uint // Protocol version negotiated
|
|
lastRange atomic.Pointer[BlockRangeUpdatePacket]
|
|
|
|
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
|
|
|
|
tracker *tracker.Tracker
|
|
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
|
|
}
|
|
|
|
// NewPeer creates a wrapper for a network connection and negotiated protocol
|
|
// version.
|
|
func NewPeer(version uint, p *p2p.Peer, rw p2p.MsgReadWriter, txpool TxPool) *Peer {
|
|
cap := p2p.Cap{Name: ProtocolName, Version: version}
|
|
id := p.ID().String()
|
|
peer := &Peer{
|
|
id: id,
|
|
Peer: p,
|
|
rw: rw,
|
|
version: version,
|
|
knownTxs: newKnownCache(maxKnownTxs),
|
|
txBroadcast: make(chan []common.Hash),
|
|
txAnnounce: make(chan []common.Hash),
|
|
tracker: tracker.New(cap, id, 5*time.Minute),
|
|
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.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
|
|
}
|
|
|
|
// BlockRange returns the latest announced block range.
|
|
// This will be nil for peers below protocol version eth/69.
|
|
func (p *Peer) BlockRange() *BlockRangeUpdatePacket {
|
|
return p.lastRange.Load()
|
|
}
|
|
|
|
// KnownTransaction returns whether peer is known to already have a transaction.
|
|
func (p *Peer) KnownTransaction(hash common.Hash) bool {
|
|
return p.knownTxs.Contains(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))
|
|
}
|
|
}
|
|
|
|
// sendPooledTransactionHashes 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) sendPooledTransactionHashes(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, NewPooledTransactionHashesPacket{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,
|
|
})
|
|
}
|
|
|
|
// 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,
|
|
numItems: 1,
|
|
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,
|
|
numItems: amount,
|
|
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,
|
|
numItems: amount,
|
|
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,
|
|
numItems: len(hashes),
|
|
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,
|
|
numItems: len(hashes),
|
|
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().Trace("Fetching batch of transactions", "count", len(hashes))
|
|
id := rand.Uint64()
|
|
|
|
err := p.tracker.Track(tracker.Request{
|
|
ID: id,
|
|
ReqCode: GetPooledTransactionsMsg,
|
|
RespCode: PooledTransactionsMsg,
|
|
Size: len(hashes),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return p2p.Send(p.rw, GetPooledTransactionsMsg, &GetPooledTransactionsPacket{
|
|
RequestId: id,
|
|
GetPooledTransactionsRequest: hashes,
|
|
})
|
|
}
|
|
|
|
// SendBlockRangeUpdate sends a notification about our available block range to the peer.
|
|
func (p *Peer) SendBlockRangeUpdate(msg BlockRangeUpdatePacket) error {
|
|
if p.version < ETH69 {
|
|
return nil
|
|
}
|
|
return p2p.Send(p.rw, BlockRangeUpdateMsg, &msg)
|
|
}
|
|
|
|
// 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()
|
|
}
|