mirror of
https://github.com/tendermint/tendermint.git
synced 2026-02-11 06:11:07 +00:00
## Description - Add `context.Context` to Privval interface This pr does not introduce context into our custom privval connection protocol because this will be removed in the next release. When this pr is released.
428 lines
13 KiB
Go
428 lines
13 KiB
Go
package internal
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/signal"
|
|
"time"
|
|
|
|
"github.com/tendermint/tendermint/crypto/tmhash"
|
|
|
|
"github.com/tendermint/tendermint/crypto/ed25519"
|
|
"github.com/tendermint/tendermint/privval"
|
|
"github.com/tendermint/tendermint/state"
|
|
|
|
"github.com/tendermint/tendermint/libs/log"
|
|
tmnet "github.com/tendermint/tendermint/libs/net"
|
|
tmos "github.com/tendermint/tendermint/libs/os"
|
|
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
|
"github.com/tendermint/tendermint/types"
|
|
)
|
|
|
|
// Test harness error codes (which act as exit codes when the test harness fails).
|
|
const (
|
|
NoError int = iota // 0
|
|
ErrInvalidParameters // 1
|
|
ErrMaxAcceptRetriesReached // 2
|
|
ErrFailedToLoadGenesisFile // 3
|
|
ErrFailedToCreateListener // 4
|
|
ErrFailedToStartListener // 5
|
|
ErrInterrupted // 6
|
|
ErrOther // 7
|
|
ErrTestPublicKeyFailed // 8
|
|
ErrTestSignProposalFailed // 9
|
|
ErrTestSignVoteFailed // 10
|
|
)
|
|
|
|
var voteTypes = []tmproto.SignedMsgType{tmproto.PrevoteType, tmproto.PrecommitType}
|
|
|
|
// TestHarnessError allows us to keep track of which exit code should be used
|
|
// when exiting the main program.
|
|
type TestHarnessError struct {
|
|
Code int // The exit code to return
|
|
Err error // The original error
|
|
Info string // Any additional information
|
|
}
|
|
|
|
var _ error = (*TestHarnessError)(nil)
|
|
|
|
// TestHarness allows for testing of a remote signer to ensure compatibility
|
|
// with this version of Tendermint.
|
|
type TestHarness struct {
|
|
addr string
|
|
signerClient *privval.SignerClient
|
|
fpv *privval.FilePV
|
|
chainID string
|
|
acceptRetries int
|
|
logger log.Logger
|
|
exitWhenComplete bool
|
|
exitCode int
|
|
}
|
|
|
|
// TestHarnessConfig provides configuration to set up a remote signer test
|
|
// harness.
|
|
type TestHarnessConfig struct {
|
|
BindAddr string
|
|
|
|
KeyFile string
|
|
StateFile string
|
|
GenesisFile string
|
|
|
|
AcceptDeadline time.Duration
|
|
ConnDeadline time.Duration
|
|
AcceptRetries int
|
|
|
|
SecretConnKey ed25519.PrivKey
|
|
|
|
ExitWhenComplete bool // Whether or not to call os.Exit when the harness has completed.
|
|
}
|
|
|
|
// timeoutError can be used to check if an error returned from the netp package
|
|
// was due to a timeout.
|
|
type timeoutError interface {
|
|
Timeout() bool
|
|
}
|
|
|
|
// NewTestHarness will load Tendermint data from the given files (including
|
|
// validator public/private keypairs and chain details) and create a new
|
|
// harness.
|
|
func NewTestHarness(logger log.Logger, cfg TestHarnessConfig) (*TestHarness, error) {
|
|
keyFile := ExpandPath(cfg.KeyFile)
|
|
stateFile := ExpandPath(cfg.StateFile)
|
|
logger.Info("Loading private validator configuration", "keyFile", keyFile, "stateFile", stateFile)
|
|
// NOTE: LoadFilePV ultimately calls os.Exit on failure. No error will be
|
|
// returned if this call fails.
|
|
fpv, err := privval.LoadFilePV(keyFile, stateFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
genesisFile := ExpandPath(cfg.GenesisFile)
|
|
logger.Info("Loading chain ID from genesis file", "genesisFile", genesisFile)
|
|
st, err := state.MakeGenesisDocFromFile(genesisFile)
|
|
if err != nil {
|
|
return nil, newTestHarnessError(ErrFailedToLoadGenesisFile, err, genesisFile)
|
|
}
|
|
logger.Info("Loaded genesis file", "chainID", st.ChainID)
|
|
|
|
spv, err := newTestHarnessListener(logger, cfg)
|
|
if err != nil {
|
|
return nil, newTestHarnessError(ErrFailedToCreateListener, err, "")
|
|
}
|
|
|
|
signerClient, err := privval.NewSignerClient(spv, st.ChainID)
|
|
if err != nil {
|
|
return nil, newTestHarnessError(ErrFailedToCreateListener, err, "")
|
|
}
|
|
|
|
return &TestHarness{
|
|
addr: cfg.BindAddr,
|
|
signerClient: signerClient,
|
|
fpv: fpv,
|
|
chainID: st.ChainID,
|
|
acceptRetries: cfg.AcceptRetries,
|
|
logger: logger,
|
|
exitWhenComplete: cfg.ExitWhenComplete,
|
|
exitCode: 0,
|
|
}, nil
|
|
}
|
|
|
|
// Run will execute the tests associated with this test harness. The intention
|
|
// here is to call this from one's `main` function, as the way it succeeds or
|
|
// fails at present is to call os.Exit() with an exit code related to the error
|
|
// that caused the tests to fail, or exit code 0 on success.
|
|
func (th *TestHarness) Run() {
|
|
c := make(chan os.Signal, 1)
|
|
signal.Notify(c, os.Interrupt)
|
|
go func() {
|
|
for sig := range c {
|
|
th.logger.Info("Caught interrupt, terminating...", "sig", sig)
|
|
th.Shutdown(newTestHarnessError(ErrInterrupted, nil, ""))
|
|
}
|
|
}()
|
|
|
|
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.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)
|
|
th.Shutdown(newTestHarnessError(ErrFailedToStartListener, err, ""))
|
|
// we need the return statements in case this is being run
|
|
// from a unit test - otherwise this function will just die
|
|
// when os.Exit is called
|
|
return
|
|
}
|
|
startErr = err
|
|
} else {
|
|
th.logger.Info("Accepted external connection")
|
|
accepted = true
|
|
break
|
|
}
|
|
}
|
|
if !accepted {
|
|
th.logger.Error("Maximum accept retries reached", "acceptRetries", th.acceptRetries)
|
|
th.Shutdown(newTestHarnessError(ErrMaxAcceptRetriesReached, startErr, ""))
|
|
return
|
|
}
|
|
|
|
// Run the tests
|
|
if err := th.TestPublicKey(); err != nil {
|
|
th.Shutdown(err)
|
|
return
|
|
}
|
|
if err := th.TestSignProposal(); err != nil {
|
|
th.Shutdown(err)
|
|
return
|
|
}
|
|
if err := th.TestSignVote(); err != nil {
|
|
th.Shutdown(err)
|
|
return
|
|
}
|
|
th.logger.Info("SUCCESS! All tests passed.")
|
|
th.Shutdown(nil)
|
|
}
|
|
|
|
// TestPublicKey just validates that we can (1) fetch the public key from the
|
|
// remote signer, and (2) it matches the public key we've configured for our
|
|
// local Tendermint version.
|
|
func (th *TestHarness) TestPublicKey() error {
|
|
th.logger.Info("TEST: Public key of remote signer")
|
|
fpvk, err := th.fpv.GetPubKey(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
th.logger.Info("Local", "pubKey", fpvk)
|
|
sck, err := th.signerClient.GetPubKey(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
th.logger.Info("Remote", "pubKey", sck)
|
|
if !bytes.Equal(fpvk.Bytes(), sck.Bytes()) {
|
|
th.logger.Error("FAILED: Local and remote public keys do not match")
|
|
return newTestHarnessError(ErrTestPublicKeyFailed, nil, "")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TestSignProposal makes sure the remote signer can successfully sign
|
|
// proposals.
|
|
func (th *TestHarness) TestSignProposal() error {
|
|
th.logger.Info("TEST: Signing of proposals")
|
|
// sha256 hash of "hash"
|
|
hash := tmhash.Sum([]byte("hash"))
|
|
prop := &types.Proposal{
|
|
Type: tmproto.ProposalType,
|
|
Height: 100,
|
|
Round: 0,
|
|
POLRound: -1,
|
|
BlockID: types.BlockID{
|
|
Hash: hash,
|
|
PartSetHeader: types.PartSetHeader{
|
|
Hash: hash,
|
|
Total: 1000000,
|
|
},
|
|
},
|
|
Timestamp: time.Now(),
|
|
}
|
|
p := prop.ToProto()
|
|
propBytes := types.ProposalSignBytes(th.chainID, p)
|
|
if err := th.signerClient.SignProposal(context.Background(), th.chainID, p); err != nil {
|
|
th.logger.Error("FAILED: Signing of proposal", "err", err)
|
|
return newTestHarnessError(ErrTestSignProposalFailed, err, "")
|
|
}
|
|
prop.Signature = p.Signature
|
|
th.logger.Debug("Signed proposal", "prop", prop)
|
|
// first check that it's a basically valid proposal
|
|
if err := prop.ValidateBasic(); err != nil {
|
|
th.logger.Error("FAILED: Signed proposal is invalid", "err", err)
|
|
return newTestHarnessError(ErrTestSignProposalFailed, err, "")
|
|
}
|
|
sck, err := th.signerClient.GetPubKey(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// now validate the signature on the proposal
|
|
if sck.VerifySignature(propBytes, prop.Signature) {
|
|
th.logger.Info("Successfully validated proposal signature")
|
|
} else {
|
|
th.logger.Error("FAILED: Proposal signature validation failed")
|
|
return newTestHarnessError(ErrTestSignProposalFailed, nil, "signature validation failed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TestSignVote makes sure the remote signer can successfully sign all kinds of
|
|
// votes.
|
|
func (th *TestHarness) TestSignVote() error {
|
|
th.logger.Info("TEST: Signing of votes")
|
|
for _, voteType := range voteTypes {
|
|
th.logger.Info("Testing vote type", "type", voteType)
|
|
hash := tmhash.Sum([]byte("hash"))
|
|
vote := &types.Vote{
|
|
Type: voteType,
|
|
Height: 101,
|
|
Round: 0,
|
|
BlockID: types.BlockID{
|
|
Hash: hash,
|
|
PartSetHeader: types.PartSetHeader{
|
|
Hash: hash,
|
|
Total: 1000000,
|
|
},
|
|
},
|
|
ValidatorIndex: 0,
|
|
ValidatorAddress: tmhash.SumTruncated([]byte("addr")),
|
|
Timestamp: time.Now(),
|
|
}
|
|
v := vote.ToProto()
|
|
voteBytes := types.VoteSignBytes(th.chainID, v)
|
|
// sign the vote
|
|
if err := th.signerClient.SignVote(context.Background(), th.chainID, v); err != nil {
|
|
th.logger.Error("FAILED: Signing of vote", "err", err)
|
|
return newTestHarnessError(ErrTestSignVoteFailed, err, fmt.Sprintf("voteType=%d", voteType))
|
|
}
|
|
vote.Signature = v.Signature
|
|
th.logger.Debug("Signed vote", "vote", vote)
|
|
// validate the contents of the vote
|
|
if err := vote.ValidateBasic(); err != nil {
|
|
th.logger.Error("FAILED: Signed vote is invalid", "err", err)
|
|
return newTestHarnessError(ErrTestSignVoteFailed, err, fmt.Sprintf("voteType=%d", voteType))
|
|
}
|
|
sck, err := th.signerClient.GetPubKey(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// now validate the signature on the proposal
|
|
if sck.VerifySignature(voteBytes, vote.Signature) {
|
|
th.logger.Info("Successfully validated vote signature", "type", voteType)
|
|
} else {
|
|
th.logger.Error("FAILED: Vote signature validation failed", "type", voteType)
|
|
return newTestHarnessError(ErrTestSignVoteFailed, nil, "signature validation failed")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Shutdown will kill the test harness and attempt to close all open sockets
|
|
// gracefully. If the supplied error is nil, it is assumed that the exit code
|
|
// should be 0. If err is not nil, it will exit with an exit code related to the
|
|
// error.
|
|
func (th *TestHarness) Shutdown(err error) {
|
|
var exitCode int
|
|
|
|
if err == nil {
|
|
exitCode = NoError
|
|
} else if therr, ok := err.(*TestHarnessError); ok {
|
|
exitCode = therr.Code
|
|
} else {
|
|
exitCode = ErrOther
|
|
}
|
|
th.exitCode = exitCode
|
|
|
|
// in case sc.Stop() takes too long
|
|
if th.exitWhenComplete {
|
|
go func() {
|
|
time.Sleep(time.Duration(5) * time.Second)
|
|
th.logger.Error("Forcibly exiting program after timeout")
|
|
os.Exit(exitCode)
|
|
}()
|
|
}
|
|
|
|
err = th.signerClient.Close()
|
|
if err != nil {
|
|
th.logger.Error("Failed to cleanly stop listener: %s", err.Error())
|
|
}
|
|
|
|
if th.exitWhenComplete {
|
|
os.Exit(exitCode)
|
|
}
|
|
}
|
|
|
|
// newTestHarnessListener creates our client instance which we will use for testing.
|
|
func newTestHarnessListener(logger log.Logger, cfg TestHarnessConfig) (*privval.SignerListenerEndpoint, error) {
|
|
proto, addr := tmnet.ProtocolAndAddress(cfg.BindAddr)
|
|
if proto == "unix" {
|
|
// make sure the socket doesn't exist - if so, try to delete it
|
|
if tmos.FileExists(addr) {
|
|
if err := os.Remove(addr); err != nil {
|
|
logger.Error("Failed to remove existing Unix domain socket", "addr", addr)
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
ln, err := net.Listen(proto, addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
logger.Info("Listening", "proto", proto, "addr", addr)
|
|
var svln net.Listener
|
|
switch proto {
|
|
case "unix":
|
|
unixLn := privval.NewUnixListener(ln)
|
|
privval.UnixListenerTimeoutAccept(cfg.AcceptDeadline)(unixLn)
|
|
privval.UnixListenerTimeoutReadWrite(cfg.ConnDeadline)(unixLn)
|
|
svln = unixLn
|
|
case "tcp":
|
|
tcpLn := privval.NewTCPListener(ln, cfg.SecretConnKey)
|
|
privval.TCPListenerTimeoutAccept(cfg.AcceptDeadline)(tcpLn)
|
|
privval.TCPListenerTimeoutReadWrite(cfg.ConnDeadline)(tcpLn)
|
|
logger.Info("Resolved TCP address for listener", "addr", tcpLn.Addr())
|
|
svln = tcpLn
|
|
default:
|
|
_ = ln.Close()
|
|
logger.Error("Unsupported protocol (must be unix:// or tcp://)", "proto", proto)
|
|
return nil, newTestHarnessError(ErrInvalidParameters, nil, fmt.Sprintf("Unsupported protocol: %s", proto))
|
|
}
|
|
return privval.NewSignerListenerEndpoint(logger, svln), nil
|
|
}
|
|
|
|
func newTestHarnessError(code int, err error, info string) *TestHarnessError {
|
|
return &TestHarnessError{
|
|
Code: code,
|
|
Err: err,
|
|
Info: info,
|
|
}
|
|
}
|
|
|
|
func (e *TestHarnessError) Error() string {
|
|
var msg string
|
|
switch e.Code {
|
|
case ErrInvalidParameters:
|
|
msg = "Invalid parameters supplied to application"
|
|
case ErrMaxAcceptRetriesReached:
|
|
msg = "Maximum accept retries reached"
|
|
case ErrFailedToLoadGenesisFile:
|
|
msg = "Failed to load genesis file"
|
|
case ErrFailedToCreateListener:
|
|
msg = "Failed to create listener"
|
|
case ErrFailedToStartListener:
|
|
msg = "Failed to start listener"
|
|
case ErrInterrupted:
|
|
msg = "Interrupted"
|
|
case ErrTestPublicKeyFailed:
|
|
msg = "Public key validation test failed"
|
|
case ErrTestSignProposalFailed:
|
|
msg = "Proposal signing validation test failed"
|
|
case ErrTestSignVoteFailed:
|
|
msg = "Vote signing validation test failed"
|
|
default:
|
|
msg = "Unknown error"
|
|
}
|
|
if len(e.Info) > 0 {
|
|
msg = fmt.Sprintf("%s: %s", msg, e.Info)
|
|
}
|
|
if e.Err != nil {
|
|
msg = fmt.Sprintf("%s (original error: %s)", msg, e.Err.Error())
|
|
}
|
|
return msg
|
|
}
|