github.com/vipernet-xyz/tm@v0.34.24/tools/tm-signer-harness/internal/test_harness.go (about)

     1  package internal
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"net"
     7  	"os"
     8  	"os/signal"
     9  	"time"
    10  
    11  	"github.com/vipernet-xyz/tm/crypto/tmhash"
    12  
    13  	"github.com/vipernet-xyz/tm/crypto/ed25519"
    14  	"github.com/vipernet-xyz/tm/privval"
    15  	"github.com/vipernet-xyz/tm/state"
    16  
    17  	"github.com/vipernet-xyz/tm/libs/log"
    18  	tmnet "github.com/vipernet-xyz/tm/libs/net"
    19  	tmos "github.com/vipernet-xyz/tm/libs/os"
    20  	tmproto "github.com/vipernet-xyz/tm/proto/tendermint/types"
    21  	"github.com/vipernet-xyz/tm/types"
    22  )
    23  
    24  // Test harness error codes (which act as exit codes when the test harness fails).
    25  const (
    26  	NoError                    int = iota // 0
    27  	ErrInvalidParameters                  // 1
    28  	ErrMaxAcceptRetriesReached            // 2
    29  	ErrFailedToLoadGenesisFile            // 3
    30  	ErrFailedToCreateListener             // 4
    31  	ErrFailedToStartListener              // 5
    32  	ErrInterrupted                        // 6
    33  	ErrOther                              // 7
    34  	ErrTestPublicKeyFailed                // 8
    35  	ErrTestSignProposalFailed             // 9
    36  	ErrTestSignVoteFailed                 // 10
    37  )
    38  
    39  var voteTypes = []tmproto.SignedMsgType{tmproto.PrevoteType, tmproto.PrecommitType}
    40  
    41  // TestHarnessError allows us to keep track of which exit code should be used
    42  // when exiting the main program.
    43  type TestHarnessError struct {
    44  	Code int    // The exit code to return
    45  	Err  error  // The original error
    46  	Info string // Any additional information
    47  }
    48  
    49  var _ error = (*TestHarnessError)(nil)
    50  
    51  // TestHarness allows for testing of a remote signer to ensure compatibility
    52  // with this version of Tendermint.
    53  type TestHarness struct {
    54  	addr             string
    55  	signerClient     *privval.SignerClient
    56  	fpv              *privval.FilePV
    57  	chainID          string
    58  	acceptRetries    int
    59  	logger           log.Logger
    60  	exitWhenComplete bool
    61  	exitCode         int
    62  }
    63  
    64  // TestHarnessConfig provides configuration to set up a remote signer test
    65  // harness.
    66  type TestHarnessConfig struct {
    67  	BindAddr string
    68  
    69  	KeyFile     string
    70  	StateFile   string
    71  	GenesisFile string
    72  
    73  	AcceptDeadline time.Duration
    74  	ConnDeadline   time.Duration
    75  	AcceptRetries  int
    76  
    77  	SecretConnKey ed25519.PrivKey
    78  
    79  	ExitWhenComplete bool // Whether or not to call os.Exit when the harness has completed.
    80  }
    81  
    82  // timeoutError can be used to check if an error returned from the netp package
    83  // was due to a timeout.
    84  type timeoutError interface {
    85  	Timeout() bool
    86  }
    87  
    88  // NewTestHarness will load Tendermint data from the given files (including
    89  // validator public/private keypairs and chain details) and create a new
    90  // harness.
    91  func NewTestHarness(logger log.Logger, cfg TestHarnessConfig) (*TestHarness, error) {
    92  	keyFile := ExpandPath(cfg.KeyFile)
    93  	stateFile := ExpandPath(cfg.StateFile)
    94  	logger.Info("Loading private validator configuration", "keyFile", keyFile, "stateFile", stateFile)
    95  	// NOTE: LoadFilePV ultimately calls os.Exit on failure. No error will be
    96  	// returned if this call fails.
    97  	fpv := privval.LoadFilePV(keyFile, stateFile)
    98  
    99  	genesisFile := ExpandPath(cfg.GenesisFile)
   100  	logger.Info("Loading chain ID from genesis file", "genesisFile", genesisFile)
   101  	st, err := state.MakeGenesisDocFromFile(genesisFile)
   102  	if err != nil {
   103  		return nil, newTestHarnessError(ErrFailedToLoadGenesisFile, err, genesisFile)
   104  	}
   105  	logger.Info("Loaded genesis file", "chainID", st.ChainID)
   106  
   107  	spv, err := newTestHarnessListener(logger, cfg)
   108  	if err != nil {
   109  		return nil, newTestHarnessError(ErrFailedToCreateListener, err, "")
   110  	}
   111  
   112  	signerClient, err := privval.NewSignerClient(spv, st.ChainID)
   113  	if err != nil {
   114  		return nil, newTestHarnessError(ErrFailedToCreateListener, err, "")
   115  	}
   116  
   117  	return &TestHarness{
   118  		addr:             cfg.BindAddr,
   119  		signerClient:     signerClient,
   120  		fpv:              fpv,
   121  		chainID:          st.ChainID,
   122  		acceptRetries:    cfg.AcceptRetries,
   123  		logger:           logger,
   124  		exitWhenComplete: cfg.ExitWhenComplete,
   125  		exitCode:         0,
   126  	}, nil
   127  }
   128  
   129  // Run will execute the tests associated with this test harness. The intention
   130  // here is to call this from one's `main` function, as the way it succeeds or
   131  // fails at present is to call os.Exit() with an exit code related to the error
   132  // that caused the tests to fail, or exit code 0 on success.
   133  func (th *TestHarness) Run() {
   134  	c := make(chan os.Signal, 1)
   135  	signal.Notify(c, os.Interrupt)
   136  	go func() {
   137  		for sig := range c {
   138  			th.logger.Info("Caught interrupt, terminating...", "sig", sig)
   139  			th.Shutdown(newTestHarnessError(ErrInterrupted, nil, ""))
   140  		}
   141  	}()
   142  
   143  	th.logger.Info("Starting test harness")
   144  	accepted := false
   145  	var startErr error
   146  
   147  	for acceptRetries := th.acceptRetries; acceptRetries > 0; acceptRetries-- {
   148  		th.logger.Info("Attempting to accept incoming connection", "acceptRetries", acceptRetries)
   149  
   150  		if err := th.signerClient.WaitForConnection(10 * time.Millisecond); err != nil {
   151  			// if it wasn't a timeout error
   152  			if _, ok := err.(timeoutError); !ok {
   153  				th.logger.Error("Failed to start listener", "err", err)
   154  				th.Shutdown(newTestHarnessError(ErrFailedToStartListener, err, ""))
   155  				// we need the return statements in case this is being run
   156  				// from a unit test - otherwise this function will just die
   157  				// when os.Exit is called
   158  				return
   159  			}
   160  			startErr = err
   161  		} else {
   162  			th.logger.Info("Accepted external connection")
   163  			accepted = true
   164  			break
   165  		}
   166  	}
   167  	if !accepted {
   168  		th.logger.Error("Maximum accept retries reached", "acceptRetries", th.acceptRetries)
   169  		th.Shutdown(newTestHarnessError(ErrMaxAcceptRetriesReached, startErr, ""))
   170  		return
   171  	}
   172  
   173  	// Run the tests
   174  	if err := th.TestPublicKey(); err != nil {
   175  		th.Shutdown(err)
   176  		return
   177  	}
   178  	if err := th.TestSignProposal(); err != nil {
   179  		th.Shutdown(err)
   180  		return
   181  	}
   182  	if err := th.TestSignVote(); err != nil {
   183  		th.Shutdown(err)
   184  		return
   185  	}
   186  	th.logger.Info("SUCCESS! All tests passed.")
   187  	th.Shutdown(nil)
   188  }
   189  
   190  // TestPublicKey just validates that we can (1) fetch the public key from the
   191  // remote signer, and (2) it matches the public key we've configured for our
   192  // local Tendermint version.
   193  func (th *TestHarness) TestPublicKey() error {
   194  	th.logger.Info("TEST: Public key of remote signer")
   195  	fpvk, err := th.fpv.GetPubKey()
   196  	if err != nil {
   197  		return err
   198  	}
   199  	th.logger.Info("Local", "pubKey", fpvk)
   200  	sck, err := th.signerClient.GetPubKey()
   201  	if err != nil {
   202  		return err
   203  	}
   204  	th.logger.Info("Remote", "pubKey", sck)
   205  	if !bytes.Equal(fpvk.Bytes(), sck.Bytes()) {
   206  		th.logger.Error("FAILED: Local and remote public keys do not match")
   207  		return newTestHarnessError(ErrTestPublicKeyFailed, nil, "")
   208  	}
   209  	return nil
   210  }
   211  
   212  // TestSignProposal makes sure the remote signer can successfully sign
   213  // proposals.
   214  func (th *TestHarness) TestSignProposal() error {
   215  	th.logger.Info("TEST: Signing of proposals")
   216  	// sha256 hash of "hash"
   217  	hash := tmhash.Sum([]byte("hash"))
   218  	prop := &types.Proposal{
   219  		Type:     tmproto.ProposalType,
   220  		Height:   100,
   221  		Round:    0,
   222  		POLRound: -1,
   223  		BlockID: types.BlockID{
   224  			Hash: hash,
   225  			PartSetHeader: types.PartSetHeader{
   226  				Hash:  hash,
   227  				Total: 1000000,
   228  			},
   229  		},
   230  		Timestamp: time.Now(),
   231  	}
   232  	p := prop.ToProto()
   233  	propBytes := types.ProposalSignBytes(th.chainID, p)
   234  	if err := th.signerClient.SignProposal(th.chainID, p); err != nil {
   235  		th.logger.Error("FAILED: Signing of proposal", "err", err)
   236  		return newTestHarnessError(ErrTestSignProposalFailed, err, "")
   237  	}
   238  	prop.Signature = p.Signature
   239  	th.logger.Debug("Signed proposal", "prop", prop)
   240  	// first check that it's a basically valid proposal
   241  	if err := prop.ValidateBasic(); err != nil {
   242  		th.logger.Error("FAILED: Signed proposal is invalid", "err", err)
   243  		return newTestHarnessError(ErrTestSignProposalFailed, err, "")
   244  	}
   245  	sck, err := th.signerClient.GetPubKey()
   246  	if err != nil {
   247  		return err
   248  	}
   249  	// now validate the signature on the proposal
   250  	if sck.VerifySignature(propBytes, prop.Signature) {
   251  		th.logger.Info("Successfully validated proposal signature")
   252  	} else {
   253  		th.logger.Error("FAILED: Proposal signature validation failed")
   254  		return newTestHarnessError(ErrTestSignProposalFailed, nil, "signature validation failed")
   255  	}
   256  	return nil
   257  }
   258  
   259  // TestSignVote makes sure the remote signer can successfully sign all kinds of
   260  // votes.
   261  func (th *TestHarness) TestSignVote() error {
   262  	th.logger.Info("TEST: Signing of votes")
   263  	for _, voteType := range voteTypes {
   264  		th.logger.Info("Testing vote type", "type", voteType)
   265  		hash := tmhash.Sum([]byte("hash"))
   266  		vote := &types.Vote{
   267  			Type:   voteType,
   268  			Height: 101,
   269  			Round:  0,
   270  			BlockID: types.BlockID{
   271  				Hash: hash,
   272  				PartSetHeader: types.PartSetHeader{
   273  					Hash:  hash,
   274  					Total: 1000000,
   275  				},
   276  			},
   277  			ValidatorIndex:   0,
   278  			ValidatorAddress: tmhash.SumTruncated([]byte("addr")),
   279  			Timestamp:        time.Now(),
   280  		}
   281  		v := vote.ToProto()
   282  		voteBytes := types.VoteSignBytes(th.chainID, v)
   283  		// sign the vote
   284  		if err := th.signerClient.SignVote(th.chainID, v); err != nil {
   285  			th.logger.Error("FAILED: Signing of vote", "err", err)
   286  			return newTestHarnessError(ErrTestSignVoteFailed, err, fmt.Sprintf("voteType=%d", voteType))
   287  		}
   288  		vote.Signature = v.Signature
   289  		th.logger.Debug("Signed vote", "vote", vote)
   290  		// validate the contents of the vote
   291  		if err := vote.ValidateBasic(); err != nil {
   292  			th.logger.Error("FAILED: Signed vote is invalid", "err", err)
   293  			return newTestHarnessError(ErrTestSignVoteFailed, err, fmt.Sprintf("voteType=%d", voteType))
   294  		}
   295  		sck, err := th.signerClient.GetPubKey()
   296  		if err != nil {
   297  			return err
   298  		}
   299  
   300  		// now validate the signature on the proposal
   301  		if sck.VerifySignature(voteBytes, vote.Signature) {
   302  			th.logger.Info("Successfully validated vote signature", "type", voteType)
   303  		} else {
   304  			th.logger.Error("FAILED: Vote signature validation failed", "type", voteType)
   305  			return newTestHarnessError(ErrTestSignVoteFailed, nil, "signature validation failed")
   306  		}
   307  	}
   308  	return nil
   309  }
   310  
   311  // Shutdown will kill the test harness and attempt to close all open sockets
   312  // gracefully. If the supplied error is nil, it is assumed that the exit code
   313  // should be 0. If err is not nil, it will exit with an exit code related to the
   314  // error.
   315  func (th *TestHarness) Shutdown(err error) {
   316  	var exitCode int
   317  
   318  	if err == nil {
   319  		exitCode = NoError
   320  	} else if therr, ok := err.(*TestHarnessError); ok {
   321  		exitCode = therr.Code
   322  	} else {
   323  		exitCode = ErrOther
   324  	}
   325  	th.exitCode = exitCode
   326  
   327  	// in case sc.Stop() takes too long
   328  	if th.exitWhenComplete {
   329  		go func() {
   330  			time.Sleep(time.Duration(5) * time.Second)
   331  			th.logger.Error("Forcibly exiting program after timeout")
   332  			os.Exit(exitCode)
   333  		}()
   334  	}
   335  
   336  	err = th.signerClient.Close()
   337  	if err != nil {
   338  		th.logger.Error("Failed to cleanly stop listener: %s", err.Error())
   339  	}
   340  
   341  	if th.exitWhenComplete {
   342  		os.Exit(exitCode)
   343  	}
   344  }
   345  
   346  // newTestHarnessListener creates our client instance which we will use for testing.
   347  func newTestHarnessListener(logger log.Logger, cfg TestHarnessConfig) (*privval.SignerListenerEndpoint, error) {
   348  	proto, addr := tmnet.ProtocolAndAddress(cfg.BindAddr)
   349  	if proto == "unix" {
   350  		// make sure the socket doesn't exist - if so, try to delete it
   351  		if tmos.FileExists(addr) {
   352  			if err := os.Remove(addr); err != nil {
   353  				logger.Error("Failed to remove existing Unix domain socket", "addr", addr)
   354  				return nil, err
   355  			}
   356  		}
   357  	}
   358  	ln, err := net.Listen(proto, addr)
   359  	if err != nil {
   360  		return nil, err
   361  	}
   362  	logger.Info("Listening", "proto", proto, "addr", addr)
   363  	var svln net.Listener
   364  	switch proto {
   365  	case "unix":
   366  		unixLn := privval.NewUnixListener(ln)
   367  		privval.UnixListenerTimeoutAccept(cfg.AcceptDeadline)(unixLn)
   368  		privval.UnixListenerTimeoutReadWrite(cfg.ConnDeadline)(unixLn)
   369  		svln = unixLn
   370  	case "tcp":
   371  		tcpLn := privval.NewTCPListener(ln, cfg.SecretConnKey)
   372  		privval.TCPListenerTimeoutAccept(cfg.AcceptDeadline)(tcpLn)
   373  		privval.TCPListenerTimeoutReadWrite(cfg.ConnDeadline)(tcpLn)
   374  		logger.Info("Resolved TCP address for listener", "addr", tcpLn.Addr())
   375  		svln = tcpLn
   376  	default:
   377  		_ = ln.Close()
   378  		logger.Error("Unsupported protocol (must be unix:// or tcp://)", "proto", proto)
   379  		return nil, newTestHarnessError(ErrInvalidParameters, nil, fmt.Sprintf("Unsupported protocol: %s", proto))
   380  	}
   381  	return privval.NewSignerListenerEndpoint(logger, svln), nil
   382  }
   383  
   384  func newTestHarnessError(code int, err error, info string) *TestHarnessError {
   385  	return &TestHarnessError{
   386  		Code: code,
   387  		Err:  err,
   388  		Info: info,
   389  	}
   390  }
   391  
   392  func (e *TestHarnessError) Error() string {
   393  	var msg string
   394  	switch e.Code {
   395  	case ErrInvalidParameters:
   396  		msg = "Invalid parameters supplied to application"
   397  	case ErrMaxAcceptRetriesReached:
   398  		msg = "Maximum accept retries reached"
   399  	case ErrFailedToLoadGenesisFile:
   400  		msg = "Failed to load genesis file"
   401  	case ErrFailedToCreateListener:
   402  		msg = "Failed to create listener"
   403  	case ErrFailedToStartListener:
   404  		msg = "Failed to start listener"
   405  	case ErrInterrupted:
   406  		msg = "Interrupted"
   407  	case ErrTestPublicKeyFailed:
   408  		msg = "Public key validation test failed"
   409  	case ErrTestSignProposalFailed:
   410  		msg = "Proposal signing validation test failed"
   411  	case ErrTestSignVoteFailed:
   412  		msg = "Vote signing validation test failed"
   413  	default:
   414  		msg = "Unknown error"
   415  	}
   416  	if len(e.Info) > 0 {
   417  		msg = fmt.Sprintf("%s: %s", msg, e.Info)
   418  	}
   419  	if e.Err != nil {
   420  		msg = fmt.Sprintf("%s (original error: %s)", msg, e.Err.Error())
   421  	}
   422  	return msg
   423  }