1.clique中的概念和定义
- EPOCH_LENGTH?: epoch长度是30000个block, 每次进入新的epoch,前面的投票都被清空,重新开始记录,这里的投票是指加入或移除signer
- BLOCK_PERIOD?: 出块时间, 默认是15s
- UNCLE_HASH?: 总是?Keccak256(RLP([]))?,因为没有uncle
- SIGNER_COUNT?: 每个block都有一个signers的数量
- SIGNER_LIMIT?: 等于?(SIGNER_COUNT / 2) + 1?. 每个singer只能签名连续SIGNER_LIMIT个block中的1个
- 比如有5个signer:ABCDE, 对4个block进行签名, 不允许签名者为ABAC, 因为A在连续3个block中签名了2次
- NONCE_AUTH?: 表示投票类型是加入新的signer; 值=?0xffffffffffffffff
- NONCE_DROP?: 表示投票类型是踢除旧的的signer; 值=?0x0000000000000000
- EXTRA_VANITY?: 代表block头中Extra字段中的保留字段长度: 32字节
- EXTRA_SEAL?: 代表block头中Extra字段中的存储签名数据的长度: 65字节
- IN-TURN/OUT-OF-TURN?: 每个block都有一个in-turn的signer, 其他signers是out-of-turn, in-turn的signer的权重大一些, 出块的时间会快一点, 这样可以保证该高度的block被in-turn的signer挖到的概率很大.
- 创世块中的Extra字段包括:
- 32字节的前缀(extraVanity)
- 所有signer的地址
- 65字节的后缀(extraSeal): 保存signer的签名
- 其他block的Extra字段只包括extraVanity和extraSeal
- Time字段表示产生block的时间间隔是:blockPeriod(15s)
- Nonce字段表示进行一个投票: 添加( nonceAuthVote:?0xffffffffffffffff?)或者移除( nonceDropVote:?0x0000000000000000?)一个signer
- Coinbase字段存放?被投票?的地址
- 举个栗子: signerA的一个投票:加入signerB, 那么Coinbase存放B的地址
- Difficulty字段的值: 1-是?本block的签名者?(in turn), 2-?非本block的签名者?(out of turn)
2. PoA的特点
- PoA是依靠预设好的授权节点(signers),负责产生block。普通节点不能挖矿无生成区块的权利
- 可以由已授权的signer选举(投票超过50%)加入新的signer。
- 即使存在恶意signer,他最多只能攻击连续块(数量是 (SIGNER_COUNT / 2) + 1)?中的1个,期间可以由其他signer投票踢出该恶意signer。
- 可指定产生block的时间。
- 无挖矿奖励
3. PoA的工作流程及接口
- 在创世块中指定一组初始授权的signers, 所有地址保存在创世块Extra字段中
- 启动挖矿后, 该组signers开始对生成的block进行 签名并广播.
- 签名结果?保存在区块头的Extra字段中
- Extra中更新当前高度已授权的 所有signers的地址?,因为有新加入或踢出的signer
- 每一高度都有一个signer处于IN-TURN状态, 其他signer处于OUT-OF-TURN状态, IN-TURN的signer签名的block会 立即广播?, OUT-OF-TURN的signer签名的block会 延时?一点随机时间后再广播, 保证IN-TURN的签名block有更高的优先级上链
- 如果需要加入一个新的signer, signer通过API接口发起一个proposal, 该proposal通过复用区块头 Coinbase(新signer地址)和Nonce("0xffffffffffffffff")?字段广播给其他节点. 所有已授权的signers对该新的signer进行"加入"投票, 如果赞成票超过signers总数的50%, 表示同意加入
- 如果需要踢出一个旧的signer, 所有已授权的signers对该旧的signer进行"踢出"投票, 如果赞成票超过signers总数的50%, 表示同意踢出
signer对区块头进行签名
- Extra的长度至少65字节以上(签名结果是65字节,即R, S , V, V是0或1)
- 对blockHeader中所有字段除了Extra的 后65字节?外进行 RLP编码
- 对编码后的数据进行 Keccak256?hash
- 签名后的数据(65字节)保存到Extra的 后65字节?中
clique.go中实现了consensus中的所有接口完成POA算法的实现
Clique.Prepare(chain , header)
Prepare是共识引擎接口之一. 该函数配置header中共识相关的参数(Cionbase, Difficulty, Extra, MixDigest, Time)
- 对于非epoch的block(?number % Epoch != 0?):
- 得到Clique.proposals中的投票数据(例:A加入C, B踢除D)
- 根据snapshot的signers分析投票数否有效(例: C原先没有在signers中, 加入投票有效, D原先在signers中,踢除投票有效)
- 从被投票的地址列表(C,D)中,?随机选择一个地址?,作为该header的Coinbase,设置Nonce为加入(?0xffffffffffffffff?)或者踢除(?0x0000000000000000?)
- Clique.signer?如果是本轮的签名者(in-turn), 设置header.Difficulty = diffInTurn(1), 否则就是diffNoTurn(2)
- 配置header.Extra的数据为[?extraVanity?+?snap中的全部signers?+?extraSeal?]
- MixDigest需要配置为nil
- 配置时间戳:Time为父块的时间+15s
共识引擎clique的初始化
在?Ethereum.StartMining?中,如果Ethereum.engine配置为clique.Clique, 根据当前节点的矿工地址(默认是acounts[0]), 配置clique的?签名者?:?clique.Authorize(eb, wallet.SignHash)?,其中?签名函数?是SignHash,对给定的hash进行签名.
Clique.snapshot(chain,number,hash,parents)
快照在指定的时间内检索授权的快照,首先封装了在内存和磁盘中,寻找快照;
如果是在创世区块中则创建一个新的快照;
如果没有区块头的快照,则收集区块向后移,有明确的父就强制到父,没有明确的父到数据库中找
找到快照后,将所有的header的后半部分前移
再通过区块头生成一个新的快照,将当前区块的hash保存到最近的快照中,将生成的快照保存到磁盘上
Snapshot.apply(headers)
创建一个新的授权signers的快照, 将从上一个snapshot开始的区块头中的proposals更新到最新的snapshot上
- 对入参headers进行完整性检查: 因为可能传入多个区块头,?block号必须连续
- 遍历所有的header, 如果block号刚好处于epoch的起始(number%Epoch == 0),将snapshot中的Votes和Tally复位(?丢弃历史全部数据?)
- 对于每一个header,从签名中得到?signer
- 如果该signer在snap.Recents中, 说明?最近已经有过签名?, 不允许再次签名, 返回空
- 记录?该signer是该block的签名者:?snap.Recents[number] = signer
- 统计header.Coinbase的投票数,如果?超过signers总数的50%
- 执行加入或移除操作
- 删除snap.Recents中的一个signer记录: key=number- (uint64(len(snap.Signers)/2 + 1)), 表示释放该signer,下次可以对block进行签名了
- 清空被移除的Coinbase的投票
- 移除snap.Votes中该Conibase的所有投票记录
- 移除snap.Tally中该Conibase的所有投票数记录
?Clique.Seal(chain, block , stop)
Seal也是共识引擎接口之一. 该函数用clique.signer对block的进行签名. 实现共识,引擎,尝试使用创建密封块
可对一个调用过 Finalize()的区块进行授权或封印,成功时返回的区块全部成员齐整,可视为一个正常区块,可被广播到整个网络中,也可以被插入区块链等
- 不支持密封genesis区块
- 如果signer没有在snapshot的signers中,不允许对block进行签名
- signer不是本区块的签名者需要延时随机一段时候后再签名,是本区块的签名者则直接签名
- 当前签名者在‘最近签名者’中,则等待下一个epoch
- 不支持0-period的链,不支持空块密封,没有奖励但是能够密封
- 把签名的结果用copy替换保存到区块头Extra字段的extraSeal的65字节中中
Clique.VerifySeal(chain, header)
VerifySeal也是共识引擎接口之一.
- 从header的签名中恢复账户地址,改地址要求在snapshot的signers中
- 检查区块头中的计算难度是否匹配(in turn或out of turn)
Clique.Finalize
Finalize也是共识引擎接口之一. 该函数生成一个block, 没有叔块处理,也没有奖励机制
- header.Root?: 状态根保持原状
- header.UncleHash?: 为nil
- types.NewBlock(header, txs, nil, receipts)?: 封装并返回最终的block
API.Propose(addr, auth)
添加一个proposal: 调用者对addr的投票, auth表示加入还是踢出
verifyUncles(chain,block)
判断block中的叔伯块是否大于零,由于这个共识中不允许有叔伯块
ecercover(header,sigcache)
从签名头中提取以太坊帐户地址
CalcDifficulty(chain,time,parent)
返回区块的计算难度,计算难度是难度调整算法
Difficulty字段的值: 1-是?本block的签名者?(in turn), 2-?非本block的签名者?(out of turn)
投票策略
因为blockchain可能会小范围重组(small reorgs), 常规的投票机制(cast-and-forget, 投票和忘记)可能不是最佳的,因为包含单个投票的block可能不会在最终的链上,会因为已有最新的block而被抛弃。
一个简单但有效的办法是对signers配置"提议(proposal)".例如 "add 0x...", "drop 0x...", 有多个并发的提议时, 签名代码"随机"选择一个提议注入到该签名者签名的block中,这样多个并发的提议和重组(reorgs)都可以保存在链上.
该列表可能在一定数量的block/epoch 之后过期,提案通过并不意味着它不会被重新调用,因此在提议通过时不应立即丢弃。
- 加入和踢除新的signer的投票都是立即生效的,参与下一次投票计数
- 加入和踢除都需要 超过当前signer总数的50%?的signer进行投票
- 可以踢除自己(也需要超过50%投票)
- 可以并行投票(A,B交叉对C,D进行投票), 只要最终投票数操作50%
- 再没进入新的epoch对于以一个signer的投票未被通过时,后面有其他signer对该signer的投票也算入判断中
- 进入一个新的epoch, 所有之前的pending投票都作废, 重新开始统计投票
投票场景举例
- ABCD, AB先分别踢除CD, C踢除D, 结果是剩下ABC
- ABCD, AB先分别踢除CD, C踢除D, B又投给C留下的票, 结果是剩下ABC
- ABCD, AB先分别踢除CD, C踢除D, 即使C投给自己留下的票, 结果是剩下AB
- ABCDE, ABC先分别加入F(成功,ABCDEF), BCDE踢除F(成功,ABCDE), DE加入F(失败,ABCDE), BCD踢除A(成功, BCDE), B加入F(由于DE加入F还存在,此时BDE加入F,满足超过50%投票), 结果是剩下BCDEF
4. PoA中的攻击及防御
- 恶意签名者(Malicious signer). 恶意用户被添加到签名者列表中,或签名者密钥/机器遭到入侵. 解决方案是,N个授权签名人的列表,任一签名者只能对每K个block签名其中的1个。这样尽量减少损害,其余的矿工可以投票踢出恶意用户。
- 审查签名者(Censoring signer). 如果一个签名者(或一组签名者)试图检查block中其他signer的提议(特别是投票踢出他们), 为了解决这个问题,我们将签名者的允许的挖矿频率限制在1/(N/2)。如果他不想被踢出出去, 就必须控制超过50%的signers.
- "垃圾邮件"签名者(Spamming signer). 这些signer在每个他们签名的block中都注入一个新的投票提议.由于节点需要统计所有投票以创建授权签名者列表, 久而久之之后会产生大量垃圾的无用的投票, 导致系统运行变慢.通过epoch的机制,每次进入新的epoch都会丢弃旧的投票
- 并发块(Concurrent blocks). 如果授权签名者的数量为N,我们允许每个签名者签名是1/K,那么在任何时候,至少N-K个签名者都可以成功签名一个block。为了避免这些block竞争( 分叉?),每个签名者生成一个新block时都会加一点随机延时。这确保了很难发生分叉。
|