From ede34eb29321690b9c09a41e2209379dcde313af Mon Sep 17 00:00:00 2001 From: b00f Date: Wed, 26 Jun 2024 23:30:35 +0800 Subject: [PATCH] chore: remove fast consensus package (#1370) Co-authored-by: Javad Rajabzadeh --- fastconsensus/commit.go | 46 - fastconsensus/config.go | 45 - fastconsensus/config_test.go | 40 - fastconsensus/consensus.go | 544 ------------ fastconsensus/consensus_test.go | 1068 ----------------------- fastconsensus/cp.go | 340 -------- fastconsensus/cp_decide.go | 59 -- fastconsensus/cp_mainvote.go | 103 --- fastconsensus/cp_prevote.go | 98 --- fastconsensus/cp_test.go | 452 ---------- fastconsensus/errors.go | 24 - fastconsensus/height.go | 60 -- fastconsensus/height_test.go | 63 -- fastconsensus/interface.go | 47 - fastconsensus/log/log.go | 126 --- fastconsensus/log/log_test.go | 105 --- fastconsensus/log/messages.go | 58 -- fastconsensus/manager.go | 220 ----- fastconsensus/manager_test.go | 195 ----- fastconsensus/mediator.go | 53 -- fastconsensus/mock.go | 148 ---- fastconsensus/precommit.go | 76 -- fastconsensus/precommit_test.go | 37 - 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 189468 -> 0 bytes fastconsensus/spec/Pactus.tla | 550 ------------ fastconsensus/spec/README.md | 24 - fastconsensus/state.go | 15 - fastconsensus/ticker.go | 41 - fastconsensus/voteset/binary_voteset.go | 185 ---- fastconsensus/voteset/block_voteset.go | 140 --- fastconsensus/voteset/vote_box.go | 25 - fastconsensus/voteset/vote_box_test.go | 28 - fastconsensus/voteset/voteset.go | 46 - fastconsensus/voteset/voteset_test.go | 439 ---------- 40 files changed, 5896 deletions(-) delete mode 100644 fastconsensus/commit.go delete mode 100644 fastconsensus/config.go delete mode 100644 fastconsensus/config_test.go delete mode 100644 fastconsensus/consensus.go delete mode 100644 fastconsensus/consensus_test.go delete mode 100644 fastconsensus/cp.go delete mode 100644 fastconsensus/cp_decide.go delete mode 100644 fastconsensus/cp_mainvote.go delete mode 100644 fastconsensus/cp_prevote.go delete mode 100644 fastconsensus/cp_test.go delete mode 100644 fastconsensus/errors.go delete mode 100644 fastconsensus/height.go delete mode 100644 fastconsensus/height_test.go delete mode 100644 fastconsensus/interface.go delete mode 100644 fastconsensus/log/log.go delete mode 100644 fastconsensus/log/log_test.go delete mode 100644 fastconsensus/log/messages.go delete mode 100644 fastconsensus/manager.go delete mode 100644 fastconsensus/manager_test.go delete mode 100644 fastconsensus/mediator.go delete mode 100644 fastconsensus/mock.go delete mode 100644 fastconsensus/precommit.go delete mode 100644 fastconsensus/precommit_test.go delete mode 100644 fastconsensus/prepare.go delete mode 100644 fastconsensus/prepare_test.go delete mode 100644 fastconsensus/propose.go delete mode 100644 fastconsensus/propose_test.go delete mode 100644 fastconsensus/spec/.gitignore delete mode 100644 fastconsensus/spec/Pactus.cfg delete mode 100644 fastconsensus/spec/Pactus.pdf delete mode 100644 fastconsensus/spec/Pactus.tla delete mode 100644 fastconsensus/spec/README.md delete mode 100644 fastconsensus/state.go delete mode 100644 fastconsensus/ticker.go delete mode 100644 fastconsensus/voteset/binary_voteset.go delete mode 100644 fastconsensus/voteset/block_voteset.go delete mode 100644 fastconsensus/voteset/vote_box.go delete mode 100644 fastconsensus/voteset/vote_box_test.go delete mode 100644 fastconsensus/voteset/voteset.go delete mode 100644 fastconsensus/voteset/voteset_test.go diff --git a/fastconsensus/commit.go b/fastconsensus/commit.go deleted file mode 100644 index 65088fac2..000000000 --- a/fastconsensus/commit.go +++ /dev/null @@ -1,46 +0,0 @@ -package fastconsensus - -import ( - "github.com/pactus-project/pactus/types/proposal" - "github.com/pactus-project/pactus/types/vote" -) - -type commitState struct { - *consensus -} - -func (s *commitState) enter() { - s.decide() -} - -func (s *commitState) decide() { - roundProposal := s.log.RoundProposal(s.round) - certBlock := roundProposal.Block() - err := s.bcState.CommitBlock(certBlock, s.blockCert) - if err != nil { - s.logger.Error("committing block failed", "block", certBlock, "error", err) - } else { - s.logger.Info("block committed, schedule new height", "hash", certBlock.Hash()) - - // Now we can announce the committed block and certificate - s.announceNewBlock(certBlock, s.blockCert) - } - - s.enterNewState(s.newHeightState) -} - -func (*commitState) onAddVote(_ *vote.Vote) { - panic("Unreachable") -} - -func (*commitState) onSetProposal(_ *proposal.Proposal) { - panic("Unreachable") -} - -func (*commitState) onTimeout(_ *ticker) { - panic("Unreachable") -} - -func (*commitState) name() string { - return "commit" -} diff --git a/fastconsensus/config.go b/fastconsensus/config.go deleted file mode 100644 index 19fbfebd2..000000000 --- a/fastconsensus/config.go +++ /dev/null @@ -1,45 +0,0 @@ -package fastconsensus - -import "time" - -type Config struct { - ChangeProposerTimeout time.Duration `toml:"-"` - ChangeProposerDelta time.Duration `toml:"-"` - QueryVoteTimeout time.Duration `toml:"-"` - MinimumAvailabilityScore float64 `toml:"-"` -} - -func DefaultConfig() *Config { - return &Config{ - ChangeProposerTimeout: 5 * time.Second, - ChangeProposerDelta: 5 * time.Second, - QueryVoteTimeout: 5 * time.Second, - MinimumAvailabilityScore: 0.666667, - } -} - -// BasicCheck performs basic checks on the configuration. -func (conf *Config) BasicCheck() error { - if conf.ChangeProposerTimeout <= 0 { - return ConfigError{ - Reason: "change proposer timeout must be greater than zero", - } - } - if conf.ChangeProposerDelta <= 0 { - return ConfigError{ - Reason: "change proposer delta must be greater than zero", - } - } - if conf.MinimumAvailabilityScore < 0 || conf.MinimumAvailabilityScore > 1 { - return ConfigError{ - Reason: "minimum availability score can't be negative or more than 1", - } - } - - return nil -} - -func (conf *Config) CalculateChangeProposerTimeout(round int16) time.Duration { - return conf.ChangeProposerTimeout + - conf.ChangeProposerDelta*time.Duration(round) -} diff --git a/fastconsensus/config_test.go b/fastconsensus/config_test.go deleted file mode 100644 index 356238ccc..000000000 --- a/fastconsensus/config_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package fastconsensus - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestDefaultConfigCheck(t *testing.T) { - c1 := DefaultConfig() - c2 := DefaultConfig() - c3 := DefaultConfig() - c4 := DefaultConfig() - c5 := DefaultConfig() - assert.NoError(t, c1.BasicCheck()) - - c2.ChangeProposerDelta = 0 * time.Second - assert.ErrorIs(t, c2.BasicCheck(), ConfigError{Reason: "change proposer delta must be greater than zero"}) - - c3.ChangeProposerTimeout = 0 * time.Second - assert.ErrorIs(t, c3.BasicCheck(), ConfigError{Reason: "change proposer timeout must be greater than zero"}) - - c4.ChangeProposerTimeout = -1 * time.Second - assert.ErrorIs(t, c4.BasicCheck(), ConfigError{Reason: "change proposer timeout must be greater than zero"}) - - c5.MinimumAvailabilityScore = 1.5 - assert.ErrorIs(t, c5.BasicCheck(), ConfigError{Reason: "minimum availability score can't be negative or more than 1"}) - - c5.MinimumAvailabilityScore = -0.8 - assert.ErrorIs(t, c5.BasicCheck(), ConfigError{Reason: "minimum availability score can't be negative or more than 1"}) -} - -func TestCalculateChangeProposerTimeout(t *testing.T) { - c := DefaultConfig() - - assert.Equal(t, c.CalculateChangeProposerTimeout(0), c.ChangeProposerTimeout) - assert.Equal(t, c.CalculateChangeProposerTimeout(1), c.ChangeProposerTimeout+c.ChangeProposerDelta) - assert.Equal(t, c.CalculateChangeProposerTimeout(4), c.ChangeProposerTimeout+(4*c.ChangeProposerDelta)) -} diff --git a/fastconsensus/consensus.go b/fastconsensus/consensus.go deleted file mode 100644 index 2b8647ca7..000000000 --- a/fastconsensus/consensus.go +++ /dev/null @@ -1,544 +0,0 @@ -package fastconsensus - -import ( - "fmt" - "sync" - "time" - - "github.com/pactus-project/pactus/crypto" - "github.com/pactus-project/pactus/crypto/bls" - "github.com/pactus-project/pactus/crypto/hash" - "github.com/pactus-project/pactus/fastconsensus/log" - "github.com/pactus-project/pactus/state" - "github.com/pactus-project/pactus/sync/bundle/message" - "github.com/pactus-project/pactus/types/block" - "github.com/pactus-project/pactus/types/certificate" - "github.com/pactus-project/pactus/types/proposal" - "github.com/pactus-project/pactus/types/validator" - "github.com/pactus-project/pactus/types/vote" - "github.com/pactus-project/pactus/util" - "github.com/pactus-project/pactus/util/logger" -) - -type broadcaster func(crypto.Address, message.Message) - -type consensus struct { - lk sync.RWMutex - - config *Config - logger *logger.SubLogger - log *log.Log - validators []*validator.Validator - cpWeakValidity *hash.Hash // The change proposer's weak validity that is a prepared block hash - cpDecided int - height uint32 - round int16 - cpRound int16 - valKey *bls.ValidatorKey - rewardAddr crypto.Address - bcState state.Facade // Blockchain state - blockCert *certificate.BlockCertificate - changeProposer *changeProposer - newHeightState consState - proposeState consState - prepareState consState - precommitState consState - commitState consState - cpPreVoteState consState - cpMainVoteState consState - cpDecideState consState - currentState consState - broadcaster broadcaster - mediator mediator - active bool -} - -func NewConsensus( - conf *Config, - bcState state.Facade, - valKey *bls.ValidatorKey, - rewardAddr crypto.Address, - broadcastCh chan message.Message, - mediator mediator, -) Consensus { - broadcaster := func(_ crypto.Address, msg message.Message) { - broadcastCh <- msg - } - - return makeConsensus(conf, bcState, - valKey, rewardAddr, broadcaster, mediator) -} - -func makeConsensus( - conf *Config, - bcState state.Facade, - valKey *bls.ValidatorKey, - rewardAddr crypto.Address, - broadcaster broadcaster, - mediator mediator, -) *consensus { - cs := &consensus{ - config: conf, - bcState: bcState, - broadcaster: broadcaster, - valKey: valKey, - } - - // Update height later, See enterNewHeight. - cs.log = log.NewLog() - cs.logger = logger.NewSubLogger("_consensus", cs) - cs.rewardAddr = rewardAddr - - cs.changeProposer = &changeProposer{cs} - cs.newHeightState = &newHeightState{cs} - cs.proposeState = &proposeState{cs} - cs.prepareState = &prepareState{cs, false} - cs.precommitState = &precommitState{cs, false} - cs.commitState = &commitState{cs} - cs.cpPreVoteState = &cpPreVoteState{cs.changeProposer} - cs.cpMainVoteState = &cpMainVoteState{cs.changeProposer} - cs.cpDecideState = &cpDecideState{cs.changeProposer} - cs.currentState = cs.newHeightState - cs.mediator = mediator - - cs.height = 0 - cs.round = 0 - cs.active = false - cs.mediator = mediator - - mediator.Register(cs) - - logger.Info("consensus instance created", - "validator address", valKey.Address().String(), - "reward address", rewardAddr.String()) - - return cs -} - -func (cs *consensus) Start() { - cs.lk.Lock() - defer cs.lk.Unlock() - - cs.moveToNewHeight() -} - -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) 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) 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 - } - - if err := p.BasicCheck(); err != nil { - cs.logger.Warn("invalid proposal", "proposal", p, "error", err) - - 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) 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.Round() < cs.round { - cs.logger.Trace("vote for expired round", "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) - - // If there is a proper and justified "Decide" vote for a subsequent round, move consensus to that round. - // This especially helps validators to catch up with the network when they restart their node. - if v.Type() == vote.VoteTypeCPDecided { - if v.Round() > cs.round { - cs.changeProposer.cpDecide(v.Round(), v.CPValue()) - } - } - } -} - -func (cs *consensus) proposer(round int16) *validator.Validator { - return cs.bcState.Proposer(round) -} - -func (cs *consensus) IsProposer() bool { - cs.lk.RLock() - defer cs.lk.RUnlock() - - return cs.isProposer() -} - -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.round, cs.valKey.Address())) -} - -// queryVotes is an anti-entropy mechanism to retrieve missed votes -// when a validator falls behind the network. -// However, invoking this method might result in unnecessary bandwidth usage. -func (cs *consensus) queryVotes() { - cs.broadcaster(cs.valKey.Address(), - message.NewQueryVotesMessage(cs.height, cs.round, cs.valKey.Address())) -} - -func (cs *consensus) broadcastProposal(p *proposal.Proposal) { - go cs.mediator.OnPublishProposal(cs, p) - cs.broadcaster(cs.valKey.Address(), - message.NewProposalMessage(p)) -} - -func (cs *consensus) broadcastVote(v *vote.Vote) { - go cs.mediator.OnPublishVote(cs, v) - cs.broadcaster(cs.valKey.Address(), - message.NewVoteMessage(v)) -} - -func (cs *consensus) announceNewBlock(blk *block.Block, cert *certificate.BlockCertificate) { - go cs.mediator.OnBlockAnnounce(cs) - cs.broadcaster(cs.valKey.Address(), - message.NewBlockAnnounceMessage(blk, cert)) -} - -func (cs *consensus) makeBlockCertificate(votes map[crypto.Address]*vote.Vote, fastPath bool, -) *certificate.BlockCertificate { - cert := certificate.NewBlockCertificate(cs.height, cs.round, fastPath) - cert.SetSignature(cs.signersInfo(votes)) - - return cert -} - -func (cs *consensus) makeVoteCertificate(votes map[crypto.Address]*vote.Vote, -) *certificate.VoteCertificate { - cert := certificate.NewVoteCertificate(cs.height, cs.round) - cert.SetSignature(cs.signersInfo(votes)) - - return cert -} - -// signersInfo processes a map of votes from validators and provides these information: -// - A list of all validators' numbers eligible to vote in this step. -// - A list of absentee validators' numbers who did not vote in this step. -// - An aggregated signature generated from the signatures of participating validators. -func (cs *consensus) signersInfo(votes map[crypto.Address]*vote.Vote) ([]int32, []int32, *bls.Signature) { - vals := cs.validators - committers := make([]int32, len(vals)) - absentees := make([]int32, 0) - sigs := make([]*bls.Signature, 0) - - for i, val := range vals { - vte := votes[val.Address()] - if vte != nil { - sigs = append(sigs, vte.Signature()) - } else { - absentees = append(absentees, val.Number()) - } - - committers[i] = val.Number() - } - - aggSig := bls.SignatureAggregate(sigs...) - - return committers, absentees, aggSig -} - -// IsActive checks if the consensus is in an active state and participating in the consensus algorithm. -func (cs *consensus) IsActive() bool { - cs.lk.RLock() - defer cs.lk.RUnlock() - - return cs.active -} - -// TODO: Improve the performance? -func (cs *consensus) PickRandomVote(round int16) *vote.Vote { - cs.lk.RLock() - defer cs.lk.RUnlock() - - votes := []*vote.Vote{} - switch { - case round < cs.round: - // Past round: Only broadcast cp:decided votes - vs := cs.log.CPDecidedVoteSet(round) - votes = append(votes, vs.AllVotes()...) - - case round == cs.round: - // Current round - m := cs.log.RoundMessages(round) - votes = append(votes, m.AllVotes()...) - - case round > cs.round: - // Future round - } - - 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, "proposer", cs.proposer(cs.round).Address()) - 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 deleted file mode 100644 index 969356731..000000000 --- a/fastconsensus/consensus_test.go +++ /dev/null @@ -1,1068 +0,0 @@ -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 - QueryVoteTimeout: 1 * time.Hour, // Disabling timers - } -} - -func setup(t *testing.T) *testData { - t.Helper() - - 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 and - // avoid double entries for new heights in some tests. - getTime := util.RoundNow(params.BlockIntervalInSecond). - Add(time.Duration(params.BlockIntervalInSecond) * time.Second) - genDoc := genesis.MakeGenesis(getTime, accs, vals, params) - - consMessages := make([]consMessage, 0) - td := &testData{ - TestSuite: ts, - valKeys: valKeys, - txPool: txPool, - genDoc: genDoc, - consMessages: consMessages, - } - broadcasterFunc := func(sender crypto.Address, msg message.Message) { - fmt.Printf("received a message %s: %s\n", msg.Type(), msg.String()) - td.consMessages = append(td.consMessages, consMessage{ - sender: sender, - message: msg, - }) - } - - instances := make([]*consensus, len(valKeys)) - for i, valKey := range valKeys { - bcState, err := state.LoadOrNewState(genDoc, []*bls.ValidatorKey{valKey}, - store.MockingStore(ts), txPool, nil) - require.NoError(t, err) - - instances[i] = makeConsensus(testConfig(), bcState, valKey, - valKey.PublicKey().AccountAddress(), broadcasterFunc, newConcreteMediator()) - } - - td.consX = instances[tIndexX] - td.consY = instances[tIndexY] - td.consB = instances[tIndexB] - td.consP = instances[tIndexP] - td.consM = instances[tIndexM] - td.consN = instances[tIndexN] - - // ------------------------------- - // Better logging during testing - overrideLogger := func(cons *consensus, name string) { - cons.logger = logger.NewSubLogger("_consensus", - testsuite.NewOverrideStringer(fmt.Sprintf("%s - %s: ", name, t.Name()), cons)) - } - - overrideLogger(td.consX, "consX") - overrideLogger(td.consY, "consY") - overrideLogger(td.consB, "consB") - overrideLogger(td.consP, "consP") - overrideLogger(td.consM, "consM") - overrideLogger(td.consN, "consN") - // ------------------------------- - - logger.Info("setup finished, start running the test", "name", t.Name()) - - return td -} - -func (td *testData) shouldNotPublish(t *testing.T, cons *consensus, msgType message.Type) { - t.Helper() - - for _, consMsg := range td.consMessages { - if consMsg.sender == cons.valKey.Address() && - consMsg.message.Type() == msgType { - require.Error(t, fmt.Errorf("should not publish %s", msgType)) - } - } -} - -func (td *testData) shouldPublishBlockAnnounce(t *testing.T, cons *consensus, h hash.Hash) { - t.Helper() - - for _, consMsg := range td.consMessages { - if consMsg.sender == cons.valKey.Address() && - consMsg.message.Type() == message.TypeBlockAnnounce { - m := consMsg.message.(*message.BlockAnnounceMessage) - assert.Equal(t, m.Block.Hash(), h) - - return - } - } - require.NoError(t, fmt.Errorf("Block announce message not published")) -} - -func (td *testData) shouldPublishProposal(t *testing.T, cons *consensus, - height uint32, round int16, -) *proposal.Proposal { - t.Helper() - - for _, consMsg := range td.consMessages { - if consMsg.sender == cons.valKey.Address() && - consMsg.message.Type() == message.TypeProposal { - m := consMsg.message.(*message.ProposalMessage) - require.Equal(t, m.Proposal.Height(), height) - require.Equal(t, m.Proposal.Round(), round) - - return m.Proposal - } - } - require.NoError(t, fmt.Errorf("Proposal message not published")) - - return nil -} - -func (td *testData) shouldPublishQueryProposal(t *testing.T, cons *consensus, height uint32, round int16) { - 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.Round, round) - assert.Equal(t, m.Querier, cons.valKey.Address()) - - return - } - require.NoError(t, fmt.Errorf("Query proposal message not published")) -} - -func (td *testData) shouldPublishQueryVote(t *testing.T, cons *consensus, height uint32, round int16) { - t.Helper() - - for _, consMsg := range td.consMessages { - if consMsg.sender != cons.valKey.Address() || - consMsg.message.Type() != message.TypeQueryVote { - continue - } - - m := consMsg.message.(*message.QueryVotesMessage) - assert.Equal(t, m.Height, height) - assert.Equal(t, m.Round, round) - assert.Equal(t, m.Querier, cons.valKey.Address()) - - return - } - require.NoError(t, fmt.Errorf("Query proposal message not published")) -} - -func (td *testData) shouldPublishVote(t *testing.T, cons *consensus, voteType vote.Type, h hash.Hash) *vote.Vote { - t.Helper() - - for i := len(td.consMessages) - 1; i >= 0; i-- { - consMsg := td.consMessages[i] - if consMsg.sender == cons.valKey.Address() && - consMsg.message.Type() == message.TypeVote { - m := consMsg.message.(*message.VoteMessage) - if m.Vote.Type() == voteType && - m.Vote.BlockHash() == h { - return m.Vote - } - } - } - require.NoError(t, fmt.Errorf("Vote message not published")) - - return nil -} - -func (*testData) checkHeightRound(t *testing.T, cons *consensus, height uint32, round int16) { - t.Helper() - - h, r := cons.HeightRound() - assert.Equal(t, h, height) - assert.Equal(t, r, round) -} - -func (td *testData) addPrepareVote(cons *consensus, blockHash hash.Hash, height uint32, round int16, - valID int, -) *vote.Vote { - v := vote.NewPrepareVote(blockHash, height, round, td.valKeys[valID].Address()) - - return td.addVote(cons, v, valID) -} - -func (td *testData) addPrecommitVote(cons *consensus, blockHash hash.Hash, height uint32, round int16, - valID int, -) *vote.Vote { - v := vote.NewPrecommitVote(blockHash, height, round, td.valKeys[valID].Address()) - - return td.addVote(cons, v, valID) -} - -func (td *testData) addCPPreVote(cons *consensus, blockHash hash.Hash, height uint32, round int16, - cpVal vote.CPValue, just vote.Just, valID int, -) *vote.Vote { - v := vote.NewCPPreVote(blockHash, height, round, 0, cpVal, just, td.valKeys[valID].Address()) - - return td.addVote(cons, v, valID) -} - -func (td *testData) addCPMainVote(cons *consensus, blockHash hash.Hash, height uint32, round int16, - cpVal vote.CPValue, just vote.Just, valID int, -) *vote.Vote { - v := vote.NewCPMainVote(blockHash, height, round, 0, cpVal, just, td.valKeys[valID].Address()) - - return td.addVote(cons, v, valID) -} - -func (td *testData) addCPDecidedVote(cons *consensus, blockHash hash.Hash, height uint32, round int16, - cpVal vote.CPValue, just vote.Just, valID int, -) *vote.Vote { - v := vote.NewCPDecidedVote(blockHash, height, round, 0, cpVal, just, td.valKeys[valID].Address()) - - return td.addVote(cons, v, valID) -} - -func (td *testData) addVote(cons *consensus, v *vote.Vote, valID int) *vote.Vote { - td.HelperSignVote(td.valKeys[valID], v) - cons.AddVote(v) - - return v -} - -func (*testData) newHeightTimeout(cons *consensus) { - cons.lk.Lock() - cons.currentState.onTimeout(&ticker{0, cons.height, cons.round, tickerTargetNewHeight}) - cons.lk.Unlock() -} - -func (*testData) queryProposalTimeout(cons *consensus) { - cons.lk.Lock() - cons.currentState.onTimeout(&ticker{0, cons.height, cons.round, tickerTargetQueryProposal}) - cons.lk.Unlock() -} - -func (*testData) changeProposerTimeout(cons *consensus) { - cons.lk.Lock() - cons.currentState.onTimeout(&ticker{0, cons.height, cons.round, tickerTargetChangeProposer}) - cons.lk.Unlock() -} - -// enterNewHeight helps tests to enter new height safely -// without scheduling new height. It boosts the test speed. -func (td *testData) enterNewHeight(cons *consensus) { - cons.lk.Lock() - cons.enterNewState(cons.newHeightState) - cons.lk.Unlock() - - td.newHeightTimeout(cons) -} - -// enterNextRound helps tests to enter next round safely. -func (*testData) enterNextRound(cons *consensus) { - cons.lk.Lock() - cons.round++ - cons.enterNewState(cons.proposeState) - cons.lk.Unlock() -} - -func (td *testData) commitBlockForAllStates(t *testing.T) (*block.Block, *certificate.BlockCertificate) { - t.Helper() - - height := td.consX.bcState.LastBlockHeight() - var err error - prop := td.makeProposal(t, height+1, 0) - - cert := certificate.NewBlockCertificate(height+1, 0, true) - sb := cert.SignBytes(prop.Block().Hash()) - sig0 := td.consX.valKey.Sign(sb) - sig1 := td.consY.valKey.Sign(sb) - sig2 := td.consB.valKey.Sign(sb) - sig3 := td.consP.valKey.Sign(sb) - sig4 := td.consM.valKey.Sign(sb) - - sig := bls.SignatureAggregate(sig0, sig1, sig2, sig3, sig4) - cert.SetSignature([]int32{0, 1, 2, 3, 4, 5}, []int32{5}, sig) - blk := prop.Block() - - err = td.consX.bcState.CommitBlock(blk, cert) - assert.NoError(t, err) - err = td.consY.bcState.CommitBlock(blk, cert) - assert.NoError(t, err) - err = td.consB.bcState.CommitBlock(blk, cert) - assert.NoError(t, err) - err = td.consP.bcState.CommitBlock(blk, cert) - assert.NoError(t, err) - err = td.consM.bcState.CommitBlock(blk, cert) - assert.NoError(t, err) - err = td.consN.bcState.CommitBlock(blk, cert) - assert.NoError(t, err) - - return blk, cert -} - -// makeProposal generates a signed and valid proposal for the given height and round. -func (td *testData) makeProposal(t *testing.T, height uint32, round int16) *proposal.Proposal { - t.Helper() - - var cons *consensus - switch (height % 6) + uint32(round%6) { - case 1: - cons = td.consX - case 2: - cons = td.consY - case 3: - cons = td.consB - case 4: - cons = td.consP - case 5: - cons = td.consM - case 0, 6: - cons = td.consN - } - - blk, err := cons.bcState.ProposeBlock(cons.valKey, cons.rewardAddr) - require.NoError(t, err) - p := proposal.NewProposal(height, round, blk) - td.HelperSignProposal(cons.valKey, p) - - return p -} - -// makeChangeProposerJusts generates justifications for changing the proposer at the specified height and round. -// If `proposal` is nil, it creates justifications for not changing the proposer; -// otherwise, it generates justifications to change the proposer. -// It returns three justifications: -// -// 1. `JustInitNo` if the proposal is set, or `JustInitYes` if not for the pre-vote step, -// 2. `JustMainVoteNoConflict` for the main-vote step, -// 3. `JustDecided` for the decided step. -func (td *testData) makeChangeProposerJusts(t *testing.T, propBlockHash hash.Hash, - height uint32, round int16, -) (vote.Just, vote.Just, vote.Just) { - t.Helper() - - cpRound := int16(0) - - // Create PreVote Justification - var preVoteJust vote.Just - var cpValue vote.CPValue - - if propBlockHash != hash.UndefHash { - cpValue = vote.CPValueNo - prepareCommitters := []int32{} - prepareSigs := []*bls.Signature{} - for i, val := range td.consP.validators { - prepareVote := vote.NewPrepareVote(propBlockHash, height, round, val.Address()) - signBytes := prepareVote.SignBytes() - - prepareCommitters = append(prepareCommitters, val.Number()) - prepareSigs = append(prepareSigs, td.valKeys[i].Sign(signBytes)) - } - prepareAggSig := bls.SignatureAggregate(prepareSigs...) - certPrepare := certificate.NewVoteCertificate(height, round) - certPrepare.SetSignature(prepareCommitters, []int32{}, prepareAggSig) - - preVoteJust = &vote.JustInitNo{ - QCert: certPrepare, - } - } else { - cpValue = vote.CPValueYes - preVoteJust = &vote.JustInitYes{} - } - - // Create MainVote Justification - preVoteCommitters := []int32{} - preVoteSigs := []*bls.Signature{} - for i, val := range td.consP.validators { - preVote := vote.NewCPPreVote(propBlockHash, height, round, - cpRound, cpValue, preVoteJust, val.Address()) - signBytes := preVote.SignBytes() - - preVoteCommitters = append(preVoteCommitters, val.Number()) - preVoteSigs = append(preVoteSigs, td.valKeys[i].Sign(signBytes)) - } - preVoteAggSig := bls.SignatureAggregate(preVoteSigs...) - certPreVote := certificate.NewVoteCertificate(height, round) - certPreVote.SetSignature(preVoteCommitters, []int32{}, preVoteAggSig) - mainVoteJust := &vote.JustMainVoteNoConflict{QCert: certPreVote} - - // Create Decided Justification - mainVoteCommitters := []int32{} - mainVoteSigs := []*bls.Signature{} - for i, val := range td.consP.validators { - mainVote := vote.NewCPMainVote(propBlockHash, height, round, - cpRound, cpValue, mainVoteJust, val.Address()) - signBytes := mainVote.SignBytes() - - mainVoteCommitters = append(mainVoteCommitters, val.Number()) - mainVoteSigs = append(mainVoteSigs, td.valKeys[i].Sign(signBytes)) - } - mainVoteAggSig := bls.SignatureAggregate(mainVoteSigs...) - certMainVote := certificate.NewVoteCertificate(height, round) - certMainVote.SetSignature(mainVoteCommitters, []int32{}, mainVoteAggSig) - decidedJust := &vote.JustDecided{QCert: certMainVote} - - return preVoteJust, mainVoteJust, decidedJust -} - -func TestStart(t *testing.T) { - td := setup(t) - - td.consX.Start() - td.checkHeightRound(t, td.consX, 1, 0) -} - -func TestNotInCommittee(t *testing.T) { - td := setup(t) - - valKey := td.RandValKey() - str := store.MockingStore(td.TestSuite) - - st, _ := state.LoadOrNewState(td.genDoc, []*bls.ValidatorKey{valKey}, str, td.txPool, nil) - consInst := NewConsensus(testConfig(), st, valKey, valKey.Address(), make(chan message.Message, 100), - newConcreteMediator()) - cons := consInst.(*consensus) - - td.enterNewHeight(cons) - td.newHeightTimeout(cons) - assert.Equal(t, cons.currentState.name(), "new-height") -} - -func TestVoteWithInvalidHeight(t *testing.T) { - td := setup(t) - - td.commitBlockForAllStates(t) // height 1 - td.enterNewHeight(td.consP) - - v1 := td.addPrepareVote(td.consP, td.RandHash(), 1, 0, tIndexX) - v2 := td.addPrepareVote(td.consP, td.RandHash(), 2, 0, tIndexX) - v3 := td.addPrepareVote(td.consP, td.RandHash(), 2, 0, tIndexY) - v4 := td.addPrepareVote(td.consP, td.RandHash(), 3, 0, tIndexX) - - require.False(t, td.consP.HasVote(v1.Hash())) - require.True(t, td.consP.HasVote(v2.Hash())) - require.True(t, td.consP.HasVote(v3.Hash())) - require.False(t, td.consP.HasVote(v4.Hash())) -} - -func TestConsensusFastPath(t *testing.T) { - td := setup(t) - - td.commitBlockForAllStates(t) // height 1 - - td.enterNewHeight(td.consX) - td.checkHeightRound(t, td.consX, 2, 0) - - prop := td.makeProposal(t, 2, 0) - td.consX.SetProposal(prop) - - td.addPrepareVote(td.consX, prop.Block().Hash(), 2, 0, tIndexY) - td.addPrepareVote(td.consX, prop.Block().Hash(), 2, 0, tIndexB) - td.addPrepareVote(td.consX, prop.Block().Hash(), 2, 0, tIndexP) - td.addPrepareVote(td.consX, prop.Block().Hash(), 2, 0, tIndexM) - td.shouldPublishVote(t, td.consX, vote.VoteTypePrepare, prop.Block().Hash()) - - td.shouldPublishBlockAnnounce(t, td.consX, prop.Block().Hash()) -} - -func TestConsensusAddVote(t *testing.T) { - td := setup(t) - - td.enterNewHeight(td.consP) - td.enterNextRound(td.consP) - - v1 := td.addPrepareVote(td.consP, td.RandHash(), 1, 0, tIndexX) - v2 := td.addPrepareVote(td.consP, td.RandHash(), 1, 2, tIndexX) - v3 := td.addPrepareVote(td.consP, td.RandHash(), 1, 1, tIndexX) - v4 := td.addPrecommitVote(td.consP, td.RandHash(), 1, 1, tIndexX) - v5 := td.addPrepareVote(td.consP, td.RandHash(), 2, 0, tIndexX) - v6, _ := td.GenerateTestPrepareVote(1, 0) - td.consP.AddVote(v6) - - assert.False(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{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, r) - - // consP receives proposal now - td.consP.SetProposal(prop) - - td.shouldPublishVote(t, td.consP, vote.VoteTypePrepare, blockHash) - td.shouldPublishBlockAnnounce(t, td.consP, blockHash) -} - -// TestConsensusVeryLateProposal tests the scenario where a slow node doesn't have the proposal -// in precommit phase. -func TestConsensusVeryLateProposal(t *testing.T) { - td := setup(t) - - td.commitBlockForAllStates(t) // height 1 - - td.enterNewHeight(td.consP) - - h := uint32(2) - r := int16(0) - prop := td.makeProposal(t, h, r) - blockHash := prop.Block().Hash() - - td.addPrepareVote(td.consP, blockHash, h, r, tIndexX) - td.addPrepareVote(td.consP, blockHash, h, r, tIndexY) - td.addPrepareVote(td.consP, blockHash, h, r, tIndexM) - td.addPrepareVote(td.consP, blockHash, h, r, tIndexN) - - // consP timed out - td.changeProposerTimeout(td.consP) - - _, _, decidedJust := td.makeChangeProposerJusts(t, prop.Block().Hash(), h, r) - td.addCPDecidedVote(td.consP, prop.Block().Hash(), h, r, vote.CPValueNo, decidedJust, tIndexX) - - td.addPrecommitVote(td.consP, blockHash, h, r, tIndexX) - td.addPrecommitVote(td.consP, blockHash, h, r, tIndexY) - td.addPrecommitVote(td.consP, blockHash, h, r, tIndexM) - td.addPrecommitVote(td.consP, blockHash, h, r, tIndexN) - - td.shouldPublishQueryProposal(t, td.consP, h, r) - - // consP receives proposal now - td.consP.SetProposal(prop) - - td.shouldPublishVote(t, td.consP, vote.VoteTypePrecommit, prop.Block().Hash()) - td.shouldPublishBlockAnnounce(t, td.consP, prop.Block().Hash()) -} - -func TestPickRandomVote(t *testing.T) { - td := setup(t) - - td.enterNewHeight(td.consP) - assert.Nil(t, td.consP.PickRandomVote(0)) - - h := uint32(1) - r := int16(0) - - preVoteJust, mainVoteJust, decidedJust := td.makeChangeProposerJusts(t, hash.UndefHash, h, r) - - // round 0 - v1 := td.addPrepareVote(td.consP, td.RandHash(), h, r, tIndexX) - v2 := td.addPrepareVote(td.consP, td.RandHash(), h, r, tIndexY) - v3 := td.addCPPreVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, preVoteJust, tIndexY) - v4 := td.addCPMainVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, mainVoteJust, tIndexY) - v5 := td.addCPDecidedVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, decidedJust, tIndexY) - - // Round 1 - td.enterNextRound(td.consP) - v6 := td.addPrepareVote(td.consP, td.RandHash(), h, r+1, tIndexY) - - require.True(t, td.consP.HasVote(v1.Hash())) - require.True(t, td.consP.HasVote(v2.Hash())) - require.True(t, td.consP.HasVote(v3.Hash())) - require.True(t, td.consP.HasVote(v4.Hash())) - require.True(t, td.consP.HasVote(v5.Hash())) - require.True(t, td.consP.HasVote(v6.Hash())) - - rndVote0 := td.consP.PickRandomVote(r) - assert.Equal(t, rndVote0, v5, "for past round should pick Decided votes only") - - rndVote1 := td.consP.PickRandomVote(r + 1) - assert.Equal(t, rndVote1, v6) - - rndVote2 := td.consP.PickRandomVote(r + 2) - assert.Nil(t, rndVote2) -} - -func TestSetProposalFromPreviousRound(t *testing.T) { - td := setup(t) - - prop := td.makeProposal(t, 1, 0) - td.enterNewHeight(td.consP) - td.enterNextRound(td.consP) - - // It should ignore proposal for previous rounds - td.consP.SetProposal(prop) - - assert.Nil(t, td.consP.Proposal()) - td.checkHeightRound(t, td.consP, 1, 1) -} - -func TestSetProposalFromPreviousHeight(t *testing.T) { - td := setup(t) - - prop := td.makeProposal(t, 1, 0) - td.commitBlockForAllStates(t) // height 1 - - td.enterNewHeight(td.consP) - - td.consP.SetProposal(prop) - assert.Nil(t, td.consP.Proposal()) - td.checkHeightRound(t, td.consP, 2, 0) -} - -func TestDuplicateProposal(t *testing.T) { - td := setup(t) - - td.commitBlockForAllStates(t) - td.commitBlockForAllStates(t) - td.commitBlockForAllStates(t) - - td.enterNewHeight(td.consX) - - h := uint32(4) - r := int16(0) - prop1 := td.makeProposal(t, h, r) - trx := tx.NewTransferTx(h, td.consX.rewardAddr, - td.RandAccAddress(), 1000, 1000, "proposal changer") - td.HelperSignTransaction(td.consX.valKey.PrivateKey(), trx) - - assert.NoError(t, td.txPool.AppendTx(trx)) - prop2 := td.makeProposal(t, h, r) - assert.NotEqual(t, prop1.Hash(), prop2.Hash()) - - td.consX.SetProposal(prop1) - td.consX.SetProposal(prop2) - - assert.Equal(t, td.consX.Proposal().Hash(), prop1.Hash()) -} - -func TestNonActiveValidator(t *testing.T) { - td := setup(t) - - valKey := td.RandValKey() - consInst := NewConsensus(testConfig(), state.MockingState(td.TestSuite), - valKey, valKey.Address(), make(chan message.Message, 100), newConcreteMediator()) - nonActiveCons := consInst.(*consensus) - - t.Run("non-active instances should be in new-height state", func(t *testing.T) { - nonActiveCons.MoveToNewHeight() - td.newHeightTimeout(nonActiveCons) - td.checkHeightRound(t, nonActiveCons, 1, 0) - - // Double entry - nonActiveCons.MoveToNewHeight() - td.newHeightTimeout(nonActiveCons) - td.checkHeightRound(t, nonActiveCons, 1, 0) - - assert.False(t, nonActiveCons.IsActive()) - assert.Equal(t, nonActiveCons.currentState.name(), "new-height") - }) - - t.Run("non-active instances should ignore proposals", func(t *testing.T) { - prop := td.makeProposal(t, 1, 0) - nonActiveCons.SetProposal(prop) - - assert.Nil(t, nonActiveCons.Proposal()) - }) - - t.Run("non-active instances should ignore votes", func(t *testing.T) { - v := td.addPrepareVote(nonActiveCons, td.RandHash(), 1, 0, tIndexX) - - assert.False(t, nonActiveCons.HasVote(v.Hash())) - }) - - t.Run("non-active instances should move to new height", func(t *testing.T) { - b1, cert1 := td.commitBlockForAllStates(t) - - nonActiveCons.MoveToNewHeight() - td.checkHeightRound(t, nonActiveCons, 1, 0) - - assert.NoError(t, nonActiveCons.bcState.CommitBlock(b1, cert1)) - - nonActiveCons.MoveToNewHeight() - td.newHeightTimeout(nonActiveCons) - td.checkHeightRound(t, nonActiveCons, 2, 0) - }) -} - -func TestVoteWithBigRound(t *testing.T) { - td := setup(t) - - td.enterNewHeight(td.consX) - - v := td.addPrepareVote(td.consX, td.RandHash(), 1, util.MaxInt16, tIndexB) - assert.True(t, td.consX.HasVote(v.Hash())) -} - -func TestProposalWithBigRound(t *testing.T) { - td := setup(t) - - td.enterNewHeight(td.consP) - - prop := td.makeProposal(t, 1, util.MaxInt16) - td.consP.SetProposal(prop) - assert.Nil(t, td.consP.Proposal()) -} - -func TestInvalidProposal(t *testing.T) { - td := setup(t) - - td.enterNewHeight(td.consP) - - p := td.makeProposal(t, 1, 0) - p.SetSignature(nil) // Make proposal invalid - td.consP.SetProposal(p) - 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)"}, - {1717870730391411396, 2, "move to the next round on decided vote"}, - } - - for i, test := range tests { - td := setupWithSeed(t, test.seed) - td.commitBlockForAllStates(t) - - td.enterNewHeight(td.consX) - td.enterNewHeight(td.consY) - td.enterNewHeight(td.consB) - td.enterNewHeight(td.consP) - td.enterNewHeight(td.consM) - td.enterNewHeight(td.consN) - - cert, err := checkConsensus(td, 2, nil) - require.NoError(t, err, - "test %v failed: %s", i+1, err) - require.Equal(t, cert.Round(), test.round, - "test %v failed. round not matched (expected %d, got %d)", - i+1, test.round, cert.Round()) - } -} - -func TestFaulty(t *testing.T) { - for i := 0; i < 10; i++ { - td := setup(t) - td.commitBlockForAllStates(t) - - td.enterNewHeight(td.consX) - td.enterNewHeight(td.consY) - td.enterNewHeight(td.consB) - td.enterNewHeight(td.consP) - td.enterNewHeight(td.consM) - td.enterNewHeight(td.consN) - - _, err := checkConsensus(td, 2, nil) - require.NoError(t, err) - } -} - -// In this test, B is a Byzantine node and the network is partitioned. -// B acts maliciously by double proposing: -// sending one proposal to X and Y, and another proposal to P, M and N. -// -// Once the partition is healed, honest nodes should either reach consensus -// on one proposal or change the proposer. -// This is due to the randomness of the binary agreement. -func TestByzantine(t *testing.T) { - td := setup(t) - - for i := 0; i < 8; i++ { - td.commitBlockForAllStates(t) - } - - h := uint32(9) - r := int16(0) - - // ================================= - // X, Y votes - td.enterNewHeight(td.consX) - td.enterNewHeight(td.consY) - - prop := td.makeProposal(t, h, r) - require.Equal(t, prop.Block().Header().ProposerAddress(), td.consB.valKey.Address()) - - // X and Y receive the Seconds proposal - td.consX.SetProposal(prop) - td.consY.SetProposal(prop) - - td.shouldPublishVote(t, td.consX, vote.VoteTypePrepare, prop.Block().Hash()) - td.shouldPublishVote(t, td.consY, vote.VoteTypePrepare, prop.Block().Hash()) - - // X and Y don't have enough votes, so they request to change the proposer - td.changeProposerTimeout(td.consX) - td.changeProposerTimeout(td.consY) - - // X and Y are unable to progress - - // ================================= - // P, M and N votes - // Byzantine node create the second proposal and send it to the partitioned nodes - byzTrx := tx.NewTransferTx(h, - td.consB.rewardAddr, td.RandAccAddress(), 1000, 1000, "") - td.HelperSignTransaction(td.consB.valKey.PrivateKey(), byzTrx) - assert.NoError(t, td.txPool.AppendTx(byzTrx)) - byzProp := td.makeProposal(t, h, r) - - require.NotEqual(t, prop.Block().Hash(), byzProp.Block().Hash()) - require.Equal(t, byzProp.Block().Header().ProposerAddress(), td.consB.valKey.Address()) - - td.enterNewHeight(td.consP) - td.enterNewHeight(td.consM) - td.enterNewHeight(td.consN) - - // P, M and N receive the Seconds proposal - td.consP.SetProposal(byzProp) - td.consM.SetProposal(byzProp) - td.consN.SetProposal(byzProp) - - voteP := td.shouldPublishVote(t, td.consP, vote.VoteTypePrepare, byzProp.Block().Hash()) - voteM := td.shouldPublishVote(t, td.consM, vote.VoteTypePrepare, byzProp.Block().Hash()) - voteN := td.shouldPublishVote(t, td.consN, vote.VoteTypePrepare, byzProp.Block().Hash()) - - // P, M and N don't have enough votes, so they request to change the proposer - td.changeProposerTimeout(td.consP) - td.changeProposerTimeout(td.consM) - td.changeProposerTimeout(td.consN) - - // P, M and N are unable to progress - - // ================================= - // B votes - // B requests to NOT change the proposer - - td.enterNewHeight(td.consB) - - voteB := vote.NewPrepareVote(byzProp.Block().Hash(), h, r, td.consB.valKey.Address()) - td.HelperSignVote(td.consB.valKey, voteB) - byzJust0Block := &vote.JustInitNo{ - QCert: td.consB.makeVoteCertificate( - map[crypto.Address]*vote.Vote{ - voteP.Signer(): voteP, - voteM.Signer(): voteM, - voteN.Signer(): voteN, - voteB.Signer(): voteB, - }), - } - byzVote := vote.NewCPPreVote(byzProp.Block().Hash(), h, r, 0, vote.CPValueNo, byzJust0Block, td.consB.valKey.Address()) - td.HelperSignVote(td.consB.valKey, byzVote) - - // ================================= - - td.checkHeightRound(t, td.consX, h, r) - td.checkHeightRound(t, td.consY, h, r) - td.checkHeightRound(t, td.consP, h, r) - td.checkHeightRound(t, td.consM, h, r) - td.checkHeightRound(t, td.consN, h, r) - - // ================================= - // Now, Partition heals - fmt.Println("== Partition heals") - cert, err := checkConsensus(td, h, []*vote.Vote{byzVote}) - - require.NoError(t, err) - require.Equal(t, cert.Height(), h) - require.Contains(t, cert.Absentees(), int32(tIndexB)) -} - -func checkConsensus(td *testData, height uint32, byzVotes []*vote.Vote) ( - *certificate.BlockCertificate, error, -) { - instances := []*consensus{td.consX, td.consY, td.consB, td.consP, td.consM, td.consN} - - if len(byzVotes) > 0 { - for _, v := range byzVotes { - td.consB.broadcastVote(v) - } - - // remove byzantine node (Byzantine node goes offline) - instances = []*consensus{td.consX, td.consY, td.consP, td.consM, td.consN} - } - - // 70% chance for the first block to be lost - changeProposerChance := 70 - - blockAnnounces := map[crypto.Address]*message.BlockAnnounceMessage{} - for len(td.consMessages) > 0 { - rndIndex := td.RandInt(len(td.consMessages)) - rndMsg := td.consMessages[rndIndex] - td.consMessages = slices.Delete(td.consMessages, rndIndex, rndIndex+1) - - switch rndMsg.message.Type() { - case message.TypeVote: - m := rndMsg.message.(*message.VoteMessage) - if m.Vote.Height() == height { - for _, cons := range instances { - cons.AddVote(m.Vote) - } - } - - case message.TypeProposal: - m := rndMsg.message.(*message.ProposalMessage) - if m.Proposal.Height() == height { - for _, cons := range instances { - cons.SetProposal(m.Proposal) - } - } - - case message.TypeQueryProposal: - for _, cons := range instances { - p := cons.Proposal() - if p != nil { - td.consMessages = append(td.consMessages, consMessage{ - sender: cons.valKey.Address(), - message: message.NewProposalMessage(p), - }) - } - } - case message.TypeQueryVote: - // To make the test reproducible, we ignore the QueryVote message. - // This is because QueryVote returns a random vote that can make the test non-reproducible. - - case message.TypeBlockAnnounce: - m := rndMsg.message.(*message.BlockAnnounceMessage) - blockAnnounces[rndMsg.sender] = m - - case - message.TypeHello, - message.TypeHelloAck, - message.TypeTransaction, - message.TypeBlocksRequest, - message.TypeBlocksResponse: - // - } - - for _, cons := range instances { - rnd := td.RandInt(100) - if rnd < changeProposerChance || - len(td.consMessages) == 0 { - td.changeProposerTimeout(cons) - } - } - changeProposerChance -= 5 - } - - // Verify whether more than (3t+1) nodes have committed to the same block. - if len(blockAnnounces) >= 4 { - var firstAnnounce *message.BlockAnnounceMessage - for _, msg := range blockAnnounces { - if firstAnnounce == nil { - firstAnnounce = msg - } else if msg.Block.Hash() != firstAnnounce.Block.Hash() { - return nil, fmt.Errorf("consensus violated, seed %v", td.TestSuite.Seed) - } - } - - // everything is ok - return firstAnnounce.Certificate, nil - } - - return nil, fmt.Errorf("unable to reach consensus, seed %v", td.TestSuite.Seed) -} diff --git a/fastconsensus/cp.go b/fastconsensus/cp.go deleted file mode 100644 index 14ebcaa79..000000000 --- a/fastconsensus/cp.go +++ /dev/null @@ -1,340 +0,0 @@ -package fastconsensus - -import ( - "fmt" - - "github.com/pactus-project/pactus/crypto/hash" - "github.com/pactus-project/pactus/types/proposal" - "github.com/pactus-project/pactus/types/vote" -) - -type changeProposer struct { - *consensus -} - -func (*changeProposer) onSetProposal(_ *proposal.Proposal) { - // Ignore proposal -} - -func (cp *changeProposer) onTimeout(t *ticker) { - if t.Target == tickerTargetQueryVotes { - cp.queryVotes() - cp.scheduleTimeout(t.Duration*2, cp.height, cp.round, tickerTargetQueryVotes) - } -} - -func (*changeProposer) cpCheckCPValue(value vote.CPValue, allowedValues ...vote.CPValue) error { - for _, v := range allowedValues { - if value == v { - return nil - } - } - - return invalidJustificationError{ - Reason: fmt.Sprintf("invalid value: %v", value), - } -} - -func (cp *changeProposer) cpCheckJustInitNo(just vote.Just, - blockHash hash.Hash, cpRound int16, cpValue vote.CPValue, -) error { - j, ok := just.(*vote.JustInitNo) - if !ok { - return invalidJustificationError{ - Reason: "invalid just data", - } - } - - if cpRound != 0 { - return invalidJustificationError{ - Reason: fmt.Sprintf("invalid round: %v", cpRound), - } - } - - err := cp.cpCheckCPValue(cpValue, vote.CPValueNo) - if err != nil { - return err - } - - err = j.QCert.ValidatePrepare(cp.validators, blockHash) - if err != nil { - return err - } - - return nil -} - -func (cp *changeProposer) cpCheckJustInitYes(just vote.Just, - blockHash hash.Hash, cpRound int16, cpValue vote.CPValue, -) error { - _, ok := just.(*vote.JustInitYes) - if !ok { - return invalidJustificationError{ - Reason: "invalid just data", - } - } - - if cpRound != 0 { - return invalidJustificationError{ - Reason: fmt.Sprintf("invalid round: %v", cpRound), - } - } - - err := cp.cpCheckCPValue(cpValue, vote.CPValueYes) - if err != nil { - return err - } - - if !blockHash.IsUndef() { - return invalidJustificationError{ - Reason: fmt.Sprintf("invalid block hash: %s", blockHash), - } - } - - return nil -} - -func (cp *changeProposer) cpCheckJustPreVoteHard(just vote.Just, - blockHash hash.Hash, cpRound int16, cpValue vote.CPValue, -) error { - j, ok := just.(*vote.JustPreVoteHard) - if !ok { - return invalidJustificationError{ - Reason: "invalid just data", - } - } - - if cpRound == 0 { - return invalidJustificationError{ - Reason: "invalid round: 0", - } - } - - err := cp.cpCheckCPValue(cpValue, vote.CPValueNo, vote.CPValueYes) - if err != nil { - return err - } - - err = j.QCert.ValidateCPPreVote(cp.validators, - blockHash, cpRound-1, byte(cpValue)) - if err != nil { - return invalidJustificationError{ - Reason: err.Error(), - } - } - - return nil -} - -func (cp *changeProposer) cpCheckJustPreVoteSoft(just vote.Just, - blockHash hash.Hash, cpRound int16, cpValue vote.CPValue, -) error { - j, ok := just.(*vote.JustPreVoteSoft) - if !ok { - return invalidJustificationError{ - Reason: "invalid just data", - } - } - - if cpRound == 0 { - return invalidJustificationError{ - Reason: "invalid round: 0", - } - } - - err := cp.cpCheckCPValue(cpValue, vote.CPValueNo, vote.CPValueYes) - if err != nil { - return err - } - - err = j.QCert.ValidateCPMainVote(cp.validators, - blockHash, cpRound-1, byte(vote.CPValueAbstain)) - if err != nil { - return invalidJustificationError{ - Reason: err.Error(), - } - } - - return nil -} - -func (cp *changeProposer) cpCheckJustMainVoteNoConflict(just vote.Just, - blockHash hash.Hash, cpRound int16, cpValue vote.CPValue, -) error { - j, ok := just.(*vote.JustMainVoteNoConflict) - if !ok { - return invalidJustificationError{ - Reason: "invalid just data", - } - } - err := cp.cpCheckCPValue(cpValue, vote.CPValueNo, vote.CPValueYes) - if err != nil { - return err - } - - err = j.QCert.ValidateCPPreVote(cp.validators, - blockHash, cpRound, byte(cpValue)) - if err != nil { - return invalidJustificationError{ - Reason: err.Error(), - } - } - - return nil -} - -func (cp *changeProposer) cpCheckJustMainVoteConflict(just vote.Just, - blockHash hash.Hash, cpRound int16, cpValue vote.CPValue, -) error { - j, ok := just.(*vote.JustMainVoteConflict) - if !ok { - return invalidJustificationError{ - Reason: "invalid just data", - } - } - - err := cp.cpCheckCPValue(cpValue, vote.CPValueAbstain) - if err != nil { - return err - } - - switch j.JustNo.Type() { - case vote.JustTypeInitNo: - err := cp.cpCheckJustInitNo(j.JustNo, blockHash, cpRound, vote.CPValueNo) - if err != nil { - return err - } - case vote.JustTypePreVoteHard: - err := cp.cpCheckJustPreVoteHard(j.JustNo, blockHash, cpRound, vote.CPValueNo) - if err != nil { - return err - } - case vote.JustTypePreVoteSoft: - err := cp.cpCheckJustPreVoteSoft(j.JustNo, blockHash, cpRound, vote.CPValueNo) - if err != nil { - return err - } - - case vote.JustTypeInitYes, - vote.JustTypeMainVoteConflict, - vote.JustTypeMainVoteNoConflict, - vote.JustTypeDecided: - return invalidJustificationError{ - Reason: fmt.Sprintf("unexpected justification: %s", j.JustNo.Type()), - } - } - - switch j.JustYes.Type() { - case vote.JustTypeInitYes: - err := cp.cpCheckJustInitYes(j.JustYes, hash.UndefHash, cpRound, vote.CPValueYes) - if err != nil { - return err - } - - case vote.JustTypePreVoteHard: - err := cp.cpCheckJustPreVoteHard(j.JustYes, hash.UndefHash, cpRound, vote.CPValueYes) - if err != nil { - return err - } - - case vote.JustTypeInitNo, - vote.JustTypePreVoteSoft, - vote.JustTypeMainVoteConflict, - vote.JustTypeMainVoteNoConflict, - vote.JustTypeDecided: - return invalidJustificationError{ - Reason: fmt.Sprintf("unexpected justification: %s", j.JustNo.Type()), - } - } - - return nil -} - -func (cp *changeProposer) cpCheckJustDecide(just vote.Just, - blockHash hash.Hash, cpRound int16, cpValue vote.CPValue, -) error { - j, ok := just.(*vote.JustDecided) - if !ok { - return invalidJustificationError{ - Reason: "invalid just data", - } - } - - err := cp.cpCheckCPValue(cpValue, vote.CPValueNo, vote.CPValueYes) - if err != nil { - return err - } - - err = j.QCert.ValidateCPMainVote(cp.validators, - blockHash, cpRound, byte(cpValue)) - if err != nil { - return invalidJustificationError{ - Reason: err.Error(), - } - } - - return nil -} - -func (cp *changeProposer) cpCheckJust(v *vote.Vote) error { - switch v.CPJust().Type() { - case vote.JustTypeInitYes: - return cp.cpCheckJustInitYes(v.CPJust(), - v.BlockHash(), v.CPRound(), v.CPValue()) - - case vote.JustTypeInitNo: - return cp.cpCheckJustInitNo(v.CPJust(), - v.BlockHash(), v.CPRound(), v.CPValue()) - - case vote.JustTypePreVoteSoft: - return cp.cpCheckJustPreVoteSoft(v.CPJust(), - v.BlockHash(), v.CPRound(), v.CPValue()) - - case vote.JustTypePreVoteHard: - return cp.cpCheckJustPreVoteHard(v.CPJust(), - v.BlockHash(), v.CPRound(), v.CPValue()) - - case vote.JustTypeMainVoteNoConflict: - return cp.cpCheckJustMainVoteNoConflict(v.CPJust(), - v.BlockHash(), v.CPRound(), v.CPValue()) - - case vote.JustTypeMainVoteConflict: - return cp.cpCheckJustMainVoteConflict(v.CPJust(), - v.BlockHash(), v.CPRound(), v.CPValue()) - - case vote.JustTypeDecided: - return cp.cpCheckJustDecide(v.CPJust(), - v.BlockHash(), v.CPRound(), v.CPValue()) - - default: - panic("unreachable") - } -} - -// cpStrongTermination decides if the Change Proposer phase should be terminated. -// If there is only one proper and justified `Decided` vote, the validators can -// move on to the next phase. -// If the `Decided` vote is for "No", then validators move to the precommit step and -// wait for committing the current proposal by gathering enough precommit votes. -// If the `Decided` vote is for "Yes", then the validator moves to the propose step -// and starts a new round. -func (cp *changeProposer) cpStrongTermination() { - cpDecided := cp.log.CPDecidedVoteSet(cp.round) - if cpDecided.HasAnyVoteFor(cp.cpRound, vote.CPValueNo) { - cp.cpDecide(cp.round, vote.CPValueNo) - } else if cpDecided.HasAnyVoteFor(cp.cpRound, vote.CPValueYes) { - cp.cpDecide(cp.round, vote.CPValueYes) - } -} - -func (cp *changeProposer) cpDecide(round int16, cpValue vote.CPValue) { - if cpValue == vote.CPValueYes { - cp.round = round + 1 - cp.cpDecided = 1 - cp.enterNewState(cp.proposeState) - } else if cpValue == vote.CPValueNo { - cp.round = round - cp.cpDecided = 0 - cp.enterNewState(cp.precommitState) - } -} diff --git a/fastconsensus/cp_decide.go b/fastconsensus/cp_decide.go deleted file mode 100644 index 4e98d3211..000000000 --- a/fastconsensus/cp_decide.go +++ /dev/null @@ -1,59 +0,0 @@ -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.cpDecide(s.round, vote.CPValueYes) - } 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.cpDecide(s.round, vote.CPValueNo) - } else { - // conflicting votes - s.logger.Debug("conflicting main votes", "round", s.cpRound) - s.cpRound++ - s.enterNewState(s.cpPreVoteState) - } - } -} - -func (s *cpDecideState) onAddVote(_ *vote.Vote) { - s.decide() -} - -func (*cpDecideState) name() string { - return "cp:decide" -} diff --git a/fastconsensus/cp_mainvote.go b/fastconsensus/cp_mainvote.go deleted file mode 100644 index 602762501..000000000 --- a/fastconsensus/cp_mainvote.go +++ /dev/null @@ -1,103 +0,0 @@ -package fastconsensus - -import ( - "github.com/pactus-project/pactus/crypto/hash" - "github.com/pactus-project/pactus/types/proposal" - "github.com/pactus-project/pactus/types/vote" -) - -type cpMainVoteState struct { - *changeProposer -} - -func (s *cpMainVoteState) enter() { - s.decide() -} - -func (s *cpMainVoteState) decide() { - s.strongCommit() - s.cpStrongTermination() - s.checkForWeakValidity() - s.detectByzantineProposal() - - cpPreVotes := s.log.CPPreVoteVoteSet(s.round) - if cpPreVotes.HasTwoFPlusOneVotes(s.cpRound) { - if cpPreVotes.HasTwoFPlusOneVotesFor(s.cpRound, vote.CPValueYes) { - s.logger.Debug("cp: quorum for pre-votes", "value", "yes") - - votes := cpPreVotes.BinaryVotes(s.cpRound, vote.CPValueYes) - cert := s.makeVoteCertificate(votes) - just := &vote.JustMainVoteNoConflict{ - QCert: cert, - } - s.signAddCPMainVote(hash.UndefHash, s.cpRound, vote.CPValueYes, just) - s.enterNewState(s.cpDecideState) - } else if cpPreVotes.HasTwoFPlusOneVotesFor(s.cpRound, vote.CPValueNo) { - s.logger.Debug("cp: quorum for pre-votes", "value", "no") - - votes := cpPreVotes.BinaryVotes(s.cpRound, vote.CPValueNo) - cert := s.makeVoteCertificate(votes) - just := &vote.JustMainVoteNoConflict{ - QCert: cert, - } - s.signAddCPMainVote(*s.cpWeakValidity, s.cpRound, vote.CPValueNo, just) - s.enterNewState(s.cpDecideState) - } else { - s.logger.Debug("cp: no-quorum for pre-votes", "value", "abstain") - - vote0 := cpPreVotes.GetRandomVote(s.cpRound, vote.CPValueNo) - vote1 := cpPreVotes.GetRandomVote(s.cpRound, vote.CPValueYes) - - just := &vote.JustMainVoteConflict{ - JustNo: vote0.CPJust(), - JustYes: vote1.CPJust(), - } - - s.signAddCPMainVote(*s.cpWeakValidity, s.cpRound, vote.CPValueAbstain, just) - s.enterNewState(s.cpDecideState) - } - } -} - -func (s *cpMainVoteState) checkForWeakValidity() { - if s.cpWeakValidity == nil { - preVotes := s.log.CPPreVoteVoteSet(s.round) - randVote := preVotes.GetRandomVote(s.cpRound, vote.CPValueNo) - if randVote != nil { - bh := randVote.BlockHash() - s.cpWeakValidity = &bh - } - } -} - -func (s *cpMainVoteState) detectByzantineProposal() { - if s.cpWeakValidity != nil { - roundProposal := s.log.RoundProposal(s.round) - - if roundProposal != nil && - roundProposal.Block().Hash() != *s.cpWeakValidity { - s.logger.Warn("double proposal detected", - "prepared", s.cpWeakValidity, - "roundProposal", roundProposal.Block().Hash()) - - s.log.SetRoundProposal(s.round, nil) - s.queryProposal() - } - } -} - -func (s *cpMainVoteState) onAddVote(_ *vote.Vote) { - s.decide() -} - -func (*cpMainVoteState) onSetProposal(_ *proposal.Proposal) { - // Ignore proposal -} - -func (*cpMainVoteState) onTimeout(_ *ticker) { - // Ignore timeouts -} - -func (*cpMainVoteState) name() string { - return "cp:main-vote" -} diff --git a/fastconsensus/cp_prevote.go b/fastconsensus/cp_prevote.go deleted file mode 100644 index ca01aa997..000000000 --- a/fastconsensus/cp_prevote.go +++ /dev/null @@ -1,98 +0,0 @@ -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 cpPreVoteState struct { - *changeProposer -} - -func (s *cpPreVoteState) enter() { - s.decide() -} - -//nolint:nestif // complexity can't be reduced more. -func (s *cpPreVoteState) decide() { - s.strongCommit() - s.cpStrongTermination() - - if s.cpRound == 0 { - // broadcast the initial value - prepares := s.log.PrepareVoteSet(s.round) - prepareQH := prepares.QuorumHash() - if prepareQH != nil { - s.cpWeakValidity = prepareQH - votes := prepares.BlockVotes(*prepareQH) - cert := s.makeVoteCertificate(votes) - just := &vote.JustInitNo{ - QCert: cert, - } - s.signAddCPPreVote(*s.cpWeakValidity, s.cpRound, 0, just) - } else { - if prepares.HasVoted(s.valKey.Address()) { - preVotes := s.log.CPPreVoteVoteSet(s.round) - if !preVotes.HasFPlusOneVotesFor(s.cpRound, vote.CPValueYes) { - s.logger.Debug("we have proposal but not minority of pre-votes for 'Yes'") - - return - } - } - just := &vote.JustInitYes{} - s.signAddCPPreVote(hash.UndefHash, s.cpRound, 1, just) - } - s.scheduleTimeout(s.config.QueryVoteTimeout, s.height, s.round, tickerTargetQueryVotes) - } else { - cpMainVotes := s.log.CPMainVoteVoteSet(s.round) - switch { - case cpMainVotes.HasAnyVoteFor(s.cpRound-1, vote.CPValueYes): - s.logger.Debug("cp: one main-vote for one", "b", "1") - - vote1 := cpMainVotes.GetRandomVote(s.cpRound-1, vote.CPValueYes) - just1 := &vote.JustPreVoteHard{ - QCert: vote1.CPJust().(*vote.JustMainVoteNoConflict).QCert, - } - s.signAddCPPreVote(hash.UndefHash, s.cpRound, vote.CPValueYes, just1) - - case cpMainVotes.HasAnyVoteFor(s.cpRound-1, vote.CPValueNo): - s.logger.Debug("cp: one main-vote for zero", "b", "0") - - vote0 := cpMainVotes.GetRandomVote(s.cpRound-1, vote.CPValueNo) - just0 := &vote.JustPreVoteHard{ - QCert: vote0.CPJust().(*vote.JustMainVoteNoConflict).QCert, - } - s.signAddCPPreVote(*s.cpWeakValidity, s.cpRound, vote.CPValueNo, just0) - - case cpMainVotes.HasAllVotesFor(s.cpRound-1, vote.CPValueAbstain): - s.logger.Debug("cp: all main-votes are abstain", "b", "0 (biased)") - - votes := cpMainVotes.BinaryVotes(s.cpRound-1, vote.CPValueAbstain) - cert := s.makeVoteCertificate(votes) - just := &vote.JustPreVoteSoft{ - QCert: cert, - } - s.signAddCPPreVote(*s.cpWeakValidity, s.cpRound, vote.CPValueNo, just) - - default: - s.logger.Panic("protocol violated. We have combination of votes for one and zero") - } - } - - s.enterNewState(s.cpMainVoteState) -} - -func (s *cpPreVoteState) onAddVote(_ *vote.Vote) { - s.decide() -} - -func (*cpPreVoteState) onSetProposal(_ *proposal.Proposal) { -} - -func (*cpPreVoteState) onTimeout(_ *ticker) { -} - -func (*cpPreVoteState) name() string { - return "cp:pre-vote" -} diff --git a/fastconsensus/cp_test.go b/fastconsensus/cp_test.go deleted file mode 100644 index b4eeb3e78..000000000 --- a/fastconsensus/cp_test.go +++ /dev/null @@ -1,452 +0,0 @@ -package fastconsensus - -import ( - "fmt" - "testing" - - "github.com/pactus-project/pactus/crypto" - "github.com/pactus-project/pactus/crypto/hash" - "github.com/pactus-project/pactus/types/vote" - "github.com/stretchr/testify/assert" -) - -func TestChangeProposer(t *testing.T) { - td := setup(t) - - td.enterNewHeight(td.consP) - td.changeProposerTimeout(td.consP) - - td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, hash.UndefHash) -} - -func TestSetProposalAfterChangeProposer(t *testing.T) { - td := setup(t) - - td.commitBlockForAllStates(t) - - td.enterNewHeight(td.consP) - td.changeProposerTimeout(td.consP) - - td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, hash.UndefHash) - - prop := td.makeProposal(t, 2, 0) - td.consP.SetProposal(prop) - assert.NotNil(t, td.consP.Proposal()) -} - -func TestChangeProposerAgreement1(t *testing.T) { - td := setup(t) - - h := uint32(1) - r := int16(0) - td.enterNewHeight(td.consP) - td.checkHeightRound(t, td.consP, h, r) - - td.changeProposerTimeout(td.consP) - - preVote0 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, hash.UndefHash) - td.addCPPreVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, preVote0.CPJust(), tIndexX) - td.addCPPreVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, preVote0.CPJust(), tIndexY) - - mainVote0 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPMainVote, hash.UndefHash) - td.addCPMainVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, mainVote0.CPJust(), tIndexX) - td.addCPMainVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, mainVote0.CPJust(), tIndexY) - - td.shouldPublishVote(t, td.consP, vote.VoteTypeCPDecided, hash.UndefHash) - td.checkHeightRound(t, td.consP, h, r+1) -} - -func TestChangeProposerAgreement0(t *testing.T) { - td := setup(t) - - td.commitBlockForAllStates(t) // height 1 - - h := uint32(2) - r := int16(1) - td.enterNewHeight(td.consP) - td.enterNextRound(td.consP) - td.checkHeightRound(t, td.consP, h, r) - - prop := td.makeProposal(t, h, r) - blockHash := prop.Block().Hash() - - td.consP.SetProposal(prop) - td.addPrepareVote(td.consP, blockHash, h, r, tIndexX) - td.addPrepareVote(td.consP, blockHash, h, r, tIndexY) - td.addPrepareVote(td.consP, blockHash, h, r, tIndexM) - - td.changeProposerTimeout(td.consP) - - preVote0 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, blockHash) - td.addCPPreVote(td.consP, blockHash, h, r, vote.CPValueNo, preVote0.CPJust(), tIndexX) - td.addCPPreVote(td.consP, blockHash, h, r, vote.CPValueNo, preVote0.CPJust(), tIndexY) - - mainVote0 := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPMainVote, blockHash) - td.addCPMainVote(td.consP, blockHash, h, r, vote.CPValueNo, mainVote0.CPJust(), tIndexX) - td.addCPMainVote(td.consP, blockHash, h, r, vote.CPValueNo, mainVote0.CPJust(), tIndexY) - - td.shouldPublishVote(t, td.consP, vote.VoteTypeCPDecided, blockHash) - td.addPrecommitVote(td.consP, blockHash, h, r, tIndexX) - td.addPrecommitVote(td.consP, blockHash, h, r, tIndexY) - td.checkHeightRound(t, td.consP, h, r) -} - -// ConsP receives all PRE-VOTE:0 votes before receiving a proposal or prepare votes. -// It should vote PRE-VOTES:1 and MAIN-VOTE:0. -func TestCrashOnTestnet(t *testing.T) { - td := setup(t) - - td.commitBlockForAllStates(t) // height 1 - - h := uint32(2) - r := int16(0) - td.consP.MoveToNewHeight() - - blockHash := td.RandHash() - v1 := vote.NewPrepareVote(blockHash, h, r, td.consX.valKey.Address()) - v2 := vote.NewPrepareVote(blockHash, h, r, td.consY.valKey.Address()) - v3 := vote.NewPrepareVote(blockHash, h, r, td.consB.valKey.Address()) - - td.HelperSignVote(td.consX.valKey, v1) - td.HelperSignVote(td.consY.valKey, v2) - td.HelperSignVote(td.consB.valKey, v3) - - votes := map[crypto.Address]*vote.Vote{} - votes[v1.Signer()] = v1 - votes[v2.Signer()] = v2 - votes[v3.Signer()] = v3 - - cert := td.consP.makeVoteCertificate(votes) - just0 := &vote.JustInitNo{QCert: cert} - td.addCPPreVote(td.consP, blockHash, h, r, vote.CPValueNo, just0, tIndexX) - td.addCPPreVote(td.consP, blockHash, h, r, vote.CPValueNo, just0, tIndexY) - td.addCPPreVote(td.consP, blockHash, h, r, vote.CPValueNo, just0, tIndexB) - - td.newHeightTimeout(td.consP) - td.changeProposerTimeout(td.consP) - - preVote := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, hash.UndefHash) - mainVote := td.shouldPublishVote(t, td.consP, vote.VoteTypeCPMainVote, blockHash) - assert.Equal(t, vote.CPValueYes, preVote.CPValue()) - assert.Equal(t, vote.CPValueNo, mainVote.CPValue()) -} - -func TestInvalidJustInitYes(t *testing.T) { - td := setup(t) - - td.enterNewHeight(td.consX) - h := uint32(1) - r := int16(0) - just := &vote.JustInitYes{} - - t.Run("invalid value: no", func(t *testing.T) { - v := vote.NewCPPreVote(hash.UndefHash, h, r, 0, vote.CPValueNo, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: "invalid value: no", - }) - }) - - t.Run("cp-round should be 0", func(t *testing.T) { - v := vote.NewCPPreVote(hash.UndefHash, h, r, 1, vote.CPValueYes, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: "invalid round: 1", - }) - }) - - t.Run("invalid block hash", func(t *testing.T) { - blockHash := td.RandHash() - v := vote.NewCPPreVote(blockHash, h, r, 0, vote.CPValueYes, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: "invalid block hash: " + blockHash.String(), - }) - }) -} - -func TestInvalidJustInitNo(t *testing.T) { - td := setup(t) - - td.enterNewHeight(td.consX) - h := uint32(1) - r := int16(0) - just := &vote.JustInitNo{ - QCert: td.GenerateTestPrepareCertificate(h), - } - - t.Run("invalid value: yes", func(t *testing.T) { - v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueYes, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: "invalid value: yes", - }) - }) - - t.Run("cp-round should be 0", func(t *testing.T) { - v := vote.NewCPPreVote(td.RandHash(), h, r, 1, vote.CPValueNo, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: "invalid round: 1", - }) - }) - - t.Run("invalid certificate", func(t *testing.T) { - v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueNo, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.Error(t, err) - }) -} - -func TestInvalidJustPreVoteHard(t *testing.T) { - td := setup(t) - - td.enterNewHeight(td.consX) - h := uint32(1) - r := int16(0) - just := &vote.JustPreVoteHard{ - QCert: td.GenerateTestPrepareCertificate(h), - } - - t.Run("invalid value: abstain", func(t *testing.T) { - v := vote.NewCPPreVote(td.RandHash(), h, r, 1, vote.CPValueAbstain, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: "invalid value: abstain", - }) - }) - - t.Run("cp-round should not be 0", func(t *testing.T) { - v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueNo, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: "invalid round: 0", - }) - }) - - t.Run("invalid certificate", func(t *testing.T) { - v := vote.NewCPPreVote(td.RandHash(), h, r, 1, vote.CPValueNo, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just.QCert.Committers()), - }) - }) -} - -func TestInvalidJustPreVoteSoft(t *testing.T) { - td := setup(t) - - td.enterNewHeight(td.consX) - h := uint32(1) - r := int16(0) - just := &vote.JustPreVoteSoft{ - QCert: td.GenerateTestPrepareCertificate(h), - } - - t.Run("invalid value: abstain", func(t *testing.T) { - v := vote.NewCPPreVote(td.RandHash(), h, r, 1, vote.CPValueAbstain, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: "invalid value: abstain", - }) - }) - - t.Run("cp-round should not be 0", func(t *testing.T) { - v := vote.NewCPPreVote(td.RandHash(), h, r, 0, vote.CPValueNo, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: "invalid round: 0", - }) - }) - - t.Run("invalid certificate", func(t *testing.T) { - v := vote.NewCPPreVote(td.RandHash(), h, r, 1, vote.CPValueNo, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just.QCert.Committers()), - }) - }) -} - -func TestInvalidJustMainVoteNoConflict(t *testing.T) { - td := setup(t) - - td.enterNewHeight(td.consX) - h := uint32(1) - r := int16(0) - just := &vote.JustMainVoteNoConflict{ - QCert: td.GenerateTestPrepareCertificate(h), - } - - t.Run("invalid value: abstain", func(t *testing.T) { - v := vote.NewCPMainVote(td.RandHash(), h, r, 1, vote.CPValueAbstain, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: "invalid value: abstain", - }) - }) - - t.Run("invalid certificate", func(t *testing.T) { - v := vote.NewCPMainVote(td.RandHash(), h, r, 1, vote.CPValueNo, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just.QCert.Committers()), - }) - }) -} - -func TestInvalidJustMainVoteConflict(t *testing.T) { - td := setup(t) - - td.enterNewHeight(td.consX) - h := uint32(1) - r := int16(0) - - t.Run("invalid value: no", func(t *testing.T) { - just := &vote.JustMainVoteConflict{ - JustNo: &vote.JustInitNo{ - QCert: td.GenerateTestPrepareCertificate(h), - }, - JustYes: &vote.JustInitYes{}, - } - v := vote.NewCPMainVote(td.RandHash(), h, r, 0, vote.CPValueNo, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: "invalid value: no", - }) - }) - - t.Run("invalid value: yes", func(t *testing.T) { - just := &vote.JustMainVoteConflict{ - JustNo: &vote.JustInitNo{ - QCert: td.GenerateTestPrepareCertificate(h), - }, - JustYes: &vote.JustInitYes{}, - } - v := vote.NewCPMainVote(td.RandHash(), h, r, 0, vote.CPValueYes, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: "invalid value: yes", - }) - }) - - t.Run("invalid value: unexpected justification (justNo)", func(t *testing.T) { - just := &vote.JustMainVoteConflict{ - JustNo: &vote.JustInitYes{}, - JustYes: &vote.JustInitYes{}, - } - v := vote.NewCPMainVote(td.RandHash(), h, r, 0, vote.CPValueAbstain, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: "unexpected justification: JustInitYes", - }) - }) - - t.Run("invalid value: unexpected justification", func(t *testing.T) { - just := &vote.JustMainVoteConflict{ - JustNo: &vote.JustInitNo{ - QCert: td.GenerateTestPrepareCertificate(h), - }, - JustYes: &vote.JustPreVoteSoft{ - QCert: td.GenerateTestPrepareCertificate(h), - }, - } - v := vote.NewCPMainVote(td.RandHash(), h, r, 1, vote.CPValueAbstain, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: "invalid round: 1", - }) - }) - - t.Run("invalid certificate", func(t *testing.T) { - just0 := &vote.JustPreVoteSoft{ - QCert: td.GenerateTestPrepareCertificate(h), - } - just := &vote.JustMainVoteConflict{ - JustNo: just0, - JustYes: &vote.JustPreVoteSoft{ - QCert: td.GenerateTestPrepareCertificate(h), - }, - } - v := vote.NewCPMainVote(td.RandHash(), h, r, 1, vote.CPValueAbstain, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just0.QCert.Committers()), - }) - }) -} - -func TestInvalidJustDecided(t *testing.T) { - td := setup(t) - - td.enterNewHeight(td.consX) - h := uint32(1) - r := int16(0) - just := &vote.JustDecided{ - QCert: td.GenerateTestPrepareCertificate(h), - } - - t.Run("invalid value: abstain", func(t *testing.T) { - v := vote.NewCPDecidedVote(td.RandHash(), h, r, 0, vote.CPValueAbstain, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: "invalid value: abstain", - }) - }) - - t.Run("invalid certificate", func(t *testing.T) { - v := vote.NewCPDecidedVote(hash.UndefHash, h, r, 0, vote.CPValueYes, just, td.consB.valKey.Address()) - - err := td.consX.changeProposer.cpCheckJust(v) - assert.ErrorIs(t, err, invalidJustificationError{ - Reason: fmt.Sprintf("certificate has an unexpected committers: %v", just.QCert.Committers()), - }) - }) -} - -func TestMoveToNextRoundOnDecidedVoteYes(t *testing.T) { - td := setup(t) - - td.enterNewHeight(td.consP) - h := uint32(1) - r := int16(3) - - _, _, decideJust := td.makeChangeProposerJusts(t, hash.UndefHash, h, r) - td.addCPDecidedVote(td.consP, hash.UndefHash, h, r, vote.CPValueYes, decideJust, tIndexX) - - td.checkHeightRound(t, td.consP, h, r+1) -} - -func TestMoveToNextRoundOnDecidedVoteNo(t *testing.T) { - td := setup(t) - - td.enterNewHeight(td.consP) - h := uint32(1) - r := int16(3) - propHash := td.RandHash() - - _, _, decideJust := td.makeChangeProposerJusts(t, propHash, h, r) - td.addCPDecidedVote(td.consP, propHash, h, r, vote.CPValueNo, decideJust, tIndexX) - - td.checkHeightRound(t, td.consP, h, r) -} diff --git a/fastconsensus/errors.go b/fastconsensus/errors.go deleted file mode 100644 index 023c20558..000000000 --- a/fastconsensus/errors.go +++ /dev/null @@ -1,24 +0,0 @@ -package fastconsensus - -import ( - "fmt" -) - -// invalidJustificationError is returned when the justification for a change-proposer -// vote is invalid. -type invalidJustificationError struct { - Reason string -} - -func (e invalidJustificationError) Error() string { - return fmt.Sprintf("invalid justification: %s", e.Reason) -} - -// ConfigError is returned when the config is not valid with a descriptive Reason message. -type ConfigError struct { - Reason string -} - -func (e ConfigError) Error() string { - return e.Reason -} diff --git a/fastconsensus/height.go b/fastconsensus/height.go deleted file mode 100644 index c5a4012d2..000000000 --- a/fastconsensus/height.go +++ /dev/null @@ -1,60 +0,0 @@ -package fastconsensus - -import ( - "github.com/pactus-project/pactus/types/proposal" - "github.com/pactus-project/pactus/types/vote" - "github.com/pactus-project/pactus/util" -) - -type newHeightState struct { - *consensus -} - -func (s *newHeightState) enter() { - s.decide() -} - -func (s *newHeightState) decide() { - sateHeight := s.bcState.LastBlockHeight() - - validators := s.bcState.CommitteeValidators() - s.log.MoveToNewHeight(validators) - - s.validators = validators - s.height = sateHeight + 1 - s.round = 0 - s.blockCert = nil - s.active = s.bcState.IsInCommittee(s.valKey.Address()) - s.logger.Info("entering new height", "height", s.height, "active", s.active) - - sleep := s.bcState.LastBlockTime().Add(s.bcState.Params().BlockInterval()).Sub(util.Now()) - s.scheduleTimeout(sleep, s.height, s.round, tickerTargetNewHeight) -} - -func (s *newHeightState) onAddVote(_ *vote.Vote) { - prepares := s.log.PrepareVoteSet(s.round) - if prepares.HasQuorumHash() { - // Added logic to detect when the network majority has voted for a block, - // but the new height timer has not yet started. This situation can occur if the system - // time is lagging behind the network time. - s.logger.Warn("detected network majority voting for a block, but the new height timer has not started yet. " + - "system time may be behind the network.") - s.enterNewState(s.proposeState) - } -} - -func (*newHeightState) onSetProposal(_ *proposal.Proposal) { - // Ignore proposal -} - -func (s *newHeightState) onTimeout(t *ticker) { - if t.Target == tickerTargetNewHeight { - if s.active { - s.enterNewState(s.proposeState) - } - } -} - -func (*newHeightState) name() string { - return "new-height" -} diff --git a/fastconsensus/height_test.go b/fastconsensus/height_test.go deleted file mode 100644 index 1085d376d..000000000 --- a/fastconsensus/height_test.go +++ /dev/null @@ -1,63 +0,0 @@ -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 deleted file mode 100644 index 0637450ce..000000000 --- a/fastconsensus/interface.go +++ /dev/null @@ -1,47 +0,0 @@ -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 - IsProposer() bool -} - -type Consensus interface { - Reader - - Start() - MoveToNewHeight() - AddVote(vte *vote.Vote) - SetProposal(prop *proposal.Proposal) -} - -type ManagerReader interface { - Instances() []Reader - PickRandomVote(round int16) *vote.Vote - Proposal() *proposal.Proposal - HeightRound() (uint32, int16) - HasActiveInstance() bool - HasProposer() bool -} - -type Manager interface { - ManagerReader - - Start() error - Stop() - MoveToNewHeight() - AddVote(vte *vote.Vote) - SetProposal(prop *proposal.Proposal) -} diff --git a/fastconsensus/log/log.go b/fastconsensus/log/log.go deleted file mode 100644 index c3f75c26b..000000000 --- a/fastconsensus/log/log.go +++ /dev/null @@ -1,126 +0,0 @@ -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.NewCPDecidedVoteSet(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) CPDecidedVoteSet(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 deleted file mode 100644 index 2cef01594..000000000 --- a/fastconsensus/log/log_test.go +++ /dev/null @@ -1,105 +0,0 @@ -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 deleted file mode 100644 index c5cf5db13..000000000 --- a/fastconsensus/log/messages.go +++ /dev/null @@ -1,58 +0,0 @@ -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 deleted file mode 100644 index f1d95fa49..000000000 --- a/fastconsensus/manager.go +++ /dev/null @@ -1,220 +0,0 @@ -package fastconsensus - -import ( - "github.com/pactus-project/pactus/crypto" - "github.com/pactus-project/pactus/crypto/bls" - "github.com/pactus-project/pactus/state" - "github.com/pactus-project/pactus/sync/bundle/message" - "github.com/pactus-project/pactus/types/proposal" - "github.com/pactus-project/pactus/types/vote" - "github.com/pactus-project/pactus/util/logger" - "golang.org/x/exp/slices" -) - -type manager struct { - instances []Consensus - - // Caching future votes and proposals due to potential server time misalignments. - // Votes and proposals for upcoming blocks may be received before - // the current block's consensus is complete. - upcomingVotes []*vote.Vote // Map to cache votes for future block heights - upcomingProposals []*proposal.Proposal // Map to cache proposals for future block heights - state state.Facade -} - -// NewManager creates a new manager instance that manages a set of consensus instances, -// each associated with a validator key and a reward address. -// It is not thread-safe. -func NewManager( - conf *Config, - st state.Facade, - valKeys []*bls.ValidatorKey, - rewardAddrs []crypto.Address, - broadcastCh chan message.Message, -) Manager { - mgr := &manager{ - instances: make([]Consensus, len(valKeys)), - upcomingVotes: make([]*vote.Vote, 0), - upcomingProposals: make([]*proposal.Proposal, 0), - state: st, - } - mediatorConcrete := newConcreteMediator() - - for i, key := range valKeys { - cons := NewConsensus(conf, st, key, rewardAddrs[i], broadcastCh, mediatorConcrete) - - mgr.instances[i] = cons - } - - return mgr -} - -// Start starts the manager. -func (mgr *manager) Start() error { - logger.Debug("starting consensus instances") - for _, cons := range mgr.instances { - cons.Start() - } - - return nil -} - -// Stop stops the manager. -func (*manager) Stop() { -} - -// Instances return all consensus instances that are read-only and -// can be safely accessed without modifying their state. -func (mgr *manager) Instances() []Reader { - readers := make([]Reader, len(mgr.instances)) - for i, cons := range mgr.instances { - readers[i] = cons - } - - return readers -} - -// PickRandomVote returns a random vote from a random consensus instance. -func (mgr *manager) PickRandomVote(round int16) *vote.Vote { - cons := mgr.getBestInstance() - - return cons.PickRandomVote(round) -} - -// Proposal returns the proposal for a specific round from a random consensus instance. -func (mgr *manager) Proposal() *proposal.Proposal { - cons := mgr.getBestInstance() - - return cons.Proposal() -} - -// HeightRound retrieves the current height and round from a random consensus instance. -func (mgr *manager) HeightRound() (uint32, int16) { - cons := mgr.getBestInstance() - - return cons.HeightRound() -} - -// HasActiveInstance checks if any of the consensus instances are currently active. -func (mgr *manager) HasActiveInstance() bool { - for _, cons := range mgr.instances { - if cons.IsActive() { - return true - } - } - - return false -} - -// HasProposer checks if any of the consensus instances is the proposer -// for the current round. -func (mgr *manager) HasProposer() bool { - for _, cons := range mgr.instances { - if cons.IsProposer() { - 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 deleted file mode 100644 index 3b6c69e89..000000000 --- a/fastconsensus/manager_test.go +++ /dev/null @@ -1,195 +0,0 @@ -package fastconsensus - -import ( - "testing" - - "github.com/pactus-project/pactus/crypto" - "github.com/pactus-project/pactus/crypto/bls" - "github.com/pactus-project/pactus/state" - "github.com/pactus-project/pactus/sync/bundle/message" - "github.com/pactus-project/pactus/types/proposal" - "github.com/pactus-project/pactus/types/vote" - "github.com/pactus-project/pactus/util/logger" - "github.com/pactus-project/pactus/util/testsuite" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestManager(t *testing.T) { - ts := testsuite.NewTestSuite(t) - - st := state.MockingState(ts) - st.TestCommittee.Validators() - - rewardAddrs := []crypto.Address{ts.RandAccAddress(), ts.RandAccAddress()} - valKeys := []*bls.ValidatorKey{st.TestValKeys[0], ts.RandValKey()} - broadcastCh := make(chan message.Message, 500) - - randomHeight := ts.RandHeight() - blk, cert := ts.GenerateTestBlock(randomHeight) - st.TestStore.SaveBlock(blk, cert) - - mgrInst := NewManager(testConfig(), st, valKeys, rewardAddrs, broadcastCh) - mgr := mgrInst.(*manager) - - consA := mgr.instances[0].(*consensus) // active - consB := mgr.instances[1].(*consensus) // inactive - - t.Run("Check if keys are assigned properly", func(t *testing.T) { - assert.Equal(t, valKeys[0].PublicKey(), consA.ConsensusKey()) - assert.Equal(t, valKeys[1].PublicKey(), consB.ConsensusKey()) - }) - - t.Run("Check if all instances move to new height", func(t *testing.T) { - stateHeight := mgr.state.LastBlockHeight() - assert.False(t, mgr.HasActiveInstance()) - - mgr.MoveToNewHeight() - consHeight, consRound := mgr.HeightRound() - - assert.True(t, mgr.HasActiveInstance()) - assert.Equal(t, consHeight, stateHeight+1) - assert.Zero(t, consRound) - }) - - t.Run("Testing add vote", func(t *testing.T) { - consHeight, _ := mgr.HeightRound() - v := vote.NewPrepareVote(ts.RandHash(), consHeight, 0, valKeys[0].Address()) - ts.HelperSignVote(valKeys[0], v) - - mgr.AddVote(v) - - assert.True(t, consA.HasVote(v.Hash())) - assert.False(t, consB.HasVote(v.Hash())) - }) - - t.Run("Testing set proposal", func(t *testing.T) { - consHeight, _ := mgr.HeightRound() - b, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) - p := proposal.NewProposal(consHeight, 0, b) - ts.HelperSignProposal(valKeys[0], p) - - mgr.SetProposal(p) - - assert.Equal(t, p, consA.Proposal()) - assert.Nil(t, consB.Proposal()) - }) - - t.Run("Check discarding old votes", func(t *testing.T) { - consHeight, _ := mgr.HeightRound() - v := vote.NewPrepareVote(ts.RandHash(), consHeight-1, 0, st.TestValKeys[2].Address()) - ts.HelperSignVote(st.TestValKeys[2], v) - - mgr.AddVote(v) - assert.Empty(t, mgr.upcomingVotes) - }) - - t.Run("Check discarding old proposals", func(t *testing.T) { - consHeight, _ := mgr.HeightRound() - b, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) - p := proposal.NewProposal(consHeight-1, 1, b) - ts.HelperSignProposal(valKeys[0], p) - - mgr.SetProposal(p) - assert.Empty(t, mgr.upcomingProposals) - }) - - t.Run("Processing upcoming votes", func(t *testing.T) { - consHeight, _ := mgr.HeightRound() - v1 := vote.NewPrepareVote(ts.RandHash(), consHeight+1, 0, valKeys[0].Address()) - v2 := vote.NewPrepareVote(ts.RandHash(), consHeight+2, 0, valKeys[0].Address()) - v3 := vote.NewPrepareVote(ts.RandHash(), consHeight+3, 0, valKeys[0].Address()) - - ts.HelperSignVote(valKeys[0], v1) - ts.HelperSignVote(valKeys[0], v2) - ts.HelperSignVote(valKeys[0], v3) - - mgr.AddVote(v1) - mgr.AddVote(v2) - mgr.AddVote(v3) - - assert.Len(t, mgr.upcomingVotes, 3) - - blk1, cert1 := ts.GenerateTestBlock(consHeight) - err := st.CommitBlock(blk1, cert1) - assert.NoError(t, err) - - blk2, cert2 := ts.GenerateTestBlock(consHeight + 1) - err = st.CommitBlock(blk2, cert2) - assert.NoError(t, err) - - mgr.MoveToNewHeight() - - assert.Len(t, mgr.upcomingVotes, 1) - }) - - t.Run("Processing upcoming proposal", func(t *testing.T) { - consHeight, _ := mgr.HeightRound() - b1, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) - p1 := proposal.NewProposal(consHeight+1, 0, b1) - - b2, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) - p2 := proposal.NewProposal(consHeight+2, 0, b2) - - b3, _ := st.ProposeBlock(valKeys[0], valKeys[0].Address()) - p3 := proposal.NewProposal(consHeight+3, 0, b3) - - ts.HelperSignProposal(valKeys[0], p1) - ts.HelperSignProposal(valKeys[0], p2) - ts.HelperSignProposal(valKeys[0], p3) - - mgr.SetProposal(p1) - mgr.SetProposal(p2) - mgr.SetProposal(p3) - - assert.Len(t, mgr.upcomingProposals, 3) - - blk1, cert1 := ts.GenerateTestBlock(consHeight) - err := st.CommitBlock(blk1, cert1) - assert.NoError(t, err) - - blk2, cert2 := ts.GenerateTestBlock(consHeight + 1) - err = st.CommitBlock(blk2, cert2) - assert.NoError(t, err) - - mgr.MoveToNewHeight() - - assert.Len(t, mgr.upcomingProposals, 1) - }) -} - -func TestMediator(t *testing.T) { - ts := testsuite.NewTestSuite(t) - - st := state.MockingState(ts) - cmt, valKeys := ts.GenerateTestCommittee(4) - st.TestCommittee = cmt - st.TestParams.BlockIntervalInSecond = 1 - - rewardAddrs := []crypto.Address{ - ts.RandAccAddress(), ts.RandAccAddress(), - ts.RandAccAddress(), ts.RandAccAddress(), - } - broadcastCh := make(chan message.Message, 500) - - stateHeight := ts.RandHeight() - blk, cert := ts.GenerateTestBlock(stateHeight) - st.TestStore.SaveBlock(blk, cert) - - mgrInst := NewManager(testConfig(), st, valKeys, rewardAddrs, broadcastCh) - mgr := mgrInst.(*manager) - - mgr.MoveToNewHeight() - - for { - msg := <-broadcastCh - logger.Info("shouldPublishProposal", "msg", msg) - - m, ok := msg.(*message.BlockAnnounceMessage) - if ok { - require.Equal(t, m.Height(), stateHeight+1) - - return - } - } -} diff --git a/fastconsensus/mediator.go b/fastconsensus/mediator.go deleted file mode 100644 index 29ba371f4..000000000 --- a/fastconsensus/mediator.go +++ /dev/null @@ -1,53 +0,0 @@ -package fastconsensus - -import ( - "github.com/pactus-project/pactus/types/proposal" - "github.com/pactus-project/pactus/types/vote" -) - -// The `mediator“ interface defines a mechanism for setting proposals and votes -// between independent consensus instances. -type mediator interface { - OnPublishProposal(from Consensus, prop *proposal.Proposal) - OnPublishVote(from Consensus, vte *vote.Vote) - OnBlockAnnounce(from Consensus) - Register(cons Consensus) -} - -// ConcreteMediator struct. -type ConcreteMediator struct { - instances []Consensus -} - -func newConcreteMediator() mediator { - return &ConcreteMediator{} -} - -func (m *ConcreteMediator) OnPublishProposal(from Consensus, prop *proposal.Proposal) { - for _, cons := range m.instances { - if cons != from { - cons.SetProposal(prop) - } - } -} - -func (m *ConcreteMediator) OnPublishVote(from Consensus, vte *vote.Vote) { - for _, cons := range m.instances { - if cons != from { - cons.AddVote(vte) - } - } -} - -func (m *ConcreteMediator) OnBlockAnnounce(from Consensus) { - for _, cons := range m.instances { - if cons != from { - cons.MoveToNewHeight() - } - } -} - -// Register a new Consensus instance to the mediator. -func (m *ConcreteMediator) Register(cons Consensus) { - m.instances = append(m.instances, cons) -} diff --git a/fastconsensus/mock.go b/fastconsensus/mock.go deleted file mode 100644 index 82af6c773..000000000 --- a/fastconsensus/mock.go +++ /dev/null @@ -1,148 +0,0 @@ -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 - Proposer bool - Height uint32 - Round int16 -} - -func MockingManager(ts *testsuite.TestSuite, valKeys []*bls.ValidatorKey) (Manager, []*MockConsensus) { - mocks := make([]*MockConsensus, len(valKeys)) - instances := make([]Consensus, len(valKeys)) - for i, s := range valKeys { - cons := MockingConsensus(ts, s) - mocks[i] = cons - instances[i] = cons - } - - return &manager{ - instances: instances, - upcomingVotes: make([]*vote.Vote, 0), - upcomingProposals: make([]*proposal.Proposal, 0), - }, mocks -} - -func MockingConsensus(ts *testsuite.TestSuite, valKey *bls.ValidatorKey) *MockConsensus { - return &MockConsensus{ - ts: ts, - ValKey: valKey, - } -} - -func (m *MockConsensus) ConsensusKey() *bls.PublicKey { - return m.ValKey.PublicKey() -} - -func (m *MockConsensus) MoveToNewHeight() { - m.lk.Lock() - defer m.lk.Unlock() - - m.Height++ -} - -func (*MockConsensus) Start() {} - -func (m *MockConsensus) AddVote(v *vote.Vote) { - m.lk.Lock() - defer m.lk.Unlock() - - m.Votes = append(m.Votes, v) -} - -func (m *MockConsensus) AllVotes() []*vote.Vote { - m.lk.Lock() - defer m.lk.Unlock() - - return m.Votes -} - -func (m *MockConsensus) SetProposal(p *proposal.Proposal) { - m.lk.Lock() - defer m.lk.Unlock() - - m.CurProposal = p -} - -func (m *MockConsensus) HasVote(h hash.Hash) bool { - m.lk.Lock() - defer m.lk.Unlock() - - for _, v := range m.Votes { - if v.Hash() == h { - return true - } - } - - return false -} - -func (m *MockConsensus) Proposal() *proposal.Proposal { - m.lk.Lock() - defer m.lk.Unlock() - - return m.CurProposal -} - -func (m *MockConsensus) HeightRound() (uint32, int16) { - m.lk.Lock() - defer m.lk.Unlock() - - return m.Height, m.Round -} - -func (*MockConsensus) String() string { - return "" -} - -func (m *MockConsensus) PickRandomVote(_ int16) *vote.Vote { - m.lk.Lock() - defer m.lk.Unlock() - - if len(m.Votes) == 0 { - return nil - } - r := m.ts.RandInt32(int32(len(m.Votes))) - - return m.Votes[r] -} - -func (m *MockConsensus) IsActive() bool { - m.lk.Lock() - defer m.lk.Unlock() - - return m.Active -} - -func (m *MockConsensus) IsProposer() bool { - m.lk.Lock() - defer m.lk.Unlock() - - return m.Proposer -} - -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 deleted file mode 100644 index 726b88207..000000000 --- a/fastconsensus/precommit.go +++ /dev/null @@ -1,76 +0,0 @@ -package fastconsensus - -import ( - "github.com/pactus-project/pactus/types/proposal" - "github.com/pactus-project/pactus/types/vote" -) - -type precommitState struct { - *consensus - hasVoted bool -} - -func (s *precommitState) enter() { - s.hasVoted = false - - s.decide() -} - -func (s *precommitState) decide() { - s.vote() - s.strongCommit() - - precommits := s.log.PrecommitVoteSet(s.round) - precommitQH := precommits.QuorumHash() - if precommitQH != nil { - s.logger.Debug("pre-commit has quorum", "hash", precommitQH) - - roundProposal := s.log.RoundProposal(s.round) - if roundProposal == nil { - // There is a consensus about a proposal that we don't have yet. - // Ask peers for this proposal. - s.logger.Info("query for a decided proposal", "precommitQH", precommitQH) - s.queryProposal() - - return - } - - votes := precommits.BlockVotes(*precommitQH) - s.blockCert = s.makeBlockCertificate(votes, false) - - s.enterNewState(s.commitState) - } -} - -func (s *precommitState) vote() { - if s.hasVoted { - return - } - - roundProposal := s.log.RoundProposal(s.round) - if roundProposal == nil { - s.logger.Debug("no proposal yet") - - return - } - - // Everything is good - s.signAddPrecommitVote(roundProposal.Block().Hash()) - s.hasVoted = true -} - -func (s *precommitState) onAddVote(_ *vote.Vote) { - s.decide() -} - -func (s *precommitState) onSetProposal(_ *proposal.Proposal) { - s.decide() -} - -func (*precommitState) onTimeout(_ *ticker) { - // Ignore timeouts -} - -func (*precommitState) name() string { - return "precommit" -} diff --git a/fastconsensus/precommit_test.go b/fastconsensus/precommit_test.go deleted file mode 100644 index f1e34f5aa..000000000 --- a/fastconsensus/precommit_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package fastconsensus - -import ( - "testing" - - "github.com/pactus-project/pactus/types/vote" - "github.com/stretchr/testify/assert" -) - -func TestPrecommitQueryProposal(t *testing.T) { - td := setup(t) - - td.commitBlockForAllStates(t) - h := uint32(2) - r := int16(0) - - td.enterNewHeight(td.consP) - td.changeProposerTimeout(td.consP) - - prop := td.makeProposal(t, h, r) - propBlockHash := prop.Block().Hash() - - _, _, decidedJust := td.makeChangeProposerJusts(t, propBlockHash, h, r) - - decideVote := vote.NewCPDecidedVote(propBlockHash, h, r, 0, vote.CPValueNo, decidedJust, td.consX.valKey.Address()) - td.HelperSignVote(td.consX.valKey, decideVote) - - td.consP.AddVote(decideVote) - assert.Equal(t, "precommit", td.consP.currentState.name()) - - td.addPrecommitVote(td.consP, propBlockHash, h, r, tIndexX) - td.addPrecommitVote(td.consP, propBlockHash, h, r, tIndexY) - td.addPrecommitVote(td.consP, propBlockHash, h, r, tIndexM) - td.addPrecommitVote(td.consP, propBlockHash, h, r, tIndexN) - - td.shouldPublishQueryProposal(t, td.consP, h, r) -} diff --git a/fastconsensus/prepare.go b/fastconsensus/prepare.go deleted file mode 100644 index 5c3317ef3..000000000 --- a/fastconsensus/prepare.go +++ /dev/null @@ -1,83 +0,0 @@ -package fastconsensus - -import ( - "github.com/pactus-project/pactus/types/proposal" - "github.com/pactus-project/pactus/types/vote" -) - -type prepareState struct { - *consensus - hasVoted bool -} - -func (s *prepareState) enter() { - s.hasVoted = false - - changeProperTimeout := s.config.CalculateChangeProposerTimeout(s.round) - queryProposalTimeout := changeProperTimeout / 2 - s.scheduleTimeout(queryProposalTimeout, s.height, s.round, tickerTargetQueryProposal) - s.scheduleTimeout(changeProperTimeout, s.height, s.round, tickerTargetChangeProposer) - - s.decide() -} - -func (s *prepareState) decide() { - s.vote() - s.strongCommit() - - // - // If a validator receives a set of f+1 valid cp:PRE-VOTE votes for this round, - // it starts changing the proposer phase, even if its timer has not expired; - // This prevents it from starting the change-proposer phase too late. - // - cpPreVotes := s.log.CPPreVoteVoteSet(s.round) - if cpPreVotes.HasFPlusOneVotesFor(0, vote.CPValueYes) { - s.startChangingProposer() - } -} - -func (s *prepareState) vote() { - if s.hasVoted { - return - } - - roundProposal := s.log.RoundProposal(s.round) - if roundProposal == nil { - s.logger.Debug("no proposal yet") - - return - } - - // Everything is good - s.signAddPrepareVote(roundProposal.Block().Hash()) - s.hasVoted = true -} - -func (s *prepareState) onTimeout(t *ticker) { - if t.Target == tickerTargetQueryProposal { - roundProposal := s.log.RoundProposal(s.round) - if roundProposal == nil { - s.queryProposal() - } - if s.isProposer() { - s.queryVotes() - } - } else if t.Target == tickerTargetChangeProposer { - s.startChangingProposer() - } -} - -func (s *prepareState) onAddVote(v *vote.Vote) { - if v.Type() == vote.VoteTypePrepare || - v.Type() == vote.VoteTypeCPPreVote { - s.decide() - } -} - -func (s *prepareState) onSetProposal(_ *proposal.Proposal) { - s.decide() -} - -func (*prepareState) name() string { - return "prepare" -} diff --git a/fastconsensus/prepare_test.go b/fastconsensus/prepare_test.go deleted file mode 100644 index 49e6d6acc..000000000 --- a/fastconsensus/prepare_test.go +++ /dev/null @@ -1,104 +0,0 @@ -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, 1) - td.shouldNotPublish(t, td.consP, message.TypeQueryVote) -} - -func TestQueryVotes(t *testing.T) { - td := setup(t) - - td.commitBlockForAllStates(t) - td.commitBlockForAllStates(t) - h := uint32(3) - r := int16(1) - - td.enterNewHeight(td.consP) - td.enterNextRound(td.consP) - - // consP is the proposer for this round, but there are not enough votes. - td.queryProposalTimeout(td.consP) - td.shouldPublishProposal(t, td.consP, h, r) - td.shouldPublishQueryVote(t, td.consP, h, r) - td.shouldNotPublish(t, td.consP, message.TypeQueryProposal) -} - -func TestGoToChangeProposerFromPrepare(t *testing.T) { - td := setup(t) - - td.commitBlockForAllStates(t) - - td.enterNewHeight(td.consP) - - td.addCPPreVote(td.consP, hash.UndefHash, 2, 0, vote.CPValueYes, &vote.JustInitYes{}, tIndexX) - td.addCPPreVote(td.consP, hash.UndefHash, 2, 0, vote.CPValueYes, &vote.JustInitYes{}, tIndexY) - - // should move to the change proposer phase, even if it has the proposal and - // its timer has not expired, if it has received 1/3 of the change-proposer votes. - prop := td.makeProposal(t, 2, 0) - td.consP.SetProposal(prop) - td.shouldPublishVote(t, td.consP, vote.VoteTypeCPPreVote, hash.UndefHash) -} - -func TestByzantineProposal(t *testing.T) { - td := setup(t) - - td.commitBlockForAllStates(t) - td.commitBlockForAllStates(t) - h := uint32(3) - r := int16(0) - prop := td.makeProposal(t, h, r) - propBlockHash := prop.Block().Hash() - - td.enterNewHeight(td.consP) - - td.addPrepareVote(td.consP, propBlockHash, h, r, tIndexX) - td.addPrepareVote(td.consP, propBlockHash, h, r, tIndexY) - td.addPrepareVote(td.consP, propBlockHash, h, r, tIndexB) - td.addPrepareVote(td.consP, propBlockHash, h, r, tIndexM) - td.addPrepareVote(td.consP, propBlockHash, h, r, tIndexN) - - assert.Nil(t, td.consP.Proposal()) - td.shouldPublishQueryProposal(t, td.consP, h, r) - - // 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, r) - td.checkHeightRound(t, td.consP, h, r) -} diff --git a/fastconsensus/propose.go b/fastconsensus/propose.go deleted file mode 100644 index 1adaaaba5..000000000 --- a/fastconsensus/propose.go +++ /dev/null @@ -1,81 +0,0 @@ -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 the Minimum threshold, - // we initiate the Change-Proposer phase. - if score < s.config.MinimumAvailabilityScore { - s.logger.Info("availability score of proposer is low", - "score", score, "proposer", proposer.Address()) - s.startChangingProposer() - } else { - s.enterNewState(s.prepareState) - } -} - -func (s *proposeState) createProposal(height uint32, round int16) { - block, err := s.bcState.ProposeBlock(s.valKey, s.rewardAddr) - if err != nil { - s.logger.Error("unable to propose a block!", "error", err) - - return - } - if err := s.bcState.ValidateBlock(block, round); err != nil { - s.logger.Error("proposed block is invalid!", "error", err) - - return - } - - prop := proposal.NewProposal(height, round, block) - sig := s.valKey.Sign(prop.SignBytes()) - prop.SetSignature(sig) - - s.log.SetRoundProposal(round, prop) - - s.broadcastProposal(prop) - - s.logger.Info("proposal signed and broadcasted", "proposal", prop) -} - -func (*proposeState) onAddVote(_ *vote.Vote) { - panic("Unreachable") -} - -func (*proposeState) onSetProposal(_ *proposal.Proposal) { - panic("Unreachable") -} - -func (*proposeState) onTimeout(_ *ticker) { - panic("Unreachable") -} - -func (*proposeState) name() string { - return "propose" -} diff --git a/fastconsensus/propose_test.go b/fastconsensus/propose_test.go deleted file mode 100644 index c0d699cb2..000000000 --- a/fastconsensus/propose_test.go +++ /dev/null @@ -1,108 +0,0 @@ -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, r) - - // 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 deleted file mode 100644 index 72a0d554a..000000000 --- a/fastconsensus/spec/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -*.dot -*.out -*.toolbox -states -*.tex -*.dvi -*.aux -*.log diff --git a/fastconsensus/spec/Pactus.cfg b/fastconsensus/spec/Pactus.cfg deleted file mode 100644 index b42182dfa..000000000 --- a/fastconsensus/spec/Pactus.cfg +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 5fb24d905a040a8c767436b22a897f323ab9556d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 189468 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

-lw@BV(C_skywgExRwKF9J=`PRa}k35%Y38urmU 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 IXQt2kPa$^@e*gdg diff --git a/fastconsensus/spec/Pactus.tla b/fastconsensus/spec/Pactus.tla deleted file mode 100644 index 060d28733..000000000 --- a/fastconsensus/spec/Pactus.tla +++ /dev/null @@ -1,550 +0,0 @@ --------------------------------- MODULE Pactus -------------------------------- -(***************************************************************************) -(* The specification of the Pactus consensus algorithm: *) -(* `^\url{https://pactus.org/learn/consensus/protocol/}^' *) -(***************************************************************************) -EXTENDS Integers, Sequences, FiniteSets, TLC - -CONSTANT - \* The maximum number of height. - \* This limits the range of behaviors evaluated by TLC - MaxHeight, - \* The maximum number of round per height. - \* This limits the range of behaviors evaluated by TLC - MaxRound, - \* The maximum number of cp-round per height. - \* This limits the range of behaviors evaluated by TLC - MaxCPRound, - \* The total number of nodes in the network, - \* denoted as `n` in the protocol. - n, - \* The maximum number of faulty node in change-proposer phase, - \* denoted as `f` in the protocol. - f, - \* The maximum number of faulty node in block-creation phase, - \* denoted as `t` in the protocol. - t, - \* The indices of faulty nodes. - FaultyNodes - -VARIABLES - \* `log` is a set of messages received by the system. - log, - \* `states` represents the state of each replica in the consensus protocol. - states - -\* TwoFPlusOne is equal to `2f+1' -TwoFPlusOne == (2 * f) + 1 -\* OneFPlusOne is equal to `f+1' -OneFPlusOne == (1 * f) + 1 - -\* FourTPlusOne is equal to `4t+1' -FourTPlusOne == (4 * t) + 1 -\* ThreeTPlusOne is equal to `3t+1' -ThreeTPlusOne == (3 * t) + 1 - -\* A tuple containing all variables in the spec (for ease of use in temporal conditions). -vars == <> - -ASSUME - \* Ensure that the number of nodes is sufficient to tolerate the specified number of faults - \* in change-proposer phase. - /\ n >= (3*f)+1 - \* Ensure that the number of nodes is sufficient to tolerate the specified number of faults - \* in block-creation phase. - /\ n >= (5*t)+1 - \* Ensure that `FaultyNodes` is a valid subset of node indices. - /\ FaultyNodes \subseteq 0..n-1 - ------------------------------------------------------------------------------ -(***************************************************************************) -(* Helper functions *) -(***************************************************************************) - -\* Fetch a subset of messages in the network based on the params filter. -SubsetOfMsgs(params) == - {msg \in log: \A field \in DOMAIN params: msg[field] = params[field]} - -\* IsProposer checks if the replica is the proposer for this round. -\* To simplify, we assume the proposer always starts with the first replica, -\* and moves to the next by the change-proposer phase. -IsProposer(index) == - states[index].round % n = index - -\* IsFaulty checks if a node is faulty or not. -IsFaulty(index) == index \in FaultyNodes - -\* HasPrepareAbsoluteQuorum checks whether the node with the given index -\* has received `4t+1` PREPARE votes for a proposal. -HasPrepareAbsoluteQuorum(index) == - Cardinality(SubsetOfMsgs([ - type |-> "PREPARE", - height |-> states[index].height, - round |-> states[index].round])) >= FourTPlusOne - -\* HasPrepareQuorum checks whether the node with the given index -\* has received `3t+1` PREPARE votes for a proposal. -HasPrepareQuorum(index) == - Cardinality(SubsetOfMsgs([ - type |-> "PREPARE", - height |-> states[index].height, - round |-> states[index].round])) >= ThreeTPlusOne - -\* HasPrecommitQuorum checks whether the node with the given index -\* has received `3t+1` the PRECOMMIT votes for a proposal. -HasPrecommitQuorum(index) == - Cardinality(SubsetOfMsgs([ - type |-> "PRECOMMIT", - height |-> states[index].height, - round |-> states[index].round])) >= ThreeTPlusOne - -CPHasPreVotesMinorityQuorum(index) == - Cardinality(SubsetOfMsgs([ - type |-> "CP:PRE-VOTE", - height |-> states[index].height, - round |-> states[index].round, - cp_round |-> 0, - cp_val |-> 1])) >= OneFPlusOne - -CPHasPreVotesQuorum(index) == - Cardinality(SubsetOfMsgs([ - type |-> "CP:PRE-VOTE", - height |-> states[index].height, - round |-> states[index].round, - cp_round |-> states[index].cp_round])) >= TwoFPlusOne - -CPHasPreVotesQuorumForOne(index) == - Cardinality(SubsetOfMsgs([ - type |-> "CP:PRE-VOTE", - height |-> states[index].height, - round |-> states[index].round, - cp_round |-> states[index].cp_round, - cp_val |-> 1])) >= TwoFPlusOne - -CPHasPreVotesQuorumForZero(index) == - Cardinality(SubsetOfMsgs([ - type |-> "CP:PRE-VOTE", - height |-> states[index].height, - round |-> states[index].round, - cp_round |-> states[index].cp_round, - cp_val |-> 0])) >= TwoFPlusOne - -CPHasPreVotesForZeroAndOne(index) == - /\ Cardinality(SubsetOfMsgs([ - type |-> "CP:PRE-VOTE", - height |-> states[index].height, - round |-> states[index].round, - cp_round |-> states[index].cp_round, - cp_val |-> 0])) >= 1 - /\ Cardinality(SubsetOfMsgs([ - type |-> "CP:PRE-VOTE", - height |-> states[index].height, - round |-> states[index].round, - cp_round |-> states[index].cp_round, - cp_val |-> 1])) >= 1 - -CPHasAMainVotesZeroInPrvRound(index) == - Cardinality(SubsetOfMsgs([ - type |-> "CP:MAIN-VOTE", - height |-> states[index].height, - round |-> states[index].round, - cp_round |-> states[index].cp_round - 1, - cp_val |-> 0])) > 0 - -CPHasAMainVotesOneInPrvRound(index) == - Cardinality(SubsetOfMsgs([ - type |-> "CP:MAIN-VOTE", - height |-> states[index].height, - round |-> states[index].round, - cp_round |-> states[index].cp_round - 1, - cp_val |-> 1])) > 0 - -CPAllMainVotesAbstainInPrvRound(index) == - Cardinality(SubsetOfMsgs([ - type |-> "CP:MAIN-VOTE", - height |-> states[index].height, - round |-> states[index].round, - cp_round |-> states[index].cp_round - 1, - cp_val |-> 2])) >= TwoFPlusOne - -CPOneFPlusOneMainVotesAbstainInPrvRound(index) == - Cardinality(SubsetOfMsgs([ - type |-> "CP:MAIN-VOTE", - height |-> states[index].height, - round |-> states[index].round, - cp_round |-> states[index].cp_round - 1, - cp_val |-> 2])) >= OneFPlusOne - -CPHasMainVotesQuorum(index) == - Cardinality(SubsetOfMsgs([ - type |-> "CP:MAIN-VOTE", - height |-> states[index].height, - round |-> states[index].round, - cp_round |-> states[index].cp_round])) >= TwoFPlusOne - -CPHasMainVotesQuorumForOne(index) == - Cardinality(SubsetOfMsgs([ - type |-> "CP:MAIN-VOTE", - height |-> states[index].height, - round |-> states[index].round, - cp_round |-> states[index].cp_round, - cp_val |-> 1])) >= TwoFPlusOne - -CPHasMainVotesQuorumForZero(index) == - Cardinality(SubsetOfMsgs([ - type |-> "CP:MAIN-VOTE", - height |-> states[index].height, - round |-> states[index].round, - cp_round |-> states[index].cp_round, - cp_val |-> 0])) >= TwoFPlusOne - -CPHasDecideVotesForZero(index) == - Cardinality(SubsetOfMsgs([ - type |-> "CP:DECIDE", - height |-> states[index].height, - round |-> states[index].round, - cp_val |-> 0])) > 0 - -CPHasDecideVotesForOne(index) == - Cardinality(SubsetOfMsgs([ - type |-> "CP:DECIDE", - height |-> states[index].height, - round |-> states[index].round, - cp_val |-> 1])) > 0 - -GetProposal(height, round) == - SubsetOfMsgs([type |-> "PROPOSAL", height |-> height, round |-> round]) - -HasProposal(index) == - Cardinality(GetProposal(states[index].height, states[index].round)) > 0 - -HasPrepared(index) == - Cardinality(SubsetOfMsgs([ - type |-> "PREPARE", - height |-> states[index].height, - round |-> states[index].round, - index |-> index])) = 1 - -HasBlockAnnounce(index) == - Cardinality(SubsetOfMsgs([ - type |-> "BLOCK-ANNOUNCE", - height |-> states[index].height, - round |-> states[index].round])) >= 1 - -\* Helper function to check if the block is committed or not. -\* A block is considered committed iff supermajority of non-faulty replicas announce the same block. -IsCommitted == - LET subset == SubsetOfMsgs([ - type |-> "BLOCK-ANNOUNCE", - height |-> MaxHeight]) - IN /\ Cardinality(subset) >= TwoFPlusOne - /\ \A m1, m2 \in subset : m1.round = m2.round - ------------------------------------------------------------------------------ -(***************************************************************************) -(* Network functions *) -(***************************************************************************) - -\* `SendMsg` simulates a replica sending a message by appending it to the `log`. -SendMsg(msg) == - IF msg.cp_round < MaxCPRound - THEN log' = log \cup {msg} - ELSE log' = log - -\* SendProposal is used to broadcast the PROPOSAL into the network. -SendProposal(index) == - SendMsg([ - type |-> "PROPOSAL", - height |-> states[index].height, - round |-> states[index].round, - index |-> index, - cp_round |-> 0, - cp_val |-> 0]) - -\* SendPrepareVote is used to broadcast PREPARE votes into the network. -SendPrepareVote(index) == - SendMsg([ - type |-> "PREPARE", - height |-> states[index].height, - round |-> states[index].round, - index |-> index, - cp_round |-> 0, - cp_val |-> 0]) - -\* SendPrecommitVote is used to broadcast PRECOMMIT votes into the network. -SendPrecommitVote(index) == - SendMsg([ - type |-> "PRECOMMIT", - height |-> states[index].height, - round |-> states[index].round, - index |-> index, - cp_round |-> 0, - cp_val |-> 0]) - -\* SendCPPreVote is used to broadcast CP:PRE-VOTE votes into the network. -SendCPPreVote(index, cp_val) == - SendMsg([ - type |-> "CP:PRE-VOTE", - height |-> states[index].height, - round |-> states[index].round, - index |-> index, - cp_round |-> states[index].cp_round, - cp_val |-> cp_val]) - -\* SendCPMainVote is used to broadcast CP:MAIN-VOTE votes into the network. -SendCPMainVote(index, cp_val) == - SendMsg([ - type |-> "CP:MAIN-VOTE", - height |-> states[index].height, - round |-> states[index].round, - index |-> index, - cp_round |-> states[index].cp_round, - cp_val |-> cp_val]) - -\* SendCPDeciedVote is used to broadcast CP:DECIDE votes into the network. -SendCPDeciedVote(index, cp_val) == - SendMsg([ - type |-> "CP:DECIDE", - height |-> states[index].height, - round |-> states[index].round, - cp_round |-> states[index].cp_round, - index |-> -1, \* reduce the model size - cp_val |-> cp_val]) - -\* AnnounceBlock is used to broadcast BLOCK-ANNOUNCE messages into the network. -AnnounceBlock(index) == - SendMsg([ - type |-> "BLOCK-ANNOUNCE", - height |-> states[index].height, - round |-> states[index].round, - index |-> index, - cp_round |-> 0, - cp_val |-> 0]) - ------------------------------------------------------------------------------ -(***************************************************************************) -(* States functions *) -(***************************************************************************) - -\* NewHeight state -NewHeight(index) == - IF states[index].height >= MaxHeight - THEN UNCHANGED <> - ELSE - /\ ~IsFaulty(index) - /\ states[index].name = "new-height" - /\ states' = [states EXCEPT - ![index].name = "propose", - ![index].height = states[index].height + 1, - ![index].round = 0] - /\ log' = log - - -\* Propose state -Propose(index) == - /\ ~IsFaulty(index) - /\ states[index].name = "propose" - /\ IF IsProposer(index) - THEN SendProposal(index) - ELSE log' = log - /\ states' = [states EXCEPT - ![index].name = "prepare", - ![index].cp_round = 0] - - -\* Prepare state -Prepare(index) == - /\ ~IsFaulty(index) - /\ states[index].name = "prepare" - /\ HasProposal(index) - /\ SendPrepareVote(index) - /\ states' = states - -\* Precommit state -Precommit(index) == - /\ ~IsFaulty(index) - /\ states[index].name = "precommit" - /\ IF HasPrecommitQuorum(index) - THEN /\ states' = [states EXCEPT ![index].name = "commit"] - /\ log' = log - ELSE /\ HasProposal(index) - /\ SendPrecommitVote(index) - /\ states' = states - -\* Commit state -Commit(index) == - /\ ~IsFaulty(index) - /\ states[index].name = "commit" - /\ AnnounceBlock(index) - /\ states' = [states EXCEPT - ![index].name = "new-height"] - -\* Timeout: A non-faulty Replica try to change the proposer if its timer expires. -Timeout(index) == - /\ ~IsFaulty(index) - /\ states[index].name = "prepare" - /\ states[index].round < MaxRound - /\ states' = [states EXCEPT ![index].name = "cp:pre-vote"] - /\ log' = log - - -CPPreVote(index) == - /\ ~IsFaulty(index) - /\ states[index].name = "cp:pre-vote" - /\ IF states[index].cp_round = 0 - THEN - IF HasPrepareQuorum(index) - THEN /\ SendCPPreVote(index, 0) - /\ states' = [states EXCEPT ![index].name = "cp:main-vote"] - ELSE IF HasPrepared(index) - THEN /\ CPHasPreVotesMinorityQuorum(index) - /\ SendCPPreVote(index, 1) - /\ states' = [states EXCEPT ![index].name = "cp:main-vote"] - ELSE /\ SendCPPreVote(index, 1) - /\ states' = [states EXCEPT ![index].name = "cp:main-vote"] - ELSE - /\ - \/ - /\ CPHasAMainVotesOneInPrvRound(index) - /\ SendCPPreVote(index, 1) - \/ - /\ CPHasAMainVotesZeroInPrvRound(index) - /\ SendCPPreVote(index, 0) - \/ - /\ CPAllMainVotesAbstainInPrvRound(index) - /\ SendCPPreVote(index, 0) \* biased to zero - /\ states' = [states EXCEPT ![index].name = "cp:main-vote"] - - -CPMainVote(index) == - /\ ~IsFaulty(index) - /\ states[index].name = "cp:main-vote" - /\ CPHasPreVotesQuorum(index) - /\ - \/ - \* all votes for 1 - /\ CPHasPreVotesQuorumForOne(index) - /\ SendCPMainVote(index, 1) - /\ states' = [states EXCEPT ![index].name = "cp:decide"] - \/ - \* all votes for 0 - /\ CPHasPreVotesQuorumForZero(index) - /\ SendCPMainVote(index, 0) - /\ states' = [states EXCEPT ![index].name = "cp:decide"] - \/ - \* Abstain vote - /\ CPHasPreVotesForZeroAndOne(index) - /\ SendCPMainVote(index, 2) - /\ states' = [states EXCEPT ![index].name = "cp:decide"] - -CPDecide(index) == - /\ ~IsFaulty(index) - /\ states[index].name = "cp:decide" - /\ CPHasMainVotesQuorum(index) - /\ - IF CPHasMainVotesQuorumForZero(index) - THEN - /\ SendCPDeciedVote(index, 0) - /\ states' = states - ELSE IF CPHasMainVotesQuorumForOne(index) - THEN - /\ SendCPDeciedVote(index, 1) - /\ states' = states - ELSE - /\ states' = [states EXCEPT ![index].name = "cp:pre-vote", - ![index].cp_round = states[index].cp_round + 1] - /\ log' = log - - -CPStrongTerminate(index) == - /\ ~IsFaulty(index) - /\ - \/ states[index].name = "cp:pre-vote" - \/ states[index].name = "cp:main-vote" - \/ states[index].name = "cp:decide" - /\ - IF CPHasDecideVotesForOne(index) - THEN /\ states' = [states EXCEPT ![index].name = "propose", - ![index].round = states[index].round + 1] - /\ log' = log - ELSE IF CPHasDecideVotesForZero(index) - THEN - /\ states' = [states EXCEPT ![index].name = "precommit"] - /\ log' = log - ELSE IF /\ states[index].cp_round = MaxCPRound - /\ CPOneFPlusOneMainVotesAbstainInPrvRound(index) - THEN - /\ states' = [states EXCEPT ![index].name = "precommit"] - /\ log' = log - ELSE - /\ states' = states - /\ log' = log - -StrongCommit(index) == - /\ ~IsFaulty(index) - /\ - \/ states[index].name = "prepare" - \/ states[index].name = "precommit" - \/ states[index].name = "cp:pre-vote" - \/ states[index].name = "cp:main-vote" - \/ states[index].name = "cp:decide" - /\ HasPrepareAbsoluteQuorum(index) - /\ states' = [states EXCEPT ![index].name = "commit"] - /\ log' = log - ------------------------------------------------------------------------------ - -Init == - /\ log = {} - /\ states = [index \in 0..n-1 |-> [ - name |-> "new-height", - height |-> 0, - round |-> 0, - cp_round |-> 0]] - -Next == - \E index \in 0..n-1: - \/ NewHeight(index) - \/ Propose(index) - \/ Prepare(index) - \/ Precommit(index) - \/ Timeout(index) - \/ Commit(index) - \/ StrongCommit(index) - \/ CPPreVote(index) - \/ CPMainVote(index) - \/ CPDecide(index) - \/ CPStrongTerminate(index) - -Spec == - Init /\ [][Next]_vars /\ WF_vars(Next) - - -(***************************************************************************) -(* Success: All non-faulty nodes eventually commit at MaxHeight. *) -(***************************************************************************) -Success == <>(IsCommitted) - -(***************************************************************************) -(* TypeOK is the type-correctness invariant. *) -(***************************************************************************) -TypeOK == - /\ \A index \in 0..n-1: - /\ states[index].name \in {"new-height", "propose", "prepare", - "precommit", "commit", "cp:pre-vote", "cp:main-vote", "cp:decide"} - /\ states[index].height <= MaxHeight - /\ states[index].round <= MaxRound - /\ states[index].cp_round <= MaxCPRound - /\ states[index].name = "new-height" /\ states[index].height > 0 => - /\ HasBlockAnnounce(index) - /\ states[index].name = "precommit" => - /\ HasPrepareQuorum(index) - /\ HasProposal(index) - /\ states[index].name = "commit" => - /\ HasPrepareQuorum(index) - /\ HasProposal(index) - /\ \A round \in 0..states[index].round: - \* Not more than one proposal per round - /\ Cardinality(GetProposal(states[index].height, round)) <= 1 - -============================================================================= diff --git a/fastconsensus/spec/README.md b/fastconsensus/spec/README.md deleted file mode 100644 index 26740b498..000000000 --- a/fastconsensus/spec/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Consensus specification - -This folder contains the consensus specification for the Pactus blockchain, -which is based on the TLA+ formal language. -The specification defines the consensus algorithm used by the blockchain. - -More info can be found [here](https://pactus.org/learn/consensus/specification/) - -## Model checking - -To run the model checker, you will need to download and install the [TLA+ Toolbox](https://lamport.azurewebsites.net/tla/toolbox.html), -which includes the TLC model checker. Follow the steps below to run the TLC model checker: - -- Add the `Pactus.tla` spec to your TLA+ Toolbox project. -- Create a new model and specify a temporal formula as `Spec`. -- Specify an invariants formula as `TypeOK`. -- Specify a properties formula as `Success`. -- Define the required constants: - - `NumFaulty`: the number of faulty nodes (e.g. 1) - - `FaultyNodes`: the index of faulty nodes (e.g. {3}) - - `MaxHeight`: the maximum height of the system (e.g. 1) - - `MaxRound`: the maximum block-creation round of the consensus algorithm (e.g. 1) - - `MaxCPRound`: the maximum change-proposer round of the consensus algorithm (e.g. 1) -- Run the TLC checker to check the correctness of the specification. diff --git a/fastconsensus/state.go b/fastconsensus/state.go deleted file mode 100644 index b851522fd..000000000 --- a/fastconsensus/state.go +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index e7384a7ea..000000000 --- a/fastconsensus/ticker.go +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index 69dc0a75b..000000000 --- a/fastconsensus/voteset/binary_voteset.go +++ /dev/null @@ -1,185 +0,0 @@ -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 NewCPDecidedVoteSet(round int16, totalPower int64, - validators map[crypto.Address]*validator.Validator, -) *BinaryVoteSet { - voteSet := newVoteSet(round, totalPower, validators) - - return newBinaryVoteSet(voteSet) -} - -func newBinaryVoteSet(voteSet *voteSet) *BinaryVoteSet { - return &BinaryVoteSet{ - voteSet: voteSet, - roundVotes: make([]*roundVotes, 0, 1), - } -} - -func (vs *BinaryVoteSet) mustGetRoundVotes(cpRound int16) *roundVotes { - for i := len(vs.roundVotes); i <= int(cpRound); i++ { - rv := newRoundVotes() - vs.roundVotes = append(vs.roundVotes, rv) - } - - return vs.roundVotes[cpRound] -} - -// AllVotes returns a list of all votes in the VoteSet. -func (vs *BinaryVoteSet) AllVotes() []*vote.Vote { - votes := make([]*vote.Vote, 0) - for _, rv := range vs.roundVotes { - for _, v := range rv.allVotes { - votes = append(votes, v) - } - } - - return votes -} - -// AddVote attempts to add a vote to the VoteSet. Returns an error if the vote is invalid. -func (vs *BinaryVoteSet) AddVote(v *vote.Vote) (bool, error) { - power, err := vs.voteSet.verifyVote(v) - if err != nil { - return false, err - } - - roundVotes := vs.mustGetRoundVotes(v.CPRound()) - existingVote, ok := roundVotes.allVotes[v.Signer()] - if ok { - if existingVote.Hash() == v.Hash() { - // The vote is already added - return false, nil - } - - // It is a duplicated vote - err = errors.Error(errors.ErrDuplicateVote) - } else { - roundVotes.allVotes[v.Signer()] = v - roundVotes.votedPower += power - } - - roundVotes.addVote(v, power) - - return true, err -} - -func (vs *BinaryVoteSet) HasTwoFPlusOneVotes(cpRound int16) bool { - roundVotes := vs.mustGetRoundVotes(cpRound) - - return vs.hasTwoFPlusOnePower(roundVotes.votedPower) -} - -func (vs *BinaryVoteSet) HasAnyVoteFor(cpRound int16, cpValue vote.CPValue) bool { - roundVotes := vs.mustGetRoundVotes(cpRound) - - return roundVotes.voteBoxes[cpValue].votedPower > 0 -} - -func (vs *BinaryVoteSet) HasAllVotesFor(cpRound int16, cpValue vote.CPValue) bool { - roundVotes := vs.mustGetRoundVotes(cpRound) - - return roundVotes.voteBoxes[cpValue].votedPower == roundVotes.votedPower -} - -func (vs *BinaryVoteSet) HasFPlusOneVotesFor(cpRound int16, cpValue vote.CPValue) bool { - roundVotes := vs.mustGetRoundVotes(cpRound) - - return vs.hasFPlusOnePower(roundVotes.voteBoxes[cpValue].votedPower) -} - -func (vs *BinaryVoteSet) HasTwoFPlusOneVotesFor(cpRound int16, cpValue vote.CPValue) bool { - roundVotes := vs.mustGetRoundVotes(cpRound) - - return vs.hasTwoFPlusOnePower(roundVotes.voteBoxes[cpValue].votedPower) -} - -func (vs *BinaryVoteSet) BinaryVotes(cpRound int16, cpValue vote.CPValue) map[crypto.Address]*vote.Vote { - votes := map[crypto.Address]*vote.Vote{} - roundVotes := vs.mustGetRoundVotes(cpRound) - voteBox := roundVotes.voteBoxes[cpValue] - for a, v := range voteBox.votes { - votes[a] = v - } - - return votes -} - -func (vs *BinaryVoteSet) GetRandomVote(cpRound int16, cpValue vote.CPValue) *vote.Vote { - roundVotes := vs.mustGetRoundVotes(cpRound) - for _, v := range roundVotes.voteBoxes[cpValue].votes { - return v - } - - return nil -} - -// faultyPower calculates the faulty power based on the total power. -// The formula used is: f = (n - 1) / 5, where n is the total power. -func (vs *BinaryVoteSet) faultyPower() int64 { - return (vs.totalPower - 1) / 3 -} - -// hasTwoFPlusOnePower checks whether the given power is greater than or equal to 2f+1, -// where f is the faulty power. -func (vs *BinaryVoteSet) hasTwoFPlusOnePower(power int64) bool { - return power >= (2*vs.faultyPower() + 1) -} - -// hasFPlusOnePower checks whether the given power is greater than or equal to f+1, -// where f is the faulty power. -func (vs *BinaryVoteSet) hasFPlusOnePower(power int64) bool { - return power >= (vs.faultyPower() + 1) -} diff --git a/fastconsensus/voteset/block_voteset.go b/fastconsensus/voteset/block_voteset.go deleted file mode 100644 index 6a4ab2f1e..000000000 --- a/fastconsensus/voteset/block_voteset.go +++ /dev/null @@ -1,140 +0,0 @@ -package voteset - -import ( - "github.com/pactus-project/pactus/crypto" - "github.com/pactus-project/pactus/crypto/hash" - "github.com/pactus-project/pactus/types/validator" - "github.com/pactus-project/pactus/types/vote" - "github.com/pactus-project/pactus/util/errors" -) - -type BlockVoteSet struct { - *voteSet - blockVotes map[hash.Hash]*voteBox - allVotes map[crypto.Address]*vote.Vote - quorumHash *hash.Hash -} - -func NewPrepareVoteSet(round int16, totalPower int64, - validators map[crypto.Address]*validator.Validator, -) *BlockVoteSet { - voteSet := newVoteSet(round, totalPower, validators) - - return newBlockVoteSet(voteSet) -} - -func NewPrecommitVoteSet(round int16, totalPower int64, - validators map[crypto.Address]*validator.Validator, -) *BlockVoteSet { - voteSet := newVoteSet(round, totalPower, validators) - - return newBlockVoteSet(voteSet) -} - -func newBlockVoteSet(voteSet *voteSet) *BlockVoteSet { - return &BlockVoteSet{ - voteSet: voteSet, - blockVotes: make(map[hash.Hash]*voteBox), - allVotes: make(map[crypto.Address]*vote.Vote), - } -} - -func (vs *BlockVoteSet) BlockVotes(blockHash hash.Hash) map[crypto.Address]*vote.Vote { - votes := map[crypto.Address]*vote.Vote{} - blockVotes := vs.mustGetBlockVotes(blockHash) - for a, v := range blockVotes.votes { - votes[a] = v - } - - return votes -} - -func (vs *BlockVoteSet) mustGetBlockVotes(blockHash hash.Hash) *voteBox { - bv, exists := vs.blockVotes[blockHash] - if !exists { - bv = newVoteBox() - vs.blockVotes[blockHash] = bv - } - - return bv -} - -// AllVotes returns a list of all votes in the VoteSet. -func (vs *BlockVoteSet) AllVotes() []*vote.Vote { - votes := make([]*vote.Vote, 0) - for _, v := range vs.allVotes { - votes = append(votes, v) - } - - return votes -} - -// AddVote attempts to add a vote to the VoteSet. Returns an error if the vote is invalid. -func (vs *BlockVoteSet) AddVote(v *vote.Vote) (bool, error) { - power, err := vs.voteSet.verifyVote(v) - if err != nil { - return false, err - } - - existingVote, ok := vs.allVotes[v.Signer()] - if ok { - if existingVote.Hash() == v.Hash() { - // The vote is already added - return false, nil - } - - // It is a duplicated vote - err = errors.Error(errors.ErrDuplicateVote) - } else { - vs.allVotes[v.Signer()] = v - } - - blockVotes := vs.mustGetBlockVotes(v.BlockHash()) - blockVotes.addVote(v, power) - if vs.hasThreeTPlusOnePower(blockVotes.votedPower) { - quorumHash := v.BlockHash() - vs.quorumHash = &quorumHash - } - - return true, err -} - -func (vs *BlockVoteSet) HasVoted(addr crypto.Address) bool { - return vs.allVotes[addr] != nil -} - -// HasAbsoluteQuorum checks if there is a block that has received an absolute quorum of votes (4t+1 of total power). -func (vs *BlockVoteSet) HasAbsoluteQuorum(blockHash hash.Hash) bool { - blockVotes := vs.mustGetBlockVotes(blockHash) - - return vs.hasFourTPlusOnePower(blockVotes.votedPower) -} - -// HasQuorumHash checks if there is a block that has received a quorum of votes (3t+1 of total power). -func (vs *BlockVoteSet) HasQuorumHash() bool { - return vs.quorumHash != nil -} - -// QuorumHash returns the hash of the block that has received a quorum of votes (3t+1 of total power). -// If no block has received the quorum threshold, it returns nil. -func (vs *BlockVoteSet) QuorumHash() *hash.Hash { - return vs.quorumHash -} - -// thresholdPower calculates the threshold power based on the total power. -// The formula used is: t = (n - 1) / 5, where n is the total power. -func (vs *BlockVoteSet) thresholdPower() int64 { - return (vs.totalPower - 1) / 5 -} - -// hasFourTPlusOnePower checks whether the given power is greater than or equal to 4t+1, -// where t is the threshold power. -func (vs *BlockVoteSet) hasFourTPlusOnePower(power int64) bool { - return power >= (4*vs.thresholdPower() + 1) -} - -// hasThreeTPlusOnePower checks whether the given power is greater than or equal to 3t+1, -// where t is the threshold power. -func (vs *BlockVoteSet) hasThreeTPlusOnePower(power int64) bool { - return power >= (3*vs.thresholdPower() + 1) -} diff --git a/fastconsensus/voteset/vote_box.go b/fastconsensus/voteset/vote_box.go deleted file mode 100644 index fbf0866a7..000000000 --- a/fastconsensus/voteset/vote_box.go +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 33d7ad301..000000000 --- a/fastconsensus/voteset/vote_box_test.go +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index f3c2f0c2f..000000000 --- a/fastconsensus/voteset/voteset.go +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index 722e1d826..000000000 --- a/fastconsensus/voteset/voteset_test.go +++ /dev/null @@ -1,439 +0,0 @@ -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 := NewCPDecidedVoteSet(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()) -}