Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node peering rework #84

Merged
merged 16 commits into from
Oct 24, 2023
22 changes: 22 additions & 0 deletions api/peeropts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright © 2023 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

// PeerOpts are the options for client side peer filtering.
type PeerOpts struct {
// State of the connection (disconnected, connecting, connected, disconnecting)
State []string
mcdee marked this conversation as resolved.
Show resolved Hide resolved
// Direction of the connection (inbound, outbound)
Direction []string
}
83 changes: 83 additions & 0 deletions api/v1/peers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package v1

import (
"encoding/json"
"fmt"

"github.com/pkg/errors"
)

// Peer contains all the available information about a nodes peer.
type Peer struct {
PeerID string `json:"peer_id"`
Enr string `json:"enr,omitempty"`
LastSeenP2PAddress string `json:"last_seen_p2p_address"`
State string `json:"state"`
Direction string `json:"direction"`
}

type peerJSON struct {
PeerID string `json:"peer_id"`
Enr string `json:"enr,omitempty"`
LastSeenP2PAddress string `json:"last_seen_p2p_address"`
State string `json:"state"`
Direction string `json:"direction"`
}

// validPeerDirections are all the accepted options for peer direction.
var validPeerDirections = map[string]int{"inbound": 1, "outbound": 1}

// validPeerStates are all the accepted options for peer states.
var validPeerStates = map[string]int{"connected": 1, "connecting": 1, "disconnected": 1, "disconnecting": 1}

func (p *Peer) MarshalJSON() ([]byte, error) {
// make sure we have valid peer states and directions
_, exists := validPeerDirections[p.Direction]
if !exists {
return nil, fmt.Errorf("invalid value for peer direction: %s", p.Direction)
}
_, exists = validPeerStates[p.State]
if !exists {
return nil, fmt.Errorf("invalid value for peer state: %s", p.State)
}

return json.Marshal(&peerJSON{
PeerID: p.PeerID,
Enr: p.Enr,
LastSeenP2PAddress: p.LastSeenP2PAddress,
State: p.State,
Direction: p.Direction,
})
}

func (p *Peer) UnmarshalJSON(input []byte) error {
var peerJSON peerJSON

if err := json.Unmarshal(input, &peerJSON); err != nil {
return errors.Wrap(err, "invalid JSON")
}
_, ok := validPeerStates[peerJSON.State]
if !ok {
return fmt.Errorf("invalid value for peer state: %s", peerJSON.State)
}
p.State = peerJSON.State
_, ok = validPeerDirections[peerJSON.Direction]
if !ok {
return fmt.Errorf("invalid value for peer direction: %s", peerJSON.Direction)
}
p.Direction = peerJSON.Direction
p.Enr = peerJSON.Enr
p.PeerID = peerJSON.PeerID
p.LastSeenP2PAddress = peerJSON.LastSeenP2PAddress

return nil
}

func (p *Peer) String() string {
data, err := json.Marshal(p)
if err != nil {
return fmt.Sprintf("ERR: %v", err)
}

return string(data)
}
60 changes: 60 additions & 0 deletions api/v1/peers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package v1

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
require "github.com/stretchr/testify/require"
)

func TestNodePeerJSON(t *testing.T) {
tests := []struct {
name string
input []byte
err string
}{
{
name: "Empty",
err: "unexpected end of JSON input",
},
{
name: "GoodNoENR",
input: []byte(`{"peer_id":"16Uiu2HAm7ukVy4XugqVShYbLih4H2jBJjYevevznBZaHsmd1FM96","last_seen_p2p_address":"/ip4/10.0.20.8/tcp/43402","state":"connected","direction":"inbound"}`),
},
{
name: "GoodWithENR",
input: []byte(`{"peer_id":"16Uiu2HAmTJgqKuVcN1QReyWzwELRkfWCjLAfBSu3KxuBuWFvvaLX","enr":"enr:-MS4QExfvXqHhj-nqAqkg1Sn55uV7YgpRtlImGCvMJkrkbnLDo8sGhecAGid9B3NjXzN3UtGxpOOUqHZVcEDQxkniwoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpAEg2rFBAAGZgIAAAAAAAAAgmlkgnY0gmlwhAoAFBWEcXVpY4IjKYlzZWNwMjU2azGhA9mr4bIskWVeMt0dEn4IlQJhOFgOqgR9V3gkHTl1lTioiHN5bmNuZXRzAIN0Y3CCIyiDdWRwgiMo","last_seen_p2p_address":"/ip4/10.0.20.21/udp/9001/quic-v1/p2p/16Uiu2HAmTJgqKuVcN1QReyWzwELRkfWCjLAfBSu3KxuBuWFvvaLX","state":"connected","direction":"outbound"}`),
},
{
name: "BadDirection",
input: []byte(`{"peer_id":"16Uiu2HAm7ukVy4XugqVShYbLih4H2jBJjYevevznBZaHsmd1FM96","last_seen_p2p_address":"/ip4/10.0.20.8/tcp/43402","state":"connected","direction":"backwards"}`),
err: "invalid value for peer direction: backwards",
},
{
name: "BadState",
input: []byte(`{"peer_id":"16Uiu2HAm7ukVy4XugqVShYbLih4H2jBJjYevevznBZaHsmd1FM96","last_seen_p2p_address":"/ip4/10.0.20.8/tcp/43402","state":"tightly-coupled","direction":"inbound"}`),
err: "invalid value for peer state: tightly-coupled",
},
{
name: "JSONBad",
input: []byte("[]"),
err: "invalid JSON: json: cannot unmarshal array into Go value of type v1.peerJSON",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var res Peer
err := json.Unmarshal(test.input, &res)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
rt, err := json.Marshal(&res)
require.NoError(t, err)
assert.Equal(t, string(test.input), string(rt))
assert.Equal(t, string(rt), res.String())
}
})
}
}
47 changes: 47 additions & 0 deletions http/nodepeers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package http

import (
"bytes"
"context"
"fmt"
"strings"

"github.com/attestantio/go-eth2-client/api"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
)

func (s *Service) NodePeers(ctx context.Context, opts *api.PeerOpts) (*api.Response[[]*apiv1.Peer], error) {
// all options are considered optional
request := "/eth/v1/node/peers"
additionalFields := make([]string, 0, len(opts.State)+len(opts.Direction))

for _, stateFilter := range opts.State {
additionalFields = append(additionalFields, fmt.Sprintf("state=%s", stateFilter))
}

for _, directionFilter := range opts.Direction {
additionalFields = append(additionalFields, fmt.Sprintf("direction=%s", directionFilter))
}

if len(additionalFields) > 0 {
request = fmt.Sprintf("%s?%s", request, strings.Join(additionalFields, "&"))
}

httpResponse, err := s.get2(ctx, request)
if err != nil {
return nil, err
}

if httpResponse.contentType != ContentTypeJSON {
return nil, fmt.Errorf("unexpected content type %v (expected JSON)", httpResponse.contentType)
}
data, meta, err := decodeJSONResponse(bytes.NewReader(httpResponse.body), []*apiv1.Peer{})
if err != nil {
return nil, err
}

return &api.Response[[]*apiv1.Peer]{
Data: data,
Metadata: meta,
}, nil
}
69 changes: 69 additions & 0 deletions http/nodepeers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright © 2020, 2021 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_test

import (
"context"
client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/attestantio/go-eth2-client/http"
"github.com/stretchr/testify/require"
"os"
"testing"
)

func TestNodePeers(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

tests := []struct {
name string
opts *api.PeerOpts
}{
{
name: "AllPeers",
opts: &api.PeerOpts{},
},
{
name: "AllInboundPeers",
opts: &api.PeerOpts{Direction: []string{"inbound"}},
},
{
name: "AllConnectedPeers",
opts: &api.PeerOpts{State: []string{"connected"}},
},
{
name: "AllConnectedOutboundPeers",
opts: &api.PeerOpts{
State: []string{"connected"},
Direction: []string{"outbound"},
},
},
}

service, err := http.New(ctx,
http.WithTimeout(timeout),
http.WithAddress(os.Getenv("HTTP_ADDRESS")),
)
require.NoError(t, err)

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
response, err := service.(client.NodePeersProvider).NodePeers(ctx, test.opts)
require.NoError(t, err)
require.NotNil(t, response)
require.NotNil(t, response.Data)
})
}
}
33 changes: 33 additions & 0 deletions mock/nodepeers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright © 2021, 2023 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"
)

// NodePeers provides the peers of the node.
func (s *Service) NodePeers(_ context.Context, _ *api.PeerOpts) (*api.Response[[]*apiv1.Peer], error) {
return &api.Response[[]*apiv1.Peer]{
Data: []*apiv1.Peer{{
PeerID: "MOCK16Uiu2HAm7ukVy4XugqVShYbLih4H2jBJjYevevznBZaHsmd1FM96",
LastSeenP2PAddress: "/ip4/10.0.20.8/tcp/43402",
State: "connected",
Direction: "outbound",
}},
}, nil
}
39 changes: 39 additions & 0 deletions multi/nodepeers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright © 2021, 2023 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"
)

// NodePeers provides the peers of the node.
func (s *Service) NodePeers(ctx context.Context, opts *api.PeerOpts) (*api.Response[[]*apiv1.Peer], error) {
res, err := s.doCall(ctx, func(ctx context.Context, client consensusclient.Service) (interface{}, error) {
nodePeers, err := client.(consensusclient.NodePeersProvider).NodePeers(ctx, opts)
if err != nil {
return nil, err
}

return nodePeers, nil
}, nil)
if err != nil {
return nil, err
}

return res.(*api.Response[[]*apiv1.Peer]), nil
}
Loading