github.com/pokt-network/tendermint@v0.32.11-0.20230426215212-59310158d3e9/tools/tm-signer-harness/internal/test_harness.go (about)

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