From ce6ef98b956b0dec15b76ba486c3690b1eb43711 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 2 May 2019 15:15:15 -0400 Subject: [PATCH 01/43] main, host: Wire up JoinPeers This probably doesn't work due to our internal host/client schema, but it's a good start. --- host.go | 7 +++++++ host/host.go | 25 +++++++++++++++++++++++++ main.go | 13 +++++++------ 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/host.go b/host.go index 482b596..2a26080 100644 --- a/host.go +++ b/host.go @@ -73,6 +73,7 @@ func runHost(options Options) error { } logger.Infof("Connected to vipnode pool: %s", options.Host.Pool) + // Register reverse-directional RPC calls available on the host rpcServer := &jsonrpc2.Server{} if err := rpcServer.RegisterMethod("vipnode_whitelist", h, "Whitelist"); err != nil { return err @@ -98,6 +99,12 @@ func runHost(options Options) error { errChan <- h.Wait() }() + if options.Host.JoinPeers > 0 { + if err := h.ConnectPeers(remotePool, options.Host.JoinPeers); err != nil { + return err + } + } + sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt) go func() { diff --git a/host/host.go b/host/host.go index 9f57ad7..76d151a 100644 --- a/host/host.go +++ b/host/host.go @@ -148,6 +148,31 @@ func (h *Host) Start(p pool.Pool) error { return nil } +// ConnectPeers requests num host peers from the pool. The host will whitelist +// and connect to them. This is useful for increasing full node peering for +// your node with other nodes under the same pool. +func (h *Host) ConnectPeers(p pool.Pool, num int) error { + // Hosts are full nodes, so we don't care what kind of host peer we get. + // Full nodes speak to all full nodes. + kind := "" + ctx := context.Background() + resp, err := p.Client(ctx, pool.ClientRequest{Kind: kind}) + if err != nil { + return err + } + nodes := resp.Hosts + if len(nodes) == 0 { + return pool.NoHostNodesError{} + } + logger.Printf("Received %d host candidates from pool (version %s), connecting...", len(nodes), resp.PoolVersion) + for _, node := range nodes { + if err := h.node.ConnectPeer(ctx, node.URI); err != nil { + return err + } + } + return nil +} + func (h *Host) serveUpdates(p pool.Pool) error { ticker := time.Tick(store.KeepaliveInterval) for { diff --git a/main.go b/main.go index 873f0aa..0a9e800 100644 --- a/main.go +++ b/main.go @@ -51,11 +51,12 @@ type Options struct { } `command:"client" description:"Connect to a vipnode as a client."` Host struct { - Pool string `long:"pool" description:"Pool to participate in." default:"wss://pool.vipnode.org/"` - RPC string `long:"rpc" description:"RPC path or URL of the host node."` - NodeKey string `long:"nodekey" description:"Path to the host node's private key."` - NodeURI string `long:"enode" description:"Public enode://... URI for clients to connect to. (If node is on a different IP from the vipnode agent)"` - Payout string `long:"payout" description:"Ethereum wallet address to receive pool payments."` + Pool string `long:"pool" description:"Pool to participate in." default:"wss://pool.vipnode.org/"` + RPC string `long:"rpc" description:"RPC path or URL of the host node."` + NodeKey string `long:"nodekey" description:"Path to the host node's private key."` + NodeURI string `long:"enode" description:"Public enode://... URI for clients to connect to. (If node is on a different IP from the vipnode agent)"` + Payout string `long:"payout" description:"Ethereum wallet address to receive pool payments."` + JoinPeers int `long:"join-peers" description:"Whitelist and connect to N other hosts on the pool." default:"0"` } `command:"host" description:"Host a vipnode."` Pool struct { @@ -189,7 +190,7 @@ func subcommand(cmd string, options Options) error { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt) - backoff := []int{5, 30, 60, 90, 300} // Backoff in sequence in seconds. + backoff := []int{5, 30, 60, 90, 300} // Backoff sequence in seconds. clearTimeout := time.Second * 300 // Time between attempts before we reset the backoff var err error for i := 0; ; i++ { From 02f259c07c0219fa46c8a1e2692b3456bfc84acc Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 2 May 2019 16:31:22 -0400 Subject: [PATCH 02/43] internal/mkcluster: Add helper for generating a pool/host/client cluster in testing --- internal/mkcluster/mkcluster.go | 126 ++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 internal/mkcluster/mkcluster.go diff --git a/internal/mkcluster/mkcluster.go b/internal/mkcluster/mkcluster.go new file mode 100644 index 0000000..8ef0463 --- /dev/null +++ b/internal/mkcluster/mkcluster.go @@ -0,0 +1,126 @@ +package mkcluster + +import ( + "crypto/ecdsa" + "fmt" + "io" + "strings" + + "github.com/ethereum/go-ethereum/p2p/discv5" + "github.com/vipnode/vipnode/client" + "github.com/vipnode/vipnode/host" + "github.com/vipnode/vipnode/internal/fakenode" + "github.com/vipnode/vipnode/jsonrpc2" + "github.com/vipnode/vipnode/pool" + "github.com/vipnode/vipnode/pool/store/memory" +) + +// Cluster represents a set of active hosts and clients connected to a pool. +type Cluster struct { + Clients []*client.Client + Hosts []*host.Host + Pool *pool.VipnodePool + + pipes []io.Closer +} + +// MakeCluster returns a pre-connected pool of hosts and clients. +func MakeCluster(hostKeys []*ecdsa.PrivateKey, clientKeys []*ecdsa.PrivateKey) (*Cluster, error) { + cluster := &Cluster{ + Hosts: []*host.Host{}, + Clients: []*client.Client{}, + pipes: []io.Closer{}, + } + + cluster.Pool = pool.New(memory.New(), nil) + payout := "" + for _, hostKey := range hostKeys { + rpcPool2Host, rpcHost2Pool := jsonrpc2.ServePipe() + cluster.pipes = append(cluster.pipes, rpcPool2Host, rpcHost2Pool) + if err := rpcPool2Host.Server.Register("vipnode_", cluster.Pool); err != nil { + return nil, err + } + + hostNodeID := discv5.PubkeyID(&hostKey.PublicKey).String() + hostNode := fakenode.Node(hostNodeID) + hostNodeURI := fmt.Sprintf("enode://%s@127.0.0.1:30303", hostNodeID) + h := host.New(hostNode, payout) + if err := rpcHost2Pool.Server.RegisterMethod("vipnode_whitelist", h, "Whitelist"); err != nil { + return nil, err + } + h.NodeURI = hostNodeURI + hostPool := pool.Remote(rpcHost2Pool, hostKey) + + if err := h.Start(hostPool); err != nil { + return nil, err + } + + cluster.Hosts = append(cluster.Hosts, h) + } + + for _, clientKey := range clientKeys { + rpcPool2Client, rpcClient2Pool := jsonrpc2.ServePipe() + cluster.pipes = append(cluster.pipes, rpcPool2Client, rpcClient2Pool) + rpcPool2Client.Server.Register("vipnode_", cluster.Pool) + + clientNodeID := discv5.PubkeyID(&clientKey.PublicKey).String() + clientNode := fakenode.Node(clientNodeID) + c := client.New(clientNode) + clientPool := pool.Remote(rpcClient2Pool, clientKey) + if err := c.Start(clientPool); err != nil { + return nil, err + } + cluster.Clients = append(cluster.Clients, c) + } + return cluster, nil +} + +// Close shuts down all the open pipes. +func (c *Cluster) Close() error { + errors := []error{} + for _, pipe := range c.pipes { + if err := pipe.Close(); err != nil { + errors = append(errors, err) + } + } + for _, host := range c.Hosts { + host.Stop() + } + for _, client := range c.Clients { + client.Stop() + } + for _, host := range c.Hosts { + if err := host.Wait(); err != nil { + errors = append(errors, err) + } + } + for _, client := range c.Clients { + if err := client.Wait(); err != nil { + errors = append(errors, err) + } + } + if len(errors) > 0 { + return CloseErrors(errors) + } + return nil +} + +// CloseErrors are used to return a set of errors that occurred while +// attempting to shut down a cluster. +type CloseErrors []error + +func (e CloseErrors) Error() string { + if len(e) == 0 { + return "no close errors" + } + + var s strings.Builder + fmt.Fprintf(&s, "failed to close with %d errors: ", len(e)) + for i, err := range e { + s.WriteString(err.Error()) + if i != len(e)-1 { + s.WriteString("; ") + } + } + return s.String() +} From fc4f9bced3d7d326b78ad26c955591117ac4e05a Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 2 May 2019 16:31:33 -0400 Subject: [PATCH 03/43] internal/keygen: Hardcode more keys --- internal/keygen/keygen.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/internal/keygen/keygen.go b/internal/keygen/keygen.go index e9bce74..ed92644 100644 --- a/internal/keygen/keygen.go +++ b/internal/keygen/keygen.go @@ -13,6 +13,22 @@ var hardcodedKeys = []string{ `OX9lnxz+fWNmEEBXCKfEmEsh5oGhCXXdHJBqilgZPNc=`, `pebDrvrc9iHxN8k7YJva6bvr6Mzimb9ZbFrGpVy/Wb0=`, `cl3X1He2rNZiXbsii3M9zxBTi9B7gB1Tqgk6u5rMytE=`, + `+0HMUDyMBxFbNat59Vl6Sg+3EcVgiXt1y+JNtnjKb18=`, + `exk5qBaBxCex5A5Rx9/0qokyz4tu2aAwCJNzsEtIXnk=`, + `eqS1AIeHCa6xA4WWE2Q8hooTFVyUtQasJeDH3TSxkGU=`, + `DvqsYyCf9KmbmcC34hTwIjzVWSJnXKTxSxJAlKABSBs=`, + `gciM+wfV80vZxh5RAevJzcRBJyGkoxvmWRxyaNkOlpc=`, + `q8Ye3Cq3+/JKASV1KrSKy6wLLwjUdkffM4sBtKcOyPQ=`, + `dAoxmCIoohJePxhAlwL1res/Ict7U+ZhpTHlSXD6zt4=`, + `bx7mVtX5RV8BgsMmWqGs9aPnSNaSRfjB0sPBBKMfex4=`, + `wRQRSoYNlu/7SO75LJYieZv25TlEgH/NE8fyq/OWUQQ=`, + `O5oR0U9/xwfmOQZenln+Vr5rUZe3ooCs2ZFkKYx9VDQ=`, + `MhQtBdZogSKlwB0S7UTffnVrT6G/LXncFiaCaCQBTrQ=`, + `1raBg6tKGFSJ3JVV+cmwezOdNKYRAG1/CwMfw8SdHxQ=`, + `hyHctcYKirNqWFPyU46aTsU1DVOW6SbBH8IR+jLOIa0=`, + `vQzyvauJh+RZojBXy1jzziBr8PBWP+rV3Qefx0ANzsU=`, + `qHC5oCzkoXfYEzNmkS/CN4aJTKUHI1/NoWW9u8AbbQM=`, + `WalBCrYbrAA4Npnaez6nDaEIyX4NMyC6GGpY7Y0iMoM=`, } func HardcodedKey(t *testing.T) *ecdsa.PrivateKey { @@ -34,6 +50,11 @@ func HardcodedKeyIdx(t *testing.T, idx int) *ecdsa.PrivateKey { return privkey } +func EncodeKey(key *ecdsa.PrivateKey) string { + data := crypto.FromECDSA(key) + return base64.StdEncoding.EncodeToString(data) +} + func NewKey(t *testing.T) *ecdsa.PrivateKey { key, err := crypto.GenerateKey() if err != nil { From 18eefa9c4cd0361f098887d8c2a4e95b2cde2e54 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 2 May 2019 16:32:21 -0400 Subject: [PATCH 04/43] main: Add TestPoolHostconnectPeers, unfinished --- poolhostclient_test.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/poolhostclient_test.go b/poolhostclient_test.go index 289c02f..f30aa3f 100644 --- a/poolhostclient_test.go +++ b/poolhostclient_test.go @@ -1,6 +1,7 @@ package main import ( + "crypto/ecdsa" "fmt" "io" "net" @@ -12,6 +13,7 @@ import ( "github.com/vipnode/vipnode/host" "github.com/vipnode/vipnode/internal/fakenode" "github.com/vipnode/vipnode/internal/keygen" + "github.com/vipnode/vipnode/internal/mkcluster" "github.com/vipnode/vipnode/jsonrpc2" "github.com/vipnode/vipnode/pool" "github.com/vipnode/vipnode/pool/store/memory" @@ -154,3 +156,33 @@ func TestCloseHost(t *testing.T) { h.Stop() h.Wait() } + +func TestPoolHostConnectPeers(t *testing.T) { + hostKeys := []*ecdsa.PrivateKey{} + clientKeys := []*ecdsa.PrivateKey{} + + for i := 0; i < 4; i++ { + hostKeys = append(hostKeys, keygen.HardcodedKeyIdx(t, i)) + } + + cluster, err := mkcluster.MakeCluster(hostKeys, clientKeys) + if err != nil { + t.Fatal(err) + } + + stats, err := cluster.Pool.Store.Stats() + if err != nil { + t.Error(err) + } + + if stats.NumActiveHosts != 4 { + t.Errorf("wrong stats: %+v", stats) + } + + // XXX: Test host.ConnectPeers + + err = cluster.Close() + if err != nil { + t.Error(err) + } +} From bce18bae3e5a30381b5e8b49ea5afb634023738c Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Fri, 3 May 2019 10:58:32 -0400 Subject: [PATCH 05/43] internal/mkcluster -> internal/fakecluster --- .../fakecluster.go} | 26 +++++++++---------- poolhostclient_test.go | 13 +++++++--- 2 files changed, 23 insertions(+), 16 deletions(-) rename internal/{mkcluster/mkcluster.go => fakecluster/fakecluster.go} (84%) diff --git a/internal/mkcluster/mkcluster.go b/internal/fakecluster/fakecluster.go similarity index 84% rename from internal/mkcluster/mkcluster.go rename to internal/fakecluster/fakecluster.go index 8ef0463..512be34 100644 --- a/internal/mkcluster/mkcluster.go +++ b/internal/fakecluster/fakecluster.go @@ -1,4 +1,4 @@ -package mkcluster +package fakecluster import ( "crypto/ecdsa" @@ -17,18 +17,18 @@ import ( // Cluster represents a set of active hosts and clients connected to a pool. type Cluster struct { - Clients []*client.Client - Hosts []*host.Host + clients []*client.Client + hosts []*host.Host Pool *pool.VipnodePool pipes []io.Closer } -// MakeCluster returns a pre-connected pool of hosts and clients. -func MakeCluster(hostKeys []*ecdsa.PrivateKey, clientKeys []*ecdsa.PrivateKey) (*Cluster, error) { +// New returns a pre-connected pool of hosts and clients. +func New(hostKeys []*ecdsa.PrivateKey, clientKeys []*ecdsa.PrivateKey) (*Cluster, error) { cluster := &Cluster{ - Hosts: []*host.Host{}, - Clients: []*client.Client{}, + hosts: []*host.Host{}, + clients: []*client.Client{}, pipes: []io.Closer{}, } @@ -55,7 +55,7 @@ func MakeCluster(hostKeys []*ecdsa.PrivateKey, clientKeys []*ecdsa.PrivateKey) ( return nil, err } - cluster.Hosts = append(cluster.Hosts, h) + cluster.hosts = append(cluster.hosts, h) } for _, clientKey := range clientKeys { @@ -70,7 +70,7 @@ func MakeCluster(hostKeys []*ecdsa.PrivateKey, clientKeys []*ecdsa.PrivateKey) ( if err := c.Start(clientPool); err != nil { return nil, err } - cluster.Clients = append(cluster.Clients, c) + cluster.clients = append(cluster.clients, c) } return cluster, nil } @@ -83,18 +83,18 @@ func (c *Cluster) Close() error { errors = append(errors, err) } } - for _, host := range c.Hosts { + for _, host := range c.hosts { host.Stop() } - for _, client := range c.Clients { + for _, client := range c.clients { client.Stop() } - for _, host := range c.Hosts { + for _, host := range c.hosts { if err := host.Wait(); err != nil { errors = append(errors, err) } } - for _, client := range c.Clients { + for _, client := range c.clients { if err := client.Wait(); err != nil { errors = append(errors, err) } diff --git a/poolhostclient_test.go b/poolhostclient_test.go index f30aa3f..ffa1996 100644 --- a/poolhostclient_test.go +++ b/poolhostclient_test.go @@ -11,9 +11,9 @@ import ( "github.com/ethereum/go-ethereum/p2p/discv5" "github.com/vipnode/vipnode/client" "github.com/vipnode/vipnode/host" + "github.com/vipnode/vipnode/internal/fakecluster" "github.com/vipnode/vipnode/internal/fakenode" "github.com/vipnode/vipnode/internal/keygen" - "github.com/vipnode/vipnode/internal/mkcluster" "github.com/vipnode/vipnode/jsonrpc2" "github.com/vipnode/vipnode/pool" "github.com/vipnode/vipnode/pool/store/memory" @@ -30,7 +30,6 @@ func TestPoolHostClient(t *testing.T) { if err := rpcPool2Host.Server.Register("vipnode_", p); err != nil { t.Fatalf("failed to register vipnode_ rpc for pool: %s", err) } - hostNodeID := discv5.PubkeyID(&privkey.PublicKey).String() hostNode := fakenode.Node(hostNodeID) hostNodeURI := fmt.Sprintf("enode://%s@127.0.0.1:30303", hostNodeID) @@ -165,11 +164,17 @@ func TestPoolHostConnectPeers(t *testing.T) { hostKeys = append(hostKeys, keygen.HardcodedKeyIdx(t, i)) } - cluster, err := mkcluster.MakeCluster(hostKeys, clientKeys) + cluster, err := fakecluster.New(hostKeys, clientKeys) if err != nil { t.Fatal(err) } + /* + if got, want := len(cluster.Hosts), 4; got != want { + t.Errorf("wrong number of hosts: got %d; want %d", got, want) + } + */ + stats, err := cluster.Pool.Store.Stats() if err != nil { t.Error(err) @@ -180,6 +185,8 @@ func TestPoolHostConnectPeers(t *testing.T) { } // XXX: Test host.ConnectPeers + //host := cluster.Hosts[0] + //host.ConnectPeers(cluster.Pool, 3) err = cluster.Close() if err != nil { From 444e58787dcd97b85ac0acf9700ea22155b13f4e Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Fri, 3 May 2019 14:07:19 -0400 Subject: [PATCH 06/43] main: TestPoolHostConnectPeer is comprehensive and passes --- internal/fakecluster/fakecluster.go | 48 +++++++++++++++++++----- poolhostclient_test.go | 57 +++++++++++++++++++++++++---- 2 files changed, 87 insertions(+), 18 deletions(-) diff --git a/internal/fakecluster/fakecluster.go b/internal/fakecluster/fakecluster.go index 512be34..35a2a7b 100644 --- a/internal/fakecluster/fakecluster.go +++ b/internal/fakecluster/fakecluster.go @@ -15,10 +15,26 @@ import ( "github.com/vipnode/vipnode/pool/store/memory" ) +type clusterHost struct { + *host.Host + Node *fakenode.FakeNode + In *jsonrpc2.Remote + Out *jsonrpc2.Remote + Key *ecdsa.PrivateKey +} + +type clusterClient struct { + *client.Client + Node *fakenode.FakeNode + In *jsonrpc2.Remote + Out *jsonrpc2.Remote + Key *ecdsa.PrivateKey +} + // Cluster represents a set of active hosts and clients connected to a pool. type Cluster struct { - clients []*client.Client - hosts []*host.Host + Clients []clusterClient + Hosts []clusterHost Pool *pool.VipnodePool pipes []io.Closer @@ -27,8 +43,8 @@ type Cluster struct { // New returns a pre-connected pool of hosts and clients. func New(hostKeys []*ecdsa.PrivateKey, clientKeys []*ecdsa.PrivateKey) (*Cluster, error) { cluster := &Cluster{ - hosts: []*host.Host{}, - clients: []*client.Client{}, + Hosts: []clusterHost{}, + Clients: []clusterClient{}, pipes: []io.Closer{}, } @@ -55,7 +71,13 @@ func New(hostKeys []*ecdsa.PrivateKey, clientKeys []*ecdsa.PrivateKey) (*Cluster return nil, err } - cluster.hosts = append(cluster.hosts, h) + cluster.Hosts = append(cluster.Hosts, clusterHost{ + Host: h, + Node: hostNode, + In: rpcPool2Host, + Out: rpcHost2Pool, + Key: hostKey, + }) } for _, clientKey := range clientKeys { @@ -70,7 +92,13 @@ func New(hostKeys []*ecdsa.PrivateKey, clientKeys []*ecdsa.PrivateKey) (*Cluster if err := c.Start(clientPool); err != nil { return nil, err } - cluster.clients = append(cluster.clients, c) + cluster.Clients = append(cluster.Clients, clusterClient{ + Client: c, + Node: clientNode, + In: rpcPool2Client, + Out: rpcClient2Pool, + Key: clientKey, + }) } return cluster, nil } @@ -83,18 +111,18 @@ func (c *Cluster) Close() error { errors = append(errors, err) } } - for _, host := range c.hosts { + for _, host := range c.Hosts { host.Stop() } - for _, client := range c.clients { + for _, client := range c.Clients { client.Stop() } - for _, host := range c.hosts { + for _, host := range c.Hosts { if err := host.Wait(); err != nil { errors = append(errors, err) } } - for _, client := range c.clients { + for _, client := range c.Clients { if err := client.Wait(); err != nil { errors = append(errors, err) } diff --git a/poolhostclient_test.go b/poolhostclient_test.go index ffa1996..b456351 100644 --- a/poolhostclient_test.go +++ b/poolhostclient_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "crypto/ecdsa" "fmt" "io" @@ -169,11 +170,9 @@ func TestPoolHostConnectPeers(t *testing.T) { t.Fatal(err) } - /* - if got, want := len(cluster.Hosts), 4; got != want { - t.Errorf("wrong number of hosts: got %d; want %d", got, want) - } - */ + if got, want := len(cluster.Hosts), 4; got != want { + t.Errorf("wrong number of hosts: got %d; want %d", got, want) + } stats, err := cluster.Pool.Store.Stats() if err != nil { @@ -184,9 +183,51 @@ func TestPoolHostConnectPeers(t *testing.T) { t.Errorf("wrong stats: %+v", stats) } - // XXX: Test host.ConnectPeers - //host := cluster.Hosts[0] - //host.ConnectPeers(cluster.Pool, 3) + numHosts := len(hostKeys) + { + // Connect a host to all the peers (numHosts = peers + 1, because it includes self) + host := cluster.Hosts[0] + if peers, err := host.Node.Peers(context.Background()); err != nil { + t.Fatal(err) + } else if len(peers) > 0 { + t.Errorf("host has unexpected peers: %s", peers) + } + hostPool := pool.Remote(host.Out, host.Key) + + if err := host.ConnectPeers(hostPool, numHosts); err != nil { + t.Fatal(err) + } + + if peers, err := host.Node.Peers(context.Background()); err != nil { + t.Fatal(err) + } else if len(peers) != numHosts-1 { + t.Errorf("host has wrong number of peers: %s", peers) + } + + numCalls := 0 + for _, call := range host.Node.Calls { + if call.Method != "ConnectPeer" { + t.Errorf("unexpected call to host: %s", call) + continue + } + numCalls++ + } + if got, want := numCalls, numHosts-1; got != want { + t.Errorf("wrong number of ConnectPeer calls: got %d; want %d", got, want) + } + } + + { + // Check a peer + host := cluster.Hosts[1] + peer := cluster.Hosts[0] + want := fakenode.Calls{fakenode.Call("AddTrustedPeer", peer.Node.NodeID)} + if got := host.Node.Calls; !reflect.DeepEqual(got, want) { + t.Errorf("peer host calls do not match:\n got: %s;\n want: %s", got, want) + } + // We can't check the Peers of the host here because it's a FakeNode so + // it does not actually initiate a connection on Connect. + } err = cluster.Close() if err != nil { From 6b530c7f95df8364e61f89c93def8169c31e4ec2 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Tue, 7 May 2019 16:28:17 -0400 Subject: [PATCH 07/43] jsonrpc: Fix vet complaints --- jsonrpc2/types.go | 11 +++++++++++ jsonrpc2/types_test.go | 15 +++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 jsonrpc2/types_test.go diff --git a/jsonrpc2/types.go b/jsonrpc2/types.go index 4985684..6a71722 100644 --- a/jsonrpc2/types.go +++ b/jsonrpc2/types.go @@ -2,6 +2,7 @@ package jsonrpc2 import ( "encoding/json" + "fmt" ) const Version = "2.0" @@ -24,6 +25,16 @@ type Message struct { Version string `json:"jsonrpc"` // TODO: Replace this with a null-type that encodes to 2.0, like https://go-review.googlesource.com/c/tools/+/136675/1/internal/jsonrpc2/jsonrpc2.go#221 } +func (m Message) String() string { + // This method is here to satisfy vet + b, err := json.Marshal(m) + if err != nil { + // This shouldn't happen. Might even be worth panic'ing? + return fmt.Sprintf("failed to marshal %T: %s", m, err) + } + return string(b) +} + type Request struct { Method string `json:"method"` Params json.RawMessage `json:"params,omitempty"` diff --git a/jsonrpc2/types_test.go b/jsonrpc2/types_test.go new file mode 100644 index 0000000..139b2c6 --- /dev/null +++ b/jsonrpc2/types_test.go @@ -0,0 +1,15 @@ +package jsonrpc2 + +import "testing" + +func TestMessageFormat(t *testing.T) { + msg := &Message{ + ID: []byte("42"), + Version: "2.0", + } + + got, want := msg.String(), `{"id":42,"jsonrpc":"2.0"}` + if got != want { + t.Errorf("wrong message string formatting:\n got: %s;\n want: %s", got, want) + } +} From 702e64b9303229caad2a8c98a3cdac6e8871968a Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Mon, 6 May 2019 12:10:29 -0400 Subject: [PATCH 08/43] ethnode: Add Caps, Protocols, Network to PeerInfo --- ethnode/rpc.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ethnode/rpc.go b/ethnode/rpc.go index e47d8f4..cecd186 100644 --- a/ethnode/rpc.go +++ b/ethnode/rpc.go @@ -2,6 +2,7 @@ package ethnode import ( "context" + "encoding/json" "strconv" "strings" @@ -134,8 +135,14 @@ func DetectClient(client *rpc.Client) (*UserAgent, error) { // PeerInfo stores the node ID and client metadata about a peer. type PeerInfo struct { - ID string `json:"id"` // Unique node identifier (also the encryption pubkey) - Name string `json:"name"` // Name of the node, including client type, version, OS, custom data + ID string `json:"id"` // Unique node identifier (also the encryption pubkey) + Name string `json:"name"` // Name of the node, including client type, version, OS, custom data + Caps []string `json:"caps"` // Capabilities the node is advertising. + Protocols map[string]json.RawMessage `json:"protocols"` // Sub-protocol specific metadata fields + Network struct { + LocalAddress string `json:"localAddress"` // Local endpoint of the TCP data connection + RemoteAddress string `json:"remoteAddress"` // Remote endpoint of the TCP data connection + } `json:"network"` } // EthNode is the normalized interface between different kinds of nodes. From ce69982f5022f5535ef10cc1b8a5419ae81cb5e2 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Mon, 6 May 2019 12:10:40 -0400 Subject: [PATCH 09/43] ethnode: Parity note and initial parsing test --- ethnode/parity.go | 2 ++ ethnode/parity_test.go | 75 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 ethnode/parity_test.go diff --git a/ethnode/parity.go b/ethnode/parity.go index 10106f3..2306017 100644 --- a/ethnode/parity.go +++ b/ethnode/parity.go @@ -55,6 +55,8 @@ func (n *parityNode) Peers(ctx context.Context) ([]PeerInfo, error) { if err != nil { return nil, err } + // FIXME: Only return connected peers who completed the handshake? In that + // case, need to filter by non-empty Protocols return result.Peers, nil } diff --git a/ethnode/parity_test.go b/ethnode/parity_test.go new file mode 100644 index 0000000..334e92e --- /dev/null +++ b/ethnode/parity_test.go @@ -0,0 +1,75 @@ +package ethnode + +import ( + "encoding/json" + "reflect" + "testing" +) + +func TestParityParsePeerInfo(t *testing.T) { + // Some examples of parity_netPeers output + testcases := []struct { + input []byte + want parityPeers + }{ + { + input: []byte(`{ + "active":0, + "connected":1, + "max":25, + "peers":[ + { + "caps":[ + "eth/62", + "eth/63", + "par/1", + "par/2", + "pip/1" + ], + "id":"a117e71696a740f2d0e5427fed5fa0ab1f343799aa873b46c361d405e9b3319689b26867e9b76921b0714ad79470d374ae4ef15f28ad4fe521de4c4f3ce702bc", + "name":"Parity/v1.8.0-beta-9882902-20171015/x86_64-linux-gnu/rustc1.21.0", + "network":{ + "localAddress":"172.18.0.2:41980", + "remoteAddress":"172.18.0.3:30303" + }, + "protocols":{ + "eth":{}, + "pip":{} + } + } + ] + }`), + want: parityPeers{ + Peers: []PeerInfo{ + { + ID: "a117e71696a740f2d0e5427fed5fa0ab1f343799aa873b46c361d405e9b3319689b26867e9b76921b0714ad79470d374ae4ef15f28ad4fe521de4c4f3ce702bc", + Name: "Parity/v1.8.0-beta-9882902-20171015/x86_64-linux-gnu/rustc1.21.0", + Caps: []string{"eth/62", "eth/63", "par/1", "par/2", "pip/1"}, + Network: struct { + LocalAddress string `json:"localAddress"` + RemoteAddress string `json:"remoteAddress"` + }{"172.18.0.2:41980", "172.18.0.3:30303"}, + Protocols: map[string]json.RawMessage{ + "eth": json.RawMessage("{}"), + "pip": json.RawMessage("{}"), + }, + }, + }, + }, + }, + } + + for i, tc := range testcases { + var result parityPeers + err := json.Unmarshal(tc.input, &result) + if err != nil { + t.Errorf("[case %d] unexpected error for testcase: %s", i, err) + continue + } + // Clear protocol values for comparison + if !reflect.DeepEqual(result, tc.want) { + t.Errorf("[case %d] wrong agent values:\n got: %+v;\n want: %+v", i, result, tc.want) + } + } + +} From 134b223dda9e166eb958bfe97e66f414e7a724f2 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Tue, 7 May 2019 10:22:23 -0400 Subject: [PATCH 10/43] ethnode/parity: Filter out inactive peers --- ethnode/parity.go | 19 ++++++++++++++++--- ethnode/parity_test.go | 32 +++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/ethnode/parity.go b/ethnode/parity.go index 2306017..fe8458e 100644 --- a/ethnode/parity.go +++ b/ethnode/parity.go @@ -55,9 +55,7 @@ func (n *parityNode) Peers(ctx context.Context) ([]PeerInfo, error) { if err != nil { return nil, err } - // FIXME: Only return connected peers who completed the handshake? In that - // case, need to filter by non-empty Protocols - return result.Peers, nil + return filterActivePeers(result.Peers), nil } func (n *parityNode) Enode(ctx context.Context) (string, error) { @@ -75,3 +73,18 @@ func (n *parityNode) BlockNumber(ctx context.Context) (uint64, error) { } return strconv.ParseUint(result, 0, 64) } + +// filterActivePeers filters out any peers that have not completed the +// handshake yet. In Parity, these are peers without any specified Protocols. +func filterActivePeers(peers []PeerInfo) []PeerInfo { + if len(peers) == 0 { + return peers + } + activePeers := make([]PeerInfo, 0, len(peers)) + for _, peer := range peers { + if len(peer.Protocols) > 0 { + activePeers = append(activePeers, peer) + } + } + return activePeers +} diff --git a/ethnode/parity_test.go b/ethnode/parity_test.go index 334e92e..5b965cd 100644 --- a/ethnode/parity_test.go +++ b/ethnode/parity_test.go @@ -57,6 +57,36 @@ func TestParityParsePeerInfo(t *testing.T) { }, }, }, + { + input: []byte(`{ + "peers":[ + { + "caps":["foo"], + "id":"someid", + "name":"somename", + "protocols":{} + }, + { + "caps":["bar"], + "id":"anotherid", + "name":"anothername", + "protocols":{"bar": "baz"} + } + ] + }`), + want: parityPeers{ + Peers: []PeerInfo{ + { + ID: "anotherid", + Name: "anothername", + Caps: []string{"bar"}, + Protocols: map[string]json.RawMessage{ + "bar": json.RawMessage(`"baz"`), + }, + }, + }, + }, + }, } for i, tc := range testcases { @@ -66,7 +96,7 @@ func TestParityParsePeerInfo(t *testing.T) { t.Errorf("[case %d] unexpected error for testcase: %s", i, err) continue } - // Clear protocol values for comparison + result.Peers = filterActivePeers(result.Peers) if !reflect.DeepEqual(result, tc.want) { t.Errorf("[case %d] wrong agent values:\n got: %+v;\n want: %+v", i, result, tc.want) } From 741ffebf9e3bfd74648ac4e0b698c7d85408568f Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Tue, 7 May 2019 11:05:15 -0400 Subject: [PATCH 11/43] internal/fakenode: Include caps in PeerInfo --- internal/fakenode/fakenode.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/fakenode/fakenode.go b/internal/fakenode/fakenode.go index d329bff..740ecfd 100644 --- a/internal/fakenode/fakenode.go +++ b/internal/fakenode/fakenode.go @@ -2,6 +2,7 @@ package fakenode import ( "context" + "encoding/json" "fmt" "net/url" @@ -59,7 +60,11 @@ func (n *FakeNode) ConnectPeer(ctx context.Context, nodeURI string) error { return err } n.FakePeers = append(n.FakePeers, ethnode.PeerInfo{ - ID: uri.User.Username(), + ID: uri.User.Username(), + Caps: []string{"fake/1", "eth/62", "eth/63", "les/2"}, + Protocols: map[string]json.RawMessage{ + "fake": json.RawMessage("{}"), + }, }) return nil } From fb73032bf9cce8b297bcada75b038a8465926739 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Wed, 8 May 2019 15:46:59 -0400 Subject: [PATCH 12/43] ethnode: Add ParseNodeKind(string) NodeKind --- ethnode/rpc.go | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/ethnode/rpc.go b/ethnode/rpc.go index cecd186..581b084 100644 --- a/ethnode/rpc.go +++ b/ethnode/rpc.go @@ -19,6 +19,17 @@ const ( Parity ) +func ParseNodeKind(s string) NodeKind { + switch strings.ToLower(s) { + case "geth": + return Geth + case "parity": + return Parity + default: + return Unknown + } +} + type NetworkID int const ( @@ -63,13 +74,13 @@ func (n NodeKind) String() string { // UserAgent is the metadata about node client. type UserAgent struct { - Version string // Result of web3_clientVersion - EthProtocol string // Result of eth_protocolVersion + Version string `json:"version"` // Result of web3_clientVersion + EthProtocol string `json:"eth_protocol"` // Result of eth_protocolVersion // Parsed/derived values - Kind NodeKind // Node implementation - Network NetworkID // Network ID - IsFullNode bool // Is this a full node? (or a light client?) + Kind NodeKind `json:"kind"` // Node implementation + Network NetworkID `json:"network"` // Network ID + IsFullNode bool `json:"is_full_node"` // Is this a full node? (or a light client?) } // ParseUserAgent takes string values as output from the web3 RPC for @@ -139,7 +150,9 @@ type PeerInfo struct { Name string `json:"name"` // Name of the node, including client type, version, OS, custom data Caps []string `json:"caps"` // Capabilities the node is advertising. Protocols map[string]json.RawMessage `json:"protocols"` // Sub-protocol specific metadata fields - Network struct { + + // FIXME: Do we want to include node-local network address state? Or is it unnecessary security leakage? + Network struct { LocalAddress string `json:"localAddress"` // Local endpoint of the TCP data connection RemoteAddress string `json:"remoteAddress"` // Remote endpoint of the TCP data connection } `json:"network"` From edd20fc4793058b90d893f058c98b05f4796ba90 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Wed, 8 May 2019 15:47:37 -0400 Subject: [PATCH 13/43] pool: Add Pool.Connect(...) and deprecate pool.Client(...), pool.Host(...) --- pool/pool.go | 67 +++++++++++++++++++--- pool/remote.go | 20 +++++++ pool/service.go | 140 +++++++++++++++++++++++++++++++-------------- pool/staticpool.go | 4 ++ 4 files changed, 182 insertions(+), 49 deletions(-) diff --git a/pool/pool.go b/pool/pool.go index a7a5754..5dbcbcb 100644 --- a/pool/pool.go +++ b/pool/pool.go @@ -3,16 +3,57 @@ package pool import ( "context" + "github.com/vipnode/vipnode/ethnode" "github.com/vipnode/vipnode/pool/store" ) // TODO: Add HostRequest.Network and ClientRequest.Network? // TODO: Add HostRequest.HostVersion? +// ConnectRequest is a base request done when a vipnode agent connects to a pool. +// It is common between hosts and clients +type ConnectRequest struct { + // VipnodeVersion is the version string of the vipnode agent + VipnodeVersion string `json:"vipnode_version"` + + // XXX: Add Protocols/Capabilities + + // NodeInfo is the metadata of the Ethereum node, includes node kind. + NodeInfo ethnode.UserAgent `json:"node_info"` + + // NodeURI is an optional public node URI override, useful if the vipnode + // agent runs on a separate IP from the actual node host. Otherwise, the + // pool will automatically use the same IP and default port as the host + // connecting. + NodeURI string `json:"node_uri,omitempty"` + + // NumHosts is the number of hosts to request from the pool. (Optional) + NumHosts int `json:"num_hosts,omitempty"` + + // Payout sets the wallet account to register the host credit towards. (Optional) + Payout string `json:"payout"` +} + +// ConnectResponse is the response a vipnode agent receives from the pool after +// the first connection request. It is common between hosts and clients. +type ConnectResponse struct { + // PoolVersion is the version of vipnode-pool that is running. + PoolVersion string `json:"pool_version"` + // Hosts that have whitelisted the NodeID and are ready for the node to + // connect to. + Hosts []store.Node `json:"hosts,omitempty"` + // Message contains a prompt for the client from the pool, possibly + // instructions for interfacing with this pool. For example, a link to the + // DApp for adding a balance deposit. + Message string `json:"message,omitempty"` +} + // HostRequest is the request type for Host RPC calls. +// DEPRECATED: Use ConnectRequest/ConnectResponse type HostRequest struct { // Kind is the type of node the host supports: geth, parity - Kind string `json:"kind"` + Kind string `json:"kind,omitempty"` + // Payout sets the wallet account to register the host credit towards. Payout string `json:"payout"` // Optional public node URI override, useful if the vipnode agent runs on a @@ -22,20 +63,26 @@ type HostRequest struct { } // HostResponse is the response type for Host RPC calls. +// DEPRECATED: Use ConnectRequest/ConnectResponse type HostResponse struct { PoolVersion string `json:"pool_version"` } // ClientRequest is the request type for Client RPC calls. +// DEPRECATED: Use ConnectRequest/ConnectResponse type ClientRequest struct { - Kind string `json:"kind"` - NumHosts int `json:"num_hosts,omitempty"` // NumHosts is the number of hosts to request from the pool. (Optional) + // Kind is the type of node the host supports: geth, parity + Kind string `json:"kind,omitempty"` + + // NumHosts is the number of hosts to request from the pool. (Optional) + NumHosts int `json:"num_hosts,omitempty"` } // ClientResponse is the response type for Client RPC calls. +// DEPRECATED: Use ConnectRequest/ConnectResponse type ClientResponse struct { - // Hosts that have whitelisted the client NodeID and are ready for the - // client to connect. + // Hosts that have whitelisted the NodeID and are ready for the node to + // connect to. Hosts []store.Node `json:"hosts"` // PoolVersion is the version of vipnode-pool that is running. PoolVersion string `json:"pool_version"` @@ -47,8 +94,9 @@ type ClientResponse struct { // UpdateRequest is the request type for Update RPC calls. type UpdateRequest struct { - Peers []string `json:"peers"` - BlockNumber uint64 `json:"block_number"` + Peers []string `json:"peers"` // DEPRECATED + PeerInfo []ethnode.PeerInfo `json:"peers_info"` + BlockNumber uint64 `json:"block_number"` } // UpdateResponse is the response type for Update RPC calls. @@ -60,11 +108,16 @@ type UpdateResponse struct { // Pool represents a vipnode pool for coordinating between clients and hosts. type Pool interface { // Host subscribes a host to receive vipnode_whitelist instructions. + // DEREPCATED: Use Connect Host(ctx context.Context, req HostRequest) (*HostResponse, error) // Client requests for available hosts to connect to as a client. + // DEREPCATED: Use Connect Client(ctx context.Context, req ClientRequest) (*ClientResponse, error) + // Host subscribes a host to receive vipnode_whitelist instructions. + Connect(ctx context.Context, req ConnectRequest) (*ConnectResponse, error) + // Disconnect stops tracking the connection and billing, will prompt a // disconnect from both ends. Disconnect(ctx context.Context) error diff --git a/pool/remote.go b/pool/remote.go index b300205..b07ecc7 100644 --- a/pool/remote.go +++ b/pool/remote.go @@ -73,6 +73,26 @@ func (p *RemotePool) Client(ctx context.Context, req ClientRequest) (*ClientResp return &resp, nil } +func (p *RemotePool) Connect(ctx context.Context, req ConnectRequest) (*ConnectResponse, error) { + signedReq := request.NodeRequest{ + Method: "vipnode_connect", + NodeID: p.nodeID, + Nonce: p.getNonce(), + ExtraArgs: []interface{}{req}, + } + + args, err := signedReq.SignedArgs(p.privkey) + if err != nil { + return nil, err + } + var resp ConnectResponse + if err := p.client.Call(ctx, &resp, signedReq.Method, args...); err != nil { + return nil, err + } + + return &resp, nil +} + func (p *RemotePool) Disconnect(ctx context.Context) error { signedReq := request.NodeRequest{ Method: "vipnode_disconnect", diff --git a/pool/service.go b/pool/service.go index 3c4210a..acc2edf 100644 --- a/pool/service.go +++ b/pool/service.go @@ -7,6 +7,7 @@ import ( "sync" "time" + "github.com/vipnode/vipnode/ethnode" "github.com/vipnode/vipnode/internal/pretty" "github.com/vipnode/vipnode/jsonrpc2" "github.com/vipnode/vipnode/pool/balance" @@ -46,7 +47,8 @@ type VipnodePool struct { Store store.Store BalanceManager balance.Manager ClientMessager func(nodeID string) string - MaxRequestHosts int // MaxRequestHosts is the maximum number of hosts a client is allowed to request (0 is unlimited) + MaxRequestHosts int // MaxRequestHosts is the maximum number of hosts a client is allowed to request (0 is unlimited) + RestrictNetwork ethnode.NetworkID // TODO: Wire this up skipWhitelist bool // skipWhitelist is used for testing. @@ -157,69 +159,87 @@ func (p *VipnodePool) Update(ctx context.Context, sig string, nodeID string, non } // Host registers a full node to participate as a vipnode host in this pool. +// DEPRECATED: Use Connect func (p *VipnodePool) Host(ctx context.Context, sig string, nodeID string, nonce int64, req HostRequest) (*HostResponse, error) { - // TODO: Send capabilities? + // This is a backport of Host using Connect behind the scenes. if err := p.verify(sig, "vipnode_host", nodeID, nonce, req); err != nil { return nil, err } - service, err := jsonrpc2.CtxService(ctx) + connectReq := ConnectRequest{ + NodeInfo: ethnode.UserAgent{ + Kind: ethnode.ParseNodeKind(req.Kind), + IsFullNode: true, + }, + Payout: req.Payout, + NodeURI: req.NodeURI, + } + connectResp, err := p.connect(ctx, nodeID, connectReq) if err != nil { return nil, err } - remoteHost := "" - if withAddr, ok := service.(interface{ RemoteAddr() string }); ok { - remoteHost = (&url.URL{Host: withAddr.RemoteAddr()}).Hostname() + resp := &HostResponse{ + PoolVersion: connectResp.PoolVersion, } - defaultPort := "30303" - nodeURI, err := normalizeNodeURI(req.NodeURI, nodeID, remoteHost, defaultPort) - if err != nil { + return resp, nil +} + +// Client returns a list of enodes who are ready for the client node to connect. +// DEPRECATED: Use Connect +func (p *VipnodePool) Client(ctx context.Context, sig string, nodeID string, nonce int64, req ClientRequest) (*ClientResponse, error) { + // This is a backport of Client using Connect behind the scenes. + if err := p.verify(sig, "vipnode_client", nodeID, nonce, req); err != nil { return nil, err } - - // TODO: Confirm that it's a full node, not a light node? Doesn't super matter since if i - // TODO: Check versions? - - logger.Printf("New %q host: %q", req.Kind, nodeURI) - - node := store.Node{ - ID: store.NodeID(nodeID), - URI: nodeURI, - Kind: req.Kind, - LastSeen: time.Now(), - IsHost: true, - Payout: store.Account(req.Payout), + // Clients have a default number of hosts they request. Hosts don't. + numRequestHosts := defaultRequestNumHosts + if req.NumHosts > 0 { + numRequestHosts = req.NumHosts + } + connectReq := ConnectRequest{ + NumHosts: numRequestHosts, + NodeInfo: ethnode.UserAgent{ + Kind: ethnode.ParseNodeKind(req.Kind), + IsFullNode: false, + }, } - err = p.Store.SetNode(node) + connectResp, err := p.connect(ctx, nodeID, connectReq) if err != nil { return nil, err } - - // FIXME: Clean up disconnected hosts - p.mu.Lock() - p.remoteHosts[node.ID] = service - p.mu.Unlock() - - resp := &HostResponse{ - PoolVersion: p.Version, + resp := &ClientResponse{ + Hosts: connectResp.Hosts, + PoolVersion: connectResp.PoolVersion, + Message: connectResp.Message, } return resp, nil } // Client returns a list of enodes who are ready for the client node to connect. -func (p *VipnodePool) Client(ctx context.Context, sig string, nodeID string, nonce int64, req ClientRequest) (*ClientResponse, error) { - if err := p.verify(sig, "vipnode_client", nodeID, nonce, req); err != nil { +func (p *VipnodePool) Connect(ctx context.Context, sig string, nodeID string, nonce int64, req ConnectRequest) (*ConnectResponse, error) { + if err := p.verify(sig, "vipnode_connect", nodeID, nonce, req); err != nil { return nil, err } - kind := req.Kind - numRequestHosts := defaultRequestNumHosts + return p.connect(ctx, nodeID, req) +} + +// connect is same as Connect without signature verification. Used as a helper. +// TODO: We can inline connect into Connect once Client/Host are removed. +func (p *VipnodePool) connect(ctx context.Context, nodeID string, req ConnectRequest) (*ConnectResponse, error) { + kind := req.NodeInfo.Kind.String() + isHost := req.NodeInfo.IsFullNode + if p.RestrictNetwork != 0 && p.RestrictNetwork != req.NodeInfo.Network { + return nil, fmt.Errorf("node is on the wrong network, pool requires: %s", p.RestrictNetwork) + } + + numRequestHosts := req.NumHosts if p.MaxRequestHosts > 0 && numRequestHosts > p.MaxRequestHosts { numRequestHosts = p.MaxRequestHosts } - response := &ClientResponse{ + response := &ConnectResponse{ PoolVersion: p.Version, } if p.ClientMessager != nil { @@ -233,8 +253,33 @@ func (p *VipnodePool) Client(ctx context.Context, sig string, nodeID string, non ID: store.NodeID(nodeID), Kind: kind, LastSeen: time.Now(), - IsHost: false, + IsHost: isHost, + Payout: store.Account(req.Payout), + } + + if isHost { + // We only care about publicly-visible nodeURIs for hosts. + service, err := jsonrpc2.CtxService(ctx) + if err != nil { + return nil, err + } + + remoteHost := "" + if withAddr, ok := service.(interface{ RemoteAddr() string }); ok { + remoteHost = (&url.URL{Host: withAddr.RemoteAddr()}).Hostname() + } + defaultPort := "30303" + node.URI, err = normalizeNodeURI(req.NodeURI, nodeID, remoteHost, defaultPort) + if err != nil { + return nil, err + } + + // TODO: Clean up disconnected/failed host services + p.mu.Lock() + p.remoteHosts[node.ID] = service + p.mu.Unlock() } + if err := p.Store.SetNode(node); err != nil { return nil, err } @@ -243,17 +288,22 @@ func (p *VipnodePool) Client(ctx context.Context, sig string, nodeID string, non return nil, err } + if numRequestHosts == 0 { + // Nothing left to do + return response, nil + } + r, err := p.Store.ActiveHosts(kind, numRequestHosts) if err != nil { return nil, err } - if len(r) == 0 { - logger.Printf("New %q client: %q (no active hosts found)", kind, pretty.Abbrev(nodeID)) + if !isHost && len(r) == 0 { + logger.Printf("New %q peer: %q (no active hosts found)", kind, pretty.Abbrev(nodeID)) return nil, NoHostNodesError{} } if p.skipWhitelist { - logger.Printf("New %q client: %q (%d hosts found, skipping whitelist)", kind, pretty.Abbrev(nodeID), len(r)) + logger.Printf("New %q peer: %q (%d hosts found, skipping whitelist)", kind, pretty.Abbrev(nodeID), len(r)) response.Hosts = r return response, nil } @@ -302,9 +352,9 @@ func (p *VipnodePool) Client(ctx context.Context, sig string, nodeID string, non // TODO: Penalize hosts that failed to respond within the deadline? if len(errors) > 0 { - logger.Printf("New %q client: %s (%d hosts found, %d accepted) %s", kind, nodeID[:8], len(remotes), len(accepted), RemoteHostErrors{"vipnode_whitelist", errors}) + logger.Printf("New %q peer: %s (%d hosts found, %d accepted) %s", kind, nodeID[:8], len(remotes), len(accepted), RemoteHostErrors{"vipnode_whitelist", errors}) } else { - logger.Printf("New %q client: %s (%d hosts found, %d accepted)", kind, nodeID[:8], len(remotes), len(accepted)) + logger.Printf("New %q peer: %s (%d hosts found, %d accepted)", kind, nodeID[:8], len(remotes), len(accepted)) } if len(accepted) >= 1 { @@ -316,6 +366,12 @@ func (p *VipnodePool) Client(ctx context.Context, sig string, nodeID string, non return nil, RemoteHostErrors{"vipnode_whitelist", errors} } + if isHost { + // Hosts are ok without peers + return response, nil + } + + // FIXME: Should clients also be ok without peers? Just stay connected and retry later? return nil, NoHostNodesError{len(r)} } diff --git a/pool/staticpool.go b/pool/staticpool.go index cd5c328..f00bcf0 100644 --- a/pool/staticpool.go +++ b/pool/staticpool.go @@ -32,6 +32,10 @@ func (s *StaticPool) Client(ctx context.Context, req ClientRequest) (*ClientResp return &ClientResponse{Hosts: s.Nodes}, nil } +func (s *StaticPool) Connect(ctx context.Context, req ConnectRequest) (*ConnectResponse, error) { + return &ConnectResponse{Hosts: s.Nodes}, nil +} + func (s *StaticPool) Disconnect(ctx context.Context) error { return nil } From 6bce1dd34c45df1c483d316d0b2a1a286786b15b Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 9 May 2019 16:11:47 -0400 Subject: [PATCH 14/43] pool: Split out requestHosts (after host2host rebase) Also changes some logging messages and flows. --- pool/service.go | 72 +++++++++++++++++++++++++++--------------- poolhostclient_test.go | 2 +- 2 files changed, 47 insertions(+), 27 deletions(-) diff --git a/pool/service.go b/pool/service.go index acc2edf..ea3f4e6 100644 --- a/pool/service.go +++ b/pool/service.go @@ -229,16 +229,15 @@ func (p *VipnodePool) Connect(ctx context.Context, sig string, nodeID string, no // TODO: We can inline connect into Connect once Client/Host are removed. func (p *VipnodePool) connect(ctx context.Context, nodeID string, req ConnectRequest) (*ConnectResponse, error) { kind := req.NodeInfo.Kind.String() + if kind == "unknown" { + kind = "" + } + isHost := req.NodeInfo.IsFullNode if p.RestrictNetwork != 0 && p.RestrictNetwork != req.NodeInfo.Network { return nil, fmt.Errorf("node is on the wrong network, pool requires: %s", p.RestrictNetwork) } - numRequestHosts := req.NumHosts - if p.MaxRequestHosts > 0 && numRequestHosts > p.MaxRequestHosts { - numRequestHosts = p.MaxRequestHosts - } - response := &ConnectResponse{ PoolVersion: p.Version, } @@ -288,24 +287,51 @@ func (p *VipnodePool) connect(ctx context.Context, nodeID string, req ConnectReq return nil, err } - if numRequestHosts == 0 { - // Nothing left to do - return response, nil + reqKind := kind + if isHost { + // Any kind of host peer will do. + reqKind = "" } - r, err := p.Store.ActiveHosts(kind, numRequestHosts) + hosts, err := p.requestHosts(ctx, nodeID, req.NumHosts, reqKind) if err != nil { return nil, err } - if !isHost && len(r) == 0 { + + if !isHost && len(hosts) == 0 { logger.Printf("New %q peer: %q (no active hosts found)", kind, pretty.Abbrev(nodeID)) return nil, NoHostNodesError{} } + response.Hosts = hosts + if p.skipWhitelist { + logger.Printf("New %q peer: %q (%d hosts found, skipping whitelist)", kind, pretty.Abbrev(nodeID), len(hosts)) + } else { + logger.Printf("New %q peer: %q (%d hosts found)", kind, pretty.Abbrev(nodeID), len(hosts)) + } + + return response, nil +} + +func (p *VipnodePool) requestHosts(ctx context.Context, nodeID string, numRequestHosts int, kind string) ([]store.Node, error) { + if p.MaxRequestHosts > 0 && numRequestHosts > p.MaxRequestHosts { + numRequestHosts = p.MaxRequestHosts + } + + var hosts []store.Node + if numRequestHosts == 0 { + // Nothing left to do + return hosts, nil + } + + r, err := p.Store.ActiveHosts(kind, numRequestHosts) + if err != nil { + return nil, err + } + if p.skipWhitelist { - logger.Printf("New %q peer: %q (%d hosts found, skipping whitelist)", kind, pretty.Abbrev(nodeID), len(r)) - response.Hosts = r - return response, nil + // Bypass whitelisting, used for making testing simpler + return r, nil } errors := []error{} @@ -352,26 +378,20 @@ func (p *VipnodePool) connect(ctx context.Context, nodeID string, req ConnectReq // TODO: Penalize hosts that failed to respond within the deadline? if len(errors) > 0 { - logger.Printf("New %q peer: %s (%d hosts found, %d accepted) %s", kind, nodeID[:8], len(remotes), len(accepted), RemoteHostErrors{"vipnode_whitelist", errors}) - } else { - logger.Printf("New %q peer: %s (%d hosts found, %d accepted)", kind, nodeID[:8], len(remotes), len(accepted)) + err = RemoteHostErrors{"vipnode_whitelist", errors} } - if len(accepted) >= 1 { - response.Hosts = accepted - return response, nil - } + logger.Printf("Request hosts from %q: kind=%q (%d hosts found, %d accepted), failures=%s", pretty.Abbrev(nodeID), kind, len(remotes), len(accepted), err) - if len(errors) > 0 { - return nil, RemoteHostErrors{"vipnode_whitelist", errors} + if len(accepted) >= 1 { + // We're okay returning without an error as long as some hosts succeeded. + return accepted, nil } - if isHost { - // Hosts are ok without peers - return response, nil + if err != nil { + return nil, err } - // FIXME: Should clients also be ok without peers? Just stay connected and retry later? return nil, NoHostNodesError{len(r)} } diff --git a/poolhostclient_test.go b/poolhostclient_test.go index b456351..d92bb73 100644 --- a/poolhostclient_test.go +++ b/poolhostclient_test.go @@ -195,7 +195,7 @@ func TestPoolHostConnectPeers(t *testing.T) { hostPool := pool.Remote(host.Out, host.Key) if err := host.ConnectPeers(hostPool, numHosts); err != nil { - t.Fatal(err) + t.Error(err) } if peers, err := host.Node.Peers(context.Background()); err != nil { From 91452adc9fc276d6bc6e9e6071624d0e7aa3a2f5 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 9 May 2019 17:17:29 -0400 Subject: [PATCH 15/43] pool: Doc fix --- pool/pool.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pool/pool.go b/pool/pool.go index 5dbcbcb..8db368a 100644 --- a/pool/pool.go +++ b/pool/pool.go @@ -115,7 +115,7 @@ type Pool interface { // DEREPCATED: Use Connect Client(ctx context.Context, req ClientRequest) (*ClientResponse, error) - // Host subscribes a host to receive vipnode_whitelist instructions. + // Connect subscribes to the active nodes set. Connect(ctx context.Context, req ConnectRequest) (*ConnectResponse, error) // Disconnect stops tracking the connection and billing, will prompt a From c63ed574eef004defbf47bc3932786e37f77272e Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Fri, 10 May 2019 15:31:13 -0400 Subject: [PATCH 16/43] pool, host, client: Use UpdateRequest.PeerInfo instead of Peers --- client/client.go | 11 +++-------- host/host.go | 10 +++------- pool/pool.go | 4 +++- pool/service.go | 42 ++++++++++++++++++++++++++++++++++++++---- 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/client/client.go b/client/client.go index 5569ce5..0503e0e 100644 --- a/client/client.go +++ b/client/client.go @@ -106,12 +106,7 @@ func (c *Client) updatePeers(ctx context.Context, p pool.Pool) error { if err != nil { return err } - peerIDs := make([]string, 0, len(peers)) - for _, p := range peers { - peerIDs = append(peerIDs, p.ID) - } - - update, err := p.Update(ctx, pool.UpdateRequest{Peers: peerIDs}) + update, err := p.Update(ctx, pool.UpdateRequest{PeerInfo: peers}) if err != nil { return err } @@ -124,9 +119,9 @@ func (c *Client) updatePeers(ctx context.Context, p pool.Pool) error { // tracking their host. That means the client is getting a free ride // and it's up to the host to kick the client when the host deems // necessary. - logger.Printf("Sent update: %d peers connected, %d expired in pool. Pool response: %s", len(peerIDs), len(update.InvalidPeers), update.Balance.String()) + logger.Printf("Sent update: %d peers connected, %d expired in pool. Pool response: %s", len(peers), len(update.InvalidPeers), update.Balance.String()) } else { - logger.Printf("Sent update: %d peers connected. Pool response: %s", len(peerIDs), update.Balance.String()) + logger.Printf("Sent update: %d peers connected. Pool response: %s", len(peers), update.Balance.String()) } return nil diff --git a/host/host.go b/host/host.go index 76d151a..fb55091 100644 --- a/host/host.go +++ b/host/host.go @@ -72,22 +72,18 @@ func (h *Host) updatePeers(ctx context.Context, p pool.Pool) error { if err != nil { return err } - peerUpdate := make([]string, 0, len(peers)) - for _, peer := range peers { - peerUpdate = append(peerUpdate, peer.ID) - } update, err := p.Update(ctx, pool.UpdateRequest{ - Peers: peerUpdate, + PeerInfo: peers, BlockNumber: block, }) if err != nil { return err } if len(update.InvalidPeers) == 0 { - logger.Printf("Sent update: %d peers. Pool response: %s", len(peerUpdate), update.Balance.String()) + logger.Printf("Sent update: %d peers. Pool response: %s", len(peers), update.Balance.String()) return nil } - logger.Printf("Sent update: %d peers. Pool response: Disconnect from %d invalid peers, %s", len(peerUpdate), len(update.InvalidPeers), update.Balance.String()) + logger.Printf("Sent update: %d peers. Pool response: Disconnect from %d invalid peers, %s", len(peers), len(update.InvalidPeers), update.Balance.String()) for _, peerID := range update.InvalidPeers { // FIXME: Are there recoverable errors here? if err := h.node.RemoveTrustedPeer(ctx, peerID); err != nil { diff --git a/pool/pool.go b/pool/pool.go index 8db368a..4d91e8d 100644 --- a/pool/pool.go +++ b/pool/pool.go @@ -94,7 +94,7 @@ type ClientResponse struct { // UpdateRequest is the request type for Update RPC calls. type UpdateRequest struct { - Peers []string `json:"peers"` // DEPRECATED + Peers []string `json:"peers,omitempty"` // DEPRECATED PeerInfo []ethnode.PeerInfo `json:"peers_info"` BlockNumber uint64 `json:"block_number"` } @@ -127,6 +127,8 @@ type Pool interface { // balance for the node (if relevant). Update(ctx context.Context, req UpdateRequest) (*UpdateResponse, error) + // TODO: RequestHosts(ctx context.Context, req RequestHostsRequest) (*RequestHostsRequest, error) + // Withdraw prompts a request to settle the node's balance. Withdraw(ctx context.Context) error } diff --git a/pool/service.go b/pool/service.go index ea3f4e6..064e847 100644 --- a/pool/service.go +++ b/pool/service.go @@ -115,8 +115,17 @@ func (p *VipnodePool) Update(ctx context.Context, sig string, nodeID string, non } nodeBeforeUpdate := *node - peers := req.Peers - inactive, err := p.Store.UpdateNodePeers(store.NodeID(nodeID), peers, req.BlockNumber) + peerTypes := segmentPeers(req.PeerInfo) + if peerTypes.Count == 0 { + // DEPRECATED: Remove this backport for req.Peers once deprecation is complete. + peerTypes.Clients = req.Peers + peerTypes.Count = len(req.Peers) + } + + // FIXME: We only consider peers who are light clients, and effectively + // ignore host peers. It may be useful to take hosts into account in the + // future too. + inactive, err := p.Store.UpdateNodePeers(store.NodeID(nodeID), peerTypes.Clients, req.BlockNumber) if err != nil { return nil, err } @@ -150,9 +159,9 @@ func (p *VipnodePool) Update(ctx context.Context, sig string, nodeID string, non resp.Balance = &nodeBalance if node.IsHost { - logger.Printf("Host update %q: %d peers, %d active, %d invalid. %s", pretty.Abbrev(nodeID), len(peers), len(validPeers), len(inactive), nodeBalance.String()) + logger.Printf("Host update %q: %d peers, %d active, %d invalid. %s", pretty.Abbrev(nodeID), peerTypes.Count, len(validPeers), len(inactive), nodeBalance.String()) } else { - logger.Printf("Client update %q: %d peers, %d active, %d invalid. %s", pretty.Abbrev(nodeID), len(peers), len(validPeers), len(inactive), nodeBalance.String()) + logger.Printf("Client update %q: %d peers, %d active, %d invalid. %s", pretty.Abbrev(nodeID), peerTypes.Count, len(validPeers), len(inactive), nodeBalance.String()) } return &resp, nil @@ -399,3 +408,28 @@ func (p *VipnodePool) requestHosts(ctx context.Context, nodeID string, numReques func (p *VipnodePool) Ping(ctx context.Context) string { return "pong" } + +// peerTypes stores the subset of peer IDs by type, derived from the capabilities. +type peerTypes struct { + Count int + Hosts []string + Clients []string +} + +func segmentPeers(peers []ethnode.PeerInfo) peerTypes { + r := peerTypes{} + for _, p := range peers { + r.Count++ + + if _, ok := p.Protocols["eth"]; ok { + // Hosts have eth/62 or eth/63 capability (comes up as "eth" protocol) + r.Hosts = append(r.Hosts, p.ID) + } else if len(p.Protocols) > 0 { + // Anything else is probably "les" or "pip" + r.Clients = append(r.Clients, p.ID) + } + // Empty p.Protocols means the peer has not completed the handshake yet, + // so we can ignore them. + } + return r +} From d716a54275cb67c2668ba36a417328266482bbd6 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Mon, 13 May 2019 14:42:32 -0400 Subject: [PATCH 17/43] jsonrpc2: Add method whitelisting support to Server.Register --- jsonrpc2/server.go | 24 +++++++++++++++++++++--- jsonrpc2/server_test.go | 17 ++++++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/jsonrpc2/server.go b/jsonrpc2/server.go index 9a51838..29e42ee 100644 --- a/jsonrpc2/server.go +++ b/jsonrpc2/server.go @@ -17,8 +17,9 @@ var ErrNoPublicMethods = errors.New("no public methods") type Handler interface { // Handle takes a request message and returns a response message. Handle(ctx context.Context, request *Message) (response *Message) + // FIXME: Register* really shouldn't be part of this signature, right? - Register(prefix string, receiver interface{}) error + Register(prefix string, receiver interface{}, onlyMethods ...string) error RegisterMethod(rpcName string, receiver interface{}, methodName string) error } @@ -34,7 +35,7 @@ type Server struct { // Register adds valid methods from the receiver to the registry with the given // prefix. Method names are lowercased. -func (s *Server) Register(prefix string, receiver interface{}) error { +func (s *Server) Register(prefix string, receiver interface{}, onlyMethods ...string) error { s.mu.Lock() defer s.mu.Unlock() @@ -52,13 +53,30 @@ func (s *Server) Register(prefix string, receiver interface{}) error { return ErrNoPublicMethods } + var methodWhitelist map[string]struct{} + if len(onlyMethods) > 0 { + methodWhitelist = map[string]struct{}{} + for _, name := range onlyMethods { + methodWhitelist[name] = struct{}{} + } + } + var buf bytes.Buffer for name, m := range methods { + buf.Reset() buf.WriteString(prefix) buf.WriteRune(unicode.ToLower(rune(name[0]))) buf.WriteString(name[1:]) + + if methodWhitelist != nil { + // Skip methods that are not whitelisted + methodName := buf.String()[len(prefix):] + if _, ok := methodWhitelist[methodName]; !ok { + continue + } + } + s.registry[buf.String()] = m - buf.Reset() } return nil } diff --git a/jsonrpc2/server_test.go b/jsonrpc2/server_test.go index 7a5ce31..eeaa994 100644 --- a/jsonrpc2/server_test.go +++ b/jsonrpc2/server_test.go @@ -9,7 +9,7 @@ import ( func TestServer(t *testing.T) { service := &FruitService{} s := Server{} - if err := s.Register("foo_", service); err != nil { + if err := s.Register("foo_", service, "apple", "banana"); err != nil { t.Error(err) } @@ -42,4 +42,19 @@ func TestServer(t *testing.T) { if string(resp.Response.Result) != "null" { t.Errorf("unexpected result: %q", resp.Result) } + + resp = s.Handle(context.Background(), &Message{ + ID: json.RawMessage([]byte("3")), + Version: Version, + Request: &Request{ + Method: "foo_cherry", + }, + }) + if resp.Error == nil { + t.Errorf("expected error, got: %q", resp) + } + + if resp.Error.Message != "method not found: foo_cherry" { + t.Errorf("unexpected error message: %q", resp.Error) + } } From c8817be511022c29f64364e43fb175803789777d Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Mon, 13 May 2019 14:44:10 -0400 Subject: [PATCH 18/43] pool, main: Add cleanup of pool.remoteHosts on disconnect --- pool.go | 5 +++-- pool/service.go | 32 +++++++++++++++++++++++++------- server.go | 15 ++++++++++++--- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/pool.go b/pool.go index 834aea7..62f29c7 100644 --- a/pool.go +++ b/pool.go @@ -191,8 +191,9 @@ func runPool(options Options) error { } handler := &server{ - ws: &ws.Upgrader{}, - header: http.Header{}, + ws: &ws.Upgrader{}, + header: http.Header{}, + onDisconnect: p.OnDisconnect, } if options.Pool.AllowOrigin != "" { handler.header.Set("Access-Control-Allow-Origin", options.Pool.AllowOrigin) diff --git a/pool/service.go b/pool/service.go index 064e847..0d5fe9e 100644 --- a/pool/service.go +++ b/pool/service.go @@ -31,9 +31,10 @@ func New(storeDriver store.Store, manager balance.Manager) *VipnodePool { manager = balance.NoBalance{} } return &VipnodePool{ - Store: storeDriver, - BalanceManager: manager, - remoteHosts: map[store.NodeID]jsonrpc2.Service{}, + Store: storeDriver, + BalanceManager: manager, + remoteHosts: map[store.NodeID]jsonrpc2.Service{}, + remoteNodeLookup: map[jsonrpc2.Service]store.NodeID{}, } } @@ -49,11 +50,28 @@ type VipnodePool struct { ClientMessager func(nodeID string) string MaxRequestHosts int // MaxRequestHosts is the maximum number of hosts a client is allowed to request (0 is unlimited) RestrictNetwork ethnode.NetworkID // TODO: Wire this up + skipWhitelist bool // skipWhitelist is used for testing. - skipWhitelist bool // skipWhitelist is used for testing. + mu sync.Mutex + remoteHosts map[store.NodeID]jsonrpc2.Service + remoteNodeLookup map[jsonrpc2.Service]store.NodeID // Reverse lookup +} + +// OnDisconnect is to be called when a remote service is disconnected. It is used to clean up state. +func (p *VipnodePool) OnDisconnect(remote jsonrpc2.Service) error { + p.mu.Lock() + defer p.mu.Unlock() - mu sync.Mutex - remoteHosts map[store.NodeID]jsonrpc2.Service + nodeID, ok := p.remoteNodeLookup[remote] + if !ok { + // Nothing to clean up + return nil + } + + delete(p.remoteNodeLookup, remote) + delete(p.remoteHosts, nodeID) + + return nil } func (p *VipnodePool) verify(sig string, method string, nodeID string, nonce int64, args ...interface{}) error { @@ -282,9 +300,9 @@ func (p *VipnodePool) connect(ctx context.Context, nodeID string, req ConnectReq return nil, err } - // TODO: Clean up disconnected/failed host services p.mu.Lock() p.remoteHosts[node.ID] = service + p.remoteNodeLookup[service] = node.ID p.mu.Unlock() } diff --git a/server.go b/server.go index e34ba10..34ea1aa 100644 --- a/server.go +++ b/server.go @@ -14,9 +14,10 @@ type wsHandler interface { type server struct { jsonrpc2.HTTPServer - ws ws.Upgrader - debugLog bool - header http.Header + ws ws.Upgrader + debugLog bool + header http.Header + onDisconnect func(remote jsonrpc2.Service) error } func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -43,6 +44,8 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { if s.debugLog { codec = jsonrpc2.DebugCodec(r.RemoteAddr, codec) } + defer codec.Close() + remote := &jsonrpc2.Remote{ Codec: codec, Server: &s.HTTPServer.Server, @@ -54,6 +57,12 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := remote.Serve(); err != nil && err != io.EOF { logger.Warningf("jsonrpc2.Remote.Serve() error: %s", err) } + + if s.onDisconnect != nil { + if err := s.onDisconnect(remote); err != nil { + logger.Warningf("jsonrpc2.Service disconnect error: %s", err) + } + } default: http.Error(w, "unsupported method", http.StatusUnsupportedMediaType) } From e280fbe948fd63d748eac8e849f53c99ee1b7f3a Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Mon, 13 May 2019 15:06:01 -0400 Subject: [PATCH 19/43] main: Whitelist vipnode_ rpc methods just in case --- pool.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pool.go b/pool.go index 62f29c7..e4a2f2b 100644 --- a/pool.go +++ b/pool.go @@ -199,7 +199,7 @@ func runPool(options Options) error { handler.header.Set("Access-Control-Allow-Origin", options.Pool.AllowOrigin) } - if err := handler.Register("vipnode_", p); err != nil { + if err := handler.Register("vipnode_", p, "connect", "disconnect", "ping", "update", "client", "host"); err != nil { return err } From 13a542f1fc42bccc99fba3178a55ede6de6e7308 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Mon, 13 May 2019 15:06:30 -0400 Subject: [PATCH 20/43] main: Clean up logging, shutdown signal, fakehostpool simulation --- Makefile | 2 +- host.go | 19 ++++++++++--------- pool/service.go | 5 +++-- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index d84f0e6..5725ffb 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ fakehost: $(BINARY) $(RUN) -vv host --pool "ws://$(FAKEBIND)" --rpc "fakenode://f21f0692b06019ae3f40d78d8b309487fc75f75b76df71d76196c3514272adf30aca4b2451181eb22208757cd4363923e17723d2f2ddf7b0175ecb87dada7ca1?fakepeers=$(FAKEPEERS)" fakehostpool: $(BINARY) - $(RUN) -vv host --pool ":memory:" --enode="enode://f21f0692b06019ae3f40d78d8b309487fc75f75b76df71d76196c3514272adf30aca4b2451181eb22208757cd4363923e17723d2f2ddf7b0175ecb87dada7ca1@[::]:30303?discport=0" --rpc "fakenode://f21f0692b06019ae3f40d78d8b309487fc75f75b76df71d76196c3514272adf30aca4b2451181eb22208757cd4363923e17723d2f2ddf7b0175ecb87dada7ca1?fakepeers=$(FAKEPEERS)" + $(RUN) -vv host --pool ":memory:" --enode="enode://f21f0692b06019ae3f40d78d8b309487fc75f75b76df71d76196c3514272adf30aca4b2451181eb22208757cd4363923e17723d2f2ddf7b0175ecb87dada7ca1@127.0.0.1:30303?discport=0" --rpc "fakenode://f21f0692b06019ae3f40d78d8b309487fc75f75b76df71d76196c3514272adf30aca4b2451181eb22208757cd4363923e17723d2f2ddf7b0175ecb87dada7ca1?fakepeers=$(FAKEPEERS)" fakeclient: $(BINARY) $(RUN) -vv client "http://$(FAKEBIND)" --nodekey=./nodekey --rpc "fakenode://85fbed4332ed4329ca2283f26606618815ae83a870c523bb99b0b2e9dfe5af3b4699c2830ecdeb67519d62362db44aa5a8cafee523e3ab8c76aeef1016f424a4?fakepeers=$(FAKEPEERS)" diff --git a/host.go b/host.go index 2a26080..637fa1f 100644 --- a/host.go +++ b/host.go @@ -49,10 +49,20 @@ func runHost(options Options) error { h.NodeURI = remoteEnode } + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt) + go func() { + for _ = range sigCh { + logger.Info("Shutting down...") + h.Stop() + } + }() + if options.Host.Pool == ":memory:" { // Support for in-memory pool. This is primarily for testing. logger.Infof("Starting in-memory vipnode pool.") p := pool.New(memory.New(), nil) + p.Version = fmt.Sprintf("vipnode/pool/%s", Version) rpcPool := &jsonrpc2.Local{} if err := rpcPool.Server.Register("vipnode_", p); err != nil { return err @@ -105,15 +115,6 @@ func runHost(options Options) error { } } - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, os.Interrupt) - go func() { - for _ = range sigCh { - logger.Info("Shutting down...") - h.Stop() - } - }() - return <-errChan } diff --git a/pool/service.go b/pool/service.go index 0d5fe9e..069a67b 100644 --- a/pool/service.go +++ b/pool/service.go @@ -406,10 +406,11 @@ func (p *VipnodePool) requestHosts(ctx context.Context, nodeID string, numReques if len(errors) > 0 { err = RemoteHostErrors{"vipnode_whitelist", errors} + logger.Printf("Request %q hosts: %q (%d hosts found, %d accepted); failures: %s", kind, pretty.Abbrev(nodeID), len(remotes), len(accepted), err) + } else { + logger.Printf("Request %q hosts: %q (%d hosts found, %d accepted)", kind, pretty.Abbrev(nodeID), len(remotes), len(accepted)) } - logger.Printf("Request hosts from %q: kind=%q (%d hosts found, %d accepted), failures=%s", pretty.Abbrev(nodeID), kind, len(remotes), len(accepted), err) - if len(accepted) >= 1 { // We're okay returning without an error as long as some hosts succeeded. return accepted, nil From 32cb553f8cd931d526e7cb869d59c2acd2967a86 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Mon, 13 May 2019 15:49:16 -0400 Subject: [PATCH 21/43] pool: VipnodePool.OnDisconnect -> CloseRemote --- pool.go | 2 +- pool/service.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pool.go b/pool.go index e4a2f2b..1a09e51 100644 --- a/pool.go +++ b/pool.go @@ -193,7 +193,7 @@ func runPool(options Options) error { handler := &server{ ws: &ws.Upgrader{}, header: http.Header{}, - onDisconnect: p.OnDisconnect, + onDisconnect: p.CloseRemote, } if options.Pool.AllowOrigin != "" { handler.header.Set("Access-Control-Allow-Origin", options.Pool.AllowOrigin) diff --git a/pool/service.go b/pool/service.go index 069a67b..9aa70b1 100644 --- a/pool/service.go +++ b/pool/service.go @@ -57,8 +57,8 @@ type VipnodePool struct { remoteNodeLookup map[jsonrpc2.Service]store.NodeID // Reverse lookup } -// OnDisconnect is to be called when a remote service is disconnected. It is used to clean up state. -func (p *VipnodePool) OnDisconnect(remote jsonrpc2.Service) error { +// CloseRemote is to be called when a remote service is disconnected. It is used to clean up state. +func (p *VipnodePool) CloseRemote(remote jsonrpc2.Service) error { p.mu.Lock() defer p.mu.Unlock() From 3bef97a6104a2d1ab35967cd8b90b189abb4462a Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Mon, 13 May 2019 16:03:34 -0400 Subject: [PATCH 22/43] jsonrpc2: FIXME --- jsonrpc2/remote.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jsonrpc2/remote.go b/jsonrpc2/remote.go index 789b089..8c3f1de 100644 --- a/jsonrpc2/remote.go +++ b/jsonrpc2/remote.go @@ -13,6 +13,8 @@ import ( // ServePipe sets up symmetric server/clients over a net.Pipe() and starts // both in goroutines. Useful for testing. Services still need to be registered. +// FIXME: This is a testing helper, ideally we want to get rid of it. It leaks +// goroutines by design. func ServePipe() (*Remote, *Remote) { c1, c2 := net.Pipe() client := Remote{ From aba142fd493a5c0311bb3a976a3d17126fbae8f8 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Mon, 13 May 2019 16:04:02 -0400 Subject: [PATCH 23/43] pool, internal/fakecluster: Test CloseRemote --- internal/fakecluster/fakecluster.go | 3 +++ pool/service.go | 9 +++++++++ poolhostclient_test.go | 8 ++++++++ 3 files changed, 20 insertions(+) diff --git a/internal/fakecluster/fakecluster.go b/internal/fakecluster/fakecluster.go index 35a2a7b..eb4b043 100644 --- a/internal/fakecluster/fakecluster.go +++ b/internal/fakecluster/fakecluster.go @@ -118,6 +118,9 @@ func (c *Cluster) Close() error { client.Stop() } for _, host := range c.Hosts { + if err := c.Pool.CloseRemote(host.In); err != nil { + errors = append(errors, err) + } if err := host.Wait(); err != nil { errors = append(errors, err) } diff --git a/pool/service.go b/pool/service.go index 9aa70b1..3c543d7 100644 --- a/pool/service.go +++ b/pool/service.go @@ -57,6 +57,8 @@ type VipnodePool struct { remoteNodeLookup map[jsonrpc2.Service]store.NodeID // Reverse lookup } +// TODO: Move CloseRemote and NumRemotes, and remoteHosts etc into a separate struct? + // CloseRemote is to be called when a remote service is disconnected. It is used to clean up state. func (p *VipnodePool) CloseRemote(remote jsonrpc2.Service) error { p.mu.Lock() @@ -74,6 +76,13 @@ func (p *VipnodePool) CloseRemote(remote jsonrpc2.Service) error { return nil } +// NumRemotes returns the number of remote hosts that the pool is currently maintaining. +func (p *VipnodePool) NumRemotes() int { + p.mu.Lock() + defer p.mu.Unlock() + return len(p.remoteHosts) +} + func (p *VipnodePool) verify(sig string, method string, nodeID string, nonce int64, args ...interface{}) error { // TODO: Switch nonce to strictly timestamp within X time // TODO: Switch NodeID to pubkey? diff --git a/poolhostclient_test.go b/poolhostclient_test.go index d92bb73..e642396 100644 --- a/poolhostclient_test.go +++ b/poolhostclient_test.go @@ -174,6 +174,10 @@ func TestPoolHostConnectPeers(t *testing.T) { t.Errorf("wrong number of hosts: got %d; want %d", got, want) } + if got, want := cluster.Pool.NumRemotes(), 4; got != want { + t.Errorf("wrong number of remotes: got %d; want %d", got, want) + } + stats, err := cluster.Pool.Store.Stats() if err != nil { t.Error(err) @@ -233,4 +237,8 @@ func TestPoolHostConnectPeers(t *testing.T) { if err != nil { t.Error(err) } + + if got, want := cluster.Pool.NumRemotes(), 0; got != want { + t.Errorf("wrong number of remotes: got %d; want %d", got, want) + } } From c83f2d9ddeeb2fd32e7ec8416d6aeca161177f05 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Mon, 13 May 2019 16:19:44 -0400 Subject: [PATCH 24/43] pool: Add NodeVersion and VipnodeVersion to store.Node and status hosts --- pool/service.go | 12 +++++++----- pool/status/status.go | 6 ++++++ pool/store/store.go | 3 +++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pool/service.go b/pool/service.go index 3c543d7..df6d5ee 100644 --- a/pool/service.go +++ b/pool/service.go @@ -285,11 +285,13 @@ func (p *VipnodePool) connect(ctx context.Context, nodeID string, req ConnectReq // get successfully whitelisted, then switched to host status thus bypass // billing? node := store.Node{ - ID: store.NodeID(nodeID), - Kind: kind, - LastSeen: time.Now(), - IsHost: isHost, - Payout: store.Account(req.Payout), + ID: store.NodeID(nodeID), + Kind: kind, + LastSeen: time.Now(), + IsHost: isHost, + Payout: store.Account(req.Payout), + NodeVersion: req.NodeInfo.Version, + VipnodeVersion: req.VipnodeVersion, } if isHost { diff --git a/pool/status/status.go b/pool/status/status.go index ab36157..f4f45d9 100644 --- a/pool/status/status.go +++ b/pool/status/status.go @@ -20,6 +20,9 @@ type Host struct { Kind string `json:"kind"` BlockNumber uint64 `json:"block_number"` + NodeVersion string `json:"node_version"` + VipnodeVersion string `json:"vipnode_version"` + // TODO: Add peers } @@ -33,6 +36,9 @@ func nodeHost(n store.Node) Host { LastSeen: n.LastSeen, Kind: n.Kind, BlockNumber: n.BlockNumber, + + NodeVersion: n.NodeVersion, + VipnodeVersion: n.VipnodeVersion, } } diff --git a/pool/store/store.go b/pool/store/store.go index 29dc913..1c9168f 100644 --- a/pool/store/store.go +++ b/pool/store/store.go @@ -63,6 +63,9 @@ type Node struct { IsHost bool Payout Account BlockNumber uint64 `json:"block_number"` + + NodeVersion string `json:"node_version"` + VipnodeVersion string `json:"vipnode_version"` } // Stats contains various aggregate stats of the store state, used for From b5a2ca10733b857f1c4ea18e991588bac6275722 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Tue, 14 May 2019 15:03:52 -0400 Subject: [PATCH 25/43] Makefile: Add poolstatus helper for querying fakepool status --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 5725ffb..393e27a 100644 --- a/Makefile +++ b/Makefile @@ -51,6 +51,9 @@ fakehostpool: $(BINARY) fakeclient: $(BINARY) $(RUN) -vv client "http://$(FAKEBIND)" --nodekey=./nodekey --rpc "fakenode://85fbed4332ed4329ca2283f26606618815ae83a870c523bb99b0b2e9dfe5af3b4699c2830ecdeb67519d62362db44aa5a8cafee523e3ab8c76aeef1016f424a4?fakepeers=$(FAKEPEERS)" +poolstatus: + @curl -s "http://$(FAKEBIND)" -H 'Origin: http://localhost:3030/status' --data-binary '{"id":1,"method":"pool_status"}' + release: GOOS=linux GOARCH=amd64 LDFLAGS=$(LDFLAGS) ./build_release "$(PKG)" README.md LICENSE GOOS=linux GOARCH=386 LDFLAGS=$(LDFLAGS) ./build_release "$(PKG)" README.md LICENSE From e3c7caf39a936cc13b37768677ff2a2f9830810a Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Tue, 14 May 2019 15:04:13 -0400 Subject: [PATCH 26/43] internal/fakenode: Add UserAgent --- internal/fakenode/fakenode.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/internal/fakenode/fakenode.go b/internal/fakenode/fakenode.go index 740ecfd..7688cf2 100644 --- a/internal/fakenode/fakenode.go +++ b/internal/fakenode/fakenode.go @@ -24,9 +24,10 @@ func Call(method string, args ...interface{}) call { func Node(nodeID string) *FakeNode { return &FakeNode{ - NodeKind: ethnode.Geth, - NodeID: nodeID, - Calls: Calls{}, + NodeKind: ethnode.Geth, + NodeID: nodeID, + Calls: Calls{}, + IsFullNode: true, } } @@ -37,12 +38,21 @@ type FakeNode struct { Calls Calls FakePeers []ethnode.PeerInfo FakeBlockNumber uint64 + IsFullNode bool } func (n *FakeNode) ContractBackend() bind.ContractBackend { return ðclient.Client{} } +func (n *FakeNode) UserAgent() ethnode.UserAgent { + return ethnode.UserAgent{ + Version: "Geth/Fakenode/Go-tests", + Network: 1, + IsFullNode: n.IsFullNode, + Kind: n.NodeKind, + } +} func (n *FakeNode) Kind() ethnode.NodeKind { return n.NodeKind } func (n *FakeNode) Enode(ctx context.Context) (string, error) { return n.NodeID, nil } func (n *FakeNode) AddTrustedPeer(ctx context.Context, nodeID string) error { From cfec5d702be4ed6a712e90544bd166ce139d5f30 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Tue, 14 May 2019 15:08:42 -0400 Subject: [PATCH 27/43] ethnode: Add UserAgent --- ethnode/geth.go | 5 +++++ ethnode/parity.go | 5 +++++ ethnode/rpc.go | 16 ++++++++++++---- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/ethnode/geth.go b/ethnode/geth.go index e2d288b..47bdcfd 100644 --- a/ethnode/geth.go +++ b/ethnode/geth.go @@ -20,6 +20,7 @@ type codedError interface { var _ EthNode = &gethNode{} type gethNode struct { + agent UserAgent client *rpc.Client } @@ -27,6 +28,10 @@ func (n *gethNode) ContractBackend() bind.ContractBackend { return ethclient.NewClient(n.client) } +func (n *gethNode) UserAgent() UserAgent { + return n.agent +} + func (n *gethNode) Kind() NodeKind { return Geth } diff --git a/ethnode/parity.go b/ethnode/parity.go index fe8458e..17677c5 100644 --- a/ethnode/parity.go +++ b/ethnode/parity.go @@ -16,6 +16,7 @@ type parityPeers struct { } type parityNode struct { + agent UserAgent client *rpc.Client } @@ -23,6 +24,10 @@ func (n *parityNode) ContractBackend() bind.ContractBackend { return ethclient.NewClient(n.client) } +func (n *parityNode) UserAgent() UserAgent { + return n.agent +} + func (n *parityNode) Kind() NodeKind { return Parity } diff --git a/ethnode/rpc.go b/ethnode/rpc.go index 581b084..a5d07d9 100644 --- a/ethnode/rpc.go +++ b/ethnode/rpc.go @@ -164,6 +164,8 @@ type EthNode interface { // Kind returns the kind of node this is. Kind() NodeKind + // UserAgent returns the versions of the client. + UserAgent() UserAgent // Enode returns this node's enode://... Enode(ctx context.Context) (string, error) // AddTrustedPeer adds a nodeID to a set of nodes that can always connect, even @@ -184,17 +186,23 @@ type EthNode interface { // RemoteNode autodetects the node kind and returns the appropriate EthNode // implementation. func RemoteNode(client *rpc.Client) (EthNode, error) { - version, err := DetectClient(client) + agent, err := DetectClient(client) if err != nil { return nil, err } - switch version.Kind { + switch agent.Kind { case Parity: - return &parityNode{client: client}, nil + return &parityNode{ + agent: *agent, + client: client, + }, nil default: // Treat everything else as Geth // FIXME: Is this a bad idea? - node := &gethNode{client: client} + node := &gethNode{ + agent: *agent, + client: client, + } ctx := context.TODO() if err := node.CheckCompatible(ctx); err != nil { return nil, err From 097377bb3d3da1cf2c03bd2fa943629c46f98d26 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Tue, 14 May 2019 15:09:26 -0400 Subject: [PATCH 28/43] all: Add Vipnode versions, switch to Pool.Connect RPC --- client.go | 1 + client/client.go | 25 ++++++++++++++++++++----- host.go | 1 + host/host.go | 23 ++++++++++++++--------- pool/pool.go | 5 ----- pool/service.go | 2 ++ pool/status/status.go | 13 +++++++++---- poolhostclient_test.go | 10 +++++++++- 8 files changed, 56 insertions(+), 24 deletions(-) diff --git a/client.go b/client.go index 6673242..e3305ca 100644 --- a/client.go +++ b/client.go @@ -37,6 +37,7 @@ func runClient(options Options) error { errChan := make(chan error) c := client.New(remoteNode) + c.Version = fmt.Sprintf("vipnode/client/%s", Version) c.PoolMessageCallback = func(msg string) { logger.Alertf("Message from pool: %s", msg) } diff --git a/client/client.go b/client/client.go index 0503e0e..f42edb2 100644 --- a/client/client.go +++ b/client/client.go @@ -10,14 +10,19 @@ import ( "github.com/vipnode/vipnode/pool/store" ) +const defaultNumHosts = 3 + // ErrAlreadyConnected is returned on Connect() if the client is already connected. var ErrAlreadyConnected = errors.New("client already connected") func New(node ethnode.EthNode) *Client { return &Client{ - EthNode: node, - stopCh: make(chan struct{}), - waitCh: make(chan error, 1), + Version: "dev", + EthNode: node, + NumHosts: defaultNumHosts, + + stopCh: make(chan struct{}), + waitCh: make(chan error, 1), } } @@ -25,6 +30,9 @@ func New(node ethnode.EthNode) *Client { type Client struct { ethnode.EthNode + // Version is the vipnode agent version that the client is using. + Version string + // BalanceCallback is called whenever the client receives a balance update // from the pool. It can be used for displaying the current balance to the // client. @@ -36,6 +44,10 @@ type Client struct { // displayed to the client. PoolMessageCallback func(string) + // NumHosts is the number of vipnode hosts the client should try to connect + // with. + NumHosts int + connectedHosts []store.Node stopCh chan struct{} waitCh chan error @@ -52,8 +64,11 @@ func (c *Client) Wait() error { func (c *Client) Start(p pool.Pool) error { logger.Printf("Requesting host candidates...") starCtx := context.Background() - kind := c.EthNode.Kind().String() - resp, err := p.Client(starCtx, pool.ClientRequest{Kind: kind}) + resp, err := p.Connect(starCtx, pool.ConnectRequest{ + VipnodeVersion: c.Version, + NodeInfo: c.EthNode.UserAgent(), + NumHosts: c.NumHosts, + }) if err != nil { return err } diff --git a/host.go b/host.go index 637fa1f..7003263 100644 --- a/host.go +++ b/host.go @@ -38,6 +38,7 @@ func runHost(options Options) error { } h := host.New(remoteNode, options.Host.Payout) + h.Version = fmt.Sprintf("vipnode/host/%s", Version) if options.Host.NodeURI != "" { if err := matchEnode(options.Host.NodeURI, nodeID); err != nil { return err diff --git a/host/host.go b/host/host.go index fb55091..290f7e9 100644 --- a/host/host.go +++ b/host/host.go @@ -21,10 +21,11 @@ type client struct { func New(node ethnode.EthNode, payout string) *Host { return &Host{ - node: node, - payout: payout, - stopCh: make(chan struct{}), - waitCh: make(chan error, 1), + Version: "dev", + node: node, + payout: payout, + stopCh: make(chan struct{}), + waitCh: make(chan error, 1), } } @@ -41,6 +42,9 @@ type Host struct { // node runs on a different IP from the vipnode agent. NodeURI string + // Version is the version of the vipnode agent that the host is running. + Version string + node ethnode.EthNode payout string stopCh chan struct{} @@ -120,12 +124,13 @@ func (h *Host) Start(p pool.Pool) error { } logger.Printf("Connected to local node: %s", enode) - hostReq := pool.HostRequest{ - Kind: h.node.Kind().String(), - Payout: h.payout, - NodeURI: h.NodeURI, + connectReq := pool.ConnectRequest{ + Payout: h.payout, + NodeURI: h.NodeURI, + VipnodeVersion: h.Version, + NodeInfo: h.node.UserAgent(), } - resp, err := p.Host(startCtx, hostReq) + resp, err := p.Connect(startCtx, connectReq) if err != nil { return err } diff --git a/pool/pool.go b/pool/pool.go index 4d91e8d..a33c18a 100644 --- a/pool/pool.go +++ b/pool/pool.go @@ -7,17 +7,12 @@ import ( "github.com/vipnode/vipnode/pool/store" ) -// TODO: Add HostRequest.Network and ClientRequest.Network? -// TODO: Add HostRequest.HostVersion? - // ConnectRequest is a base request done when a vipnode agent connects to a pool. // It is common between hosts and clients type ConnectRequest struct { // VipnodeVersion is the version string of the vipnode agent VipnodeVersion string `json:"vipnode_version"` - // XXX: Add Protocols/Capabilities - // NodeInfo is the metadata of the Ethereum node, includes node kind. NodeInfo ethnode.UserAgent `json:"node_info"` diff --git a/pool/service.go b/pool/service.go index df6d5ee..381032e 100644 --- a/pool/service.go +++ b/pool/service.go @@ -31,6 +31,8 @@ func New(storeDriver store.Store, manager balance.Manager) *VipnodePool { manager = balance.NoBalance{} } return &VipnodePool{ + Version: "dev", + Store: storeDriver, BalanceManager: manager, remoteHosts: map[store.NodeID]jsonrpc2.Service{}, diff --git a/pool/status/status.go b/pool/status/status.go index f4f45d9..4afea7b 100644 --- a/pool/status/status.go +++ b/pool/status/status.go @@ -19,14 +19,13 @@ type Host struct { LastSeen time.Time `json:"last_seen"` Kind string `json:"kind"` BlockNumber uint64 `json:"block_number"` + NumPeers int `json:"num_peers"` NodeVersion string `json:"node_version"` VipnodeVersion string `json:"vipnode_version"` - - // TODO: Add peers } -func nodeHost(n store.Node) Host { +func nodeHost(n store.Node, numPeers int) Host { shortID := string(n.ID) if len(shortID) > 12 { shortID = shortID[:12] @@ -36,6 +35,7 @@ func nodeHost(n store.Node) Host { LastSeen: n.LastSeen, Kind: n.Kind, BlockNumber: n.BlockNumber, + NumPeers: numPeers, NodeVersion: n.NodeVersion, VipnodeVersion: n.VipnodeVersion, @@ -123,7 +123,12 @@ func (s *PoolStatus) getStatus() (*StatusResponse, error) { r.ActiveHosts = make([]Host, 0, len(nodes)) for _, n := range nodes { - r.ActiveHosts = append(r.ActiveHosts, nodeHost(n)) + peers, err := s.Store.NodePeers(n.ID) + if err != nil { + r.Error = err + return r, err + } + r.ActiveHosts = append(r.ActiveHosts, nodeHost(n, len(peers))) } return r, nil diff --git a/poolhostclient_test.go b/poolhostclient_test.go index e642396..ce6a302 100644 --- a/poolhostclient_test.go +++ b/poolhostclient_test.go @@ -24,7 +24,8 @@ func TestPoolHostClient(t *testing.T) { privkey := keygen.HardcodedKeyIdx(t, 0) payout := "" - p := pool.New(memory.New(), nil) + db := memory.New() + p := pool.New(db, nil) rpcPool2Host, rpcHost2Pool := jsonrpc2.ServePipe() defer rpcPool2Host.Close() defer rpcHost2Pool.Close() @@ -46,6 +47,12 @@ func TestPoolHostClient(t *testing.T) { } defer h.Stop() + if stats, err := db.Stats(); err != nil { + t.Fatal(err) + } else if stats.NumActiveHosts != 1 { + t.Errorf("wrong number of active hosts: %+v", stats) + } + rpcPool2Client, rpcClient2Pool := jsonrpc2.ServePipe() defer rpcPool2Client.Close() defer rpcClient2Pool.Close() @@ -54,6 +61,7 @@ func TestPoolHostClient(t *testing.T) { clientPrivkey := keygen.HardcodedKeyIdx(t, 1) clientNodeID := discv5.PubkeyID(&clientPrivkey.PublicKey).String() clientNode := fakenode.Node(clientNodeID) + clientNode.IsFullNode = false c := client.New(clientNode) clientPool := pool.Remote(rpcClient2Pool, clientPrivkey) if err := c.Start(clientPool); err != nil { From 5f23d90196a90ac8e830b9447e90a02104ac132e Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Tue, 14 May 2019 15:21:25 -0400 Subject: [PATCH 29/43] client: Force NodeInfo.IsFullNode=false for now --- client/client.go | 20 ++++++++++++++------ pool/service.go | 3 ++- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/client/client.go b/client/client.go index f42edb2..5423837 100644 --- a/client/client.go +++ b/client/client.go @@ -26,6 +26,8 @@ func New(node ethnode.EthNode) *Client { } } +// FIXME: I think we can rid of Client altogether, and merge with Host which is almost a superset. + // Client represents a vipnode client which connects to a vipnode host. type Client struct { ethnode.EthNode @@ -44,13 +46,12 @@ type Client struct { // displayed to the client. PoolMessageCallback func(string) - // NumHosts is the number of vipnode hosts the client should try to connect - // with. + // NumHosts is the number of vipnode hosts the client should try to connect with. + // TODO: Autorequest more hosts if the number drops below this. NumHosts int - connectedHosts []store.Node - stopCh chan struct{} - waitCh chan error + stopCh chan struct{} + waitCh chan error } // Wait blocks until the client is stopped. @@ -63,10 +64,17 @@ func (c *Client) Wait() error { // break out into a separate goroutine and Start returns. func (c *Client) Start(p pool.Pool) error { logger.Printf("Requesting host candidates...") + + // We override IsFullNode here just because Client does not bother to + // expose a reverse RPC service which the Connect RPC expects for hosts to + // be able to whitelist. This will be moot when we merge Client+Host. + nodeInfo := c.EthNode.UserAgent() + nodeInfo.IsFullNode = false + starCtx := context.Background() resp, err := p.Connect(starCtx, pool.ConnectRequest{ VipnodeVersion: c.Version, - NodeInfo: c.EthNode.UserAgent(), + NodeInfo: nodeInfo, NumHosts: c.NumHosts, }) if err != nil { diff --git a/pool/service.go b/pool/service.go index 381032e..a406cd1 100644 --- a/pool/service.go +++ b/pool/service.go @@ -297,12 +297,13 @@ func (p *VipnodePool) connect(ctx context.Context, nodeID string, req ConnectReq } if isHost { - // We only care about publicly-visible nodeURIs for hosts. + // Hosts expose a reverse-RPC for vipnode_whitelist. service, err := jsonrpc2.CtxService(ctx) if err != nil { return nil, err } + // We only care about publicly-visible nodeURIs for hosts. remoteHost := "" if withAddr, ok := service.(interface{ RemoteAddr() string }); ok { remoteHost = (&url.URL{Host: withAddr.RemoteAddr()}).Hostname() From 8a8a3fe45140897d080fa8b39f6f38bced49e1ee Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Wed, 15 May 2019 15:34:15 -0400 Subject: [PATCH 30/43] pool: Add Pool.Peer RPC --- pool/pool.go | 21 ++++++++++++++++++++- pool/remote.go | 20 ++++++++++++++++++++ pool/service.go | 20 +++++++++++++++++++- pool/staticpool.go | 4 ++++ 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/pool/pool.go b/pool/pool.go index a33c18a..bec69c9 100644 --- a/pool/pool.go +++ b/pool/pool.go @@ -23,6 +23,7 @@ type ConnectRequest struct { NodeURI string `json:"node_uri,omitempty"` // NumHosts is the number of hosts to request from the pool. (Optional) + // XXX: Remove and replace with call to Peer NumHosts int `json:"num_hosts,omitempty"` // Payout sets the wallet account to register the host credit towards. (Optional) @@ -100,6 +101,21 @@ type UpdateResponse struct { InvalidPeers []string `json:"invalid_peers"` } +// PeerRequest is the request type for Peer RPC calls. +type PeerRequest struct { + // Num is the number of peers + Num int `json:"num"` + // Kind is the type of node we desire, such as "parity" or "geth" (optional) + Kind string `json:"kind,omitempty"` +} + +// PeerResponse is the response type for Peer RPC calls. +type PeerResponse struct { + // Peers that have whitelisted the NodeID and are ready for the node to + // connect to. + Peers []store.Node `json:"peers"` +} + // Pool represents a vipnode pool for coordinating between clients and hosts. type Pool interface { // Host subscribes a host to receive vipnode_whitelist instructions. @@ -122,7 +138,10 @@ type Pool interface { // balance for the node (if relevant). Update(ctx context.Context, req UpdateRequest) (*UpdateResponse, error) - // TODO: RequestHosts(ctx context.Context, req RequestHostsRequest) (*RequestHostsRequest, error) + // Peer initiates a peering request for valid hosts. The returned set of peers + // match the request query and are ready to peer with. By default, it + // should only return full node hosts as peers. + Peer(ctx context.Context, req PeerRequest) (*PeerResponse, error) // Withdraw prompts a request to settle the node's balance. Withdraw(ctx context.Context) error diff --git a/pool/remote.go b/pool/remote.go index b07ecc7..82cbb8f 100644 --- a/pool/remote.go +++ b/pool/remote.go @@ -93,6 +93,26 @@ func (p *RemotePool) Connect(ctx context.Context, req ConnectRequest) (*ConnectR return &resp, nil } +func (p *RemotePool) Peer(ctx context.Context, req PeerRequest) (*PeerResponse, error) { + signedReq := request.NodeRequest{ + Method: "vipnode_peer", + NodeID: p.nodeID, + Nonce: p.getNonce(), + ExtraArgs: []interface{}{req}, + } + + args, err := signedReq.SignedArgs(p.privkey) + if err != nil { + return nil, err + } + var resp PeerResponse + if err := p.client.Call(ctx, &resp, signedReq.Method, args...); err != nil { + return nil, err + } + + return &resp, nil +} + func (p *RemotePool) Disconnect(ctx context.Context) error { signedReq := request.NodeRequest{ Method: "vipnode_disconnect", diff --git a/pool/service.go b/pool/service.go index a406cd1..d857373 100644 --- a/pool/service.go +++ b/pool/service.go @@ -254,7 +254,7 @@ func (p *VipnodePool) Client(ctx context.Context, sig string, nodeID string, non return resp, nil } -// Client returns a list of enodes who are ready for the client node to connect. +// Connect returns a list of enodes who are ready for the client node to connect. func (p *VipnodePool) Connect(ctx context.Context, sig string, nodeID string, nonce int64, req ConnectRequest) (*ConnectResponse, error) { if err := p.verify(sig, "vipnode_connect", nodeID, nonce, req); err != nil { return nil, err @@ -354,6 +354,24 @@ func (p *VipnodePool) connect(ctx context.Context, nodeID string, req ConnectReq return response, nil } +// Peer returns a list of enodes who are ready for the node to connect. +func (p *VipnodePool) Peer(ctx context.Context, sig string, nodeID string, nonce int64, req PeerRequest) (*PeerResponse, error) { + // TODO: Should we use protocol capability (eth, les, pip) instead of Kind? + // It's hard to get self-reported protocol capability versions though (les/2 vs just les). + hosts, err := p.requestHosts(ctx, nodeID, req.Num, req.Kind) + if err != nil { + return nil, err + } + + // TODO: Move logs here. + + response := &PeerResponse{ + Peers: hosts, + } + return response, nil + +} + func (p *VipnodePool) requestHosts(ctx context.Context, nodeID string, numRequestHosts int, kind string) ([]store.Node, error) { if p.MaxRequestHosts > 0 && numRequestHosts > p.MaxRequestHosts { numRequestHosts = p.MaxRequestHosts diff --git a/pool/staticpool.go b/pool/staticpool.go index f00bcf0..e1f259a 100644 --- a/pool/staticpool.go +++ b/pool/staticpool.go @@ -36,6 +36,10 @@ func (s *StaticPool) Connect(ctx context.Context, req ConnectRequest) (*ConnectR return &ConnectResponse{Hosts: s.Nodes}, nil } +func (s *StaticPool) Peer(ctx context.Context, req PeerRequest) (*PeerResponse, error) { + return &PeerResponse{Peers: s.Nodes}, nil +} + func (s *StaticPool) Disconnect(ctx context.Context) error { return nil } From d41a2121e2a662bb970746e8e9594c9142e4ded3 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Wed, 15 May 2019 16:17:04 -0400 Subject: [PATCH 31/43] pool, client: Remove implicit vipnode_peer from Pool.Connect --- client/client.go | 15 +++++++++------ pool/pool.go | 4 ---- pool/service.go | 43 +++++++++++++------------------------------ pool/service_test.go | 4 +++- 4 files changed, 25 insertions(+), 41 deletions(-) diff --git a/client/client.go b/client/client.go index 5423837..a510151 100644 --- a/client/client.go +++ b/client/client.go @@ -72,22 +72,25 @@ func (c *Client) Start(p pool.Pool) error { nodeInfo.IsFullNode = false starCtx := context.Background() - resp, err := p.Connect(starCtx, pool.ConnectRequest{ + connResp, err := p.Connect(starCtx, pool.ConnectRequest{ VipnodeVersion: c.Version, NodeInfo: nodeInfo, - NumHosts: c.NumHosts, }) if err != nil { return err } - if resp.Message != "" && c.PoolMessageCallback != nil { - c.PoolMessageCallback(resp.Message) + if connResp.Message != "" && c.PoolMessageCallback != nil { + c.PoolMessageCallback(connResp.Message) } - nodes := resp.Hosts + + peerResp, err := p.Peer(starCtx, pool.PeerRequest{ + Num: c.NumHosts, + }) + nodes := peerResp.Peers if len(nodes) == 0 { return pool.NoHostNodesError{} } - logger.Printf("Received %d host candidates from pool (version %s), connecting...", len(nodes), resp.PoolVersion) + logger.Printf("Received %d host candidates from pool (version %s), connecting...", len(nodes), connResp.PoolVersion) for _, node := range nodes { if err := c.EthNode.ConnectPeer(starCtx, node.URI); err != nil { return err diff --git a/pool/pool.go b/pool/pool.go index bec69c9..4063464 100644 --- a/pool/pool.go +++ b/pool/pool.go @@ -22,10 +22,6 @@ type ConnectRequest struct { // connecting. NodeURI string `json:"node_uri,omitempty"` - // NumHosts is the number of hosts to request from the pool. (Optional) - // XXX: Remove and replace with call to Peer - NumHosts int `json:"num_hosts,omitempty"` - // Payout sets the wallet account to register the host credit towards. (Optional) Payout string `json:"payout"` } diff --git a/pool/service.go b/pool/service.go index d857373..7676df6 100644 --- a/pool/service.go +++ b/pool/service.go @@ -230,13 +230,7 @@ func (p *VipnodePool) Client(ctx context.Context, sig string, nodeID string, non if err := p.verify(sig, "vipnode_client", nodeID, nonce, req); err != nil { return nil, err } - // Clients have a default number of hosts they request. Hosts don't. - numRequestHosts := defaultRequestNumHosts - if req.NumHosts > 0 { - numRequestHosts = req.NumHosts - } connectReq := ConnectRequest{ - NumHosts: numRequestHosts, NodeInfo: ethnode.UserAgent{ Kind: ethnode.ParseNodeKind(req.Kind), IsFullNode: false, @@ -246,8 +240,19 @@ func (p *VipnodePool) Client(ctx context.Context, sig string, nodeID string, non if err != nil { return nil, err } + + // Clients have a default number of hosts they request. Hosts don't. + numRequestHosts := defaultRequestNumHosts + if req.NumHosts > 0 { + numRequestHosts = req.NumHosts + } + hosts, err := p.requestHosts(ctx, nodeID, numRequestHosts, req.Kind) + if err != nil { + return nil, err + } + resp := &ClientResponse{ - Hosts: connectResp.Hosts, + Hosts: hosts, PoolVersion: connectResp.PoolVersion, Message: connectResp.Message, } @@ -327,29 +332,7 @@ func (p *VipnodePool) connect(ctx context.Context, nodeID string, req ConnectReq if err := p.BalanceManager.OnClient(node); err != nil { return nil, err } - - reqKind := kind - if isHost { - // Any kind of host peer will do. - reqKind = "" - } - - hosts, err := p.requestHosts(ctx, nodeID, req.NumHosts, reqKind) - if err != nil { - return nil, err - } - - if !isHost && len(hosts) == 0 { - logger.Printf("New %q peer: %q (no active hosts found)", kind, pretty.Abbrev(nodeID)) - return nil, NoHostNodesError{} - } - - response.Hosts = hosts - if p.skipWhitelist { - logger.Printf("New %q peer: %q (%d hosts found, skipping whitelist)", kind, pretty.Abbrev(nodeID), len(hosts)) - } else { - logger.Printf("New %q peer: %q (%d hosts found)", kind, pretty.Abbrev(nodeID), len(hosts)) - } + logger.Printf("New %q peer: %q", kind, pretty.Abbrev(nodeID)) return response, nil } diff --git a/pool/service_test.go b/pool/service_test.go index 0e8204f..1cef808 100644 --- a/pool/service_test.go +++ b/pool/service_test.go @@ -71,7 +71,9 @@ func TestPoolService(t *testing.T) { t.Fatal(err) } var result interface{} - if err := client.Call(context.TODO(), &result, req.Method, args...); err.Error() != (NoHostNodesError{}).Error() { + if err := client.Call(context.TODO(), &result, req.Method, args...); err == nil { + t.Error("expected NoHostsNodeError, got nil") + } else if err.Error() != (NoHostNodesError{}).Error() { t.Error(err) } } From ba313198804583dd292f80a3c68eb8ed33fd5a3b Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 16 May 2019 14:15:22 -0400 Subject: [PATCH 32/43] pool: Add PoolVersion to StaticPool --- pool/pool.go | 1 + pool/staticpool.go | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pool/pool.go b/pool/pool.go index 4063464..0379538 100644 --- a/pool/pool.go +++ b/pool/pool.go @@ -95,6 +95,7 @@ type UpdateRequest struct { type UpdateResponse struct { Balance *store.Balance `json:"balance,omitempty"` InvalidPeers []string `json:"invalid_peers"` + // TODO: Add PoolPeers []string // PoolPeers is the set of peers that are members of the pool. } // PeerRequest is the request type for Peer RPC calls. diff --git a/pool/staticpool.go b/pool/staticpool.go index e1f259a..1e88f99 100644 --- a/pool/staticpool.go +++ b/pool/staticpool.go @@ -33,7 +33,10 @@ func (s *StaticPool) Client(ctx context.Context, req ClientRequest) (*ClientResp } func (s *StaticPool) Connect(ctx context.Context, req ConnectRequest) (*ConnectResponse, error) { - return &ConnectResponse{Hosts: s.Nodes}, nil + return &ConnectResponse{ + PoolVersion: "staticpool", + Hosts: s.Nodes, + }, nil } func (s *StaticPool) Peer(ctx context.Context, req PeerRequest) (*PeerResponse, error) { From 4c93a4cdcee40d5bb317ce69f71b71cb2d41aeb8 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 16 May 2019 14:15:59 -0400 Subject: [PATCH 33/43] client: Add automatic request for more hosts --- client/client.go | 72 +++++++++++++++++++++++++++++------------- client/client_test.go | 13 ++++---- poolhostclient_test.go | 4 +-- 3 files changed, 59 insertions(+), 30 deletions(-) diff --git a/client/client.go b/client/client.go index a510151..deacce4 100644 --- a/client/client.go +++ b/client/client.go @@ -63,7 +63,7 @@ func (c *Client) Wait() error { // blocks until registration is complete, then the keepalive peering updates // break out into a separate goroutine and Start returns. func (c *Client) Start(p pool.Pool) error { - logger.Printf("Requesting host candidates...") + logger.Printf("Connecting to pool...") // We override IsFullNode here just because Client does not bother to // expose a reverse RPC service which the Connect RPC expects for hosts to @@ -79,46 +79,43 @@ func (c *Client) Start(p pool.Pool) error { if err != nil { return err } + logger.Printf("Connected to pool (version %s), updating state...", connResp.PoolVersion) + if connResp.Message != "" && c.PoolMessageCallback != nil { c.PoolMessageCallback(connResp.Message) } - peerResp, err := p.Peer(starCtx, pool.PeerRequest{ - Num: c.NumHosts, - }) - nodes := peerResp.Peers - if len(nodes) == 0 { - return pool.NoHostNodesError{} - } - logger.Printf("Received %d host candidates from pool (version %s), connecting...", len(nodes), connResp.PoolVersion) - for _, node := range nodes { - if err := c.EthNode.ConnectPeer(starCtx, node.URI); err != nil { - return err - } - } if err := c.updatePeers(context.Background(), p); err != nil { return err } go func() { - c.waitCh <- c.serveUpdates(p, nodes) + c.waitCh <- c.serveUpdates(p) }() return nil } -func (c *Client) serveUpdates(p pool.Pool, connectedHosts []store.Node) error { +func (c *Client) serveUpdates(p pool.Pool) error { ticker := time.Tick(store.KeepaliveInterval) for { select { case <-ticker: if err := c.updatePeers(context.Background(), p); err != nil { + // FIXME: Does it make sense to continue updating for certain + // errors? Eg if no hosts are found, we could keep sending + // updates until we find some. return err } case <-c.stopCh: closeCtx := context.Background() - for _, node := range connectedHosts { - if err := c.EthNode.DisconnectPeer(closeCtx, node.URI); err != nil { + // FIXME: Should we only disconnect from vipnode hosts? + peers, err := c.EthNode.Peers(closeCtx) + if err != nil { + return err + } + for _, node := range peers { + if err := c.EthNode.DisconnectPeer(closeCtx, node.ID); err != nil { return err } } @@ -132,27 +129,58 @@ func (c *Client) updatePeers(ctx context.Context, p pool.Pool) error { if err != nil { return err } + + // Do we need more peers? + // FIXME: Does it make sense to request more peers before sending a vipnode_update? + if needMore := c.NumHosts - len(peers); needMore > 0 { + if err := c.addPeers(ctx, p, needMore); err != nil { + return err + } + } + update, err := p.Update(ctx, pool.UpdateRequest{PeerInfo: peers}) if err != nil { return err } + var balance store.Balance if c.BalanceCallback != nil && update.Balance != nil { - c.BalanceCallback(*update.Balance) + balance = *update.Balance + c.BalanceCallback(balance) } - if len(update.InvalidPeers) > 0 { // Client doesn't really need to do anything if the pool stopped // tracking their host. That means the client is getting a free ride // and it's up to the host to kick the client when the host deems // necessary. - logger.Printf("Sent update: %d peers connected, %d expired in pool. Pool response: %s", len(peers), len(update.InvalidPeers), update.Balance.String()) + logger.Printf("Sent update: %d peers connected, %d expired in pool. Pool response: %s", len(peers), len(update.InvalidPeers), balance.String()) } else { - logger.Printf("Sent update: %d peers connected. Pool response: %s", len(peers), update.Balance.String()) + logger.Printf("Sent update: %d peers connected. Pool response: %s", len(peers), balance.String()) } return nil } +func (c *Client) addPeers(ctx context.Context, p pool.Pool, num int) error { + logger.Printf("Requesting %d more hosts from pool...", num) + peerResp, err := p.Peer(ctx, pool.PeerRequest{ + Num: num, + }) + if err != nil { + return err + } + nodes := peerResp.Peers + if len(nodes) == 0 { + return pool.NoHostNodesError{} + } + logger.Printf("Received %d host candidates from pool, connecting...", len(nodes)) + for _, node := range nodes { + if err := c.EthNode.ConnectPeer(ctx, node.URI); err != nil { + return err + } + } + return nil +} + // Disconnect from hosts, also stop serving updates. func (c *Client) Stop() { c.stopCh <- struct{}{} diff --git a/client/client_test.go b/client/client_test.go index 380697b..410d594 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1,6 +1,7 @@ package client import ( + "os" "testing" "github.com/vipnode/vipnode/internal/fakenode" @@ -9,16 +10,16 @@ import ( ) func TestClient(t *testing.T) { - client := Client{ - EthNode: &fakenode.FakeNode{ - NodeID: "foo", - }, - } + SetLogger(os.Stderr) + + client := New(&fakenode.FakeNode{ + NodeID: "foo", + }) p := pool.StaticPool{} err := client.Start(&p) if _, ok := err.(pool.NoHostNodesError); !ok { - t.Errorf("unexpected no nodes error, got: %q", err) + t.Errorf("expected no nodes error, got: %q", err) } p.Nodes = append(p.Nodes, store.Node{ diff --git a/poolhostclient_test.go b/poolhostclient_test.go index ce6a302..13ef29b 100644 --- a/poolhostclient_test.go +++ b/poolhostclient_test.go @@ -86,7 +86,7 @@ func TestPoolHostClient(t *testing.T) { } want = fakenode.Calls{ fakenode.Call("ConnectPeer", hostNodeURI), - fakenode.Call("DisconnectPeer", hostNodeURI), + fakenode.Call("DisconnectPeer", hostNodeID), } if got := clientNode.Calls; !reflect.DeepEqual(got, want) { t.Errorf("clientNode.Calls:\n got %q;\n want %q", got, want) @@ -98,7 +98,7 @@ func TestPoolHostClient(t *testing.T) { } want = fakenode.Calls{ fakenode.Call("ConnectPeer", hostNodeURI), - fakenode.Call("DisconnectPeer", hostNodeURI), + fakenode.Call("DisconnectPeer", hostNodeID), fakenode.Call("ConnectPeer", hostNodeURI), } if got := clientNode.Calls; !reflect.DeepEqual(got, want) { From 831e6a80df04636750eedb06052a90bbde33eda8 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 16 May 2019 14:27:45 -0400 Subject: [PATCH 34/43] client: Request specific kind of host peers --- client/client.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/client/client.go b/client/client.go index deacce4..5ce3175 100644 --- a/client/client.go +++ b/client/client.go @@ -52,6 +52,8 @@ type Client struct { stopCh chan struct{} waitCh chan error + + nodeInfo ethnode.UserAgent } // Wait blocks until the client is stopped. @@ -65,10 +67,12 @@ func (c *Client) Wait() error { func (c *Client) Start(p pool.Pool) error { logger.Printf("Connecting to pool...") - // We override IsFullNode here just because Client does not bother to + c.nodeInfo = c.EthNode.UserAgent() + + // FIXME: We override IsFullNode here just because Client does not bother to // expose a reverse RPC service which the Connect RPC expects for hosts to // be able to whitelist. This will be moot when we merge Client+Host. - nodeInfo := c.EthNode.UserAgent() + nodeInfo := c.nodeInfo nodeInfo.IsFullNode = false starCtx := context.Background() @@ -161,9 +165,10 @@ func (c *Client) updatePeers(ctx context.Context, p pool.Pool) error { } func (c *Client) addPeers(ctx context.Context, p pool.Pool, num int) error { - logger.Printf("Requesting %d more hosts from pool...", num) + logger.Printf("Requesting %d more %q hosts from pool...", c.nodeInfo.Kind, num) peerResp, err := p.Peer(ctx, pool.PeerRequest{ - Num: num, + Num: num, + Kind: c.nodeInfo.Kind.String(), }) if err != nil { return err From 5ad8aa372678cbd755a0229f95588a56d68fd891 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 16 May 2019 14:28:32 -0400 Subject: [PATCH 35/43] pool: Skip self when returning active host peers --- pool/service.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pool/service.go b/pool/service.go index 7676df6..e0a6766 100644 --- a/pool/service.go +++ b/pool/service.go @@ -380,6 +380,10 @@ func (p *VipnodePool) requestHosts(ctx context.Context, nodeID string, numReques remotes := make([]hostService, 0, len(r)) p.mu.Lock() for _, node := range r { + if node.ID.String() == nodeID { + // Skip self + continue + } remote, ok := p.remoteHosts[node.ID] if ok { remotes = append(remotes, hostService{ From 0130e0481bb377810672902f299eb08a55b3822c Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 16 May 2019 14:28:58 -0400 Subject: [PATCH 36/43] main: Make test error a bit more readable --- poolhostclient_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/poolhostclient_test.go b/poolhostclient_test.go index 13ef29b..2737895 100644 --- a/poolhostclient_test.go +++ b/poolhostclient_test.go @@ -212,8 +212,8 @@ func TestPoolHostConnectPeers(t *testing.T) { if peers, err := host.Node.Peers(context.Background()); err != nil { t.Fatal(err) - } else if len(peers) != numHosts-1 { - t.Errorf("host has wrong number of peers: %s", peers) + } else if got, want := len(peers), numHosts-1; got != want { + t.Errorf("host has wrong number of peers: got %d; want %d", got, want) } numCalls := 0 From a0b9cbe4b6bb7e1cb09ad4afa7fddb9d2d7835db Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 16 May 2019 14:29:32 -0400 Subject: [PATCH 37/43] host: Use new Pool.Peer() RPC for ConnectPeers --- host/host.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/host/host.go b/host/host.go index 290f7e9..dfd39e1 100644 --- a/host/host.go +++ b/host/host.go @@ -153,19 +153,19 @@ func (h *Host) Start(p pool.Pool) error { // and connect to them. This is useful for increasing full node peering for // your node with other nodes under the same pool. func (h *Host) ConnectPeers(p pool.Pool, num int) error { - // Hosts are full nodes, so we don't care what kind of host peer we get. - // Full nodes speak to all full nodes. - kind := "" ctx := context.Background() - resp, err := p.Client(ctx, pool.ClientRequest{Kind: kind}) + resp, err := p.Peer(ctx, pool.PeerRequest{ + Num: num, + // Kind is unspecified, since hosts are happy with any kind of full node. + }) if err != nil { return err } - nodes := resp.Hosts + nodes := resp.Peers if len(nodes) == 0 { return pool.NoHostNodesError{} } - logger.Printf("Received %d host candidates from pool (version %s), connecting...", len(nodes), resp.PoolVersion) + logger.Printf("Received %d host candidates from pool, connecting...", len(nodes)) for _, node := range nodes { if err := h.node.ConnectPeer(ctx, node.URI); err != nil { return err From 283705a695038b461033c8cb1313da325aa333cf Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Sun, 19 May 2019 20:41:00 -0400 Subject: [PATCH 38/43] agent: Add new package, combining client and host --- agent/agent.go | 201 ++++++++++++++++++++++++++++++++++++++++++++++++ agent/logger.go | 20 +++++ 2 files changed, 221 insertions(+) create mode 100644 agent/agent.go create mode 100644 agent/logger.go diff --git a/agent/agent.go b/agent/agent.go new file mode 100644 index 0000000..c0490f7 --- /dev/null +++ b/agent/agent.go @@ -0,0 +1,201 @@ +package agent + +import ( + "context" + "time" + + "github.com/vipnode/vipnode/ethnode" + "github.com/vipnode/vipnode/pool" + "github.com/vipnode/vipnode/pool/store" +) + +const defaultNumHosts = 3 + +var startTimeout = 10 * time.Second +var updateTimeout = 10 * time.Second + +func New(node ethnode.EthNode) *Agent { + return &Agent{ + EthNode: node, + Version: "dev", + stopCh: make(chan struct{}), + waitCh: make(chan error, 1), + } +} + +type Agent struct { + ethnode.EthNode + + // NodeURI can be used to set the enode:// connection string that + // the pool should advertise to clients. Normally, the pool will + // automatically deduce this string from the connection IP and nodeID, but + // we can provide an override if there is a non-standard port or if the + // node runs on a different IP from the vipnode agent. + NodeURI string + + // Version is the version of the vipnode agent that the host is running. + Version string + + // BalanceCallback is called whenever the client receives a balance update + // from the pool. It can be used for displaying the current balance to the + // client. + BalanceCallback func(store.Balance) + + // PoolMessageCallback is called whenever the client receives a message + // from the pool. This can be a welcome message including rules and + // instructions for how to manage the client's balance. It should be + // displayed to the client. + PoolMessageCallback func(string) + + // NumHosts is the minimum number of vipnode hosts the + // client should maintain connections with. + NumHosts int + + // Payout is the address to register to associate pool credits towards. + Payout string + + stopCh chan struct{} + waitCh chan error + nodeInfo ethnode.UserAgent // cached during Start +} + +// Start registers the node on the given pool and starts sending peer updates +// every store.KeepaliveInterval. It returns after +// successfully registering with the pool. +func (a *Agent) Start(p pool.Pool) error { + startCtx, cancel := context.WithTimeout(context.Background(), startTimeout) + defer cancel() + + enode, err := a.EthNode.Enode(startCtx) + if err != nil { + return err + } + logger.Printf("Connected to local node: %s", enode) + + connectReq := pool.ConnectRequest{ + Payout: a.Payout, + NodeURI: a.NodeURI, + VipnodeVersion: a.Version, + NodeInfo: a.EthNode.UserAgent(), + } + a.nodeInfo = connectReq.NodeInfo + resp, err := p.Connect(startCtx, connectReq) + if err != nil { + return err + } + logger.Printf("Registered on pool: Version %s", resp.PoolVersion) + + if resp.Message != "" && a.PoolMessageCallback != nil { + a.PoolMessageCallback(resp.Message) + } + + if err := a.updatePeers(startCtx, p); err != nil { + return err + } + + go func() { + a.waitCh <- a.serveUpdates(p) + }() + return nil +} + +// Stop shuts down all the active connections cleanly. +func (a *Agent) Stop() { + a.stopCh <- struct{}{} +} + +// Wait blocks until the agent is stopped. It returns any errors that occur +// during stopping. +func (a *Agent) Wait() error { + return <-a.waitCh +} + +func (a *Agent) serveUpdates(p pool.Pool) error { + ticker := time.Tick(store.KeepaliveInterval) + for { + select { + case <-ticker: + // FIXME: Sametimes a.updatePeers could take a while, does it make + // sense to keep ticking every interval regardless? Or should we + // make sure there is an offset between calls? What if there's + // overlap, do we want a mutex? + if err := a.updatePeers(context.Background(), p); err != nil { + // FIXME: Does it make sense to continue updating for certain + // errors? Eg if no hosts are found, we could keep sending + // updates until we find some. + return err + } + case <-a.stopCh: + closeCtx := context.Background() + // FIXME: Should we only disconnect from vipnode hosts? + peers, err := a.EthNode.Peers(closeCtx) + if err != nil { + return err + } + for _, node := range peers { + if err := a.EthNode.DisconnectPeer(closeCtx, node.ID); err != nil { + return err + } + } + return nil + } + } +} + +func (a *Agent) updatePeers(ctx context.Context, p pool.Pool) error { + peers, err := a.EthNode.Peers(ctx) + if err != nil { + return err + } + + // Do we need more peers? + // FIXME: Does it make sense to request more peers before sending a vipnode_update? + if needMore := a.NumHosts - len(peers); needMore > 0 { + if err := a.addPeers(ctx, p, needMore); err != nil { + return err + } + } + + update, err := p.Update(ctx, pool.UpdateRequest{PeerInfo: peers}) + if err != nil { + return err + } + var balance store.Balance + if a.BalanceCallback != nil && update.Balance != nil { + balance = *update.Balance + a.BalanceCallback(balance) + } + if len(update.InvalidPeers) > 0 { + // Client doesn't really need to do anything if the pool stopped + // tracking their host. That means the client is getting a free ride + // and it's up to the host to kick the client when the host deems + // necessary. + logger.Printf("Sent update: %d peers connected, %d expired in pool. Pool response: %s", len(peers), len(update.InvalidPeers), balance.String()) + } else { + logger.Printf("Sent update: %d peers connected. Pool response: %s", len(peers), balance.String()) + } + + return nil +} + +func (a *Agent) addPeers(ctx context.Context, p pool.Pool, num int) error { + logger.Printf("Requesting %d more %q hosts from pool...", a.EthNode.Kind(), num) + peerResp, err := p.Peer(ctx, pool.PeerRequest{ + Num: num, + Kind: a.nodeInfo.Kind.String(), + }) + if err != nil { + return err + } + nodes := peerResp.Peers + if len(nodes) == 0 { + return pool.NoHostNodesError{} + } + logger.Printf("Received %d host candidates from pool, connecting...", len(nodes)) + for _, node := range nodes { + if err := a.EthNode.ConnectPeer(ctx, node.URI); err != nil { + return err + } + } + return nil +} diff --git a/agent/logger.go b/agent/logger.go new file mode 100644 index 0000000..8e549af --- /dev/null +++ b/agent/logger.go @@ -0,0 +1,20 @@ +package agent + +import ( + "io" + "io/ioutil" + "log" +) + +var logger *log.Logger + +// SetLogger overrides the logger output for this package. +func SetLogger(w io.Writer) { + flags := log.Flags() + prefix := "[agent] " + logger = log.New(w, prefix, flags) +} + +func init() { + SetLogger(ioutil.Discard) +} From 9173bffcc1effee867a1bd4847da7909c92b72e5 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Sun, 19 May 2019 20:47:01 -0400 Subject: [PATCH 39/43] pool: Remove unused Disconnect RPC --- pool/pool.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pool/pool.go b/pool/pool.go index 0379538..4ab389c 100644 --- a/pool/pool.go +++ b/pool/pool.go @@ -126,10 +126,6 @@ type Pool interface { // Connect subscribes to the active nodes set. Connect(ctx context.Context, req ConnectRequest) (*ConnectResponse, error) - // Disconnect stops tracking the connection and billing, will prompt a - // disconnect from both ends. - Disconnect(ctx context.Context) error - // Update is a keep-alive for sharing the node's peering info. It returns // a list of peers that are no longer corroborated by the pool, and current // balance for the node (if relevant). From 11d333cc825c6d3345e646d3acbc6d7d76e6db2c Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Sun, 19 May 2019 20:47:19 -0400 Subject: [PATCH 40/43] agent: Factor out disconnectPeers --- agent/agent.go | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index c0490f7..26428a5 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -126,22 +126,29 @@ func (a *Agent) serveUpdates(p pool.Pool) error { return err } case <-a.stopCh: - closeCtx := context.Background() - // FIXME: Should we only disconnect from vipnode hosts? - peers, err := a.EthNode.Peers(closeCtx) - if err != nil { - return err - } - for _, node := range peers { - if err := a.EthNode.DisconnectPeer(closeCtx, node.ID); err != nil { - return err - } - } + // FIXME: Does it make sense to call a.disconnectPeers(...) here? return nil } } } +// disconnectPeers tells the node to disconnect from its peers. +// DEPRECATED: This method is unused. It might be useful at some point though? +// If not, remove later. Or maybe it should be changed to only disconnect from +// vipnode-tracked peers? +func (a *Agent) disconnectPeers(ctx context.Context) error { + peers, err := a.EthNode.Peers(ctx) + if err != nil { + return err + } + for _, node := range peers { + if err := a.EthNode.DisconnectPeer(ctx, node.ID); err != nil { + return err + } + } + return nil +} + func (a *Agent) updatePeers(ctx context.Context, p pool.Pool) error { peers, err := a.EthNode.Peers(ctx) if err != nil { From d53b64be08173bc34b482df7ef4918e7a1fdfd33 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Sun, 19 May 2019 21:08:16 -0400 Subject: [PATCH 41/43] agent: Remove New(), allow start without hosts, add test --- agent/agent.go | 57 ++++++++++++++++++++++++++++----------------- agent/agent_test.go | 48 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 21 deletions(-) create mode 100644 agent/agent_test.go diff --git a/agent/agent.go b/agent/agent.go index 26428a5..02fcbce 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -2,6 +2,8 @@ package agent import ( "context" + "errors" + "sync" "time" "github.com/vipnode/vipnode/ethnode" @@ -14,23 +16,16 @@ const defaultNumHosts = 3 var startTimeout = 10 * time.Second var updateTimeout = 10 * time.Second -func New(node ethnode.EthNode) *Agent { - return &Agent{ - EthNode: node, - Version: "dev", - stopCh: make(chan struct{}), - waitCh: make(chan error, 1), - } -} - +// Agent is a companion process for nodes that manages the node's communication +// with a Vipnode Pool. type Agent struct { ethnode.EthNode - // NodeURI can be used to set the enode:// connection string that - // the pool should advertise to clients. Normally, the pool will + // NodeURI can be used to override the enode:// connection string that + // the pool should advertise to other peers. Normally, the pool will // automatically deduce this string from the connection IP and nodeID, but // we can provide an override if there is a non-standard port or if the - // node runs on a different IP from the vipnode agent. + // node runs on a different IP from the vipnode agent. (Optional) NodeURI string // Version is the version of the vipnode agent that the host is running. @@ -38,22 +33,25 @@ type Agent struct { // BalanceCallback is called whenever the client receives a balance update // from the pool. It can be used for displaying the current balance to the - // client. + // client. (Optional) BalanceCallback func(store.Balance) // PoolMessageCallback is called whenever the client receives a message // from the pool. This can be a welcome message including rules and // instructions for how to manage the client's balance. It should be - // displayed to the client. + // displayed to the client. (Optional) PoolMessageCallback func(string) - // NumHosts is the minimum number of vipnode hosts the - // client should maintain connections with. + // NumHosts is the minimum number of vipnode hosts the client should + // maintain connections with. (Optional) NumHosts int // Payout is the address to register to associate pool credits towards. + // (Optional) Payout string + mu sync.Mutex + started bool stopCh chan struct{} waitCh chan error nodeInfo ethnode.UserAgent // cached during Start @@ -63,6 +61,17 @@ type Agent struct { // every store.KeepaliveInterval. It returns after // successfully registering with the pool. func (a *Agent) Start(p pool.Pool) error { + a.mu.Lock() + if a.started { + a.mu.Unlock() + return errors.New("pool already started") + } + if a.stopCh == nil { + a.stopCh = make(chan struct{}) + a.waitCh = make(chan error, 1) + } + a.mu.Unlock() + startCtx, cancel := context.WithTimeout(context.Background(), startTimeout) defer cancel() @@ -72,10 +81,15 @@ func (a *Agent) Start(p pool.Pool) error { } logger.Printf("Connected to local node: %s", enode) + version := a.Version + if version == "" { + version = "dev" + } + connectReq := pool.ConnectRequest{ Payout: a.Payout, NodeURI: a.NodeURI, - VipnodeVersion: a.Version, + VipnodeVersion: version, NodeInfo: a.EthNode.UserAgent(), } a.nodeInfo = connectReq.NodeInfo @@ -126,6 +140,10 @@ func (a *Agent) serveUpdates(p pool.Pool) error { return err } case <-a.stopCh: + a.mu.Lock() + a.started = false + a.mu.Unlock() + // FIXME: Does it make sense to call a.disconnectPeers(...) here? return nil } @@ -195,10 +213,7 @@ func (a *Agent) addPeers(ctx context.Context, p pool.Pool, num int) error { return err } nodes := peerResp.Peers - if len(nodes) == 0 { - return pool.NoHostNodesError{} - } - logger.Printf("Received %d host candidates from pool, connecting...", len(nodes)) + logger.Printf("Received %d host candidates from pool.", len(nodes)) for _, node := range nodes { if err := a.EthNode.ConnectPeer(ctx, node.URI); err != nil { return err diff --git a/agent/agent_test.go b/agent/agent_test.go new file mode 100644 index 0000000..e4ea957 --- /dev/null +++ b/agent/agent_test.go @@ -0,0 +1,48 @@ +package agent + +import ( + "context" + "os" + "testing" + + "github.com/vipnode/vipnode/internal/fakenode" + "github.com/vipnode/vipnode/pool" + "github.com/vipnode/vipnode/pool/store" +) + +func TestAgent(t *testing.T) { + SetLogger(os.Stderr) + + agent := Agent{ + EthNode: &fakenode.FakeNode{ + NodeID: "foo", + }, + NumHosts: 3, + } + + p := &pool.StaticPool{} + if err := agent.Start(p); err != nil { + t.Fatal(err) + } + + if peers, err := agent.EthNode.Peers(context.Background()); err != nil { + t.Fatal(err) + } else if got, want := len(peers), 0; got != want { + t.Errorf("wrong number of peers: got %d; want %d", got, want) + } + + p.Nodes = append(p.Nodes, store.Node{ + URI: "foo", + }) + + // Force update + if err := agent.updatePeers(context.Background(), p); err != nil { + t.Fatal(err) + } + + if peers, err := agent.EthNode.Peers(context.Background()); err != nil { + t.Fatal(err) + } else if got, want := len(peers), 1; got != want { + t.Errorf("wrong number of peers: got %d; want %d", got, want) + } +} From 7c32f032f5aab107e6a28e6dd78055f0e8476bf0 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Tue, 21 May 2019 17:45:08 -0400 Subject: [PATCH 42/43] all: Replace client and host packages with agent --- agent/agent.go | 16 ++- client.go | 8 +- client/client.go | 192 ---------------------------- client/client_test.go | 28 ---- client/logger.go | 20 --- host.go | 17 +-- host/host.go | 192 ---------------------------- host/logger.go | 20 --- internal/fakecluster/fakecluster.go | 65 +++++----- main.go | 6 +- poolhostclient_test.go | 24 ++-- 11 files changed, 71 insertions(+), 517 deletions(-) delete mode 100644 client/client.go delete mode 100644 client/client_test.go delete mode 100644 client/logger.go delete mode 100644 host/host.go delete mode 100644 host/logger.go diff --git a/agent/agent.go b/agent/agent.go index 02fcbce..e66dbae 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -113,6 +113,12 @@ func (a *Agent) Start(p pool.Pool) error { return nil } +// Whitelist a peer for this node. +func (a *Agent) Whitelist(ctx context.Context, nodeID string) error { + logger.Printf("Received whitelist request: %s", nodeID) + return a.EthNode.AddTrustedPeer(ctx, nodeID) +} + // Stop shuts down all the active connections cleanly. func (a *Agent) Stop() { a.stopCh <- struct{}{} @@ -176,7 +182,7 @@ func (a *Agent) updatePeers(ctx context.Context, p pool.Pool) error { // Do we need more peers? // FIXME: Does it make sense to request more peers before sending a vipnode_update? if needMore := a.NumHosts - len(peers); needMore > 0 { - if err := a.addPeers(ctx, p, needMore); err != nil { + if err := a.AddPeers(ctx, p, needMore); err != nil { return err } } @@ -203,7 +209,8 @@ func (a *Agent) updatePeers(ctx context.Context, p pool.Pool) error { return nil } -func (a *Agent) addPeers(ctx context.Context, p pool.Pool, num int) error { +// AddPeers requests num peers from the pool and connects the node to them. +func (a *Agent) AddPeers(ctx context.Context, p pool.Pool, num int) error { logger.Printf("Requesting %d more %q hosts from pool...", a.EthNode.Kind(), num) peerResp, err := p.Peer(ctx, pool.PeerRequest{ Num: num, @@ -221,3 +228,8 @@ func (a *Agent) addPeers(ctx context.Context, p pool.Pool, num int) error { } return nil } + +// Service is the set of RPC calls exposed by an agent. +type Service interface { + Whitelist(ctx context.Context, nodeID string) error +} diff --git a/client.go b/client.go index e3305ca..706baf3 100644 --- a/client.go +++ b/client.go @@ -8,7 +8,7 @@ import ( "os/signal" "github.com/ethereum/go-ethereum/p2p/discv5" - "github.com/vipnode/vipnode/client" + "github.com/vipnode/vipnode/agent" "github.com/vipnode/vipnode/jsonrpc2" "github.com/vipnode/vipnode/pool" @@ -36,8 +36,10 @@ func runClient(options Options) error { } errChan := make(chan error) - c := client.New(remoteNode) - c.Version = fmt.Sprintf("vipnode/client/%s", Version) + c := agent.Agent{ + EthNode: remoteNode, + Version: fmt.Sprintf("vipnode/client/%s", Version), + } c.PoolMessageCallback = func(msg string) { logger.Alertf("Message from pool: %s", msg) } diff --git a/client/client.go b/client/client.go deleted file mode 100644 index 5ce3175..0000000 --- a/client/client.go +++ /dev/null @@ -1,192 +0,0 @@ -package client - -import ( - "context" - "errors" - "time" - - "github.com/vipnode/vipnode/ethnode" - "github.com/vipnode/vipnode/pool" - "github.com/vipnode/vipnode/pool/store" -) - -const defaultNumHosts = 3 - -// ErrAlreadyConnected is returned on Connect() if the client is already connected. -var ErrAlreadyConnected = errors.New("client already connected") - -func New(node ethnode.EthNode) *Client { - return &Client{ - Version: "dev", - EthNode: node, - NumHosts: defaultNumHosts, - - stopCh: make(chan struct{}), - waitCh: make(chan error, 1), - } -} - -// FIXME: I think we can rid of Client altogether, and merge with Host which is almost a superset. - -// Client represents a vipnode client which connects to a vipnode host. -type Client struct { - ethnode.EthNode - - // Version is the vipnode agent version that the client is using. - Version string - - // BalanceCallback is called whenever the client receives a balance update - // from the pool. It can be used for displaying the current balance to the - // client. - BalanceCallback func(store.Balance) - - // PoolMessageCallback is called whenever the client receives a message - // from the pool. This can be a welcome message including rules and - // instructions for how to manage the client's balance. It should be - // displayed to the client. - PoolMessageCallback func(string) - - // NumHosts is the number of vipnode hosts the client should try to connect with. - // TODO: Autorequest more hosts if the number drops below this. - NumHosts int - - stopCh chan struct{} - waitCh chan error - - nodeInfo ethnode.UserAgent -} - -// Wait blocks until the client is stopped. -func (c *Client) Wait() error { - return <-c.waitCh -} - -// Start retrieves compatible hosts from the pool and connects to them. Start -// blocks until registration is complete, then the keepalive peering updates -// break out into a separate goroutine and Start returns. -func (c *Client) Start(p pool.Pool) error { - logger.Printf("Connecting to pool...") - - c.nodeInfo = c.EthNode.UserAgent() - - // FIXME: We override IsFullNode here just because Client does not bother to - // expose a reverse RPC service which the Connect RPC expects for hosts to - // be able to whitelist. This will be moot when we merge Client+Host. - nodeInfo := c.nodeInfo - nodeInfo.IsFullNode = false - - starCtx := context.Background() - connResp, err := p.Connect(starCtx, pool.ConnectRequest{ - VipnodeVersion: c.Version, - NodeInfo: nodeInfo, - }) - if err != nil { - return err - } - logger.Printf("Connected to pool (version %s), updating state...", connResp.PoolVersion) - - if connResp.Message != "" && c.PoolMessageCallback != nil { - c.PoolMessageCallback(connResp.Message) - } - - if err := c.updatePeers(context.Background(), p); err != nil { - return err - } - - go func() { - c.waitCh <- c.serveUpdates(p) - }() - - return nil -} - -func (c *Client) serveUpdates(p pool.Pool) error { - ticker := time.Tick(store.KeepaliveInterval) - for { - select { - case <-ticker: - if err := c.updatePeers(context.Background(), p); err != nil { - // FIXME: Does it make sense to continue updating for certain - // errors? Eg if no hosts are found, we could keep sending - // updates until we find some. - return err - } - case <-c.stopCh: - closeCtx := context.Background() - // FIXME: Should we only disconnect from vipnode hosts? - peers, err := c.EthNode.Peers(closeCtx) - if err != nil { - return err - } - for _, node := range peers { - if err := c.EthNode.DisconnectPeer(closeCtx, node.ID); err != nil { - return err - } - } - return nil - } - } -} - -func (c *Client) updatePeers(ctx context.Context, p pool.Pool) error { - peers, err := c.EthNode.Peers(ctx) - if err != nil { - return err - } - - // Do we need more peers? - // FIXME: Does it make sense to request more peers before sending a vipnode_update? - if needMore := c.NumHosts - len(peers); needMore > 0 { - if err := c.addPeers(ctx, p, needMore); err != nil { - return err - } - } - - update, err := p.Update(ctx, pool.UpdateRequest{PeerInfo: peers}) - if err != nil { - return err - } - var balance store.Balance - if c.BalanceCallback != nil && update.Balance != nil { - balance = *update.Balance - c.BalanceCallback(balance) - } - if len(update.InvalidPeers) > 0 { - // Client doesn't really need to do anything if the pool stopped - // tracking their host. That means the client is getting a free ride - // and it's up to the host to kick the client when the host deems - // necessary. - logger.Printf("Sent update: %d peers connected, %d expired in pool. Pool response: %s", len(peers), len(update.InvalidPeers), balance.String()) - } else { - logger.Printf("Sent update: %d peers connected. Pool response: %s", len(peers), balance.String()) - } - - return nil -} - -func (c *Client) addPeers(ctx context.Context, p pool.Pool, num int) error { - logger.Printf("Requesting %d more %q hosts from pool...", c.nodeInfo.Kind, num) - peerResp, err := p.Peer(ctx, pool.PeerRequest{ - Num: num, - Kind: c.nodeInfo.Kind.String(), - }) - if err != nil { - return err - } - nodes := peerResp.Peers - if len(nodes) == 0 { - return pool.NoHostNodesError{} - } - logger.Printf("Received %d host candidates from pool, connecting...", len(nodes)) - for _, node := range nodes { - if err := c.EthNode.ConnectPeer(ctx, node.URI); err != nil { - return err - } - } - return nil -} - -// Disconnect from hosts, also stop serving updates. -func (c *Client) Stop() { - c.stopCh <- struct{}{} -} diff --git a/client/client_test.go b/client/client_test.go deleted file mode 100644 index 410d594..0000000 --- a/client/client_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package client - -import ( - "os" - "testing" - - "github.com/vipnode/vipnode/internal/fakenode" - "github.com/vipnode/vipnode/pool" - "github.com/vipnode/vipnode/pool/store" -) - -func TestClient(t *testing.T) { - SetLogger(os.Stderr) - - client := New(&fakenode.FakeNode{ - NodeID: "foo", - }) - - p := pool.StaticPool{} - err := client.Start(&p) - if _, ok := err.(pool.NoHostNodesError); !ok { - t.Errorf("expected no nodes error, got: %q", err) - } - - p.Nodes = append(p.Nodes, store.Node{ - URI: "foo", - }) -} diff --git a/client/logger.go b/client/logger.go deleted file mode 100644 index bb1c28b..0000000 --- a/client/logger.go +++ /dev/null @@ -1,20 +0,0 @@ -package client - -import ( - "io" - "io/ioutil" - "log" -) - -var logger *log.Logger - -// SetLogger overrides the logger output for this package. -func SetLogger(w io.Writer) { - flags := log.Flags() - prefix := "[client] " - logger = log.New(w, prefix, flags) -} - -func init() { - SetLogger(ioutil.Discard) -} diff --git a/host.go b/host.go index 7003263..4ebfb4a 100644 --- a/host.go +++ b/host.go @@ -7,7 +7,7 @@ import ( "os/signal" "github.com/ethereum/go-ethereum/p2p/discv5" - "github.com/vipnode/vipnode/host" + "github.com/vipnode/vipnode/agent" "github.com/vipnode/vipnode/jsonrpc2" ws "github.com/vipnode/vipnode/jsonrpc2/ws/gorilla" "github.com/vipnode/vipnode/pool" @@ -37,8 +37,11 @@ func runHost(options Options) error { logger.Warning("No --payout address provided, will not receive pool payments.") } - h := host.New(remoteNode, options.Host.Payout) - h.Version = fmt.Sprintf("vipnode/host/%s", Version) + h := agent.Agent{ + EthNode: remoteNode, + Payout: options.Host.Payout, + Version: fmt.Sprintf("vipnode/host/%s", Version), + } if options.Host.NodeURI != "" { if err := matchEnode(options.Host.NodeURI, nodeID); err != nil { return err @@ -86,7 +89,7 @@ func runHost(options Options) error { // Register reverse-directional RPC calls available on the host rpcServer := &jsonrpc2.Server{} - if err := rpcServer.RegisterMethod("vipnode_whitelist", h, "Whitelist"); err != nil { + if err := rpcServer.RegisterMethod("vipnode_whitelist", &h, "Whitelist"); err != nil { return err } rpcPool := jsonrpc2.Remote{ @@ -110,12 +113,6 @@ func runHost(options Options) error { errChan <- h.Wait() }() - if options.Host.JoinPeers > 0 { - if err := h.ConnectPeers(remotePool, options.Host.JoinPeers); err != nil { - return err - } - } - return <-errChan } diff --git a/host/host.go b/host/host.go deleted file mode 100644 index dfd39e1..0000000 --- a/host/host.go +++ /dev/null @@ -1,192 +0,0 @@ -package host - -import ( - "context" - "time" - - "github.com/vipnode/vipnode/ethnode" - "github.com/vipnode/vipnode/pool" - "github.com/vipnode/vipnode/pool/store" -) - -var startTimeout = 10 * time.Second -var updateTimeout = 10 * time.Second - -type nodeID string - -type client struct { - nodeID nodeID - expire time.Time -} - -func New(node ethnode.EthNode, payout string) *Host { - return &Host{ - Version: "dev", - node: node, - payout: payout, - stopCh: make(chan struct{}), - waitCh: make(chan error, 1), - } -} - -type HostService interface { - Whitelist(ctx context.Context, nodeID string) error -} - -// Host represents a single vipnode host. -type Host struct { - // NodeURI can be used to set the enode:// connection string that - // the pool should advertise to clients. Normally, the pool will - // automatically deduce this string from the connection IP and nodeID, but - // we can provide an override if there is a non-standard port or if the - // node runs on a different IP from the vipnode agent. - NodeURI string - - // Version is the version of the vipnode agent that the host is running. - Version string - - node ethnode.EthNode - payout string - stopCh chan struct{} - waitCh chan error -} - -// Whitelist a client for this host. -func (h *Host) Whitelist(ctx context.Context, nodeID string) error { - logger.Printf("Received whitelist request: %s", nodeID) - return h.node.AddTrustedPeer(ctx, nodeID) -} - -// Disconnect a client from this host and remove from whitelist. -func (h *Host) Disconnect(ctx context.Context, nodeID string) error { - logger.Printf("Received disconnect request: %s", nodeID) - if err := h.node.RemoveTrustedPeer(ctx, nodeID); err != nil { - return err - } - return h.node.DisconnectPeer(ctx, nodeID) -} - -func (h *Host) updatePeers(ctx context.Context, p pool.Pool) error { - block, err := h.node.BlockNumber(ctx) - if err != nil { - return err - } - - peers, err := h.node.Peers(ctx) - if err != nil { - return err - } - update, err := p.Update(ctx, pool.UpdateRequest{ - PeerInfo: peers, - BlockNumber: block, - }) - if err != nil { - return err - } - if len(update.InvalidPeers) == 0 { - logger.Printf("Sent update: %d peers. Pool response: %s", len(peers), update.Balance.String()) - return nil - } - logger.Printf("Sent update: %d peers. Pool response: Disconnect from %d invalid peers, %s", len(peers), len(update.InvalidPeers), update.Balance.String()) - for _, peerID := range update.InvalidPeers { - // FIXME: Are there recoverable errors here? - if err := h.node.RemoveTrustedPeer(ctx, peerID); err != nil { - return err - } - if err := h.node.DisconnectPeer(ctx, peerID); err != nil { - return err - } - } - return nil -} - -// Stop will terminate the update peers loop, which will cause Start to return. -func (h *Host) Stop() { - h.stopCh <- struct{}{} -} - -// Wait blocks until the host is stopped. It returns any errors that occur -// during stopping. -func (h *Host) Wait() error { - return <-h.waitCh -} - -// Start registers the host on the given pool and starts sending peer updates -// every store.KeepaliveInterval. It returns after -// successfully registering with the pool. -func (h *Host) Start(p pool.Pool) error { - startCtx, cancel := context.WithTimeout(context.Background(), startTimeout) - defer cancel() - - enode, err := h.node.Enode(startCtx) - if err != nil { - return err - } - logger.Printf("Connected to local node: %s", enode) - - connectReq := pool.ConnectRequest{ - Payout: h.payout, - NodeURI: h.NodeURI, - VipnodeVersion: h.Version, - NodeInfo: h.node.UserAgent(), - } - resp, err := p.Connect(startCtx, connectReq) - if err != nil { - return err - } - logger.Printf("Registered on pool: Version %s", resp.PoolVersion) - - // TODO: Resume tracking peers that we care about (in case of interrupted - // shutdown)? - - if err := h.updatePeers(startCtx, p); err != nil { - return err - } - - go func() { - h.waitCh <- h.serveUpdates(p) - }() - return nil -} - -// ConnectPeers requests num host peers from the pool. The host will whitelist -// and connect to them. This is useful for increasing full node peering for -// your node with other nodes under the same pool. -func (h *Host) ConnectPeers(p pool.Pool, num int) error { - ctx := context.Background() - resp, err := p.Peer(ctx, pool.PeerRequest{ - Num: num, - // Kind is unspecified, since hosts are happy with any kind of full node. - }) - if err != nil { - return err - } - nodes := resp.Peers - if len(nodes) == 0 { - return pool.NoHostNodesError{} - } - logger.Printf("Received %d host candidates from pool, connecting...", len(nodes)) - for _, node := range nodes { - if err := h.node.ConnectPeer(ctx, node.URI); err != nil { - return err - } - } - return nil -} - -func (h *Host) serveUpdates(p pool.Pool) error { - ticker := time.Tick(store.KeepaliveInterval) - for { - select { - case <-ticker: - ctx, cancel := context.WithTimeout(context.Background(), updateTimeout) - err := h.updatePeers(ctx, p) - cancel() - if err != nil { - return err - } - case <-h.stopCh: - return nil - } - } -} diff --git a/host/logger.go b/host/logger.go deleted file mode 100644 index 99bf116..0000000 --- a/host/logger.go +++ /dev/null @@ -1,20 +0,0 @@ -package host - -import ( - "io" - "io/ioutil" - "log" -) - -var logger *log.Logger - -// SetLogger overrides the logger output for this package. -func SetLogger(w io.Writer) { - flags := log.Flags() - prefix := "[host] " - logger = log.New(w, prefix, flags) -} - -func init() { - SetLogger(ioutil.Discard) -} diff --git a/internal/fakecluster/fakecluster.go b/internal/fakecluster/fakecluster.go index eb4b043..3e3ad6e 100644 --- a/internal/fakecluster/fakecluster.go +++ b/internal/fakecluster/fakecluster.go @@ -7,34 +7,26 @@ import ( "strings" "github.com/ethereum/go-ethereum/p2p/discv5" - "github.com/vipnode/vipnode/client" - "github.com/vipnode/vipnode/host" + "github.com/vipnode/vipnode/agent" "github.com/vipnode/vipnode/internal/fakenode" "github.com/vipnode/vipnode/jsonrpc2" "github.com/vipnode/vipnode/pool" "github.com/vipnode/vipnode/pool/store/memory" ) -type clusterHost struct { - *host.Host - Node *fakenode.FakeNode - In *jsonrpc2.Remote - Out *jsonrpc2.Remote - Key *ecdsa.PrivateKey -} - -type clusterClient struct { - *client.Client - Node *fakenode.FakeNode - In *jsonrpc2.Remote - Out *jsonrpc2.Remote - Key *ecdsa.PrivateKey +type clusterAgent struct { + *agent.Agent + Node *fakenode.FakeNode + RemotePool pool.Pool + In *jsonrpc2.Remote + Out *jsonrpc2.Remote + Key *ecdsa.PrivateKey } // Cluster represents a set of active hosts and clients connected to a pool. type Cluster struct { - Clients []clusterClient - Hosts []clusterHost + Clients []clusterAgent + Hosts []clusterAgent Pool *pool.VipnodePool pipes []io.Closer @@ -43,8 +35,8 @@ type Cluster struct { // New returns a pre-connected pool of hosts and clients. func New(hostKeys []*ecdsa.PrivateKey, clientKeys []*ecdsa.PrivateKey) (*Cluster, error) { cluster := &Cluster{ - Hosts: []clusterHost{}, - Clients: []clusterClient{}, + Hosts: []clusterAgent{}, + Clients: []clusterAgent{}, pipes: []io.Closer{}, } @@ -60,7 +52,7 @@ func New(hostKeys []*ecdsa.PrivateKey, clientKeys []*ecdsa.PrivateKey) (*Cluster hostNodeID := discv5.PubkeyID(&hostKey.PublicKey).String() hostNode := fakenode.Node(hostNodeID) hostNodeURI := fmt.Sprintf("enode://%s@127.0.0.1:30303", hostNodeID) - h := host.New(hostNode, payout) + h := &agent.Agent{EthNode: hostNode, Payout: payout} if err := rpcHost2Pool.Server.RegisterMethod("vipnode_whitelist", h, "Whitelist"); err != nil { return nil, err } @@ -71,12 +63,13 @@ func New(hostKeys []*ecdsa.PrivateKey, clientKeys []*ecdsa.PrivateKey) (*Cluster return nil, err } - cluster.Hosts = append(cluster.Hosts, clusterHost{ - Host: h, - Node: hostNode, - In: rpcPool2Host, - Out: rpcHost2Pool, - Key: hostKey, + cluster.Hosts = append(cluster.Hosts, clusterAgent{ + Agent: h, + Node: hostNode, + In: rpcPool2Host, + Out: rpcHost2Pool, + Key: hostKey, + RemotePool: hostPool, }) } @@ -87,17 +80,21 @@ func New(hostKeys []*ecdsa.PrivateKey, clientKeys []*ecdsa.PrivateKey) (*Cluster clientNodeID := discv5.PubkeyID(&clientKey.PublicKey).String() clientNode := fakenode.Node(clientNodeID) - c := client.New(clientNode) + c := &agent.Agent{ + EthNode: clientNode, + NumHosts: 3, + } clientPool := pool.Remote(rpcClient2Pool, clientKey) if err := c.Start(clientPool); err != nil { return nil, err } - cluster.Clients = append(cluster.Clients, clusterClient{ - Client: c, - Node: clientNode, - In: rpcPool2Client, - Out: rpcClient2Pool, - Key: clientKey, + cluster.Clients = append(cluster.Clients, clusterAgent{ + Agent: c, + Node: clientNode, + In: rpcPool2Client, + Out: rpcClient2Pool, + Key: clientKey, + RemotePool: clientPool, }) } return cluster, nil diff --git a/main.go b/main.go index 0a9e800..c696e48 100644 --- a/main.go +++ b/main.go @@ -20,9 +20,8 @@ import ( "github.com/alexcesaro/log/golog" "github.com/ethereum/go-ethereum/crypto" flags "github.com/jessevdk/go-flags" - "github.com/vipnode/vipnode/client" + "github.com/vipnode/vipnode/agent" "github.com/vipnode/vipnode/ethnode" - "github.com/vipnode/vipnode/host" "github.com/vipnode/vipnode/internal/fakenode" "github.com/vipnode/vipnode/internal/pretty" "github.com/vipnode/vipnode/jsonrpc2" @@ -282,8 +281,7 @@ func main() { if logLevel == log.Debug { // Enable logging from subpackages pool.SetLogger(logWriter) - client.SetLogger(logWriter) - host.SetLogger(logWriter) + agent.SetLogger(logWriter) payment.SetLogger(logWriter) ethnode.SetLogger(logWriter) jsonrpc2.SetLogger(logWriter) diff --git a/poolhostclient_test.go b/poolhostclient_test.go index 2737895..96ab0b8 100644 --- a/poolhostclient_test.go +++ b/poolhostclient_test.go @@ -10,8 +10,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/p2p/discv5" - "github.com/vipnode/vipnode/client" - "github.com/vipnode/vipnode/host" + "github.com/vipnode/vipnode/agent" "github.com/vipnode/vipnode/internal/fakecluster" "github.com/vipnode/vipnode/internal/fakenode" "github.com/vipnode/vipnode/internal/keygen" @@ -35,8 +34,8 @@ func TestPoolHostClient(t *testing.T) { hostNodeID := discv5.PubkeyID(&privkey.PublicKey).String() hostNode := fakenode.Node(hostNodeID) hostNodeURI := fmt.Sprintf("enode://%s@127.0.0.1:30303", hostNodeID) - h := host.New(hostNode, payout) - if err := rpcHost2Pool.Server.RegisterMethod("vipnode_whitelist", h, "Whitelist"); err != nil { + h := agent.Agent{EthNode: hostNode, Payout: payout} + if err := rpcHost2Pool.Server.RegisterMethod("vipnode_whitelist", &h, "Whitelist"); err != nil { t.Fatalf("failed to register vipnode_ rpc for host: %s", err) } h.NodeURI = hostNodeURI @@ -62,7 +61,10 @@ func TestPoolHostClient(t *testing.T) { clientNodeID := discv5.PubkeyID(&clientPrivkey.PublicKey).String() clientNode := fakenode.Node(clientNodeID) clientNode.IsFullNode = false - c := client.New(clientNode) + c := agent.Agent{ + EthNode: clientNode, + NumHosts: 3, + } clientPool := pool.Remote(rpcClient2Pool, clientPrivkey) if err := c.Start(clientPool); err != nil { t.Fatalf("failed to start client: %s", err) @@ -86,7 +88,6 @@ func TestPoolHostClient(t *testing.T) { } want = fakenode.Calls{ fakenode.Call("ConnectPeer", hostNodeURI), - fakenode.Call("DisconnectPeer", hostNodeID), } if got := clientNode.Calls; !reflect.DeepEqual(got, want) { t.Errorf("clientNode.Calls:\n got %q;\n want %q", got, want) @@ -98,7 +99,6 @@ func TestPoolHostClient(t *testing.T) { } want = fakenode.Calls{ fakenode.Call("ConnectPeer", hostNodeURI), - fakenode.Call("DisconnectPeer", hostNodeID), fakenode.Call("ConnectPeer", hostNodeURI), } if got := clientNode.Calls; !reflect.DeepEqual(got, want) { @@ -145,8 +145,8 @@ func TestCloseHost(t *testing.T) { hostNodeID := discv5.PubkeyID(&privkey.PublicKey).String() hostNode := fakenode.Node(hostNodeID) hostNodeURI := fmt.Sprintf("enode://%s@127.0.0.1", hostNodeID) - h := host.New(hostNode, payout) - if err := rpcHost2Pool.Server.RegisterMethod("vipnode_whitelist", h, "Whitelist"); err != nil { + h := agent.Agent{EthNode: hostNode, Payout: payout} + if err := rpcHost2Pool.Server.RegisterMethod("vipnode_whitelist", &h, "Whitelist"); err != nil { t.Fatalf("failed to register vipnode_ rpc for host: %s", err) } h.NodeURI = hostNodeURI @@ -204,10 +204,10 @@ func TestPoolHostConnectPeers(t *testing.T) { } else if len(peers) > 0 { t.Errorf("host has unexpected peers: %s", peers) } - hostPool := pool.Remote(host.Out, host.Key) - if err := host.ConnectPeers(hostPool, numHosts); err != nil { - t.Error(err) + // Request peers + if err := host.AddPeers(context.Background(), host.RemotePool, 10); err != nil { + t.Fatal(err) } if peers, err := host.Node.Peers(context.Background()); err != nil { From 09739f98f21cdd829bc387edc4bf040cf9d6ebc7 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Wed, 22 May 2019 10:42:04 -0400 Subject: [PATCH 43/43] agent, pool: Get rid of some obsolete FIXMEs --- agent/agent.go | 7 ------- pool/service.go | 3 --- 2 files changed, 10 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index e66dbae..d782871 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -135,14 +135,7 @@ func (a *Agent) serveUpdates(p pool.Pool) error { for { select { case <-ticker: - // FIXME: Sametimes a.updatePeers could take a while, does it make - // sense to keep ticking every interval regardless? Or should we - // make sure there is an offset between calls? What if there's - // overlap, do we want a mutex? if err := a.updatePeers(context.Background(), p); err != nil { - // FIXME: Does it make sense to continue updating for certain - // errors? Eg if no hosts are found, we could keep sending - // updates until we find some. return err } case <-a.stopCh: diff --git a/pool/service.go b/pool/service.go index e0a6766..980f27e 100644 --- a/pool/service.go +++ b/pool/service.go @@ -170,9 +170,6 @@ func (p *VipnodePool) Update(ctx context.Context, sig string, nodeID string, non return nil, err } - // FIXME: Is there a bug here when a host is connected to another host? - // TODO: Test InvalidPeers - nodeBalance, err := p.BalanceManager.OnUpdate(nodeBeforeUpdate, validPeers) if err != nil { if _, ok := err.(balance.LowBalanceError); ok {