Skip to content

Commit

Permalink
Add RPC client (filecoin-project#103)
Browse files Browse the repository at this point in the history
* add json rpc client

* support onchain reconfiguration

* use file membership by default

* add compatibility test
  • Loading branch information
dnkolegov committed Mar 9, 2023
1 parent 553f30a commit c6bc7db
Show file tree
Hide file tree
Showing 11 changed files with 346 additions and 16 deletions.
28 changes: 28 additions & 0 deletions chain/consensus/mir/validator/membership.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package validator

import (
"github.com/filecoin-project/lotus/chain/ipcagent/rpc"
)

type FileMembership struct {
FileName string
}
Expand Down Expand Up @@ -32,3 +36,27 @@ type EnvMembership string
func (e EnvMembership) GetValidatorSet() (*Set, error) {
return NewValidatorSetFromEnv(string(e))
}

// -----

const JSONRPCServerURL = "http://127.0.0.1:3030"

type ActorMembership struct {
client rpc.JSONRPCRequestSender
}

func NewActorMembershipClient(client rpc.JSONRPCRequestSender) *ActorMembership {
return &ActorMembership{
client: client,
}
}

// GetValidatorSet gets the membership config from the actor state.
func (c *ActorMembership) GetValidatorSet() (*Set, error) {
var set Set
err := c.client.SendRequest("ipc_queryValidatorSet", nil, &set)
if err != nil {
return nil, err
}
return &set, err
}
1 change: 0 additions & 1 deletion chain/consensus/mir/validator/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ type Validator struct {
}

// NewValidatorFromString parses a validator address from the string.
// OpaqueNetAddr can contain GRPC or libp2p addresses.
//
// Examples of validator strings:
// - t1wpixt5mihkj75lfhrnaa6v56n27epvlgwparujy@/ip4/127.0.0.1/tcp/10000/p2p/12D3KooWJhKBXvytYgPCAaiRtiNLJNSFG5jreKDu2jiVpJetzvVJ
Expand Down
2 changes: 2 additions & 0 deletions chain/ipcagent/ipcagent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package ipcagent maintains IPC Agent client.
package ipcagent
87 changes: 87 additions & 0 deletions chain/ipcagent/rpc/rpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package rpc

import (
"bytes"
"context"
"fmt"
"net/http"

rpc "github.com/gorilla/rpc/v2/json2"
)

type JSONRPCRequestSender interface {
SendRequest(method string, params interface{}, reply interface{}) error
}

var _ JSONRPCRequestSender = &JSONRPCClient{}

type JSONRPCClient struct {
ctx context.Context
token string
url string
}

func NewJSONRPCClient(url, token string) *JSONRPCClient {
return &JSONRPCClient{
ctx: context.Background(),
token: token,
url: url,
}
}

// NewInsecureJSONRPCClient creates a JSON RPC client with empty credentials.
func NewInsecureJSONRPCClient(url string) *JSONRPCClient {
return &JSONRPCClient{
ctx: context.Background(),
token: "",
url: url,
}
}

func NewJSONRPCClientWithContext(ctx context.Context, url, token string) *JSONRPCClient {
return &JSONRPCClient{
ctx: ctx,
token: token,
url: url,
}
}

func (c *JSONRPCClient) SendRequest(method string, params interface{}, reply interface{}) error {
paramBytes, err := rpc.EncodeClientRequest(method, params)
if err != nil {
return fmt.Errorf("failed to encode client params: %w", err)
}

req, err := http.NewRequestWithContext(
c.ctx,
"POST",
c.url,
bytes.NewBuffer(paramBytes),
)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if c.token != "" {
req.Header.Add("Authorization", "Bearer "+c.token)
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("failed to issue request: %w", err)
}

// Return an error for any nonsuccessful status code
if resp.StatusCode < 200 || resp.StatusCode > 299 {
// Drop any error during close to report the original error
_ = resp.Body.Close()
return fmt.Errorf("received status code: %d", resp.StatusCode)
}

if err := rpc.DecodeClientResponse(resp.Body, reply); err != nil {
// Drop any error during close to report the original error
_ = resp.Body.Close()
return fmt.Errorf("failed to decode client response: %w", err)
}
return resp.Body.Close()
}
132 changes: 132 additions & 0 deletions chain/ipcagent/rpc/rpc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package rpc

import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/gorilla/rpc/v2"
"github.com/gorilla/rpc/v2/json2"
"github.com/stretchr/testify/require"

addr "github.com/filecoin-project/go-address"
)

type TestService struct{}

type TestServiceRequest struct {
A int
B int
}

type TestServiceResponse struct {
O int
}

func (t *TestService) Multiply(_ *http.Request, req *TestServiceRequest, res *TestServiceResponse) error {
res.O = req.A * req.B
return nil
}

func TestClient(t *testing.T) {
s := rpc.NewServer()
s.RegisterCodec(json2.NewCodec(), "application/json")
err := s.RegisterService(new(TestService), "")
require.NoError(t, err)
require.Equal(t, true, s.HasMethod("TestService.Multiply"))

token := "token123"

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "POST", r.Method)
require.Equal(t, "application/json", r.Header.Get("Content-Type"))
require.Equal(t, "Bearer "+token, r.Header.Get("Authorization"))

s.ServeHTTP(w, r)
}))
defer srv.Close()

c := NewJSONRPCClient(srv.URL, token)

var resp *TestServiceResponse
err = c.SendRequest("TestService.Multiply", &TestServiceRequest{A: 1, B: 4}, &resp)
require.NoError(t, err)
require.Equal(t, 4, resp.O)
}

type Validator struct {
Addr addr.Address `json:"addr"`
NetAddr string `json:"net_addr"`
W int64 `json:"weight"`
}

type Set struct {
ConfigurationNumber uint64 `json:"config_number"`
Validators []Validator `json:"validators"`
}

type confServiceResponse struct {
ValidatorSet Set `json:"validator_set"`
}

func TestClientCompatibleWithIPCAgent(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "POST", r.Method)
require.Equal(t, "application/json", r.Header.Get("Content-Type"))

obj := make(map[string]interface{})

d := json.NewDecoder(r.Body)
err := d.Decode(&obj)
require.NoError(t, err)

require.Equal(t, "2.0", obj["jsonrpc"])
require.NotEqual(t, "", obj["id"])
require.NotEqual(t, "", obj["params"])
require.Equal(t, "ipc_queryValidatorSet", obj["method"])

var result = `
{
"jsonrpc": "2.0",
"result": {
"validator_set": {
"config_number": 22,
"validators": [{
"addr": "f1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq",
"net_addr": "/ip4/127.0.0.1/tcp/38443/p2p/12D3KooWM4Z6tymWBUC9LQ7NNJ2RtzoakV1vDSyzehzC17Dpo367",
"weight": 0
},
{
"addr": "f1akaouty2buxxwb46l27pzrhl3te2lw5jem67xuy",
"net_addr": "/ip4/127.0.0.1/tcp/40315/p2p/12D3KooWD9DHVsaPvBN5H16aWZ9KDChyrDSKVCnZegsJguuwd76E",
"weight": 0
}
]
}
},
"id": "5577006791947779410"
}
`
_, err = fmt.Fprint(w, result)
require.NoError(t, err)
}))
defer srv.Close()

req := struct {
subnet string
tipSet string
}{
subnet: "/root/test",
tipSet: "QmPK1s3pNYLi9ERiq3BDxKa3XosgWwFRQUydHUtz4YgpqB",
}

var resp *confServiceResponse
c := NewJSONRPCClient(srv.URL, "")
err := c.SendRequest("ipc_queryValidatorSet", req, &resp)
require.NoError(t, err)

require.Equal(t, uint64(22), resp.ValidatorSet.ConfigurationNumber)
require.Equal(t, 2, len(resp.ValidatorSet.Validators))
}
38 changes: 26 additions & 12 deletions cmd/eudico/mirvalidator/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

"github.com/filecoin-project/go-address"
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/lotus/chain/consensus/mir/validator"

"github.com/filecoin-project/mir/pkg/checkpoint"
mirlibp2p "github.com/filecoin-project/mir/pkg/net/libp2p"
t "github.com/filecoin-project/mir/pkg/types"
Expand All @@ -23,6 +23,7 @@ import (
"github.com/filecoin-project/lotus/build"
"github.com/filecoin-project/lotus/chain/consensus/mir"
mirkv "github.com/filecoin-project/lotus/chain/consensus/mir/db/kv"
"github.com/filecoin-project/lotus/chain/consensus/mir/validator"
lcli "github.com/filecoin-project/lotus/cli"
"github.com/filecoin-project/lotus/eudico-core/global"
"github.com/filecoin-project/lotus/lib/ulimit"
Expand Down Expand Up @@ -60,6 +61,16 @@ var runCmd = &cli.Command{
Name: "init-checkpoint",
Usage: "pass initial checkpoint as a file (it overwrites 'init-height' flag)",
},
&cli.StringFlag{
Name: "membership",
Usage: "membership type: onchain, file",
Value: "file",
},
&cli.StringFlag{
Name: "membership-file",
Usage: "membership type: onchain, file",
Value: MembershipCfgPath,
},
&cli.StringFlag{
Name: "restore-configuration-number",
Usage: "use persisted configuration number",
Expand Down Expand Up @@ -128,10 +139,6 @@ var runCmd = &cli.Command{
return err
}

// Membership config.
// TODO: Make this configurable.
membershipFile := filepath.Join(cctx.String("repo"), MembershipCfgPath)

// Segment length period.
segmentLength := cctx.Int("segment-length")

Expand Down Expand Up @@ -168,7 +175,14 @@ var runCmd = &cli.Command{
log.Info("Initializing mir validator from checkpoint in height: %d", cctx.Int("init-height"))
}

membership := validator.NewFileMembership(membershipFile)
var membership validator.Reader
switch cctx.String("membership") {
case "file":
mf := filepath.Join(cctx.String("repo"), cctx.String("membership-file"))
membership = validator.NewFileMembership(mf)
default:
return xerrors.Errorf("membership is currently only supported with file")
}

var netLogger = mir.NewLogger(validatorID.String())
netTransport := mirlibp2p.NewTransport(mirlibp2p.DefaultParams(), t.NodeID(validatorID.String()), h, netLogger)
Expand All @@ -185,25 +199,25 @@ var runCmd = &cli.Command{

func validatorIDFromFlag(ctx context.Context, cctx *cli.Context, nodeApi api.FullNode) (address.Address, error) {
var (
validator address.Address
err error
addr address.Address
err error
)

if cctx.Bool("default-key") {
validator, err = nodeApi.WalletDefaultAddress(ctx)
addr, err = nodeApi.WalletDefaultAddress(ctx)
if err != nil {
return address.Undef, err
}
}
if cctx.String("from") != "" {
validator, err = address.NewFromString(cctx.String("from"))
addr, err = address.NewFromString(cctx.String("from"))
if err != nil {
return address.Undef, err
}
}
if validator == address.Undef {
if addr == address.Undef {
return address.Undef, xerrors.Errorf("no validator address specified as first argument for validator")
}

return validator, nil
return addr, nil
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ require (
github.com/golang/mock v1.6.0
github.com/google/uuid v1.3.0
github.com/gorilla/mux v1.8.0
github.com/gorilla/rpc v1.2.0
github.com/gorilla/websocket v1.5.0
github.com/gregdhill/go-openrpc v0.0.0-20220114144539-ae6f44720487
github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,8 @@ github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk=
github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
Expand Down
Loading

0 comments on commit c6bc7db

Please sign in to comment.