diff --git a/api/nodeidentityopts.go b/api/nodeidentityopts.go new file mode 100644 index 00000000..709f1208 --- /dev/null +++ b/api/nodeidentityopts.go @@ -0,0 +1,19 @@ +// 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. +type NodeIdentityOpts struct { + Common CommonOpts +} diff --git a/api/v1/identity.go b/api/v1/identity.go new file mode 100644 index 00000000..34ccd282 --- /dev/null +++ b/api/v1/identity.go @@ -0,0 +1,75 @@ +// 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 ( + "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/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)) diff --git a/http/nodeidentity.go b/http/nodeidentity.go new file mode 100644 index 00000000..5efb2e39 --- /dev/null +++ b/http/nodeidentity.go @@ -0,0 +1,59 @@ +// 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 ( + "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..77aefc7a --- /dev/null +++ b/mock/nodeidentity.go @@ -0,0 +1,46 @@ +// 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 ( + "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..b1cc5855 --- /dev/null +++ b/multi/nodeidentity.go @@ -0,0 +1,44 @@ +// 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 ( + "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..49e3b145 100644 --- a/service.go +++ b/service.go @@ -527,6 +527,17 @@ type GenesisProvider interface { ) } +// 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,