github.com/number571/tendermint@v0.34.11-gost/tools/tm-signer-harness/internal/test_harness.go (about)

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