以太坊2.0 Staking 硬核四问
作为广受瞩目的全球顶尖公链项目,以太坊2.0完全颠覆了从前的设计,旨在最大程度地同时实现去中心化和扩容目标。与以太坊1.0不同的是,以太坊2.0使用 PoS (权益证明)算法来推动区块链的运行,并通过「信标链+多分片链」 的架构来提高可扩展性。
以太坊2.0 的研发和部署计划历时已久。在所有客户端均顺利实现规范的最终版本 v0.12.1后,6月底将启动一个实现最终版本规范的多客户端测试网,7月则可启动最后的公共测试网。此后,最终版本的公共多客户端测试网若能稳定运行两至三个月,则可开始准备以太坊2.0的主网启动工作。若一切顺利,阶段0将于11 月上线。但若版本规范仍有待修复,且所有客户端需再次实现新规范,则上线时间可能推迟到2021年。
本文将为读者展示 HashQuark 研究团队从以太坊2.0技术层面出发、对最受技术人员关心的四个硬核问题给出的详细回答。
如何成为验证人?
以太坊采用存款合约(deposit contract)作为以太坊1.0与以太坊2.0之间的桥梁,当用户向存款合约存入32 ETH后,便可以作为以太坊2.0的验证者参与工作,并获得以太坊2.0奖励。
以 prysm 官网教程为例,加入测试网来直观感受下如何成为验证人。
准备工作
prysm 的实现主要包含两部分: 信标链客户端和验证者客户端。前者负责信标链的状态管理,后者负责验证者的出块和⻅证。为方便这一流程,prysm 提供了简易脚本 prysm.sh 来下载安装:
mkdir prysm && cd prysm curl https://raw.githubusercontent.com/prysmaticlabs/prysm/master/prysm.sh --output prysm.sh && chmod +x prysm.sh
(向左滑动,查看完整代码)
上述命令会在当前目录新建 prysm 文件夹,并下载可执行文件 prysm.sh。
除此之外,我们还需要至少32 ETH 的以太坊账户和浏览器插件 metamask 以便发送交易。可以在测试网上申请一些测试币,如下图所示:
创建验证者密钥对
一个验证人需要创建两对密钥对,一对用作验证人出块和见证,另一对用于管理存入合约的资金。运行如下命令来创建密钥:
./prysm.sh validator accounts create
命令将默认在~/.eth2validators/ 目录下创建两个 keystore 格式的文件,如下图所示:
前者用于资金管理,后者用于出块等。
创建完密钥的命令会在终端输出 Deposit Data:
这是根据上面的密钥对生成的交易信息,我们将它复制到网页上的交易数据部分:
启动节点
采用如下命令在两个终端分别启动信标链节点和验证者节点:
./prysm.sh beacon-chain./prysm.sh validator
发送存款交易
通过网页调用 metamask 填充上面的 Deposit Data,方便用户直接发送存款交易。
点击图中按钮,metamask 会跳出确认对话框:
可以看出共发送了32 ETH 给存款合约,交易的 Data 部分正是我们复制的 Deposit Data。点击确认发送交易,发送成功后等待 4-5 小时即可成功激活验证人。
用代码解读成为验证人的过程
创建验证人的主要过程为创建密钥对、生成数据调用合约、合约执行、信标链处理。
创建密钥对
创建验证者密钥对时,通常需要两对密钥(采用BLS12-381曲线):验证者密钥对(Validator PubKey, Validator PrivateKey)和提取存款的密钥对(Withdrawal PubKey, Withdrawal PrivateKey)。
生成数据调用合约
调用合约除了通常的合约地址、金额参数外,还需要构造要调用的合约方法的参数:pubkey(验证者公钥)、withdrawal_credentials (提取存款权限信息),signature(签名)和deposit_data_root(防止篡改标识)。在用户生成两对密钥后,就可以生成上面这些参数来构造要发送的交易数据,参考下图:
可以看到:
pubkey 是 Validator PubKey,这也是创建的验证者的公钥标识
withdrawal_crendentials由一个固定前缀拼接 Withdrawal PubKey 的哈希(sha256)构成
amount 是本次合约发送的金额,至少为1 ETH
signature 是采用 Validator PrivateKey对pubkey、withdrawal_credentials、amount 的哈希(HashTreeRoot)结果的签名
deposit_data_root 是前面4个参数的哈希(HashTreeRoot)
在生成上述的参数后,会按照合约接口编码成一定的格式,然后发送给存款合约完成调用。
合约执行
合约的主要方法 deposit,定义了收到一笔存款交易时如何处理,存款交易正是通过调用这一方法来实现存款。
deposit 方法接受 pubkey(验证者公钥)、withdrawal_credentials(提取存款权限信息),signature(签名)和 deposit_data_root(防止篡改标识)作为参数。主要分成参数基础校验、触发存款事件、检验数据完整性、更新数据结构几个部分,如以下代码所示:
@public def deposit(pubkey: bytes[PUBKEY_LENGTH], withdrawal_credentials: bytes[WITHDRAWAL_CREDENTIALS_LENGTH], signature: bytes[SIGNATURE_LENGTH], deposit_data_root: bytes32): ############## 1. 参数基础校验 ################ # Avoid overflowing the Merkle tree (and prevent edge case in computing `self.branch`) assert self.deposit_count < MAX_DEPOSIT_COUNT # Check deposit amount deposit_amount: uint256 = msg.value / as_wei_value(1, "gwei") assert deposit_amount >= MIN_DEPOSIT_AMOUNT # Length checks for safety assert len(pubkey) == PUBKEY_LENGTH assert len(withdrawal_credentials) == WITHDRAWAL_CREDENTIALS_LENGTH assert len(signature) == SIGNATURE_LENGTH ######################################### # Emit `DepositEvent` log amount: bytes[8] = self.to_little_endian_64(deposit_amount) ############## 2. 触发存款事件 ################ log.DepositEvent(pubkey, withdrawal_credentials, amount, signature, self.to_little_endian_64(self.deposit_count)) ############## 3. 校验数据完整性 ################ # Compute deposit data root (`DepositData` hash tree root) zero_bytes32: bytes32 = 0x0000000000000000000000000000000000000000000000000000000000000000 pubkey_root: bytes32 = sha256(concat(pubkey, slice(zero_bytes32, start=0, len=64 - PUBKEY_LENGTH))) signature_root: bytes32 = sha256(concat( sha256(slice(signature, start=0, len=64)), sha256(concat(slice(signature, start=64, len=SIGNATURE_LENGTH - 64), zero_bytes32)), )) node: bytes32 = sha256(concat( sha256(concat(pubkey_root, withdrawal_credentials)), sha256(concat(amount, slice(zero_bytes32, start=0, len=32 - AMOUNT_LENGTH), signature_root)), )) # Verify computed and expected deposit data roots match assert node == deposit_data_root ########################################### ############## 4. 更新Merkle Tree ################ # Add deposit data root to Merkle tree (update a single `branch` node) self.deposit_count += 1 size: uint256 = self.deposit_count for height in range(DEPOSIT_CONTRACT_TREE_DEPTH): if bitwise_and(size, 1) == 1: # More gas efficient than `size % 2 == 1` self.branch[height] = node break node = sha256(concat(self.branch[height], node)) size /= 2
(向左滑动,查看完整代码)
对于上述第3部分数据完整性的校验与构造交易数据一致,校验的逻辑关系参考下图:
可以看到,由于合约接收的参数是固定的格式和语义的,所以直接将各部分 pack 成32 bytes,然后做 merkleize 求值,最后与接收到的 deposit_data_root 进行比较,如果相同则说明数据没有被篡改。
信标链的处理
当存款交易成功被以太坊1.0链上执行后,以太坊2.0的信标链接下来如何处理?
信标链会一直监听存款合约的 DepositEvent 事件,如果存在新的存款合约,那么会启动相应的处理程序。以下为规范上对 deposit 的处理:
def process_deposit(state: BeaconState, deposit: Deposit) -> None: ##################### 必要检验 ################### # Verify the Merkle branch assert is_valid_merkle_branch( leaf=hash_tree_root(deposit.data), branch=deposit.proof, depth=DEPOSIT_CONTRACT_TREE_DEPTH + 1, # Add 1 for the List length mix-in index=state.eth1_deposit_index, root=state.eth1_data.deposit_root, ) # Deposits must be processed in order 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: # Verify the deposit signature (proof of possession) which is not checked by the deposit contract deposit_message = DepositMessage( pubkey=deposit.data.pubkey, withdrawal_credentials=deposit.data.withdrawal_credentials, amount=deposit.data.amount, ) domain = compute_domain(DOMAIN_DEPOSIT) # Fork-agnostic domain since deposits are valid across forks signing_root = compute_signing_root(deposit_message, domain) if not bls.Verify(pubkey, signing_root, deposit.data.signature): return ################################################# ##################### 添加新验证人 ################### # Add validator and balance entries state.validators.append(get_validator_from_deposit(state, deposit)) state.balances.append(amount) ################################################# else: ################ 同一个验证人多次存入 ############### # Increase balance by deposit amount index = ValidatorIndex(validator_pubkeys.index(pubkey)) increase_balance(state, index, amount) #################################################
(向左滑动,查看完整代码)
可以看到,如果存入的已经是一个验证者,只需增加其余额便可。如果存入的是新的验证者,会在进行必要的校验后,在全局状态注册一个新的验证者。
新建验证者的设置如下:
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, )
(向左滑动,查看完整代码)
验证者生命周期相关参数均设置为 FAR_FUTURE_EPOCH。
之后再处理这些新注册的验证者:
def process_registry_updates(state: BeaconState) -> None: # Process activation eligibility and ejections 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)) # 限制每周期成为验证者数量 # Queue validators eligible for activation and not yet dequeued for activation 15. activation_queue = sorted([ index for index, validator in enumerate(state.validators) if is_eligible_for_activation(state, validator) # Order by the sequence of activation_eligibility_epoch setting and then index ], key=lambda index: (state.validators[index].activation_eligibility_epoch, index)) # Dequeued validators for activation up to churn limit 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))
(向左滑动,查看完整代码)
在处理新增加的验证者时,会按照一定比例设置一个等待队列,这会限制同一时间可以增加的新的验证者数量,也能够防止一瞬间涌入的大批新的验证者对于网络安全和协议的影响,保证一定的稳定和安全性。
知识点
密钥对管理方案
验证者密钥对可以是随机生成的两对 BLS 密钥对,但若要创建多个验证人,密钥对数量过多则会难于管理。一个可选的解决方案是通过一个种子密钥来衍生出一对对相关的密钥对,只记录这个种子密钥即可。也可以通过将种子密钥映射成助记词来方便记录和保存。以太坊提案 EIP2333 和 EIP2334 给出了具体的规范说明。密钥衍生示意图参考如下:
[m / 0] - [m / 0 / 0] / \ / [m / 0 / 1][m] - [m / 1] \ ... [m / i]
另一方面,对于单独的一对密钥对,相比直接存储私钥,存储成 keystore 格式加上密码验证加密更加安全,存储也更加方便,可以参考以太坊提案 EIP2335 给出了相关建议。
名词解释
哈希方法HashTreeRoot
HashTreeRoot 提供了将一个对象按一定格式构建默克尔树、并求得树的默克尔根值的方法。HashTreeRoot 可以将一个对象(bit、bytes、vector、list、containers 等等)的各个部分按序排列然后构建默克尔树,获得根值。具体规范参考:https://github.com/ethereum/eth2.0-specs/blob/dev/ssz/simple-serialize.md
具体来讲,在对 pubkey、withdrawal_credentials、amount、signature 求取 HashTreeRoot 时,会经历以下过程:
对pubkey、withdrawal_credentials、amount、signature 进行 HashTreeRoot,求得 deposit_data_root
对于整体的 HashTreeRoot,实际上是对各个部分分别求取 HashTreeRoot,最后一起 Merkleize
每个部分的 HashTreeRoot 实际上是先 Pack 再将结果 Merkleize
如何进行 Pack?
可以看到,Pack 会把值按照一定长度切割,如不够就用零字节补充,这样会得到一个个按序排列的32字节的数据块,也为接下来的 Merkleize 提供初始数据。
如何进行 Merkleize?
可以看到,Merkleize 是上一个步骤产生的数据块按照二叉树的方式从底部两两哈希求值再拼接,一层一层向上计算得到根值,所以 HashTreeRoot 本质上讲是数据按照一定长度排列,然后一层一层默克尔求值,直到获得最上层的根值。
探秘信标链(Beacon Chain)共识
信标链是整个以太坊2.0的核心,它是一条与当前以太坊 PoW 链并行的一条独立链。它负责存储和维护验证者注册表,处理分片链和信标链之间的交联(Crosslink),以及完成信标链共识。想要了解信标链的共识过程,就需要了解最重要的几个概念:间隙(Slot)、时段(Epoch)、验证人(Validator)、⻅证消息(attestation)、验证人委员会 (Committee)、检查点(Checkpoint)、合理化(justified)、敲定的(Finalized)、Casper FFG(Casper, the Friendly Finality Gadget)、LMD GHOST 分叉规则。
基础知识
间隙(Slot)和时段(Epoch)
Slot 和 Epoch 表示信标链的出块时间和共识结算周期。按最新的信标链的技术规范v0.12,一个 Slot 的时间是12秒。每一个 Epoch 由32个Slot组成,大约6.4分钟。也就是说在正常情况下,信标链每12秒就产出一个区块。每6.4分钟是一个新的共识周期。
来源:https://ethos.dev/beacon-chain/
验证人(Validator)
信标链启动时需要至少16,384个 Validator(一共524,288个 ETH),才能成功激活信标链。验证人们负责对信标链和分片链(目前未实现,将在 Phase1实现)的最新区块进行投票共识。
验证人委员会(Committee)
每一个 Epoch 开始时,信标链都会通过 RANDAO 伪随机算法为信标链和分片选举出由至少128个验证人组成的 Committee。每一个 Slot 都会有一个 Committee 和一个出块者 (Proposer)共同完成出块。
来源:https://ethos.dev/beacon-chain/
⻅证消息(Attestation)
验证人的投票在信标链中称为⻅证消息(Attestation),在标准中一条⻅证消息由三个投票组成:
使用 LMD Ghost 分叉算法,选出主链。
使用 Casper FFG 进行检查点(Checkpoint)的敲定。
对分片链状态(Crosslink)的投票(在 Phase 0 阶段还没有实现))
LMD GHOST和Casper FFG
所有 PoS 类型的区块链都面临着两个最重要的安全问题:
无利害关系(Nothing-at-Stake)
在 PoS 共识机制中,矿工可以在所有分叉上进行挖矿而没有成本,从而达到收益最大化。
⻓程攻击 (Long-Range-Attack)
攻击者首先获得一些私钥,只要这些私钥曾获得足够多的股权,便可以从这一时刻开始分叉进行 51% 攻击,制造一条分叉链。而由于 PoS 的出块不需要进行工作量证明,攻击者可以短时间内让重写历史的分叉链追赶上原本的主链,从而造成 PoS 链和安全性威胁。
以太坊2.0就是通过 LMD GHOST 和 Capser FFG 一起来保证链上的共识的完成。
LMD GHOST
在比特币PoW共识算法中,分叉规则遵循的是最⻓链原则,即积累算力最多的链,也称为主链;其他则被称为分叉链。随着共识的不断进行,主链积累的算力也有可能被其他分叉超过,成为分叉链。
LMD 让消息 (messages)发挥了作用,即以太坊 2.0 链上的最终性是由最新消息驱动的。消息即证明 (attestation),总结来说,拥有最多投票的分叉链将被认为是主链。
上图体现了由最新消息驱动的分叉选择规则:绿色区块表示经由 LMD GHOST 分叉选择规则证明了的区块,笑脸符号表示最新的验证者证明 (attestations),某个区块中的 证明总量 (笑脸总数) 就是该区块的权重,用区块中的数字表示。
尽管位于上方的分叉链是最⻓的链,但下方由绿色区块组成的链才是主链,因为绿色区块包含了最多的证明,也就是拥有最多的验证者投票。
Casper FFG
Casper FFG 全称为「Casper the Friendly Finality Gadget (Casper 友好的最终性小工具)」 ,是 Vitalik 提出的一个 PoW/PoS 混合的算法,目的是使 Ethereum 平滑过渡到纯PoS。
Vitalik 总结了四条规则,任何违反此四条规则的行为都要被取走押金。
提交(commit_req):收到 2/3 节点的预备讯息后才能提交。
预备(prepare_req):每个预备讯息只能指向某个也具有 2/3 节点预备讯息的高度(Epoch),且这些预备讯息也必须都指向同一个高度。
预备提交一致性(prepare_commit_consistency):任何新的预备讯息只能指向最后一个已提交的或其他比其更新的高度。
不重复预备(no_double_prepare):不能在同一个高度送出两次预备。
这四条规则可以进一步简化为两条:
某验证节点 v 必不可发出两个相异的投票: <ν,>及
<ν,>, 且使下列任一条件成立:
1. h(t1) = h(t2)
验证节点必不可对某高度发出两个相异投票。
2. h(s1) < h(s2) < h(t2) < h(t1)
验证节点必不可投出高度围绕/被围绕于另一投票高度的投票。
Casper FFG 运作
Casper FFG 通过检查点(Checkpoint)的合理化(Justified)和敲定(Finalized))来完成共识。
在共识的过程中,验证人除了对每个 Slot 进行共识出块,还要对 Epoch 的检查点进行投票,Epoch 的检查点一般为第一个 Slot 的区块。每个验证节点都要对检查点进行投票,投票的内容是由两个Epoch的检查点组成的连接(Link),连接的起点称为源头(Source),终点称为目标(Target);若投票给某个连接的票数超过2/3,则该连接被称为绝对多数连接(Supermajority Link)。
由根检查点开始,若某连接为一个绝对多数连接,则该连接的目标进入为已合理化(Justified)状态,该连接的源头进入已敲定(Finalized)状态。进入敲定状态的交易不 可逆。
信标链的激励机制
信标链的奖励由五部分组成:
⻅证被包含在最新的区块中
⻅证中包括了正确的检查点的投票
⻅证中包含了正确的最新区块
⻅证被很快地包含到链上
⻅证包含正确的分片区块(Phase1阶段实现)
def get_attestation_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: """ Return attestation reward/penalty deltas for each validator. """ source_rewards, source_penalties = get_source_deltas(state) # 计算检查点source奖惩 target_rewards, target_penalties = get_target_deltas(state) # 计算检查点target奖惩 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
(向左滑动,查看完整代码)
上面的代码段就是信标链最新的标准。验证人的奖励实际上奖励由基础奖励(B) * 执行正确投票的验证人比例(P)构成,任何一个没有正确投票的验证人都将受到-B的惩罚。这样一来,做出正确投票的人越多,大家得到的奖励就会越多,从而抑制作恶的投票。
目前决定一个验证人基础奖励的计算公式如下标准代码所示:
def get_attestation_component_deltas(state: BeaconState, attestations: Sequence[PendingAttestation] ) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: """ Helper with shared logic for use by get source, target, and head deltas functions """ 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 # Factored out from balance totals to avoid uint64 overflow if is_in_inactivity_leak(state): # Since full base reward will be canceled out by inactivity penalty deltas, # optimal participation receives full base reward compensation here. 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)
(向左滑动,查看完整代码)
核心代码行:
reward_numerator = get_base_reward(state, index) * (attesting_balance // increment) rewards[index] += reward_numerator // (total_balance // increment)
(向左滑动,查看完整代码)
在理想状态下验证人的收益就是4倍的 BaseReward ,而 BaseReward 的计算公式如下:
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) # BASE_REWARD_FACTOR = 64 基础奖励倍数 # BASE_REWARDS_PER_EPOCH = 4 每个Epoch的基础奖励 # effective_balance 验证人的有效余额 # integer_squareroot(total_balance) 所有有效余额的开平方
(向左滑动,查看完整代码)
出块人将会得到 BaseReward / 8的出块奖励:
def get_proposer_reward(state: BeaconState, attesting_index: ValidatorIndex) -> Gwei: return Gwei(get_base_reward(state, attesting_index) // PROPOSER_REWARD_QUOTIENT)
(向左滑动,查看完整代码)
参考资料:
https://www.chainnews.com/articles/502750353901.htm
https://prylabs.net/participate
https://github.com/ethereum/eth2.0-specs/blob/dev/deposit_contract/contracts/validator_registration.vy
https://github.com/ethereum/eth2.0-specs/blob/dev/specs/phase0/beacon-chain.md
https://ethos.dev/beacon-chain
微信扫描关注公众号,及时掌握新动向
2.本文版权归属原作所有,仅代表作者本人观点,不代表比特范的观点或立场
2.本文版权归属原作所有,仅代表作者本人观点,不代表比特范的观点或立场