本文主要介绍以太坊 gas 相关的机制,并尝试解释一下为什么这样设计。最后会总结一下节省 gas 的相关实践。
以太坊里的 gas 跟它的名字是一个意思,gas就是驱动以太坊的燃料。gas 并不是以太币,它是一个独立的虚拟货币,跟以太币之间存在汇率关系。
这可能跟我们的经验相冲突,毕竟我们在使用钱包的时候,钱包一般给我们的提示的 gas 费都是以以太币来计算的。这其实是钱包给我们做了一些计算后简化的结果。在发起交易的时候有两个参数跟 gas 有关。一个是 gas limit,表示交易发起方愿意为这个交易支付的最大 gas 数量。一个是 gas price,表示交易发起方愿意为每单位 gas 支付的以太币数量,计量单位是 wei/gas(wei 是以太币的最小单位)。钱包会根据最近几个区块中的 gas price 的平均值,并结合发起的交易的复杂度估算出的 gas 消耗数量,最终给出一个建议的以以太币计价的 gas 提示。
以太坊交易费用的基本机制如下
2021年8月由于伦敦升级,略微改变了 gas 计算方式和运作方式。从伦敦升级开始,每一个区块都设置了基本的价格费用,这是将你的交易包含在区块内的最低价格,由网络根据对区块空间的需求而计算,这些费用将被烧毁。在基本费之外,还引入了小费的概念,基本上设置更高的消费,你能获得更高的优先级。
升级后 gas 计算公式为:
gas fees = gas_limit * (base price + priority price)
这个网站可以看到当前 gas 相关的信息。
我们都知道如果一个系统或者编程语言能够解决你交给它的所有问题,它就是图灵完备的。这也意味着:有些问题需要无限的资源去解决。这里的关键就是:我们无法仅通过观察一个计算机程序来确定它是否需要无限的资源去执行。我们不得不去实际执行这个程序,然后等待它的结束来确定这个问题。当然,如果它需要花费无限的资源去执行,我们也只能无限地等待下去才能知道结果。这就是所谓的“停机问题”。
如果以太坊里不能解决这个问题,这就会成为一个巨大的麻烦。以太坊就像是一个单线程的计算机,没有调度程序,所以它会被无限循环卡住,而这会使它变得不可用。而通过 gas 这个问题就得到了解决:如果在一个预先指定的最大计算量被用尽的时候计算还没有结束,那么所有处理都会无条件地停止。这就使EVM成为一个准图灵完备的机器:它可以解决你交给它的所有问题,但前提是这个问题可以在一定量的计算量内被解决。
在以太坊里,这个限制并不是固定的,你可以通过支付费用来将它提高到最大值(也就是所谓的“区块 gas 限制”)并且所有人可以同意随时间的推移提高这个最大值。尽管如此,在某个时间,必然会有一个限制,当交易花费太多 gas 执行时它就会被中止。
要想了解 gas 的计算就不得不先了解一下以太坊虚拟机 EVM。
EVM 是以太坊协议的一部分,它用来处理智能合约的部署和执行。事实上,除了在 EOA(由用户私钥控制的所谓“外部账户”)之间的简单转账交易以外,其他所有涉及状态更新的操作都是通过 EVM 来计算的。从高层抽象的角度,运行在以太坊区块链上的 EVM 可以被想象成一个包含了数百万可执行对象的全球化的去中心化计算机,这些可执行对象都拥有它们各自的永久数据存储。而 Solidity 这样的高级智能合约开发语言开发出来的智能合约,最终会被编译为由 EVM 执行的字节码指令集。最后交由 EVM 进行执行。
具体来说就是以太坊区块链上其实存储的都是一些确定的信息,这些信息共同构成了以太坊世界状态。世界状态是一个以太坊地址到账户数据的映射。每个以太坊地址所对应的账户数据都由以下几部分组成:
当一个交易最终反映为一次智能合约代码的执行时,一个EVM实例会基于当前正在创建的区块信息和当前这个交易的信息被初始化出来。
首先将调用的合约账户所对应的智能合约代码编译后产生的字节码加载到 EVM 的 ROM 中,程序计数器置为0,从调用的合约账户所对应的存储中加载存储数据,将内存清零并将与区块和其他环境变量相关的信息设置好。这个执行中的关键变量就是提供给这次执行的 gas,这个 gas 被设定为原始交易开始时由交易发送者支付的 gas 总量。
随后开始按字节码进行执行。在执行的过程中,gas 会随着操作执行相应减少,每个字节码消耗的gas都不尽相同。只要gas的供给减小到0,我们就会得到一个“out of gas”(OOG)异常;执行会立即停止,相应的交易也即失败。反之如果执行成功结束,那么真正的世界状态就会被更改。包含所有对调用过的合约的存储数据的更改、新创建的合约以及其间所有以太币余额的转移。
上面的链接是全部字节码及对应的 gas 开销。
通过上面我们可以知道要想节省 gas 就要减少操作的数量。其实我们还可以观察一下各个操作对应的 gas 消耗都是不一样的,其中消耗 gas 最多的一个字节码是 SSTORE。
0x55 SSTORE Save word to storage - 20000**
SSTORE 作用是存储数据到链上。它要消耗 20000 个单位的 gas。在一众个位数,百位数的 gas 消耗里面一骑绝尘。
因此要减少 gas 消耗就有两个方向,一个是尽量减少操作的数量,另外一个是尽量减少链上数据的存储。因此可以进行以下几个优化。
进行设计的时候,首先要选好哪些数据是必须存在链上的,哪些数据是可以存在链下的。链上的数据越少 gas 消耗就越小。比如 NFT 项目中的元数据,如果单纯存在链上会造成很大的 gas 开销。所以目前大部分的 NFT 的元数据都是存放在链下或者其他的去中心化存储平台如 IPFS,AR 等等。
在开发中编译器编译 Solidity 代码的时候默认是没有开启编译优化的,这样可以提高编译速度。但是到了真正开始部署的使用一定要指定一个优化标志来告诉 Solidity 编译器生成高度优化的字节码。这样生成的字节码将消耗更少的 gas。
使用汇编语言可以编写非常接近操作码级别的代码。当然这么低级别的代码写起来很难,但好处是可以手动优化操作码,在某些情况下优于 Solidity 字节码。
Yul 是一种中间编程语言,可编译为字节码,以满足不同编译器后端的需求。Solidity 编译器有一个使用 Yul 作为中间语言的实验性实现。
Yul 提供高级构造,例如循环、函数调用以及 if 和 switch 语句,可读性比较好。目前有一些项目是 Yul 语言编写的,宣称可以节省30%左右的 gas。
struct Normal {
uint a;
uint b;
uint c;
}
struct Mini {
uint32 a;
uint b;
uint32 c;
}
// 占据的空间最小
struct Minist {
uint32 a;
uint32 b;
uint c;
}
上面简单介绍了以太坊的 gas机制,及如何降低 gas 的方法。可以看出以太坊的实现真的是很精妙的一个设计。
当然降低 gas 消耗的方法肯定不止以上几种,后续随着学习和实践的深入在来添加吧。也希望知道的同学不吝赐教!