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 }