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