package internal import ( "bytes" "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 := privval.LoadFilePV(keyFile, stateFile) 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) 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() if err != nil { return err } th.logger.Info("Local", "pubKey", fpvk) sck, err := th.signerClient.GetPubKey() 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(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() if err != nil { return err } // now validate the signature on the proposal if sck.VerifyBytes(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(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() if err != nil { return err } // now validate the signature on the proposal if sck.VerifyBytes(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: 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 }