Skip to content

Commit

Permalink
Add NodeInfo implementation (#3046)
Browse files Browse the repository at this point in the history
* Add NodeInfo implementation

* replace magic value with a constant.

* update dependencies

* bump minor version

* add nodes deduplication logic.

* shuffle values in test cases a little.
  • Loading branch information
dsavelev authored Nov 30, 2021
1 parent d7ee2b1 commit d4f6cef
Show file tree
Hide file tree
Showing 11 changed files with 299 additions and 8 deletions.
41 changes: 41 additions & 0 deletions cmd/rpcdaemon/commands/admin_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package commands

import (
"context"
"errors"
"fmt"

"github.com/ledgerwatch/erigon/cmd/rpcdaemon/services"
"github.com/ledgerwatch/erigon/p2p"
)

// AdminAPI the interface for the admin_* RPC commands.
type AdminAPI interface {
// NodeInfo returns a collection of metadata known about the host.
NodeInfo(ctx context.Context) (*p2p.NodeInfo, error)
}

// AdminAPIImpl data structure to store things needed for admin_* commands.
type AdminAPIImpl struct {
ethBackend services.ApiBackend
}

// NewAdminAPI returns AdminAPIImpl instance.
func NewAdminAPI(eth services.ApiBackend) *AdminAPIImpl {
return &AdminAPIImpl{
ethBackend: eth,
}
}

func (api *AdminAPIImpl) NodeInfo(ctx context.Context) (*p2p.NodeInfo, error) {
nodes, err := api.ethBackend.NodeInfo(ctx, 1)
if err != nil {
return nil, fmt.Errorf("node info request error: %w", err)
}

if len(nodes) == 0 {
return nil, errors.New("empty nodesInfo response")
}

return &nodes[0], nil
}
10 changes: 9 additions & 1 deletion cmd/rpcdaemon/commands/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ func APIList(ctx context.Context, db kv.RoDB,
base.EnableTevmExperiment()
}
ethImpl := NewEthAPI(base, db, eth, txPool, mining, cfg.Gascap)
erigonImpl := NewErigonAPI(base, db)
erigonImpl := NewErigonAPI(base, db, eth)
txpoolImpl := NewTxPoolAPI(base, db, txPool)
netImpl := NewNetAPIImpl(eth)
debugImpl := NewPrivateDebugAPI(base, db, cfg.Gascap)
traceImpl := NewTraceAPI(base, db, &cfg)
web3Impl := NewWeb3APIImpl(eth)
dbImpl := NewDBAPIImpl() /* deprecated */
engineImpl := NewEngineAPI(base, db, eth)
adminImpl := NewAdminAPI(eth)

for _, enabledAPI := range cfg.API {
switch enabledAPI {
Expand Down Expand Up @@ -100,6 +101,13 @@ func APIList(ctx context.Context, db kv.RoDB,
Service: EngineAPI(engineImpl),
Version: "1.0",
})
case "admin":
defaultAPIList = append(defaultAPIList, rpc.API{
Namespace: "admin",
Public: false,
Service: AdminAPI(adminImpl),
Version: "1.0",
})
}
}

Expand Down
15 changes: 11 additions & 4 deletions cmd/rpcdaemon/commands/erigon_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"context"

"github.com/ledgerwatch/erigon-lib/kv"
"github.com/ledgerwatch/erigon/cmd/rpcdaemon/services"
"github.com/ledgerwatch/erigon/common"
"github.com/ledgerwatch/erigon/core/types"
"github.com/ledgerwatch/erigon/p2p"
"github.com/ledgerwatch/erigon/rpc"
)

Expand All @@ -26,18 +28,23 @@ type ErigonAPI interface {
// BlockReward(ctx context.Context, blockNr rpc.BlockNumber) (Issuance, error)
// UncleReward(ctx context.Context, blockNr rpc.BlockNumber) (Issuance, error)
Issuance(ctx context.Context, blockNr rpc.BlockNumber) (Issuance, error)

// NodeInfo returns a collection of metadata known about the host.
NodeInfo(ctx context.Context) ([]p2p.NodeInfo, error)
}

// ErigonImpl is implementation of the ErigonAPI interface
type ErigonImpl struct {
*BaseAPI
db kv.RoDB
db kv.RoDB
ethBackend services.ApiBackend
}

// NewErigonAPI returns ErigonImpl instance
func NewErigonAPI(base *BaseAPI, db kv.RoDB) *ErigonImpl {
func NewErigonAPI(base *BaseAPI, db kv.RoDB, eth services.ApiBackend) *ErigonImpl {
return &ErigonImpl{
BaseAPI: base,
db: db,
BaseAPI: base,
db: db,
ethBackend: eth,
}
}
16 changes: 16 additions & 0 deletions cmd/rpcdaemon/commands/erigon_nodeInfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package commands

import (
"context"

"github.com/ledgerwatch/erigon/p2p"
)

const (
// allNodesInfo used in NodeInfo request to receive meta data from all running sentries.
allNodesInfo = 0
)

func (api *ErigonImpl) NodeInfo(ctx context.Context) ([]p2p.NodeInfo, error) {
return api.ethBackend.NodeInfo(ctx, allNodesInfo)
}
41 changes: 41 additions & 0 deletions cmd/rpcdaemon/services/eth_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package services

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
Expand All @@ -14,6 +15,7 @@ import (
"github.com/ledgerwatch/erigon/common"
"github.com/ledgerwatch/erigon/core/types"
"github.com/ledgerwatch/erigon/ethdb/privateapi"
"github.com/ledgerwatch/erigon/p2p"
"github.com/ledgerwatch/log/v3"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
Expand All @@ -33,6 +35,7 @@ type ApiBackend interface {
BlockWithSenders(ctx context.Context, tx kv.Tx, hash common.Hash, blockHeight uint64) (block *types.Block, senders []common.Address, err error)
EngineExecutePayloadV1(ctx context.Context, payload *types2.ExecutionPayload) (*remote.EngineExecutePayloadReply, error)
EngineGetPayloadV1(ctx context.Context, payloadId uint64) (*types2.ExecutionPayload, error)
NodeInfo(ctx context.Context, limit uint32) ([]p2p.NodeInfo, error)
}

type RemoteBackend struct {
Expand Down Expand Up @@ -166,3 +169,41 @@ func (back *RemoteBackend) EngineGetPayloadV1(ctx context.Context, payloadId uin
PayloadId: payloadId,
})
}

func (back *RemoteBackend) NodeInfo(ctx context.Context, limit uint32) ([]p2p.NodeInfo, error) {
nodes, err := back.remoteEthBackend.NodeInfo(ctx, &remote.NodesInfoRequest{Limit: limit})
if err != nil {
return nil, fmt.Errorf("nodes info request error: %w", err)
}

if nodes == nil || len(nodes.NodesInfo) == 0 {
return nil, errors.New("empty nodesInfo response")
}

ret := make([]p2p.NodeInfo, 0, len(nodes.NodesInfo))
for _, node := range nodes.NodesInfo {
var protocols map[string]interface{}
if err = json.Unmarshal(node.Protocols, &protocols); err != nil {
return nil, fmt.Errorf("cannot decode protocols metadata: %w", err)
}

ret = append(ret, p2p.NodeInfo{
Enode: node.Enode,
ID: node.Id,
IP: node.Enode,
ENR: node.Enr,
ListenAddr: node.ListenerAddr,
Name: node.Name,
Ports: struct {
Discovery int `json:"discovery"`
Listener int `json:"listener"`
}{
Discovery: int(node.Ports.Discovery),
Listener: int(node.Ports.Listener),
},
Protocols: protocols,
})
}

return ret, nil
}
29 changes: 29 additions & 0 deletions cmd/sentry/download/sentry.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package download
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"math"
Expand Down Expand Up @@ -980,6 +982,33 @@ func (ss *SentryServerImpl) Peers(req *proto_sentry.PeersRequest, server proto_s
}
}

func (ss *SentryServerImpl) NodeInfo(_ context.Context, _ *emptypb.Empty) (*proto_types.NodeInfoReply, error) {
if ss.P2pServer == nil {
return nil, errors.New("p2p server was not started")
}

info := ss.P2pServer.NodeInfo()
ret := &proto_types.NodeInfoReply{
Id: info.ID,
Name: info.Name,
Enode: info.Enode,
Enr: info.ENR,
Ports: &proto_types.NodeInfoPorts{
Discovery: uint32(info.Ports.Discovery),
Listener: uint32(info.Ports.Listener),
},
ListenerAddr: info.ListenAddr,
}

protos, err := json.Marshal(info.Protocols)
if err != nil {
return nil, fmt.Errorf("cannot encode protocols map: %w", err)
}

ret.Protocols = protos
return ret, nil
}

// PeersStreams - it's safe to use this class as non-pointer
type PeersStreams struct {
mu sync.RWMutex
Expand Down
26 changes: 26 additions & 0 deletions eth/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ import (
"github.com/ledgerwatch/erigon-lib/direct"
"github.com/ledgerwatch/erigon-lib/etl"
"github.com/ledgerwatch/erigon-lib/gointerfaces/grpcutil"
"github.com/ledgerwatch/erigon-lib/gointerfaces/remote"
"github.com/ledgerwatch/erigon-lib/gointerfaces/sentry"
txpool_proto "github.com/ledgerwatch/erigon-lib/gointerfaces/txpool"
prototypes "github.com/ledgerwatch/erigon-lib/gointerfaces/types"
"github.com/ledgerwatch/erigon-lib/kv"
"github.com/ledgerwatch/erigon-lib/kv/kvcache"
"github.com/ledgerwatch/erigon-lib/kv/remotedbserver"
Expand Down Expand Up @@ -69,6 +71,7 @@ import (
"github.com/ledgerwatch/log/v3"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"modernc.org/sortutil"
)

// Config contains the configuration options of the ETH protocol.
Expand Down Expand Up @@ -629,6 +632,29 @@ func (s *Ethereum) NetPeerCount() (uint64, error) {
return sentryPc, nil
}

func (s *Ethereum) NodesInfo(limit int) (*remote.NodesInfoReply, error) {
if limit == 0 || limit > len(s.sentries) {
limit = len(s.sentries)
}

nodes := make([]*prototypes.NodeInfoReply, 0, limit)
for i := 0; i < limit; i++ {
sc := s.sentries[i]

nodeInfo, err := sc.NodeInfo(context.Background(), nil)
if err != nil {
log.Error("sentry nodeInfo", "err", err)
}

nodes = append(nodes, nodeInfo)
}

nodesInfo := &remote.NodesInfoReply{NodesInfo: nodes}
nodesInfo.NodesInfo = nodesInfo.NodesInfo[:sortutil.Dedupe(nodesInfo)]

return nodesInfo, nil
}

// Protocols returns all the currently configured
// network protocols to start.
func (s *Ethereum) Protocols() []p2p.Protocol {
Expand Down
108 changes: 108 additions & 0 deletions eth/backend_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package eth

import (
"context"
"testing"

"github.com/ledgerwatch/erigon-lib/direct"
"github.com/ledgerwatch/erigon-lib/gointerfaces/sentry"
"github.com/ledgerwatch/erigon-lib/gointerfaces/types"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
)

func TestNodesInfo_Deduplication(t *testing.T) {
tests := []struct {
name string
limit int
nodes []*types.NodeInfoReply
want []*types.NodeInfoReply
}{
{
name: "one node",
nodes: []*types.NodeInfoReply{{Name: "name", Enode: "enode"}},
want: []*types.NodeInfoReply{{Name: "name", Enode: "enode"}},
},
{
name: "two different nodes",
nodes: []*types.NodeInfoReply{
{Name: "name1", Enode: "enode1"},
{Name: "name", Enode: "enode"},
},
want: []*types.NodeInfoReply{
{Name: "name", Enode: "enode"},
{Name: "name1", Enode: "enode1"},
},
},
{
name: "two same nodes",
nodes: []*types.NodeInfoReply{
{Name: "name", Enode: "enode"},
{Name: "name", Enode: "enode"},
},
want: []*types.NodeInfoReply{
{Name: "name", Enode: "enode"},
},
},
{
name: "three nodes",
nodes: []*types.NodeInfoReply{
{Name: "name2", Enode: "enode2"},
{Name: "name", Enode: "enode"},
{Name: "name1", Enode: "enode1"},
},
want: []*types.NodeInfoReply{
{Name: "name", Enode: "enode"},
{Name: "name1", Enode: "enode1"},
{Name: "name2", Enode: "enode2"},
},
},
{
name: "three nodes with repeats",
nodes: []*types.NodeInfoReply{
{Name: "name", Enode: "enode"},
{Name: "name1", Enode: "enode1"},
{Name: "name", Enode: "enode"},
},
want: []*types.NodeInfoReply{
{Name: "name", Enode: "enode"},
{Name: "name1", Enode: "enode1"},
},
},
{
name: "three same nodes",
nodes: []*types.NodeInfoReply{
{Name: "name", Enode: "enode"},
{Name: "name", Enode: "enode"},
{Name: "name", Enode: "enode"},
},
want: []*types.NodeInfoReply{
{Name: "name", Enode: "enode"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
eth := Ethereum{}

for _, n := range tt.nodes {
n := n
eth.sentries = append(eth.sentries,
direct.NewSentryClientRemote(&sentry.SentryClientMock{
NodeInfoFunc: func(context.Context, *emptypb.Empty, ...grpc.CallOption) (*types.NodeInfoReply, error) {
return n, nil
},
}),
)
}

got, err := eth.NodesInfo(tt.limit)
if err != nil {
t.Error(err)
}

assert.Equal(t, tt.want, got.NodesInfo)
})
}
}
Loading

0 comments on commit d4f6cef

Please sign in to comment.