mirror of
https://github.com/tendermint/tendermint.git
synced 2026-09-30 03:35:34 +00:00
privval: refactor Remote signers (#3370)
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
This commit is contained in:
@@ -49,7 +49,7 @@ var _ error = (*TestHarnessError)(nil)
|
||||
// with this version of Tendermint.
|
||||
type TestHarness struct {
|
||||
addr string
|
||||
spv *privval.SignerValidatorEndpoint
|
||||
signerClient *privval.SignerClient
|
||||
fpv *privval.FilePV
|
||||
chainID string
|
||||
acceptRetries int
|
||||
@@ -101,14 +101,19 @@ func NewTestHarness(logger log.Logger, cfg TestHarnessConfig) (*TestHarness, err
|
||||
}
|
||||
logger.Info("Loaded genesis file", "chainID", st.ChainID)
|
||||
|
||||
spv, err := newTestHarnessSocketVal(logger, cfg)
|
||||
spv, err := newTestHarnessListener(logger, cfg)
|
||||
if err != nil {
|
||||
return nil, newTestHarnessError(ErrFailedToCreateListener, err, "")
|
||||
}
|
||||
|
||||
signerClient, err := privval.NewSignerClient(spv)
|
||||
if err != nil {
|
||||
return nil, newTestHarnessError(ErrFailedToCreateListener, err, "")
|
||||
}
|
||||
|
||||
return &TestHarness{
|
||||
addr: cfg.BindAddr,
|
||||
spv: spv,
|
||||
signerClient: signerClient,
|
||||
fpv: fpv,
|
||||
chainID: st.ChainID,
|
||||
acceptRetries: cfg.AcceptRetries,
|
||||
@@ -135,9 +140,11 @@ func (th *TestHarness) Run() {
|
||||
th.logger.Info("Starting test harness")
|
||||
accepted := false
|
||||
var startErr error
|
||||
|
||||
for acceptRetries := th.acceptRetries; acceptRetries > 0; acceptRetries-- {
|
||||
th.logger.Info("Attempting to accept incoming connection", "acceptRetries", acceptRetries)
|
||||
if err := th.spv.Start(); err != nil {
|
||||
|
||||
if err := th.signerClient.WaitForConnection(10 * time.Millisecond); err != nil {
|
||||
// if it wasn't a timeout error
|
||||
if _, ok := err.(timeoutError); !ok {
|
||||
th.logger.Error("Failed to start listener", "err", err)
|
||||
@@ -149,6 +156,7 @@ func (th *TestHarness) Run() {
|
||||
}
|
||||
startErr = err
|
||||
} else {
|
||||
th.logger.Info("Accepted external connection")
|
||||
accepted = true
|
||||
break
|
||||
}
|
||||
@@ -182,8 +190,8 @@ func (th *TestHarness) Run() {
|
||||
func (th *TestHarness) TestPublicKey() error {
|
||||
th.logger.Info("TEST: Public key of remote signer")
|
||||
th.logger.Info("Local", "pubKey", th.fpv.GetPubKey())
|
||||
th.logger.Info("Remote", "pubKey", th.spv.GetPubKey())
|
||||
if th.fpv.GetPubKey() != th.spv.GetPubKey() {
|
||||
th.logger.Info("Remote", "pubKey", th.signerClient.GetPubKey())
|
||||
if th.fpv.GetPubKey() != th.signerClient.GetPubKey() {
|
||||
th.logger.Error("FAILED: Local and remote public keys do not match")
|
||||
return newTestHarnessError(ErrTestPublicKeyFailed, nil, "")
|
||||
}
|
||||
@@ -211,7 +219,7 @@ func (th *TestHarness) TestSignProposal() error {
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
propBytes := prop.SignBytes(th.chainID)
|
||||
if err := th.spv.SignProposal(th.chainID, prop); err != nil {
|
||||
if err := th.signerClient.SignProposal(th.chainID, prop); err != nil {
|
||||
th.logger.Error("FAILED: Signing of proposal", "err", err)
|
||||
return newTestHarnessError(ErrTestSignProposalFailed, err, "")
|
||||
}
|
||||
@@ -222,7 +230,7 @@ func (th *TestHarness) TestSignProposal() error {
|
||||
return newTestHarnessError(ErrTestSignProposalFailed, err, "")
|
||||
}
|
||||
// now validate the signature on the proposal
|
||||
if th.spv.GetPubKey().VerifyBytes(propBytes, prop.Signature) {
|
||||
if th.signerClient.GetPubKey().VerifyBytes(propBytes, prop.Signature) {
|
||||
th.logger.Info("Successfully validated proposal signature")
|
||||
} else {
|
||||
th.logger.Error("FAILED: Proposal signature validation failed")
|
||||
@@ -255,7 +263,7 @@ func (th *TestHarness) TestSignVote() error {
|
||||
}
|
||||
voteBytes := vote.SignBytes(th.chainID)
|
||||
// sign the vote
|
||||
if err := th.spv.SignVote(th.chainID, vote); err != nil {
|
||||
if err := th.signerClient.SignVote(th.chainID, vote); err != nil {
|
||||
th.logger.Error("FAILED: Signing of vote", "err", err)
|
||||
return newTestHarnessError(ErrTestSignVoteFailed, err, fmt.Sprintf("voteType=%d", voteType))
|
||||
}
|
||||
@@ -266,7 +274,7 @@ func (th *TestHarness) TestSignVote() error {
|
||||
return newTestHarnessError(ErrTestSignVoteFailed, err, fmt.Sprintf("voteType=%d", voteType))
|
||||
}
|
||||
// now validate the signature on the proposal
|
||||
if th.spv.GetPubKey().VerifyBytes(voteBytes, vote.Signature) {
|
||||
if th.signerClient.GetPubKey().VerifyBytes(voteBytes, vote.Signature) {
|
||||
th.logger.Info("Successfully validated vote signature", "type", voteType)
|
||||
} else {
|
||||
th.logger.Error("FAILED: Vote signature validation failed", "type", voteType)
|
||||
@@ -301,10 +309,9 @@ func (th *TestHarness) Shutdown(err error) {
|
||||
}()
|
||||
}
|
||||
|
||||
if th.spv.IsRunning() {
|
||||
if err := th.spv.Stop(); err != nil {
|
||||
th.logger.Error("Failed to cleanly stop listener: %s", err.Error())
|
||||
}
|
||||
err = th.signerClient.Close()
|
||||
if err != nil {
|
||||
th.logger.Error("Failed to cleanly stop listener: %s", err.Error())
|
||||
}
|
||||
|
||||
if th.exitWhenComplete {
|
||||
@@ -312,9 +319,8 @@ func (th *TestHarness) Shutdown(err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// newTestHarnessSocketVal creates our client instance which we will use for
|
||||
// testing.
|
||||
func newTestHarnessSocketVal(logger log.Logger, cfg TestHarnessConfig) (*privval.SignerValidatorEndpoint, error) {
|
||||
// newTestHarnessListener creates our client instance which we will use for testing.
|
||||
func newTestHarnessListener(logger log.Logger, cfg TestHarnessConfig) (*privval.SignerListenerEndpoint, error) {
|
||||
proto, addr := cmn.ProtocolAndAddress(cfg.BindAddr)
|
||||
if proto == "unix" {
|
||||
// make sure the socket doesn't exist - if so, try to delete it
|
||||
@@ -329,7 +335,7 @@ func newTestHarnessSocketVal(logger log.Logger, cfg TestHarnessConfig) (*privval
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Info("Listening at", "proto", proto, "addr", addr)
|
||||
logger.Info("Listening", "proto", proto, "addr", addr)
|
||||
var svln net.Listener
|
||||
switch proto {
|
||||
case "unix":
|
||||
@@ -347,7 +353,7 @@ func newTestHarnessSocketVal(logger log.Logger, cfg TestHarnessConfig) (*privval
|
||||
logger.Error("Unsupported protocol (must be unix:// or tcp://)", "proto", proto)
|
||||
return nil, newTestHarnessError(ErrInvalidParameters, nil, fmt.Sprintf("Unsupported protocol: %s", proto))
|
||||
}
|
||||
return privval.NewSignerValidatorEndpoint(logger, svln), nil
|
||||
return privval.NewSignerListenerEndpoint(logger, svln), nil
|
||||
}
|
||||
|
||||
func newTestHarnessError(code int, err error, info string) *TestHarnessError {
|
||||
|
||||
@@ -3,19 +3,18 @@ package internal
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
"github.com/tendermint/tendermint/privval"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
"github.com/tendermint/tendermint/crypto/ed25519"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/privval"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -85,8 +84,8 @@ func TestRemoteSignerTestHarnessMaxAcceptRetriesReached(t *testing.T) {
|
||||
func TestRemoteSignerTestHarnessSuccessfulRun(t *testing.T) {
|
||||
harnessTest(
|
||||
t,
|
||||
func(th *TestHarness) *privval.SignerServiceEndpoint {
|
||||
return newMockRemoteSigner(t, th, th.fpv.Key.PrivKey, false, false)
|
||||
func(th *TestHarness) *privval.SignerServer {
|
||||
return newMockSignerServer(t, th, th.fpv.Key.PrivKey, false, false)
|
||||
},
|
||||
NoError,
|
||||
)
|
||||
@@ -95,8 +94,8 @@ func TestRemoteSignerTestHarnessSuccessfulRun(t *testing.T) {
|
||||
func TestRemoteSignerPublicKeyCheckFailed(t *testing.T) {
|
||||
harnessTest(
|
||||
t,
|
||||
func(th *TestHarness) *privval.SignerServiceEndpoint {
|
||||
return newMockRemoteSigner(t, th, ed25519.GenPrivKey(), false, false)
|
||||
func(th *TestHarness) *privval.SignerServer {
|
||||
return newMockSignerServer(t, th, ed25519.GenPrivKey(), false, false)
|
||||
},
|
||||
ErrTestPublicKeyFailed,
|
||||
)
|
||||
@@ -105,8 +104,8 @@ func TestRemoteSignerPublicKeyCheckFailed(t *testing.T) {
|
||||
func TestRemoteSignerProposalSigningFailed(t *testing.T) {
|
||||
harnessTest(
|
||||
t,
|
||||
func(th *TestHarness) *privval.SignerServiceEndpoint {
|
||||
return newMockRemoteSigner(t, th, th.fpv.Key.PrivKey, true, false)
|
||||
func(th *TestHarness) *privval.SignerServer {
|
||||
return newMockSignerServer(t, th, th.fpv.Key.PrivKey, true, false)
|
||||
},
|
||||
ErrTestSignProposalFailed,
|
||||
)
|
||||
@@ -115,28 +114,30 @@ func TestRemoteSignerProposalSigningFailed(t *testing.T) {
|
||||
func TestRemoteSignerVoteSigningFailed(t *testing.T) {
|
||||
harnessTest(
|
||||
t,
|
||||
func(th *TestHarness) *privval.SignerServiceEndpoint {
|
||||
return newMockRemoteSigner(t, th, th.fpv.Key.PrivKey, false, true)
|
||||
func(th *TestHarness) *privval.SignerServer {
|
||||
return newMockSignerServer(t, th, th.fpv.Key.PrivKey, false, true)
|
||||
},
|
||||
ErrTestSignVoteFailed,
|
||||
)
|
||||
}
|
||||
|
||||
func newMockRemoteSigner(t *testing.T, th *TestHarness, privKey crypto.PrivKey, breakProposalSigning bool, breakVoteSigning bool) *privval.SignerServiceEndpoint {
|
||||
return privval.NewSignerServiceEndpoint(
|
||||
func newMockSignerServer(t *testing.T, th *TestHarness, privKey crypto.PrivKey, breakProposalSigning bool, breakVoteSigning bool) *privval.SignerServer {
|
||||
mockPV := types.NewMockPVWithParams(privKey, breakProposalSigning, breakVoteSigning)
|
||||
|
||||
dialerEndpoint := privval.NewSignerDialerEndpoint(
|
||||
th.logger,
|
||||
th.chainID,
|
||||
types.NewMockPVWithParams(privKey, breakProposalSigning, breakVoteSigning),
|
||||
privval.DialTCPFn(
|
||||
th.addr,
|
||||
time.Duration(defaultConnDeadline)*time.Millisecond,
|
||||
ed25519.GenPrivKey(),
|
||||
),
|
||||
)
|
||||
|
||||
return privval.NewSignerServer(dialerEndpoint, th.chainID, mockPV)
|
||||
}
|
||||
|
||||
// For running relatively standard tests.
|
||||
func harnessTest(t *testing.T, rsMaker func(th *TestHarness) *privval.SignerServiceEndpoint, expectedExitCode int) {
|
||||
func harnessTest(t *testing.T, signerServerMaker func(th *TestHarness) *privval.SignerServer, expectedExitCode int) {
|
||||
cfg := makeConfig(t, 100, 3)
|
||||
defer cleanup(cfg)
|
||||
|
||||
@@ -148,10 +149,10 @@ func harnessTest(t *testing.T, rsMaker func(th *TestHarness) *privval.SignerServ
|
||||
th.Run()
|
||||
}()
|
||||
|
||||
rs := rsMaker(th)
|
||||
require.NoError(t, rs.Start())
|
||||
assert.True(t, rs.IsRunning())
|
||||
defer rs.Stop()
|
||||
ss := signerServerMaker(th)
|
||||
require.NoError(t, ss.Start())
|
||||
assert.True(t, ss.IsRunning())
|
||||
defer ss.Stop()
|
||||
|
||||
<-donec
|
||||
assert.Equal(t, expectedExitCode, th.exitCode)
|
||||
@@ -159,7 +160,7 @@ func harnessTest(t *testing.T, rsMaker func(th *TestHarness) *privval.SignerServ
|
||||
|
||||
func makeConfig(t *testing.T, acceptDeadline, acceptRetries int) TestHarnessConfig {
|
||||
return TestHarnessConfig{
|
||||
BindAddr: testFreeTCPAddr(t),
|
||||
BindAddr: privval.GetFreeLocalhostAddrPort(),
|
||||
KeyFile: makeTempFile("tm-testharness-keyfile", keyFileContents),
|
||||
StateFile: makeTempFile("tm-testharness-statefile", stateFileContents),
|
||||
GenesisFile: makeTempFile("tm-testharness-genesisfile", genesisFileContents),
|
||||
@@ -191,12 +192,3 @@ func makeTempFile(name, content string) string {
|
||||
}
|
||||
return tempFile.Name()
|
||||
}
|
||||
|
||||
// testFreeTCPAddr claims a free port so we don't block on listener being ready.
|
||||
func testFreeTCPAddr(t *testing.T) string {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
defer ln.Close()
|
||||
|
||||
return fmt.Sprintf("127.0.0.1:%d", ln.Addr().(*net.TCPAddr).Port)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user