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