这里规定在签名前验证提案和投票的规则。 首先,我们给出一些适用于这两类消息的通用数据结构验证说明。 然后,我们分别给出各自的具体验证规则。最后,我们给出用于防止双重签名的验证规则。

SignedMsgType

SignedMsgType 是一个单字节值,用于表示被签名消息的类型。 其在 Go 中定义如下:
// SignedMsgType is a type of signed message in the consensus.
type SignedMsgType byte

const (
 // Votes
 PrevoteType   SignedMsgType = 0x01
 PrecommitType SignedMsgType = 0x02

 // Proposals
 ProposalType SignedMsgType = 0x20
)
所有签名消息都必须对应上述类型之一。

Timestamp

时间戳校验较为微妙,目前对提案或投票中包含的时间戳没有设置边界限制。 通常预期验证者会诚实地报告其本地时钟时间。 提交中所有时间戳的中位数会被用作下一个区块高度的时间戳。 对于同一个验证者,时间戳应当严格单调递增,不过当前并未强制执行这一点。

ChainID

ChainID 是一个非结构化字符串,最大长度为 50 字节。 未来 ChainID 可能会变成结构化形式,也可能支持更长长度。 目前建议签名器为特定的 ChainID 进行配置,并且只对与该 ChainID 对应的投票和提案进行签名。

BlockID

BlockID 是用于表示区块的结构:
type BlockID struct {
 Hash        []byte
 PartsHeader PartSetHeader
}

type PartSetHeader struct {
 Hash  []byte
 Total int
}
要被包含在有效投票或提案中,BlockID 必须要么表示一个 nil 区块,要么表示一个完整区块。 为此,我们分别引入 BlockID.IsZero() 和 BlockID.IsComplete() 两个方法。 对于 BlockID b,如果以下各项都为真,则 BlockID.IsZero() 返回 true:
b.Hash == nil
b.PartsHeader.Total == 0
b.PartsHeader.Hash == nil
对于 BlockID b,如果以下各项都为真,则 BlockID.IsComplete() 返回 true:
len(b.Hash) == 32
b.PartsHeader.Total > 0
len(b.PartsHeader.Hash) == 32

Proposals

用于签名的提案结构如下:
type CanonicalProposal struct {
 Type      SignedMsgType // type alias for byte
 Height    int64         `binary:"fixed64"`
 Round     int64         `binary:"fixed64"`
 POLRound  int64         `binary:"fixed64"`
 BlockID   BlockID
 Timestamp time.Time
 ChainID   string
}
如果对于提案 p,以下每一行都求值为 true,则该提案有效:
p.Type == 0x20
p.Height > 0
p.Round >= 0
p.POLRound >= -1
p.BlockID.IsComplete()
换句话说,当一个提案包含 Proposal 类型(0x20)、具有正且非零的高度、非负的轮次、不小于 -1 的 POLRound,以及完整的 BlockID 时,它才是可签名的有效提案。

Votes

用于签名的投票结构如下:
type CanonicalVote struct {
 Type      SignedMsgType // type alias for byte
 Height    int64         `binary:"fixed64"`
 Round     int64         `binary:"fixed64"`
 BlockID   BlockID
 Timestamp time.Time
 ChainID   string
}
如果对于投票 v,以下每一行都求值为 true,则该投票有效:
v.Type == 0x1 || v.Type == 0x2
v.Height > 0
v.Round >= 0
v.BlockID.IsZero() || v.BlockID.IsComplete()
换句话说,当一个投票包含 Prevote 或 Precommit 类型(分别为 0x1 或 0x2)、具有正且非零的高度、非负的轮次,以及空的或有效的 BlockID 时,它才是可签名的有效投票。

Invalid Votes and Proposals

不满足上述规则的投票和提案会被视为无效。 传播无效投票和提案的对等节点,可能会被网络中的其他对等节点断开连接。 不过请注意,目前并没有显式机制去惩罚那些签署了不满足这些基础校验规则的投票或提案的验证者。

Double Signing

签名器必须谨慎避免对互相冲突的消息进行签名,这也称为“双重签名”或“作恶性重复表态(equivocating)”。 CometBFT 提供了发布验证者签署冲突投票证据的机制,因此应用层可以对其进行惩罚。 需要注意的是,CometBFT 当前尚不处理冲突提案的证据,不过未来可能会支持。

State

为了防止这类双重签名,签名器必须跟踪最近一次已签名消息的高度、轮次和类型。 假设签名器保存以下状态 s:
type LastSigned struct {
 Height int64
 Round int64
 Type SignedMsgType // byte
}
在对投票或提案 m 签名后,签名器设置:
s.Height = m.Height
s.Round = m.Round
s.Type = m.Type

Proposals

仅当以下任一条件为真时,签名器才应对提案 p 进行签名:
p.Height > s.Height
p.Height == s.Height && p.Round > s.Round
换句话说,提案只有在高度更高,或在相同高度下轮次更高时,才应该被签名。 一旦某个高度和轮次已经签署过提案或投票,就绝不应再为相同高度和轮次签署提案。

Votes

仅当以下任一条件为真时,签名器才应对投票 v 进行签名:
v.Height > s.Height
v.Height == s.Height && v.Round > s.Round
v.Height == s.Height && v.Round == s.Round && v.Step == 0x1 && s.Step == 0x20
v.Height == s.Height && v.Round == s.Round && v.Step == 0x2 && s.Step != 0x2
换句话说,投票只有在以下情况下才应该被签名:
  • 高度更高
  • 在相同高度下轮次更高
  • 在相同高度和轮次下,是一个 prevote,且我们尚未签署 prevote 或 precommit(但已签署 proposal)
  • 在相同高度和轮次下,是一个 precommit,且我们尚未签署 precommit(但已签署 proposal 和/或 prevote)
这意味着,一旦验证者在某个给定高度和轮次上签署了 prevote,那么在该高度和轮次上它唯一还能签署的另一条消息就是 precommit。 而一旦验证者在某个给定高度和轮次上签署了 precommit,它就不得再为同一高度和轮次签署任何其他消息。 请注意,这也包括对 nil 的投票,也就是 BlockID.IsZero() 为 true 的情况。 如果签名器已经签署过一个 BlockID.IsZero() 为 true 的投票,那么它就不能再对相同高度、轮次和类型的另一个 BlockID.IsComplete() 为 true 的投票进行签名。 因此,在相同高度和轮次下,某一特定类型的投票(即 0x01 或 0x02)只能签署一次。

Other Rules

根据 CometBFT 所采用的 Tendermint 共识算法规则,一旦验证者对某个区块进行了 precommit,它就会被“锁定”在该区块上,这意味着除非它看到充分的理由(即来自更高轮次的 polka),否则不能再对另一个区块进行 prevote。 更多细节请参见共识规范。 违反这条规则被称为“amnesia”。 与容易检测的 equivocation 不同,amnesia 很难被检测,除非能够获得所有验证者的投票,因为这正是“解锁”所需理由的组成部分。 因此,amnesia 不会在协议内受到惩罚,而且签名器也无法轻易阻止它发生。 如果足够多的验证者同时发起 amnesia 攻击,它们可能导致区块链分叉;此时必须启用链下协议来收集所有验证者的投票,并确定是谁存在不当行为。 更多细节请参见分叉检测。
Here we specify the rules for validating a proposal and vote before signing. First we include some general notes on validating data structures common to both types. We then provide specific validation rules for each. Finally, we include validation rules to prevent double-sigining.

SignedMsgType

The SignedMsgType is a single byte that refers to the type of the message being signed. It is defined in Go as follows:
// SignedMsgType is a type of signed message in the consensus.
type SignedMsgType byte

const (
 // Votes
 PrevoteType   SignedMsgType = 0x01
 PrecommitType SignedMsgType = 0x02

 // Proposals
 ProposalType SignedMsgType = 0x20
)
All signed messages must correspond to one of these types.

Timestamp

Timestamp validation is subtle and there are currently no bounds placed on the timestamp included in a proposal or vote. It is expected that validators will honestly report their local clock time. The median of all timestamps included in a commit is used as the timestamp for the next block height. Timestamps are expected to be strictly monotonic for a given validator, though this is not currently enforced.

ChainID

ChainID is an unstructured string with a max length of 50-bytes. In the future, the ChainID may become structured, and may take on longer lengths. For now, it is recommended that signers be configured for a particular ChainID, and to only sign votes and proposals corresponding to that ChainID.

BlockID

BlockID is the structure used to represent the block:
type BlockID struct {
 Hash        []byte
 PartsHeader PartSetHeader
}

type PartSetHeader struct {
 Hash  []byte
 Total int
}
To be included in a valid vote or proposal, BlockID must either represent a nil block, or a complete one. We introduce two methods, BlockID.IsZero() and BlockID.IsComplete() for these cases, respectively. BlockID.IsZero() returns true for BlockID b if each of the following are true:
b.Hash == nil
b.PartsHeader.Total == 0
b.PartsHeader.Hash == nil
BlockID.IsComplete() returns true for BlockID b if each of the following are true:
len(b.Hash) == 32
b.PartsHeader.Total > 0
len(b.PartsHeader.Hash) == 32

Proposals

The structure of a proposal for signing looks like:
type CanonicalProposal struct {
 Type      SignedMsgType // type alias for byte
 Height    int64         `binary:"fixed64"`
 Round     int64         `binary:"fixed64"`
 POLRound  int64         `binary:"fixed64"`
 BlockID   BlockID
 Timestamp time.Time
 ChainID   string
}
A proposal is valid if each of the following lines evaluates to true for proposal p:
p.Type == 0x20
p.Height > 0
p.Round >= 0
p.POLRound >= -1
p.BlockID.IsComplete()
In other words, a proposal is valid for signing if it contains the type of a Proposal (0x20), has a positive, non-zero height, a non-negative round, a POLRound not less than -1, and a complete BlockID.

Votes

The structure of a vote for signing looks like:
type CanonicalVote struct {
 Type      SignedMsgType // type alias for byte
 Height    int64         `binary:"fixed64"`
 Round     int64         `binary:"fixed64"`
 BlockID   BlockID
 Timestamp time.Time
 ChainID   string
}
A vote is valid if each of the following lines evaluates to true for vote v:
v.Type == 0x1 || v.Type == 0x2
v.Height > 0
v.Round >= 0
v.BlockID.IsZero() || v.BlockID.IsComplete()
In other words, a vote is valid for signing if it contains the type of a Prevote or Precommit (0x1 or 0x2, respectively), has a positive, non-zero height, a non-negative round, and an empty or valid BlockID.

Invalid Votes and Proposals

Votes and proposals which do not satisfy the above rules are considered invalid. Peers gossipping invalid votes and proposals may be disconnected from other peers on the network. Note, however, that there is not currently any explicit mechanism to punish validators signing votes or proposals that fail these basic validation rules.

Double Signing

Signers must be careful not to sign conflicting messages, also known as “double signing” or “equivocating”. CometBFT has mechanisms to publish evidence of validators that signed conflicting votes, so they can be punished by the application. Note CometBFT does not currently handle evidence of conflciting proposals, though it may in the future.

State

To prevent such double signing, signers must track the height, round, and type of the last message signed. Assume the signer keeps the following state, s:
type LastSigned struct {
 Height int64
 Round int64
 Type SignedMsgType // byte
}
After signing a vote or proposal m, the signer sets:
s.Height = m.Height
s.Round = m.Round
s.Type = m.Type

Proposals

A signer should only sign a proposal p if any of the following lines are true:
p.Height > s.Height
p.Height == s.Height && p.Round > s.Round
In other words, a proposal should only be signed if it’s at a higher height, or a higher round for the same height. Once a proposal or vote has been signed for a given height and round, a proposal should never be signed for the same height and round.

Votes

A signer should only sign a vote v if any of the following lines are true:
v.Height > s.Height
v.Height == s.Height && v.Round > s.Round
v.Height == s.Height && v.Round == s.Round && v.Step == 0x1 && s.Step == 0x20
v.Height == s.Height && v.Round == s.Round && v.Step == 0x2 && s.Step != 0x2
In other words, a vote should only be signed if it’s:
  • at a higher height
  • at a higher round for the same height
  • a prevote for the same height and round where we haven’t signed a prevote or precommit (but have signed a proposal)
  • a precommit for the same height and round where we haven’t signed a precommit (but have signed a proposal and/or a prevote)
This means that once a validator signs a prevote for a given height and round, the only other message it can sign for that height and round is a precommit. And once a validator signs a precommit for a given height and round, it must not sign any other message for that same height and round. Note this includes votes for nil, ie. where BlockID.IsZero() is true. If a signer has already signed a vote where BlockID.IsZero() is true, it cannot sign another vote with the same type for the same height and round where BlockID.IsComplete() is true. Thus only a single vote of a particular type (ie. 0x01 or 0x02) can be signed for the same height and round.

Other Rules

According to the rules of Tendermint consensus algorithm, adopted in CometBFT, once a validator precommits for a block, they become “locked” on that block, which means they can’t prevote for another block unless they see sufficient justification (ie. a polka from a higher round). For more details, see the consensus spec. Violating this rule is known as “amnesia”. In contrast to equivocation, which is easy to detect, amnesia is difficult to detect without access to votes from all the validators, as this is what constitutes the justification for “unlocking”. Hence, amnesia is not punished within the protocol, and cannot easily be prevented by a signer. If enough validators simultaneously commit an amnesia attack, they may cause a fork of the blockchain, at which point an off-chain protocol must be engaged to collect votes from all the validators and determine who misbehaved. For more details, see fork detection.