Alert Source Discuss
🚧 Stagnant Standards Track: Core

EIP-7657: 同步委员会的罚没

恶意同步委员会消息的罚没条件

Authors Etan Kissling (@etan-status)
Created 2024-03-21
Discussion Link https://ethereum-magicians.org/t/eip-7657-sync-committee-slashings/19288

摘要

本 EIP 定义了恶意同步委员会消息的罚没条件。

动机

不诚实的同步委员会成员的绝对多数能够说服依赖以太坊轻客户端同步协议的应用程序接受非规范的最终确定头,并有可能接管未来 SyncCommitteePeriod 的同步权限。通过签署恶意的信标区块根,可以形成恶意的(但有效的!)LightClientUpdate 消息,并随后用于例如利用基于轻客户端同步协议的最小信任桥合约。

引入了一种额外的罚没类型,以阻止作为同步委员会成员签署非规范信标区块根。与 ProposerSlashingAttesterSlashing 一样,只有恶意行为才会被罚没。这包括跨多个链分支的同时矛盾参与,但仅仅是被诱骗同步到不正确的检查点的验证者不应被罚没,即使它参与了非规范链。请注意,即使没有访问历史记录,也必须可以验证罚没,例如,通过检查点同步的信标节点。

请注意,无论采用何种罚没机制,罚没只能在攻击发生后追溯应用。保护金额大于 SYNC_COMMITTEE_SIZE * MAX_EFFECTIVE_BALANCE = 512 * 32 ETH = 16384 ETH(在主网上)的用例应将轻客户端同步协议与其他已建立的方法(如多重签名)相结合,或者可能需要发布额外的抵押品才有资格提交更新。其他方法不属于本 EIP 的范围。

规范

本文档中的关键词“必须 (MUST)”,“禁止 (MUST NOT)”,“需要 (REQUIRED)”,“应该 (SHALL)”,“不应该 (SHALL NOT)”,“推荐 (SHOULD)”,“不推荐 (SHOULD NOT)”,“可以 (MAY)”和“可选 (OPTIONAL)”应按照 RFC 2119 和 RFC 8174 中的描述进行解释。

状态转换检查

注意:这仍然允许在证明/提案和同步委员会消息之间存在矛盾。此外,按照设计,这允许验证者根本不参与诚实的同步委员会消息,而仅参与不诚实的同步委员会消息。

名称
BLOCK_STATE_ROOT_INDEX get_generalized_index(BeaconBlock, 'state_root') (= 11)
STATE_BLOCK_ROOTS_INDEX get_generalized_index(BeaconState, 'block_roots') (= 37)
STATE_HISTORICAL_ROOTS_INDEX get_generalized_index(BeaconState, 'historical_roots') (= 39)
HISTORICAL_BATCH_BLOCK_ROOTS_INDEX get_generalized_index(HistoricalBatch, 'block_roots') (= 2)
MAX_SYNC_COMMITTEE_SLASHINGS 2**0 (= 1)
class SyncCommitteeSlashingEvidence(Container):
    attested_header: BeaconBlockHeader
    next_sync_committee: SyncCommittee
    next_sync_committee_branch: Vector[Root, floorlog2(NEXT_SYNC_COMMITTEE_INDEX)]
    finalized_header: BeaconBlockHeader
    finality_branch: Vector[Root, floorlog2(FINALIZED_ROOT_INDEX)]
    sync_aggregate: SyncAggregate
    signature_slot: Slot
    sync_committee_pubkeys: Vector[BLSPubkey, SYNC_COMMITTEE_SIZE]
    actual_finalized_block_root: Root
    actual_finalized_branch: List[Root, (
        floorlog2(BLOCK_STATE_ROOT_INDEX)
        + floorlog2(STATE_HISTORICAL_ROOTS_INDEX)
        + 1 + floorlog2(HISTORICAL_ROOTS_LIMIT)
        + floorlog2(HISTORICAL_BATCH_BLOCK_ROOTS_INDEX)
        + 1 + floorlog2(SLOTS_PER_HISTORICAL_ROOT))]

class SyncCommitteeSlashing(Container):
    slashable_validators: List[ValidatorIndex, SYNC_COMMITTEE_SIZE]
    evidence_1: SyncCommitteeSlashingEvidence
    evidence_2: SyncCommitteeSlashingEvidence
    recent_finalized_block_root: Root
    recent_finalized_slot: Slot

def sync_committee_slashing_evidence_has_sync_committee(evidence: SyncCommitteeSlashingEvidence) -> bool:
    return evidence.next_sync_committee_branch != [Root() for _ in range(floorlog2(NEXT_SYNC_COMMITTEE_INDEX))]

def sync_committee_slashing_evidence_has_finality(evidence: SyncCommitteeSlashingEvidence) -> bool:
    return evidence.finality_branch != [Root() for _ in range(floorlog2(FINALIZED_ROOT_INDEX))]

def is_valid_sync_committee_slashing_evidence(evidence: SyncCommitteeSlashingEvidence,
                                              recent_finalized_block_root: Root,
                                              recent_finalized_slot: Slot,
                                              genesis_validators_root: Root) -> bool:
    # 验证同步委员会是否有足够的参与者
    sync_aggregate = evidence.sync_aggregate
    if sum(sync_aggregate.sync_committee_bits) < MIN_SYNC_COMMITTEE_PARTICIPANTS:
        return False

    # 验证 `finality_branch`(如果存在)确认 `finalized_header`
    # 以匹配保存在 `attested_header` 状态中的最终确定检查点根。
    # 请注意,创世最终确定的检查点根表示为零哈希。
    if not sync_committee_slashing_evidence_has_finality(evidence):
        if evidence.actual_finalized_block_root != Root():
            return False
        if evidence.finalized_header != BeaconBlockHeader():
            return False
    else:
        if evidence.finalized_header.slot == GENESIS_SLOT:
            if evidence.actual_finalized_block_root != Root():
                return False
            if evidence.finalized_header != BeaconBlockHeader():
                return False
            finalized_root = Root()
        else:
            finalized_root = hash_tree_root(evidence.finalized_header)
        if not is_valid_merkle_branch(
            leaf=finalized_root,
            branch=evidence.finality_branch,
            depth=floorlog2(FINALIZED_ROOT_INDEX),
            index=get_subtree_index(FINALIZED_ROOT_INDEX),
            root=evidence.attested_header.state_root,
        ):
            return False

    # 验证 `next_sync_committee`(如果存在)实际上是保存在
    # `attested_header` 状态中的下一个同步委员会
    if not sync_committee_slashing_evidence_has_sync_committee(evidence):
        if evidence.next_sync_committee != SyncCommittee():
            return False
    else:
        if not is_valid_merkle_branch(
            leaf=hash_tree_root(evidence.next_sync_committee),
            branch=evidence.next_sync_committee_branch,
            depth=floorlog2(NEXT_SYNC_COMMITTEE_INDEX),
            index=get_subtree_index(NEXT_SYNC_COMMITTEE_INDEX),
            root=evidence.attested_header.state_root,
        ):
            return False

    # 验证 `actual_finalized_block_root`(如果存在)是否被 `actual_finalized_branch` 确认
    # 是相对于 `recent_finalized_block_root` 的 slot `finalized_header.slot` 处的区块根
    if recent_finalized_block_root == Root():
        if evidence.actual_finalized_block_root != Root():
            return False
    if evidence.actual_finalized_block_root == Root():
        if len(evidence.actual_finalized_branch) != 0:
            return False
    else:
        finalized_slot = evidence.finalized_header.slot
        if recent_finalized_slot < finalized_slot:
            return False
        distance = recent_finalized_slot - finalized_slot
        if distance == 0:
            gindex = GeneralizedIndex(1)
        else:
            gindex = BLOCK_STATE_ROOT_INDEX
            if distance <= SLOTS_PER_HISTORICAL_ROOT:
                gindex = (gindex << floorlog2(STATE_BLOCK_ROOTS_INDEX)) + STATE_BLOCK_ROOTS_INDEX
            else:
                gindex = (gindex << floorlog2(STATE_HISTORICAL_ROOTS_INDEX)) + STATE_HISTORICAL_ROOTS_INDEX
                gindex = (gindex << uint64(1)) + 0  # `mix_in_length`
                historical_batch_index = finalized_slot // SLOTS_PER_HISTORICAL_ROOT
                gindex = (gindex << floorlog2(HISTORICAL_ROOTS_LIMIT)) + historical_batch_index
                gindex = (gindex << floorlog2(HISTORICAL_BATCH_BLOCK_ROOTS_INDEX)) + HISTORICAL_BATCH_BLOCK_ROOTS_INDEX
            gindex = (gindex << uint64(1)) + 0  # `mix_in_length`
            block_root_index = finalized_slot % SLOTS_PER_HISTORICAL_ROOT
            gindex = (gindex << floorlog2(SLOTS_PER_HISTORICAL_ROOT)) + block_root_index
        if len(evidence.actual_finalized_branch) != floorlog2(gindex):
            return False
        if not is_valid_merkle_branch(
            leaf=evidence.actual_finalized_block_root,
            branch=evidence.actual_finalized_branch,
            depth=floorlog2(gindex),
            index=get_subtree_index(gindex),
            root=recent_finalized_block_root,
        ):
            return False

    # 验证同步委员会聚合签名
    sync_committee_pubkeys = evidence.sync_committee_pubkeys
    participant_pubkeys = [
        pubkey for (bit, pubkey) in zip(sync_aggregate.sync_committee_bits, sync_committee_pubkeys)
        if bit
    ]
    fork_version = compute_fork_version(compute_epoch_at_slot(evidence.signature_slot))
    domain = compute_domain(DOMAIN_SYNC_COMMITTEE, fork_version, genesis_validators_root)
    signing_root = compute_signing_root(evidence.attested_header, domain)
    return bls.FastAggregateVerify(participant_pubkeys, signing_root, sync_aggregate.sync_committee_signature)

def process_sync_committee_slashing(state: BeaconState, sync_committee_slashing: SyncCommitteeSlashing) -> None:
    is_slashable = False

    # 检查证据是否按 `attested_header.slot` 降序排列且不是来自未来
    evidence_1 = sync_committee_slashing.evidence_1
    evidence_2 = sync_committee_slashing.evidence_2
    assert state.slot >= evidence_1.signature_slot > evidence_1.attested_header.slot >= evidence_1.finalized_header.slot
    assert state.slot >= evidence_2.signature_slot > evidence_2.attested_header.slot >= evidence_2.finalized_header.slot
    assert evidence_1.attested_header.slot >= evidence_2.attested_header.slot

    # 只有当前和之前同步委员会期间之间的冲突数据才是可罚没的;
    # 在新周期中,同步委员会最初会签署之前同步委员会期间的区块。
    # 这允许与恶意检查点同步的验证者在未来的周期中再次做出贡献
    evidence_1_attested_period = compute_sync_committee_period_at_slot(evidence_1.attested_header.slot)
    evidence_2_attested_period = compute_sync_committee_period_at_slot(evidence_2.attested_header.slot)
    assert evidence_1_attested_period <= evidence_2_attested_period + 1

    # 不允许为给定的 slot 签署冲突的 `attested_header`
    if evidence_1.attested_header.slot == evidence_2.attested_header.slot:
        if evidence_1.attested_header != evidence_2.attested_header:
            is_slashable = True

    # 不允许签署冲突的已最终确定的 `next_sync_committee`
    evidence_1_finalized_period = compute_sync_committee_period_at_slot(evidence_1.finalized_header.slot)
    evidence_2_finalized_period = compute_sync_committee_period_at_slot(evidence_2.finalized_header.slot)
    if (
        evidence_1_attested_period == evidence_2_attested_period
        and evidence_1_finalized_period == evidence_1_attested_period
        and evidence_2_finalized_period == evidence_2_attested_period
        and sync_committee_slashing_evidence_has_finality(evidence_1)
        and sync_committee_slashing_evidence_has_finality(evidence_2)
        and sync_committee_slashing_evidence_has_sync_committee(evidence_1)
        and sync_committee_slashing_evidence_has_sync_committee(evidence_2)
    ):
        if evidence_1.next_sync_committee != evidence_2.next_sync_committee:
            is_slashable = True

    # 不允许签署非线性的最终确定历史记录
    recent_finalized_slot = sync_committee_slashing.recent_finalized_slot
    recent_finalized_block_root = sync_committee_slashing.recent_finalized_block_root
    if (
        not sync_committee_slashing_evidence_has_finality(evidence_1)
        or not sync_committee_slashing_evidence_has_finality(evidence_2)
    ):
        assert recent_finalized_block_root == Root()
    if recent_finalized_block_root == Root():
        assert recent_finalized_slot == 0
    else:
        # 可以包括 Merkle 证明,以表明 `finalized_header` 与
        # 相对于给定 `recent_finalized_block_root` 的 `actual_finalized_block_root` 不匹配。
        # 最终确定的历史记录是线性的。因此,不匹配表示在不相关的链上签名。
        # 请注意,仅签署替代历史记录是不可罚没的,只要它是一致的即可。
        # 这允许与恶意检查点同步的验证者在未来的周期中再次做出贡献
        linear_1 = (evidence_1.actual_finalized_block_root == hash_tree_root(evidence_1.finalized_header))
        linear_2 = (evidence_2.actual_finalized_block_root == hash_tree_root(evidence_2.finalized_header))
        assert not linear_1 or not linear_2
        assert linear_1 or linear_2  # 不要在仅签署替代历史记录时进行罚没

        # `actual_finalized_branch` 可能以提供的具有最高 slot 的 `finalized_header` 为根
        rooted_in_evidence_1 = (
            evidence_1.finalized_header.slot >= evidence_2.finalized_header.slot
            and recent_finalized_slot == evidence_1.finalized_header.slot
            and recent_finalized_block_root == evidence_1.actual_finalized_block_root and linear_1
        )
        rooted_in_evidence_2 = (
            evidence_2.finalized_header.slot >= evidence_1.finalized_header.slot
            and recent_finalized_slot == evidence_2.finalized_header.slot
            and recent_finalized_block_root == evidence_2.actual_finalized_block_root and linear_2
        )

        # 或者,如果无法直接从攻击中获得有关非线性的证据,
        # 则可以证明其中一个 `finalized_header` 是我们的信标节点
        # 同步到的规范最终确定链的一部分,而另一个 `finalized_header` 是不相关的。
        rooted_in_canonical = (
            recent_finalized_slot < state.slot <= recent_finalized_slot + SLOTS_PER_HISTORICAL_ROOT
            and recent_finalized_slot <= compute_start_slot_at_epoch(state.finalized_checkpoint.epoch)
            and recent_finalized_block_root == state.state_roots[recent_finalized_slot % SLOTS_PER_HISTORICAL_ROOT]
        )
        assert rooted_in_evidence_1 or rooted_in_evidence_2 or rooted_in_canonical
        is_slashable = True

    assert is_slashable

    # 检查可罚没的验证者是否已排序、已知且参与了两个签名
    will_slash_any = False
    sync_aggregate_1 = evidence_1.sync_aggregate
    sync_aggregate_2 = evidence_2.sync_aggregate
    sync_committee_pubkeys_1 = evidence_1.sync_committee_pubkeys
    sync_committee_pubkeys_2 = evidence_2.sync_committee_pubkeys
    participant_pubkeys_1 = [
        pubkey for (bit, pubkey) in zip(sync_aggregate_1.sync_committee_bits, sync_committee_pubkeys_1)
        if bit
    ]
    participant_pubkeys_2 = [
        pubkey for (bit, pubkey) in zip(sync_aggregate_2.sync_committee_bits, sync_committee_pubkeys_2)
        if bit
    ]
    slashable_validators = sync_committee_slashing.slashable_validators
    num_validators = len(state.validators)
    for i, index in enumerate(slashable_validators):
        assert (
            index < num_validators
            and (i == 0 or index > slashable_validators[i - 1])
        )
        assert state.validators[index].pubkey in participant_pubkeys_1
        assert state.validators[index].pubkey in participant_pubkeys_2
        if is_slashable_validator(state.validators[index], get_current_epoch(state)):
            will_slash_any = True
    assert will_slash_any

    # 验证证据,包括签名
    assert is_valid_sync_committee_slashing_evidence(
        evidence_1,
        recent_finalized_block_root,
        recent_finalized_slot,
        state.genesis_validator_root,
    )
    assert is_valid_sync_committee_slashing_evidence(
        evidence_2,
        recent_finalized_block_root,
        recent_finalized_slot,
        state.genesis_validator_root,
    )

    # 执行罚没
    for index in slashable_validators:
        if is_slashable_validator(state.validators[index], get_current_epoch(state)):
            slash_validator(state, index)

理由

用例是什么?

如果没有罚没,轻客户端同步协议会受到一些限制。虽然钱包应用程序可能会从中受益(风险在于显示不正确的数据),并且新的信标节点可能会使用它来加速链同步,但其他有趣的用例(如桥、代币分发或其他需要证明的系统)取决于提供更高安全保证的机制。

通过使同步委员会成员的攻击可罚没,可以提供足够高的威慑力。即使在最简单的情况下,也必须贿赂大多数同步委员会才能成功发起攻击,这代表了相当大的可罚没余额。

向后兼容性

本 EIP 需要进行硬分叉,因为它引入了新的共识验证规则。

一旦共识验证规则到位,就可以单独引入支持基础设施,包括但不限于:

  • 罚没保护 DB 更新,以保证诚实的验证者不会在重组时被罚没
  • 验证者客户端/远程签名者 API,以传递与罚没保护相关的信息
  • 用于在信标节点之间交换罚没证据的 libp2p 网格
  • Slasher,用于监控潜在目标并构建罚没证据
  • 信标 API,用于提交和监控罚没证据

测试用例

待定

参考实现

待定

安全考虑

待定

版权

通过 CC0 放弃版权和相关权利。

Citation

Please cite this document as:

Etan Kissling (@etan-status), "EIP-7657: 同步委员会的罚没 [DRAFT]," Ethereum Improvement Proposals, no. 7657, March 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7657.