Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(lib/grandpa): fix threshold checking to be strictly greater than 2/3 #1891

Merged
merged 27 commits into from
Oct 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f9d3d0c
implement tipSyncer.hasCurrentWorker
noot Oct 11, 2021
f11edb8
don't handle transaction messages when syncing
noot Oct 11, 2021
af49037
fix core tests
noot Oct 11, 2021
da7f10e
add unit test
noot Oct 11, 2021
56136ea
fix grandpa >2/3 check
noot Oct 11, 2021
3449217
lint
noot Oct 11, 2021
04fc7b0
fix grandpa unit tests
noot Oct 12, 2021
701dc09
fix grandpa skipped tests
noot Oct 12, 2021
d839ed9
fix get grandpa-ghost
noot Oct 12, 2021
a58ef35
update tracker, update network to cache/not re-gossip consensus msgs
noot Oct 12, 2021
e411560
merge w other branch
noot Oct 12, 2021
cd71260
re-gossip consensus msgs again
noot Oct 12, 2021
eb87877
Merge branch 'development' into noot/sync-cont
noot Oct 12, 2021
2a7eb15
update logs
noot Oct 13, 2021
3c23837
address comments
noot Oct 13, 2021
e267b53
Merge branch 'noot/sync-cont' of github.com:ChainSafe/gossamer into n…
noot Oct 13, 2021
2cc41c2
Merge branch 'development' of github.com:ChainSafe/gossamer into noot…
noot Oct 13, 2021
7aa48d0
Merge branch 'noot/sync-cont' of github.com:ChainSafe/gossamer into n…
noot Oct 13, 2021
9345b0e
lint
noot Oct 13, 2021
2294703
Merge branch 'development' of github.com:ChainSafe/gossamer into noot…
noot Oct 13, 2021
2f59f64
cleanup
noot Oct 13, 2021
21d2fb3
update log
noot Oct 14, 2021
ec5fba9
address comments
noot Oct 15, 2021
edfee2c
Merge branch 'development' into noot/sync-finality
noot Oct 15, 2021
bae7217
merge w development
noot Oct 15, 2021
4fccb85
Merge branch 'noot/sync-finality' of github.com:ChainSafe/gossamer in…
noot Oct 15, 2021
5ce9c8e
fix grandpa round test
noot Oct 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions dot/state/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,11 +159,16 @@ func AddBlocksToStateWithFixedBranches(t *testing.T, blockState *BlockState, dep
// create base tree
startNum := int(head.Number.Int64())
for i := startNum + 1; i <= depth; i++ {
d := types.NewBabePrimaryPreDigest(0, uint64(i), [32]byte{}, [64]byte{})
digest := types.NewDigest()
_ = digest.Add(*d.ToPreRuntimeDigest())

block := &types.Block{
Header: types.Header{
ParentHash: previousHash,
Number: big.NewInt(int64(i)),
StateRoot: trie.EmptyHash,
Digest: digest,
},
Body: types.Body{},
}
Expand Down
98 changes: 42 additions & 56 deletions lib/grandpa/grandpa.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ func NewService(cfg *Config) (*Service, error) {
}

s.messageHandler = NewMessageHandler(s, s.blockState)
s.tracker = newTracker(s.blockState, s.messageHandler)
s.paused.Store(false)
return s, nil
}
Expand All @@ -187,6 +188,8 @@ func (s *Service) Start() error {
return nil
}

s.tracker.start()

go func() {
if err := s.initiate(); err != nil {
logger.Crit("failed to initiate", "error", err)
Expand All @@ -203,7 +206,6 @@ func (s *Service) Stop() error {
defer s.chanLock.Unlock()

s.cancel()

s.blockState.FreeFinalisedNotifierChannel(s.finalisedCh)

if !s.authority {
Expand Down Expand Up @@ -256,13 +258,14 @@ func (s *Service) updateAuthorities() error {

s.state.voters = nextAuthorities
s.state.setID = currSetID
s.state.round = 1 // round resets to 1 after a set ID change
s.state.round = 0 // round resets to 1 after a set ID change, setting to 0 before incrementing indicates the setID has been increased
return nil
}

func (s *Service) publicKeyBytes() ed25519.PublicKeyBytes {
return s.keypair.Public().(*ed25519.PublicKey).AsBytes()
}

func (s *Service) initiateRound() error {
// if there is an authority change, execute it
err := s.updateAuthorities()
Expand Down Expand Up @@ -297,7 +300,7 @@ func (s *Service) initiateRound() error {
}

// there was a setID change, or the node was started from genesis
if s.state.round == 1 {
if s.state.round == 0 {
s.chanLock.Lock()
s.mapLock.Lock()
s.preVotedBlock[0] = NewVoteFromHeader(s.head)
Expand All @@ -310,17 +313,10 @@ func (s *Service) initiateRound() error {
s.roundLock.Lock()
s.state.round++
logger.Debug("incrementing grandpa round", "next round", s.state.round)
if s.tracker != nil {
s.tracker.stop()
}

s.prevotes = new(sync.Map)
s.precommits = new(sync.Map)
s.pvEquivocations = make(map[ed25519.PublicKeyBytes][]*SignedVote)
s.pcEquivocations = make(map[ed25519.PublicKeyBytes][]*SignedVote)
s.tracker = newTracker(s.blockState, s.messageHandler)
s.tracker.start()
logger.Trace("started message tracker")
s.roundLock.Unlock()

best, err := s.blockState.BestBlockHeader()
Expand All @@ -333,7 +329,8 @@ func (s *Service) initiateRound() error {
}

// don't begin grandpa until we are at block 1
return s.waitForFirstBlock()
s.waitForFirstBlock()
return nil
}

// initiate initates the grandpa service to begin voting in sequential rounds
Expand Down Expand Up @@ -364,30 +361,21 @@ func (s *Service) initiate() error {
}
}

func (s *Service) waitForFirstBlock() error {
func (s *Service) waitForFirstBlock() {
ch := s.blockState.GetImportedBlockNotifierChannel()

defer s.blockState.FreeImportedBlockNotifierChannel(ch)

// loop until block 1
for {
done := false

select {
case block := <-ch:
if block != nil && block.Header.Number.Int64() > 0 {
done = true
return
}
case <-s.ctx.Done():
return nil
}

if done {
break
return
}
}

return nil
}

func (s *Service) handleIsPrimary() (bool, error) {
Expand Down Expand Up @@ -452,6 +440,8 @@ func (s *Service) primaryBroadcastCommitMessage() {
// at the end of this round, a block will be finalised.
func (s *Service) playGrandpaRound() error {
logger.Debug("starting round", "round", s.state.round, "setID", s.state.setID)
start := time.Now()

ctx, cancel := context.WithCancel(s.ctx)
defer cancel()

Expand Down Expand Up @@ -514,12 +504,12 @@ func (s *Service) playGrandpaRound() error {
// continue to send precommit messages until round is done
go s.sendVoteMessage(precommit, pcm, roundComplete)

err = s.attemptToFinalize()
if err != nil {
if err = s.attemptToFinalize(); err != nil {
logger.Error("failed to finalise", "error", err)
return err
}

logger.Debug("round completed", "duration", time.Since(start))
return nil
}

Expand All @@ -532,8 +522,7 @@ func (s *Service) sendVoteMessage(stage Subround, msg *VoteMessage, roundComplet
return
}

err := s.sendMessage(msg)
if err != nil {
if err := s.sendMessage(msg); err != nil {
logger.Warn("could not send message", "stage", stage, "error", err)
}

Expand Down Expand Up @@ -588,12 +577,11 @@ func (s *Service) attemptToFinalize() error {
return err
}

if bfc.Number < uint32(s.head.Number.Int64()) || pc < s.state.threshold() {
if bfc.Number < uint32(s.head.Number.Int64()) || pc <= s.state.threshold() {
continue
}

err = s.finalise()
if err != nil {
if err = s.finalise(); err != nil {
return err
}

Expand Down Expand Up @@ -749,7 +737,7 @@ func (s *Service) isFinalisable(round uint64) (bool, error) {
return false, errors.New("cannot find best final candidate for previous round")
}

if bfc.Number <= pvb.Number && (s.state.round == 0 || prevBfc.Number <= bfc.Number) && pc >= s.state.threshold() {
if bfc.Number <= pvb.Number && (s.state.round == 0 || prevBfc.Number <= bfc.Number) && pc > s.state.threshold() {
return true, nil
}

Expand Down Expand Up @@ -867,20 +855,20 @@ func (s *Service) derivePrimary() Voter {
}

// getBestFinalCandidate calculates the set of blocks that are less than or equal to the pre-voted block in height,
// with >= 2/3 pre-commit votes, then returns the block with the highest number from this set.
// with >2/3 pre-commit votes, then returns the block with the highest number from this set.
func (s *Service) getBestFinalCandidate() (*Vote, error) {
prevoted, err := s.getPreVotedBlock()
if err != nil {
return nil, err
}

// get all blocks with >=2/3 pre-commits
// get all blocks with >2/3 pre-commits
blocks, err := s.getPossibleSelectedBlocks(precommit, s.state.threshold())
if err != nil {
return nil, err
}

// if there are no blocks with >=2/3 pre-commits, just return the pre-voted block
// if there are no blocks with >2/3 pre-commits, just return the pre-voted block
// TODO: is this correct? the spec implies that it should return nil, but discussions have suggested
// that we return the prevoted block. (#1815)
if len(blocks) == 0 {
Expand All @@ -890,6 +878,7 @@ func (s *Service) getBestFinalCandidate() (*Vote, error) {
// if there are multiple blocks, get the one with the highest number
// that is also an ancestor of the prevoted block (or is the prevoted block)
bfc := &Vote{
Hash: s.blockState.GenesisHash(),
Number: 0,
}

Expand All @@ -901,7 +890,7 @@ func (s *Service) getBestFinalCandidate() (*Vote, error) {
}

if !isDescendant {
// find common ancestor, implicitly has >=2/3 votes
// find common ancestor, implicitly has >2/3 votes
pred, err := s.blockState.HighestCommonAncestor(h, prevoted.Hash)
if err != nil {
return nil, err
Expand All @@ -925,17 +914,13 @@ func (s *Service) getBestFinalCandidate() (*Vote, error) {
}
}

if [32]byte(bfc.Hash) == [32]byte{} {
return &prevoted, nil
}

return bfc, nil
}

// isCompletable returns true if the round is completable, false otherwise
func (s *Service) isCompletable() (bool, error) {
// haven't received enough votes, not completable
if uint64(s.lenVotes(precommit)+len(s.pcEquivocations)) < s.state.threshold() {
if uint64(s.lenVotes(precommit)+len(s.pcEquivocations)) <= s.state.threshold() {
return false, nil
}

Expand All @@ -947,7 +932,7 @@ func (s *Service) isCompletable() (bool, error) {
votes := s.getVotes(precommit)

// for each block with at least 1 vote that's a descendant of the prevoted block,
// check that (total precommits - total pc equivocations - precommits for that block) >= 2/3 |V|
// check that (total precommits - total pc equivocations - precommits for that block) >2/3|V|
// ie. there must not be a descendent of the prevotes block that is preferred
for _, v := range votes {
if prevoted.Hash == v.Hash {
Expand All @@ -969,7 +954,7 @@ func (s *Service) isCompletable() (bool, error) {
return false, err
}

if uint64(len(votes)-len(s.pcEquivocations))-c < s.state.threshold() {
if uint64(len(votes)-len(s.pcEquivocations))-c <= s.state.threshold() {
// round isn't completable
return false, nil
}
Expand All @@ -980,7 +965,7 @@ func (s *Service) isCompletable() (bool, error) {

// getPreVotedBlock returns the current pre-voted block B. also known as GRANDPA-GHOST.
// the pre-voted block is the block with the highest block number in the set of all the blocks with
// total votes >= 2/3 the total number of voters, where the total votes is determined by getTotalVotesForBlock.
// total votes >2/3 the total number of voters, where the total votes is determined by getTotalVotesForBlock.
func (s *Service) getPreVotedBlock() (Vote, error) {
blocks, err := s.getPossibleSelectedBlocks(prevote, s.state.threshold())
if err != nil {
Expand Down Expand Up @@ -1035,10 +1020,11 @@ func (s *Service) getGrandpaGHOST() (Vote, error) {
return Vote{}, err
}

threshold--
if len(blocks) > 0 || threshold == 0 {
break
}

threshold--
}

if len(blocks) == 0 {
Expand All @@ -1062,36 +1048,36 @@ func (s *Service) getGrandpaGHOST() (Vote, error) {
return highest, nil
}

// getPossibleSelectedBlocks returns blocks with total votes >=threshold in a map of block hash -> block number.
// if there are no blocks that have >=threshold direct votes, this function will find ancestors of those blocks that do have >=threshold votes.
// getPossibleSelectedBlocks returns blocks with total votes >threshold in a map of block hash -> block number.
// if there are no blocks that have >threshold direct votes, this function will find ancestors of those blocks that do have >threshold votes.
// note that by voting for a block, all of its ancestor blocks are automatically voted for.
// thus, if there are no blocks with >=threshold total votes, but the sum of votes for blocks A and B is >=threshold, then this function returns
// thus, if there are no blocks with >threshold total votes, but the sum of votes for blocks A and B is >threshold, then this function returns
// the first common ancestor of A and B.
// in general, this function will return the highest block on each chain with >=threshold votes.
// in general, this function will return the highest block on each chain with >threshold votes.
func (s *Service) getPossibleSelectedBlocks(stage Subround, threshold uint64) (map[common.Hash]uint32, error) {
// get blocks that were directly voted for
votes := s.getDirectVotes(stage)
blocks := make(map[common.Hash]uint32)

// check if any of them have >=threshold votes
// check if any of them have >threshold votes
for v := range votes {
total, err := s.getTotalVotesForBlock(v.Hash, stage)
if err != nil {
return nil, err
}

if total >= threshold {
if total > threshold {
blocks[v.Hash] = v.Number
}
}

// since we want to select the block with the highest number that has >=threshold votes,
// since we want to select the block with the highest number that has >threshold votes,
// we can return here since their ancestors won't have a higher number.
if len(blocks) != 0 {
return blocks, nil
}

// no block has >=threshold direct votes, check for votes for ancestors recursively
// no block has >threshold direct votes, check for votes for ancestors recursively
var err error
va := s.getVotes(stage)

Expand All @@ -1105,15 +1091,15 @@ func (s *Service) getPossibleSelectedBlocks(stage Subround, threshold uint64) (m
return blocks, nil
}

// getPossibleSelectedAncestors recursively searches for ancestors with >=2/3 votes
// it returns a map of block hash -> number, such that the blocks in the map have >=2/3 votes
// getPossibleSelectedAncestors recursively searches for ancestors with >2/3 votes
// it returns a map of block hash -> number, such that the blocks in the map have >2/3 votes
func (s *Service) getPossibleSelectedAncestors(votes []Vote, curr common.Hash, selected map[common.Hash]uint32, stage Subround, threshold uint64) (map[common.Hash]uint32, error) {
for _, v := range votes {
if v.Hash == curr {
continue
}

// find common ancestor, check if votes for it is >=threshold or not
// find common ancestor, check if votes for it is >threshold or not
pred, err := s.blockState.HighestCommonAncestor(v.Hash, curr)
if err == blocktree.ErrNodeNotFound {
continue
Expand All @@ -1130,7 +1116,7 @@ func (s *Service) getPossibleSelectedAncestors(votes []Vote, curr common.Hash, s
return nil, err
}

if total >= threshold {
if total > threshold {
var h *types.Header
h, err = s.blockState.GetHeader(pred)
if err != nil {
Expand Down
Loading