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 }