1️⃣ 参考
- 北京大学肖臻老师《区块链技术与应用》
1️⃣8️⃣ Ghost
核心问题:Ghost协议如何解决临时性分叉的?
① 引言
BTC系统中出块时间为10分钟,而以太坊中出块时间被降低到10几秒左右,虽然有效提高了系统反应时间和吞吐率,却也导致系统临时性分叉变成常态,且分叉数目更多。
在BTC系统中,采用最长合法链来解决
- 最长合法链的功能
- 区块数据防篡改
- 解决临时性分叉,提供了出现临时性分叉后合并的机制
在BTC系统中,不在最长合法链上的节点最后都是作废的。但如果在以太坊系统中,如果也这样处理,则矿工挖到矿很大可能会被废弃,这会大大降低矿工挖矿积极性。
- 例如:现在有A、B、C,D其中A、B、C为个人矿工,D是矿厂。此时A、B、C、D同时获得记账权,大概率下个优先获得记账权的是D,那么此时A、B、C的努力就白费了,因为后来的区块会沿着D往后挖
对此,以太坊设计了新的公式协议——GHOST
协议(该协议并非原创,而是对原本就有的Ghost
协议进行了改进)。
② Ghost协议最初版本
核心思想:补偿“白费”,没有功劳,也有苦劳
-
为了补偿这些区块所属矿工所作的工作,给这些区块一些“补偿”,并称其为
Uncle Block
(叔父区块) -
规定E区块在发布时可以将A、B、C叔父区块包含进来,A、B、C叔父区块可以得到出块奖励的7/8
-
而为了激励E包含叔父区块,规定E每包含一个叔父区块可以额外得到1/32的出块奖励。为了防止E大量包含叔父区块,规定一个区块只能最多包含两个叔父区块,因此E只能包含A、B、C中最多只能包含两个区块作为自己的出块奖励
补充
- 2021年8月,以太坊实施了EIP-1559提案,引入了一种新的燃烧机制。 燃烧机制会销毁一部分交易费用,导致流通中的以太币数量减少。 这也被认为是一种奖励形式,因为它增加了剩余以太币的价值。
- 现在(本文发布日期),每个区块固定奖励2个ETH,不再是视频中的的3
- 正在挖的E区块如何包含B和C区块?
- 假定一个矿工发布了D,此时他沿着其所在链继续挖,而他知道C是和自己“同辈”,则可以将A包含进区块挖矿,若挖矿过程中又听到C也是“同辈”,则可以停止挖矿,将B包含进来重新组织成一个新区块重新挖矿,实际中,由于挖矿过程的无记忆性,这样并不会降低成功挖到矿的概率,并不会因为吃亏。
- 最多只能包含两个,那其他区块不是浪费了吗?
- 因为多了容易让ETH不值钱(因为叔父区块奖励很高)
- 出现故意不包含叔父区块的情况怎么办?
因商业竞争关系,不包含叔父区块,导致叔父区块7/8出块奖励没了,而自己仅仅损失1/32,这样对自己损失小而对对方损失大- 不限制辈分,只要叔父区块在,不管后面出现多少区块,它都是叔父区块
- 此时,如果“故意不包含”区块仍坚持不包含,那么别人可能会去包含它的叔父区块来获得奖励
③ Ghost协议新版本
- 那无限制的不限辈分,最开始挖矿难度低的时候产生的叔父区块也要被包含?
- 不会,最多7代,且奖励逐级递减
- 不会,最多7代,且奖励逐级递减
叔父区块的定义:在7代以内,有共同的祖先,合法的叔父只有6个辈分
- 为什么要这么设计?
- 如果代数不限,又所有节点都要验证之前的区块,工作量太大
- 设计逐级递减的奖励策略,可以鼓励区块出现分叉后尽早合并
④ 比特币与以太坊中的奖励
- BTC:静态奖励
static reward
(出块奖励block reward
) + 动态奖励(交易费tx fee
,占据比例很小) - ETH:
- 正常区块: 静态奖励【出块奖励 + 包含叔父区块的奖励
2
+
(
1
32
∗
2
∗
2
)
2 + (\frac{1}{32} * 2 * 2)
2+(321∗2∗2) 】+ 动态奖励(汽油费
gas fee
,占据比例很小) - 叔父区块:静态奖励【叔父区块的奖励 ( 7 8 ∗ 2 ) (\frac{7}{8} * 2) (87∗2) 】
- 正常区块: 静态奖励【出块奖励 + 包含叔父区块的奖励
2
+
(
1
32
∗
2
∗
2
)
2 + (\frac{1}{32} * 2 * 2)
2+(321∗2∗2) 】+ 动态奖励(汽油费
问题 1
- 要不要执行叔父区块中的交易?
- 不执行,叔父区块和同辈的主链上区块有可能包含有冲突的交易。而且我们前文也提到,叔父区块是没有动态奖励的。因此,一个节点在收到一个叔父区块的时候,只检查区块合法性(是否符合挖矿难度)而不检查其中交易的合法性。
- 在比特币系统中检查交易的合法性是为了防止双花问题,但在以太坊中双花问题是通过检查账户余额实现。先执行父区块交易,再执行叔父区块交易,可能有些交易就变成了非法交易
问题 2
- 我的交易在叔父区块中了,不执行叔父区块,那我的交易怎么办?
- 交易被包含在叔父区块中,意味着它没有被纳入以太坊主链。因此,该交易目前不会被执行。但是,并不一定会丢失。叔父区块中的交易有以下几种可能性:
- 重新打包到主链中: 叔父区块有可能被重新打包到主链中,这种情况通常发生在主链出现分叉时。
- 被纳入下一个区块: 被矿工打包到下一个区块中,从而被执行。
- 永远无法执行: 如果叔父区块不被重新打包到主链,也没有被纳入下一个区块,那么该交易将永远无法执行。
- 如何提高交易被执行的可能性?
- 增加交易费用
- 等待网络拥堵情况缓解
问题 3
- **分叉后的堂哥区块怎么办?**例如下图所示,A->F该链并非一个最长合法链,所以B->F这些区块怎么办?该给挖矿补偿吗?
- 如果规定将下面整条链作为一个整体,给予出块奖励,这一定程度上鼓励了分叉攻击(降低了分叉攻击的成本,成了回滚交易A->A’,败了获得叔父奖励)。因此,ETH系统中规定,只认可A区块为叔父区块,给予其补偿,而其后的区块全部作废。
⑤ 叔父区块实例
1️⃣9️⃣ 挖矿算法
核心问题:ETH是如何做到
ASIC Resistance
的?
挖矿这一过虽然并没有创造什么实际价值,但挖矿本身维持了比特币系统的稳定(Block chain is secured by mining
)
当然,比特币系统的挖矿算法也存在一定问题,其中最为突出的就是导致了挖矿设备的专业化,普通计算机用户难以参与进去,这违背了最初设想(one cpu, one vote
)
因此,在比特币之后包括以太坊在内的许多加密货币针对该缺陷进行改进,试图做到ASIC Resistance
。由于ASIC芯片相对普通计算机来说,算力强但访问内存性能差距不大,因此常用的方法为Memory Hard Mining Puzzle
,即增加对内存访问的需求。
- 那怎么设计呢? LiteCoin 莱特币
① LiteCoin(莱特币)
莱特币的puzzle
基于Scrypt
。Scrypt
为一个对内存性能要求较高的哈希函数,之前多用于计算机安全密码学领域。
-
算法基本思想
设置一个很大的数组,按照顺序填充伪随机数。(当然也不能是真随机数,不然无法验证) -
算法实现
Seed
为种子节点,通过Seed
进行一些运算获得第一个数,往后都是通过前一个位置的值取哈希得到。可以看到,这样的数组中取值存在前后依赖关系
-
解Puzzle的设计
按照伪随机顺序,从数组中读取一些数,每次读取位置与前一个数相关。例如:第一次,从A位置读取其中数据,根据A中数据计算获得下一次读取位置B;第二次,从B位置读取其中数据,根据B中数据计算获得下一次读取位置C;
- 为什么说它增加对内存访问的需求,实现对ASIC芯片不友好呢 ?
- 如果数组足够大,对于挖矿矿工来说,必须保存该数组以便查询,否则计算都要从头开始找到计算位置,这对于矿工来说,对内存的需求大幅度上升。
- 这算法有什么问题吗?
- 先来看看好的puzzle设计原则:
difficult to solve, but easy to verify
- 参考这个原则会发现,验证与求解puzzle所需的内存区域几乎是一样大的,也就是说,矿工解puzzle不友好的同时也对轻节点验证puzzle不友好
- 这就导致了在实际应用中,莱特币在设计内存大小是不敢设置太大(128K)。这样的设计并未起到预期作用,也就是说,128k对于
ASIC Resistance
来说过小了。
- 先来看看好的puzzle设计原则:
- 莱特币的优点
- 解决了莱特币“启动”问题,通过宣传这一设计目标,有效吸引了大批矿工参与,毕竟基于工作量证明(POW)的加密货币挖矿人太少是不安全的
此外,莱特币和比特币另一区别为出块时间,莱特币为2.5min,为比特币的1/4。除了这些不同外,这两种货币基本一样。
② 以太坊
以太坊也是采用 Memory Hard Mining Puzzle
,但具体设计上与莱特币不同。
- 不同在哪?
- 以太坊中,设计了两个数据集,一大一小。小的为16MB的
Cache
,大的数据集为1G的dataset
(DAG)。其关系为,1G的数据集是通过16MB数据集生成而来的。
- 以太坊中,设计了两个数据集,一大一小。小的为16MB的
- 为什么要设计成一大一小两个数据集?
- 便于验证,轻节点保存16MB的
Cache
进行验证就行 - 增加对内存访问的需求,矿工为了挖矿更快,减少重复计算则需要存储1GB大小的大数据集。
- 便于验证,轻节点保存16MB的
②① Cache和DAG(dataset)生成
- 以太坊的Cache设计
- Cache大小初始是16M且会定期增大,比莱特币的128k大很多
- 元素生成方式与莱特币一致,第一个元素通过种子计算得出,往后元素都是前一个元素的哈希值
- 以太坊的DAG设计
- DAG小初始是1G且也会定期增大
- DAG中每个元素都是从Cache中按照伪随机顺序读取一些元素,方法同莱特币中相同。
- DAG生成方式
- 如第一次读取A位置数据,对当前哈希值更新迭代算出下一次读取位置B,再进行哈希值更新迭代计算出C位置元素。
- 如此来回迭代读取256次,最终算出一个数作为DAG中第一个元素,如此类推,DAG中每个元素生成方式都依次类推。
补充
- 在2021年,以太坊完成了“合并”,其主网从PoW机制切换到了PoS机制。这意味着ETH的挖矿不再需要使用GPU,而是需要质押ETH来获得挖矿奖励。而ETHW(Ethereum PoW)仍然使用PoW机制进行挖矿,且它DAG大小会随着时间而不断增加。
- 本文发布时的DAG大小,查询网址
②② 求解Puzzle
求解由Cache
生成而来的DAG
,按照伪随机的顺序读取128个数,一开始根据block header
算出初始的哈希来映射到DAG
中某个位置(E),读取并进行运算出下一个要读取的位置(F)
与Cache
中读取的区别:需要读取相邻元素(E’、F’),这样循环64次,每次读2个元素。(元素虽然相邻,但毫不相干)
最后,计算出一个哈希值与挖矿难度目标阈值比较,若不符合就重新更换块头中Nonce,重复以上操作直到最终计算哈希值符合难度要求或当前区块已经被挖出
③ 以太坊挖矿算法(Ethash)
- 先看看这名字
- 我一开始是看成
ETH
ash
,这是指燃烧掉的ETH吗? - 后面看老师理解的是
ET
Hash
,噢噢,哈希算法
- 我一开始是看成
③① Ethash伪代码
# 通过seed计算cache
def mkcache(cache_size, seed):
o = [hash(seed)]
for i in range(1, cache_size)
o.append(hash(o[-1]))
return o
- 每隔30000个块会重新生成seed值(对原来的seed重新求哈希),并用新的seed生成新的cache;
- cache初始大小为16M,每隔30000个块重新生成时增加初始大小的1/128(128K)
# 通过cache生成第i个dataset(DAG)的值:
def calc_dataset_item(cache, i):
cache_size = cache.size
# 为了保证每隔初始的mix都不同,将i也参与了哈希计算
mix = hash(cache[i % cache_size] ^ i)
for j in range(256)
# 根据当前的mix值求得下一个要访问的cache元素的下标
cache_index = get_int_from_item(mix)
# 求新mix,由于初始的mix值都不同,所以访问cache序列也是不同的
mix = make_item(mix, cache[cache_index % cache_size])
return hash(mix)
- DAG初始大小为1G,也是每隔30000个块更新,同样增加初始大小的1/128(8M)
cache_index = get_int_from_item(mix)
这行代码表面,下一个用到的cache中的元素的位置是通过当前用到的cache的元素的值计算得到的,这样具体的访问顺序事先不可预知,满足伪随机性
# 计算出整个DAG
def calc_dataset(full_size, cache):
# 通过不断调用上面的calc_dataset_item函数来依次生成dataset中全部full_size个元素;
return [calc_dataset_item(cache, i) for i in range(full_size)]
# 全节点挖矿算法
def hashimoto_full(header, nonce, full_size, dataset):
mix = hash(header, nonce)
for i in range(64):
# 根据当前的mix值求出要访问的dataset的元素的下标
dataset_index = get_int_from_item(mix) % full_size
# 然后根据这个下标访问dataset中两个连续的值
mix = make_item(mix, dataset[dataset_index])
mix = make_item(mix, dataset[dataset_index + 1])
# 返回mix的哈希值,用来和target比较
return hash(mix)
# 轻节点验证算法
def hashimoto_light(header, nonce, full_size, cache):
mix = hash(header, nonce)
# 进行64次混合运算
for i in range(64):
dataset_index = get_int_from_item(mix) % full_size
mix = make_item(mix, calc_dataset_item(cache, dataset_index))
mix = make_item(mix, calc_dataset_item(cache, dataset_index + 1))
return hash(mix)
# 矿工挖矿主体循环
def mine(full_size, dataset, header, target):
nonce = random.randint(0, 2**64)
# 不断尝试nonce
while hashimoto_full(header, nonce, full_size, dataset) > target:
nonce = (nonce + 1) % 2**64
return nonce
③② 为什么说增加对内存访问的需求 ?
- 因为不保存效率很低
- 由于矿工需要验证非常多的
nonce
,如果每次都要从16M的cache
中重新生成 - 里面有大量的重复计算:随机选取的
dataset
的元素中有很多是重复的,可能是之前尝试
- 由于矿工需要验证非常多的
所以,矿工采取以空间换时间的策略,把整个dataset保存下来轻节点由于只验证一个nonce
,验证的时候就直接生成要用到的dataset
中的元素就行了。
③③ 与莱特币差不多的设计,为什么以太坊实现ASIC Resistance ?
-
内存的需求
- 莱特币是验证和矿工都是128k,这ASIC已经可以满足
- 以太坊是验证16M,矿工1G,且还会随出块变化
-
预期
- 以太坊早期就说要从工作量证明(POW)转向权益证明(POS),这对需要长时间研发的ASIC来说是血亏的,不停宣传使得逐利的ASIC不敢研发,不过2021年底确实已经装为POS
④ 预挖矿(Pre-Mining)与预售(Pre-Sale)
- 这个预挖矿中预是预留的意思,是给在以太坊上开发者预留的
- Pre-Sale指的是将预留出的货币出售掉用于后续开发,类似于拉风投或众筹。
⑤ 以太坊统计数据的查询网站
- Ether Distribution Overview 以太坊总供应量
挖矿再努力,关键还是起跑线
⑥ 其他观点
本篇中挖矿算法设计一直趋向于让大众参与,这一才是公平的。且由于参与人员的分散,算力分散,也进一步使得系统更安全。
也有人认为让普通计算机参与挖矿是不安全的,像比特币那样,让中心化矿池参与挖矿才是安全的。为什么呢?
因为要攻击系统,需要购入大量只能进行特定货币挖矿的矿机通过算力进行强行51%攻击,而攻击成功后,必然导致该币种的价值跳水,攻击者投入的硬件成本将会全部打水漂。而如果让通用计算机也参与挖矿,发动攻击成本便大幅度降低。因此认为,在挖矿上面,ASIC矿机“一统天下”才是最安全的方式。