privval: add grpc (#5725)

Co-authored-by: Anton Kaliaev <anton.kalyaev@gmail.com>
This commit is contained in:
Marko
2021-01-06 10:49:30 -08:00
committed by GitHub
co-authored by Anton Kaliaev
parent e986602649
commit 09cf0bcb01
25 changed files with 1233 additions and 45 deletions
+108
View File
@@ -0,0 +1,108 @@
package grpc
import (
"context"
"time"
grpc "google.golang.org/grpc"
"google.golang.org/grpc/status"
"github.com/tendermint/tendermint/crypto"
cryptoenc "github.com/tendermint/tendermint/crypto/encoding"
"github.com/tendermint/tendermint/libs/log"
privvalproto "github.com/tendermint/tendermint/proto/tendermint/privval"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
"github.com/tendermint/tendermint/types"
)
// SignerClient implements PrivValidator.
// Handles remote validator connections that provide signing services
type SignerClient struct {
logger log.Logger
client privvalproto.PrivValidatorAPIClient
conn *grpc.ClientConn
chainID string
}
var _ types.PrivValidator = (*SignerClient)(nil)
// NewSignerClient returns an instance of SignerClient.
// it will start the endpoint (if not already started)
func NewSignerClient(conn *grpc.ClientConn,
chainID string, log log.Logger) (*SignerClient, error) {
sc := &SignerClient{
logger: log,
chainID: chainID,
client: privvalproto.NewPrivValidatorAPIClient(conn), // Create the Private Validator Client
}
return sc, nil
}
// Close closes the underlying connection
func (sc *SignerClient) Close() error {
sc.logger.Info("Stopping service")
if sc.conn != nil {
return sc.conn.Close()
}
return nil
}
//--------------------------------------------------------
// Implement PrivValidator
// GetPubKey retrieves a public key from a remote signer
// returns an error if client is not able to provide the key
func (sc *SignerClient) GetPubKey() (crypto.PubKey, error) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // Todo: should this be configurable?
defer cancel()
resp, err := sc.client.GetPubKey(ctx, &privvalproto.PubKeyRequest{ChainId: sc.chainID})
if err != nil {
errStatus, _ := status.FromError(err)
sc.logger.Error("SignerClient::GetPubKey", "err", errStatus.Message())
return nil, errStatus.Err()
}
pk, err := cryptoenc.PubKeyFromProto(resp.PubKey)
if err != nil {
return nil, err
}
return pk, nil
}
// SignVote requests a remote signer to sign a vote
func (sc *SignerClient) SignVote(chainID string, vote *tmproto.Vote) error {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
resp, err := sc.client.SignVote(ctx, &privvalproto.SignVoteRequest{ChainId: sc.chainID, Vote: vote})
if err != nil {
errStatus, _ := status.FromError(err)
sc.logger.Error("Client SignVote", "err", errStatus.Message())
return errStatus.Err()
}
*vote = resp.Vote
return nil
}
// SignProposal requests a remote signer to sign a proposal
func (sc *SignerClient) SignProposal(chainID string, proposal *tmproto.Proposal) error {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
resp, err := sc.client.SignProposal(
ctx, &privvalproto.SignProposalRequest{ChainId: chainID, Proposal: proposal})
if err != nil {
errStatus, _ := status.FromError(err)
sc.logger.Error("SignerClient::SignProposal", "err", errStatus.Message())
return errStatus.Err()
}
*proposal = resp.Proposal
return nil
}
+168
View File
@@ -0,0 +1,168 @@
package grpc_test
import (
"context"
"net"
"testing"
"time"
grpc "google.golang.org/grpc"
"google.golang.org/grpc/test/bufconn"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/tmhash"
"github.com/tendermint/tendermint/libs/log"
tmrand "github.com/tendermint/tendermint/libs/rand"
tmgrpc "github.com/tendermint/tendermint/privval/grpc"
privvalproto "github.com/tendermint/tendermint/proto/tendermint/privval"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
"github.com/tendermint/tendermint/types"
)
const chainID = "chain-id"
func dialer(pv types.PrivValidator, logger log.Logger) (*grpc.Server, func(context.Context, string) (net.Conn, error)) {
listener := bufconn.Listen(1024 * 1024)
server := grpc.NewServer()
s := tmgrpc.NewSignerServer(chainID, pv, logger)
privvalproto.RegisterPrivValidatorAPIServer(server, s)
go func() {
if err := server.Serve(listener); err != nil {
panic(err)
}
}()
return server, func(context.Context, string) (net.Conn, error) {
return listener.Dial()
}
}
func TestSignerClient_GetPubKey(t *testing.T) {
ctx := context.Background()
mockPV := types.NewMockPV()
logger := log.TestingLogger()
srv, dialer := dialer(mockPV, logger)
defer srv.Stop()
conn, err := grpc.DialContext(ctx, "", grpc.WithInsecure(), grpc.WithContextDialer(dialer))
if err != nil {
panic(err)
}
defer conn.Close()
client, err := tmgrpc.NewSignerClient(conn, chainID, logger)
require.NoError(t, err)
pk, err := client.GetPubKey()
require.NoError(t, err)
assert.Equal(t, mockPV.PrivKey.PubKey(), pk)
}
func TestSignerClient_SignVote(t *testing.T) {
ctx := context.Background()
mockPV := types.NewMockPV()
logger := log.TestingLogger()
srv, dialer := dialer(mockPV, logger)
defer srv.Stop()
conn, err := grpc.DialContext(ctx, "", grpc.WithInsecure(), grpc.WithContextDialer(dialer))
if err != nil {
panic(err)
}
defer conn.Close()
client, err := tmgrpc.NewSignerClient(conn, chainID, logger)
require.NoError(t, err)
ts := time.Now()
hash := tmrand.Bytes(tmhash.Size)
valAddr := tmrand.Bytes(crypto.AddressSize)
want := &types.Vote{
Type: tmproto.PrecommitType,
Height: 1,
Round: 2,
BlockID: types.BlockID{Hash: hash, PartSetHeader: types.PartSetHeader{Hash: hash, Total: 2}},
Timestamp: ts,
ValidatorAddress: valAddr,
ValidatorIndex: 1,
}
have := &types.Vote{
Type: tmproto.PrecommitType,
Height: 1,
Round: 2,
BlockID: types.BlockID{Hash: hash, PartSetHeader: types.PartSetHeader{Hash: hash, Total: 2}},
Timestamp: ts,
ValidatorAddress: valAddr,
ValidatorIndex: 1,
}
pbHave := have.ToProto()
err = client.SignVote(chainID, pbHave)
require.NoError(t, err)
pbWant := want.ToProto()
require.NoError(t, mockPV.SignVote(chainID, pbWant))
assert.Equal(t, pbWant.Signature, pbHave.Signature)
}
func TestSignerClient_SignProposal(t *testing.T) {
ctx := context.Background()
mockPV := types.NewMockPV()
logger := log.TestingLogger()
srv, dialer := dialer(mockPV, logger)
defer srv.Stop()
conn, err := grpc.DialContext(ctx, "", grpc.WithInsecure(), grpc.WithContextDialer(dialer))
if err != nil {
panic(err)
}
defer conn.Close()
client, err := tmgrpc.NewSignerClient(conn, chainID, logger)
require.NoError(t, err)
ts := time.Now()
hash := tmrand.Bytes(tmhash.Size)
have := &types.Proposal{
Type: tmproto.ProposalType,
Height: 1,
Round: 2,
POLRound: 2,
BlockID: types.BlockID{Hash: hash, PartSetHeader: types.PartSetHeader{Hash: hash, Total: 2}},
Timestamp: ts,
}
want := &types.Proposal{
Type: tmproto.ProposalType,
Height: 1,
Round: 2,
POLRound: 2,
BlockID: types.BlockID{Hash: hash, PartSetHeader: types.PartSetHeader{Hash: hash, Total: 2}},
Timestamp: ts,
}
pbHave := have.ToProto()
err = client.SignProposal(chainID, pbHave)
require.NoError(t, err)
pbWant := want.ToProto()
require.NoError(t, mockPV.SignProposal(chainID, pbWant))
assert.Equal(t, pbWant.Signature, pbHave.Signature)
}
+87
View File
@@ -0,0 +1,87 @@
package grpc
import (
context "context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/tendermint/tendermint/crypto"
cryptoenc "github.com/tendermint/tendermint/crypto/encoding"
"github.com/tendermint/tendermint/libs/log"
privvalproto "github.com/tendermint/tendermint/proto/tendermint/privval"
"github.com/tendermint/tendermint/types"
)
// SignerServer implements PrivValidatorAPIServer 9generated via protobuf services)
// Handles remote validator connections that provide signing services
type SignerServer struct {
logger log.Logger
chainID string
privVal types.PrivValidator
}
func NewSignerServer(chainID string,
privVal types.PrivValidator, log log.Logger) *SignerServer {
return &SignerServer{
logger: log,
chainID: chainID,
privVal: privVal,
}
}
var _ privvalproto.PrivValidatorAPIServer = (*SignerServer)(nil)
// PubKey receives a request for the pubkey
// returns the pubkey on success and error on failure
func (ss *SignerServer) GetPubKey(ctx context.Context, req *privvalproto.PubKeyRequest) (
*privvalproto.PubKeyResponse, error) {
var pubKey crypto.PubKey
pubKey, err := ss.privVal.GetPubKey()
if err != nil {
return nil, status.Errorf(codes.NotFound, "error getting pubkey: %v", err)
}
pk, err := cryptoenc.PubKeyToProto(pubKey)
if err != nil {
return nil, status.Errorf(codes.Internal, "error transitioning pubkey to proto: %v", err)
}
ss.logger.Info("SignerServer: GetPubKey Success")
return &privvalproto.PubKeyResponse{PubKey: pk}, nil
}
// SignVote receives a vote sign requests, attempts to sign it
// returns SignedVoteResponse on success and error on failure
func (ss *SignerServer) SignVote(ctx context.Context, req *privvalproto.SignVoteRequest) (
*privvalproto.SignedVoteResponse, error) {
vote := req.Vote
err := ss.privVal.SignVote(req.ChainId, vote)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error signing vote: %v", err)
}
ss.logger.Info("SignerServer: SignVote Success")
return &privvalproto.SignedVoteResponse{Vote: *vote}, nil
}
// SignProposal receives a proposal sign requests, attempts to sign it
// returns SignedProposalResponse on success and error on failure
func (ss *SignerServer) SignProposal(ctx context.Context, req *privvalproto.SignProposalRequest) (
*privvalproto.SignedProposalResponse, error) {
proposal := req.Proposal
err := ss.privVal.SignProposal(req.ChainId, proposal)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error signing proposal: %v", err)
}
ss.logger.Info("SignerServer: SignProposal Success")
return &privvalproto.SignedProposalResponse{Proposal: *proposal}, nil
}
+187
View File
@@ -0,0 +1,187 @@
package grpc_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/tmhash"
"github.com/tendermint/tendermint/libs/log"
tmrand "github.com/tendermint/tendermint/libs/rand"
tmgrpc "github.com/tendermint/tendermint/privval/grpc"
privvalproto "github.com/tendermint/tendermint/proto/tendermint/privval"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
"github.com/tendermint/tendermint/types"
)
const ChainID = "123"
func TestGetPubKey(t *testing.T) {
testCases := []struct {
name string
pv types.PrivValidator
err bool
}{
{name: "valid", pv: types.NewMockPV(), err: false},
{name: "error on pubkey", pv: types.NewErroringMockPV(), err: true},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
s := tmgrpc.NewSignerServer(ChainID, tc.pv, log.TestingLogger())
req := &privvalproto.PubKeyRequest{ChainId: ChainID}
resp, err := s.GetPubKey(context.Background(), req)
if tc.err {
require.Error(t, err)
} else {
pk, err := tc.pv.GetPubKey()
require.NoError(t, err)
assert.Equal(t, resp.PubKey.GetEd25519(), pk.Bytes())
}
})
}
}
func TestSignVote(t *testing.T) {
ts := time.Now()
hash := tmrand.Bytes(tmhash.Size)
valAddr := tmrand.Bytes(crypto.AddressSize)
testCases := []struct {
name string
pv types.PrivValidator
have, want *types.Vote
err bool
}{
{name: "valid", pv: types.NewMockPV(), have: &types.Vote{
Type: tmproto.PrecommitType,
Height: 1,
Round: 2,
BlockID: types.BlockID{Hash: hash, PartSetHeader: types.PartSetHeader{Hash: hash, Total: 2}},
Timestamp: ts,
ValidatorAddress: valAddr,
ValidatorIndex: 1,
}, want: &types.Vote{
Type: tmproto.PrecommitType,
Height: 1,
Round: 2,
BlockID: types.BlockID{Hash: hash, PartSetHeader: types.PartSetHeader{Hash: hash, Total: 2}},
Timestamp: ts,
ValidatorAddress: valAddr,
ValidatorIndex: 1,
},
err: false},
{name: "invalid vote", pv: types.NewErroringMockPV(), have: &types.Vote{
Type: tmproto.PrecommitType,
Height: 1,
Round: 2,
BlockID: types.BlockID{Hash: hash, PartSetHeader: types.PartSetHeader{Hash: hash, Total: 2}},
Timestamp: ts,
ValidatorAddress: valAddr,
ValidatorIndex: 1,
Signature: []byte("signed"),
}, want: &types.Vote{
Type: tmproto.PrecommitType,
Height: 1,
Round: 2,
BlockID: types.BlockID{Hash: hash, PartSetHeader: types.PartSetHeader{Hash: hash, Total: 2}},
Timestamp: ts,
ValidatorAddress: valAddr,
ValidatorIndex: 1,
Signature: []byte("signed"),
},
err: true},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
s := tmgrpc.NewSignerServer(ChainID, tc.pv, log.TestingLogger())
req := &privvalproto.SignVoteRequest{ChainId: ChainID, Vote: tc.have.ToProto()}
resp, err := s.SignVote(context.Background(), req)
if tc.err {
require.Error(t, err)
} else {
pbVote := tc.want.ToProto()
require.NoError(t, tc.pv.SignVote(ChainID, pbVote))
assert.Equal(t, pbVote.Signature, resp.Vote.Signature)
}
})
}
}
func TestSignProposal(t *testing.T) {
ts := time.Now()
hash := tmrand.Bytes(tmhash.Size)
testCases := []struct {
name string
pv types.PrivValidator
have, want *types.Proposal
err bool
}{
{name: "valid", pv: types.NewMockPV(), have: &types.Proposal{
Type: tmproto.ProposalType,
Height: 1,
Round: 2,
POLRound: 2,
BlockID: types.BlockID{Hash: hash, PartSetHeader: types.PartSetHeader{Hash: hash, Total: 2}},
Timestamp: ts,
}, want: &types.Proposal{
Type: tmproto.ProposalType,
Height: 1,
Round: 2,
POLRound: 2,
BlockID: types.BlockID{Hash: hash, PartSetHeader: types.PartSetHeader{Hash: hash, Total: 2}},
Timestamp: ts,
},
err: false},
{name: "invalid proposal", pv: types.NewErroringMockPV(), have: &types.Proposal{
Type: tmproto.ProposalType,
Height: 1,
Round: 2,
POLRound: 2,
BlockID: types.BlockID{Hash: hash, PartSetHeader: types.PartSetHeader{Hash: hash, Total: 2}},
Timestamp: ts,
Signature: []byte("signed"),
}, want: &types.Proposal{
Type: tmproto.ProposalType,
Height: 1,
Round: 2,
POLRound: 2,
BlockID: types.BlockID{Hash: hash, PartSetHeader: types.PartSetHeader{Hash: hash, Total: 2}},
Timestamp: ts,
Signature: []byte("signed"),
},
err: true},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
s := tmgrpc.NewSignerServer(ChainID, tc.pv, log.TestingLogger())
req := &privvalproto.SignProposalRequest{ChainId: ChainID, Proposal: tc.have.ToProto()}
resp, err := s.SignProposal(context.Background(), req)
if tc.err {
require.Error(t, err)
} else {
pbProposal := tc.want.ToProto()
require.NoError(t, tc.pv.SignProposal(ChainID, pbProposal))
assert.Equal(t, pbProposal.Signature, resp.Proposal.Signature)
}
})
}
}
+82
View File
@@ -0,0 +1,82 @@
package grpc
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"os"
"time"
grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
"github.com/tendermint/tendermint/libs/log"
grpc "google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/keepalive"
)
// DefaultDialOptions constructs a list of grpc dial options
func DefaultDialOptions(
extraOpts ...grpc.DialOption,
) []grpc.DialOption {
const (
retries = 50 // 50 * 100ms = 5s total
timeout = 1 * time.Second
maxCallRecvMsgSize = 1 << 20 // Default 5Mb
)
var kacp = keepalive.ClientParameters{
Time: 10 * time.Second, // send pings every 10 seconds if there is no activity
Timeout: 2 * time.Second, // wait 2 seconds for ping ack before considering the connection dead
}
opts := []grpc_retry.CallOption{
grpc_retry.WithBackoff(grpc_retry.BackoffExponential(timeout)),
}
dialOpts := []grpc.DialOption{
grpc.WithKeepaliveParams(kacp),
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(maxCallRecvMsgSize),
grpc_retry.WithMax(retries),
),
grpc.WithUnaryInterceptor(
grpc_retry.UnaryClientInterceptor(opts...),
),
}
dialOpts = append(dialOpts, extraOpts...)
return dialOpts
}
func GenerateTLS(certPath, keyPath, ca string, log log.Logger) grpc.DialOption {
certificate, err := tls.LoadX509KeyPair(
certPath,
keyPath,
)
if err != nil {
log.Error("error", err)
os.Exit(1)
}
certPool := x509.NewCertPool()
bs, err := ioutil.ReadFile(ca)
if err != nil {
log.Error("failed to read ca cert:", "error", err)
os.Exit(1)
}
ok := certPool.AppendCertsFromPEM(bs)
if !ok {
log.Error("failed to append certs")
os.Exit(1)
}
transportCreds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{certificate},
RootCAs: certPool,
MinVersion: tls.VersionTLS13,
})
return grpc.WithTransportCredentials(transportCreds)
}