初识pos
写在前面,这是之前为了课堂分享写的一个学习笔记,大佬们绕行,有不足之处请指正,文末罗列了一些资料,或许对您有帮助。
- 1.介绍以太坊合并升级以及路线图,什么是→eth1(执行层),eth2(共识层)
- 2.Gasper=Casper FFG + LMD-GHOST
- 3.介绍pos的两个客户端和官方仓库
- 4.如何成为质押节点(独立质押/协议质押)
1.以太坊的合并与升级
🌐 以太坊=共识层(eth 2) + 执行层(eth 1)
以太坊是一个永不停歇的世界计算机,相比于比特币,它更注重在区块链构建去中心化应用,在很长一段时间,以太坊的共识机制都是pow(ethash),通过显卡挖矿开采区块,出于可拓展性和绿色能源的考虑,以太坊决定升级并将共识替换为pos。这种共识的简单解释就是将算力竞争变为代币权益的竞争。持有代币的用户通过投票来选举产生新区块,这样就避免了pow模式下挖矿造成的电力浪费。
以太坊在2014年开始了长达8年的pos研究之旅,2020年,以太坊上线了信标链( beacon chain),并在这个链上尝试做一些pos的实验,2022年9月15日,将信标链与以太坊主链合并,至此完成了以太坊的升级,宣告了pow时代的结束。在上面这张图中,左边代表以太坊的执行层也就是一直运行的主链,右边白熊是共识层(信标链),它们共同组成了全新的以太坊。
这是一张以太坊的升级路线图,合并只是以太坊更新的第一步,在后续工作中,还会通过分片扩容提高rollup拓展性等等。
所以,我们可以通过一张简单的图概括现在的以太坊形态,从上到下分别是共识层,执行层,以太坊的64个分片。
接下来,我们将重点聚焦到信标链和pos算法的介绍上。
2.Gasper
🐻 Gasper=Casper FFG + LMD-GHOST
pos算法是一类算法的统称,最早的一种2014年由点点币提出,并被以太坊深入研究发展出了另外两种。以太坊选择的算法叫做Casper FFG,在此基础上,引入另一个规则LMD-GHOST,他们共同组成了信标链的pos算法,也就是Gasper。前者是一种投票选择的规定,后者用于分叉情况的处理,它们的关系同样可以用这张熊猫图概括。
2.1 基本概念
大家都知道pos其实就是个类似股东投票过程,谁的“股权”多,谁的资历老,谁的话语权就大,所以在介绍协议之前,让我们了解一下这个投票过程是什么样的。
用户通过质押32个eth成为一名验证者(validator),相当于获得了投票的权利。为了解决节点通信量大的问题,以太坊做了一些时间层次的划分。如图,一个slot(插槽)代表一个出块时间,这个时间是12秒,32个slot组成一个Epoch(纪元),代表一个大周期,时间为6m24s,在这一点上pos的出块稳定程度是高于pow的。
接下来,将会随机选择一名验证者,去发起一个区块提议(propose),由它去出块。同时,其它的验证者会组成一个人数≥128委员会(committee),委员会通过投票来确认区块,整个过程由一个伪随机算法RANDAO选出。
上图最重要的部分是RANDAO的随机选择部分,整个过程比较复杂,概括来说主要包括验证者私钥签名以及VDF计算,最后得到一个随机选择。
2.2 Casper FFG
Casper这种pos算法其实都是bft类型的容错算法,它由pbft共识算法改进而来,所以,如果你还不熟悉pbft,我们可以先回顾一下pbft算法的核心思想。
这张图一共有四个节点,其中三号节点默认是出于掉线状态,也就是说,其他每个节点要收到另外两个节点投票才能完成共识,也就是我们常说的拜占庭算法的1/3容错问题。投票过程分为两轮,第一轮为prepare阶段,第二轮为commit阶段,这样就达成了确定,也就是所谓的最终性。
- 投票需要两轮确定。
- pbft的优点是终局性,达成终局性的区块不可分叉。
- pbft缺点是通信量大,节点多了就不好处理。
可不可以提出一种算法既满足终局性又可以在节点较多的区块链网络中使用?
Capser就是为了解决这两个问题诞生。在2.1中,我们提到了分组,将一个区块时间定为一个slot,32个slot就是一个Epoch,这样做就是为了解决通信数据大不好处理的问题,那最终性是怎么敲定的呢,与pbft类似,这个过程同样是一个两轮投票。
在2.1中我们提到,每一个slot都分配了一个提议者和委员会,提议者负责出块,委员会负责投票确定。但委员会负责投的票有两种,一种是LMD-GHOST类型投票,负责选举当前slot正确的区块,另一种是Casper FFG类型,针对上一个Epoch的投票,由他们来负责检查上一个Epoch的合法性,当前Epoch结束后,会汇总32个委员会的投票结果,如果对前一个Epoch的投票结果大于2/3,就说明上一个Epoch进入了证明状态(justified),上上个Epoch达成了终局性(finalized)。
为什么对上一个Epoch投票,使上上个Epoch达成了终局性?
这点就非常像pbft的二轮投票,如上图,当你对Epoch1完成投票时就默认也认可了Epoch0,所以Epoch0收到两轮2/3以上节点认可,Epoch1收到一轮2/3以上节点认可。至此,Ephoch0完成终局性,里面的所有区块不可以再分叉
上面的图解释如下,检查点就是每个Epoch的第一个slot,一次投票的范围为当前Epoch的检查点到上一个Epoch检查点。
- slot 96处产生一个新的区块,标志着epoch 2结束。该区块内存储了关于epoch 2检查点(即slot 64处区块)的投票;
- 信标链检查epoch 2检查点(即slot 64处区块)是否获得验证人超过2/3的证明;如果超过,则epoch 2检查点(即slot 64处区块)及epoch 1中的所有区块都被证明(justified);
- 如果epoch 2检查点(即slot 64处区块)被证明,那么epoch 1检查点(即slot 32处区块)及之前被证明的所有区块都最终(finalized)。
所有最终确认(finalized)的检查点都成为规范链(区块链历史的一部分),所有忠诚节点都默认接收规范链,即「最终检查点」之后的区块可以随意分叉,但之前的区块不允许分叉。但如果节点主动选择作恶,则需要承担高昂的攻击成本。所以,通过Casper FFG保证了ETH2.0的安全性。
如果一个Epoch达不到2/3投票怎么办?
如果Epoch收集不到两轮投票,那么它就一直无法确定终局性,在不久之前,5.11日由于客户端自身缺陷导致大量验证者离线,使得Epoch敲定推迟了三个Epoch,短时间后自动恢复正常,这得益于LMD-GHOST机制,因为以太坊的区块生产和正式确认是两个环节,虽然Casper堵了,但LMD-GHOST负责的生产部分仍在进行,所以业内人士认为以太坊网络具有韧性和自我修复能力。还有一部分原因在于客户端的多样化和大多数链上节点诚信投票(奖惩机制作用)。
🤖完整的分析报告请看:以太坊网络Finalize延迟事件分析
2.3 LMD-GHOST
LMD-GHOST(Latest Message Driven GHOST)是一种分叉选择的规则,主要用于处理pos中的分叉。其实在以太坊上一个版本中就提到过GHOST协议,用于产生分叉带来叔块引用,我们将此协议做了pos环境下的升级,依据投票比重来选择正确的区块。
通过下面一张图来简单理解一下,由于网络延迟或者潜在攻击的问题,新的区块产生可能会发生分叉,这时,我们选择的策略就叫LMD(最新的消息驱动),我们会选择票数最多(权重)的链作为权威链,这一点区分于pow中的最长链原则。下图的一个笑脸代表一个验证者投票,我们选择笑脸最多的链作为合法链,虽然比上面的分叉少一个区块,但它的票数多代表认可多。
两种规则都是投票,怎么理解其中的关系?
可以将LMD GHOST理解为地方选举,将Casper理解为全国选举,它们负责的投票范围不同,具体来说,以太坊区块的生产和敲定是分开的,LMD GHOST 指导区块生产过程,并以 12 秒的 slot 时间尺度和验证器的子抽样委员会运行。因此,它可以被视为负责 PoS 以太坊区块链末端附近较弱的“短期共识”。一旦就交易账本达成短期共识,它就会被移交给 Casper FFG 进行额外的加固,其在包含 32 个 slot(一个 epoch 时间段,也就是 6.4 分钟)的时间尺度上运行,并涉及完整的验证器集。因此,Casper FFG 负责提供更强大的“长期共识”,提供确定性和负责任的安全性。
2.4网址演示
可自行登录https://beaconcha.in/
查看信标链上的Epoch和slot情况,实例截图如下。
从上面的图,可以很直观的看到Epoch和slot的关系,以及一些投票参与率,验证者人数,区块提议者等等,可以自己进入网址去浏览更多的信息。
2.5 pos奖惩
💻 为了更好的鼓励验证者诚信投票,pos针对投票设计了一套奖惩机制。
奖励的组成很简单,如下表所示,参与委员会,成为提议者,验证区块都可以获得奖励,其中,提议者的奖励是最多的,具体的比例和计算公式可从附录提供的资料查阅。
惩罚的情况有几种,作为一个验证者和委员会延迟,错过或者不正确的证明都会收到奖励缩减,提议者错过区块暂时没有明确的处罚,只是将当前slot记为空,并进入下一个slot。有一个比较特殊的设置,称为slash(罚没),这是一种非常严厉且特殊的惩罚,比如下图就是一个被slash的验证者。
当触发以下几种情况时,将会大幅度扣除eth乃至剥夺身份(slash)。
- 在一个slot提议两个区块
- 对一个区块的候选区块进行双重投票(double voting)
- FFG环绕投票,相当于篡改区块历史,指一个FFG投票的区间包括了另一个FFG投票的区间,如下图
2.6 pos共识攻击
以太坊共识层面可能存在的攻击有很多种,这里主要介绍长程攻击(long range attacks),无利害攻击(noting to stake attacks)和大型质押攻击。
长程攻击有两种情况,第一种情况是,攻击者作为参与创始区块的验证者,在原本的区块链旁维护一个单独的区块链分叉,并最终说服诚实的验证者在很久以后的某个时间点切换过去。但是该攻击在信标链上无法实现,因为“finality gadget”可确保所有验证者定期就诚实链的状态(“检查点”)达成一致,此后检查点之后的区块将无法再进行重组。**第二种情况是,当新节点加入网络时,将从离其最近的节点处获取信息(称为弱主观性检查点)作为伪创始区块构建区块链。**这将为加入网络的新节点创建一个“信任网关”,然后其才能开始自己验证区块。然而,从区块浏览器等客户端收集构建检查点所需的可信区块信息,并不能增加客户端本身的可信度,因此主观性是“弱的”。因为根据定义,检查点由网络上的所有节点共享,所以不诚实的检查点是共识失败的状态。
无利害攻击就是说相对于pow而言,pos分叉成本非常低,因为对于单个矿工来说,自身算力是有限的,所以为了利益它必须遵守最长链原则,而pos一旦获得投票资格,可以随意支持区块造成分叉所以才会有2.5节中奖惩机制的设立,避免这种攻击(摆烂)行为。
大型质押攻击就是说攻击者持有相应比例的eth时,会触发以下危机:
- 33%:延迟终局性
- 34%:导致双重终局性
- 51%:控制区块链的未来
- 66%:控制区块链的过去
质押攻击主要的应对措施就是通过惩罚机制将攻击者强制退出网络,发起社区提议罚没攻击者资产,在现实社交网络进行协调等等,可以通过这篇文章加深认识。
😈proof of stake Ethereum attack and defense
3.pos部分代码导读
☯️ 简单介绍一下geth,prysm和官方repo的共识部分
写在前面,本来是想找个代码读一下的,发现工程量比较大,几天时间弄不完,所以本章取名叫导读。目前没有一个很简单的demo能解释Gasper(过程太复杂),所以本章第一部分找了一个相对较新的pos代码(有validator部分),能明白权重的感觉就好,后面的两部分可以选看。
第二部分我将梳理一下geth(eth1)和prysm(eth2)这两个客户端(go语言)的becaon-chain部分。
第三部分讲一下官方的repo(python语言),在那里对一些变量有清晰的定义。
这一章参考资料较少,且个人水平有限,有错误欢迎指正,并会根据学习进度适时更新
文中涉及的仓库地址
🤪geth:https://github.com/ethereum/go-ethereum
🤪prysm:https://github.com/prysmaticlabs/prysm
🤪官方pos-repo:https://github.com/ethereum/consensus-specs
3.1 pos-demo
https://github.com/blockchainGuide/Consensus_Algorithm
项目引用@mindcaver,涉及到的知识有cobra(命令行框架),go并发,db操作和go语言基础。
/* Consensus_Algorithm/mypos */ |-blockchain//介绍区块链的构建 |-addBlock.go//添加区块 |-blockchain.go//区块的哈希序列化操作 |-create.go//创世区块生成 |-view.go//从db中获取block切片组 |-common |-common.go//介绍一些block和validator参数 |-consensus//共识代码 |-addValidator.go//添加validator |-consensus.go//共识主要部分 |-initValidator.go//初始化validator |-viewValidator.go//从db中读取validator |-main.go//包含基于cobra实现的一些cli操作
//common中给出了block和validator参数 type Block struct { Index int TimeStamp string Data string HashCode string PrevHash string Validator Validator } type Validator struct { Tokens int Days int Address string }
这里主要介绍consensus.go这个文件。pos函数为这个文件中核心部分,我们将函数中最重要的权重出块,随机选择代码单独列出来。
// 根据验证者拥有的token数量及时间得出权重,权重越高,被选取为出块节点的概率越大 for i := 0; i < len(validators) - 1; i++{ for j := 0; j < validators[i].Tokens * validators[i].Days; j++{ tempValidators = append(tempValidators, validators[i]) } } /* 第一个循环,遍历所有验证者,并根据验证者拥有的代币数量和代币年龄计算出验证者的权重。 第二个循环,根据权重,将每个验证者的数量复制到一个验证者切片中,以便在随机选择时使用。 */ //从切片中随机选一个validator填到区块里 rand.Seed(time.Now().Unix()) var rd =rand.Intn(len(tempValidators)) block.Validator = validators[rd %len(validators)]
由于涉及知识点多,时间少唠不过来,以下部分为选读,我在原有的基础上添加了注释,应该难度不大。
🐙贴一个之前写的go并发笔记
package consensus import ( "encoding/json" "fmt" "github.com/boltdb/bolt"//数据库的库 "github.com/spf13/cobra"//命令行操作的库 "log" "math/rand" "my-pos/common" "sync" "time" ) //并发锁 var mutex sync.Mutex //返回一个*cobra.Command对象,管理三个子命令 func ConsensusCmd () *cobra.Command{ consensusCmd.AddCommand(addValidatorCmd) consensusCmd.AddCommand(viewValidatorCmd) consensusCmd.AddCommand(initValidatorCmd) return consensusCmd } //初始化命令对象,一个命令有三个组成部分,run后接一个空函数表示不做 var consensusCmd = &cobra.Command{ Use: "consensus", Short: "consensus", Long: "consensus manage", Run: func(cmd *cobra.Command, args []string) { }, }
start两部分,一部分为加区块,一部分为循环共识计算
func Start(){ fmt.Println("开始共识...") // 不断读取候选区块通道,如果有新的区块,就追加到临时区块切片中 // func()这个匿名闭包函数单独在一个goroutine执行 go func() { for candidate := range common.CandidateBlokcs{ fmt.Println("有新的临时区块") common.TempBlocks = append(common.TempBlocks, candidate) } }() for { // 循环pos共识算法 pos() } }
pos()为主要的共识模块,主要内容就是挑选validator加入区块。
func pos() { // 复制临时区块 temp := common.TempBlocks // 根据temp的长度判断是否存在临时区块 if len(temp) > 0{ fmt.Println("准备出块...") var tempValidators []common.Validator // 获取所有的验证者 validators := getAllValidators() // 根据验证者拥有的token数量及时间得出权重,权重越高,被选取为出块节点的概率越大 for i := 0; i < len(validators) - 1; i++{ for j := 0; j < validators[i].Tokens * validators[i].Days; j++{ tempValidators = append(tempValidators, validators[i]) } } // 获取数据库句柄 db := common.GetDB() defer db.Close() for _, block := range temp{ // 挑选验证者 rand.Seed(time.Now().Unix()) var rd =rand.Intn(len(tempValidators)) //随机加入一个验证者 block.Validator = validators[rd %len(validators)] // 持久化,给临时区块添加验证者信息,更新db db.Update(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(common.BlocksBucket)) //将block序列化为字节数据存入 err := bucket.Put([]byte(block.HashCode), serializeBlock(block)) if err != nil { log.Fatal(err) } err = bucket.Put([]byte("lastHash"), []byte(block.HashCode)) if err != nil { log.Fatal(err) } return nil }) fmt.Println("区块添加到区块链完成!") } mutex.Lock() //发送一个信号暂停挖矿 common.ExitChan <- true temp = []common.Block{} common.TempBlocks = []common.Block{} mutex.Unlock() } }
这一部分给了一个读取validator和序列化block的方法
func getAllValidators() []common.Validator { var validators []common.Validator//存储切片 db := common.GetDB()//获取数据库实例 defer db.Close()//确保函数结束后关闭 db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(common.PeerBucket)) cursor := bucket.Cursor() for k, v := cursor.First(); k != nil; k, v = cursor.Next(){ //遍历桶,如果k不是lasthash,就将其反序列化存储切片 if string(k) == "lastHash"{ continue } block := deserializeValidator(v) validators = append(validators, block) } return nil }) return validators } func serializeBlock (block common.Block)[]byte { //将common.Blcok类型的block序列化为字节数组 result, err := json.Marshal(block) if err != nil{ log.Fatal("marshal block error: ", err) } return result }
以下内容3.2和3.3为选读,写多少是多少~
3.2 geth和prysm
在第一章提到了升级后的以太坊分为共识层和执行层两个部分,相应的,开发者也要启动两个类型的客户端去成为一个网络节点,在这里我们选择的执行层客户端为geth,共识层客户端为prysm,它们都是go语言写的。在客户端的源码上去理解beacon-chain会更加直观。
geth的consensus目录结构下放置了和共识有关的文件,目录如下
|-becaon//存放与信标链有关的pos代码 |-consensus.go |-clique//poa共识 |-ethash//pow共识 |-misc//dao,有关gas和提款的eip提案
- 虽然升级了,但这里代码中还有pow,poa的部分,是因为迁就一些没来得及更新老客户端,便于它们转向pos,TTD到了,就会切换过来(共识引擎的切换)。【此处由@Jude指出】
以太坊当初为了顺利升级,设置了一个难度炸弹,当一个客户端还坚持老的pow共识,会因为挖矿难度指数增长挖不到矿,所以此处代码就是为了做一个过渡转接验证,同时涉及一些shanghai升级(支持质押提款)的处理,更多的共识细节参考共识层。consensus.go中主要有以下几种类型的函数。
type consensus.Engine interface{ Prepare()//向Header填充与交易无关的字段 FinalizeAndAssemble()//区块填满后调用,更新状态,将不完整的区块头和信息列表组装为区块返回 Seal()//区块发布前的准备工作,通过channel返回block VerifySeal()//验证区块头是否满足密码学要求 VerifyHeader()//检查区块头是否合法(时间,分叉等) VerifyHeaders()//批量检查区块头 Finalize()//状态更新 Author()//输入块头返回地址 CalcDifficulty()//难度调整 SealHash()//输入一个未签名的块头,返回哈希 }
prysm是使用最多的共识层客户端,在becaon-chain/core目录下有比较详细的go语言代码实现,设计原理还请参考官方的repo(3.3节)
3.3 ethereum/consensus-spec
https://github.com/ethereum/consensus-specs
目前的版本已经迭代到了第四版,可以从phase0/beacon-chain.md看起。通过这节补充几个点,比如信标链区块结构,RANDAO等等
BeaconBlock
class BeaconBlock(Container): slot: Slot proposer_index: ValidatorIndex parent_root: Root state_root: Root body: BeaconBlockBody
Validator
class Validator(Container): pubkey: BLSPubkey withdrawal_credentials: Bytes32 # Commitment to pubkey for withdrawals effective_balance: Gwei # Balance at stake slashed: boolean # Status epochs activation_eligibility_epoch: Epoch # When criteria for activation were met activation_epoch: Epoch exit_epoch: Epoch withdrawable_epoch: Epoch # When validator can withdraw funds
RANDAO,RANDAO的核心目的就是生成一个不可预测的随机数,简单来说就是一个人自己想一个数字,然后逐个公布加一起就是一个很随机的数,但是嘞,最后一个公布的人就权利就很大,他可以去计算一下看一下最终结果对自己是不是有利的,整个环节可能堵死在最后一人那。所以以太坊又引入了一个延迟函数(VDF),这个函数使得最后一个人计算起来最终结果很费事,一个Epoch内他是肯定算不出来的,所以只能老老实实的和别人一样正常提供随机数,不然超时提供他就错过了一轮投票赚钱的机会,而且以太坊有专门用来验证VDF的机器,这就导致作恶者在软件硬件上都很难找到机会。
def process_randao(state: BeaconState, body: BeaconBlockBody) -> None: epoch = get_current_epoch(state) # Verify RANDAO reveal 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) # Mix in RANDAO reveal mix = xor(get_randao_mix(state, epoch), hash(body.randao_reveal))#核心是这句 state.randao_mixes[epoch % EPOCHS_PER_HISTORICAL_VECTOR] = mix
4.如何成为一个质押节点
⚠️本章所有内容仅供学习使用,数据由服务商提供,使用goeril eth测试教学,不构成任何投资建议。
质押,简单来说就是成为验证者,质押32eth就可以变成网络的一部分,以太坊有一个专门介绍质押的网站,地址是以太坊质押 |ethereum.org,里面介绍了几种不同的质押方式。我们今天主要介绍两种,单独质押和集合质押。
4.1独立质押
第一种,就是在你的电脑上跑一个节点,你需要准备的是两个客户端(geth,prysm),32个eth,一台16GB的电脑,大于500GB的SSD,怎么下载客户端就不提了。
运行网址为Staking Launchpad (ethereum.org),参考教程为以太坊2.0主网质押教学(Ubuntu/Prysm) · Ethereum Community Network
运行客户端,下载一个存款工具cli,按提示输入密码生成助记词完成验证,会生成这样一个页面。代表你已经有了一个存款数据文件和密钥文件,将第一个文件发送到网站上,完成身份验证。
链接你的metamask钱包(确保里面余额大于32eth),逐步确认交易就完成了,同步数据时间很长~ 注意一点是,一个验证者要保证24小时在线,掉线多长时间就扣多少钱,所以理论上一天至少在线时间大于50%才能有收益。
4.2集合质押
如果你的代币足够,但硬件跟不上,可以在云服务器上跑节点,24小时不断电。
但如果你和我一样,以上两种条件都不满足(硬件不想做,eth也不够),可以考虑集合质押,把很多人的钱放一起凑成一个验证者,赚了再平分。这一点和余额宝吃利息没啥区别。接下来介绍一种去中心化的质押方式,登录https://testnet.rocketpool.net/
把你的eth放进去(最少0.01个),这种收益大概在4%左右。
如果你不习惯链上操作,也可以在中心化交易所完成。
大多数用户其实都是依靠后面这一种方式质押的,所以可以思考一下,stake的集中和云服务器节点的集中会不会带来新的危机?
参考资料
pos的资料跨度很大,推荐看最新的,注意甄别内容。前三个英文资料很好
- Ethereum-docs-pos
- How does the NEW Ethereum work?
- eth2book
- proof of stake FAQ 2017
- What is Proof of Stake
- The Beacon Chain Ethereum 2.0 explainer you need to read first
- Why Proof of Stake (Nov 2020) 2020
- A Proof of Stake Design Philosophy2016
- Proof-of-stake Ethereum attack and defense
- Nothing-at-stake problem
- 以太坊2.0@crypto_white
- 解读以太坊POS工作原理:Epoch、Slot 与信标区块
- 浅析以太坊
- ETH2.0出块笔记
- 浅谈Casper
- 以太坊质押教程
- Casper and GHOST论文
评论前必须登录!
注册