pbft投票选举新的授权节点(signer) - Agzs/geth-pbft-study GitHub Wiki

console界面执行pbft.Propose("0x....", true),实际调用consensus/pbft目录下的api.go中的Propose()函数,该函数如下:

func (api *API) Propose(address common.Address, auth bool) {
	api.pbft.lock.Lock()
	defer api.pbft.lock.Unlock()

	api.pbft.proposals[address] = auth
}

该函数将在pbftproposals成员变量中添加新的元素(address, auth),该变量主要在pbft.go中的Prepare()函数中使用

func (c *PBFT) Prepare(chain consensus.ChainReader, header *types.Header) error {
	// If the block isn't a checkpoint, cast a random vote (good enough for now)
	header.Coinbase = common.Address{}
	header.Nonce = types.BlockNonce{}

	number := header.Number.Uint64()

	// Assemble the voting snapshot to check which votes make sense
	snap, err := c.snapshot(chain, number-1, header.ParentHash, nil)
	...

	if number%c.config.Epoch != 0 {
		c.lock.RLock()

		// Gather all the proposals that make sense voting on
		addresses := make([]common.Address, 0, len(c.proposals))
		for address, authorize := range c.proposals {
			if snap.validVote(address, authorize) {
				addresses = append(addresses, address)
			}
		}
		// If there's pending proposals, cast a vote on them
		if len(addresses) > 0 {
			header.Coinbase = addresses[rand.Intn(len(addresses))]
			if c.proposals[header.Coinbase] {
				copy(header.Nonce[:], nonceAuthVote)
			} else {
				copy(header.Nonce[:], nonceDropVote)
			}
		}
                c.lock.RUnlock()
	}
        ...
}

Prepare()函数会对proposals进行处理,如果proposals不为空,便将proposals的信息添加到addresses切片中,然后随机从下标[0,n)中选取一个数,将其对应的元素值保存到header.Coinbase,然后后期只对header.coinbase进行处理。另外,将授权与撤销的标识通过header.Nonce[:]进行表示。该函数在func (self *worker) commitNewWork()中被调用。

注:在pow机制中,header.coinbase是作为挖矿接收奖励的账户的,而在clique和pbft中该变量被用来保存授权投票地址。

后期,会通过调用func (c *PBFT) snapshot(chain consensus.ChainReader, number uint64, hash common.Hash, parents []*types.Header) (*Snapshot, error)函数间接调用 func (s *Snapshot) apply(headers []*types.Header) (*Snapshot, error),在apply()函数中对header.Coinbase进行处理:

func (s *Snapshot) apply(headers []*types.Header) (*Snapshot, error) {
          ...
          for _, header := range headers {
		...

		// Tally up the new vote from the signer
		var authorize bool
		switch {
		case bytes.Compare(header.Nonce[:], nonceAuthVote) == 0:
			authorize = true
		case bytes.Compare(header.Nonce[:], nonceDropVote) == 0:
			authorize = false
		default:
			return nil, errInvalidVote
		}
		if snap.cast(header.Coinbase, authorize) {
			snap.Votes = append(snap.Votes, &Vote{
				Signer:    signer,
				Block:     number,
				Address:   header.Coinbase,
				Authorize: authorize,
			})
		}
		// If the vote passed, update the list of signers
		if tally := snap.Tally[header.Coinbase]; tally.Votes > len(snap.Signers)/2 {
			if tally.Authorize {
				snap.Signers[header.Coinbase] = struct{}{}
			} else {
				...
			}
			...
                }
	    }
}

从该函数可以看出,该函数通过调用cast()函数为head.coinbase投票

// cast adds a new vote into the tally.
func (s *Snapshot) cast(address common.Address, authorize bool) bool {
	// Ensure the vote is meaningful
	if !s.validVote(address, authorize) {
		return false
	}
	// Cast the vote into an existing or new tally
	if old, ok := s.Tally[address]; ok {
		old.Votes++
		s.Tally[address] = old
	} else {
		s.Tally[address] = Tally{Authorize: authorize, Votes: 1}
	}
	return true
}

cast()函数会对head.coinbase进行验证,并为其投上一票,最终返回trueapply()函数,并在apply()函数中将投票信息保存到snap.Votes中,此后验证投票是否超过当前所有signers总数的一半,若超过一半,则该address成为新的signer(授权节点),可参与挖矿。

需要注意的是,由于投票是当前有效的signers每人一票才能作数,所以:

如果在clique中的话,由于每个signer轮流挖块,所以至少需要新挖出len(signers)/2个区块,就能使被投票的address成为正式的signer(授权节点);

如果在pbft中,该机制显然不行,因为只有每次发生viewchange时,才会切换主节点,然后新的主节点才能投上一票,因此至少需要发生len(signers)/2viewchange,才能使被投票的address成为正式的signer(授权节点),这显然不现实,该机制需要进一步修改。

=========================================================================

大体思路:由于每个block都是通过pbft共识后,才将其上链,pbft共识的过程就相当于进行了投票,所以将被授权的节点地址保存到header.coinbase中,然后进行共识,共识成功后的区块被放到链上,然后主节点广播新的区块;其他节点收到新的区块后,对其解析出header.coinbase,之前本来打算在每个节点收到NewBlockMsg标识的消息后,从Block中读取出header然后进行处理,后来阅读代码,发现关于授权投票在snap.apply()中进行,而该函数仅在pbft.snapshot()中被调用,而pbft.snapshot()又在各种验证函数中被调用,所以相当于在验证区块的时候,顺带着就处理了被授权节点的投票,所以我们只需要修改投票计数的规则就好了,将原来的一半以上的投票改成主节点的一票就可以了。

被授权节点处理过程:

    1. 主节点通过pbft.propose()投票
    1. 主节点将投票信息保存到header.coinbase,并为自己处理该投票
    1. 主节点将header放到block中pbft共识该区块
    1. 共识成功后,区块上链,主节点广播新区块
    1. 其他节点接收到新区块后,进行区块验证,顺便处理下投票
    1. 所有节点都导入新的区块后,每个节点的pbft.Signer数组保存的授权节点都是相同的,即共同添加了新的授权节点。

其他节点也可以进行pbft.propose()操作,也可以存储到header.coinbase,但是由于无法进行挖矿,所以无法将header保存到block中。但是当发生viewchange时,该节点将会获得挖矿机会,由于proposals提案一直保存,所以此时会将投票信息中的授权节点的地址保存到header.coinbase中,然后执行后续操作,由于viewchange发生的不确定性,所以采用主节点进行投票授权。

在snapshot.go中的apply()函数中,修改代码:

    // If the vote passed, update the list of signers
    //=> if tally := snap.Tally[header.Coinbase]; tally.Votes > len(snap.Signers)/2 {
    if tally, ok := snap.Tally[header.Coinbase]; ok {
        ...
     }

可能存在问题:

1、主节点权限过高

2、共识过程只是对block的digest进行了验证,并未涉及内部具体数据的验证