diff --git a/consensus/consensus.go b/consensus/consensus.go index 05d96a0d5..60b5e3480 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -288,7 +288,8 @@ func (cs *consensus) AddVote(v *vote.Vote) { } if v.Type() == vote.VoteTypeCPPreVote || - v.Type() == vote.VoteTypeCPMainVote { + v.Type() == vote.VoteTypeCPMainVote || + v.Type() == vote.VoteTypeCPDecided { err := cs.changeProposer.checkJust(v) if err != nil { cs.logger.Error("error on adding a cp vote", "vote", v, "error", err) @@ -331,6 +332,14 @@ func (cs *consensus) signAddCPMainVote(hash hash.Hash, cs.signAddVote(v) } +func (cs *consensus) signAddCPDecidedVote(hash hash.Hash, + cpRound int16, cpValue vote.CPValue, just vote.Just, +) { + v := vote.NewCPDecidedVote(hash, cs.height, cs.round, + cpRound, cpValue, just, cs.valKey.Address()) + cs.signAddVote(v) +} + func (cs *consensus) signAddPrepareVote(hash hash.Hash) { v := vote.NewPrepareVote(hash, cs.height, cs.round, cs.valKey.Address()) cs.signAddVote(v) @@ -424,11 +433,9 @@ func (cs *consensus) PickRandomVote(round int16) *vote.Vote { m := cs.log.RoundMessages(round) votes = append(votes, m.AllVotes()...) } else { - // Don't broadcast prepare and precommit votes for previous rounds - vs0 := cs.log.CPPreVoteVoteSet(round) - vs1 := cs.log.CPMainVoteVoteSet(round) - votes = append(votes, vs0.AllVotes()...) - votes = append(votes, vs1.AllVotes()...) + // Only broadcast cp:decided votes + vs := cs.log.CPDecidedVoteVoteSet(round) + votes = append(votes, vs.AllVotes()...) } if len(votes) == 0 { return nil @@ -440,7 +447,7 @@ 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.Debug("changing proposer started", "cpRound", cs.cpRound) + cs.logger.Info("changing proposer started", "cpRound", cs.cpRound) cs.enterNewState(cs.cpPreVoteState) } } diff --git a/consensus/consensus_test.go b/consensus/consensus_test.go index dd55d7bec..423a7a4ab 100644 --- a/consensus/consensus_test.go +++ b/consensus/consensus_test.go @@ -288,6 +288,13 @@ func (td *testData) addCPMainVote(cons *consensus, blockHash hash.Hash, height u 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) @@ -546,11 +553,44 @@ func TestPickRandomVote(t *testing.T) { td.enterNewHeight(td.consP) assert.Nil(t, td.consP.PickRandomVote(0)) + 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)) + + 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)) + + committers := []int32{} + preVoteSigs := []*bls.Signature{} + mainVoteSigs := []*bls.Signature{} + for i, val := range td.consP.validators { + committers = append(committers, val.Number()) + preVoteSigs = append(preVoteSigs, td.valKeys[i].Sign(sbPreVote)) + 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) + // ==== // 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, 0, vote.CPValueOne, &vote.JustInitOne{}, tIndexY) + td.addCPPreVote(td.consP, hash.UndefHash, 1, 0, cpRound+1, vote.CPValueOne, + &vote.JustPreVoteHard{QCert: certPreVote}, tIndexY) + td.addCPMainVote(td.consP, hash.UndefHash, 1, 0, cpRound, vote.CPValueOne, + &vote.JustMainVoteNoConflict{QCert: certPreVote}, tIndexY) + td.addCPDecidedVote(td.consP, hash.UndefHash, 1, 0, cpRound, vote.CPValueOne, + &vote.JustDecided{QCert: certMainVote}, tIndexY) assert.NotNil(t, td.consP.PickRandomVote(0)) @@ -693,10 +733,11 @@ func TestCases(t *testing.T) { round int16 description string }{ - {1694848856237853398, 2, "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"}, - {1694849186681644508, 1, "Conflicting votes, cp-round=1"}, + {1697900665869342730, 1, "Conflicting votes, cp-round=1"}, + {1697887970998950590, 1, "consP & consB: Change Proposer, consX & consY: Commit (2 block announces)"}, } for i, test := range tests { @@ -910,7 +951,7 @@ func checkConsensus(td *testData, height uint32, byzVotes []*vote.Vote) ( } // Check if more than 1/3 of nodes has committed the same block - if len(blockAnnounces) >= 3 { + if len(blockAnnounces) >= 2 { var firstAnnounce *message.BlockAnnounceMessage for _, msg := range blockAnnounces { if firstAnnounce == nil { diff --git a/consensus/cp.go b/consensus/cp.go index f88dd19b7..dab5cc9dc 100644 --- a/consensus/cp.go +++ b/consensus/cp.go @@ -313,12 +313,67 @@ func (cp *changeProposer) checkJustMainVote(v *vote.Vote) error { } } +func (cp *changeProposer) checkJustDecide(v *vote.Vote) error { + err := cp.checkCPValue(v, vote.CPValueZero, vote.CPValueOne) + if err != nil { + return err + } + j, ok := v.CPJust().(*vote.JustDecided) + if !ok { + return invalidJustificationError{ + JustType: j.Type(), + Reason: "invalid just data", + } + } + + 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) + if err != nil { + return invalidJustificationError{ + JustType: j.Type(), + Reason: err.Error(), + } + } + return nil +} + func (cp *changeProposer) checkJust(v *vote.Vote) error { - if v.Type() == vote.VoteTypeCPPreVote { + switch v.Type() { + case vote.VoteTypeCPPreVote: return cp.checkJustPreVote(v) - } else if v.Type() == vote.VoteTypeCPMainVote { + case vote.VoteTypeCPMainVote: return cp.checkJustMainVote(v) - } else { + case vote.VoteTypeCPDecided: + return cp.checkJustDecide(v) + default: panic("unreachable") } } + +func (cp *changeProposer) checkForTermination(v *vote.Vote) { + if v.Type() == vote.VoteTypeCPDecided && + v.Round() == cp.round { + cp.cpDecide(v.CPValue()) + } +} + +func (cp *changeProposer) cpDecide(cpValue vote.CPValue) { + if cpValue == vote.CPValueOne { + cp.round++ + cp.cpDecided = 1 + cp.enterNewState(cp.proposeState) + } else if cpValue == vote.CPValueZero { + roundProposal := cp.log.RoundProposal(cp.round) + if roundProposal == nil { + cp.queryProposal() + } + cp.cpDecided = 0 + cp.enterNewState(cp.prepareState) + } +} diff --git a/consensus/cp_decide.go b/consensus/cp_decide.go index 6e6dcf1b8..a3c00ac08 100644 --- a/consensus/cp_decide.go +++ b/consensus/cp_decide.go @@ -1,6 +1,7 @@ package consensus import ( + "github.com/pactus-project/pactus/crypto/hash" "github.com/pactus-project/pactus/types/vote" ) @@ -13,33 +14,33 @@ func (s *cpDecideState) enter() { } func (s *cpDecideState) decide() { - if s.cpDecided == 1 { - s.round++ - s.enterNewState(s.proposeState) - } else if s.cpDecided == 0 { - roundProposal := s.log.RoundProposal(s.round) - if roundProposal == nil { - s.queryProposal() - } - s.enterNewState(s.prepareState) - } else { - cpMainVotes := s.log.CPMainVoteVoteSet(s.round) - if cpMainVotes.HasTwoThirdOfTotalPower(s.cpRound) { - if cpMainVotes.HasQuorumVotesFor(s.cpRound, vote.CPValueOne) { - // decided for yes, and proceeds to the next round - s.logger.Info("binary agreement decided", "value", 1, "round", s.cpRound) - - s.cpDecided = 1 - } else if cpMainVotes.HasQuorumVotesFor(s.cpRound, vote.CPValueZero) { - // decided for no and proceeds to the next round - s.logger.Info("binary agreement decided", "value", 0, "round", s.cpRound) - - s.cpDecided = 0 - } else { - // conflicting votes - s.logger.Debug("conflicting main votes", "round", s.cpRound) + cpMainVotes := s.log.CPMainVoteVoteSet(s.round) + if cpMainVotes.HasTwoThirdOfTotalPower(s.cpRound) { + if cpMainVotes.HasQuorumVotesFor(s.cpRound, vote.CPValueOne) { + // 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) + 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) { + // 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) + just := &vote.JustDecided{ + QCert: cert, + } + s.signAddCPDecidedVote(*s.cpWeakValidity, s.cpRound, vote.CPValueZero, just) + s.cpDecide(vote.CPValueZero) + } else { + // conflicting votes + s.logger.Debug("conflicting main votes", "round", s.cpRound) s.cpRound++ s.enterNewState(s.cpPreVoteState) } @@ -50,6 +51,8 @@ func (s *cpDecideState) onAddVote(v *vote.Vote) { if v.Type() == vote.VoteTypeCPMainVote { s.decide() } + + s.checkForTermination(v) } func (s *cpDecideState) name() string { diff --git a/consensus/cp_mainvote.go b/consensus/cp_mainvote.go index 10bac5283..0151d38f0 100644 --- a/consensus/cp_mainvote.go +++ b/consensus/cp_mainvote.go @@ -49,6 +49,7 @@ func (s *cpMainVoteState) decide() { Just0: vote0.CPJust(), Just1: vote1.CPJust(), } + s.signAddCPMainVote(*s.cpWeakValidity, s.cpRound, vote.CPValueAbstain, just) s.enterNewState(s.cpDecideState) } @@ -88,6 +89,8 @@ func (s *cpMainVoteState) onAddVote(v *vote.Vote) { if v.Type() == vote.VoteTypeCPPreVote { s.decide() } + + s.checkForTermination(v) } func (s *cpMainVoteState) name() string { diff --git a/consensus/cp_test.go b/consensus/cp_test.go index df7bc9805..3c8b27914 100644 --- a/consensus/cp_test.go +++ b/consensus/cp_test.go @@ -52,14 +52,7 @@ func TestChangeProposerAgreement1(t *testing.T) { 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) - preVote1 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, hash.UndefHash) - td.addCPPreVote(td.consP, hash.UndefHash, h, r, 1, vote.CPValueOne, preVote1.CPJust(), tIndexX) - td.addCPPreVote(td.consP, hash.UndefHash, h, r, 1, vote.CPValueOne, preVote1.CPJust(), tIndexY) - - mainVote1 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPMainVote, hash.UndefHash) - td.addCPMainVote(td.consP, hash.UndefHash, h, r, 1, vote.CPValueOne, mainVote1.CPJust(), tIndexX) - td.addCPMainVote(td.consP, hash.UndefHash, h, r, 1, vote.CPValueOne, mainVote1.CPJust(), tIndexY) - + td.shouldPublishVote(t, td.consP, vote.VoteTypeCPDecided, hash.UndefHash) checkHeightRound(t, td.consP, h, r+1) } @@ -90,14 +83,7 @@ func TestChangeProposerAgreement0(t *testing.T) { 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) - preVote1 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, p.Block().Hash()) - td.addCPPreVote(td.consP, p.Block().Hash(), h, r, 1, vote.CPValueZero, preVote1.CPJust(), tIndexX) - td.addCPPreVote(td.consP, p.Block().Hash(), h, r, 1, vote.CPValueZero, preVote1.CPJust(), tIndexY) - - mainVote1 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPMainVote, p.Block().Hash()) - td.addCPMainVote(td.consP, p.Block().Hash(), h, r, 1, vote.CPValueZero, mainVote1.CPJust(), tIndexX) - td.addCPMainVote(td.consP, p.Block().Hash(), h, r, 1, vote.CPValueZero, mainVote1.CPJust(), tIndexY) - + td.shouldPublishVote(t, td.consP, vote.VoteTypeCPDecided, p.Block().Hash()) td.shouldPublishQueryProposal(t, td.consP, h) td.addPrecommitVote(td.consP, p.Block().Hash(), h, r, tIndexX) td.addPrecommitVote(td.consP, p.Block().Hash(), h, r, tIndexY) @@ -457,3 +443,34 @@ func TestInvalidJustMainVoteConflict(t *testing.T) { }) }) } + +func TestInvalidJustDecided(t *testing.T) { + td := setup(t) + + td.enterNewHeight(td.consX) + h := uint32(1) + r := int16(0) + just := &vote.JustDecided{ + QCert: td.GenerateTestCertificate(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.checkJust(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.CPValueOne, just, td.consB.valKey.Address()) + + err := td.consX.changeProposer.checkJust(v) + assert.ErrorIs(t, err, invalidJustificationError{ + JustType: just.Type(), + Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just.QCert.Committers()), + }) + }) +} diff --git a/consensus/log/log.go b/consensus/log/log.go index 0ab34d2e6..68ed32e14 100644 --- a/consensus/log/log.go +++ b/consensus/log/log.go @@ -42,6 +42,7 @@ func (log *Log) mustGetRoundMessages(round int16) *Messages { 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 } @@ -74,6 +75,11 @@ func (log *Log) CPMainVoteVoteSet(round int16) *voteset.BinaryVoteSet { 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 } diff --git a/consensus/log/messages.go b/consensus/log/messages.go index 8393f22f7..c7aca7fc7 100644 --- a/consensus/log/messages.go +++ b/consensus/log/messages.go @@ -14,6 +14,7 @@ type Messages struct { 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 } @@ -27,6 +28,8 @@ func (m *Messages) addVote(v *vote.Vote) (bool, error) { 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()) @@ -48,6 +51,7 @@ func (m *Messages) AllVotes() []*vote.Vote { 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/consensus/voteset/binary_voteset.go b/consensus/voteset/binary_voteset.go index 08c1590da..e38f81e2b 100644 --- a/consensus/voteset/binary_voteset.go +++ b/consensus/voteset/binary_voteset.go @@ -40,14 +40,21 @@ type BinaryVoteSet struct { func NewCPPreVoteVoteSet(round int16, totalPower int64, validators map[crypto.Address]*validator.Validator, ) *BinaryVoteSet { - voteSet := newVoteSet(vote.VoteTypeCPPreVote, round, totalPower, validators) + voteSet := newVoteSet(round, totalPower, validators) return newBinaryVoteSet(voteSet) } func NewCPMainVoteVoteSet(round int16, totalPower int64, validators map[crypto.Address]*validator.Validator, ) *BinaryVoteSet { - voteSet := newVoteSet(vote.VoteTypeCPMainVote, round, totalPower, validators) + 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) } diff --git a/consensus/voteset/block_voteset.go b/consensus/voteset/block_voteset.go index 73bb1a34a..3dfdded5a 100644 --- a/consensus/voteset/block_voteset.go +++ b/consensus/voteset/block_voteset.go @@ -18,14 +18,14 @@ type BlockVoteSet struct { func NewPrepareVoteSet(round int16, totalPower int64, validators map[crypto.Address]*validator.Validator, ) *BlockVoteSet { - voteSet := newVoteSet(vote.VoteTypePrepare, round, totalPower, validators) + voteSet := newVoteSet(round, totalPower, validators) return newBlockVoteSet(voteSet) } func NewPrecommitVoteSet(round int16, totalPower int64, validators map[crypto.Address]*validator.Validator, ) *BlockVoteSet { - voteSet := newVoteSet(vote.VoteTypePrecommit, round, totalPower, validators) + voteSet := newVoteSet(round, totalPower, validators) return newBlockVoteSet(voteSet) } diff --git a/consensus/voteset/voteset.go b/consensus/voteset/voteset.go index cd9a7df5c..c1135a31b 100644 --- a/consensus/voteset/voteset.go +++ b/consensus/voteset/voteset.go @@ -1,8 +1,6 @@ package voteset import ( - "fmt" - "github.com/pactus-project/pactus/crypto" "github.com/pactus-project/pactus/types/validator" "github.com/pactus-project/pactus/types/vote" @@ -10,27 +8,21 @@ import ( ) type voteSet struct { - voteType vote.Type round int16 validators map[crypto.Address]*validator.Validator totalPower int64 } -func newVoteSet(voteType vote.Type, round int16, totalPower int64, +func newVoteSet(round int16, totalPower int64, validators map[crypto.Address]*validator.Validator, ) *voteSet { return &voteSet{ round: round, - voteType: voteType, validators: validators, totalPower: totalPower, } } -func (vs *voteSet) Type() vote.Type { - return vs.voteType -} - // Round returns the round number for the VoteSet. func (vs *voteSet) Round() int16 { return vs.round @@ -61,7 +53,3 @@ func (vs *voteSet) isTwoThirdOfTotalPower(power int64) bool { func (vs *voteSet) isOneThirdOfTotalPower(power int64) bool { return power > (vs.totalPower * 1 / 3) } - -func (vs *voteSet) String() string { - return fmt.Sprintf("{%v/%s TOTAL:%v}", vs.round, vs.voteType, vs.totalPower) -} diff --git a/consensus/voteset/voteset_test.go b/consensus/voteset/voteset_test.go index d3180573e..5dc7b7e7a 100644 --- a/consensus/voteset/voteset_test.go +++ b/consensus/voteset/voteset_test.go @@ -379,3 +379,23 @@ func TestOneThirdPower(t *testing.T) { 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) + + hash := ts.RandHash() + height := ts.RandHeight() + round := ts.RandRound() + just := &vote.JustInitOne{} + vs := NewCPDecidedVoteVoteSet(round, totalPower, valsMap) + + v1 := vote.NewCPDecidedVote(hash, height, round, 0, vote.CPValueOne, 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)) +} diff --git a/network/utils.go b/network/utils.go index 6b6ee54eb..ff8023dbe 100644 --- a/network/utils.go +++ b/network/utils.go @@ -29,7 +29,7 @@ func ConnectAsync(ctx context.Context, h lp2phost.Host, addrInfo lp2ppeer.AddrIn } } else { if logger != nil { - logger.Debug("connected", "addr", addrInfo.Addrs, "err", err) + logger.Debug("connected", "addr", addrInfo.Addrs) } } }() diff --git a/types/vote/cp_just.go b/types/vote/cp_just.go index 508e0b31b..58e71192d 100644 --- a/types/vote/cp_just.go +++ b/types/vote/cp_just.go @@ -14,6 +14,7 @@ const ( JustTypePreVoteHard = JustType(4) JustTypeMainVoteConflict = JustType(5) JustTypeMainVoteNoConflict = JustType(6) + JustTypeDecided = JustType(7) ) func (t JustType) String() string { @@ -30,6 +31,8 @@ func (t JustType) String() string { return "JustMainVoteConflict" case JustTypeMainVoteNoConflict: return "JustMainVoteNoConflict" + case JustTypeDecided: + return "JustDecided" default: return "Unknown" @@ -55,6 +58,8 @@ func makeJust(t JustType) (Just, error) { return &JustMainVoteConflict{}, nil case JustTypeMainVoteNoConflict: return &JustMainVoteNoConflict{}, nil + case JustTypeDecided: + return &JustDecided{}, nil default: return nil, errors.Errorf(errors.ErrInvalidVote, "invalid justification") @@ -83,6 +88,10 @@ type JustMainVoteNoConflict struct { QCert *certificate.Certificate `cbor:"1,keyasint"` } +type JustDecided struct { + QCert *certificate.Certificate `cbor:"1,keyasint"` +} + func (j *JustInitZero) Type() JustType { return JustTypeInitZero } @@ -107,6 +116,10 @@ func (j *JustMainVoteNoConflict) Type() JustType { return JustTypeMainVoteNoConflict } +func (j *JustDecided) Type() JustType { + return JustTypeDecided +} + func (j *JustInitZero) BasicCheck() error { return j.QCert.BasicCheck() } @@ -133,3 +146,7 @@ func (j *JustMainVoteConflict) BasicCheck() error { func (j *JustMainVoteNoConflict) BasicCheck() error { return j.QCert.BasicCheck() } + +func (j *JustDecided) BasicCheck() error { + return j.QCert.BasicCheck() +} diff --git a/types/vote/vote.go b/types/vote/vote.go index 7b939dd9d..1c948c2fe 100644 --- a/types/vote/vote.go +++ b/types/vote/vote.go @@ -65,6 +65,20 @@ func NewCPMainVote(blockHash hash.Hash, height uint32, round int16, return v } +// NewCPDecidedVote creates a new cp:Decided with the specified parameters. +func NewCPDecidedVote(blockHash hash.Hash, height uint32, round int16, + cpRound int16, cpValue CPValue, just Just, signer crypto.Address, +) *Vote { + v := newVote(VoteTypeCPDecided, blockHash, height, round, signer) + v.data.CPVote = &cpVote{ + Round: cpRound, + Value: cpValue, + Just: just, + } + + return v +} + // newVote creates a new vote with the specified parameters. func newVote(voteType Type, blockHash hash.Hash, height uint32, round int16, signer crypto.Address, @@ -90,7 +104,7 @@ func (v *Vote) SignBytes() []byte { case VoteTypePrepare: sb = append(sb, util.StringToBytes(t.String())...) - case VoteTypeCPPreVote, VoteTypeCPMainVote: + case VoteTypeCPPreVote, VoteTypeCPMainVote, VoteTypeCPDecided: sb = append(sb, util.StringToBytes(t.String())...) sb = append(sb, util.Int16ToSlice(v.data.CPVote.Round)...) sb = append(sb, byte(v.data.CPVote.Value)) @@ -196,7 +210,8 @@ func (v *Vote) BasicCheck() error { } } if v.data.Type == VoteTypeCPPreVote || - v.data.Type == VoteTypeCPMainVote { + v.data.Type == VoteTypeCPMainVote || + v.data.Type == VoteTypeCPDecided { if v.data.CPVote == nil { return errors.Errorf(errors.ErrInvalidVote, "should have cp data") } @@ -209,7 +224,7 @@ func (v *Vote) BasicCheck() error { } } if v.Signature() == nil { - return errors.Errorf(errors.ErrInvalidVote, "no signature") + return errors.Errorf(errors.ErrInvalidSignature, "no signature") } return nil } @@ -224,7 +239,7 @@ func (v *Vote) String() string { v.BlockHash().ShortString(), v.Signer().ShortString(), ) - case VoteTypeCPPreVote, VoteTypeCPMainVote: + case VoteTypeCPPreVote, VoteTypeCPMainVote, VoteTypeCPDecided: return fmt.Sprintf("{%d/%d/%s/%d/%d ⌘ %v 👤 %s}", v.Height(), v.Round(), diff --git a/types/vote/vote_test.go b/types/vote/vote_test.go index 6b50435ad..6572d09b9 100644 --- a/types/vote/vote_test.go +++ b/types/vote/vote_test.go @@ -5,7 +5,6 @@ import ( "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/vote" @@ -173,6 +172,27 @@ func TestVoteMarshaling(t *testing.T) { "JustMainVoteNoConflict", "00000000000000000000000000000000000000000000000000000000000000003200000001004d41494e2d564f5445010001", }, + { + "A7" + // map(7) + "0105" + // Type: 4 (cp:decided) + "021832" + // Height: 50 + "0301" + // Round: 1 + "0458200000000000000000000000000000000000000000000000000000000000000000" + // Block Hash + "055501AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + // Signer + "06" + // CP_vote + "A4" + // map(4) + "0101" + // CP_Round: 1 + "0201" + // CP_Value: 1 + "0307" + // Just type: 7 + "045840" + // Just: JustTypeDecided + "A1" + // map(1) + "01583C" + // Certificate (60 bytes) + "32000000010004010203040094D25422904AC1D130AC981374AA4424F988" + // Certificate Data + "61E99131078EFEFD62FC52CF072B0C08BB04E4E6496BA48DE4F3D3309AAB" + + "07f6", // Signature -> Null + "JustDecided", + "000000000000000000000000000000000000000000000000000000000000000032000000010044454349444544010001", + }, } ts := testsuite.NewTestSuite(t) @@ -245,15 +265,15 @@ func TestCPPreVote(t *testing.T) { t.Run("Invalid value", func(t *testing.T) { v := vote.NewCPPreVote(hash.UndefHash, h, r, - 1, vote.CPValueAbstain, just, ts.RandAccAddress()) + 1, 3, just, ts.RandAccAddress()) err := v.BasicCheck() assert.Equal(t, errors.Code(err), errors.ErrInvalidVote) }) t.Run("Ok", func(t *testing.T) { - v := vote.NewCPPreVote(hash.UndefHash, h, r, 1, - vote.CPValueZero, just, ts.RandAccAddress()) + v := vote.NewCPPreVote(hash.UndefHash, h, r, + 1, vote.CPValueZero, just, ts.RandAccAddress()) v.SetSignature(ts.RandBLSSignature()) err := v.BasicCheck() @@ -291,8 +311,8 @@ func TestCPMainVote(t *testing.T) { }) t.Run("Invalid value", func(t *testing.T) { - v := vote.NewCPMainVote(hash.UndefHash, h, r, 1, - 4, just, ts.RandAccAddress()) + v := vote.NewCPMainVote(hash.UndefHash, h, r, + 1, 3, just, ts.RandAccAddress()) err := v.BasicCheck() assert.Equal(t, errors.Code(err), errors.ErrInvalidVote) @@ -311,6 +331,53 @@ func TestCPMainVote(t *testing.T) { }) } +func TestCPDecided(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + h := ts.RandHeight() + r := ts.RandRound() + just := &vote.JustInitOne{} + + t.Run("Invalid round", func(t *testing.T) { + v := vote.NewCPDecidedVote(hash.UndefHash, h, r, + -1, vote.CPValueZero, just, ts.RandAccAddress()) + + err := v.BasicCheck() + assert.Equal(t, errors.Code(err), errors.ErrInvalidRound) + }) + + t.Run("No CP data", func(t *testing.T) { + data, _ := hex.DecodeString("A701050218320301045820BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" + + "055501AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA06f607f6") + v := new(vote.Vote) + err := v.UnmarshalCBOR(data) + assert.NoError(t, err) + v.SetSignature(ts.RandBLSSignature()) + + assert.Error(t, v.BasicCheck()) + }) + + t.Run("Invalid value", func(t *testing.T) { + v := vote.NewCPDecidedVote(hash.UndefHash, h, r, + 1, 3, just, ts.RandAccAddress()) + + err := v.BasicCheck() + assert.Equal(t, errors.Code(err), errors.ErrInvalidVote) + }) + + t.Run("Ok", func(t *testing.T) { + v := vote.NewCPDecidedVote(hash.UndefHash, h, r, + 1, vote.CPValueAbstain, 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.CPValueAbstain) + assert.NotNil(t, v.CPJust()) + }) +} + func TestBasicCheck(t *testing.T) { ts := testsuite.NewTestSuite(t) @@ -343,7 +410,7 @@ func TestBasicCheck(t *testing.T) { v := vote.NewPrepareVote(ts.RandHash(), 100, 0, ts.RandAccAddress()) err := v.BasicCheck() - assert.Equal(t, errors.Code(err), errors.ErrInvalidVote) + assert.Equal(t, errors.Code(err), errors.ErrInvalidSignature) }) t.Run("Has CP data", func(t *testing.T) { diff --git a/types/vote/vote_type.go b/types/vote/vote_type.go index e00632529..1760101a0 100644 --- a/types/vote/vote_type.go +++ b/types/vote/vote_type.go @@ -7,12 +7,13 @@ const ( VoteTypePrecommit = Type(2) // precommit vote VoteTypeCPPreVote = Type(3) // change-proposer:pre-vote VoteTypeCPMainVote = Type(4) // change-proposer:main-vote + VoteTypeCPDecided = Type(5) // change-proposer:decided ) func (t Type) IsValid() bool { switch t { case VoteTypePrepare, VoteTypePrecommit, - VoteTypeCPPreVote, VoteTypeCPMainVote: + VoteTypeCPPreVote, VoteTypeCPMainVote, VoteTypeCPDecided: return true } @@ -29,6 +30,8 @@ func (t Type) String() string { return "PRE-VOTE" case VoteTypeCPMainVote: return "MAIN-VOTE" + case VoteTypeCPDecided: + return "DECIDED" default: return ("invalid vote type") }