// take new heartbeat (our own), sign it, and package it into a signedHearteat // I'm pretty sure this only follows a newHeartbeat() call; they can be merged func (s *State) signHeartbeat(hb *heartbeat) (sh *signedHeartbeat, err error) { sh = new(signedHeartbeat) // confirm heartbeat and hash sh.heartbeat = hb marshalledHb := hb.marshal() sh.heartbeatHash, err = crypto.CalculateTruncatedHash([]byte(marshalledHb)) if err != nil { return } // fill out sigantures sh.signatures = make([]crypto.Signature, 1) signedHb, err := crypto.Sign(s.secretKey, string(sh.heartbeatHash[:])) if err != nil { return } sh.signatures[0] = signedHb.Signature sh.signatories = make([]participantIndex, 1) sh.signatories[0] = s.participantIndex return }
// TestHandleSignedHeartbeat should probably be reviewed and rehashed func TestHandleSignedHeartbeat(t *testing.T) { // create a state and populate it with the signatories as participants s, err := CreateState(common.NewZeroNetwork(), 0) if err != nil { t.Fatal(err) } // create keypairs pubKey1, secKey1, err := crypto.CreateKeyPair() if err != nil { t.Fatal(err) } pubKey2, secKey2, err := crypto.CreateKeyPair() if err != nil { t.Fatal(err) } // create participants and add them to s p1 := new(Participant) p2 := new(Participant) p1.PublicKey = pubKey1 p2.PublicKey = pubKey2 s.AddParticipant(p1, 1) s.AddParticipant(p2, 2) // create SignedHeartbeat var sh signedHeartbeat sh.heartbeat, err = s.newHeartbeat() if err != nil { t.Fatal(err) } sh.heartbeatHash, err = crypto.CalculateTruncatedHash([]byte(sh.heartbeat.marshal())) if err != nil { t.Fatal(err) } sh.signatures = make([]crypto.Signature, 2) sh.signatories = make([]participantIndex, 2) // Create a set of signatures for the SignedHeartbeat signature1, err := crypto.Sign(secKey1, string(sh.heartbeatHash[:])) if err != nil { t.Fatal("error signing HeartbeatHash") } signature2, err := crypto.Sign(secKey2, signature1.CombinedMessage()) if err != nil { t.Fatal("error with second signing") } // build a valid SignedHeartbeat sh.signatures[0] = signature1.Signature sh.signatures[1] = signature2.Signature sh.signatories[0] = 1 sh.signatories[1] = 2 // handle the signed heartbeat, expecting code 0 msh, err := sh.marshal() if err != nil { t.Fatal(err) } returnCode := s.handleSignedHeartbeat(msh) if returnCode != 0 { t.Fatal("expected heartbeat to succeed:", returnCode) } // verify that a repeat heartbeat gets ignored msh, err = sh.marshal() if err != nil { t.Fatal(err) } returnCode = s.handleSignedHeartbeat(msh) if returnCode != 8 { t.Fatal("expected heartbeat to get ignored as a duplicate:", returnCode) } // create a different heartbeat, this will be used to test the fail conditions sh.heartbeat, err = s.newHeartbeat() if err != nil { t.Fatal(err) } sh.heartbeatHash, err = crypto.CalculateTruncatedHash([]byte(sh.heartbeat.marshal())) if err != nil { t.Fatal(err) } // verify a heartbeat with bad signatures is rejected msh, err = sh.marshal() if err != nil { t.Fatal(err) } returnCode = s.handleSignedHeartbeat(msh) if returnCode != 6 { t.Fatal("expected heartbeat to get ignored as having invalid signatures: ", returnCode) } // give heartbeat repeat signatures signature1, err = crypto.Sign(secKey1, string(sh.heartbeatHash[:])) if err != nil { t.Fatal("error with third signing") } signature2, err = crypto.Sign(secKey1, signature1.CombinedMessage()) if err != nil { t.Fatal("error with fourth signing") } // adjust signatories slice sh.signatures[0] = signature1.Signature sh.signatures[1] = signature2.Signature sh.signatories[0] = 1 sh.signatories[1] = 1 // verify repeated signatures are rejected msh, err = sh.marshal() if err != nil { t.Fatal(err) } returnCode = s.handleSignedHeartbeat(msh) if returnCode != 5 { t.Fatal("expected heartbeat to be rejected for duplicate signatures: ", returnCode) } // remove second signature sh.signatures = sh.signatures[:1] sh.signatories = sh.signatories[:1] // handle heartbeat when tick is larger than num signatures s.currentStep = 2 msh, err = sh.marshal() if err != nil { t.Fatal(err) } returnCode = s.handleSignedHeartbeat(msh) if returnCode != 2 { t.Fatal("expected heartbeat to be rejected as out-of-sync: ", returnCode) } // send a heartbeat right at the edge of a new block // test takes time; skip in short tests if testing.Short() { t.Skip() } // put block at edge s.currentStep = common.QuorumSize // submit heartbeat in separate thread go func() { msh, err = sh.marshal() if err != nil { t.Fatal(err) } returnCode = s.handleSignedHeartbeat(msh) if returnCode != 0 { t.Fatal("expected heartbeat to succeed!: ", returnCode) } }() time.Sleep(time.Second) }
// handleSignedHeartbeat takes the payload of an incoming message of type // 'incomingSignedHeartbeat' and verifies it according to rules established by // the specification. // // The return code is currently purely for the testing suite, the numbers // have been chosen arbitrarily func (s *State) handleSignedHeartbeat(payload []byte) (returnCode int) { // covert payload to SignedHeartbeat sh, err := unmarshalSignedHeartbeat(payload) if err != nil { log.Infoln("Received bad message SignedHeartbeat: ", err) returnCode = 11 return } // Check that the slices of signatures and signatories are of the same length if len(sh.signatures) != len(sh.signatories) { log.Infoln("SignedHeartbeat has mismatched signatures") returnCode = 1 return } // check that there are not too many signatures and signatories if len(sh.signatories) > common.QuorumSize { log.Infoln("Received an over-signed signedHeartbeat") returnCode = 12 return } s.stepLock.Lock() // prevents a benign race condition; is here to follow best practices // s.CurrentStep must be less than or equal to len(sh.Signatories), unless // there is a new block and s.CurrentStep is common.QuorumSize if s.currentStep > len(sh.signatories) { if s.currentStep == common.QuorumSize && len(sh.signatories) == 1 { // by waiting common.StepDuration, the new block will be compiled time.Sleep(common.StepDuration) // now continue to rest of function } else { log.Infoln("Received an out-of-sync SignedHeartbeat") returnCode = 2 return } } s.stepLock.Unlock() // Check bounds on first signatory if int(sh.signatories[0]) >= common.QuorumSize { log.Infoln("Received an out of bounds index") returnCode = 9 return } // we are starting to read from memory, initiate locks s.participantsLock.RLock() s.heartbeatsLock.Lock() // check that first sigatory is a participant if s.participants[sh.signatories[0]] == nil { log.Infoln("Received heartbeat from non-participant") returnCode = 10 return } // Check if we have already received this heartbeat _, exists := s.heartbeats[sh.signatories[0]][sh.heartbeatHash] if exists { returnCode = 8 return } // Check if we already have two heartbeats from this host if len(s.heartbeats[sh.signatories[0]]) >= 2 { log.Infoln("Received many invalid heartbeats from one host") returnCode = 13 return } // iterate through the signatures and make sure each is legal var signedMessage crypto.SignedMessage // grows each iteration signedMessage.Message = string(sh.heartbeatHash[:]) previousSignatories := make(map[participantIndex]bool) // which signatories have already signed for i, signatory := range sh.signatories { // Check bounds on the signatory if int(signatory) >= common.QuorumSize { log.Infoln("Received an out of bounds index") returnCode = 9 return } // Verify that the signatory is a participant in the quorum if s.participants[signatory] == nil { log.Infoln("Received a heartbeat signed by an invalid signatory") returnCode = 4 return } // Verify that the signatory has only been seen once in the current SignedHeartbeat if previousSignatories[signatory] { log.Infoln("Received a double-signed heartbeat") returnCode = 5 return } // record that we've seen this signatory in the current SignedHeartbeat previousSignatories[signatory] = true // verify the signature signedMessage.Signature = sh.signatures[i] verification, err := crypto.Verify(s.participants[signatory].publicKey, signedMessage) if err != nil { log.Fatalln(err) return } // check status of verification if !verification { log.Infoln("Received invalid signature in SignedHeartbeat") returnCode = 6 return } // throwing the signature into the message here makes code cleaner in the loop // and after we sign it to send it to everyone else signedMessage.Message = signedMessage.CombinedMessage() } // Add heartbeat to list of seen heartbeats s.heartbeats[sh.signatories[0]][sh.heartbeatHash] = sh.heartbeat // Sign the stack of signatures and send it to all hosts signedMessage, err = crypto.Sign(s.secretKey, signedMessage.Message) if err != nil { log.Fatalln(err) } // add our signature to the signedHeartbeat sh.signatures = append(sh.signatures, signedMessage.Signature) sh.signatories = append(sh.signatories, s.participantIndex) // broadcast the message to the quorum err = s.announceSignedHeartbeat(sh) if err != nil { log.Fatalln(err) } returnCode = 0 return }
// HandleSignedHeartbeat takes a heartbeat that has been signed // as a part of the concensus algorithm, and follows all the rules // that are necessary to ensure that all honest hosts arrive at // the same conclusions about the actions of their peers. // // See the paper 'The Byzantine Generals Problem' for more insight // on the algorithms used here. Paper can be found in // doc/The Byzantine Generals Problem // // This function is called concurrently, mutexes will be needed when // accessing or altering the State // // It is assumed that when this function is called, the Heartbeat in // question will already be in memory, and was correctly signed by the // first signatory, the the first signatory is a participant, and that // it matches its hash. And that the first signatory is used to store // the heartbeat // // The return code is purely for the testing suite. The numbers are chosen // arbitrarily func (s *State) handleSignedHeartbeat(message []byte) (returnCode int) { // covert message to SignedHeartbeat sh, err := unmarshalSignedHeartbeat(message) if err != nil { log.Infoln("Received bad message SignedHeartbeat: ", err) return } // Check that the slices of signatures and signatories are of the same length if len(sh.signatures) != len(sh.signatories) { log.Infoln("SignedHeartbeat has mismatched signatures") returnCode = 1 return } // s.CurrentStep must be less than or equal to len(sh.Signatories), unless the // current step is common.QuorumSize and len(sh.Signatories) == 1 if s.currentStep > len(sh.signatories) { if s.currentStep == common.QuorumSize && len(sh.signatories) == 1 { // sleep long enough to pass the first requirement time.Sleep(common.StepDuration) // now continue to rest of function } else { log.Infoln("Received an out-of-sync SignedHeartbeat") returnCode = 2 return } } // Check bounds on first signatory if int(sh.signatories[0]) >= common.QuorumSize { log.Infoln("Received an out of bounds index") returnCode = 9 return } // Check existence of first signatory if s.participants[sh.signatories[0]] == nil { log.Infoln("Received heartbeat from non-participant") returnCode = 10 return } // Check if we have already received this heartbeat _, exists := s.heartbeats[sh.signatories[0]][sh.heartbeatHash] if exists { returnCode = 8 return } // while processing signatures, signedMessage will keep growing var signedMessage crypto.SignedMessage signedMessage.Message = string(sh.heartbeatHash[:]) // keep a map of which signatories have already been confirmed previousSignatories := make(map[participantIndex]bool) for i, signatory := range sh.signatories { // Check bounds on the signatory if int(signatory) >= common.QuorumSize { log.Infoln("Received an out of bounds index") returnCode = 9 return } // Verify that the signatory is a participant in the quorum if s.participants[signatory] == nil { log.Infoln("Received a heartbeat signed by an invalid signatory") returnCode = 4 return } // Verify that the signatory has only been seen once in the current SignedHeartbeat if previousSignatories[signatory] { log.Infoln("Received a double-signed heartbeat") returnCode = 5 return } // record that we've seen this signatory in the current SignedHeartbeat previousSignatories[signatory] = true // verify the signature signedMessage.Signature = sh.signatures[i] verification, err := crypto.Verify(s.participants[signatory].PublicKey, signedMessage) if err != nil { log.Fatalln(err) return } // check status of verification if !verification { log.Infoln("Received invalid signature in SignedHeartbeat") returnCode = 6 return } // throwing the signature into the message here makes code cleaner in the loop // and after we sign it to send it to everyone else signedMessage.Message = signedMessage.CombinedMessage() } // Add heartbeat to list of seen heartbeats // Don't check if heartbeat is valid, that's for compile() s.heartbeats[sh.signatories[0]][sh.heartbeatHash] = sh.heartbeat // Sign the stack of signatures and send it to all hosts _, err = crypto.Sign(s.secretKey, signedMessage.Message) if err != nil { log.Fatalln(err) } // Send the new message to everybody returnCode = 0 return }