# Solidity Gas优化的奇技淫巧 **Published by:** [Mobius](https://paragraph.com/@0xmobius/) **Published on:** 2021-12-15 **URL:** https://paragraph.com/@0xmobius/solidity-gas ## Content 以太坊上存储256 bit数据大约消耗20k Gas、如此换算,仅1 GB存储资源要花费32,000ETH,按2021年12月14日ETH价格,大约要花费超过1亿美元。且不说当前身为贵族链Gas费很有可能继续水涨船高,放在早些年其Gas消耗也不是一笔小数目。因此,以太坊Gas优化是Dapp开发一直难绕的问题,也是Solidity开发者From Zero To Senior的必修之技。本文从我最近半年的以太坊项目开发的实战经验出发,从2个维度归纳了Gas优化的trick。一、数据类型优化多数时候,定义大小为256bit的变量(以便填满整个slot)是gas消耗最优的选择。EVM一个slot 256bit,当数据未填满slot,则需额外操作将剩余slot填为0。此外,数值类型也会先转化为uint256再进行计算。 (1)某些时候,将struct中连续定义的变量打包在一个slot中也可以减少gas消耗。需注意的是,如果不是对这些变量同时进行读写操作,那么也难以实现gas优化的效果。 Coding Examples:struct定义、赋值在struct内,需将存储在同一slot内的变量进行连续赋值。如此,编译器便会将小于等于256 bits的变量堆并赋值。需要注意的是,该优化只有在SOLC optimize模式开启,与此同时,function内的局部变量不超过16个才有效。struct Data { uint128 a; uint128 b; uint256 c; } Data public data; constructor(uint128 a_, uint128 b_, uint256 c_) public { Data.a = a_; Data.b = b_; Data.c = c_; } 内联汇编赋值、读取更简单粗暴的方式是直接用汇编语言将变量堆并。如此极限操作若非对gas锱铢必较的场景,大可不必使用,毕竟汇编语言大大降低了代码的可读性。// 将变量堆并写入,类似编码过程 function encode(uint64 a_, uint64 b_, uint128 c_) public returns (bytes32 x) { assembly { // 从0x20开始的32 bytes内存储依次存入c_,b_,a_;但内存空间中位置顺序依此为a_,b_,c_ mstore(0x20, c_) mstore(0x10, b_) mstore(0x8, a_) x := mload(0x20) } } // 将变量读取,类似解码过程 function decode(bytes32 x_) public returns (uint64 a, uint64 b, uint128 c) { assembly { c := x_ mstore(0x18, x_) a := mload(0) mstore(0x10, x_) b := mload(0) } } (2)定义常量数据时添加constant关键字。带有constant关键字的变量在合约部署时,被存为合约的bytecode,不用占用storage的slot,同时也减免了SLOAD变量所需的200 gas消耗。需要注意的是,若将constant变量赋值为timestamp类型,则会在读取时根据运行的context动态变化。 Code Examples:timestamp变量uint256 public constant PRESALE_START_DATE = now; uint256 public constant PRESALE_END_DATE = PRESALE_START_DATE + 15 minutes; uint256 public constant OWNER_CLAWBACK_DATE = PRESALE_START_DATE + 20 minutes; 以上三个变量存储的不是固定的timestamp,而是在now基础上的动态timestamp。二、数据压缩使用Merkle树减少存储加载Merkle树在区块链的应用十分广泛,在此就不赘述。由于Merkle根信息包含了压缩之后的全局信息,因此,只需在合约中存储Merkle根信息即可完成信息的验证。 Code Examples:bytes32 public merkleRoot; function check( bytes32 hash4, bytes32 hash12, uint256 a, uint32 b, bytes32 c, string d, string e, bool f, uint256 g, uint256 h ) public view returns (bool success) { bytes32 hash3 = keccak256(abi.encodePacked(a, b, c, d, e, f, g, h)); bytes32 hash34 = keccak256(abi.encodePacked(hash3, hash4)); require(keccak256(abi.encodePacked(hash12, hash34)) == merkleRoot, "Wrong Element"); return true; } 需注意,当特定变量被频繁访问或更新时,将其直接存储在合约中是更直接高效的方式。此外,根部分支变量太多可能会超出一个transaction允许最大的slot占用量。 2. 无状态合约 将数据存在链上的方式不仅限于在合约中加入storage变量,通过transaction input和event calls也能将数据完整存于链上。因此,通过transaction input和event calls替代storage变量的读写也是gas优化的一条蹊径。 Code Examples: 下面的案例通过无状态合约定义function的input,作为transaction存储信息的接口;通过后端Filter将transaction记录中的input信息捕获。无状态合约// 存储状态的合约 contract DataStore { mapping(address => mapping(bytes32 => string)) public store; event Save(address indexed from, bytes32 indexed key, string value); function save(bytes32 key, string value) { store[msg.sender][key] = value; Save(msg.sender, key, value); } } // 无状态合约 contract DataStore { function save(bytes32 key, string value) {} } Filter# 安装transaction解析函数:InputDataDecoder npm install ethereum-input-data-decoder // 定义transaction input解码函数 const decoder = new InputDataDecoder(abi); const decodeInput = input => decoder.decodeData(input); // 定义input数据处理函数 const processArgs = input => input.inputs.map((arg, i) => { const type = input.types[i]; if (type === "string") { return arg; } if (type === "bytes32") { const toHex = `0x${arg.toString("hex")}`; return web3.toUtf8(toHex); } return arg; }); // 后端存储的接口abi信息 const abi = [ { constant: false, inputs: [ { name: "key", type: "bytes32" }, { name: "value", type: "string" } ], name: "save", outputs: [], payable: false, type: "function" } ]; // transaction input信息捕获 const run = async () => { const tx = "0xc9fdf51d..."; const transaction = await web3.eth.getTransaction(tx); const input = decodeInput(transaction.input); if (input.name === "save") { const args = processArgs(input); const address = transaction.from; const key = args[0]; const value = args[1]; // save the address / key / value to a database ... } }; 上述方法的缺点在于,其他合约无法访问这些通过transaction input的数据;为了更快定位transaction,需要在function中添加空event用于标记。 3. 在IPFS等去中心化存储网络中存储数据,然后将指向源数据的hash上链。 从技术细节的角度看,无论是数据类型优化,还是数据压缩,都是根据以太坊的存储设计、编译原理以及EVM特性,有的放矢地做了一些优化。从更宏观的视角看,伴随以太坊往PoS转型、Layer2的技术发展,从可扩展性的层面做数据存储容量扩展才是大势所趋,因此,完成以太坊Gas优化和安全漏洞总结后,会多写写这些方面的内容。Reference:https://ethereum.stackexchange.com/questions/872/what-is-the-cost-to-store-1kb-10kb-100kb-worth-of-data-into-the-ethereum-block https://medium.com/coinmonks/8-ways-of-reducing-the-gas-consumption-of-your-smart-contracts-9a506b339c0a https://kubertu.com/blog/solidity-storage-in-depth/ https://dev.to/javier123454321/solidity-gas-optimizations-pt-3-packing-structs-23f4 https://ethereum.stackexchange.com/questions/7949/why-do-constant-state-variables-get-initialised-every-time ## Publication Information - [Mobius](https://paragraph.com/@0xmobius/): Publication homepage - [All Posts](https://paragraph.com/@0xmobius/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@0xmobius): Subscribe to updates - [Twitter](https://twitter.com/___Mobius___): Follow on Twitter