enr

package
v0.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 9, 2025 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package enr implements Ethereum Node Records (ENR) as defined in EIP-778.

An ENR is a signed, versioned data structure containing information about a node's network addresses and capabilities. Each record contains:

  • A sequence number (incremented on updates)
  • An identity scheme and signature
  • Arbitrary key-value pairs for node metadata

ENRs are limited to 300 bytes and are encoded using RLP (Recursive Length Prefix).

Index

Constants

View Source
const (
	// MaxRecordSize is the maximum allowed size of an ENR in bytes.
	// This limit ensures records can fit in UDP packets.
	MaxRecordSize = 300
)

Variables

View Source
var (
	// ErrRecordTooLarge is returned when an ENR exceeds MaxRecordSize.
	ErrRecordTooLarge = errors.New("enr: record size exceeds 300 bytes")

	// ErrInvalidSignature is returned when signature verification fails.
	ErrInvalidSignature = errors.New("enr: invalid signature")

	// ErrNoKey is returned when a requested key is not present in the record.
	ErrNoKey = errors.New("enr: key not found")

	// ErrInvalidRecord is returned when a record has invalid structure.
	ErrInvalidRecord = errors.New("enr: invalid record structure")
)

Functions

func IsLANAddress

func IsLANAddress(ip net.IP) bool

IsLANAddress checks if an IP address is a private/local address.

Returns true for:

  • 10.0.0.0/8 (RFC1918)
  • 172.16.0.0/12 (RFC1918)
  • 192.168.0.0/16 (RFC1918)
  • fc00::/7 (IPv6 ULA)
  • fe80::/10 (IPv6 link-local)
  • 127.0.0.0/8 (loopback)
  • ::1 (IPv6 loopback)

func IsWANAddress

func IsWANAddress(ip net.IP) bool

IsWANAddress checks if an IP address is a publicly routable address.

This is the inverse of IsLANAddress - returns true for public internet IPs.

func WithAttnets

func WithAttnets(attnets []byte) (string, interface{})

WithAttnets sets the attnets field in the record.

This field is a bitvector indicating which attestation subnets the Ethereum 2.0 node is subscribed to (used for subnet discovery).

Example:

attnets := make([]byte, 8) // 64-bit bitvector
attnets[0] = 0xFF // Subscribed to first 8 subnets
record.Set(WithAttnets(attnets))

func WithEth2

func WithEth2(data Eth2ENRData) (string, interface{})

WithEth2 sets the eth2 field in the record.

This field contains Ethereum 2.0 specific metadata including the fork digest, next fork version, and next fork epoch.

Example:

eth2Data := Eth2ENRData{
    ForkDigest: [4]byte{0x01, 0x02, 0x03, 0x04},
    NextForkVersion: [4]byte{0x00, 0x00, 0x00, 0x00},
    NextForkEpoch: 0,
}
record.Set(WithEth2(eth2Data))

func WithIP

func WithIP(ip net.IP) (string, interface{})

WithIP sets the IPv4 address in the record.

Example:

record.Set(WithIP(net.IPv4(192, 168, 1, 1)))

func WithIP6

func WithIP6(ip net.IP) (string, interface{})

WithIP6 sets the IPv6 address in the record.

Example:

record.Set(WithIP6(net.ParseIP("2001:db8::1")))

func WithIdentityScheme

func WithIdentityScheme(scheme string) (string, interface{})

WithIdentityScheme sets the identity scheme in the record.

Common schemes:

  • "v4": secp256k1-based identity (most common)

Example:

record.Set(WithIdentityScheme("v4"))

func WithPublicKey

func WithPublicKey(pubKey *ecdsa.PublicKey) (string, interface{})

WithPublicKey sets the secp256k1 public key in the record.

The key is stored in compressed form (33 bytes).

Example:

privateKey, _ := crypto.GenerateKey()
record.Set(WithPublicKey(&privateKey.PublicKey))

func WithSyncnets

func WithSyncnets(syncnets []byte) (string, interface{})

WithSyncnets sets the syncnets field in the record.

This field is a bitvector indicating which sync committee subnets the Ethereum 2.0 node is subscribed to.

Example:

syncnets := make([]byte, 1) // 4-bit bitvector
syncnets[0] = 0x0F // Subscribed to all 4 subnets
record.Set(WithSyncnets(syncnets))

func WithTCP

func WithTCP(port uint16) (string, interface{})

WithTCP sets the TCP port in the record.

Example:

record.Set(WithTCP(9000))

func WithUDP

func WithUDP(port uint16) (string, interface{})

WithUDP sets the UDP port in the record.

Example:

record.Set(WithUDP(9000))

Types

type ENRFilter

type ENRFilter func(*Record) bool

ENRFilter is a function that filters ENR records based on arbitrary criteria.

Filters return true if the record should be accepted, false if it should be rejected. They are used in two stages of the discovery process:

  1. Admission filtering: Before adding nodes to the local routing table
  2. Response filtering: When serving FINDNODE responses to remote peers

Example filters:

  • Check for specific protocol support (e.g., eth2 fork digest)
  • Verify IP address ranges
  • Check client versions or capabilities
  • Validate custom application-specific fields

Example:

// Filter for nodes with a specific eth2 fork digest
filter := func(r *Record) bool {
    var eth2Data Eth2ENRData
    if err := r.Get("eth2", &eth2Data); err != nil {
        return false // No eth2 field
    }
    return bytes.Equal(eth2Data.ForkDigest[:], expectedDigest[:])
}

func ByIP

func ByIP(cidr string) ENRFilter

ByIP creates a filter that checks if the node's IP address is in the given range.

The range is specified as a CIDR notation string (e.g., "192.168.0.0/16").

Example:

// Filter for nodes in private network
filter := ByIP("192.168.0.0/16")

func ByIdentityScheme

func ByIdentityScheme(scheme string) ENRFilter

ByIdentityScheme creates a filter that checks the identity scheme.

Common schemes:

  • "v4": secp256k1-based identity (most common)

Example:

// Filter for v4 identity scheme
filter := ByIdentityScheme("v4")

func ByKey

func ByKey(key string) ENRFilter

ByKey creates a filter that checks if a key exists in the record.

This is useful for filtering nodes that support specific features or protocols.

Example:

// Filter for nodes that have UDP port set
filter := ByKey("udp")

func ByUDPPort

func ByUDPPort(port uint16) ENRFilter

ByUDPPort creates a filter that checks if the UDP port matches.

Example:

// Filter for nodes on port 9000
filter := ByUDPPort(9000)

func ChainFilters

func ChainFilters(filters ...ENRFilter) ENRFilter

ChainFilters combines multiple filters with AND logic.

The combined filter returns true only if all filters return true. If any filter returns false, the combined filter returns false.

Short-circuit evaluation: Filters are evaluated in order and evaluation stops at the first filter that returns false.

Example:

// Combine eth2 fork check and IP range check
filter := ChainFilters(
    Eth2ForkFilter(expectedForkDigest),
    IPRangeFilter(allowedRanges),
)

func Eth2ForkFilter

func Eth2ForkFilter(expectedForkDigest [4]byte) ENRFilter

Eth2ForkFilter creates a filter that checks for a specific eth2 fork digest.

The fork digest identifies which Ethereum 2.0 network and fork the node is operating on (mainnet, testnet, etc.).

This is commonly used as an admission filter to ensure discovered nodes are on the same network.

Example:

// Filter for mainnet nodes (example fork digest)
mainnetForkDigest := [4]byte{0x01, 0x02, 0x03, 0x04}
filter := Eth2ForkFilter(mainnetForkDigest)

type Entry

type Entry interface {
	// Key returns the ENR key for this entry
	Key() string
}

Entry is an interface for ENR key-value entries.

Each entry knows how to encode itself into a record and provides metadata about the entry (key name, description).

type Eth2ENRData

type Eth2ENRData struct {
	ForkDigest      [4]byte
	NextForkVersion [4]byte
	NextForkEpoch   uint64
}

Eth2ENRData represents the eth2 field in an ENR record.

This field contains Ethereum 2.0 specific metadata including:

  • ForkDigest: 4-byte identifier for the current fork
  • NextForkVersion: Version of the next planned fork
  • NextForkEpoch: Epoch when the next fork activates

type EthENRData

type EthENRData []struct {
	ForkID        [4]byte
	NextForkEpoch uint64
}

EthENRData represents the eth field in an ENR record.

This field contains Ethereum specific metadata including:

  • ForkID: 4-byte identifier for the current fork
  • NextForkEpoch: Epoch when the next fork activates

type Record

type Record struct {
	// contains filtered or unexported fields
}

Record represents an Ethereum Node Record (ENR) as defined in EIP-778.

A record consists of:

  • Signature: Cryptographic signature over the record content
  • Seq: Sequence number, incremented on each update
  • Pairs: Key-value pairs containing node metadata

The record is immutable once created. To update a record, create a new one with an incremented sequence number.

func CreateSignedRecord

func CreateSignedRecord(privKey *ecdsa.PrivateKey, entries ...interface{}) (*Record, error)

CreateSignedRecord creates and signs a new ENR record with the given entries.

This is a convenience function that:

  1. Creates a new record
  2. Sets all provided entries
  3. Signs the record with the private key

Example:

privateKey, _ := crypto.GenerateKey()
record, err := CreateSignedRecord(
    privateKey,
    WithIP(net.IPv4(192, 168, 1, 1)),
    WithUDP(9000),
)

func DecodeBase64

func DecodeBase64(input string) (*Record, error)

DecodeBase64 decodes a base64-encoded record.

The input must be in the format "enr:BASE64" where BASE64 is URL-safe base64 without padding.

Example:

record := New()
if err := record.DecodeBase64("enr:-IS4QHCYrYZ..."); err != nil {
    // Handle error
}

func Load

func Load(data []byte) (*Record, error)

Load loads a record from an RLP-encoded byte slice.

This is a convenience function equivalent to:

record := New()
record.DecodeRLPBytes(data)

func New

func New() *Record

New creates a new empty ENR record.

The record will have a sequence number of 0 and no entries. Use Load or Decode to create a record from existing data.

func NewRecord

func NewRecord(entries ...interface{}) (*Record, error)

NewRecord creates a new ENR record with the given key-value pairs.

This is a convenience function for building records with multiple fields.

Example:

record, err := NewRecord(
    WithIP(net.IPv4(192, 168, 1, 1)),
    WithUDP(9000),
    WithTCP(9000),
)

func UpdateRecord

func UpdateRecord(old *Record, privKey *ecdsa.PrivateKey, entries ...interface{}) (*Record, error)

UpdateRecord creates an updated version of an existing record.

The new record will have:

  • Incremented sequence number
  • All entries from the old record
  • Updates from the provided entries
  • New signature

Example:

// Update IP address and UDP port
newRecord, err := UpdateRecord(
    oldRecord,
    privateKey,
    WithIP(net.IPv4(192, 168, 1, 2)),
    WithUDP(9001),
)

func (*Record) Clone

func (r *Record) Clone() (*Record, error)

Clone creates a deep copy of the record by marshaling and unmarshaling.

This is useful when you need to modify a record without affecting the original. The cloned record will have the same sequence number and all fields.

Example:

clone := record.Clone()
clone.SetSeq(record.Seq() + 1)
clone.Set("ip", newIP)

func (*Record) DecodeRLP

func (r *Record) DecodeRLP(s *rlp.Stream) error

DecodeRLP decodes an RLP stream into a record.

This method implements the rlp.Decoder interface, allowing Record to be automatically decoded when embedded in other structures using rlp.DecodeBytes.

The stream must contain the raw RLP bytes of the ENR record.

func (*Record) DecodeRLPBytes

func (r *Record) DecodeRLPBytes(data []byte) error

DecodeRLPBytes decodes an RLP-encoded record from a byte slice.

The input must be a valid RLP list containing: [signature, seq, k1, v1, k2, v2, ...]

After decoding, the signature is automatically verified. Returns ErrInvalidSignature if verification fails.

Example:

encoded := []byte{...}
record := New()
if err := record.DecodeRLPBytes(encoded); err != nil {
    // Handle error
}

func (*Record) EncodeBase64

func (r *Record) EncodeBase64() (string, error)

EncodeBase64 encodes the record in base64 format.

This is the standard format for sharing ENRs in text form. The output is URL-safe base64 without padding.

Example output: "enr:-IS4QHCYrYZ..."

func (*Record) EncodeRLP

func (r *Record) EncodeRLP() ([]byte, error)

EncodeRLP returns the RLP encoding of the record.

The encoding format is: [signature, seq, k1, v1, k2, v2, ...] where keys are sorted lexicographically.

The encoding is cached after first call. The cache is invalidated when the record is modified.

Returns ErrRecordTooLarge if the encoded record exceeds 300 bytes.

func (*Record) Eth

func (r *Record) Eth() (EthENRData, bool)

Eth returns the Ethereum metadata from the record.

Returns nil and false if no "eth" key is present or if decoding fails.

func (*Record) Eth2

func (r *Record) Eth2() (*Eth2ENRData, bool)

Eth2 returns the Ethereum 2.0 metadata from the record.

Returns nil and false if no "eth2" key is present or if decoding fails.

func (*Record) Get

func (r *Record) Get(key string, dest interface{}) error

Get retrieves a value from the record by key.

The value is decoded into the provided destination pointer. Returns ErrNoKey if the key is not present in the record.

Example:

var ip net.IP
if err := r.Get("ip", &ip); err != nil {
    // Key not found or decoding error
}

func (*Record) Has

func (r *Record) Has(key string) bool

Has checks if a key exists in the record.

Example:

if r.Has("udp") {
    var port uint16
    r.Get("udp", &port)
}

func (*Record) IP

func (r *Record) IP() net.IP

IP returns the IPv4 address from the record.

Returns nil if no "ip" key is present or if the value is not a valid IP.

func (*Record) IP6

func (r *Record) IP6() net.IP

IP6 returns the IPv6 address from the record.

Returns nil if no "ip6" key is present or if the value is not a valid IP.

func (*Record) IdentityScheme

func (r *Record) IdentityScheme() string

IdentityScheme returns the identity scheme of the record.

Common schemes:

  • "v4": secp256k1-based identity (most common)

Returns empty string if no "id" key is present.

func (*Record) Keys

func (r *Record) Keys() []string

Keys returns all keys present in the record.

The returned slice is a copy and can be safely modified.

func (*Record) NodeID

func (r *Record) NodeID() []byte

NodeID returns the node ID derived from the public key.

The node ID is the keccak256 hash of the uncompressed public key (without the 0x04 prefix). Returns nil if the record has no valid public key.

func (*Record) Pairs

func (r *Record) Pairs() map[string]interface{}

Pairs returns a copy of all key-value pairs in the record.

The returned map is a copy and can be safely modified.

func (*Record) PublicKey

func (r *Record) PublicKey() *ecdsa.PublicKey

PublicKey returns the secp256k1 public key from the record.

Returns nil if no "secp256k1" key is present or if the key is invalid.

func (*Record) Seq

func (r *Record) Seq() uint64

Seq returns the sequence number of the record.

The sequence number is incremented each time the record is updated. It allows peers to determine which version of a record is newer.

func (*Record) Set

func (r *Record) Set(key string, value interface{}) error

Set stores a key-value pair in the record.

The key must be a string and the value must be RLP-encodable. Common keys include:

  • "id": Identity scheme (e.g., "v4")
  • "ip": IPv4 address
  • "ip6": IPv6 address
  • "tcp": TCP port
  • "udp": UDP port
  • "secp256k1": Compressed secp256k1 public key
  • "eth2": Ethereum 2.0 metadata

Example:

r.Set("ip", net.IPv4(192, 168, 1, 1))
r.Set("udp", uint16(9000))

func (*Record) SetSeq

func (r *Record) SetSeq(seq uint64)

SetSeq sets the sequence number of the record.

This should be called when creating an updated version of a record. The new sequence number must be greater than the current one.

func (*Record) Sign

func (r *Record) Sign(privKey *ecdsa.PrivateKey) error

Sign signs the record with the provided private key.

This updates the signature field and invalidates any cached encoding. The identity scheme is automatically set to "v4" (secp256k1).

Example:

privateKey, _ := crypto.GenerateKey()
record := New()
record.Set("ip", net.IPv4(192, 168, 1, 1))
record.Set("udp", uint16(9000))
if err := record.Sign(privateKey); err != nil {
    // Handle error
}

func (*Record) Size

func (r *Record) Size() int

Size returns the RLP-encoded size of the record in bytes.

This is useful for checking if the record exceeds the maximum size (300 bytes).

func (*Record) String

func (r *Record) String() string

String returns a human-readable representation of the record.

Format: ENR[seq=X, keys=[k1, k2, ...]]

func (*Record) TCP

func (r *Record) TCP() uint16

TCP returns the TCP port from the record.

Returns 0 if no "tcp" key is present or if the value is not a valid port.

func (*Record) ToEnode

func (r *Record) ToEnode() *enode.Node

ToEnode converts the ENR record to a go-ethereum enode.Node.

This is useful for interoperability with go-ethereum's p2p stack. Returns nil if the record cannot be converted (missing required fields).

func (*Record) UDP

func (r *Record) UDP() uint16

UDP returns the UDP port from the record.

Returns 0 if no "udp" key is present or if the value is not a valid port.

func (*Record) VerifySignature

func (r *Record) VerifySignature() bool

VerifySignature verifies the record's signature.

Returns true if the signature is valid for the record's content and public key. Returns false if the signature is missing, invalid, or doesn't match the public key.

type ResponseFilter

type ResponseFilter func(requester *net.UDPAddr, record *Record) bool

ResponseFilter is a context-aware filter for serving FINDNODE responses.

Unlike ENRFilter which operates only on the record, ResponseFilter also receives information about the requester (their network address).

This enables filtering based on the relationship between requester and node:

  • Don't serve LAN IPs to WAN requesters
  • Apply geographic filtering
  • Implement custom privacy policies

Example:

// Don't serve LAN nodes to WAN requesters
filter := func(requester *net.UDPAddr, r *Record) bool {
    if IsWANAddress(requester.IP) && IsLANAddress(r.IP()) {
        return false
    }
    return true
}

func ChainResponseFilters

func ChainResponseFilters(filters ...ResponseFilter) ResponseFilter

ChainResponseFilters combines multiple response filters with AND logic.

Similar to ChainFilters but for ResponseFilter types.

func LANAwareResponseFilter

func LANAwareResponseFilter() ResponseFilter

LANAwareResponseFilter creates a response filter that prevents serving LAN (private network) IPs to WAN (public internet) requesters.

This is a security and efficiency measure:

  • WAN peers can't connect to LAN addresses
  • Prevents leaking internal network topology

LAN addresses include:

  • 10.0.0.0/8 (RFC1918)
  • 172.16.0.0/12 (RFC1918)
  • 192.168.0.0/16 (RFC1918)
  • fc00::/7 (IPv6 ULA)
  • fe80::/10 (IPv6 link-local)
  • 127.0.0.0/8 (loopback)
  • ::1 (IPv6 loopback)

Example:

config := &Config{
    ResponseFilter: LANAwareResponseFilter(),
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL