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 }