以太坊君士坦丁堡升级深入分析

以太坊即将在7080000高度升级到Constantinople(君士坦丁堡)版本,预计时间为1月16号。这是以太坊第三阶段Metropolis(大都会)的第2个版本,也是最后一个版本。在这之后,以太坊将步入最终阶段Serenity(宁静)。

这次升级一共涉及5个EIP,改动不算大,网络上也已经有很多文章,但大多是一些语焉不详的简短翻译,笔者试图深入分析一下各项改动的具体细节。

EIP 145: 增加SHL/SHR移位指令

注意这只是EVM层面的优化,和solidity没有关系,目的是减少移位操作消耗的gas(从35减小到3)

比如我们执行一个移位操作:

    function hello() public {
        int x = 1;
        int y = x << 3;
    }

以前会被编译成很多条指令:
以太坊君士坦丁堡升级深入分析
升级完成后,只需要1条指令就搞定了~

EIP 1014:增加CREATE2指令,支持“Counterfactual Instantiation”

这项提案是V神亲自敲定的,不过估计真正能看懂的人少之又少。这个改动不是给普通智能合约用的,是为了支持状态通道

首先简单介绍一下状态通道:状态通道是指把一部分区块链状态“锁定”到一个多签名的合约中,由所有参与者共同控制。这种“状态锁定”称之为“state deposit”,比方说锁定一定的ETH或者ERC20代币,甚至是锁定一只以太猫。

然后通道中的所有人就可以进行链下交易了,由于交易数据不上链,因此就不需要消耗gas。但是,每笔交易必须由交易双方或者多方共同签名,这被称为“unanimous consent”。这样,虽然在当前时刻该笔交易没有上链,但是在将来的任意时刻,交易的任何一方都有权把这笔交易提交到链上,并通过验证。因此,和链上交易需要等待多个区块确认不同,链下交易可以被“即时确认”,这一特性被称之为“instant finality”。

有了这些铺垫,现在就可以引入一个新的概念:Counterfactual,翻译过来叫“反现实”,是不是完全摸不着头脑?实际上,如果我们说一个事件X是“counterfactual”的,代表了下面3层含义:

  • X可以在链上发生,但是目前还没有
  • 任何一方都可以单方面导致X在链上发生
  • 因此,参与者们可以假装认为X已经在链上发生了

举个例子:Alice在状态通道中给Bob发送了4 ETH,他们俩都对该笔交易签名。这样,这笔交易可以在任意时刻被Alice或者Bob提交到链上,但是目前它还没有上链。我们就说在“counterfactual”的语境下,Alice给了Bob 4个ETH。

讲了这么多,跟EIP 1014有什么关系呢?实际上,在状态通道里不仅可以交易,还可以部署合约!比如你开发了一个扑克或者下棋的游戏,你可以不用先部署到链上,而是部署到状态通道里,这样你连1分钱都不用花!这就叫做Counterfactual Instantiation,反现实实例化。那么这是怎么做到的呢?状态通道的所有参与者需要向多签名合约发送“确认”:如果将来这个游戏合约部署到链上了,可以按照通道里的合约状态,从“锁定”资金中转出相应的额度到该链上合约中。

那么现在就变成“鸡生蛋蛋生鸡”的问题了:合约还没有部署到链上,怎么知道它的地址呢?

之前以太坊的合约地址是这么生成的:

keccak256(RLP(sender_address, nonce))[12:]

账户的nonce是随着交易数递增的,因此合约地址取决于该账户何时部署合约。我们需要一种不依赖nonce的确定性的算法来生成合约地址,因此V神提出了EIP 1014:

keccak256( 0xff ++ sender_address ++ salt ++ keccak256(init_code)))[12:]

其中init_code就是用于创建合约实例的字节码,salt是一个32字节的值,作为一个随机因子(比如你想部署多个形同的合约)。这样,我们就可以预先知道合约的链上地址并在状态通道中与之交互了。

EIP 1052:增加EXTCODEHASH指令

这个改动目的比较明确:减少校验合约代码的gas消耗

很多合约需要验证其他合约的字节码,确认其代码是否安全、合规。之前的做法是先通过EXTCODECOPY把目标合约的字节码拷贝一份,然后再计算、比对哈希值。EXTCODECOPY会消耗很多的gas,实际上我们并不需要目标合约的字节码,只需要字节码的哈希,EIP 1052就是为了满足这个需求诞生的。

EIP 1283:优化SSTORE指令的gas消耗计算

大家都知道,SSTORE是一个比较昂贵的指令,因为它会修改StateDB(存储)。

之前的gas消耗计算简单粗暴:每调用一次SSTORE,就扣一次gas。比如在同一笔交易中,如果我先执行x=1,再执行x=2,最后执行x=3,会被收3次钱!这显然是不合理的,EIP 1283就是为了优化gas的计算方式。

以太坊底层使用的LevelDB是K-V型数据库,我们把每个键值对称为一个“存储槽”(Storage Slot)。每个存储槽可能有三种值:

  • 原始值:执行当前交易之前的初始值
  • 当前值:执行SSTORE指令之前的值
  • 新值:执行SSTORE指令之后的值

因为可能会执行多条SSTORE指令,所以当前值不一定等于原始值。

SSTORE的gas消耗可以分为3种情况:

  • No-op:当前值等于新值,EVM不需要做任何事情
  • Fresh:当前值等于原始值,而新值不等于当前值
  • Dirty:当前值不等于原始值,而新值也不等于当前值

No-op的情况比较简单,固定收取200 gas。一般情况下我们会从Fresh状态开始,然后进入Dirty,当然如果设回原来的值,又会重新回到Fresh状态,此时会触发refund(退费)。具体状态机参见下图:
以太坊君士坦丁堡升级深入分析
详细解释:

  • No-op状态:收取200 gas
  • Fresh状态:
    • 如果原始值是0,收取20000 gas
    • 否则,收取5000 gas。如果新值是0,退还15000 gas
  • Dirty状态:收取200 gas,并检查下面2个条件:
    • 如果原始值不是0
      • 如果当前值是0(说明新值不是0),收回退还的15000 gas
      • 如果新值是0(说明当前值不是0),退还15000 gas
    • 如果原始值等于新值(被reset回原始值了)
      • 如果原始值是0,退还19800 gas
      • 否则,退还4800 gas

所以退费会记录到一个refund计数器中,并确保退费金额不会减成负数。

总结一下,EIP 1283修正了SSTORE指令重复计费的问题,新的计费机制更加合理,对dApp开发者是一个利好。

EIP 1234:延迟难度炸弹,调整出块奖励

以太坊创立之初,就明确提出最终会使用PoS作为共识算法,PoW只是暂时地过渡。为了保证平稳过渡,代码中设置了一个“难度炸弹”,也就是挖矿难度会随着出块数量的增加而逐渐加大,最终进入“冰河世纪”,也就是矿工们无法挖出区块,或者挖矿成本远远高于收益,导致矿工们放弃PoW,转向PoS。

然而现实总是不会尽如人意,以太坊Casper开发进度缓慢,不得不一再延期。EIP 1234会把难度炸弹延迟12个月,从代码实现层面来看,就是在计算难度时采用一个fake的区块高度,往前推500万个区块:

fake_block_number = max(0, block.number - 5_000_000) if block.number >= CNSTNTNPL_FORK_BLKNUM else block.number

另外,出块奖励也从3 ETH减少到2 ETH,叔块奖励也相应减少。此举也是为了刺激矿工从PoW向PoS过渡。

更多文章欢迎关注“鑫鑫点灯”专栏:https://blog.csdn.net/turkeycock
或关注飞久微信公众号:
以太坊君士坦丁堡升级深入分析