From 8047dbad363fb26d9a6c264da7850e91af51243d Mon Sep 17 00:00:00 2001 From: Diogo Santos Date: Wed, 28 Jan 2026 11:10:59 +0000 Subject: [PATCH 1/4] add node identity --- api/nodeidentityopts.go | 6 ++++ api/v1/identity.go | 62 +++++++++++++++++++++++++++++++++++++++++ http/nodeidentity.go | 46 ++++++++++++++++++++++++++++++ mock/nodeidentity.go | 33 ++++++++++++++++++++++ mock/service.go | 1 + multi/nodeidentity.go | 31 +++++++++++++++++++++ service.go | 11 +++++++- testclients/erroring.go | 18 ++++++++++++ testclients/sleepy.go | 16 +++++++++++ 9 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 api/nodeidentityopts.go create mode 100644 api/v1/identity.go create mode 100644 http/nodeidentity.go create mode 100644 mock/nodeidentity.go create mode 100644 multi/nodeidentity.go diff --git a/api/nodeidentityopts.go b/api/nodeidentityopts.go new file mode 100644 index 00000000..5189e19e --- /dev/null +++ b/api/nodeidentityopts.go @@ -0,0 +1,6 @@ +package api + +// NodeIdentityOpts are the options for obtaining the node identity. +type NodeIdentityOpts struct { + Common CommonOpts +} diff --git a/api/v1/identity.go b/api/v1/identity.go new file mode 100644 index 00000000..ad231eeb --- /dev/null +++ b/api/v1/identity.go @@ -0,0 +1,62 @@ +package v1 + +import ( + "encoding/json" + "fmt" + + "github.com/pkg/errors" +) + +// NodeIdentity contains the node identity information. +type NodeIdentity struct { + PeerID string `json:"peer_id"` + Enr string `json:"enr"` + P2PAddresses []string `json:"p2p_addresses"` + DiscoveryAddresses []string `json:"discovery_addresses"` + Metadata map[string]string `json:"metadata"` +} + +type nodeIdentityJSON struct { + PeerID string `json:"peer_id"` + Enr string `json:"enr"` + P2PAddresses []string `json:"p2p_addresses"` + DiscoveryAddresses []string `json:"discovery_addresses"` + Metadata map[string]string `json:"metadata"` +} + +// MarshalJSON implements json.Marshaler. +func (n *NodeIdentity) MarshalJSON() ([]byte, error) { + return json.Marshal(&nodeIdentityJSON{ + PeerID: n.PeerID, + Enr: n.Enr, + P2PAddresses: n.P2PAddresses, + DiscoveryAddresses: n.DiscoveryAddresses, + Metadata: n.Metadata, + }) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (n *NodeIdentity) UnmarshalJSON(input []byte) error { + var nodeIdentityJSON nodeIdentityJSON + + if err := json.Unmarshal(input, &nodeIdentityJSON); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + n.PeerID = nodeIdentityJSON.PeerID + n.Enr = nodeIdentityJSON.Enr + n.P2PAddresses = nodeIdentityJSON.P2PAddresses + n.DiscoveryAddresses = nodeIdentityJSON.DiscoveryAddresses + n.Metadata = nodeIdentityJSON.Metadata + + return nil +} + +func (n *NodeIdentity) String() string { + data, err := json.Marshal(n) + if err != nil { + return fmt.Sprintf("ERR: %v", err) + } + + return string(data) +} diff --git a/http/nodeidentity.go b/http/nodeidentity.go new file mode 100644 index 00000000..30f3c5d4 --- /dev/null +++ b/http/nodeidentity.go @@ -0,0 +1,46 @@ +package http + +import ( + "bytes" + "context" + "fmt" + + client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/api" + apiv1 "github.com/attestantio/go-eth2-client/api/v1" +) + +// NodeIdentity provides the identity information of the node. +func (s *Service) NodeIdentity(ctx context.Context, + opts *api.NodeIdentityOpts, +) ( + *api.Response[*apiv1.NodeIdentity], + error, +) { + if err := s.assertIsActive(ctx); err != nil { + return nil, err + } + if opts == nil { + return nil, client.ErrNoOptions + } + + endpoint := "/eth/v1/node/identity" + httpResponse, err := s.get(ctx, endpoint, "", &opts.Common, false) + if err != nil { + return nil, err + } + + if httpResponse.contentType != ContentTypeJSON { + return nil, fmt.Errorf("unexpected content type %v (expected JSON)", httpResponse.contentType) + } + + data, metadata, err := decodeJSONResponse(bytes.NewReader(httpResponse.body), &apiv1.NodeIdentity{}) + if err != nil { + return nil, err + } + + return &api.Response[*apiv1.NodeIdentity]{ + Data: data, + Metadata: metadata, + }, nil +} diff --git a/mock/nodeidentity.go b/mock/nodeidentity.go new file mode 100644 index 00000000..f1748888 --- /dev/null +++ b/mock/nodeidentity.go @@ -0,0 +1,33 @@ +package mock + +import ( + "context" + + "github.com/attestantio/go-eth2-client/api" + apiv1 "github.com/attestantio/go-eth2-client/api/v1" +) + +// NodeIdentity provides the identity information of the node. +func (s *Service) NodeIdentity(ctx context.Context, + opts *api.NodeIdentityOpts, +) ( + *api.Response[*apiv1.NodeIdentity], + error, +) { + if s.NodeIdentityFunc != nil { + return s.NodeIdentityFunc(ctx, opts) + } + + return &api.Response[*apiv1.NodeIdentity]{ + Data: &apiv1.NodeIdentity{ + PeerID: "16Uiu2HAmMockPeerID", + Enr: "enr:-mock-enr-value", + P2PAddresses: []string{"/ip4/127.0.0.1/tcp/9000"}, + DiscoveryAddresses: []string{"/ip4/127.0.0.1/udp/9000"}, + Metadata: map[string]string{ + "seq_number": "1", + "attnets": "0xffffffffffffffff", + }, + }, + }, nil +} diff --git a/mock/service.go b/mock/service.go index 32d2b84f..47032ef6 100644 --- a/mock/service.go +++ b/mock/service.go @@ -68,6 +68,7 @@ type Service struct { ForkFunc func(context.Context, *api.ForkOpts) (*api.Response[*phase0.Fork], error) ForkScheduleFunc func(context.Context, *api.ForkScheduleOpts) (*api.Response[[]*phase0.Fork], error) GenesisFunc func(context.Context, *api.GenesisOpts) (*api.Response[*apiv1.Genesis], error) + NodeIdentityFunc func(context.Context, *api.NodeIdentityOpts) (*api.Response[*apiv1.NodeIdentity], error) NodePeersFunc func(context.Context, *api.NodePeersOpts) (*api.Response[[]*apiv1.Peer], error) NodePeerCountFunc func(context.Context, *api.NodePeerCountOpts) (*api.Response[*apiv1.PeerCount], error) NodeSyncingFunc func(context.Context, *api.NodeSyncingOpts) (*api.Response[*apiv1.SyncState], error) diff --git a/multi/nodeidentity.go b/multi/nodeidentity.go new file mode 100644 index 00000000..3215fab4 --- /dev/null +++ b/multi/nodeidentity.go @@ -0,0 +1,31 @@ +package multi + +import ( + "context" + + consensusclient "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/api" + apiv1 "github.com/attestantio/go-eth2-client/api/v1" +) + +// NodeIdentity provides the identity information of the node. +func (s *Service) NodeIdentity(ctx context.Context, opts *api.NodeIdentityOpts) (*api.Response[*apiv1.NodeIdentity], error) { + res, err := s.doCall(ctx, func(ctx context.Context, client consensusclient.Service) (any, error) { + nodeIdentity, err := client.(consensusclient.NodeIdentityProvider).NodeIdentity(ctx, opts) + if err != nil { + return nil, err + } + + return nodeIdentity, nil + }, nil) + if err != nil { + return nil, err + } + + response, isResponse := res.(*api.Response[*apiv1.NodeIdentity]) + if !isResponse { + return nil, ErrIncorrectType + } + + return response, nil +} diff --git a/service.go b/service.go index 8cae1eb7..33149b23 100644 --- a/service.go +++ b/service.go @@ -526,7 +526,16 @@ type GenesisProvider interface { error, ) } - +// NodeIdentityProvider is the interface for providing node identity information. +type NodeIdentityProvider interface { + // NodeIdentity provides the identity information of the node. + NodeIdentity(ctx context.Context, + opts *api.NodeIdentityOpts, + ) ( + *api.Response[*apiv1.NodeIdentity], + error, + ) +} // NodePeersProvider is the interface for providing peer information. type NodePeersProvider interface { // NodePeers provides the peers of the node. diff --git a/testclients/erroring.go b/testclients/erroring.go index df5b9256..17cb267d 100644 --- a/testclients/erroring.go +++ b/testclients/erroring.go @@ -126,6 +126,24 @@ func (s *Erroring) SlotFromStateID(ctx context.Context, stateID string) (phase0. return next.SlotFromStateID(ctx, stateID) } +// NodeIdentity provides the identity information of the node. +func (s *Erroring) NodeIdentity(ctx context.Context, + opts *api.NodeIdentityOpts, +) ( + *api.Response[*apiv1.NodeIdentity], + error, +) { + if err := s.maybeError(ctx); err != nil { + return nil, err + } + next, isNext := s.next.(consensusclient.NodeIdentityProvider) + if !isNext { + return nil, fmt.Errorf("%s@%s does not support this call", s.next.Name(), s.next.Address()) + } + + return next.NodeIdentity(ctx, opts) +} + // NodeVersion returns a free-text string with the node version. func (s *Erroring) NodeVersion(ctx context.Context, opts *api.NodeVersionOpts, diff --git a/testclients/sleepy.go b/testclients/sleepy.go index ccfec3ca..41b4b20d 100644 --- a/testclients/sleepy.go +++ b/testclients/sleepy.go @@ -119,6 +119,22 @@ func (s *Sleepy) SlotFromStateID(ctx context.Context, stateID string) (phase0.Sl return next.SlotFromStateID(ctx, stateID) } +// NodeIdentity provides the identity information of the node. +func (s *Sleepy) NodeIdentity(ctx context.Context, + opts *api.NodeIdentityOpts, +) ( + *api.Response[*apiv1.NodeIdentity], + error, +) { + s.sleep(ctx) + next, isNext := s.next.(consensusclient.NodeIdentityProvider) + if !isNext { + return nil, errors.New("next does not support this call") + } + + return next.NodeIdentity(ctx, opts) +} + // NodeVersion returns a free-text string with the node version. func (s *Sleepy) NodeVersion(ctx context.Context, opts *api.NodeVersionOpts, From d052c4db7a84c7fddde11d1a4179fabba24e1afe Mon Sep 17 00:00:00 2001 From: Diogo Santos Date: Wed, 28 Jan 2026 11:19:22 +0000 Subject: [PATCH 2/4] lint --- api/nodeidentityopts.go | 13 +++++++++++++ api/v1/identity.go | 13 +++++++++++++ http/nodeidentity.go | 13 +++++++++++++ mock/nodeidentity.go | 13 +++++++++++++ multi/nodeidentity.go | 13 +++++++++++++ service.go | 2 ++ 6 files changed, 67 insertions(+) diff --git a/api/nodeidentityopts.go b/api/nodeidentityopts.go index 5189e19e..709f1208 100644 --- a/api/nodeidentityopts.go +++ b/api/nodeidentityopts.go @@ -1,3 +1,16 @@ +// Copyright © 2025 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package api // NodeIdentityOpts are the options for obtaining the node identity. diff --git a/api/v1/identity.go b/api/v1/identity.go index ad231eeb..05fb997d 100644 --- a/api/v1/identity.go +++ b/api/v1/identity.go @@ -1,3 +1,16 @@ +// Copyright © 2025 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package v1 import ( diff --git a/http/nodeidentity.go b/http/nodeidentity.go index 30f3c5d4..5efb2e39 100644 --- a/http/nodeidentity.go +++ b/http/nodeidentity.go @@ -1,3 +1,16 @@ +// Copyright © 2025 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package http import ( diff --git a/mock/nodeidentity.go b/mock/nodeidentity.go index f1748888..77aefc7a 100644 --- a/mock/nodeidentity.go +++ b/mock/nodeidentity.go @@ -1,3 +1,16 @@ +// Copyright © 2025 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package mock import ( diff --git a/multi/nodeidentity.go b/multi/nodeidentity.go index 3215fab4..b1cc5855 100644 --- a/multi/nodeidentity.go +++ b/multi/nodeidentity.go @@ -1,3 +1,16 @@ +// Copyright © 2025 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package multi import ( diff --git a/service.go b/service.go index 33149b23..49e3b145 100644 --- a/service.go +++ b/service.go @@ -526,6 +526,7 @@ type GenesisProvider interface { error, ) } + // NodeIdentityProvider is the interface for providing node identity information. type NodeIdentityProvider interface { // NodeIdentity provides the identity information of the node. @@ -536,6 +537,7 @@ type NodeIdentityProvider interface { error, ) } + // NodePeersProvider is the interface for providing peer information. type NodePeersProvider interface { // NodePeers provides the peers of the node. From 0bab31b70391e140214453d16e1d7eaa5fba4c61 Mon Sep 17 00:00:00 2001 From: Diogo Santos Date: Wed, 28 Jan 2026 11:28:13 +0000 Subject: [PATCH 3/4] fix gci error --- api/v1/identity.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/api/v1/identity.go b/api/v1/identity.go index 05fb997d..34ccd282 100644 --- a/api/v1/identity.go +++ b/api/v1/identity.go @@ -22,19 +22,19 @@ import ( // NodeIdentity contains the node identity information. type NodeIdentity struct { - PeerID string `json:"peer_id"` - Enr string `json:"enr"` - P2PAddresses []string `json:"p2p_addresses"` - DiscoveryAddresses []string `json:"discovery_addresses"` - Metadata map[string]string `json:"metadata"` + PeerID string `json:"peer_id"` + Enr string `json:"enr"` + P2PAddresses []string `json:"p2p_addresses"` + DiscoveryAddresses []string `json:"discovery_addresses"` + Metadata map[string]string `json:"metadata"` } type nodeIdentityJSON struct { - PeerID string `json:"peer_id"` - Enr string `json:"enr"` - P2PAddresses []string `json:"p2p_addresses"` - DiscoveryAddresses []string `json:"discovery_addresses"` - Metadata map[string]string `json:"metadata"` + PeerID string `json:"peer_id"` + Enr string `json:"enr"` + P2PAddresses []string `json:"p2p_addresses"` + DiscoveryAddresses []string `json:"discovery_addresses"` + Metadata map[string]string `json:"metadata"` } // MarshalJSON implements json.Marshaler. From 8c01df184004df3112c6760d3cafc9a027ea3d73 Mon Sep 17 00:00:00 2001 From: Diogo Santos Date: Wed, 28 Jan 2026 11:42:00 +0000 Subject: [PATCH 4/4] fix prealloc --- auto/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auto/service.go b/auto/service.go index 8c617ccb..799b471e 100644 --- a/auto/service.go +++ b/auto/service.go @@ -53,7 +53,7 @@ func New(ctx context.Context, params ...Parameter) (consensusclient.Service, err } func tryHTTP(ctx context.Context, parameters *parameters) (consensusclient.Service, error) { - httpParameters := make([]http.Parameter, 0) + httpParameters := make([]http.Parameter, 0, 3) httpParameters = append(httpParameters, http.WithLogLevel(parameters.logLevel)) httpParameters = append(httpParameters, http.WithAddress(parameters.address)) httpParameters = append(httpParameters, http.WithTimeout(parameters.timeout))