diff --git a/clients/consensus/client.go b/clients/consensus/client.go index 1b19aed..3d1e0a0 100644 --- a/clients/consensus/client.go +++ b/clients/consensus/client.go @@ -11,6 +11,7 @@ import ( "github.com/ethpandaops/dora/clients/consensus/rpc" "github.com/ethpandaops/dora/clients/sshtunnel" + "github.com/ethpandaops/dora/types" ) type ClientConfig struct { @@ -33,7 +34,8 @@ type Client struct { isSyncing bool isOptimistic bool versionStr string - peerId string + enr string + nodeIdentity *types.NodeIdentity clientType ClientType lastEvent time.Time retryCounter uint64 @@ -112,8 +114,8 @@ func (client *Client) GetVersion() string { return client.versionStr } -func (client *Client) GetPeerID() string { - return client.peerId +func (client *Client) GetNodeIdentity() *types.NodeIdentity { + return client.nodeIdentity } func (client *Client) GetEndpointConfig() *ClientConfig { diff --git a/clients/consensus/clientlogic.go b/clients/consensus/clientlogic.go index e361da1..09a9a28 100644 --- a/clients/consensus/clientlogic.go +++ b/clients/consensus/clientlogic.go @@ -264,7 +264,7 @@ func (client *Client) updateNodePeers(ctx context.Context) error { defer cancel() var err error - client.peerId, err = client.rpcClient.GetNodePeerId(ctx) + client.nodeIdentity, err = client.rpcClient.GetNodeIdentity(ctx) if err != nil { return fmt.Errorf("could not get node peer id: %v", err) } diff --git a/clients/consensus/rpc/beaconapi.go b/clients/consensus/rpc/beaconapi.go index cd49b3e..a40cefb 100644 --- a/clients/consensus/rpc/beaconapi.go +++ b/clients/consensus/rpc/beaconapi.go @@ -25,6 +25,7 @@ import ( "golang.org/x/crypto/ssh" "github.com/ethpandaops/dora/clients/sshtunnel" + "github.com/ethpandaops/dora/types" ) type BeaconClient struct { @@ -470,18 +471,16 @@ func (bc *BeaconClient) GetNodePeers(ctx context.Context) ([]*v1.Peer, error) { return result.Data, nil } -func (bc *BeaconClient) GetNodePeerId(ctx context.Context) (string, error) { - nodeIdentity := struct { - Data struct { - PeerId string `json:"peer_id"` - } `json:"data"` +func (bc *BeaconClient) GetNodeIdentity(ctx context.Context) (*types.NodeIdentity, error) { + response := struct { + Data *types.NodeIdentity `json:"data"` }{} - err := bc.getJSON(ctx, fmt.Sprintf("%s/eth/v1/node/identity", bc.endpoint), &nodeIdentity) + err := bc.getJSON(ctx, fmt.Sprintf("%s/eth/v1/node/identity", bc.endpoint), &response) if err != nil { - return "", fmt.Errorf("error retrieving node identity: %v", err) + return nil, fmt.Errorf("error retrieving node identity: %v", err) } - return nodeIdentity.Data.PeerId, nil + return response.Data, nil } func (bc *BeaconClient) SubmitBLSToExecutionChanges(ctx context.Context, blsChanges []*capella.SignedBLSToExecutionChange) error { diff --git a/go.mod b/go.mod index 597092c..d6f38d9 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,6 @@ require ( github.com/juliangruber/go-intersect v1.1.0 github.com/kelseyhightower/envconfig v1.4.0 github.com/lib/pq v1.10.9 - github.com/lucasb-eyer/go-colorful v1.2.0 github.com/mashingan/smapping v0.1.19 github.com/mitchellh/mapstructure v1.5.0 github.com/pk910/dynamic-ssz v0.0.5 @@ -27,7 +26,6 @@ require ( github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15 github.com/rs/zerolog v1.33.0 github.com/sirupsen/logrus v1.9.3 - github.com/stdatiks/jdenticon-go v0.1.0 github.com/tdewolff/minify v2.3.6+incompatible github.com/urfave/negroni v1.0.0 golang.org/x/crypto v0.26.0 diff --git a/go.sum b/go.sum index 5540658..e8c7839 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,6 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -273,9 +272,6 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightclient/go-ethereum v0.0.0-20240726203109-4a0622f95d30 h1:mhkIkcs3GhL1kZyyCko1gUlnwCpfYpfYckqiz20k8HU= github.com/lightclient/go-ethereum v0.0.0-20240726203109-4a0622f95d30/go.mod h1:RKrX5zEFmD/CQ8XLRxc3eOEcqqwN4no8ZzuNkVxEFFY= -github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mashingan/smapping v0.1.19 h1:SsEtuPn2UcM1croIupPtGLgWgpYRuS0rSQMvKD9g2BQ= github.com/mashingan/smapping v0.1.19/go.mod h1:FjfiwFxGOuNxL/OT1WcrNAwTPx0YJeg5JiXwBB1nyig= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= @@ -385,8 +381,6 @@ github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= -github.com/stdatiks/jdenticon-go v0.1.0 h1:yf0xbl3OIu1oafVmgqGaB6m7QMNOaNkwsW/omzSyN5g= -github.com/stdatiks/jdenticon-go v0.1.0/go.mod h1:hzZIjAw3ZhYi3S5IOjIvC7C/dsc17L8Kc3AtEnP0ucw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= diff --git a/handlers/clients_cl.go b/handlers/clients_cl.go index bdd1896..9616674 100644 --- a/handlers/clients_cl.go +++ b/handlers/clients_cl.go @@ -7,9 +7,11 @@ import ( "strings" "time" + "github.com/ethereum/go-ethereum/p2p/enr" "github.com/ethpandaops/dora/services" "github.com/ethpandaops/dora/templates" "github.com/ethpandaops/dora/types/models" + "github.com/ethpandaops/dora/utils" "github.com/sirupsen/logrus" ) @@ -65,13 +67,16 @@ func buildCLPeerMapData() *models.ClientCLPageDataPeerMap { edges := make(map[string]*models.ClientCLDataMapPeerMapEdge) for _, client := range services.GlobalBeaconService.GetConsensusClients() { - peerID := client.GetPeerID() + id := client.GetNodeIdentity() + if id == nil { + continue + } + peerID := id.PeerID if _, ok := nodes[peerID]; !ok { node := models.ClientCLPageDataPeerMapNode{ ID: peerID, Label: client.GetName(), Group: "internal", - Shape: "circularImage", } nodes[peerID] = &node peerMap.ClientPageDataMapNode = append(peerMap.ClientPageDataMapNode, &node) @@ -79,7 +84,11 @@ func buildCLPeerMapData() *models.ClientCLPageDataPeerMap { } for _, client := range services.GlobalBeaconService.GetConsensusClients() { - peerId := client.GetPeerID() + id := client.GetNodeIdentity() + if id == nil { + continue + } + peerId := id.PeerID peers := client.GetNodePeers() for _, peer := range peers { peerId := peerId @@ -89,7 +98,6 @@ func buildCLPeerMapData() *models.ClientCLPageDataPeerMap { ID: peer.PeerID, Label: fmt.Sprintf("%s...%s", peer.PeerID[0:5], peer.PeerID[len(peer.PeerID)-5:]), Group: "external", - Shape: "circularImage", } nodes[peer.PeerID] = &node peerMap.ClientPageDataMapNode = append(peerMap.ClientPageDataMapNode, &node) @@ -131,8 +139,9 @@ func buildCLPeerMapData() *models.ClientCLPageDataPeerMap { func buildCLClientsPageData() (*models.ClientsCLPageData, time.Duration) { logrus.Debugf("clients page called") pageData := &models.ClientsCLPageData{ - Clients: []*models.ClientsCLPageDataClient{}, - PeerMap: buildCLPeerMapData(), + Clients: []*models.ClientsCLPageDataClient{}, + PeerMap: buildCLPeerMapData(), + ShowSensitivePeerInfos: utils.Config.Frontend.ShowSensitivePeerInfos, } chainState := services.GlobalBeaconService.GetChainState() @@ -145,7 +154,11 @@ func buildCLClientsPageData() (*models.ClientsCLPageData, time.Duration) { aliases := map[string]string{} for _, client := range services.GlobalBeaconService.GetConsensusClients() { - aliases[client.GetPeerID()] = client.GetName() + id := client.GetNodeIdentity() + if id == nil { + continue + } + aliases[id.PeerID] = client.GetName() } for _, client := range services.GlobalBeaconService.GetConsensusClients() { @@ -162,12 +175,30 @@ func buildCLClientsPageData() (*models.ClientsCLPageData, time.Duration) { peerAlias = alias peerType = "internal" } + + enrKeyValues := map[string]interface{}{} + var nodeID string + + if peer.Enr != "" { // Some clients might not announce the ENR of their peers + parsedEnr, err := utils.DecodeENR(peer.Enr) + if err != nil { + logrus.WithFields(logrus.Fields{"client": client.GetName(), "peer_enr": peer.Enr}).Error("failed to decode peer enr. ", err) + parsedEnr = &enr.Record{} + } + enrKeyValues = utils.GetKeyValuesFromENR(parsedEnr) + nodeID = utils.GetNodeIDFromENR(parsedEnr) + } + resPeers = append(resPeers, &models.ClientCLPageDataClientPeers{ - ID: peer.PeerID, - State: peer.State, - Direction: peer.Direction, - Alias: peerAlias, - Type: peerType, + ID: peer.PeerID, + State: peer.State, + Direction: peer.Direction, + Alias: peerAlias, + Type: peerType, + ENR: peer.Enr, + ENRKeyValues: enrKeyValues, + NodeID: nodeID, + LastSeenP2PAddress: peer.LastSeenP2PAddress, }) if peer.Direction == "inbound" { @@ -183,18 +214,39 @@ func buildCLClientsPageData() (*models.ClientsCLPageData, time.Duration) { return resPeers[i].Type > resPeers[j].Type }) + id := client.GetNodeIdentity() + if id == nil { + continue + } + + rec, err := utils.DecodeENR(id.Enr) + if err != nil { + logrus.WithFields(logrus.Fields{"client": client.GetName(), "enr": id.Enr}).Error("failed to decode enr. ", err) + rec = &enr.Record{} + } + + enrkv := utils.GetKeyValuesFromENR(rec) + + nodeID := utils.GetNodeIDFromENR(rec) + resClient := &models.ClientsCLPageDataClient{ - Index: int(client.GetIndex()) + 1, - Name: client.GetName(), - Version: client.GetVersion(), - Peers: resPeers, - PeerID: client.GetPeerID(), - PeersInboundCounter: inPeerCount, - PeersOutboundCounter: outPeerCount, - HeadSlot: uint64(lastHeadSlot), - HeadRoot: lastHeadRoot[:], - Status: client.GetStatus().String(), - LastRefresh: client.GetLastEventTime(), + Index: int(client.GetIndex()) + 1, + Name: client.GetName(), + Version: client.GetVersion(), + Peers: resPeers, + PeerID: id.PeerID, + ENR: id.Enr, + ENRKeyValues: enrkv, + NodeID: nodeID, + P2PAddresses: id.P2PAddresses, + DisoveryAddresses: id.DiscoveryAddresses, + AttestationSubnetSubs: id.Metadata.Attnets, + PeersInboundCounter: inPeerCount, + PeersOutboundCounter: outPeerCount, + HeadSlot: uint64(lastHeadSlot), + HeadRoot: lastHeadRoot[:], + Status: client.GetStatus().String(), + LastRefresh: client.GetLastEventTime(), } lastError := client.GetLastClientError() diff --git a/static/css/clients.css b/static/css/clients.css index 438f122..1b73e1b 100644 --- a/static/css/clients.css +++ b/static/css/clients.css @@ -1,11 +1,34 @@ /*--------------------------------------------- Client peers table ---------------------------------------------*/ -.client-node-peerinfo { - text-align: center; - padding-bottom: 10px; +.client-row-divider { + margin: 0 auto; + padding: 0; + padding-top: 3px; + padding-bottom: 5px; + font-weight: 600; border-bottom: 1px dashed; + text-align: center; +} + +.client-row-divider:not(:first-child) { + border-top: 1px dashed; +} + +.client-table-info { + padding-left: 30px; + padding-right: 30px; + display: inline-block; +} + +.client-node-peerinfo { + text-align: left; + padding-top: 20px; margin-bottom: 5px; + text-wrap: pretty; +} +.client-node-enr-infos{ + text-align: left; } .client-node-icon { border-radius:50%; @@ -26,9 +49,9 @@ Client peers table .peer-table-column{ display: inline-block; - width: 50%; vertical-align: top; - text-align: center; + text-align: left; + width: 100%; } .peer-table-icon.connected { diff --git a/static/css/layout.css b/static/css/layout.css index 50e630c..8947b1d 100644 --- a/static/css/layout.css +++ b/static/css/layout.css @@ -308,3 +308,17 @@ span.validator-label { .ellipsis-copy-btn { float: right; } + + +.table-borderless > tbody > tr > td, +.table-borderless > tbody > tr > th, +.table-borderless > tfoot > tr > td, +.table-borderless > tfoot > tr > th, +.table-borderless > thead > tr > td, +.table-borderless > thead > tr > th { + border: none; +} + +.table-sm>:not(caption)>*>* { + padding: 1px .25rem; +} diff --git a/templates/clients/clients_cl.html b/templates/clients/clients_cl.html index 49c196a..852e138 100644 --- a/templates/clients/clients_cl.html +++ b/templates/clients/clients_cl.html @@ -57,6 +57,7 @@

+ {{ $root := . }} {{ range $i, $client := .Clients }} {{ $client.Index }} @@ -104,52 +105,124 @@

{{ end }} - {{ $client.Version }} + {{ $client.Version }} - + +
Node identity
- Peer ID: {{ $client.PeerID }} - -
-
-
-
Inbound
- {{ range $j, $peer := $client.Peers }} - {{if eq "inbound" $peer.Direction}} -
- - - {{ $peer.Alias }} - {{ if eq $peer.Type "internal" }} - {{ trunc (sub (len $peer.ID) (add (len $peer.Alias) 4) ) $peer.ID }}... - {{ end }} - - -
- {{end}} - {{ end }} + + + + + + + {{ if $root.ShowSensitivePeerInfos }} + + + + + + + + + {{ end }} + +
Peer ID + {{ $client.PeerID }} + +
Node ID + {{ $client.NodeID }} + +
ENR +
+ {{ $client.ENR }} + +
+
+ {{ if $root.ShowSensitivePeerInfos }} +
ENR fields
+ + + {{ range $k, $v := $client.ENRKeyValues }} + + + + + {{ end }} + +
{{ $k }} + {{ $v }} + +
+ {{ end }} +
Peers
+
-
Outbound
{{ range $j, $peer := $client.Peers }} - {{if eq "outbound" $peer.Direction}}
+ {{ if eq $peer.Direction "inbound" }} + + {{ else }} + + {{ end}} - {{ $peer.Alias }} - {{ if eq $peer.Type "internal" }} - {{ trunc (sub (len $peer.ID) (add (len $peer.Alias) 4) ) $peer.ID }}... - {{ end }} + {{ $peer.ID }} + {{ if eq $peer.Type "internal" }} + {{ $peer.Alias }} + {{ end }}
- {{end}} + {{ if $root.ShowSensitivePeerInfos }} + +
+ + + + + + + + + + + {{ if ne $peer.ENR "" }} + + + + + {{ range $k, $v := $peer.ENRKeyValues }} + + + + + {{ end }} + {{ end }} + +
P2P Addr + {{ $peer.LastSeenP2PAddress }} + +
ENR +
+ {{ if eq $peer.ENR "" }}Unknown{{ else }}{{ $peer.ENR }}{{ end }} + +
+
Node ID + {{ $peer.NodeID }} + +
{{ $k }} + {{ $v }} + +
+
+ {{ end}} {{ end }}
-
{{ end }} diff --git a/types/config.go b/types/config.go index 8d79fd2..557e948 100644 --- a/types/config.go +++ b/types/config.go @@ -47,6 +47,8 @@ type Config struct { HttpWriteTimeout time.Duration `yaml:"httpWriteTimeout" envconfig:"FRONTEND_HTTP_WRITE_TIMEOUT"` HttpIdleTimeout time.Duration `yaml:"httpIdleTimeout" envconfig:"FRONTEND_HTTP_IDLE_TIMEOUT"` AllowDutyLoading bool `yaml:"allowDutyLoading" envconfig:"FRONTEND_ALLOW_DUTY_LOADING"` + + ShowSensitivePeerInfos bool `yaml:"showSensitivePeerInfos" envconfig:"FRONTEND_SHOW_SENSITIVE_PEER_INFOS"` } `yaml:"frontend"` RateLimit struct { diff --git a/types/models/clients_cl.go b/types/models/clients_cl.go index e17b613..704aca7 100644 --- a/types/models/clients_cl.go +++ b/types/models/clients_cl.go @@ -6,32 +6,43 @@ import ( // ClientsCLPageData is a struct to hold info for the clients page type ClientsCLPageData struct { - Clients []*ClientsCLPageDataClient `json:"clients"` - ClientCount uint64 `json:"client_count"` - PeerMap *ClientCLPageDataPeerMap `json:"peer_map"` + Clients []*ClientsCLPageDataClient `json:"clients"` + ClientCount uint64 `json:"client_count"` + PeerMap *ClientCLPageDataPeerMap `json:"peer_map"` + ShowSensitivePeerInfos bool `json:"show_sensitive_peer_infos"` } type ClientsCLPageDataClient struct { - Index int `json:"index"` - Name string `json:"name"` - Version string `json:"version"` - HeadSlot uint64 `json:"head_slot"` - HeadRoot []byte `json:"head_root"` - Status string `json:"status"` - LastRefresh time.Time `json:"refresh"` - LastError string `json:"error"` - PeerID string `json:"peer_id"` - Peers []*ClientCLPageDataClientPeers `json:"peers"` - PeersInboundCounter uint32 `json:"peers_inbound_counter"` - PeersOutboundCounter uint32 `json:"peers_outbound_counter"` + Index int `json:"index"` + Name string `json:"name"` + Version string `json:"version"` + HeadSlot uint64 `json:"head_slot"` + HeadRoot []byte `json:"head_root"` + Status string `json:"status"` + LastRefresh time.Time `json:"refresh"` + LastError string `json:"error"` + PeerID string `json:"peer_id"` + ENR string `json:"enr"` + ENRKeyValues map[string]interface{} `json:"enr_kv"` + NodeID string `json:"node_id"` + P2PAddresses []string `json:"p2p_addresses"` + DisoveryAddresses []string `json:"discovery_addresses"` + AttestationSubnetSubs string `json:"attestation_subnets_subs"` + Peers []*ClientCLPageDataClientPeers `json:"peers"` + PeersInboundCounter uint32 `json:"peers_inbound_counter"` + PeersOutboundCounter uint32 `json:"peers_outbound_counter"` } type ClientCLPageDataClientPeers struct { - ID string `json:"id"` - Alias string `json:"alias"` - Type string `json:"type"` - State string `json:"state"` - Direction string `json:"direction"` + ID string `json:"id"` + Alias string `json:"alias"` + Type string `json:"type"` + State string `json:"state"` + Direction string `json:"direction"` + ENR string `json:"enr"` + ENRKeyValues map[string]interface{} `json:"enr_kv"` + NodeID string `json:"node_id"` + LastSeenP2PAddress string `json:"last_seen_p2p_address"` } type ClientCLPageDataPeerMap struct { ClientPageDataMapNode []*ClientCLPageDataPeerMapNode `json:"nodes"` diff --git a/types/nodeidentity.go b/types/nodeidentity.go new file mode 100644 index 0000000..d450b38 --- /dev/null +++ b/types/nodeidentity.go @@ -0,0 +1,12 @@ +package types + +type NodeIdentity struct { + PeerID string `json:"peer_id"` + Enr string `json:"enr"` + P2PAddresses []string `json:"p2p_addresses"` + DiscoveryAddresses []string `json:"discovery_addresses"` + Metadata struct { + Attnets string `json:"attnets"` + //SeqNumber string `json:"seq_number"` // BUG: Teku and Grandine have an int type for this field + } `json:"metadata"` +} diff --git a/utils/enr.go b/utils/enr.go new file mode 100644 index 0000000..fabbabc --- /dev/null +++ b/utils/enr.go @@ -0,0 +1,106 @@ +package utils + +import ( + "bytes" + "encoding/base64" + "encoding/hex" + "net" + "strconv" + + "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethereum/go-ethereum/p2p/enr" + "github.com/ethereum/go-ethereum/rlp" +) + +func DecodeENR(raw string) (*enr.Record, error) { + b := []byte(raw) + if bytes.HasPrefix(b, []byte("enr:")) { + b = b[4:] + } + dec := make([]byte, base64.RawURLEncoding.DecodedLen(len(b))) + n, err := base64.RawURLEncoding.Decode(dec, b) + if err != nil { + return nil, err + } + var r enr.Record + err = rlp.DecodeBytes(dec[:n], &r) + return &r, err +} + +func GetKeyValuesFromENR(r *enr.Record) map[string]interface{} { + fields := make(map[string]interface{}) + // Get sequence number + fields["seq"] = r.Seq() + + // Get signature + n, err := enode.New(enode.ValidSchemes, r) + if err == nil { + record := n.Record() + if record != nil { + fields["signature"] = "0x" + hex.EncodeToString(record.Signature()) + } + } + + // Get all key value pairs + kv := r.AppendElements(nil)[1:] + for i := 0; i < len(kv); i += 2 { + key := kv[i].(string) + val := kv[i+1].(rlp.RawValue) + formatter := attrFormatters[key] + if formatter == nil { + formatter = formatAttrRaw + } + fmtval, ok := formatter(val) + if ok { + fields[key] = fmtval + } else { + fields[key] = "0x" + hex.EncodeToString(val) + " (!)" + } + } + return fields +} + +func GetNodeIDFromENR(r *enr.Record) string { + n, err := enode.New(enode.ValidSchemes, r) + if err != nil { + return "" + } + return n.ID().String() +} + +// attrFormatters contains formatting functions for well-known ENR keys. +var attrFormatters = map[string]func(rlp.RawValue) (string, bool){ + "id": formatAttrString, + "ip": formatAttrIP, + "ip6": formatAttrIP, + "tcp": formatAttrUint, + "tcp6": formatAttrUint, + "udp": formatAttrUint, + "udp6": formatAttrUint, +} + +func formatAttrRaw(v rlp.RawValue) (string, bool) { + content, _, err := rlp.SplitString(v) + return "0x" + hex.EncodeToString(content), err == nil +} + +func formatAttrString(v rlp.RawValue) (string, bool) { + content, _, err := rlp.SplitString(v) + return string(content), err == nil +} + +func formatAttrIP(v rlp.RawValue) (string, bool) { + content, _, err := rlp.SplitString(v) + if err != nil || len(content) != 4 && len(content) != 6 { + return "", false + } + return net.IP(content).String(), true +} + +func formatAttrUint(v rlp.RawValue) (string, bool) { + var x uint64 + if err := rlp.DecodeBytes(v, &x); err != nil { + return "", false + } + return strconv.FormatUint(x, 10), true +}