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 }