本文详细介绍了以太坊2.0协议的设计与实现,包括权益证明、分片机制及其如何工作。通过对共识机制、激励机制和各种类型的数据结构的介绍,作者提供了对以太坊2.0的深度理解,并提供了相应的代码与示例,使读者能更好地理解这一复杂系统的运作原理。
注意:本文档写于 2020 年 7 月至 8 月。
除了本文档外,我强烈推荐 Ben Edgington 的注释版规范和 Danny Ryan 的“Phase 0 人类指南”。本文档更侧重于设计原理问题;阅读两者有助于更好地理解 eth2 协议。
<!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
Fork
ForkData
Checkpoint
Validator
AttestationData
IndexedAttestation
PendingAttestation
Eth1Data
HistoricalBatch
[旁注:存款流程说明]
DepositMessage
DepositData
BeaconBlockHeader
[旁注:域分离]
SigningData
ProposerSlashing
AttesterSlashing
Attestation
Deposit
VoluntaryExit
BeaconBlockBody
BeaconBlock
BeaconState
SignedVoluntaryExit
SignedBeaconBlock
SignedBeaconBlockHeader
integer_squareroot
xor
uint_to_bytes
bytes_to_uint64
hash
hash_tree_root
[旁注:验证者生命周期说明]
is_active_validator
is_eligible_for_activation_queue
is_eligible_for_activation
is_slashable_validator
is_slashable_attestation_data
is_valid_indexed_attestation
is_valid_merkle_branch
compute_shuffled_index
compute_proposer_index
compute_committee
compute_epoch_at_slot
compute_start_slot_at_epoch
compute_activation_exit_epoch
compute_fork_data_root
compute_fork_digest
compute_domain
compute_signing_root
get_current_epoch
get_previous_epoch
get_block_root
get_block_root_at_slot
get_randao_mix
get_active_validator_indices
get_validator_churn_limit
get_seed
get_committee_count_per_slot
get_beacon_committee
get_beacon_proposer_index
get_total_balance
get_total_active_balance
get_domain
get_indexed_attestation
get_attesting_indices
increase_balance
decrease_balance
initiate_validator_exit
slash_validator
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
以太坊 2.0(又称 eth2,又称 Serenity)是以太坊协议的下一个主要版本,是多年来对权益证明和分片研究的结晶。eth2 协议是对以太坊系统中共识关键部分的全面重新设计:从工作量证明到权益证明的共识变更,以及分片的引入是两个最关键的变更。截至本文撰写时,eth2 最大限度地保留了应用层部分不变;也就是说,交易和智能合约继续以与之前相同的方式工作,因此应用程序不需要更改(除了补偿一些 gas 成本变化)即可与 eth2 兼容。然而,确保网络就交易达成共识的引擎发生了根本性的变化。
详细答案请参见:
简而言之:
eth2 权益证明共识的参与者称为验证者。要成为验证者,你需要存入 32 ETH,可以从 eth1 链或分片链(当分片链启用时)存入。一旦你存入 32 ETH,你将被放入激活队列,一段时间后你将成为活跃验证者。
信标链上的时间分为周期和时隙。每个时隙为 12 秒(例如,如果链今天在 14:00:00 开始,当前时间是 14:01:06,那么你处于第 5 个时隙的中间;时隙编号从 0 开始计数)。一个周期是 32 个时隙(或 6.4 分钟)。还有更长的时间单位;按照惯例,2048 个周期(约 9.1 天)称为一个周(“ethereum week”);信标链上一些需要较长时间的操作可以用周来衡量。
在每个周期中,每个验证者都会进行一次证明。证明包含:
链通过这些证明达成共识。粗略地说,如果 2/3 的活跃验证者签署了支持某个区块的证明,则该区块将被最终化(实际上更复杂,需要两轮签名;详见 Casper FFG 论文)。与 PoW 中任何区块都可以被撤销不同,最终化的区块永远不会被撤销。
如果验证者正确地进行证明,他们将获得奖励。如果验证者错过了他们的时隙或进行了错误的证明,他们将受到惩罚。如果验证者明确地自相矛盾(例如在同一周期内投票支持两个冲突的区块),他们将被罚没。被罚没的验证者(i)将遭受其存款 3-100% 的惩罚,(ii)被强制从验证者集中移除,并且(iii)他们的代币将被强制锁定额外的 4 周才能提取。一般来说,只要你运行正确的验证者软件,这种情况就不会发生,只要你保持在线时间超过约 55-70%,验证将是有利可图的。
验证者可以随时自愿发起退出,尽管每个周期退出的数量有限制;如果太多验证者同时尝试退出,他们将被放入队列,并保持活跃状态,直到他们排到队列的前面。验证者成功退出后,经过 1/8 周后他们将能够提取(尽管此功能仅在 eth1 和 eth2 的“合并”后启用)。
请参见阶段 1 文档中的相应部分。
为了降低风险,eth2 的部署过程分为多个阶段:
另请参见路线图:
规范描述了 eth2 中使用的数据类型,以及状态转换函数(和用于定义状态转换函数的辅助函数)。这些函数用 Python 编写,以在形式化(规范完全可执行)和易读性之间取得平衡。所有函数都是强类型的,具有许多不同的类型,以明确输入和输出的含义。
类型系统基于 SimpleSerialize(SSZ),这是一个包含简单类型和复合类型(向量、结构体...)的类型系统,以及用于(i)序列化和(ii)对这些类型的对象进行 Merkle 哈希的算法(对象的 SSZ Merkle 哈希通常称为根)。SSZ 旨在在很大程度上取代 eth1 中的 RLP。
SSZ 中最常见的基本类型是整数(通常是 uint64
)和哈希(即 Bytes32
);也存在较罕见的类型,如 bool
、可变长度的 Bytes
、Bytes4
等。SSZ 中有四种复合类型:(i)固定长度的列表(称为向量),(ii)具有固定最大长度的可变长度列表(称为列表),(iii)结构体(称为容器)和(iv)联合类型(“要么是 X,要么是 Y”),在阶段 0 中未使用。
现在,让我们进入实际的规范...(阶段 0)
我们定义了以下 Python 自定义类型,用于类型提示和可读性:
名称 | SSZ 等效 | 描述 |
---|---|---|
Slot |
uint64 |
时隙编号 |
Epoch |
uint64 |
周期编号(通常,周期 i 由时隙 EPOCH_LENGTH*i ... EPOCH_LENGTH*(i+1)-1 组成) |
CommitteeIndex |
uint64 |
在每个周期中,验证者集被随机分为 EPOCH_LENGTH 部分,每个时隙对应一部分,但在每个时隙内,该时隙的验证者进一步分为委员会。在阶段 0 中,这种划分没有作用,但在阶段 1 中,这些不同的委员会将被分配到不同的分片。CommitteeIndex 只是一个整数类型,该整数指的是时隙内委员会的索引(它是第一个委员会、第二个、第三个?) |
ValidatorIndex |
uint64 |
每个验证者在存入时都会被分配一个验证者索引 |
Gwei |
uint64 |
以 Gwei 为单位的金额 |
Root |
Bytes32 |
Merkle 根(通常是 SSZ 对象的根) |
Version |
Bytes4 |
分叉版本号(用于防止一个 eth2 网络上的消息意外地在另一个 eth2 网络上有效,例如主网与测试网或主网与 ETC 类分叉链) |
DomainType |
Bytes4 |
域类型(为不同的签名消息赋予不同的域标签,以防止为一个功能签名的消息意外地在另一个功能中有效) |
ForkDigest |
Bytes4 |
当前分叉数据的摘要(用于重放保护) |
Domain |
Bytes32 |
签名域(结合了域类型和分叉版本的信息,因此我们在两个维度上都获得了重放保护) |
BLSPubkey |
Bytes48 |
BLS12-381 公钥(有关 BLS 签名方案及其优点的解释,请参见此处) |
BLSSignature |
Bytes96 |
BLS12-381 签名 |
当你看到像 def get_block_root_at_slot(state: BeaconState, slot: Slot) -> Root:
(规范中的一个真实示例)这样的函数时,将其解释为“此函数以信标链状态和一个表示时隙编号的整数作为输入,并输出一个 Bytes32
,即 Merkle 根”。在这种情况下,它输出的 Merkle 根是给定时隙的区块的根哈希(从名称可以看出);但一般来说,关注类型将有助于你更容易理解正在发生的事情。除了作为调试辅助工具外,强类型系统还充当一种注释。
以下值是在整个规范中使用的(不可配置的)常量。这些常量相当无聊;它们只是为了可读性而添加到规范中。
名称 | 值 |
---|---|
GENESIS_SLOT |
Slot(0) |
GENESIS_EPOCH |
Epoch(0) |
FAR_FUTURE_EPOCH |
Epoch(2**64 - 1) |
BASE_REWARDS_PER_EPOCH |
uint64(4) |
DEPOSIT_CONTRACT_TREE_DEPTH |
uint64(2**5) (= 32) |
JUSTIFICATION_BITS_LENGTH |
uint64(4) |
ENDIANNESS |
'little' |
在这里,我们有可配置的常量,即如果你将这些常量中的任何一个调整 2 倍或更多,网络可能不会崩溃。也就是说,设置这些常量的方式经过了大量思考,因此值得理解为什么每个值都设置为现在这样。
ETH1_FOLLOW_DISTANCE |
uint64(2**10) (= 1,024) |
---|
为了处理 eth1 存款,eth2 链跟踪 eth1 链的区块哈希。为了简化操作,eth2 链仅在延迟后(ETH1_FOLLOW_DISTANCE = 1,024
个区块)才关注 eth1 区块。假设 eth1 链不会回滚那么远,这让我们可以依赖以下假设:如果 eth2 看到一个 eth1 区块,它不会“取消看到”它(如果 eth1 确实回滚那么远,eth2 方面将需要采取紧急行动)。1024 个区块对应约 3.7 小时的延迟(请注意,将 eth1 区块接受到 eth2 还需要约 1.7 小时)。历史上,eth1 网络上的所有问题都在此时间段内得到响应。延长此时间段将(i)增加存款延迟,并且(ii)使 eth2 作为 eth1 的轻客户端不太方便。
MAX_COMMITTEES_PER_SLOT |
uint64(2**6) (= 64) |
---|
在阶段 0 中,每个时隙有多个委员会的想法没有作用;相反,这是为阶段 1 做准备,在阶段 1 中,每个委员会将被分配到不同的分片。我们计划开始时拥有 64 个分片。分片数量较少会导致可扩展性不足;分片数量较多会导致两个不良后果:
TARGET_COMMITTEE_SIZE |
uint64(2**7) (= 128) |
---|
为了使委员会安全,在任何给定周期内,其 2/3 被破坏的概率(假设攻击者占全局验证者集的 <1/3)必须极其微小。我们可以通过二项式公式估计这种破坏概率:
>>> # 阶乘
>>> def fac(n): return 1 if n==0 else n*fac(n-1)
>>> # 从 n 个项目中取 k 个项目的不同组合数
>>> def choose(n, k): return fac(n) // fac(k) // fac(n-k)
>>> # 如果事件在每次“试验”中发生的概率为 p,返回
>>> # 在 n 次试验中事件恰好发生 k 次的概率
>>> def prob(n, k, p): return p**k * (1-p)**(n-k) * choose(n, k)
>>> # 如果事件在每次“试验”中发生的概率为 p,返回
>>> # 在 n 次试验中事件至少发生 k 次的概率
>>> def probge(n, k, p): return sum([prob(n, i, p) for i in range(k, n+1)])
调用 probge(128, 86, 1/3)
(86 是 128 2/3 的最小整数)返回 `5.55 10**-15`(即 5.55 在千万亿分之一)。这是一个极低的概率,考虑到攻击者可能会“磨”许多随机种子以尝试获得有利的委员会(尽管这在 RANDAO 和特别是 VDFs 下非常困难)。如果委员会大小改为 64,这个概率会高得多,因此在攻击者拥有 1/3 总质押的情况下,委员会将不再足够安全。另一方面,将委员会大小增加到 256 将是多余的,只会增加不必要的低效。
MAX_VALIDATORS_PER_COMMITTEE |
uint64(2**11) (= 2,048) |
---|
<a id="churn" />
支持的最大验证者数量为 2**22
(=4,194,304),或约 1.34 亿 ETH 质押。假设每个周期 32 个时隙,每个时隙 64 个委员会,这使我们每个委员会最多有 2048 个验证者。
MIN_PER_EPOCH_CHURN_LIMIT |
uint64(2**2) (= 4) |
---|---|
CHURN_LIMIT_QUOTIENT |
`uint64(216)` (= 65,536)** |
这两个参数设置了验证者进入和退出验证者集的速率。最低速率为每个周期 4 个进入 + 4 个退出,但如果验证者数量足够多,这个速率会增加:如果有超过 262,144 个验证者(8,388,608 ETH),那么每个周期可以进入的验证者数量等于验证者集大小的 1/65536,同样数量的验证者可以退出。
限制进入和退出速率的目的是防止大量恶意验证者执行某些恶意操作,然后立即退出以逃避被罚没。我们主要担心的恶意操作是最终化两个不兼容的区块。Casper FFG 协议(参见此处的论文)确保这只有在至少 1/3 的验证者实施了可证明的恶意操作时才会发生,他们可以被罚没;然而,如果他们先退出,他们可能会逃避这种惩罚。
使用上述数字,如果有超过 8,388,608 ETH 质押,1/3 的验证者退出至少需要 65536/3 个周期,或 10.67 周(然而,如果没有攻击,退出队列通常会很短)。
设置较长的退出延迟是为了确保攻击者无法通过长时间向用户隐藏分叉,然后将其发布给离线一段时间的客户端来逃避被罚没。理论上,攻击者可以通过隐藏分叉超过 10.67 周来逃避被罚没;因此,我们有一个规则,客户端必须至少每 10.67 周上线一次(实际上比这稍微不频繁一些)以保留其完整的安全保证(这称为弱主观性)。
研究:
SHUFFLE_ROUND_COUNT |
uint64(90) |
---|
交换或不交换洗牌的轮数;更多信息请参见下面的 compute_shuffled_index
函数描述。专家密码学家建议我们 ~4*log2(n)
足够安全;在我们的情况下,n <= 2**22
,因此约 90 轮。
MIN_GENESIS_ACTIVE_VALIDATOR_COUNT |
uint64(2**14) (= 16,384) |
---|
启动 eth2 链所需的验证者存款数量。这需要 524,288 ETH,足以使攻击超出除少数非常富有的行为者之外的所有人的能力范围。
MIN_GENESIS_TIME |
uint64(1578009600) (2020 年 1 月 3 日) |
---|
即使有足够的验证者存款,创世也不会在此时间之前开始。
<a id="hysteresis" />
HYSTERESIS_QUOTIENT |
uint64(4) |
---|---|
HYSTERESIS_DOWNWARD_MULTIPLIER |
uint64(1) |
HYSTERESIS_UPWARD_MULTIPLIER |
uint64(5) |
EFFECTIVE_BALANCE_INCREMENT |
`Gwei(20 * 109)` (= 1,000,000,000) |
我们将验证者余额存储在两个地方:(i)验证者记录中的“有效余额”,和(ii)单独记录中的“精确余额”。这样做是为了提高效率。
精确余额每个周期都会变化(由于奖励和惩罚),因此我们将它们存储在一个紧凑的数组中,该数组只需要重新哈希 <32 MB 即可更新,而有效余额(用于所有其他需要验证者余额的计算)使用滞后公式更新:如果有效余额为 n
ETH,并且如果精确余额低于 n-0.25
ETH,则有效余额设置为 n-1
ETH,如果精确余额高于 n+1.25
ETH,则有效余额设置为 n+1
ETH。
由于精确余额必须至少变化 0.5 ETH 才能触发有效余额更新,这确保了攻击者无法通过反复将精确余额推高,然后推低某些阈值,使有效余额每个周期都更新——从而使处理链变得非常缓慢。
MIN_DEPOSIT_AMOUNT |
Gwei(2**0 * 10**9) (= 1,000,000,000) |
---|
最低存款金额防止了通过向链上发送非常小的存款进行 DoS 攻击(请注意,1 ETH 只能获得一个验证者槽位;除非你存入完整的 32 ETH,否则它不会激活)。
MAX_EFFECTIVE_BALANCE |
Gwei(2**5 * 10**9) (= 32,000,000,000) |
---|
这里有两个需要解释的选择。首先,为什么强制验证者槽位为固定数量的 ETH,而不是允许它们为任意大小?其次,为什么固定大小为 32 ETH,而不是 1 ETH 或 1000 ETH?
允许可变余额的问题在于,随机选择(例如区块提议者)和洗牌(用于委员会)的算法变得更加复杂。你需要一种算法来选择区块提议者,使得算法选择特定提议者的概率与提议者的余额成正比,在余额不断变化且验证者总是进入和退出的情况下。这可以通过花哨的二叉树结构来完成,但会很复杂。在委员会选择的情况下,富有的验证者不能被分配到一个委员会(因为他们会主导并能够攻击它);他们的权重需要分散到许多委员会中。通过简单地将富有的验证者正式表示为许多相同大小的单独验证者,可以更容易地解决这两个问题。
经济审查表明,在当前 32 ETH 水平下,质押的硬件成本足以对验证者回报产生重大影响,尽管不是致命的。这意味着,如果存款规模减少到 16 ETH,那么链的开销将翻倍,每个验证者的奖励将减半,因此使用单个验证者槽位进行质押将变得四倍困难,这已经是一个潜在不安全的水平。因此,32 ETH 是最具包容性的存款规模,不会因增加开销而自相矛盾。
EJECTION_BALANCE |
Gwei(2**4 * 10**9) (= 16,000,000,000) |
---|
低于 16 ETH 的验证者将被强制退出(即强制退出)。此最低限额确保所有活跃验证者的余额(几乎总是)在 2 倍“带”内(最大有效余额为 32 ETH;超过的部分只是保存的奖励,不计入质押目的)。这个狭窄的范围确保了委员会的稳定性;如果允许更大的差异,少数富有的恶意验证者可能会随机进入同一个委员会,并以其较大的余额接管它。
GENESIS_FORK_VERSION |
Version('0x00000000') |
---|
有关 Version
类型的解释,请参见上文。从 0 开始的版本是不言自明的(为什么不是 1?因为我们是计算机科学家,不是普通人,狗屎!)。
BLS_WITHDRAWAL_PREFIX |
Bytes1('0x00') |
---|
当验证者存入时,他们提供两个密钥:一个签名密钥和一个提款密钥。提款密钥用于在提取资金时访问资金;这种双密钥结构降低了验证者的风险,因为他们可以将提款密钥保存在冷存储中。
BLS 提款前缀实际上是提款密钥的“版本号”;第一个版本只是公钥的哈希;提款将揭示公钥以及使用该公钥签名的签名,指定进一步的目的地。未来版本将允许直接指定 eth2 任何分片上的智能合约地址。
GENESIS_DELAY |
uint64(172800) |
秒 | 2 天 |
---|
当存款数量足以启动 eth2 链时,启动会延迟 2 天,以便大家有时间准备。
SECONDS_PER_SLOT |
uint64(12) |
秒 | 12 秒 |
---|
区块链速度和风险之间的权衡。请注意,在未来的阶段中,一个时隙内必须发生多个步骤:信标区块 -> 分片区块 -> 信标区块,以及最终一轮数据可用性采样,因此保守一些是好的。
Eth1 延迟通常约为 1 秒;12 秒在此基础上提供了健康的安全边际。
SECONDS_PER_ETH1_BLOCK |
uint64(14) |
秒 | 14 秒 |
---|
对 eth1 区块平均出现频率的估计。 | MIN_ATTESTATION_INCLUSION_DELAY |
uint64(2**0) (= 1) |
时隙 | 12 秒 |
---|
在时隙 N 中生成的证明可以在时隙 N+1 中包含。
SLOTS_PER_EPOCH |
uint64(2**5) (= 32) |
时隙 | 6.4 分钟 |
---|
有两个原因不能将每个 epoch 的时隙数降低到 32 以下:
超过 32 会使区块达到最终性所需的时间不必要地延长(这需要 2 个 epoch)。因此,每个 epoch 32 个时隙似乎是最优的。
<a id="seeds" />
[旁注:RANDAO、种子和委员会生成]
在任何权益证明系统中,我们需要有一些机制来确定谁是区块的提议者(以及其他不需要所有活跃验证者同时参与的角色)。在 PoW 中,这是自动发生的:每个人都在尝试创建一个区块,但平均每(以太坊中 13 秒 | 比特币中 600 秒)只有一个人成功,你无法提前预测谁会成功。然而,在 PoS 中,这种随机选择必须明确进行。
显然,区块链中没有真正的“随机性”,因为所有节点必须就结果达成共识,不同的计算机调用 random()
会得到不同的输出。相反,我们从作为区块链一部分计算和更新的种子生成伪随机性。
挑战在于如何使种子不可预测。如果种子是可预测的(例如,我们在创世时将 hash(42)
作为种子),那么验证者可以策略性地决定何时存入和提取,或者他们的公钥是什么,以成为区块提议者(或成为特定委员会的一部分),这将为攻击打开大门(这是一种权益研磨)。
为了完全防止这种操纵,我们使用一种机制,其中验证者集提前 4 个 epoch 固定(即,epoch N 中的操作只能影响从 epoch N+5 开始的验证者集),并且有一个不断更新种子的过程。因此,验证者集操纵是无效的,因为在某个 epoch 的验证者集固定后,该 epoch 的有效种子可以保证在一段时间内不可预测地更新。也就是说,我们仍然需要一个种子如何更新的过程。
我们使用受 RANDAO 启发的机制在每个区块更新种子(或更准确地说,randao mix,用于生成种子):区块的提议者提供一个哈希值,该哈希值与种子混合(即,与种子进行异或运算);这个哈希值在公开之前是未知的,但它是预先提交的,因为提议者只能提交一个有效的哈希值。这是通过对当前 epoch 进行 BLS 签名来完成的;BLS 签名方案具有以下特性:对于任何给定的密钥,任何给定的消息只有一个有效的签名(与例如 ECDSA 不同,ECDSA 中可以使用相同的密钥为相同的消息生成许多可能的有效签名)。
epoch N 开始时的 randao mix 用于计算 epoch N+1 的种子;这确保提议者和委员会角色提前一个 epoch 已知,给验证者准备的机会。
这种机制确保提议者只有一个“操纵位”:他们可以通过正常发布区块将下一个种子设置为某个哈希 R1,或者通过不发布(并牺牲他们的区块奖励)将其设置为另一个哈希 R2。还要注意,只有最后一个提议者真正拥有任何操纵能力,因为任何其他提议者都知道种子将被未来的提议者以不可预测的方式改变,因此他们无法知道他们尝试的任何操纵的效果。这两个因素共同使得通过操纵种子进行“权益研磨”非常困难,几乎总是不值得。
要查看提议者和委员会选择算法,它们将 (i) 活跃验证者集和 (ii) 种子作为输入,并输出当前区块提议者和委员会,请参见这里。
MIN_SEED_LOOKAHEAD |
uint64(2**0) (= 1) |
epochs | 6.4 分钟 |
---|---|---|---|
MAX_SEED_LOOKAHEAD |
`uint64(22)` (= 4)** | epochs | 25.6 分钟 |
参见上图。MIN_SEED_LOOKAHEAD
意味着用于计算提议者和委员会的种子基于超过 1 个 epoch 前的 randao mix(具体来说,epoch N 的种子基于 epoch N-2 结束时的 randao mix);这允许验证者提前 >1 个 epoch 确定他们的委员会和提议责任。
MAX_SEED_LOOKAHEAD
实际上是验证者激活和退出的最小延迟;它基本上意味着策略性激活和退出的验证者只能影响未来 4 个 epoch 的种子,留下 3 个 epoch 的空间,提议者可以在其中混合未知信息以扰乱种子,从而使通过激活或退出验证者进行权益研磨不可行。
MIN_EPOCHS_TO_INACTIVITY_PENALTY |
uint64(2**2) (= 4) |
epochs | 25.6 分钟 |
---|
参见这里了解不活跃泄漏是什么;这个常数只是说不活跃泄漏在 4 个 epoch 的非最终性后开始。
EPOCHS_PER_ETH1_VOTING_PERIOD |
uint64(2**5) (= 32) |
epochs | ~3.4 小时 |
---|
eth2 链通过投票机制了解 eth1 区块(以便它可以验证存款的 Merkle 证明),其中区块提议者对 eth1 区块进行投票;诚实验证者指南详细说明了验证者选择哪个区块。投票期设置为 1024 个时隙,以确保足够的委员会规模,并给予时间应对潜在的故障;此外,投票期比 ETH1_FOLLOW_DISTANCE
(也约为 3.7 小时)短得多几乎没有价值。
<a id="slots_per_historical_root" />
SLOTS_PER_HISTORICAL_ROOT |
uint64(2**13) (= 8,192) |
时隙 | ~27 小时 |
---|
eth2 链包含其自身历史区块的 Merkle 树。这是通过两种数据结构完成的:(i) 一个旋转的“最近历史日志”(state.block_roots
和 state.state_roots
)和 (ii) 一个持续的累加器(state.historical_roots
),它存储最近历史日志的 Merkle 根。如果两者的长度大致相似,则总状态大小是最优的,两者都 ~sqrt(链的长度)
;将其设置为 8192 个时隙确保在 67,108,864 个时隙(= 1,024 周,约 20 年)时满足此条件。如果需要,可以在一个世纪后重新平衡长度以提高效率,尽管收益将微乎其微。
MIN_VALIDATOR_WITHDRAWABILITY_DELAY |
uint64(2**8) (= 256) |
epochs | ~27 小时 |
---|
提供合理的时间以确保可以惩罚行为不当的个别验证者。
SHARD_COMMITTEE_PERIOD |
uint64(2**8) (= 256) |
epochs | ~27 小时 |
---|
在第 1 阶段,这是分片上的提议者委员会重新洗牌的频率。在第 0 阶段,验证者只有在活跃至少这么长时间后才被允许退出;这可以防止垃圾邮件退出队列并反复退出和重新进入以强制自己进入特定分片。
EPOCHS_PER_HISTORICAL_VECTOR |
uint64(2**16) (= 65,536) |
epochs | ~0.8 年 |
---|
随机种子可见的时间范围;这实际上是验证者可以被惩罚的最长时间。
EPOCHS_PER_SLASHINGS_VECTOR |
uint64(2**13) (= 8,192) |
epochs | ~36 天 |
---|
这是验证者在被惩罚后必须等待的最短时间才能提取;在此期间,他们会受到与同一时间段内其他被惩罚验证者数量成比例的惩罚。
有关更多详细信息,请参见惩罚部分,以及这里了解为什么这样做。
HISTORICAL_ROOTS_LIMIT |
uint64(2**24) (= 16,777,216) |
历史根 | ~52,262 年 |
---|---|---|---|
VALIDATOR_REGISTRY_LIMIT |
`uint64(240)` (= 1,099,511,627,776)** | 验证者 |
SSZ 中的所有列表都必须有某个限制;52,262 年对于实际目的来说已经接近“永远”,并确保 Merkle 分支不会不必要地变长。1.1 万亿验证者也需要很长时间才能达到(假设所有 ETH 都在质押,每个 epoch 最多可以激活 64 个验证者,因此列表需要 ~160 亿个 epoch ~= 209052 年才能填满;假设巧妙地使用 1 ETH 验证者时隙、奖励等,这可能会加速,但仍需要数千年)。
BASE_REWARD_FACTOR |
uint64(2**6) (= 64) |
---|
有关详细信息,请参见帮助程序部分中的 get_base_reward
。
WHISTLEBLOWER_REWARD_QUOTIENT |
uint64(2**9) (= 512) |
---|
如果你提交导致验证者被惩罚的证据,你将获得其余额的 1/512 作为奖励。
PROPOSER_REWARD_QUOTIENT |
uint64(2**3) (= 8) |
---|
作为一般经验法则,区块的提议者获得他们包含的区块中其他验证者奖励的 1/8。这确保了包含证明和其他对象的足够激励,以及生产区块的激励。
<a id="inactivity-quotient" />
INACTIVITY_PENALTY_QUOTIENT |
uint64(2**24) (= 16,777,216) |
---|
有关不活跃泄漏的描述,请参见 Casper FFG 论文,该机制是如果链未能最终化,不活跃的验证者将开始遭受非常高的惩罚,直到活跃的验证者重新达到总验证者集的 2/3 以上(按余额加权),最终化可以重新开始。
一个 epoch 期间的惩罚大小与自链上次最终化以来经过的 epoch 数量成正比;这导致泄漏的总量随时间二次增长(注意泄漏在 4 个 epoch 的非最终性后开始):
自最终化以来的 epoch | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
本 epoch 泄漏 | 0 | 0 | 0 | 0 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
自最终化以来的总泄漏 | 0 | 0 | 0 | 0 | 5 | 11 | 18 | 26 | 35 | 45 | 56 | 68 |
INACTIVITY_PENALTY_QUOTIENT
是这里的“单位”,例如,如果总泄漏列为 68,这意味着你已经损失了 68/INACTIVITY_PENALTY_QUOTIENT ~= 1/246,723
的余额。
请注意,当总泄漏成为存款大小的相当一部分时,泄漏量开始减少,因为它是按当前余额的百分比计算的;因此,在这些情况下,总泄漏最好近似为指数函数:你原始余额的剩余部分不是 1 - 1/2 * epochs**2 / INACTIVITY_PENALTY_QUOTIENT
,而是 (1 - 1/INACTIVITY_PENALTY_QUOTIENT) ** (epochs**2/2)
。
剩余余额的另一种几乎等效的近似是 e ** -(epochs**2/(2*INACTIVITY_PENALTY_QUOTIENT))
,其中 e ~= 2.71828
。这意味着在 2**12
个 epoch(2 周)后,你原始余额的剩余部分是 e**(-1/2)
,即大约 60.6% 的原始余额。
MIN_SLASHING_PENALTY_QUOTIENT |
uint64(2**5) (= 32) |
---|
如果你被惩罚,你至少会损失 1/32 的存款(即使你是唯一被惩罚的人,被惩罚也必须有些伤害...)
名称 | 值 |
---|---|
MAX_PROPOSER_SLASHINGS |
2**4 (= 16) |
MAX_ATTESTER_SLASHINGS |
2**1 (= 2) |
MAX_ATTESTATIONS |
2**7 (= 128) |
MAX_DEPOSITS |
2**4 (= 16) |
MAX_VOLUNTARY_EXITS |
2**4 (= 16) |
这些操作是基于可以安全处理多少操作的计算设置的,尽管还有一个额外的约束,即 MAX_ATTESTATIONS
必须等于最大委员会数量(64)加上一个安全边际,以应对错过的提议或延迟或不同意的证明。
名称 | 值 |
---|---|
DOMAIN_BEACON_PROPOSER |
DomainType('0x00000000') |
DOMAIN_BEACON_ATTESTER |
DomainType('0x01000000') |
DOMAIN_RANDAO |
DomainType('0x02000000') |
DOMAIN_DEPOSIT |
DomainType('0x03000000') |
DOMAIN_VOLUNTARY_EXIT |
DomainType('0x04000000') |
DOMAIN_SELECTION_PROOF |
DomainType('0x05000000') |
DOMAIN_AGGREGATE_AND_PROOF |
DomainType('0x06000000') |
这些值在签名每种类型的消息时混合到消息中;这可以防止为一种目的签名的消息在另一种上下文中意外有效。
以下类型是 SimpleSerialize (SSZ) 容器。
注意:定义按拓扑顺序排列,以便于执行规范。
注意:如果某个容器类型的对象在初始化时未设置某些字段(例如,x = Fork(epoch=6)
,缺少 previous_version
和 current_version
),这些字段将设置为零(零容器当然递归地定义为所有字段都设置为零的容器)。
Fork
class Fork(Container):
previous_version: Version
current_version: Version
epoch: Epoch # 最新分叉的 epoch
此结构存在于状态中以存储 eth2 协议的当前版本。当发生硬分叉时,版本号会更改:如果某些新的硬分叉规则应该在 epoch N
生效,那么作为 epoch N
状态转换的一部分,state.fork
被修改为:
state.fork.previous_version
等于旧的 state.fork.current_version
state.fork.current_version
等于某个新选择的版本号state.fork.epoch
等于 N
意图是,如果当前 epoch 小于 state.fork.epoch
,则“当前分叉版本”等于 state.fork.previous_version
,如果当前 epoch 等于或大于 state.fork.epoch
,则等于 state.fork.current_version
。当前分版版本混合到所有 BLS 签名消息的签名数据中(参见 get_domain
)。
请注意,所有消息(区块、证明、VoluntaryExits...)都有一些相关的 epoch 编号。区块在其声明的时隙处理,但证明和其他结构确实有一个边缘情况:证明可以以某个自声明的 epoch E1
创建,但仅在某个后来的 epoch E2 > E1
包含在链上。边缘情况是,如果 E1
在分叉之前但 E2
在分叉之后呢?那么,即使消息是在新分叉时代处理的,消息的验证也假设签名数据混合了旧分叉版本。这就是为什么我们在状态中维护 state.fork.previous_version
。
如果有人想继续旧链,他们可以简单地不实施更改,包括不更改 state.fork
。在这种情况下,从分叉 epoch 开始,一个分叉的区块在另一个分叉中无效。分叉前生成的证明和其他对象可以包含在两个分叉中,但分叉后生成的证明和其他对象只能在一侧或另一侧有效。
ForkData
class ForkData(Container):
current_version: Version
genesis_validators_root: Root
这是一个虚拟结构,用于在 get_domain
中混合分叉版本和创世。
Checkpoint
class Checkpoint(Container):
epoch: Epoch
root: Root
以太坊的 Casper FFG 实现通过处理 epoch 边界哈希(即 epoch 开始前链中最近的区块哈希)来达成共识。Casper FFG 投票通常包括 (i) 源 epoch,(ii) 源区块哈希,(iii) 目标 epoch,(iv) 目标区块哈希,在状态中我们需要存储最新的证明 epoch(和哈希)以知道接受什么源,以及最新的最终化 epoch。
我们通过创建一个容器包装器来表示 epoch 和哈希来简化这一点,因此我们减少到包含两个检查点(源和目标)的 Casper FFG 投票,以及存储当前和上一个 epoch 的最新证明检查点和最新最终化检查点的状态。
Validator
class Validator(Container):
pubkey: BLSPubkey
withdrawal_credentials: Bytes32 # 用于提取的公钥承诺
effective_balance: Gwei # 质押的余额
slashed: boolean
# 状态 epoch
activation_eligibility_epoch: Epoch # 满足激活条件的时间
activation_epoch: Epoch
exit_epoch: Epoch
withdrawable_epoch: Epoch # 验证者可以提取资金的时间
这是包含与特定验证者相关的所有重要信息的结构(除了其确切余额;有关信息,请参见上面的滞后部分)。从上到下:
pubkey
:用于签名的公钥(即“在线质押密钥”)withdrawal_credentials
:将用于提取的公钥的哈希(私钥可以保存在冷存储中)。effective_balance
:验证者的余额,用于所有计算(当计算某些证明的总支持时,当计算奖励和惩罚时,等等...)slashed
:验证者是否被惩罚?activation_eligibility_epoch
:验证者何时有资格激活(这用于处理激活队列:验证者按他们获得资格的顺序激活)activation_epoch
:验证者何时被激活exit_epoch
:验证者何时退出(无论是自愿还是由于低余额或惩罚)withdrawable_epoch
:验证者何时有资格提取其余额精确的 epoch 保留在状态中,因为我们不仅需要能够计算当前的活跃验证者集,还需要计算历史的活跃验证者集,以便我们可以计算历史委员会,从而验证历史证明和惩罚。
此外,存储每个阶段转换的 epoch 简化了协议。替代方案是存储一个变量 current_state
和标志(例如,0 = 尚未有资格激活,1 = 有资格激活,2 = 活跃,3 = 在退出队列中,4 = 已退出,5 = 已提取)以及下一个转换的 epoch,但这会增加协议复杂性,因为例如目前 (3)、(4) 和 (5) 都由两行代码处理(initiate_validator_exit
的底部),如果它们是分开的,这将很困难。
AttestationData
class AttestationData(Container):
slot: Slot
index: CommitteeIndex
# LMD GHOST 投票
beacon_block_root: Root
# FFG 投票
source: Checkpoint
target: Checkpoint
出于效率原因,我们要求每个验证者在每个 epoch 中只签署一个证明。然而,这个证明有三个目的:(i) Casper FFG 投票,(ii) 通过投票当前头来稳定短期逐块分叉选择,以及 (iii) 分片区块投票(在第 1 阶段添加)。在每个 epoch 中,每个验证者被分配到一个时隙内的单个委员会,他们证明(即签署)这个结合了 (i)(source
和 target
)和 (ii)(beacon_block_root
)的数据结构。
IndexedAttestation
class IndexedAttestation(Container):
attesting_indices: List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE]
data: AttestationData
signature: BLSSignature
一个 AttestationData
,一个签名,以及参与者的索引列表。这是在 AttesterSlashing
对象中包含证明以惩罚验证者不当行为时的格式。想法是从外部链导入的证明可能与当前链的委员会不同,因此我们需要明确提供参与验证者的列表,以便可以验证证明,并在需要时惩罚参与者。
PendingAttestation
class PendingAttestation(Container):
aggregation_bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE]
data: AttestationData
inclusion_delay: Slot
proposer_index: ValidatorIndex
出于效率原因,我们不会立即处理包含在区块中的证明的全部效果;这将需要编辑 Merkle 树中 1/32 的所有验证者的确切余额,这几乎与简单地重新哈希整个向量一样昂贵。此外,它还需要存储一个额外的数据结构来记录“谁已经证明”以防止重复计算。相反,我们只是将所有收到的证明存储在状态中,减去它们的签名(因为它们不再必要),加上关于谁包含它们以及延迟多少的信息(以计算奖励)。这些待处理的证明然后在 epoch 结束时处理。
Eth1Data
class Eth1Data(Container):
deposit_root: Root
deposit_count: uint64
block_hash: Bytes32
每个 eth2 区块包含对 eth1 区块的投票。此投票包含 eth1 区块的哈希,并且为了更方便地验证存款,它还包含存款树的根和已经进行的存款数量。从技术上讲,存款树根和大小将从 eth1 区块哈希进行 Merkle 证明,但这将涉及验证六进制 RLP Patricia 树 Merkle 分支,这是不必要的复杂。
HistoricalBatch
class HistoricalBatch(Container):
block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
参见上面 SLOTS_PER_HISTORICAL_ROOT
部分的讨论。
[旁注:关于存款过程的说明]
eth1 上的人存款成为 eth2 上的验证者的过程如下:
pubkey
)和一个离线提取密钥(其哈希称为 withdrawal_credentials
)。deposit
函数向存款合约发送 32 ETH,并在该调用中提供 pubkey
、withdrawal_credentials
和 signature
作为参数,签名使用公钥对两个密钥(以及存款金额,在特殊情况下可能不是 32 ETH)进行签名。DepositData
记录)添加到存款树中DepositData
记录所属的存款树的根哈希。DepositMessage
class DepositMessage(Container):
pubkey: BLSPubkey
withdrawal_credentials: Bytes32
amount: Gwei
这是 DepositData
中 signature
签名的数据。签名是必要的,并且必须对所有这三个字段进行签名,原因有两个:
DepositData
class DepositData(Container):
pubkey: BLSPubkey
withdrawal_credentials: Bytes32
amount: Gwei
signature: BLSSignature # 对 DepositMessage 签名
此结构进入 eth1 侧存款合约保存的存款树。请注意,此数据在 eth1 侧不进行验证(因为我们尚未在 eth1 上拥有 BLS-12-381);无效的签名可能会进入存款树,eth2 链有责任忽略它们。
BeaconBlockHeader
class BeaconBlockHeader(Container):
slot: Slot
proposer_index: ValidatorIndex
parent_root: Root
state_root: Root
body_root: Root
信标链区块的区块头:包含时隙、提议者索引、状态根、父区块的根哈希以及信标链区块中其他所有内容的根哈希。与其他区块链(包括比特币和 eth1)中的区块头基本相似。
<a id="domain_separation" />
[旁注:域分离]
eth2 中的域分离是为了防止一种情况,即一种类型和上下文中的对象的签名意外地成为另一种类型或不同上下文中的对象的有效签名。这可能是因为相同的数据恰好作为多种数据类型有效;这种情况可能会被攻击者触发,导致惩罚或其他问题。域分离明确地使这不可能。
eth2 中有两种主要的域分离类型:
我们通过在签名消息时混合域哈希来实现域分离;也就是说,当我们签名某个 object
时,我们实际上是在签名 hash(root_hash(object), domain_hash)
。域哈希本身混合了 domain_type
和 fork_version
(代表链),参见 get_domain
了解其工作原理的逻辑。
domain_type
是一个 4 字节的值;参见域类型列表。fork_version
(可以将其视为类似于链 ID,除了它在每次硬分叉时更改以促进在故意的 ETH/ETC 类分叉期间的重放保护)是基于上面分叉部分中描述的逻辑计算的。
SigningData
class SigningData(Container):
object_root: Root
domain: Domain
Eth2 大量使用签名容器:结构中有一些内部容器 C1
,以及一个外部容器 C2(message: C1, signature: BLSSignature)
。为了实现域分离(参见上面的部分),签名不是直接对签名的消息的根哈希进行签名,而是对包含该消息和域(通过 get_domain
计算)的结构的根哈希进行签名。
SigningData
是一个用于计算 hash(root_hash(object), domain_hash)
的虚拟结构;一般来说,规范在美学上决定广泛避免显式的内联位连接(hash(x + y)
),而是使用结构,SSZ Merkle 哈希在内部执行这些位连接。
ProposerSlashing
class ProposerSlashing(Container):
signed_header_1: SignedBeaconBlockHeader
signed_header_2: SignedBeaconBlockHeader
提议者可以因为在同一时隙签署两个不同的头而被惩罚。此对象可以包含在链上以执行该惩罚。
AttesterSlashing
class AttesterSlashing(Container):
attestation_1: IndexedAttestation
attestation_2: IndexedAttestation
证明者可以因为签署两个一起违反 Casper FFG 惩罚条件的证明而被惩罚。此对象可以包含在链上以执行该惩罚。
Attestation
class Attestation(Container):
aggregation_bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE]
data: AttestationData
signature: BLSSignature
一个记录,指定某个委员会的一部分(使用位字段标识哪一部分)签署了某个 AttestationData
。Eth2 使用 BLS 签名聚合以提高效率,因此不是每个验证者的证明都单独包含,证明首先广播到网络中的聚合层,然后区块提议者可以将所有签署完全相同 AttestationData
的证明(在正常情况下这是大多数)合并为包含在链上的单个 Attestation
。
Deposit
class Deposit(Container):
proof: Vector[Bytes32, DEPOSIT_CONTRACT_TREE_DEPTH + 1] # 到存款根的 Merkle 路径
data: DepositData
证明验证者已存款。这些按索引顺序处理;每个证明是一个 Merkle 分支,证明存款在 eth1 存款合约创建的存款树中的正确位置。
VoluntaryExit
class VoluntaryExit(Container):
epoch: Epoch # 可以处理自愿退出的最早 epoch
validator_index: ValidatorIndex
当验证者希望自愿退出时,他们可以创建、签署并广播此类型的消息。
BeaconBlockBody
class BeaconBlockBody(Container):
randao_reveal: BLSSignature
eth1_data: Eth1Data # Eth1 数据投票
graffiti: Bytes32 # 任意数据
# 操作
proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS]
attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS]
attestations: List[Attestation, MAX_ATTESTATIONS]
deposits: List[Deposit, MAX_DEPOSITS]
voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS]
信标区块的“主要”部分。这里最重要的是证明,因为这些需要被包括,以便链可以跟踪其自身的最终性状态并应用奖励和惩罚,但这还包括惩罚、存款、自愿退出、调整区块随机种子的揭示值、eth1 投票和一个开放的“涂鸦”字段。
BeaconBlock
class BeaconBlock(Container):
slot: Slot
proposer_index: ValidatorIndex
parent_root: Root
state_root: Root
body: BeaconBlockBody
一个完整的信标区块;基本上是一个信标区块头,但用完整的 body 替换了 body 根。
class BeaconState(Container):
genesis_time: uint64
genesis_validators_root: Root
slot: Slot
fork: Fork
# 历史记录
latest_block_header: BeaconBlockHeader
block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT]
# Eth1
eth1_data: Eth1Data
eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH]
eth1_deposit_index: uint64
# 注册表
validators: List[Validator, VALIDATOR_REGISTRY_LIMIT]
balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT]
# 随机性
randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR]
# 罚没
slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] # 每个 epoch 的罚没有效余额总和
# 证明
previous_epoch_attestations: List[PendingAttestation, MAX_ATTESTATIONS * SLOTS_PER_EPOCH]
current_epoch_attestations: List[PendingAttestation, MAX_ATTESTATIONS * SLOTS_PER_EPOCH]
# 最终性
justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] # 每个最近被证明的 epoch 的位集
previous_justified_checkpoint: Checkpoint # 上一个 epoch 的快照
current_justified_checkpoint: Checkpoint
finalized_checkpoint: Checkpoint
这是这里最重要的数据结构;它是本规范中定义的状态转换函数所修改的对象。它包含了处理下一个信标区块所需的所有信息,可以分为以下几类:
* 杂项和版本控制(slot、fork 版本、genesis 时间...)
* 需要访问的历史记录:历史区块哈希、状态根、随机性种子...
* 维护 eth1 投票系统所需的数据
* 验证者注册表(加上单独的余额数组)
* 每个 epoch 的罚没总和(用于跟踪有多少验证者被罚没,以便计算比例惩罚)
* 待处理的证明
* 与 Casper FFG 相关的数据
#### 签名信封
这些只是上述许多容器的签名版本:
##### `SignedVoluntaryExit`
```python
class SignedVoluntaryExit(Container):
message: VoluntaryExit
signature: BLSSignature
SignedBeaconBlock
class SignedBeaconBlock(Container):
message: BeaconBlock
signature: BLSSignature
SignedBeaconBlockHeader
class SignedBeaconBlockHeader(Container):
message: BeaconBlockHeader
signature: BLSSignature
这第一组函数由相对简单的“辅助”函数组成,然后在规范的其余部分中使用。
注意:以下定义是为了规范目的,不一定是优化的实现。
integer_squareroot
def integer_squareroot(n: uint64) -> uint64:
"""
返回最大的整数 ``x``,使得 ``x**2 <= n``。
"""
x = n
y = (x + 1) // 2
while y < x:
x = y
y = (x + n // x) // 2
return x
一个平方根函数,使用Babylon方法以提高效率。保证提供精确的整数结果:最大的整数 x
使得 x**2 <= n
(例如,sqrt(14) = 3, sqrt(15) = 3, sqrt(16) = 4, sqrt(17) = 4)。实际实现可以根据需要使用其他算法;只有输出中的这种精确数值属性是强制性的。
xor
def xor(bytes_1: Bytes32, bytes_2: Bytes32) -> Bytes32:
"""
返回两个 32 字节字符串的异或。
"""
return Bytes32(a ^ b for a, b in zip(bytes_1, bytes_2))
对输入进行逐位 XOR 操作。
uint_to_bytes
def uint_to_bytes(n: uint) -> bytes
是一个将 uint
类型对象序列化为 ENDIANNESS
字节序的字节的函数。输出的预期长度是 uint
类型的字节长度。
bytes_to_uint64
def bytes_to_uint64(data: bytes) -> uint64:
"""
返回将 ``data`` 解释为 ``ENDIANNESS`` 字节序的整数反序列化结果。
"""
return uint64(int.from_bytes(data, ENDIANNESS))
将 8 个字节转换为 64 位整数。
hash
def hash(data: bytes) -> Bytes32
是 SHA256。
hash_tree_root
def hash_tree_root(object: SSZSerializable) -> Root
是一个通过利用哈希树结构将对象哈希为单个根的函数,如 SSZ 规范 中所定义。
Eth2 使用 BLS 签名,如 IETF 草案 BLS 规范 draft-irtf-cfrg-bls-signature-02 中所指定,但使用 Hashing to Elliptic Curves - draft-irtf-cfrg-hash-to-curve-07 而不是 draft-irtf-cfrg-hash-to-curve-06。具体来说,eth2 使用 BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_
密码套件,它实现了以下接口:
def Sign(SK: int, message: Bytes) -> BLSSignature
def Verify(PK: BLSPubkey, message: Bytes, signature: BLSSignature) -> bool
def Aggregate(signatures: Sequence[BLSSignature]) -> BLSSignature
def FastAggregateVerify(PKs: Sequence[BLSPubkey], message: Bytes, signature: BLSSignature) -> bool
def AggregateVerify(PKs: Sequence[BLSPubkey], messages: Sequence[Bytes], signature: BLSSignature) -> bool
在这些规范中,BLS 签名被视为一个模块以保持符号清晰,因此要验证签名,使用 bls.Verify(...)
。
注意:BLS 和哈希到曲线规范的非标准配置是暂时的,一旦 IETF 发布 BLS 规范草案 3,将得到解决。
BLS 被使用因为它的聚合友好性:许多 BLS 签名可以聚合为一个签名,如果签名是同一消息的,这种聚合非常快速,并且聚合签名的验证成本极低(每个参与者一次椭圆曲线加法(!!),加上一次配对验证签名,无论有多少参与者)。这是允许 eth2 支持非常多的验证者的关键魔法。
<a id="lifecycle" />
[旁注:关于验证者生命周期的说明]
(这个故事从上一个关于存款过程的旁注结束时开始)
当处理验证者存款时,验证者记录被添加到验证者注册表(state.validators
)中(或者,如果它是一个已经在验证者集中的公钥的存款,则被视为对该现有验证者余额的充值)。如果在存款或充值后,验证者的余额 >= 32 ETH,验证者将被置于有资格激活的阶段。
处于有资格激活阶段的验证者会自动进入激活队列(队列实际上并不作为共识数据结构存在;相反,共识规则只是说按照验证者成为有资格激活的顺序激活验证者)。请参阅上面的关于流失的讨论了解为什么存在队列以及每个 epoch 可以激活多少验证者。
请注意,当验证者到达队列的前端时,他们的激活时间被设置为未来的 4 个 epoch;这是为了确保委员会可以提前预测,因为委员会的计算取决于活跃的验证者集。
当验证者处于活跃状态时,他们被分配了完整的验证者职责。这些职责包括:
验证者保持活跃状态,直到他们 (i) 签署了包含在链上的 VoluntaryExit
消息,(ii) 低于最低余额 16 ETH,或 (iii) 被罚没。
请注意,在所有三种情况下,退出步骤都是通过 initiate_validator_exit
函数完成的,该函数将验证者放入退出队列。因此,即使是被罚没的验证者也可以暂时保持活跃状态。这很尴尬,但这样做有三个原因:
请注意,目前,被罚没确实会立即将验证者的余额减少 1/32,这会影响 2/3 最终性计算中的分母,但这种影响非常小。
退出队列以与激活队列相同的速率处理,并且退出也有类似的 4 个 epoch 延迟。验证者退出后,如果他们退出时没有被罚没,则可以在 MIN_VALIDATOR_WITHDRAWABILITY_DELAY
(约 1 天)后提款,如果因被罚没而退出,则可以在 EPOCHS_PER_SLASHINGS_VECTOR
(4 eek)后提款。
被罚没的验证者将受到三种惩罚:
1/MIN_SLASHING_PENALTY_QUOTIENT
的余额)process_slashings
函数)第三点是为了防止自罚没成为逃避不活跃泄漏的一种方式。
一旦验证者提款,在阶段 0 中,他们从协议的角度来看实际上是无效的。在后续阶段中,将添加显式的“提款”功能,这将把验证者的余额转移到 eth2 上适当分片中的适当账户。
有了这个背景(另请参阅 Validator 结构定义),希望接下来的四个函数是相对自解释的:
is_active_validator
def is_active_validator(validator: Validator, epoch: Epoch) -> bool:
"""
检查 ``validator`` 是否处于活跃状态。
"""
return validator.activation_epoch <= epoch < validator.exit_epoch
is_eligible_for_activation_queue
def is_eligible_for_activation_queue(validator: Validator) -> bool:
"""
检查 ``validator`` 是否有资格被放入激活队列。
"""
return (
validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH
and validator.effective_balance == MAX_EFFECTIVE_BALANCE
)
is_eligible_for_activation
def is_eligible_for_activation(state: BeaconState, validator: Validator) -> bool:
"""
检查 ``validator`` 是否有资格激活。
"""
return (
# 队列中的位置已最终确定
validator.activation_eligibility_epoch <= state.finalized_checkpoint.epoch
# 尚未被激活
and validator.activation_epoch == FAR_FUTURE_EPOCH
)
请注意,激活队列只处理在信标链知道的最后一个最终确定区块之前注册的激活;有关此内容的更多信息,请参阅关于注册表更新的部分。
is_slashable_validator
def is_slashable_validator(validator: Validator, epoch: Epoch) -> bool:
"""
检查 ``validator`` 是否可被罚没。
"""
return (not validator.slashed) and (validator.activation_epoch <= epoch < validator.withdrawable_epoch)
is_slashable_attestation_data
def is_slashable_attestation_data(data_1: AttestationData, data_2: AttestationData) -> bool:
"""
检查 ``data_1`` 和 ``data_2`` 是否根据 Casper FFG 规则可被罚没。
"""
return (
# 双重投票
(data_1 != data_2 and data_1.target.epoch == data_2.target.epoch) or
# 包围投票
(data_1.source.epoch < data_2.source.epoch and data_2.target.epoch < data_1.target.epoch)
)
此函数确定两个 AttestationData
对象是否相互冲突,因此根据 Casper FFG 规则被视为自相矛盾(即双重投票)。如果是,则任何签署了这两者的验证者都可以被罚没。
is_valid_indexed_attestation
def is_valid_indexed_attestation(state: BeaconState, indexed_attestation: IndexedAttestation) -> bool:
"""
检查 ``indexed_attestation`` 是否不为空,具有排序且唯一的索引,并且具有有效的聚合签名。
"""
# 验证索引是否已排序且唯一
indices = indexed_attestation.attesting_indices
if len(indices) == 0 or not indices == sorted(set(indices)):
return False
# 验证聚合签名
pubkeys = [state.validators[i].pubkey for i in indices]
domain = get_domain(state, DOMAIN_BEACON_ATTESTER, indexed_attestation.data.target.epoch)
signing_root = compute_signing_root(indexed_attestation.data, domain)
return bls.FastAggregateVerify(pubkeys, signing_root, indexed_attestation.signature)
验证证明的有效性(主要是提取签名者的公钥,然后验证签名)。此函数适用于索引证明,但请注意,常规证明验证在将位字段转换为验证者索引列表后也会通过此函数。
is_valid_merkle_branch
def is_valid_merkle_branch(leaf: Bytes32, branch: Sequence[Bytes32], depth: uint64, index: uint64, root: Root) -> bool:
"""
检查 ``leaf`` 在 ``index`` 处是否验证了 Merkle ``root`` 和 ``branch``。
"""
value = leaf
for i in range(depth):
if index // (2**i) % 2:
value = hash(branch[i] + value)
else:
value = hash(value + branch[i])
return value == root
一个通用的 Merkle 分支验证器。
compute_shuffled_index
def compute_shuffled_index(index: uint64, index_count: uint64, seed: Bytes32) -> uint64:
"""
返回与 ``seed``(和 ``index_count``)对应的洗牌索引。
"""
assert index < index_count
# 交换或不交换 (https://link.springer.com/content/pdf/10.1007%2F978-3-642-32009-5_1.pdf)
# 参见第 3 页的“广义域”算法
for current_round in range(SHUFFLE_ROUND_COUNT):
pivot = bytes_to_uint64(hash(seed + uint_to_bytes(uint8(current_round)))[0:8]) % index_count
flip = (pivot + index_count - index) % index_count
position = max(index, flip)
source = hash(
seed
+ uint_to_bytes(uint8(current_round))
+ uint_to_bytes(uint32(position // 256))
)
byte = uint8(source[(position % 256) // 8])
bit = (byte >> (position % 8)) % 2
index = flip if bit else index
return index
Eth2 需要某种形式的“随机抽样”来将验证者分配到委员会;如果每个验证者可以选择他们所在的委员会,一小部分恶意验证者可以针对一个特定的分片进行攻击,并为该分片做出虚假证明。我们可以将其建模为一种洗牌算法,取一个长度为 N 的数组(填充了该 epoch 中的活跃验证者索引)并伪随机地洗牌(例如,[0, 1, 2, 3, 5, 6] -> [3, 2, 0, 5, 6, 1]
);然后委员会可以只是输出数组中所需长度的连续切片。
这种洗牌算法有几个期望的特性:
i
,很容易计算 shuffle(i)
。这是必要的,以便单个验证者可以有效地确定他们的职责。shuffle(i)
,很容易确定 i
。这是必要的,以便轻客户端可以有效地确定任何单个委员会中的验证者。请注意,其中一些相同的期望也适用于提议者选择:
我们使用来自 https://link.springer.com/content/pdf/10.1007%2F978-3-642-32009-5_1.pdf 的“交换或不交换”算法,它有效地满足了上述所有属性。
交换或不交换算法通过执行 90 轮以下过程来工作:
p
x
,可能将位置 x
的值与 p-x
的值交换(如果需要,环绕列表)。这个“可能”是通过使用哈希函数伪随机生成 N
位(N
是正在洗牌的列表的大小)并检查 max(x, p-x)
'th 位是否等于 1 来确定的(这种技巧是为了确保你得到 x
和 p-x
的相同答案)它可以高效地向前或向后运行(你不必在每轮生成所有 N
位,只需生成包含 max(x, p-x)
的块),并且开销相当低。
compute_proposer_index
def compute_proposer_index(state: BeaconState, indices: Sequence[ValidatorIndex], seed: Bytes32) -> ValidatorIndex:
"""
从 ``indices`` 中返回一个按有效余额随机采样的索引。
"""
assert len(indices) > 0
MAX_RANDOM_BYTE = 2**8 - 1
i = uint64(0)
total = uint64(len(indices))
while True:
candidate_index = indices[compute_shuffled_index(i % total, total, seed)]
random_byte = hash(seed + uint_to_bytes(uint64(i // 32)))[i % 32]
effective_balance = state.validators[candidate_index].effective_balance
if effective_balance * MAX_RANDOM_BYTE >= MAX_EFFECTIVE_BALANCE * random_byte:
return candidate_index
i += 1
计算提议者索引。这个函数有些复杂;其思想是它选择一个提议者,以 BALANCE/32
的概率接受他们,如果失败则继续尝试。这样做是为了使被选为提议者的概率与余额成比例。
compute_committee
def compute_committee(indices: Sequence[ValidatorIndex],
seed: Bytes32,
index: uint64,
count: uint64) -> Sequence[ValidatorIndex]:
"""
返回与 ``indices``、``seed``、``index`` 和委员会 ``count`` 对应的委员会。
"""
start = (len(indices) * index) // count
end = (len(indices) * (index + 1)) // count
return [indices[compute_shuffled_index(uint64(i), uint64(len(indices)), seed)] for i in range(start, end)]
取验证者索引列表的一个切片(假设为活跃验证者索引列表),并返回洗牌后的 index
'th 切片(总共 count
个切片)。
compute_epoch_at_slot
def compute_epoch_at_slot(slot: Slot) -> Epoch:
"""
返回 ``slot`` 处的 epoch 编号。
"""
return Epoch(slot // SLOTS_PER_EPOCH)
compute_start_slot_at_epoch
def compute_start_slot_at_epoch(epoch: Epoch) -> Slot:
"""
返回 ``epoch`` 的起始 slot。
"""
return Slot(epoch * SLOTS_PER_EPOCH)
compute_activation_exit_epoch
def compute_activation_exit_epoch(epoch: Epoch) -> Epoch:
"""
返回在 ``epoch`` 中启动的验证者激活和退出生效的 epoch。
"""
return Epoch(epoch + 1 + MAX_SEED_LOOKAHEAD)
此函数以 epoch 作为输入(在实践中总是当前 epoch),并输出在该 epoch 中计划激活的验证者将被激活的 epoch。4 个 epoch 的延迟用于保持委员会的可预测性。
compute_fork_data_root
def compute_fork_data_root(current_version: Version, genesis_validators_root: Root) -> Root:
"""
返回 ``current_version`` 和 ``genesis_validators_root`` 的 32 字节 fork 数据根。
这主要用于签名域中,以避免跨 fork/链的冲突。
"""
return hash_tree_root(ForkData(
current_version=current_version,
genesis_validators_root=genesis_validators_root,
))
创世验证者集的根哈希与 fork 版本混合,以增加进一步的域分离,允许具有不同创世的链自动具有不同的版本。这使得拥有许多具有重放保护的测试网变得更加容易。
compute_fork_digest
def compute_fork_digest(current_version: Version, genesis_validators_root: Root) -> ForkDigest:
"""
返回 ``current_version`` 和 ``genesis_validators_root`` 的 4 字节 fork 摘要。
这是一个主要用于 p2p 层上的域分离的摘要。
4 字节足以实现 fork/链的实际分离。
"""
return ForkDigest(compute_fork_data_root(current_version, genesis_validators_root)[:4])
fork 摘要的前四个字节用于在 p2p 层上将不同链的验证者分离到不同的网络中。
compute_domain
def compute_domain(domain_type: DomainType, fork_version: Version=None, genesis_validators_root: Root=None) -> Domain:
"""
返回 ``domain_type`` 和 ``fork_version`` 的域。
"""
if fork_version is None:
fork_version = GENESIS_FORK_VERSION
if genesis_validators_root is None:
genesis_validators_root = Root() # 默认情况下所有字节为零
fork_data_root = compute_fork_data_root(fork_version, genesis_validators_root)
return Domain(domain_type + fork_data_root[:28])
一个由 get_domain
使用的辅助函数。将域类型和 fork 版本(参见关于 fork 的部分)组合成一个 Domain
对象。
另请参阅关于域分离的部分。
compute_signing_root
def compute_signing_root(ssz_object: SSZObject, domain: Domain) -> Root:
"""
返回相应签名数据的签名根。
"""
return hash_tree_root(SigningData(
object_root=hash_tree_root(ssz_object),
domain=domain,
))
计算当 SSZ 容器被签名时正在签名的哈希。这是通过创建一个将原始容器和域放在一起的临时 SSZ 容器,并输出其根来完成的。
这组函数访问信标链状态。
get_current_epoch
def get_current_epoch(state: BeaconState) -> Epoch:
"""
返回当前 epoch。
"""
return compute_epoch_at_slot(state.slot)
get_previous_epoch
def get_previous_epoch(state: BeaconState) -> Epoch:
"""`
返回上一个 epoch(除非当前 epoch 是 ``GENESIS_EPOCH``)。
"""
current_epoch = get_current_epoch(state)
return GENESIS_EPOCH if current_epoch == GENESIS_EPOCH else Epoch(current_epoch - 1)
get_block_root
def get_block_root(state: BeaconState, epoch: Epoch) -> Root:
"""
返回最近 ``epoch`` 开始时的区块根。
"""
return get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch))
get_block_root_at_slot
def get_block_root_at_slot(state: BeaconState, slot: Slot) -> Root:
"""
返回最近 ``slot`` 处的区块根。
"""
assert slot < state.slot <= slot + SLOTS_PER_HISTORICAL_ROOT
return state.block_roots[slot % SLOTS_PER_HISTORICAL_ROOT]
get_randao_mix
def get_randao_mix(state: BeaconState, epoch: Epoch) -> Bytes32:
"""
返回最近 ``epoch`` 处的 randao 混合值。
"""
return state.randao_mixes[epoch % EPOCHS_PER_HISTORICAL_VECTOR]
在状态中,我们存储了一个历史 randao 混合值(即伪随机性种子)的数组。这是必要的,因为出于许多原因,我们希望能够计算历史委员会。有时我们关心非常近的历史(例如,epoch N 的证明可以包含在 epoch N+1 中,因此 epoch N+1 的 epoch 结束处理需要知道 epoch N 中使用的随机性种子是什么,以便它可以计算该 epoch 的委员会),但有时我们希望回顾很久以前,例如,我们希望能够计算几个月前的委员会以验证罚没。拥有 32 eek 的历史随机性种子存储有助于我们做到这一点。
get_active_validator_indices
def get_active_validator_indices(state: BeaconState, epoch: Epoch) -> Sequence[ValidatorIndex]:
"""
返回 ``epoch`` 处的活跃验证者索引序列。
"""
return [ValidatorIndex(i) for i, v in enumerate(state.validators) if is_active_validator(v, epoch)]
返回在给定 epoch 中活跃的所有验证者索引的子集(请注意,此方法还可以获取任何更早 epoch 的历史活跃验证者索引集,因为状态存储了所有验证者的激活和退出 epoch)
get_validator_churn_limit
def get_validator_churn_limit(state: BeaconState) -> uint64:
"""
返回当前 epoch 的验证者流失限制。
"""
active_validator_indices = get_active_validator_indices(state, get_current_epoch(state))
return max(MIN_PER_EPOCH_CHURN_LIMIT, uint64(len(active_validator_indices)) // CHURN_LIMIT_QUOTIENT)
参见关于流失的部分。
get_seed
def get_seed(state: BeaconState, epoch: Epoch, domain_type: DomainType) -> Bytes32:
"""
返回 ``epoch`` 处的种子。
"""
mix = get_randao_mix(state, Epoch(epoch + EPOCHS_PER_HISTORICAL_VECTOR - MIN_SEED_LOOKAHEAD - 1)) # 避免下溢
return hash(domain_type + uint_to_bytes(epoch) + mix)
返回给定 epoch 的随机性种子。请注意这里的精确连接方式:在给定 epoch 中相关的种子是生成于 5 个 epoch 前的种子。为简单起见,你可以将其视为 get_randao_mix(state, Epoch(epoch - MIN_SEED_LOOKAHEAD - 1))
。
这里的技术细节是,历史随机性种子存储在一个循环覆盖自身的数组中,例如,如果 EPOCHS_PER_HISTORICAL_VECTOR
等于 10,并且当前 epoch 是 53,那么 state.randao_mixes
将包含来自 epoch [50, 51, 52, 53, 44, 45, 46, 47, 48, 49]
的 10 个种子。因此,如果你在 epoch 53 期间调用 get_seed
,它将返回数组中位置 (53 - 1 - 4) % 10 = 8
处的值,环绕回到末尾。
添加 + EPOCHS_PER_HISTORICAL_VECTOR
是为了确保在 epoch < 5
的异常情况下,“环绕回到末尾”的行为仍然有效,但规范将避免在计算中间出现任何负数(这是规范中达成一致的目标;能够用 uint64
表示几乎所有整数的简单性超过了像这里这样的小复杂性增加)。
另请参阅:关于随机性种子的部分。
get_committee_count_per_slot
def get_committee_count_per_slot(state: BeaconState, epoch: Epoch) -> uint64:
"""
返回给定 ``epoch`` 中每个 slot 的委员会数量。
"""
return max(uint64(1), min(
MAX_COMMITTEES_PER_SLOT,
uint64(len(get_active_validator_indices(state, epoch))) // SLOTS_PER_EPOCH // TARGET_COMMITTEE_SIZE,
))
返回每个 slot 中的委员会数量(在阶段 1+ 中,每个 slot 中交叉链接的分片数量)。如果有足够的验证者来填充分片(128 个验证者)的完整委员会(*64)用于 epoch 中的每个 slot(*32),即 >= 262,144 个验证者或 8,388,608 ETH,那么我们将获得每个 slot 的完整 64 个委员会,并且每个 slot 都会交叉链接每个分片。
如果验证者数量少于这个数量,那么我们会减少每个 slot 的委员会数量,以确保每个委员会保持安全大小,但代价是不在每个 slot 中交叉链接每个分片(而是轮换:例如,如果每个 slot 只有 25 个委员会,那么 slot 1 将处理分片 0...24,slot 2 将处理 25...49,slot 3 将处理 50...63 并环绕以处理 0...10,等等)。
如果没有足够的验证者来组成一个完整的委员会(即少于 128 * 32 = 4,096 个验证者,或 124,288 ETH),那么委员会大小开始下降,尽管在这种情况下,低委员会大小相对于存在许多可以单方面发起 51% 攻击的参与者这一更大的问题来说,可能是一个小问题。
get_beacon_committee
def get_beacon_committee(state: BeaconState, slot: Slot, index: CommitteeIndex) -> Sequence[ValidatorIndex]:
"""
返回 ``slot`` 处 ``index`` 的信标委员会。
"""
epoch = compute_epoch_at_slot(slot)
committees_per_slot = get_committee_count_per_slot(state, epoch)
return compute_committee(
indices=get_active_validator_indices(state, epoch),
seed=get_seed(state, epoch, DOMAIN_BEACON_ATTESTER),
index=(slot % SLOTS_PER_EPOCH) * committees_per_slot + index,
count=committees_per_slot * SLOTS_PER_EPOCH,
)
获取给定 slot 的第 i 个委员会。
get_beacon_proposer_index
def get_beacon_proposer_index(state: BeaconState) -> ValidatorIndex:
"""
返回当前 slot 的信标提议者索引。
"""
epoch = get_current_epoch(state)
seed = hash(get_seed(state, epoch, DOMAIN_BEACON_PROPOSER) + uint_to_bytes(state.slot))
indices = get_active_validator_indices(state, epoch)
return compute_proposer_index(state, indices, seed)
获取当前区块提议者。请注意,compute_proposer_index
与此代码分开维护,因为在阶段 1 中,我们计划添加也使用该函数的分片提议者选择代码。
get_total_balance
def get_total_balance(state: BeaconState, indices: Set[ValidatorIndex]) -> Gwei:
"""
返回 ``indices`` 的组合有效余额。
``EFFECTIVE_BALANCE_INCREMENT`` Gwei 最小值以避免除以零。
数学上安全到约 10B ETH,之后会溢出 uint64。
"""
return Gwei(max(EFFECTIVE_BALANCE_INCREMENT, sum([state.validators[index].effective_balance for index in indices])))
获取给定验证者索引集的总余额(这是一个辅助函数;我们使用它来获取总活跃余额和批准某些 FFG 投票或分片区块的总余额)。
get_total_active_balance
def get_total_active_balance(state: BeaconState) -> Gwei:
"""
返回活跃验证者的组合有效余额。
注意:``get_total_balance`` 返回 ``EFFECTIVE_BALANCE_INCREMENT`` Gwei 最小值以避免除以零。
"""
return get_total_balance(state, set(get_active_validator_indices(state, get_current_epoch(state))))
get_domain
def get_domain(state: BeaconState, domain_type: DomainType, epoch: Epoch=None) -> Domain:
"""
返回消息的签名域(fork 版本与域类型连接)。
"""
epoch = get_current_epoch(state) if epoch is None else epoch
fork_version = state.fork.previous_version if epoch < state.fork.epoch else state.fork.current_version
return compute_domain(domain_type, fork_version, state.genesis_validators_root)
返回特定 DomainType
的域哈希(与正在签名的消息混合的数据)。这用于实现域分离;有关更多信息,请参阅关于域分离的部分。
get_indexed_attestation
def get_indexed_attestation(state: BeaconState, attestation: Attestation) -> IndexedAttestation:
"""
返回与 ``attestation`` 对应的索引证明。
"""
attesting_indices = get_attesting_indices(state, attestation.data, attestation.aggregation_bits)
return IndexedAttestation(
attesting_indices=sorted(attesting_indices),
data=attestation.data,
signature=attestation.signature,
)
将常规范式的证明(其中签名者集由确定委员会成员参与的位字段定义)转换为直接包含参与者验证者索引的证明(即用于罚没的类型)。
我们有逻辑将一种类型的证明转换为另一种类型,以便验证常规证明和验证罚没中的证明的方法可以共享大部分相同的代码。
def get_attesting_indices(state: BeaconState,
data: AttestationData,
bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE]) -> Set[ValidatorIndex]:
"""
返回与 data
和 bits
对应的见证索引集合。
"""
committee = get_beacon_committee(state, data.slot, data.index)
return set(index for i, index in enumerate(committee) if bits[i])
计算需要签署具有特定 `AttestationData` 的见证的委员会,并使用该委员会和见证中的位字段来确定参与见证的验证者索引的原始列表。
#### Beacon state 修改器
这些方法(不再是纯函数)修改了信标链状态。
##### `increase_balance`
```python
def increase_balance(state: BeaconState, index: ValidatorIndex, delta: Gwei) -> None:
"""
将索引为 ``index`` 的验证者余额增加 ``delta``。
"""
state.balances[index] += delta
decrease_balance
def decrease_balance(state: BeaconState, index: ValidatorIndex, delta: Gwei) -> None:
"""
将索引为 ``index`` 的验证者余额减少 ``delta``,并防止下溢。
"""
state.balances[index] = 0 if delta > state.balances[index] else state.balances[index] - delta
initiate_validator_exit
def initiate_validator_exit(state: BeaconState, index: ValidatorIndex) -> None:
"""
启动索引为 ``index`` 的验证者的退出。
"""
# 如果验证者已经启动退出,则返回
validator = state.validators[index]
if validator.exit_epoch != FAR_FUTURE_EPOCH:
return
# 计算退出队列的 epoch
exit_epochs = [v.exit_epoch for v in state.validators if v.exit_epoch != FAR_FUTURE_EPOCH]
exit_queue_epoch = max(exit_epochs + [compute_activation_exit_epoch(get_current_epoch(state))])
exit_queue_churn = len([v for v in state.validators if v.exit_epoch == exit_queue_epoch])
if exit_queue_churn >= get_validator_churn_limit(state):
exit_queue_epoch += Epoch(1)
# 设置验证者的退出 epoch 和可提取 epoch
validator.exit_epoch = exit_queue_epoch
validator.withdrawable_epoch = Epoch(validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY)
此函数启动验证者退出的过程,并由以下情况调用:(i) VoluntaryExit
处理,(ii) 强制执行“如果余额低于 16 ETH,则强制退出”规则的代码,以及 (iii) 惩罚。
这里的代码强制执行 (i) “至少 4 个 epoch 延迟规则”和 (ii) 退出队列(在太多验证者同时尝试退出的情况下)。实现如下。从当前 epoch + 5 开始(当前 epoch 已经部分结束,因此需要 +5 以确保延迟 >=4 个 epoch)。查看是否有太多验证者在该 epoch 退出;如果没有,则在该 epoch 退出,否则尝试下一个 epoch。这会在拥塞情况下创建一个实际的先进先出退出队列。
slash_validator
def slash_validator(state: BeaconState,
slashed_index: ValidatorIndex,
whistleblower_index: ValidatorIndex=None) -> None:
"""
惩罚索引为 ``slashed_index`` 的验证者。
"""
epoch = get_current_epoch(state)
initiate_validator_exit(state, slashed_index)
validator = state.validators[slashed_index]
validator.slashed = True
validator.withdrawable_epoch = max(validator.withdrawable_epoch, Epoch(epoch + EPOCHS_PER_SLASHINGS_VECTOR))
state.slashings[epoch % EPOCHS_PER_SLASHINGS_VECTOR] += validator.effective_balance
decrease_balance(state, slashed_index, validator.effective_balance // MIN_SLASHING_PENALTY_QUOTIENT)
# 应用提议者和举报者奖励
proposer_index = get_beacon_proposer_index(state)
if whistleblower_index is None:
whistleblower_index = proposer_index
whistleblower_reward = Gwei(validator.effective_balance // WHISTLEBLOWER_REWARD_QUOTIENT)
proposer_reward = Gwei(whistleblower_reward // PROPOSER_REWARD_QUOTIENT)
increase_balance(state, proposer_index, proposer_reward)
increase_balance(state, whistleblower_index, Gwei(whistleblower_reward - proposer_reward))
惩罚验证者(即强制退出并惩罚验证者,如果他们做了可证明的非法行为,例如在同一 epoch 中签署两条冲突的消息)。惩罚执行以下操作:
slashed
标志设置为 truestate.slashings
数组中指定位置的值(这是一个循环重写的数组,其中第 i 个 epoch 位置 i % EPOCHS_PER_SLASHINGS_VECTOR
被重写)。该数组用于跟踪被惩罚的验证者总数,用于计算总惩罚(通常称为“反相关惩罚”)这里定义的主要函数 initialize_beacon_state_from_eth1
接受一个 eth1 区块哈希和时间戳以及一系列存款,并生成一个 eth2 创世状态。所有客户端在链首次启动时都会运行此函数以计算创世状态。
在 Ethereum 2.0 创世触发之前,对于每个 Ethereum 1.0 区块,让 candidate_state = initialize_beacon_state_from_eth1(eth1_block_hash, eth1_timestamp, deposits)
,其中:
eth1_block_hash
是 Ethereum 1.0 区块的哈希eth1_timestamp
是与 eth1_block_hash
对应的 Unix 时间戳deposits
是按时间顺序排列的所有存款序列,直到(并包括)哈希为 eth1_block_hash
的区块Eth1 区块只有在至少 SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE
秒后才会被考虑(即 eth1_timestamp + SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE <= current_unix_time
)。由于此约束,如果 GENESIS_DELAY < SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE
,则 genesis_time
可能发生在时间/状态首次已知之前。应配置值以避免这种情况。
def initialize_beacon_state_from_eth1(eth1_block_hash: Bytes32,
eth1_timestamp: uint64,
deposits: Sequence[Deposit]) -> BeaconState:
fork = Fork(
previous_version=GENESIS_FORK_VERSION,
current_version=GENESIS_FORK_VERSION,
epoch=GENESIS_EPOCH,
)
state = BeaconState(
genesis_time=eth1_timestamp + GENESIS_DELAY,
fork=fork,
eth1_data=Eth1Data(block_hash=eth1_block_hash, deposit_count=len(deposits)),
latest_block_header=BeaconBlockHeader(body_root=hash_tree_root(BeaconBlockBody())),
randao_mixes=[eth1_block_hash] * EPOCHS_PER_HISTORICAL_VECTOR, # 使用 Eth1 熵种子 RANDAO
)
# 处理存款
leaves = list(map(lambda deposit: deposit.data, deposits))
for index, deposit in enumerate(deposits):
deposit_data_list = List[DepositData, 2**DEPOSIT_CONTRACT_TREE_DEPTH](*leaves[:index + 1])
state.eth1_data.deposit_root = hash_tree_root(deposit_data_list)
process_deposit(state, deposit)
# 处理激活
for index, validator in enumerate(state.validators):
balance = state.balances[index]
validator.effective_balance = min(balance - balance % EFFECTIVE_BALANCE_INCREMENT, MAX_EFFECTIVE_BALANCE)
if validator.effective_balance == MAX_EFFECTIVE_BALANCE:
validator.activation_eligibility_epoch = GENESIS_EPOCH
validator.activation_epoch = GENESIS_EPOCH
# 设置创世验证者根以进行域分离和链版本控制
state.genesis_validators_root = hash_tree_root(state.validators)
return state
注意:满足最低创世活跃验证者数量标准的 eth1_timestamp
的 ETH1 区块也可能在 MIN_GENESIS_TIME
之前发生。
当 is_valid_genesis_state(candidate_state)
第一次为 True
时,让 genesis_state = candidate_state
。
def is_valid_genesis_state(state: BeaconState) -> bool:
if state.genesis_time < MIN_GENESIS_TIME:
return False
if len(get_active_validator_indices(state, GENESIS_EPOCH)) < MIN_GENESIS_ACTIVE_VALIDATOR_COUNT:
return False
return True
注意:is_valid_genesis_state
函数(包括 MIN_GENESIS_TIME
和 MIN_GENESIS_ACTIVE_VALIDATOR_COUNT
)是用于测试的占位符。它尚未由社区最终确定,可以根据需要进行更新。
这里的想法是,你可以将客户端视为反复尝试使用上述算法创建创世状态,但仅在状态满足上述函数时才接受该状态。实际上,客户端不会以这种方式工作,因为效率太低(最好只是跟踪有效的 eth1 存款和 eth1 的时间戳,并在两者都达到目标时激活)。
让 genesis_block = BeaconBlock(state_root=hash_tree_root(genesis_state))
。
在这里,我们终于定义了规范中的主要函数,该函数定义了在处理区块时如何修改状态。该函数还能够声明区块无效(通常通过 assert
完成,尽管任何导致代码抛出异常的情况,例如访问超出范围的列表,以及 uint64 溢出或下溢,都算作区块无效)。
我们从一个高级定义开始,将其分为两部分:(i) 每个 slot 的状态转换(process_slots
),无论是否有区块,都会在每个 slot 中发生,以及 (ii) 以区块为输入的每个区块的状态转换。例如,如果一个区块的 slot 为 66,而其父区块的 slot 为 62,则 process_slot
函数将在两者之间的所有四个 slot 中被调用(process_slot
会依次调用 epoch 边界处理函数 process_epoch
,因为 slot 64 是 epoch 边界,介于 epoch 1 [slots 32...63] 和 epoch 2 [slots 64...95] 之间)。
与预状态 state
和签名区块 signed_block
对应的后状态定义为 state_transition(state, signed_block)
。触发未处理异常的状态转换(例如失败的 assert
或访问超出范围的列表)被视为无效。导致 uint64
溢出或下溢的状态转换也被视为无效。
def state_transition(state: BeaconState, signed_block: SignedBeaconBlock, validate_result: bool=True) -> BeaconState:
block = signed_block.message
# 处理自区块以来的所有 slot(包括没有区块的 slot)
process_slots(state, block.slot)
# 验证签名
if validate_result:
assert verify_block_signature(state, signed_block)
# 处理区块
process_block(state, block)
# 验证状态根
if validate_result:
assert block.state_root == hash_tree_root(state)
# 返回后状态
return state
def verify_block_signature(state: BeaconState, signed_block: SignedBeaconBlock) -> bool:
proposer = state.validators[signed_block.message.proposer_index]
signing_root = compute_signing_root(signed_block.message, get_domain(state, DOMAIN_BEACON_PROPOSER))
return bls.Verify(proposer.pubkey, signing_root, signed_block.signature)
def process_slots(state: BeaconState, slot: Slot) -> None:
assert state.slot < slot
while state.slot < slot:
process_slot(state)
# 在下一个 epoch 的起始 slot 处理 epoch
if (state.slot + 1) % SLOTS_PER_EPOCH == 0:
process_epoch(state)
state.slot = Slot(state.slot + 1)
处理父区块的 slot 和输入 slot(当前 slot)之间的所有 slot,如果 slot 跨越了 epoch 边界,则应用 process_epoch
函数。
<a id="process_slot_notes" />
def process_slot(state: BeaconState) -> None:
# 缓存状态根
previous_state_root = hash_tree_root(state)
state.state_roots[state.slot % SLOTS_PER_HISTORICAL_ROOT] = previous_state_root
# 缓存最新区块头的状态根
if state.latest_block_header.state_root == Bytes32():
state.latest_block_header.state_root = previous_state_root
# 缓存区块根
previous_block_root = hash_tree_root(state.latest_block_header)
state.block_roots[state.slot % SLOTS_PER_HISTORICAL_ROOT] = previous_block_root
process_slot
函数的主要功能是更新历史 block_roots
和 state_roots
数组。状态根操作是解决一个具有挑战性问题的巧妙技巧。即,我们希望将 slot n
的区块根包含到 slot n
的历史中。最自然的时间是在处理区块时。但这给区块创建者带来了一个问题:区块的后状态根只能在状态转换完全处理后生成,但在 slot n
期间将区块根包含到历史中会要求在状态转换期间知道区块的后状态根!
我们通过以下策略解决了这个问题。在处理 slot n
的区块时(在 process_block
中),我们添加区块头但将状态根置零。然后,在 slot N+1 的 process_slot
函数开始时(此时状态在 slot n
处理后尚未被修改),我们编辑保存的区块头并填写后状态根。
请注意,这需要一个额外的数据结构 state.latest_block_header
,尽管我们只真正关心存储历史根;这里的复杂性增加被认为是值得的,以保持状态转换函数本身为干净的 state_transition(state, block) -> new_state
(而不是要求前一个区块作为显式参数)。
def process_epoch(state: BeaconState) -> None:
process_justification_and_finalization(state)
process_rewards_and_penalties(state)
process_registry_updates(state)
process_slashings(state)
process_final_updates(state)
在 epoch 边界(即一个 epoch 的最后一个 slot 结束后),我们执行一系列过程,主要是处理在当前和上一个 epoch 中保存的 PendingAttestations
,尽管还有一些其他工作也会完成。
首先,我们定义一些辅助函数:
def get_matching_source_attestations(state: BeaconState, epoch: Epoch) -> Sequence[PendingAttestation]:
assert epoch in (get_previous_epoch(state), get_current_epoch(state))
return state.current_epoch_attestations if epoch == get_current_epoch(state) else state.previous_epoch_attestations
在处理见证时,我们只接受具有正确 Casper FFG 源检查点的见证(具体来说,是链已知的最新证明检查点)。此函数的目的是获取所有具有正确 Casper FFG 源的见证。因此,它可以安全地返回所需 epoch(当前或上一个)的所有 PendingAttestation
。
def get_matching_target_attestations(state: BeaconState, epoch: Epoch) -> Sequence[PendingAttestation]:
return [
a for a in get_matching_source_attestations(state, epoch)
if a.data.target.root == get_block_root(state, epoch)
]
返回具有正确 Casper FFG 目标(即当前链的一部分的检查点)的 PendingAttestation
子集。
def get_matching_head_attestations(state: BeaconState, epoch: Epoch) -> Sequence[PendingAttestation]:
return [
a for a in get_matching_target_attestations(state, epoch)
if a.data.beacon_block_root == get_block_root_at_slot(state, a.data.slot)
]
返回具有正确头部(即他们投票的头部最终成为链的头部)的 PendingAttestation
子集。
def get_unslashed_attesting_indices(state: BeaconState,
attestations: Sequence[PendingAttestation]) -> Set[ValidatorIndex]:
output = set() # type: Set[ValidatorIndex]
for a in attestations:
output = output.union(get_attesting_indices(state, a.data, a.aggregation_bits))
return set(filter(lambda index: not state.validators[index].slashed, output))
从一组见证中获取见证索引列表,过滤掉已被惩罚的索引。这里的想法是,如果你被惩罚,你仍然“技术上”是验证者集合的一部分(参见验证者生命周期的说明了解原因),但你的见证不会被计算。
def get_attesting_balance(state: BeaconState, attestations: Sequence[PendingAttestation]) -> Gwei:
"""
返回参与 ``attestations`` 的未惩罚验证者的组合有效余额。
注意:``get_total_balance`` 返回 ``EFFECTIVE_BALANCE_INCREMENT`` Gwei 最小值以避免除以零。
"""
return get_total_balance(state, get_unslashed_attesting_indices(state, attestations))
从见证列表中获取总见证余额(不包括被惩罚的验证者)。
在下面的函数中,我们会看到一个模式。eth2 关注见证的四个主要属性,既用于内部记录,也用于奖励/惩罚计算:
对于每一个属性,我们将使用上面定义的辅助函数之一来确定在其见证中具有该属性的所有验证者索引。然后,我们将使用此信息来 (i) 奖励或惩罚他们,以及 (ii) 将他们的余额计入总数。总数有时本身用于计算奖励/惩罚,但也用于确定是否满足 Casper FFG 或分片委员会的 2/3 阈值。
我们关心当前和上一个 epoch 中包含的证明,因为有可能上一个 epoch 的某个 slot 的见证被包含在当前 epoch 中,因此我们需要将边界两侧的见证结合起来。如果不这样做,epoch 末尾的几个恶意提议者很容易阻止链检测证明和最终性。
def process_justification_and_finalization(state: BeaconState) -> None:
if get_current_epoch(state) <= GENESIS_EPOCH + 1:
return
previous_epoch = get_previous_epoch(state)
current_epoch = get_current_epoch(state)
old_previous_justified_checkpoint = state.previous_justified_checkpoint
old_current_justified_checkpoint = state.current_justified_checkpoint
# 处理证明
state.previous_justified_checkpoint = state.current_justified_checkpoint
state.justification_bits[1:] = state.justification_bits[:JUSTIFICATION_BITS_LENGTH - 1]
state.justification_bits[0] = 0b0
matching_target_attestations = get_matching_target_attestations(state, previous_epoch) # 上一个 epoch
if get_attesting_balance(state, matching_target_attestations) * 3 >= get_total_active_balance(state) * 2:
state.current_justified_checkpoint = Checkpoint(epoch=previous_epoch,
root=get_block_root(state, previous_epoch))
state.justification_bits[1] = 0b1
matching_target_attestations = get_matching_target_attestations(state, current_epoch) # 当前 epoch
if get_attesting_balance(state, matching_target_attestations) * 3 >= get_total_active_balance(state) * 2:
state.current_justified_checkpoint = Checkpoint(epoch=current_epoch,
root=get_block_root(state, current_epoch))
state.justification_bits[0] = 0b1
# 处理最终性
bits = state.justification_bits
# 第 2/3/4 个最近的 epoch 被证明,第 2 个使用第 4 个作为源
if all(bits[1:4]) and old_previous_justified_checkpoint.epoch + 3 == current_epoch:
state.finalized_checkpoint = old_previous_justified_checkpoint
# 第 2/3 个最近的 epoch 被证明,第 2 个使用第 3 个作为源
if all(bits[1:3]) and old_previous_justified_checkpoint.epoch + 2 == current_epoch:
state.finalized_checkpoint = old_previous_justified_checkpoint
# 第 1/2/3 个最近的 epoch 被证明,第 1 个使用第 3 个作为源
if all(bits[0:3]) and old_current_justified_checkpoint.epoch + 2 == current_epoch:
state.finalized_checkpoint = old_current_justified_checkpoint
# 第 1/2 个最近的 epoch 被证明,第 1 个使用第 2 个作为源
if all(bits[0:2]) and old_current_justified_checkpoint.epoch + 1 == current_epoch:
state.finalized_checkpoint = old_current_justified_checkpoint
此函数处理信标链自身对其历史中哪些证明和最终性区块的记录。大致上,此函数的前半部分检查当前 epoch 开始时的检查点是否已被证明,意味着 2/3 的活跃验证者投票支持它(记住:这是我们当前处于的 epoch 的末尾),并且还对上一个 epoch 进行相同的检查。这些数据保存在 justification_bits
数组中,该数组跟踪哪些最近的 epoch 已被证明。
代码的后半部分使用此证明历史,以及当前或上一个 epoch 中使用的源 epoch,来确定区块是否已最终化(参见 Gasper 论文 了解其工作原理)。
def get_base_reward(state: BeaconState, index: ValidatorIndex) -> Gwei:
total_balance = get_total_active_balance(state)
effective_balance = state.validators[index].effective_balance
return Gwei(effective_balance * BASE_REWARD_FACTOR // integer_squareroot(total_balance) // BASE_REWARDS_PER_EPOCH)
这是几乎所有其他以太坊奖励的计算基础。特别是,请注意,规范的一个期望目标是 effective_balance * BASE_REWARD_FACTOR // integer_squareroot(total_balance)
是验证者在理论最佳条件下获得的每 epoch 平均奖励;为了实现这一点,基础奖励等于该金额除以 BASE_REWARDS_PER_EPOCH
,这是该大小的奖励将被应用的次数。
def get_proposer_reward(state: BeaconState, attesting_index: ValidatorIndex) -> Gwei:
return Gwei(get_base_reward(state, attesting_index) // PROPOSER_REWARD_QUOTIENT)
提议者每包含一个见证者,最多可以获得 1/8 的基础奖励(尽管他们还会因包含惩罚和其他类型的对象而获得其他奖励,在阶段 1+ 中也是如此)。
def get_finality_delay(state: BeaconState) -> uint64:
return get_previous_epoch(state) - state.finalized_checkpoint.epoch
获取自链上次最终化以来的区块数。
def is_in_inactivity_leak(state: BeaconState) -> bool:
return get_finality_delay(state) > MIN_EPOCHS_TO_INACTIVITY_PENALTY
如果链在 >4 个 epoch 内未被最终化,则链进入“不活跃泄漏”模式,其中不活跃的验证者会逐渐受到越来越多的惩罚,以减少他们的影响,直到区块再次被最终化。参见此处了解不活跃泄漏是什么、它的用途以及它的工作原理。
def get_eligible_validator_indices(state: BeaconState) -> Sequence[ValidatorIndex]:
previous_epoch = get_previous_epoch(state)
return [
ValidatorIndex(index) for index, v in enumerate(state.validators)
if is_active_validator(v, previous_epoch) or (v.slashed and previous_epoch + 1 < v.withdrawable_epoch)
]
活跃的验证者和被惩罚但尚未提取的验证者都有资格受到惩罚。这样做是为了防止自我惩罚成为逃避不活跃泄漏的一种方式。
def get_attestation_component_deltas(state: BeaconState,
attestations: Sequence[PendingAttestation]
) -> Tuple[Sequence[Gwei], Sequence[Gwei]]:
"""
辅助函数,包含用于获取源、目标和头部 delta 函数的共享逻辑
"""
rewards = [Gwei(0)] * len(state.validators)
penalties = [Gwei(0)] * len(state.validators)
total_balance = get_total_active_balance(state)
unslashed_attesting_indices = get_unslashed_attesting_indices(state, attestations)
attesting_balance = get_total_balance(state, unslashed_attesting_indices)
for index in get_eligible_validator_indices(state):
if index in unslashed_attesting_indices:
increment = EFFECTIVE_BALANCE_INCREMENT # 从余额总数中分解出来以避免 uint64 溢出
if is_in_inactivity_leak(state):
# 由于完整的基础奖励将被不活跃惩罚 delta 抵消,
# 最佳参与在这里获得完整的基础奖励补偿。
rewards[index] += get_base_reward(state, index)
else:
reward_numerator = get_base_reward(state, index) * (attesting_balance // increment)
rewards[index] += reward_numerator // (total_balance // increment)
else:
penalties[index] += get_base_reward(state, index)
return rewards, penalties
这是一个辅助函数,输出验证者的奖励和惩罚列表;它用于正确源、正确目标和正确头部的奖励。一般方法是:如果验证者在其见证中实现了某个属性的比例为 p
(例如 p=0.9
表示 90%),那么这些验证者将获得 base_reward * p
的奖励,而未实现该属性的验证者将受到 base_reward
的惩罚。
我们需要惩罚来确保验证只有在至少在线约 2/3 的时间时才是有利可图的(实际上数字比这稍微宽松一些,但不多)。我们不希望无法满足最低活跃度的验证者,因为这样的验证者会通过阻碍最终性(需要 2/3 在线)来造成更多伤害。
这条规则,即如果其他验证者表现不佳,你的奖励会减少,是为了阻止伤害其他验证者;参见我关于劝阻攻击的文章(以及 Barnabe 的总结)了解为什么这是一个好主意。
def get_source_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence[Gwei]]:
"""
返回每个验证者的源投票的见证者微奖励/惩罚。
"""
matching_source_attestations = get_matching_source_attestations(state, get_previous_epoch(state))
return get_attestation_component_deltas(state, matching_source_attestations)
def get_target_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence[Gwei]]:
"""
返回每个验证者的目标投票的见证者微奖励/惩罚。
"""
matching_target_attestations = get_matching_target_attestations(state, get_previous_epoch(state))
return get_attestation_component_deltas(state, matching_target_attestations)
def get_head_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence[Gwei]]:
"""
返回每个验证者的头部投票的见证者微奖励/惩罚。
"""
matching_head_attestations = get_matching_head_attestations(state, get_previous_epoch(state))
return get_attestation_component_deltas(state, matching_head_attestations)
上述三个函数仅使用 get_attestation_component_deltas
辅助函数来计算正确 FFG 源、正确 FFG 目标和正确头部的奖励和惩罚。
def get_inclusion_delay_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence[Gwei]]:
"""
返回每个验证者的提议者和包含延迟的微奖励/惩罚。
"""
rewards = [Gwei(0) for _ in range(len(state.validators))]
matching_source_attestations = get_matching_source_attestations(state, get_previous_epoch(state))
for index in get_unslashed_attesting_indices(state, matching_source_attestations):
attestation = min([
a for a in matching_source_attestations
if index in get_attesting_indices(state, a.data, a.aggregation_bits)
], key=lambda a: a.inclusion_delay)
rewards[attestation.proposer_index] += get_proposer_reward(state, index)
max_attester_reward = get_base_reward(state, index) - get_proposer_reward(state, index)
rewards[index] += Gwei(max_attester_reward // attestation.inclusion_delay)
# 没有与包含延迟相关的惩罚
penalties = [Gwei(0) for _ in range(len(state.validators))]
return rewards, penalties
此函数处理快速包含你的见证的奖励:如果它在下一个 slot 中被包含,则获得完整的基础奖励,如果在 k
个 slot 后被包含,则获得 1/k
的基础奖励。这激励了及时性,减少了等待超过一个 slot 以确保你有正确的目标或头部的动机。
def get_inactivity_penalty_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence[Gwei]]:
"""
返回每个验证者的不活跃奖励/惩罚 delta。
"""
penalties = [Gwei(0) for _ in range(len(state.validators))]
if is_in_inactivity_leak(state):
matching_target_attestations = get_matching_target_attestations(state, get_previous_epoch(state))
matching_target_attesting_indices = get_unslashed_attesting_indices(state, matching_target_attestations)
for index in get_eligible_validator_indices(state):
# 如果验证者表现最佳,这将取消所有奖励以保持中性余额
base_reward = get_base_reward(state, index)
penalties[index] += Gwei(BASE_REWARDS_PER_EPOCH * base_reward - get_proposer_reward(state, index))
if index not in matching_target_attesting_indices:
effective_balance = state.validators[index].effective_balance
penalties[index] += Gwei(effective_balance * get_finality_delay(state) // INACTIVITY_PENALTY_QUOTIENT)
# 没有与不活跃惩罚相关的奖励
rewards = [Gwei(0) for _ in range(len(state.validators))]
return rewards, penalties
此代码实现了不活跃泄漏。
get_attestation_deltas
def get_attestation_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence[Gwei]]:
"""
返回每个验证者的见证奖励/惩罚 delta。
"""
source_rewards, source_penalties = get_source_deltas(state)
target_rewards, target_penalties = get_target_deltas(state)
head_rewards, head_penalties = get_head_deltas(state)
inclusion_delay_rewards, _ = get_inclusion_delay_deltas(state)
_, inactivity_penalties = get_inactivity_penalty_deltas(state)
rewards = [
source_rewards[i] + target_rewards[i] + head_rewards[i] + inclusion_delay_rewards[i]
for i in range(len(state.validators))
]
penalties = [
source_penalties[i] + target_penalties[i] + head_penalties[i] + inactivity_penalties[i]
for i in range(len(state.validators))
]
return rewards, penalties
此函数将所有上述来源的奖励和惩罚合并为总奖励和惩罚。
process_rewards_and_penalties
def process_rewards_and_penalties(state: BeaconState) -> None:
if get_current_epoch(state) == GENESIS_EPOCH:
return
rewards, penalties = get_attestation_deltas(state)
for index in range(len(state.validators)):
increase_balance(state, ValidatorIndex(index), rewards[index])
decrease_balance(state, ValidatorIndex(index), penalties[index])
此函数结合了上述所有逻辑并处理这些奖励和惩罚。
def process_registry_updates(state: BeaconState) -> None:
# 处理激活资格和退出
for index, validator in enumerate(state.validators):
if is_eligible_for_activation_queue(validator):
validator.activation_eligibility_epoch = get_current_epoch(state) + 1
if is_active_validator(validator, get_current_epoch(state)) and validator.effective_balance <= EJECTION_BALANCE:
initiate_validator_exit(state, ValidatorIndex(index))
# 排队等待激活的验证者且尚未出队进行激活
activation_queue = sorted([
index for index, validator in enumerate(state.validators)
if is_eligible_for_activation(state, validator)
# 按 activation_eligibility_epoch 设置顺序和索引排序
], key=lambda index: (state.validators[index].activation_eligibility_epoch, index))
# 出队验证者进行激活,直到达到 churn 限制
for index in activation_queue[:get_validator_churn_limit(state)]:
validator = state.validators[index]
validator.activation_epoch = compute_activation_exit_epoch(get_current_epoch(state))
此函数处理 (i) 验证者激活队列,以及 (ii) 余额 <= 16 ETH 的验证者被强制退出的规则。请注意,验证者激活队列的实现比退出队列更复杂,后者只是立即分配退出 epoch。
我们无法在这里这样做的原因是,我们只想处理已经在最终化的区块中启动的激活。这样做是为了确保,除非在极端情况下两个冲突的区块被最终化,任何在一条链上活跃的验证者也必须在另一条链上至少被分配一个索引(并且在两侧的索引相同)。这样做是为了确保一条链生成的 indexed_attestations
可以在另一条链上处理以进行惩罚。如果一条链可以包含另一条链完全未知的验证者,惩罚处理将中断,因为另一条链将不知道这些验证者的公钥(并且包含公钥会更占用空间;每个验证者 48 字节而不是 3 字节)。
请注意,如果两个冲突的区块确实被最终化,第一次发生这种情况必须是由共享一个共同最后最终化区块的见证完成的,因此在那时可以对双重最终化进行惩罚。
<a id="anti-correlation" />
[旁注:Eth2 中的反相关惩罚]
在 eth2 中,反相关惩罚是这样一种惩罚结构,即如果你与其他许多验证者在同一时间犯同样的错误,你会受到更多的惩罚。我们可以通过一个例子来理解 (3) 的工作原理。假设有两个质押池(或云服务,或客户端),一个占总质押量的 10%,另一个占 20%。假设两者的可靠性相同;也就是说,它们在任何一个时间段内失败的概率相同。然而,由于反相关性惩罚,第二个池将遭受两倍的惩罚,因为 20% 的验证者失败而不是 10% 本身会使每个验证者的惩罚翻倍。因此,对于新用户来说,加入第一个池的风险更小。
在 eth2 中,主要有两种类型的反相关性惩罚:
3s/D
的押金,其中 s
是在你被惩罚前 2 周到你被惩罚后 2 周期间被惩罚的其他验证者的总 ETH,D
是总押金。例如,如果有 1000 万 ETH 在质押,你被惩罚,并且在你被惩罚前后 2 周内有价值 30 万 ETH 的验证者被惩罚,你将损失 9% 的押金(这是在固定的 1/32 最低惩罚之外的)。被惩罚的验证者会面临 4 周的不活跃惩罚,这也可以说是一种反相关性惩罚,尽管它惩罚的是不同类型的不当行为之间的相关性,这与前两种惩罚有所不同且用处较小;该规则的主要任务是防止自我惩罚成为逃避不活跃泄漏的可行方式。
def process_slashings(state: BeaconState) -> None:
epoch = get_current_epoch(state)
total_balance = get_total_active_balance(state)
for index, validator in enumerate(state.validators):
if validator.slashed and epoch + EPOCHS_PER_SLASHINGS_VECTOR // 2 == validator.withdrawable_epoch:
increment = EFFECTIVE_BALANCE_INCREMENT # 从惩罚分子中提取以避免 uint64 溢出
penalty_numerator = validator.effective_balance // increment * min(sum(state.slashings) * 3, total_balance)
penalty = penalty_numerator // total_balance * increment
decrease_balance(state, ValidatorIndex(index), penalty)
这是处理上述比例性惩罚规则的代码。state.slashings
是一个数组,其中第 i 个元素包含在最近 (i % EPOCHS_PER_SLASHINGS_VECTOR)
个 epoch 中被惩罚的验证者的总 ETH 余额,其中 EPOCHS_PER_SLASHINGS_VECTOR
是 4 周内的 epoch 数。例如,如果当前 epoch 是 53,并且 EPOCHS_PER_SLASHINGS_VECTOR
等于 10,则其元素将分别存储在 epoch [50, 51, 52, 53, 44, 45, 46, 47, 48, 49]
中被惩罚的总 ETH 余额。如果我们简单地取这个数组的和,我们得到的是过去 EPOCHS_PER_SLASHINGS_VECTOR
个 epoch 中的总惩罚,无论数组中的哪个位置当前正在更新。
请注意,我们在 4 周期间的中途为被惩罚的验证者计算惩罚,这既是惩罚验证者的强制提款延迟,也是惩罚向量的长度。这意味着如果你被惩罚,你的惩罚是基于在你被惩罚前后 2 周期间被惩罚的验证者部分计算的。这样做是因为其他替代方案(使用 [被惩罚前 4 周..... 被惩罚时] 或 [被惩罚时... 被惩罚后 4 周] 的时间跨度)会遇到一个问题,即即使在同一时间有很多验证者被惩罚,第一个或最后一个被惩罚的验证者也会受到非常小的惩罚。
def process_final_updates(state: BeaconState) -> None:
current_epoch = get_current_epoch(state)
next_epoch = Epoch(current_epoch + 1)
# 重置 eth1 数据投票
if next_epoch % EPOCHS_PER_ETH1_VOTING_PERIOD == 0:
state.eth1_data_votes = []
# 使用滞后更新有效余额
for index, validator in enumerate(state.validators):
balance = state.balances[index]
HYSTERESIS_INCREMENT = EFFECTIVE_BALANCE_INCREMENT // HYSTERESIS_QUOTIENT
DOWNWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_DOWNWARD_MULTIPLIER
UPWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_UPWARD_MULTIPLIER
if (
balance + DOWNWARD_THRESHOLD < validator.effective_balance
or validator.effective_balance + UPWARD_THRESHOLD < balance
):
validator.effective_balance = min(balance - balance % EFFECTIVE_BALANCE_INCREMENT, MAX_EFFECTIVE_BALANCE)
# 重置惩罚
state.slashings[next_epoch % EPOCHS_PER_SLASHINGS_VECTOR] = Gwei(0)
# 设置 randao 混合
state.randao_mixes[next_epoch % EPOCHS_PER_HISTORICAL_VECTOR] = get_randao_mix(state, current_epoch)
# 设置历史根累加器
if next_epoch % (SLOTS_PER_HISTORICAL_ROOT // SLOTS_PER_EPOCH) == 0:
historical_batch = HistoricalBatch(block_roots=state.block_roots, state_roots=state.state_roots)
state.historical_roots.append(hash_tree_root(historical_batch))
# 旋转当前/上一个 epoch 证明
state.previous_epoch_attestations = state.current_epoch_attestations
state.current_epoch_attestations = []
这个函数执行一些杂项操作,特别是:
PendingAttestations
列表转移到“上一个”epoch 的列表中接下来的部分最终涉及处理区块本身的程序。这部分出人意料地并不复杂;大部分复杂性要么在辅助函数中,要么在 epoch 结束时的处理中。
def process_block(state: BeaconState, block: BeaconBlock) -> None:
process_block_header(state, block)
process_randao(state, block.body)
process_eth1_data(state, block.body)
process_operations(state, block.body)
我们处理的主要有四个部分:
VoluntaryExit
等)def process_block_header(state: BeaconState, block: BeaconBlock) -> None:
# 验证 slot 是否匹配
assert block.slot == state.slot
# 验证区块比最新的区块头更新
assert block.slot > state.latest_block_header.slot
# 验证提议者索引是否正确
assert block.proposer_index == get_beacon_proposer_index(state)
# 验证父区块是否匹配
assert block.parent_root == hash_tree_root(state.latest_block_header)
# 将当前区块缓存为新的最新区块
state.latest_block_header = BeaconBlockHeader(
slot=block.slot,
proposer_index=block.proposer_index,
parent_root=block.parent_root,
state_root=Bytes32(), # 在下一个 process_slot 调用中被覆盖
body_root=hash_tree_root(block.body),
)
# 验证提议者未被惩罚
proposer = state.validators[block.proposer_index]
assert not proposer.slashed
这部分相当自解释;只是检查区块的一些基本正确性属性,并将区块头缓存到缓存中,但不包括其状态根(因为我们还不知道其状态根;详见process_slot
部分以更全面地了解发生了什么)。
def process_randao(state: BeaconState, body: BeaconBlockBody) -> None:
epoch = get_current_epoch(state)
# 验证 RANDAO 揭示
proposer = state.validators[get_beacon_proposer_index(state)]
signing_root = compute_signing_root(epoch, get_domain(state, DOMAIN_RANDAO))
assert bls.Verify(proposer.pubkey, signing_root, body.randao_reveal)
# 混合 RANDAO 揭示
mix = xor(get_randao_mix(state, epoch), hash(body.randao_reveal))
state.randao_mixes[epoch % EPOCHS_PER_HISTORICAL_VECTOR] = mix
详见种子部分以了解这里发生了什么。
def process_eth1_data(state: BeaconState, body: BeaconBlockBody) -> None:
state.eth1_data_votes.append(body.eth1_data)
if state.eth1_data_votes.count(body.eth1_data) * 2 > EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH:
state.eth1_data = body.eth1_data
存储每个有投票的 eth1 区块的投票计数;如果任何 eth1 区块在 1024 个 slot 的投票期内获得多数支持,则正式接受该 eth1 区块并将其设置为 eth2 状态中的官方“最新已知 eth1 区块”。
def process_operations(state: BeaconState, body: BeaconBlockBody) -> None:
# 验证未处理的存款是否已处理到最大存款数
assert len(body.deposits) == min(MAX_DEPOSITS, state.eth1_data.deposit_count - state.eth1_deposit_index)
def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None:
for operation in operations:
fn(state, operation)
for_ops(body.proposer_slashings, process_proposer_slashing)
for_ops(body.attester_slashings, process_attester_slashing)
for_ops(body.attestations, process_attestation)
for_ops(body.deposits, process_deposit)
for_ops(body.voluntary_exits, process_voluntary_exit)
基本上,对于区块中的每种操作类型,运行其关联的函数。此外,验证是否包含了最大可能的存款数。请注意,所有操作类型都有最大值,尽管这里不需要显式强制执行,因为它们已经包含在信标区块体 SSZ 数据类型中。
def process_proposer_slashing(state: BeaconState, proposer_slashing: ProposerSlashing) -> None:
header_1 = proposer_slashing.signed_header_1.message
header_2 = proposer_slashing.signed_header_2.message
# 验证区块头 slot 匹配
assert header_1.slot == header_2.slot
# 验证区块头提议者索引匹配
assert header_1.proposer_index == header_2.proposer_index
# 验证区块头不同
assert header_1 != header_2
# 验证提议者可被惩罚
proposer = state.validators[header_1.proposer_index]
assert is_slashable_validator(proposer, get_current_epoch(state))
# 验证签名
for signed_header in (proposer_slashing.signed_header_1, proposer_slashing.signed_header_2):
domain = get_domain(state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(signed_header.message.slot))
signing_root = compute_signing_root(signed_header.message, domain)
assert bls.Verify(proposer.pubkey, signing_root, signed_header.signature)
slash_validator(state, header_1.proposer_index)
惩罚在同一 slot 中提议了两个不同区块的验证者。
def process_attester_slashing(state: BeaconState, attester_slashing: AttesterSlashing) -> None:
attestation_1 = attester_slashing.attestation_1
attestation_2 = attester_slashing.attestation_2
assert is_slashable_attestation_data(attestation_1.data, attestation_2.data)
assert is_valid_indexed_attestation(state, attestation_1)
assert is_valid_indexed_attestation(state, attestation_2)
slashed_any = False
indices = set(attestation_1.attesting_indices).intersection(attestation_2.attesting_indices)
for index in sorted(indices):
if is_slashable_validator(state.validators[index], get_current_epoch(state)):
slash_validator(state, index)
slashed_any = True
assert slashed_any
给定两个证明(包含在 AttesterSlashing
中):
def process_attestation(state: BeaconState, attestation: Attestation) -> None:
data = attestation.data
assert data.target.epoch in (get_previous_epoch(state), get_current_epoch(state))
assert data.target.epoch == compute_epoch_at_slot(data.slot)
assert data.slot + MIN_ATTESTATION_INCLUSION_DELAY <= state.slot <= data.slot + SLOTS_PER_EPOCH
assert data.index < get_committee_count_per_slot(state, data.target.epoch)
committee = get_beacon_committee(state, data.slot, data.index)
assert len(attestation.aggregation_bits) == len(committee)
pending_attestation = PendingAttestation(
data=data,
aggregation_bits=attestation.aggregation_bits,
inclusion_delay=state.slot - data.slot,
proposer_index=get_beacon_proposer_index(state),
)
if data.target.epoch == get_current_epoch(state):
assert data.source == state.current_justified_checkpoint
state.current_epoch_attestations.append(pending_attestation)
else:
assert data.source == state.previous_justified_checkpoint
state.previous_epoch_attestations.append(pending_attestation)
# 验证签名
assert is_valid_indexed_attestation(state, get_indexed_attestation(state, attestation))
为了确保链最终确定,我们强制证明者 (i) 使用最新的已证明区块作为其源,并且 (ii) 使用正确的 epoch 作为其目标(尽管可能使用错误的区块,因为目标区块可能尚未作为链的一部分稳定下来)。我们进行一些基本的健全性检查(证明不是来自未来,并且证明委员会索引不超过该 slot 中的委员会数量)。然后我们验证证明,并将其保存为 PendingAttestation
,将所有证明的更详细处理留到 epoch 结束时。
def get_validator_from_deposit(state: BeaconState, deposit: Deposit) -> Validator:
amount = deposit.data.amount
effective_balance = min(amount - amount % EFFECTIVE_BALANCE_INCREMENT, MAX_EFFECTIVE_BALANCE)
return Validator(
pubkey=deposit.data.pubkey,
withdrawal_credentials=deposit.data.withdrawal_credentials,
activation_eligibility_epoch=FAR_FUTURE_EPOCH,
activation_epoch=FAR_FUTURE_EPOCH,
exit_epoch=FAR_FUTURE_EPOCH,
withdrawable_epoch=FAR_FUTURE_EPOCH,
effective_balance=effective_balance,
)
将 Deposit
记录(由 eth1 存款合约创建)转换为进入 eth2 状态的 Validator
对象。
def process_deposit(state: BeaconState, deposit: Deposit) -> None:
# 验证 Merkle 分支
assert is_valid_merkle_branch(
leaf=hash_tree_root(deposit.data),
branch=deposit.proof,
depth=DEPOSIT_CONTRACT_TREE_DEPTH + 1, # 添加 1 以包含 List 长度混合
index=state.eth1_deposit_index,
root=state.eth1_data.deposit_root,
)
# 存款必须按顺序处理
state.eth1_deposit_index += 1
pubkey = deposit.data.pubkey
amount = deposit.data.amount
validator_pubkeys = [v.pubkey for v in state.validators]
if pubkey not in validator_pubkeys:
# 验证存款签名(所有权证明),存款合约未检查
deposit_message = DepositMessage(
pubkey=deposit.data.pubkey,
withdrawal_credentials=deposit.data.withdrawal_credentials,
amount=deposit.data.amount,
)
domain = compute_domain(DOMAIN_DEPOSIT) # 分叉无关的域,因为存款在分叉间有效
signing_root = compute_signing_root(deposit_message, domain)
if not bls.Verify(pubkey, signing_root, deposit.data.signature):
return
##BeaconBlockBody
# 添加验证者和余额条目
state.validators.append(get_validator_from_deposit(state, deposit))
state.balances.append(amount)
else:
# 按存款金额增加余额
index = ValidatorIndex(validator_pubkeys.index(pubkey))
increase_balance(state, index, amount)
处理存款;这包括 (i) 验证 Merkle 分支,证明存款是 eth1 存款合约创建的存款树的一部分,(ii) 验证存款是按顺序处理的,(iii) 验证存款上的签名,最后 (iv) 将其添加到验证者集合中。如果存款公钥已经在验证者集合中,则存款被视为余额充值。
(注意:是的,余额充值确实有点绕过了激活队列,但请注意,攻击者要从中受益,他们需要已经丢失了正在充值的 ETH [因为存款需要 32 ETH,而 32 ETH 是最大有效余额],所以这不是一个攻击向量)
def process_voluntary_exit(state: BeaconState, signed_voluntary_exit: SignedVoluntaryExit) -> None:
voluntary_exit = signed_voluntary_exit.message
validator = state.validators[voluntary_exit.validator_index]
# 验证验证者处于活跃状态
assert is_active_validator(validator, get_current_epoch(state))
# 验证退出尚未启动
assert validator.exit_epoch == FAR_FUTURE_EPOCH
# 退出必须指定一个生效的 epoch;在此之前无效
assert get_current_epoch(state) >= voluntary_exit.epoch
# 验证验证者已活跃足够长时间
assert get_current_epoch(state) >= validator.activation_epoch + SHARD_COMMITTEE_PERIOD
# 验证签名
domain = get_domain(state, DOMAIN_VOLUNTARY_EXIT, voluntary_exit.epoch)
signing_root = compute_signing_root(voluntary_exit, domain)
assert bls.Verify(validator.pubkey, signing_root, signed_voluntary_exit.signature)
# 启动退出
initiate_validator_exit(state, voluntary_exit.validator_index)
验证者可以自愿签署一条消息,该消息可以包含在链上以退出验证者集合。请注意,验证者在退出前必须至少活跃约 1 天;这防止验证者反复存款和提款以尝试进入特定的分片委员会,以及一般污染存款/提款队列。
最小 epoch 规则(assert get_current_epoch(state) >= voluntary_exit.epoch
)的引入是为了确保构建隐藏攻击链的攻击者无法在历史早期重放退出,并利用这一点来帮助避免不活跃泄漏或更快地达到最终性。
- 原文链接: github.com/ethereum/anno...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!