Files
node/network/peer/peer.go
T
Hanzo AI 6de839a515 codec: rip github.com/luxfi/codec from leaf-misc (Wave 1D)
Wave 1D of the luxfi/codec final rip: leaf-misc tier under node/.
This batch removes the direct codec/wrappers, codec/linearcodec, and
codec.Manager imports from p2p network protocol, IP/port encoding,
indexer/keystore on-disk storage, warp socket IPC, x/archivedb +
merkledb internals, and the primary-network wallet UTXO loader.

Migration shape:

(a) codec/wrappers.Packer + length constants
    → node/utils/wrappers (same shape, already in tree as the local
      canonical home for binary IO helpers). 14 files: indexer/,
      network/, utils/ips/, utils/metric/, warp/, x/.

(b) codec.Manager + linearcodec for on-disk container storage
    → hand-rolled big-endian binary marshal/unmarshal. Hard cut;
      no codec-version prefix; payload size bounded explicitly:
      - indexer/codec.go: marshalContainer/unmarshalContainer
      - service/keystore/codec.go: marshalHash/unmarshalHash and
        marshalUser/unmarshalUser, 16 MiB blob cap retained.

(c) codec.Manager for cross-chain UTXO parsing (wallet/network/primary)
    → utxo.ParseUTXO via the ZAP wire dispatcher already registered
      by node/vms/components/lux. Drops the per-chain codec.Manager
      slot from FetchState; AddAllUTXOs no longer takes a codec.
      Underscore-import of vms/components/lux ensures the dispatcher
      is wired even for callers that don't already pull platformvm.

No snow/snowman/snowball/avalanche/avm/subnet residue introduced.
2026-06-05 11:57:14 -07:00

1339 lines
36 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package peer
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"math"
"net"
"net/netip"
"sync"
"sync/atomic"
"time"
"github.com/luxfi/log"
"github.com/luxfi/constants"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/net/endpoints"
"github.com/luxfi/node/message"
"github.com/luxfi/node/proto/p2p"
"github.com/luxfi/node/staking"
"github.com/luxfi/node/utils/bloom"
"github.com/luxfi/node/utils/json"
"github.com/luxfi/node/utils/wrappers"
"github.com/luxfi/node/version"
"github.com/luxfi/utils"
)
const (
// maxBloomSaltLen restricts the allowed size of the bloom salt to prevent
// excessively expensive bloom filter contains checks.
maxBloomSaltLen = 32
// maxNumTrackedChains limits how many chains a peer can track to prevent
// excessive memory usage.
maxNumTrackedChains = 16
disconnectingLog = "disconnecting from peer"
failedToCreateMessageLog = "failed to create message"
failedToSetDeadlineLog = "failed to set connection deadline"
failedToGetUptimeLog = "failed to get peer uptime percentage"
malformedMessageLog = "malformed message"
)
var (
errClosed = errors.New("closed")
_ Peer = (*peer)(nil)
)
// Peer encapsulates all of the functionality required to send and receive
// messages with a remote peer.
type Peer interface {
// ID returns the nodeID of the remote peer.
ID() ids.NodeID
// Cert returns the certificate that the remote peer is using to
// authenticate their messages.
Cert() *staking.Certificate
// LastSent returns the last time a message was sent to the peer.
LastSent() time.Time
// LastReceived returns the last time a message was received from the peer.
LastReceived() time.Time
// Ready returns true if the peer has finished the p2p handshake and is
// ready to send and receive messages.
Ready() bool
// AwaitReady will block until the peer has finished the p2p handshake. If
// the context is cancelled or the peer starts closing, then an error will
// be returned.
AwaitReady(ctx context.Context) error
// Info returns a description of the state of this peer. It should only be
// called after [Ready] returns true.
Info() Info
// IP returns the claimed IP and signature provided by this peer during the
// handshake. It should only be called after [Ready] returns true.
IP() *SignedIP
// Version returns the claimed node version this peer is running. It should
// only be called after [Ready] returns true.
Version() *version.Application
// TrackedChains returns the chains this peer is running. It should only
// be called after [Ready] returns true.
TrackedChains() set.Set[ids.ID]
// ObservedUptime returns the local node's primary network uptime according to the
// peer. The value ranges from [0, 100]. It should only be called after
// [Ready] returns true.
ObservedUptime() uint32
// Send attempts to send [msg] to the peer. The peer takes ownership of
// [msg] for reference counting. This returns false if the message is
// guaranteed not to be delivered to the peer.
Send(ctx context.Context, msg message.OutboundMessage) bool
// StartSendGetPeerList attempts to send a GetPeerList message to this peer
// on this peer's gossip routine. It is not guaranteed that a GetPeerList
// will be sent.
StartSendGetPeerList()
// StartClose will begin shutting down the peer. It will not block.
StartClose()
// Closed returns true once the peer has been fully shutdown. It is
// guaranteed that no more messages will be received by this peer once this
// returns true.
Closed() bool
// AwaitClosed will block until the peer has been fully shutdown. If the
// context is cancelled, then an error will be returned.
AwaitClosed(ctx context.Context) error
}
type peer struct {
*Config
// the connection object that is used to read/write messages from
conn net.Conn
// [cert] is this peer's certificate, specifically the leaf of the
// certificate chain they provided.
cert *staking.Certificate
// node ID of this peer.
id ids.NodeID
// queue of messages to send to this peer.
messageQueue MessageQueue
// ip is the claimed IP the peer gave us in the Handshake message.
ip *SignedIP
// version is the claimed version the peer is running that we received in
// the Handshake message.
version *version.Application
// trackedChains are the chainIDs the peer sent us in the Handshake
// message. The primary network ID is always included.
trackedChains set.Set[ids.ID]
// options of LPs provided in the Handshake message.
supportedLPs set.Set[uint32]
objectedLPs set.Set[uint32]
// txIDOfVerifiedBLSKey is the txID that added the BLS key that was most
// recently verified to have signed the IP.
//
// Invariant: Prior to the handshake being completed, this can only be
// accessed by the reader goroutine. After the handshake has been completed,
// this can only be accessed by the message sender goroutine.
txIDOfVerifiedBLSKey ids.ID
// Our primary network uptime perceived by the peer
observedUptime utils.Atomic[uint32]
// True if this peer has sent us a valid Handshake message and
// is running a compatible version.
// Only modified on the connection's reader routine.
gotHandshake utils.Atomic[bool]
// True if the peer:
// * Has sent us a Handshake message
// * Has sent us a PeerList message
// * Is running a compatible version
// Only modified on the connection's reader routine.
finishedHandshake utils.Atomic[bool]
// onFinishHandshake is closed when the peer finishes the p2p handshake.
onFinishHandshake chan struct{}
// numExecuting is the number of goroutines this peer is currently using
numExecuting int64
startClosingOnce sync.Once
// onClosingCtx is canceled when the peer starts closing
onClosingCtx context.Context
// onClosingCtxCancel cancels onClosingCtx
onClosingCtxCancel func()
// onClosed is closed when the peer is closed
onClosed chan struct{}
// Unix time of the last message sent and received respectively
// Must only be accessed atomically
lastSent, lastReceived int64
// getPeerListChan signals that we should attempt to send a GetPeerList to
// this peer
getPeerListChan chan struct{}
// isIngress is true only if the remote peer is connected to this node,
// in contrast of this node being connected to the remote peer.
isIngress bool
// pqAEADKey is the 32-byte AEAD session key derived by the strict-PQ
// application-layer handshake (ML-KEM + ML-DSA-65). Zero-valued on
// chains that don't run the PQ handshake; non-zero only after
// runPQHandshakeIfRequired completes successfully. The AEAD wrapper
// over p.conn reads this key once at handshake completion; it is
// never mutated after Start returns.
pqAEADKey [32]byte
}
// Start a new peer instance.
//
// Invariant: There must only be one peer running at a time with a reference to
// the same [config.InboundMsgThrottler].
//
// On strict-PQ chains (config.PQHandshakeConfig non-nil) Start runs the
// application-layer ML-KEM + ML-DSA-65 handshake synchronously on the wire
// before launching the message-pump goroutines. The role is derived from
// isIngress — the dial side is the initiator, the accept side is the
// responder. If the PQ handshake fails the connection is closed and the
// returned Peer is in the closed state; no fallback to bare TLS.
func Start(
config *Config,
conn net.Conn,
cert *staking.Certificate,
id ids.NodeID,
messageQueue MessageQueue,
isIngress bool,
) Peer {
onClosingCtx, onClosingCtxCancel := context.WithCancel(context.Background())
p := &peer{
isIngress: isIngress,
Config: config,
conn: conn,
cert: cert,
id: id,
messageQueue: messageQueue,
onFinishHandshake: make(chan struct{}),
numExecuting: 3,
onClosingCtx: onClosingCtx,
onClosingCtxCancel: onClosingCtxCancel,
onClosed: make(chan struct{}),
getPeerListChan: make(chan struct{}, 1),
trackedChains: make(set.Set[ids.ID]),
}
if isIngress {
p.IngressConnectionCount.Add(1)
}
// PQ handshake gate: strict-PQ chains run the application-layer
// ML-KEM + ML-DSA-65 handshake on the wire BEFORE any p2p message
// is exchanged. Permissive / classical-compat chains skip this step.
if err := p.runPQHandshakeIfRequired(); err != nil {
p.Log.Warn("PQ handshake refused; closing connection",
log.Stringer("nodeID", p.id),
log.Bool("isIngress", isIngress),
log.Reflect("error", err),
)
// Mark all 3 goroutines as not running so close() doesn't wait
// for them. The connection is torn down; the peer is closed.
atomic.StoreInt64(&p.numExecuting, 1)
_ = p.conn.Close()
p.close()
return p
}
go p.readMessages()
go p.writeMessages()
go p.sendNetworkMessages()
return p
}
// runPQHandshakeIfRequired runs the strict-PQ application-layer handshake
// synchronously over p.conn. Returns nil when the chain is on the legacy
// bare-TLS path (no PQHandshakeConfig pinned) so the caller proceeds
// unchanged. On strict-PQ, completes INIT / RESP / FINISH and stashes the
// derived AEAD key on the peer; subsequent application messages are
// authenticated against this key by the AEAD wrapper installed by the
// next CR. Wire framing mirrors the peer's existing 4-byte length-prefix
// scheme so the framing logic is identical on both sides of the wire.
func (p *peer) runPQHandshakeIfRequired() error {
cfg := p.Config
if cfg == nil || cfg.PQHandshakeConfig == nil || cfg.PQLocalIdentity == nil {
return nil
}
deadline := time.Now().Add(cfg.MaxClockDifference + 30*time.Second)
if err := p.conn.SetDeadline(deadline); err != nil {
return fmt.Errorf("set PQ handshake deadline: %w", err)
}
defer func() {
// Clear deadline so the rest of the peer flow uses its own.
_ = p.conn.SetDeadline(time.Time{})
}()
if p.isIngress {
// Responder: read INIT, send RESP, derive AEAD.
initBytes, err := readPQFrame(p.conn)
if err != nil {
return fmt.Errorf("read PQ INIT: %w", err)
}
init, err := parsePQHandshakeInit(initBytes, cfg.PQHandshakeConfig.KEMScheme)
if err != nil {
return fmt.Errorf("parse PQ INIT: %w", err)
}
resp, result, err := RespondHandshake(cfg.PQHandshakeConfig, cfg.PQLocalIdentity, init)
if err != nil {
return fmt.Errorf("respond PQ handshake: %w", err)
}
if err := writePQFrame(p.conn, resp.canonicalBytes()); err != nil {
return fmt.Errorf("write PQ RESP: %w", err)
}
p.pqAEADKey = result.AEADKey
return nil
}
// Initiator: send INIT, read RESP, finalize.
init, kemSec, err := InitiateHandshake(cfg.PQHandshakeConfig, cfg.PQLocalIdentity)
if err != nil {
return fmt.Errorf("initiate PQ handshake: %w", err)
}
if err := writePQFrame(p.conn, init.canonicalBytes()); err != nil {
return fmt.Errorf("write PQ INIT: %w", err)
}
respBytes, err := readPQFrame(p.conn)
if err != nil {
return fmt.Errorf("read PQ RESP: %w", err)
}
resp, err := parsePQHandshakeResp(respBytes, cfg.PQHandshakeConfig.KEMScheme)
if err != nil {
return fmt.Errorf("parse PQ RESP: %w", err)
}
result, err := FinishInitiatorHandshake(cfg.PQHandshakeConfig, cfg.PQLocalIdentity, init, resp, kemSec)
if err != nil {
return fmt.Errorf("finish PQ handshake: %w", err)
}
p.pqAEADKey = result.AEADKey
return nil
}
func (p *peer) ID() ids.NodeID {
return p.id
}
func (p *peer) Cert() *staking.Certificate {
return p.cert
}
func (p *peer) LastSent() time.Time {
return time.Unix(
atomic.LoadInt64(&p.lastSent),
0,
)
}
func (p *peer) LastReceived() time.Time {
return time.Unix(
atomic.LoadInt64(&p.lastReceived),
0,
)
}
func (p *peer) Ready() bool {
return p.finishedHandshake.Get()
}
func (p *peer) AwaitReady(ctx context.Context) error {
select {
case <-p.onFinishHandshake:
return nil
case <-p.onClosed:
return errClosed
case <-ctx.Done():
return ctx.Err()
}
}
func (p *peer) Info() Info {
primaryUptime := p.ObservedUptime()
ip, _ := endpoints.ParseAddrPort(p.conn.RemoteAddr().String())
return Info{
IP: ip,
PublicIP: p.ip.AddrPort,
ID: p.id,
Version: p.version.String(),
LastSent: p.LastSent(),
LastReceived: p.LastReceived(),
ObservedUptime: json.Uint32(primaryUptime),
TrackedChains: p.trackedChains,
SupportedLPs: p.supportedLPs,
ObjectedLPs: p.objectedLPs,
}
}
func (p *peer) IP() *SignedIP {
return p.ip
}
func (p *peer) Version() *version.Application {
return p.version
}
func (p *peer) TrackedChains() set.Set[ids.ID] {
return p.trackedChains
}
func (p *peer) ObservedUptime() uint32 {
return p.observedUptime.Get()
}
func (p *peer) Send(ctx context.Context, msg message.OutboundMessage) bool {
return p.messageQueue.Push(ctx, msg)
}
func (p *peer) StartSendGetPeerList() {
select {
case p.getPeerListChan <- struct{}{}:
default:
}
}
func (p *peer) StartClose() {
p.startClosingOnce.Do(func() {
if p.conn != nil {
if err := p.conn.Close(); err != nil {
if !p.Log.IsZero() {
p.Log.Debug("failed to close connection",
log.Stringer("nodeID", p.id),
log.Reflect("error", err),
)
}
}
}
if p.messageQueue != nil {
p.messageQueue.Close()
}
if p.onClosingCtxCancel != nil {
p.onClosingCtxCancel()
}
})
}
func (p *peer) Closed() bool {
select {
case _, ok := <-p.onClosed:
return !ok
default:
return false
}
}
func (p *peer) AwaitClosed(ctx context.Context) error {
select {
case <-p.onClosed:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
// close should be called at the end of each goroutine that has been spun up.
// When the last goroutine is exiting, the peer will be marked as closed.
func (p *peer) close() {
if atomic.AddInt64(&p.numExecuting, -1) != 0 {
return
}
if p.isIngress {
p.IngressConnectionCount.Add(-1)
}
p.Network.Disconnected(p.id)
close(p.onClosed)
}
// Read and handle messages from this peer.
// When this method returns, the connection is closed.
func (p *peer) readMessages() {
// Track this node with the inbound message throttler.
p.InboundMsgThrottler.AddNode(p.id)
defer func() {
p.InboundMsgThrottler.RemoveNode(p.id)
p.StartClose()
p.close()
}()
// Continuously read and handle messages from this peer.
reader := bufio.NewReaderSize(p.conn, p.Config.ReadBufferSize)
msgLenBytes := make([]byte, wrappers.IntLen)
for {
// Time out and close connection if we can't read the message length
if err := p.conn.SetReadDeadline(p.nextTimeout()); err != nil {
p.Log.Debug(failedToSetDeadlineLog,
log.Stringer("nodeID", p.id),
log.String("direction", "read"),
log.Reflect("error", err),
)
return
}
// Read the message length
if _, err := io.ReadFull(reader, msgLenBytes); err != nil {
p.Log.Debug("error reading message length",
log.Stringer("nodeID", p.id),
log.Reflect("error", err),
)
return
}
// Parse the message length
msgLen, err := readMsgLen(msgLenBytes, constants.DefaultMaxMessageSize)
if err != nil {
p.Log.Debug("error parsing message length",
log.Stringer("nodeID", p.id),
log.Reflect("error", err),
)
return
}
// Wait until the throttler says we can proceed to read the message.
//
// Invariant: When done processing this message, onFinishedHandling() is
// called exactly once. If this is not honored, the message throttler
// will leak until no new messages can be read. You can look at message
// throttler metrics to verify that there is no leak.
//
// Invariant: There must only be one call to Acquire at any given time
// with the same nodeID. In this package, only this goroutine ever
// performs Acquire. Additionally, we ensure that this goroutine has
// exited before calling [Network.Disconnected] to guarantee that there
// can't be multiple instances of this goroutine running over different
// peer instances.
onFinishedHandling := p.InboundMsgThrottler.Acquire(
p.onClosingCtx,
uint64(msgLen),
p.id,
)
// If the peer is shutting down, there's no need to read the message.
if err := p.onClosingCtx.Err(); err != nil {
onFinishedHandling()
return
}
// Time out and close connection if we can't read message
if err := p.conn.SetReadDeadline(p.nextTimeout()); err != nil {
p.Log.Debug(failedToSetDeadlineLog,
log.Stringer("nodeID", p.id),
log.String("direction", "read"),
log.Reflect("error", err),
)
onFinishedHandling()
return
}
// Read the message
msgBytes := make([]byte, msgLen)
if _, err := io.ReadFull(reader, msgBytes); err != nil {
p.Log.Debug("error reading message",
log.Stringer("nodeID", p.id),
log.Reflect("error", err),
)
onFinishedHandling()
return
}
// Track the time it takes from now until the time the message is
// handled (in the event this message is handled at the network level)
// or the time the message is handed to the router (in the event this
// message is not handled at the network level.)
// Resource tracking is now handled internally by ResourceTracker
p.Log.Debug("parsing message",
log.Stringer("nodeID", p.id),
log.Binary("messageBytes", msgBytes),
)
// Parse the message
msg, err := p.MessageCreator.Parse(msgBytes, p.id, onFinishedHandling)
if err != nil {
p.Log.Debug("failed to parse message",
log.Stringer("nodeID", p.id),
log.Binary("messageBytes", msgBytes),
log.Reflect("error", err),
)
p.Metrics.NumFailedToParse.Inc()
// Couldn't parse the message. Read the next one.
onFinishedHandling()
continue
}
now := p.Clock.Time()
p.storeLastReceived(now)
p.Metrics.Received(msg, msgLen)
// Handle the message. Note that when we are done handling this message,
// we must call [msg.OnFinishedHandling()].
p.handle(msg)
}
}
func (p *peer) writeMessages() {
defer func() {
p.StartClose()
p.close()
}()
writer := bufio.NewWriterSize(p.conn, p.Config.WriteBufferSize)
// Make sure that the Handshake is the first message sent
mySignedIP, err := p.IPSigner.GetSignedIP()
if err != nil {
p.Log.Error("failed to get signed IP",
log.Stringer("nodeID", p.id),
log.Reflect("error", err),
)
return
}
if port := mySignedIP.AddrPort.Port(); port == 0 {
p.Log.Error("signed IP has invalid port",
log.Stringer("nodeID", p.id),
log.Uint16("port", port),
)
return
}
myVersion := p.VersionCompatibility.Version()
knownPeersFilter, knownPeersSalt := p.Network.KnownPeers()
_, areWeAPrimaryNetworkValidator := p.Validators.GetValidator(constants.PrimaryNetworkID, p.MyNodeID)
msg, err := p.MessageCreator.Handshake(
p.NetworkID,
p.Clock.Unix(),
mySignedIP.AddrPort,
myVersion.Name,
uint32(myVersion.Major),
uint32(myVersion.Minor),
uint32(myVersion.Patch),
mySignedIP.Timestamp,
mySignedIP.TLSSignature,
mySignedIP.BLSSignatureBytes,
p.MyChains.List(),
p.SupportedLPs,
p.ObjectedLPs,
knownPeersFilter,
knownPeersSalt,
areWeAPrimaryNetworkValidator,
mySignedIP.MLDSASignature,
)
if err != nil {
p.Log.Error(failedToCreateMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.HandshakeOp),
log.Reflect("error", err),
)
return
}
p.writeMessage(writer, msg)
for {
msg, ok := p.messageQueue.PopNow()
if ok {
p.writeMessage(writer, msg)
continue
}
// Make sure the peer was fully sent all prior messages before
// blocking.
if err := writer.Flush(); err != nil {
p.Log.Debug("failed to flush writer",
log.Stringer("nodeID", p.id),
log.Reflect("error", err),
)
return
}
msg, ok = p.messageQueue.Pop()
if !ok {
// This peer is closing
return
}
p.writeMessage(writer, msg)
}
}
func (p *peer) writeMessage(writer io.Writer, msg message.OutboundMessage) {
msgBytes := msg.Bytes()
p.Log.Verbo("sending message",
log.Stringer("op", msg.Op()),
log.Stringer("nodeID", p.id),
log.Binary("messageBytes", msgBytes),
)
if err := p.conn.SetWriteDeadline(p.nextTimeout()); err != nil {
p.Log.Debug(failedToSetDeadlineLog,
log.Stringer("nodeID", p.id),
log.String("direction", "write"),
log.Reflect("error", err),
)
return
}
msgLen := uint32(len(msgBytes))
msgLenBytes, err := writeMsgLen(msgLen, constants.DefaultMaxMessageSize)
if err != nil {
p.Log.Debug("error writing message length",
log.Stringer("nodeID", p.id),
log.Reflect("error", err),
)
return
}
// Write the message
var buf net.Buffers = [][]byte{msgLenBytes[:], msgBytes}
if _, err := io.CopyN(writer, &buf, int64(wrappers.IntLen+msgLen)); err != nil {
p.Log.Debug("error writing message",
log.Stringer("nodeID", p.id),
log.Reflect("error", err),
)
return
}
now := p.Clock.Time()
p.storeLastSent(now)
p.Metrics.Sent(msg)
}
func (p *peer) sendNetworkMessages() {
sendPingsTicker := time.NewTicker(p.PingFrequency)
defer func() {
sendPingsTicker.Stop()
p.StartClose()
p.close()
}()
for {
select {
case <-p.getPeerListChan:
knownPeersFilter, knownPeersSalt := p.Config.Network.KnownPeers()
_, areWeAPrimaryNetworkValidator := p.Validators.GetValidator(constants.PrimaryNetworkID, p.MyNodeID)
msg, err := p.Config.MessageCreator.GetPeerList(
knownPeersFilter,
knownPeersSalt,
areWeAPrimaryNetworkValidator,
)
if err != nil {
p.Log.Error(failedToCreateMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.GetPeerListOp),
log.Reflect("error", err),
)
return
}
p.Send(p.onClosingCtx, msg)
case <-sendPingsTicker.C:
if !p.Network.AllowConnection(p.id) {
p.Log.Debug(disconnectingLog,
log.String("reason", "connection is no longer desired"),
log.Stringer("nodeID", p.id),
)
return
}
// Only check if we should disconnect after the handshake is
// finished to avoid race conditions and accessing uninitialized
// values.
if p.finishedHandshake.Get() && p.shouldDisconnect() {
return
}
primaryUptime := p.getUptime()
pingMessage, err := p.MessageCreator.Ping(primaryUptime)
if err != nil {
p.Log.Error(failedToCreateMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.PingOp),
log.Reflect("error", err),
)
return
}
p.Send(p.onClosingCtx, pingMessage)
case <-p.onClosingCtx.Done():
return
}
}
}
// shouldDisconnect is called both during receipt of the Handshake message and
// periodically when sending a Ping message (after finishing the handshake!).
//
// It is called during the Handshake to prevent marking a peer as connected and
// then immediately disconnecting from them.
//
// It is called when sending a Ping message to account for validator set
// changes. It's called when sending a Ping rather than in a validator set
// callback to avoid signature verification on the P-chain accept path.
func (p *peer) shouldDisconnect() bool {
if err := p.VersionCompatibility.Compatible(p.version); err != nil {
p.Log.Debug(disconnectingLog,
log.String("reason", "version not compatible"),
log.Stringer("nodeID", p.id),
log.Stringer("peerVersion", p.version),
log.Reflect("error", err),
)
return true
}
// Enforce that all validators that have registered a BLS key are signing
// their IP with the signed-IP payload.
vdr, ok := p.Validators.GetValidator(constants.PrimaryNetworkID, p.id)
if !ok || vdr.PublicKey == nil || vdr.TxID == p.txIDOfVerifiedBLSKey {
return false
}
// Convert []byte public key to *bls.PublicKey
// Validator's public key is stored in uncompressed format (96 bytes)
blsPublicKey := bls.PublicKeyFromValidUncompressedBytes(vdr.PublicKey)
if blsPublicKey == nil {
p.Log.Debug(disconnectingLog,
log.String("reason", "invalid BLS public key"),
log.Stringer("nodeID", p.id),
)
return true
}
validSignature := bls.VerifyProofOfPossession(
blsPublicKey,
p.ip.BLSSignature,
p.ip.UnsignedIP.bytes(),
)
if !validSignature {
p.Log.Debug(disconnectingLog,
log.String("reason", "invalid BLS signature"),
log.Stringer("nodeID", p.id),
)
return true
}
// Avoid unnecessary signature verifications by only verifying the signature
// once per validation period.
p.txIDOfVerifiedBLSKey = vdr.TxID
return false
}
func (p *peer) handle(msg message.InboundMessage) {
switch m := msg.Message().(type) { // Network-related message types
case *p2p.Ping:
p.handlePing(m)
msg.OnFinishedHandling()
return
case *p2p.Pong:
p.handlePong(m)
msg.OnFinishedHandling()
return
case *p2p.Handshake:
p.handleHandshake(m)
msg.OnFinishedHandling()
return
case *p2p.GetPeerList:
p.handleGetPeerList(m)
msg.OnFinishedHandling()
return
case *p2p.PeerList:
p.handlePeerList(m)
msg.OnFinishedHandling()
return
}
if !p.finishedHandshake.Get() {
p.Log.Debug("dropping message",
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", msg.Op()),
log.String("reason", "handshake isn't finished"),
)
msg.OnFinishedHandling()
return
}
// Consensus and app-level messages
// Route the message to the application layer handler
p.Router.HandleInbound(context.Background(), msg)
msg.OnFinishedHandling()
}
func (p *peer) handlePing(msg *p2p.Ping) {
if msg.Uptime > 100 {
p.Log.Debug(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.PingOp),
log.Stringer("netID", constants.PrimaryNetworkID),
log.Uint32("uptime", msg.Uptime),
)
p.StartClose()
return
}
p.observedUptime.Set(msg.Uptime)
pongMessage, err := p.MessageCreator.Pong()
if err != nil {
p.Log.Error(failedToCreateMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.PongOp),
log.Reflect("error", err),
)
p.StartClose()
return
}
p.Send(p.onClosingCtx, pongMessage)
}
func (p *peer) getUptime() uint32 {
primaryUptime, err := p.UptimeCalculator.CalculateUptimePercent(
p.id,
constants.PrimaryNetworkID,
)
if err != nil {
p.Log.Debug(failedToGetUptimeLog,
log.Stringer("nodeID", p.id),
log.Stringer("netID", constants.PrimaryNetworkID),
log.Reflect("error", err),
)
primaryUptime = 0
}
primaryUptimePercent := uint32(primaryUptime * 100)
return primaryUptimePercent
}
func (*peer) handlePong(*p2p.Pong) {}
func (p *peer) handleHandshake(msg *p2p.Handshake) {
if p.gotHandshake.Get() {
p.Log.Debug(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.HandshakeOp),
log.String("reason", "already received handshake"),
)
p.StartClose()
return
}
if msg.NetworkId != p.NetworkID {
p.Log.Debug(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.HandshakeOp),
log.String("field", "networkID"),
log.Uint32("peerNetworkID", msg.NetworkId),
log.Uint32("ourNetworkID", p.NetworkID),
)
p.StartClose()
return
}
localTime := p.Clock.Time()
localUnixTime := uint64(localTime.Unix())
clockDifference := math.Abs(float64(msg.MyTime) - float64(localUnixTime))
p.Metrics.ClockSkewCount.Inc()
p.Metrics.ClockSkewSum.Add(clockDifference)
if clockDifference > p.MaxClockDifference.Seconds() {
logFunc := p.Log.Debug
if _, ok := p.Beacons.GetValidator(constants.PrimaryNetworkID, p.id); ok {
logFunc = p.Log.Warn
}
logFunc(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.HandshakeOp),
log.String("field", "myTime"),
log.Uint64("peerTime", msg.MyTime),
log.Uint64("localTime", localUnixTime),
)
p.StartClose()
return
}
p.version = &version.Application{
Name: msg.Client.GetName(),
Major: int(msg.Client.GetMajor()),
Minor: int(msg.Client.GetMinor()),
Patch: int(msg.Client.GetPatch()),
}
if p.VersionCompatibility.Version().Before(p.version) {
logFunc := p.Log.Debug
if _, ok := p.Beacons.GetValidator(constants.PrimaryNetworkID, p.id); ok {
logFunc = p.Log.Info
}
logFunc("peer attempting to connect with newer version. You may want to update your client",
log.Stringer("nodeID", p.id),
log.Stringer("peerVersion", p.version),
)
}
// handle chain IDs
if numTrackedChains := len(msg.TrackedNets); numTrackedChains > maxNumTrackedChains {
p.Log.Debug(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.HandshakeOp),
log.String("field", "trackedChains"),
log.Int("numTrackedChains", numTrackedChains),
)
p.StartClose()
return
}
p.trackedChains.Add(constants.PrimaryNetworkID)
for _, chainIDBytes := range msg.TrackedNets {
chainID, err := ids.ToID(chainIDBytes)
if err != nil {
p.Log.Debug(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.HandshakeOp),
log.String("field", "trackedChains"),
log.Reflect("error", err),
)
p.StartClose()
return
}
p.trackedChains.Add(chainID)
}
for _, lp := range msg.SupportedLps {
if constants.CurrentLPs.Contains(lp) {
p.supportedLPs.Add(lp)
}
}
for _, lp := range msg.ObjectedLps {
if constants.CurrentLPs.Contains(lp) {
p.objectedLPs.Add(lp)
}
}
if p.supportedLPs.Overlaps(p.objectedLPs) {
p.Log.Debug(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.HandshakeOp),
log.String("field", "lps"),
log.Reflect("supportedLPs", p.supportedLPs),
log.Reflect("objectedLPs", p.objectedLPs),
)
p.StartClose()
return
}
var (
knownPeers = bloom.EmptyFilter
salt []byte
)
if msg.KnownPeers != nil {
var err error
knownPeers, err = bloom.Parse(msg.KnownPeers.Filter)
if err != nil {
p.Log.Debug(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.HandshakeOp),
log.String("field", "knownPeers.filter"),
log.Reflect("error", err),
)
p.StartClose()
return
}
salt = msg.KnownPeers.Salt
if saltLen := len(salt); saltLen > maxBloomSaltLen {
p.Log.Debug(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.HandshakeOp),
log.String("field", "knownPeers.salt"),
log.Int("saltLen", saltLen),
)
p.StartClose()
return
}
}
addr, ok := endpoints.AddrFromSlice(msg.IpAddr)
if !ok {
p.Log.Debug(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.HandshakeOp),
log.String("field", "ip"),
log.Int("ipLen", len(msg.IpAddr)),
)
p.StartClose()
return
}
port := uint16(msg.IpPort)
if msg.IpPort == 0 {
p.Log.Debug(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.HandshakeOp),
log.String("field", "port"),
log.Uint16("port", port),
)
p.StartClose()
return
}
p.ip = &SignedIP{
UnsignedIP: UnsignedIP{
AddrPort: netip.AddrPortFrom(
addr,
port,
),
Timestamp: msg.IpSigningTime,
},
TLSSignature: msg.IpNodeIdSig,
// Empty on legacy peers (classical-only); the SignedIP.Verify
// call below only enforces the ML-DSA leg under strict-PQ
// profile and only when the validator's ML-DSA public key is
// known to the verifier. The wire format tolerates absence.
MLDSASignature: msg.GetIpMldsaSig(),
}
maxTimestamp := localTime.Add(p.MaxClockDifference)
if err := p.ip.Verify(p.cert, maxTimestamp); err != nil {
logFunc := p.Log.Debug
if _, ok := p.Beacons.GetValidator(constants.PrimaryNetworkID, p.id); ok {
logFunc = p.Log.Warn
}
logFunc(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.HandshakeOp),
log.String("field", "tlsSignature"),
log.Uint64("peerTime", msg.MyTime),
log.Uint64("localTime", localUnixTime),
log.Reflect("error", err),
)
p.StartClose()
return
}
signature, err := bls.SignatureFromBytes(msg.IpBlsSig)
if err != nil {
p.Log.Debug(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.HandshakeOp),
log.String("field", "blsSignature"),
log.Reflect("error", err),
)
p.StartClose()
return
}
p.ip.BLSSignature = signature
p.ip.BLSSignatureBytes = msg.IpBlsSig
// If the peer is running an incompatible version or has an invalid BLS
// signature, disconnect from them prior to marking the handshake as
// completed.
if p.shouldDisconnect() {
p.StartClose()
return
}
p.gotHandshake.Set(true)
peerIPs := p.Network.Peers(p.id, p.trackedChains, msg.AllChains, knownPeers, salt)
// We bypass throttling here to ensure that the handshake message is
// acknowledged correctly.
peerListMsg, err := p.Config.MessageCreator.PeerList(peerIPs, true /*=bypassThrottling*/)
if err != nil {
p.Log.Error(failedToCreateMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.PeerListOp),
log.Reflect("error", err),
)
p.StartClose()
return
}
if !p.Send(p.onClosingCtx, peerListMsg) {
// Because throttling was marked to be bypassed with this message,
// sending should only fail if the peer has started closing.
p.Log.Debug("failed to send reliable message",
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.PeerListOp),
log.Reflect("error", p.onClosingCtx.Err()),
)
p.StartClose()
}
}
func (p *peer) handleGetPeerList(msg *p2p.GetPeerList) {
if !p.finishedHandshake.Get() {
p.Log.Debug(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.GetPeerListOp),
log.String("reason", "not finished handshake"),
)
return
}
knownPeersMsg := msg.GetKnownPeers()
filter, err := bloom.Parse(knownPeersMsg.GetFilter())
if err != nil {
p.Log.Debug(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.GetPeerListOp),
log.String("field", "knownPeers.filter"),
log.Reflect("error", err),
)
p.StartClose()
return
}
salt := knownPeersMsg.GetSalt()
if saltLen := len(salt); saltLen > maxBloomSaltLen {
p.Log.Debug(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.GetPeerListOp),
log.String("field", "knownPeers.salt"),
log.Int("saltLen", saltLen),
)
p.StartClose()
return
}
peerIPs := p.Network.Peers(p.id, p.trackedChains, msg.AllChains, filter, salt)
if len(peerIPs) == 0 {
p.Log.Debug("skipping sending of empty peer list",
log.Stringer("nodeID", p.id),
)
return
}
// Bypass throttling is disabled here to follow the non-handshake message
// sending pattern.
peerListMsg, err := p.Config.MessageCreator.PeerList(peerIPs, false /*=bypassThrottling*/)
if err != nil {
p.Log.Error(failedToCreateMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.PeerListOp),
log.Reflect("error", err),
)
return
}
p.Send(p.onClosingCtx, peerListMsg)
}
func (p *peer) handlePeerList(msg *p2p.PeerList) {
if !p.finishedHandshake.Get() {
if !p.gotHandshake.Get() {
return
}
p.Network.Connected(p.id)
p.finishedHandshake.Set(true)
close(p.onFinishHandshake)
}
discoveredIPs := make([]*endpoints.ClaimedIPPort, len(msg.ClaimedIpPorts)) // the peers this peer told us about
for i, claimedIPPort := range msg.ClaimedIpPorts {
tlsCert, err := staking.ParseCertificate(claimedIPPort.X509Certificate)
if err != nil {
p.Log.Debug(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.PeerListOp),
log.String("field", "cert"),
log.Reflect("error", err),
)
p.StartClose()
return
}
addr, ok := endpoints.AddrFromSlice(claimedIPPort.IpAddr)
if !ok {
p.Log.Debug(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.PeerListOp),
log.String("field", "ip"),
log.Int("ipLen", len(claimedIPPort.IpAddr)),
)
p.StartClose()
return
}
port := uint16(claimedIPPort.IpPort)
if port == 0 {
p.Log.Debug(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.PeerListOp),
log.String("field", "port"),
log.Uint16("port", port),
)
p.StartClose()
return
}
discoveredIPs[i] = endpoints.NewClaimedIPPort(
tlsCert,
netip.AddrPortFrom(
addr,
port,
),
claimedIPPort.Timestamp,
claimedIPPort.Signature,
)
}
if err := p.Network.Track(discoveredIPs); err != nil {
p.Log.Debug(malformedMessageLog,
log.Stringer("nodeID", p.id),
log.Stringer("messageOp", message.PeerListOp),
log.String("field", "claimedIP"),
log.Reflect("error", err),
)
}
}
func (p *peer) nextTimeout() time.Time {
return p.Clock.Time().Add(p.PongTimeout)
}
func (p *peer) storeLastSent(time time.Time) {
unixTime := time.Unix()
atomic.StoreInt64(&p.Config.LastSent, unixTime)
atomic.StoreInt64(&p.lastSent, unixTime)
}
func (p *peer) storeLastReceived(time time.Time) {
unixTime := time.Unix()
atomic.StoreInt64(&p.Config.LastReceived, unixTime)
atomic.StoreInt64(&p.lastReceived, unixTime)
}