EIP-7732 -- 分叉选择

fradamt 发布于 2025-08-19 阅读 21

EIP-7732 对以太坊分叉选择算法进行了重大修改,引入了 PayloadStatus(COMMITTED、EMPTY、FULL)区分共识块和执行负载状态,新增 PTC(Payload Timeliness Committee)投票机制,并通过 ForkChoiceNode 结构体在 LMD-GHOST 中同时考虑区块根和负载状态。

注意:本文档仍在开发中,面向研究人员和实现者。

<!-- mdformat-toc start --slug=github --no-anchors --maxlevel=6 --minlevel=2 -->

<!-- mdformat-toc end -->

介绍

这是伴随 EIP-7732 升级的分叉选择修改。

常量

名称
PAYLOAD_TIMELY_THRESHOLD PTC_SIZE // 2 (= 256)
INTERVALS_PER_SLOT 4 # [在 EIP-7732 中修改]

容器

新的 PayloadStatus

class PayloadStatus(IntEnum):
    COMMITTED = 0
    EMPTY = 1
    FULL = 2

新的 ForkChoiceNode

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

辅助函数

修改后的 LatestMessage

注意:该类已修改为跟踪 Slot 而不是 Epoch。

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

修改后的 update_latest_messages

注意:函数 update_latest_messages 已更新为使用 Attestation Slot 而不是 Target。请注意,此函数仅在已验证的 Attestation 上调用,且验证者不能在同一个 Epoch 内进行两次 Attestation(否则构成双重投票,Equivocating)。另请注意,Target Epoch 编号和 Slot 编号在 validate_on_attestation 中已通过验证。

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
            )

修改后的 Store

注意Store 已修改为跟踪“空”共识区块的中间状态,即那些对应的 Execution Payload 尚未揭示或尚未上链的共识区块。

@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, boolean] = 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)
    payload_states: Dict[Root, BeaconState] = field(default_factory=dict)  # [EIP-7732 中新增]
    ptc_vote: Dict[Root, Vector[boolean, PTC_SIZE]] = field(default_factory=dict)  # [EIP-7732 中新增]

修改后的 get_forkchoice_store

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)},
        checkpoint_states={justified_checkpoint: copy(anchor_state)},
        unrealized_justifications={anchor_root: justified_checkpoint},
        payload_states={anchor_root: copy(anchor_state)},  # [EIP-7732 中新增]
        ptc_vote={anchor_root: Vector[boolean, PTC_SIZE]()},
    )

notify_ptc_messages

def notify_ptc_messages(
    store: Store, state: BeaconState, payload_attestations: Sequence[PayloadAttestation]
) -> None:
    """
    从 ``payload_attestations`` 中提取 ``PayloadAttestationMessage`` 列表并用它们更新 Store。
    这些 Payload Attestation 被假定已包含在信标区块中,因此无需签名验证。
    """
    if state.slot == 0:
        return
    for payload_attestation in payload_attestations:
        indexed_payload_attestation = get_indexed_payload_attestation(
            state, Slot(state.slot - 1), 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,
                ),
            )

is_payload_present

def is_payload_present(store: Store, beacon_block_root: Root) -> bool:
    """
    返回具有 Root ``beacon_block_root`` 的信标区块的 Execution Payload 是否被 PTC 投票为存在,并且在本地被判定为可用。
    """
    # 信标区块 Root 必须是已知的
    assert beacon_block_root in store.ptc_vote
    return (
        sum(store.ptc_vote[beacon_block_root]) > PAYLOAD_TIMELY_THRESHOLD
        and beacon_block_root in store.payload_states
    )

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_header.message.parent_block_hash
    message_block_hash = parent.body.signed_execution_payload_header.message.block_hash
    return PayloadStatus.FULL if parent_block_hash == message_block_hash else PayloadStatus.EMPTY

is_parent_node_full

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

修改后的 get_ancestor

注意get_ancestor 已修改为返回链是基于 区块还是 区块。

def get_ancestor(store: Store, root: Root, slot: Slot) -> ForkChoiceNode:
    """
    返回具有 ``root`` 的信标区块在 ``slot`` 处的祖先信标区块 Root 和 Payload 状态。
    如果具有 ``root`` 的信标区块已经处于 ``slot`` 或我们正在请求“未来”的祖先,则返回 ``PayloadStatus.COMMITTED``。
    """
    block = store.blocks[root]
    if block.slot &lt;= slot:
        return ForkChoiceNode(root=root, payload_status=PayloadStatus.COMMITTED)

    parent = store.blocks[block.parent_root]
    if parent.slot > slot:
        return get_ancestor(store, block.parent_root, slot)
    else: 
        return ForkChoiceNode(
        root=block.parent_root,
        payload_status=get_parent_payload_status(store, block),
    )

修改后的 get_checkpoint_block

注意get_checkpoint_block 已修改为使用新的 get_ancestor

def get_checkpoint_block(store: Store, root: Root, epoch: Epoch) -> Root:
    """
    计算区块 ``root`` 链中 epoch ``epoch`` 的 Checkpoint 区块。
    """
    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`` 的 Slot 期间以 ``node.payload_status`` 指示的 Payload 内容作为 Head。
    """
    block = store.blocks[node.root]
    if node.root == message.root:
        if node.payload_status == PayloadStatus.COMMITTED: 
            return True
        if message.slot &lt;= block.slot:
            return False
        if message.payload_present:
            return node.payload_status == PayloadStatus.FULL
        else:
            return node.payload_status == PayloadStatus.EMPTY

    else:
        ancestor = get_ancestor(store, message.root, block.slot)
        return node.root == ancestor.root and (
            node.payload_status == PayloadStatus.COMMITTED  
            or node.payload_status == ancestor.payload_status
        )

修改后的 get_weight

def get_weight(store: Store, node: ForkChoiceNode) -> Gwei:
    # 在决定前一个 Slot 的区块的 Payload 状态时,
    # 如果 PTC 的大多数成员为其投票,或者当前的 Proposer 基于该区块构建了区块,
    # 我们就认为 Payload 存在
    if (
        store.blocks[node.root].slot + 1 == get_current_slot(store)
        and node.payload_status != PayloadStatus.COMMITTED
    ):
        proposer_root = store.proposer_boost_root
        if is_payload_present(store, node.root) or (
            proposer_root != Root()
            and store.blocks[proposer_root].parent_root == node.root
            and is_parent_node_full(store, store.blocks[proposer_root])
        ):
            # 如果我们认为 Payload 存在,FULL 状态的权重
            # 将高于 EMPTY,反之亦然
            return Gwei(1) if node.payload_status == PayloadStatus.FULL else Gwei(0)
        else:
            return Gwei(1) if node.payload_status == PayloadStatus.EMPTY else Gwei(0)
    else:
        state = store.checkpoint_states[store.justified_checkpoint]
        unslashed_and_active_indices = [
            i
            for i in get_active_validator_indices(state, get_current_epoch(state))
            if not state.validators[i].slashed
        ]
        attestation_score = 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
                    and is_supporting_vote(store, node, store.latest_messages[i])
                )
            )
        )

        if store.proposer_boost_root == Root():
            # 如果未设置 ``proposer_boost_root``,则仅返回 Attestation 分数
            return attestation_score

        # 如果设置了 ``proposer_boost_root``,则计算 Proposer 分数
        proposer_score = Gwei(0)

        # ``proposer_boost_root`` 被视为对当前 Slot 中
        # Proposer 区块的投票。Proposer Boost
        # 会相应地应用于所有祖先
        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

新的 get_fork_choice_children

def get_fork_choice_children(store: Store, blocks: Dict[Root, BeaconBlock], node: ForkChoiceNode) -> Sequence[ForkChoiceNode]:
    if node.payload_status == PayloadStatus.COMMITTED:
        return [
        ForkChoiceNode(root=node.root, payload_status=payload_status) 
        for payload_status in (PayloadStatus.FULL, PayloadStatus.EMPTY)
        ]
    else:
        return [
            ForkChoiceNode(root=root, payload_status=PayloadStatus.COMMITTED) 
            for root in blocks.keys()
            if (
                blocks[root].parent_root == node.root
                and node.payload_status == get_parent_payload_status(store, blocks[root])
            )
        ]

修改后的 get_head

注意get_head 已修改为使用新的 get_weight 函数。它返回对应于 Head 区块的 ForkChoiceNode 对象。

def get_head(store: Store) -> ForkChoiceNode:
    # 获取仅包含可行分支的过滤后的区块树
    blocks = get_filtered_block_tree(store)
    # 执行 LMD-GHOST 分叉选择
    head = ForkChoiceNode(
        root=store.justified_checkpoint.root,
        payload_status=PayloadStatus.COMMITTED,
    )
    
    while True:
        children = get_fork_choice_children(store, blocks, head)
        if len(children) == 0:
            return head
        # 按最新的投票余额排序,平局时按字典顺序打破
        head = max(children, key=lambda child: (get_weight(store, child), child.root, child.payload_status))

更新后的分叉选择处理器

修改后的 on_block

注意:处理器 on_block 已修改为不仅根据父区块 Root,还根据父区块 Hash 来考虑给定共识信标区块的 Pre state。此外,我们将 Blob 数据可用性的检查延迟到 Execution Payload 的处理过程中。

def on_block(store: Store, signed_block: SignedBeaconBlock) -> None:
    """
    收到新区块时运行 ``on_block``。
    """
    block = signed_block.message
    # 父区块必须是已知的
    assert block.parent_root in store.block_states

    # 检查此区块是构建在空的还是满的父区块上
    parent_block = store.blocks[block.parent_root]
    header = block.body.signed_execution_payload_header.message
    parent_header = parent_block.body.signed_execution_payload_header.message
    # 制作状态副本以避免可变性问题
    if is_parent_node_full(store, block):
        assert block.parent_root in store.payload_states
        state = copy(store.payload_states[block.parent_root])
    else:
        assert header.parent_block_hash == parent_header.parent_block_hash
        state = copy(store.block_states[block.parent_root])

    # 区块不能处于未来。如果是,必须延迟考虑它们,直到它们处于过去。
    current_slot = get_current_slot(store)
    assert current_slot >= block.slot

    # 检查区块是否晚于已 Finalized 的 Epoch Slot(减少调用 get_ancestor 的优化)
    finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)
    assert block.slot > finalized_slot
    # 检查区块是否是 Checkpoint Finalized Slot 处已 Finalized 区块的后代
    finalized_checkpoint_block = get_checkpoint_block(
        store,
        block.parent_root,
        store.finalized_checkpoint.epoch,
    )
    assert store.finalized_checkpoint.root == finalized_checkpoint_block

    # 检查区块是否有效并计算 Post-state
    block_root = hash_tree_root(block)
    state_transition(state, signed_block, True)

    # 将新区块添加到 Store
    store.blocks[block_root] = block
    # 为此区块向 Store 添加新状态
    store.block_states[block_root] = state
    # 为此区块向 Store 添加新的 PTC 投票
    store.ptc_vote[block_root] = [False] * PTC_SIZE

    # 通知 Store 关于区块中的 payload_attestations
    notify_ptc_messages(store, state, block.body.payload_attestations)
    # 如果区块及时,则添加 Proposer 分数 Boost
    time_into_slot = (store.time - store.genesis_time) % SECONDS_PER_SLOT
    is_before_attesting_interval = time_into_slot &lt; SECONDS_PER_SLOT // INTERVALS_PER_SLOT
    is_timely = get_current_slot(store) == block.slot and is_before_attesting_interval
    store.block_timeliness[hash_tree_root(block)] = is_timely

    # 如果区块及时且不与现有区块冲突,则添加 Proposer 分数 Boost
    is_first_block = store.proposer_boost_root == Root()
    if is_timely and is_first_block:
        store.proposer_boost_root = hash_tree_root(block)

    # 必要时更新 Store 中的 Checkpoint
    update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint)

    # 提前计算未实现的 Justification 和 Finality。
    compute_pulled_up_tip(store, block_root)

新的分叉选择处理器

新的 on_execution_payload

处理器 on_execution_payload 在节点收到用于同步的 SignedExecutionPayloadEnvelope 时被调用。

def on_execution_payload(store: Store, signed_envelope: SignedExecutionPayloadEnvelope) -> None:
    """
    收到新 Execution Payload 时运行 ``on_execution_payload``。
    """
    envelope = signed_envelope.message
    # 相应的信标区块 Root 需要是已知的
    assert envelope.beacon_block_root in store.block_states

    # 检查 Blob 数据是否可用
    # 如果不可用,此 Payload 可能会被排队,并在 Blob 数据可用时随后被考虑
    assert is_data_available(envelope.beacon_block_root, envelope.blob_kzg_commitments)

    # 制作状态副本以避免可变性问题
    state = copy(store.block_states[envelope.beacon_block_root])

    # 处理 Execution Payload
    process_execution_payload(state, signed_envelope, EXECUTION_ENGINE)

    # 为此 Payload 向 Store 添加新状态
    store.payload_states[envelope.beacon_block_root] = state

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``。
    """
    # 信标区块 Root 必须是已知的
    data = ptc_message.data
    # PTC Attestation 必须针对已知区块。如果区块未知,延迟考虑直到找到该区块
    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:
        # 检查 Attestation 是否针对当前 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 投票
    ptc_index = ptc.index(ptc_message.validator_index)
    ptc_vote = store.ptc_vote[data.beacon_block_root]
    ptc_vote[ptc_index] = data.payload_present

    # 仅当区块针对当前 Slot 且处于早期时,才使用来自区块的 Attestation 更新 Payload Boost
    if is_from_block and data.slot + 1 != get_current_slot(store):
        return
    time_into_slot = (store.time - store.genesis_time) % SECONDS_PER_SLOT
    if is_from_block and time_into_slot >= SECONDS_PER_SLOT // INTERVALS_PER_SLOT:
        return

validate_on_attestation

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

    # 如果给定的 Attestation 不是来自信标区块消息,我们必须检查 Target Epoch 范围。
    if not is_from_block:
        validate_target_epoch_against_current_time(store, attestation)

    # 检查 Epoch 编号和 Slot 编号是否匹配
    assert target.epoch == compute_epoch_at_slot(attestation.data.slot)

    # Attestation Target 必须针对已知区块。如果 Target 区块未知,延迟考虑直到找到该区块
    assert target.root in store.blocks

    # Attestation 必须针对已知区块。如果区块未知,延迟考虑直到找到该区块
    assert attestation.data.beacon_block_root in store.blocks
    # Attestation 不得针对未来的区块。如果不是,则不应考虑该 Attestation
    slot = store.blocks[attestation.data.beacon_block_root].slot
    assert slot &lt;= attestation.data.slot

    # [EIP-7732 中新增] 
    assert attestation.data.index in [0,1]
    if slot == attestation.data.slot:
        assert attestation.data.index == 0

    # LMD 投票必须与 FFG 投票 Target 一致
    assert target.root == get_checkpoint_block(
        store, attestation.data.beacon_block_root, target.epoch
    )

    # Attestation 只能影响后续 Slot 的分叉选择。
    # 延迟在分叉选择中的考虑,直到它们的 Slot 处于过去。
    assert get_current_slot(store) >= attestation.data.slot + 1

修改后的 validate_merge_block

函数 validate_merge_block 已针对测试目的进行了修改

def validate_merge_block(block: BeaconBlock) -> None:
    """
    检查 Execution Payload 的父 PoW 区块是否是有效的终端 PoW 区块。

    注意:不可用的 PoW 区块稍后可能会变得可用,
    客户端软件可以延迟调用 ``validate_merge_block``,
    直到 PoW 区块变得可用。
    """
    if TERMINAL_BLOCK_HASH != Hash32():
        # 如果使用 `TERMINAL_BLOCK_HASH` 作为覆盖,则必须达到激活 Epoch。
        assert compute_epoch_at_slot(block.slot) >= TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH
        assert (
            block.body.signed_execution_payload_header.message.parent_block_hash
            == TERMINAL_BLOCK_HASH
        )
        return

    pow_block = get_pow_block(block.body.signed_execution_payload_header.message.parent_block_hash)
    # 检查 `pow_block` 是否可用
    assert pow_block is not None
    pow_parent = get_pow_block(pow_block.parent_hash)
    # 检查 `pow_parent` 是否可用
    assert pow_parent is not None
    # 检查 `pow_block` 是否是有效的终端 PoW 区块
    assert is_valid_terminal_pow_block(pow_block, pow_parent)
  • 原文链接: github.com/fradamt/conse...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论