github.com/jimmyx0x/go-ethereum@v1.10.28/eth/protocols/snap/handler.go (about)

     1  // Copyright 2020 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package snap
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"time"
    23  
    24  	"github.com/ethereum/go-ethereum/common"
    25  	"github.com/ethereum/go-ethereum/core"
    26  	"github.com/ethereum/go-ethereum/light"
    27  	"github.com/ethereum/go-ethereum/log"
    28  	"github.com/ethereum/go-ethereum/metrics"
    29  	"github.com/ethereum/go-ethereum/p2p"
    30  	"github.com/ethereum/go-ethereum/p2p/enode"
    31  	"github.com/ethereum/go-ethereum/p2p/enr"
    32  	"github.com/ethereum/go-ethereum/trie"
    33  )
    34  
    35  const (
    36  	// softResponseLimit is the target maximum size of replies to data retrievals.
    37  	softResponseLimit = 2 * 1024 * 1024
    38  
    39  	// maxCodeLookups is the maximum number of bytecodes to serve. This number is
    40  	// there to limit the number of disk lookups.
    41  	maxCodeLookups = 1024
    42  
    43  	// stateLookupSlack defines the ratio by how much a state response can exceed
    44  	// the requested limit in order to try and avoid breaking up contracts into
    45  	// multiple packages and proving them.
    46  	stateLookupSlack = 0.1
    47  
    48  	// maxTrieNodeLookups is the maximum number of state trie nodes to serve. This
    49  	// number is there to limit the number of disk lookups.
    50  	maxTrieNodeLookups = 1024
    51  
    52  	// maxTrieNodeTimeSpent is the maximum time we should spend on looking up trie nodes.
    53  	// If we spend too much time, then it's a fairly high chance of timing out
    54  	// at the remote side, which means all the work is in vain.
    55  	maxTrieNodeTimeSpent = 5 * time.Second
    56  )
    57  
    58  // Handler is a callback to invoke from an outside runner after the boilerplate
    59  // exchanges have passed.
    60  type Handler func(peer *Peer) error
    61  
    62  // Backend defines the data retrieval methods to serve remote requests and the
    63  // callback methods to invoke on remote deliveries.
    64  type Backend interface {
    65  	// Chain retrieves the blockchain object to serve data.
    66  	Chain() *core.BlockChain
    67  
    68  	// RunPeer is invoked when a peer joins on the `eth` protocol. The handler
    69  	// should do any peer maintenance work, handshakes and validations. If all
    70  	// is passed, control should be given back to the `handler` to process the
    71  	// inbound messages going forward.
    72  	RunPeer(peer *Peer, handler Handler) error
    73  
    74  	// PeerInfo retrieves all known `snap` information about a peer.
    75  	PeerInfo(id enode.ID) interface{}
    76  
    77  	// Handle is a callback to be invoked when a data packet is received from
    78  	// the remote peer. Only packets not consumed by the protocol handler will
    79  	// be forwarded to the backend.
    80  	Handle(peer *Peer, packet Packet) error
    81  }
    82  
    83  // MakeProtocols constructs the P2P protocol definitions for `snap`.
    84  func MakeProtocols(backend Backend, dnsdisc enode.Iterator) []p2p.Protocol {
    85  	// Filter the discovery iterator for nodes advertising snap support.
    86  	dnsdisc = enode.Filter(dnsdisc, func(n *enode.Node) bool {
    87  		var snap enrEntry
    88  		return n.Load(&snap) == nil
    89  	})
    90  
    91  	protocols := make([]p2p.Protocol, len(ProtocolVersions))
    92  	for i, version := range ProtocolVersions {
    93  		version := version // Closure
    94  
    95  		protocols[i] = p2p.Protocol{
    96  			Name:    ProtocolName,
    97  			Version: version,
    98  			Length:  protocolLengths[version],
    99  			Run: func(p *p2p.Peer, rw p2p.MsgReadWriter) error {
   100  				return backend.RunPeer(NewPeer(version, p, rw), func(peer *Peer) error {
   101  					return Handle(backend, peer)
   102  				})
   103  			},
   104  			NodeInfo: func() interface{} {
   105  				return nodeInfo(backend.Chain())
   106  			},
   107  			PeerInfo: func(id enode.ID) interface{} {
   108  				return backend.PeerInfo(id)
   109  			},
   110  			Attributes:     []enr.Entry{&enrEntry{}},
   111  			DialCandidates: dnsdisc,
   112  		}
   113  	}
   114  	return protocols
   115  }
   116  
   117  // Handle is the callback invoked to manage the life cycle of a `snap` peer.
   118  // When this function terminates, the peer is disconnected.
   119  func Handle(backend Backend, peer *Peer) error {
   120  	for {
   121  		if err := HandleMessage(backend, peer); err != nil {
   122  			peer.Log().Debug("Message handling failed in `snap`", "err", err)
   123  			return err
   124  		}
   125  	}
   126  }
   127  
   128  // HandleMessage is invoked whenever an inbound message is received from a
   129  // remote peer on the `snap` protocol. The remote connection is torn down upon
   130  // returning any error.
   131  func HandleMessage(backend Backend, peer *Peer) error {
   132  	// Read the next message from the remote peer, and ensure it's fully consumed
   133  	msg, err := peer.rw.ReadMsg()
   134  	if err != nil {
   135  		return err
   136  	}
   137  	if msg.Size > maxMessageSize {
   138  		return fmt.Errorf("%w: %v > %v", errMsgTooLarge, msg.Size, maxMessageSize)
   139  	}
   140  	defer msg.Discard()
   141  	start := time.Now()
   142  	// Track the amount of time it takes to serve the request and run the handler
   143  	if metrics.Enabled {
   144  		h := fmt.Sprintf("%s/%s/%d/%#02x", p2p.HandleHistName, ProtocolName, peer.Version(), msg.Code)
   145  		defer func(start time.Time) {
   146  			sampler := func() metrics.Sample {
   147  				return metrics.ResettingSample(
   148  					metrics.NewExpDecaySample(1028, 0.015),
   149  				)
   150  			}
   151  			metrics.GetOrRegisterHistogramLazy(h, nil, sampler).Update(time.Since(start).Microseconds())
   152  		}(start)
   153  	}
   154  	// Handle the message depending on its contents
   155  	switch {
   156  	case msg.Code == GetAccountRangeMsg:
   157  		// Decode the account retrieval request
   158  		var req GetAccountRangePacket
   159  		if err := msg.Decode(&req); err != nil {
   160  			return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
   161  		}
   162  		// Service the request, potentially returning nothing in case of errors
   163  		accounts, proofs := ServiceGetAccountRangeQuery(backend.Chain(), &req)
   164  
   165  		// Send back anything accumulated (or empty in case of errors)
   166  		return p2p.Send(peer.rw, AccountRangeMsg, &AccountRangePacket{
   167  			ID:       req.ID,
   168  			Accounts: accounts,
   169  			Proof:    proofs,
   170  		})
   171  
   172  	case msg.Code == AccountRangeMsg:
   173  		// A range of accounts arrived to one of our previous requests
   174  		res := new(AccountRangePacket)
   175  		if err := msg.Decode(res); err != nil {
   176  			return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
   177  		}
   178  		// Ensure the range is monotonically increasing
   179  		for i := 1; i < len(res.Accounts); i++ {
   180  			if bytes.Compare(res.Accounts[i-1].Hash[:], res.Accounts[i].Hash[:]) >= 0 {
   181  				return fmt.Errorf("accounts not monotonically increasing: #%d [%x] vs #%d [%x]", i-1, res.Accounts[i-1].Hash[:], i, res.Accounts[i].Hash[:])
   182  			}
   183  		}
   184  		requestTracker.Fulfil(peer.id, peer.version, AccountRangeMsg, res.ID)
   185  
   186  		return backend.Handle(peer, res)
   187  
   188  	case msg.Code == GetStorageRangesMsg:
   189  		// Decode the storage retrieval request
   190  		var req GetStorageRangesPacket
   191  		if err := msg.Decode(&req); err != nil {
   192  			return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
   193  		}
   194  		// Service the request, potentially returning nothing in case of errors
   195  		slots, proofs := ServiceGetStorageRangesQuery(backend.Chain(), &req)
   196  
   197  		// Send back anything accumulated (or empty in case of errors)
   198  		return p2p.Send(peer.rw, StorageRangesMsg, &StorageRangesPacket{
   199  			ID:    req.ID,
   200  			Slots: slots,
   201  			Proof: proofs,
   202  		})
   203  
   204  	case msg.Code == StorageRangesMsg:
   205  		// A range of storage slots arrived to one of our previous requests
   206  		res := new(StorageRangesPacket)
   207  		if err := msg.Decode(res); err != nil {
   208  			return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
   209  		}
   210  		// Ensure the ranges are monotonically increasing
   211  		for i, slots := range res.Slots {
   212  			for j := 1; j < len(slots); j++ {
   213  				if bytes.Compare(slots[j-1].Hash[:], slots[j].Hash[:]) >= 0 {
   214  					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[:])
   215  				}
   216  			}
   217  		}
   218  		requestTracker.Fulfil(peer.id, peer.version, StorageRangesMsg, res.ID)
   219  
   220  		return backend.Handle(peer, res)
   221  
   222  	case msg.Code == GetByteCodesMsg:
   223  		// Decode bytecode retrieval request
   224  		var req GetByteCodesPacket
   225  		if err := msg.Decode(&req); err != nil {
   226  			return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
   227  		}
   228  		// Service the request, potentially returning nothing in case of errors
   229  		codes := ServiceGetByteCodesQuery(backend.Chain(), &req)
   230  
   231  		// Send back anything accumulated (or empty in case of errors)
   232  		return p2p.Send(peer.rw, ByteCodesMsg, &ByteCodesPacket{
   233  			ID:    req.ID,
   234  			Codes: codes,
   235  		})
   236  
   237  	case msg.Code == ByteCodesMsg:
   238  		// A batch of byte codes arrived to one of our previous requests
   239  		res := new(ByteCodesPacket)
   240  		if err := msg.Decode(res); err != nil {
   241  			return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
   242  		}
   243  		requestTracker.Fulfil(peer.id, peer.version, ByteCodesMsg, res.ID)
   244  
   245  		return backend.Handle(peer, res)
   246  
   247  	case msg.Code == GetTrieNodesMsg:
   248  		// Decode trie node retrieval request
   249  		var req GetTrieNodesPacket
   250  		if err := msg.Decode(&req); err != nil {
   251  			return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
   252  		}
   253  		// Service the request, potentially returning nothing in case of errors
   254  		nodes, err := ServiceGetTrieNodesQuery(backend.Chain(), &req, start)
   255  		if err != nil {
   256  			return err
   257  		}
   258  		// Send back anything accumulated (or empty in case of errors)
   259  		return p2p.Send(peer.rw, TrieNodesMsg, &TrieNodesPacket{
   260  			ID:    req.ID,
   261  			Nodes: nodes,
   262  		})
   263  
   264  	case msg.Code == TrieNodesMsg:
   265  		// A batch of trie nodes arrived to one of our previous requests
   266  		res := new(TrieNodesPacket)
   267  		if err := msg.Decode(res); err != nil {
   268  			return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
   269  		}
   270  		requestTracker.Fulfil(peer.id, peer.version, TrieNodesMsg, res.ID)
   271  
   272  		return backend.Handle(peer, res)
   273  
   274  	default:
   275  		return fmt.Errorf("%w: %v", errInvalidMsgCode, msg.Code)
   276  	}
   277  }
   278  
   279  // ServiceGetAccountRangeQuery assembles the response to an account range query.
   280  // It is exposed to allow external packages to test protocol behavior.
   281  func ServiceGetAccountRangeQuery(chain *core.BlockChain, req *GetAccountRangePacket) ([]*AccountData, [][]byte) {
   282  	if req.Bytes > softResponseLimit {
   283  		req.Bytes = softResponseLimit
   284  	}
   285  	// Retrieve the requested state and bail out if non existent
   286  	tr, err := trie.New(trie.StateTrieID(req.Root), chain.StateCache().TrieDB())
   287  	if err != nil {
   288  		return nil, nil
   289  	}
   290  	it, err := chain.Snapshots().AccountIterator(req.Root, req.Origin)
   291  	if err != nil {
   292  		return nil, nil
   293  	}
   294  	// Iterate over the requested range and pile accounts up
   295  	var (
   296  		accounts []*AccountData
   297  		size     uint64
   298  		last     common.Hash
   299  	)
   300  	for it.Next() {
   301  		hash, account := it.Hash(), common.CopyBytes(it.Account())
   302  
   303  		// Track the returned interval for the Merkle proofs
   304  		last = hash
   305  
   306  		// Assemble the reply item
   307  		size += uint64(common.HashLength + len(account))
   308  		accounts = append(accounts, &AccountData{
   309  			Hash: hash,
   310  			Body: account,
   311  		})
   312  		// If we've exceeded the request threshold, abort
   313  		if bytes.Compare(hash[:], req.Limit[:]) >= 0 {
   314  			break
   315  		}
   316  		if size > req.Bytes {
   317  			break
   318  		}
   319  	}
   320  	it.Release()
   321  
   322  	// Generate the Merkle proofs for the first and last account
   323  	proof := light.NewNodeSet()
   324  	if err := tr.Prove(req.Origin[:], 0, proof); err != nil {
   325  		log.Warn("Failed to prove account range", "origin", req.Origin, "err", err)
   326  		return nil, nil
   327  	}
   328  	if last != (common.Hash{}) {
   329  		if err := tr.Prove(last[:], 0, proof); err != nil {
   330  			log.Warn("Failed to prove account range", "last", last, "err", err)
   331  			return nil, nil
   332  		}
   333  	}
   334  	var proofs [][]byte
   335  	for _, blob := range proof.NodeList() {
   336  		proofs = append(proofs, blob)
   337  	}
   338  	return accounts, proofs
   339  }
   340  
   341  func ServiceGetStorageRangesQuery(chain *core.BlockChain, req *GetStorageRangesPacket) ([][]*StorageData, [][]byte) {
   342  	if req.Bytes > softResponseLimit {
   343  		req.Bytes = softResponseLimit
   344  	}
   345  	// TODO(karalabe): Do we want to enforce > 0 accounts and 1 account if origin is set?
   346  	// TODO(karalabe):   - Logging locally is not ideal as remote faults annoy the local user
   347  	// TODO(karalabe):   - Dropping the remote peer is less flexible wrt client bugs (slow is better than non-functional)
   348  
   349  	// Calculate the hard limit at which to abort, even if mid storage trie
   350  	hardLimit := uint64(float64(req.Bytes) * (1 + stateLookupSlack))
   351  
   352  	// Retrieve storage ranges until the packet limit is reached
   353  	var (
   354  		slots  [][]*StorageData
   355  		proofs [][]byte
   356  		size   uint64
   357  	)
   358  	for _, account := range req.Accounts {
   359  		// If we've exceeded the requested data limit, abort without opening
   360  		// a new storage range (that we'd need to prove due to exceeded size)
   361  		if size >= req.Bytes {
   362  			break
   363  		}
   364  		// The first account might start from a different origin and end sooner
   365  		var origin common.Hash
   366  		if len(req.Origin) > 0 {
   367  			origin, req.Origin = common.BytesToHash(req.Origin), nil
   368  		}
   369  		var limit = common.HexToHash("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
   370  		if len(req.Limit) > 0 {
   371  			limit, req.Limit = common.BytesToHash(req.Limit), nil
   372  		}
   373  		// Retrieve the requested state and bail out if non existent
   374  		it, err := chain.Snapshots().StorageIterator(req.Root, account, origin)
   375  		if err != nil {
   376  			return nil, nil
   377  		}
   378  		// Iterate over the requested range and pile slots up
   379  		var (
   380  			storage []*StorageData
   381  			last    common.Hash
   382  			abort   bool
   383  		)
   384  		for it.Next() {
   385  			if size >= hardLimit {
   386  				abort = true
   387  				break
   388  			}
   389  			hash, slot := it.Hash(), common.CopyBytes(it.Slot())
   390  
   391  			// Track the returned interval for the Merkle proofs
   392  			last = hash
   393  
   394  			// Assemble the reply item
   395  			size += uint64(common.HashLength + len(slot))
   396  			storage = append(storage, &StorageData{
   397  				Hash: hash,
   398  				Body: slot,
   399  			})
   400  			// If we've exceeded the request threshold, abort
   401  			if bytes.Compare(hash[:], limit[:]) >= 0 {
   402  				break
   403  			}
   404  		}
   405  		if len(storage) > 0 {
   406  			slots = append(slots, storage)
   407  		}
   408  		it.Release()
   409  
   410  		// Generate the Merkle proofs for the first and last storage slot, but
   411  		// only if the response was capped. If the entire storage trie included
   412  		// in the response, no need for any proofs.
   413  		if origin != (common.Hash{}) || (abort && len(storage) > 0) {
   414  			// Request started at a non-zero hash or was capped prematurely, add
   415  			// the endpoint Merkle proofs
   416  			accTrie, err := trie.NewStateTrie(trie.StateTrieID(req.Root), chain.StateCache().TrieDB())
   417  			if err != nil {
   418  				return nil, nil
   419  			}
   420  			acc, err := accTrie.TryGetAccountByHash(account)
   421  			if err != nil || acc == nil {
   422  				return nil, nil
   423  			}
   424  			id := trie.StorageTrieID(req.Root, account, acc.Root)
   425  			stTrie, err := trie.NewStateTrie(id, chain.StateCache().TrieDB())
   426  			if err != nil {
   427  				return nil, nil
   428  			}
   429  			proof := light.NewNodeSet()
   430  			if err := stTrie.Prove(origin[:], 0, proof); err != nil {
   431  				log.Warn("Failed to prove storage range", "origin", req.Origin, "err", err)
   432  				return nil, nil
   433  			}
   434  			if last != (common.Hash{}) {
   435  				if err := stTrie.Prove(last[:], 0, proof); err != nil {
   436  					log.Warn("Failed to prove storage range", "last", last, "err", err)
   437  					return nil, nil
   438  				}
   439  			}
   440  			for _, blob := range proof.NodeList() {
   441  				proofs = append(proofs, blob)
   442  			}
   443  			// Proof terminates the reply as proofs are only added if a node
   444  			// refuses to serve more data (exception when a contract fetch is
   445  			// finishing, but that's that).
   446  			break
   447  		}
   448  	}
   449  	return slots, proofs
   450  }
   451  
   452  // ServiceGetByteCodesQuery assembles the response to a byte codes query.
   453  // It is exposed to allow external packages to test protocol behavior.
   454  func ServiceGetByteCodesQuery(chain *core.BlockChain, req *GetByteCodesPacket) [][]byte {
   455  	if req.Bytes > softResponseLimit {
   456  		req.Bytes = softResponseLimit
   457  	}
   458  	if len(req.Hashes) > maxCodeLookups {
   459  		req.Hashes = req.Hashes[:maxCodeLookups]
   460  	}
   461  	// Retrieve bytecodes until the packet size limit is reached
   462  	var (
   463  		codes [][]byte
   464  		bytes uint64
   465  	)
   466  	for _, hash := range req.Hashes {
   467  		if hash == emptyCode {
   468  			// Peers should not request the empty code, but if they do, at
   469  			// least sent them back a correct response without db lookups
   470  			codes = append(codes, []byte{})
   471  		} else if blob, err := chain.ContractCodeWithPrefix(hash); err == nil {
   472  			codes = append(codes, blob)
   473  			bytes += uint64(len(blob))
   474  		}
   475  		if bytes > req.Bytes {
   476  			break
   477  		}
   478  	}
   479  	return codes
   480  }
   481  
   482  // ServiceGetTrieNodesQuery assembles the response to a trie nodes query.
   483  // It is exposed to allow external packages to test protocol behavior.
   484  func ServiceGetTrieNodesQuery(chain *core.BlockChain, req *GetTrieNodesPacket, start time.Time) ([][]byte, error) {
   485  	if req.Bytes > softResponseLimit {
   486  		req.Bytes = softResponseLimit
   487  	}
   488  	// Make sure we have the state associated with the request
   489  	triedb := chain.StateCache().TrieDB()
   490  
   491  	accTrie, err := trie.NewStateTrie(trie.StateTrieID(req.Root), triedb)
   492  	if err != nil {
   493  		// We don't have the requested state available, bail out
   494  		return nil, nil
   495  	}
   496  	// The 'snap' might be nil, in which case we cannot serve storage slots.
   497  	snap := chain.Snapshots().Snapshot(req.Root)
   498  	// Retrieve trie nodes until the packet size limit is reached
   499  	var (
   500  		nodes [][]byte
   501  		bytes uint64
   502  		loads int // Trie hash expansions to count database reads
   503  	)
   504  	for _, pathset := range req.Paths {
   505  		switch len(pathset) {
   506  		case 0:
   507  			// Ensure we penalize invalid requests
   508  			return nil, fmt.Errorf("%w: zero-item pathset requested", errBadRequest)
   509  
   510  		case 1:
   511  			// If we're only retrieving an account trie node, fetch it directly
   512  			blob, resolved, err := accTrie.TryGetNode(pathset[0])
   513  			loads += resolved // always account database reads, even for failures
   514  			if err != nil {
   515  				break
   516  			}
   517  			nodes = append(nodes, blob)
   518  			bytes += uint64(len(blob))
   519  
   520  		default:
   521  			var stRoot common.Hash
   522  			// Storage slots requested, open the storage trie and retrieve from there
   523  			if snap == nil {
   524  				// We don't have the requested state snapshotted yet (or it is stale),
   525  				// but can look up the account via the trie instead.
   526  				account, err := accTrie.TryGetAccountByHash(common.BytesToHash(pathset[0]))
   527  				loads += 8 // We don't know the exact cost of lookup, this is an estimate
   528  				if err != nil || account == nil {
   529  					break
   530  				}
   531  				stRoot = account.Root
   532  			} else {
   533  				account, err := snap.Account(common.BytesToHash(pathset[0]))
   534  				loads++ // always account database reads, even for failures
   535  				if err != nil || account == nil {
   536  					break
   537  				}
   538  				stRoot = common.BytesToHash(account.Root)
   539  			}
   540  			id := trie.StorageTrieID(req.Root, common.BytesToHash(pathset[0]), stRoot)
   541  			stTrie, err := trie.NewStateTrie(id, triedb)
   542  			loads++ // always account database reads, even for failures
   543  			if err != nil {
   544  				break
   545  			}
   546  			for _, path := range pathset[1:] {
   547  				blob, resolved, err := stTrie.TryGetNode(path)
   548  				loads += resolved // always account database reads, even for failures
   549  				if err != nil {
   550  					break
   551  				}
   552  				nodes = append(nodes, blob)
   553  				bytes += uint64(len(blob))
   554  
   555  				// Sanity check limits to avoid DoS on the store trie loads
   556  				if bytes > req.Bytes || loads > maxTrieNodeLookups || time.Since(start) > maxTrieNodeTimeSpent {
   557  					break
   558  				}
   559  			}
   560  		}
   561  		// Abort request processing if we've exceeded our limits
   562  		if bytes > req.Bytes || loads > maxTrieNodeLookups || time.Since(start) > maxTrieNodeTimeSpent {
   563  			break
   564  		}
   565  	}
   566  	return nodes, nil
   567  }
   568  
   569  // NodeInfo represents a short summary of the `snap` sub-protocol metadata
   570  // known about the host peer.
   571  type NodeInfo struct{}
   572  
   573  // nodeInfo retrieves some `snap` protocol metadata about the running host node.
   574  func nodeInfo(chain *core.BlockChain) *NodeInfo {
   575  	return &NodeInfo{}
   576  }