diff --git a/cmd/cmd.go b/cmd/cmd.go index 7cef097bc..ed5ca7730 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -435,8 +435,9 @@ func makeLocalGenesis(w wallet.Wallet) *genesis.Genesis { crypto.TreasuryAddress: acc, } - vals := make([]*validator.Validator, 4) - for i := 0; i < 4; i++ { + genValNum := 6 + vals := make([]*validator.Validator, genValNum) + for i := 0; i < genValNum; i++ { info := w.AddressInfo(w.AddressInfos()[i].Address) pub, _ := bls.PublicKeyFromString(info.PublicKey) vals[i] = validator.NewValidator(pub, int32(i)) diff --git a/committee/committee_test.go b/committee/committee_test.go index f014a4586..f91bce538 100644 --- a/committee/committee_test.go +++ b/committee/committee_test.go @@ -23,11 +23,11 @@ func TestContains(t *testing.T) { func TestProposer(t *testing.T) { ts := testsuite.NewTestSuite(t) - cmt, _ := ts.GenerateTestCommittee(4) + cmt, _ := ts.GenerateTestCommittee(6) assert.Equal(t, cmt.Proposer(0).Number(), int32(0)) assert.Equal(t, cmt.Proposer(3).Number(), int32(3)) - assert.Equal(t, cmt.Proposer(4).Number(), int32(0)) + assert.Equal(t, cmt.Proposer(6).Number(), int32(0)) cmt.Update(0, nil) assert.Equal(t, cmt.Proposer(0).Number(), int32(1)) diff --git a/consensus/commit.go b/consensus/commit.go index 1cc1df9f6..d71cd4a8d 100644 --- a/consensus/commit.go +++ b/consensus/commit.go @@ -18,7 +18,7 @@ func (s *commitState) decide() { certBlock := roundProposal.Block() precommits := s.log.PrecommitVoteSet(s.round) votes := precommits.BlockVotes(certBlock.Hash()) - cert := s.makeCertificate(votes) + cert := s.makeBlockCertificate(votes) err := s.bcState.CommitBlock(certBlock, cert) if err != nil { s.logger.Error("committing block failed", "block", certBlock, "error", err) diff --git a/consensus/config.go b/consensus/config.go index 8ba67136c..275cc689c 100644 --- a/consensus/config.go +++ b/consensus/config.go @@ -20,7 +20,7 @@ func DefaultConfig() *Config { func (conf *Config) BasicCheck() error { if conf.ChangeProposerTimeout <= 0 { return ConfigError{ - Reason: "timeout for change proposer must be greater than zero", + Reason: "change proposer timeout must be greater than zero", } } if conf.ChangeProposerDelta <= 0 { diff --git a/consensus/config_test.go b/consensus/config_test.go index f75318d46..d612c3651 100644 --- a/consensus/config_test.go +++ b/consensus/config_test.go @@ -19,10 +19,10 @@ func TestDefaultConfigCheck(t *testing.T) { assert.ErrorIs(t, c2.BasicCheck(), ConfigError{Reason: "change proposer delta must be greater than zero"}) c3.ChangeProposerTimeout = 0 * time.Second - assert.ErrorIs(t, c3.BasicCheck(), ConfigError{Reason: "timeout for change proposer must be greater than zero"}) + assert.ErrorIs(t, c3.BasicCheck(), ConfigError{Reason: "change proposer timeout must be greater than zero"}) c4.ChangeProposerTimeout = -1 * time.Second - assert.ErrorIs(t, c4.BasicCheck(), ConfigError{Reason: "timeout for change proposer must be greater than zero"}) + assert.ErrorIs(t, c4.BasicCheck(), ConfigError{Reason: "change proposer timeout must be greater than zero"}) c5.MinimumAvailabilityScore = 1.5 assert.ErrorIs(t, c5.BasicCheck(), ConfigError{Reason: "minimum availability score can't be negative or more than 1"}) diff --git a/consensus/consensus.go b/consensus/consensus.go index 76acec464..0d7bfe90a 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -400,13 +400,25 @@ func (cs *consensus) broadcastVote(v *vote.Vote) { message.NewVoteMessage(v)) } -func (cs *consensus) announceNewBlock(blk *block.Block, cert *certificate.Certificate) { +func (cs *consensus) announceNewBlock(blk *block.Block, cert *certificate.BlockCertificate) { go cs.mediator.OnBlockAnnounce(cs) cs.broadcaster(cs.valKey.Address(), message.NewBlockAnnounceMessage(blk, cert)) } -func (cs *consensus) makeCertificate(votes map[crypto.Address]*vote.Vote) *certificate.Certificate { +func (cs *consensus) makeBlockCertificate(votes map[crypto.Address]*vote.Vote, +) *certificate.BlockCertificate { + cert := certificate.NewBlockCertificate(cs.height, cs.round, false) + cert.SetSignature(cs.signersInfo(votes)) + + return cert +} + +// signersInfo processes a map of votes from validators and provides these information: +// - A list of all validators' numbers eligible to vote in this step. +// - A list of absentee validators' numbers who did not vote in this step. +// - An aggregated signature generated from the signatures of participating validators. +func (cs *consensus) signersInfo(votes map[crypto.Address]*vote.Vote) ([]int32, []int32, *bls.Signature) { vals := cs.validators committers := make([]int32, len(vals)) absentees := make([]int32, 0) @@ -425,7 +437,15 @@ func (cs *consensus) makeCertificate(votes map[crypto.Address]*vote.Vote) *certi aggSig := bls.SignatureAggregate(sigs...) - return certificate.NewCertificate(cs.height, cs.round, committers, absentees, aggSig) + return committers, absentees, aggSig +} + +func (cs *consensus) makeVoteCertificate(votes map[crypto.Address]*vote.Vote, +) *certificate.VoteCertificate { + cert := certificate.NewVoteCertificate(cs.height, cs.round) + cert.SetSignature(cs.signersInfo(votes)) + + return cert } // IsActive checks if the consensus is in an active state and participating in the consensus algorithm. diff --git a/consensus/consensus_test.go b/consensus/consensus_test.go index 4a211c186..da95f69a5 100644 --- a/consensus/consensus_test.go +++ b/consensus/consensus_test.go @@ -274,23 +274,23 @@ func (td *testData) addPrecommitVote(cons *consensus, blockHash hash.Hash, heigh } func (td *testData) addCPPreVote(cons *consensus, blockHash hash.Hash, height uint32, round int16, - cpRound int16, cpVal vote.CPValue, just vote.Just, valID int, + cpVal vote.CPValue, just vote.Just, valID int, ) { - v := vote.NewCPPreVote(blockHash, height, round, cpRound, cpVal, just, td.valKeys[valID].Address()) + v := vote.NewCPPreVote(blockHash, height, round, 0, cpVal, just, td.valKeys[valID].Address()) td.addVote(cons, v, valID) } func (td *testData) addCPMainVote(cons *consensus, blockHash hash.Hash, height uint32, round int16, - cpRound int16, cpVal vote.CPValue, just vote.Just, valID int, + cpVal vote.CPValue, just vote.Just, valID int, ) { - v := vote.NewCPMainVote(blockHash, height, round, cpRound, cpVal, just, td.valKeys[valID].Address()) + v := vote.NewCPMainVote(blockHash, height, round, 0, cpVal, just, td.valKeys[valID].Address()) td.addVote(cons, v, valID) } func (td *testData) addCPDecidedVote(cons *consensus, blockHash hash.Hash, height uint32, round int16, - cpRound int16, cpVal vote.CPValue, just vote.Just, valID int, + cpVal vote.CPValue, just vote.Just, valID int, ) { - v := vote.NewCPDecidedVote(blockHash, height, round, cpRound, cpVal, just, td.valKeys[valID].Address()) + v := vote.NewCPDecidedVote(blockHash, height, round, 0, cpVal, just, td.valKeys[valID].Address()) td.addVote(cons, v, valID) } @@ -337,23 +337,23 @@ func (*testData) enterNextRound(cons *consensus) { cons.lk.Unlock() } -func (td *testData) commitBlockForAllStates(t *testing.T) (*block.Block, *certificate.Certificate) { +func (td *testData) commitBlockForAllStates(t *testing.T) (*block.Block, *certificate.BlockCertificate) { t.Helper() height := td.consX.bcState.LastBlockHeight() var err error - p := td.makeProposal(t, height+1, 0) + prop := td.makeProposal(t, height+1, 0) - sb := certificate.BlockCertificateSignBytes(p.Block().Hash(), height+1, 0) + cert := certificate.NewBlockCertificate(height+1, 0, false) + sb := cert.SignBytes(prop.Block().Hash()) sig1 := td.consX.valKey.Sign(sb) sig2 := td.consY.valKey.Sign(sb) sig3 := td.consB.valKey.Sign(sb) sig4 := td.consP.valKey.Sign(sb) sig := bls.SignatureAggregate(sig1, sig2, sig3, sig4) - cert := certificate.NewCertificate(height+1, 0, - []int32{tIndexX, tIndexY, tIndexB, tIndexP}, []int32{}, sig) - blk := p.Block() + cert.SetSignature([]int32{tIndexX, tIndexY, tIndexB, tIndexP}, []int32{}, sig) + blk := prop.Block() err = td.consX.bcState.CommitBlock(blk, cert) assert.NoError(t, err) @@ -549,43 +549,48 @@ func TestPickRandomVote(t *testing.T) { td.enterNewHeight(td.consP) assert.Nil(t, td.consP.PickRandomVote(0)) - cpRound := int16(1) + cpRound := int16(0) // === make valid certificate - sbPreVote := certificate.BlockCertificateSignBytes(hash.UndefHash, 1, 0) - sbPreVote = append(sbPreVote, util.StringToBytes(vote.VoteTypeCPPreVote.String())...) - sbPreVote = append(sbPreVote, util.Int16ToSlice(cpRound)...) - sbPreVote = append(sbPreVote, byte(vote.CPValueOne)) + preVoteCommitters := []int32{} + preVoteSigs := []*bls.Signature{} + for i, val := range td.consP.validators { + preVoteJust := &vote.JustInitYes{} + preVote := vote.NewCPPreVote(hash.UndefHash, 1, 0, cpRound, vote.CPValueYes, preVoteJust, val.Address()) + sbPreVote := preVote.SignBytes() - sbMainVote := certificate.BlockCertificateSignBytes(hash.UndefHash, 1, 0) - sbMainVote = append(sbMainVote, util.StringToBytes(vote.VoteTypeCPMainVote.String())...) - sbMainVote = append(sbMainVote, util.Int16ToSlice(cpRound)...) - sbMainVote = append(sbMainVote, byte(vote.CPValueOne)) + preVoteCommitters = append(preVoteCommitters, val.Number()) + preVoteSigs = append(preVoteSigs, td.valKeys[i].Sign(sbPreVote)) + } + preVoteAggSig := bls.SignatureAggregate(preVoteSigs...) + certPreVote := certificate.NewVoteCertificate(1, 0) + certPreVote.SetSignature(preVoteCommitters, []int32{}, preVoteAggSig) - committers := []int32{} - preVoteSigs := []*bls.Signature{} + mainVoteCommitters := []int32{} mainVoteSigs := []*bls.Signature{} for i, val := range td.consP.validators { - committers = append(committers, val.Number()) - preVoteSigs = append(preVoteSigs, td.valKeys[i].Sign(sbPreVote)) + mainVoteJust := &vote.JustMainVoteNoConflict{ + QCert: certPreVote, + } + mainVote := vote.NewCPMainVote(hash.UndefHash, 1, 0, cpRound, vote.CPValueYes, mainVoteJust, val.Address()) + sbMainVote := mainVote.SignBytes() + + mainVoteCommitters = append(mainVoteCommitters, val.Number()) mainVoteSigs = append(mainVoteSigs, td.valKeys[i].Sign(sbMainVote)) } - - preVoteAggSig := bls.SignatureAggregate(preVoteSigs...) mainVoteAggSig := bls.SignatureAggregate(mainVoteSigs...) - - certPreVote := certificate.NewCertificate(1, 0, committers, []int32{}, preVoteAggSig) - certMainVote := certificate.NewCertificate(1, 0, committers, []int32{}, mainVoteAggSig) + certMainVote := certificate.NewVoteCertificate(1, 0) + certMainVote.SetSignature(mainVoteCommitters, []int32{}, mainVoteAggSig) // ==== // round 0 td.addPrepareVote(td.consP, td.RandHash(), 1, 0, tIndexX) td.addPrepareVote(td.consP, td.RandHash(), 1, 0, tIndexY) - td.addCPPreVote(td.consP, hash.UndefHash, 1, 0, cpRound+1, vote.CPValueOne, - &vote.JustPreVoteHard{QCert: certPreVote}, tIndexY) - td.addCPMainVote(td.consP, hash.UndefHash, 1, 0, cpRound, vote.CPValueOne, + td.addCPPreVote(td.consP, hash.UndefHash, 1, 0, vote.CPValueYes, + &vote.JustInitYes{}, tIndexY) + td.addCPMainVote(td.consP, hash.UndefHash, 1, 0, vote.CPValueYes, &vote.JustMainVoteNoConflict{QCert: certPreVote}, tIndexY) - td.addCPDecidedVote(td.consP, hash.UndefHash, 1, 0, cpRound, vote.CPValueOne, + td.addCPDecidedVote(td.consP, hash.UndefHash, 1, 0, vote.CPValueYes, &vote.JustDecided{QCert: certMainVote}, tIndexY) assert.NotNil(t, td.consP.PickRandomVote(0)) @@ -881,7 +886,7 @@ func TestByzantine(t *testing.T) { } func checkConsensus(td *testData, height uint32, byzVotes []*vote.Vote) ( - *certificate.Certificate, error, + *certificate.BlockCertificate, error, ) { instances := []*consensus{td.consX, td.consY, td.consB, td.consP} diff --git a/consensus/cp.go b/consensus/cp.go index 8c94f3eff..7292b9b86 100644 --- a/consensus/cp.go +++ b/consensus/cp.go @@ -4,10 +4,8 @@ import ( "fmt" "github.com/pactus-project/pactus/crypto/hash" - "github.com/pactus-project/pactus/types/certificate" "github.com/pactus-project/pactus/types/proposal" "github.com/pactus-project/pactus/types/vote" - "github.com/pactus-project/pactus/util" ) type changeProposer struct { @@ -39,7 +37,7 @@ func (*changeProposer) checkCPValue(vte *vote.Vote, allowedValues ...vote.CPValu } func (cp *changeProposer) checkJustInitZero(just vote.Just, blockHash hash.Hash) error { - j, ok := just.(*vote.JustInitZero) + j, ok := just.(*vote.JustInitNo) if !ok { return invalidJustificationError{ JustType: just.Type(), @@ -47,12 +45,7 @@ func (cp *changeProposer) checkJustInitZero(just vote.Just, blockHash hash.Hash) } } - sb := certificate.BlockCertificateSignBytes(blockHash, - j.QCert.Height(), - j.QCert.Round()) - sb = append(sb, util.StringToBytes(vote.VoteTypePrepare.String())...) - - err := j.QCert.Validate(cp.height, cp.validators, sb) + err := j.QCert.ValidatePrepare(cp.validators, blockHash) if err != nil { return invalidJustificationError{ JustType: j.Type(), @@ -64,7 +57,7 @@ func (cp *changeProposer) checkJustInitZero(just vote.Just, blockHash hash.Hash) } func (*changeProposer) checkJustInitOne(just vote.Just) error { - _, ok := just.(*vote.JustInitOne) + _, ok := just.(*vote.JustInitYes) if !ok { return invalidJustificationError{ JustType: just.Type(), @@ -86,14 +79,8 @@ func (cp *changeProposer) checkJustPreVoteHard(just vote.Just, } } - sb := certificate.BlockCertificateSignBytes(blockHash, - j.QCert.Height(), - j.QCert.Round()) - sb = append(sb, util.StringToBytes(vote.VoteTypeCPPreVote.String())...) - sb = append(sb, util.Int16ToSlice(cpRound-1)...) - sb = append(sb, byte(cpValue)) - - err := j.QCert.Validate(cp.height, cp.validators, sb) + err := j.QCert.ValidateCPPreVote(cp.validators, + blockHash, cpRound-1, byte(cpValue)) if err != nil { return invalidJustificationError{ JustType: just.Type(), @@ -115,14 +102,8 @@ func (cp *changeProposer) checkJustPreVoteSoft(just vote.Just, } } - sb := certificate.BlockCertificateSignBytes(blockHash, - j.QCert.Height(), - j.QCert.Round()) - sb = append(sb, util.StringToBytes(vote.VoteTypeCPMainVote.String())...) - sb = append(sb, util.Int16ToSlice(cpRound-1)...) - sb = append(sb, byte(vote.CPValueAbstain)) - - err := j.QCert.Validate(cp.height, cp.validators, sb) + err := j.QCert.ValidateCPMainVote(cp.validators, + blockHash, cpRound-1, byte(vote.CPValueAbstain)) if err != nil { return invalidJustificationError{ JustType: just.Type(), @@ -144,14 +125,8 @@ func (cp *changeProposer) checkJustMainVoteNoConflict(just vote.Just, } } - sb := certificate.BlockCertificateSignBytes(blockHash, - j.QCert.Height(), - j.QCert.Round()) - sb = append(sb, util.StringToBytes(vote.VoteTypeCPPreVote.String())...) - sb = append(sb, util.Int16ToSlice(cpRound)...) - sb = append(sb, byte(cpValue)) - - err := j.QCert.Validate(cp.height, cp.validators, sb) + err := j.QCert.ValidateCPPreVote(cp.validators, + blockHash, cpRound, byte(cpValue)) if err != nil { return invalidJustificationError{ JustType: j.Type(), @@ -175,12 +150,12 @@ func (cp *changeProposer) checkJustMainVoteConflict(just vote.Just, } if cpRound == 0 { - err := cp.checkJustInitZero(j.Just0, blockHash) + err := cp.checkJustInitZero(j.JustNo, blockHash) if err != nil { return err } - err = cp.checkJustInitOne(j.Just1) + err = cp.checkJustInitOne(j.JustYes) if err != nil { return err } @@ -189,25 +164,25 @@ func (cp *changeProposer) checkJustMainVoteConflict(just vote.Just, } // Just0 can be for Zero or Abstain values. - switch j.Just0.Type() { + switch j.JustNo.Type() { case vote.JustTypePreVoteSoft: - err := cp.checkJustPreVoteSoft(j.Just0, blockHash, cpRound) + err := cp.checkJustPreVoteSoft(j.JustNo, blockHash, cpRound) if err != nil { return err } case vote.JustTypePreVoteHard: - err := cp.checkJustPreVoteHard(j.Just0, blockHash, cpRound, vote.CPValueZero) + err := cp.checkJustPreVoteHard(j.JustNo, blockHash, cpRound, vote.CPValueNo) if err != nil { return err } default: return invalidJustificationError{ JustType: just.Type(), - Reason: fmt.Sprintf("unexpected justification: %s", j.Just0.Type()), + Reason: fmt.Sprintf("unexpected justification: %s", j.JustNo.Type()), } } - err := cp.checkJustPreVoteHard(j.Just1, hash.UndefHash, cpRound, vote.CPValueOne) + err := cp.checkJustPreVoteHard(j.JustYes, hash.UndefHash, cpRound, vote.CPValueYes) if err != nil { return err } @@ -220,16 +195,16 @@ func (cp *changeProposer) checkJustPreVote(v *vote.Vote) error { just := v.CPJust() if v.CPRound() == 0 { switch just.Type() { - case vote.JustTypeInitZero: - err := cp.checkCPValue(v, vote.CPValueZero) + case vote.JustTypeInitNo: + err := cp.checkCPValue(v, vote.CPValueNo) if err != nil { return err } return cp.checkJustInitZero(just, v.BlockHash()) - case vote.JustTypeInitOne: - err := cp.checkCPValue(v, vote.CPValueOne) + case vote.JustTypeInitYes: + err := cp.checkCPValue(v, vote.CPValueYes) if err != nil { return err } @@ -244,7 +219,7 @@ func (cp *changeProposer) checkJustPreVote(v *vote.Vote) error { } else { switch just.Type() { case vote.JustTypePreVoteSoft: - err := cp.checkCPValue(v, vote.CPValueZero, vote.CPValueOne) + err := cp.checkCPValue(v, vote.CPValueNo, vote.CPValueYes) if err != nil { return err } @@ -252,7 +227,7 @@ func (cp *changeProposer) checkJustPreVote(v *vote.Vote) error { return cp.checkJustPreVoteSoft(just, v.BlockHash(), v.CPRound()) case vote.JustTypePreVoteHard: - err := cp.checkCPValue(v, vote.CPValueZero, vote.CPValueOne) + err := cp.checkCPValue(v, vote.CPValueNo, vote.CPValueYes) if err != nil { return err } @@ -273,7 +248,7 @@ func (cp *changeProposer) checkJustMainVote(v *vote.Vote) error { just := v.CPJust() switch just.Type() { case vote.JustTypeMainVoteNoConflict: - err := cp.checkCPValue(v, vote.CPValueZero, vote.CPValueOne) + err := cp.checkCPValue(v, vote.CPValueNo, vote.CPValueYes) if err != nil { return err } @@ -297,7 +272,7 @@ func (cp *changeProposer) checkJustMainVote(v *vote.Vote) error { } func (cp *changeProposer) checkJustDecide(v *vote.Vote) error { - err := cp.checkCPValue(v, vote.CPValueZero, vote.CPValueOne) + err := cp.checkCPValue(v, vote.CPValueNo, vote.CPValueYes) if err != nil { return err } @@ -309,14 +284,8 @@ func (cp *changeProposer) checkJustDecide(v *vote.Vote) error { } } - sb := certificate.BlockCertificateSignBytes(v.BlockHash(), - j.QCert.Height(), - j.QCert.Round()) - sb = append(sb, util.StringToBytes(vote.VoteTypeCPMainVote.String())...) - sb = append(sb, util.Int16ToSlice(v.CPRound())...) - sb = append(sb, byte(v.CPValue())) - - err = j.QCert.Validate(cp.height, cp.validators, sb) + err = j.QCert.ValidateCPMainVote(cp.validators, + v.BlockHash(), v.CPRound(), byte(v.CPValue())) if err != nil { return invalidJustificationError{ JustType: j.Type(), @@ -343,19 +312,19 @@ func (cp *changeProposer) checkJust(v *vote.Vote) error { func (cp *changeProposer) strongTermination() { cpDecided := cp.log.CPDecidedVoteVoteSet(cp.round) - if cpDecided.HasAnyVoteFor(cp.cpRound, vote.CPValueZero) { - cp.cpDecide(vote.CPValueZero) - } else if cpDecided.HasAnyVoteFor(cp.cpRound, vote.CPValueOne) { - cp.cpDecide(vote.CPValueOne) + if cpDecided.HasAnyVoteFor(cp.cpRound, vote.CPValueNo) { + cp.cpDecide(vote.CPValueNo) + } else if cpDecided.HasAnyVoteFor(cp.cpRound, vote.CPValueYes) { + cp.cpDecide(vote.CPValueYes) } } func (cp *changeProposer) cpDecide(cpValue vote.CPValue) { - if cpValue == vote.CPValueOne { + if cpValue == vote.CPValueYes { cp.round++ cp.cpDecided = 1 cp.enterNewState(cp.proposeState) - } else if cpValue == vote.CPValueZero { + } else if cpValue == vote.CPValueNo { roundProposal := cp.log.RoundProposal(cp.round) if roundProposal == nil { cp.queryProposal() diff --git a/consensus/cp_decide.go b/consensus/cp_decide.go index 6a6cad9bf..ddcd4fa5f 100644 --- a/consensus/cp_decide.go +++ b/consensus/cp_decide.go @@ -16,28 +16,28 @@ func (s *cpDecideState) enter() { func (s *cpDecideState) decide() { cpMainVotes := s.log.CPMainVoteVoteSet(s.round) if cpMainVotes.HasTwoThirdOfTotalPower(s.cpRound) { - if cpMainVotes.HasQuorumVotesFor(s.cpRound, vote.CPValueOne) { + if cpMainVotes.HasQuorumVotesFor(s.cpRound, vote.CPValueYes) { // decided for yes, and proceeds to the next round s.logger.Info("binary agreement decided", "value", 1, "round", s.cpRound) - votes := cpMainVotes.BinaryVotes(s.cpRound, vote.CPValueOne) - cert := s.makeCertificate(votes) + votes := cpMainVotes.BinaryVotes(s.cpRound, vote.CPValueYes) + cert := s.makeVoteCertificate(votes) just := &vote.JustDecided{ QCert: cert, } - s.signAddCPDecidedVote(hash.UndefHash, s.cpRound, vote.CPValueOne, just) - s.cpDecide(vote.CPValueOne) - } else if cpMainVotes.HasQuorumVotesFor(s.cpRound, vote.CPValueZero) { + s.signAddCPDecidedVote(hash.UndefHash, s.cpRound, vote.CPValueYes, just) + s.cpDecide(vote.CPValueYes) + } else if cpMainVotes.HasQuorumVotesFor(s.cpRound, vote.CPValueNo) { // decided for no and proceeds to the next round s.logger.Info("binary agreement decided", "value", 0, "round", s.cpRound) - votes := cpMainVotes.BinaryVotes(s.cpRound, vote.CPValueZero) - cert := s.makeCertificate(votes) + votes := cpMainVotes.BinaryVotes(s.cpRound, vote.CPValueNo) + cert := s.makeVoteCertificate(votes) just := &vote.JustDecided{ QCert: cert, } - s.signAddCPDecidedVote(*s.cpWeakValidity, s.cpRound, vote.CPValueZero, just) - s.cpDecide(vote.CPValueZero) + s.signAddCPDecidedVote(*s.cpWeakValidity, s.cpRound, vote.CPValueNo, just) + s.cpDecide(vote.CPValueNo) } else { // conflicting votes s.logger.Debug("conflicting main votes", "round", s.cpRound) diff --git a/consensus/cp_mainvote.go b/consensus/cp_mainvote.go index 01139380d..cd03d9e0a 100644 --- a/consensus/cp_mainvote.go +++ b/consensus/cp_mainvote.go @@ -19,35 +19,35 @@ func (s *cpMainVoteState) decide() { cpPreVotes := s.log.CPPreVoteVoteSet(s.round) if cpPreVotes.HasTwoThirdOfTotalPower(s.cpRound) { - if cpPreVotes.HasQuorumVotesFor(s.cpRound, vote.CPValueOne) { + if cpPreVotes.HasQuorumVotesFor(s.cpRound, vote.CPValueYes) { s.logger.Debug("cp: quorum for pre-votes", "v", "1") - votes := cpPreVotes.BinaryVotes(s.cpRound, vote.CPValueOne) - cert := s.makeCertificate(votes) + votes := cpPreVotes.BinaryVotes(s.cpRound, vote.CPValueYes) + cert := s.makeVoteCertificate(votes) just := &vote.JustMainVoteNoConflict{ QCert: cert, } - s.signAddCPMainVote(hash.UndefHash, s.cpRound, vote.CPValueOne, just) + s.signAddCPMainVote(hash.UndefHash, s.cpRound, vote.CPValueYes, just) s.enterNewState(s.cpDecideState) - } else if cpPreVotes.HasQuorumVotesFor(s.cpRound, vote.CPValueZero) { + } else if cpPreVotes.HasQuorumVotesFor(s.cpRound, vote.CPValueNo) { s.logger.Debug("cp: quorum for pre-votes", "v", "0") - votes := cpPreVotes.BinaryVotes(s.cpRound, vote.CPValueZero) - cert := s.makeCertificate(votes) + votes := cpPreVotes.BinaryVotes(s.cpRound, vote.CPValueNo) + cert := s.makeVoteCertificate(votes) just := &vote.JustMainVoteNoConflict{ QCert: cert, } - s.signAddCPMainVote(*s.cpWeakValidity, s.cpRound, vote.CPValueZero, just) + s.signAddCPMainVote(*s.cpWeakValidity, s.cpRound, vote.CPValueNo, just) s.enterNewState(s.cpDecideState) } else { s.logger.Debug("cp: no-quorum for pre-votes", "v", "abstain") - vote0 := cpPreVotes.GetRandomVote(s.cpRound, vote.CPValueZero) - vote1 := cpPreVotes.GetRandomVote(s.cpRound, vote.CPValueOne) + vote0 := cpPreVotes.GetRandomVote(s.cpRound, vote.CPValueNo) + vote1 := cpPreVotes.GetRandomVote(s.cpRound, vote.CPValueYes) just := &vote.JustMainVoteConflict{ - Just0: vote0.CPJust(), - Just1: vote1.CPJust(), + JustNo: vote0.CPJust(), + JustYes: vote1.CPJust(), } s.signAddCPMainVote(*s.cpWeakValidity, s.cpRound, vote.CPValueAbstain, just) @@ -59,7 +59,7 @@ func (s *cpMainVoteState) decide() { func (s *cpMainVoteState) checkForWeakValidity() { if s.cpWeakValidity == nil { preVotes := s.log.CPPreVoteVoteSet(s.round) - preVotesZero := preVotes.BinaryVotes(s.cpRound, vote.CPValueZero) + preVotesZero := preVotes.BinaryVotes(s.cpRound, vote.CPValueNo) for _, v := range preVotesZero { bh := v.BlockHash() diff --git a/consensus/cp_prevote.go b/consensus/cp_prevote.go index eb653f928..8c0b002ce 100644 --- a/consensus/cp_prevote.go +++ b/consensus/cp_prevote.go @@ -24,46 +24,46 @@ func (s *cpPreVoteState) decide() { preparesQH := prepares.QuorumHash() if preparesQH != nil { s.cpWeakValidity = preparesQH - cert := s.makeCertificate(prepares.BlockVotes(*preparesQH)) - just := &vote.JustInitZero{ + cert := s.makeVoteCertificate(prepares.BlockVotes(*preparesQH)) + just := &vote.JustInitNo{ QCert: cert, } s.signAddCPPreVote(*s.cpWeakValidity, s.cpRound, 0, just) } else { - just := &vote.JustInitOne{} + just := &vote.JustInitYes{} s.signAddCPPreVote(hash.UndefHash, s.cpRound, 1, just) } s.scheduleTimeout(queryVoteInitialTimeout, s.height, s.round, tickerTargetQueryVotes) } else { cpMainVotes := s.log.CPMainVoteVoteSet(s.round) switch { - case cpMainVotes.HasAnyVoteFor(s.cpRound-1, vote.CPValueOne): + case cpMainVotes.HasAnyVoteFor(s.cpRound-1, vote.CPValueYes): s.logger.Debug("cp: one main-vote for one", "b", "1") - vote1 := cpMainVotes.GetRandomVote(s.cpRound-1, vote.CPValueOne) + vote1 := cpMainVotes.GetRandomVote(s.cpRound-1, vote.CPValueYes) just1 := &vote.JustPreVoteHard{ QCert: vote1.CPJust().(*vote.JustMainVoteNoConflict).QCert, } - s.signAddCPPreVote(hash.UndefHash, s.cpRound, vote.CPValueOne, just1) + s.signAddCPPreVote(hash.UndefHash, s.cpRound, vote.CPValueYes, just1) - case cpMainVotes.HasAnyVoteFor(s.cpRound-1, vote.CPValueZero): + case cpMainVotes.HasAnyVoteFor(s.cpRound-1, vote.CPValueNo): s.logger.Debug("cp: one main-vote for zero", "b", "0") - vote0 := cpMainVotes.GetRandomVote(s.cpRound-1, vote.CPValueZero) + vote0 := cpMainVotes.GetRandomVote(s.cpRound-1, vote.CPValueNo) just0 := &vote.JustPreVoteHard{ QCert: vote0.CPJust().(*vote.JustMainVoteNoConflict).QCert, } - s.signAddCPPreVote(*s.cpWeakValidity, s.cpRound, vote.CPValueZero, just0) + s.signAddCPPreVote(*s.cpWeakValidity, s.cpRound, vote.CPValueNo, just0) case cpMainVotes.HasAllVotesFor(s.cpRound-1, vote.CPValueAbstain): s.logger.Debug("cp: all main-votes are abstain", "b", "0 (biased)") votes := cpMainVotes.BinaryVotes(s.cpRound-1, vote.CPValueAbstain) - cert := s.makeCertificate(votes) + cert := s.makeVoteCertificate(votes) just := &vote.JustPreVoteSoft{ QCert: cert, } - s.signAddCPPreVote(*s.cpWeakValidity, s.cpRound, vote.CPValueZero, just) + s.signAddCPPreVote(*s.cpWeakValidity, s.cpRound, vote.CPValueNo, just) default: s.logger.Panic("protocol violated. We have combination of votes for one and zero") diff --git a/consensus/cp_test.go b/consensus/cp_test.go index 8ea80af08..bebd17998 100644 --- a/consensus/cp_test.go +++ b/consensus/cp_test.go @@ -45,12 +45,12 @@ func TestChangeProposerAgreement1(t *testing.T) { td.changeProposerTimeout(td.consP) preVote0 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, hash.UndefHash) - td.addCPPreVote(td.consP, hash.UndefHash, h, r, 0, vote.CPValueOne, preVote0.CPJust(), tIndexX) - td.addCPPreVote(td.consP, hash.UndefHash, h, r, 0, vote.CPValueOne, preVote0.CPJust(), tIndexY) + td.addCPPreVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, preVote0.CPJust(), tIndexX) + td.addCPPreVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, preVote0.CPJust(), tIndexY) mainVote0 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPMainVote, hash.UndefHash) - td.addCPMainVote(td.consP, hash.UndefHash, h, r, 0, vote.CPValueOne, mainVote0.CPJust(), tIndexX) - td.addCPMainVote(td.consP, hash.UndefHash, h, r, 0, vote.CPValueOne, mainVote0.CPJust(), tIndexY) + td.addCPMainVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, mainVote0.CPJust(), tIndexX) + td.addCPMainVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, mainVote0.CPJust(), tIndexY) td.shouldPublishVote(t, td.consP, vote.VoteTypeCPDecided, hash.UndefHash) td.checkHeightRound(t, td.consP, h, r+1) @@ -76,12 +76,12 @@ func TestChangeProposerAgreement0(t *testing.T) { td.changeProposerTimeout(td.consP) preVote0 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, p.Block().Hash()) - td.addCPPreVote(td.consP, p.Block().Hash(), h, r, 0, vote.CPValueZero, preVote0.CPJust(), tIndexX) - td.addCPPreVote(td.consP, p.Block().Hash(), h, r, 0, vote.CPValueZero, preVote0.CPJust(), tIndexY) + td.addCPPreVote(td.consP, p.Block().Hash(), h, r, vote.CPValueNo, preVote0.CPJust(), tIndexX) + td.addCPPreVote(td.consP, p.Block().Hash(), h, r, vote.CPValueNo, preVote0.CPJust(), tIndexY) mainVote0 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPMainVote, p.Block().Hash()) - td.addCPMainVote(td.consP, p.Block().Hash(), h, r, 0, vote.CPValueZero, mainVote0.CPJust(), tIndexX) - td.addCPMainVote(td.consP, p.Block().Hash(), h, r, 0, vote.CPValueZero, mainVote0.CPJust(), tIndexY) + td.addCPMainVote(td.consP, p.Block().Hash(), h, r, vote.CPValueNo, mainVote0.CPJust(), tIndexX) + td.addCPMainVote(td.consP, p.Block().Hash(), h, r, vote.CPValueNo, mainVote0.CPJust(), tIndexY) td.shouldPublishVote(t, td.consP, vote.VoteTypeCPDecided, p.Block().Hash()) td.shouldPublishQueryProposal(t, td.consP, h) @@ -115,19 +115,19 @@ func TestCrashOnTestnet(t *testing.T) { votes[v2.Signer()] = v2 votes[v3.Signer()] = v3 - qCert := td.consP.makeCertificate(votes) - just0 := &vote.JustInitZero{QCert: qCert} - td.addCPPreVote(td.consP, blockHash, h, r, 0, vote.CPValueZero, just0, tIndexX) - td.addCPPreVote(td.consP, blockHash, h, r, 0, vote.CPValueZero, just0, tIndexY) - td.addCPPreVote(td.consP, blockHash, h, r, 0, vote.CPValueZero, just0, tIndexB) + qCert := td.consP.makeVoteCertificate(votes) + just0 := &vote.JustInitNo{QCert: qCert} + td.addCPPreVote(td.consP, blockHash, h, r, vote.CPValueNo, just0, tIndexX) + td.addCPPreVote(td.consP, blockHash, h, r, vote.CPValueNo, just0, tIndexY) + td.addCPPreVote(td.consP, blockHash, h, r, vote.CPValueNo, just0, tIndexB) td.newHeightTimeout(td.consP) td.changeProposerTimeout(td.consP) preVote := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, hash.UndefHash) mainVote := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPMainVote, blockHash) - assert.Equal(t, vote.CPValueOne, preVote.CPValue()) - assert.Equal(t, vote.CPValueZero, mainVote.CPValue()) + assert.Equal(t, vote.CPValueYes, preVote.CPValue()) + assert.Equal(t, vote.CPValueNo, mainVote.CPValue()) } func TestInvalidJustInitOne(t *testing.T) { @@ -136,20 +136,20 @@ func TestInvalidJustInitOne(t *testing.T) { td.enterNewHeight(td.consX) h := uint32(1) r := int16(0) - just := &vote.JustInitOne{} + just := &vote.JustInitYes{} - t.Run("invalid value: zero", func(t *testing.T) { - v := vote.NewCPPreVote(hash.UndefHash, h, r, 0, vote.CPValueZero, just, td.consB.valKey.Address()) + t.Run("invalid value: no", func(t *testing.T) { + v := vote.NewCPPreVote(hash.UndefHash, h, r, 0, vote.CPValueNo, just, td.consB.valKey.Address()) err := td.consX.changeProposer.checkJust(v) assert.ErrorIs(t, err, invalidJustificationError{ JustType: just.Type(), - Reason: "invalid value: zero", + Reason: "invalid value: no", }) }) t.Run("invalid block hash", func(t *testing.T) { - v := vote.NewCPPreVote(hash.UndefHash, h, r, 1, vote.CPValueOne, just, td.consB.valKey.Address()) + v := vote.NewCPPreVote(hash.UndefHash, h, r, 1, vote.CPValueYes, just, td.consB.valKey.Address()) err := td.consX.changeProposer.checkJust(v) assert.ErrorIs(t, err, invalidJustificationError{ @@ -160,7 +160,7 @@ func TestInvalidJustInitOne(t *testing.T) { t.Run("with main-vote justification", func(t *testing.T) { invJust := &vote.JustMainVoteNoConflict{} - v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueOne, invJust, td.consB.valKey.Address()) + v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueYes, invJust, td.consB.valKey.Address()) err := td.consX.changeProposer.checkJust(v) assert.ErrorIs(t, err, invalidJustificationError{ @@ -176,22 +176,22 @@ func TestInvalidJustInitZero(t *testing.T) { td.enterNewHeight(td.consX) h := uint32(1) r := int16(0) - just := &vote.JustInitZero{ - QCert: td.GenerateTestCertificate(h), + just := &vote.JustInitNo{ + QCert: td.GenerateTestPrepareCertificate(h), } - t.Run("invalid value: one", func(t *testing.T) { - v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueOne, just, td.consB.valKey.Address()) + t.Run("invalid value: yes", func(t *testing.T) { + v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueYes, just, td.consB.valKey.Address()) err := td.consX.changeProposer.checkJust(v) assert.ErrorIs(t, err, invalidJustificationError{ JustType: just.Type(), - Reason: "invalid value: one", + Reason: "invalid value: yes", }) }) t.Run("cp-round should be zero", func(t *testing.T) { - v := vote.NewCPPreVote(td.RandHash(), h, r, 1, vote.CPValueZero, just, td.consB.valKey.Address()) + v := vote.NewCPPreVote(td.RandHash(), h, r, 1, vote.CPValueNo, just, td.consB.valKey.Address()) err := td.consX.changeProposer.checkJust(v) assert.ErrorIs(t, err, invalidJustificationError{ @@ -201,7 +201,7 @@ func TestInvalidJustInitZero(t *testing.T) { }) t.Run("invalid certificate", func(t *testing.T) { - v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueZero, just, td.consB.valKey.Address()) + v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueNo, just, td.consB.valKey.Address()) err := td.consX.changeProposer.checkJust(v) assert.ErrorIs(t, err, invalidJustificationError{ @@ -218,7 +218,7 @@ func TestInvalidJustPreVoteHard(t *testing.T) { h := uint32(1) r := int16(0) just := &vote.JustPreVoteHard{ - QCert: td.GenerateTestCertificate(h), + QCert: td.GenerateTestPrepareCertificate(h), } t.Run("invalid value: abstain", func(t *testing.T) { @@ -232,7 +232,7 @@ func TestInvalidJustPreVoteHard(t *testing.T) { }) t.Run("cp-round should not be zero", func(t *testing.T) { - v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueZero, just, td.consB.valKey.Address()) + v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueNo, just, td.consB.valKey.Address()) err := td.consX.changeProposer.checkJust(v) assert.ErrorIs(t, err, invalidJustificationError{ @@ -242,7 +242,7 @@ func TestInvalidJustPreVoteHard(t *testing.T) { }) t.Run("invalid certificate", func(t *testing.T) { - v := vote.NewCPPreVote(td.RandHash(), h, r, 1, vote.CPValueZero, just, td.consB.valKey.Address()) + v := vote.NewCPPreVote(td.RandHash(), h, r, 1, vote.CPValueNo, just, td.consB.valKey.Address()) err := td.consX.changeProposer.checkJust(v) assert.ErrorIs(t, err, invalidJustificationError{ @@ -259,7 +259,7 @@ func TestInvalidJustPreVoteSoft(t *testing.T) { h := uint32(1) r := int16(0) just := &vote.JustPreVoteSoft{ - QCert: td.GenerateTestCertificate(h), + QCert: td.GenerateTestPrepareCertificate(h), } t.Run("invalid value: abstain", func(t *testing.T) { @@ -273,7 +273,7 @@ func TestInvalidJustPreVoteSoft(t *testing.T) { }) t.Run("cp-round should not be zero", func(t *testing.T) { - v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueZero, just, td.consB.valKey.Address()) + v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueNo, just, td.consB.valKey.Address()) err := td.consX.changeProposer.checkJust(v) assert.ErrorIs(t, err, invalidJustificationError{ @@ -283,7 +283,7 @@ func TestInvalidJustPreVoteSoft(t *testing.T) { }) t.Run("invalid certificate", func(t *testing.T) { - v := vote.NewCPPreVote(td.RandHash(), h, r, 1, vote.CPValueZero, just, td.consB.valKey.Address()) + v := vote.NewCPPreVote(td.RandHash(), h, r, 1, vote.CPValueNo, just, td.consB.valKey.Address()) err := td.consX.changeProposer.checkJust(v) assert.ErrorIs(t, err, invalidJustificationError{ @@ -300,7 +300,7 @@ func TestInvalidJustMainVoteNoConflict(t *testing.T) { h := uint32(1) r := int16(0) just := &vote.JustMainVoteNoConflict{ - QCert: td.GenerateTestCertificate(h), + QCert: td.GenerateTestPrepareCertificate(h), } t.Run("invalid value: abstain", func(t *testing.T) { @@ -314,7 +314,7 @@ func TestInvalidJustMainVoteNoConflict(t *testing.T) { }) t.Run("invalid certificate", func(t *testing.T) { - v := vote.NewCPMainVote(td.RandHash(), h, r, 1, vote.CPValueZero, just, td.consB.valKey.Address()) + v := vote.NewCPMainVote(td.RandHash(), h, r, 1, vote.CPValueNo, just, td.consB.valKey.Address()) err := td.consX.changeProposer.checkJust(v) assert.ErrorIs(t, err, invalidJustificationError{ @@ -331,44 +331,44 @@ func TestInvalidJustMainVoteConflict(t *testing.T) { h := uint32(1) r := int16(0) - t.Run("invalid value: zero", func(t *testing.T) { + t.Run("invalid value: no", func(t *testing.T) { just := &vote.JustMainVoteConflict{ - Just0: &vote.JustInitZero{ - QCert: td.GenerateTestCertificate(h), + JustNo: &vote.JustInitNo{ + QCert: td.GenerateTestPrepareCertificate(h), }, - Just1: &vote.JustInitOne{}, + JustYes: &vote.JustInitYes{}, } - v := vote.NewCPMainVote(td.RandHash(), h, r, 0, vote.CPValueZero, just, td.consB.valKey.Address()) + v := vote.NewCPMainVote(td.RandHash(), h, r, 0, vote.CPValueNo, just, td.consB.valKey.Address()) err := td.consX.changeProposer.checkJust(v) assert.ErrorIs(t, err, invalidJustificationError{ JustType: just.Type(), - Reason: "invalid value: zero", + Reason: "invalid value: no", }) }) - t.Run("invalid value: one", func(t *testing.T) { + t.Run("invalid value: yes", func(t *testing.T) { just := &vote.JustMainVoteConflict{ - Just0: &vote.JustInitZero{ - QCert: td.GenerateTestCertificate(h), + JustNo: &vote.JustInitNo{ + QCert: td.GenerateTestPrepareCertificate(h), }, - Just1: &vote.JustInitOne{}, + JustYes: &vote.JustInitYes{}, } - v := vote.NewCPMainVote(td.RandHash(), h, r, 0, vote.CPValueOne, just, td.consB.valKey.Address()) + v := vote.NewCPMainVote(td.RandHash(), h, r, 0, vote.CPValueYes, just, td.consB.valKey.Address()) err := td.consX.changeProposer.checkJust(v) assert.ErrorIs(t, err, invalidJustificationError{ JustType: just.Type(), - Reason: "invalid value: one", + Reason: "invalid value: yes", }) }) t.Run("invalid value: unexpected justification (just0)", func(t *testing.T) { just := &vote.JustMainVoteConflict{ - Just0: &vote.JustPreVoteSoft{ - QCert: td.GenerateTestCertificate(h), + JustNo: &vote.JustPreVoteSoft{ + QCert: td.GenerateTestPrepareCertificate(h), }, - Just1: &vote.JustInitOne{}, + JustYes: &vote.JustInitYes{}, } v := vote.NewCPMainVote(td.RandHash(), h, r, 0, vote.CPValueAbstain, just, td.consB.valKey.Address()) @@ -381,11 +381,11 @@ func TestInvalidJustMainVoteConflict(t *testing.T) { t.Run("invalid value: unexpected justification", func(t *testing.T) { just := &vote.JustMainVoteConflict{ - Just0: &vote.JustInitZero{ - QCert: td.GenerateTestCertificate(h), + JustNo: &vote.JustInitNo{ + QCert: td.GenerateTestPrepareCertificate(h), }, - Just1: &vote.JustPreVoteSoft{ - QCert: td.GenerateTestCertificate(h), + JustYes: &vote.JustPreVoteSoft{ + QCert: td.GenerateTestPrepareCertificate(h), }, } v := vote.NewCPMainVote(td.RandHash(), h, r, 1, vote.CPValueAbstain, just, td.consB.valKey.Address()) @@ -393,17 +393,17 @@ func TestInvalidJustMainVoteConflict(t *testing.T) { err := td.consX.changeProposer.checkJust(v) assert.ErrorIs(t, err, invalidJustificationError{ JustType: just.Type(), - Reason: "unexpected justification: JustInitZero", + Reason: "unexpected justification: JustInitNo", }) }) t.Run("invalid certificate", func(t *testing.T) { - just0 := &vote.JustInitZero{ - QCert: td.GenerateTestCertificate(h), + just0 := &vote.JustInitNo{ + QCert: td.GenerateTestPrepareCertificate(h), } just := &vote.JustMainVoteConflict{ - Just0: just0, - Just1: &vote.JustInitOne{}, + JustNo: just0, + JustYes: &vote.JustInitYes{}, } v := vote.NewCPMainVote(td.RandHash(), h, r, 0, vote.CPValueAbstain, just, td.consB.valKey.Address()) @@ -416,12 +416,12 @@ func TestInvalidJustMainVoteConflict(t *testing.T) { t.Run("invalid certificate", func(t *testing.T) { just0 := &vote.JustPreVoteSoft{ - QCert: td.GenerateTestCertificate(h), + QCert: td.GenerateTestPrepareCertificate(h), } just := &vote.JustMainVoteConflict{ - Just0: just0, - Just1: &vote.JustPreVoteSoft{ - QCert: td.GenerateTestCertificate(h), + JustNo: just0, + JustYes: &vote.JustPreVoteSoft{ + QCert: td.GenerateTestPrepareCertificate(h), }, } v := vote.NewCPMainVote(td.RandHash(), h, r, 1, vote.CPValueAbstain, just, td.consB.valKey.Address()) @@ -441,7 +441,7 @@ func TestInvalidJustDecided(t *testing.T) { h := uint32(1) r := int16(0) just := &vote.JustDecided{ - QCert: td.GenerateTestCertificate(h), + QCert: td.GenerateTestPrepareCertificate(h), } t.Run("invalid value: abstain", func(t *testing.T) { @@ -455,7 +455,7 @@ func TestInvalidJustDecided(t *testing.T) { }) t.Run("invalid certificate", func(t *testing.T) { - v := vote.NewCPDecidedVote(td.RandHash(), h, r, 0, vote.CPValueOne, just, td.consB.valKey.Address()) + v := vote.NewCPDecidedVote(td.RandHash(), h, r, 0, vote.CPValueYes, just, td.consB.valKey.Address()) err := td.consX.changeProposer.checkJust(v) assert.ErrorIs(t, err, invalidJustificationError{ diff --git a/consensus/log/log_test.go b/consensus/log/log_test.go index 91a7c9489..bb3ca8ff6 100644 --- a/consensus/log/log_test.go +++ b/consensus/log/log_test.go @@ -34,8 +34,8 @@ func TestAddValidVote(t *testing.T) { v1 := vote.NewPrepareVote(ts.RandHash(), h, r, valKeys[0].Address()) v2 := vote.NewPrecommitVote(ts.RandHash(), h, r, valKeys[0].Address()) - v3 := vote.NewCPPreVote(ts.RandHash(), h, r, 0, vote.CPValueOne, &vote.JustInitOne{}, valKeys[0].Address()) - v4 := vote.NewCPMainVote(ts.RandHash(), h, r, 0, vote.CPValueZero, &vote.JustInitOne{}, valKeys[0].Address()) + v3 := vote.NewCPPreVote(ts.RandHash(), h, r, 0, vote.CPValueYes, &vote.JustInitYes{}, valKeys[0].Address()) + v4 := vote.NewCPMainVote(ts.RandHash(), h, r, 0, vote.CPValueNo, &vote.JustInitYes{}, valKeys[0].Address()) for _, v := range []*vote.Vote{v1, v2, v3, v4} { ts.HelperSignVote(valKeys[0], v) diff --git a/consensus/precommit_test.go b/consensus/precommit_test.go index fe36e1474..0ad7a14b7 100644 --- a/consensus/precommit_test.go +++ b/consensus/precommit_test.go @@ -85,8 +85,8 @@ func TestGoToChangeProposerFromPrecommit(t *testing.T) { td.addPrepareVote(td.consP, blockHash, h, r, tIndexY) td.addPrepareVote(td.consP, blockHash, h, r, tIndexB) - td.addCPPreVote(td.consP, hash.UndefHash, h, r, 0, vote.CPValueOne, &vote.JustInitOne{}, tIndexX) - td.addCPPreVote(td.consP, hash.UndefHash, h, r, 0, vote.CPValueOne, &vote.JustInitOne{}, tIndexY) + td.addCPPreVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, &vote.JustInitYes{}, tIndexX) + td.addCPPreVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, &vote.JustInitYes{}, tIndexY) td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, blockHash) } diff --git a/consensus/prepare_test.go b/consensus/prepare_test.go index 03b234959..ec0c3802a 100644 --- a/consensus/prepare_test.go +++ b/consensus/prepare_test.go @@ -56,8 +56,8 @@ func TestGoToChangeProposerFromPrepare(t *testing.T) { td.enterNewHeight(td.consP) - td.addCPPreVote(td.consP, hash.UndefHash, 2, 0, 0, vote.CPValueOne, &vote.JustInitOne{}, tIndexX) - td.addCPPreVote(td.consP, hash.UndefHash, 2, 0, 0, vote.CPValueOne, &vote.JustInitOne{}, tIndexY) + td.addCPPreVote(td.consP, hash.UndefHash, 2, 0, vote.CPValueYes, &vote.JustInitYes{}, tIndexX) + td.addCPPreVote(td.consP, hash.UndefHash, 2, 0, vote.CPValueYes, &vote.JustInitYes{}, tIndexY) // should move to the change proposer phase, even if it has the proposal and // its timer has not expired, if it has received 1/3 of the change-proposer votes. diff --git a/consensus/voteset/binary_voteset.go b/consensus/voteset/binary_voteset.go index 3559ca010..107addb4b 100644 --- a/consensus/voteset/binary_voteset.go +++ b/consensus/voteset/binary_voteset.go @@ -16,8 +16,8 @@ type roundVotes struct { func newRoundVotes() *roundVotes { voteBoxes := [3]*voteBox{} - voteBoxes[vote.CPValueZero] = newVoteBox() - voteBoxes[vote.CPValueOne] = newVoteBox() + voteBoxes[vote.CPValueNo] = newVoteBox() + voteBoxes[vote.CPValueYes] = newVoteBox() voteBoxes[vote.CPValueAbstain] = newVoteBox() return &roundVotes{ diff --git a/consensus/voteset/voteset_test.go b/consensus/voteset/voteset_test.go index 0358cc0c4..6154b49a3 100644 --- a/consensus/voteset/voteset_test.go +++ b/consensus/voteset/voteset_test.go @@ -85,7 +85,7 @@ func TestAddBinaryVote(t *testing.T) { round := ts.RandRound() cpRound := ts.RandRound() cpVal := ts.RandInt(2) - just := &vote.JustInitOne{} + just := &vote.JustInitYes{} invKey := ts.RandValKey() valKey := valKeys[0] vs := NewCPPreVoteVoteSet(round, totalPower, valsMap) @@ -171,9 +171,9 @@ func TestDuplicateBinaryVote(t *testing.T) { addr := valKeys[0].Address() vs := NewCPPreVoteVoteSet(0, totalPower, valsMap) - correctVote := vote.NewCPPreVote(h1, 1, 0, 0, vote.CPValueOne, &vote.JustInitOne{}, addr) - duplicatedVote1 := vote.NewCPPreVote(h2, 1, 0, 0, vote.CPValueOne, &vote.JustInitOne{}, addr) - duplicatedVote2 := vote.NewCPPreVote(h3, 1, 0, 0, vote.CPValueOne, &vote.JustInitOne{}, addr) + correctVote := vote.NewCPPreVote(h1, 1, 0, 0, vote.CPValueYes, &vote.JustInitYes{}, addr) + duplicatedVote1 := vote.NewCPPreVote(h2, 1, 0, 0, vote.CPValueYes, &vote.JustInitYes{}, addr) + duplicatedVote2 := vote.NewCPPreVote(h3, 1, 0, 0, vote.CPValueYes, &vote.JustInitYes{}, addr) // sign the votes ts.HelperSignVote(valKeys[0], correctVote) @@ -290,9 +290,9 @@ func TestAllBinaryVotes(t *testing.T) { vs := NewCPMainVoteVoteSet(1, totalPower, valsMap) - v1 := vote.NewCPMainVote(hash.UndefHash, 1, 1, 0, vote.CPValueZero, &vote.JustInitOne{}, valKeys[0].Address()) - v2 := vote.NewCPMainVote(hash.UndefHash, 1, 1, 1, vote.CPValueOne, &vote.JustInitOne{}, valKeys[1].Address()) - v3 := vote.NewCPMainVote(hash.UndefHash, 1, 1, 2, vote.CPValueAbstain, &vote.JustInitOne{}, valKeys[2].Address()) + v1 := vote.NewCPMainVote(hash.UndefHash, 1, 1, 0, vote.CPValueNo, &vote.JustInitYes{}, valKeys[0].Address()) + v2 := vote.NewCPMainVote(hash.UndefHash, 1, 1, 1, vote.CPValueYes, &vote.JustInitYes{}, valKeys[1].Address()) + v3 := vote.NewCPMainVote(hash.UndefHash, 1, 1, 2, vote.CPValueAbstain, &vote.JustInitYes{}, valKeys[2].Address()) ts.HelperSignVote(valKeys[0], v1) ts.HelperSignVote(valKeys[1], v2) @@ -313,10 +313,10 @@ func TestAllBinaryVotes(t *testing.T) { assert.Contains(t, vs.AllVotes(), v2) assert.Contains(t, vs.AllVotes(), v3) - ranVote1 := vs.GetRandomVote(1, vote.CPValueZero) + ranVote1 := vs.GetRandomVote(1, vote.CPValueNo) assert.Nil(t, ranVote1) - ranVote2 := vs.GetRandomVote(1, vote.CPValueOne) + ranVote2 := vs.GetRandomVote(1, vote.CPValueYes) assert.Equal(t, ranVote2, v2) } @@ -331,13 +331,13 @@ func TestOneThirdPower(t *testing.T) { h := ts.RandHash() height := ts.RandHeight() round := ts.RandRound() - just := &vote.JustInitOne{} + just := &vote.JustInitYes{} vs := NewCPPreVoteVoteSet(round, totalPower, valsMap) - v1 := vote.NewCPPreVote(h, height, round, 0, vote.CPValueOne, just, valKeys[0].Address()) - v2 := vote.NewCPPreVote(h, height, round, 0, vote.CPValueOne, just, valKeys[1].Address()) - v3 := vote.NewCPPreVote(h, height, round, 0, vote.CPValueOne, just, valKeys[2].Address()) - v4 := vote.NewCPPreVote(h, height, round, 0, vote.CPValueZero, just, valKeys[3].Address()) + v1 := vote.NewCPPreVote(h, height, round, 0, vote.CPValueYes, just, valKeys[0].Address()) + v2 := vote.NewCPPreVote(h, height, round, 0, vote.CPValueYes, just, valKeys[1].Address()) + v3 := vote.NewCPPreVote(h, height, round, 0, vote.CPValueYes, just, valKeys[2].Address()) + v4 := vote.NewCPPreVote(h, height, round, 0, vote.CPValueNo, just, valKeys[3].Address()) ts.HelperSignVote(valKeys[0], v1) ts.HelperSignVote(valKeys[1], v2) @@ -347,8 +347,8 @@ func TestOneThirdPower(t *testing.T) { _, err := vs.AddVote(v1) assert.NoError(t, err) assert.False(t, vs.HasOneThirdOfTotalPower(0)) - assert.True(t, vs.HasAnyVoteFor(0, vote.CPValueOne)) - assert.False(t, vs.HasAnyVoteFor(0, vote.CPValueZero)) + assert.True(t, vs.HasAnyVoteFor(0, vote.CPValueYes)) + assert.False(t, vs.HasAnyVoteFor(0, vote.CPValueNo)) assert.False(t, vs.HasAnyVoteFor(0, vote.CPValueAbstain)) _, err = vs.AddVote(v2) @@ -359,21 +359,21 @@ func TestOneThirdPower(t *testing.T) { _, err = vs.AddVote(v3) assert.NoError(t, err) assert.True(t, vs.HasTwoThirdOfTotalPower(0)) - assert.False(t, vs.HasAnyVoteFor(0, vote.CPValueZero)) - assert.True(t, vs.HasAnyVoteFor(0, vote.CPValueOne)) - assert.False(t, vs.HasQuorumVotesFor(0, vote.CPValueZero)) - assert.True(t, vs.HasQuorumVotesFor(0, vote.CPValueOne)) - assert.True(t, vs.HasAllVotesFor(0, vote.CPValueOne)) + assert.False(t, vs.HasAnyVoteFor(0, vote.CPValueNo)) + assert.True(t, vs.HasAnyVoteFor(0, vote.CPValueYes)) + assert.False(t, vs.HasQuorumVotesFor(0, vote.CPValueNo)) + assert.True(t, vs.HasQuorumVotesFor(0, vote.CPValueYes)) + assert.True(t, vs.HasAllVotesFor(0, vote.CPValueYes)) _, err = vs.AddVote(v4) assert.NoError(t, err) - assert.True(t, vs.HasAnyVoteFor(0, vote.CPValueZero)) - assert.False(t, vs.HasQuorumVotesFor(0, vote.CPValueZero)) - assert.True(t, vs.HasQuorumVotesFor(0, vote.CPValueOne)) - assert.False(t, vs.HasAllVotesFor(0, vote.CPValueOne)) + assert.True(t, vs.HasAnyVoteFor(0, vote.CPValueNo)) + assert.False(t, vs.HasQuorumVotesFor(0, vote.CPValueNo)) + assert.True(t, vs.HasQuorumVotesFor(0, vote.CPValueYes)) + assert.False(t, vs.HasAllVotesFor(0, vote.CPValueYes)) - bv1 := vs.BinaryVotes(0, vote.CPValueOne) - bv2 := vs.BinaryVotes(0, vote.CPValueZero) + bv1 := vs.BinaryVotes(0, vote.CPValueYes) + bv2 := vs.BinaryVotes(0, vote.CPValueNo) assert.Contains(t, bv1, v1.Signer()) assert.Contains(t, bv1, v2.Signer()) @@ -388,15 +388,15 @@ func TestDecidedVoteset(t *testing.T) { h := ts.RandHash() height := ts.RandHeight() round := ts.RandRound() - just := &vote.JustInitOne{} + just := &vote.JustInitYes{} vs := NewCPDecidedVoteVoteSet(round, totalPower, valsMap) - v1 := vote.NewCPDecidedVote(h, height, round, 0, vote.CPValueOne, just, valKeys[0].Address()) + v1 := vote.NewCPDecidedVote(h, height, round, 0, vote.CPValueYes, just, valKeys[0].Address()) ts.HelperSignVote(valKeys[0], v1) _, err := vs.AddVote(v1) assert.NoError(t, err) - assert.True(t, vs.HasAnyVoteFor(0, vote.CPValueOne)) - assert.False(t, vs.HasAnyVoteFor(0, vote.CPValueZero)) + assert.True(t, vs.HasAnyVoteFor(0, vote.CPValueYes)) + assert.False(t, vs.HasAnyVoteFor(0, vote.CPValueNo)) } diff --git a/crypto/address.go b/crypto/address.go index 94d7ff27e..4a4254893 100644 --- a/crypto/address.go +++ b/crypto/address.go @@ -20,8 +20,7 @@ const ( ) const ( - SignatureTypeTreasury byte = 0 - SignatureTypeBLS byte = 1 + SignatureTypeBLS byte = 1 ) const ( diff --git a/crypto/bls/bls.go b/crypto/bls/bls.go index 57ff1f611..b549a46c3 100644 --- a/crypto/bls/bls.go +++ b/crypto/bls/bls.go @@ -46,9 +46,3 @@ func PublicKeyAggregate(pubs ...*PublicKey) *PublicKey { pointG2: aggPointG2, } } - -func VerifyAggregated(sig *Signature, pubs []*PublicKey, msg []byte) error { - aggPub := PublicKeyAggregate(pubs...) - - return aggPub.Verify(msg, sig) -} diff --git a/crypto/bls/bls_test.go b/crypto/bls/bls_test.go index c6500321a..70d0308b4 100644 --- a/crypto/bls/bls_test.go +++ b/crypto/bls/bls_test.go @@ -84,19 +84,6 @@ func TestAggregateFailed(t *testing.T) { assert.Error(t, pub2.Verify(msg1, agg1)) assert.Error(t, pub3.Verify(msg1, agg1)) - assert.NoError(t, bls.VerifyAggregated(agg1, pubs1, msg1)) - assert.Error(t, bls.VerifyAggregated(agg1, pubs1, msg2)) - assert.Error(t, bls.VerifyAggregated(agg2, pubs1, msg1)) - assert.Error(t, bls.VerifyAggregated(agg1, pubs2, msg1)) - assert.NoError(t, bls.VerifyAggregated(agg2, pubs2, msg1)) - assert.Error(t, bls.VerifyAggregated(agg2, pubs2, msg2)) - assert.Error(t, bls.VerifyAggregated(agg3, pubs1, msg1)) - assert.Error(t, bls.VerifyAggregated(agg3, pubs1, msg2)) - assert.Error(t, bls.VerifyAggregated(agg4, pubs1, msg1)) - assert.Error(t, bls.VerifyAggregated(agg1, pubs3, msg1)) - assert.NoError(t, bls.VerifyAggregated(agg5, pubs1, msg1)) - assert.NoError(t, bls.VerifyAggregated(agg1, pubs4, msg1)) - assert.Nil(t, pubAgg1.Verify(msg1, agg1)) assert.NotNil(t, pubAgg1.Verify(msg2, agg1)) assert.NotNil(t, pubAgg1.Verify(msg1, agg2)) @@ -152,12 +139,6 @@ func TestDuplicatedAggregate(t *testing.T) { pubAgg1 := bls.PublicKeyAggregate(pubs1...) pubAgg2 := bls.PublicKeyAggregate(pubs2...) assert.False(t, pubAgg1.EqualsTo(pubAgg2)) - - assert.Error(t, bls.VerifyAggregated(agg1, pubs1, msg1)) - assert.NotNil(t, pubAgg1.Verify(msg1, agg1)) - - assert.NoError(t, bls.VerifyAggregated(agg1, pubs2, msg1)) - assert.Nil(t, pubAgg2.Verify(msg1, agg1)) } // TestHashToCurve ensures that the hash-to-curve function in kilic/bls12-381 diff --git a/fastconsensus/commit.go b/fastconsensus/commit.go new file mode 100644 index 000000000..65088fac2 --- /dev/null +++ b/fastconsensus/commit.go @@ -0,0 +1,46 @@ +package fastconsensus + +import ( + "github.com/pactus-project/pactus/types/proposal" + "github.com/pactus-project/pactus/types/vote" +) + +type commitState struct { + *consensus +} + +func (s *commitState) enter() { + s.decide() +} + +func (s *commitState) decide() { + roundProposal := s.log.RoundProposal(s.round) + certBlock := roundProposal.Block() + err := s.bcState.CommitBlock(certBlock, s.blockCert) + if err != nil { + s.logger.Error("committing block failed", "block", certBlock, "error", err) + } else { + s.logger.Info("block committed, schedule new height", "hash", certBlock.Hash()) + + // Now we can announce the committed block and certificate + s.announceNewBlock(certBlock, s.blockCert) + } + + s.enterNewState(s.newHeightState) +} + +func (*commitState) onAddVote(_ *vote.Vote) { + panic("Unreachable") +} + +func (*commitState) onSetProposal(_ *proposal.Proposal) { + panic("Unreachable") +} + +func (*commitState) onTimeout(_ *ticker) { + panic("Unreachable") +} + +func (*commitState) name() string { + return "commit" +} diff --git a/fastconsensus/config.go b/fastconsensus/config.go new file mode 100644 index 000000000..805a94294 --- /dev/null +++ b/fastconsensus/config.go @@ -0,0 +1,44 @@ +package fastconsensus + +import "time" + +type Config struct { + ChangeProposerTimeout time.Duration `toml:"-"` + ChangeProposerDelta time.Duration `toml:"-"` + MinimumAvailabilityScore float64 `toml:"-"` +} + +func DefaultConfig() *Config { + return &Config{ + ChangeProposerTimeout: 8 * time.Second, + ChangeProposerDelta: 4 * time.Second, + MinimumAvailabilityScore: 0.9, + } +} + +// BasicCheck performs basic checks on the configuration. +func (conf *Config) BasicCheck() error { + if conf.ChangeProposerTimeout <= 0 { + return ConfigError{ + Reason: "change proposer timeout must be greater than zero", + } + } + if conf.ChangeProposerDelta <= 0 { + return ConfigError{ + Reason: "change proposer delta must be greater than zero", + } + } + if conf.MinimumAvailabilityScore < 0 || conf.MinimumAvailabilityScore > 1 { + return ConfigError{ + Reason: "minimum availability score can't be negative or more than 1", + } + } + + return nil +} + +func (conf *Config) CalculateChangeProposerTimeout(round int16) time.Duration { + return time.Duration( + conf.ChangeProposerTimeout.Milliseconds()+conf.ChangeProposerDelta.Milliseconds()*int64(round), + ) * time.Millisecond +} diff --git a/fastconsensus/config_test.go b/fastconsensus/config_test.go new file mode 100644 index 000000000..356238ccc --- /dev/null +++ b/fastconsensus/config_test.go @@ -0,0 +1,40 @@ +package fastconsensus + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestDefaultConfigCheck(t *testing.T) { + c1 := DefaultConfig() + c2 := DefaultConfig() + c3 := DefaultConfig() + c4 := DefaultConfig() + c5 := DefaultConfig() + assert.NoError(t, c1.BasicCheck()) + + c2.ChangeProposerDelta = 0 * time.Second + assert.ErrorIs(t, c2.BasicCheck(), ConfigError{Reason: "change proposer delta must be greater than zero"}) + + c3.ChangeProposerTimeout = 0 * time.Second + assert.ErrorIs(t, c3.BasicCheck(), ConfigError{Reason: "change proposer timeout must be greater than zero"}) + + c4.ChangeProposerTimeout = -1 * time.Second + assert.ErrorIs(t, c4.BasicCheck(), ConfigError{Reason: "change proposer timeout must be greater than zero"}) + + c5.MinimumAvailabilityScore = 1.5 + assert.ErrorIs(t, c5.BasicCheck(), ConfigError{Reason: "minimum availability score can't be negative or more than 1"}) + + c5.MinimumAvailabilityScore = -0.8 + assert.ErrorIs(t, c5.BasicCheck(), ConfigError{Reason: "minimum availability score can't be negative or more than 1"}) +} + +func TestCalculateChangeProposerTimeout(t *testing.T) { + c := DefaultConfig() + + assert.Equal(t, c.CalculateChangeProposerTimeout(0), c.ChangeProposerTimeout) + assert.Equal(t, c.CalculateChangeProposerTimeout(1), c.ChangeProposerTimeout+c.ChangeProposerDelta) + assert.Equal(t, c.CalculateChangeProposerTimeout(4), c.ChangeProposerTimeout+(4*c.ChangeProposerDelta)) +} diff --git a/fastconsensus/consensus.go b/fastconsensus/consensus.go new file mode 100644 index 000000000..d45fff1f9 --- /dev/null +++ b/fastconsensus/consensus.go @@ -0,0 +1,515 @@ +package fastconsensus + +import ( + "fmt" + "sync" + "time" + + "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/crypto/bls" + "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/fastconsensus/log" + "github.com/pactus-project/pactus/state" + "github.com/pactus-project/pactus/sync/bundle/message" + "github.com/pactus-project/pactus/types/block" + "github.com/pactus-project/pactus/types/certificate" + "github.com/pactus-project/pactus/types/proposal" + "github.com/pactus-project/pactus/types/validator" + "github.com/pactus-project/pactus/types/vote" + "github.com/pactus-project/pactus/util" + "github.com/pactus-project/pactus/util/logger" +) + +type broadcaster func(crypto.Address, message.Message) + +type consensus struct { + lk sync.RWMutex + + config *Config + logger *logger.SubLogger + log *log.Log + validators []*validator.Validator + cpWeakValidity *hash.Hash // The change proposer's weak validity that is a prepared block hash + cpDecided int + height uint32 + round int16 + cpRound int16 + valKey *bls.ValidatorKey + rewardAddr crypto.Address + bcState state.Facade // Blockchain state + blockCert *certificate.BlockCertificate + changeProposer *changeProposer + newHeightState consState + proposeState consState + prepareState consState + precommitState consState + commitState consState + cpPreVoteState consState + cpMainVoteState consState + cpDecideState consState + currentState consState + broadcaster broadcaster + mediator mediator + active bool +} + +func NewConsensus( + conf *Config, + bcState state.Facade, + valKey *bls.ValidatorKey, + rewardAddr crypto.Address, + broadcastCh chan message.Message, + mediator mediator, +) Consensus { + broadcaster := func(_ crypto.Address, msg message.Message) { + broadcastCh <- msg + } + + return makeConsensus(conf, bcState, + valKey, rewardAddr, broadcaster, mediator) +} + +func makeConsensus( + conf *Config, + bcState state.Facade, + valKey *bls.ValidatorKey, + rewardAddr crypto.Address, + broadcaster broadcaster, + mediator mediator, +) *consensus { + cs := &consensus{ + config: conf, + bcState: bcState, + broadcaster: broadcaster, + valKey: valKey, + } + + // Update height later, See enterNewHeight. + cs.log = log.NewLog() + cs.logger = logger.NewSubLogger("_consensus", cs) + cs.rewardAddr = rewardAddr + + cs.changeProposer = &changeProposer{cs} + cs.newHeightState = &newHeightState{cs} + cs.proposeState = &proposeState{cs} + cs.prepareState = &prepareState{cs, false} + cs.precommitState = &precommitState{cs, false} + cs.commitState = &commitState{cs} + cs.cpPreVoteState = &cpPreVoteState{cs.changeProposer} + cs.cpMainVoteState = &cpMainVoteState{cs.changeProposer} + cs.cpDecideState = &cpDecideState{cs.changeProposer} + cs.currentState = cs.newHeightState + cs.mediator = mediator + + cs.height = 0 + cs.round = 0 + cs.active = false + cs.mediator = mediator + + mediator.Register(cs) + + logger.Info("consensus instance created", + "validator address", valKey.Address().String(), + "reward address", rewardAddr.String()) + + return cs +} + +func (cs *consensus) Start() { + cs.lk.Lock() + defer cs.lk.Unlock() + + cs.doMoveToNewHeight() + // We have just started the consensus (possibly restarting the node). + // Therefore, let's query the votes and proposals in case we missed any. + if cs.active { + cs.queryProposal() + cs.queryVotes() + } +} + +func (cs *consensus) String() string { + return fmt.Sprintf("{%s %d/%d/%s/%d}", + cs.valKey.Address().ShortString(), + cs.height, cs.round, cs.currentState.name(), cs.cpRound) +} + +func (cs *consensus) ConsensusKey() *bls.PublicKey { + cs.lk.RLock() + defer cs.lk.RUnlock() + + return cs.valKey.PublicKey() +} + +func (cs *consensus) HeightRound() (uint32, int16) { + cs.lk.RLock() + defer cs.lk.RUnlock() + + return cs.height, cs.round +} + +func (cs *consensus) Proposal() *proposal.Proposal { + cs.lk.RLock() + defer cs.lk.RUnlock() + + return cs.log.RoundProposal(cs.round) +} + +func (cs *consensus) HasVote(h hash.Hash) bool { + cs.lk.RLock() + defer cs.lk.RUnlock() + + return cs.log.HasVote(h) +} + +// AllVotes returns all valid votes inside the consensus log up to and including +// the current consensus round. +// Valid votes from subsequent rounds are not included. +func (cs *consensus) AllVotes() []*vote.Vote { + cs.lk.RLock() + defer cs.lk.RUnlock() + + votes := []*vote.Vote{} + for r := int16(0); r <= cs.round; r++ { + m := cs.log.RoundMessages(r) + votes = append(votes, m.AllVotes()...) + } + + return votes +} + +func (cs *consensus) enterNewState(s consState) { + cs.currentState = s + cs.currentState.enter() +} + +func (cs *consensus) MoveToNewHeight() { + cs.lk.Lock() + defer cs.lk.Unlock() + + cs.doMoveToNewHeight() +} + +func (cs *consensus) doMoveToNewHeight() { + stateHeight := cs.bcState.LastBlockHeight() + if cs.height != stateHeight+1 { + cs.enterNewState(cs.newHeightState) + } +} + +func (cs *consensus) scheduleTimeout(duration time.Duration, height uint32, round int16, target tickerTarget) { + ti := &ticker{duration, height, round, target} + timer := time.NewTimer(duration) + cs.logger.Trace("new timer scheduled ⏱️", "duration", duration, "height", height, "round", round, "target", target) + + go func() { + <-timer.C + cs.handleTimeout(ti) + }() +} + +func (cs *consensus) SetProposal(p *proposal.Proposal) { + cs.lk.Lock() + defer cs.lk.Unlock() + + if !cs.active { + cs.logger.Trace("we are not in the committee") + + return + } + + if p.Height() != cs.height { + cs.logger.Trace("invalid height", "proposal", p) + + return + } + + if p.Round() < cs.round { + cs.logger.Trace("expired round", "proposal", p) + + return + } + + roundProposal := cs.log.RoundProposal(p.Round()) + if roundProposal != nil { + cs.logger.Trace("this round has proposal", "proposal", p) + + return + } + + if p.Height() == cs.bcState.LastBlockHeight() { + // A slow validator might receive a proposal after committing the proposed block. + // In this case, the proposal is accepted and the slow validator continues. + // By doing so, the validator can broadcast its votes and + // prevent itself from being marked as absent in the block certificate. + cs.logger.Trace("block is committed for this height", "proposal", p) + if p.Block().Hash() != cs.bcState.LastBlockHash() { + cs.logger.Warn("proposal is not for the committed block", "proposal", p) + + return + } + } else { + proposer := cs.proposer(p.Round()) + if err := p.Verify(proposer.PublicKey()); err != nil { + cs.logger.Warn("proposal is invalid", "proposal", p, "error", err) + + return + } + + if err := cs.bcState.ValidateBlock(p.Block(), p.Round()); err != nil { + cs.logger.Warn("invalid block", "proposal", p, "error", err) + + return + } + } + + cs.logger.Info("proposal set", "proposal", p) + cs.log.SetRoundProposal(p.Round(), p) + + cs.currentState.onSetProposal(p) +} + +func (cs *consensus) handleTimeout(t *ticker) { + cs.lk.Lock() + defer cs.lk.Unlock() + + cs.logger.Trace("handle ticker", "ticker", t) + + // Old tickers might be triggered now. Ignore them. + if cs.height != t.Height || cs.round != t.Round { + cs.logger.Trace("stale ticker", "ticker", t) + + return + } + + cs.logger.Debug("timer expired", "ticker", t) + cs.currentState.onTimeout(t) +} + +func (cs *consensus) AddVote(v *vote.Vote) { + cs.lk.Lock() + defer cs.lk.Unlock() + + if !cs.active { + cs.logger.Trace("we are not in the committee") + + return + } + + if v.Height() != cs.height { + cs.logger.Trace("vote has invalid height", "vote", v) + + return + } + + if v.Type() == vote.VoteTypeCPPreVote || + v.Type() == vote.VoteTypeCPMainVote || + v.Type() == vote.VoteTypeCPDecided { + err := cs.changeProposer.cpCheckJust(v) + if err != nil { + cs.logger.Error("error on adding a cp vote", "vote", v, "error", err) + + return + } + } + + added, err := cs.log.AddVote(v) + if err != nil { + cs.logger.Error("error on adding a vote", "vote", v, "error", err) + } + if added { + cs.logger.Info("new vote added", "vote", v) + + cs.currentState.onAddVote(v) + } +} + +func (cs *consensus) proposer(round int16) *validator.Validator { + return cs.bcState.Proposer(round) +} + +func (cs *consensus) isProposer() bool { + return cs.proposer(cs.round).Address() == cs.valKey.Address() +} + +func (cs *consensus) signAddCPPreVote(h hash.Hash, + cpRound int16, cpValue vote.CPValue, just vote.Just, +) { + v := vote.NewCPPreVote(h, cs.height, + cs.round, cpRound, cpValue, just, cs.valKey.Address()) + cs.signAddVote(v) +} + +func (cs *consensus) signAddCPMainVote(h hash.Hash, + cpRound int16, cpValue vote.CPValue, just vote.Just, +) { + v := vote.NewCPMainVote(h, cs.height, cs.round, + cpRound, cpValue, just, cs.valKey.Address()) + cs.signAddVote(v) +} + +func (cs *consensus) signAddCPDecidedVote(h hash.Hash, + cpRound int16, cpValue vote.CPValue, just vote.Just, +) { + v := vote.NewCPDecidedVote(h, cs.height, cs.round, + cpRound, cpValue, just, cs.valKey.Address()) + cs.signAddVote(v) +} + +func (cs *consensus) signAddPrepareVote(h hash.Hash) { + v := vote.NewPrepareVote(h, cs.height, cs.round, cs.valKey.Address()) + cs.signAddVote(v) +} + +func (cs *consensus) signAddPrecommitVote(h hash.Hash) { + v := vote.NewPrecommitVote(h, cs.height, cs.round, cs.valKey.Address()) + cs.signAddVote(v) +} + +func (cs *consensus) signAddVote(v *vote.Vote) { + sig := cs.valKey.Sign(v.SignBytes()) + v.SetSignature(sig) + cs.logger.Info("our vote signed and broadcasted", "vote", v) + + _, err := cs.log.AddVote(v) + if err != nil { + cs.logger.Error("error on adding our vote", "error", err, "vote", v) + } + cs.broadcastVote(v) +} + +func (cs *consensus) queryProposal() { + cs.broadcaster(cs.valKey.Address(), + message.NewQueryProposalMessage(cs.height, cs.valKey.Address())) +} + +// queryVotes is an anti-entropy mechanism to retrieve missed votes +// when a validator falls behind the network. +// However, invoking this method might result in unnecessary bandwidth usage. +func (cs *consensus) queryVotes() { + cs.broadcaster(cs.valKey.Address(), + message.NewQueryVotesMessage(cs.height, cs.round, cs.valKey.Address())) +} + +func (cs *consensus) broadcastProposal(p *proposal.Proposal) { + go cs.mediator.OnPublishProposal(cs, p) + cs.broadcaster(cs.valKey.Address(), + message.NewProposalMessage(p)) +} + +func (cs *consensus) broadcastVote(v *vote.Vote) { + go cs.mediator.OnPublishVote(cs, v) + cs.broadcaster(cs.valKey.Address(), + message.NewVoteMessage(v)) +} + +func (cs *consensus) announceNewBlock(blk *block.Block, cert *certificate.BlockCertificate) { + go cs.mediator.OnBlockAnnounce(cs) + cs.broadcaster(cs.valKey.Address(), + message.NewBlockAnnounceMessage(blk, cert)) +} + +func (cs *consensus) makeBlockCertificate(votes map[crypto.Address]*vote.Vote, fastPath bool, +) *certificate.BlockCertificate { + cert := certificate.NewBlockCertificate(cs.height, cs.round, fastPath) + cert.SetSignature(cs.signersInfo(votes)) + + return cert +} + +func (cs *consensus) makeVoteCertificate(votes map[crypto.Address]*vote.Vote, +) *certificate.VoteCertificate { + cert := certificate.NewVoteCertificate(cs.height, cs.round) + cert.SetSignature(cs.signersInfo(votes)) + + return cert +} + +// signersInfo processes a map of votes from validators and provides these information: +// - A list of all validators' numbers eligible to vote in this step. +// - A list of absentee validators' numbers who did not vote in this step. +// - An aggregated signature generated from the signatures of participating validators. +func (cs *consensus) signersInfo(votes map[crypto.Address]*vote.Vote) ([]int32, []int32, *bls.Signature) { + vals := cs.validators + committers := make([]int32, len(vals)) + absentees := make([]int32, 0) + sigs := make([]*bls.Signature, 0) + + for i, val := range vals { + vte := votes[val.Address()] + if vte != nil { + sigs = append(sigs, vte.Signature()) + } else { + absentees = append(absentees, val.Number()) + } + + committers[i] = val.Number() + } + + aggSig := bls.SignatureAggregate(sigs...) + + return committers, absentees, aggSig +} + +// IsActive checks if the consensus is in an active state and participating in the consensus algorithm. +func (cs *consensus) IsActive() bool { + cs.lk.RLock() + defer cs.lk.RUnlock() + + return cs.active +} + +// TODO: Improve the performance? +func (cs *consensus) PickRandomVote(round int16) *vote.Vote { + cs.lk.RLock() + defer cs.lk.RUnlock() + + votes := []*vote.Vote{} + if round == cs.round { + m := cs.log.RoundMessages(round) + votes = append(votes, m.AllVotes()...) + } else { + // Only broadcast cp:decided votes + vs := cs.log.CPDecidedVoteVoteSet(round) + votes = append(votes, vs.AllVotes()...) + } + if len(votes) == 0 { + return nil + } + + return votes[util.RandInt32(int32(len(votes)))] +} + +func (cs *consensus) startChangingProposer() { + // If it is not decided yet. + // TODO: can we remove this condition in new consensus model? + if cs.cpDecided == -1 { + cs.logger.Info("changing proposer started", "cpRound", cs.cpRound) + cs.enterNewState(cs.cpPreVoteState) + } +} + +func (cs *consensus) strongCommit() { + prepares := cs.log.PrepareVoteSet(cs.round) + prepareQH := prepares.QuorumHash() + if prepareQH != nil { + if prepares.HasAbsoluteQuorum(*prepareQH) { + cs.logger.Debug("prepare has absolute quorum", "hash", prepareQH.ShortString()) + + roundProposal := cs.log.RoundProposal(cs.round) + if roundProposal == nil { + // There is a consensus about a proposal that we don't have yet. + // Ask peers for this proposal. + cs.logger.Info("query for a decided proposal", "prepareQH", prepareQH) + cs.queryProposal() + + return + } + + votes := prepares.BlockVotes(*prepareQH) + cs.blockCert = cs.makeBlockCertificate(votes, true) + + cs.enterNewState(cs.commitState) + } + } +} diff --git a/fastconsensus/consensus_test.go b/fastconsensus/consensus_test.go new file mode 100644 index 000000000..9cd4479d8 --- /dev/null +++ b/fastconsensus/consensus_test.go @@ -0,0 +1,1052 @@ +package fastconsensus + +import ( + "fmt" + "testing" + "time" + + "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/crypto/bls" + "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/genesis" + "github.com/pactus-project/pactus/state" + "github.com/pactus-project/pactus/store" + "github.com/pactus-project/pactus/sync/bundle/message" + "github.com/pactus-project/pactus/txpool" + "github.com/pactus-project/pactus/types/account" + "github.com/pactus-project/pactus/types/block" + "github.com/pactus-project/pactus/types/certificate" + "github.com/pactus-project/pactus/types/param" + "github.com/pactus-project/pactus/types/proposal" + "github.com/pactus-project/pactus/types/tx" + "github.com/pactus-project/pactus/types/validator" + "github.com/pactus-project/pactus/types/vote" + "github.com/pactus-project/pactus/util" + "github.com/pactus-project/pactus/util/logger" + "github.com/pactus-project/pactus/util/testsuite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/exp/slices" +) + +const ( + tIndexX = 0 + tIndexY = 1 + tIndexB = 2 + tIndexP = 3 + tIndexM = 4 + tIndexN = 5 +) + +type consMessage struct { + sender crypto.Address + message message.Message +} +type testData struct { + *testsuite.TestSuite + + valKeys []*bls.ValidatorKey + txPool *txpool.MockTxPool + genDoc *genesis.Genesis + consX *consensus // Good peer + consY *consensus // Good peer + consB *consensus // Byzantine or offline peer + consP *consensus // Partitioned peer + consM *consensus // Witness Peer + consN *consensus // Witness Peer + consMessages []consMessage +} + +func testConfig() *Config { + return &Config{ + ChangeProposerTimeout: 1 * time.Hour, // Disabling timers + ChangeProposerDelta: 1 * time.Hour, // Disabling timers + } +} + +func setup(t *testing.T) *testData { + t.Helper() + queryVoteInitialTimeout = 2 * time.Hour + + return setupWithSeed(t, testsuite.GenerateSeed()) +} + +func setupWithSeed(t *testing.T, seed int64) *testData { + t.Helper() + + fmt.Printf("=== test %s, seed: %d\n", t.Name(), seed) + + ts := testsuite.NewTestSuiteForSeed(seed) + + _, valKeys := ts.GenerateTestCommittee(6) + txPool := txpool.MockingTxPool() + + vals := make([]*validator.Validator, 6) + for i, key := range valKeys { + val := validator.NewValidator(key.PublicKey(), int32(i)) + vals[i] = val + } + + acc := account.NewAccount(0) + acc.AddToBalance(21 * 1e14) + accs := map[crypto.Address]*account.Account{crypto.TreasuryAddress: acc} + params := param.DefaultParams() + params.CommitteeSize = 6 + + // to prevent triggering timers before starting the tests to avoid double entries for new heights in some tests. + getTime := util.RoundNow(params.BlockIntervalInSecond).Add(time.Duration(params.BlockIntervalInSecond) * time.Second) + genDoc := genesis.MakeGenesis(getTime, accs, vals, params) + + consMessages := make([]consMessage, 0) + td := &testData{ + TestSuite: ts, + valKeys: valKeys, + txPool: txPool, + genDoc: genDoc, + consMessages: consMessages, + } + broadcasterFunc := func(sender crypto.Address, msg message.Message) { + fmt.Printf("received a message %s: %s\n", msg.Type(), msg.String()) + td.consMessages = append(td.consMessages, consMessage{ + sender: sender, + message: msg, + }) + } + + instances := make([]*consensus, len(valKeys)) + for i, valKey := range valKeys { + bcState, err := state.LoadOrNewState(genDoc, []*bls.ValidatorKey{valKey}, + store.MockingStore(ts), txPool, nil) + require.NoError(t, err) + + instances[i] = makeConsensus(testConfig(), bcState, valKey, + valKey.PublicKey().AccountAddress(), broadcasterFunc, newConcreteMediator()) + } + + td.consX = instances[tIndexX] + td.consY = instances[tIndexY] + td.consB = instances[tIndexB] + td.consP = instances[tIndexP] + td.consM = instances[tIndexM] + td.consN = instances[tIndexN] + + // ------------------------------- + // Better logging during testing + overrideLogger := func(cons *consensus, name string) { + cons.logger = logger.NewSubLogger("_consensus", + testsuite.NewOverrideStringer(fmt.Sprintf("%s - %s: ", name, t.Name()), cons)) + } + + overrideLogger(td.consX, "consX") + overrideLogger(td.consY, "consY") + overrideLogger(td.consB, "consB") + overrideLogger(td.consP, "consP") + overrideLogger(td.consM, "consM") + overrideLogger(td.consN, "consN") + // ------------------------------- + + logger.Info("setup finished, start running the test", "name", t.Name()) + + return td +} + +func (td *testData) shouldNotPublish(t *testing.T, cons *consensus, msgType message.Type) { + t.Helper() + + for _, consMsg := range td.consMessages { + if consMsg.sender == cons.valKey.Address() && + consMsg.message.Type() == msgType { + require.Error(t, fmt.Errorf("should not publish %s", msgType)) + } + } +} + +func (td *testData) shouldPublishBlockAnnounce(t *testing.T, cons *consensus, h hash.Hash) { + t.Helper() + + for _, consMsg := range td.consMessages { + if consMsg.sender == cons.valKey.Address() && + consMsg.message.Type() == message.TypeBlockAnnounce { + m := consMsg.message.(*message.BlockAnnounceMessage) + assert.Equal(t, m.Block.Hash(), h) + + return + } + } + require.NoError(t, fmt.Errorf("Block announce message not published")) +} + +func (td *testData) shouldPublishProposal(t *testing.T, cons *consensus, + height uint32, round int16, +) *proposal.Proposal { + t.Helper() + + for _, consMsg := range td.consMessages { + if consMsg.sender == cons.valKey.Address() && + consMsg.message.Type() == message.TypeProposal { + m := consMsg.message.(*message.ProposalMessage) + require.Equal(t, m.Proposal.Height(), height) + require.Equal(t, m.Proposal.Round(), round) + + return m.Proposal + } + } + require.NoError(t, fmt.Errorf("Proposal message not published")) + + return nil +} + +func (td *testData) shouldPublishQueryProposal(t *testing.T, cons *consensus, height uint32) { + t.Helper() + + for _, consMsg := range td.consMessages { + if consMsg.sender != cons.valKey.Address() || + consMsg.message.Type() != message.TypeQueryProposal { + continue + } + + m := consMsg.message.(*message.QueryProposalMessage) + assert.Equal(t, m.Height, height) + assert.Equal(t, m.Querier, cons.valKey.Address()) + + return + } + require.NoError(t, fmt.Errorf("Query proposal message not published")) +} + +func (td *testData) shouldPublishQueryVote(t *testing.T, cons *consensus, height uint32, round int16) { + t.Helper() + + for _, consMsg := range td.consMessages { + if consMsg.sender != cons.valKey.Address() || + consMsg.message.Type() != message.TypeQueryVote { + continue + } + + m := consMsg.message.(*message.QueryVotesMessage) + assert.Equal(t, m.Height, height) + assert.Equal(t, m.Round, round) + assert.Equal(t, m.Querier, cons.valKey.Address()) + + return + } + require.NoError(t, fmt.Errorf("Query proposal message not published")) +} + +func (td *testData) shouldPublishVote(t *testing.T, cons *consensus, voteType vote.Type, h hash.Hash) *vote.Vote { + t.Helper() + + for i := len(td.consMessages) - 1; i >= 0; i-- { + consMsg := td.consMessages[i] + if consMsg.sender == cons.valKey.Address() && + consMsg.message.Type() == message.TypeVote { + m := consMsg.message.(*message.VoteMessage) + if m.Vote.Type() == voteType && + m.Vote.BlockHash() == h { + return m.Vote + } + } + } + require.NoError(t, fmt.Errorf("Vote message not published")) + + return nil +} + +func (*testData) checkHeightRound(t *testing.T, cons *consensus, height uint32, round int16) { + t.Helper() + + h, r := cons.HeightRound() + assert.Equal(t, h, height) + assert.Equal(t, r, round) +} + +func (td *testData) addPrepareVote(cons *consensus, blockHash hash.Hash, height uint32, round int16, + valID int, +) *vote.Vote { + v := vote.NewPrepareVote(blockHash, height, round, td.valKeys[valID].Address()) + + return td.addVote(cons, v, valID) +} + +func (td *testData) addPrecommitVote(cons *consensus, blockHash hash.Hash, height uint32, round int16, + valID int, +) *vote.Vote { + v := vote.NewPrecommitVote(blockHash, height, round, td.valKeys[valID].Address()) + + return td.addVote(cons, v, valID) +} + +func (td *testData) addCPPreVote(cons *consensus, blockHash hash.Hash, height uint32, round int16, + cpVal vote.CPValue, just vote.Just, valID int, +) *vote.Vote { + v := vote.NewCPPreVote(blockHash, height, round, 0, cpVal, just, td.valKeys[valID].Address()) + + return td.addVote(cons, v, valID) +} + +func (td *testData) addCPMainVote(cons *consensus, blockHash hash.Hash, height uint32, round int16, + cpVal vote.CPValue, just vote.Just, valID int, +) *vote.Vote { + v := vote.NewCPMainVote(blockHash, height, round, 0, cpVal, just, td.valKeys[valID].Address()) + + return td.addVote(cons, v, valID) +} + +func (td *testData) addCPDecidedVote(cons *consensus, blockHash hash.Hash, height uint32, round int16, + cpVal vote.CPValue, just vote.Just, valID int, +) *vote.Vote { + v := vote.NewCPDecidedVote(blockHash, height, round, 0, cpVal, just, td.valKeys[valID].Address()) + + return td.addVote(cons, v, valID) +} + +func (td *testData) addVote(cons *consensus, v *vote.Vote, valID int) *vote.Vote { + td.HelperSignVote(td.valKeys[valID], v) + cons.AddVote(v) + + return v +} + +func (*testData) newHeightTimeout(cons *consensus) { + cons.lk.Lock() + cons.currentState.onTimeout(&ticker{0, cons.height, cons.round, tickerTargetNewHeight}) + cons.lk.Unlock() +} + +func (*testData) queryProposalTimeout(cons *consensus) { + cons.lk.Lock() + cons.currentState.onTimeout(&ticker{0, cons.height, cons.round, tickerTargetQueryProposal}) + cons.lk.Unlock() +} + +func (*testData) changeProposerTimeout(cons *consensus) { + cons.lk.Lock() + cons.currentState.onTimeout(&ticker{0, cons.height, cons.round, tickerTargetChangeProposer}) + cons.lk.Unlock() +} + +// enterNewHeight helps tests to enter new height safely +// without scheduling new height. It boosts the test speed. +func (td *testData) enterNewHeight(cons *consensus) { + cons.lk.Lock() + cons.enterNewState(cons.newHeightState) + cons.lk.Unlock() + + td.newHeightTimeout(cons) +} + +// enterNextRound helps tests to enter next round safely. +func (*testData) enterNextRound(cons *consensus) { + cons.lk.Lock() + cons.round++ + cons.enterNewState(cons.proposeState) + cons.lk.Unlock() +} + +func (td *testData) commitBlockForAllStates(t *testing.T) (*block.Block, *certificate.BlockCertificate) { + t.Helper() + + height := td.consX.bcState.LastBlockHeight() + var err error + prop := td.makeProposal(t, height+1, 0) + + cert := certificate.NewBlockCertificate(height+1, 0, true) + sb := cert.SignBytes(prop.Block().Hash()) + sig0 := td.consX.valKey.Sign(sb) + sig1 := td.consY.valKey.Sign(sb) + sig2 := td.consB.valKey.Sign(sb) + sig3 := td.consP.valKey.Sign(sb) + sig4 := td.consM.valKey.Sign(sb) + + sig := bls.SignatureAggregate(sig0, sig1, sig2, sig3, sig4) + cert.SetSignature([]int32{0, 1, 2, 3, 4, 5}, []int32{5}, sig) + blk := prop.Block() + + err = td.consX.bcState.CommitBlock(blk, cert) + assert.NoError(t, err) + err = td.consY.bcState.CommitBlock(blk, cert) + assert.NoError(t, err) + err = td.consB.bcState.CommitBlock(blk, cert) + assert.NoError(t, err) + err = td.consP.bcState.CommitBlock(blk, cert) + assert.NoError(t, err) + err = td.consM.bcState.CommitBlock(blk, cert) + assert.NoError(t, err) + err = td.consN.bcState.CommitBlock(blk, cert) + assert.NoError(t, err) + + return blk, cert +} + +func (td *testData) makeProposal(t *testing.T, height uint32, round int16) *proposal.Proposal { + t.Helper() + + var cons *consensus + switch (height % 6) + uint32(round%6) { + case 1: + cons = td.consX + case 2: + cons = td.consY + case 3: + cons = td.consB + case 4: + cons = td.consP + case 5: + cons = td.consM + case 0, 6: + cons = td.consN + } + + blk, err := cons.bcState.ProposeBlock(cons.valKey, cons.rewardAddr) + require.NoError(t, err) + p := proposal.NewProposal(height, round, blk) + td.HelperSignProposal(cons.valKey, p) + + return p +} + +// makeChangeProposerJusts generates justifications for changing the proposer at the specified height and round. +// If `proposal` is nil, it creates justifications for not changing the proposer; +// otherwise, it generates justifications to change the proposer. +// It returns three justifications: +// +// 1. `JustInitNo` if the proposal is set, or `JustInitYes` if not for the pre-vote step, +// 2. `JustMainVoteNoConflict` for the main-vote step, +// 3. `JustDecided` for the decided step. +func (td *testData) makeChangeProposerJusts(t *testing.T, propBlockHash hash.Hash, + height uint32, round int16, +) (vote.Just, vote.Just, vote.Just) { + t.Helper() + + cpRound := int16(0) + + // Create PreVote Justification + var preVoteJust vote.Just + var cpValue vote.CPValue + + if propBlockHash != hash.UndefHash { + cpValue = vote.CPValueNo + prepareCommitters := []int32{} + prepareSigs := []*bls.Signature{} + for i, val := range td.consP.validators { + prepareVote := vote.NewPrepareVote(propBlockHash, height, round, val.Address()) + signBytes := prepareVote.SignBytes() + + prepareCommitters = append(prepareCommitters, val.Number()) + prepareSigs = append(prepareSigs, td.valKeys[i].Sign(signBytes)) + } + prepareAggSig := bls.SignatureAggregate(prepareSigs...) + certPrepare := certificate.NewVoteCertificate(height, round) + certPrepare.SetSignature(prepareCommitters, []int32{}, prepareAggSig) + + preVoteJust = &vote.JustInitNo{ + QCert: certPrepare, + } + } else { + cpValue = vote.CPValueYes + preVoteJust = &vote.JustInitYes{} + } + + // Create MainVote Justification + preVoteCommitters := []int32{} + preVoteSigs := []*bls.Signature{} + for i, val := range td.consP.validators { + preVote := vote.NewCPPreVote(propBlockHash, height, round, + cpRound, cpValue, preVoteJust, val.Address()) + signBytes := preVote.SignBytes() + + preVoteCommitters = append(preVoteCommitters, val.Number()) + preVoteSigs = append(preVoteSigs, td.valKeys[i].Sign(signBytes)) + } + preVoteAggSig := bls.SignatureAggregate(preVoteSigs...) + certPreVote := certificate.NewVoteCertificate(height, round) + certPreVote.SetSignature(preVoteCommitters, []int32{}, preVoteAggSig) + mainVoteJust := &vote.JustMainVoteNoConflict{QCert: certPreVote} + + // Create Decided Justification + mainVoteCommitters := []int32{} + mainVoteSigs := []*bls.Signature{} + for i, val := range td.consP.validators { + mainVote := vote.NewCPMainVote(propBlockHash, height, round, + cpRound, cpValue, mainVoteJust, val.Address()) + signBytes := mainVote.SignBytes() + + mainVoteCommitters = append(mainVoteCommitters, val.Number()) + mainVoteSigs = append(mainVoteSigs, td.valKeys[i].Sign(signBytes)) + } + mainVoteAggSig := bls.SignatureAggregate(mainVoteSigs...) + certMainVote := certificate.NewVoteCertificate(height, round) + certMainVote.SetSignature(mainVoteCommitters, []int32{}, mainVoteAggSig) + decidedJust := &vote.JustDecided{QCert: certMainVote} + + return preVoteJust, mainVoteJust, decidedJust +} + +func TestStart(t *testing.T) { + td := setup(t) + + td.consX.Start() + td.shouldPublishQueryProposal(t, td.consX, 1) + td.shouldPublishQueryVote(t, td.consX, 1, 0) +} + +func TestNotInCommittee(t *testing.T) { + td := setup(t) + + valKey := td.RandValKey() + str := store.MockingStore(td.TestSuite) + + st, _ := state.LoadOrNewState(td.genDoc, []*bls.ValidatorKey{valKey}, str, td.txPool, nil) + consInst := NewConsensus(testConfig(), st, valKey, valKey.Address(), make(chan message.Message, 100), + newConcreteMediator()) + cons := consInst.(*consensus) + + td.enterNewHeight(cons) + td.newHeightTimeout(cons) + assert.Equal(t, cons.currentState.name(), "new-height") +} + +func TestVoteWithInvalidHeight(t *testing.T) { + td := setup(t) + + td.commitBlockForAllStates(t) // height 1 + td.enterNewHeight(td.consP) + + v1 := td.addPrepareVote(td.consP, td.RandHash(), 1, 0, tIndexX) + v2 := td.addPrepareVote(td.consP, td.RandHash(), 2, 0, tIndexX) + v3 := td.addPrepareVote(td.consP, td.RandHash(), 2, 0, tIndexY) + v4 := td.addPrepareVote(td.consP, td.RandHash(), 3, 0, tIndexX) + + require.False(t, td.consP.HasVote(v1.Hash())) + require.True(t, td.consP.HasVote(v2.Hash())) + require.True(t, td.consP.HasVote(v3.Hash())) + require.False(t, td.consP.HasVote(v4.Hash())) +} + +func TestConsensusFastPath(t *testing.T) { + td := setup(t) + + td.commitBlockForAllStates(t) // height 1 + + td.enterNewHeight(td.consX) + td.checkHeightRound(t, td.consX, 2, 0) + + prop := td.makeProposal(t, 2, 0) + td.consX.SetProposal(prop) + + td.addPrepareVote(td.consX, prop.Block().Hash(), 2, 0, tIndexY) + td.addPrepareVote(td.consX, prop.Block().Hash(), 2, 0, tIndexB) + td.addPrepareVote(td.consX, prop.Block().Hash(), 2, 0, tIndexP) + td.addPrepareVote(td.consX, prop.Block().Hash(), 2, 0, tIndexM) + td.shouldPublishVote(t, td.consX, vote.VoteTypePrepare, prop.Block().Hash()) + + td.shouldPublishBlockAnnounce(t, td.consX, prop.Block().Hash()) +} + +func TestConsensusAddVote(t *testing.T) { + td := setup(t) + + td.enterNewHeight(td.consP) + td.enterNextRound(td.consP) + + v1 := td.addPrepareVote(td.consP, td.RandHash(), 1, 0, tIndexX) + v2 := td.addPrepareVote(td.consP, td.RandHash(), 1, 2, tIndexX) + v3 := td.addPrepareVote(td.consP, td.RandHash(), 1, 1, tIndexX) + v4 := td.addPrecommitVote(td.consP, td.RandHash(), 1, 1, tIndexX) + v5 := td.addPrepareVote(td.consP, td.RandHash(), 2, 0, tIndexX) + v6, _ := td.GenerateTestPrepareVote(1, 0) + td.consP.AddVote(v6) + + assert.True(t, td.consP.HasVote(v1.Hash())) // previous round + assert.True(t, td.consP.HasVote(v2.Hash())) // next round + assert.True(t, td.consP.HasVote(v3.Hash())) + assert.True(t, td.consP.HasVote(v4.Hash())) + assert.False(t, td.consP.HasVote(v5.Hash())) // valid votes for the next height + assert.False(t, td.consP.HasVote(v6.Hash())) // invalid votes + + assert.Equal(t, td.consP.AllVotes(), []*vote.Vote{v1, v3, v4}) + assert.NotContains(t, td.consP.AllVotes(), v2) +} + +// TestConsensusLateProposal tests the scenario where a slow node doesn't have the proposal +// in prepare phase. +func TestConsensusLateProposal(t *testing.T) { + td := setup(t) + + td.commitBlockForAllStates(t) // height 1 + + td.enterNewHeight(td.consP) + + h := uint32(2) + r := int16(0) + prop := td.makeProposal(t, h, r) + blockHash := prop.Block().Hash() + + td.commitBlockForAllStates(t) // height 2 + + // consP receives all the votes first + td.addPrepareVote(td.consP, blockHash, h, r, tIndexX) + td.addPrepareVote(td.consP, blockHash, h, r, tIndexY) + td.addPrepareVote(td.consP, blockHash, h, r, tIndexB) + td.addPrepareVote(td.consP, blockHash, h, r, tIndexM) + td.addPrepareVote(td.consP, blockHash, h, r, tIndexN) + + td.shouldPublishQueryProposal(t, td.consP, h) + + // consP receives proposal now + td.consP.SetProposal(prop) + + td.shouldPublishVote(t, td.consP, vote.VoteTypePrepare, blockHash) + td.shouldPublishBlockAnnounce(t, td.consP, blockHash) +} + +// TestConsensusVeryLateProposal tests the scenario where a slow node doesn't have the proposal +// in precommit phase. +func TestConsensusVeryLateProposal(t *testing.T) { + td := setup(t) + + td.commitBlockForAllStates(t) // height 1 + + td.enterNewHeight(td.consP) + + h := uint32(2) + r := int16(0) + prop := td.makeProposal(t, h, r) + blockHash := prop.Block().Hash() + + td.addPrepareVote(td.consP, blockHash, h, r, tIndexX) + td.addPrepareVote(td.consP, blockHash, h, r, tIndexY) + td.addPrepareVote(td.consP, blockHash, h, r, tIndexM) + td.addPrepareVote(td.consP, blockHash, h, r, tIndexN) + + // consP timed out + td.changeProposerTimeout(td.consP) + + _, _, decidedJust := td.makeChangeProposerJusts(t, prop.Block().Hash(), h, r) + td.addCPDecidedVote(td.consP, prop.Block().Hash(), h, r, vote.CPValueNo, decidedJust, tIndexX) + + td.addPrecommitVote(td.consP, blockHash, h, r, tIndexX) + td.addPrecommitVote(td.consP, blockHash, h, r, tIndexY) + td.addPrecommitVote(td.consP, blockHash, h, r, tIndexM) + td.addPrecommitVote(td.consP, blockHash, h, r, tIndexN) + + td.shouldPublishQueryProposal(t, td.consP, h) + + // consP receives proposal now + td.consP.SetProposal(prop) + + td.shouldPublishVote(t, td.consP, vote.VoteTypePrecommit, prop.Block().Hash()) + td.shouldPublishBlockAnnounce(t, td.consP, prop.Block().Hash()) +} + +func TestPickRandomVote(t *testing.T) { + td := setup(t) + + td.enterNewHeight(td.consP) + assert.Nil(t, td.consP.PickRandomVote(0)) + + h := uint32(1) + r := int16(0) + + preVoteJust, mainVoteJust, decidedJust := td.makeChangeProposerJusts(t, hash.UndefHash, h, r) + + // round 0 + v1 := td.addPrepareVote(td.consP, td.RandHash(), h, r, tIndexX) + v2 := td.addPrepareVote(td.consP, td.RandHash(), h, r, tIndexY) + v3 := td.addCPPreVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, preVoteJust, tIndexY) + v4 := td.addCPMainVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, mainVoteJust, tIndexY) + v5 := td.addCPDecidedVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, decidedJust, tIndexY) + + // Round 1 + td.enterNextRound(td.consP) + v6 := td.addPrepareVote(td.consP, td.RandHash(), h, r+1, tIndexY) + + require.True(t, td.consP.HasVote(v1.Hash())) + require.True(t, td.consP.HasVote(v2.Hash())) + require.True(t, td.consP.HasVote(v3.Hash())) + require.True(t, td.consP.HasVote(v4.Hash())) + require.True(t, td.consP.HasVote(v5.Hash())) + require.True(t, td.consP.HasVote(v6.Hash())) + + rndVote0 := td.consP.PickRandomVote(r) + assert.Equal(t, rndVote0, v5, "for past round should pick Decided votes only") + + rndVote1 := td.consP.PickRandomVote(r + 1) + assert.Equal(t, rndVote1, v6) +} + +func TestSetProposalFromPreviousRound(t *testing.T) { + td := setup(t) + + prop := td.makeProposal(t, 1, 0) + td.enterNewHeight(td.consP) + td.enterNextRound(td.consP) + + // It should ignore proposal for previous rounds + td.consP.SetProposal(prop) + + assert.Nil(t, td.consP.Proposal()) + td.checkHeightRound(t, td.consP, 1, 1) +} + +func TestSetProposalFromPreviousHeight(t *testing.T) { + td := setup(t) + + prop := td.makeProposal(t, 1, 0) + td.commitBlockForAllStates(t) // height 1 + + td.enterNewHeight(td.consP) + + td.consP.SetProposal(prop) + assert.Nil(t, td.consP.Proposal()) + td.checkHeightRound(t, td.consP, 2, 0) +} + +func TestDuplicateProposal(t *testing.T) { + td := setup(t) + + td.commitBlockForAllStates(t) + td.commitBlockForAllStates(t) + td.commitBlockForAllStates(t) + + td.enterNewHeight(td.consX) + + h := uint32(4) + r := int16(0) + prop1 := td.makeProposal(t, h, r) + trx := tx.NewTransferTx(h, td.consX.rewardAddr, + td.RandAccAddress(), 1000, 1000, "proposal changer") + td.HelperSignTransaction(td.consX.valKey.PrivateKey(), trx) + + assert.NoError(t, td.txPool.AppendTx(trx)) + prop2 := td.makeProposal(t, h, r) + assert.NotEqual(t, prop1.Hash(), prop2.Hash()) + + td.consX.SetProposal(prop1) + td.consX.SetProposal(prop2) + + assert.Equal(t, td.consX.Proposal().Hash(), prop1.Hash()) +} + +func TestNonActiveValidator(t *testing.T) { + td := setup(t) + + valKey := td.RandValKey() + consInst := NewConsensus(testConfig(), state.MockingState(td.TestSuite), + valKey, valKey.Address(), make(chan message.Message, 100), newConcreteMediator()) + nonActiveCons := consInst.(*consensus) + + t.Run("non-active instances should be in new-height state", func(t *testing.T) { + nonActiveCons.MoveToNewHeight() + td.newHeightTimeout(nonActiveCons) + td.checkHeightRound(t, nonActiveCons, 1, 0) + + // Double entry + nonActiveCons.MoveToNewHeight() + td.newHeightTimeout(nonActiveCons) + td.checkHeightRound(t, nonActiveCons, 1, 0) + + assert.False(t, nonActiveCons.IsActive()) + assert.Equal(t, nonActiveCons.currentState.name(), "new-height") + }) + + t.Run("non-active instances should ignore proposals", func(t *testing.T) { + prop := td.makeProposal(t, 1, 0) + nonActiveCons.SetProposal(prop) + + assert.Nil(t, nonActiveCons.Proposal()) + }) + + t.Run("non-active instances should ignore votes", func(t *testing.T) { + v := td.addPrepareVote(nonActiveCons, td.RandHash(), 1, 0, tIndexX) + + assert.False(t, nonActiveCons.HasVote(v.Hash())) + }) + + t.Run("non-active instances should move to new height", func(t *testing.T) { + b1, cert1 := td.commitBlockForAllStates(t) + + nonActiveCons.MoveToNewHeight() + td.checkHeightRound(t, nonActiveCons, 1, 0) + + assert.NoError(t, nonActiveCons.bcState.CommitBlock(b1, cert1)) + + nonActiveCons.MoveToNewHeight() + td.newHeightTimeout(nonActiveCons) + td.checkHeightRound(t, nonActiveCons, 2, 0) + }) +} + +func TestVoteWithBigRound(t *testing.T) { + td := setup(t) + + td.enterNewHeight(td.consX) + + v := td.addPrepareVote(td.consX, td.RandHash(), 1, util.MaxInt16, tIndexB) + assert.True(t, td.consX.HasVote(v.Hash())) +} + +func TestProposalWithBigRound(t *testing.T) { + td := setup(t) + + td.enterNewHeight(td.consP) + + prop := td.makeProposal(t, 1, util.MaxInt16) + td.consP.SetProposal(prop) + assert.Equal(t, td.consP.log.RoundProposal(util.MaxInt16), prop) + assert.Nil(t, td.consP.Proposal()) +} + +func TestCases(t *testing.T) { + tests := []struct { + seed int64 + round int16 + description string + }{ + // {1697898884837384019, 2, "1/3+ cp:PRE-VOTE in prepare step"}, + // {1694848907840926239, 0, "1/3+ cp:PRE-VOTE in precommit step"}, + // {1694849103290580532, 1, "Conflicting votes, cp-round=0"}, + // {1697900665869342730, 1, "Conflicting votes, cp-round=1"}, + // {1697887970998950590, 1, "consP & consB: Change Proposer, consX & consY: Commit (2 block announces)"}, + {1702913410152124511, 0, ""}, + } + + for i, test := range tests { + td := setupWithSeed(t, test.seed) + td.commitBlockForAllStates(t) + + td.enterNewHeight(td.consX) + td.enterNewHeight(td.consY) + td.enterNewHeight(td.consB) + td.enterNewHeight(td.consP) + td.enterNewHeight(td.consM) + td.enterNewHeight(td.consN) + + cert, err := checkConsensus(td, 2, nil) + require.NoError(t, err, + "test %v failed: %s", i+1, err) + require.Equal(t, cert.Round(), test.round, + "test %v failed. round not matched (expected %d, got %d)", + i+1, test.round, cert.Round()) + } +} + +func TestFaulty(t *testing.T) { + for i := 0; i < 10; i++ { + td := setup(t) + td.commitBlockForAllStates(t) + + td.enterNewHeight(td.consX) + td.enterNewHeight(td.consY) + td.enterNewHeight(td.consB) + td.enterNewHeight(td.consP) + td.enterNewHeight(td.consM) + td.enterNewHeight(td.consN) + + _, err := checkConsensus(td, 2, nil) + require.NoError(t, err) + } +} + +// In this test, B is a Byzantine node and the network is partitioned. +// B acts maliciously by double proposing: +// sending one proposal to X and Y, and another proposal to P, M and N. +// +// Once the partition is healed, honest nodes should either reach consensus +// on one proposal or change the proposer. +// This is due to the randomness of the binary agreement. +func TestByzantine(t *testing.T) { + td := setup(t) + + for i := 0; i < 8; i++ { + td.commitBlockForAllStates(t) + } + + h := uint32(9) + r := int16(0) + + // ================================= + // X, Y votes + td.enterNewHeight(td.consX) + td.enterNewHeight(td.consY) + + prop := td.makeProposal(t, h, r) + require.Equal(t, prop.Block().Header().ProposerAddress(), td.consB.valKey.Address()) + + // X and Y receive the Seconds proposal + td.consX.SetProposal(prop) + td.consY.SetProposal(prop) + + td.shouldPublishVote(t, td.consX, vote.VoteTypePrepare, prop.Block().Hash()) + td.shouldPublishVote(t, td.consY, vote.VoteTypePrepare, prop.Block().Hash()) + + // X and Y don't have enough votes, so they request to change the proposer + td.changeProposerTimeout(td.consX) + td.changeProposerTimeout(td.consY) + + // X and Y are unable to progress + + // ================================= + // P, M and N votes + // Byzantine node create the second proposal and send it to the partitioned nodes + byzTrx := tx.NewTransferTx(h, + td.consB.rewardAddr, td.RandAccAddress(), 1000, 1000, "") + td.HelperSignTransaction(td.consB.valKey.PrivateKey(), byzTrx) + assert.NoError(t, td.txPool.AppendTx(byzTrx)) + byzProp := td.makeProposal(t, h, r) + + require.NotEqual(t, prop.Block().Hash(), byzProp.Block().Hash()) + require.Equal(t, byzProp.Block().Header().ProposerAddress(), td.consB.valKey.Address()) + + td.enterNewHeight(td.consP) + td.enterNewHeight(td.consM) + td.enterNewHeight(td.consN) + + // P, M and N receive the Seconds proposal + td.consP.SetProposal(byzProp) + td.consM.SetProposal(byzProp) + td.consN.SetProposal(byzProp) + + voteP := td.shouldPublishVote(t, td.consP, vote.VoteTypePrepare, byzProp.Block().Hash()) + voteM := td.shouldPublishVote(t, td.consM, vote.VoteTypePrepare, byzProp.Block().Hash()) + voteN := td.shouldPublishVote(t, td.consN, vote.VoteTypePrepare, byzProp.Block().Hash()) + + // P, M and N don't have enough votes, so they request to change the proposer + td.changeProposerTimeout(td.consP) + td.changeProposerTimeout(td.consM) + td.changeProposerTimeout(td.consN) + + // P, M and N are unable to progress + + // ================================= + // B votes + // B requests to NOT change the proposer + + td.enterNewHeight(td.consB) + + voteB := vote.NewPrepareVote(byzProp.Block().Hash(), h, r, td.consB.valKey.Address()) + td.HelperSignVote(td.consB.valKey, voteB) + byzJust0Block := &vote.JustInitNo{ + QCert: td.consB.makeVoteCertificate( + map[crypto.Address]*vote.Vote{ + voteP.Signer(): voteP, + voteM.Signer(): voteM, + voteN.Signer(): voteN, + voteB.Signer(): voteB, + }), + } + byzVote := vote.NewCPPreVote(byzProp.Block().Hash(), h, r, 0, vote.CPValueNo, byzJust0Block, td.consB.valKey.Address()) + td.HelperSignVote(td.consB.valKey, byzVote) + + // ================================= + + td.checkHeightRound(t, td.consX, h, r) + td.checkHeightRound(t, td.consY, h, r) + td.checkHeightRound(t, td.consP, h, r) + td.checkHeightRound(t, td.consM, h, r) + td.checkHeightRound(t, td.consN, h, r) + + // ================================= + // Now, Partition heals + fmt.Println("== Partition heals") + cert, err := checkConsensus(td, h, []*vote.Vote{byzVote}) + + require.NoError(t, err) + require.Equal(t, cert.Height(), h) + require.Contains(t, cert.Absentees(), int32(tIndexB)) +} + +func checkConsensus(td *testData, height uint32, byzVotes []*vote.Vote) ( + *certificate.BlockCertificate, error, +) { + instances := []*consensus{td.consX, td.consY, td.consB, td.consP, td.consM, td.consN} + + if len(byzVotes) > 0 { + for _, v := range byzVotes { + td.consB.broadcastVote(v) + } + + // remove byzantine node (Byzantine node goes offline) + instances = []*consensus{td.consX, td.consY, td.consP, td.consM, td.consN} + } + + // 70% chance for the first block to be lost + changeProposerChance := 70 + + blockAnnounces := map[crypto.Address]*message.BlockAnnounceMessage{} + for len(td.consMessages) > 0 { + rndIndex := td.RandInt(len(td.consMessages)) + rndMsg := td.consMessages[rndIndex] + td.consMessages = slices.Delete(td.consMessages, rndIndex, rndIndex+1) + + switch rndMsg.message.Type() { + case message.TypeVote: + m := rndMsg.message.(*message.VoteMessage) + if m.Vote.Height() == height { + for _, cons := range instances { + cons.AddVote(m.Vote) + } + } + + case message.TypeProposal: + m := rndMsg.message.(*message.ProposalMessage) + if m.Proposal.Height() == height { + for _, cons := range instances { + cons.SetProposal(m.Proposal) + } + } + + case message.TypeQueryProposal: + for _, cons := range instances { + p := cons.Proposal() + if p != nil { + td.consMessages = append(td.consMessages, consMessage{ + sender: cons.valKey.Address(), + message: message.NewProposalMessage(p), + }) + } + } + case message.TypeQueryVote: + // To make the test reproducible, we ignore the QueryVote message. + // This is because QueryVote returns a random vote that can make the test non-reproducible. + + case message.TypeBlockAnnounce: + m := rndMsg.message.(*message.BlockAnnounceMessage) + blockAnnounces[rndMsg.sender] = m + + case + message.TypeHello, + message.TypeHelloAck, + message.TypeTransaction, + message.TypeBlocksRequest, + message.TypeBlocksResponse: + // + } + + for _, cons := range instances { + rnd := td.RandInt(100) + if rnd < changeProposerChance || + len(td.consMessages) == 0 { + td.changeProposerTimeout(cons) + } + } + changeProposerChance -= 5 + } + + // Verify whether more than (3t+1) nodes have committed to the same block. + if len(blockAnnounces) >= 4 { + var firstAnnounce *message.BlockAnnounceMessage + for _, msg := range blockAnnounces { + if firstAnnounce == nil { + firstAnnounce = msg + } else if msg.Block.Hash() != firstAnnounce.Block.Hash() { + return nil, fmt.Errorf("consensus violated, seed %v", td.TestSuite.Seed) + } + } + + // everything is ok + return firstAnnounce.Certificate, nil + } + + return nil, fmt.Errorf("unable to reach consensus, seed %v", td.TestSuite.Seed) +} diff --git a/fastconsensus/cp.go b/fastconsensus/cp.go new file mode 100644 index 000000000..ee5ef01a1 --- /dev/null +++ b/fastconsensus/cp.go @@ -0,0 +1,331 @@ +package fastconsensus + +import ( + "fmt" + + "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/types/proposal" + "github.com/pactus-project/pactus/types/vote" +) + +type changeProposer struct { + *consensus +} + +func (*changeProposer) onSetProposal(_ *proposal.Proposal) { + // Ignore proposal +} + +func (cp *changeProposer) onTimeout(t *ticker) { + if t.Target == tickerTargetQueryVotes { + cp.queryVotes() + cp.scheduleTimeout(t.Duration*2, cp.height, cp.round, tickerTargetQueryVotes) + } +} + +func (*changeProposer) cpCheckCPValue(value vote.CPValue, allowedValues ...vote.CPValue) error { + for _, v := range allowedValues { + if value == v { + return nil + } + } + + return invalidJustificationError{ + Reason: fmt.Sprintf("invalid value: %v", value), + } +} + +func (cp *changeProposer) cpCheckJustInitNo(just vote.Just, + blockHash hash.Hash, cpRound int16, cpValue vote.CPValue, +) error { + j, ok := just.(*vote.JustInitNo) + if !ok { + return invalidJustificationError{ + Reason: "invalid just data", + } + } + + if cpRound != 0 { + return invalidJustificationError{ + Reason: fmt.Sprintf("invalid round: %v", cpRound), + } + } + + err := cp.cpCheckCPValue(cpValue, vote.CPValueNo) + if err != nil { + return err + } + + err = j.QCert.ValidatePrepare(cp.validators, blockHash) + if err != nil { + return err + } + + return nil +} + +func (cp *changeProposer) cpCheckJustInitYes(just vote.Just, + blockHash hash.Hash, cpRound int16, cpValue vote.CPValue, +) error { + _, ok := just.(*vote.JustInitYes) + if !ok { + return invalidJustificationError{ + Reason: "invalid just data", + } + } + + if cpRound != 0 { + return invalidJustificationError{ + Reason: fmt.Sprintf("invalid round: %v", cpRound), + } + } + + err := cp.cpCheckCPValue(cpValue, vote.CPValueYes) + if err != nil { + return err + } + + if !blockHash.IsUndef() { + return invalidJustificationError{ + Reason: fmt.Sprintf("invalid block hash: %s", blockHash), + } + } + + return nil +} + +func (cp *changeProposer) cpCheckJustPreVoteHard(just vote.Just, + blockHash hash.Hash, cpRound int16, cpValue vote.CPValue, +) error { + j, ok := just.(*vote.JustPreVoteHard) + if !ok { + return invalidJustificationError{ + Reason: "invalid just data", + } + } + + if cpRound == 0 { + return invalidJustificationError{ + Reason: "invalid round: 0", + } + } + + err := cp.cpCheckCPValue(cpValue, vote.CPValueNo, vote.CPValueYes) + if err != nil { + return err + } + + err = j.QCert.ValidateCPPreVote(cp.validators, + blockHash, cpRound-1, byte(cpValue)) + if err != nil { + return invalidJustificationError{ + Reason: err.Error(), + } + } + + return nil +} + +func (cp *changeProposer) cpCheckJustPreVoteSoft(just vote.Just, + blockHash hash.Hash, cpRound int16, cpValue vote.CPValue, +) error { + j, ok := just.(*vote.JustPreVoteSoft) + if !ok { + return invalidJustificationError{ + Reason: "invalid just data", + } + } + + if cpRound == 0 { + return invalidJustificationError{ + Reason: "invalid round: 0", + } + } + + err := cp.cpCheckCPValue(cpValue, vote.CPValueNo, vote.CPValueYes) + if err != nil { + return err + } + + err = j.QCert.ValidateCPMainVote(cp.validators, + blockHash, cpRound-1, byte(vote.CPValueAbstain)) + if err != nil { + return invalidJustificationError{ + Reason: err.Error(), + } + } + + return nil +} + +func (cp *changeProposer) cpCheckJustMainVoteNoConflict(just vote.Just, + blockHash hash.Hash, cpRound int16, cpValue vote.CPValue, +) error { + j, ok := just.(*vote.JustMainVoteNoConflict) + if !ok { + return invalidJustificationError{ + Reason: "invalid just data", + } + } + err := cp.cpCheckCPValue(cpValue, vote.CPValueNo, vote.CPValueYes) + if err != nil { + return err + } + + err = j.QCert.ValidateCPPreVote(cp.validators, + blockHash, cpRound, byte(cpValue)) + if err != nil { + return invalidJustificationError{ + Reason: err.Error(), + } + } + + return nil +} + +func (cp *changeProposer) cpCheckJustMainVoteConflict(just vote.Just, + blockHash hash.Hash, cpRound int16, cpValue vote.CPValue, +) error { + j, ok := just.(*vote.JustMainVoteConflict) + if !ok { + return invalidJustificationError{ + Reason: "invalid just data", + } + } + + err := cp.cpCheckCPValue(cpValue, vote.CPValueAbstain) + if err != nil { + return err + } + + switch j.JustNo.Type() { + case vote.JustTypeInitNo: + err := cp.cpCheckJustInitNo(j.JustNo, blockHash, cpRound, vote.CPValueNo) + if err != nil { + return err + } + case vote.JustTypePreVoteHard: + err := cp.cpCheckJustPreVoteHard(j.JustNo, blockHash, cpRound, vote.CPValueNo) + if err != nil { + return err + } + case vote.JustTypePreVoteSoft: + err := cp.cpCheckJustPreVoteSoft(j.JustNo, blockHash, cpRound, vote.CPValueNo) + if err != nil { + return err + } + + case vote.JustTypeInitYes, + vote.JustTypeMainVoteConflict, + vote.JustTypeMainVoteNoConflict, + vote.JustTypeDecided: + return invalidJustificationError{ + Reason: fmt.Sprintf("unexpected justification: %s", j.JustNo.Type()), + } + } + + switch j.JustYes.Type() { + case vote.JustTypeInitYes: + err := cp.cpCheckJustInitYes(j.JustYes, hash.UndefHash, cpRound, vote.CPValueYes) + if err != nil { + return err + } + + case vote.JustTypePreVoteHard: + err := cp.cpCheckJustPreVoteHard(j.JustYes, hash.UndefHash, cpRound, vote.CPValueYes) + if err != nil { + return err + } + + case vote.JustTypeInitNo, + vote.JustTypePreVoteSoft, + vote.JustTypeMainVoteConflict, + vote.JustTypeMainVoteNoConflict, + vote.JustTypeDecided: + return invalidJustificationError{ + Reason: fmt.Sprintf("unexpected justification: %s", j.JustNo.Type()), + } + } + + return nil +} + +func (cp *changeProposer) cpCheckJustDecide(just vote.Just, + blockHash hash.Hash, cpRound int16, cpValue vote.CPValue, +) error { + j, ok := just.(*vote.JustDecided) + if !ok { + return invalidJustificationError{ + Reason: "invalid just data", + } + } + + err := cp.cpCheckCPValue(cpValue, vote.CPValueNo, vote.CPValueYes) + if err != nil { + return err + } + + err = j.QCert.ValidateCPMainVote(cp.validators, + blockHash, cpRound, byte(cpValue)) + if err != nil { + return invalidJustificationError{ + Reason: err.Error(), + } + } + + return nil +} + +func (cp *changeProposer) cpCheckJust(v *vote.Vote) error { + switch v.CPJust().Type() { + case vote.JustTypeInitYes: + return cp.cpCheckJustInitYes(v.CPJust(), + v.BlockHash(), v.CPRound(), v.CPValue()) + + case vote.JustTypeInitNo: + return cp.cpCheckJustInitNo(v.CPJust(), + v.BlockHash(), v.CPRound(), v.CPValue()) + + case vote.JustTypePreVoteSoft: + return cp.cpCheckJustPreVoteSoft(v.CPJust(), + v.BlockHash(), v.CPRound(), v.CPValue()) + + case vote.JustTypePreVoteHard: + return cp.cpCheckJustPreVoteHard(v.CPJust(), + v.BlockHash(), v.CPRound(), v.CPValue()) + + case vote.JustTypeMainVoteNoConflict: + return cp.cpCheckJustMainVoteNoConflict(v.CPJust(), + v.BlockHash(), v.CPRound(), v.CPValue()) + + case vote.JustTypeMainVoteConflict: + return cp.cpCheckJustMainVoteConflict(v.CPJust(), + v.BlockHash(), v.CPRound(), v.CPValue()) + + case vote.JustTypeDecided: + return cp.cpCheckJustDecide(v.CPJust(), + v.BlockHash(), v.CPRound(), v.CPValue()) + + default: + panic("unreachable") + } +} + +// cpStrongTermination decides if the Change Proposer phase should be terminated. +// If there is only one proper and justified `decided` vote, the validators can +// move on to the next phase. +// If the decided vote is for "No," then validators move to the precommit step and +// wait for committing the current proposal by gathering enough precommit votes. +// If the decided vote is for "Yes," then the validator moves to the propose step +// and starts a new round. +func (cp *changeProposer) cpStrongTermination() { + cpDecided := cp.log.CPDecidedVoteVoteSet(cp.round) + if cpDecided.HasAnyVoteFor(cp.cpRound, vote.CPValueNo) { + cp.cpDecided = 0 + cp.enterNewState(cp.precommitState) + } else if cpDecided.HasAnyVoteFor(cp.cpRound, vote.CPValueYes) { + cp.round++ + cp.cpDecided = 1 + cp.enterNewState(cp.proposeState) + } +} diff --git a/fastconsensus/cp_decide.go b/fastconsensus/cp_decide.go new file mode 100644 index 000000000..53abbb51a --- /dev/null +++ b/fastconsensus/cp_decide.go @@ -0,0 +1,59 @@ +package fastconsensus + +import ( + "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/types/vote" +) + +type cpDecideState struct { + *changeProposer +} + +func (s *cpDecideState) enter() { + s.decide() +} + +func (s *cpDecideState) decide() { + s.strongCommit() + s.cpStrongTermination() + + cpMainVotes := s.log.CPMainVoteVoteSet(s.round) + if cpMainVotes.HasTwoFPlusOneVotes(s.cpRound) { + if cpMainVotes.HasTwoFPlusOneVotesFor(s.cpRound, vote.CPValueYes) { + // decided for yes, and proceeds to the next round + s.logger.Info("binary agreement decided", "value", "yes", "round", s.cpRound) + + votes := cpMainVotes.BinaryVotes(s.cpRound, vote.CPValueYes) + cert := s.makeVoteCertificate(votes) + just := &vote.JustDecided{ + QCert: cert, + } + s.signAddCPDecidedVote(hash.UndefHash, s.cpRound, vote.CPValueYes, just) + s.cpStrongTermination() + } else if cpMainVotes.HasTwoFPlusOneVotesFor(s.cpRound, vote.CPValueNo) { + // decided for no and proceeds to the next round + s.logger.Info("binary agreement decided", "value", "no", "round", s.cpRound) + + votes := cpMainVotes.BinaryVotes(s.cpRound, vote.CPValueNo) + cert := s.makeVoteCertificate(votes) + just := &vote.JustDecided{ + QCert: cert, + } + s.signAddCPDecidedVote(*s.cpWeakValidity, s.cpRound, vote.CPValueNo, just) + s.cpStrongTermination() + } else { + // conflicting votes + s.logger.Debug("conflicting main votes", "round", s.cpRound) + s.cpRound++ + s.enterNewState(s.cpPreVoteState) + } + } +} + +func (s *cpDecideState) onAddVote(_ *vote.Vote) { + s.decide() +} + +func (*cpDecideState) name() string { + return "cp:decide" +} diff --git a/fastconsensus/cp_mainvote.go b/fastconsensus/cp_mainvote.go new file mode 100644 index 000000000..602762501 --- /dev/null +++ b/fastconsensus/cp_mainvote.go @@ -0,0 +1,103 @@ +package fastconsensus + +import ( + "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/types/proposal" + "github.com/pactus-project/pactus/types/vote" +) + +type cpMainVoteState struct { + *changeProposer +} + +func (s *cpMainVoteState) enter() { + s.decide() +} + +func (s *cpMainVoteState) decide() { + s.strongCommit() + s.cpStrongTermination() + s.checkForWeakValidity() + s.detectByzantineProposal() + + cpPreVotes := s.log.CPPreVoteVoteSet(s.round) + if cpPreVotes.HasTwoFPlusOneVotes(s.cpRound) { + if cpPreVotes.HasTwoFPlusOneVotesFor(s.cpRound, vote.CPValueYes) { + s.logger.Debug("cp: quorum for pre-votes", "value", "yes") + + votes := cpPreVotes.BinaryVotes(s.cpRound, vote.CPValueYes) + cert := s.makeVoteCertificate(votes) + just := &vote.JustMainVoteNoConflict{ + QCert: cert, + } + s.signAddCPMainVote(hash.UndefHash, s.cpRound, vote.CPValueYes, just) + s.enterNewState(s.cpDecideState) + } else if cpPreVotes.HasTwoFPlusOneVotesFor(s.cpRound, vote.CPValueNo) { + s.logger.Debug("cp: quorum for pre-votes", "value", "no") + + votes := cpPreVotes.BinaryVotes(s.cpRound, vote.CPValueNo) + cert := s.makeVoteCertificate(votes) + just := &vote.JustMainVoteNoConflict{ + QCert: cert, + } + s.signAddCPMainVote(*s.cpWeakValidity, s.cpRound, vote.CPValueNo, just) + s.enterNewState(s.cpDecideState) + } else { + s.logger.Debug("cp: no-quorum for pre-votes", "value", "abstain") + + vote0 := cpPreVotes.GetRandomVote(s.cpRound, vote.CPValueNo) + vote1 := cpPreVotes.GetRandomVote(s.cpRound, vote.CPValueYes) + + just := &vote.JustMainVoteConflict{ + JustNo: vote0.CPJust(), + JustYes: vote1.CPJust(), + } + + s.signAddCPMainVote(*s.cpWeakValidity, s.cpRound, vote.CPValueAbstain, just) + s.enterNewState(s.cpDecideState) + } + } +} + +func (s *cpMainVoteState) checkForWeakValidity() { + if s.cpWeakValidity == nil { + preVotes := s.log.CPPreVoteVoteSet(s.round) + randVote := preVotes.GetRandomVote(s.cpRound, vote.CPValueNo) + if randVote != nil { + bh := randVote.BlockHash() + s.cpWeakValidity = &bh + } + } +} + +func (s *cpMainVoteState) detectByzantineProposal() { + if s.cpWeakValidity != nil { + roundProposal := s.log.RoundProposal(s.round) + + if roundProposal != nil && + roundProposal.Block().Hash() != *s.cpWeakValidity { + s.logger.Warn("double proposal detected", + "prepared", s.cpWeakValidity, + "roundProposal", roundProposal.Block().Hash()) + + s.log.SetRoundProposal(s.round, nil) + s.queryProposal() + } + } +} + +func (s *cpMainVoteState) onAddVote(_ *vote.Vote) { + s.decide() +} + +func (*cpMainVoteState) onSetProposal(_ *proposal.Proposal) { + // Ignore proposal +} + +func (*cpMainVoteState) onTimeout(_ *ticker) { + // Ignore timeouts +} + +func (*cpMainVoteState) name() string { + return "cp:main-vote" +} diff --git a/fastconsensus/cp_prevote.go b/fastconsensus/cp_prevote.go new file mode 100644 index 000000000..0ed758092 --- /dev/null +++ b/fastconsensus/cp_prevote.go @@ -0,0 +1,102 @@ +package fastconsensus + +import ( + "time" + + "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/types/proposal" + "github.com/pactus-project/pactus/types/vote" +) + +var queryVoteInitialTimeout = 2 * time.Second + +type cpPreVoteState struct { + *changeProposer +} + +func (s *cpPreVoteState) enter() { + s.decide() +} + +//nolint:nestif // complexity can't be reduced more. +func (s *cpPreVoteState) decide() { + s.strongCommit() + s.cpStrongTermination() + + if s.cpRound == 0 { + // broadcast the initial value + prepares := s.log.PrepareVoteSet(s.round) + prepareQH := prepares.QuorumHash() + if prepareQH != nil { + s.cpWeakValidity = prepareQH + votes := prepares.BlockVotes(*prepareQH) + cert := s.makeVoteCertificate(votes) + just := &vote.JustInitNo{ + QCert: cert, + } + s.signAddCPPreVote(*s.cpWeakValidity, s.cpRound, 0, just) + } else { + if prepares.HasVoted(s.valKey.Address()) { + preVotes := s.log.CPPreVoteVoteSet(s.round) + if !preVotes.HasFPlusOneVotesFor(s.cpRound, vote.CPValueYes) { + s.logger.Debug("we have proposal but not minority of pre-votes for 'Yes'") + + return + } + } + just := &vote.JustInitYes{} + s.signAddCPPreVote(hash.UndefHash, s.cpRound, 1, just) + } + s.scheduleTimeout(queryVoteInitialTimeout, s.height, s.round, tickerTargetQueryVotes) + } else { + cpMainVotes := s.log.CPMainVoteVoteSet(s.round) + switch { + case cpMainVotes.HasAnyVoteFor(s.cpRound-1, vote.CPValueYes): + s.logger.Debug("cp: one main-vote for one", "b", "1") + + vote1 := cpMainVotes.GetRandomVote(s.cpRound-1, vote.CPValueYes) + just1 := &vote.JustPreVoteHard{ + QCert: vote1.CPJust().(*vote.JustMainVoteNoConflict).QCert, + } + s.signAddCPPreVote(hash.UndefHash, s.cpRound, vote.CPValueYes, just1) + + case cpMainVotes.HasAnyVoteFor(s.cpRound-1, vote.CPValueNo): + s.logger.Debug("cp: one main-vote for zero", "b", "0") + + vote0 := cpMainVotes.GetRandomVote(s.cpRound-1, vote.CPValueNo) + just0 := &vote.JustPreVoteHard{ + QCert: vote0.CPJust().(*vote.JustMainVoteNoConflict).QCert, + } + s.signAddCPPreVote(*s.cpWeakValidity, s.cpRound, vote.CPValueNo, just0) + + case cpMainVotes.HasAllVotesFor(s.cpRound-1, vote.CPValueAbstain): + s.logger.Debug("cp: all main-votes are abstain", "b", "0 (biased)") + + votes := cpMainVotes.BinaryVotes(s.cpRound-1, vote.CPValueAbstain) + cert := s.makeVoteCertificate(votes) + just := &vote.JustPreVoteSoft{ + QCert: cert, + } + s.signAddCPPreVote(*s.cpWeakValidity, s.cpRound, vote.CPValueNo, just) + + default: + s.logger.Panic("protocol violated. We have combination of votes for one and zero") + } + } + + s.enterNewState(s.cpMainVoteState) +} + +func (s *cpPreVoteState) onAddVote(_ *vote.Vote) { + s.decide() +} + +func (*cpPreVoteState) onSetProposal(_ *proposal.Proposal) { +} + +func (*cpPreVoteState) onTimeout(_ *ticker) { +} + +func (*cpPreVoteState) name() string { + return "cp:pre-vote" +} diff --git a/fastconsensus/cp_test.go b/fastconsensus/cp_test.go new file mode 100644 index 000000000..377b6fed3 --- /dev/null +++ b/fastconsensus/cp_test.go @@ -0,0 +1,425 @@ +package fastconsensus + +import ( + "fmt" + "testing" + + "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/types/vote" + "github.com/stretchr/testify/assert" +) + +func TestChangeProposer(t *testing.T) { + td := setup(t) + + td.enterNewHeight(td.consP) + td.changeProposerTimeout(td.consP) + + td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, hash.UndefHash) +} + +func TestSetProposalAfterChangeProposer(t *testing.T) { + td := setup(t) + + td.commitBlockForAllStates(t) + + td.enterNewHeight(td.consP) + td.changeProposerTimeout(td.consP) + + td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, hash.UndefHash) + + prop := td.makeProposal(t, 2, 0) + td.consP.SetProposal(prop) + assert.NotNil(t, td.consP.Proposal()) +} + +func TestChangeProposerAgreement1(t *testing.T) { + td := setup(t) + + h := uint32(1) + r := int16(0) + td.enterNewHeight(td.consP) + td.checkHeightRound(t, td.consP, h, r) + + td.changeProposerTimeout(td.consP) + + preVote0 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, hash.UndefHash) + td.addCPPreVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, preVote0.CPJust(), tIndexX) + td.addCPPreVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, preVote0.CPJust(), tIndexY) + + mainVote0 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPMainVote, hash.UndefHash) + td.addCPMainVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, mainVote0.CPJust(), tIndexX) + td.addCPMainVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, mainVote0.CPJust(), tIndexY) + + td.shouldPublishVote(t, td.consP, vote.VoteTypeCPDecided, hash.UndefHash) + td.checkHeightRound(t, td.consP, h, r+1) +} + +func TestChangeProposerAgreement0(t *testing.T) { + td := setup(t) + + td.commitBlockForAllStates(t) // height 1 + + h := uint32(2) + r := int16(1) + td.enterNewHeight(td.consP) + td.enterNextRound(td.consP) + td.checkHeightRound(t, td.consP, h, r) + + prop := td.makeProposal(t, h, r) + blockHash := prop.Block().Hash() + + td.consP.SetProposal(prop) + td.addPrepareVote(td.consP, blockHash, h, r, tIndexX) + td.addPrepareVote(td.consP, blockHash, h, r, tIndexY) + td.addPrepareVote(td.consP, blockHash, h, r, tIndexM) + + td.changeProposerTimeout(td.consP) + + preVote0 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, blockHash) + td.addCPPreVote(td.consP, blockHash, h, r, vote.CPValueNo, preVote0.CPJust(), tIndexX) + td.addCPPreVote(td.consP, blockHash, h, r, vote.CPValueNo, preVote0.CPJust(), tIndexY) + + mainVote0 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPMainVote, blockHash) + td.addCPMainVote(td.consP, blockHash, h, r, vote.CPValueNo, mainVote0.CPJust(), tIndexX) + td.addCPMainVote(td.consP, blockHash, h, r, vote.CPValueNo, mainVote0.CPJust(), tIndexY) + + td.shouldPublishVote(t, td.consP, vote.VoteTypeCPDecided, blockHash) + td.addPrecommitVote(td.consP, blockHash, h, r, tIndexX) + td.addPrecommitVote(td.consP, blockHash, h, r, tIndexY) + td.checkHeightRound(t, td.consP, h, r) +} + +// ConsP receives all PRE-VOTE:0 votes before receiving a proposal or prepare votes. +// It should vote PRE-VOTES:1 and MAIN-VOTE:0. +func TestCrashOnTestnet(t *testing.T) { + td := setup(t) + + td.commitBlockForAllStates(t) // height 1 + + h := uint32(2) + r := int16(0) + td.consP.MoveToNewHeight() + + blockHash := td.RandHash() + v1 := vote.NewPrepareVote(blockHash, h, r, td.consX.valKey.Address()) + v2 := vote.NewPrepareVote(blockHash, h, r, td.consY.valKey.Address()) + v3 := vote.NewPrepareVote(blockHash, h, r, td.consB.valKey.Address()) + + td.HelperSignVote(td.consX.valKey, v1) + td.HelperSignVote(td.consY.valKey, v2) + td.HelperSignVote(td.consB.valKey, v3) + + votes := map[crypto.Address]*vote.Vote{} + votes[v1.Signer()] = v1 + votes[v2.Signer()] = v2 + votes[v3.Signer()] = v3 + + cert := td.consP.makeVoteCertificate(votes) + just0 := &vote.JustInitNo{QCert: cert} + td.addCPPreVote(td.consP, blockHash, h, r, vote.CPValueNo, just0, tIndexX) + td.addCPPreVote(td.consP, blockHash, h, r, vote.CPValueNo, just0, tIndexY) + td.addCPPreVote(td.consP, blockHash, h, r, vote.CPValueNo, just0, tIndexB) + + td.newHeightTimeout(td.consP) + td.changeProposerTimeout(td.consP) + + preVote := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, hash.UndefHash) + mainVote := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPMainVote, blockHash) + assert.Equal(t, vote.CPValueYes, preVote.CPValue()) + assert.Equal(t, vote.CPValueNo, mainVote.CPValue()) +} + +func TestInvalidJustInitYes(t *testing.T) { + td := setup(t) + + td.enterNewHeight(td.consX) + h := uint32(1) + r := int16(0) + just := &vote.JustInitYes{} + + t.Run("invalid value: no", func(t *testing.T) { + v := vote.NewCPPreVote(hash.UndefHash, h, r, 0, vote.CPValueNo, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: "invalid value: no", + }) + }) + + t.Run("cp-round should be 0", func(t *testing.T) { + v := vote.NewCPPreVote(hash.UndefHash, h, r, 1, vote.CPValueYes, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: "invalid round: 1", + }) + }) + + t.Run("invalid block hash", func(t *testing.T) { + blockHash := td.RandHash() + v := vote.NewCPPreVote(blockHash, h, r, 0, vote.CPValueYes, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: "invalid block hash: " + blockHash.String(), + }) + }) +} + +func TestInvalidJustInitNo(t *testing.T) { + td := setup(t) + + td.enterNewHeight(td.consX) + h := uint32(1) + r := int16(0) + just := &vote.JustInitNo{ + QCert: td.GenerateTestPrepareCertificate(h), + } + + t.Run("invalid value: yes", func(t *testing.T) { + v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueYes, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: "invalid value: yes", + }) + }) + + t.Run("cp-round should be 0", func(t *testing.T) { + v := vote.NewCPPreVote(td.RandHash(), h, r, 1, vote.CPValueNo, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: "invalid round: 1", + }) + }) + + t.Run("invalid certificate", func(t *testing.T) { + v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueNo, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.Error(t, err) + }) +} + +func TestInvalidJustPreVoteHard(t *testing.T) { + td := setup(t) + + td.enterNewHeight(td.consX) + h := uint32(1) + r := int16(0) + just := &vote.JustPreVoteHard{ + QCert: td.GenerateTestPrepareCertificate(h), + } + + t.Run("invalid value: abstain", func(t *testing.T) { + v := vote.NewCPPreVote(td.RandHash(), h, r, 1, vote.CPValueAbstain, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: "invalid value: abstain", + }) + }) + + t.Run("cp-round should not be 0", func(t *testing.T) { + v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueNo, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: "invalid round: 0", + }) + }) + + t.Run("invalid certificate", func(t *testing.T) { + v := vote.NewCPPreVote(td.RandHash(), h, r, 1, vote.CPValueNo, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just.QCert.Committers()), + }) + }) +} + +func TestInvalidJustPreVoteSoft(t *testing.T) { + td := setup(t) + + td.enterNewHeight(td.consX) + h := uint32(1) + r := int16(0) + just := &vote.JustPreVoteSoft{ + QCert: td.GenerateTestPrepareCertificate(h), + } + + t.Run("invalid value: abstain", func(t *testing.T) { + v := vote.NewCPPreVote(td.RandHash(), h, r, 1, vote.CPValueAbstain, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: "invalid value: abstain", + }) + }) + + t.Run("cp-round should not be 0", func(t *testing.T) { + v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueNo, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: "invalid round: 0", + }) + }) + + t.Run("invalid certificate", func(t *testing.T) { + v := vote.NewCPPreVote(td.RandHash(), h, r, 1, vote.CPValueNo, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just.QCert.Committers()), + }) + }) +} + +func TestInvalidJustMainVoteNoConflict(t *testing.T) { + td := setup(t) + + td.enterNewHeight(td.consX) + h := uint32(1) + r := int16(0) + just := &vote.JustMainVoteNoConflict{ + QCert: td.GenerateTestPrepareCertificate(h), + } + + t.Run("invalid value: abstain", func(t *testing.T) { + v := vote.NewCPMainVote(td.RandHash(), h, r, 1, vote.CPValueAbstain, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: "invalid value: abstain", + }) + }) + + t.Run("invalid certificate", func(t *testing.T) { + v := vote.NewCPMainVote(td.RandHash(), h, r, 1, vote.CPValueNo, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just.QCert.Committers()), + }) + }) +} + +func TestInvalidJustMainVoteConflict(t *testing.T) { + td := setup(t) + + td.enterNewHeight(td.consX) + h := uint32(1) + r := int16(0) + + t.Run("invalid value: no", func(t *testing.T) { + just := &vote.JustMainVoteConflict{ + JustNo: &vote.JustInitNo{ + QCert: td.GenerateTestPrepareCertificate(h), + }, + JustYes: &vote.JustInitYes{}, + } + v := vote.NewCPMainVote(td.RandHash(), h, r, 0, vote.CPValueNo, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: "invalid value: no", + }) + }) + + t.Run("invalid value: yes", func(t *testing.T) { + just := &vote.JustMainVoteConflict{ + JustNo: &vote.JustInitNo{ + QCert: td.GenerateTestPrepareCertificate(h), + }, + JustYes: &vote.JustInitYes{}, + } + v := vote.NewCPMainVote(td.RandHash(), h, r, 0, vote.CPValueYes, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: "invalid value: yes", + }) + }) + + t.Run("invalid value: unexpected justification (justNo)", func(t *testing.T) { + just := &vote.JustMainVoteConflict{ + JustNo: &vote.JustInitYes{}, + JustYes: &vote.JustInitYes{}, + } + v := vote.NewCPMainVote(td.RandHash(), h, r, 0, vote.CPValueAbstain, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: "unexpected justification: JustInitYes", + }) + }) + + t.Run("invalid value: unexpected justification", func(t *testing.T) { + just := &vote.JustMainVoteConflict{ + JustNo: &vote.JustInitNo{ + QCert: td.GenerateTestPrepareCertificate(h), + }, + JustYes: &vote.JustPreVoteSoft{ + QCert: td.GenerateTestPrepareCertificate(h), + }, + } + v := vote.NewCPMainVote(td.RandHash(), h, r, 1, vote.CPValueAbstain, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: "invalid round: 1", + }) + }) + + t.Run("invalid certificate", func(t *testing.T) { + just0 := &vote.JustPreVoteSoft{ + QCert: td.GenerateTestPrepareCertificate(h), + } + just := &vote.JustMainVoteConflict{ + JustNo: just0, + JustYes: &vote.JustPreVoteSoft{ + QCert: td.GenerateTestPrepareCertificate(h), + }, + } + v := vote.NewCPMainVote(td.RandHash(), h, r, 1, vote.CPValueAbstain, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just0.QCert.Committers()), + }) + }) +} + +func TestInvalidJustDecided(t *testing.T) { + td := setup(t) + + td.enterNewHeight(td.consX) + h := uint32(1) + r := int16(0) + just := &vote.JustDecided{ + QCert: td.GenerateTestPrepareCertificate(h), + } + + t.Run("invalid value: abstain", func(t *testing.T) { + v := vote.NewCPDecidedVote(td.RandHash(), h, r, 0, vote.CPValueAbstain, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: "invalid value: abstain", + }) + }) + + t.Run("invalid certificate", func(t *testing.T) { + v := vote.NewCPDecidedVote(td.RandHash(), h, r, 0, vote.CPValueYes, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just.QCert.Committers()), + }) + }) +} diff --git a/fastconsensus/errors.go b/fastconsensus/errors.go new file mode 100644 index 000000000..023c20558 --- /dev/null +++ b/fastconsensus/errors.go @@ -0,0 +1,24 @@ +package fastconsensus + +import ( + "fmt" +) + +// invalidJustificationError is returned when the justification for a change-proposer +// vote is invalid. +type invalidJustificationError struct { + Reason string +} + +func (e invalidJustificationError) Error() string { + return fmt.Sprintf("invalid justification: %s", e.Reason) +} + +// ConfigError is returned when the config is not valid with a descriptive Reason message. +type ConfigError struct { + Reason string +} + +func (e ConfigError) Error() string { + return e.Reason +} diff --git a/fastconsensus/height.go b/fastconsensus/height.go new file mode 100644 index 000000000..c5a4012d2 --- /dev/null +++ b/fastconsensus/height.go @@ -0,0 +1,60 @@ +package fastconsensus + +import ( + "github.com/pactus-project/pactus/types/proposal" + "github.com/pactus-project/pactus/types/vote" + "github.com/pactus-project/pactus/util" +) + +type newHeightState struct { + *consensus +} + +func (s *newHeightState) enter() { + s.decide() +} + +func (s *newHeightState) decide() { + sateHeight := s.bcState.LastBlockHeight() + + validators := s.bcState.CommitteeValidators() + s.log.MoveToNewHeight(validators) + + s.validators = validators + s.height = sateHeight + 1 + s.round = 0 + s.blockCert = nil + s.active = s.bcState.IsInCommittee(s.valKey.Address()) + s.logger.Info("entering new height", "height", s.height, "active", s.active) + + sleep := s.bcState.LastBlockTime().Add(s.bcState.Params().BlockInterval()).Sub(util.Now()) + s.scheduleTimeout(sleep, s.height, s.round, tickerTargetNewHeight) +} + +func (s *newHeightState) onAddVote(_ *vote.Vote) { + prepares := s.log.PrepareVoteSet(s.round) + if prepares.HasQuorumHash() { + // Added logic to detect when the network majority has voted for a block, + // but the new height timer has not yet started. This situation can occur if the system + // time is lagging behind the network time. + s.logger.Warn("detected network majority voting for a block, but the new height timer has not started yet. " + + "system time may be behind the network.") + s.enterNewState(s.proposeState) + } +} + +func (*newHeightState) onSetProposal(_ *proposal.Proposal) { + // Ignore proposal +} + +func (s *newHeightState) onTimeout(t *ticker) { + if t.Target == tickerTargetNewHeight { + if s.active { + s.enterNewState(s.proposeState) + } + } +} + +func (*newHeightState) name() string { + return "new-height" +} diff --git a/fastconsensus/height_test.go b/fastconsensus/height_test.go new file mode 100644 index 000000000..1085d376d --- /dev/null +++ b/fastconsensus/height_test.go @@ -0,0 +1,63 @@ +package fastconsensus + +import ( + "testing" + + "github.com/pactus-project/pactus/types/vote" + "github.com/stretchr/testify/assert" +) + +func TestNewHeightTimeout(t *testing.T) { + td := setup(t) + + td.enterNewHeight(td.consY) + td.commitBlockForAllStates(t) + + s := &newHeightState{td.consY} + s.enter() + + // Invalid target + s.onTimeout(&ticker{Height: 2, Target: -1}) + td.checkHeightRound(t, td.consY, 2, 0) + + s.onTimeout(&ticker{Height: 2, Target: tickerTargetNewHeight}) + td.checkHeightRound(t, td.consY, 2, 0) + td.shouldPublishProposal(t, td.consY, 2, 0) +} + +func TestNewHeightDoubleEntry(t *testing.T) { + td := setup(t) + + td.commitBlockForAllStates(t) + + td.consX.MoveToNewHeight() + td.newHeightTimeout(td.consX) + + // double entry and timeout + td.consX.MoveToNewHeight() + + td.checkHeightRound(t, td.consX, 2, 0) + assert.True(t, td.consX.active) + assert.NotEqual(t, td.consX.currentState.name(), "new-height") +} + +func TestNewHeightTimeBehindNetwork(t *testing.T) { + td := setup(t) + + td.commitBlockForAllStates(t) + td.consP.MoveToNewHeight() + + h := uint32(2) + r := int16(0) + p := td.makeProposal(t, h, r) + blockHash := p.Block().Hash() + + td.consP.SetProposal(p) + td.addPrepareVote(td.consP, blockHash, h, r, tIndexX) + td.addPrepareVote(td.consP, blockHash, h, r, tIndexY) + td.addPrepareVote(td.consP, blockHash, h, r, tIndexM) + td.addPrepareVote(td.consP, blockHash, h, r, tIndexN) + + td.shouldPublishVote(t, td.consP, vote.VoteTypePrepare, blockHash) + td.shouldPublishBlockAnnounce(t, td.consP, blockHash) +} diff --git a/fastconsensus/interface.go b/fastconsensus/interface.go new file mode 100644 index 000000000..adae51b8e --- /dev/null +++ b/fastconsensus/interface.go @@ -0,0 +1,45 @@ +package fastconsensus + +import ( + "github.com/pactus-project/pactus/crypto/bls" + "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/types/proposal" + "github.com/pactus-project/pactus/types/vote" +) + +type Reader interface { + ConsensusKey() *bls.PublicKey + AllVotes() []*vote.Vote + PickRandomVote(round int16) *vote.Vote + Proposal() *proposal.Proposal + HasVote(h hash.Hash) bool + HeightRound() (uint32, int16) + IsActive() bool +} + +type Consensus interface { + Reader + + Start() + MoveToNewHeight() + AddVote(vte *vote.Vote) + SetProposal(prop *proposal.Proposal) +} + +type ManagerReader interface { + Instances() []Reader + PickRandomVote(round int16) *vote.Vote + Proposal() *proposal.Proposal + HeightRound() (uint32, int16) + HasActiveInstance() bool +} + +type Manager interface { + ManagerReader + + Start() error + Stop() + MoveToNewHeight() + AddVote(vot *vote.Vote) + SetProposal(prop *proposal.Proposal) +} diff --git a/fastconsensus/log/log.go b/fastconsensus/log/log.go new file mode 100644 index 000000000..56ddd3b0a --- /dev/null +++ b/fastconsensus/log/log.go @@ -0,0 +1,126 @@ +package log + +import ( + "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/fastconsensus/voteset" + "github.com/pactus-project/pactus/types/proposal" + "github.com/pactus-project/pactus/types/validator" + "github.com/pactus-project/pactus/types/vote" +) + +type Log struct { + validators map[crypto.Address]*validator.Validator + totalPower int64 + roundMessages map[int16]*Messages +} + +func NewLog() *Log { + return &Log{ + roundMessages: make(map[int16]*Messages, 0), + } +} + +func (log *Log) RoundMessages(round int16) *Messages { + return log.mustGetRoundMessages(round) +} + +func (log *Log) HasVote(h hash.Hash) bool { + for _, m := range log.roundMessages { + if m.HasVote(h) { + return true + } + } + + return false +} + +func (log *Log) mustGetRoundMessages(round int16) *Messages { + rm, ok := log.roundMessages[round] + if !ok { + rm = &Messages{ + prepareVotes: voteset.NewPrepareVoteSet(round, log.totalPower, log.validators), + precommitVotes: voteset.NewPrecommitVoteSet(round, log.totalPower, log.validators), + cpPreVotes: voteset.NewCPPreVoteVoteSet(round, log.totalPower, log.validators), + cpMainVotes: voteset.NewCPMainVoteVoteSet(round, log.totalPower, log.validators), + cpDecidedVotes: voteset.NewCPDecidedVoteVoteSet(round, log.totalPower, log.validators), + } + log.roundMessages[round] = rm + } + + return rm +} + +func (log *Log) AddVote(v *vote.Vote) (bool, error) { + m := log.mustGetRoundMessages(v.Round()) + + return m.addVote(v) +} + +func (log *Log) PrepareVoteSet(round int16) *voteset.BlockVoteSet { + m := log.mustGetRoundMessages(round) + + return m.prepareVotes +} + +func (log *Log) PrecommitVoteSet(round int16) *voteset.BlockVoteSet { + m := log.mustGetRoundMessages(round) + + return m.precommitVotes +} + +func (log *Log) CPPreVoteVoteSet(round int16) *voteset.BinaryVoteSet { + m := log.mustGetRoundMessages(round) + + return m.cpPreVotes +} + +func (log *Log) CPMainVoteVoteSet(round int16) *voteset.BinaryVoteSet { + m := log.mustGetRoundMessages(round) + + return m.cpMainVotes +} + +func (log *Log) CPDecidedVoteVoteSet(round int16) *voteset.BinaryVoteSet { + m := log.mustGetRoundMessages(round) + + return m.cpDecidedVotes +} + +func (log *Log) HasRoundProposal(round int16) bool { + return log.RoundProposal(round) != nil +} + +func (log *Log) RoundProposal(round int16) *proposal.Proposal { + m := log.RoundMessages(round) + if m == nil { + return nil + } + + return m.proposal +} + +func (log *Log) SetRoundProposal(round int16, prop *proposal.Proposal) { + m := log.mustGetRoundMessages(round) + m.proposal = prop +} + +func (log *Log) MoveToNewHeight(validators []*validator.Validator) { + log.roundMessages = make(map[int16]*Messages) + log.validators = make(map[crypto.Address]*validator.Validator) + log.totalPower = 0 + for _, val := range validators { + log.totalPower += val.Power() + log.validators[val.Address()] = val + } +} + +func (log *Log) CanVote(addr crypto.Address) bool { + for _, val := range log.validators { + if val.Address() == addr { + return true + } + } + + return false +} diff --git a/fastconsensus/log/log_test.go b/fastconsensus/log/log_test.go new file mode 100644 index 000000000..2cef01594 --- /dev/null +++ b/fastconsensus/log/log_test.go @@ -0,0 +1,105 @@ +package log + +import ( + "encoding/hex" + "testing" + + "github.com/pactus-project/pactus/types/vote" + "github.com/pactus-project/pactus/util/testsuite" + "github.com/stretchr/testify/assert" +) + +func TestMustGetRound(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + cmt, _ := ts.GenerateTestCommittee(6) + log := NewLog() + log.MoveToNewHeight(cmt.Validators()) + assert.NotNil(t, log.RoundMessages(ts.RandRound())) +} + +func TestAddValidVote(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + cmt, valKeys := ts.GenerateTestCommittee(6) + log := NewLog() + log.MoveToNewHeight(cmt.Validators()) + h := ts.RandHeight() + r := ts.RandRound() + + prepares := log.PrepareVoteSet(r) + precommits := log.PrecommitVoteSet(r) + preVotes := log.CPPreVoteVoteSet(r) + mainVotes := log.CPMainVoteVoteSet(r) + + v1 := vote.NewPrepareVote(ts.RandHash(), h, r, valKeys[0].Address()) + v2 := vote.NewPrecommitVote(ts.RandHash(), h, r, valKeys[0].Address()) + v3 := vote.NewCPPreVote(ts.RandHash(), h, r, 0, vote.CPValueYes, &vote.JustInitYes{}, valKeys[0].Address()) + v4 := vote.NewCPMainVote(ts.RandHash(), h, r, 0, vote.CPValueNo, &vote.JustInitYes{}, valKeys[0].Address()) + + for _, v := range []*vote.Vote{v1, v2, v3, v4} { + ts.HelperSignVote(valKeys[0], v) + + added, err := log.AddVote(v) + assert.NoError(t, err) + assert.True(t, added) + } + + assert.True(t, log.HasVote(v1.Hash())) + assert.True(t, log.HasVote(v2.Hash())) + assert.True(t, log.HasVote(v3.Hash())) + assert.True(t, log.HasVote(v4.Hash())) + assert.False(t, log.HasVote(ts.RandHash())) + + assert.Contains(t, prepares.AllVotes(), v1) + assert.Contains(t, precommits.AllVotes(), v2) + assert.Contains(t, preVotes.AllVotes(), v3) + assert.Contains(t, mainVotes.AllVotes(), v4) +} + +func TestAddInvalidVoteType(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + cmt, _ := ts.GenerateTestCommittee(6) + log := NewLog() + log.MoveToNewHeight(cmt.Validators()) + + data, _ := hex.DecodeString("A701050218320301045820BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" + + "055501AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA06f607f6") + invVote := new(vote.Vote) + err := invVote.UnmarshalCBOR(data) + assert.NoError(t, err) + + added, err := log.AddVote(invVote) + assert.Error(t, err) + assert.False(t, added) + assert.False(t, log.HasVote(invVote.Hash())) +} + +func TestSetRoundProposal(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + cmt, _ := ts.GenerateTestCommittee(6) + prop, _ := ts.GenerateTestProposal(101, 0) + log := NewLog() + log.MoveToNewHeight(cmt.Validators()) + log.SetRoundProposal(4, prop) + assert.False(t, log.HasRoundProposal(0)) + assert.True(t, log.HasRoundProposal(4)) + assert.True(t, log.HasRoundProposal(4)) + assert.Nil(t, log.RoundProposal(0)) + assert.Nil(t, log.RoundProposal(5)) + assert.Equal(t, log.RoundProposal(4).Hash(), prop.Hash()) +} + +func TestCanVote(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + cmt, valKeys := ts.GenerateTestCommittee(6) + log := NewLog() + log.MoveToNewHeight(cmt.Validators()) + + addr := ts.RandAccAddress() + assert.True(t, log.CanVote(valKeys[0].Address())) + assert.False(t, log.CanVote(addr)) +} diff --git a/fastconsensus/log/messages.go b/fastconsensus/log/messages.go new file mode 100644 index 000000000..c5cf5db13 --- /dev/null +++ b/fastconsensus/log/messages.go @@ -0,0 +1,58 @@ +package log + +import ( + "fmt" + + "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/fastconsensus/voteset" + "github.com/pactus-project/pactus/types/proposal" + "github.com/pactus-project/pactus/types/vote" +) + +type Messages struct { + prepareVotes *voteset.BlockVoteSet // Prepare votes + precommitVotes *voteset.BlockVoteSet // Precommit votes + cpPreVotes *voteset.BinaryVoteSet // Change proposer Pre-votes + cpMainVotes *voteset.BinaryVoteSet // Change proposer Main-votes + cpDecidedVotes *voteset.BinaryVoteSet // Change proposer Decided-votes + proposal *proposal.Proposal +} + +func (m *Messages) addVote(v *vote.Vote) (bool, error) { + switch v.Type() { + case vote.VoteTypePrepare: + return m.prepareVotes.AddVote(v) + case vote.VoteTypePrecommit: + return m.precommitVotes.AddVote(v) + case vote.VoteTypeCPPreVote: + return m.cpPreVotes.AddVote(v) + case vote.VoteTypeCPMainVote: + return m.cpMainVotes.AddVote(v) + case vote.VoteTypeCPDecided: + return m.cpDecidedVotes.AddVote(v) + } + + return false, fmt.Errorf("unexpected vote type: %v", v.Type()) +} + +func (m *Messages) HasVote(h hash.Hash) bool { + votes := m.AllVotes() + for _, v := range votes { + if v.Hash() == h { + return true + } + } + + return false +} + +func (m *Messages) AllVotes() []*vote.Vote { + votes := []*vote.Vote{} + votes = append(votes, m.prepareVotes.AllVotes()...) + votes = append(votes, m.precommitVotes.AllVotes()...) + votes = append(votes, m.cpPreVotes.AllVotes()...) + votes = append(votes, m.cpMainVotes.AllVotes()...) + votes = append(votes, m.cpDecidedVotes.AllVotes()...) + + return votes +} diff --git a/fastconsensus/manager.go b/fastconsensus/manager.go new file mode 100644 index 000000000..62f177fb0 --- /dev/null +++ b/fastconsensus/manager.go @@ -0,0 +1,208 @@ +package fastconsensus + +import ( + "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/crypto/bls" + "github.com/pactus-project/pactus/state" + "github.com/pactus-project/pactus/sync/bundle/message" + "github.com/pactus-project/pactus/types/proposal" + "github.com/pactus-project/pactus/types/vote" + "github.com/pactus-project/pactus/util/logger" + "golang.org/x/exp/slices" +) + +type manager struct { + instances []Consensus + + // Caching future votes and proposals due to potential server time misalignments. + // Votes and proposals for upcoming blocks may be received before + // the current block's consensus is complete. + upcomingVotes []*vote.Vote // Map to cache votes for future block heights + upcomingProposals []*proposal.Proposal // Map to cache proposals for future block heights + state state.Facade +} + +// NewManager creates a new manager instance that manages a set of consensus instances, +// each associated with a validator key and a reward address. +// It is not thread-safe. +func NewManager( + conf *Config, + st state.Facade, + valKeys []*bls.ValidatorKey, + rewardAddrs []crypto.Address, + broadcastCh chan message.Message, +) Manager { + mgr := &manager{ + instances: make([]Consensus, len(valKeys)), + upcomingVotes: make([]*vote.Vote, 0), + upcomingProposals: make([]*proposal.Proposal, 0), + state: st, + } + mediatorConcrete := newConcreteMediator() + + for i, key := range valKeys { + cons := NewConsensus(conf, st, key, rewardAddrs[i], broadcastCh, mediatorConcrete) + + mgr.instances[i] = cons + } + + return mgr +} + +// Start starts the manager. +func (mgr *manager) Start() error { + logger.Debug("starting consensus instances") + for _, cons := range mgr.instances { + cons.Start() + } + + return nil +} + +// Stop stops the manager. +func (*manager) Stop() { +} + +// Instances return all consensus instances that are read-only and +// can be safely accessed without modifying their state. +func (mgr *manager) Instances() []Reader { + readers := make([]Reader, len(mgr.instances)) + for i, cons := range mgr.instances { + readers[i] = cons + } + + return readers +} + +// PickRandomVote returns a random vote from a random consensus instance. +func (mgr *manager) PickRandomVote(round int16) *vote.Vote { + cons := mgr.getBestInstance() + + return cons.PickRandomVote(round) +} + +// Proposal returns the proposal for a specific round from a random consensus instance. +func (mgr *manager) Proposal() *proposal.Proposal { + cons := mgr.getBestInstance() + + return cons.Proposal() +} + +// HeightRound retrieves the current height and round from a random consensus instance. +func (mgr *manager) HeightRound() (uint32, int16) { + cons := mgr.getBestInstance() + + return cons.HeightRound() +} + +// HasActiveInstance checks if any of the consensus instances are currently active. +func (mgr *manager) HasActiveInstance() bool { + for _, cons := range mgr.instances { + if cons.IsActive() { + return true + } + } + + return false +} + +// MoveToNewHeight moves all consensus instances to a new height. +func (mgr *manager) MoveToNewHeight() { + for _, cons := range mgr.instances { + cons.MoveToNewHeight() + } + + inst := mgr.getBestInstance() + curHeight, _ := inst.HeightRound() + for i := len(mgr.upcomingProposals) - 1; i >= 0; i-- { + p := mgr.upcomingProposals[i] + switch { + case p.Height() < curHeight: + // Ignore old proposals + + case p.Height() > curHeight: + // keep this vote + continue + + case p.Height() == curHeight: + logger.Debug("upcoming proposal processed", "height", curHeight) + for _, cons := range mgr.instances { + cons.SetProposal(p) + } + } + + mgr.upcomingProposals = slices.Delete(mgr.upcomingProposals, i, i+1) + } + + for i := len(mgr.upcomingVotes) - 1; i >= 0; i-- { + v := mgr.upcomingVotes[i] + switch { + case v.Height() < curHeight: + // Ignore old votes + + case v.Height() > curHeight: + // keep this vote + continue + + case v.Height() == curHeight: + logger.Debug("upcoming votes processed", "height", curHeight) + for _, cons := range mgr.instances { + cons.AddVote(v) + } + } + + mgr.upcomingVotes = slices.Delete(mgr.upcomingVotes, i, i+1) + } +} + +// AddVote adds a vote to all consensus instances. +func (mgr *manager) AddVote(v *vote.Vote) { + inst := mgr.getBestInstance() + curHeight, _ := inst.HeightRound() + switch { + case v.Height() < curHeight: + _ = mgr.state.UpdateLastCertificate(v) + + case v.Height() > curHeight: + mgr.upcomingVotes = append(mgr.upcomingVotes, v) + + case v.Height() == curHeight: + for _, cons := range mgr.instances { + cons.AddVote(v) + } + } +} + +// SetProposal sets the proposal for all consensus instances. +func (mgr *manager) SetProposal(p *proposal.Proposal) { + inst := mgr.getBestInstance() + curHeight, _ := inst.HeightRound() + switch { + case p.Height() < curHeight: + // discard the old proposal + + case p.Height() > curHeight: + mgr.upcomingProposals = append(mgr.upcomingProposals, p) + + case p.Height() == curHeight: + for _, cons := range mgr.instances { + cons.SetProposal(p) + } + } +} + +// getBestInstance iterates through all consensus instances and returns the instance +// that is currently active, if there is one. +// If there are no active instances, it returns the first instance. +// +// Note that all active instances are assumed to be in the same state, and all inactive +// instances are assumed to be in the same state as well. +func (mgr *manager) getBestInstance() Consensus { + for _, cons := range mgr.instances { + if cons.IsActive() { + return cons + } + } + + return mgr.instances[0] +} diff --git a/fastconsensus/manager_test.go b/fastconsensus/manager_test.go new file mode 100644 index 000000000..3b6c69e89 --- /dev/null +++ b/fastconsensus/manager_test.go @@ -0,0 +1,195 @@ +package fastconsensus + +import ( + "testing" + + "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/crypto/bls" + "github.com/pactus-project/pactus/state" + "github.com/pactus-project/pactus/sync/bundle/message" + "github.com/pactus-project/pactus/types/proposal" + "github.com/pactus-project/pactus/types/vote" + "github.com/pactus-project/pactus/util/logger" + "github.com/pactus-project/pactus/util/testsuite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestManager(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + st := state.MockingState(ts) + st.TestCommittee.Validators() + + rewardAddrs := []crypto.Address{ts.RandAccAddress(), ts.RandAccAddress()} + valKeys := []*bls.ValidatorKey{st.TestValKeys[0], ts.RandValKey()} + broadcastCh := make(chan message.Message, 500) + + randomHeight := ts.RandHeight() + blk, cert := ts.GenerateTestBlock(randomHeight) + st.TestStore.SaveBlock(blk, cert) + + mgrInst := NewManager(testConfig(), st, valKeys, rewardAddrs, broadcastCh) + mgr := mgrInst.(*manager) + + consA := mgr.instances[0].(*consensus) // active + consB := mgr.instances[1].(*consensus) // inactive + + t.Run("Check if keys are assigned properly", func(t *testing.T) { + assert.Equal(t, valKeys[0].PublicKey(), consA.ConsensusKey()) + assert.Equal(t, valKeys[1].PublicKey(), consB.ConsensusKey()) + }) + + t.Run("Check if all instances move to new height", func(t *testing.T) { + stateHeight := mgr.state.LastBlockHeight() + assert.False(t, mgr.HasActiveInstance()) + + mgr.MoveToNewHeight() + consHeight, consRound := mgr.HeightRound() + + assert.True(t, mgr.HasActiveInstance()) + assert.Equal(t, consHeight, stateHeight+1) + assert.Zero(t, consRound) + }) + + t.Run("Testing add vote", func(t *testing.T) { + consHeight, _ := mgr.HeightRound() + v := vote.NewPrepareVote(ts.RandHash(), consHeight, 0, valKeys[0].Address()) + ts.HelperSignVote(valKeys[0], v) + + mgr.AddVote(v) + + assert.True(t, consA.HasVote(v.Hash())) + assert.False(t, consB.HasVote(v.Hash())) + }) + + t.Run("Testing set proposal", func(t *testing.T) { + consHeight, _ := mgr.HeightRound() + b, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) + p := proposal.NewProposal(consHeight, 0, b) + ts.HelperSignProposal(valKeys[0], p) + + mgr.SetProposal(p) + + assert.Equal(t, p, consA.Proposal()) + assert.Nil(t, consB.Proposal()) + }) + + t.Run("Check discarding old votes", func(t *testing.T) { + consHeight, _ := mgr.HeightRound() + v := vote.NewPrepareVote(ts.RandHash(), consHeight-1, 0, st.TestValKeys[2].Address()) + ts.HelperSignVote(st.TestValKeys[2], v) + + mgr.AddVote(v) + assert.Empty(t, mgr.upcomingVotes) + }) + + t.Run("Check discarding old proposals", func(t *testing.T) { + consHeight, _ := mgr.HeightRound() + b, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) + p := proposal.NewProposal(consHeight-1, 1, b) + ts.HelperSignProposal(valKeys[0], p) + + mgr.SetProposal(p) + assert.Empty(t, mgr.upcomingProposals) + }) + + t.Run("Processing upcoming votes", func(t *testing.T) { + consHeight, _ := mgr.HeightRound() + v1 := vote.NewPrepareVote(ts.RandHash(), consHeight+1, 0, valKeys[0].Address()) + v2 := vote.NewPrepareVote(ts.RandHash(), consHeight+2, 0, valKeys[0].Address()) + v3 := vote.NewPrepareVote(ts.RandHash(), consHeight+3, 0, valKeys[0].Address()) + + ts.HelperSignVote(valKeys[0], v1) + ts.HelperSignVote(valKeys[0], v2) + ts.HelperSignVote(valKeys[0], v3) + + mgr.AddVote(v1) + mgr.AddVote(v2) + mgr.AddVote(v3) + + assert.Len(t, mgr.upcomingVotes, 3) + + blk1, cert1 := ts.GenerateTestBlock(consHeight) + err := st.CommitBlock(blk1, cert1) + assert.NoError(t, err) + + blk2, cert2 := ts.GenerateTestBlock(consHeight + 1) + err = st.CommitBlock(blk2, cert2) + assert.NoError(t, err) + + mgr.MoveToNewHeight() + + assert.Len(t, mgr.upcomingVotes, 1) + }) + + t.Run("Processing upcoming proposal", func(t *testing.T) { + consHeight, _ := mgr.HeightRound() + b1, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) + p1 := proposal.NewProposal(consHeight+1, 0, b1) + + b2, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) + p2 := proposal.NewProposal(consHeight+2, 0, b2) + + b3, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) + p3 := proposal.NewProposal(consHeight+3, 0, b3) + + ts.HelperSignProposal(valKeys[0], p1) + ts.HelperSignProposal(valKeys[0], p2) + ts.HelperSignProposal(valKeys[0], p3) + + mgr.SetProposal(p1) + mgr.SetProposal(p2) + mgr.SetProposal(p3) + + assert.Len(t, mgr.upcomingProposals, 3) + + blk1, cert1 := ts.GenerateTestBlock(consHeight) + err := st.CommitBlock(blk1, cert1) + assert.NoError(t, err) + + blk2, cert2 := ts.GenerateTestBlock(consHeight + 1) + err = st.CommitBlock(blk2, cert2) + assert.NoError(t, err) + + mgr.MoveToNewHeight() + + assert.Len(t, mgr.upcomingProposals, 1) + }) +} + +func TestMediator(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + st := state.MockingState(ts) + cmt, valKeys := ts.GenerateTestCommittee(4) + st.TestCommittee = cmt + st.TestParams.BlockIntervalInSecond = 1 + + rewardAddrs := []crypto.Address{ + ts.RandAccAddress(), ts.RandAccAddress(), + ts.RandAccAddress(), ts.RandAccAddress(), + } + broadcastCh := make(chan message.Message, 500) + + stateHeight := ts.RandHeight() + blk, cert := ts.GenerateTestBlock(stateHeight) + st.TestStore.SaveBlock(blk, cert) + + mgrInst := NewManager(testConfig(), st, valKeys, rewardAddrs, broadcastCh) + mgr := mgrInst.(*manager) + + mgr.MoveToNewHeight() + + for { + msg := <-broadcastCh + logger.Info("shouldPublishProposal", "msg", msg) + + m, ok := msg.(*message.BlockAnnounceMessage) + if ok { + require.Equal(t, m.Height(), stateHeight+1) + + return + } + } +} diff --git a/fastconsensus/mediator.go b/fastconsensus/mediator.go new file mode 100644 index 000000000..29ba371f4 --- /dev/null +++ b/fastconsensus/mediator.go @@ -0,0 +1,53 @@ +package fastconsensus + +import ( + "github.com/pactus-project/pactus/types/proposal" + "github.com/pactus-project/pactus/types/vote" +) + +// The `mediator“ interface defines a mechanism for setting proposals and votes +// between independent consensus instances. +type mediator interface { + OnPublishProposal(from Consensus, prop *proposal.Proposal) + OnPublishVote(from Consensus, vte *vote.Vote) + OnBlockAnnounce(from Consensus) + Register(cons Consensus) +} + +// ConcreteMediator struct. +type ConcreteMediator struct { + instances []Consensus +} + +func newConcreteMediator() mediator { + return &ConcreteMediator{} +} + +func (m *ConcreteMediator) OnPublishProposal(from Consensus, prop *proposal.Proposal) { + for _, cons := range m.instances { + if cons != from { + cons.SetProposal(prop) + } + } +} + +func (m *ConcreteMediator) OnPublishVote(from Consensus, vte *vote.Vote) { + for _, cons := range m.instances { + if cons != from { + cons.AddVote(vte) + } + } +} + +func (m *ConcreteMediator) OnBlockAnnounce(from Consensus) { + for _, cons := range m.instances { + if cons != from { + cons.MoveToNewHeight() + } + } +} + +// Register a new Consensus instance to the mediator. +func (m *ConcreteMediator) Register(cons Consensus) { + m.instances = append(m.instances, cons) +} diff --git a/fastconsensus/mock.go b/fastconsensus/mock.go new file mode 100644 index 000000000..f7dbcc6ca --- /dev/null +++ b/fastconsensus/mock.go @@ -0,0 +1,140 @@ +package fastconsensus + +import ( + "sync" + + "github.com/pactus-project/pactus/crypto/bls" + "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/types/proposal" + "github.com/pactus-project/pactus/types/vote" + "github.com/pactus-project/pactus/util/testsuite" +) + +var _ Consensus = &MockConsensus{} + +type MockConsensus struct { + // This locks prevents the Data Race in tests + lk sync.RWMutex + ts *testsuite.TestSuite + + ValKey *bls.ValidatorKey + Votes []*vote.Vote + CurProposal *proposal.Proposal + Active bool + Height uint32 + Round int16 +} + +func MockingManager(ts *testsuite.TestSuite, valKeys []*bls.ValidatorKey) (Manager, []*MockConsensus) { + mocks := make([]*MockConsensus, len(valKeys)) + instances := make([]Consensus, len(valKeys)) + for i, s := range valKeys { + cons := MockingConsensus(ts, s) + mocks[i] = cons + instances[i] = cons + } + + return &manager{ + instances: instances, + upcomingVotes: make([]*vote.Vote, 0), + upcomingProposals: make([]*proposal.Proposal, 0), + }, mocks +} + +func MockingConsensus(ts *testsuite.TestSuite, valKey *bls.ValidatorKey) *MockConsensus { + return &MockConsensus{ + ts: ts, + ValKey: valKey, + } +} + +func (m *MockConsensus) ConsensusKey() *bls.PublicKey { + return m.ValKey.PublicKey() +} + +func (m *MockConsensus) MoveToNewHeight() { + m.lk.Lock() + defer m.lk.Unlock() + + m.Height++ +} + +func (*MockConsensus) Start() {} + +func (m *MockConsensus) AddVote(v *vote.Vote) { + m.lk.Lock() + defer m.lk.Unlock() + + m.Votes = append(m.Votes, v) +} + +func (m *MockConsensus) AllVotes() []*vote.Vote { + m.lk.Lock() + defer m.lk.Unlock() + + return m.Votes +} + +func (m *MockConsensus) SetProposal(p *proposal.Proposal) { + m.lk.Lock() + defer m.lk.Unlock() + + m.CurProposal = p +} + +func (m *MockConsensus) HasVote(h hash.Hash) bool { + m.lk.Lock() + defer m.lk.Unlock() + + for _, v := range m.Votes { + if v.Hash() == h { + return true + } + } + + return false +} + +func (m *MockConsensus) Proposal() *proposal.Proposal { + m.lk.Lock() + defer m.lk.Unlock() + + return m.CurProposal +} + +func (m *MockConsensus) HeightRound() (uint32, int16) { + m.lk.Lock() + defer m.lk.Unlock() + + return m.Height, m.Round +} + +func (*MockConsensus) String() string { + return "" +} + +func (m *MockConsensus) PickRandomVote(_ int16) *vote.Vote { + m.lk.Lock() + defer m.lk.Unlock() + + if len(m.Votes) == 0 { + return nil + } + r := m.ts.RandInt32(int32(len(m.Votes))) + + return m.Votes[r] +} + +func (m *MockConsensus) IsActive() bool { + m.lk.Lock() + defer m.lk.Unlock() + + return m.Active +} + +func (m *MockConsensus) SetActive(active bool) { + m.lk.Lock() + defer m.lk.Unlock() + + m.Active = active +} diff --git a/fastconsensus/precommit.go b/fastconsensus/precommit.go new file mode 100644 index 000000000..726b88207 --- /dev/null +++ b/fastconsensus/precommit.go @@ -0,0 +1,76 @@ +package fastconsensus + +import ( + "github.com/pactus-project/pactus/types/proposal" + "github.com/pactus-project/pactus/types/vote" +) + +type precommitState struct { + *consensus + hasVoted bool +} + +func (s *precommitState) enter() { + s.hasVoted = false + + s.decide() +} + +func (s *precommitState) decide() { + s.vote() + s.strongCommit() + + precommits := s.log.PrecommitVoteSet(s.round) + precommitQH := precommits.QuorumHash() + if precommitQH != nil { + s.logger.Debug("pre-commit has quorum", "hash", precommitQH) + + roundProposal := s.log.RoundProposal(s.round) + if roundProposal == nil { + // There is a consensus about a proposal that we don't have yet. + // Ask peers for this proposal. + s.logger.Info("query for a decided proposal", "precommitQH", precommitQH) + s.queryProposal() + + return + } + + votes := precommits.BlockVotes(*precommitQH) + s.blockCert = s.makeBlockCertificate(votes, false) + + s.enterNewState(s.commitState) + } +} + +func (s *precommitState) vote() { + if s.hasVoted { + return + } + + roundProposal := s.log.RoundProposal(s.round) + if roundProposal == nil { + s.logger.Debug("no proposal yet") + + return + } + + // Everything is good + s.signAddPrecommitVote(roundProposal.Block().Hash()) + s.hasVoted = true +} + +func (s *precommitState) onAddVote(_ *vote.Vote) { + s.decide() +} + +func (s *precommitState) onSetProposal(_ *proposal.Proposal) { + s.decide() +} + +func (*precommitState) onTimeout(_ *ticker) { + // Ignore timeouts +} + +func (*precommitState) name() string { + return "precommit" +} diff --git a/fastconsensus/precommit_test.go b/fastconsensus/precommit_test.go new file mode 100644 index 000000000..2604e4000 --- /dev/null +++ b/fastconsensus/precommit_test.go @@ -0,0 +1,37 @@ +package fastconsensus + +import ( + "testing" + + "github.com/pactus-project/pactus/types/vote" + "github.com/stretchr/testify/assert" +) + +func TestPrecommitQueryProposal(t *testing.T) { + td := setup(t) + + td.commitBlockForAllStates(t) + h := uint32(2) + r := int16(0) + + td.enterNewHeight(td.consP) + td.changeProposerTimeout(td.consP) + + prop := td.makeProposal(t, h, r) + propBlockHash := prop.Block().Hash() + + _, _, decidedJust := td.makeChangeProposerJusts(t, propBlockHash, h, r) + + decideVote := vote.NewCPDecidedVote(propBlockHash, h, r, 0, vote.CPValueNo, decidedJust, td.consX.valKey.Address()) + td.HelperSignVote(td.consX.valKey, decideVote) + + td.consP.AddVote(decideVote) + assert.Equal(t, "precommit", td.consP.currentState.name()) + + td.addPrecommitVote(td.consP, propBlockHash, h, r, tIndexX) + td.addPrecommitVote(td.consP, propBlockHash, h, r, tIndexY) + td.addPrecommitVote(td.consP, propBlockHash, h, r, tIndexM) + td.addPrecommitVote(td.consP, propBlockHash, h, r, tIndexN) + + td.shouldPublishQueryProposal(t, td.consP, h) +} diff --git a/fastconsensus/prepare.go b/fastconsensus/prepare.go new file mode 100644 index 000000000..5c3317ef3 --- /dev/null +++ b/fastconsensus/prepare.go @@ -0,0 +1,83 @@ +package fastconsensus + +import ( + "github.com/pactus-project/pactus/types/proposal" + "github.com/pactus-project/pactus/types/vote" +) + +type prepareState struct { + *consensus + hasVoted bool +} + +func (s *prepareState) enter() { + s.hasVoted = false + + changeProperTimeout := s.config.CalculateChangeProposerTimeout(s.round) + queryProposalTimeout := changeProperTimeout / 2 + s.scheduleTimeout(queryProposalTimeout, s.height, s.round, tickerTargetQueryProposal) + s.scheduleTimeout(changeProperTimeout, s.height, s.round, tickerTargetChangeProposer) + + s.decide() +} + +func (s *prepareState) decide() { + s.vote() + s.strongCommit() + + // + // If a validator receives a set of f+1 valid cp:PRE-VOTE votes for this round, + // it starts changing the proposer phase, even if its timer has not expired; + // This prevents it from starting the change-proposer phase too late. + // + cpPreVotes := s.log.CPPreVoteVoteSet(s.round) + if cpPreVotes.HasFPlusOneVotesFor(0, vote.CPValueYes) { + s.startChangingProposer() + } +} + +func (s *prepareState) vote() { + if s.hasVoted { + return + } + + roundProposal := s.log.RoundProposal(s.round) + if roundProposal == nil { + s.logger.Debug("no proposal yet") + + return + } + + // Everything is good + s.signAddPrepareVote(roundProposal.Block().Hash()) + s.hasVoted = true +} + +func (s *prepareState) onTimeout(t *ticker) { + if t.Target == tickerTargetQueryProposal { + roundProposal := s.log.RoundProposal(s.round) + if roundProposal == nil { + s.queryProposal() + } + if s.isProposer() { + s.queryVotes() + } + } else if t.Target == tickerTargetChangeProposer { + s.startChangingProposer() + } +} + +func (s *prepareState) onAddVote(v *vote.Vote) { + if v.Type() == vote.VoteTypePrepare || + v.Type() == vote.VoteTypeCPPreVote { + s.decide() + } +} + +func (s *prepareState) onSetProposal(_ *proposal.Proposal) { + s.decide() +} + +func (*prepareState) name() string { + return "prepare" +} diff --git a/fastconsensus/prepare_test.go b/fastconsensus/prepare_test.go new file mode 100644 index 000000000..d74b23216 --- /dev/null +++ b/fastconsensus/prepare_test.go @@ -0,0 +1,104 @@ +package fastconsensus + +import ( + "testing" + + "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/sync/bundle/message" + "github.com/pactus-project/pactus/types/tx" + "github.com/pactus-project/pactus/types/vote" + "github.com/stretchr/testify/assert" +) + +func TestChangeProposerTimeout(t *testing.T) { + td := setup(t) + + td.enterNewHeight(td.consP) + td.changeProposerTimeout(td.consP) + + td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, hash.UndefHash) +} + +func TestQueryProposal(t *testing.T) { + td := setup(t) + + td.commitBlockForAllStates(t) + h := uint32(2) + + td.enterNewHeight(td.consP) + td.enterNextRound(td.consP) + td.queryProposalTimeout(td.consP) + + td.shouldPublishQueryProposal(t, td.consP, h) + td.shouldNotPublish(t, td.consP, message.TypeQueryVote) +} + +func TestQueryVotes(t *testing.T) { + td := setup(t) + + td.commitBlockForAllStates(t) + td.commitBlockForAllStates(t) + h := uint32(3) + r := int16(1) + + td.enterNewHeight(td.consP) + td.enterNextRound(td.consP) + + // consP is the proposer for this round, but there are not enough votes. + td.queryProposalTimeout(td.consP) + td.shouldPublishProposal(t, td.consP, h, r) + td.shouldPublishQueryVote(t, td.consP, h, r) + td.shouldNotPublish(t, td.consP, message.TypeQueryProposal) +} + +func TestGoToChangeProposerFromPrepare(t *testing.T) { + td := setup(t) + + td.commitBlockForAllStates(t) + + td.enterNewHeight(td.consP) + + td.addCPPreVote(td.consP, hash.UndefHash, 2, 0, vote.CPValueYes, &vote.JustInitYes{}, tIndexX) + td.addCPPreVote(td.consP, hash.UndefHash, 2, 0, vote.CPValueYes, &vote.JustInitYes{}, tIndexY) + + // should move to the change proposer phase, even if it has the proposal and + // its timer has not expired, if it has received 1/3 of the change-proposer votes. + prop := td.makeProposal(t, 2, 0) + td.consP.SetProposal(prop) + td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, hash.UndefHash) +} + +func TestByzantineProposal(t *testing.T) { + td := setup(t) + + td.commitBlockForAllStates(t) + td.commitBlockForAllStates(t) + h := uint32(3) + r := int16(0) + prop := td.makeProposal(t, h, r) + propBlockHash := prop.Block().Hash() + + td.enterNewHeight(td.consP) + + td.addPrepareVote(td.consP, propBlockHash, h, r, tIndexX) + td.addPrepareVote(td.consP, propBlockHash, h, r, tIndexY) + td.addPrepareVote(td.consP, propBlockHash, h, r, tIndexB) + td.addPrepareVote(td.consP, propBlockHash, h, r, tIndexM) + td.addPrepareVote(td.consP, propBlockHash, h, r, tIndexN) + + assert.Nil(t, td.consP.Proposal()) + td.shouldPublishQueryProposal(t, td.consP, h) + + // Byzantine node sends second proposal to Partitioned node. + trx := tx.NewTransferTx(h, td.consX.rewardAddr, + td.RandAccAddress(), 1000, 1000, "invalid proposal") + td.HelperSignTransaction(td.consX.valKey.PrivateKey(), trx) + assert.NoError(t, td.txPool.AppendTx(trx)) + byzProp := td.makeProposal(t, h, r) + assert.NotEqual(t, prop.Hash(), byzProp.Hash()) + + td.consP.SetProposal(byzProp) + assert.Nil(t, td.consP.Proposal()) + td.shouldPublishQueryProposal(t, td.consP, h) + td.checkHeightRound(t, td.consP, h, r) +} diff --git a/fastconsensus/propose.go b/fastconsensus/propose.go new file mode 100644 index 000000000..99dacabb2 --- /dev/null +++ b/fastconsensus/propose.go @@ -0,0 +1,81 @@ +package fastconsensus + +import ( + "github.com/pactus-project/pactus/types/proposal" + "github.com/pactus-project/pactus/types/vote" +) + +type proposeState struct { + *consensus +} + +func (s *proposeState) enter() { + s.decide() +} + +func (s *proposeState) decide() { + proposer := s.proposer(s.round) + if proposer.Address() == s.valKey.Address() { + s.logger.Info("our turn to propose", "proposer", proposer.Address()) + s.createProposal(s.height, s.round) + } else { + s.logger.Debug("not our turn to propose", "proposer", proposer.Address()) + } + + s.cpRound = 0 + s.cpDecided = -1 + s.cpWeakValidity = nil + + // TODO: write test for me + score := s.bcState.AvailabilityScore(proposer.Number()) + + // Based on PIP-19, if the Availability Score is less than 0.9, + // we initiate the Change-Proposer phase. + if score < s.config.MinimumAvailabilityScore { + s.logger.Info("availability score of proposer is low", + "score", score, "proposer", proposer.Address()) + s.startChangingProposer() + } else { + s.enterNewState(s.prepareState) + } +} + +func (s *proposeState) createProposal(height uint32, round int16) { + block, err := s.bcState.ProposeBlock(s.valKey, s.rewardAddr) + if err != nil { + s.logger.Error("unable to propose a block!", "error", err) + + return + } + if err := s.bcState.ValidateBlock(block, round); err != nil { + s.logger.Error("proposed block is invalid!", "error", err) + + return + } + + prop := proposal.NewProposal(height, round, block) + sig := s.valKey.Sign(prop.SignBytes()) + prop.SetSignature(sig) + + s.log.SetRoundProposal(round, prop) + + s.broadcastProposal(prop) + + s.logger.Info("proposal signed and broadcasted", "proposal", prop) +} + +func (*proposeState) onAddVote(_ *vote.Vote) { + panic("Unreachable") +} + +func (*proposeState) onSetProposal(_ *proposal.Proposal) { + panic("Unreachable") +} + +func (*proposeState) onTimeout(_ *ticker) { + panic("Unreachable") +} + +func (*proposeState) name() string { + return "propose" +} diff --git a/fastconsensus/propose_test.go b/fastconsensus/propose_test.go new file mode 100644 index 000000000..cea898393 --- /dev/null +++ b/fastconsensus/propose_test.go @@ -0,0 +1,108 @@ +package fastconsensus + +import ( + "testing" + + "github.com/pactus-project/pactus/types/proposal" + "github.com/pactus-project/pactus/types/vote" + "github.com/stretchr/testify/assert" +) + +func TestProposeBlock(t *testing.T) { + td := setup(t) + + td.enterNewHeight(td.consX) + p := td.shouldPublishProposal(t, td.consX, 1, 0) + assert.Equal(t, td.consX.valKey.Address(), p.Block().Header().ProposerAddress()) +} + +func TestSetProposalInvalidProposer(t *testing.T) { + td := setup(t) + + td.enterNewHeight(td.consY) + assert.Nil(t, td.consY.Proposal()) + + addr := td.consB.valKey.Address() + blk, _ := td.GenerateTestBlockWithProposer(1, addr) + invalidProp := proposal.NewProposal(1, 0, blk) + + td.consY.SetProposal(invalidProp) + assert.Nil(t, td.consY.Proposal()) + + td.HelperSignProposal(td.consB.valKey, invalidProp) + td.consY.SetProposal(invalidProp) + assert.Nil(t, td.consY.Proposal()) +} + +func TestSetProposalInvalidBlock(t *testing.T) { + td := setup(t) + + addr := td.consB.valKey.Address() + blk, _ := td.GenerateTestBlockWithProposer(1, addr) + invProp := proposal.NewProposal(1, 2, blk) + td.HelperSignProposal(td.consB.valKey, invProp) + + td.enterNewHeight(td.consP) + td.enterNextRound(td.consP) + td.enterNextRound(td.consP) + + td.consP.SetProposal(invProp) + assert.Nil(t, td.consP.Proposal()) +} + +func TestSetProposalInvalidHeight(t *testing.T) { + td := setup(t) + + addr := td.consB.valKey.Address() + blk, _ := td.GenerateTestBlockWithProposer(2, addr) + invProp := proposal.NewProposal(2, 0, blk) + td.HelperSignProposal(td.consB.valKey, invProp) + + td.enterNewHeight(td.consY) + td.consY.SetProposal(invProp) + assert.Nil(t, td.consY.Proposal()) +} + +func TestNetworkLagging(t *testing.T) { + td := setup(t) + + td.enterNewHeight(td.consP) + + h := uint32(1) + r := int16(0) + prop := td.makeProposal(t, h, r) + + // consP doesn't have the proposal, but it has received prepared votes from other peers + td.addPrepareVote(td.consP, prop.Block().Hash(), h, r, tIndexX) + td.addPrepareVote(td.consP, prop.Block().Hash(), h, r, tIndexY) + + td.queryProposalTimeout(td.consP) + td.shouldPublishQueryProposal(t, td.consP, h) + + // Proposal is received now + td.consP.SetProposal(prop) + + td.shouldPublishVote(t, td.consP, vote.VoteTypePrepare, prop.Block().Hash()) +} + +func TestProposalNextRound(t *testing.T) { + td := setup(t) + + td.commitBlockForAllStates(t) + + td.enterNewHeight(td.consX) + + // Byzantine node sends proposal for the second round (his turn) even before the first round is started + b, err := td.consB.bcState.ProposeBlock(td.consB.valKey, td.consB.rewardAddr) + assert.NoError(t, err) + p := proposal.NewProposal(2, 1, b) + td.HelperSignProposal(td.consB.valKey, p) + + td.consX.SetProposal(p) + + // consX accepts his proposal, but doesn't move to the next round + assert.NotNil(t, td.consX.log.RoundProposal(1)) + assert.Nil(t, td.consX.Proposal()) + assert.Equal(t, td.consX.height, uint32(2)) + assert.Equal(t, td.consX.round, int16(0)) +} diff --git a/fastconsensus/spec/.gitignore b/fastconsensus/spec/.gitignore new file mode 100644 index 000000000..72a0d554a --- /dev/null +++ b/fastconsensus/spec/.gitignore @@ -0,0 +1,8 @@ +*.dot +*.out +*.toolbox +states +*.tex +*.dvi +*.aux +*.log diff --git a/fastconsensus/spec/Pactus.cfg b/fastconsensus/spec/Pactus.cfg new file mode 100644 index 000000000..b42182dfa --- /dev/null +++ b/fastconsensus/spec/Pactus.cfg @@ -0,0 +1,12 @@ +SPECIFICATION Spec +CONSTANTS + NumNodes = 6 + f = 1 + t = 1 + FaultyNodes = {5} + MaxHeight = 1 + MaxRound = 1 + MaxCPRound = 1 + +INVARIANT TypeOK +PROPERTY Success diff --git a/fastconsensus/spec/Pactus.pdf b/fastconsensus/spec/Pactus.pdf new file mode 100644 index 000000000..5fb24d905 Binary files /dev/null and b/fastconsensus/spec/Pactus.pdf differ diff --git a/fastconsensus/spec/Pactus.tla b/fastconsensus/spec/Pactus.tla new file mode 100644 index 000000000..060d28733 --- /dev/null +++ b/fastconsensus/spec/Pactus.tla @@ -0,0 +1,550 @@ +-------------------------------- MODULE Pactus -------------------------------- +(***************************************************************************) +(* The specification of the Pactus consensus algorithm: *) +(* `^\url{https://pactus.org/learn/consensus/protocol/}^' *) +(***************************************************************************) +EXTENDS Integers, Sequences, FiniteSets, TLC + +CONSTANT + \* The maximum number of height. + \* This limits the range of behaviors evaluated by TLC + MaxHeight, + \* The maximum number of round per height. + \* This limits the range of behaviors evaluated by TLC + MaxRound, + \* The maximum number of cp-round per height. + \* This limits the range of behaviors evaluated by TLC + MaxCPRound, + \* The total number of nodes in the network, + \* denoted as `n` in the protocol. + n, + \* The maximum number of faulty node in change-proposer phase, + \* denoted as `f` in the protocol. + f, + \* The maximum number of faulty node in block-creation phase, + \* denoted as `t` in the protocol. + t, + \* The indices of faulty nodes. + FaultyNodes + +VARIABLES + \* `log` is a set of messages received by the system. + log, + \* `states` represents the state of each replica in the consensus protocol. + states + +\* TwoFPlusOne is equal to `2f+1' +TwoFPlusOne == (2 * f) + 1 +\* OneFPlusOne is equal to `f+1' +OneFPlusOne == (1 * f) + 1 + +\* FourTPlusOne is equal to `4t+1' +FourTPlusOne == (4 * t) + 1 +\* ThreeTPlusOne is equal to `3t+1' +ThreeTPlusOne == (3 * t) + 1 + +\* A tuple containing all variables in the spec (for ease of use in temporal conditions). +vars == <> + +ASSUME + \* Ensure that the number of nodes is sufficient to tolerate the specified number of faults + \* in change-proposer phase. + /\ n >= (3*f)+1 + \* Ensure that the number of nodes is sufficient to tolerate the specified number of faults + \* in block-creation phase. + /\ n >= (5*t)+1 + \* Ensure that `FaultyNodes` is a valid subset of node indices. + /\ FaultyNodes \subseteq 0..n-1 + +----------------------------------------------------------------------------- +(***************************************************************************) +(* Helper functions *) +(***************************************************************************) + +\* Fetch a subset of messages in the network based on the params filter. +SubsetOfMsgs(params) == + {msg \in log: \A field \in DOMAIN params: msg[field] = params[field]} + +\* IsProposer checks if the replica is the proposer for this round. +\* To simplify, we assume the proposer always starts with the first replica, +\* and moves to the next by the change-proposer phase. +IsProposer(index) == + states[index].round % n = index + +\* IsFaulty checks if a node is faulty or not. +IsFaulty(index) == index \in FaultyNodes + +\* HasPrepareAbsoluteQuorum checks whether the node with the given index +\* has received `4t+1` PREPARE votes for a proposal. +HasPrepareAbsoluteQuorum(index) == + Cardinality(SubsetOfMsgs([ + type |-> "PREPARE", + height |-> states[index].height, + round |-> states[index].round])) >= FourTPlusOne + +\* HasPrepareQuorum checks whether the node with the given index +\* has received `3t+1` PREPARE votes for a proposal. +HasPrepareQuorum(index) == + Cardinality(SubsetOfMsgs([ + type |-> "PREPARE", + height |-> states[index].height, + round |-> states[index].round])) >= ThreeTPlusOne + +\* HasPrecommitQuorum checks whether the node with the given index +\* has received `3t+1` the PRECOMMIT votes for a proposal. +HasPrecommitQuorum(index) == + Cardinality(SubsetOfMsgs([ + type |-> "PRECOMMIT", + height |-> states[index].height, + round |-> states[index].round])) >= ThreeTPlusOne + +CPHasPreVotesMinorityQuorum(index) == + Cardinality(SubsetOfMsgs([ + type |-> "CP:PRE-VOTE", + height |-> states[index].height, + round |-> states[index].round, + cp_round |-> 0, + cp_val |-> 1])) >= OneFPlusOne + +CPHasPreVotesQuorum(index) == + Cardinality(SubsetOfMsgs([ + type |-> "CP:PRE-VOTE", + height |-> states[index].height, + round |-> states[index].round, + cp_round |-> states[index].cp_round])) >= TwoFPlusOne + +CPHasPreVotesQuorumForOne(index) == + Cardinality(SubsetOfMsgs([ + type |-> "CP:PRE-VOTE", + height |-> states[index].height, + round |-> states[index].round, + cp_round |-> states[index].cp_round, + cp_val |-> 1])) >= TwoFPlusOne + +CPHasPreVotesQuorumForZero(index) == + Cardinality(SubsetOfMsgs([ + type |-> "CP:PRE-VOTE", + height |-> states[index].height, + round |-> states[index].round, + cp_round |-> states[index].cp_round, + cp_val |-> 0])) >= TwoFPlusOne + +CPHasPreVotesForZeroAndOne(index) == + /\ Cardinality(SubsetOfMsgs([ + type |-> "CP:PRE-VOTE", + height |-> states[index].height, + round |-> states[index].round, + cp_round |-> states[index].cp_round, + cp_val |-> 0])) >= 1 + /\ Cardinality(SubsetOfMsgs([ + type |-> "CP:PRE-VOTE", + height |-> states[index].height, + round |-> states[index].round, + cp_round |-> states[index].cp_round, + cp_val |-> 1])) >= 1 + +CPHasAMainVotesZeroInPrvRound(index) == + Cardinality(SubsetOfMsgs([ + type |-> "CP:MAIN-VOTE", + height |-> states[index].height, + round |-> states[index].round, + cp_round |-> states[index].cp_round - 1, + cp_val |-> 0])) > 0 + +CPHasAMainVotesOneInPrvRound(index) == + Cardinality(SubsetOfMsgs([ + type |-> "CP:MAIN-VOTE", + height |-> states[index].height, + round |-> states[index].round, + cp_round |-> states[index].cp_round - 1, + cp_val |-> 1])) > 0 + +CPAllMainVotesAbstainInPrvRound(index) == + Cardinality(SubsetOfMsgs([ + type |-> "CP:MAIN-VOTE", + height |-> states[index].height, + round |-> states[index].round, + cp_round |-> states[index].cp_round - 1, + cp_val |-> 2])) >= TwoFPlusOne + +CPOneFPlusOneMainVotesAbstainInPrvRound(index) == + Cardinality(SubsetOfMsgs([ + type |-> "CP:MAIN-VOTE", + height |-> states[index].height, + round |-> states[index].round, + cp_round |-> states[index].cp_round - 1, + cp_val |-> 2])) >= OneFPlusOne + +CPHasMainVotesQuorum(index) == + Cardinality(SubsetOfMsgs([ + type |-> "CP:MAIN-VOTE", + height |-> states[index].height, + round |-> states[index].round, + cp_round |-> states[index].cp_round])) >= TwoFPlusOne + +CPHasMainVotesQuorumForOne(index) == + Cardinality(SubsetOfMsgs([ + type |-> "CP:MAIN-VOTE", + height |-> states[index].height, + round |-> states[index].round, + cp_round |-> states[index].cp_round, + cp_val |-> 1])) >= TwoFPlusOne + +CPHasMainVotesQuorumForZero(index) == + Cardinality(SubsetOfMsgs([ + type |-> "CP:MAIN-VOTE", + height |-> states[index].height, + round |-> states[index].round, + cp_round |-> states[index].cp_round, + cp_val |-> 0])) >= TwoFPlusOne + +CPHasDecideVotesForZero(index) == + Cardinality(SubsetOfMsgs([ + type |-> "CP:DECIDE", + height |-> states[index].height, + round |-> states[index].round, + cp_val |-> 0])) > 0 + +CPHasDecideVotesForOne(index) == + Cardinality(SubsetOfMsgs([ + type |-> "CP:DECIDE", + height |-> states[index].height, + round |-> states[index].round, + cp_val |-> 1])) > 0 + +GetProposal(height, round) == + SubsetOfMsgs([type |-> "PROPOSAL", height |-> height, round |-> round]) + +HasProposal(index) == + Cardinality(GetProposal(states[index].height, states[index].round)) > 0 + +HasPrepared(index) == + Cardinality(SubsetOfMsgs([ + type |-> "PREPARE", + height |-> states[index].height, + round |-> states[index].round, + index |-> index])) = 1 + +HasBlockAnnounce(index) == + Cardinality(SubsetOfMsgs([ + type |-> "BLOCK-ANNOUNCE", + height |-> states[index].height, + round |-> states[index].round])) >= 1 + +\* Helper function to check if the block is committed or not. +\* A block is considered committed iff supermajority of non-faulty replicas announce the same block. +IsCommitted == + LET subset == SubsetOfMsgs([ + type |-> "BLOCK-ANNOUNCE", + height |-> MaxHeight]) + IN /\ Cardinality(subset) >= TwoFPlusOne + /\ \A m1, m2 \in subset : m1.round = m2.round + +----------------------------------------------------------------------------- +(***************************************************************************) +(* Network functions *) +(***************************************************************************) + +\* `SendMsg` simulates a replica sending a message by appending it to the `log`. +SendMsg(msg) == + IF msg.cp_round < MaxCPRound + THEN log' = log \cup {msg} + ELSE log' = log + +\* SendProposal is used to broadcast the PROPOSAL into the network. +SendProposal(index) == + SendMsg([ + type |-> "PROPOSAL", + height |-> states[index].height, + round |-> states[index].round, + index |-> index, + cp_round |-> 0, + cp_val |-> 0]) + +\* SendPrepareVote is used to broadcast PREPARE votes into the network. +SendPrepareVote(index) == + SendMsg([ + type |-> "PREPARE", + height |-> states[index].height, + round |-> states[index].round, + index |-> index, + cp_round |-> 0, + cp_val |-> 0]) + +\* SendPrecommitVote is used to broadcast PRECOMMIT votes into the network. +SendPrecommitVote(index) == + SendMsg([ + type |-> "PRECOMMIT", + height |-> states[index].height, + round |-> states[index].round, + index |-> index, + cp_round |-> 0, + cp_val |-> 0]) + +\* SendCPPreVote is used to broadcast CP:PRE-VOTE votes into the network. +SendCPPreVote(index, cp_val) == + SendMsg([ + type |-> "CP:PRE-VOTE", + height |-> states[index].height, + round |-> states[index].round, + index |-> index, + cp_round |-> states[index].cp_round, + cp_val |-> cp_val]) + +\* SendCPMainVote is used to broadcast CP:MAIN-VOTE votes into the network. +SendCPMainVote(index, cp_val) == + SendMsg([ + type |-> "CP:MAIN-VOTE", + height |-> states[index].height, + round |-> states[index].round, + index |-> index, + cp_round |-> states[index].cp_round, + cp_val |-> cp_val]) + +\* SendCPDeciedVote is used to broadcast CP:DECIDE votes into the network. +SendCPDeciedVote(index, cp_val) == + SendMsg([ + type |-> "CP:DECIDE", + height |-> states[index].height, + round |-> states[index].round, + cp_round |-> states[index].cp_round, + index |-> -1, \* reduce the model size + cp_val |-> cp_val]) + +\* AnnounceBlock is used to broadcast BLOCK-ANNOUNCE messages into the network. +AnnounceBlock(index) == + SendMsg([ + type |-> "BLOCK-ANNOUNCE", + height |-> states[index].height, + round |-> states[index].round, + index |-> index, + cp_round |-> 0, + cp_val |-> 0]) + +----------------------------------------------------------------------------- +(***************************************************************************) +(* States functions *) +(***************************************************************************) + +\* NewHeight state +NewHeight(index) == + IF states[index].height >= MaxHeight + THEN UNCHANGED <> + ELSE + /\ ~IsFaulty(index) + /\ states[index].name = "new-height" + /\ states' = [states EXCEPT + ![index].name = "propose", + ![index].height = states[index].height + 1, + ![index].round = 0] + /\ log' = log + + +\* Propose state +Propose(index) == + /\ ~IsFaulty(index) + /\ states[index].name = "propose" + /\ IF IsProposer(index) + THEN SendProposal(index) + ELSE log' = log + /\ states' = [states EXCEPT + ![index].name = "prepare", + ![index].cp_round = 0] + + +\* Prepare state +Prepare(index) == + /\ ~IsFaulty(index) + /\ states[index].name = "prepare" + /\ HasProposal(index) + /\ SendPrepareVote(index) + /\ states' = states + +\* Precommit state +Precommit(index) == + /\ ~IsFaulty(index) + /\ states[index].name = "precommit" + /\ IF HasPrecommitQuorum(index) + THEN /\ states' = [states EXCEPT ![index].name = "commit"] + /\ log' = log + ELSE /\ HasProposal(index) + /\ SendPrecommitVote(index) + /\ states' = states + +\* Commit state +Commit(index) == + /\ ~IsFaulty(index) + /\ states[index].name = "commit" + /\ AnnounceBlock(index) + /\ states' = [states EXCEPT + ![index].name = "new-height"] + +\* Timeout: A non-faulty Replica try to change the proposer if its timer expires. +Timeout(index) == + /\ ~IsFaulty(index) + /\ states[index].name = "prepare" + /\ states[index].round < MaxRound + /\ states' = [states EXCEPT ![index].name = "cp:pre-vote"] + /\ log' = log + + +CPPreVote(index) == + /\ ~IsFaulty(index) + /\ states[index].name = "cp:pre-vote" + /\ IF states[index].cp_round = 0 + THEN + IF HasPrepareQuorum(index) + THEN /\ SendCPPreVote(index, 0) + /\ states' = [states EXCEPT ![index].name = "cp:main-vote"] + ELSE IF HasPrepared(index) + THEN /\ CPHasPreVotesMinorityQuorum(index) + /\ SendCPPreVote(index, 1) + /\ states' = [states EXCEPT ![index].name = "cp:main-vote"] + ELSE /\ SendCPPreVote(index, 1) + /\ states' = [states EXCEPT ![index].name = "cp:main-vote"] + ELSE + /\ + \/ + /\ CPHasAMainVotesOneInPrvRound(index) + /\ SendCPPreVote(index, 1) + \/ + /\ CPHasAMainVotesZeroInPrvRound(index) + /\ SendCPPreVote(index, 0) + \/ + /\ CPAllMainVotesAbstainInPrvRound(index) + /\ SendCPPreVote(index, 0) \* biased to zero + /\ states' = [states EXCEPT ![index].name = "cp:main-vote"] + + +CPMainVote(index) == + /\ ~IsFaulty(index) + /\ states[index].name = "cp:main-vote" + /\ CPHasPreVotesQuorum(index) + /\ + \/ + \* all votes for 1 + /\ CPHasPreVotesQuorumForOne(index) + /\ SendCPMainVote(index, 1) + /\ states' = [states EXCEPT ![index].name = "cp:decide"] + \/ + \* all votes for 0 + /\ CPHasPreVotesQuorumForZero(index) + /\ SendCPMainVote(index, 0) + /\ states' = [states EXCEPT ![index].name = "cp:decide"] + \/ + \* Abstain vote + /\ CPHasPreVotesForZeroAndOne(index) + /\ SendCPMainVote(index, 2) + /\ states' = [states EXCEPT ![index].name = "cp:decide"] + +CPDecide(index) == + /\ ~IsFaulty(index) + /\ states[index].name = "cp:decide" + /\ CPHasMainVotesQuorum(index) + /\ + IF CPHasMainVotesQuorumForZero(index) + THEN + /\ SendCPDeciedVote(index, 0) + /\ states' = states + ELSE IF CPHasMainVotesQuorumForOne(index) + THEN + /\ SendCPDeciedVote(index, 1) + /\ states' = states + ELSE + /\ states' = [states EXCEPT ![index].name = "cp:pre-vote", + ![index].cp_round = states[index].cp_round + 1] + /\ log' = log + + +CPStrongTerminate(index) == + /\ ~IsFaulty(index) + /\ + \/ states[index].name = "cp:pre-vote" + \/ states[index].name = "cp:main-vote" + \/ states[index].name = "cp:decide" + /\ + IF CPHasDecideVotesForOne(index) + THEN /\ states' = [states EXCEPT ![index].name = "propose", + ![index].round = states[index].round + 1] + /\ log' = log + ELSE IF CPHasDecideVotesForZero(index) + THEN + /\ states' = [states EXCEPT ![index].name = "precommit"] + /\ log' = log + ELSE IF /\ states[index].cp_round = MaxCPRound + /\ CPOneFPlusOneMainVotesAbstainInPrvRound(index) + THEN + /\ states' = [states EXCEPT ![index].name = "precommit"] + /\ log' = log + ELSE + /\ states' = states + /\ log' = log + +StrongCommit(index) == + /\ ~IsFaulty(index) + /\ + \/ states[index].name = "prepare" + \/ states[index].name = "precommit" + \/ states[index].name = "cp:pre-vote" + \/ states[index].name = "cp:main-vote" + \/ states[index].name = "cp:decide" + /\ HasPrepareAbsoluteQuorum(index) + /\ states' = [states EXCEPT ![index].name = "commit"] + /\ log' = log + +----------------------------------------------------------------------------- + +Init == + /\ log = {} + /\ states = [index \in 0..n-1 |-> [ + name |-> "new-height", + height |-> 0, + round |-> 0, + cp_round |-> 0]] + +Next == + \E index \in 0..n-1: + \/ NewHeight(index) + \/ Propose(index) + \/ Prepare(index) + \/ Precommit(index) + \/ Timeout(index) + \/ Commit(index) + \/ StrongCommit(index) + \/ CPPreVote(index) + \/ CPMainVote(index) + \/ CPDecide(index) + \/ CPStrongTerminate(index) + +Spec == + Init /\ [][Next]_vars /\ WF_vars(Next) + + +(***************************************************************************) +(* Success: All non-faulty nodes eventually commit at MaxHeight. *) +(***************************************************************************) +Success == <>(IsCommitted) + +(***************************************************************************) +(* TypeOK is the type-correctness invariant. *) +(***************************************************************************) +TypeOK == + /\ \A index \in 0..n-1: + /\ states[index].name \in {"new-height", "propose", "prepare", + "precommit", "commit", "cp:pre-vote", "cp:main-vote", "cp:decide"} + /\ states[index].height <= MaxHeight + /\ states[index].round <= MaxRound + /\ states[index].cp_round <= MaxCPRound + /\ states[index].name = "new-height" /\ states[index].height > 0 => + /\ HasBlockAnnounce(index) + /\ states[index].name = "precommit" => + /\ HasPrepareQuorum(index) + /\ HasProposal(index) + /\ states[index].name = "commit" => + /\ HasPrepareQuorum(index) + /\ HasProposal(index) + /\ \A round \in 0..states[index].round: + \* Not more than one proposal per round + /\ Cardinality(GetProposal(states[index].height, round)) <= 1 + +============================================================================= diff --git a/fastconsensus/spec/README.md b/fastconsensus/spec/README.md new file mode 100644 index 000000000..26740b498 --- /dev/null +++ b/fastconsensus/spec/README.md @@ -0,0 +1,24 @@ +# Consensus specification + +This folder contains the consensus specification for the Pactus blockchain, +which is based on the TLA+ formal language. +The specification defines the consensus algorithm used by the blockchain. + +More info can be found [here](https://pactus.org/learn/consensus/specification/) + +## Model checking + +To run the model checker, you will need to download and install the [TLA+ Toolbox](https://lamport.azurewebsites.net/tla/toolbox.html), +which includes the TLC model checker. Follow the steps below to run the TLC model checker: + +- Add the `Pactus.tla` spec to your TLA+ Toolbox project. +- Create a new model and specify a temporal formula as `Spec`. +- Specify an invariants formula as `TypeOK`. +- Specify a properties formula as `Success`. +- Define the required constants: + - `NumFaulty`: the number of faulty nodes (e.g. 1) + - `FaultyNodes`: the index of faulty nodes (e.g. {3}) + - `MaxHeight`: the maximum height of the system (e.g. 1) + - `MaxRound`: the maximum block-creation round of the consensus algorithm (e.g. 1) + - `MaxCPRound`: the maximum change-proposer round of the consensus algorithm (e.g. 1) +- Run the TLC checker to check the correctness of the specification. diff --git a/fastconsensus/state.go b/fastconsensus/state.go new file mode 100644 index 000000000..b851522fd --- /dev/null +++ b/fastconsensus/state.go @@ -0,0 +1,15 @@ +package fastconsensus + +import ( + "github.com/pactus-project/pactus/types/proposal" + "github.com/pactus-project/pactus/types/vote" +) + +type consState interface { + enter() + decide() + onAddVote(v *vote.Vote) + onSetProposal(p *proposal.Proposal) + onTimeout(t *ticker) + name() string +} diff --git a/fastconsensus/ticker.go b/fastconsensus/ticker.go new file mode 100644 index 000000000..e7384a7ea --- /dev/null +++ b/fastconsensus/ticker.go @@ -0,0 +1,41 @@ +package fastconsensus + +import ( + "fmt" + "time" +) + +type tickerTarget int + +const ( + tickerTargetNewHeight = tickerTarget(1) + tickerTargetChangeProposer = tickerTarget(2) + tickerTargetQueryProposal = tickerTarget(3) + tickerTargetQueryVotes = tickerTarget(4) +) + +func (rs tickerTarget) String() string { + switch rs { + case tickerTargetNewHeight: + return "new-height" + case tickerTargetChangeProposer: + return "change-proposer" + case tickerTargetQueryProposal: + return "query-proposal" + case tickerTargetQueryVotes: + return "query-votes" + default: + return "Unknown" + } +} + +type ticker struct { + Duration time.Duration + Height uint32 + Round int16 + Target tickerTarget +} + +func (ti ticker) String() string { + return fmt.Sprintf("%v@ %d/%d/%s", ti.Duration, ti.Height, ti.Round, ti.Target) +} diff --git a/fastconsensus/voteset/binary_voteset.go b/fastconsensus/voteset/binary_voteset.go new file mode 100644 index 000000000..8221d9032 --- /dev/null +++ b/fastconsensus/voteset/binary_voteset.go @@ -0,0 +1,185 @@ +package voteset + +import ( + "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/types/validator" + "github.com/pactus-project/pactus/types/vote" + "github.com/pactus-project/pactus/util/errors" +) + +type roundVotes struct { + // Each vote can have one of 3 possible values: {0,1,Abstain}. + voteBoxes [3]*voteBox + allVotes map[crypto.Address]*vote.Vote + votedPower int64 +} + +func newRoundVotes() *roundVotes { + voteBoxes := [3]*voteBox{} + voteBoxes[vote.CPValueNo] = newVoteBox() + voteBoxes[vote.CPValueYes] = newVoteBox() + voteBoxes[vote.CPValueAbstain] = newVoteBox() + + return &roundVotes{ + voteBoxes: voteBoxes, + allVotes: make(map[crypto.Address]*vote.Vote), + votedPower: 0, + } +} + +func (rv *roundVotes) addVote(v *vote.Vote, power int64) { + vb := rv.voteBoxes[v.CPValue()] + vb.addVote(v, power) +} + +type BinaryVoteSet struct { + *voteSet + roundVotes []*roundVotes +} + +func NewCPPreVoteVoteSet(round int16, totalPower int64, + validators map[crypto.Address]*validator.Validator, +) *BinaryVoteSet { + voteSet := newVoteSet(round, totalPower, validators) + + return newBinaryVoteSet(voteSet) +} + +func NewCPMainVoteVoteSet(round int16, totalPower int64, + validators map[crypto.Address]*validator.Validator, +) *BinaryVoteSet { + voteSet := newVoteSet(round, totalPower, validators) + + return newBinaryVoteSet(voteSet) +} + +func NewCPDecidedVoteVoteSet(round int16, totalPower int64, + validators map[crypto.Address]*validator.Validator, +) *BinaryVoteSet { + voteSet := newVoteSet(round, totalPower, validators) + + return newBinaryVoteSet(voteSet) +} + +func newBinaryVoteSet(voteSet *voteSet) *BinaryVoteSet { + return &BinaryVoteSet{ + voteSet: voteSet, + roundVotes: make([]*roundVotes, 0, 1), + } +} + +func (vs *BinaryVoteSet) mustGetRoundVotes(cpRound int16) *roundVotes { + for i := len(vs.roundVotes); i <= int(cpRound); i++ { + rv := newRoundVotes() + vs.roundVotes = append(vs.roundVotes, rv) + } + + return vs.roundVotes[cpRound] +} + +// AllVotes returns a list of all votes in the VoteSet. +func (vs *BinaryVoteSet) AllVotes() []*vote.Vote { + votes := make([]*vote.Vote, 0) + for _, rv := range vs.roundVotes { + for _, v := range rv.allVotes { + votes = append(votes, v) + } + } + + return votes +} + +// AddVote attempts to add a vote to the VoteSet. Returns an error if the vote is invalid. +func (vs *BinaryVoteSet) AddVote(v *vote.Vote) (bool, error) { + power, err := vs.voteSet.verifyVote(v) + if err != nil { + return false, err + } + + roundVotes := vs.mustGetRoundVotes(v.CPRound()) + existingVote, ok := roundVotes.allVotes[v.Signer()] + if ok { + if existingVote.Hash() == v.Hash() { + // The vote is already added + return false, nil + } + + // It is a duplicated vote + err = errors.Error(errors.ErrDuplicateVote) + } else { + roundVotes.allVotes[v.Signer()] = v + roundVotes.votedPower += power + } + + roundVotes.addVote(v, power) + + return true, err +} + +func (vs *BinaryVoteSet) HasTwoFPlusOneVotes(cpRound int16) bool { + roundVotes := vs.mustGetRoundVotes(cpRound) + + return vs.hasTwoFPlusOnePower(roundVotes.votedPower) +} + +func (vs *BinaryVoteSet) HasAnyVoteFor(cpRound int16, cpValue vote.CPValue) bool { + roundVotes := vs.mustGetRoundVotes(cpRound) + + return roundVotes.voteBoxes[cpValue].votedPower > 0 +} + +func (vs *BinaryVoteSet) HasAllVotesFor(cpRound int16, cpValue vote.CPValue) bool { + roundVotes := vs.mustGetRoundVotes(cpRound) + + return roundVotes.voteBoxes[cpValue].votedPower == roundVotes.votedPower +} + +func (vs *BinaryVoteSet) HasFPlusOneVotesFor(cpRound int16, cpValue vote.CPValue) bool { + roundVotes := vs.mustGetRoundVotes(cpRound) + + return vs.hasFPlusOnePower(roundVotes.voteBoxes[cpValue].votedPower) +} + +func (vs *BinaryVoteSet) HasTwoFPlusOneVotesFor(cpRound int16, cpValue vote.CPValue) bool { + roundVotes := vs.mustGetRoundVotes(cpRound) + + return vs.hasTwoFPlusOnePower(roundVotes.voteBoxes[cpValue].votedPower) +} + +func (vs *BinaryVoteSet) BinaryVotes(cpRound int16, cpValue vote.CPValue) map[crypto.Address]*vote.Vote { + votes := map[crypto.Address]*vote.Vote{} + roundVotes := vs.mustGetRoundVotes(cpRound) + voteBox := roundVotes.voteBoxes[cpValue] + for a, v := range voteBox.votes { + votes[a] = v + } + + return votes +} + +func (vs *BinaryVoteSet) GetRandomVote(cpRound int16, cpValue vote.CPValue) *vote.Vote { + roundVotes := vs.mustGetRoundVotes(cpRound) + for _, v := range roundVotes.voteBoxes[cpValue].votes { + return v + } + + return nil +} + +// faultyPower calculates the faulty power based on the total power. +// The formula used is: f = (n - 1) / 5, where n is the total power. +func (vs *BinaryVoteSet) faultyPower() int64 { + return (vs.totalPower - 1) / 3 +} + +// hasTwoFPlusOnePower checks whether the given power is greater than or equal to 2f+1, +// where f is the faulty power. +func (vs *BinaryVoteSet) hasTwoFPlusOnePower(power int64) bool { + return power >= (2*vs.faultyPower() + 1) +} + +// hasFPlusOnePower checks whether the given power is greater than or equal to f+1, +// where f is the faulty power. +func (vs *BinaryVoteSet) hasFPlusOnePower(power int64) bool { + return power >= (vs.faultyPower() + 1) +} diff --git a/fastconsensus/voteset/block_voteset.go b/fastconsensus/voteset/block_voteset.go new file mode 100644 index 000000000..6a4ab2f1e --- /dev/null +++ b/fastconsensus/voteset/block_voteset.go @@ -0,0 +1,140 @@ +package voteset + +import ( + "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/types/validator" + "github.com/pactus-project/pactus/types/vote" + "github.com/pactus-project/pactus/util/errors" +) + +type BlockVoteSet struct { + *voteSet + blockVotes map[hash.Hash]*voteBox + allVotes map[crypto.Address]*vote.Vote + quorumHash *hash.Hash +} + +func NewPrepareVoteSet(round int16, totalPower int64, + validators map[crypto.Address]*validator.Validator, +) *BlockVoteSet { + voteSet := newVoteSet(round, totalPower, validators) + + return newBlockVoteSet(voteSet) +} + +func NewPrecommitVoteSet(round int16, totalPower int64, + validators map[crypto.Address]*validator.Validator, +) *BlockVoteSet { + voteSet := newVoteSet(round, totalPower, validators) + + return newBlockVoteSet(voteSet) +} + +func newBlockVoteSet(voteSet *voteSet) *BlockVoteSet { + return &BlockVoteSet{ + voteSet: voteSet, + blockVotes: make(map[hash.Hash]*voteBox), + allVotes: make(map[crypto.Address]*vote.Vote), + } +} + +func (vs *BlockVoteSet) BlockVotes(blockHash hash.Hash) map[crypto.Address]*vote.Vote { + votes := map[crypto.Address]*vote.Vote{} + blockVotes := vs.mustGetBlockVotes(blockHash) + for a, v := range blockVotes.votes { + votes[a] = v + } + + return votes +} + +func (vs *BlockVoteSet) mustGetBlockVotes(blockHash hash.Hash) *voteBox { + bv, exists := vs.blockVotes[blockHash] + if !exists { + bv = newVoteBox() + vs.blockVotes[blockHash] = bv + } + + return bv +} + +// AllVotes returns a list of all votes in the VoteSet. +func (vs *BlockVoteSet) AllVotes() []*vote.Vote { + votes := make([]*vote.Vote, 0) + for _, v := range vs.allVotes { + votes = append(votes, v) + } + + return votes +} + +// AddVote attempts to add a vote to the VoteSet. Returns an error if the vote is invalid. +func (vs *BlockVoteSet) AddVote(v *vote.Vote) (bool, error) { + power, err := vs.voteSet.verifyVote(v) + if err != nil { + return false, err + } + + existingVote, ok := vs.allVotes[v.Signer()] + if ok { + if existingVote.Hash() == v.Hash() { + // The vote is already added + return false, nil + } + + // It is a duplicated vote + err = errors.Error(errors.ErrDuplicateVote) + } else { + vs.allVotes[v.Signer()] = v + } + + blockVotes := vs.mustGetBlockVotes(v.BlockHash()) + blockVotes.addVote(v, power) + if vs.hasThreeTPlusOnePower(blockVotes.votedPower) { + quorumHash := v.BlockHash() + vs.quorumHash = &quorumHash + } + + return true, err +} + +func (vs *BlockVoteSet) HasVoted(addr crypto.Address) bool { + return vs.allVotes[addr] != nil +} + +// HasAbsoluteQuorum checks if there is a block that has received an absolute quorum of votes (4t+1 of total power). +func (vs *BlockVoteSet) HasAbsoluteQuorum(blockHash hash.Hash) bool { + blockVotes := vs.mustGetBlockVotes(blockHash) + + return vs.hasFourTPlusOnePower(blockVotes.votedPower) +} + +// HasQuorumHash checks if there is a block that has received a quorum of votes (3t+1 of total power). +func (vs *BlockVoteSet) HasQuorumHash() bool { + return vs.quorumHash != nil +} + +// QuorumHash returns the hash of the block that has received a quorum of votes (3t+1 of total power). +// If no block has received the quorum threshold, it returns nil. +func (vs *BlockVoteSet) QuorumHash() *hash.Hash { + return vs.quorumHash +} + +// thresholdPower calculates the threshold power based on the total power. +// The formula used is: t = (n - 1) / 5, where n is the total power. +func (vs *BlockVoteSet) thresholdPower() int64 { + return (vs.totalPower - 1) / 5 +} + +// hasFourTPlusOnePower checks whether the given power is greater than or equal to 4t+1, +// where t is the threshold power. +func (vs *BlockVoteSet) hasFourTPlusOnePower(power int64) bool { + return power >= (4*vs.thresholdPower() + 1) +} + +// hasThreeTPlusOnePower checks whether the given power is greater than or equal to 3t+1, +// where t is the threshold power. +func (vs *BlockVoteSet) hasThreeTPlusOnePower(power int64) bool { + return power >= (3*vs.thresholdPower() + 1) +} diff --git a/fastconsensus/voteset/vote_box.go b/fastconsensus/voteset/vote_box.go new file mode 100644 index 000000000..fbf0866a7 --- /dev/null +++ b/fastconsensus/voteset/vote_box.go @@ -0,0 +1,25 @@ +package voteset + +import ( + "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/types/vote" +) + +type voteBox struct { + votes map[crypto.Address]*vote.Vote + votedPower int64 +} + +func newVoteBox() *voteBox { + return &voteBox{ + votes: make(map[crypto.Address]*vote.Vote), + votedPower: 0, + } +} + +func (vs *voteBox) addVote(vte *vote.Vote, power int64) { + if vs.votes[vte.Signer()] == nil { + vs.votes[vte.Signer()] = vte + vs.votedPower += power + } +} diff --git a/fastconsensus/voteset/vote_box_test.go b/fastconsensus/voteset/vote_box_test.go new file mode 100644 index 000000000..33d7ad301 --- /dev/null +++ b/fastconsensus/voteset/vote_box_test.go @@ -0,0 +1,28 @@ +package voteset + +import ( + "testing" + + "github.com/pactus-project/pactus/types/vote" + "github.com/pactus-project/pactus/util/testsuite" + "github.com/stretchr/testify/assert" +) + +func TestDuplicateVote(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + hash := ts.RandHash() + height := ts.RandHeight() + round := ts.RandRound() + signer := ts.RandValAddress() + power := ts.RandInt64(1000) + + v := vote.NewPrepareVote(hash, height, round, signer) + + vb := newVoteBox() + + vb.addVote(v, power) + vb.addVote(v, power) + + assert.Equal(t, vb.votedPower, power) +} diff --git a/fastconsensus/voteset/voteset.go b/fastconsensus/voteset/voteset.go new file mode 100644 index 000000000..f3c2f0c2f --- /dev/null +++ b/fastconsensus/voteset/voteset.go @@ -0,0 +1,46 @@ +package voteset + +import ( + "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/types/validator" + "github.com/pactus-project/pactus/types/vote" + "github.com/pactus-project/pactus/util/errors" +) + +type voteSet struct { + round int16 + validators map[crypto.Address]*validator.Validator + totalPower int64 +} + +func newVoteSet(round int16, totalPower int64, + validators map[crypto.Address]*validator.Validator, +) *voteSet { + return &voteSet{ + round: round, + validators: validators, + totalPower: totalPower, + } +} + +// Round returns the round number for the VoteSet. +func (vs *voteSet) Round() int16 { + return vs.round +} + +// verifyVote checks if the given vote is valid. +// It returns the voting power of if valid, or an error if not. +func (vs *voteSet) verifyVote(v *vote.Vote) (int64, error) { + signer := v.Signer() + val := vs.validators[signer] + if val == nil { + return 0, errors.Errorf(errors.ErrInvalidAddress, + "cannot find validator %s in committee", signer) + } + + if err := v.Verify(val.PublicKey()); err != nil { + return 0, err + } + + return val.Power(), nil +} diff --git a/fastconsensus/voteset/voteset_test.go b/fastconsensus/voteset/voteset_test.go new file mode 100644 index 000000000..c803e4ad1 --- /dev/null +++ b/fastconsensus/voteset/voteset_test.go @@ -0,0 +1,439 @@ +package voteset + +import ( + "testing" + + "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/crypto/bls" + "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/types/amount" + "github.com/pactus-project/pactus/types/validator" + "github.com/pactus-project/pactus/types/vote" + "github.com/pactus-project/pactus/util/errors" + "github.com/pactus-project/pactus/util/testsuite" + "github.com/stretchr/testify/assert" +) + +func setupCommittee(ts *testsuite.TestSuite, stakes ...amount.Amount) ( + map[crypto.Address]*validator.Validator, []*bls.ValidatorKey, int64, +) { + valKeys := []*bls.ValidatorKey{} + valsMap := map[crypto.Address]*validator.Validator{} + totalPower := int64(0) + for i, s := range stakes { + pub, prv := ts.RandBLSKeyPair() + val := validator.NewValidator(pub, int32(i)) + val.AddToStake(s) + valsMap[val.Address()] = val + totalPower += val.Power() + valKeys = append(valKeys, bls.NewValidatorKey(prv)) + } + + return valsMap, valKeys, totalPower +} + +func TestAddBlockVote(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + valsMap, valKeys, totalPower := setupCommittee(ts, 1, 1, 1, 1, 1, 1) + + hash1 := ts.RandHash() + hash2 := ts.RandHash() + height := ts.RandHeight() + round := ts.RandRound() + invKey := ts.RandValKey() + valKey := valKeys[0] + vs := NewPrepareVoteSet(round, totalPower, valsMap) + assert.Equal(t, vs.Round(), round) + + v1 := vote.NewPrepareVote(hash1, height, round, invKey.Address()) + v2 := vote.NewPrepareVote(hash1, height, round, valKey.Address()) + v3 := vote.NewPrepareVote(hash2, height, round, valKey.Address()) + + ts.HelperSignVote(invKey, v1) + added, err := vs.AddVote(v1) + assert.Equal(t, errors.Code(err), errors.ErrInvalidAddress) // unknown validator + assert.False(t, added) + + ts.HelperSignVote(invKey, v2) + added, err = vs.AddVote(v2) + assert.ErrorIs(t, err, crypto.ErrInvalidSignature) + assert.False(t, added) + + ts.HelperSignVote(valKey, v2) + added, err = vs.AddVote(v2) + assert.NoError(t, err) // ok + assert.True(t, added) + + added, err = vs.AddVote(v2) // Adding again + assert.False(t, added) + assert.NoError(t, err) + + ts.HelperSignVote(valKey, v3) + added, err = vs.AddVote(v3) + assert.Equal(t, errors.Code(err), errors.ErrDuplicateVote) + assert.True(t, added) +} + +func TestAddBinaryVote(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + valsMap, valKeys, totalPower := setupCommittee(ts, 1, 1, 1, 1) + + hash1 := ts.RandHash() + hash2 := ts.RandHash() + height := ts.RandHeight() + round := ts.RandRound() + cpRound := ts.RandRound() + cpVal := ts.RandInt(2) + just := &vote.JustInitYes{} + invKey := ts.RandValKey() + valKey := valKeys[0] + vs := NewCPPreVoteVoteSet(round, totalPower, valsMap) + + v1 := vote.NewCPPreVote(hash1, height, round, cpRound, vote.CPValue(cpVal), just, invKey.Address()) + v2 := vote.NewCPPreVote(hash1, height, round, cpRound, vote.CPValue(cpVal), just, valKey.Address()) + v3 := vote.NewCPPreVote(hash2, height, round, cpRound, vote.CPValue(cpVal), just, valKey.Address()) + + ts.HelperSignVote(invKey, v1) + added, err := vs.AddVote(v1) + assert.Equal(t, errors.Code(err), errors.ErrInvalidAddress) // unknown validator + assert.False(t, added) + + ts.HelperSignVote(invKey, v2) + added, err = vs.AddVote(v2) + assert.ErrorIs(t, err, crypto.ErrInvalidSignature) + assert.False(t, added) + + ts.HelperSignVote(valKey, v2) + added, err = vs.AddVote(v2) + assert.NoError(t, err) // ok + assert.True(t, added) + + added, err = vs.AddVote(v2) // Adding again + assert.False(t, added) + assert.NoError(t, err) + + ts.HelperSignVote(valKey, v3) + added, err = vs.AddVote(v3) + assert.Equal(t, errors.Code(err), errors.ErrDuplicateVote) + assert.True(t, added) +} + +func TestDuplicateBlockVote(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + valsMap, valKeys, totalPower := setupCommittee(ts, 1, 1, 1, 1, 1, 1) + + h1 := ts.RandHash() + h2 := ts.RandHash() + h3 := ts.RandHash() + addr := valKeys[0].Address() + vs := NewPrepareVoteSet(0, totalPower, valsMap) + + correctVote := vote.NewPrepareVote(h1, 1, 0, addr) + duplicatedVote1 := vote.NewPrepareVote(h2, 1, 0, addr) + duplicatedVote2 := vote.NewPrepareVote(h3, 1, 0, addr) + + // sign the votes + ts.HelperSignVote(valKeys[0], correctVote) + ts.HelperSignVote(valKeys[0], duplicatedVote1) + ts.HelperSignVote(valKeys[0], duplicatedVote2) + + added, err := vs.AddVote(correctVote) + assert.NoError(t, err) + assert.True(t, added) + + added, err = vs.AddVote(duplicatedVote1) + assert.Equal(t, errors.Code(err), errors.ErrDuplicateVote) + assert.True(t, added) + + added, err = vs.AddVote(duplicatedVote2) + assert.Equal(t, errors.Code(err), errors.ErrDuplicateVote) + assert.True(t, added) + + bv1 := vs.BlockVotes(h1) + bv2 := vs.BlockVotes(h2) + bv3 := vs.BlockVotes(h3) + assert.Equal(t, bv1[addr], correctVote) + assert.Equal(t, bv2[addr], duplicatedVote1) + assert.Equal(t, bv3[addr], duplicatedVote2) + assert.False(t, vs.HasQuorumHash()) + + assert.Contains(t, vs.AllVotes(), correctVote) + assert.NotContains(t, vs.AllVotes(), duplicatedVote1) + assert.NotContains(t, vs.AllVotes(), duplicatedVote2) +} + +func TestDuplicateBinaryVote(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + valsMap, valKeys, totalPower := setupCommittee(ts, 1, 1, 1, 1) + + h1 := ts.RandHash() + h2 := ts.RandHash() + h3 := ts.RandHash() + addr := valKeys[0].Address() + vs := NewCPPreVoteVoteSet(0, totalPower, valsMap) + + correctVote := vote.NewCPPreVote(h1, 1, 0, 0, vote.CPValueYes, &vote.JustInitYes{}, addr) + duplicatedVote1 := vote.NewCPPreVote(h2, 1, 0, 0, vote.CPValueYes, &vote.JustInitYes{}, addr) + duplicatedVote2 := vote.NewCPPreVote(h3, 1, 0, 0, vote.CPValueYes, &vote.JustInitYes{}, addr) + + // sign the votes + ts.HelperSignVote(valKeys[0], correctVote) + ts.HelperSignVote(valKeys[0], duplicatedVote1) + ts.HelperSignVote(valKeys[0], duplicatedVote2) + + added, err := vs.AddVote(correctVote) + assert.NoError(t, err) + assert.True(t, added) + + added, err = vs.AddVote(duplicatedVote1) + assert.Equal(t, errors.Code(err), errors.ErrDuplicateVote) + assert.True(t, added) + + added, err = vs.AddVote(duplicatedVote2) + assert.Equal(t, errors.Code(err), errors.ErrDuplicateVote) + assert.True(t, added) + + assert.False(t, vs.HasFPlusOneVotesFor(0, vote.CPValueNo)) + + assert.Contains(t, vs.AllVotes(), correctVote) + assert.NotContains(t, vs.AllVotes(), duplicatedVote1) + assert.NotContains(t, vs.AllVotes(), duplicatedVote2) +} + +func TestQuorum(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + // N = 4501 + // t = 900 + // 3t+1 = 2700 + 1 + // 4t+1 = 3600 + 1 + valsMap, valKeys, totalPower := setupCommittee(ts, 1000, 900, 801, 700, 600, 500) + + vs := NewPrepareVoteSet(0, totalPower, valsMap) + blockHash := ts.RandHash() + v1 := vote.NewPrepareVote(blockHash, 1, 0, valKeys[0].Address()) + v2 := vote.NewPrepareVote(blockHash, 1, 0, valKeys[1].Address()) + v3 := vote.NewPrepareVote(blockHash, 1, 0, valKeys[2].Address()) + v4 := vote.NewPrepareVote(blockHash, 1, 0, valKeys[3].Address()) + v5 := vote.NewPrepareVote(blockHash, 1, 0, valKeys[4].Address()) + v6 := vote.NewPrepareVote(blockHash, 1, 0, valKeys[5].Address()) + + ts.HelperSignVote(valKeys[0], v1) + ts.HelperSignVote(valKeys[1], v2) + ts.HelperSignVote(valKeys[2], v3) + ts.HelperSignVote(valKeys[3], v4) + ts.HelperSignVote(valKeys[4], v5) + ts.HelperSignVote(valKeys[5], v6) + + _, err := vs.AddVote(v1) + assert.NoError(t, err) + _, err = vs.AddVote(v2) + assert.NoError(t, err) + + assert.Nil(t, vs.QuorumHash()) + assert.False(t, vs.HasQuorumHash()) + + // Add more votes + _, err = vs.AddVote(v3) + assert.NoError(t, err) + + assert.True(t, vs.HasQuorumHash()) + assert.Equal(t, vs.QuorumHash(), &blockHash) + assert.False(t, vs.HasAbsoluteQuorum(blockHash)) + + // Add more votes + _, err = vs.AddVote(v4) + assert.NoError(t, err) + _, err = vs.AddVote(v5) + assert.NoError(t, err) + + assert.True(t, vs.HasAbsoluteQuorum(blockHash)) + + // Add more votes + _, err = vs.AddVote(v6) + assert.NoError(t, err) + assert.True(t, vs.HasAbsoluteQuorum(blockHash)) +} + +func TestAllBlockVotes(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + valsMap, valKeys, totalPower := setupCommittee(ts, 1, 1, 1, 1, 1, 1) + + vs := NewPrecommitVoteSet(1, totalPower, valsMap) + + h1 := ts.RandHash() + v1 := vote.NewPrecommitVote(h1, 1, 1, valKeys[0].Address()) + v2 := vote.NewPrecommitVote(h1, 1, 1, valKeys[1].Address()) + v3 := vote.NewPrecommitVote(h1, 1, 1, valKeys[2].Address()) + v4 := vote.NewPrecommitVote(h1, 1, 1, valKeys[3].Address()) + + ts.HelperSignVote(valKeys[0], v1) + ts.HelperSignVote(valKeys[1], v2) + ts.HelperSignVote(valKeys[2], v3) + ts.HelperSignVote(valKeys[3], v4) + + _, err := vs.AddVote(v1) + assert.NoError(t, err) + + _, err = vs.AddVote(v2) + assert.NoError(t, err) + + _, err = vs.AddVote(v3) + assert.NoError(t, err) + + _, err = vs.AddVote(v4) + assert.NoError(t, err) + + assert.Equal(t, vs.QuorumHash(), &h1) + + // Check accumulated power + assert.Equal(t, vs.QuorumHash(), &h1) + + // Check previous votes + assert.Contains(t, vs.AllVotes(), v1) + assert.Contains(t, vs.AllVotes(), v2) + assert.Contains(t, vs.AllVotes(), v3) + assert.Contains(t, vs.AllVotes(), v4) +} + +func TestAllBinaryVotes(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + valsMap, valKeys, totalPower := setupCommittee(ts, 1, 1, 1, 1) + + vs := NewCPMainVoteVoteSet(1, totalPower, valsMap) + + v1 := vote.NewCPMainVote(hash.UndefHash, 1, 1, 0, vote.CPValueNo, &vote.JustInitYes{}, valKeys[0].Address()) + v2 := vote.NewCPMainVote(hash.UndefHash, 1, 1, 1, vote.CPValueYes, &vote.JustInitYes{}, valKeys[1].Address()) + v3 := vote.NewCPMainVote(hash.UndefHash, 1, 1, 2, vote.CPValueAbstain, &vote.JustInitYes{}, valKeys[2].Address()) + + ts.HelperSignVote(valKeys[0], v1) + ts.HelperSignVote(valKeys[1], v2) + ts.HelperSignVote(valKeys[2], v3) + + assert.Empty(t, vs.AllVotes()) + + _, err := vs.AddVote(v1) + assert.NoError(t, err) + + _, err = vs.AddVote(v2) + assert.NoError(t, err) + + _, err = vs.AddVote(v3) + assert.NoError(t, err) + + assert.Contains(t, vs.AllVotes(), v1) + assert.Contains(t, vs.AllVotes(), v2) + assert.Contains(t, vs.AllVotes(), v3) + + ranVote1 := vs.GetRandomVote(1, vote.CPValueNo) + assert.Nil(t, ranVote1) + + ranVote2 := vs.GetRandomVote(1, vote.CPValueYes) + assert.Equal(t, ranVote2, v2) +} + +func TestOneThirdPower(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + // N = 3001 + // f = 1000 + // f+1 = 1001 + // 2f+1 = 2001 + // 3f+1 = 3001 + valsMap, valKeys, totalPower := setupCommittee(ts, 1000, 1, 1000, 1000) + + h := ts.RandHash() + height := ts.RandHeight() + round := ts.RandRound() + just := &vote.JustInitYes{} + vs := NewCPPreVoteVoteSet(round, totalPower, valsMap) + + v1 := vote.NewCPPreVote(h, height, round, 0, vote.CPValueYes, just, valKeys[0].Address()) + v2 := vote.NewCPPreVote(h, height, round, 0, vote.CPValueYes, just, valKeys[1].Address()) + v3 := vote.NewCPPreVote(h, height, round, 0, vote.CPValueYes, just, valKeys[2].Address()) + v4 := vote.NewCPPreVote(h, height, round, 0, vote.CPValueNo, just, valKeys[3].Address()) + + ts.HelperSignVote(valKeys[0], v1) + ts.HelperSignVote(valKeys[1], v2) + ts.HelperSignVote(valKeys[2], v3) + ts.HelperSignVote(valKeys[3], v4) + + _, err := vs.AddVote(v1) + assert.NoError(t, err) + assert.False(t, vs.HasFPlusOneVotesFor(0, vote.CPValueNo)) + assert.True(t, vs.HasAnyVoteFor(0, vote.CPValueYes)) + assert.False(t, vs.HasAnyVoteFor(0, vote.CPValueNo)) + assert.False(t, vs.HasAnyVoteFor(0, vote.CPValueAbstain)) + + _, err = vs.AddVote(v2) + assert.NoError(t, err) + assert.True(t, vs.HasFPlusOneVotesFor(0, vote.CPValueYes)) + assert.False(t, vs.HasTwoFPlusOneVotes(0)) + + _, err = vs.AddVote(v3) + assert.NoError(t, err) + assert.True(t, vs.HasTwoFPlusOneVotes(0)) + assert.False(t, vs.HasAnyVoteFor(0, vote.CPValueNo)) + assert.True(t, vs.HasAnyVoteFor(0, vote.CPValueYes)) + assert.False(t, vs.HasTwoFPlusOneVotesFor(0, vote.CPValueNo)) + assert.True(t, vs.HasTwoFPlusOneVotesFor(0, vote.CPValueYes)) + assert.True(t, vs.HasAllVotesFor(0, vote.CPValueYes)) + + _, err = vs.AddVote(v4) + assert.NoError(t, err) + assert.True(t, vs.HasAnyVoteFor(0, vote.CPValueNo)) + assert.False(t, vs.HasTwoFPlusOneVotesFor(0, vote.CPValueNo)) + assert.True(t, vs.HasTwoFPlusOneVotesFor(0, vote.CPValueYes)) + assert.False(t, vs.HasAllVotesFor(0, vote.CPValueYes)) + + bv1 := vs.BinaryVotes(0, vote.CPValueYes) + bv2 := vs.BinaryVotes(0, vote.CPValueNo) + + assert.Contains(t, bv1, v1.Signer()) + assert.Contains(t, bv1, v2.Signer()) + assert.Contains(t, bv1, v3.Signer()) + assert.Contains(t, bv2, v4.Signer()) +} + +func TestDecidedVoteset(t *testing.T) { + ts := testsuite.NewTestSuite(t) + valsMap, valKeys, totalPower := setupCommittee(ts, 1, 1, 1, 1) + + h := ts.RandHash() + height := ts.RandHeight() + round := ts.RandRound() + just := &vote.JustInitYes{} + vs := NewCPDecidedVoteVoteSet(round, totalPower, valsMap) + + v1 := vote.NewCPDecidedVote(h, height, round, 0, vote.CPValueYes, just, valKeys[0].Address()) + + ts.HelperSignVote(valKeys[0], v1) + + _, err := vs.AddVote(v1) + assert.NoError(t, err) + assert.True(t, vs.HasAnyVoteFor(0, vote.CPValueYes)) + assert.False(t, vs.HasAnyVoteFor(0, vote.CPValueNo)) +} + +// This test ensures that `3t` is always less than `2f`. +func TestFaultyPower(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + powers := []amount.Amount{} + for i := 0; i < 51; i++ { + randPower := ts.RandAmount() + powers = append(powers, randPower) + } + valsMap, _, totalPower := setupCommittee(ts, powers...) + + precommitVoteSet := NewPrecommitVoteSet(0, totalPower, valsMap) + preVoteVoteSet := NewCPPreVoteVoteSet(0, totalPower, valsMap) + + assert.Less(t, 3*precommitVoteSet.thresholdPower(), 2*preVoteVoteSet.faultyPower()) +} diff --git a/state/errors.go b/state/errors.go index 278913924..0bbbe8b03 100644 --- a/state/errors.go +++ b/state/errors.go @@ -18,12 +18,12 @@ func (e InvalidVoteForCertificateError) Error() string { e.Vote.String()) } -// InvalidCertificateError is returned when the given certificate is invalid. -type InvalidCertificateError struct { - Cert *certificate.Certificate +// InvalidBlockCertificateError is returned when the given certificate is invalid. +type InvalidBlockCertificateError struct { + Cert *certificate.BlockCertificate } -func (e InvalidCertificateError) Error() string { +func (e InvalidBlockCertificateError) Error() string { return fmt.Sprintf("invalid certificate for block %d", e.Cert.Height()) } diff --git a/state/execution_test.go b/state/execution_test.go index 00bcb5f62..593ea04da 100644 --- a/state/execution_test.go +++ b/state/execution_test.go @@ -43,8 +43,8 @@ func TestProposeBlock(t *testing.T) { assert.True(t, blk.Transactions()[0].IsSubsidyTx()) assert.NoError(t, td.state.CommitBlock(blk, cert)) - assert.Equal(t, td.state.TotalPower(), int64(1000000004)) - assert.Equal(t, td.state.committee.TotalPower(), int64(4)) + assert.Equal(t, td.state.TotalPower(), int64(1000000006)) + assert.Equal(t, td.state.committee.TotalPower(), int64(6)) } func TestExecuteBlock(t *testing.T) { diff --git a/state/facade.go b/state/facade.go index 6fa2c0914..cb4dcecc2 100644 --- a/state/facade.go +++ b/state/facade.go @@ -24,11 +24,11 @@ type Facade interface { LastBlockHeight() uint32 LastBlockHash() hash.Hash LastBlockTime() time.Time - LastCertificate() *certificate.Certificate + LastCertificate() *certificate.BlockCertificate UpdateLastCertificate(v *vote.Vote) error ProposeBlock(valKey *bls.ValidatorKey, rewardAddr crypto.Address) (*block.Block, error) ValidateBlock(blk *block.Block, round int16) error - CommitBlock(blk *block.Block, cert *certificate.Certificate) error + CommitBlock(blk *block.Block, cert *certificate.BlockCertificate) error CommitteeValidators() []*validator.Validator IsInCommittee(addr crypto.Address) bool Proposer(round int16) *validator.Validator diff --git a/state/lastinfo/last_info.go b/state/lastinfo/last_info.go index 4619c40ba..dfb52e4ef 100644 --- a/state/lastinfo/last_info.go +++ b/state/lastinfo/last_info.go @@ -21,7 +21,7 @@ type LastInfo struct { lastSortitionSeed sortition.VerifiableSeed lastBlockHash hash.Hash - lastCert *certificate.Certificate + lastCert *certificate.BlockCertificate lastBlockTime time.Time lastValidators []*validator.Validator } @@ -55,7 +55,7 @@ func (li *LastInfo) BlockHash() hash.Hash { return li.lastBlockHash } -func (li *LastInfo) Certificate() *certificate.Certificate { +func (li *LastInfo) Certificate() *certificate.BlockCertificate { li.lk.RLock() defer li.lk.RUnlock() @@ -90,7 +90,7 @@ func (li *LastInfo) UpdateBlockHash(lastBlockHash hash.Hash) { li.lastBlockHash = lastBlockHash } -func (li *LastInfo) UpdateCertificate(lastCertificate *certificate.Certificate) { +func (li *LastInfo) UpdateCertificate(lastCertificate *certificate.BlockCertificate) { li.lk.Lock() defer li.lk.Unlock() diff --git a/state/lastinfo/last_info_test.go b/state/lastinfo/last_info_test.go index 0746f63fe..c65919075 100644 --- a/state/lastinfo/last_info_test.go +++ b/state/lastinfo/last_info_test.go @@ -73,7 +73,7 @@ func setup(t *testing.T) *testData { ts.HelperSignTransaction(prv4, trx) prevHash := ts.RandHash() lastHeight := ts.RandHeight() - prevCert := ts.GenerateTestCertificate(lastHeight - 1) + prevCert := ts.GenerateTestBlockCertificate(lastHeight - 1) lastSeed := ts.RandSeed() lastBlock := block.MakeBlock(1, util.Now(), block.Txs{trx}, prevHash, @@ -81,7 +81,8 @@ func setup(t *testing.T) *testData { prevCert, lastSeed, val2.Address()) sig := valKey.Sign([]byte("fatdog")) - lastCert := certificate.NewCertificate(lastHeight, 0, committers, []int32{}, sig) + lastCert := certificate.NewBlockCertificate(lastHeight, 0, true) + lastCert.SetSignature(committers, []int32{}, sig) mockStore.SaveBlock(lastBlock, lastCert) assert.Equal(t, mockStore.LastHeight, lastHeight) diff --git a/state/mock.go b/state/mock.go index 415a4d2fa..f6aa842ba 100644 --- a/state/mock.go +++ b/state/mock.go @@ -90,7 +90,7 @@ func (m *MockState) LastBlockTime() time.Time { return m.Genesis().GenesisTime() } -func (m *MockState) LastCertificate() *certificate.Certificate { +func (m *MockState) LastCertificate() *certificate.BlockCertificate { m.lk.RLock() defer m.lk.RUnlock() @@ -101,7 +101,7 @@ func (*MockState) UpdateLastCertificate(_ *vote.Vote) error { return nil } -func (m *MockState) CommitBlock(b *block.Block, cert *certificate.Certificate) error { +func (m *MockState) CommitBlock(b *block.Block, cert *certificate.BlockCertificate) error { m.lk.Lock() defer m.lk.Unlock() diff --git a/state/score/score.go b/state/score/score.go index a76298538..debea936d 100644 --- a/state/score/score.go +++ b/state/score/score.go @@ -8,20 +8,20 @@ type scoreData struct { } type Manager struct { - certs map[uint32]*certificate.Certificate + certs map[uint32]*certificate.BlockCertificate vals map[int32]*scoreData maxCert uint32 } func NewScoreManager(maxCert uint32) *Manager { return &Manager{ - certs: make(map[uint32]*certificate.Certificate), + certs: make(map[uint32]*certificate.BlockCertificate), vals: make(map[int32]*scoreData), maxCert: maxCert, } } -func (sm *Manager) SetCertificate(cert *certificate.Certificate) { +func (sm *Manager) SetCertificate(cert *certificate.BlockCertificate) { lastHeight := cert.Height() sm.certs[lastHeight] = cert diff --git a/state/score/score_test.go b/state/score/score_test.go index a329017a5..dd15e4f3f 100644 --- a/state/score/score_test.go +++ b/state/score/score_test.go @@ -11,14 +11,23 @@ func TestScoreManager(t *testing.T) { maxCert := uint32(3) sm := NewScoreManager(maxCert) - cert1 := certificate.NewCertificate(1, 0, []int32{0, 1, 2, 3}, []int32{0}, nil) - cert2 := certificate.NewCertificate(2, 0, []int32{0, 1, 2, 3}, []int32{3}, nil) - cert3 := certificate.NewCertificate(3, 0, []int32{1, 2, 3, 4}, []int32{2}, nil) - cert4 := certificate.NewCertificate(4, 0, []int32{1, 2, 3, 4}, []int32{2}, nil) - cert5 := certificate.NewCertificate(5, 0, []int32{1, 2, 3, 4}, []int32{2}, nil) + cert1 := certificate.NewBlockCertificate(1, 0, false) + cert1.SetSignature([]int32{0, 1, 2, 3}, []int32{0}, nil) + + cert2 := certificate.NewBlockCertificate(2, 0, false) + cert2.SetSignature([]int32{0, 1, 2, 3}, []int32{3}, nil) + + cert3 := certificate.NewBlockCertificate(3, 0, false) + cert3.SetSignature([]int32{1, 2, 3, 4}, []int32{2}, nil) + + cert4 := certificate.NewBlockCertificate(4, 0, false) + cert4.SetSignature([]int32{1, 2, 3, 4}, []int32{2}, nil) + + cert5 := certificate.NewBlockCertificate(5, 0, false) + cert5.SetSignature([]int32{1, 2, 3, 4}, []int32{2}, nil) tests := []struct { - cert *certificate.Certificate + cert *certificate.BlockCertificate score0 float64 score1 float64 score2 float64 diff --git a/state/state.go b/state/state.go index e379806d9..f506ad9ea 100644 --- a/state/state.go +++ b/state/state.go @@ -198,7 +198,8 @@ func (st *state) loadMerkels() { st.store.IterateAccounts(func(_ crypto.Address, acc *account.Account) bool { // Let's keep this check, even we have tested it if acc.Number() >= totalAccount { - panic("Account number is out of range") + panic(fmt.Sprintf( + "Account number is out of range: %v >= %v", acc.Number(), totalAccount)) } st.accountMerkle.SetHash(int(acc.Number()), acc.Hash()) @@ -209,7 +210,8 @@ func (st *state) loadMerkels() { st.store.IterateValidators(func(val *validator.Validator) bool { // Let's keep this check, even we have tested it if val.Number() >= totalValidator { - panic("Validator number is out of range") + panic(fmt.Sprintf( + "Validator number is out of range: %v >= %v", val.Number(), totalValidator)) } st.validatorMerkle.SetHash(int(val.Number()), val.Hash()) @@ -293,7 +295,7 @@ func (st *state) LastBlockTime() time.Time { return st.lastInfo.BlockTime() } -func (st *state) LastCertificate() *certificate.Certificate { +func (st *state) LastCertificate() *certificate.BlockCertificate { st.lk.RLock() defer st.lk.RUnlock() @@ -417,7 +419,7 @@ func (st *state) ValidateBlock(blk *block.Block, round int16) error { return st.executeBlock(blk, sb) } -func (st *state) CommitBlock(blk *block.Block, cert *certificate.Certificate) error { +func (st *state) CommitBlock(blk *block.Block, cert *certificate.BlockCertificate) error { st.lk.Lock() defer st.lk.Unlock() @@ -428,7 +430,7 @@ func (st *state) CommitBlock(blk *block.Block, cert *certificate.Certificate) er return nil } - err := st.validateCertificate(cert, blk.Hash()) + err := st.validateCurCertificate(cert, blk.Hash()) if err != nil { return err } diff --git a/state/state_test.go b/state/state_test.go index d7fd413a7..c980f9962 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -39,9 +39,10 @@ func setup(t *testing.T) *testData { ts := testsuite.NewTestSuite(t) - genValKeys := make([]*bls.ValidatorKey, 0, 4) - genVals := make([]*validator.Validator, 0, 4) - for i := 0; i < 4; i++ { + genValNum := 6 + genValKeys := make([]*bls.ValidatorKey, 0, genValNum) + genVals := make([]*validator.Validator, 0, genValNum) + for i := 0; i < genValNum; i++ { valKey := ts.RandValKey() val := validator.NewValidator(valKey.PublicKey(), int32(i)) @@ -91,7 +92,7 @@ func setup(t *testing.T) *testData { } func (td *testData) makeBlockAndCertificate(t *testing.T, round int16) ( - *block.Block, *certificate.Certificate, + *block.Block, *certificate.BlockCertificate, ) { t.Helper() @@ -106,21 +107,26 @@ func (td *testData) makeBlockAndCertificate(t *testing.T, round int16) ( return blk, cert } -func (td *testData) makeCertificateAndSign(t *testing.T, blockHash hash.Hash, round int16) *certificate.Certificate { +func (td *testData) makeCertificateAndSign(t *testing.T, blockHash hash.Hash, + round int16, +) *certificate.BlockCertificate { t.Helper() sigs := make([]*bls.Signature, 0, len(td.genValKeys)) height := td.state.LastBlockHeight() - signBytes := certificate.BlockCertificateSignBytes(blockHash, height+1, round) - committers := []int32{0, 1, 2, 3} - absentees := []int32{3} + cert := certificate.NewBlockCertificate(height+1, round, true) + signBytes := cert.SignBytes(blockHash) + committers := []int32{0, 1, 2, 3, 4, 5} + absentees := []int32{5} for _, key := range td.genValKeys[:len(td.genValKeys)-1] { sig := key.Sign(signBytes) sigs = append(sigs, sig) } - return certificate.NewCertificate(height+1, round, committers, absentees, bls.SignatureAggregate(sigs...)) + cert.SetSignature(committers, absentees, bls.SignatureAggregate(sigs...)) + + return cert } func (td *testData) commitBlocks(t *testing.T, count int) { @@ -166,7 +172,7 @@ func TestTryCommitInvalidCertificate(t *testing.T) { td := setup(t) blk, _ := td.makeBlockAndCertificate(t, td.RandRound()) - invCert := td.GenerateTestCertificate(td.state.LastBlockHeight() + 1) + invCert := td.GenerateTestBlockCertificate(td.state.LastBlockHeight() + 1) assert.Error(t, td.state.CommitBlock(blk, invCert)) } @@ -396,7 +402,7 @@ func TestSortition(t *testing.T) { myValKey := td.state.valKeys[0] assert.False(t, td.state.evaluateSortition()) // not a validator assert.False(t, td.state.IsValidator(myValKey.Address())) - assert.Equal(t, td.state.CommitteePower(), int64(4)) + assert.Equal(t, td.state.CommitteePower(), int64(6)) trx := tx.NewBondTx(1, td.genAccKey.PublicKeyNative().AccountAddress(), myValKey.Address(), myValKey.PublicKey(), 1000000000, 100000, "") @@ -407,7 +413,7 @@ func TestSortition(t *testing.T) { assert.False(t, td.state.evaluateSortition()) // bonding period assert.True(t, td.state.IsValidator(myValKey.Address())) - assert.Equal(t, td.state.CommitteePower(), int64(4)) + assert.Equal(t, td.state.CommitteePower(), int64(6)) assert.False(t, td.state.committee.Contains(myValKey.Address())) // Not in the committee // Committing another 10 blocks @@ -419,7 +425,7 @@ func TestSortition(t *testing.T) { td.commitBlocks(t, 1) assert.True(t, td.state.IsValidator(myValKey.Address())) - assert.Equal(t, td.state.CommitteePower(), int64(1000000004)) + assert.Equal(t, td.state.CommitteePower(), int64(1000000006)) assert.True(t, td.state.committee.Contains(myValKey.Address())) // In the committee } @@ -557,7 +563,7 @@ func TestLoadState(t *testing.T) { assert.ElementsMatch(t, td.state.ValidatorAddresses(), newState.ValidatorAddresses()) assert.Equal(t, int32(13), td.state.TotalAccounts()) // 11 subsidy addrs + 2 genesis addrs - assert.Equal(t, int32(5), td.state.TotalValidators()) + assert.Equal(t, int32(7), td.state.TotalValidators()) // Try committing the next block require.NoError(t, newState.CommitBlock(blk6, cert6)) @@ -571,7 +577,7 @@ func TestLoadStateAfterChangingGenesis(t *testing.T) { require.NoError(t, err) pub, _ := td.RandBLSKeyPair() - val := validator.NewValidator(pub, 4) + val := validator.NewValidator(pub, 6) newVals := append(td.state.genDoc.Validators(), val) genDoc := genesis.MakeGenesis( @@ -590,8 +596,8 @@ func TestIsValidator(t *testing.T) { td := setup(t) assert.True(t, td.state.IsInCommittee(td.genValKeys[0].Address())) - assert.True(t, td.state.IsProposer(td.genValKeys[2].Address(), 0)) - assert.True(t, td.state.IsProposer(td.genValKeys[3].Address(), 1)) + assert.True(t, td.state.IsProposer(td.genValKeys[4].Address(), 0)) + assert.True(t, td.state.IsProposer(td.genValKeys[5].Address(), 1)) assert.True(t, td.state.IsInCommittee(td.genValKeys[1].Address())) assert.True(t, td.state.IsValidator(td.genValKeys[1].Address())) diff --git a/state/validation.go b/state/validation.go index 8348ee98a..253460805 100644 --- a/state/validation.go +++ b/state/validation.go @@ -34,7 +34,7 @@ func (st *state) doValidateBlock(blk *block.Block, round int16) error { } // validatePrevCertificate validates certificate for the previous block. -func (st *state) validatePrevCertificate(cert *certificate.Certificate, blockHash hash.Hash) error { +func (st *state) validatePrevCertificate(cert *certificate.BlockCertificate, blockHash hash.Hash) error { if cert == nil { if !st.lastInfo.BlockHash().IsUndef() { return errors.Errorf(errors.ErrInvalidBlock, @@ -42,15 +42,14 @@ func (st *state) validatePrevCertificate(cert *certificate.Certificate, blockHas } } else { if cert.Round() != st.lastInfo.Certificate().Round() { - // TODO: we should panic here. + // TODO: we should panic here? // It is impossible, unless we have a fork on the latest block - return InvalidCertificateError{ + return InvalidBlockCertificateError{ Cert: cert, } } - signBytes := certificate.BlockCertificateSignBytes(blockHash, cert.Height(), cert.Round()) - err := cert.Validate(st.lastInfo.BlockHeight(), st.lastInfo.Validators(), signBytes) + err := cert.Validate(st.lastInfo.Validators(), blockHash) if err != nil { return err } @@ -59,10 +58,9 @@ func (st *state) validatePrevCertificate(cert *certificate.Certificate, blockHas return nil } -// validateCertificate validates certificate for the current height. -func (st *state) validateCertificate(cert *certificate.Certificate, blockHash hash.Hash) error { - signBytes := certificate.BlockCertificateSignBytes(blockHash, cert.Height(), cert.Round()) - err := cert.Validate(st.lastInfo.BlockHeight()+1, st.committee.Validators(), signBytes) +// validateCurCertificate validates certificate for the current height. +func (st *state) validateCurCertificate(cert *certificate.BlockCertificate, blockHash hash.Hash) error { + err := cert.Validate(st.committee.Validators(), blockHash) if err != nil { return err } diff --git a/state/validation_test.go b/state/validation_test.go index 61c82e1ca..d4a05a7a5 100644 --- a/state/validation_test.go +++ b/state/validation_test.go @@ -74,9 +74,12 @@ func TestBlockValidation(t *testing.T) { t.Run("Invalid PrevCertificate", func(t *testing.T) { blk0, _ := td.makeBlockAndCertificate(t, round) - invPrevCertificate := certificate.NewCertificate( + invPrevCert := certificate.NewBlockCertificate( blk0.PrevCertificate().Height(), blk0.PrevCertificate().Round(), + blk0.PrevCertificate().FastPath(), + ) + invPrevCert.SetSignature( blk0.PrevCertificate().Committers(), blk0.PrevCertificate().Absentees(), td.RandBLSSignature()) @@ -87,7 +90,7 @@ func TestBlockValidation(t *testing.T) { blk0.Transactions(), blk0.Header().PrevBlockHash(), blk0.Header().StateRoot(), - invPrevCertificate, + invPrevCert, blk0.Header().SortitionSeed(), blk0.Header().ProposerAddress()) cert := td.makeCertificateAndSign(t, blk.Hash(), round) diff --git a/store/interface.go b/store/interface.go index b07d4bb2f..5c51ecacf 100644 --- a/store/interface.go +++ b/store/interface.go @@ -96,7 +96,7 @@ type Reader interface { IterateValidators(consumer func(*validator.Validator) (stop bool)) IterateAccounts(consumer func(crypto.Address, *account.Account) (stop bool)) TotalValidators() int32 - LastCertificate() *certificate.Certificate + LastCertificate() *certificate.BlockCertificate IsBanned(addr crypto.Address) bool } @@ -105,7 +105,7 @@ type Store interface { UpdateAccount(addr crypto.Address, acc *account.Account) UpdateValidator(val *validator.Validator) - SaveBlock(blk *block.Block, cert *certificate.Certificate) + SaveBlock(blk *block.Block, cert *certificate.BlockCertificate) WriteBatch() error Close() } diff --git a/store/mock.go b/store/mock.go index b531b8442..117e42011 100644 --- a/store/mock.go +++ b/store/mock.go @@ -23,7 +23,7 @@ type MockStore struct { Blocks map[uint32]*block.Block Accounts map[crypto.Address]*account.Account Validators map[crypto.Address]*validator.Validator - LastCert *certificate.Certificate + LastCert *certificate.BlockCertificate LastHeight uint32 } @@ -227,13 +227,13 @@ func (m *MockStore) IterateValidators(consumer func(*validator.Validator) (stop } } -func (m *MockStore) SaveBlock(b *block.Block, cert *certificate.Certificate) { +func (m *MockStore) SaveBlock(b *block.Block, cert *certificate.BlockCertificate) { m.Blocks[cert.Height()] = b m.LastHeight = cert.Height() m.LastCert = cert } -func (m *MockStore) LastCertificate() *certificate.Certificate { +func (m *MockStore) LastCertificate() *certificate.BlockCertificate { if m.LastHeight == 0 { return nil } diff --git a/store/store.go b/store/store.go index e9067e5ca..0621b85ed 100644 --- a/store/store.go +++ b/store/store.go @@ -138,7 +138,7 @@ func (s *store) Close() { } } -func (s *store) SaveBlock(blk *block.Block, cert *certificate.Certificate) { +func (s *store) SaveBlock(blk *block.Block, cert *certificate.BlockCertificate) { s.lk.Lock() defer s.lk.Unlock() @@ -337,7 +337,7 @@ func (s *store) UpdateValidator(acc *validator.Validator) { s.validatorStore.updateValidator(s.batch, acc) } -func (s *store) LastCertificate() *certificate.Certificate { +func (s *store) LastCertificate() *certificate.BlockCertificate { s.lk.Lock() defer s.lk.Unlock() @@ -348,7 +348,7 @@ func (s *store) LastCertificate() *certificate.Certificate { } r := bytes.NewReader(data) version := int32(0) - cert := new(certificate.Certificate) + cert := new(certificate.BlockCertificate) err := encoding.ReadElements(r, &version) if err != nil { return nil diff --git a/sync/bundle/message/block_announce.go b/sync/bundle/message/block_announce.go index 09b6aed4a..e7696cd21 100644 --- a/sync/bundle/message/block_announce.go +++ b/sync/bundle/message/block_announce.go @@ -8,11 +8,11 @@ import ( ) type BlockAnnounceMessage struct { - Block *block.Block `cbor:"1,keyasint"` - Certificate *certificate.Certificate `cbor:"2,keyasint"` + Block *block.Block `cbor:"1,keyasint"` + Certificate *certificate.BlockCertificate `cbor:"2,keyasint"` } -func NewBlockAnnounceMessage(blk *block.Block, cert *certificate.Certificate) *BlockAnnounceMessage { +func NewBlockAnnounceMessage(blk *block.Block, cert *certificate.BlockCertificate) *BlockAnnounceMessage { return &BlockAnnounceMessage{ Block: blk, Certificate: cert, diff --git a/sync/bundle/message/block_announce_test.go b/sync/bundle/message/block_announce_test.go index 9c1b9ab21..787745152 100644 --- a/sync/bundle/message/block_announce_test.go +++ b/sync/bundle/message/block_announce_test.go @@ -19,7 +19,7 @@ func TestBlockAnnounceMessage(t *testing.T) { t.Run("Invalid certificate", func(t *testing.T) { blk, _ := ts.GenerateTestBlock(ts.RandHeight()) - cert := certificate.NewCertificate(0, 0, nil, nil, nil) + cert := certificate.NewBlockCertificate(0, 0, false) m := NewBlockAnnounceMessage(blk, cert) err := m.BasicCheck() diff --git a/sync/bundle/message/blocks_response.go b/sync/bundle/message/blocks_response.go index 86e4e7f78..fab79148c 100644 --- a/sync/bundle/message/blocks_response.go +++ b/sync/bundle/message/blocks_response.go @@ -7,16 +7,16 @@ import ( ) type BlocksResponseMessage struct { - ResponseCode ResponseCode `cbor:"1,keyasint"` - SessionID int `cbor:"2,keyasint"` - From uint32 `cbor:"3,keyasint"` - CommittedBlocksData [][]byte `cbor:"4,keyasint"` - LastCertificate *certificate.Certificate `cbor:"5,keyasint"` - Reason string `cbor:"6,keyasint"` + ResponseCode ResponseCode `cbor:"1,keyasint"` + SessionID int `cbor:"2,keyasint"` + From uint32 `cbor:"3,keyasint"` + CommittedBlocksData [][]byte `cbor:"4,keyasint"` + LastCertificate *certificate.BlockCertificate `cbor:"5,keyasint"` + Reason string `cbor:"6,keyasint"` } func NewBlocksResponseMessage(code ResponseCode, reason string, sid int, from uint32, - blocksData [][]byte, lastCert *certificate.Certificate, + blocksData [][]byte, lastCert *certificate.BlockCertificate, ) *BlocksResponseMessage { return &BlocksResponseMessage{ ResponseCode: code, diff --git a/sync/bundle/message/blocks_response_test.go b/sync/bundle/message/blocks_response_test.go index b8eb43e66..6e0cba06f 100644 --- a/sync/bundle/message/blocks_response_test.go +++ b/sync/bundle/message/blocks_response_test.go @@ -19,7 +19,7 @@ func TestBlocksResponseMessage(t *testing.T) { sid := 123 t.Run("Invalid certificate", func(t *testing.T) { blk, _ := ts.GenerateTestBlock(ts.RandHeight()) - cert := certificate.NewCertificate(0, 0, nil, nil, nil) + cert := certificate.NewBlockCertificate(0, 0, false) d, _ := blk.Bytes() m := NewBlocksResponseMessage(ResponseCodeMoreBlocks, ResponseCodeMoreBlocks.String(), sid, ts.RandHeight(), [][]byte{d}, cert) diff --git a/sync/cache/cache.go b/sync/cache/cache.go index 6557e7486..5216f862a 100644 --- a/sync/cache/cache.go +++ b/sync/cache/cache.go @@ -9,7 +9,7 @@ import ( type Cache struct { blocks *lru.Cache[uint32, *block.Block] // it's thread safe - certs *lru.Cache[uint32, *certificate.Certificate] + certs *lru.Cache[uint32, *certificate.BlockCertificate] } func NewCache(size int) (*Cache, error) { @@ -18,7 +18,7 @@ func NewCache(size int) (*Cache, error) { return nil, err } - c, err := lru.New[uint32, *certificate.Certificate](size) + c, err := lru.New[uint32, *certificate.BlockCertificate](size) if err != nil { return nil, err } @@ -52,7 +52,7 @@ func (c *Cache) AddBlock(blk *block.Block) { } } -func (c *Cache) GetCertificate(height uint32) *certificate.Certificate { +func (c *Cache) GetCertificate(height uint32) *certificate.BlockCertificate { cert, ok := c.certs.Get(height) if ok { return cert @@ -61,7 +61,7 @@ func (c *Cache) GetCertificate(height uint32) *certificate.Certificate { return nil } -func (c *Cache) AddCertificate(cert *certificate.Certificate) { +func (c *Cache) AddCertificate(cert *certificate.BlockCertificate) { if cert != nil { c.certs.Add(cert.Height(), cert) } diff --git a/sync/handler_block_announce_test.go b/sync/handler_block_announce_test.go index f0de2f572..5c02b0a0a 100644 --- a/sync/handler_block_announce_test.go +++ b/sync/handler_block_announce_test.go @@ -38,7 +38,7 @@ func TestInvalidBlockAnnounce(t *testing.T) { pid := td.RandPeerID() height := td.state.LastBlockHeight() + 1 blk, _ := td.GenerateTestBlock(height) - invCert := certificate.NewCertificate(height, 0, nil, nil, nil) + invCert := certificate.NewBlockCertificate(height, 0, false) msg := message.NewBlockAnnounceMessage(blk, invCert) err := td.receivingNewMessage(td.sync, msg, pid) diff --git a/sync/handler_blocks_response_test.go b/sync/handler_blocks_response_test.go index f004b7d28..1e78b3c2d 100644 --- a/sync/handler_blocks_response_test.go +++ b/sync/handler_blocks_response_test.go @@ -28,8 +28,8 @@ func TestInvalidBlockData(t *testing.T) { td.state.CommitTestBlocks(10) lastHeight := td.state.LastBlockHeight() - prevCert := td.GenerateTestCertificate(lastHeight) - cert := td.GenerateTestCertificate(lastHeight + 1) + prevCert := td.GenerateTestBlockCertificate(lastHeight) + cert := td.GenerateTestBlockCertificate(lastHeight + 1) blk := block.MakeBlock(1, time.Now(), nil, td.RandHash(), td.RandHash(), prevCert, td.RandSeed(), td.RandValAddress()) data, _ := blk.Bytes() @@ -91,7 +91,7 @@ func TestStrippedPublicKey(t *testing.T) { trxs0 := []*tx.Tx{trx0} blk0 := block.MakeBlock(1, time.Now(), trxs0, td.RandHash(), td.RandHash(), td.state.LastCertificate(), td.RandSeed(), td.RandValAddress()) - cert0 := td.GenerateTestCertificate(lastHeight + 1) + cert0 := td.GenerateTestBlockCertificate(lastHeight + 1) err := td.state.CommitBlock(blk0, cert0) require.NoError(t, err) lastHeight++ @@ -132,7 +132,7 @@ func TestStrippedPublicKey(t *testing.T) { for _, test := range tests { blkData, _ := test.blk.Bytes() sid := td.RandInt(1000) - cert := td.GenerateTestCertificate(lastHeight + 1) + cert := td.GenerateTestBlockCertificate(lastHeight + 1) msg := message.NewBlocksResponseMessage(message.ResponseCodeMoreBlocks, message.ResponseCodeMoreBlocks.String(), sid, lastHeight+1, [][]byte{blkData}, cert) err := td.receivingNewMessage(td.sync, msg, pid) diff --git a/tests/main_test.go b/tests/main_test.go index 7f402879f..f611451ec 100644 --- a/tests/main_test.go +++ b/tests/main_test.go @@ -43,8 +43,10 @@ const ( tNodeIdx2 = 1 tNodeIdx3 = 2 tNodeIdx4 = 3 - tTotalNodes = 4 // each node has 3 validators - tCommitteeSize = 7 + tNodeIdx5 = 4 + tNodeIdx6 = 5 + tTotalNodes = 6 // each node has 3 validators + tCommitteeSize = 11 ) func TestMain(m *testing.M) { @@ -114,11 +116,13 @@ func TestMain(m *testing.M) { key.PublicKeyNative().AccountAddress(): acc2, } - vals := make([]*validator.Validator, 4) + vals := make([]*validator.Validator, 6) vals[0] = validator.NewValidator(tValKeys[tNodeIdx1][0].PublicKey(), 0) vals[1] = validator.NewValidator(tValKeys[tNodeIdx2][0].PublicKey(), 1) vals[2] = validator.NewValidator(tValKeys[tNodeIdx3][0].PublicKey(), 2) vals[3] = validator.NewValidator(tValKeys[tNodeIdx4][0].PublicKey(), 3) + vals[4] = validator.NewValidator(tValKeys[tNodeIdx5][0].PublicKey(), 4) + vals[5] = validator.NewValidator(tValKeys[tNodeIdx6][0].PublicKey(), 5) params := param.DefaultParams() params.MinimumStake = 1000 params.BlockIntervalInSecond = 2 @@ -186,7 +190,7 @@ func TestMain(m *testing.M) { if block.Height == 1 { panic("block height should be greater than 1") } - if len(cert.Committers) == 4 { + if len(cert.Committers) == 7 { panic("Sortition didn't work") } diff --git a/types/block/block.go b/types/block/block.go index 2fefb0cc6..bbd442065 100644 --- a/types/block/block.go +++ b/types/block/block.go @@ -24,11 +24,11 @@ type Block struct { type blockData struct { Header *Header - PrevCert *certificate.Certificate + PrevCert *certificate.BlockCertificate Txs Txs } -func NewBlock(header *Header, prevCert *certificate.Certificate, txs Txs) *Block { +func NewBlock(header *Header, prevCert *certificate.BlockCertificate, txs Txs) *Block { return &Block{ data: blockData{ Header: header, @@ -51,7 +51,7 @@ func FromBytes(data []byte) (*Block, error) { func MakeBlock(version uint8, timestamp time.Time, txs Txs, prevBlockHash, stateRoot hash.Hash, - prevCert *certificate.Certificate, sortitionSeed sortition.VerifiableSeed, proposer crypto.Address, + prevCert *certificate.BlockCertificate, sortitionSeed sortition.VerifiableSeed, proposer crypto.Address, ) *Block { header := NewHeader(version, timestamp, stateRoot, prevBlockHash, sortitionSeed, proposer) @@ -63,7 +63,7 @@ func (b *Block) Header() *Header { return b.data.Header } -func (b *Block) PrevCertificate() *certificate.Certificate { +func (b *Block) PrevCertificate() *certificate.BlockCertificate { return b.data.PrevCert } @@ -190,7 +190,7 @@ func (b *Block) Decode(r io.Reader) error { return err } if !b.data.Header.PrevBlockHash().IsUndef() { - b.data.PrevCert = new(certificate.Certificate) + b.data.PrevCert = new(certificate.BlockCertificate) if err := b.data.PrevCert.Decode(r); err != nil { return err } diff --git a/types/certificate/block_certificate.go b/types/certificate/block_certificate.go new file mode 100644 index 000000000..ec4c019ee --- /dev/null +++ b/types/certificate/block_certificate.go @@ -0,0 +1,71 @@ +package certificate + +import ( + "github.com/pactus-project/pactus/crypto/bls" + "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/types/validator" + "github.com/pactus-project/pactus/util" +) + +// BlockCertificate represents a certificate used for block validation, +// verifying if a block is signed by a majority of validators. +type BlockCertificate struct { + baseCertificate +} + +// NewBlockCertificate creates a new BlockCertificate. +func NewBlockCertificate(height uint32, round int16, fastPath bool) *BlockCertificate { + return &BlockCertificate{ + baseCertificate: baseCertificate{ + height: height, + round: round, + fastPath: fastPath, + }, + } +} + +func (cert *BlockCertificate) SignBytes(blockHash hash.Hash) []byte { + sb := blockHash.Bytes() + sb = append(sb, util.Uint32ToSlice(cert.height)...) + sb = append(sb, util.Int16ToSlice(cert.round)...) + + if cert.fastPath { + sb = append(sb, util.StringToBytes("PREPARE")...) + } + + return sb +} + +func (cert *BlockCertificate) Validate(validators []*validator.Validator, blockHash hash.Hash) error { + calcRequiredPowerFn := func(committeePower int64) int64 { + t := (committeePower - 1) / 5 + p := (3 * t) + 1 + if cert.fastPath { + p = (4 * t) + 1 + } + + return p + } + + signBytes := cert.SignBytes(blockHash) + + return cert.baseCertificate.validate(validators, signBytes, calcRequiredPowerFn) +} + +func (cert *BlockCertificate) Clone() *BlockCertificate { + cloned := &BlockCertificate{ + baseCertificate: baseCertificate{ + height: cert.height, + round: cert.round, + committers: make([]int32, len(cert.committers)), + absentees: make([]int32, len(cert.absentees)), + signature: new(bls.Signature), + }, + } + + copy(cloned.committers, cert.committers) + copy(cloned.absentees, cert.absentees) + *cloned.signature = *cert.signature + + return cloned +} diff --git a/types/certificate/block_certificate_test.go b/types/certificate/block_certificate_test.go new file mode 100644 index 000000000..81502f289 --- /dev/null +++ b/types/certificate/block_certificate_test.go @@ -0,0 +1,257 @@ +package certificate_test + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/fxamacker/cbor/v2" + "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/crypto/bls" + "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/types/certificate" + "github.com/pactus-project/pactus/types/validator" + "github.com/pactus-project/pactus/util/testsuite" + "github.com/stretchr/testify/assert" + "golang.org/x/exp/slices" +) + +func TestBlockCertificate(t *testing.T) { + expectedData, _ := hex.DecodeString( + "04030201" + // Height + "0100" + // Round + "06010203040506" + // Committers + "0102" + // Absentees + "b53d79e156e9417e010fa21f2b2a96bee6be46fcd233295d2f697cdb9e782b6112ac01c80d0d9d64c2320664c77fa2a6") // Signature + + certHash, _ := hash.FromString("ac755295a6850b141286bde42bb8ba06ae1671f0562cbef90043924091177815") + r := bytes.NewReader(expectedData) + cert := new(certificate.BlockCertificate) + err := cert.Decode(r) + assert.NoError(t, err) + assert.Equal(t, cert.Height(), uint32(0x01020304)) + assert.Equal(t, cert.Round(), int16(0x0001)) + assert.Equal(t, cert.FastPath(), false) + assert.Equal(t, cert.Committers(), []int32{1, 2, 3, 4, 5, 6}) + assert.Equal(t, cert.Absentees(), []int32{2}) + assert.Equal(t, cert.Hash(), certHash) + + blockHash, _ := hash.FromString("000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f") + expectedSignByte, _ := hex.DecodeString( + "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f" + // Block hash + "04030201" + // Height + "0100") // Round + + assert.Equal(t, expectedSignByte, cert.SignBytes(blockHash)) +} + +func TestBlockCertificateFastPath(t *testing.T) { + expectedData, _ := hex.DecodeString( + "04030201" + // Height + "0180" + // Round + "06010203040506" + // Committers + "0102" + // Absentees + "b53d79e156e9417e010fa21f2b2a96bee6be46fcd233295d2f697cdb9e782b6112ac01c80d0d9d64c2320664c77fa2a6") // Signature + + certHash, _ := hash.FromString("e4ef7d58c1e6e10537e9d8047b8e5d619c0d21745cd2ae528b94543ca016f32f") + r := bytes.NewReader(expectedData) + cert := new(certificate.BlockCertificate) + err := cert.Decode(r) + assert.NoError(t, err) + assert.Equal(t, cert.Height(), uint32(0x01020304)) + assert.Equal(t, cert.Round(), int16(0x0001)) + assert.Equal(t, cert.FastPath(), true) + assert.Equal(t, cert.Committers(), []int32{1, 2, 3, 4, 5, 6}) + assert.Equal(t, cert.Absentees(), []int32{2}) + assert.Equal(t, cert.Hash(), certHash) + + blockHash, _ := hash.FromString("000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f") + expectedSignByte, _ := hex.DecodeString( + "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f" + // Block hash + "04030201" + // Height + "0100" + // Round + "50524550415245") // "Prepare" + + assert.Equal(t, expectedSignByte, cert.SignBytes(blockHash)) +} + +func TestBlockCertificateCBORMarshaling(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + cert1 := ts.GenerateTestBlockCertificate(ts.RandHeight()) + bz1, err := cbor.Marshal(cert1) + assert.NoError(t, err) + var cert2 certificate.BlockCertificate + err = cbor.Unmarshal(bz1, &cert2) + assert.NoError(t, err) + assert.NoError(t, cert2.BasicCheck()) + assert.Equal(t, cert1.Hash(), cert1.Hash()) + + assert.Equal(t, cert1.Hash(), cert2.Hash()) + + err = cbor.Unmarshal([]byte{1}, &cert2) + assert.Error(t, err) +} + +func TestBlockCertificateSignBytes(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + h := ts.RandHash() + height := ts.RandHeight() + round := ts.RandRound() + cert1 := certificate.NewBlockCertificate(height, round, true) + cert2 := certificate.NewBlockCertificate(height, round, false) + + sb1 := cert1.SignBytes(h) + sb2 := cert2.SignBytes(h) + + assert.NotEqual(t, sb1, sb2) + assert.Contains(t, string(sb1), "PREPARE") +} + +func TestBlockCertificateHash(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + height := ts.RandHeight() + round := ts.RandRound() + committers := []int32{1, 2, 3, 4, 5, 6} + absentees := []int32{6} + sig := ts.RandBLSSignature() + + cert1 := certificate.NewBlockCertificate(height, round, true) + cert1.SetSignature(committers, absentees, sig) + + cert2 := certificate.NewBlockCertificate(height, round, false) + cert2.SetSignature(committers, absentees, sig) + + assert.NotEqual(t, cert1.Hash(), cert2.Hash()) +} + +func TestBlockCertificateValidation(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + blockHash := ts.RandHash() + height := ts.RandHeight() + round := ts.RandRound() + cert := certificate.NewBlockCertificate(height, round, false) + signBytes := cert.SignBytes(blockHash) + committers := ts.RandSlice(6) + sigs := []*bls.Signature{} + validators := []*validator.Validator{} + + for _, committer := range committers { + valKey := ts.RandValKey() + val := validator.NewValidator(valKey.PublicKey(), committer) + sig := valKey.Sign(signBytes) + + validators = append(validators, val) + sigs = append(sigs, sig) + } + + t.Run("Invalid committer, should return error", func(t *testing.T) { + invCommitters := slices.Clone(committers) + invCommitters = append(invCommitters, ts.Rand.Int31n(10000)) + absentees := committers[4:] + aggSig := bls.SignatureAggregate(sigs[:4]...) + cert.SetSignature(invCommitters, absentees, aggSig) + + err := cert.Validate(validators, blockHash) + assert.ErrorIs(t, err, certificate.UnexpectedCommittersError{ + Committers: invCommitters, + }) + }) + + t.Run("Invalid validator", func(t *testing.T) { + absentees := committers[4:] + aggSig := bls.SignatureAggregate(sigs[:4]...) + cert.SetSignature(committers, absentees, aggSig) + + invValidators := slices.Clone(validators) + invValidators[0], _ = ts.GenerateTestValidator(0) + err := cert.Validate(invValidators, blockHash) + assert.ErrorIs(t, err, certificate.UnexpectedCommittersError{ + Committers: committers, + }) + }) + + t.Run("Doesn't have 3t+1 majority", func(t *testing.T) { + absentees := committers[3:] + aggSig := bls.SignatureAggregate(sigs[:3]...) + cert.SetSignature(committers, absentees, aggSig) + + err := cert.Validate(validators, blockHash) + assert.ErrorIs(t, err, certificate.InsufficientPowerError{ + SignedPower: 3, + RequiredPower: 4, + }) + }) + + t.Run("One signature short, should return an error for invalid signature", func(t *testing.T) { + absentees := committers[4:] + aggSig := bls.SignatureAggregate(sigs[3:]...) + cert.SetSignature(committers, absentees, aggSig) + + err := cert.Validate(validators, blockHash) + assert.ErrorIs(t, err, crypto.ErrInvalidSignature) + }) + + t.Run("Ok, should return no error", func(t *testing.T) { + absentees := committers[4:] + aggSig := bls.SignatureAggregate(sigs[:4]...) + cert.SetSignature(committers, absentees, aggSig) + + err := cert.Validate(validators, blockHash) + assert.NoError(t, err) + }) +} + +func TestBlockCertificateValidationFastPath(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + blockHash := ts.RandHash() + height := ts.RandHeight() + round := ts.RandRound() + cert := certificate.NewBlockCertificate(height, round, true) + signBytes := cert.SignBytes(blockHash) + committers := ts.RandSlice(6) + sigs := []*bls.Signature{} + validators := []*validator.Validator{} + + for _, committer := range committers { + valKey := ts.RandValKey() + val := validator.NewValidator(valKey.PublicKey(), committer) + sig := valKey.Sign(signBytes) + + validators = append(validators, val) + sigs = append(sigs, sig) + } + + t.Run("Invalid signature", func(t *testing.T) { + aggSig := ts.RandBLSSignature() + cert.SetSignature(committers, nil, aggSig) + + err := cert.Validate(validators, blockHash) + assert.ErrorIs(t, err, crypto.ErrInvalidSignature) + }) + + t.Run("Doesn't have 4t+1 majority", func(t *testing.T) { + absentees := committers[4:] + aggSig := bls.SignatureAggregate(sigs[:4]...) + cert.SetSignature(committers, absentees, aggSig) + + err := cert.Validate(validators, blockHash) + assert.ErrorIs(t, err, certificate.InsufficientPowerError{ + SignedPower: 4, + RequiredPower: 5, + }) + }) + + t.Run("Ok, should return no error", func(t *testing.T) { + absentees := committers[5:] + aggSig := bls.SignatureAggregate(sigs[:5]...) + cert.SetSignature(committers, absentees, aggSig) + + err := cert.Validate(validators, blockHash) + assert.NoError(t, err) + }) +} diff --git a/types/certificate/certificate.go b/types/certificate/certificate.go index bbd1d477c..a99c9fb79 100644 --- a/types/certificate/certificate.go +++ b/types/certificate/certificate.go @@ -13,88 +13,79 @@ import ( "github.com/pactus-project/pactus/util/encoding" ) -type Certificate struct { - data certificateData +// baseCertificate represents a base structure for both BlockCertificate and VoteCertificate. +// As a BlockCertificate, it verifies if a block is signed by a majority of validators. +// As a VoteCertificate, it checks whether a majority of validators have voted in the consensus step. +type baseCertificate struct { + height uint32 + round int16 + fastPath bool + committers []int32 + absentees []int32 + signature *bls.Signature } -type certificateData struct { - Height uint32 - Round int16 - Committers []int32 - Absentees []int32 - Signature *bls.Signature -} - -func NewCertificate(height uint32, round int16, committers, absentees []int32, signature *bls.Signature) *Certificate { - cert := &Certificate{ - data: certificateData{ - Height: height, - Round: round, - Committers: committers, - Absentees: absentees, - Signature: signature, - }, - } - return cert +func (cert *baseCertificate) Height() uint32 { + return cert.height } -func (cert *Certificate) Height() uint32 { - return cert.data.Height +func (cert *baseCertificate) Round() int16 { + return cert.round } -func (cert *Certificate) Round() int16 { - return cert.data.Round +func (cert *baseCertificate) FastPath() bool { + return cert.fastPath } -func (cert *Certificate) Committers() []int32 { - return cert.data.Committers +func (cert *baseCertificate) Committers() []int32 { + return cert.committers } -func (cert *Certificate) Absentees() []int32 { - return cert.data.Absentees +func (cert *baseCertificate) Absentees() []int32 { + return cert.absentees } -func (cert *Certificate) Signature() *bls.Signature { - return cert.data.Signature +func (cert *baseCertificate) Signature() *bls.Signature { + return cert.signature } -func (cert *Certificate) BasicCheck() error { - if cert.Height() <= 0 { +func (cert *baseCertificate) BasicCheck() error { + if cert.height <= 0 { return BasicCheckError{ - Reason: fmt.Sprintf("height is not positive: %d", cert.Height()), + Reason: fmt.Sprintf("height is not positive: %d", cert.height), } } - if cert.Round() < 0 { + if cert.round < 0 { return BasicCheckError{ - Reason: fmt.Sprintf("round is negative: %d", cert.Round()), + Reason: fmt.Sprintf("round is negative: %d", cert.round), } } - if cert.Signature() == nil { + if cert.signature == nil { return BasicCheckError{ Reason: "signature is missing", } } - if cert.Committers() == nil { + if cert.committers == nil { return BasicCheckError{ Reason: "committers is missing", } } - if cert.Absentees() == nil { + if cert.absentees == nil { return BasicCheckError{ Reason: "absentees is missing", } } - if !util.IsSubset(cert.Committers(), cert.Absentees()) { + if !util.IsSubset(cert.committers, cert.absentees) { return BasicCheckError{ Reason: fmt.Sprintf("absentees are not a subset of committers: %v, %v", - cert.Committers(), cert.Absentees()), + cert.committers, cert.absentees), } } return nil } -func (cert *Certificate) Hash() hash.Hash { +func (cert *baseCertificate) Hash() hash.Hash { w := bytes.NewBuffer(make([]byte, 0, cert.SerializeSize())) if err := cert.Encode(w); err != nil { return hash.UndefHash @@ -103,43 +94,31 @@ func (cert *Certificate) Hash() hash.Hash { return hash.CalcHash(w.Bytes()) } -func (cert *Certificate) Clone() *Certificate { - cloned := &Certificate{ - data: certificateData{ - Height: cert.Height(), - Round: cert.Round(), - Committers: make([]int32, len(cert.data.Committers)), - Absentees: make([]int32, len(cert.data.Absentees)), - Signature: new(bls.Signature), - }, - } - - copy(cloned.data.Committers, cert.data.Committers) - copy(cloned.data.Absentees, cert.data.Absentees) - *cloned.data.Signature = *cert.data.Signature - - return cloned +func (cert *baseCertificate) SetSignature(committers, absentees []int32, signature *bls.Signature) { + cert.committers = committers + cert.absentees = absentees + cert.signature = signature } // SerializeSize returns the number of bytes it would take to serialize the block. -func (cert *Certificate) SerializeSize() int { +func (cert *baseCertificate) SerializeSize() int { sz := 6 + // height (4) + round(2) - encoding.VarIntSerializeSize(uint64(len(cert.Committers()))) + - encoding.VarIntSerializeSize(uint64(len(cert.Absentees()))) + + encoding.VarIntSerializeSize(uint64(len(cert.committers))) + + encoding.VarIntSerializeSize(uint64(len(cert.absentees))) + bls.SignatureSize - for _, n := range cert.Committers() { + for _, n := range cert.committers { sz += encoding.VarIntSerializeSize(uint64(n)) } - for _, n := range cert.Absentees() { + for _, n := range cert.absentees { sz += encoding.VarIntSerializeSize(uint64(n)) } return sz } -func (cert *Certificate) MarshalCBOR() ([]byte, error) { +func (cert *baseCertificate) MarshalCBOR() ([]byte, error) { buf := bytes.NewBuffer(make([]byte, 0, cert.SerializeSize())) if err := cert.Encode(buf); err != nil { return nil, err @@ -148,7 +127,7 @@ func (cert *Certificate) MarshalCBOR() ([]byte, error) { return cbor.Marshal(buf.Bytes()) } -func (cert *Certificate) UnmarshalCBOR(bs []byte) error { +func (cert *baseCertificate) UnmarshalCBOR(bs []byte) error { data := make([]byte, 0, cert.SerializeSize()) err := cbor.Unmarshal(bs, &data) if err != nil { @@ -159,36 +138,47 @@ func (cert *Certificate) UnmarshalCBOR(bs []byte) error { return cert.Decode(buf) } -func (cert *Certificate) Encode(w io.Writer) error { - if err := encoding.WriteElements(w, cert.data.Height, cert.data.Round); err != nil { +func (cert *baseCertificate) Encode(w io.Writer) error { + roundAndPath := uint16(cert.round) + if cert.FastPath() { + roundAndPath |= 0x8000 + } + + if err := encoding.WriteElements(w, cert.height, roundAndPath); err != nil { return err } - if err := encoding.WriteVarInt(w, uint64(len(cert.data.Committers))); err != nil { + if err := encoding.WriteVarInt(w, uint64(len(cert.committers))); err != nil { return err } - for _, n := range cert.data.Committers { + for _, n := range cert.committers { if err := encoding.WriteVarInt(w, uint64(n)); err != nil { return err } } - if err := encoding.WriteVarInt(w, uint64(len(cert.data.Absentees))); err != nil { + if err := encoding.WriteVarInt(w, uint64(len(cert.absentees))); err != nil { return err } - for _, n := range cert.data.Absentees { + for _, n := range cert.absentees { if err := encoding.WriteVarInt(w, uint64(n)); err != nil { return err } } - return cert.data.Signature.Encode(w) + return cert.signature.Encode(w) } -func (cert *Certificate) Decode(r io.Reader) error { - err := encoding.ReadElements(r, &cert.data.Height, &cert.data.Round) +func (cert *baseCertificate) Decode(r io.Reader) error { + roundAndPath := uint16(0) + err := encoding.ReadElements(r, &cert.height, &roundAndPath) if err != nil { return err } + cert.round = int16(roundAndPath & 0x7FFF) + if roundAndPath&0x8000 == 0x8000 { + cert.fastPath = true + } + lenCommitters, err := encoding.ReadVarInt(r) if err != nil { return err @@ -220,80 +210,63 @@ func (cert *Certificate) Decode(r io.Reader) error { return err } - cert.data.Committers = committers - cert.data.Absentees = absentees - cert.data.Signature = sig + cert.committers = committers + cert.absentees = absentees + cert.signature = sig return nil } -func (cert *Certificate) Validate(height uint32, - validators []*validator.Validator, signBytes []byte, +func (cert *baseCertificate) validate(validators []*validator.Validator, + signBytes []byte, calcRequiredPowerFn func(int64) int64, ) error { - if cert.Height() != height { - return UnexpectedHeightError{ - Expected: height, - Got: cert.Height(), - } - } - - if len(validators) != len(cert.Committers()) { + if len(validators) != len(cert.committers) { return UnexpectedCommittersError{ - Committers: cert.Committers(), + Committers: cert.committers, } } - pubs := make([]*bls.PublicKey, 0, len(cert.Committers())) + pubs := make([]*bls.PublicKey, 0, len(cert.committers)) committeePower := int64(0) signedPower := int64(0) - for index, num := range cert.Committers() { + for index, num := range cert.committers { val := validators[index] if val.Number() != num { return UnexpectedCommittersError{ - Committers: cert.Committers(), + Committers: cert.committers, } } - if !util.Contains(cert.Absentees(), num) { + if !util.Contains(cert.absentees, num) { pubs = append(pubs, val.PublicKey()) signedPower += val.Power() } committeePower += val.Power() } - // Check if signers have 2/3+ of total power - if signedPower <= committeePower*2/3 { + requiredPower := calcRequiredPowerFn(committeePower) + + // Check if signers have enough power + if signedPower < requiredPower { return InsufficientPowerError{ SignedPower: signedPower, - RequiredPower: committeePower*2/3 + 1, + RequiredPower: requiredPower, } } - // Check signature - err := bls.VerifyAggregated(cert.Signature(), pubs, signBytes) - if err != nil { - return err - } + aggPub := bls.PublicKeyAggregate(pubs...) - return nil + return aggPub.Verify(signBytes, cert.signature) } // AddSignature adds a new signature to the certificate. // It does not check the validity of the signature. // The caller should ensure that the signature is valid. -func (cert *Certificate) AddSignature(valNum int32, sig *bls.Signature) { - absentees, removed := util.RemoveFirstOccurrenceOf(cert.data.Absentees, valNum) +func (cert *baseCertificate) AddSignature(valNum int32, sig *bls.Signature) { + absentees, removed := util.RemoveFirstOccurrenceOf(cert.absentees, valNum) if removed { - cert.data.Signature = bls.SignatureAggregate(cert.data.Signature, sig) - cert.data.Absentees = absentees + cert.signature = bls.SignatureAggregate(cert.signature, sig) + cert.absentees = absentees } } - -func BlockCertificateSignBytes(blockHash hash.Hash, height uint32, round int16) []byte { - sb := blockHash.Bytes() - sb = append(sb, util.Uint32ToSlice(height)...) - sb = append(sb, util.Int16ToSlice(round)...) - - return sb -} diff --git a/types/certificate/certificate_test.go b/types/certificate/certificate_test.go index b61fff880..3889e4700 100644 --- a/types/certificate/certificate_test.go +++ b/types/certificate/certificate_test.go @@ -1,15 +1,11 @@ package certificate_test import ( - "bytes" - "encoding/hex" "fmt" "testing" "github.com/fxamacker/cbor/v2" - "github.com/pactus-project/pactus/crypto" "github.com/pactus-project/pactus/crypto/bls" - "github.com/pactus-project/pactus/crypto/hash" "github.com/pactus-project/pactus/types/certificate" "github.com/pactus-project/pactus/types/validator" "github.com/pactus-project/pactus/util" @@ -17,62 +13,28 @@ import ( "github.com/stretchr/testify/assert" ) -func TestCertificate(t *testing.T) { - d, _ := hex.DecodeString( - "04030201" + // Height - "0100" + // Round - "0401020304" + // Committers - "0102" + // Absentees - "b53d79e156e9417e010fa21f2b2a96bee6be46fcd233295d2f697cdb9e782b6112ac01c80d0d9d64c2320664c77fa2a6") // Signature - - h, _ := hash.FromString("6d5fee07c7cc35384f2f1bc695f6b6afa339df6d867dec0d324a60a48803e1aa") - r := bytes.NewReader(d) - cert := new(certificate.Certificate) - err := cert.Decode(r) - assert.NoError(t, err) - assert.Equal(t, cert.Height(), uint32(0x01020304)) - assert.Equal(t, cert.Round(), int16(0x0001)) - assert.Equal(t, cert.Committers(), []int32{1, 2, 3, 4}) - assert.Equal(t, cert.Absentees(), []int32{2}) - assert.Equal(t, cert.Hash(), h) -} - func TestCertificateCBORMarshaling(t *testing.T) { ts := testsuite.NewTestSuite(t) - cert1 := ts.GenerateTestCertificate(ts.RandHeight()) + cert1 := ts.GenerateTestBlockCertificate(ts.RandHeight()) bz1, err := cbor.Marshal(cert1) assert.NoError(t, err) - var cert2 certificate.Certificate + var cert2 certificate.BlockCertificate err = cbor.Unmarshal(bz1, &cert2) assert.NoError(t, err) assert.NoError(t, cert2.BasicCheck()) assert.Equal(t, cert1.Hash(), cert1.Hash()) - - assert.Equal(t, cert1.Hash(), cert2.Hash()) + assert.True(t, cert1.Signature().EqualsTo(cert2.Signature())) err = cbor.Unmarshal([]byte{1}, &cert2) assert.Error(t, err) } -func TestCertificateSignBytes(t *testing.T) { - ts := testsuite.NewTestSuite(t) - - h := ts.RandHash() - height := ts.RandHeight() - cert := ts.GenerateTestCertificate(height) - bz := certificate.BlockCertificateSignBytes(h, height, cert.Round()) - assert.NotEqual(t, bz, certificate.BlockCertificateSignBytes(h, height, cert.Round()+1)) - assert.NotEqual(t, bz, certificate.BlockCertificateSignBytes(ts.RandHash(), height, cert.Round())) -} - func TestInvalidCertificate(t *testing.T) { ts := testsuite.NewTestSuite(t) - cert0 := ts.GenerateTestCertificate(ts.RandHeight()) - t.Run("Invalid height", func(t *testing.T) { - cert := certificate.NewCertificate(0, 0, cert0.Committers(), cert0.Absentees(), cert0.Signature()) + cert := certificate.NewBlockCertificate(0, 0, false) err := cert.BasicCheck() assert.ErrorIs(t, err, certificate.BasicCheckError{ @@ -81,7 +43,7 @@ func TestInvalidCertificate(t *testing.T) { }) t.Run("Invalid round", func(t *testing.T) { - cert := certificate.NewCertificate(1, -1, cert0.Committers(), cert0.Absentees(), cert0.Signature()) + cert := certificate.NewBlockCertificate(1, -1, false) err := cert.BasicCheck() assert.ErrorIs(t, err, certificate.BasicCheckError{ @@ -90,7 +52,8 @@ func TestInvalidCertificate(t *testing.T) { }) t.Run("Committers is nil", func(t *testing.T) { - cert := certificate.NewCertificate(cert0.Height(), cert0.Round(), nil, cert0.Absentees(), cert0.Signature()) + cert := certificate.NewBlockCertificate(ts.RandHeight(), ts.RandRound(), false) + cert.SetSignature(nil, []int32{1}, ts.RandBLSSignature()) err := cert.BasicCheck() assert.ErrorIs(t, err, certificate.BasicCheckError{ @@ -99,7 +62,8 @@ func TestInvalidCertificate(t *testing.T) { }) t.Run("Absentees is nil", func(t *testing.T) { - cert := certificate.NewCertificate(cert0.Height(), cert0.Round(), cert0.Committers(), nil, cert0.Signature()) + cert := certificate.NewBlockCertificate(ts.RandHeight(), ts.RandRound(), false) + cert.SetSignature([]int32{1, 2, 3, 4}, nil, ts.RandBLSSignature()) err := cert.BasicCheck() assert.ErrorIs(t, err, certificate.BasicCheckError{ @@ -108,7 +72,8 @@ func TestInvalidCertificate(t *testing.T) { }) t.Run("Signature is nil", func(t *testing.T) { - cert := certificate.NewCertificate(cert0.Height(), cert0.Round(), cert0.Committers(), cert0.Absentees(), nil) + cert := certificate.NewBlockCertificate(ts.RandHeight(), ts.RandRound(), false) + cert.SetSignature([]int32{1, 2, 3, 4, 5, 6}, []int32{1}, nil) err := cert.BasicCheck() assert.ErrorIs(t, err, certificate.BasicCheckError{ @@ -117,57 +82,32 @@ func TestInvalidCertificate(t *testing.T) { }) t.Run("Invalid Absentees ", func(t *testing.T) { - abs := cert0.Absentees() - abs = append(abs, 0) - cert := certificate.NewCertificate(cert0.Height(), cert0.Round(), cert0.Committers(), abs, cert0.Signature()) + cert := certificate.NewBlockCertificate(ts.RandHeight(), ts.RandRound(), false) + cert.SetSignature([]int32{11, 2, 3, 4, 5, 6}, []int32{66}, ts.RandBLSSignature()) err := cert.BasicCheck() assert.ErrorIs(t, err, certificate.BasicCheckError{ Reason: fmt.Sprintf("absentees are not a subset of committers: %v, %v", - cert.Committers(), abs), + cert.Committers(), []int32{66}), }) }) t.Run("Invalid Absentees ", func(t *testing.T) { - abs := []int32{2, 1} - cert := certificate.NewCertificate(cert0.Height(), cert0.Round(), cert0.Committers(), abs, cert0.Signature()) + cert := certificate.NewBlockCertificate(ts.RandHeight(), ts.RandRound(), false) + cert.SetSignature([]int32{1, 2, 3, 4, 5, 6}, []int32{2, 1}, ts.RandBLSSignature()) err := cert.BasicCheck() assert.ErrorIs(t, err, certificate.BasicCheckError{ Reason: fmt.Sprintf("absentees are not a subset of committers: %v, %v", - cert.Committers(), abs), + cert.Committers(), []int32{2, 1}), }) }) } -func TestCertificateHash(t *testing.T) { - ts := testsuite.NewTestSuite(t) - - cert0 := ts.GenerateTestCertificate(ts.RandHeight()) - - cert1 := certificate.NewCertificate(cert0.Height(), cert0.Round(), - []int32{10, 18, 2, 6}, []int32{}, cert0.Signature()) - assert.Equal(t, cert1.Committers(), []int32{10, 18, 2, 6}) - assert.Equal(t, cert1.Absentees(), []int32{}) - assert.NoError(t, cert1.BasicCheck()) - - cert2 := certificate.NewCertificate(cert0.Height(), cert0.Round(), - []int32{10, 18, 2, 6}, []int32{2, 6}, cert0.Signature()) - assert.Equal(t, cert2.Committers(), []int32{10, 18, 2, 6}) - assert.Equal(t, cert2.Absentees(), []int32{2, 6}) - assert.NoError(t, cert2.BasicCheck()) - - cert3 := certificate.NewCertificate(cert0.Height(), cert0.Round(), - []int32{10, 18, 2, 6}, []int32{18}, cert0.Signature()) - assert.Equal(t, cert3.Committers(), []int32{10, 18, 2, 6}) - assert.Equal(t, cert3.Absentees(), []int32{18}) - assert.NoError(t, cert3.BasicCheck()) -} - func TestEncodingCertificate(t *testing.T) { ts := testsuite.NewTestSuite(t) - cert1 := ts.GenerateTestCertificate(ts.RandHeight()) + cert1 := ts.GenerateTestBlockCertificate(ts.RandHeight()) length := cert1.SerializeSize() for i := 0; i < length; i++ { @@ -178,144 +118,73 @@ func TestEncodingCertificate(t *testing.T) { assert.NoError(t, cert1.Encode(w)) for i := 0; i < length; i++ { - cert := new(certificate.Certificate) + cert := new(certificate.BlockCertificate) r := util.NewFixedReader(i, w.Bytes()) assert.Error(t, cert.Decode(r), "decode test %v failed", i) } - cert2 := new(certificate.Certificate) + cert2 := new(certificate.BlockCertificate) r := util.NewFixedReader(length, w.Bytes()) assert.NoError(t, cert2.Decode(r)) assert.Equal(t, cert1.Hash(), cert2.Hash()) } -func TestCertificateValidation(t *testing.T) { +func TestAddSignature(t *testing.T) { ts := testsuite.NewTestSuite(t) - valKey1 := ts.RandValKey() - valKey2 := ts.RandValKey() - valKey3 := ts.RandValKey() - valKey4 := ts.RandValKey() - val1 := validator.NewValidator(valKey1.PublicKey(), 1001) - val2 := validator.NewValidator(valKey2.PublicKey(), 1002) - val3 := validator.NewValidator(valKey3.PublicKey(), 1003) - val4 := validator.NewValidator(valKey4.PublicKey(), 1004) - - validators := []*validator.Validator{val1, val2, val3, val4} - committers := []int32{ - val1.Number(), val2.Number(), val3.Number(), val4.Number(), - } blockHash := ts.RandHash() - blockHeight := ts.RandHeight() - blockRound := ts.RandRound() - signBytes := certificate.BlockCertificateSignBytes(blockHash, blockHeight, blockRound) - sig1 := valKey1.Sign(signBytes) - sig3 := valKey3.Sign(signBytes) - sig4 := valKey4.Sign(signBytes) - aggSig := bls.SignatureAggregate(sig1, sig3, sig4) - - t.Run("Invalid height, should return error", func(t *testing.T) { - cert := certificate.NewCertificate(blockHeight+1, blockRound, committers, - []int32{}, aggSig) - - err := cert.Validate(blockHeight, validators, signBytes) - assert.ErrorIs(t, err, certificate.UnexpectedHeightError{ - Expected: blockHeight, - Got: blockHeight + 1, - }) - }) - - t.Run("Invalid committer, should return error", func(t *testing.T) { - invCommitters := committers - invCommitters = append(invCommitters, ts.Rand.Int31n(1000)) - cert := certificate.NewCertificate(blockHeight, blockRound, invCommitters, - []int32{}, aggSig) + height := ts.RandHeight() + round := ts.RandRound() + cert := certificate.NewBlockCertificate(height, round, false) + signBytes := cert.SignBytes(blockHash) + committers := ts.RandSlice(6) + sigs := []*bls.Signature{} + validators := []*validator.Validator{} + + for _, committer := range committers { + valKey := ts.RandValKey() + val := validator.NewValidator(valKey.PublicKey(), committer) + sig := valKey.Sign(signBytes) + + validators = append(validators, val) + sigs = append(sigs, sig) + } - err := cert.Validate(blockHeight, validators, signBytes) + absentees := committers[4:] + aggSig := bls.SignatureAggregate(sigs[:4]...) + cert.SetSignature(committers, absentees, aggSig) - assert.ErrorIs(t, err, certificate.UnexpectedCommittersError{ - Committers: invCommitters, - }) - }) + err := cert.Validate(validators, blockHash) + assert.NoError(t, err) - t.Run("Invalid committers, should return error", func(t *testing.T) { - invCommitters := []int32{ - ts.Rand.Int31n(1000), val2.Number(), val3.Number(), val4.Number(), - } - cert := certificate.NewCertificate(blockHeight, blockRound, invCommitters, - []int32{}, aggSig) + numAbsentees := len(cert.Absentees()) - err := cert.Validate(blockHeight, validators, signBytes) - assert.ErrorIs(t, err, certificate.UnexpectedCommittersError{ - Committers: invCommitters, - }) - }) + t.Run("Add an existing signature", func(t *testing.T) { + cert.AddSignature(validators[0].Number(), sigs[0]) - t.Run("Doesn't have 2/3 majority", func(t *testing.T) { - cert := certificate.NewCertificate(blockHeight, blockRound, committers, - []int32{val1.Number(), val2.Number()}, aggSig) + err := cert.Validate(validators, blockHash) + assert.NoError(t, err) - err := cert.Validate(blockHeight, validators, signBytes) - assert.ErrorIs(t, err, certificate.InsufficientPowerError{ - SignedPower: 2, - RequiredPower: 3, - }) + assert.Len(t, cert.Absentees(), numAbsentees) }) - t.Run("Invalid signature, should return error", func(t *testing.T) { - cert := certificate.NewCertificate(blockHeight, blockRound, committers, - []int32{val3.Number()}, aggSig) + t.Run("Add non existing signature", func(t *testing.T) { + cert.AddSignature(validators[5].Number(), sigs[5]) - err := cert.Validate(blockHeight, validators, signBytes) - assert.ErrorIs(t, err, crypto.ErrInvalidSignature) - }) - t.Run("Ok, should return no error", func(t *testing.T) { - cert := certificate.NewCertificate(blockHeight, blockRound, committers, - []int32{val2.Number()}, aggSig) - - err := cert.Validate(blockHeight, validators, signBytes) + err := cert.Validate(validators, blockHash) assert.NoError(t, err) - }) -} - -func TestAddSignature(t *testing.T) { - ts := testsuite.NewTestSuite(t) - valKey1 := ts.RandValKey() - valKey2 := ts.RandValKey() - valKey3 := ts.RandValKey() - valKey4 := ts.RandValKey() - blockHeight := ts.RandHeight() - blockRound := ts.RandRound() - blockHash := ts.RandHash() - - val1 := validator.NewValidator(valKey1.PublicKey(), ts.RandInt32(10000)) - val2 := validator.NewValidator(valKey2.PublicKey(), ts.RandInt32(10000)) - val3 := validator.NewValidator(valKey3.PublicKey(), ts.RandInt32(10000)) - val4 := validator.NewValidator(valKey4.PublicKey(), ts.RandInt32(10000)) - - signBytes := certificate.BlockCertificateSignBytes(blockHash, blockHeight, blockRound) - sig1 := valKey1.Sign(signBytes) - sig2 := valKey2.Sign(signBytes) - sig3 := valKey3.Sign(signBytes) - sig4 := valKey4.Sign(signBytes) - aggSig := bls.SignatureAggregate(sig1, sig2, sig3) - - cert := certificate.NewCertificate(blockHeight, blockRound, - []int32{val1.Number(), val2.Number(), val3.Number(), val4.Number()}, []int32{val4.Number()}, aggSig) - - assert.Equal(t, []int32{val4.Number()}, cert.Absentees()) - cert.AddSignature(val4.Number(), sig4) - assert.Empty(t, cert.Absentees()) - assert.NoError(t, cert.Validate(blockHeight, []*validator.Validator{val1, val2, val3, val4}, signBytes)) + assert.Len(t, cert.Absentees(), numAbsentees-1) + }) } func TestClone(t *testing.T) { ts := testsuite.NewTestSuite(t) - cert1 := ts.GenerateTestCertificate(ts.RandHeight()) + cert1 := ts.GenerateTestBlockCertificate(ts.RandHeight()) cert2 := cert1.Clone() cert2.AddSignature(cert2.Absentees()[0], ts.RandBLSSignature()) + assert.NotEqual(t, cert1.Absentees(), cert2.Absentees()) assert.NotEqual(t, cert1, cert2) } diff --git a/types/certificate/errors.go b/types/certificate/errors.go index 629916115..79a385cb5 100644 --- a/types/certificate/errors.go +++ b/types/certificate/errors.go @@ -14,18 +14,6 @@ func (e BasicCheckError) Error() string { return e.Reason } -// UnexpectedHeightError is returned when the height of the certificate -// is invalid. -type UnexpectedHeightError struct { - Expected uint32 - Got uint32 -} - -func (e UnexpectedHeightError) Error() string { - return fmt.Sprintf("certificate height is invalid (expected %v got %v)", - e.Expected, e.Got) -} - // UnexpectedCommittersError is returned when the list of committers // does not match the expectations. type UnexpectedCommittersError struct { diff --git a/types/certificate/vote_certificate.go b/types/certificate/vote_certificate.go new file mode 100644 index 000000000..e435810fe --- /dev/null +++ b/types/certificate/vote_certificate.go @@ -0,0 +1,79 @@ +package certificate + +import ( + "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/types/validator" + "github.com/pactus-project/pactus/util" +) + +// VoteCertificate represents a certificate used for consensus voting, +// checking if a majority of validators have voted in a consensus step. +type VoteCertificate struct { + baseCertificate +} + +// NewVoteCertificate creates a new VoteCertificate instance. +func NewVoteCertificate(height uint32, round int16) *VoteCertificate { + return &VoteCertificate{ + baseCertificate: baseCertificate{ + height: height, + round: round, + fastPath: false, + }, + } +} + +// SignBytes returns the sign bytes for the vote certificate. +// This method provides the same data as the `SignBytes` function in vote struct. +func (cert *VoteCertificate) SignBytes(blockHash hash.Hash, extraData ...[]byte) []byte { + sb := blockHash.Bytes() + sb = append(sb, util.Uint32ToSlice(cert.height)...) + sb = append(sb, util.Int16ToSlice(cert.round)...) + for _, data := range extraData { + sb = append(sb, data...) + } + + return sb +} + +func (cert *VoteCertificate) ValidatePrepare(validators []*validator.Validator, + blockHash hash.Hash, +) error { + signBytes := cert.SignBytes(blockHash, + util.StringToBytes("PREPARE")) + + return cert.validate(validators, signBytes) +} + +func (cert *VoteCertificate) ValidateCPPreVote(validators []*validator.Validator, + blockHash hash.Hash, cpRound int16, cpValue byte, +) error { + signBytes := cert.SignBytes(blockHash, + util.StringToBytes("PRE-VOTE"), + util.Int16ToSlice(cpRound), + []byte{cpValue}) + + return cert.validate(validators, signBytes) +} + +func (cert *VoteCertificate) ValidateCPMainVote(validators []*validator.Validator, + blockHash hash.Hash, cpRound int16, cpValue byte, +) error { + signBytes := cert.SignBytes(blockHash, + util.StringToBytes("MAIN-VOTE"), + util.Int16ToSlice(cpRound), + []byte{cpValue}) + + return cert.validate(validators, signBytes) +} + +func (cert *VoteCertificate) validate(validators []*validator.Validator, signBytes []byte) error { + calcRequiredPowerFn := func(committeePower int64) int64 { + f := (committeePower - 1) / 3 + p := (2 * f) + 1 + + return p + } + + return cert.baseCertificate.validate(validators, signBytes, calcRequiredPowerFn) +} diff --git a/types/certificate/vote_certificate_test.go b/types/certificate/vote_certificate_test.go new file mode 100644 index 000000000..6f314ff4f --- /dev/null +++ b/types/certificate/vote_certificate_test.go @@ -0,0 +1,197 @@ +package certificate_test + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/pactus-project/pactus/crypto/bls" + "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/types/certificate" + "github.com/pactus-project/pactus/types/validator" + "github.com/pactus-project/pactus/util" + "github.com/pactus-project/pactus/util/testsuite" + "github.com/stretchr/testify/assert" +) + +// TestVoteCertificate tests the general properties of the Vote certificate. +// The data for the Vote certificate is the same as the block certificate in the slow path, +// but the sign bytes are different. +func TestVoteCertificate(t *testing.T) { + expectedData, _ := hex.DecodeString( + "04030201" + // Height + "0100" + // Round + "06010203040506" + // Committers + "0102" + // Absentees + "b53d79e156e9417e010fa21f2b2a96bee6be46fcd233295d2f697cdb9e782b6112ac01c80d0d9d64c2320664c77fa2a6") // Signature + + certHash, _ := hash.FromString("ac755295a6850b141286bde42bb8ba06ae1671f0562cbef90043924091177815") + r := bytes.NewReader(expectedData) + cert := new(certificate.VoteCertificate) + err := cert.Decode(r) + assert.NoError(t, err) + assert.Equal(t, cert.Height(), uint32(0x01020304)) + assert.Equal(t, cert.Round(), int16(0x0001)) + assert.Equal(t, cert.FastPath(), false) + assert.Equal(t, cert.Committers(), []int32{1, 2, 3, 4, 5, 6}) + assert.Equal(t, cert.Absentees(), []int32{2}) + assert.Equal(t, cert.Hash(), certHash) +} + +func TestVoteCertificateValidatePrepare(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + blockHash := ts.RandHash() + height := ts.RandHeight() + round := ts.RandRound() + cert := certificate.NewVoteCertificate(height, round) + signBytes := cert.SignBytes(blockHash, + util.StringToBytes("PREPARE")) + committers := ts.RandSlice(6) + sigs := []*bls.Signature{} + validators := []*validator.Validator{} + + for _, committer := range committers { + valKey := ts.RandValKey() + val := validator.NewValidator(valKey.PublicKey(), committer) + sig := valKey.Sign(signBytes) + + validators = append(validators, val) + sigs = append(sigs, sig) + } + + t.Run("Doesn't have 2f+1 majority", func(t *testing.T) { + absentees := committers[2:] + aggSig := bls.SignatureAggregate(sigs[:2]...) + cert.SetSignature(committers, absentees, aggSig) + + err := cert.ValidatePrepare(validators, blockHash) + assert.ErrorIs(t, err, certificate.InsufficientPowerError{ + SignedPower: 2, + RequiredPower: 3, + }) + }) + + t.Run("Ok, should return no error", func(t *testing.T) { + absentees := committers[3:] + aggSig := bls.SignatureAggregate(sigs[:3]...) + cert.SetSignature(committers, absentees, aggSig) + + err := cert.ValidatePrepare(validators, blockHash) + assert.NoError(t, err) + }) +} + +func TestVoteCertificateValidateCPPreVote(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + blockHash := ts.RandHash() + height := ts.RandHeight() + round := ts.RandRound() + cpRound := ts.RandRound() + cpValue := 2 + cert := certificate.NewVoteCertificate(height, round) + signBytes := cert.SignBytes(blockHash, + util.StringToBytes("PRE-VOTE"), + util.Int16ToSlice(cpRound), + []byte{byte(cpValue)}) + committers := ts.RandSlice(6) + sigs := []*bls.Signature{} + validators := []*validator.Validator{} + + for _, committer := range committers { + valKey := ts.RandValKey() + val := validator.NewValidator(valKey.PublicKey(), committer) + sig := valKey.Sign(signBytes) + + validators = append(validators, val) + sigs = append(sigs, sig) + } + + t.Run("Invalid cpValue", func(t *testing.T) { + absentees := committers[3:] + aggSig := bls.SignatureAggregate(sigs[:3]...) + cert.SetSignature(committers, absentees, aggSig) + + err := cert.ValidateCPPreVote(validators, blockHash, cpRound, byte(0)) + assert.Error(t, err) + }) + + t.Run("Doesn't have 2f+1 majority", func(t *testing.T) { + absentees := committers[2:] + aggSig := bls.SignatureAggregate(sigs[:2]...) + cert.SetSignature(committers, absentees, aggSig) + + err := cert.ValidateCPPreVote(validators, blockHash, cpRound, byte(cpValue)) + assert.ErrorIs(t, err, certificate.InsufficientPowerError{ + SignedPower: 2, + RequiredPower: 3, + }) + }) + + t.Run("Ok, should return no error", func(t *testing.T) { + absentees := committers[3:] + aggSig := bls.SignatureAggregate(sigs[:3]...) + cert.SetSignature(committers, absentees, aggSig) + + err := cert.ValidateCPPreVote(validators, blockHash, cpRound, byte(cpValue)) + assert.NoError(t, err) + }) +} + +func TestVoteCertificateValidateCPMainVote(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + blockHash := ts.RandHash() + height := ts.RandHeight() + round := ts.RandRound() + cpRound := ts.RandRound() + cpValue := 2 + cert := certificate.NewVoteCertificate(height, round) + signBytes := cert.SignBytes(blockHash, + util.StringToBytes("MAIN-VOTE"), + util.Int16ToSlice(cpRound), + []byte{byte(cpValue)}) + committers := ts.RandSlice(6) + sigs := []*bls.Signature{} + validators := []*validator.Validator{} + + for _, committer := range committers { + valKey := ts.RandValKey() + val := validator.NewValidator(valKey.PublicKey(), committer) + sig := valKey.Sign(signBytes) + + validators = append(validators, val) + sigs = append(sigs, sig) + } + + t.Run("Invalid cpValue", func(t *testing.T) { + absentees := committers[3:] + aggSig := bls.SignatureAggregate(sigs[:3]...) + cert.SetSignature(committers, absentees, aggSig) + + err := cert.ValidateCPMainVote(validators, blockHash, cpRound, byte(0)) + assert.Error(t, err) + }) + + t.Run("Doesn't have 2f+1 majority", func(t *testing.T) { + absentees := committers[2:] + aggSig := bls.SignatureAggregate(sigs[:2]...) + cert.SetSignature(committers, absentees, aggSig) + + err := cert.ValidateCPMainVote(validators, blockHash, cpRound, byte(cpValue)) + assert.ErrorIs(t, err, certificate.InsufficientPowerError{ + SignedPower: 2, + RequiredPower: 3, + }) + }) + + t.Run("Ok, should return no error", func(t *testing.T) { + absentees := committers[3:] + aggSig := bls.SignatureAggregate(sigs[:3]...) + cert.SetSignature(committers, absentees, aggSig) + + err := cert.ValidateCPMainVote(validators, blockHash, cpRound, byte(cpValue)) + assert.NoError(t, err) + }) +} diff --git a/types/proposal/proposal.go b/types/proposal/proposal.go index 39c7b6272..2fdd4ac41 100644 --- a/types/proposal/proposal.go +++ b/types/proposal/proposal.go @@ -44,7 +44,7 @@ func (p *Proposal) Block() *block.Block { return p.data.Block } -func (p *Proposal) Signature() crypto.Signature { +func (p *Proposal) Signature() *bls.Signature { return p.data.Signature } @@ -73,11 +73,7 @@ func (p *Proposal) SetSignature(sig *bls.Signature) { } func (p *Proposal) SignBytes() []byte { - sb := p.Block().Hash().Bytes() - sb = append(sb, util.Uint32ToSlice(p.Height())...) - sb = append(sb, util.Int16ToSlice(p.Round())...) - - return sb + return SignBytes(p.Block().Hash(), p.Height(), p.Round()) } func (p *Proposal) MarshalCBOR() ([]byte, error) { @@ -112,3 +108,19 @@ func (p Proposal) String() string { return fmt.Sprintf("{%v/%v 🗃 %v}", p.data.Height, p.data.Round, b.String()) } + +func SignBytes(blockHash hash.Hash, height uint32, round int16) []byte { + sb := blockHash.Bytes() + sb = append(sb, util.Uint32ToSlice(height)...) + sb = append(sb, util.Int16ToSlice(round)...) + + return sb +} + +func ChecKSignature(blockHash hash.Hash, height uint32, round int16, + sig *bls.Signature, pubKey *bls.PublicKey, +) error { + sb := SignBytes(blockHash, height, round) + + return pubKey.Verify(sb, sig) +} diff --git a/types/vote/cp_just.go b/types/vote/cp_just.go index ee53dcc61..95d81c5e3 100644 --- a/types/vote/cp_just.go +++ b/types/vote/cp_just.go @@ -7,8 +7,8 @@ import ( type JustType uint8 const ( - JustTypeInitZero = JustType(1) - JustTypeInitOne = JustType(2) + JustTypeInitNo = JustType(1) + JustTypeInitYes = JustType(2) JustTypePreVoteSoft = JustType(3) JustTypePreVoteHard = JustType(4) JustTypeMainVoteConflict = JustType(5) @@ -18,10 +18,10 @@ const ( func (t JustType) String() string { switch t { - case JustTypeInitZero: - return "JustInitZero" - case JustTypeInitOne: - return "JustInitOne" + case JustTypeInitNo: + return "JustInitNo" + case JustTypeInitYes: + return "JustInitYes" case JustTypePreVoteSoft: return "JustPreVoteSoft" case JustTypePreVoteHard: @@ -45,10 +45,10 @@ type Just interface { func makeJust(t JustType) Just { switch t { - case JustTypeInitZero: - return &JustInitZero{} - case JustTypeInitOne: - return &JustInitOne{} + case JustTypeInitNo: + return &JustInitNo{} + case JustTypeInitYes: + return &JustInitYes{} case JustTypePreVoteSoft: return &JustPreVoteSoft{} case JustTypePreVoteHard: @@ -64,38 +64,38 @@ func makeJust(t JustType) Just { } } -type JustInitZero struct { - QCert *certificate.Certificate `cbor:"1,keyasint"` +type JustInitNo struct { + QCert *certificate.VoteCertificate `cbor:"1,keyasint"` } -type JustInitOne struct { +type JustInitYes struct { // } type JustPreVoteSoft struct { - QCert *certificate.Certificate `cbor:"1,keyasint"` + QCert *certificate.VoteCertificate `cbor:"1,keyasint"` } type JustPreVoteHard struct { - QCert *certificate.Certificate `cbor:"1,keyasint"` + QCert *certificate.VoteCertificate `cbor:"1,keyasint"` } type JustMainVoteConflict struct { - Just0 Just - Just1 Just + JustNo Just + JustYes Just } type JustMainVoteNoConflict struct { - QCert *certificate.Certificate `cbor:"1,keyasint"` + QCert *certificate.VoteCertificate `cbor:"1,keyasint"` } type JustDecided struct { - QCert *certificate.Certificate `cbor:"1,keyasint"` + QCert *certificate.VoteCertificate `cbor:"1,keyasint"` } -func (*JustInitZero) Type() JustType { - return JustTypeInitZero +func (*JustInitNo) Type() JustType { + return JustTypeInitNo } -func (*JustInitOne) Type() JustType { - return JustTypeInitOne +func (*JustInitYes) Type() JustType { + return JustTypeInitYes } func (*JustPreVoteSoft) Type() JustType { @@ -118,11 +118,11 @@ func (*JustDecided) Type() JustType { return JustTypeDecided } -func (j *JustInitZero) BasicCheck() error { - return j.QCert.BasicCheck() +func (*JustInitNo) BasicCheck() error { + return nil } -func (*JustInitOne) BasicCheck() error { +func (*JustInitYes) BasicCheck() error { return nil } @@ -135,11 +135,11 @@ func (j *JustPreVoteHard) BasicCheck() error { } func (j *JustMainVoteConflict) BasicCheck() error { - if err := j.Just0.BasicCheck(); err != nil { + if err := j.JustNo.BasicCheck(); err != nil { return err } - return j.Just1.BasicCheck() + return j.JustYes.BasicCheck() } func (j *JustMainVoteNoConflict) BasicCheck() error { diff --git a/types/vote/cp_vote.go b/types/vote/cp_vote.go index 7797c3a3a..f5a1afac5 100644 --- a/types/vote/cp_vote.go +++ b/types/vote/cp_vote.go @@ -10,17 +10,17 @@ import ( type CPValue int8 const ( - CPValueZero = CPValue(0) - CPValueOne = CPValue(1) + CPValueNo = CPValue(0) + CPValueYes = CPValue(1) CPValueAbstain = CPValue(2) ) func (v CPValue) String() string { switch v { - case CPValueZero: - return "zero" - case CPValueOne: - return "one" + case CPValueNo: + return "no" + case CPValueYes: + return "yes" case CPValueAbstain: return "abstain" default: @@ -38,7 +38,7 @@ func (v *cpVote) BasicCheck() error { if v.Round < 0 { return errors.Error(errors.ErrInvalidRound) } - if v.Value < CPValueZero || + if v.Value < CPValueNo || v.Value > CPValueAbstain { // Invalid values return errors.Errorf(errors.ErrInvalidVote, "cp value should be 0, 1 or abstain") @@ -66,19 +66,19 @@ func (v *cpVote) MarshalCBOR() ([]byte, error) { justData := []byte{} if v.Just.Type() == JustTypeMainVoteConflict { conflictJust := v.Just.(*JustMainVoteConflict) - data0, err := cbor.Marshal(conflictJust.Just0) + data0, err := cbor.Marshal(conflictJust.JustNo) if err != nil { return nil, err } - data1, err := cbor.Marshal(conflictJust.Just1) + data1, err := cbor.Marshal(conflictJust.JustYes) if err != nil { return nil, err } _conflictingJust := _JustMainVoteConflict{ - Just0Type: conflictJust.Just0.Type(), + Just0Type: conflictJust.JustNo.Type(), Just0Data: data0, - Just1Type: conflictJust.Just1.Type(), + Just1Type: conflictJust.JustYes.Type(), Just1Data: data1, } data, err := cbor.Marshal(_conflictingJust) @@ -133,8 +133,8 @@ func (v *cpVote) UnmarshalCBOR(bs []byte) error { } just = &JustMainVoteConflict{ - Just0: just0, - Just1: just1, + JustNo: just0, + JustYes: just1, } } else { just = makeJust(_cp.JustType) diff --git a/types/vote/vote.go b/types/vote/vote.go index f03e03793..e01c4ec05 100644 --- a/types/vote/vote.go +++ b/types/vote/vote.go @@ -7,7 +7,6 @@ import ( "github.com/pactus-project/pactus/crypto" "github.com/pactus-project/pactus/crypto/bls" "github.com/pactus-project/pactus/crypto/hash" - "github.com/pactus-project/pactus/types/certificate" "github.com/pactus-project/pactus/util" "github.com/pactus-project/pactus/util/errors" ) @@ -96,7 +95,10 @@ func newVote(voteType Type, blockHash hash.Hash, height uint32, round int16, // SignBytes generates the bytes to be signed for the vote. func (v *Vote) SignBytes() []byte { - sb := certificate.BlockCertificateSignBytes(v.data.BlockHash, v.data.Height, v.data.Round) + sb := v.data.BlockHash.Bytes() + sb = append(sb, util.Uint32ToSlice(v.data.Height)...) + sb = append(sb, util.Int16ToSlice(v.data.Round)...) + switch t := v.Type(); t { case VoteTypePrecommit: // Nothing @@ -249,7 +251,7 @@ func (v *Vote) String() string { v.Signer().ShortString(), ) case VoteTypeCPPreVote, VoteTypeCPMainVote, VoteTypeCPDecided: - return fmt.Sprintf("{%d/%d/%s/%d/%d ⌘ %v 👤 %s}", + return fmt.Sprintf("{%d/%d/%s/%d/%s ⌘ %v 👤 %s}", v.Height(), v.Round(), v.Type(), diff --git a/types/vote/vote_test.go b/types/vote/vote_test.go index 3bda1cdee..36a3122dc 100644 --- a/types/vote/vote_test.go +++ b/types/vote/vote_test.go @@ -6,7 +6,6 @@ import ( "github.com/pactus-project/pactus/crypto/bls" "github.com/pactus-project/pactus/crypto/hash" - "github.com/pactus-project/pactus/types/certificate" "github.com/pactus-project/pactus/types/vote" "github.com/pactus-project/pactus/util/errors" "github.com/pactus-project/pactus/util/testsuite" @@ -55,13 +54,13 @@ func TestVoteMarshaling(t *testing.T) { "0100" + // CP_Round: 0 "0200" + // CP_Value: 0 "0301" + // Just type: 1 - "045840" + // Just: JustTypeInitZero + "045840" + // Just: JustTypeInitNo "A1" + // map(1) "01583C" + // Certificate (60 bytes) "32000000010004010203040094D25422904AC1D130AC981374AA4424F988" + // Certificate Data "61E99131078EFEFD62FC52CF072B0C08BB04E4E6496BA48DE4F3D3309AAB" + "07f6", // Signature -> Null - "JustInitZero", + "JustInitNo", "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB3200000001005052452d564f5445000000", }, { @@ -76,10 +75,10 @@ func TestVoteMarshaling(t *testing.T) { "0100" + // CP_Round: 0 "0201" + // CP_Value: 1 "0302" + // Just type: 2 - "0441" + // Just: JustTypeInitOne + "0441" + // Just: JustTypeInitYes "A0" + // Empty Array "07f6", // Signature -> Null - "JustInitOne", + "JustInitYes", "00000000000000000000000000000000000000000000000000000000000000003200000001005052452d564f5445000001", }, { @@ -138,13 +137,13 @@ func TestVoteMarshaling(t *testing.T) { "0305" + // Just type: 5 "04584b" + // Just: JustTypeMainVoteConflict "A4" + // map(4) - "0101" + // Just0: Type (JustTypeInitZero) + "0101" + // Just0: Type (No) "025840" + // Just0Data "A1" + // map(1) "01583C" + // Certificate (60 bytes) "32000000010004010203040094D25422904AC1D130AC981374AA4424F988" + // Certificate Data "61E99131078EFEFD62FC52CF072B0C08BB04E4E6496BA48DE4F3D3309AAB" + - "0302" + // Just1: Type (JustTypeInitOne) + "0302" + // Just1: Type (JustTypeInitYes) "0441" + // Just1Data "A0" + // Empty Array "07f6", // Signature -> Null @@ -253,11 +252,11 @@ func TestCPPreVote(t *testing.T) { h := ts.RandHeight() r := ts.RandRound() - just := &vote.JustInitOne{} + just := &vote.JustInitYes{} t.Run("Invalid round", func(t *testing.T) { v := vote.NewCPPreVote(hash.UndefHash, h, r, - -1, vote.CPValueOne, just, ts.RandAccAddress()) + -1, vote.CPValueYes, just, ts.RandAccAddress()) err := v.BasicCheck() assert.Equal(t, errors.Code(err), errors.ErrInvalidRound) @@ -273,13 +272,13 @@ func TestCPPreVote(t *testing.T) { t.Run("Ok", func(t *testing.T) { v := vote.NewCPPreVote(hash.UndefHash, h, r, - 1, vote.CPValueZero, just, ts.RandAccAddress()) + 1, vote.CPValueNo, just, ts.RandAccAddress()) v.SetSignature(ts.RandBLSSignature()) err := v.BasicCheck() assert.NoError(t, err) assert.Equal(t, v.CPRound(), int16(1)) - assert.Equal(t, v.CPValue(), vote.CPValueZero) + assert.Equal(t, v.CPValue(), vote.CPValueNo) assert.NotNil(t, v.CPJust()) }) } @@ -289,11 +288,11 @@ func TestCPMainVote(t *testing.T) { h := ts.RandHeight() r := ts.RandRound() - just := &vote.JustInitOne{} + just := &vote.JustInitYes{} t.Run("Invalid round", func(t *testing.T) { v := vote.NewCPMainVote(hash.UndefHash, h, r, - -1, vote.CPValueZero, just, ts.RandAccAddress()) + -1, vote.CPValueNo, just, ts.RandAccAddress()) err := v.BasicCheck() assert.Equal(t, errors.Code(err), errors.ErrInvalidRound) @@ -336,11 +335,11 @@ func TestCPDecided(t *testing.T) { h := ts.RandHeight() r := ts.RandRound() - just := &vote.JustInitOne{} + just := &vote.JustInitYes{} t.Run("Invalid round", func(t *testing.T) { v := vote.NewCPDecidedVote(hash.UndefHash, h, r, - -1, vote.CPValueZero, just, ts.RandAccAddress()) + -1, vote.CPValueNo, just, ts.RandAccAddress()) err := v.BasicCheck() assert.Equal(t, errors.Code(err), errors.ErrInvalidRound) @@ -438,34 +437,32 @@ func TestSignBytes(t *testing.T) { signer := ts.RandAccAddress() blockHash := ts.RandHash() height := uint32(100) - round := int16(2) - just := &vote.JustInitZero{} + round := int16(10) + cpRound := int16(10) + just := &vote.JustInitNo{} v1 := vote.NewPrepareVote(blockHash, height, round, signer) v2 := vote.NewPrecommitVote(blockHash, height, round, signer) - v3 := vote.NewCPPreVote(blockHash, height, round, 1, vote.CPValueZero, just, signer) - v4 := vote.NewCPMainVote(blockHash, height, round, 1, vote.CPValueAbstain, just, signer) + v3 := vote.NewCPPreVote(blockHash, height, round, cpRound, vote.CPValueNo, just, signer) + v4 := vote.NewCPMainVote(blockHash, height, round, cpRound, vote.CPValueAbstain, just, signer) + v5 := vote.NewCPDecidedVote(blockHash, height, round, cpRound, vote.CPValueYes, just, signer) sb1 := v1.SignBytes() sb2 := v2.SignBytes() sb3 := v3.SignBytes() sb4 := v4.SignBytes() - sb5 := certificate.BlockCertificateSignBytes(blockHash, height, round) + sb5 := v5.SignBytes() assert.Equal(t, len(sb1), 45) assert.Equal(t, len(sb2), 38) assert.Equal(t, len(sb3), 49) assert.Equal(t, len(sb4), 50) + assert.Equal(t, len(sb5), 48) assert.Contains(t, string(sb1), "PREPARE") assert.Contains(t, string(sb3), "PRE-VOTE") assert.Contains(t, string(sb4), "MAIN-VOTE") - assert.NotEqual(t, sb1, sb2) - assert.NotEqual(t, sb1, sb3) - assert.NotEqual(t, sb1, sb4) - assert.NotEqual(t, sb2, sb3) - assert.NotEqual(t, sb3, sb4) - assert.Equal(t, sb2, sb5) + assert.Contains(t, string(sb5), "DECIDED") } func TestLog(t *testing.T) { @@ -475,11 +472,11 @@ func TestLog(t *testing.T) { blockHash := ts.RandHash() height := uint32(100) round := int16(2) - just := &vote.JustInitZero{} + just := &vote.JustInitNo{} v1 := vote.NewPrepareVote(blockHash, height, round, signer) v2 := vote.NewPrecommitVote(blockHash, height, round, signer) - v3 := vote.NewCPPreVote(blockHash, height, round, 1, vote.CPValueZero, just, signer) + v3 := vote.NewCPPreVote(blockHash, height, round, 1, vote.CPValueNo, just, signer) v4 := vote.NewCPMainVote(blockHash, height, round, 1, vote.CPValueAbstain, just, signer) assert.Contains(t, v1.String(), "100/2/PREPARE") @@ -489,8 +486,8 @@ func TestLog(t *testing.T) { } func TestCPValueToString(t *testing.T) { - assert.Equal(t, vote.CPValueZero.String(), "zero") - assert.Equal(t, vote.CPValueOne.String(), "one") + assert.Equal(t, vote.CPValueNo.String(), "no") + assert.Equal(t, vote.CPValueYes.String(), "yes") assert.Equal(t, vote.CPValueAbstain.String(), "abstain") assert.Equal(t, vote.CPValue(-1).String(), "unknown: -1") } @@ -508,7 +505,7 @@ func TestCPInvalidJustType(t *testing.T) { "0100" + // CP_Round: 0 "0201" + // CP_Value: 1 "0308" + // Just type: 8 <<<(Unknown Just Type)>>> - "0441" + // Just: JustTypeInitOne + "0441" + // Just: JustTypeInitYes "A0" + // Empty Array "07f6") // Signature -> Null diff --git a/util/errors/errors.go b/util/errors/errors.go index 8bd2c74b6..bbc778ee5 100644 --- a/util/errors/errors.go +++ b/util/errors/errors.go @@ -83,12 +83,12 @@ func Code(err error) int { if err == nil { return ErrNone } - _e, ok := err.(i) //nolint + e, ok := err.(i) if !ok { return ErrGeneric } - return _e.Code() + return e.Code() } func (e *withCodeError) Error() string { diff --git a/util/slice.go b/util/slice.go index 717de7c71..3e1adddf0 100644 --- a/util/slice.go +++ b/util/slice.go @@ -205,11 +205,13 @@ func Extend[T any](s *[]T, n int) { // IsSubset checks if subSet is a subset of parentSet. // It returns true if all elements of subSet are in parentSet. func IsSubset[T comparable](parentSet, subSet []T) bool { + lastIndex := 0 for i := 0; i < len(subSet); i++ { matchFound := false - for j := 0; j < len(parentSet); j++ { + for j := lastIndex; j < len(parentSet); j++ { if subSet[i] == parentSet[j] { matchFound = true + lastIndex = j break } diff --git a/util/slice_test.go b/util/slice_test.go index 2396e60cc..38f4d34be 100644 --- a/util/slice_test.go +++ b/util/slice_test.go @@ -246,7 +246,8 @@ func TestIsSubset(t *testing.T) { arr1, arr2 []int want bool }{ - {[]int{11, 1, 13, 21, 3, 7}, []int{11, 3, 7, 1}, true}, + {[]int{11, 1, 13, 21, 3, 7}, []int{11, 3, 7}, true}, + {[]int{11, 1, 13, 21, 3, 7}, []int{3, 11, 7}, false}, {[]int{1, 2, 3, 4, 5}, []int{1, 2, 3}, true}, {[]int{1, 2, 3}, []int{1, 2, 3, 4}, false}, {[]int{1, 2, 3, 4, 5}, []int{6, 7, 8}, false}, diff --git a/util/testsuite/testsuite.go b/util/testsuite/testsuite.go index f9172dd25..c59e2b686 100644 --- a/util/testsuite/testsuite.go +++ b/util/testsuite/testsuite.go @@ -21,6 +21,7 @@ import ( "github.com/pactus-project/pactus/types/validator" "github.com/pactus-project/pactus/types/vote" "github.com/pactus-project/pactus/util" + "golang.org/x/exp/slices" ) // TestSuite provides a set of helper functions for testing purposes. @@ -65,6 +66,16 @@ func (ts *TestSuite) RandBool() bool { return ts.RandInt64(2) == 0 } +// RandInt8 returns a random int8 between 0 and max: [0, max). +func (ts *TestSuite) RandInt8(max int8) int8 { + return int8(ts.RandUint64(uint64(max))) +} + +// RandUint8 returns a random uint8 between 0 and max: [0, max). +func (ts *TestSuite) RandUint8(max uint8) uint8 { + return uint8(ts.RandUint64(uint64(max))) +} + // RandInt16 returns a random int16 between 0 and max: [0, max). func (ts *TestSuite) RandInt16(max int16) int16 { return int16(ts.RandUint64(uint64(max))) @@ -175,6 +186,21 @@ func (ts *TestSuite) RandBytes(length int) []byte { return buf } +// RandSlice generates a random non-repeating slice of int32 elements with the specified length. +func (ts *TestSuite) RandSlice(length int) []int32 { + slice := []int32{} + for { + randInt := ts.RandInt32(1000) + if !slices.Contains(slice, randInt) { + slice = append(slice, randInt) + } + + if len(slice) == length { + return slice + } + } +} + // RandString generates a random string of the given length. func (ts *TestSuite) RandString(length int) string { const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -290,23 +316,23 @@ func (ts *TestSuite) GenerateTestValidator(number int32) (*validator.Validator, // GenerateTestBlockWithProposer generates a block with the give proposer address for testing purposes. func (ts *TestSuite) GenerateTestBlockWithProposer(height uint32, proposer crypto.Address, -) (*block.Block, *certificate.Certificate) { +) (*block.Block, *certificate.BlockCertificate) { return ts.makeTestBlock(height, proposer, util.Now()) } // GenerateTestBlockWithTime generates a block with the given time for testing purposes. func (ts *TestSuite) GenerateTestBlockWithTime(height uint32, tme time.Time, -) (*block.Block, *certificate.Certificate) { +) (*block.Block, *certificate.BlockCertificate) { return ts.makeTestBlock(height, ts.RandValAddress(), tme) } // GenerateTestBlock generates a block for testing purposes. -func (ts *TestSuite) GenerateTestBlock(height uint32) (*block.Block, *certificate.Certificate) { +func (ts *TestSuite) GenerateTestBlock(height uint32) (*block.Block, *certificate.BlockCertificate) { return ts.makeTestBlock(height, ts.RandValAddress(), util.Now()) } func (ts *TestSuite) makeTestBlock(height uint32, proposer crypto.Address, tme time.Time, -) (*block.Block, *certificate.Certificate) { +) (*block.Block, *certificate.BlockCertificate) { txs := block.NewTxs() tx1, _ := ts.GenerateTestTransferTx() tx2, _ := ts.GenerateTestSortitionTx() @@ -320,15 +346,15 @@ func (ts *TestSuite) makeTestBlock(height uint32, proposer crypto.Address, tme t txs.Append(tx4) txs.Append(tx5) - var prevCert *certificate.Certificate + var prevCert *certificate.BlockCertificate prevBlockHash := ts.RandHash() if height == 1 { prevCert = nil prevBlockHash = hash.UndefHash } else { - prevCert = ts.GenerateTestCertificate(height - 1) + prevCert = ts.GenerateTestBlockCertificate(height - 1) } - blockCert := ts.GenerateTestCertificate(height) + blockCert := ts.GenerateTestBlockCertificate(height) header := block.NewHeader(1, tme, ts.RandHash(), prevBlockHash, @@ -345,20 +371,33 @@ func (ts *TestSuite) makeTestBlock(height uint32, proposer crypto.Address, tme t return blk, blockCert } -// GenerateTestCertificate generates a certificate for testing purposes. -func (ts *TestSuite) GenerateTestCertificate(height uint32) *certificate.Certificate { +// GenerateTestBlockCertificate generates a block certificate for testing purposes. +func (ts *TestSuite) GenerateTestBlockCertificate(height uint32) *certificate.BlockCertificate { + sig := ts.RandBLSSignature() + + cert := certificate.NewBlockCertificate(height, ts.RandRound(), false) + + committers := ts.RandSlice(6) + absentees := []int32{committers[5]} + cert.SetSignature(committers, absentees, sig) + + err := cert.BasicCheck() + if err != nil { + panic(err) + } + + return cert +} + +// GenerateTestPrepareCertificate generates a prepare certificate for testing purposes. +func (ts *TestSuite) GenerateTestPrepareCertificate(height uint32) *certificate.VoteCertificate { sig := ts.RandBLSSignature() - c1 := ts.RandInt32NonZero(10) - c2 := ts.RandInt32NonZero(10) + 10 - c3 := ts.RandInt32NonZero(10) + 20 - c4 := ts.RandInt32NonZero(10) + 30 - cert := certificate.NewCertificate( - height, - ts.RandRound(), - []int32{c1, c2, c3, c4}, - []int32{c2}, - sig) + cert := certificate.NewVoteCertificate(height, ts.RandRound()) + + committers := ts.RandSlice(6) + absentees := []int32{committers[5]} + cert.SetSignature(committers, absentees, sig) err := cert.BasicCheck() if err != nil { @@ -454,6 +493,9 @@ func (ts *TestSuite) GenerateTestPrepareVote(height uint32, round int16) (*vote. // GenerateTestCommittee generates a committee for testing purposes. // All committee members have the same power. func (ts *TestSuite) GenerateTestCommittee(num int) (committee.Committee, []*bls.ValidatorKey) { + if num < 4 { + panic("the number of committee members must be at least 4") + } valKeys := make([]*bls.ValidatorKey, num) vals := make([]*validator.Validator, num) for i := int32(0); i < int32(num); i++ { diff --git a/www/grpc/server.go b/www/grpc/server.go index 36e6e972c..99d7876a3 100644 --- a/www/grpc/server.go +++ b/www/grpc/server.go @@ -94,6 +94,7 @@ func (s *Server) startListening(listener net.Listener) error { s.logger.Info("grpc started listening", "address", listener.Addr().String()) go func() { + s.logger.Info("grpc server started", "addr", listener.Addr()) if err := s.grpc.Serve(listener); err != nil { s.logger.Error("error on grpc serve", "error", err) } diff --git a/www/http/server.go b/www/http/server.go index e7dfa304f..f137bb9c7 100644 --- a/www/http/server.go +++ b/www/http/server.go @@ -107,6 +107,7 @@ func (s *Server) StartServer(grpcServer string) error { s.logger.Error("error on a connection", "error", err) } } + s.logger.Info("http server started", "addr", listener.Addr()) } }()