mirror of
https://github.com/tendermint/tendermint.git
synced 2026-08-19 05:36:15 +00:00
This PR is related to #3107 and a continuation of #3351
It is important to emphasise that in the privval original design, client/server and listening/dialing roles are inverted and do not follow a conventional interaction.
Given two hosts A and B:
Host A is listener/client
Host B is dialer/server (contains the secret key)
When A requires a signature, it needs to wait for B to dial in before it can issue a request.
A only accepts a single connection and any failure leads to dropping the connection and waiting for B to reconnect.
The original rationale behind this design was based on security.
Host B only allows outbound connections to a list of whitelisted hosts.
It is not possible to reach B unless B dials in. There are no listening/open ports in B.
This PR results in the following changes:
Refactors ping/heartbeat to avoid previously existing race conditions.
Separates transport (dialer/listener) from signing (client/server) concerns to simplify workflow.
Unifies and abstracts away the differences between unix and tcp sockets.
A single signer endpoint implementation unifies connection handling code (read/write/close/connection obj)
The signer request handler (server side) is customizable to increase testability.
Updates and extends unit tests
A high level overview of the classes is as follows:
Transport (endpoints): The following classes take care of establishing a connection
SignerDialerEndpoint
SignerListeningEndpoint
SignerEndpoint groups common functionality (read/write/timeouts/etc.)
Signing (client/server): The following classes take care of exchanging request/responses
SignerClient
SignerServer
This PR also closes #3601
Commits:
* refactoring - work in progress
* reworking unit tests
* Encapsulating and fixing unit tests
* Improve tests
* Clean up
* Fix/improve unit tests
* clean up tests
* Improving service endpoint
* fixing unit test
* fix linter issues
* avoid invalid cache values (improve later?)
* complete implementation
* wip
* improved connection loop
* Improve reconnections + fixing unit tests
* addressing comments
* small formatting changes
* clean up
* Update node/node.go
Co-Authored-By: jleni <juan.leni@zondax.ch>
* Update privval/signer_client.go
Co-Authored-By: jleni <juan.leni@zondax.ch>
* Update privval/signer_client_test.go
Co-Authored-By: jleni <juan.leni@zondax.ch>
* check during initialization
* dropping connecting when writing fails
* removing break
* use t.log instead
* unifying and using cmn.GetFreePort()
* review fixes
* reordering and unifying drop connection
* closing instead of signalling
* refactored service loop
* removed superfluous brackets
* GetPubKey can return errors
* Revert "GetPubKey can return errors"
This reverts commit 68c06f19b4.
* adding entry to changelog
* Update CHANGELOG_PENDING.md
Co-Authored-By: jleni <juan.leni@zondax.ch>
* Update privval/signer_client.go
Co-Authored-By: jleni <juan.leni@zondax.ch>
* Update privval/signer_dialer_endpoint.go
Co-Authored-By: jleni <juan.leni@zondax.ch>
* Update privval/signer_dialer_endpoint.go
Co-Authored-By: jleni <juan.leni@zondax.ch>
* Update privval/signer_dialer_endpoint.go
Co-Authored-By: jleni <juan.leni@zondax.ch>
* Update privval/signer_dialer_endpoint.go
Co-Authored-By: jleni <juan.leni@zondax.ch>
* Update privval/signer_listener_endpoint_test.go
Co-Authored-By: jleni <juan.leni@zondax.ch>
* updating node.go
* review fixes
* fixes linter
* fixing unit test
* small fixes in comments
* addressing review comments
* addressing review comments 2
* reverting suggestion
* Update privval/signer_client_test.go
Co-Authored-By: Anton Kaliaev <anton.kalyaev@gmail.com>
* Update privval/signer_client_test.go
Co-Authored-By: Anton Kaliaev <anton.kalyaev@gmail.com>
* Update privval/signer_listener_endpoint_test.go
Co-Authored-By: Anton Kaliaev <anton.kalyaev@gmail.com>
* do not expose brokenSignerDialerEndpoint
* clean up logging
* unifying methods
shorten test time
signer also drops
* reenabling pings
* improving testability + unit test
* fixing go fmt + unit test
* remove unused code
* Addressing review comments
* simplifying connection workflow
* fix linter/go import issue
* using base service quit
* updating comment
* Simplifying design + adjusting names
* fixing linter issues
* refactoring test harness + fixes
* Addressing review comments
* cleaning up
* adding additional error check
258 lines
6.9 KiB
Go
258 lines
6.9 KiB
Go
package privval
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/tendermint/tendermint/libs/common"
|
|
"github.com/tendermint/tendermint/types"
|
|
)
|
|
|
|
type signerTestCase struct {
|
|
chainID string
|
|
mockPV types.PrivValidator
|
|
signerClient *SignerClient
|
|
signerServer *SignerServer
|
|
}
|
|
|
|
func getSignerTestCases(t *testing.T) []signerTestCase {
|
|
testCases := make([]signerTestCase, 0)
|
|
|
|
// Get test cases for each possible dialer (DialTCP / DialUnix / etc)
|
|
for _, dtc := range getDialerTestCases(t) {
|
|
chainID := common.RandStr(12)
|
|
mockPV := types.NewMockPV()
|
|
|
|
// get a pair of signer listener, signer dialer endpoints
|
|
sl, sd := getMockEndpoints(t, dtc.addr, dtc.dialer)
|
|
sc, err := NewSignerClient(sl)
|
|
require.NoError(t, err)
|
|
ss := NewSignerServer(sd, chainID, mockPV)
|
|
|
|
err = ss.Start()
|
|
require.NoError(t, err)
|
|
|
|
tc := signerTestCase{
|
|
chainID: chainID,
|
|
mockPV: mockPV,
|
|
signerClient: sc,
|
|
signerServer: ss,
|
|
}
|
|
|
|
testCases = append(testCases, tc)
|
|
}
|
|
|
|
return testCases
|
|
}
|
|
|
|
func TestSignerClose(t *testing.T) {
|
|
for _, tc := range getSignerTestCases(t) {
|
|
err := tc.signerClient.Close()
|
|
assert.NoError(t, err)
|
|
|
|
err = tc.signerServer.Stop()
|
|
assert.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
func TestSignerPing(t *testing.T) {
|
|
for _, tc := range getSignerTestCases(t) {
|
|
defer tc.signerServer.Stop()
|
|
defer tc.signerClient.Close()
|
|
|
|
err := tc.signerClient.Ping()
|
|
assert.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
func TestSignerGetPubKey(t *testing.T) {
|
|
for _, tc := range getSignerTestCases(t) {
|
|
defer tc.signerServer.Stop()
|
|
defer tc.signerClient.Close()
|
|
|
|
pubKey := tc.signerClient.GetPubKey()
|
|
expectedPubKey := tc.mockPV.GetPubKey()
|
|
|
|
assert.Equal(t, expectedPubKey, pubKey)
|
|
|
|
addr := tc.signerClient.GetPubKey().Address()
|
|
expectedAddr := tc.mockPV.GetPubKey().Address()
|
|
|
|
assert.Equal(t, expectedAddr, addr)
|
|
}
|
|
}
|
|
|
|
func TestSignerProposal(t *testing.T) {
|
|
for _, tc := range getSignerTestCases(t) {
|
|
ts := time.Now()
|
|
want := &types.Proposal{Timestamp: ts}
|
|
have := &types.Proposal{Timestamp: ts}
|
|
|
|
defer tc.signerServer.Stop()
|
|
defer tc.signerClient.Close()
|
|
|
|
require.NoError(t, tc.mockPV.SignProposal(tc.chainID, want))
|
|
require.NoError(t, tc.signerClient.SignProposal(tc.chainID, have))
|
|
|
|
assert.Equal(t, want.Signature, have.Signature)
|
|
}
|
|
}
|
|
|
|
func TestSignerVote(t *testing.T) {
|
|
for _, tc := range getSignerTestCases(t) {
|
|
ts := time.Now()
|
|
want := &types.Vote{Timestamp: ts, Type: types.PrecommitType}
|
|
have := &types.Vote{Timestamp: ts, Type: types.PrecommitType}
|
|
|
|
defer tc.signerServer.Stop()
|
|
defer tc.signerClient.Close()
|
|
|
|
require.NoError(t, tc.mockPV.SignVote(tc.chainID, want))
|
|
require.NoError(t, tc.signerClient.SignVote(tc.chainID, have))
|
|
|
|
assert.Equal(t, want.Signature, have.Signature)
|
|
}
|
|
}
|
|
|
|
func TestSignerVoteResetDeadline(t *testing.T) {
|
|
for _, tc := range getSignerTestCases(t) {
|
|
ts := time.Now()
|
|
want := &types.Vote{Timestamp: ts, Type: types.PrecommitType}
|
|
have := &types.Vote{Timestamp: ts, Type: types.PrecommitType}
|
|
|
|
defer tc.signerServer.Stop()
|
|
defer tc.signerClient.Close()
|
|
|
|
time.Sleep(testTimeoutReadWrite2o3)
|
|
|
|
require.NoError(t, tc.mockPV.SignVote(tc.chainID, want))
|
|
require.NoError(t, tc.signerClient.SignVote(tc.chainID, have))
|
|
assert.Equal(t, want.Signature, have.Signature)
|
|
|
|
// TODO(jleni): Clarify what is actually being tested
|
|
|
|
// This would exceed the deadline if it was not extended by the previous message
|
|
time.Sleep(testTimeoutReadWrite2o3)
|
|
|
|
require.NoError(t, tc.mockPV.SignVote(tc.chainID, want))
|
|
require.NoError(t, tc.signerClient.SignVote(tc.chainID, have))
|
|
assert.Equal(t, want.Signature, have.Signature)
|
|
}
|
|
}
|
|
|
|
func TestSignerVoteKeepAlive(t *testing.T) {
|
|
for _, tc := range getSignerTestCases(t) {
|
|
ts := time.Now()
|
|
want := &types.Vote{Timestamp: ts, Type: types.PrecommitType}
|
|
have := &types.Vote{Timestamp: ts, Type: types.PrecommitType}
|
|
|
|
defer tc.signerServer.Stop()
|
|
defer tc.signerClient.Close()
|
|
|
|
// Check that even if the client does not request a
|
|
// signature for a long time. The service is still available
|
|
|
|
// in this particular case, we use the dialer logger to ensure that
|
|
// test messages are properly interleaved in the test logs
|
|
tc.signerServer.Logger.Debug("TEST: Forced Wait -------------------------------------------------")
|
|
time.Sleep(testTimeoutReadWrite * 3)
|
|
tc.signerServer.Logger.Debug("TEST: Forced Wait DONE---------------------------------------------")
|
|
|
|
require.NoError(t, tc.mockPV.SignVote(tc.chainID, want))
|
|
require.NoError(t, tc.signerClient.SignVote(tc.chainID, have))
|
|
|
|
assert.Equal(t, want.Signature, have.Signature)
|
|
}
|
|
}
|
|
|
|
func TestSignerSignProposalErrors(t *testing.T) {
|
|
for _, tc := range getSignerTestCases(t) {
|
|
// Replace service with a mock that always fails
|
|
tc.signerServer.privVal = types.NewErroringMockPV()
|
|
tc.mockPV = types.NewErroringMockPV()
|
|
|
|
defer tc.signerServer.Stop()
|
|
defer tc.signerClient.Close()
|
|
|
|
ts := time.Now()
|
|
proposal := &types.Proposal{Timestamp: ts}
|
|
err := tc.signerClient.SignProposal(tc.chainID, proposal)
|
|
require.Equal(t, err.(*RemoteSignerError).Description, types.ErroringMockPVErr.Error())
|
|
|
|
err = tc.mockPV.SignProposal(tc.chainID, proposal)
|
|
require.Error(t, err)
|
|
|
|
err = tc.signerClient.SignProposal(tc.chainID, proposal)
|
|
require.Error(t, err)
|
|
}
|
|
}
|
|
|
|
func TestSignerSignVoteErrors(t *testing.T) {
|
|
for _, tc := range getSignerTestCases(t) {
|
|
ts := time.Now()
|
|
vote := &types.Vote{Timestamp: ts, Type: types.PrecommitType}
|
|
|
|
// Replace signer service privval with one that always fails
|
|
tc.signerServer.privVal = types.NewErroringMockPV()
|
|
tc.mockPV = types.NewErroringMockPV()
|
|
|
|
defer tc.signerServer.Stop()
|
|
defer tc.signerClient.Close()
|
|
|
|
err := tc.signerClient.SignVote(tc.chainID, vote)
|
|
require.Equal(t, err.(*RemoteSignerError).Description, types.ErroringMockPVErr.Error())
|
|
|
|
err = tc.mockPV.SignVote(tc.chainID, vote)
|
|
require.Error(t, err)
|
|
|
|
err = tc.signerClient.SignVote(tc.chainID, vote)
|
|
require.Error(t, err)
|
|
}
|
|
}
|
|
|
|
func brokenHandler(privVal types.PrivValidator, request SignerMessage, chainID string) (SignerMessage, error) {
|
|
var res SignerMessage
|
|
var err error
|
|
|
|
switch r := request.(type) {
|
|
|
|
// This is broken and will answer most requests with a pubkey response
|
|
case *PubKeyRequest:
|
|
res = &PubKeyResponse{nil, nil}
|
|
case *SignVoteRequest:
|
|
res = &PubKeyResponse{nil, nil}
|
|
case *SignProposalRequest:
|
|
res = &PubKeyResponse{nil, nil}
|
|
|
|
case *PingRequest:
|
|
err, res = nil, &PingResponse{}
|
|
|
|
default:
|
|
err = fmt.Errorf("unknown msg: %v", r)
|
|
}
|
|
|
|
return res, err
|
|
}
|
|
|
|
func TestSignerUnexpectedResponse(t *testing.T) {
|
|
for _, tc := range getSignerTestCases(t) {
|
|
tc.signerServer.privVal = types.NewMockPV()
|
|
tc.mockPV = types.NewMockPV()
|
|
|
|
tc.signerServer.SetRequestHandler(brokenHandler)
|
|
|
|
defer tc.signerServer.Stop()
|
|
defer tc.signerClient.Close()
|
|
|
|
ts := time.Now()
|
|
want := &types.Vote{Timestamp: ts, Type: types.PrecommitType}
|
|
|
|
e := tc.signerClient.SignVote(tc.chainID, want)
|
|
assert.EqualError(t, e, "received unexpected response")
|
|
}
|
|
}
|