Files
zeekayandHanzo Dev 37e2940ead mesh is the peer layer, network is the validator set
Two packages were named for the same word and meant different things. network/
dialed peers, exchanged keys, throttled and tracked them; nets/ tracked which
chains share a validator set and when they have all bootstrapped. One letter
apart, and neither name said which was which.

The peer layer is mesh — the word this estate already uses for it, in
lux-rs/node's own mesh.rs. That frees network for the thing that is actually a
network: a set of chains sharing a validator set. A network holds chains; a
chain belongs to one network.

github.com/luxfi/p2p stays what it is, the gossip library the VMs use. Three
concerns, three names, none of them borrowed from another.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-07 21:17:09 -07:00

134 lines
4.2 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package peer
import (
"crypto"
"net/netip"
"sync"
"time"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/crypto/mldsa"
"github.com/luxfi/timer/mockable"
"github.com/luxfi/util"
)
const (
// ipResignInterval controls how often we re-sign our IP to keep the
// signature fresh. Must be less than the reasonableClockSkewWindow in
// ip.go (10 minutes) to ensure peers always accept our signature.
ipResignInterval = 5 * time.Minute
)
// IPSigner will return a signedIP for the current value of our dynamic IP.
//
// Carries up to three signing legs: TLS (classical, staker cert),
// BLS12-381 (classical, proof-of-possession), and ML-DSA-65 (FIPS 204
// post-quantum). The ML-DSA leg is optional at construction time —
// passing mldsaSigner == nil to NewIPSigner produces classical-only
// gossip that strict-PQ verifiers refuse. Strict-PQ chains MUST be wired
// with a real ML-DSA-65 private key.
type IPSigner struct {
ip *utils.Atomic[netip.AddrPort]
clock mockable.Clock
tlsSigner crypto.Signer
blsSigner bls.Signer
mldsaSigner *mldsa.PrivateKey
// Must be held while accessing [signedIP]
signedIPLock sync.RWMutex
// Note that the values in [*signedIP] are constants and can be inspected
// without holding [signedIPLock].
signedIP *SignedIP
}
// NewIPSigner constructs a classical-only IPSigner (TLS + BLS legs). Use
// NewIPSignerPQ when the node has an ML-DSA-65 identity key and the chain
// runs under a strict-PQ profile.
func NewIPSigner(
ip *utils.Atomic[netip.AddrPort],
tlsSigner crypto.Signer,
blsSigner bls.Signer,
) *IPSigner {
return NewIPSignerPQ(ip, tlsSigner, blsSigner, nil)
}
// NewIPSignerPQ constructs an IPSigner that signs every refresh with all
// three legs the caller supplies. mldsaSigner may be nil to keep the
// classical-only behaviour (for permissive chains or for tests that have
// not yet generated a long-term ML-DSA key).
func NewIPSignerPQ(
ip *utils.Atomic[netip.AddrPort],
tlsSigner crypto.Signer,
blsSigner bls.Signer,
mldsaSigner *mldsa.PrivateKey,
) *IPSigner {
return &IPSigner{
ip: ip,
tlsSigner: tlsSigner,
blsSigner: blsSigner,
mldsaSigner: mldsaSigner,
}
}
// GetSignedIP returns the signedIP of the current value of the provided
// dynamicIP. If the dynamicIP hasn't changed and the signature is still fresh,
// then the same [SignedIP] will be returned. The signature is refreshed
// periodically to ensure it stays within the verification window.
//
// It's safe for multiple goroutines to concurrently call GetSignedIP.
func (s *IPSigner) GetSignedIP() (*SignedIP, error) {
// Optimistically, the IP should already be signed. By grabbing a read lock
// here we enable full concurrency of new connections.
s.signedIPLock.RLock()
signedIP := s.signedIP
s.signedIPLock.RUnlock()
ip := s.ip.Get()
if signedIP != nil && signedIP.AddrPort == ip && s.isFresh(signedIP) {
return signedIP, nil
}
// If our current IP hasn't been signed yet or the signature is stale,
// we should (re-)sign it.
s.signedIPLock.Lock()
defer s.signedIPLock.Unlock()
// It's possible that multiple threads read [n.signedIP] as incorrect at the
// same time, we should verify that we are the first thread to attempt to
// update it.
signedIP = s.signedIP
if signedIP != nil && signedIP.AddrPort == ip && s.isFresh(signedIP) {
return signedIP, nil
}
// We should now sign our new IP at the current timestamp.
unsignedIP := UnsignedIP{
AddrPort: ip,
Timestamp: s.clock.Unix(),
}
signedIP, err := unsignedIP.SignPQ(s.tlsSigner, s.blsSigner, s.mldsaSigner)
if err != nil {
return nil, err
}
s.signedIP = signedIP
return s.signedIP, nil
}
// isFresh returns true if the signed IP's timestamp is recent enough that
// peers will accept it within the reasonableClockSkewWindow.
func (s *IPSigner) isFresh(ip *SignedIP) bool {
now := s.clock.Unix()
return now-ip.Timestamp < uint64(ipResignInterval.Seconds())
}
// PublicKey returns the BLS public key for this IPSigner
func (s *IPSigner) PublicKey() *bls.PublicKey {
if s.blsSigner == nil {
return nil
}
return s.blsSigner.PublicKey()
}