证据是 CometBFT 安全模型中的重要组成部分。虽然核心共识协议为状态机复制提供了正确性保证,并且能够容忍少于 1/3 的故障,但证据系统旨在检测并传播合计投票权大于或等于 1/3 的拜占庭故障。需要注意的是,证据系统的设计目标纯粹是检测可能的攻击、传播这些攻击信息、将其提交上链,并通知运行在 CometBFT 之上的应用。证据本身不会惩罚“作恶者”,这一点由应用自行决定。常见的惩罚形式是削减,即移除被发现违反协议的验证者全部或部分投票权。由于在网络中仍有 1/3+ 节点是拜占庭节点的假设下,证据容易受到审查,因此它应被视为一种“尽力而为”的附加安全机制。 本文将介绍证据的各种形式,以及它们如何被检测、传播、验证和提交。
注意:这里的证据是 CometBFT 内部概念,不应与应用层证据混淆

检测

双重投票

双重投票是最基本的拜占庭故障。简单来说,为了阻止状态在所有节点之间完成复制,验证者会试图说服一部分节点提交某个区块,同时说服另一部分节点提交另一个不同的区块。这是通过重复投票实现的(因此称为 DuplicateVoteEvidence)。一次成功的重复投票攻击需要超过 1/3 的投票权,以及上述两部分节点之间存在一个临时网络分区。原因在于,在共识过程中,投票会被持续传播。当某个节点观察到同一对等节点发出的两张冲突投票时,它会将这两张投票作为证据,并开始向其他节点传播该证据。关于验证的内容将在下文进一步说明。
type DuplicateVoteEvidence struct {
    VoteA Vote
    VoteB Vote

    // and abci specific fields
}

轻客户端攻击

轻客户端同样遵循 1/3+ 安全模型,但由于它使用了不同且更轻量的验证方式,因此会受到另一类 1/3+ 攻击:拜占庭验证者可能签署一个替代性的轻区块,而轻客户端会误认为它是有效的。更详细的检测过程可见这里,其核心是与多个其他节点进行比较,并寄希望于其中至少有一个是“诚实”的。“诚实”节点会返回一个具有挑战性的轻区块,供轻客户端验证。如果这个具有挑战性的轻区块也满足验证条件,那么轻客户端就会将这个“伪造”的轻区块发送给该节点。关于验证的内容将在下文进一步说明。
type LightClientAttackEvidence struct {
    ConflictingBlock LightBlock
    CommonHeight int64

      // and abci specific fields
}

验证

如果节点接收到证据,它会先尝试验证,然后再持久化。拜占庭行为的证据只应被提交一次(唯一性),并且应在其发生后的某个限定时间内被提交(时效性)。这个时间窗口由 EvidenceParams 中的 MaxAgeNumBlocks 和 MaxAgeDuration 定义。在采用权益证明且验证者需要锁定质押的链上,证据的年龄应小于解除绑定周期,这样验证者仍然可以被惩罚。基于这两个属性,首先会进行如下检查。
  1. 证据是否已过期?这通过读取 DuplicateVoteEvidence 中 Vote 的高度,或 LightClientAttakEvidence 中 CommonHeight 的值来完成。随后使用证据高度获取对应区块的头信息,从而得到证据对应区块的时间。如果 CurrentHeight - MaxAgeNumBlocks > EvidenceHeight && CurrentTime - MaxAgeDuration > EvidenceTime,则该证据被视为已过期并被忽略。
  2. 证据是否已经被提交过?证据池会跟踪所有已提交证据的哈希,并用它来判断唯一性。如果一条新证据与某条已提交证据具有相同的哈希值,那么这条新证据将被忽略。

DuplicateVoteEvidence

有效的 DuplicateVoteEvidence 必须满足以下规则:
  • 两张投票的 Validator Address、Height、Round 和 Type 必须相同
  • 两张投票的 BlockID 必须不同(BlockID 可以是空区块的 BlockID)
  • 对应验证者在该高度必须属于验证者集合
  • 投票签名必须正确。这里也会使用 ChainID,以确认该故障发生在这条链上

LightClientAttackEvidence

有效的轻客户端攻击证据必须满足以下规则:
  • 如果轻区块的头无效,从而表明这是一次 lunatic attack,节点必须检查自己能否从共同高度处的头开始,使用 verifySkipping 验证到冲突头
  • 如果头有效,那么验证者集合相同,这种情况要么属于双重投票,要么属于失忆攻击。因此我们需要检查是否有 2/3 的验证者集合也签署了这个冲突头
  • 节点自身在与冲突头相同高度处的头,其哈希必须与冲突头的哈希不同
  • 如果节点的最新头高度低于冲突头高度,那么节点必须检查冲突区块的时间是否早于这个最新头的时间(这是一次向前的 lunatic attack)

传播

如果节点验证了一条证据,它就会将其广播给所有对等节点,并持续每 10 秒重复发送同一条证据,直到该证据出现在链上或过期。

链上提交

证据相对于普通交易具有严格优先级,因此区块会先尽可能填充证据,剩余空间才留给交易。为了减轻已经受罚的节点继续用更多证据刷爆网络的风险,区块中的证据大小可以通过 EvidenceParams.MaxBytes 进行限制。接收到包含证据区块的节点,会在发送 Prevote 和 Precommit 投票之前先验证这些证据。证据池通常会缓存验证结果,因此这一过程会快得多。

向应用发送证据

证据提交后,区块会由区块执行器处理,并通过 EndBlock 将证据交付给应用。实际证明内容会被剥离,证据会按每个作恶验证者拆分,发送给应用的只有验证者、高度、时间和证据类型。
enum EvidenceType {
  UNKNOWN             = 0;
  DUPLICATE_VOTE      = 1;
  LIGHT_CLIENT_ATTACK = 2;
}

message Evidence {
  EvidenceType type = 1;
  // The offending validator
  Validator validator = 2 [(gogoproto.nullable) = false];
  // The height when the offense occurred
  int64 height = 3;
  // The corresponding time where the offense occurred
  google.protobuf.Timestamp time = 4 [
    (gogoproto.nullable) = false, (gogoproto.stdtime) = true];
  // Total voting power of the validator set in case the ABCI application does
  // not store historical validators.
  // https://github.com/tendermint/tendermint/issues/4581
  int64 total_voting_power = 5;
}
DuplicateVoteEvidence 和 LightClientAttackEvidence 是自包含的,也就是说,可以直接从这些证据推导出发送给应用的 abci.Evidence。因此,需要额外的字段:
type DuplicateVoteEvidence struct {
  VoteA *Vote
  VoteB *Vote

  // abci specific information
  TotalVotingPower int64
  ValidatorPower   int64
  Timestamp        time.Time
}

type LightClientAttackEvidence struct {
  ConflictingBlock *LightBlock
  CommonHeight     int64

  // abci specific information
  ByzantineValidators []*Validator
  TotalVotingPower    int64
  Timestamp           time.Time
}
这些 ABCI 专用字段不会影响证据本身的有效性,但它们必须在各节点之间保持一致,并在链上达成共识。如果发送了一条带有错误 ABCI 信息的证据,节点会基于它重新创建一条新证据,并将其中的 ABCI 字段替换为正确的信息。
Evidence is an important component of CometBFT’s security model. Whilst the core consensus protocol provides correctness gaurantees for state machine replication that can tolerate less than 1/3 failures, the evidence system looks to detect and gossip byzantine faults whose combined power is greater than or equal to 1/3. It is worth noting that the evidence system is designed purely to detect possible attacks, gossip them, commit them on chain and inform the application running on top of CometBFT. Evidence in itself does not punish “bad actors”, this is left to the discretion of the application. A common form of punishment is slashing where the validators that were caught violating the protocol have all or a portion of their voting power removed. Evidence, given the assumption that 1/3+ of the network is still byzantine, is susceptible to censorship and should therefore be considered added security on a “best effort” basis. This document walks through the various forms of evidence, how they are detected, gossiped, verified and committed.
NOTE: Evidence here is internal to CometBFT and should not be confused with application evidence

Detection

Equivocation

Equivocation is the most fundamental of byzantine faults. Simply put, to prevent replication of state across all nodes, a validator tries to convince some subset of nodes to commit one block whilst convincing another subset to commit a different block. This is achieved by double voting (hence DuplicateVoteEvidence). A successful duplicate vote attack requires greater than 1/3 voting power and a (temporary) network partition between the aforementioned subsets. This is because in consensus, votes are gossiped around. When a node observes two conflicting votes from the same peer, it will use the two votes of evidence and begin gossiping this evidence to other nodes. Verification is addressed further down.
type DuplicateVoteEvidence struct {
    VoteA Vote
    VoteB Vote

    // and abci specific fields
}

Light Client Attacks

Light clients also comply with the 1/3+ security model, however, by using a different, more lightweight verification method they are subject to a different kind of 1/3+ attack whereby the byzantine validators could sign an alternative light block that the light client will think is valid. Detection, explained in greater detail here, involves comparison with multiple other nodes in the hope that at least one is “honest”. An “honest” node will return a challenging light block for the light client to validate. If this challenging light block also meets the validation criteria then the light client sends the “forged” light block to the node. Verification is addressed further down.
type LightClientAttackEvidence struct {
    ConflictingBlock LightBlock
    CommonHeight int64

      // and abci specific fields
}

Verification

If a node receives evidence, it will first try to verify it, then persist it. Evidence of byzantine behavior should only be committed once (uniqueness) and should be committed within a certain period from the point that it occurred (timely). Timelines is defined by the EvidenceParams: MaxAgeNumBlocks and MaxAgeDuration. In Proof of Stake chains where validators are bonded, evidence age should be less than the unbonding period so validators still can be punished. Given these two propoerties the following initial checks are made.
  1. Has the evidence expired? This is done by taking the height of the Vote within DuplicateVoteEvidence or CommonHeight within LightClientAttakEvidence. The evidence height is then used to retrieve the header and thus the time of the block that corresponds to the evidence. If CurrentHeight - MaxAgeNumBlocks > EvidenceHeight && CurrentTime - MaxAgeDuration > EvidenceTime, the evidence is considered expired and ignored.
  2. Has the evidence already been committed? The evidence pool tracks the hash of all committed evidence and uses this to determine uniqueness. If a new evidence has the same hash as a committed one, the new evidence will be ignored.

DuplicateVoteEvidence

Valid DuplicateVoteEvidence must adhere to the following rules:
  • Validator Address, Height, Round and Type must be the same for both votes
  • BlockID must be different for both votes (BlockID can be for a nil block)
  • Validator must have been in the validator set at that height
  • Vote signature must be correctly signed. This also uses ChainID so we know that the fault occurred on this chain

LightClientAttackEvidence

Valid Light Client Attack Evidence must adhere to the following rules:
  • If the header of the light block is invalid, thus indicating a lunatic attack, the node must check that they can use verifySkipping from their header at the common height to the conflicting header
  • If the header is valid, then the validator sets are the same and this is either a form of equivocation or amnesia. We therefore check that 2/3 of the validator set also signed the conflicting header.
  • The nodes own header at the same height as the conflicting header must have a different hash to the conflicting header.
  • If the nodes latest header is less in height to the conflicting header, then the node must check that the conflicting block has a time that is less than this latest header (This is a forward lunatic attack).

Gossiping

If a node verifies evidence it then broadcasts it to all peers, continously sending the same evidence once every 10 seconds until the evidence is seen on chain or expires.

Commiting on Chain

Evidence takes strict priority over regular transactions, thus a block is filled with evidence first and transactions take up the remainder of the space. To mitigate the threat of an already punished node from spamming the network with more evidence, the size of the evidence in a block can be capped by EvidenceParams.MaxBytes. Nodes receiving blocks with evidence will validate the evidence before sending Prevote and Precommit votes. The evidence pool will usually cache verifications so that this process is much quicker.

Sending Evidence to the Application

After evidence is committed, the block is then processed by the block executor which delivers the evidence to the application via EndBlock. Evidence is stripped of the actual proof, split up per faulty validator and only the validator, height, time and evidence type is sent.
enum EvidenceType {
  UNKNOWN             = 0;
  DUPLICATE_VOTE      = 1;
  LIGHT_CLIENT_ATTACK = 2;
}

message Evidence {
  EvidenceType type = 1;
  // The offending validator
  Validator validator = 2 [(gogoproto.nullable) = false];
  // The height when the offense occurred
  int64 height = 3;
  // The corresponding time where the offense occurred
  google.protobuf.Timestamp time = 4 [
    (gogoproto.nullable) = false, (gogoproto.stdtime) = true];
  // Total voting power of the validator set in case the ABCI application does
  // not store historical validators.
  // https://github.com/tendermint/tendermint/issues/4581
  int64 total_voting_power = 5;
}
DuplicateVoteEvidence and LightClientAttackEvidence are self-contained in the sense that the evidence can be used to derive the abci.Evidence that is sent to the application. Because of this, extra fields are necessary:
type DuplicateVoteEvidence struct {
  VoteA *Vote
  VoteB *Vote

  // abci specific information
  TotalVotingPower int64
  ValidatorPower   int64
  Timestamp        time.Time
}

type LightClientAttackEvidence struct {
  ConflictingBlock *LightBlock
  CommonHeight     int64

  // abci specific information
  ByzantineValidators []*Validator
  TotalVotingPower    int64
  Timestamp           time.Time
}
These ABCI specific fields don’t affect validity of the evidence itself but must be consistent amongst nodes and agreed upon on chain. If evidence with the incorrect abci information is sent, a node will create new evidence from it and replace the ABCI fields with the correct information.