From c9d284cf44e191c300a4d5fa8d619b3333b13780 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Mon, 29 Apr 2024 00:33:26 +0800 Subject: [PATCH 01/11] feat(consensus): fast consensus path implementation --- cmd/cmd.go | 4 +- committee/committee_test.go | 4 +- consensus/commit.go | 2 +- consensus/consensus.go | 26 +- consensus/consensus_test.go | 63 +- consensus/cp.go | 83 +- consensus/cp_decide.go | 20 +- consensus/cp_mainvote.go | 22 +- consensus/cp_prevote.go | 22 +- consensus/cp_test.go | 120 +-- consensus/log/log_test.go | 4 +- consensus/precommit_test.go | 4 +- consensus/prepare_test.go | 4 +- consensus/voteset/binary_voteset.go | 4 +- consensus/voteset/voteset_test.go | 62 +- crypto/address.go | 3 +- crypto/bls/bls.go | 6 - crypto/bls/bls_test.go | 19 - fastconsensus/commit.go | 46 + fastconsensus/config.go | 44 + fastconsensus/config_test.go | 40 + fastconsensus/consensus.go | 515 ++++++++++ fastconsensus/consensus_test.go | 1016 +++++++++++++++++++ fastconsensus/cp.go | 330 ++++++ fastconsensus/cp_decide.go | 59 ++ fastconsensus/cp_mainvote.go | 103 ++ fastconsensus/cp_prevote.go | 101 ++ fastconsensus/cp_test.go | 461 +++++++++ fastconsensus/errors.go | 28 + fastconsensus/height.go | 60 ++ fastconsensus/height_test.go | 63 ++ fastconsensus/interface.go | 45 + fastconsensus/log/log.go | 126 +++ fastconsensus/log/log_test.go | 105 ++ fastconsensus/log/messages.go | 58 ++ fastconsensus/manager.go | 208 ++++ fastconsensus/manager_test.go | 192 ++++ fastconsensus/mediator.go | 53 + fastconsensus/mock.go | 140 +++ fastconsensus/precommit.go | 76 ++ fastconsensus/precommit_test.go | 39 + fastconsensus/prepare.go | 83 ++ fastconsensus/prepare_test.go | 104 ++ fastconsensus/propose.go | 81 ++ fastconsensus/propose_test.go | 108 ++ fastconsensus/spec/.gitignore | 8 + fastconsensus/spec/Pactus.cfg | 12 + fastconsensus/spec/Pactus.pdf | Bin 0 -> 187124 bytes fastconsensus/spec/Pactus.tla | 551 ++++++++++ fastconsensus/spec/README.md | 24 + fastconsensus/spec/temporary.cfg | 11 + fastconsensus/state.go | 15 + fastconsensus/ticker.go | 41 + fastconsensus/voteset/binary_voteset.go | 184 ++++ fastconsensus/voteset/block_voteset.go | 139 +++ fastconsensus/voteset/vote_box.go | 25 + fastconsensus/voteset/vote_box_test.go | 28 + fastconsensus/voteset/voteset.go | 46 + fastconsensus/voteset/voteset_test.go | 439 ++++++++ state/errors.go | 8 +- state/execution_test.go | 4 +- state/facade.go | 4 +- state/lastinfo/last_info.go | 6 +- state/lastinfo/last_info_test.go | 5 +- state/mock.go | 4 +- state/score/score.go | 6 +- state/score/score_test.go | 21 +- state/state.go | 12 +- state/state_test.go | 40 +- state/validation.go | 16 +- state/validation_test.go | 7 +- store/interface.go | 4 +- store/mock.go | 6 +- store/store.go | 6 +- sync/bundle/message/block_announce.go | 6 +- sync/bundle/message/block_announce_test.go | 2 +- sync/bundle/message/blocks_response.go | 14 +- sync/bundle/message/blocks_response_test.go | 2 +- sync/cache/cache.go | 8 +- sync/handler_block_announce_test.go | 2 +- sync/handler_blocks_response_test.go | 8 +- tests/main_test.go | 12 +- types/block/block.go | 10 +- types/certificate/block_certificate.go | 71 ++ types/certificate/block_certificate_test.go | 257 +++++ types/certificate/certificate.go | 199 ++-- types/certificate/certificate_test.go | 241 +---- types/certificate/errors.go | 12 - types/certificate/vote_certificate.go | 77 ++ types/certificate/vote_certificate_test.go | 32 + types/proposal/proposal.go | 24 +- types/vote/cp_just.go | 28 +- types/vote/cp_vote.go | 14 +- types/vote/vote.go | 6 +- types/vote/vote_test.go | 45 +- util/slice.go | 4 +- util/slice_test.go | 3 +- util/testsuite/testsuite.go | 88 +- www/grpc/server.go | 1 + www/http/server.go | 1 + 100 files changed, 6859 insertions(+), 736 deletions(-) create mode 100644 fastconsensus/commit.go create mode 100644 fastconsensus/config.go create mode 100644 fastconsensus/config_test.go create mode 100644 fastconsensus/consensus.go create mode 100644 fastconsensus/consensus_test.go create mode 100644 fastconsensus/cp.go create mode 100644 fastconsensus/cp_decide.go create mode 100644 fastconsensus/cp_mainvote.go create mode 100644 fastconsensus/cp_prevote.go create mode 100644 fastconsensus/cp_test.go create mode 100644 fastconsensus/errors.go create mode 100644 fastconsensus/height.go create mode 100644 fastconsensus/height_test.go create mode 100644 fastconsensus/interface.go create mode 100644 fastconsensus/log/log.go create mode 100644 fastconsensus/log/log_test.go create mode 100644 fastconsensus/log/messages.go create mode 100644 fastconsensus/manager.go create mode 100644 fastconsensus/manager_test.go create mode 100644 fastconsensus/mediator.go create mode 100644 fastconsensus/mock.go create mode 100644 fastconsensus/precommit.go create mode 100644 fastconsensus/precommit_test.go create mode 100644 fastconsensus/prepare.go create mode 100644 fastconsensus/prepare_test.go create mode 100644 fastconsensus/propose.go create mode 100644 fastconsensus/propose_test.go create mode 100644 fastconsensus/spec/.gitignore create mode 100644 fastconsensus/spec/Pactus.cfg create mode 100644 fastconsensus/spec/Pactus.pdf create mode 100644 fastconsensus/spec/Pactus.tla create mode 100644 fastconsensus/spec/README.md create mode 100644 fastconsensus/spec/temporary.cfg create mode 100644 fastconsensus/state.go create mode 100644 fastconsensus/ticker.go create mode 100644 fastconsensus/voteset/binary_voteset.go create mode 100644 fastconsensus/voteset/block_voteset.go create mode 100644 fastconsensus/voteset/vote_box.go create mode 100644 fastconsensus/voteset/vote_box_test.go create mode 100644 fastconsensus/voteset/voteset.go create mode 100644 fastconsensus/voteset/voteset_test.go create mode 100644 types/certificate/block_certificate.go create mode 100644 types/certificate/block_certificate_test.go create mode 100644 types/certificate/vote_certificate.go create mode 100644 types/certificate/vote_certificate_test.go diff --git a/cmd/cmd.go b/cmd/cmd.go index 7cef097bc..d1a27a070 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -435,8 +435,8 @@ func makeLocalGenesis(w wallet.Wallet) *genesis.Genesis { crypto.TreasuryAddress: acc, } - vals := make([]*validator.Validator, 4) - for i := 0; i < 4; i++ { + vals := make([]*validator.Validator, 6) + for i := 0; i < 6; 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/consensus.go b/consensus/consensus.go index 76acec464..843e0f08e 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.signerInfo(votes)) + + return cert +} + +// signerInfo 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) signerInfo(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.signerInfo(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..e471d1e8a 100644 --- a/consensus/consensus_test.go +++ b/consensus/consensus_test.go @@ -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) @@ -552,40 +552,45 @@ func TestPickRandomVote(t *testing.T) { cpRound := int16(1) // === 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, + td.addCPPreVote(td.consP, hash.UndefHash, 1, 0, cpRound+1, vote.CPValueYes, &vote.JustPreVoteHard{QCert: certPreVote}, tIndexY) - td.addCPMainVote(td.consP, hash.UndefHash, 1, 0, cpRound, vote.CPValueOne, + td.addCPMainVote(td.consP, hash.UndefHash, 1, 0, cpRound, 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, cpRound, vote.CPValueYes, &vote.JustDecided{QCert: certMainVote}, tIndexY) assert.NotNil(t, td.consP.PickRandomVote(0)) @@ -729,10 +734,10 @@ func TestCases(t *testing.T) { round int16 description string }{ - {1697898884837384019, 2, "1/3+ cp:PRE-VOTE in prepare step"}, + {1697898884837384019, 1, "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"}, + {1697900665869342730, 2, "Conflicting votes, cp-round=1"}, {1697887970998950590, 1, "consP & consB: Change Proposer, consX & consY: Commit (2 block announces)"}, } @@ -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..81fa51d3b 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(), @@ -63,8 +56,8 @@ func (cp *changeProposer) checkJustInitZero(just vote.Just, blockHash hash.Hash) return nil } -func (*changeProposer) checkJustInitOne(just vote.Just) error { - _, ok := just.(*vote.JustInitOne) +func (cp *changeProposer) checkJustInitOne(just vote.Just) error { + _, 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(), @@ -196,7 +171,7 @@ func (cp *changeProposer) checkJustMainVoteConflict(just vote.Just, return err } case vote.JustTypePreVoteHard: - err := cp.checkJustPreVoteHard(j.Just0, blockHash, cpRound, vote.CPValueZero) + err := cp.checkJustPreVoteHard(j.Just0, blockHash, cpRound, vote.CPValueNo) if err != nil { return err } @@ -207,7 +182,7 @@ func (cp *changeProposer) checkJustMainVoteConflict(just vote.Just, } } - err := cp.checkJustPreVoteHard(j.Just1, hash.UndefHash, cpRound, vote.CPValueOne) + err := cp.checkJustPreVoteHard(j.Just1, hash.UndefHash, cpRound, vote.CPValueYes) if err != nil { return err } @@ -221,7 +196,7 @@ func (cp *changeProposer) checkJustPreVote(v *vote.Vote) error { if v.CPRound() == 0 { switch just.Type() { case vote.JustTypeInitZero: - err := cp.checkCPValue(v, vote.CPValueZero) + err := cp.checkCPValue(v, vote.CPValueNo) if err != nil { return err } @@ -229,7 +204,7 @@ func (cp *changeProposer) checkJustPreVote(v *vote.Vote) error { return cp.checkJustInitZero(just, v.BlockHash()) case vote.JustTypeInitOne: - err := cp.checkCPValue(v, vote.CPValueOne) + 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(), int16(v.CPValue()), byte(v.CPRound())) 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..c5c42a8aa 100644 --- a/consensus/cp_mainvote.go +++ b/consensus/cp_mainvote.go @@ -19,31 +19,31 @@ 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(), @@ -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..3b6e32e03 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, 0, vote.CPValueYes, preVote0.CPJust(), tIndexX) + td.addCPPreVote(td.consP, hash.UndefHash, h, r, 0, 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, 0, vote.CPValueYes, mainVote0.CPJust(), tIndexX) + td.addCPMainVote(td.consP, hash.UndefHash, h, r, 0, 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, 0, vote.CPValueNo, preVote0.CPJust(), tIndexX) + td.addCPPreVote(td.consP, p.Block().Hash(), h, r, 0, 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, 0, vote.CPValueNo, mainVote0.CPJust(), tIndexX) + td.addCPMainVote(td.consP, p.Block().Hash(), h, r, 0, 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, 0, vote.CPValueNo, just0, tIndexX) + td.addCPPreVote(td.consP, blockHash, h, r, 0, vote.CPValueNo, just0, tIndexY) + td.addCPPreVote(td.consP, blockHash, h, r, 0, 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), + Just0: &vote.JustInitNo{ + QCert: td.GenerateTestPrepareCertificate(h), }, - Just1: &vote.JustInitOne{}, + Just1: &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), + Just0: &vote.JustInitNo{ + QCert: td.GenerateTestPrepareCertificate(h), }, - Just1: &vote.JustInitOne{}, + Just1: &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), + QCert: td.GenerateTestPrepareCertificate(h), }, - Just1: &vote.JustInitOne{}, + Just1: &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), + Just0: &vote.JustInitNo{ + QCert: td.GenerateTestPrepareCertificate(h), }, Just1: &vote.JustPreVoteSoft{ - QCert: td.GenerateTestCertificate(h), + QCert: td.GenerateTestPrepareCertificate(h), }, } v := vote.NewCPMainVote(td.RandHash(), h, r, 1, vote.CPValueAbstain, just, td.consB.valKey.Address()) @@ -398,12 +398,12 @@ func TestInvalidJustMainVoteConflict(t *testing.T) { }) 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{}, + Just1: &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), + 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..a5c2977e0 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, 0, vote.CPValueYes, &vote.JustInitYes{}, tIndexX) + td.addCPPreVote(td.consP, hash.UndefHash, h, r, 0, 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..5422b919e 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, 0, vote.CPValueYes, &vote.JustInitYes{}, tIndexX) + td.addCPPreVote(td.consP, hash.UndefHash, 2, 0, 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..e10517f3a --- /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 (s *commitState) onAddVote(_ *vote.Vote) { + panic("Unreachable") +} + +func (s *commitState) onSetProposal(_ *proposal.Proposal) { + panic("Unreachable") +} + +func (s *commitState) onTimeout(_ *ticker) { + panic("Unreachable") +} + +func (s *commitState) name() string { + return "commit" +} diff --git a/fastconsensus/config.go b/fastconsensus/config.go new file mode 100644 index 000000000..380888ba1 --- /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: "timeout for change proposer can't be negative", + } + } + if conf.ChangeProposerDelta <= 0 { + return ConfigError{ + Reason: "change proposer delta can't be negative", + } + } + 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..008fea4eb --- /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 can't be negative"}) + + c3.ChangeProposerTimeout = 0 * time.Second + assert.ErrorIs(t, c3.BasicCheck(), ConfigError{Reason: "timeout for change proposer can't be negative"}) + + c4.ChangeProposerTimeout = -1 * time.Second + assert.ErrorIs(t, c4.BasicCheck(), ConfigError{Reason: "timeout for change proposer can't be negative"}) + + 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..b38853029 --- /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 newConsensus(conf, bcState, + valKey, rewardAddr, broadcaster, mediator) +} + +func newConsensus( + 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.moveToNewHeight() + // 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.moveToNewHeight() +} + +func (cs *consensus) moveToNewHeight() { + 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.signerInfo(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.signerInfo(votes)) + + return cert +} + +// signerInfo 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) signerInfo(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..2ac899cf4 --- /dev/null +++ b/fastconsensus/consensus_test.go @@ -0,0 +1,1016 @@ +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] = newConsensus(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.TypeQueryVotes { + 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 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) checkHeightRound(t *testing.T, cons *consensus, height uint32, round int16) { + t.Helper() + + checkHeightRound(t, cons, height, 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, + cpRound int16, cpVal vote.CPValue, just vote.Just, valID int, +) { + v := vote.NewCPPreVote(blockHash, height, round, cpRound, 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, +) { + v := vote.NewCPMainVote(blockHash, height, round, cpRound, 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, +) { + v := vote.NewCPDecidedVote(blockHash, height, round, cpRound, cpVal, just, td.valKeys[valID].Address()) + 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 newHeightTimeout(cons *consensus) { + cons.lk.Lock() + cons.currentState.onTimeout(&ticker{0, cons.height, cons.round, tickerTargetNewHeight}) + cons.lk.Unlock() +} + +func (td *testData) newHeightTimeout(cons *consensus) { + newHeightTimeout(cons) +} + +func (td *testData) queryProposalTimeout(cons *consensus) { + cons.lk.Lock() + cons.currentState.onTimeout(&ticker{0, cons.height, cons.round, tickerTargetQueryProposal}) + cons.lk.Unlock() +} + +func (td *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 (td *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 +} + +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) + Cons := NewConsensus(testConfig(), st, valKey, valKey.Address(), make(chan message.Message, 100), + newConcreteMediator()) + cons := Cons.(*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) +// td.commitBlockForAllStates(t) // height 2 + +// h := uint32(2) +// r := int16(0) +// prop := td.makeProposal(t, h, r) +// blockHash := p.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) + +// preVote0 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, blockHash) +// td.addCPPreVote(td.consP, p.Block().Hash(), h, r, 0, vote.CPValueNo, preVote0.CPJust(), tIndexX) +// td.addCPPreVote(td.consP, p.Block().Hash(), h, r, 0, vote.CPValueNo, preVote0.CPJust(), tIndexY) + +// mainVote0 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPMainVote, blockHash) +// td.addCPMainVote(td.consP, blockHash, h, r, 0, vote.CPValueNo, mainVote0.CPJust(), tIndexX) +// td.addCPMainVote(td.consP, blockHash, h, r, 0, 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.addPrecommitVote(td.consP, blockHash, h, r, tIndexB) + +// // consP receives proposal now +// td.consP.SetProposal(p) + +// td.shouldPublishVote(t, td.consP, vote.VoteTypePrepare, p.Block().Hash()) +// td.shouldPublishBlockAnnounce(t, td.consP, p.Block().Hash()) +// } + +func TestPickRandomVote(t *testing.T) { + td := setup(t) + + td.enterNewHeight(td.consP) + assert.Nil(t, td.consP.PickRandomVote(0)) + cpRound := int16(1) + + // === make valid certificate + 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() + + 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) + + mainVoteCommitters := []int32{} + mainVoteSigs := []*bls.Signature{} + for i, val := range td.consP.validators { + 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)) + } + mainVoteAggSig := bls.SignatureAggregate(mainVoteSigs...) + 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.CPValueYes, + &vote.JustPreVoteHard{QCert: certPreVote}, tIndexY) + td.addCPMainVote(td.consP, hash.UndefHash, 1, 0, cpRound, vote.CPValueYes, + &vote.JustMainVoteNoConflict{QCert: certPreVote}, tIndexY) + td.addCPDecidedVote(td.consP, hash.UndefHash, 1, 0, cpRound, vote.CPValueYes, + &vote.JustDecided{QCert: certMainVote}, tIndexY) + + assert.NotNil(t, td.consP.PickRandomVote(0)) + + // Round 1 + td.enterNextRound(td.consP) + td.addPrepareVote(td.consP, td.RandHash(), 1, 1, tIndexY) + + rndVote0 := td.consP.PickRandomVote(0) + assert.NotEqual(t, rndVote0.Type(), vote.VoteTypePrepare, "Should not pick prepare votes") + + rndVote1 := td.consP.PickRandomVote(1) + assert.Equal(t, rndVote1.Type(), vote.VoteTypePrepare) +} + +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() + Cons := NewConsensus(testConfig(), state.MockingState(td.TestSuite), + valKey, valKey.Address(), make(chan message.Message, 100), newConcreteMediator()) + nonActiveCons := Cons.(*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.JustInitZero{ +// QCert: td.consB.makeCPMainVoteCertificate( +// 0, +// vote.CPValueNo, +// 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.TypeQueryVotes: + // 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.TypeTransactions, + 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..a66829099 --- /dev/null +++ b/fastconsensus/cp.go @@ -0,0 +1,330 @@ +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 (cp *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 (cp *changeProposer) cpCheckCPValue(vte *vote.Vote, allowedValues ...vote.CPValue) error { + for _, v := range allowedValues { + if vte.CPValue() == v { + return nil + } + } + + return invalidJustificationError{ + JustType: vte.CPJust().Type(), + Reason: fmt.Sprintf("invalid value: %v", vte.CPValue()), + } +} + +func (cp *changeProposer) cpCheckJustInitZero(just vote.Just, blockHash hash.Hash) error { + j, ok := just.(*vote.JustInitNo) + if !ok { + return invalidJustificationError{ + JustType: just.Type(), + Reason: "invalid just data", + } + } + + err := j.QCert.ValidatePrepare(cp.validators, blockHash) + if err != nil { + return invalidJustificationError{ + JustType: j.Type(), + Reason: err.Error(), + } + } + + return nil +} + +func (cp *changeProposer) cpCheckJustInitOne(just vote.Just) error { + _, ok := just.(*vote.JustInitYes) + if !ok { + return invalidJustificationError{ + JustType: just.Type(), + Reason: "invalid just data", + } + } + + 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{ + JustType: just.Type(), + Reason: "invalid just data", + } + } + + err := j.QCert.ValidateCPPreVote(cp.validators, + blockHash, cpRound-1, byte(cpValue)) + if err != nil { + return invalidJustificationError{ + JustType: just.Type(), + Reason: err.Error(), + } + } + + return nil +} + +func (cp *changeProposer) cpCheckJustPreVoteSoft(just vote.Just, + blockHash hash.Hash, cpRound int16, +) error { + j, ok := just.(*vote.JustPreVoteSoft) + if !ok { + return invalidJustificationError{ + JustType: just.Type(), + Reason: "invalid just data", + } + } + + err := j.QCert.ValidateCPMainVote(cp.validators, + blockHash, cpRound-1, byte(vote.CPValueAbstain)) + if err != nil { + return invalidJustificationError{ + JustType: just.Type(), + 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{ + JustType: just.Type(), + Reason: "invalid just data", + } + } + + err := j.QCert.ValidateCPPreVote(cp.validators, + blockHash, cpRound, byte(cpValue)) + if err != nil { + return invalidJustificationError{ + JustType: j.Type(), + Reason: err.Error(), + } + } + + return nil +} + +//nolint:exhaustive // refactor me; check just by just_type, not vote_type +func (cp *changeProposer) cpCheckJustMainVoteConflict(just vote.Just, + blockHash hash.Hash, cpRound int16, +) error { + j, ok := just.(*vote.JustMainVoteConflict) + if !ok { + return invalidJustificationError{ + JustType: just.Type(), + Reason: "invalid just data", + } + } + + if cpRound == 0 { + err := cp.cpCheckJustInitZero(j.Just0, blockHash) + if err != nil { + return err + } + + err = cp.cpCheckJustInitOne(j.Just1) + if err != nil { + return err + } + + return nil + } + + // Just0 can be for Zero or Abstain values. + switch j.Just0.Type() { + case vote.JustTypePreVoteSoft: + err := cp.cpCheckJustPreVoteSoft(j.Just0, blockHash, cpRound) + if err != nil { + return err + } + case vote.JustTypePreVoteHard: + err := cp.cpCheckJustPreVoteHard(j.Just0, blockHash, cpRound, vote.CPValueNo) + if err != nil { + return err + } + default: + return invalidJustificationError{ + JustType: just.Type(), + Reason: fmt.Sprintf("unexpected justification: %s", j.Just0.Type()), + } + } + + err := cp.cpCheckJustPreVoteHard(j.Just1, hash.UndefHash, cpRound, vote.CPValueYes) + if err != nil { + return err + } + + return nil +} + +//nolint:exhaustive // refactor me; check just by just_type, not vote_type +func (cp *changeProposer) cpCheckJustPreVote(v *vote.Vote) error { + just := v.CPJust() + if v.CPRound() == 0 { + switch just.Type() { + case vote.JustTypeInitZero: + err := cp.cpCheckCPValue(v, vote.CPValueNo) + if err != nil { + return err + } + + return cp.cpCheckJustInitZero(just, v.BlockHash()) + + case vote.JustTypeInitOne: + err := cp.cpCheckCPValue(v, vote.CPValueYes) + if err != nil { + return err + } + + return cp.cpCheckJustInitOne(just) + default: + return invalidJustificationError{ + JustType: just.Type(), + Reason: "invalid pre-vote justification", + } + } + } else { + switch just.Type() { + case vote.JustTypePreVoteSoft: + err := cp.cpCheckCPValue(v, vote.CPValueNo, vote.CPValueYes) + if err != nil { + return err + } + + return cp.cpCheckJustPreVoteSoft(just, v.BlockHash(), v.CPRound()) + + case vote.JustTypePreVoteHard: + err := cp.cpCheckCPValue(v, vote.CPValueNo, vote.CPValueYes) + if err != nil { + return err + } + + return cp.cpCheckJustPreVoteHard(just, v.BlockHash(), v.CPRound(), v.CPValue()) + + default: + return invalidJustificationError{ + JustType: just.Type(), + Reason: "invalid pre-vote justification", + } + } + } +} + +//nolint:exhaustive // refactor me; check just by just_type, not vote_type +func (cp *changeProposer) cpCheckJustMainVote(v *vote.Vote) error { + just := v.CPJust() + switch just.Type() { + case vote.JustTypeMainVoteNoConflict: + err := cp.cpCheckCPValue(v, vote.CPValueNo, vote.CPValueYes) + if err != nil { + return err + } + + return cp.cpCheckJustMainVoteNoConflict(just, v.BlockHash(), v.CPRound(), v.CPValue()) + + case vote.JustTypeMainVoteConflict: + err := cp.cpCheckCPValue(v, vote.CPValueAbstain) + if err != nil { + return err + } + + return cp.cpCheckJustMainVoteConflict(just, v.BlockHash(), v.CPRound()) + + default: + return invalidJustificationError{ + JustType: just.Type(), + Reason: "invalid main-vote justification", + } + } +} + +func (cp *changeProposer) cpCheckJustDecide(v *vote.Vote) error { + err := cp.cpCheckCPValue(v, vote.CPValueNo, vote.CPValueYes) + if err != nil { + return err + } + j, ok := v.CPJust().(*vote.JustDecided) + if !ok { + return invalidJustificationError{ + JustType: j.Type(), + Reason: "invalid just data", + } + } + + err = j.QCert.ValidateCPMainVote(cp.validators, + v.BlockHash(), int16(v.CPValue()), byte(v.CPRound())) + if err != nil { + return invalidJustificationError{ + JustType: j.Type(), + Reason: err.Error(), + } + } + + return nil +} + +//nolint:exhaustive // refactor me; check just by just_type, not vote_type +func (cp *changeProposer) cpCheckJust(v *vote.Vote) error { + switch v.Type() { + case vote.VoteTypeCPPreVote: + return cp.cpCheckJustPreVote(v) + case vote.VoteTypeCPMainVote: + return cp.cpCheckJustMainVote(v) + case vote.VoteTypeCPDecided: + return cp.cpCheckJustDecide(v) + 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..d8182bc2f --- /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 (s *cpDecideState) name() string { + return "cp:decide" +} diff --git a/fastconsensus/cp_mainvote.go b/fastconsensus/cp_mainvote.go new file mode 100644 index 000000000..b2b1a1081 --- /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{ + Just0: vote0.CPJust(), + Just1: 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 (s *cpMainVoteState) onSetProposal(_ *proposal.Proposal) { + // Ignore proposal +} + +func (s *cpMainVoteState) onTimeout(_ *ticker) { + // Ignore timeouts +} + +func (s *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..ea0a1e11d --- /dev/null +++ b/fastconsensus/cp_prevote.go @@ -0,0 +1,101 @@ +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() +} + +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 (s *cpPreVoteState) onSetProposal(_ *proposal.Proposal) { +} + +func (s *cpPreVoteState) onTimeout(_ *ticker) { +} + +func (s *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..f80eb569f --- /dev/null +++ b/fastconsensus/cp_test.go @@ -0,0 +1,461 @@ +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, 0, vote.CPValueYes, preVote0.CPJust(), tIndexX) + td.addCPPreVote(td.consP, hash.UndefHash, h, r, 0, 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.CPValueYes, mainVote0.CPJust(), tIndexX) + td.addCPMainVote(td.consP, hash.UndefHash, h, r, 0, vote.CPValueYes, mainVote0.CPJust(), tIndexY) + + td.shouldPublishVote(t, td.consP, vote.VoteTypeCPDecided, hash.UndefHash) + 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, 0, vote.CPValueNo, preVote0.CPJust(), tIndexX) + td.addCPPreVote(td.consP, blockHash, h, r, 0, vote.CPValueNo, preVote0.CPJust(), tIndexY) + + mainVote0 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPMainVote, blockHash) + td.addCPMainVote(td.consP, blockHash, h, r, 0, vote.CPValueNo, mainVote0.CPJust(), tIndexX) + td.addCPMainVote(td.consP, blockHash, h, r, 0, 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) + 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, 0, vote.CPValueNo, just0, tIndexX) + td.addCPPreVote(td.consP, blockHash, h, r, 0, vote.CPValueNo, just0, tIndexY) + td.addCPPreVote(td.consP, blockHash, h, r, 0, 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 TestInvalidJustInitOne(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{ + JustType: just.Type(), + Reason: "invalid value: no", + }) + }) + + t.Run("invalid block hash", 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{ + JustType: just.Type(), + Reason: "invalid pre-vote justification", + }) + }) + + t.Run("with main-vote justification", func(t *testing.T) { + invJust := &vote.JustMainVoteNoConflict{} + v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueYes, invJust, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + JustType: invJust.Type(), + Reason: "invalid pre-vote justification", + }) + }) +} + +func TestInvalidJustInitZero(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{ + JustType: just.Type(), + Reason: "invalid value: yes", + }) + }) + + t.Run("cp-round should be zero", 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{ + JustType: just.Type(), + Reason: "invalid pre-vote justification", + }) + }) + + 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{ + JustType: just.Type(), + Reason: "invalid value: abstain", + }) + }) + + t.Run("cp-round should not be zero", 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{ + JustType: just.Type(), + Reason: "invalid pre-vote justification", + }) + }) + + 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{ + JustType: just.Type(), + 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{ + JustType: just.Type(), + Reason: "invalid value: abstain", + }) + }) + + t.Run("cp-round should not be zero", 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{ + JustType: just.Type(), + Reason: "invalid pre-vote justification", + }) + }) + + 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{ + JustType: just.Type(), + 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{ + JustType: just.Type(), + 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{ + JustType: just.Type(), + 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{ + Just0: &vote.JustInitNo{ + QCert: td.GenerateTestPrepareCertificate(h), + }, + Just1: &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{ + JustType: just.Type(), + Reason: "invalid value: no", + }) + }) + + t.Run("invalid value: yes", func(t *testing.T) { + just := &vote.JustMainVoteConflict{ + Just0: &vote.JustInitNo{ + QCert: td.GenerateTestPrepareCertificate(h), + }, + Just1: &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{ + JustType: just.Type(), + Reason: "invalid value: yes", + }) + }) + + t.Run("invalid value: unexpected justification (just0)", func(t *testing.T) { + just := &vote.JustMainVoteConflict{ + Just0: &vote.JustPreVoteSoft{ + QCert: td.GenerateTestPrepareCertificate(h), + }, + Just1: &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{ + JustType: vote.JustTypePreVoteSoft, + Reason: "invalid just data", + }) + }) + + t.Run("invalid value: unexpected justification", func(t *testing.T) { + just := &vote.JustMainVoteConflict{ + Just0: &vote.JustInitNo{ + QCert: td.GenerateTestPrepareCertificate(h), + }, + Just1: &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{ + JustType: just.Type(), + Reason: "unexpected justification: JustInitZero", + }) + }) + + t.Run("invalid certificate", func(t *testing.T) { + just0 := &vote.JustInitNo{ + QCert: td.GenerateTestPrepareCertificate(h), + } + just := &vote.JustMainVoteConflict{ + Just0: just0, + Just1: &vote.JustInitYes{}, + } + v := vote.NewCPMainVote(td.RandHash(), h, r, 0, vote.CPValueAbstain, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.cpCheckJust(v) + assert.Error(t, err) + }) + + t.Run("invalid certificate", func(t *testing.T) { + just0 := &vote.JustPreVoteSoft{ + QCert: td.GenerateTestPrepareCertificate(h), + } + just := &vote.JustMainVoteConflict{ + Just0: just0, + Just1: &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{ + JustType: just0.Type(), + 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{ + JustType: just.Type(), + 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{ + JustType: just.Type(), + 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..c73bfa465 --- /dev/null +++ b/fastconsensus/errors.go @@ -0,0 +1,28 @@ +package fastconsensus + +import ( + "fmt" + + "github.com/pactus-project/pactus/types/vote" +) + +// invalidJustificationError is returned when the justification for a change-proposer +// vote is invalid. +type invalidJustificationError struct { + JustType vote.JustType + Reason string +} + +func (e invalidJustificationError) Error() string { + return fmt.Sprintf("invalid justification: %s, reason: %s", + e.JustType.String(), 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..0c1915bf7 --- /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 (s *newHeightState) onSetProposal(_ *proposal.Proposal) { + // Ignore proposal +} + +func (s *newHeightState) onTimeout(t *ticker) { + if t.Target == tickerTargetNewHeight { + if s.active { + s.enterNewState(s.proposeState) + } + } +} + +func (s *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..cbcbe3a01 --- /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(vote *vote.Vote) + SetProposal(proposal *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(vote *vote.Vote) + SetProposal(proposal *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..4aeced92b --- /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 (mgr *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..029106a20 --- /dev/null +++ b/fastconsensus/manager_test.go @@ -0,0 +1,192 @@ +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) + + stateHeight := ts.RandHeight() + blk, cert := ts.GenerateTestBlock(stateHeight) + st.TestStore.SaveBlock(blk, cert) + + Mgr := NewManager(testConfig(), st, valKeys, rewardAddrs, broadcastCh) + mgr := Mgr.(*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) { + assert.False(t, mgr.HasActiveInstance()) + mgr.MoveToNewHeight() + h, r := mgr.HeightRound() + + assert.True(t, mgr.HasActiveInstance()) + assert.Equal(t, stateHeight+1, h) + assert.Zero(t, r) + }) + + t.Run("Testing add vote", func(t *testing.T) { + v := vote.NewPrepareVote(ts.RandHash(), stateHeight+1, 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) { + b, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) + p := proposal.NewProposal(stateHeight+1, 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) { + v := vote.NewPrepareVote(ts.RandHash(), stateHeight-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) { + b, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) + p := proposal.NewProposal(stateHeight-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) { + v1 := vote.NewPrepareVote(ts.RandHash(), stateHeight+2, 0, valKeys[0].Address()) + v2 := vote.NewPrepareVote(ts.RandHash(), stateHeight+3, 0, valKeys[0].Address()) + v3 := vote.NewPrepareVote(ts.RandHash(), stateHeight+4, 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) + + blk, cert := ts.GenerateTestBlock(stateHeight + 1) + err := st.CommitBlock(blk, cert) + assert.NoError(t, err) + stateHeight++ + + blk, cert = ts.GenerateTestBlock(stateHeight + 1) + err = st.CommitBlock(blk, cert) + assert.NoError(t, err) + stateHeight++ + + mgr.MoveToNewHeight() + + assert.Len(t, mgr.upcomingVotes, 1) + }) + + t.Run("Processing upcoming proposal", func(t *testing.T) { + b1, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) + p1 := proposal.NewProposal(stateHeight+2, 0, b1) + + b2, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) + p2 := proposal.NewProposal(stateHeight+3, 0, b2) + + b3, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) + p3 := proposal.NewProposal(stateHeight+4, 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) + + blk, cert := ts.GenerateTestBlock(stateHeight + 1) + err := st.CommitBlock(blk, cert) + assert.NoError(t, err) + stateHeight++ + + blk, cert = ts.GenerateTestBlock(stateHeight + 1) + err = st.CommitBlock(blk, cert) + assert.NoError(t, err) + stateHeight++ + + mgr.MoveToNewHeight() + + assert.Len(t, mgr.upcomingProposals, 1) + }) +} + +func TestMediator(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + committeeSize := 6 + st := state.MockingState(ts) + cmt, valKeys := ts.GenerateTestCommittee(committeeSize) + st.TestCommittee = cmt + st.TestParams.BlockIntervalInSecond = 1 + + rewardAddrs := []crypto.Address{} + for i := 0; i < committeeSize; i++ { + rewardAddrs = append(rewardAddrs, ts.RandAccAddress()) + } + broadcastCh := make(chan message.Message, 500) + + stateHeight := ts.RandHeight() + blk, cert := ts.GenerateTestBlock(stateHeight) + st.TestStore.SaveBlock(blk, cert) + + Mgr := NewManager(testConfig(), st, valKeys, rewardAddrs, broadcastCh) + mgr := Mgr.(*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..d527babd7 --- /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, proposal *proposal.Proposal) + OnPublishVote(from Consensus, vote *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..876b06ffd --- /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 (m *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 (m *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..0ca57b20b --- /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 (s *precommitState) onTimeout(_ *ticker) { + // Ignore timeouts +} + +func (s *precommitState) name() string { + return "precommit" +} diff --git a/fastconsensus/precommit_test.go b/fastconsensus/precommit_test.go new file mode 100644 index 000000000..faf4199de --- /dev/null +++ b/fastconsensus/precommit_test.go @@ -0,0 +1,39 @@ +package fastconsensus + +// 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() + +// cert := certificate.NewVoteCertificate(h, r) + +// signBytes := cert.SignBytes(propBlockHash) +// sigX := td.consX.valKey.Sign(signBytes) +// sigY := td.consY.valKey.Sign(signBytes) +// sigM := td.consM.valKey.Sign(signBytes) +// sig := bls.SignatureAggregate(sigX, sigY, sigM) +// cert.SetSignature([]int32{0, 1, 2, 3, 4, 5}, []int32{2, 3, 5}, sig) +// just := &vote.JustDecided{ +// QCert: cert, +// } +// decideVote := vote.NewCPDecidedVote(propBlockHash, h, r, 0, vote.CPValueNo, just, 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..96b0d17db --- /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 (s *prepareState) name() string { + return "prepare" +} diff --git a/fastconsensus/prepare_test.go b/fastconsensus/prepare_test.go new file mode 100644 index 000000000..460d6a9c2 --- /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.TypeQueryVotes) +} + +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, 0, vote.CPValueYes, &vote.JustInitYes{}, tIndexX) + td.addCPPreVote(td.consP, hash.UndefHash, 2, 0, 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..81eb7d011 --- /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 (s *proposeState) onAddVote(_ *vote.Vote) { + panic("Unreachable") +} + +func (s *proposeState) onSetProposal(_ *proposal.Proposal) { + panic("Unreachable") +} + +func (s *proposeState) onTimeout(_ *ticker) { + panic("Unreachable") +} + +func (s *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 0000000000000000000000000000000000000000..ecab6731c988cb000fc17b9eb929b769b073ffd3 GIT binary patch literal 187124 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

(w$2AFJp2vVRjcX+A=Oqu)Bgc3Cpb3jDq zgy9!0N7~Z$!F1ALW8j~1Hqnb}`NuO_^?$z$UmK*-pfej@i&QNF>`R+#k%st_9{eVzh!UO!=;b? zcx&xao}K`sq$!=iMJ1YI?;$#Ad**x2(qK77g332SfW4?bAdmTkjPSri$EDx($eAj*zqjfDvC(c zZjtmt5jVa46?Lq|zpuXwCn+Ie;psz9GPrlcjSW)#9L4Ivy+z-R}Q0j-Vw02Z6B9k}<#< zh)1^@r_XMIp{o8>*UmV>1gp|`aZU+FGB9X^9(-3Wt(cGEor#`lfER3!m29MtUO=%FKVG;Z{8oNuMv6Gt4@L|vAnbJC&K z%5N2qYh#MVuOf(g(9?X=`{atX*eYq7vQ`b5AcFzlL=YwW&sW4P+3~KLnsS}qQJRQ?UZU;9_KKN^RXEQ#4; zz_p@6Th3nRwJqDc*CXS_S( zbt^k)DH^pl^th=>Mc)_R@6Ou0I1b1%N)A1aFy$6yFA=PZy5gQ+3X!qasM-t;dN9Ul zRC)VR2|r_OZ+c=uXVsw+n-7i!utMFL;9H1K`CtyP7AKTE3{G>il$Qb5!+j_2$w

    E^_QG8|uP1)=P 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 literal 0 HcmV?d00001 diff --git a/fastconsensus/spec/Pactus.tla b/fastconsensus/spec/Pactus.tla new file mode 100644 index 000000000..118bbe813 --- /dev/null +++ b/fastconsensus/spec/Pactus.tla @@ -0,0 +1,551 @@ +-------------------------------- 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. + NumNodes, + \* The total number of faulty nodes, denoted as `f` in the protocol. + f, + \* The total number of faulty nodes, denoted as `f` 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 + +\* ThreeFPlusOne is equal to `3f+1', where `f' is the number of faulty nodes. +ThreeFPlusOne == (3 * f) + 1 +\* TwoFPlusOne is equal to `2f+1', where `f' is the number of faulty nodes. +TwoFPlusOne == (2 * f) + 1 +\* OneFPlusOne is equal to `f+1', where `f' is the number of faulty nodes. +OneFPlusOne == (1 * f) + 1 + +\* FourTPlusOne is equal to `3f+1', where `f' is the number of faulty nodes. +FourTPlusOne == (4 * t) + 1 +\* TwoFPlusOne is equal to `2f+1', where `f' is the number of faulty nodes. +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. + /\ NumNodes >= ThreeFPlusOne + \* Ensure that `FaultyNodes` is a valid subset of node indices. + /\ FaultyNodes \subseteq 0..NumNodes-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 % NumNodes = index + +\* Helper function to check if a node is faulty or not. +IsFaulty(index) == index \in FaultyNodes + +\* HasPrepareAbsoluteQuorum checks whether the node with the given index +\* has received all the PREPARE votes in this round. +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 2f+1 the PREPARE votes in this round. +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 2f+1 the PRECOMMIT votes in this round. +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" + /\ IF states[index].round >= MaxRound + THEN + /\ HasPrepareQuorum(index) + /\ states' = [states EXCEPT ![index].name = "cp:pre-vote"] + /\ log' = log + ELSE + /\ 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..NumNodes-1 |-> [ + name |-> "new-height", + height |-> 0, + round |-> 0, + cp_round |-> 0]] + +Next == + \E index \in 0..NumNodes-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..NumNodes-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/spec/temporary.cfg b/fastconsensus/spec/temporary.cfg new file mode 100644 index 000000000..26fabe4a2 --- /dev/null +++ b/fastconsensus/spec/temporary.cfg @@ -0,0 +1,11 @@ +SPECIFICATION Spec +INVARIANT TypeOK +PROPERTY Success +CONSTANTS \* regular assignments + NumNodes = 6 + f = 1 + t = 1 + FaultyNodes = {5} + MaxHeight = 1 + MaxRound = 1 + MaxCPRound = 1 \ No newline at end of file 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..0c528a7c9 --- /dev/null +++ b/fastconsensus/voteset/binary_voteset.go @@ -0,0 +1,184 @@ +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() { + err = errors.Error(errors.ErrDuplicateVote) + } else { + // The vote is already added + return false, nil + } + } 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..09a617548 --- /dev/null +++ b/fastconsensus/voteset/block_voteset.go @@ -0,0 +1,139 @@ +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() { + err = errors.Error(errors.ErrDuplicateVote) + } else { + // The vote is already added + return false, nil + } + } 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..908c228b2 --- /dev/null +++ b/types/certificate/vote_certificate.go @@ -0,0 +1,77 @@ +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, + }, + } +} + +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..d5d1c9010 --- /dev/null +++ b/types/certificate/vote_certificate_test.go @@ -0,0 +1,32 @@ +package certificate_test + +// func TestVoteCertificateSignBytes(t *testing.T) { +// ts := testsuite.NewTestSuite(t) + +// h := ts.RandHash() +// height := ts.RandHeight() +// round := ts.RandRound() +// cpRound := ts.RandRound() +// cpValue := ts.RandInt8(3) + +// cert1 := certificate.NewBlockCertificate(height, round, true) +// cert2 := certificate.NewVoteCertificate(height, round) + +// sb2 := cert2.SignBytes(h) +// sb3 := cert3.SignBytes(h) +// sb4 := cert4.SignBytes(h) +// sb5 := cert5.SignBytes(h) + +// assert.NotEqual(t, sb2, sb3) +// assert.NotEqual(t, sb2, sb4) +// assert.NotEqual(t, sb3, sb4) +// assert.NotEqual(t, sb4, sb5) + +// // BlockCertificate (fast path) has same sign bytes as Prepare certificate +// assert.Equal(t, cert1.SignBytes(h), cert2.SignBytes(h)) + +// assert.Contains(t, string(sb2), "PREPARE") +// assert.Contains(t, string(sb3), "PRE-VOTE") +// assert.Contains(t, string(sb4), "MAIN-VOTE") +// assert.Contains(t, string(sb5), "DECIDED") +// } 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..370da2e82 100644 --- a/types/vote/cp_just.go +++ b/types/vote/cp_just.go @@ -46,9 +46,9 @@ type Just interface { func makeJust(t JustType) Just { switch t { case JustTypeInitZero: - return &JustInitZero{} + return &JustInitNo{} case JustTypeInitOne: - return &JustInitOne{} + return &JustInitYes{} case JustTypePreVoteSoft: return &JustPreVoteSoft{} case JustTypePreVoteHard: @@ -64,37 +64,37 @@ 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 } 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 { +func (j *JustInitNo) Type() JustType { return JustTypeInitZero } -func (*JustInitOne) Type() JustType { +func (j *JustInitYes) Type() JustType { return JustTypeInitOne } @@ -118,11 +118,11 @@ func (*JustDecided) Type() JustType { return JustTypeDecided } -func (j *JustInitZero) BasicCheck() error { - return j.QCert.BasicCheck() +func (j *JustInitNo) BasicCheck() error { + return nil } -func (*JustInitOne) BasicCheck() error { +func (j *JustInitYes) BasicCheck() error { return nil } diff --git a/types/vote/cp_vote.go b/types/vote/cp_vote.go index 7797c3a3a..ed754af05 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") diff --git a/types/vote/vote.go b/types/vote/vote.go index f03e03793..00aedc479 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 diff --git a/types/vote/vote_test.go b/types/vote/vote_test.go index 3bda1cdee..6b955869e 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" @@ -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") } 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..11f2faf51 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) { - return ts.makeTestBlock(height, proposer, util.Now()) +) (*block.Block, *certificate.BlockCertificate) { + return ts.generateTestBlock(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) { - return ts.makeTestBlock(height, ts.RandValAddress(), tme) +) (*block.Block, *certificate.BlockCertificate) { + return ts.generateTestBlock(height, ts.RandValAddress(), tme) } // GenerateTestBlock generates a block for testing purposes. -func (ts *TestSuite) GenerateTestBlock(height uint32) (*block.Block, *certificate.Certificate) { - return ts.makeTestBlock(height, ts.RandValAddress(), util.Now()) +func (ts *TestSuite) GenerateTestBlock(height uint32) (*block.Block, *certificate.BlockCertificate) { + return ts.generateTestBlock(height, ts.RandValAddress(), util.Now()) } -func (ts *TestSuite) makeTestBlock(height uint32, proposer crypto.Address, tme time.Time, -) (*block.Block, *certificate.Certificate) { +func (ts *TestSuite) generateTestBlock(height uint32, proposer crypto.Address, tme time.Time, +) (*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()) } }() From 2fc4a2148123c33817f1539d500f21e0554daa07 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Thu, 2 May 2024 00:20:59 +0800 Subject: [PATCH 02/11] refactor: refactor just check --- consensus/cp.go | 18 +- consensus/cp_mainvote.go | 4 +- consensus/cp_test.go | 24 +-- fastconsensus/consensus_test.go | 40 +++-- fastconsensus/cp.go | 286 ++++++++++++++++---------------- fastconsensus/cp_mainvote.go | 4 +- fastconsensus/cp_test.go | 104 +++++------- fastconsensus/errors.go | 8 +- types/vote/cp_just.go | 28 ++-- types/vote/cp_vote.go | 12 +- types/vote/vote.go | 2 +- util/errors/errors.go | 4 +- 12 files changed, 256 insertions(+), 278 deletions(-) diff --git a/consensus/cp.go b/consensus/cp.go index 81fa51d3b..2ab897da0 100644 --- a/consensus/cp.go +++ b/consensus/cp.go @@ -150,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 } @@ -164,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.CPValueNo) + 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.CPValueYes) + err := cp.checkJustPreVoteHard(j.JustYes, hash.UndefHash, cpRound, vote.CPValueYes) if err != nil { return err } @@ -195,7 +195,7 @@ func (cp *changeProposer) checkJustPreVote(v *vote.Vote) error { just := v.CPJust() if v.CPRound() == 0 { switch just.Type() { - case vote.JustTypeInitZero: + case vote.JustTypeInitNo: err := cp.checkCPValue(v, vote.CPValueNo) if err != nil { return err @@ -203,7 +203,7 @@ func (cp *changeProposer) checkJustPreVote(v *vote.Vote) error { return cp.checkJustInitZero(just, v.BlockHash()) - case vote.JustTypeInitOne: + case vote.JustTypeInitYes: err := cp.checkCPValue(v, vote.CPValueYes) if err != nil { return err diff --git a/consensus/cp_mainvote.go b/consensus/cp_mainvote.go index c5c42a8aa..cd03d9e0a 100644 --- a/consensus/cp_mainvote.go +++ b/consensus/cp_mainvote.go @@ -46,8 +46,8 @@ func (s *cpMainVoteState) decide() { 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) diff --git a/consensus/cp_test.go b/consensus/cp_test.go index 3b6e32e03..87a649b64 100644 --- a/consensus/cp_test.go +++ b/consensus/cp_test.go @@ -333,10 +333,10 @@ func TestInvalidJustMainVoteConflict(t *testing.T) { t.Run("invalid value: no", func(t *testing.T) { just := &vote.JustMainVoteConflict{ - Just0: &vote.JustInitNo{ + JustNo: &vote.JustInitNo{ QCert: td.GenerateTestPrepareCertificate(h), }, - Just1: &vote.JustInitYes{}, + JustYes: &vote.JustInitYes{}, } v := vote.NewCPMainVote(td.RandHash(), h, r, 0, vote.CPValueNo, just, td.consB.valKey.Address()) @@ -349,10 +349,10 @@ func TestInvalidJustMainVoteConflict(t *testing.T) { t.Run("invalid value: yes", func(t *testing.T) { just := &vote.JustMainVoteConflict{ - Just0: &vote.JustInitNo{ + JustNo: &vote.JustInitNo{ QCert: td.GenerateTestPrepareCertificate(h), }, - Just1: &vote.JustInitYes{}, + JustYes: &vote.JustInitYes{}, } v := vote.NewCPMainVote(td.RandHash(), h, r, 0, vote.CPValueYes, just, td.consB.valKey.Address()) @@ -365,10 +365,10 @@ func TestInvalidJustMainVoteConflict(t *testing.T) { t.Run("invalid value: unexpected justification (just0)", func(t *testing.T) { just := &vote.JustMainVoteConflict{ - Just0: &vote.JustPreVoteSoft{ + JustNo: &vote.JustPreVoteSoft{ QCert: td.GenerateTestPrepareCertificate(h), }, - Just1: &vote.JustInitYes{}, + JustYes: &vote.JustInitYes{}, } v := vote.NewCPMainVote(td.RandHash(), h, r, 0, vote.CPValueAbstain, just, td.consB.valKey.Address()) @@ -381,10 +381,10 @@ func TestInvalidJustMainVoteConflict(t *testing.T) { t.Run("invalid value: unexpected justification", func(t *testing.T) { just := &vote.JustMainVoteConflict{ - Just0: &vote.JustInitNo{ + JustNo: &vote.JustInitNo{ QCert: td.GenerateTestPrepareCertificate(h), }, - Just1: &vote.JustPreVoteSoft{ + JustYes: &vote.JustPreVoteSoft{ QCert: td.GenerateTestPrepareCertificate(h), }, } @@ -402,8 +402,8 @@ func TestInvalidJustMainVoteConflict(t *testing.T) { QCert: td.GenerateTestPrepareCertificate(h), } just := &vote.JustMainVoteConflict{ - Just0: just0, - Just1: &vote.JustInitYes{}, + JustNo: just0, + JustYes: &vote.JustInitYes{}, } v := vote.NewCPMainVote(td.RandHash(), h, r, 0, vote.CPValueAbstain, just, td.consB.valKey.Address()) @@ -419,8 +419,8 @@ func TestInvalidJustMainVoteConflict(t *testing.T) { QCert: td.GenerateTestPrepareCertificate(h), } just := &vote.JustMainVoteConflict{ - Just0: just0, - Just1: &vote.JustPreVoteSoft{ + JustNo: just0, + JustYes: &vote.JustPreVoteSoft{ QCert: td.GenerateTestPrepareCertificate(h), }, } diff --git a/fastconsensus/consensus_test.go b/fastconsensus/consensus_test.go index 2ac899cf4..90a420c45 100644 --- a/fastconsensus/consensus_test.go +++ b/fastconsensus/consensus_test.go @@ -284,24 +284,26 @@ 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, -) { +) *vote.Vote { v := vote.NewCPPreVote(blockHash, height, round, cpRound, cpVal, just, td.valKeys[valID].Address()) - td.addVote(cons, v, valID) + return 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, -) { +) *vote.Vote { v := vote.NewCPMainVote(blockHash, height, round, cpRound, cpVal, just, td.valKeys[valID].Address()) - td.addVote(cons, v, valID) + + return 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, -) { +) *vote.Vote { v := vote.NewCPDecidedVote(blockHash, height, round, cpRound, cpVal, just, td.valKeys[valID].Address()) - td.addVote(cons, v, valID) + + return td.addVote(cons, v, valID) } func (td *testData) addVote(cons *consensus, v *vote.Vote, valID int) *vote.Vote { @@ -615,26 +617,32 @@ func TestPickRandomVote(t *testing.T) { // ==== // 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.CPValueYes, + v1 := td.addPrepareVote(td.consP, td.RandHash(), 1, 0, tIndexX) + v2 := td.addPrepareVote(td.consP, td.RandHash(), 1, 0, tIndexY) + v3 := td.addCPPreVote(td.consP, hash.UndefHash, 1, 0, cpRound+1, vote.CPValueYes, &vote.JustPreVoteHard{QCert: certPreVote}, tIndexY) - td.addCPMainVote(td.consP, hash.UndefHash, 1, 0, cpRound, vote.CPValueYes, + v4 := td.addCPMainVote(td.consP, hash.UndefHash, 1, 0, cpRound, vote.CPValueYes, &vote.JustMainVoteNoConflict{QCert: certPreVote}, tIndexY) - td.addCPDecidedVote(td.consP, hash.UndefHash, 1, 0, cpRound, vote.CPValueYes, + v5 := td.addCPDecidedVote(td.consP, hash.UndefHash, 1, 0, cpRound, vote.CPValueYes, &vote.JustDecided{QCert: certMainVote}, tIndexY) - assert.NotNil(t, td.consP.PickRandomVote(0)) - // Round 1 td.enterNextRound(td.consP) - td.addPrepareVote(td.consP, td.RandHash(), 1, 1, tIndexY) + v6 := td.addPrepareVote(td.consP, td.RandHash(), 1, 1, tIndexY) + + assert.True(t, td.consP.HasVote(v1.Hash())) + assert.True(t, td.consP.HasVote(v2.Hash())) + assert.True(t, td.consP.HasVote(v3.Hash())) + assert.True(t, td.consP.HasVote(v4.Hash())) + assert.True(t, td.consP.HasVote(v5.Hash())) + assert.True(t, td.consP.HasVote(v6.Hash())) + assert.NotNil(t, td.consP.PickRandomVote(0)) rndVote0 := td.consP.PickRandomVote(0) - assert.NotEqual(t, rndVote0.Type(), vote.VoteTypePrepare, "Should not pick prepare votes") + assert.Equal(t, rndVote0, v5, "for past round should pick Decided votes only") rndVote1 := td.consP.PickRandomVote(1) - assert.Equal(t, rndVote1.Type(), vote.VoteTypePrepare) + assert.Equal(t, rndVote1, v6) } func TestSetProposalFromPreviousRound(t *testing.T) { diff --git a/fastconsensus/cp.go b/fastconsensus/cp.go index a66829099..6ba1a323a 100644 --- a/fastconsensus/cp.go +++ b/fastconsensus/cp.go @@ -23,45 +23,71 @@ func (cp *changeProposer) onTimeout(t *ticker) { } } -func (cp *changeProposer) cpCheckCPValue(vte *vote.Vote, allowedValues ...vote.CPValue) error { +func (cp *changeProposer) cpCheckCPValue(value vote.CPValue, allowedValues ...vote.CPValue) error { for _, v := range allowedValues { - if vte.CPValue() == v { + if value == v { return nil } } return invalidJustificationError{ - JustType: vte.CPJust().Type(), - Reason: fmt.Sprintf("invalid value: %v", vte.CPValue()), + Reason: fmt.Sprintf("invalid value: %v", value), } } -func (cp *changeProposer) cpCheckJustInitZero(just vote.Just, blockHash hash.Hash) error { +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{ - JustType: just.Type(), - Reason: "invalid just data", + Reason: "invalid just data", } } - err := j.QCert.ValidatePrepare(cp.validators, blockHash) - if err != nil { + if cpRound != 0 { return invalidJustificationError{ - JustType: j.Type(), - Reason: err.Error(), + 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) cpCheckJustInitOne(just vote.Just) error { +func (cp *changeProposer) cpCheckJustInitYes(just vote.Just, + blockHash hash.Hash, cpRound int16, cpValue vote.CPValue, +) error { _, ok := just.(*vote.JustInitYes) if !ok { return invalidJustificationError{ - JustType: just.Type(), - Reason: "invalid just data", + 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), } } @@ -74,17 +100,26 @@ func (cp *changeProposer) cpCheckJustPreVoteHard(just vote.Just, j, ok := just.(*vote.JustPreVoteHard) if !ok { return invalidJustificationError{ - JustType: just.Type(), - Reason: "invalid just data", + Reason: "invalid just data", + } + } + + if cpRound == 0 { + return invalidJustificationError{ + Reason: "invalid round: 0", } } - err := j.QCert.ValidateCPPreVote(cp.validators, + 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{ - JustType: just.Type(), - Reason: err.Error(), + Reason: err.Error(), } } @@ -92,22 +127,31 @@ func (cp *changeProposer) cpCheckJustPreVoteHard(just vote.Just, } func (cp *changeProposer) cpCheckJustPreVoteSoft(just vote.Just, - blockHash hash.Hash, cpRound int16, + blockHash hash.Hash, cpRound int16, cpValue vote.CPValue, ) error { j, ok := just.(*vote.JustPreVoteSoft) if !ok { return invalidJustificationError{ - JustType: just.Type(), - Reason: "invalid just data", + Reason: "invalid just data", + } + } + + if cpRound == 0 { + return invalidJustificationError{ + Reason: "invalid round: 0", } } - err := j.QCert.ValidateCPMainVote(cp.validators, + 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{ - JustType: just.Type(), - Reason: err.Error(), + Reason: err.Error(), } } @@ -120,191 +164,141 @@ func (cp *changeProposer) cpCheckJustMainVoteNoConflict(just vote.Just, j, ok := just.(*vote.JustMainVoteNoConflict) if !ok { return invalidJustificationError{ - JustType: just.Type(), - Reason: "invalid just data", + Reason: "invalid just data", } } + err := cp.cpCheckCPValue(cpValue, vote.CPValueNo, vote.CPValueYes) + if err != nil { + return err + } - err := j.QCert.ValidateCPPreVote(cp.validators, + err = j.QCert.ValidateCPPreVote(cp.validators, blockHash, cpRound, byte(cpValue)) if err != nil { return invalidJustificationError{ - JustType: j.Type(), - Reason: err.Error(), + Reason: err.Error(), } } return nil } -//nolint:exhaustive // refactor me; check just by just_type, not vote_type func (cp *changeProposer) cpCheckJustMainVoteConflict(just vote.Just, - blockHash hash.Hash, cpRound int16, + blockHash hash.Hash, cpRound int16, cpValue vote.CPValue, ) error { j, ok := just.(*vote.JustMainVoteConflict) if !ok { return invalidJustificationError{ - JustType: just.Type(), - Reason: "invalid just data", + Reason: "invalid just data", } } - if cpRound == 0 { - err := cp.cpCheckJustInitZero(j.Just0, blockHash) - if err != nil { - return err - } + err := cp.cpCheckCPValue(cpValue, vote.CPValueAbstain) + if err != nil { + return err + } - err = cp.cpCheckJustInitOne(j.Just1) + switch j.JustNo.Type() { + case vote.JustTypeInitNo: + err := cp.cpCheckJustInitNo(j.JustNo, blockHash, cpRound, vote.CPValueNo) if err != nil { return err } - - return nil - } - - // Just0 can be for Zero or Abstain values. - switch j.Just0.Type() { - case vote.JustTypePreVoteSoft: - err := cp.cpCheckJustPreVoteSoft(j.Just0, blockHash, cpRound) + case vote.JustTypePreVoteHard: + err := cp.cpCheckJustPreVoteHard(j.JustNo, blockHash, cpRound, vote.CPValueNo) if err != nil { return err } - case vote.JustTypePreVoteHard: - err := cp.cpCheckJustPreVoteHard(j.Just0, blockHash, cpRound, vote.CPValueNo) + case vote.JustTypePreVoteSoft: + err := cp.cpCheckJustPreVoteSoft(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()), - } - } - - err := cp.cpCheckJustPreVoteHard(j.Just1, hash.UndefHash, cpRound, vote.CPValueYes) - if err != nil { - return err - } - - return nil -} - -//nolint:exhaustive // refactor me; check just by just_type, not vote_type -func (cp *changeProposer) cpCheckJustPreVote(v *vote.Vote) error { - just := v.CPJust() - if v.CPRound() == 0 { - switch just.Type() { - case vote.JustTypeInitZero: - err := cp.cpCheckCPValue(v, vote.CPValueNo) - if err != nil { - return err - } - - return cp.cpCheckJustInitZero(just, v.BlockHash()) - - case vote.JustTypeInitOne: - err := cp.cpCheckCPValue(v, vote.CPValueYes) - if err != nil { - return err - } - - return cp.cpCheckJustInitOne(just) - default: - return invalidJustificationError{ - JustType: just.Type(), - Reason: "invalid pre-vote justification", - } - } - } else { - switch just.Type() { - case vote.JustTypePreVoteSoft: - err := cp.cpCheckCPValue(v, vote.CPValueNo, vote.CPValueYes) - if err != nil { - return err - } - - return cp.cpCheckJustPreVoteSoft(just, v.BlockHash(), v.CPRound()) - - case vote.JustTypePreVoteHard: - err := cp.cpCheckCPValue(v, vote.CPValueNo, vote.CPValueYes) - if err != nil { - return err - } - - return cp.cpCheckJustPreVoteHard(just, v.BlockHash(), v.CPRound(), v.CPValue()) - - default: - return invalidJustificationError{ - JustType: just.Type(), - Reason: "invalid pre-vote justification", - } + Reason: fmt.Sprintf("unexpected justification: %s", j.JustNo.Type()), } } -} -//nolint:exhaustive // refactor me; check just by just_type, not vote_type -func (cp *changeProposer) cpCheckJustMainVote(v *vote.Vote) error { - just := v.CPJust() - switch just.Type() { - case vote.JustTypeMainVoteNoConflict: - err := cp.cpCheckCPValue(v, vote.CPValueNo, vote.CPValueYes) + switch j.JustYes.Type() { + case vote.JustTypeInitYes: + err := cp.cpCheckJustInitYes(j.JustYes, hash.UndefHash, cpRound, vote.CPValueYes) if err != nil { return err } - return cp.cpCheckJustMainVoteNoConflict(just, v.BlockHash(), v.CPRound(), v.CPValue()) - - case vote.JustTypeMainVoteConflict: - err := cp.cpCheckCPValue(v, vote.CPValueAbstain) + case vote.JustTypePreVoteHard: + err := cp.cpCheckJustPreVoteHard(j.JustYes, hash.UndefHash, cpRound, vote.CPValueYes) if err != nil { return err } - return cp.cpCheckJustMainVoteConflict(just, v.BlockHash(), v.CPRound()) - default: return invalidJustificationError{ - JustType: just.Type(), - Reason: "invalid main-vote justification", + Reason: fmt.Sprintf("unexpected justification: %s", j.JustNo.Type()), } } + + return nil } -func (cp *changeProposer) cpCheckJustDecide(v *vote.Vote) error { - err := cp.cpCheckCPValue(v, vote.CPValueNo, vote.CPValueYes) - if err != nil { - return err - } - j, ok := v.CPJust().(*vote.JustDecided) +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{ - JustType: j.Type(), - Reason: "invalid just data", + Reason: "invalid just data", } } + err := cp.cpCheckCPValue(cpValue, vote.CPValueNo, vote.CPValueYes) + if err != nil { + return err + } + err = j.QCert.ValidateCPMainVote(cp.validators, - v.BlockHash(), int16(v.CPValue()), byte(v.CPRound())) + blockHash, int16(cpValue), byte(cpRound)) if err != nil { return invalidJustificationError{ - JustType: j.Type(), - Reason: err.Error(), + Reason: err.Error(), } } return nil } -//nolint:exhaustive // refactor me; check just by just_type, not vote_type func (cp *changeProposer) cpCheckJust(v *vote.Vote) error { - switch v.Type() { - case vote.VoteTypeCPPreVote: - return cp.cpCheckJustPreVote(v) - case vote.VoteTypeCPMainVote: - return cp.cpCheckJustMainVote(v) - case vote.VoteTypeCPDecided: - return cp.cpCheckJustDecide(v) + 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") } diff --git a/fastconsensus/cp_mainvote.go b/fastconsensus/cp_mainvote.go index b2b1a1081..58513753f 100644 --- a/fastconsensus/cp_mainvote.go +++ b/fastconsensus/cp_mainvote.go @@ -49,8 +49,8 @@ func (s *cpMainVoteState) decide() { 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) diff --git a/fastconsensus/cp_test.go b/fastconsensus/cp_test.go index f80eb569f..84378f835 100644 --- a/fastconsensus/cp_test.go +++ b/fastconsensus/cp_test.go @@ -131,7 +131,7 @@ func TestCrashOnTestnet(t *testing.T) { assert.Equal(t, vote.CPValueNo, mainVote.CPValue()) } -func TestInvalidJustInitOne(t *testing.T) { +func TestInvalidJustInitYes(t *testing.T) { td := setup(t) td.enterNewHeight(td.consX) @@ -144,34 +144,31 @@ func TestInvalidJustInitOne(t *testing.T) { err := td.consX.changeProposer.cpCheckJust(v) assert.ErrorIs(t, err, invalidJustificationError{ - JustType: just.Type(), - Reason: "invalid value: no", + Reason: "invalid value: no", }) }) - t.Run("invalid block hash", func(t *testing.T) { + 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{ - JustType: just.Type(), - Reason: "invalid pre-vote justification", + Reason: "invalid round: 1", }) }) - t.Run("with main-vote justification", func(t *testing.T) { - invJust := &vote.JustMainVoteNoConflict{} - v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueYes, invJust, td.consB.valKey.Address()) + 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{ - JustType: invJust.Type(), - Reason: "invalid pre-vote justification", + Reason: "invalid block hash: " + blockHash.String(), }) }) } -func TestInvalidJustInitZero(t *testing.T) { +func TestInvalidJustInitNo(t *testing.T) { td := setup(t) td.enterNewHeight(td.consX) @@ -186,18 +183,16 @@ func TestInvalidJustInitZero(t *testing.T) { err := td.consX.changeProposer.cpCheckJust(v) assert.ErrorIs(t, err, invalidJustificationError{ - JustType: just.Type(), - Reason: "invalid value: yes", + Reason: "invalid value: yes", }) }) - t.Run("cp-round should be zero", func(t *testing.T) { + 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{ - JustType: just.Type(), - Reason: "invalid pre-vote justification", + Reason: "invalid round: 1", }) }) @@ -224,18 +219,16 @@ func TestInvalidJustPreVoteHard(t *testing.T) { err := td.consX.changeProposer.cpCheckJust(v) assert.ErrorIs(t, err, invalidJustificationError{ - JustType: just.Type(), - Reason: "invalid value: abstain", + Reason: "invalid value: abstain", }) }) - t.Run("cp-round should not be zero", func(t *testing.T) { + 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{ - JustType: just.Type(), - Reason: "invalid pre-vote justification", + Reason: "invalid round: 0", }) }) @@ -244,8 +237,7 @@ func TestInvalidJustPreVoteHard(t *testing.T) { err := td.consX.changeProposer.cpCheckJust(v) assert.ErrorIs(t, err, invalidJustificationError{ - JustType: just.Type(), - Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just.QCert.Committers()), + Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just.QCert.Committers()), }) }) } @@ -265,18 +257,16 @@ func TestInvalidJustPreVoteSoft(t *testing.T) { err := td.consX.changeProposer.cpCheckJust(v) assert.ErrorIs(t, err, invalidJustificationError{ - JustType: just.Type(), - Reason: "invalid value: abstain", + Reason: "invalid value: abstain", }) }) - t.Run("cp-round should not be zero", func(t *testing.T) { + 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{ - JustType: just.Type(), - Reason: "invalid pre-vote justification", + Reason: "invalid round: 0", }) }) @@ -285,8 +275,7 @@ func TestInvalidJustPreVoteSoft(t *testing.T) { err := td.consX.changeProposer.cpCheckJust(v) assert.ErrorIs(t, err, invalidJustificationError{ - JustType: just.Type(), - Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just.QCert.Committers()), + Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just.QCert.Committers()), }) }) } @@ -306,8 +295,7 @@ func TestInvalidJustMainVoteNoConflict(t *testing.T) { err := td.consX.changeProposer.cpCheckJust(v) assert.ErrorIs(t, err, invalidJustificationError{ - JustType: just.Type(), - Reason: "invalid value: abstain", + Reason: "invalid value: abstain", }) }) @@ -316,8 +304,7 @@ func TestInvalidJustMainVoteNoConflict(t *testing.T) { err := td.consX.changeProposer.cpCheckJust(v) assert.ErrorIs(t, err, invalidJustificationError{ - JustType: just.Type(), - Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just.QCert.Committers()), + Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just.QCert.Committers()), }) }) } @@ -331,58 +318,55 @@ func TestInvalidJustMainVoteConflict(t *testing.T) { t.Run("invalid value: no", func(t *testing.T) { just := &vote.JustMainVoteConflict{ - Just0: &vote.JustInitNo{ + JustNo: &vote.JustInitNo{ QCert: td.GenerateTestPrepareCertificate(h), }, - Just1: &vote.JustInitYes{}, + 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{ - JustType: just.Type(), - Reason: "invalid value: no", + Reason: "invalid value: no", }) }) t.Run("invalid value: yes", func(t *testing.T) { just := &vote.JustMainVoteConflict{ - Just0: &vote.JustInitNo{ + JustNo: &vote.JustInitNo{ QCert: td.GenerateTestPrepareCertificate(h), }, - Just1: &vote.JustInitYes{}, + 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{ - JustType: just.Type(), - Reason: "invalid value: yes", + Reason: "invalid value: yes", }) }) - t.Run("invalid value: unexpected justification (just0)", func(t *testing.T) { + t.Run("invalid value: unexpected justification (justNo)", func(t *testing.T) { just := &vote.JustMainVoteConflict{ - Just0: &vote.JustPreVoteSoft{ + JustNo: &vote.JustPreVoteHard{ QCert: td.GenerateTestPrepareCertificate(h), }, - Just1: &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{ - JustType: vote.JustTypePreVoteSoft, - Reason: "invalid just data", + Reason: "invalid round: 0", }) }) t.Run("invalid value: unexpected justification", func(t *testing.T) { just := &vote.JustMainVoteConflict{ - Just0: &vote.JustInitNo{ + JustNo: &vote.JustInitNo{ QCert: td.GenerateTestPrepareCertificate(h), }, - Just1: &vote.JustPreVoteSoft{ + JustYes: &vote.JustPreVoteSoft{ QCert: td.GenerateTestPrepareCertificate(h), }, } @@ -390,8 +374,7 @@ func TestInvalidJustMainVoteConflict(t *testing.T) { err := td.consX.changeProposer.cpCheckJust(v) assert.ErrorIs(t, err, invalidJustificationError{ - JustType: just.Type(), - Reason: "unexpected justification: JustInitZero", + Reason: "invalid round: 1", }) }) @@ -400,8 +383,8 @@ func TestInvalidJustMainVoteConflict(t *testing.T) { QCert: td.GenerateTestPrepareCertificate(h), } just := &vote.JustMainVoteConflict{ - Just0: just0, - Just1: &vote.JustInitYes{}, + JustNo: just0, + JustYes: &vote.JustInitYes{}, } v := vote.NewCPMainVote(td.RandHash(), h, r, 0, vote.CPValueAbstain, just, td.consB.valKey.Address()) @@ -414,8 +397,8 @@ func TestInvalidJustMainVoteConflict(t *testing.T) { QCert: td.GenerateTestPrepareCertificate(h), } just := &vote.JustMainVoteConflict{ - Just0: just0, - Just1: &vote.JustPreVoteSoft{ + JustNo: just0, + JustYes: &vote.JustPreVoteSoft{ QCert: td.GenerateTestPrepareCertificate(h), }, } @@ -423,8 +406,7 @@ func TestInvalidJustMainVoteConflict(t *testing.T) { err := td.consX.changeProposer.cpCheckJust(v) assert.ErrorIs(t, err, invalidJustificationError{ - JustType: just0.Type(), - Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just0.QCert.Committers()), + Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just0.QCert.Committers()), }) }) } @@ -444,8 +426,7 @@ func TestInvalidJustDecided(t *testing.T) { err := td.consX.changeProposer.cpCheckJust(v) assert.ErrorIs(t, err, invalidJustificationError{ - JustType: just.Type(), - Reason: "invalid value: abstain", + Reason: "invalid value: abstain", }) }) @@ -454,8 +435,7 @@ func TestInvalidJustDecided(t *testing.T) { err := td.consX.changeProposer.cpCheckJust(v) assert.ErrorIs(t, err, invalidJustificationError{ - JustType: just.Type(), - Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just.QCert.Committers()), + Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just.QCert.Committers()), }) }) } diff --git a/fastconsensus/errors.go b/fastconsensus/errors.go index c73bfa465..023c20558 100644 --- a/fastconsensus/errors.go +++ b/fastconsensus/errors.go @@ -2,20 +2,16 @@ package fastconsensus import ( "fmt" - - "github.com/pactus-project/pactus/types/vote" ) // invalidJustificationError is returned when the justification for a change-proposer // vote is invalid. type invalidJustificationError struct { - JustType vote.JustType - Reason string + Reason string } func (e invalidJustificationError) Error() string { - return fmt.Sprintf("invalid justification: %s, reason: %s", - e.JustType.String(), e.Reason) + return fmt.Sprintf("invalid justification: %s", e.Reason) } // ConfigError is returned when the config is not valid with a descriptive Reason message. diff --git a/types/vote/cp_just.go b/types/vote/cp_just.go index 370da2e82..24877af9b 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,9 +45,9 @@ type Just interface { func makeJust(t JustType) Just { switch t { - case JustTypeInitZero: + case JustTypeInitNo: return &JustInitNo{} - case JustTypeInitOne: + case JustTypeInitYes: return &JustInitYes{} case JustTypePreVoteSoft: return &JustPreVoteSoft{} @@ -79,8 +79,8 @@ type JustPreVoteHard struct { QCert *certificate.VoteCertificate `cbor:"1,keyasint"` } type JustMainVoteConflict struct { - Just0 Just - Just1 Just + JustNo Just + JustYes Just } type JustMainVoteNoConflict struct { QCert *certificate.VoteCertificate `cbor:"1,keyasint"` @@ -91,11 +91,11 @@ type JustDecided struct { } func (j *JustInitNo) Type() JustType { - return JustTypeInitZero + return JustTypeInitNo } func (j *JustInitYes) Type() JustType { - return JustTypeInitOne + return JustTypeInitYes } func (*JustPreVoteSoft) Type() JustType { @@ -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 ed754af05..f5a1afac5 100644 --- a/types/vote/cp_vote.go +++ b/types/vote/cp_vote.go @@ -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 00aedc479..e01c4ec05 100644 --- a/types/vote/vote.go +++ b/types/vote/vote.go @@ -251,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/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 { From 23aeb8d13e14019eea359ba35469a47d81ba777b Mon Sep 17 00:00:00 2001 From: Mostafa Date: Thu, 2 May 2024 00:49:25 +0800 Subject: [PATCH 03/11] refactor: updating just tests --- consensus/cp_test.go | 2 +- fastconsensus/cp_test.go | 20 ++------------------ types/vote/vote_test.go | 4 ++-- 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/consensus/cp_test.go b/consensus/cp_test.go index 87a649b64..4363b1348 100644 --- a/consensus/cp_test.go +++ b/consensus/cp_test.go @@ -393,7 +393,7 @@ 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", }) }) diff --git a/fastconsensus/cp_test.go b/fastconsensus/cp_test.go index 84378f835..2aeb3274d 100644 --- a/fastconsensus/cp_test.go +++ b/fastconsensus/cp_test.go @@ -348,16 +348,14 @@ func TestInvalidJustMainVoteConflict(t *testing.T) { t.Run("invalid value: unexpected justification (justNo)", func(t *testing.T) { just := &vote.JustMainVoteConflict{ - JustNo: &vote.JustPreVoteHard{ - QCert: td.GenerateTestPrepareCertificate(h), - }, + 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: "invalid round: 0", + Reason: "unexpected justification: JustInitYes", }) }) @@ -378,20 +376,6 @@ func TestInvalidJustMainVoteConflict(t *testing.T) { }) }) - t.Run("invalid certificate", func(t *testing.T) { - just0 := &vote.JustInitNo{ - QCert: td.GenerateTestPrepareCertificate(h), - } - just := &vote.JustMainVoteConflict{ - JustNo: just0, - 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.Error(t, err) - }) - t.Run("invalid certificate", func(t *testing.T) { just0 := &vote.JustPreVoteSoft{ QCert: td.GenerateTestPrepareCertificate(h), diff --git a/types/vote/vote_test.go b/types/vote/vote_test.go index 6b955869e..a099897fd 100644 --- a/types/vote/vote_test.go +++ b/types/vote/vote_test.go @@ -60,7 +60,7 @@ func TestVoteMarshaling(t *testing.T) { "32000000010004010203040094D25422904AC1D130AC981374AA4424F988" + // Certificate Data "61E99131078EFEFD62FC52CF072B0C08BB04E4E6496BA48DE4F3D3309AAB" + "07f6", // Signature -> Null - "JustInitZero", + "JustInitNo", "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB3200000001005052452d564f5445000000", }, { @@ -78,7 +78,7 @@ func TestVoteMarshaling(t *testing.T) { "0441" + // Just: JustTypeInitOne "A0" + // Empty Array "07f6", // Signature -> Null - "JustInitOne", + "JustInitYes", "00000000000000000000000000000000000000000000000000000000000000003200000001005052452d564f5445000001", }, { From 7795b31b6948ac2e3491eb43a54ae55e060953b5 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Thu, 2 May 2024 14:05:24 +0800 Subject: [PATCH 04/11] chore: update spec --- fastconsensus/spec/Pactus.pdf | Bin 187124 -> 189211 bytes fastconsensus/spec/Pactus.tla | 12 +++--------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/fastconsensus/spec/Pactus.pdf b/fastconsensus/spec/Pactus.pdf index ecab6731c988cb000fc17b9eb929b769b073ffd3..6d9be057ff0d64ad8ea9da77742d1e87c46bca4b 100644 GIT binary patch delta 76618 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

    }b9$pag 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 delta 74681 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ᯔXm&P@NJkz~F1bfq=SA>bjX<=Pg+ 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 MaxRound - THEN - /\ HasPrepareQuorum(index) - /\ states' = [states EXCEPT ![index].name = "cp:pre-vote"] - /\ log' = log - ELSE - /\ states' = [states EXCEPT ![index].name = "cp:pre-vote"] - /\ log' = log - + /\ states[index].round < MaxRound + /\ states' = [states EXCEPT ![index].name = "cp:pre-vote"] + /\ log' = log CPPreVote(index) == From 51e3f64a6bfbbaeb1bbb88d63692458400e8ee79 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Sat, 4 May 2024 01:35:07 +0800 Subject: [PATCH 05/11] test: update consensus tests --- consensus/consensus_test.go | 6 +- consensus/cp.go | 2 +- fastconsensus/consensus_test.go | 144 +++++++++++++++++++++----------- fastconsensus/cp.go | 2 +- fastconsensus/precommit_test.go | 72 ++++++++-------- types/vote/vote_test.go | 10 +-- 6 files changed, 140 insertions(+), 96 deletions(-) diff --git a/consensus/consensus_test.go b/consensus/consensus_test.go index e471d1e8a..c884d04e5 100644 --- a/consensus/consensus_test.go +++ b/consensus/consensus_test.go @@ -549,7 +549,7 @@ 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 preVoteCommitters := []int32{} @@ -586,8 +586,8 @@ func TestPickRandomVote(t *testing.T) { // 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.CPValueYes, - &vote.JustPreVoteHard{QCert: certPreVote}, tIndexY) + td.addCPPreVote(td.consP, hash.UndefHash, 1, 0, cpRound, vote.CPValueYes, + &vote.JustInitYes{}, tIndexY) td.addCPMainVote(td.consP, hash.UndefHash, 1, 0, cpRound, vote.CPValueYes, &vote.JustMainVoteNoConflict{QCert: certPreVote}, tIndexY) td.addCPDecidedVote(td.consP, hash.UndefHash, 1, 0, cpRound, vote.CPValueYes, diff --git a/consensus/cp.go b/consensus/cp.go index 2ab897da0..436506503 100644 --- a/consensus/cp.go +++ b/consensus/cp.go @@ -285,7 +285,7 @@ func (cp *changeProposer) checkJustDecide(v *vote.Vote) error { } err = j.QCert.ValidateCPMainVote(cp.validators, - v.BlockHash(), int16(v.CPValue()), byte(v.CPRound())) + v.BlockHash(), v.CPRound(), byte(v.CPValue())) if err != nil { return invalidJustificationError{ JustType: j.Type(), diff --git a/fastconsensus/consensus_test.go b/fastconsensus/consensus_test.go index 90a420c45..9c377d17d 100644 --- a/fastconsensus/consensus_test.go +++ b/fastconsensus/consensus_test.go @@ -415,6 +415,84 @@ func (td *testData) makeProposal(t *testing.T, height uint32, round int16) *prop 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 +// 2. JustMainVoteNoConflict +// 3. JustDecided +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 { + mainVoteJust := &vote.JustMainVoteNoConflict{ + QCert: certPreVote, + } + 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) @@ -582,66 +660,34 @@ func TestPickRandomVote(t *testing.T) { td.enterNewHeight(td.consP) assert.Nil(t, td.consP.PickRandomVote(0)) - cpRound := int16(1) - - // === make valid certificate - 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() - 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) - - mainVoteCommitters := []int32{} - mainVoteSigs := []*bls.Signature{} - for i, val := range td.consP.validators { - mainVoteJust := &vote.JustMainVoteNoConflict{ - QCert: certPreVote, - } - mainVote := vote.NewCPMainVote(hash.UndefHash, 1, 0, cpRound, vote.CPValueYes, mainVoteJust, val.Address()) - sbMainVote := mainVote.SignBytes() + h := uint32(1) + r := int16(0) - mainVoteCommitters = append(mainVoteCommitters, val.Number()) - mainVoteSigs = append(mainVoteSigs, td.valKeys[i].Sign(sbMainVote)) - } - mainVoteAggSig := bls.SignatureAggregate(mainVoteSigs...) - certMainVote := certificate.NewVoteCertificate(1, 0) - certMainVote.SetSignature(mainVoteCommitters, []int32{}, mainVoteAggSig) - // ==== + preVoteJust, mainVoteJust, decidedJust := td.makeChangeProposerJusts(t, hash.UndefHash, h, r) // round 0 - v1 := td.addPrepareVote(td.consP, td.RandHash(), 1, 0, tIndexX) - v2 := td.addPrepareVote(td.consP, td.RandHash(), 1, 0, tIndexY) - v3 := td.addCPPreVote(td.consP, hash.UndefHash, 1, 0, cpRound+1, vote.CPValueYes, - &vote.JustPreVoteHard{QCert: certPreVote}, tIndexY) - v4 := td.addCPMainVote(td.consP, hash.UndefHash, 1, 0, cpRound, vote.CPValueYes, - &vote.JustMainVoteNoConflict{QCert: certPreVote}, tIndexY) - v5 := td.addCPDecidedVote(td.consP, hash.UndefHash, 1, 0, cpRound, vote.CPValueYes, - &vote.JustDecided{QCert: certMainVote}, tIndexY) + 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, 0, vote.CPValueYes, preVoteJust, tIndexY) + v4 := td.addCPMainVote(td.consP, hash.UndefHash, h, r, 0, vote.CPValueYes, mainVoteJust, tIndexY) + v5 := td.addCPDecidedVote(td.consP, hash.UndefHash, h, r, 0, vote.CPValueYes, decidedJust, tIndexY) // Round 1 td.enterNextRound(td.consP) - v6 := td.addPrepareVote(td.consP, td.RandHash(), 1, 1, tIndexY) + v6 := td.addPrepareVote(td.consP, td.RandHash(), h, r+1, tIndexY) - assert.True(t, td.consP.HasVote(v1.Hash())) - assert.True(t, td.consP.HasVote(v2.Hash())) - assert.True(t, td.consP.HasVote(v3.Hash())) - assert.True(t, td.consP.HasVote(v4.Hash())) - assert.True(t, td.consP.HasVote(v5.Hash())) - assert.True(t, td.consP.HasVote(v6.Hash())) - assert.NotNil(t, td.consP.PickRandomVote(0)) + 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(0) + rndVote0 := td.consP.PickRandomVote(r) assert.Equal(t, rndVote0, v5, "for past round should pick Decided votes only") - rndVote1 := td.consP.PickRandomVote(1) + rndVote1 := td.consP.PickRandomVote(r + 1) assert.Equal(t, rndVote1, v6) } diff --git a/fastconsensus/cp.go b/fastconsensus/cp.go index 6ba1a323a..59340bbe6 100644 --- a/fastconsensus/cp.go +++ b/fastconsensus/cp.go @@ -259,7 +259,7 @@ func (cp *changeProposer) cpCheckJustDecide(just vote.Just, } err = j.QCert.ValidateCPMainVote(cp.validators, - blockHash, int16(cpValue), byte(cpRound)) + blockHash, cpRound, byte(cpValue)) if err != nil { return invalidJustificationError{ Reason: err.Error(), diff --git a/fastconsensus/precommit_test.go b/fastconsensus/precommit_test.go index faf4199de..2604e4000 100644 --- a/fastconsensus/precommit_test.go +++ b/fastconsensus/precommit_test.go @@ -1,39 +1,37 @@ package fastconsensus -// 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() - -// cert := certificate.NewVoteCertificate(h, r) - -// signBytes := cert.SignBytes(propBlockHash) -// sigX := td.consX.valKey.Sign(signBytes) -// sigY := td.consY.valKey.Sign(signBytes) -// sigM := td.consM.valKey.Sign(signBytes) -// sig := bls.SignatureAggregate(sigX, sigY, sigM) -// cert.SetSignature([]int32{0, 1, 2, 3, 4, 5}, []int32{2, 3, 5}, sig) -// just := &vote.JustDecided{ -// QCert: cert, -// } -// decideVote := vote.NewCPDecidedVote(propBlockHash, h, r, 0, vote.CPValueNo, just, 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) -// } +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/types/vote/vote_test.go b/types/vote/vote_test.go index a099897fd..36a3122dc 100644 --- a/types/vote/vote_test.go +++ b/types/vote/vote_test.go @@ -54,7 +54,7 @@ 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 @@ -75,7 +75,7 @@ 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 "JustInitYes", @@ -137,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 @@ -505,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 From 3ea99796e105d8eb56a9e0d60126e68263da73b8 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Wed, 8 May 2024 22:51:49 +0800 Subject: [PATCH 06/11] test: fix broken tests --- consensus/consensus_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/consensus/consensus_test.go b/consensus/consensus_test.go index c884d04e5..daddce443 100644 --- a/consensus/consensus_test.go +++ b/consensus/consensus_test.go @@ -734,10 +734,10 @@ func TestCases(t *testing.T) { round int16 description string }{ - {1697898884837384019, 1, "1/3+ cp:PRE-VOTE in prepare step"}, + {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, 2, "Conflicting votes, cp-round=1"}, + {1697900665869342730, 1, "Conflicting votes, cp-round=1"}, {1697887970998950590, 1, "consP & consB: Change Proposer, consX & consY: Commit (2 block announces)"}, } From 38cd7507945a7ecaaa683993757388bc5432deea Mon Sep 17 00:00:00 2001 From: Mostafa Date: Thu, 9 May 2024 00:39:25 +0800 Subject: [PATCH 07/11] test: fix linting issues --- consensus/consensus_test.go | 18 +-- consensus/cp_test.go | 22 ++-- consensus/precommit_test.go | 4 +- consensus/prepare_test.go | 4 +- fastconsensus/consensus_test.go | 204 ++++++++++++++++---------------- fastconsensus/cp.go | 11 +- fastconsensus/cp_prevote.go | 1 + fastconsensus/cp_test.go | 22 ++-- fastconsensus/prepare_test.go | 4 +- 9 files changed, 147 insertions(+), 143 deletions(-) diff --git a/consensus/consensus_test.go b/consensus/consensus_test.go index daddce443..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) } @@ -586,11 +586,11 @@ func TestPickRandomVote(t *testing.T) { // 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, vote.CPValueYes, + td.addCPPreVote(td.consP, hash.UndefHash, 1, 0, vote.CPValueYes, &vote.JustInitYes{}, tIndexY) - td.addCPMainVote(td.consP, hash.UndefHash, 1, 0, cpRound, vote.CPValueYes, + 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.CPValueYes, + td.addCPDecidedVote(td.consP, hash.UndefHash, 1, 0, vote.CPValueYes, &vote.JustDecided{QCert: certMainVote}, tIndexY) assert.NotNil(t, td.consP.PickRandomVote(0)) diff --git a/consensus/cp_test.go b/consensus/cp_test.go index 4363b1348..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.CPValueYes, preVote0.CPJust(), tIndexX) - td.addCPPreVote(td.consP, hash.UndefHash, h, r, 0, vote.CPValueYes, 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.CPValueYes, mainVote0.CPJust(), tIndexX) - td.addCPMainVote(td.consP, hash.UndefHash, h, r, 0, vote.CPValueYes, 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.CPValueNo, preVote0.CPJust(), tIndexX) - td.addCPPreVote(td.consP, p.Block().Hash(), h, r, 0, vote.CPValueNo, 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.CPValueNo, mainVote0.CPJust(), tIndexX) - td.addCPMainVote(td.consP, p.Block().Hash(), h, r, 0, vote.CPValueNo, 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) @@ -117,9 +117,9 @@ func TestCrashOnTestnet(t *testing.T) { qCert := td.consP.makeVoteCertificate(votes) just0 := &vote.JustInitNo{QCert: qCert} - td.addCPPreVote(td.consP, blockHash, h, r, 0, vote.CPValueNo, just0, tIndexX) - td.addCPPreVote(td.consP, blockHash, h, r, 0, vote.CPValueNo, just0, tIndexY) - td.addCPPreVote(td.consP, blockHash, h, r, 0, vote.CPValueNo, just0, tIndexB) + 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) diff --git a/consensus/precommit_test.go b/consensus/precommit_test.go index a5c2977e0..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.CPValueYes, &vote.JustInitYes{}, tIndexX) - td.addCPPreVote(td.consP, hash.UndefHash, h, r, 0, vote.CPValueYes, &vote.JustInitYes{}, 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 5422b919e..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.CPValueYes, &vote.JustInitYes{}, tIndexX) - td.addCPPreVote(td.consP, hash.UndefHash, 2, 0, 0, vote.CPValueYes, &vote.JustInitYes{}, 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/fastconsensus/consensus_test.go b/fastconsensus/consensus_test.go index 9c377d17d..9432d7d96 100644 --- a/fastconsensus/consensus_test.go +++ b/fastconsensus/consensus_test.go @@ -283,25 +283,25 @@ 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, ) *vote.Vote { - 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()) return 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, ) *vote.Vote { - 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()) return 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, ) *vote.Vote { - 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()) return td.addVote(cons, v, valID) } @@ -419,11 +419,12 @@ func (td *testData) makeProposal(t *testing.T, height uint32, round int16) *prop // 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 -// 2. JustMainVoteNoConflict -// 3. JustDecided +// 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) { + height uint32, round int16, +) (vote.Just, vote.Just, vote.Just) { t.Helper() cpRound := int16(0) @@ -475,9 +476,6 @@ func (td *testData) makeChangeProposerJusts(t *testing.T, propBlockHash hash.Has mainVoteCommitters := []int32{} mainVoteSigs := []*bls.Signature{} for i, val := range td.consP.validators { - mainVoteJust := &vote.JustMainVoteNoConflict{ - QCert: certPreVote, - } mainVote := vote.NewCPMainVote(propBlockHash, height, round, cpRound, cpValue, mainVoteJust, val.Address()) signBytes := mainVote.SignBytes() @@ -669,9 +667,9 @@ func TestPickRandomVote(t *testing.T) { // 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, 0, vote.CPValueYes, preVoteJust, tIndexY) - v4 := td.addCPMainVote(td.consP, hash.UndefHash, h, r, 0, vote.CPValueYes, mainVoteJust, tIndexY) - v5 := td.addCPDecidedVote(td.consP, hash.UndefHash, h, r, 0, vote.CPValueYes, decidedJust, 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) @@ -871,108 +869,106 @@ func TestFaulty(t *testing.T) { // 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) +func TestByzantine(t *testing.T) { + td := setup(t) -// for i := 0; i < 8; i++ { -// td.commitBlockForAllStates(t) -// } + for i := 0; i < 8; i++ { + td.commitBlockForAllStates(t) + } -// h := uint32(9) -// r := int16(0) + h := uint32(9) + r := int16(0) -// // ================================= -// // X, Y votes -// td.enterNewHeight(td.consX) -// td.enterNewHeight(td.consY) + // ================================= + // 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()) + 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) + // 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()) + 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 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 + // 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) + // ================================= + // 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()) + 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) + 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) -// // 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()) + 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) -// // 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.JustInitZero{ -// QCert: td.consB.makeCPMainVoteCertificate( -// 0, -// vote.CPValueNo, -// 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)) -// } + // ================================= + // 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, diff --git a/fastconsensus/cp.go b/fastconsensus/cp.go index 59340bbe6..b51af01ed 100644 --- a/fastconsensus/cp.go +++ b/fastconsensus/cp.go @@ -215,7 +215,10 @@ func (cp *changeProposer) cpCheckJustMainVoteConflict(just vote.Just, return err } - default: + case vote.JustTypeInitYes, + vote.JustTypeMainVoteConflict, + vote.JustTypeMainVoteNoConflict, + vote.JustTypeDecided: return invalidJustificationError{ Reason: fmt.Sprintf("unexpected justification: %s", j.JustNo.Type()), } @@ -234,7 +237,11 @@ func (cp *changeProposer) cpCheckJustMainVoteConflict(just vote.Just, return err } - default: + case vote.JustTypeInitNo, + vote.JustTypePreVoteSoft, + vote.JustTypeMainVoteConflict, + vote.JustTypeMainVoteNoConflict, + vote.JustTypeDecided: return invalidJustificationError{ Reason: fmt.Sprintf("unexpected justification: %s", j.JustNo.Type()), } diff --git a/fastconsensus/cp_prevote.go b/fastconsensus/cp_prevote.go index ea0a1e11d..d7c13ac43 100644 --- a/fastconsensus/cp_prevote.go +++ b/fastconsensus/cp_prevote.go @@ -18,6 +18,7 @@ func (s *cpPreVoteState) enter() { s.decide() } +//nolint:nestif // complexity can't be reduced more. func (s *cpPreVoteState) decide() { s.strongCommit() s.cpStrongTermination() diff --git a/fastconsensus/cp_test.go b/fastconsensus/cp_test.go index 2aeb3274d..fb3d3cde4 100644 --- a/fastconsensus/cp_test.go +++ b/fastconsensus/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.CPValueYes, preVote0.CPJust(), tIndexX) - td.addCPPreVote(td.consP, hash.UndefHash, h, r, 0, vote.CPValueYes, 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.CPValueYes, mainVote0.CPJust(), tIndexX) - td.addCPMainVote(td.consP, hash.UndefHash, h, r, 0, vote.CPValueYes, 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) checkHeightRound(t, td.consP, h, r+1) @@ -78,12 +78,12 @@ func TestChangeProposerAgreement0(t *testing.T) { td.changeProposerTimeout(td.consP) preVote0 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, blockHash) - td.addCPPreVote(td.consP, blockHash, h, r, 0, vote.CPValueNo, preVote0.CPJust(), tIndexX) - td.addCPPreVote(td.consP, blockHash, h, r, 0, vote.CPValueNo, preVote0.CPJust(), tIndexY) + 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, 0, vote.CPValueNo, mainVote0.CPJust(), tIndexX) - td.addCPMainVote(td.consP, blockHash, h, r, 0, vote.CPValueNo, mainVote0.CPJust(), tIndexY) + 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) @@ -118,9 +118,9 @@ func TestCrashOnTestnet(t *testing.T) { cert := td.consP.makeVoteCertificate(votes) just0 := &vote.JustInitNo{QCert: cert} - td.addCPPreVote(td.consP, blockHash, h, r, 0, vote.CPValueNo, just0, tIndexX) - td.addCPPreVote(td.consP, blockHash, h, r, 0, vote.CPValueNo, just0, tIndexY) - td.addCPPreVote(td.consP, blockHash, h, r, 0, vote.CPValueNo, just0, tIndexB) + 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) diff --git a/fastconsensus/prepare_test.go b/fastconsensus/prepare_test.go index 460d6a9c2..394aef65f 100644 --- a/fastconsensus/prepare_test.go +++ b/fastconsensus/prepare_test.go @@ -58,8 +58,8 @@ func TestGoToChangeProposerFromPrepare(t *testing.T) { td.enterNewHeight(td.consP) - td.addCPPreVote(td.consP, hash.UndefHash, 2, 0, 0, vote.CPValueYes, &vote.JustInitYes{}, tIndexX) - td.addCPPreVote(td.consP, hash.UndefHash, 2, 0, 0, vote.CPValueYes, &vote.JustInitYes{}, 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. From b8eeb95901260c1ea63e2801df3310018d8a289a Mon Sep 17 00:00:00 2001 From: Mostafa Date: Sun, 12 May 2024 20:54:15 +0800 Subject: [PATCH 08/11] chore: update the fast-consensus spec --- fastconsensus/spec/Pactus.pdf | Bin 189211 -> 189468 bytes fastconsensus/spec/Pactus.tla | 47 +++++++++++++++++-------------- fastconsensus/spec/temporary.cfg | 11 -------- 3 files changed, 26 insertions(+), 32 deletions(-) delete mode 100644 fastconsensus/spec/temporary.cfg diff --git a/fastconsensus/spec/Pactus.pdf b/fastconsensus/spec/Pactus.pdf index 6d9be057ff0d64ad8ea9da77742d1e87c46bca4b..5fb24d905a040a8c767436b22a897f323ab9556d 100644 GIT binary patch delta 37778 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 ASSUME - \* Ensure that the number of nodes is sufficient to tolerate the specified number of faults. - /\ NumNodes >= ThreeFPlusOne + \* 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..NumNodes-1 + /\ FaultyNodes \subseteq 0..n-1 ----------------------------------------------------------------------------- (***************************************************************************) @@ -64,13 +69,13 @@ SubsetOfMsgs(params) == \* 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 % NumNodes = index + states[index].round % n = index -\* Helper function to check if a node is faulty or not. +\* 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 all the PREPARE votes in this round. +\* has received `4t+1` PREPARE votes for a proposal. HasPrepareAbsoluteQuorum(index) == Cardinality(SubsetOfMsgs([ type |-> "PREPARE", @@ -78,7 +83,7 @@ HasPrepareAbsoluteQuorum(index) == round |-> states[index].round])) >= FourTPlusOne \* HasPrepareQuorum checks whether the node with the given index -\* has received 2f+1 the PREPARE votes in this round. +\* has received `3t+1` PREPARE votes for a proposal. HasPrepareQuorum(index) == Cardinality(SubsetOfMsgs([ type |-> "PREPARE", @@ -86,7 +91,7 @@ HasPrepareQuorum(index) == round |-> states[index].round])) >= ThreeTPlusOne \* HasPrecommitQuorum checks whether the node with the given index -\* has received 2f+1 the PRECOMMIT votes in this round. +\* has received `3t+1` the PRECOMMIT votes for a proposal. HasPrecommitQuorum(index) == Cardinality(SubsetOfMsgs([ type |-> "PRECOMMIT", @@ -491,14 +496,14 @@ StrongCommit(index) == Init == /\ log = {} - /\ states = [index \in 0..NumNodes-1 |-> [ + /\ states = [index \in 0..n-1 |-> [ name |-> "new-height", height |-> 0, round |-> 0, cp_round |-> 0]] Next == - \E index \in 0..NumNodes-1: + \E index \in 0..n-1: \/ NewHeight(index) \/ Propose(index) \/ Prepare(index) @@ -524,7 +529,7 @@ Success == <>(IsCommitted) (* TypeOK is the type-correctness invariant. *) (***************************************************************************) TypeOK == - /\ \A index \in 0..NumNodes-1: + /\ \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 diff --git a/fastconsensus/spec/temporary.cfg b/fastconsensus/spec/temporary.cfg deleted file mode 100644 index 26fabe4a2..000000000 --- a/fastconsensus/spec/temporary.cfg +++ /dev/null @@ -1,11 +0,0 @@ -SPECIFICATION Spec -INVARIANT TypeOK -PROPERTY Success -CONSTANTS \* regular assignments - NumNodes = 6 - f = 1 - t = 1 - FaultyNodes = {5} - MaxHeight = 1 - MaxRound = 1 - MaxCPRound = 1 \ No newline at end of file From 3a0d42fb4cad4df999d899722df777ea53805717 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Tue, 28 May 2024 04:49:28 +0800 Subject: [PATCH 09/11] chore: fix linting issues --- consensus/config.go | 2 +- consensus/config_test.go | 4 +- consensus/cp.go | 2 +- fastconsensus/commit.go | 8 +-- fastconsensus/config.go | 4 +- fastconsensus/config_test.go | 6 +- fastconsensus/consensus.go | 10 ++-- fastconsensus/consensus_test.go | 30 ++++------ fastconsensus/cp.go | 4 +- fastconsensus/cp_decide.go | 2 +- fastconsensus/cp_mainvote.go | 6 +- fastconsensus/cp_prevote.go | 6 +- fastconsensus/cp_test.go | 4 +- fastconsensus/height.go | 4 +- fastconsensus/interface.go | 8 +-- fastconsensus/manager.go | 2 +- fastconsensus/manager_test.go | 75 +++++++++++++------------ fastconsensus/mediator.go | 4 +- fastconsensus/mock.go | 4 +- fastconsensus/precommit.go | 4 +- fastconsensus/prepare.go | 2 +- fastconsensus/propose.go | 8 +-- fastconsensus/voteset/binary_voteset.go | 7 ++- fastconsensus/voteset/block_voteset.go | 7 ++- types/vote/cp_just.go | 8 +-- util/testsuite/testsuite.go | 8 +-- 26 files changed, 112 insertions(+), 117 deletions(-) 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/cp.go b/consensus/cp.go index 436506503..7292b9b86 100644 --- a/consensus/cp.go +++ b/consensus/cp.go @@ -56,7 +56,7 @@ func (cp *changeProposer) checkJustInitZero(just vote.Just, blockHash hash.Hash) return nil } -func (cp *changeProposer) checkJustInitOne(just vote.Just) error { +func (*changeProposer) checkJustInitOne(just vote.Just) error { _, ok := just.(*vote.JustInitYes) if !ok { return invalidJustificationError{ diff --git a/fastconsensus/commit.go b/fastconsensus/commit.go index e10517f3a..65088fac2 100644 --- a/fastconsensus/commit.go +++ b/fastconsensus/commit.go @@ -29,18 +29,18 @@ func (s *commitState) decide() { s.enterNewState(s.newHeightState) } -func (s *commitState) onAddVote(_ *vote.Vote) { +func (*commitState) onAddVote(_ *vote.Vote) { panic("Unreachable") } -func (s *commitState) onSetProposal(_ *proposal.Proposal) { +func (*commitState) onSetProposal(_ *proposal.Proposal) { panic("Unreachable") } -func (s *commitState) onTimeout(_ *ticker) { +func (*commitState) onTimeout(_ *ticker) { panic("Unreachable") } -func (s *commitState) name() string { +func (*commitState) name() string { return "commit" } diff --git a/fastconsensus/config.go b/fastconsensus/config.go index 380888ba1..805a94294 100644 --- a/fastconsensus/config.go +++ b/fastconsensus/config.go @@ -20,12 +20,12 @@ func DefaultConfig() *Config { func (conf *Config) BasicCheck() error { if conf.ChangeProposerTimeout <= 0 { return ConfigError{ - Reason: "timeout for change proposer can't be negative", + Reason: "change proposer timeout must be greater than zero", } } if conf.ChangeProposerDelta <= 0 { return ConfigError{ - Reason: "change proposer delta can't be negative", + Reason: "change proposer delta must be greater than zero", } } if conf.MinimumAvailabilityScore < 0 || conf.MinimumAvailabilityScore > 1 { diff --git a/fastconsensus/config_test.go b/fastconsensus/config_test.go index 008fea4eb..356238ccc 100644 --- a/fastconsensus/config_test.go +++ b/fastconsensus/config_test.go @@ -16,13 +16,13 @@ func TestDefaultConfigCheck(t *testing.T) { assert.NoError(t, c1.BasicCheck()) c2.ChangeProposerDelta = 0 * time.Second - assert.ErrorIs(t, c2.BasicCheck(), ConfigError{Reason: "change proposer delta can't be negative"}) + 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 can't be negative"}) + 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 can't be negative"}) + 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/fastconsensus/consensus.go b/fastconsensus/consensus.go index b38853029..46f9cb3b2 100644 --- a/fastconsensus/consensus.go +++ b/fastconsensus/consensus.go @@ -65,11 +65,11 @@ func NewConsensus( broadcastCh <- msg } - return newConsensus(conf, bcState, + return makeConsensus(conf, bcState, valKey, rewardAddr, broadcaster, mediator) } -func newConsensus( +func makeConsensus( conf *Config, bcState state.Facade, valKey *bls.ValidatorKey, @@ -119,7 +119,7 @@ func (cs *consensus) Start() { cs.lk.Lock() defer cs.lk.Unlock() - cs.moveToNewHeight() + 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 { @@ -187,10 +187,10 @@ func (cs *consensus) MoveToNewHeight() { cs.lk.Lock() defer cs.lk.Unlock() - cs.moveToNewHeight() + cs.doMoveToNewHeight() } -func (cs *consensus) moveToNewHeight() { +func (cs *consensus) doMoveToNewHeight() { stateHeight := cs.bcState.LastBlockHeight() if cs.height != stateHeight+1 { cs.enterNewState(cs.newHeightState) diff --git a/fastconsensus/consensus_test.go b/fastconsensus/consensus_test.go index 9432d7d96..d571dbd51 100644 --- a/fastconsensus/consensus_test.go +++ b/fastconsensus/consensus_test.go @@ -119,7 +119,7 @@ func setupWithSeed(t *testing.T, seed int64) *testData { store.MockingStore(ts), txPool, nil) require.NoError(t, err) - instances[i] = newConsensus(testConfig(), bcState, valKey, + instances[i] = makeConsensus(testConfig(), bcState, valKey, valKey.PublicKey().AccountAddress(), broadcasterFunc, newConcreteMediator()) } @@ -252,7 +252,7 @@ func (td *testData) shouldPublishVote(t *testing.T, cons *consensus, voteType vo return nil } -func checkHeightRound(t *testing.T, cons *consensus, height uint32, round int16) { +func (*testData) checkHeightRound(t *testing.T, cons *consensus, height uint32, round int16) { t.Helper() h, r := cons.HeightRound() @@ -260,12 +260,6 @@ func checkHeightRound(t *testing.T, cons *consensus, height uint32, round int16) assert.Equal(t, r, round) } -func (td *testData) checkHeightRound(t *testing.T, cons *consensus, height uint32, round int16) { - t.Helper() - - checkHeightRound(t, cons, height, round) -} - func (td *testData) addPrepareVote(cons *consensus, blockHash hash.Hash, height uint32, round int16, valID int, ) *vote.Vote { @@ -313,23 +307,19 @@ func (td *testData) addVote(cons *consensus, v *vote.Vote, valID int) *vote.Vote return v } -func newHeightTimeout(cons *consensus) { +func (*testData) newHeightTimeout(cons *consensus) { cons.lk.Lock() cons.currentState.onTimeout(&ticker{0, cons.height, cons.round, tickerTargetNewHeight}) cons.lk.Unlock() } -func (td *testData) newHeightTimeout(cons *consensus) { - newHeightTimeout(cons) -} - -func (td *testData) queryProposalTimeout(cons *consensus) { +func (*testData) queryProposalTimeout(cons *consensus) { cons.lk.Lock() cons.currentState.onTimeout(&ticker{0, cons.height, cons.round, tickerTargetQueryProposal}) cons.lk.Unlock() } -func (td *testData) changeProposerTimeout(cons *consensus) { +func (*testData) changeProposerTimeout(cons *consensus) { cons.lk.Lock() cons.currentState.onTimeout(&ticker{0, cons.height, cons.round, tickerTargetChangeProposer}) cons.lk.Unlock() @@ -346,7 +336,7 @@ func (td *testData) enterNewHeight(cons *consensus) { } // enterNextRound helps tests to enter next round safely. -func (td *testData) enterNextRound(cons *consensus) { +func (*testData) enterNextRound(cons *consensus) { cons.lk.Lock() cons.round++ cons.enterNewState(cons.proposeState) @@ -506,9 +496,9 @@ func TestNotInCommittee(t *testing.T) { str := store.MockingStore(td.TestSuite) st, _ := state.LoadOrNewState(td.genDoc, []*bls.ValidatorKey{valKey}, str, td.txPool, nil) - Cons := NewConsensus(testConfig(), st, valKey, valKey.Address(), make(chan message.Message, 100), + consInst := NewConsensus(testConfig(), st, valKey, valKey.Address(), make(chan message.Message, 100), newConcreteMediator()) - cons := Cons.(*consensus) + cons := consInst.(*consensus) td.enterNewHeight(cons) td.newHeightTimeout(cons) @@ -746,9 +736,9 @@ func TestNonActiveValidator(t *testing.T) { td := setup(t) valKey := td.RandValKey() - Cons := NewConsensus(testConfig(), state.MockingState(td.TestSuite), + consInst := NewConsensus(testConfig(), state.MockingState(td.TestSuite), valKey, valKey.Address(), make(chan message.Message, 100), newConcreteMediator()) - nonActiveCons := Cons.(*consensus) + nonActiveCons := consInst.(*consensus) t.Run("non-active instances should be in new-height state", func(t *testing.T) { nonActiveCons.MoveToNewHeight() diff --git a/fastconsensus/cp.go b/fastconsensus/cp.go index b51af01ed..ee5ef01a1 100644 --- a/fastconsensus/cp.go +++ b/fastconsensus/cp.go @@ -12,7 +12,7 @@ type changeProposer struct { *consensus } -func (cp *changeProposer) onSetProposal(_ *proposal.Proposal) { +func (*changeProposer) onSetProposal(_ *proposal.Proposal) { // Ignore proposal } @@ -23,7 +23,7 @@ func (cp *changeProposer) onTimeout(t *ticker) { } } -func (cp *changeProposer) cpCheckCPValue(value vote.CPValue, allowedValues ...vote.CPValue) error { +func (*changeProposer) cpCheckCPValue(value vote.CPValue, allowedValues ...vote.CPValue) error { for _, v := range allowedValues { if value == v { return nil diff --git a/fastconsensus/cp_decide.go b/fastconsensus/cp_decide.go index d8182bc2f..53abbb51a 100644 --- a/fastconsensus/cp_decide.go +++ b/fastconsensus/cp_decide.go @@ -54,6 +54,6 @@ func (s *cpDecideState) onAddVote(_ *vote.Vote) { s.decide() } -func (s *cpDecideState) name() string { +func (*cpDecideState) name() string { return "cp:decide" } diff --git a/fastconsensus/cp_mainvote.go b/fastconsensus/cp_mainvote.go index 58513753f..602762501 100644 --- a/fastconsensus/cp_mainvote.go +++ b/fastconsensus/cp_mainvote.go @@ -90,14 +90,14 @@ func (s *cpMainVoteState) onAddVote(_ *vote.Vote) { s.decide() } -func (s *cpMainVoteState) onSetProposal(_ *proposal.Proposal) { +func (*cpMainVoteState) onSetProposal(_ *proposal.Proposal) { // Ignore proposal } -func (s *cpMainVoteState) onTimeout(_ *ticker) { +func (*cpMainVoteState) onTimeout(_ *ticker) { // Ignore timeouts } -func (s *cpMainVoteState) name() string { +func (*cpMainVoteState) name() string { return "cp:main-vote" } diff --git a/fastconsensus/cp_prevote.go b/fastconsensus/cp_prevote.go index d7c13ac43..0ed758092 100644 --- a/fastconsensus/cp_prevote.go +++ b/fastconsensus/cp_prevote.go @@ -91,12 +91,12 @@ func (s *cpPreVoteState) onAddVote(_ *vote.Vote) { s.decide() } -func (s *cpPreVoteState) onSetProposal(_ *proposal.Proposal) { +func (*cpPreVoteState) onSetProposal(_ *proposal.Proposal) { } -func (s *cpPreVoteState) onTimeout(_ *ticker) { +func (*cpPreVoteState) onTimeout(_ *ticker) { } -func (s *cpPreVoteState) name() string { +func (*cpPreVoteState) name() string { return "cp:pre-vote" } diff --git a/fastconsensus/cp_test.go b/fastconsensus/cp_test.go index fb3d3cde4..377b6fed3 100644 --- a/fastconsensus/cp_test.go +++ b/fastconsensus/cp_test.go @@ -53,7 +53,7 @@ func TestChangeProposerAgreement1(t *testing.T) { td.addCPMainVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, mainVote0.CPJust(), tIndexY) td.shouldPublishVote(t, td.consP, vote.VoteTypeCPDecided, hash.UndefHash) - checkHeightRound(t, td.consP, h, r+1) + td.checkHeightRound(t, td.consP, h, r+1) } func TestChangeProposerAgreement0(t *testing.T) { @@ -88,7 +88,7 @@ func TestChangeProposerAgreement0(t *testing.T) { td.shouldPublishVote(t, td.consP, vote.VoteTypeCPDecided, blockHash) td.addPrecommitVote(td.consP, blockHash, h, r, tIndexX) td.addPrecommitVote(td.consP, blockHash, h, r, tIndexY) - checkHeightRound(t, td.consP, h, r) + td.checkHeightRound(t, td.consP, h, r) } // ConsP receives all PRE-VOTE:0 votes before receiving a proposal or prepare votes. diff --git a/fastconsensus/height.go b/fastconsensus/height.go index 0c1915bf7..c5a4012d2 100644 --- a/fastconsensus/height.go +++ b/fastconsensus/height.go @@ -43,7 +43,7 @@ func (s *newHeightState) onAddVote(_ *vote.Vote) { } } -func (s *newHeightState) onSetProposal(_ *proposal.Proposal) { +func (*newHeightState) onSetProposal(_ *proposal.Proposal) { // Ignore proposal } @@ -55,6 +55,6 @@ func (s *newHeightState) onTimeout(t *ticker) { } } -func (s *newHeightState) name() string { +func (*newHeightState) name() string { return "new-height" } diff --git a/fastconsensus/interface.go b/fastconsensus/interface.go index cbcbe3a01..adae51b8e 100644 --- a/fastconsensus/interface.go +++ b/fastconsensus/interface.go @@ -22,8 +22,8 @@ type Consensus interface { Start() MoveToNewHeight() - AddVote(vote *vote.Vote) - SetProposal(proposal *proposal.Proposal) + AddVote(vte *vote.Vote) + SetProposal(prop *proposal.Proposal) } type ManagerReader interface { @@ -40,6 +40,6 @@ type Manager interface { Start() error Stop() MoveToNewHeight() - AddVote(vote *vote.Vote) - SetProposal(proposal *proposal.Proposal) + AddVote(vot *vote.Vote) + SetProposal(prop *proposal.Proposal) } diff --git a/fastconsensus/manager.go b/fastconsensus/manager.go index 4aeced92b..62f177fb0 100644 --- a/fastconsensus/manager.go +++ b/fastconsensus/manager.go @@ -60,7 +60,7 @@ func (mgr *manager) Start() error { } // Stop stops the manager. -func (mgr *manager) Stop() { +func (*manager) Stop() { } // Instances return all consensus instances that are read-only and diff --git a/fastconsensus/manager_test.go b/fastconsensus/manager_test.go index 029106a20..3b6c69e89 100644 --- a/fastconsensus/manager_test.go +++ b/fastconsensus/manager_test.go @@ -25,12 +25,12 @@ func TestManager(t *testing.T) { valKeys := []*bls.ValidatorKey{st.TestValKeys[0], ts.RandValKey()} broadcastCh := make(chan message.Message, 500) - stateHeight := ts.RandHeight() - blk, cert := ts.GenerateTestBlock(stateHeight) + randomHeight := ts.RandHeight() + blk, cert := ts.GenerateTestBlock(randomHeight) st.TestStore.SaveBlock(blk, cert) - Mgr := NewManager(testConfig(), st, valKeys, rewardAddrs, broadcastCh) - mgr := Mgr.(*manager) + mgrInst := NewManager(testConfig(), st, valKeys, rewardAddrs, broadcastCh) + mgr := mgrInst.(*manager) consA := mgr.instances[0].(*consensus) // active consB := mgr.instances[1].(*consensus) // inactive @@ -41,17 +41,20 @@ func TestManager(t *testing.T) { }) 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() - h, r := mgr.HeightRound() + consHeight, consRound := mgr.HeightRound() assert.True(t, mgr.HasActiveInstance()) - assert.Equal(t, stateHeight+1, h) - assert.Zero(t, r) + assert.Equal(t, consHeight, stateHeight+1) + assert.Zero(t, consRound) }) t.Run("Testing add vote", func(t *testing.T) { - v := vote.NewPrepareVote(ts.RandHash(), stateHeight+1, 0, valKeys[0].Address()) + consHeight, _ := mgr.HeightRound() + v := vote.NewPrepareVote(ts.RandHash(), consHeight, 0, valKeys[0].Address()) ts.HelperSignVote(valKeys[0], v) mgr.AddVote(v) @@ -61,8 +64,9 @@ func TestManager(t *testing.T) { }) t.Run("Testing set proposal", func(t *testing.T) { + consHeight, _ := mgr.HeightRound() b, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) - p := proposal.NewProposal(stateHeight+1, 0, b) + p := proposal.NewProposal(consHeight, 0, b) ts.HelperSignProposal(valKeys[0], p) mgr.SetProposal(p) @@ -72,7 +76,8 @@ func TestManager(t *testing.T) { }) t.Run("Check discarding old votes", func(t *testing.T) { - v := vote.NewPrepareVote(ts.RandHash(), stateHeight-1, 0, st.TestValKeys[2].Address()) + consHeight, _ := mgr.HeightRound() + v := vote.NewPrepareVote(ts.RandHash(), consHeight-1, 0, st.TestValKeys[2].Address()) ts.HelperSignVote(st.TestValKeys[2], v) mgr.AddVote(v) @@ -80,8 +85,9 @@ func TestManager(t *testing.T) { }) t.Run("Check discarding old proposals", func(t *testing.T) { + consHeight, _ := mgr.HeightRound() b, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) - p := proposal.NewProposal(stateHeight-1, 1, b) + p := proposal.NewProposal(consHeight-1, 1, b) ts.HelperSignProposal(valKeys[0], p) mgr.SetProposal(p) @@ -89,9 +95,10 @@ func TestManager(t *testing.T) { }) t.Run("Processing upcoming votes", func(t *testing.T) { - v1 := vote.NewPrepareVote(ts.RandHash(), stateHeight+2, 0, valKeys[0].Address()) - v2 := vote.NewPrepareVote(ts.RandHash(), stateHeight+3, 0, valKeys[0].Address()) - v3 := vote.NewPrepareVote(ts.RandHash(), stateHeight+4, 0, valKeys[0].Address()) + 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) @@ -103,15 +110,13 @@ func TestManager(t *testing.T) { assert.Len(t, mgr.upcomingVotes, 3) - blk, cert := ts.GenerateTestBlock(stateHeight + 1) - err := st.CommitBlock(blk, cert) + blk1, cert1 := ts.GenerateTestBlock(consHeight) + err := st.CommitBlock(blk1, cert1) assert.NoError(t, err) - stateHeight++ - blk, cert = ts.GenerateTestBlock(stateHeight + 1) - err = st.CommitBlock(blk, cert) + blk2, cert2 := ts.GenerateTestBlock(consHeight + 1) + err = st.CommitBlock(blk2, cert2) assert.NoError(t, err) - stateHeight++ mgr.MoveToNewHeight() @@ -119,14 +124,15 @@ func TestManager(t *testing.T) { }) t.Run("Processing upcoming proposal", func(t *testing.T) { + consHeight, _ := mgr.HeightRound() b1, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) - p1 := proposal.NewProposal(stateHeight+2, 0, b1) + p1 := proposal.NewProposal(consHeight+1, 0, b1) b2, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) - p2 := proposal.NewProposal(stateHeight+3, 0, b2) + p2 := proposal.NewProposal(consHeight+2, 0, b2) b3, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) - p3 := proposal.NewProposal(stateHeight+4, 0, b3) + p3 := proposal.NewProposal(consHeight+3, 0, b3) ts.HelperSignProposal(valKeys[0], p1) ts.HelperSignProposal(valKeys[0], p2) @@ -138,15 +144,13 @@ func TestManager(t *testing.T) { assert.Len(t, mgr.upcomingProposals, 3) - blk, cert := ts.GenerateTestBlock(stateHeight + 1) - err := st.CommitBlock(blk, cert) + blk1, cert1 := ts.GenerateTestBlock(consHeight) + err := st.CommitBlock(blk1, cert1) assert.NoError(t, err) - stateHeight++ - blk, cert = ts.GenerateTestBlock(stateHeight + 1) - err = st.CommitBlock(blk, cert) + blk2, cert2 := ts.GenerateTestBlock(consHeight + 1) + err = st.CommitBlock(blk2, cert2) assert.NoError(t, err) - stateHeight++ mgr.MoveToNewHeight() @@ -157,15 +161,14 @@ func TestManager(t *testing.T) { func TestMediator(t *testing.T) { ts := testsuite.NewTestSuite(t) - committeeSize := 6 st := state.MockingState(ts) - cmt, valKeys := ts.GenerateTestCommittee(committeeSize) + cmt, valKeys := ts.GenerateTestCommittee(4) st.TestCommittee = cmt st.TestParams.BlockIntervalInSecond = 1 - rewardAddrs := []crypto.Address{} - for i := 0; i < committeeSize; i++ { - rewardAddrs = append(rewardAddrs, ts.RandAccAddress()) + rewardAddrs := []crypto.Address{ + ts.RandAccAddress(), ts.RandAccAddress(), + ts.RandAccAddress(), ts.RandAccAddress(), } broadcastCh := make(chan message.Message, 500) @@ -173,8 +176,8 @@ func TestMediator(t *testing.T) { blk, cert := ts.GenerateTestBlock(stateHeight) st.TestStore.SaveBlock(blk, cert) - Mgr := NewManager(testConfig(), st, valKeys, rewardAddrs, broadcastCh) - mgr := Mgr.(*manager) + mgrInst := NewManager(testConfig(), st, valKeys, rewardAddrs, broadcastCh) + mgr := mgrInst.(*manager) mgr.MoveToNewHeight() diff --git a/fastconsensus/mediator.go b/fastconsensus/mediator.go index d527babd7..29ba371f4 100644 --- a/fastconsensus/mediator.go +++ b/fastconsensus/mediator.go @@ -8,8 +8,8 @@ import ( // The `mediator“ interface defines a mechanism for setting proposals and votes // between independent consensus instances. type mediator interface { - OnPublishProposal(from Consensus, proposal *proposal.Proposal) - OnPublishVote(from Consensus, vote *vote.Vote) + OnPublishProposal(from Consensus, prop *proposal.Proposal) + OnPublishVote(from Consensus, vte *vote.Vote) OnBlockAnnounce(from Consensus) Register(cons Consensus) } diff --git a/fastconsensus/mock.go b/fastconsensus/mock.go index 876b06ffd..f7dbcc6ca 100644 --- a/fastconsensus/mock.go +++ b/fastconsensus/mock.go @@ -59,7 +59,7 @@ func (m *MockConsensus) MoveToNewHeight() { m.Height++ } -func (m *MockConsensus) Start() {} +func (*MockConsensus) Start() {} func (m *MockConsensus) AddVote(v *vote.Vote) { m.lk.Lock() @@ -109,7 +109,7 @@ func (m *MockConsensus) HeightRound() (uint32, int16) { return m.Height, m.Round } -func (m *MockConsensus) String() string { +func (*MockConsensus) String() string { return "" } diff --git a/fastconsensus/precommit.go b/fastconsensus/precommit.go index 0ca57b20b..726b88207 100644 --- a/fastconsensus/precommit.go +++ b/fastconsensus/precommit.go @@ -67,10 +67,10 @@ func (s *precommitState) onSetProposal(_ *proposal.Proposal) { s.decide() } -func (s *precommitState) onTimeout(_ *ticker) { +func (*precommitState) onTimeout(_ *ticker) { // Ignore timeouts } -func (s *precommitState) name() string { +func (*precommitState) name() string { return "precommit" } diff --git a/fastconsensus/prepare.go b/fastconsensus/prepare.go index 96b0d17db..5c3317ef3 100644 --- a/fastconsensus/prepare.go +++ b/fastconsensus/prepare.go @@ -78,6 +78,6 @@ func (s *prepareState) onSetProposal(_ *proposal.Proposal) { s.decide() } -func (s *prepareState) name() string { +func (*prepareState) name() string { return "prepare" } diff --git a/fastconsensus/propose.go b/fastconsensus/propose.go index 81eb7d011..99dacabb2 100644 --- a/fastconsensus/propose.go +++ b/fastconsensus/propose.go @@ -64,18 +64,18 @@ func (s *proposeState) createProposal(height uint32, round int16) { s.logger.Info("proposal signed and broadcasted", "proposal", prop) } -func (s *proposeState) onAddVote(_ *vote.Vote) { +func (*proposeState) onAddVote(_ *vote.Vote) { panic("Unreachable") } -func (s *proposeState) onSetProposal(_ *proposal.Proposal) { +func (*proposeState) onSetProposal(_ *proposal.Proposal) { panic("Unreachable") } -func (s *proposeState) onTimeout(_ *ticker) { +func (*proposeState) onTimeout(_ *ticker) { panic("Unreachable") } -func (s *proposeState) name() string { +func (*proposeState) name() string { return "propose" } diff --git a/fastconsensus/voteset/binary_voteset.go b/fastconsensus/voteset/binary_voteset.go index 0c528a7c9..8221d9032 100644 --- a/fastconsensus/voteset/binary_voteset.go +++ b/fastconsensus/voteset/binary_voteset.go @@ -99,12 +99,13 @@ func (vs *BinaryVoteSet) AddVote(v *vote.Vote) (bool, error) { roundVotes := vs.mustGetRoundVotes(v.CPRound()) existingVote, ok := roundVotes.allVotes[v.Signer()] if ok { - if existingVote.Hash() != v.Hash() { - err = errors.Error(errors.ErrDuplicateVote) - } else { + 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 diff --git a/fastconsensus/voteset/block_voteset.go b/fastconsensus/voteset/block_voteset.go index 09a617548..6a4ab2f1e 100644 --- a/fastconsensus/voteset/block_voteset.go +++ b/fastconsensus/voteset/block_voteset.go @@ -78,12 +78,13 @@ func (vs *BlockVoteSet) AddVote(v *vote.Vote) (bool, error) { existingVote, ok := vs.allVotes[v.Signer()] if ok { - if existingVote.Hash() != v.Hash() { - err = errors.Error(errors.ErrDuplicateVote) - } else { + 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 } diff --git a/types/vote/cp_just.go b/types/vote/cp_just.go index 24877af9b..95d81c5e3 100644 --- a/types/vote/cp_just.go +++ b/types/vote/cp_just.go @@ -90,11 +90,11 @@ type JustDecided struct { QCert *certificate.VoteCertificate `cbor:"1,keyasint"` } -func (j *JustInitNo) Type() JustType { +func (*JustInitNo) Type() JustType { return JustTypeInitNo } -func (j *JustInitYes) Type() JustType { +func (*JustInitYes) Type() JustType { return JustTypeInitYes } @@ -118,11 +118,11 @@ func (*JustDecided) Type() JustType { return JustTypeDecided } -func (j *JustInitNo) BasicCheck() error { +func (*JustInitNo) BasicCheck() error { return nil } -func (j *JustInitYes) BasicCheck() error { +func (*JustInitYes) BasicCheck() error { return nil } diff --git a/util/testsuite/testsuite.go b/util/testsuite/testsuite.go index 11f2faf51..c59e2b686 100644 --- a/util/testsuite/testsuite.go +++ b/util/testsuite/testsuite.go @@ -317,21 +317,21 @@ 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.BlockCertificate) { - return ts.generateTestBlock(height, proposer, util.Now()) + 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.BlockCertificate) { - return ts.generateTestBlock(height, ts.RandValAddress(), tme) + return ts.makeTestBlock(height, ts.RandValAddress(), tme) } // GenerateTestBlock generates a block for testing purposes. func (ts *TestSuite) GenerateTestBlock(height uint32) (*block.Block, *certificate.BlockCertificate) { - return ts.generateTestBlock(height, ts.RandValAddress(), util.Now()) + return ts.makeTestBlock(height, ts.RandValAddress(), util.Now()) } -func (ts *TestSuite) generateTestBlock(height uint32, proposer crypto.Address, tme time.Time, +func (ts *TestSuite) makeTestBlock(height uint32, proposer crypto.Address, tme time.Time, ) (*block.Block, *certificate.BlockCertificate) { txs := block.NewTxs() tx1, _ := ts.GenerateTestTransferTx() From 1d832658081f97732fb255f0f7107fbc5a78f241 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Fri, 7 Jun 2024 00:29:05 +0800 Subject: [PATCH 10/11] test: fix broken tests --- cmd/cmd.go | 5 +- consensus/consensus.go | 8 +- fastconsensus/consensus.go | 8 +- fastconsensus/consensus_test.go | 74 +++++++------- fastconsensus/prepare_test.go | 2 +- types/certificate/vote_certificate.go | 10 +- types/certificate/vote_certificate_test.go | 110 +++++++++++++++------ 7 files changed, 133 insertions(+), 84 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index d1a27a070..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, 6) - for i := 0; i < 6; 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/consensus/consensus.go b/consensus/consensus.go index 843e0f08e..0d7bfe90a 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -409,16 +409,16 @@ func (cs *consensus) announceNewBlock(blk *block.Block, cert *certificate.BlockC func (cs *consensus) makeBlockCertificate(votes map[crypto.Address]*vote.Vote, ) *certificate.BlockCertificate { cert := certificate.NewBlockCertificate(cs.height, cs.round, false) - cert.SetSignature(cs.signerInfo(votes)) + cert.SetSignature(cs.signersInfo(votes)) return cert } -// signerInfo processes a map of votes from validators and provides these information: +// 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) signerInfo(votes map[crypto.Address]*vote.Vote) ([]int32, []int32, *bls.Signature) { +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) @@ -443,7 +443,7 @@ func (cs *consensus) signerInfo(votes map[crypto.Address]*vote.Vote) ([]int32, [ func (cs *consensus) makeVoteCertificate(votes map[crypto.Address]*vote.Vote, ) *certificate.VoteCertificate { cert := certificate.NewVoteCertificate(cs.height, cs.round) - cert.SetSignature(cs.signerInfo(votes)) + cert.SetSignature(cs.signersInfo(votes)) return cert } diff --git a/fastconsensus/consensus.go b/fastconsensus/consensus.go index 46f9cb3b2..d45fff1f9 100644 --- a/fastconsensus/consensus.go +++ b/fastconsensus/consensus.go @@ -412,7 +412,7 @@ func (cs *consensus) announceNewBlock(blk *block.Block, cert *certificate.BlockC 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.signerInfo(votes)) + cert.SetSignature(cs.signersInfo(votes)) return cert } @@ -420,16 +420,16 @@ func (cs *consensus) makeBlockCertificate(votes map[crypto.Address]*vote.Vote, f func (cs *consensus) makeVoteCertificate(votes map[crypto.Address]*vote.Vote, ) *certificate.VoteCertificate { cert := certificate.NewVoteCertificate(cs.height, cs.round) - cert.SetSignature(cs.signerInfo(votes)) + cert.SetSignature(cs.signersInfo(votes)) return cert } -// signerInfo processes a map of votes from validators and provides these information: +// 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) signerInfo(votes map[crypto.Address]*vote.Vote) ([]int32, []int32, *bls.Signature) { +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) diff --git a/fastconsensus/consensus_test.go b/fastconsensus/consensus_test.go index d571dbd51..9cd4479d8 100644 --- a/fastconsensus/consensus_test.go +++ b/fastconsensus/consensus_test.go @@ -219,7 +219,7 @@ func (td *testData) shouldPublishQueryVote(t *testing.T, cons *consensus, height for _, consMsg := range td.consMessages { if consMsg.sender != cons.valKey.Address() || - consMsg.message.Type() != message.TypeQueryVotes { + consMsg.message.Type() != message.TypeQueryVote { continue } @@ -409,9 +409,10 @@ func (td *testData) makeProposal(t *testing.T, height uint32, round int16) *prop // 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. +// +// 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) { @@ -599,49 +600,44 @@ func TestConsensusLateProposal(t *testing.T) { 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 +// 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.enterNewHeight(td.consP) -// td.commitBlockForAllStates(t) // height 2 + td.commitBlockForAllStates(t) // height 1 -// h := uint32(2) -// r := int16(0) -// prop := td.makeProposal(t, h, r) -// blockHash := p.Block().Hash() + td.enterNewHeight(td.consP) -// 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) + h := uint32(2) + r := int16(0) + prop := td.makeProposal(t, h, r) + blockHash := prop.Block().Hash() -// // consP timed out -// td.changeProposerTimeout(td.consP) + 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) -// preVote0 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, blockHash) -// td.addCPPreVote(td.consP, p.Block().Hash(), h, r, 0, vote.CPValueNo, preVote0.CPJust(), tIndexX) -// td.addCPPreVote(td.consP, p.Block().Hash(), h, r, 0, vote.CPValueNo, preVote0.CPJust(), tIndexY) + // consP timed out + td.changeProposerTimeout(td.consP) -// mainVote0 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPMainVote, blockHash) -// td.addCPMainVote(td.consP, blockHash, h, r, 0, vote.CPValueNo, mainVote0.CPJust(), tIndexX) -// td.addCPMainVote(td.consP, blockHash, h, r, 0, vote.CPValueNo, mainVote0.CPJust(), tIndexY) + _, _, decidedJust := td.makeChangeProposerJusts(t, prop.Block().Hash(), h, r) + td.addCPDecidedVote(td.consP, prop.Block().Hash(), h, r, vote.CPValueNo, decidedJust, tIndexX) -// 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.addPrecommitVote(td.consP, blockHash, h, r, tIndexM) + td.addPrecommitVote(td.consP, blockHash, h, r, tIndexN) -// td.addPrecommitVote(td.consP, blockHash, h, r, tIndexX) -// td.addPrecommitVote(td.consP, blockHash, h, r, tIndexY) -// td.addPrecommitVote(td.consP, blockHash, h, r, tIndexB) + td.shouldPublishQueryProposal(t, td.consP, h) -// // consP receives proposal now -// td.consP.SetProposal(p) + // consP receives proposal now + td.consP.SetProposal(prop) -// td.shouldPublishVote(t, td.consP, vote.VoteTypePrepare, p.Block().Hash()) -// td.shouldPublishBlockAnnounce(t, td.consP, p.Block().Hash()) -// } + 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) @@ -1010,7 +1006,7 @@ func checkConsensus(td *testData, height uint32, byzVotes []*vote.Vote) ( }) } } - case message.TypeQueryVotes: + 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. @@ -1021,7 +1017,7 @@ func checkConsensus(td *testData, height uint32, byzVotes []*vote.Vote) ( case message.TypeHello, message.TypeHelloAck, - message.TypeTransactions, + message.TypeTransaction, message.TypeBlocksRequest, message.TypeBlocksResponse: // diff --git a/fastconsensus/prepare_test.go b/fastconsensus/prepare_test.go index 394aef65f..d74b23216 100644 --- a/fastconsensus/prepare_test.go +++ b/fastconsensus/prepare_test.go @@ -30,7 +30,7 @@ func TestQueryProposal(t *testing.T) { td.queryProposalTimeout(td.consP) td.shouldPublishQueryProposal(t, td.consP, h) - td.shouldNotPublish(t, td.consP, message.TypeQueryVotes) + td.shouldNotPublish(t, td.consP, message.TypeQueryVote) } func TestQueryVotes(t *testing.T) { diff --git a/types/certificate/vote_certificate.go b/types/certificate/vote_certificate.go index 908c228b2..e435810fe 100644 --- a/types/certificate/vote_certificate.go +++ b/types/certificate/vote_certificate.go @@ -23,7 +23,9 @@ func NewVoteCertificate(height uint32, round int16) *VoteCertificate { } } -func (cert *VoteCertificate) signBytes(blockHash hash.Hash, extraData ...[]byte) []byte { +// 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)...) @@ -37,7 +39,7 @@ func (cert *VoteCertificate) signBytes(blockHash hash.Hash, extraData ...[]byte) func (cert *VoteCertificate) ValidatePrepare(validators []*validator.Validator, blockHash hash.Hash, ) error { - signBytes := cert.signBytes(blockHash, + signBytes := cert.SignBytes(blockHash, util.StringToBytes("PREPARE")) return cert.validate(validators, signBytes) @@ -46,7 +48,7 @@ func (cert *VoteCertificate) ValidatePrepare(validators []*validator.Validator, func (cert *VoteCertificate) ValidateCPPreVote(validators []*validator.Validator, blockHash hash.Hash, cpRound int16, cpValue byte, ) error { - signBytes := cert.signBytes(blockHash, + signBytes := cert.SignBytes(blockHash, util.StringToBytes("PRE-VOTE"), util.Int16ToSlice(cpRound), []byte{cpValue}) @@ -57,7 +59,7 @@ func (cert *VoteCertificate) ValidateCPPreVote(validators []*validator.Validator func (cert *VoteCertificate) ValidateCPMainVote(validators []*validator.Validator, blockHash hash.Hash, cpRound int16, cpValue byte, ) error { - signBytes := cert.signBytes(blockHash, + signBytes := cert.SignBytes(blockHash, util.StringToBytes("MAIN-VOTE"), util.Int16ToSlice(cpRound), []byte{cpValue}) diff --git a/types/certificate/vote_certificate_test.go b/types/certificate/vote_certificate_test.go index d5d1c9010..23faa5c26 100644 --- a/types/certificate/vote_certificate_test.go +++ b/types/certificate/vote_certificate_test.go @@ -1,32 +1,82 @@ package certificate_test -// func TestVoteCertificateSignBytes(t *testing.T) { -// ts := testsuite.NewTestSuite(t) - -// h := ts.RandHash() -// height := ts.RandHeight() -// round := ts.RandRound() -// cpRound := ts.RandRound() -// cpValue := ts.RandInt8(3) - -// cert1 := certificate.NewBlockCertificate(height, round, true) -// cert2 := certificate.NewVoteCertificate(height, round) - -// sb2 := cert2.SignBytes(h) -// sb3 := cert3.SignBytes(h) -// sb4 := cert4.SignBytes(h) -// sb5 := cert5.SignBytes(h) - -// assert.NotEqual(t, sb2, sb3) -// assert.NotEqual(t, sb2, sb4) -// assert.NotEqual(t, sb3, sb4) -// assert.NotEqual(t, sb4, sb5) - -// // BlockCertificate (fast path) has same sign bytes as Prepare certificate -// assert.Equal(t, cert1.SignBytes(h), cert2.SignBytes(h)) - -// assert.Contains(t, string(sb2), "PREPARE") -// assert.Contains(t, string(sb3), "PRE-VOTE") -// assert.Contains(t, string(sb4), "MAIN-VOTE") -// assert.Contains(t, string(sb5), "DECIDED") -// } +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) + }) +} From e4e5db22344b9e851034f9a5a79929a652235762 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Fri, 7 Jun 2024 18:58:05 +0800 Subject: [PATCH 11/11] test: add tests for vote certificate --- types/certificate/vote_certificate_test.go | 117 ++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/types/certificate/vote_certificate_test.go b/types/certificate/vote_certificate_test.go index 23faa5c26..6f314ff4f 100644 --- a/types/certificate/vote_certificate_test.go +++ b/types/certificate/vote_certificate_test.go @@ -45,7 +45,8 @@ func TestVoteCertificateValidatePrepare(t *testing.T) { height := ts.RandHeight() round := ts.RandRound() cert := certificate.NewVoteCertificate(height, round) - signBytes := cert.SignBytes(blockHash, util.StringToBytes("PREPARE")) + signBytes := cert.SignBytes(blockHash, + util.StringToBytes("PREPARE")) committers := ts.RandSlice(6) sigs := []*bls.Signature{} validators := []*validator.Validator{} @@ -80,3 +81,117 @@ func TestVoteCertificateValidatePrepare(t *testing.T) { 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) + }) +}