package consensus import ( "bytes" "encoding/binary" "encoding/gob" "encoding/hex" "errors" "time" "github.com/simple-rules/harmony-benchmark/profiler" "github.com/dedis/kyber" "github.com/dedis/kyber/sign/schnorr" "github.com/simple-rules/harmony-benchmark/blockchain" "github.com/simple-rules/harmony-benchmark/crypto" "github.com/simple-rules/harmony-benchmark/log" "github.com/simple-rules/harmony-benchmark/p2p" proto_consensus "github.com/simple-rules/harmony-benchmark/proto/consensus" ) var ( startTime time.Time ) // WaitForNewBlock waits for the next new block to run consensus on func (consensus *Consensus) WaitForNewBlock(blockChannel chan blockchain.Block) { consensus.Log.Debug("Waiting for block", "consensus", consensus) for { // keep waiting for new blocks newBlock := <-blockChannel // TODO: think about potential race condition startTime = time.Now() consensus.Log.Debug("STARTING CONSENSUS", "consensus", consensus, "startTime", startTime) for consensus.state == Finished { // time.Sleep(500 * time.Millisecond) consensus.startConsensus(&newBlock) break } } } // ProcessMessageLeader dispatches consensus message for the leader. func (consensus *Consensus) ProcessMessageLeader(message []byte) { msgType, err := proto_consensus.GetConsensusMessageType(message) if err != nil { consensus.Log.Error("Failed to get consensus message type.", "err", err, "consensus", consensus) } payload, err := proto_consensus.GetConsensusMessagePayload(message) if err != nil { consensus.Log.Error("Failed to get consensus message payload.", "err", err, "consensus", consensus) } switch msgType { case proto_consensus.StartConsensus: consensus.processStartConsensusMessage(payload) case proto_consensus.Commit: consensus.processCommitMessage(payload, ChallengeDone) case proto_consensus.Response: consensus.processResponseMessage(payload, CollectiveSigDone) case proto_consensus.FinalCommit: consensus.processCommitMessage(payload, FinalChallengeDone) case proto_consensus.FinalResponse: consensus.processResponseMessage(payload, Finished) default: consensus.Log.Error("Unexpected message type", "msgType", msgType, "consensus", consensus) } } // processStartConsensusMessage is the handler for message which triggers consensus process. func (consensus *Consensus) processStartConsensusMessage(payload []byte) { // TODO: remove these method after testnet tx := blockchain.NewCoinbaseTX([20]byte{0}, "y", 0) consensus.startConsensus(blockchain.NewGenesisBlock(tx, 0)) } // startConsensus starts a new consensus for a block by broadcast a announce message to the validators func (consensus *Consensus) startConsensus(newBlock *blockchain.Block) { // Copy over block hash and block header data copy(consensus.blockHash[:], newBlock.Hash[:]) consensus.Log.Debug("Start encoding block") // prepare message and broadcast to validators byteBuffer := bytes.NewBuffer([]byte{}) encoder := gob.NewEncoder(byteBuffer) encoder.Encode(newBlock) consensus.blockHeader = byteBuffer.Bytes() consensus.Log.Debug("Stop encoding block") msgToSend := consensus.constructAnnounceMessage() p2p.BroadcastMessageFromLeader(consensus.GetValidatorPeers(), msgToSend) // Set state to AnnounceDone consensus.state = AnnounceDone consensus.commitByLeader(true) } // commitByLeader commits to the message itself before receiving others commits func (consensus *Consensus) commitByLeader(firstRound bool) { // Generate leader's own commitment secret, commitment := crypto.Commit(crypto.Ed25519Curve) consensus.secret[consensus.consensusID] = secret if firstRound { (*consensus.commitments)[consensus.nodeID] = commitment consensus.bitmap.SetKey(consensus.pubKey, true) } else { (*consensus.finalCommitments)[consensus.nodeID] = commitment consensus.finalBitmap.SetKey(consensus.pubKey, true) } } // processCommitMessage processes the commit message sent from validators func (consensus *Consensus) processCommitMessage(payload []byte, targetState State) { // Read payload data offset := 0 // 4 byte consensus id consensusID := binary.BigEndian.Uint32(payload[offset : offset+4]) offset += 4 // 32 byte block hash blockHash := payload[offset : offset+32] offset += 32 // 2 byte validator id validatorID := binary.BigEndian.Uint16(payload[offset : offset+2]) offset += 2 // 32 byte commit commitment := payload[offset : offset+32] offset += 32 // 64 byte of signature on all above data signature := payload[offset : offset+64] offset += 64 // Verify signature value, ok := consensus.validators[validatorID] if !ok { consensus.Log.Warn("Received message from unrecognized validator", "validatorID", validatorID, "consensus", consensus) return } if schnorr.Verify(crypto.Ed25519Curve, value.PubKey, payload[:offset-64], signature) != nil { consensus.Log.Warn("Received message with invalid signature", "validatorKey", consensus.leader.PubKey, "consensus", consensus) return } // check consensus Id consensus.mutex.Lock() defer consensus.mutex.Unlock() if consensusID != consensus.consensusID { consensus.Log.Warn("Received Commit with wrong consensus Id", "myConsensusId", consensus.consensusID, "theirConsensusId", consensusID, "consensus", consensus) return } if !bytes.Equal(blockHash, consensus.blockHash[:]) { consensus.Log.Warn("Received Commit with wrong blockHash", "myConsensusId", consensus.consensusID, "theirConsensusId", consensusID, "consensus", consensus) return } commitments := consensus.commitments // targetState == ChallengeDone bitmap := consensus.bitmap if targetState == FinalChallengeDone { commitments = consensus.finalCommitments bitmap = consensus.finalBitmap } // proceed only when the message is not received before _, ok = (*commitments)[validatorID] shouldProcess := !ok if len((*commitments)) >= ((len(consensus.publicKeys)*2)/3 + 1) { shouldProcess = false } if shouldProcess { point := crypto.Ed25519Curve.Point() point.UnmarshalBinary(commitment) (*commitments)[validatorID] = point consensus.Log.Debug("Received new commit message", "num", len(*commitments), "validatorID", validatorID) // Set the bitmap indicate this validate signed. TODO: figure out how to resolve the inconsistency of validators from commit and response messages bitmap.SetKey(value.PubKey, true) } if !shouldProcess { consensus.Log.Debug("Received new commit message", "validatorID", validatorID) return } if len((*commitments)) >= ((len(consensus.publicKeys)*2)/3+1) && consensus.state < targetState { consensus.Log.Debug("Enough commitments received with signatures", "num", len(*commitments), "state", consensus.state) // Broadcast challenge msgTypeToSend := proto_consensus.Challenge // targetState == ChallengeDone if targetState == FinalChallengeDone { msgTypeToSend = proto_consensus.FinalChallenge } msgToSend, challengeScalar, aggCommitment := consensus.constructChallengeMessage(msgTypeToSend) bytes, err := challengeScalar.MarshalBinary() if err != nil { log.Error("Failed to serialize challenge") } if msgTypeToSend == proto_consensus.Challenge { copy(consensus.challenge[:], bytes) consensus.aggregatedCommitment = aggCommitment } else if msgTypeToSend == proto_consensus.FinalChallenge { copy(consensus.finalChallenge[:], bytes) consensus.aggregatedFinalCommitment = aggCommitment } // Add leader's response consensus.responseByLeader(challengeScalar, targetState == ChallengeDone) // Broadcast challenge message p2p.BroadcastMessageFromLeader(consensus.GetValidatorPeers(), msgToSend) // Set state to targetState (ChallengeDone or FinalChallengeDone) consensus.state = targetState } } // Leader commit to the message itself before receiving others commits func (consensus *Consensus) responseByLeader(challenge kyber.Scalar, firstRound bool) { // Generate leader's own commitment response, err := crypto.Response(crypto.Ed25519Curve, consensus.priKey, consensus.secret[consensus.consensusID], challenge) if err == nil { if firstRound { (*consensus.responses)[consensus.nodeID] = response consensus.bitmap.SetKey(consensus.pubKey, true) } else { (*consensus.finalResponses)[consensus.nodeID] = response consensus.finalBitmap.SetKey(consensus.pubKey, true) } } else { log.Warn("Failed to generate response", "err", err) } } // Processes the response message sent from validators func (consensus *Consensus) processResponseMessage(payload []byte, targetState State) { //#### Read payload data offset := 0 // 4 byte consensus id consensusID := binary.BigEndian.Uint32(payload[offset : offset+4]) offset += 4 // 32 byte block hash blockHash := payload[offset : offset+32] offset += 32 // 2 byte validator id validatorID := binary.BigEndian.Uint16(payload[offset : offset+2]) offset += 2 // 32 byte response response := payload[offset : offset+32] offset += 32 // 64 byte of signature on previous data signature := payload[offset : offset+64] offset += 64 //#### END: Read payload data shouldProcess := true consensus.mutex.Lock() defer consensus.mutex.Unlock() // check consensus Id if consensusID != consensus.consensusID { shouldProcess = false consensus.Log.Warn("Received Response with wrong consensus Id", "myConsensusId", consensus.consensusID, "theirConsensusId", consensusID, "consensus", consensus) } if !bytes.Equal(blockHash, consensus.blockHash[:]) { consensus.Log.Warn("Received Response with wrong blockHash", "myConsensusId", consensus.consensusID, "theirConsensusId", consensusID, "consensus", consensus) return } // Verify signature value, ok := consensus.validators[validatorID] if !ok { consensus.Log.Warn("Received message from unrecognized validator", "validatorID", validatorID, "consensus", consensus) return } if schnorr.Verify(crypto.Ed25519Curve, value.PubKey, payload[:offset-64], signature) != nil { consensus.Log.Warn("Received message with invalid signature", "validatorKey", consensus.leader.PubKey, "consensus", consensus) return } commitments := consensus.commitments // targetState == CollectiveSigDone responses := consensus.responses bitmap := consensus.bitmap if targetState == Finished { commitments = consensus.finalCommitments responses = consensus.finalResponses bitmap = consensus.finalBitmap } // proceed only when the message is not received before _, ok = (*responses)[validatorID] shouldProcess = shouldProcess && !ok if len((*responses)) >= ((len(consensus.publicKeys)*2)/3 + 1) { shouldProcess = false } if shouldProcess { // verify the response matches the received commit responseScalar := crypto.Ed25519Curve.Scalar() responseScalar.UnmarshalBinary(response) err := consensus.verifyResponse(commitments, responseScalar, validatorID) if err != nil { consensus.Log.Warn("Failed to verify the response", "error", err) shouldProcess = false } else { (*responses)[validatorID] = responseScalar consensus.Log.Debug("Received new response message", "num", len(*responses), "validatorID", validatorID) // Set the bitmap indicate this validate signed. TODO: figure out how to resolve the inconsistency of validators from commit and response messages bitmap.SetKey(value.PubKey, true) } } if !shouldProcess { consensus.Log.Debug("Received new response message", "validatorID", validatorID) return } if len(*responses) >= ((len(consensus.publicKeys)*2)/3+1) && consensus.state != targetState { if len(*responses) >= ((len(consensus.publicKeys)*2)/3+1) && consensus.state != targetState { consensus.Log.Debug("Enough responses received with signatures", "num", len(*responses), "state", consensus.state) // Aggregate responses responseScalars := []kyber.Scalar{} for _, val := range *responses { responseScalars = append(responseScalars, val) } aggregatedResponse, err := crypto.AggregateResponses(crypto.Ed25519Curve, responseScalars) if err != nil { log.Error("Failed to aggregate responses") return } aggregatedCommitment := consensus.aggregatedCommitment if targetState == Finished { aggregatedCommitment = consensus.aggregatedFinalCommitment } collectiveSigAndBitmap, err := crypto.Sign(crypto.Ed25519Curve, aggregatedCommitment, aggregatedResponse, bitmap) if err != nil { log.Error("Failed to create collective signature") return } log.Info("CollectiveSig and Bitmap created.", "size", len(collectiveSigAndBitmap)) collectiveSig := [64]byte{} copy(collectiveSig[:], collectiveSigAndBitmap[:64]) bitmap := collectiveSigAndBitmap[64:] // Set state to CollectiveSigDone or Finished consensus.state = targetState if consensus.state != Finished { // Start the second round of Cosi msgToSend := consensus.constructCollectiveSigMessage(collectiveSig, bitmap) p2p.BroadcastMessageFromLeader(consensus.GetValidatorPeers(), msgToSend) consensus.commitByLeader(false) } else { consensus.Log.Debug("Consensus reached with signatures.", "numOfSignatures", len(*responses)) // Reset state to Finished, and clear other data. consensus.ResetState() consensus.consensusID++ consensus.Log.Debug("HOORAY!!! CONSENSUS REACHED!!!", "consensusID", consensus.consensusID) // TODO: reconstruct the whole block from header and transactions // For now, we used the stored whole block already stored in consensus.blockHeader txDecoder := gob.NewDecoder(bytes.NewReader(consensus.blockHeader)) var blockHeaderObj blockchain.Block err = txDecoder.Decode(&blockHeaderObj) if err != nil { consensus.Log.Debug("failed to construct the new block after consensus") } // Sign the block copy(blockHeaderObj.Signature[:], collectiveSig[:]) copy(blockHeaderObj.Bitmap[:], bitmap) consensus.OnConsensusDone(&blockHeaderObj) consensus.reportMetrics(blockHeaderObj) // Send signal to Node so the new block can be added and new round of consensus can be triggered consensus.ReadySignal <- struct{}{} } } } } func (consensus *Consensus) verifyResponse(commitments *map[uint16]kyber.Point, response kyber.Scalar, validatorID uint16) error { if response.Equal(crypto.Ed25519Curve.Scalar()) { return errors.New("response is zero valued") } _, ok := (*commitments)[validatorID] if !ok { return errors.New("no commit is received for the validator") } // TODO(RJ): enable the actual check //challenge := crypto.Ed25519Curve.Scalar() //challenge.UnmarshalBinary(consensus.challenge[:]) // //// compute Q = sG + r*pubKey //sG := crypto.Ed25519Curve.Point().Mul(response, nil) //r_pubKey := crypto.Ed25519Curve.Point().Mul(challenge, consensus.validators[validatorID].PubKey) //Q := crypto.Ed25519Curve.Point().Add(sG, r_pubKey) // //if !Q.Equal(commit) { // return errors.New("recreated commit doesn't match the received one") //} return nil } func (consensus *Consensus) reportMetrics(block blockchain.Block) { if block.IsStateBlock() { // Skip state block stats return } endTime := time.Now() timeElapsed := endTime.Sub(startTime) numOfTxs := block.NumTransactions tps := float64(numOfTxs) / timeElapsed.Seconds() consensus.Log.Info("TPS Report", "numOfTXs", numOfTxs, "startTime", startTime, "endTime", endTime, "timeElapsed", timeElapsed, "TPS", tps, "consensus", consensus) // Post metrics profiler := profiler.GetProfiler() if profiler.MetricsReportURL == "" { return } txHashes := []string{} for i, end := 0, len(block.TransactionIds); i < 3 && i < end; i++ { txHashes = append(txHashes, hex.EncodeToString(block.TransactionIds[end-1-i][:])) } metrics := map[string]interface{}{ "key": consensus.pubKey.String(), "tps": tps, "txCount": numOfTxs, "nodeCount": len(consensus.validators) + 1, "latestBlockHash": hex.EncodeToString(consensus.blockHash[:]), "latestTxHashes": txHashes, "blockLatency": int(timeElapsed / time.Millisecond), } profiler.LogMetrics(metrics) }