// 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 gobHb, err := hb.GobEncode() if err != nil { return } sh.heartbeatHash, err = crypto.CalculateTruncatedHash(gobHb) if err != nil { return } // fill out signatures sh.signatures = make([]crypto.Signature, 1) signedHb, err := s.secretKey.Sign(sh.heartbeatHash[:]) if err != nil { return } sh.signatures[0] = signedHb.Signature sh.signatories = make([]byte, 1) sh.signatories[0] = s.self.index return }
// Create a state, check the defaults func TestCreateState(t *testing.T) { // does a state create without errors? s, err := CreateState(common.NewZeroNetwork()) if err != nil { t.Fatal(err) } // check that previousEntropyStage1 is initialized correctly var emptyEntropy common.Entropy emptyHash, err := crypto.CalculateTruncatedHash(emptyEntropy[:]) if err != nil { t.Fatal(err) } for i := range s.previousEntropyStage1 { if s.previousEntropyStage1[i] != emptyHash { t.Error("previousEntropyStage1 initialized incorrectly at index ", i) } } // sanity check the default values if s.participantIndex != 255 { t.Error("s.participantIndex initialized to ", s.participantIndex) } if s.currentStep != 1 { t.Error("s.currentStep should be initialized to 1!") } if s.wallets == nil { t.Error("s.wallets was not initialized") } }
// Create and initialize a state object. func CreateState(messageRouter common.MessageRouter) (s *State, err error) { s = new(State) // check that we have a non-nil messageSender if messageRouter == nil { err = fmt.Errorf("Cannot initialize with a nil messageRouter") return } // create a signature keypair for this state pubKey, secKey, err := crypto.CreateKeyPair() if err != nil { return } // calculate the value of an empty hash (default for storedEntropyStage2 on all hosts is a blank array) emptyHash, err := crypto.CalculateTruncatedHash(s.storedEntropyStage2[:]) if err != nil { return } // set state variables to their defaults s.messageRouter = messageRouter s.self = new(participant) s.self.address = messageRouter.AddMessageHandler(s) s.self.publicKey = pubKey s.secretKey = secKey for i := range s.previousEntropyStage1 { s.previousEntropyStage1[i] = emptyHash } s.participantIndex = 255 s.currentStep = 1 s.wallets = make(map[string]uint64) return }
// compile() takes the list of heartbeats and uses them to advance the state. func (s *State) compile() { // fetch a participant ordering participantOrdering := s.participantOrdering() // Lock down s.participants and s.heartbeats for editing s.participantsLock.Lock() s.heartbeatsLock.Lock() // Read heartbeats, process them, then archive them. for _, participant := range participantOrdering { if s.participants[participant] == nil { continue } // each participant must submit exactly 1 heartbeat if len(s.heartbeats[participant]) != 1 { s.tossParticipant(participant) continue } // this is the only way I know to access the only element of a map; // the key is unknown for _, hb := range s.heartbeats[participant] { s.processHeartbeat(hb, participant) } // archive heartbeats (unimplemented) // clear heartbeat list for next block s.heartbeats[participant] = make(map[crypto.TruncatedHash]*heartbeat) } // move UpcomingEntropy to CurrentEntropy s.currentEntropy = s.upcomingEntropy // generate a new heartbeat and add it to s.Heartbeats hb, err := s.newHeartbeat() if err != nil { log.Fatalln(err) } hash, err := crypto.CalculateTruncatedHash(hb.marshal()) if err != nil { log.Fatalln(err) } s.heartbeats[s.participantIndex][hash] = hb // sign and annouce the heartbeat shb, err := s.signHeartbeat(hb) if err != nil { log.Fatalln(err) } err = s.announceSignedHeartbeat(shb) if err != nil { log.Fatalln(err) } }
// Update the state according to the information presented in the heartbeat // processHeartbeat uses return codes for testing purposes func (s *State) processHeartbeat(hb *heartbeat, i byte) (err error) { print("Confirming Participant ") println(i) // Add the entropy to UpcomingEntropy th, err := crypto.CalculateTruncatedHash(append(s.upcomingEntropy[:], hb.entropy[:]...)) s.upcomingEntropy = common.Entropy(th) return }
// Update the state according to the information presented in the heartbeat // processHeartbeat uses return codes for testing purposes func (s *State) processHeartbeat(hb *heartbeat, i participantIndex) int { // compare EntropyStage2 to the hash from the previous heartbeat expectedHash, err := crypto.CalculateTruncatedHash(hb.entropyStage2[:]) if err != nil { log.Fatalln(err) } if expectedHash != s.previousEntropyStage1[i] { s.tossParticipant(i) return 1 } // Add the EntropyStage2 to UpcomingEntropy th, err := crypto.CalculateTruncatedHash(append(s.upcomingEntropy[:], hb.entropyStage2[:]...)) s.upcomingEntropy = common.Entropy(th) // store entropyStage1 to compare with next heartbeat from this participant s.previousEntropyStage1[i] = hb.entropyStage1 return 0 }
// Removes all traces of a participant from the State func (s *State) tossParticipant(pi participantIndex) { // remove from s.Participants s.participants[pi] = nil // remove from s.PreviousEntropyStage1 var emptyEntropy common.Entropy zeroHash, err := crypto.CalculateTruncatedHash(emptyEntropy[:]) if err != nil { log.Fatal(err) } s.previousEntropyStage1[pi] = zeroHash // nil map in s.Heartbeats s.heartbeats[pi] = nil }
// compile() takes the list of heartbeats and uses them to advance the state. func (s *State) compile() { participantOrdering := s.participantOrdering() // Read read heartbeats, process them, then archive them. Other functions // concurrently access the heartbeats, so mutexes are needed. for _, participant := range participantOrdering { if s.participants[participant] == nil { continue } // each participant must submit exactly 1 heartbeat if len(s.heartbeats[participant]) != 1 { s.tossParticipant(participant) continue } for _, hb := range s.heartbeats[participant] { s.processHeartbeat(hb, participant) } // archive heartbeats // currently, archives are sent to /dev/null s.heartbeats[participant] = make(map[crypto.TruncatedHash]*heartbeat) } // move UpcomingEntropy to CurrentEntropy s.currentEntropy = s.upcomingEntropy // generate a new heartbeat and add it to s.Heartbeats hb, err := s.newHeartbeat() if err != nil { log.Fatalln(err) } hash, err := crypto.CalculateTruncatedHash(hb.marshal()) if err != nil { log.Fatalln(err) } s.heartbeats[s.participantIndex][hash] = hb // sign and annouce the heartbeat shb, err := s.signHeartbeat(hb) if err != nil { log.Fatalln(err) } s.announceSignedHeartbeat(shb) }
// convert string to signedHeartbeat func unmarshalSignedHeartbeat(msh []byte) (sh *signedHeartbeat, err error) { // we reference the nth element in the []byte, make sure there is an nth element if len(msh) <= marshalledHeartbeatLen() { err = fmt.Errorf("input for unmarshalSignedHeartbeat is too short") return } numSignatures := int(msh[marshalledHeartbeatLen()]) // the nth element // verify that the total length of msh is what is expected signatureSectionLen := numSignatures * (crypto.SignatureSize + 1) totalLen := marshalledHeartbeatLen() + 1 + signatureSectionLen if len(msh) != totalLen { err = fmt.Errorf("input for UnmarshalSignedHeartbeat is incorrect length, expecting ", totalLen, " bytes") return } // get sh.Heartbeat and sh.HeartbeatHash sh = new(signedHeartbeat) index := 0 heartbeat, err := unmarshalHeartbeat(msh[index:marshalledHeartbeatLen()]) if err != nil { return } heartbeatHash, err := crypto.CalculateTruncatedHash(msh[index:marshalledHeartbeatLen()]) if err != nil { return } sh.heartbeat = heartbeat sh.heartbeatHash = heartbeatHash // get sh.Signatures and sh.Signatories index += marshalledHeartbeatLen() index += 1 // skip the numSignatures byte sh.signatures = make([]crypto.Signature, numSignatures) sh.signatories = make([]participantIndex, numSignatures) for i := 0; i < numSignatures; i++ { copy(sh.signatures[i][:], msh[index:]) index += crypto.SignatureSize sh.signatories[i] = participantIndex(msh[index]) index += 1 } return }
// Create and initialize a state object func CreateState(messageSender common.MessageSender, participantIndex participantIndex) (s State, err error) { // check that we have a non-nil messageSender if messageSender == nil { err = fmt.Errorf("Cannot initialize with a nil messageSender") return } // check that participantIndex is legal if int(participantIndex) >= common.QuorumSize { err = fmt.Errorf("Invalid participant index!") return } // initialize crypto keys pubKey, secKey, err := crypto.CreateKeyPair() if err != nil { return } // create and fill out the participant object self := new(Participant) self.Address = messageSender.Address() self.Address.Id = common.Identifier(participantIndex) self.PublicKey = pubKey // calculate the value of an empty hash (default for storedEntropyStage2 on all hosts is a blank array) emptyHash, err := crypto.CalculateTruncatedHash(s.storedEntropyStage2[:]) if err != nil { return } // set state variables to their defaults s.messageSender = messageSender s.AddParticipant(self, participantIndex) s.secretKey = secKey for i := range s.previousEntropyStage1 { s.previousEntropyStage1[i] = emptyHash } s.participantIndex = participantIndex s.currentStep = 1 s.wallets = make(map[string]uint64) return }
// Verify that newHeartbeat() produces valid heartbeats func TestnewHeartbeat(t *testing.T) { // create a state, and then a heartbeat s, err := CreateState(common.NewZeroNetwork(), 0) if err != nil { t.Fatal(err) } hb, err := s.newHeartbeat() if err != nil { t.Fatal(err) } // verify that entropy is being properly generated when making the heartbeat storedEntropyHash, err := crypto.CalculateTruncatedHash(s.storedEntropyStage2[:]) if err != nil { t.Fatal(err) } else if hb.entropyStage1 != storedEntropyHash { t.Fatal("newHeartbeat() incorrectly producing EntropyStage1 from s.StoredEntropyStage2") } }
// Take an unstarted State and begin the consensus algorithm cycle func (s *State) Start() { // start the ticker to progress the state go s.tick() s.lock.Lock() // create first heartbeat and add it to heartbeat map, then announce it hb, err := s.newHeartbeat() if err != nil { return } heartbeatHash, err := crypto.CalculateTruncatedHash([]byte(hb.marshal())) s.heartbeats[s.participantIndex][heartbeatHash] = hb shb, err := s.signHeartbeat(hb) if err != nil { return } s.announceSignedHeartbeat(shb) s.lock.Unlock() }
// Use the entropy stored in the state to generate a random integer [low, high) // randInt only runs during compile(), when the mutexes are already locked // // needs to be converted to return uint64 func (s *State) randInt(low int, high int) (randInt int, err error) { // verify there's a gap between the numbers if low == high { err = fmt.Errorf("low and high cannot be the same number") return } // Convert CurrentEntropy into an int rollingInt := 0 for i := 0; i < 4; i++ { rollingInt = rollingInt << 8 rollingInt += int(s.currentEntropy[i]) } randInt = (rollingInt % (high - low)) + low // Convert random number seed to next value truncatedHash, err := crypto.CalculateTruncatedHash(s.currentEntropy[:]) s.currentEntropy = common.Entropy(truncatedHash) return }
// 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 }
// Add a participant to the state, tell the participant about ourselves func (s *State) addNewParticipant(payload []byte) { // extract index and participant object from payload participantIndex := payload[0] p, err := unmarshalParticipant(payload[1:]) if err != nil { return } // for this participant, make the heartbeat map and add the default heartbeat hb := new(heartbeat) emptyHash, err := crypto.CalculateTruncatedHash(hb.entropyStage2[:]) hb.entropyStage1 = emptyHash s.heartbeatsLock.Lock() s.participantsLock.Lock() s.heartbeats[participantIndex] = make(map[crypto.TruncatedHash]*heartbeat) s.heartbeats[participantIndex][emptyHash] = hb s.heartbeatsLock.Unlock() if *p == *s.self { // add our self object to the correct index in participants s.participants[participantIndex] = s.self s.tickingLock.Lock() s.ticking = true s.tickingLock.Unlock() go s.tick() } else { // add the participant to participants s.participants[participantIndex] = p // tell the new guy about ourselves m := new(common.Message) m.Destination = p.address m.Payload = append([]byte(string(newParticipant)), s.self.marshal()...) s.messageRouter.SendMessage(m) } s.participantsLock.Unlock() }
// ensures Tick() calles compile() and then resets the counter to step 1 func TestCompilationTick(t *testing.T) { // test takes common.StepDuration seconds; skip for short testing if testing.Short() { t.Skip() } // create state and give it a heartbeat to prevent it from pruning itself s, err := CreateState(common.NewZeroNetwork(), 0) if err != nil { t.Fatal(err) } hb, err := s.newHeartbeat() if err != nil { return } heartbeatHash, err := crypto.CalculateTruncatedHash([]byte(hb.marshal())) s.heartbeats[s.participantIndex][heartbeatHash] = hb // remember entropy to verify that compile() gets called currentEntropy := s.currentEntropy // verify that tick is wrapping around properly s.currentStep = common.QuorumSize go s.tick() time.Sleep(common.StepDuration) time.Sleep(time.Second) s.lock.Lock() if s.currentStep != 1 { t.Fatal("s.currentStep failed to roll over: ", s.currentStep) } // check if s.compile() got called if currentEntropy == s.currentEntropy { t.Fatal("Entropy did not change after tick wrapped around") } s.lock.Unlock() }
// Using the current State, newHeartbeat() creates a heartbeat that fulfills all // of the requirements of the quorum. func (s *State) newHeartbeat() (hb *heartbeat, err error) { hb = new(heartbeat) // Fetch value used to produce EntropyStage1 in prev. heartbeat hb.entropyStage2 = s.storedEntropyStage2 // Generate EntropyStage2 for next heartbeat entropy, err := crypto.RandomByteSlice(common.EntropyVolume) if err != nil { return } copy(s.storedEntropyStage2[:], entropy) // convert entropy from slice to byte array // Use EntropyStage2 to generate EntropyStage1 for this heartbeat hb.entropyStage1, err = crypto.CalculateTruncatedHash(s.storedEntropyStage2[:]) if err != nil { return } // more code will be added here return }
func TestHandleSignedHeartbeat(t *testing.T) { // create a state and populate it with the signatories as participants s, err := CreateState(common.NewZeroNetwork()) 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 var p1 Participant var p2 Participant p1.index = 1 p2.index = 2 p1.publicKey = pubKey1 p2.publicKey = pubKey2 err = s.AddNewParticipant(p1, nil) if err != nil { t.Fatal(err) } s.AddNewParticipant(p2, nil) if err != nil { t.Fatal(err) } // create SignedHeartbeat var sh SignedHeartbeat sh.heartbeat, err = s.newHeartbeat() if err != nil { t.Fatal(err) } esh, err := sh.heartbeat.GobEncode() if err != nil { t.Fatal(err) } sh.heartbeatHash, err = crypto.CalculateTruncatedHash(esh) if err != nil { t.Fatal(err) } sh.signatures = make([]crypto.Signature, 2) sh.signatories = make([]byte, 2) // Create a set of signatures for the SignedHeartbeat signature1, err := secKey1.Sign(sh.heartbeatHash[:]) if err != nil { t.Fatal(err) } combinedMessage, err := signature1.CombinedMessage() if err != nil { t.Fatal(err) } signature2, err := secKey2.Sign(combinedMessage) if err != nil { t.Fatal(err) } // build a valid SignedHeartbeat sh.signatures[0] = signature1.Signature sh.signatures[1] = signature2.Signature sh.signatories[0] = 1 sh.signatories[1] = 2 // delete existing heartbeat from state; makes the remaining tests easier s.heartbeats[sh.signatories[0]] = make(map[crypto.TruncatedHash]*heartbeat) // handle the signed heartbeat, expecting nil error err = s.HandleSignedHeartbeat(sh, nil) if err != nil { t.Fatal(err) } // verify that a repeat heartbeat gets ignored err = s.HandleSignedHeartbeat(sh, nil) if err != hsherrHaveHeartbeat { t.Error("expected heartbeat to get ignored as a duplicate:", err) } // create a different heartbeat, this will be used to test the fail conditions sh.heartbeat, err = s.newHeartbeat() if err != nil { t.Fatal(err) } ehb, err := sh.heartbeat.GobEncode() if err != nil { t.Fatal(err) } sh.heartbeatHash, err = crypto.CalculateTruncatedHash(ehb) if err != nil { t.Fatal(err) } // verify a heartbeat with bad signatures is rejected err = s.HandleSignedHeartbeat(sh, nil) if err != hsherrInvalidSignature { t.Error("expected heartbeat to get ignored as having invalid signatures: ", err) } // verify that a non-participant gets rejected sh.signatories[0] = 3 err = s.HandleSignedHeartbeat(sh, nil) if err != hsherrNonParticipant { t.Error("expected non-participant to be rejected: ", err) } // give heartbeat repeat signatures signature1, err = secKey1.Sign(sh.heartbeatHash[:]) if err != nil { t.Fatal(err) } combinedMessage, err = signature1.CombinedMessage() if err != nil { t.Fatal(err) } signature2, err = secKey1.Sign(combinedMessage) if err != nil { t.Error(err) } // 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 err = s.HandleSignedHeartbeat(sh, nil) if err != hsherrDoubleSigned { t.Error("expected heartbeat to be rejected for duplicate signatures: ", err) } // remove second signature sh.signatures = sh.signatures[:1] sh.signatories = sh.signatories[:1] // handle heartbeat when tick is larger than num signatures s.stepLock.Lock() s.currentStep = 2 s.stepLock.Unlock() err = s.HandleSignedHeartbeat(sh, nil) if err != hsherrNoSync { t.Error("expected heartbeat to be rejected as out-of-sync: ", err) } // remaining tests require sleep if testing.Short() { t.Skip() } // send a heartbeat right at the edge of a new block s.stepLock.Lock() s.currentStep = common.QuorumSize s.stepLock.Unlock() // submit heartbeat in separate thread go func() { err = s.HandleSignedHeartbeat(sh, nil) if err != nil { t.Fatal("expected heartbeat to succeed!: ", err) } // need some way to verify with the test that the funcion gets here }() s.stepLock.Lock() s.currentStep = 1 s.stepLock.Unlock() time.Sleep(time.Second) time.Sleep(common.StepDuration) }
joinSia byte = iota incomingSignedHeartbeat addressChangeNotification newParticipant ) // Bootstrapping var bootstrapAddress = common.Address{ ID: 1, Host: "localhost", Port: 9988, } // empty hash value var emptyEntropy = common.Entropy{} var emptyHash, _ = crypto.CalculateTruncatedHash(emptyEntropy[:]) // Identifies other members of the quorum type Participant struct { index byte address common.Address publicKey *crypto.PublicKey } // The state provides persistence to the consensus algorithms. Every participant // should have an identical state. type State struct { // Network Variables messageRouter common.MessageRouter participants [common.QuorumSize]*Participant // list of participants participantsLock sync.RWMutex // write-locks for compile only
// 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) }