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