mirror of
https://github.com/logos-messaging/logos-messaging-go.git
synced 2026-01-07 08:23:06 +00:00
232 lines
6.8 KiB
Go
232 lines
6.8 KiB
Go
package enr
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"encoding/binary"
|
|
"errors"
|
|
"math"
|
|
"math/rand"
|
|
"net"
|
|
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
"github.com/multiformats/go-multiaddr"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/waku-org/go-waku/waku/v2/node/utils"
|
|
"github.com/waku-org/go-waku/waku/v2/protocol"
|
|
)
|
|
|
|
func NewLocalnode(priv *ecdsa.PrivateKey) (*enode.LocalNode, error) {
|
|
db, err := enode.OpenDB("")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return enode.NewLocalNode(db, priv), nil
|
|
}
|
|
|
|
type ENROption func(*enode.LocalNode) error
|
|
|
|
func WithMultiaddress(multiaddrs ...multiaddr.Multiaddr) ENROption {
|
|
return func(localnode *enode.LocalNode) (err error) {
|
|
if len(multiaddrs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Randomly shuffle multiaddresses
|
|
rand.Shuffle(len(multiaddrs), func(i, j int) { multiaddrs[i], multiaddrs[j] = multiaddrs[j], multiaddrs[i] })
|
|
|
|
// Find the maximum number of multiaddresses that fit in the ENR size limit
|
|
maxFittingCount := findMaxFittingMultiaddrs(localnode, multiaddrs)
|
|
if maxFittingCount == 0 {
|
|
return errors.New("no multiaddress fit into ENR")
|
|
}
|
|
|
|
writeMultiaddressField(localnode, multiaddrs[0:maxFittingCount])
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// findMaxFittingMultiaddrs determines how many multiaddresses can fit in the ENR
|
|
func findMaxFittingMultiaddrs(localnode *enode.LocalNode, multiaddrs []multiaddr.Multiaddr) int {
|
|
privk, err := crypto.GenerateKey()
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
|
|
// Get the current committed record (after the Node() call above)
|
|
currentRecord := localnode.Node().Record()
|
|
|
|
// Binary search for optimal count
|
|
maxFitting := 0
|
|
|
|
for i := len(multiaddrs); i > 0; i-- {
|
|
if canFitMultiaddrsOnRecord(currentRecord, multiaddrs[0:i], privk, localnode.Seq()) {
|
|
// Return as soon as we can fit most of the addresses
|
|
return i
|
|
}
|
|
}
|
|
|
|
return maxFitting
|
|
}
|
|
|
|
// canFitMultiaddrsOnRecord tests if multiaddresses can fit on a specific record.
|
|
// ENR has a limit of 300 bytes. Later it will panic on signing, if the record is over the size limit.
|
|
// By simulating what the localnode does when signing the enr, but without causing a panic.
|
|
func canFitMultiaddrsOnRecord(baseRecord *enr.Record, addrs []multiaddr.Multiaddr, privk *ecdsa.PrivateKey, seq uint64) bool {
|
|
// Create a copy of the base record
|
|
testRecord := *baseRecord
|
|
|
|
// Add the multiaddress field
|
|
testRecord.Set(enr.WithEntry(MultiaddrENRField, marshalMultiaddress(addrs)))
|
|
testRecord.SetSeq(seq + 1)
|
|
|
|
// Try to sign - this will return an error if the record is too large
|
|
return enode.SignV4(&testRecord, privk) == nil
|
|
}
|
|
|
|
func WithWakuBitfield(flags WakuEnrBitfield) ENROption {
|
|
return func(localnode *enode.LocalNode) (err error) {
|
|
localnode.Set(enr.WithEntry(WakuENRField, flags))
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithIP(ipAddr *net.TCPAddr) ENROption {
|
|
return func(localnode *enode.LocalNode) (err error) {
|
|
if ipAddr.Port == 0 {
|
|
return ErrNoPortAvailable
|
|
}
|
|
|
|
localnode.SetStaticIP(ipAddr.IP)
|
|
localnode.Set(enr.TCP(uint16(ipAddr.Port))) // TODO: ipv6?
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithUDPPort(udpPort uint) ENROption {
|
|
return func(localnode *enode.LocalNode) (err error) {
|
|
if udpPort == 0 {
|
|
return nil
|
|
}
|
|
|
|
if udpPort > math.MaxUint16 {
|
|
return errors.New("invalid udp port number")
|
|
}
|
|
localnode.SetFallbackUDP(int(udpPort))
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Update applies the given ENR options to the localnode.
|
|
// This function should only be called from UpdateLocalNode, to ensure the order of options applied.
|
|
func Update(logger *zap.Logger, localnode *enode.LocalNode, enrOptions ...ENROption) error {
|
|
for _, opt := range enrOptions {
|
|
err := opt(localnode)
|
|
if err != nil {
|
|
if errors.Is(err, ErrNoPortAvailable) {
|
|
logger.Warn("no tcp port available. ENR will not contain tcp key")
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func marshalMultiaddress(addrAggr []multiaddr.Multiaddr) []byte {
|
|
var fieldRaw []byte
|
|
for _, addr := range addrAggr {
|
|
maRaw := addr.Bytes()
|
|
maSize := make([]byte, 2)
|
|
binary.BigEndian.PutUint16(maSize, uint16(len(maRaw)))
|
|
|
|
fieldRaw = append(fieldRaw, maSize...)
|
|
fieldRaw = append(fieldRaw, maRaw...)
|
|
}
|
|
return fieldRaw
|
|
}
|
|
|
|
func writeMultiaddressField(localnode *enode.LocalNode, addrAggr []multiaddr.Multiaddr) {
|
|
fieldRaw := marshalMultiaddress(addrAggr)
|
|
localnode.Set(enr.WithEntry(MultiaddrENRField, fieldRaw))
|
|
}
|
|
|
|
func DeleteField(localnode *enode.LocalNode, field string) {
|
|
localnode.Delete(enr.WithEntry(field, struct{}{}))
|
|
}
|
|
|
|
type LocalNodeParams struct {
|
|
Multiaddrs []multiaddr.Multiaddr
|
|
IPAddr *net.TCPAddr
|
|
UDPPort uint
|
|
WakuFlags WakuEnrBitfield
|
|
AdvertiseAddr []multiaddr.Multiaddr
|
|
ShouldAutoUpdate bool
|
|
RelayShards protocol.RelayShards
|
|
}
|
|
|
|
func UpdateLocalNode(logger *zap.Logger, localnode *enode.LocalNode, params *LocalNodeParams) error {
|
|
var options []ENROption
|
|
options = append(options, WithUDPPort(params.UDPPort))
|
|
options = append(options, WithWakuBitfield(params.WakuFlags))
|
|
|
|
// Reset ENR fields
|
|
DeleteField(localnode, MultiaddrENRField)
|
|
DeleteField(localnode, enr.TCP(0).ENRKey())
|
|
DeleteField(localnode, enr.IPv4{}.ENRKey())
|
|
DeleteField(localnode, enr.IPv6{}.ENRKey())
|
|
DeleteField(localnode, ShardingBitVectorEnrField)
|
|
DeleteField(localnode, ShardingIndicesListEnrField)
|
|
|
|
if params.AdvertiseAddr != nil {
|
|
// An advertised address disables libp2p address updates
|
|
// and discv5 predictions
|
|
ipAddr, err := utils.SelectMostExternalAddress(params.AdvertiseAddr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
options = append(options, WithIP(ipAddr))
|
|
} else if !params.ShouldAutoUpdate {
|
|
// We received a libp2p address update. Autoupdate is disabled
|
|
// Using a static ip will disable endpoint prediction.
|
|
options = append(options, WithIP(params.IPAddr))
|
|
} else {
|
|
if params.IPAddr.Port != 0 {
|
|
// We received a libp2p address update, but we should still
|
|
// allow discv5 to update the enr record. We set the localnode
|
|
// keys manually. It's possible that the ENR record might get
|
|
// updated automatically
|
|
ip4 := params.IPAddr.IP.To4()
|
|
ip6 := params.IPAddr.IP.To16()
|
|
if ip4 != nil && !ip4.IsUnspecified() {
|
|
localnode.SetFallbackIP(ip4)
|
|
localnode.Set(enr.IPv4(ip4))
|
|
localnode.Set(enr.TCP(uint16(params.IPAddr.Port)))
|
|
} else {
|
|
localnode.Delete(enr.IPv4{})
|
|
localnode.Delete(enr.TCP(0))
|
|
localnode.SetFallbackIP(net.IP{127, 0, 0, 1})
|
|
}
|
|
|
|
if ip4 == nil && ip6 != nil && !ip6.IsUnspecified() {
|
|
localnode.Set(enr.IPv6(ip6))
|
|
localnode.Set(enr.TCP6(params.IPAddr.Port))
|
|
} else {
|
|
localnode.Delete(enr.IPv6{})
|
|
localnode.Delete(enr.TCP6(0))
|
|
}
|
|
}
|
|
}
|
|
|
|
options = append(options, WithWakuRelaySharding(params.RelayShards))
|
|
|
|
// Writing the IP + Port has priority over writing the multiaddress which might fail or not
|
|
// depending on the enr having space
|
|
options = append(options, WithMultiaddress(params.Multiaddrs...))
|
|
|
|
return Update(logger, localnode, options...)
|
|
}
|