Gloas注解:分叉选择

  • potuz_
  • 发布于 6天前
  • 阅读 22

本文深入探讨了以太坊Gloas分叉中关于分叉选择(Forkchoice)机制的修改,重点关注EIP-7732引入的新型节点结构和状态,以及如何处理执行负载的缺失情况。

带注释的 gloas 分叉选择

目录

这是关于注释 Gloas 分叉系列的第三篇帖子。在这篇文章中,我们将介绍分叉选择的更改。分叉选择中仍然缺少一些最终细节,特别是处理 PTC 双重截止日期以及如何处理 PTC 证明中的数据可用性布尔值。由于关于这些剩余更改似乎存在全球共识,因此我冒昧地在此处指出它们,即使它们可能不完全是最终规范中的内容。这些内容将在本文档中明确标记,并且无论如何它们只是对当前规范的非常小的修改。

感谢 Manu Nalepa 继续发送更正。

简介

在今天的以太坊中,大多数时候,对于给定的 slot,最多只有一个信标区块是有效的。验证者需要考虑这个区块是否存在,是否是规范链的一部分,或者不是。在大多数情况下,一个 slot 的提议者,比如 34,当遇到如下所示的重组场景时:

34
34
32
33
31

...

需要在他们收到的最新区块(在本例中是区块 33)或前一个区块(在本例中是区块 32)之上进行构建之间做出选择。在顶部链中,区块 33 将显示为 skipped,在底部链中,区块 32 将显示为 skipped。区块可以是 fullskipped

EIP-7732 引入了一种全新的分叉选择节点类型,即对于给定的 slot,比如 32,该 slot 的共识区块可能是规范的,但相应的 execution payload 可能会丢失。由于历史原因,我将在这里将这些 slot 称为 empty,并用橙色表示。现在有一种新的重组类型,其中提议者可以尝试仅重组过去 slot 的 execution payload,但不一定是其信标区块。因此,在以下情况下:

34
33
34

32

32
31

...

33 的提议者已选择重组 32 的 payload,34 的提议者需要选择是构建在 33 之上(在这种情况下使用其 payload),还是构建在 32 之上并使用其 payload。当然,还有许多其他选择。34 的提议者可以构建在没有其 payload 的 33 之上,构建在没有其 payload 的 32 之上等等。但这种情况表明,存在这种新的可能的重组,其中顶部链只有一个 payload 丢失(32 的 payload),而底部链缺少一个完整的区块(33 的区块)。在顶部链中,橙色节点表示 slot 32 是 empty,而在底部,缺少 slot 33 是 skipped

新增/修改的结构

我们可以从头开始完全重写分叉选择,因为一个全新的节点结构的存在使得现有的 protoarray 有点难以使用。但这对于已经拥有有效分叉选择的客户端来说将是非常具有侵入性的。因此,我们通过添加一个新结构并修改另一个结构来适应当前的分叉选择。

ForkchoiceNode

class ForkChoiceNode(Container):
    root: Root
    payload_status: PayloadStatus

此结构表示如上的节点,其中 payload_status 字段可以采用值 PAYLOAD_STATUS_PENDING,当 payload 仍然预期出现时使用,例如在上面的白色节点 34 中;PAYLOAD_STATUS_EMPTY 表示如上的橙色节点;payload 未包含,或者 PAYLOAD_STATUS_FULL 表示如上的浅蓝色节点。

需要对状态 PAYLOAD_STATUS_PENDING 进行一些解释。Python 规范不会在存储中跟踪 ForkChoiceNode 对象。它会在同步区块和可选 payload 之后保留所有 post-states。当需要关于节点的 payload状态的粒度时,对象 ForkChoiceNode 会在内部帮助程序上返回。因此,这些 ForkChoiceNode 对象是在调用 get_ancestorget_node_childrenget_head 时创建的。在迭代时,值 PAYLOAD_STATUS_PENDING 在这些函数内部使用,下面将添加更多详细信息,但是在计算 head 时,循环从具有 payload 状态 PAYLOAD_STATUS_PENDING 的已证明 checkpoint 开始,并且 get_node_children 始终返回至少一个具有 PAYLOAD_STATUS_EMPTY 和相同根的子节点。如果存在从它们派生的实际信标区块,get_node_children 将返回一个列表,所有这些列表都将具有 PAYLOAD_STATUS_PENDING,并且循环的下一次迭代返回具有实际有效 payload 状态的节点(emptyfull)。因此,当前的 Python 实现重载了 pending 的含义来处理这个在子节点上降序的循环。从某种意义上说,具有 PAYLOAD_STATUS_PENDING 的分叉选择节点扮演着同时具有相同信标区块根但具有 PAYLOAD_STAUS_EMPTYPAYLOAD_STATUS_FULL 的节点的 parent 的角色。这在下面的 is_supporting_vote 中起作用,因为对 empty 或 full 的投票都支持状态为 pending 的节点。

LatestMessage

@dataclass(eq=True, frozen=True)
class LatestMessage(object):
    slot: Slot
    root: Root
    payload_present: boolean

Gloas 中的证明重载了 index 字段,以便指示它们是证明 full 还是 empty slot,我们将此值包含在 LatestMessage 修改结构的 payload_present 布尔值中。此外,我们跟踪 slot 而不是 epoch,以处理相同 slot 证明与旧区块证明的情况,正如我们已经在 first post 已经解释过。

在分叉选择中,当证明者证明先前的 slot 的区块时,他们已经具有 payload 信息,并且分叉选择必须有一种方法来考虑这一点。有两种可能的方法来处理这个问题,一种是如本文档(和当前规范)中所述,即在证明中明确声明投票是针对 full 还是 empty。另一种是使用(区块,slot,payload)类型的投票,其中在slot S对先前区块根的证明有效地反对该区块在S期间作为 head 的任何后代。最初这是为了避免对证明类型进行任何修改的设计,因为它被认为具有太强的侵入性。然而,在Electra fork中将委员会索引重载为设置为零的想法,使得可以在不对证明进行任何结构性更改的情况下发出 payload 内容信号。

由于在分叉选择中我们只存储有效的证明,所以 slot 严格给出正确的 epoch,因为 epoch 保证是目标 checkpoint 的 epoch,并且保证是与此 slot 对应的 epoch。验证者不能在同一 epoch 的不同 slot 上证明不同的消息,否则将被罚没。

update_latest_messages

def update_latest_messages(
    store: Store, attesting_indices: Sequence[ValidatorIndex], attestation: Attestation
) -> None:
    slot = attestation.data.slot
    beacon_block_root = attestation.data.beacon_block_root
    payload_present = attestation.data.index == 1
    non_equivocating_attesting_indices = [\
        i for i in attesting_indices if i not in store.equivocating_indices\
    ]
    for i in non_equivocating_attesting_indices:
        if i not in store.latest_messages or slot > store.latest_messages[i].slot:
            store.latest_messages[i] = LatestMessage(
                slot=slot, root=beacon_block_root, payload_present=payload_present
            )

这个函数的唯一区别是我们使用 slot 而不是最新消息中的 epoch。

Store

@dataclass
class Store(object):
    time: uint64
    genesis_time: uint64
    justified_checkpoint: Checkpoint
    finalized_checkpoint: Checkpoint
    unrealized_justified_checkpoint: Checkpoint
    unrealized_finalized_checkpoint: Checkpoint
    proposer_boost_root: Root
    equivocating_indices: Set[ValidatorIndex]
    blocks: Dict[Root, BeaconBlock] = field(default_factory=dict)
    block_states: Dict[Root, BeaconState] = field(default_factory=dict)
    block_timeliness: Dict[Root, Vector[boolean, NUM_BLOCK_TIMELINESS_DEADLINES]] = field(
        default_factory=dict
    )
    checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict)
    latest_messages: Dict[ValidatorIndex, LatestMessage] = field(default_factory=dict)
    unrealized_justifications: Dict[Root, Checkpoint] = field(default_factory=dict)
    # [Gloas 中的新增功能:EIP7732]
    execution_payload_states: Dict[Root, BeaconState] = field(default_factory=dict)
    # [Gloas 中的新增功能:EIP7732]
    ptc_vote: Dict[Root, Vector[boolean, PTC_SIZE]] = field(default_factory=dict)

这两个新增项是为了能够跟踪不同类型的节点。现有的字典 block_states 用于跟踪同步区块后的 post-state。在 Gloas 之后,这个 post-state 不包括 execution payload 的处理,因此,它对应于 emptyorange 节点,如上所述。我们添加了一个新的状态字典,用于跟踪处理 execution payload 后的 post-state,它对应于 fulllightblue 节点,如上所述。ptc_vote 跟踪为区块投的 PTC 证明。

此结构很可能会更改以添加微小的调整。目前有这个 open PR 可以独立跟踪数据可用性的 PTC 投票。它将重命名最新字段并添加一个新字段:

    # [Gloas 中的新增功能:EIP7732]
    payload_timeliness_vote: Dict[Root, Vector[boolean, PTC_SIZE]] = field(default_factory=dict)
    # [Gloas 中的新增功能:EIP7732]
    payload_data_availability_vote: Dict[Root, Vector[boolean, PTC_SIZE]] = field(default_factory=dict)

添加这个的目的是,正如我们在本系列的 first post 中所解释的那样,来自不同委员会成员的数据可用性投票和 Payload timeliness 投票可以用于实现其中一个的独立法定人数。

由于 Store 已修改,我们需要修改相应的 getter:

def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) -> Store:
    assert anchor_block.state_root == hash_tree_root(anchor_state)
    anchor_root = hash_tree_root(anchor_block)
    anchor_epoch = get_current_epoch(anchor_state)
    justified_checkpoint = Checkpoint(epoch=anchor_epoch, root=anchor_root)
    finalized_checkpoint = Checkpoint(epoch=anchor_epoch, root=anchor_root)
    proposer_boost_root = Root()
    return Store(
        time=uint64(anchor_state.genesis_time + SECONDS_PER_SLOT * anchor_state.slot),
        genesis_time=anchor_state.genesis_time,
        justified_checkpoint=justified_checkpoint,
        finalized_checkpoint=finalized_checkpoint,
        unrealized_justified_checkpoint=justified_checkpoint,
        unrealized_finalized_checkpoint=finalized_checkpoint,
        proposer_boost_root=proposer_boost_root,
        equivocating_indices=set(),
        blocks={anchor_root: copy(anchor_block)},
        block_states={anchor_root: copy(anchor_state)},
        block_timeliness={anchor_root: [True, True]},
        checkpoint_states={justified_checkpoint: copy(anchor_state)},
        unrealized_justifications={anchor_root: justified_checkpoint},
        # [Gloas 中的新增功能:EIP7732]
        execution_payload_states={anchor_root: copy(anchor_state)},
        # [Gloas 中的新增功能:EIP7732]
        payload_timeliness_vote={anchor_root: Vector[boolean, PTC_SIZE]()},
        # [Gloas 中的新增功能:EIP7732]
        payload_data_availability_vote={anchor_root: Vector[boolean, PTC_SIZE]()},
    )

我正在此函数中包含 payload_data_availability_vote,就好像上述提到的 PR 将被合并一样。否则,当前规范仅更新 payload timeliness 投票。

Payload Attestations(有效载荷证明)

当接收到有效负载证明时,将调用以下帮助程序

on_payload_attestation_message

def on_payload_attestation_message(
    store: Store, ptc_message: PayloadAttestationMessage, is_from_block: bool = False
) -> None:
    """
    从区块内或直接在线上收到新的 ``ptc_message`` 时,运行 ``on_payload_attestation_message``。
    """
    # 信标区块根必须已知
    data = ptc_message.data
    # PTC 证明必须针对已知的区块。如果区块未知,则延迟考虑直到找到该区块
    state = store.block_states[data.beacon_block_root]
    ptc = get_ptc(state, data.slot)
    # PTC 投票只能更改其分配的信标区块的投票,否则将提前返回
    if data.slot != state.slot:
        return
    # 检查证明者是否来自 PTC
    assert ptc_message.validator_index in ptc

    # 如果来自线路,则验证签名并检查其是否针对当前 slot
    if not is_from_block:
        # 检查证明是否针对当前 slot
        assert data.slot == get_current_slot(store)
        # 验证签名
        assert is_valid_indexed_payload_attestation(
            state,
            IndexedPayloadAttestation(
                attesting_indices=[ptc_message.validator_index],
                data=data,
                signature=ptc_message.signature,
            ),
        )
    # 更新区块的投票
    ptc_index = ptc.index(ptc_message.validator_index)
    payload_timeliness_vote = store.payload_timeliness_vote[data.beacon_block_root]
    payload_timeliness_vote[ptc_index] = data.payload_present
    payload_data_availability_vote = store.payload_data_availability_vote[data.beacon_block_root]
    payload_data_availability_vote[ptc_index] = data.blob_data_available

我们只考虑我们已经知道的区块的证明,以便我们可以获得处理该信标区块的 post-state。我们只考虑相同 slot 的证明,然后验证签名并更新存储中的两个投票字典

我正在此函数中包含 payload_data_availability_vote,就好像上述提到的 PR 将被合并一样。否则,当前规范仅更新 payload timeliness 投票。

notify_ptc_messages

此帮助程序从区块处理过程中调用,以处理有效负载证明

def notify_ptc_messages(
    store: Store, state: BeaconState, payload_attestations: Sequence[PayloadAttestation]
) -> None:
    """
    从 ``payload_attestations`` 中提取 ``PayloadAttestationMessage`` 列表,并使用它们更新存储
    这些 Payload 证明被假定为在信标区块中,因此不需要签名验证
    """
    if state.slot == 0:
        return
    for payload_attestation in payload_attestations:
        indexed_payload_attestation = get_indexed_payload_attestation(state, payload_attestation)
        for idx in indexed_payload_attestation.attesting_indices:
            on_payload_attestation_message(
                store,
                PayloadAttestationMessage(
                    validator_index=idx,
                    data=payload_attestation.data,
                    signature=BLSSignature(),
                ),
                is_from_block=True,
            )

它只是提取索引的有效负载证明以创建 on_payload_attestation 帮助程序采用的相应 PayloadAttestationMessage。回想一下,我们决定为单个有效负载证明使用不同的对象而不是聚合对象。这是避免处理单个位 bitlist 的代价。

is_payload_timely

此帮助程序只是解析 payload timeliness 投票,并检查是否存在一个阈值来认为它是及时的。

def is_payload_timely(store: Store, root: Root) -> bool:
    """
    返回是否 PTC 将具有根 ``root`` 的信标区块的 execution payload
    投票为存在,并且已在本地确定为可用。
    """
    # 信标区块根必须已知
    assert root in store.payload_timeliness_vote

    # 如果 payload 在本地不可用,则 payload
    # 无论 PTC 投票如何,都不会被认为是可用的
    if root not in store.execution_payload_states:
        return False

    return sum(store.payload_timeliness_vote[root]) > PAYLOAD_TIMELY_THRESHOLD

is_payload_data_available

此帮助程序只是解析 payload timeliness 投票,并检查是否存在一个阈值来认为其 blob 数据可用。

def is_payload_data_available(store: Store, root: Root) -> bool:
    """
    返回是否 PTC 将具有根 ``root`` 的信标区块的 blob 数据
    投票为存在,并且已在本地确定为可用。
    如果节点已恢复所有 blob 数据,则 Implemnetations 可以返回 `True`。
    """
    # 信标区块根必须已知
    assert root in store.payload_data_availability_vote

    # 如果 payload 在本地不可用,则 blob 数据
    # 无论 PTC 投票如何,都不会被认为是可用的
    if root not in store.execution_payload_states:
        return False

    return sum(store.payload_data_availability_vote[root]) > DATA_AVAILABILITY_TIMELY_THRESHOLD

我正在此函数中包含 payload_data_availability_vote,就好像上述提到的 PR 将被合并一样。否则,当前规范仅更新 payload timeliness 投票。

请注意,这些帮助程序利用了节点无法同步数据未被认为可用的 execution payload 信封这一事实。

另请注意,在完全验证 execution payload 之前,节点无法在分叉选择存储中包含 execution payload。特别是,这意味着完全执行,这意味着如果尽早调用这些帮助程序(例如,在下一个 slot 开始之前),则它们可能会返回 false,而实际上,payload 可能既及时又可用。

Parent status(父状态)

实现区块处理的一个常见主题是 incoming 区块的父区块是什么?。在 Gloas 之前,人们只需获取父区块根的 store.block_states 条目即可。现在情况并非如此,因为这将指向 empty slot。当收到一个信标区块时,我们还需要确定其 execution layer 的父级,以查看它是否构建在 emptyfull slot 之上。这些帮助程序处理这个问题。

get_parent_payload_status

def get_parent_payload_status(store: Store, block: BeaconBlock) -> PayloadStatus:
    parent = store.blocks[block.parent_root]
    parent_block_hash = block.body.signed_execution_payload_bid.message.parent_block_hash
    message_block_hash = parent.body.signed_execution_payload_bid.message.block_hash
    return PAYLOAD_STATUS_FULL if parent_block_hash == message_block_hash else PAYLOAD_STATUS_EMPTY

is_parent_node_full

def is_parent_node_full(store: Store, block: BeaconBlock) -> bool:
    return get_parent_payload_status(store, block) == PAYLOAD_STATUS_FULL

请注意,这些帮助程序与 PTC 无关,它们只是获取一个节点并检查其父节点是 full 还是 empty。另请注意,这里我说“获取一个节点”,但这些函数获取一个信标区块。原因是给定信标区块根的 full 或 empty 节点都指向同一个父节点。该帮助程序利用了这样一个事实,即如果父节点是 empty,则 CL 父节点的区块将在其提交的中包含与 incoming 区块的中相同的区块哈希。

get_ancestor

def get_ancestor(store: Store, root: Root, slot: Slot) -> ForkChoiceNode:
    """
    返回具有 ``root`` 的信标区块在 ``slot`` 的祖先的信标区块根和 payload 状态。如果信标区块具有 ``root`` 已经在 ``slot``,或者我们正在请求“未来的”祖先,它将返回 ``PAYLOAD_STATUS_PENDING``。
    """
    block = store.blocks[root]
    if block.slot <= slot:
        return ForkChoiceNode(root=root, payload_status=PAYLOAD_STATUS_PENDING)

    parent = store.blocks[block.parent_root]
    while parent.slot > slot:
        block = parent
        parent = store.blocks[block.parent_root]

    return ForkChoiceNode(
        root=block.parent_root,
        payload_status=get_parent_payload_status(store, block),
    )

回想一下,从上面我们可以知道,获取分叉选择节点的父级(因此是祖先),与获取相应信标区块根的父级/祖先相同。但是,父节点需要是一个完整的 ForkChoiceNode,因为它具有唯一的 payload 状态,它是 empty 还是 full。但是,有时会使用 block.slot <= slot 来调用此函数。这是因为有时我们希望评估 head 并已将 head 区块推进到将来的 slot(例如,考虑缺少的 slot)。在这些情况下,我们只需返回 PAYLOAD_STATUS_PENDING。Gloas 分叉选择规范减少了对 get_ancestor 的调用次数。

get_checkpoint_block

仅修改此函数是因为 get_ancestor 现在返回 ForkchoiceNode 而不是 Root

def get_checkpoint_block(store: Store, root: Root, epoch: Epoch) -> Root:
    """
    计算区块 ``root`` 链中 epoch ``epoch`` 的检查点区块
    """
    epoch_first_slot = compute_start_slot_at_epoch(epoch)
    return get_ancestor(store, root, epoch_first_slot).root

is_supporting_vote

def is_supporting_vote(store: Store, node: ForkChoiceNode, message: LatestMessage) -> bool:
    """
    返回对 ``message.root`` 的投票是否支持包含信标区块 ``node.root`` 的链,并且
    ``node.payload_status`` 指示的 payload 内容在 slot ``node.slot`` 期间作为 head。
    """
    block = store.blocks[node.root]
    if node.root == message.root:
        if node.payload_status == PAYLOAD_STATUS_PENDING:
            return True
        assert message.slot >= block.slot
        if message.slot == block.slot:
            return False
        if message.payload_present:
            return node.payload_status == PAYLOAD_STATUS_FULL
        else:
            return node.payload_status == PAYLOAD_STATUS_EMPTY
    else:
        ancestor = get_ancestor(store, message.root, block.slot)
        return (
            node.root == ancestor.root
            and (
                node.payload_status == PAYLOAD_STATUS_PENDING
                or node.payload_status == ancestor.payload_status
            )
        )

这是 EIP-7732 更改的核心。docstring 描述了这个函数的用途:给定一个在 message 中编码的证明,它是否支持分叉选择节点 node?让我们分析一下 if-else 语句的第一个分支。

Attestations directly for the node’s root

回想一下我们上面关于 pending payload 状态的含义的讨论。对信标区块根的任何投票,无论是 empty 还是 full 状态,都对 pending 状态进行计数,这是第一个分支的语句。最后两个分支是显而易见的,对 full 的证明不应计入 empty,反之亦然。

assert 强制执行与添加到 validate_on_attestation 中相同的规则,以处理来自未来的区块。当 message.slot == block.slot 时,用于处理相同 slot 证明的情况,它应该支持 pending 状态,但不支持 emptyfull

Attestations for a different block root

当证明的区块根与节点的不同时,我们请求该 slot 的祖先。只有当消息的区块根的 slot 小于或等于 block.slot 时,函数 get_ancestor 才会返回 PAYLOAD_STATUS_PENDING。在这种情况下,get_ancestor 的返回值将是

ForkChoiceNode(root=message.root, payload_status=PAYLOAD_STATUS_PENDING)

因此 node.root != ancestor.root,因为我们实际上处于这两个根不同的分支中。因此,这些投票不支持节点。请注意,在这种情况下,该区块在证明的未来或在同一 slot 中,但在争用分支中,因此这些证明肯定不应支持该区块的根。

否则,get_ancestor 恰好返回一个节点,要么是 full,要么是 empty,对 full 或 empty 的证明都支持该根的 pending 状态,否则,该证明的祖先必须具有正确的 payload 状态。例如,在以下情况中

33

32

32

31

...

在 slot 33 中,一个不喜欢 payload 重组的证明者,将证明存在 payload 的 32 的根。该证明支持 32 的信标块根作为 head(它将对应于 PAYLOAD_STATUS_PENDING),它也支持存在 payload 的 32 的信标块根作为 head(PAYLOAD_STATUS_FULL),但它不支持具有 PAYLOAD_STATUS_EMPTY 的橙色分支。

相反,对 33 的块根的证明将支持具有根 32 和 PAYLOAD_STATUS_PENDING 的 forkchoice 节点,但它不支持底部的浅蓝色分支,因为它包含具有 PAYLOAD_STATUS_PRESENT 的 32 的块根,这与具有根 33 的消息的祖先不同:祖先将是具有 PAYLOAD_STATUS_EMPTY 的 32 的根。

should_extend_payload

def should_extend_payload(store: Store, root: Root) -> bool:
    proposer_root = store.proposer_boost_root
    if not is_payload_data_available(store, root):
        return False
    return (
        is_payload_timely(store, root)
        or proposer_root == Root()
        or store.blocks[proposer_root].parent_root != root
        or is_parent_node_full(store, store.blocks[proposer_root])
    )

调用此函数是为了在 head 来自前一个 slot 时,决定 emptyfull 之间的平局打破。如果该 payload 的数据不可用,我们不应扩展该 payload。请注意,如果数据未被本地视为可用,则该 payload 甚至不会出现在存储中。辅助函数 is_payload_data_available 会考虑 PTC 投票。所有导致扩展 payload 的分支都很容易理解

  • 如果 payload 是及时的,我们会扩展它以保护构建者免受恶意攻击。
  • 如果没有设置 proposer boost,例如,如果当前 slot 没有块到达,我们会扩展 payload。
  • 如果有一个及时的块,但不是建立在 head 之上,那么我们也会扩展 payload,这是为了防止新的提议者重组 payload。
  • 最后,如果新的及时块已经到达并且是建立在带有其 payload 的 head 之上,那么我们扩展 payload,也就是遵循传入块的分支。

因此,本质上,我们在此函数中返回 false 的方式的唯一途径是,传入的及时共识块是基于没有 payload 的 head 之上,并且 payload 从我们的角度来看是迟到的,也就是说,我们让传入的块重组迟到的 payload。

get_payload_status_tiebreaker

def get_payload_status_tiebreaker(store: Store, node: ForkChoiceNode) -> uint8:
    if node.payload_status == PAYLOAD_STATUS_PENDING or store.blocks[\
        node.root\
    ].slot + 1 != get_current_slot(store):
        return node.payload_status
    else:
        # To decide on a payload from the previous slot, choose
        # between FULL and EMPTY based on `should_extend_payload`
        if node.payload_status == PAYLOAD_STATUS_EMPTY:
            return 1
        else:
            return 2 if should_extend_payload(store, node.root) else 0

在运行 head 循环时会调用此函数。它用于在节点的子节点的 fullempty 分支之间做出决定。head 循环在具有 PAYLOAD_STATUS_PENDING 的节点和具有 empty 和 full payload 的子节点之间交替。此函数重载了 PayloadStatus 枚举的值以使用它们的实际数值。默认情况下,首选 Full payload。如果我们试图确定除上一个 slot 之外的任何子节点的平局打破,我们将首选 full 节点而不是 empty 节点,而不是 pending 节点。调用此函数是为了在所有子节点都具有 PAYLOAD_STATUS_PENDING 或都没有此状态时做出决定。在前一种情况下,根应该已经决定了平局打破,因此第一次检查似乎是多余的,有一个 open PR 来删除它。

让我们分析一下这个平局打破发挥作用的两个主要案例。head 循环已到达 fork root, PAYLOAD_STATUS_PENDING 的节点,并且具有此根的块来自前一个 slot。有两个子节点,empty 节点和 full 节点。并且在此 slot 中有一个及时的传入块正在重组 payload。我们尚未处理当前 slot 的任何证明,因为证明只能在一个 slot 之后处理。因此,来自前一个 slot 的所有对 root 的证明都不能计入 fullempty,它们是相同的 slot 证明,因此它们支持 pending 状态。在这种情况下,必须调用平局打破,因为两个子节点具有相同的根,两者都具有相同的权重(没有证明支持它们)。例如,如果 payload 不及时,并且当前 slot 的块及时基于 empty,则如上所述,should_extend_payload 将返回 False 并且 empty 分支将获胜,使传入的块成为 head。另一方面,如果 payload 及时,则 full 分支将获胜,这将成为 head,忽略传入的信标块根,即使它是及时的。

should_apply_proposer_boost

def should_apply_proposer_boost(store: Store) -> bool:
    if store.proposer_boost_root == Root():
        return False

    block = store.blocks[store.proposer_boost_root]
    parent_root = block.parent_root
    parent = store.blocks[parent_root]
    slot = block.slot

    # Apply proposer boost if `parent` is not from the previous slot
    if parent.slot + 1 < slot:
        return True

    # Apply proposer boost if `parent` is not weak
    if not is_head_weak(store, parent_root):
        return True

    # If `parent` is weak and from the previous slot, apply
    # proposer boost if there are no early equivocations
    equivocations = [\
        root\
        for root, block in store.blocks.items()\
        if (\
            store.block_timeliness[root][PTC_TIMELINESS_INDEX]\
            and block.proposer_index == parent.proposer_index\
            and block.slot + 1 == slot\
            and root != parent_root\
        )\
    ]

    return len(equivocations) == 0

此辅助函数用于检查何时应应用 Proposer Boost。此函数背后的主要思想是防止一个提议者连续持有两个区块,通过发布一个伪造的区块,然后在其中一个伪造的区块之上提议他们的第二个区块,从而恶意攻击第一个 slot 的构建者,重组暴露的 payload(以及其中一个伪造的区块)并 unblinding 构建者。如果没有伪造的区块,则始终应用 Proposer Boost。如果存在伪造的区块,则不会 将其应用于来自前一个 slot 的弱的、伪造的区块。这样做的目的是让看到这些区块的提议者需要重组它们,如果他们不这样做,我们不会将 Proposer Boost 应用于它们。

有关在此处分析的这些场景的完整说明,请参见 this PR

在伪造的情况下,这里的新增内容是检查 block_timeliness,现在记录了共识区块和 payload 的及时性。其想法是,在 PTC 截止日期之前到达的任何伪造的区块,应该已被下一个提议者看到,因此它应该已经重组了弱 head。在 PTC 截止日期之后到达的区块可能没有被提议者诚实地看到,因此我们不在此处计算它们,以免不公平地惩罚提议者。

get_attestation_score

def get_attestation_score(
    store: Store,
    # [Modified in Gloas:EIP7732]
    # Removed `root`
    # [New in Gloas:EIP7732]
    node: ForkChoiceNode,
    state: BeaconState,
) -> Gwei:
    unslashed_and_active_indices = [\
        i\
        for i in get_active_validator_indices(state, get_current_epoch(state))\
        if not state.validators[i].slashed\
    ]
    return Gwei(
        sum(
            state.validators[i].effective_balance
            for i in unslashed_and_active_indices
            if (
                i in store.latest_messages
                and i not in store.equivocating_indices
                # [Modified in Gloas:EIP7732]
                and is_supporting_vote(store, node, store.latest_messages[i])
            )
        )
    )

此函数仅被修改为使用 is_supporting_vote,因为该逻辑比简单地检查祖先的根更复杂。

get_weight

def get_weight(
    store: Store,
    # [Modified in Gloas:EIP7732]
    node: ForkChoiceNode,
) -> Gwei:
    if node.payload_status == PAYLOAD_STATUS_PENDING or store.blocks[\
        node.root\
    ].slot + 1 != get_current_slot(store):
        state = store.checkpoint_states[store.justified_checkpoint]
        attestation_score = get_attestation_score(store, node, state)
        if not should_apply_proposer_boost(store):
            # Return only attestation score if
            # proposer boost should not apply
            return attestation_score

        # Calculate proposer score if `proposer_boost_root` is set
        proposer_score = Gwei(0)

        # `proposer_boost_root` is treated as a vote for the
        # proposer's block in the current slot. Proposer boost
        # is applied accordingly to all ancestors
        message = LatestMessage(
            slot=get_current_slot(store),
            root=store.proposer_boost_root,
            payload_present=False,
        )
        if is_supporting_vote(store, node, message):
            proposer_score = get_proposer_score(store)

        return attestation_score + proposer_score
    else:
        return Gwei(0)

此函数返回在遍历 forkchoice 树时比较的主要值,它返回支持该节点的所有证明权重(以及可能的 proposer boost 根)。我们应该注意到第一次检查,以查看 payload 状态是否为 PAYLOAD_STATUS_PENDING 或 slot 是否不是上一个 slot。来自上一个 slot 的信标块的证明被视为 pending 状态。这是因为它们不可能证明 full 或 empty。对于之前的 slot,我们会考虑支持 full 或 empty 的权重。

证明分数是通过调用下面描述的 get_attestation_score 获得的,proposer boost 的添加就好像它是一个证明。我们为当前 slot 和 proposer boost 根创建一个虚假证明,其中 index==0 表示 pending 状态。如果此证明支持给定的节点,则我们添加完整的 proposer boost 分数。

get_node_children

def get_node_children(
    store: Store, blocks: Dict[Root, BeaconBlock], node: ForkChoiceNode
) -> Sequence[ForkChoiceNode]:
    if node.payload_status == PAYLOAD_STATUS_PENDING:
        children = [ForkChoiceNode(root=node.root, payload_status=PAYLOAD_STATUS_EMPTY)]
        if node.root in store.execution_payload_states:
            children.append(ForkChoiceNode(root=node.root, payload_status=PAYLOAD_STATUS_FULL))
        return children
    else:
        return [\
            ForkChoiceNode(root=root, payload_status=PAYLOAD_STATUS_PENDING)\
            for root in blocks.keys()\
            if (\
                blocks[root].parent_root == node.root\
                and node.payload_status == get_parent_payload_status(store, blocks[root])\
)\
        ]

在 forkchoice 树上迭代时会使用此辅助函数。它在 pending 状态和 empty/ full 状态之间交替。这里的主要思想如下。可以将 forkchoice 节点视为两个部分。有一个共识部分,它是在导入信标块时创建的。此共识部分已经足以选择唯一的 full 父级:共识块明确指定了父共识块和父执行块。

forkchoice 节点的另一部分与执行有关。payload 要么被包含,要么不被包含。payload 上不存在可能的伪造,因为块哈希在共识块中提交的已签名 bid 中指定。因此,节点的共识部分已经足以指定一个父级,但只有 full forkchoice 节点才能有子节点(因为每个子节点都指定了 payload 状态)。

我们在遍历 forkchice 树时采用的方法是,具有 PAYLOAD_STATUS_PENDING 的 forkchoice 节点仅对应于节点的共识端部分。它已经足够指定一个父级,但不足以指定子节点。一个 full 节点(具有 payload 状态 emptyfull)唯一地指向一个 pending 节点,因为它包含共识端。

因此,我们按如下方式交替。我们从具有 pending 状态的经过验证的检查点开始(这只是节点的共识部分),并构造具有 emptyfull 状态的 full 节点(如果我们已经看到了此节点的 payload)。这些 full 节点中的每一个都具有一个子节点列表,该列表仅包含具有 pending 状态的节点(因为共识部分足以指定父 full 节点),对 full 或 empty 的证明都计入 pending 状态,因此我们首先在 pending 子节点之间做出决定,然后我们向下移动到获胜的子节点并重复该过程。

这就是辅助函数中两个分支的原因:对于 pending 节点,我们始终添加一个空子节点,如果已同步相应的 payload,则可以选择添加一个 full 子节点。另一方面,对于 full 或 empty 节点,我们仅添加 pending 状态的子节点,对于从给定 full 节点派生的每个信标块根,添加一个子节点。

请注意,不能从给定的 pending 节点派生出多个 full 子节点。这是因为我们在信标块的已签名 bid 中提交给单个块哈希和 kzg 承诺。如果我们采用 slot 拍卖而不是块拍卖,我们将不得不处理多个 full 子节点,因为构建者可能会伪造。

get_head

def get_head(store: Store) -> ForkChoiceNode:
    # Get filtered block tree that only includes viable branches
    blocks = get_filtered_block_tree(store)
    # Execute the LMD-GHOST fork-choice
    head = ForkChoiceNode(
        root=store.justified_checkpoint.root,
        payload_status=PAYLOAD_STATUS_PENDING,
    )

    while True:
        children = get_node_children(store, blocks, head)
        if len(children) == 0:
            return head
        # Sort by latest attesting balance with ties broken lexicographically
        head = max(
            children,
            key=lambda child: (
                get_weight(store, child),
                child.root,
                get_payload_status_tiebreaker(store, child),
            ),
        )

这是一个小的更改,我们从经过验证的检查点共识块开始遍历(我们将考虑从 full 或 empty 派生的 head)并交替。对于每个 full 节点,我们首先通过检查从该节点派生的 pending 节点的权重来解码,如果存在平局,我们按根进行判断,就像我们在之前的 fork 中所做的那样。一旦我们选择了一个 pending 节点,我们就会下降到它,现在在 full 或 empty 子节点之间做出决定。由于证明者在投票给之前的 slot 时确实会发出 payload 偏好信号,因此权重实际上可能有所不同。如果权重相等,则根无论如何都会相等,因为这些都是同一 pending 节点的后代,因此平局是通过上面解释的 get_payload_status_tiebreaker 决定的。

record_block_timeliness

def record_block_timeliness(store: Store, root: Root) -> None:
    block = store.blocks[root]
    seconds_since_genesis = store.time - store.genesis_time
    time_into_slot_ms = seconds_to_milliseconds(seconds_since_genesis) % SLOT_DURATION_MS
    epoch = get_current_store_epoch(store)
    attestation_threshold_ms = get_attestation_due_ms(epoch)
    # [New in Gloas:EIP7732]
    is_current_slot = get_current_slot(store) == block.slot
    ptc_threshold_ms = get_payload_attestation_due_ms(epoch)
    # [Modified in Gloas:EIP7732]
    store.block_timeliness[root] = [\
        is_current_slot and time_into_slot_ms < threshold\
        for threshold in [attestation_threshold_ms, ptc_threshold_ms]\
    ]

这只是简单地修改为不仅跟踪区块何时在证明阈值之前到达,还要跟踪 payload 及时性委员会阈值。后者用于在伪造的情况下应用 proposer boost,如 should_apply_proposer_boost 中所述。

update_proposer_boost_root

def update_proposer_boost_root(store: Store, root: Root) -> None:
    is_first_block = store.proposer_boost_root == Root()
    # [Modified in Gloas:EIP7732]
    is_timely = store.block_timeliness[root][ATTESTATION_TIMELINESS_INDEX]

    # Add proposer score boost if the block is the first timely block
    # for this slot, with the same proposer as the canonical chain.
    if is_timely and is_first_block:
        head_state = copy(store.block_states[get_head(store).root])
        slot = get_current_slot(store)
        if head_state.slot < slot:
            process_slots(head_state, slot)
        block = store.blocks[root]
        # Only update if the proposer is the same as on the canonical chain
        if block.proposer_index == get_beacon_proposer_index(head_state):
            store.proposer_boost_root = root

这里唯一的更改是关于显式使用块及时性(而不是 PTC 截止日期)来表示证明截止日期。

validate_on_attestation

def validate_on_attestation(store: Store, attestation: Attestation, is_from_block: bool) -> None:
    target = attestation.data.target

    # If the given attestation is not from a beacon block message,
    # we have to check the target epoch scope.
    if not is_from_block:
        validate_target_epoch_against_current_time(store, attestation)

    # Check that the epoch number and slot number are matching.
    assert target.epoch == compute_epoch_at_slot(attestation.data.slot)

    # Attestation target must be for a known block. If target block
    # is unknown, delay consideration until block is found.
    assert target.root in store.blocks

    # Attestations must be for a known block. If block
    # is unknown, delay consideration until the block is found.
    assert attestation.data.beacon_block_root in store.blocks
    # Attestations must not be for blocks in the future.
    # If not, the attestation should not be considered.
    block_slot = store.blocks[attestation.data.beacon_block_root].slot
    assert block_slot <= attestation.data.slot

    # [New in Gloas:EIP7732]
    assert attestation.data.index in [0, 1]
    if block_slot == attestation.data.slot:
        assert attestation.data.index == 0

    # LMD vote must be consistent with FFG vote target
    assert target.root == get_checkpoint_block(
        store, attestation.data.beacon_block_root, target.epoch
    )

    # Attestations can only affect the fork-choice of subsequent slots.
    # Delay consideration in the fork-choice until their slot is in the past.
    assert get_current_slot(store) >= attestation.data.slot + 1

此函数中唯一的更改是我们在此处强制执行 index 字段对于同一 slot 的证明必须为 0,而对于之前的 slot,它只能为 01

is_head_late

def is_head_late(store: Store, head_root: Root) -> bool:
    return not store.block_timeliness[head_root][ATTESTATION_TIMELINESS_INDEX]

唯一的更改是显式使用 ATTESTATION_TIMELINESS_INDEX 来恢复先前的行为。

is_head_weak

def is_head_weak(store: Store, head_root: Root) -> bool:
    # Calculate weight threshold for weak head
    justified_state = store.checkpoint_states[store.justified_checkpoint]
    reorg_threshold = calculate_committee_fraction(justified_state, REORG_HEAD_WEIGHT_THRESHOLD)

    # Compute head weight including equivocations
    head_state = store.block_states[head_root]
    head_block = store.blocks[head_root]
    epoch = compute_epoch_at_slot(head_block.slot)
    head_node = ForkChoiceNode(root=head_root, payload_status=PAYLOAD_STATUS_PENDING)
    head_weight = get_attestation_score(store, head_node, justified_state)
    for index in range(get_committee_count_per_slot(head_state, epoch)):
        committee = get_beacon_committee(head_state, head_block.slot, CommitteeIndex(index))
        head_weight += Gwei(
            sum(
                justified_state.validators[i].effective_balance
                for i in committee
                if i in store.equivocating_indices
            )
        )

    return head_weight < reorg_threshold

此函数中的添加是明确地添加在 head slot 的委员会中的伪造的索引的权重。原因是这些额外的证明,它们所能做的最坏的情况是使 head 不弱,因此不会惩罚建立在其之上的提议者,如 should_apply_proposer_boost 中所述。否则,伪造的验证者可能会欺骗提议者认为 head 不弱,在其之上提议,并看到其区块被重组,因为证明者没有计算这些伪造的证明,并认为 head 较弱,因此试图强制执行 propsoer 应该在伪造的情况下重组它。

is_parent_strong

def is_parent_strong(store: Store, root: Root) -> bool:
    justified_state = store.checkpoint_states[store.justified_checkpoint]
    parent_threshold = calculate_committee_fraction(justified_state, REORG_PARENT_WEIGHT_THRESHOLD)
    block = store.blocks[root]
    parent_payload_status = get_parent_payload_status(store, block)
    parent_node = ForkChoiceNode(root=block.parent_root, payload_status=parent_payload_status)
    parent_weight = get_attestation_score(store, parent_node, justified_state)
    return parent_weight > parent_threshold

此辅助函数已更改,因为它使用了需要节点而不仅仅是根的函数 get_attestation_score,请注意,此函数不再计算 Proposer Boost。关于这一点,存在一个 open issue

时间戳到期职责

由于 slot 内的时间被更改,因此以下助手被更改,并添加了新的 payload 证明截止日期。

get_attestation_due_ms

def get_attestation_due_ms(epoch: Epoch) -> uint64:
    # [New in Gloas]
    if epoch >= GLOAS_FORK_EPOCH:
        return get_slot_component_duration_ms(ATTESTATION_DUE_BPS_GLOAS)
    return get_slot_component_duration_ms(ATTESTATION_DUE_BPS)

get_aggregate_due_ms

def get_aggregate_due_ms(epoch: Epoch) -> uint64:
    # [New in Gloas]
    if epoch >= GLOAS_FORK_EPOCH:
        return get_slot_component_duration_ms(AGGREGATE_DUE_BPS_GLOAS)
    return get_slot_component_duration_ms(AGGREGATE_DUE_BPS)

get_sync_message_due_ms

def get_sync_message_due_ms(epoch: Epoch) -> uint64:
    # [New in Gloas]
    if epoch >= GLOAS_FORK_EPOCH:
        return get_slot_component_duration_ms(SYNC_MESSAGE_DUE_BPS_GLOAS)
    return get_slot_component_duration_ms(SYNC_MESSAGE_DUE_BPS)

get_contribution_due_ms

def get_contribution_due_ms(epoch: Epoch) -> uint64:
    # [New in Gloas]
    if epoch >= GLOAS_FORK_EPOCH:
        return get_slot_component_duration_ms(CONTRIBUTION_DUE_BPS_GLOAS)
    return get_slot_component_duration_ms(CONTRIBUTION_DUE_BPS)

get_payload_attestation_due_ms

def get_payload_attestation_due_ms(epoch: Epoch) -> uint64:
    return get_slot_component_duration_ms(PAYLOAD_ATTESTATION_DUE_BPS)

on_block

在 Gloas 中处理区块时的主要难点是获取正确的父状态。信标块指定了共识和执行父级,只有两者的组合才能指定唯一的 forkchoice 节点,因此指定一个前状态。下面的逻辑通过检查 is_parent_full 来表示,在这种情况下,父状态取自 store.execution_payload_states。如果区块是建立在 empty 之上,我们强制父哈希等于先前的执行层父级,即它忽略了父信标区块的 payload,但是它建立在其父级之上。

def on_block(store: Store, signed_block: SignedBeaconBlock) -> None:
    """
    Run ``on_block`` upon receiving a new block.
    """
    block = signed_block.message
    # Parent block must be known
    assert block.parent_root in store.block_states

    # Check if this blocks builds on empty or full parent block
    parent_block = store.blocks[block.parent_root]
    bid = block.body.signed_execution_payload_bid.message
    parent_bid = parent_block.body.signed_execution_payload_bid.message
    # Make a copy of the state to avoid mutability issues
    if is_parent_node_full(store, block):
        assert block.parent_root in store.execution_payload_states
        state = copy(store.execution_payload_states[block.parent_root])
    else:
        assert bid.parent_block_hash == parent_bid.parent_block_hash
        state = copy(store.block_states[block.parent_root])

    # Blocks cannot be in the future. If they are, their consideration must be delayed until they are in the past.
    current_slot = get_current_slot(store)
    assert current_slot >= block.slot

    # Check that block is later than the finalized epoch slot (optimization to reduce calls to get_ancestor)
    finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)
    assert block.slot > finalized_slot
    # Check block is a descendant of the finalized block at the checkpoint finalized slot
    finalized_checkpoint_block = get_checkpoint_block(
        store,
        block.parent_root,
        store.finalized_checkpoint.epoch,
    )
    assert store.finalized_checkpoint.root == finalized_checkpoint_block

    # Check the block is valid and compute the post-state
    block_root = hash_tree_root(block)
    state_transition(state, signed_block, True)

    # Add new block to the store
    store.blocks[block_root] = block
    # Add new state for this block to the store
    store.block_states[block_root] = state
    # Add a new PTC voting for this block to the store
    store.payload_timeliness_vote[block_root] = [False] * PTC_SIZE
    store.payload_data_availability_vote[block_root] = [False] * PTC_SIZE

    # Notify the store about the payload_attestations in the block
    notify_ptc_messages(store, state, block.body.payload_attestations)

    record_block_timeliness(store, block_root)
    update_proposer_boost_root(store, block_root)

    # Update checkpoints in store if necessary
    update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint)

    # Eagerly compute unrealized justification and finality.
    compute_pulled_up_tip(store, block_root)

在选择了正确的父状态之后,我们初始化该根的 PTC 投票,并通知存储关于该区块中的 payload 证明。

请注意,所有关于数据可用性的处理都推迟到已提交的执行 payload 包络的处理。

on_execution_payload

这是一个新的处理程序,当它的状态转换函数分为两个时被调用。数据可用性检查在此阶段完成。父状态是导入信标区块后的状态。

def on_execution_payload(store: Store, signed_envelope: SignedExecutionPayloadEnvelope) -> None:
    """
    Run ``on_execution_payload`` upon receiving a new execution payload.
    """
    envelope = signed_envelope.message
    # The corresponding beacon block root needs to be known
    assert envelope.beacon_block_root in store.block_states

    # Check if blob data is available
    # If not, this payload MAY be queued and subsequently considered when blob data becomes available
    assert is_data_available(envelope.beacon_block_root)

    # Make a copy of the state to avoid mutability issues
    state = copy(store.block_states[envelope.beacon_block_root])

    # Process the execution payload
    process_execution_payload(state, signed_envelope, EXECUTION_ENGINE)

    # Add new state for this payload to the store
    store.execution_payload_states[envelope.beacon_block_root] = state

实施额外津贴

我将不断更新本节,因为我在 Prysm 客户端中亲自实现了 Gloas forkchoice。以下问题严格来说不在规范更改中,这让我觉得是困难的设计决策。

剪枝无效分支

当我们乐观地同步时,即使我们没有验证其 payload 内容,我们也会将节点放入 forkchoice 中。当 EL 赶上时,它可能会意识到先前已同步的整个分支(或许多分支)实际上是无效的。EL 告诉我们的是正在同步该 payload 的分支中最后一个有效的 payload 哈希。Prysm 现在的工作方式是在 forkchoice 中搜索最后一个有效的 payload 哈希,它对应于唯一的信标区块,并且存在一个唯一的信标块根,它是此最后一个有效节点的直接子节点,同时是当前正在同步的无效区块的祖先。我们删除该唯一节点及其所有子节点。

由于一个简单的原因,此机制在 Gloas 中将失败:提交给第一个无效 payload 哈希的信标区块实际上是有效的!生成该 payload 的构建者有过错,但是提议者没有过错。因此,我们需要删除 full 节点,而不是 empty 节点,然后我们需要删除从该 full 节点派生的每个节点,无论是 empty 还是 full。

保持比最终确定更早的信息。

我们需要将数据(例如最终确定的 payload 哈希)传递给引擎。这是信标区块中包含的 payload 哈希,该信标块具有最终确定的检查点的根。如果检查点块实际上在上面的 orange 意义上是 empty 会发生什么?我们不能采用已提交的 bid 的 payload 哈希,因为该 payload 实际上甚至可能是无效的!因此,我们需要跟踪在最终确定的检查点之前包含的最新 payload 哈希是什么,以便告知引擎什么是实际上已经上链的最后一个 payload。对于 Prysm 来说,这很棘手,因为它迫使我们跟踪这些信息,否则通常会在最终确定时被剪除。

从属根

这是之前 @dapplion 提出的问题,但我还是想简单地写一下,以防万一。在设计 forkchoice 时,至少 Prysm 在 pending 节点(pending nodes),以及作为其子节点的 empty/full 节点方面,并没有完全遵循规范。然而,我们通过将所有共识信息放在一个单独的节点中,并在 forkchoice 节点中保留指向其共识部分的指针,从而拥有一个逻辑上等价的结构。因此,empty 和 full 节点都将指向同一个共识部分,可以将其视为当前规范中的 pending 父节点。特别地,共识节点指定了一个父节点(full 节点无关紧要),并且只有 full 节点可以有子节点(仅指向共识节点)。这等同于此规范中 get_children 的交替性质。现在,当获取信标 API 的 dependent roots 或确保我们拥有正确的洗牌时,我们需要在 epoch 中导入的最新信标区块根。这通常通过查看下一个 epoch 的目标,如果该目标根位于 slot 零,则取其父节点来获得。如果它不在 slot 零(例如,因为错过了 slot 零),则目标根和 dependent roots 匹配。

现在的问题是,节点应该将什么作为其目标来跟踪?它们可以是节点,无论是 head 的 ancestors 中的 empty/full 节点,还是这些节点的 pending 部分或仅是共识部分。关键是所有这些都具有相同的根,因此相同的 slot,因此对于目标而言,这并不重要。由于 dependent root 是目标或其父节点,因此 dependent root 仅取决于 目标节点的共识部分。因此,节点是否跟踪 full 或 empty 节点作为目标并不重要,目标根和 dependent roots 都不会依赖于此选择。

但请注意,通过仅保留根,我们稍微削弱了 justification,当为目标 checkpoint 进行证明时,节点正在选择 full 或 empty 的其中一个分支,除非可能是在 slot 0 期间投票但尚未看到 payload 的那些节点。但是,我们削弱了 LMD 和 FFG 投票之间的一致性,因为对从 slot 0 的 full 节点派生的 blockroot 的投票,也算作对 empty 分支中同一根的 justification!我们可以更改 justification 和 finalization 以解决这个问题,但这被认为是次要的:我们最多只错过了 justification 过程中的一个 payload:我们现在正在 justification 此规范表示法中的 pending 节点。

  • 原文链接: potuz.net/posts/gloas-an...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
potuz_
potuz_
江湖只有他的大名,没有他的介绍。