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...)
}