全面掌握Solidity智能合约开发

2024年09月25日更新 796 人订阅
原价: ¥ 46 限时优惠
专栏简介 跟我学 Solidity :开发环境 跟我学 Solidity:关于变量 跟我学 Solidity : 变量的存储 跟我学 Solidity :引用变量 跟我学 Solidity :函数 跟我学 Solidity :合约的创建和继承 跟我学 Solidity :工厂模式 用Web3.js构建第一个Dapp 跟我学Solidity:事件 Solidity 中 immutable (不可变量)与constant(常量) [译] Solidity 0.6.x更新:继承 解析 Solidity 0.6 新引入的 try/catch 特性 探究新的 Solidity 0.8 版本 探索以太坊合约委托调用(DelegateCall) 停止使用Solidity的transfer() 使用工厂提高智能合约安全性 Solidity 怎样写出最节省Gas的智能合约[译] Solidity 优化 - 编写 O(1) 复杂度的可迭代映射 Solidity 优化 - 控制 gas 成本 Solidity 优化 - 减少智能合约的 gas 消耗的8种方法 Solidity 优化 - 如何维护排序列表 Solidity 优化:打包变量优化 gas 使用 Solidity 瞬态存储操作码 在 Solidity中使用值数组以降低 gas 消耗 Gas 优化:Solidity 中的使用动态值数组 计算Solidity 函数的Gas 消耗 Solidity 技巧:如何减少字节码大小及节省 gas 一些简单的 Gas 优化基础 "Stack Too Deep(堆栈太深)" 解决方案 智能合约Gas 优化的几个技术 合约实践:避免区块Gas限制导致问题 如何缩减合约以规避合约大小限制 Solidity 类特性 无需gas代币和ERC20-Permit还任重而道远 智能合约实现白名单的3个机制 Solidity智能合约安全:防止重入攻击的4种方法 Solidity 十大常见安全问题 [译]更好Solidity合约调试工具: console.log 智能合约开发的最佳实践 - 强烈推荐 全面理解智能合约升级 Solidity可升级代理模式: 透明代理与UUPS代理 使用OpenZeppelin编写可升级的智能合约 实战:调整NFT智能合约,减少70%的铸币Gas成本 Solidity 优化 - 隐藏的 Gas 成本 Gas 技巧:Solidity 中利用位图大幅节省Gas费 Solidity Gas 优化 - 理解不同变量 Gas 差异 关于Solidity 事件,我希望早一点了解到这些 Solidity 编码规范推荐标准 深入了解 Solidity bytes OpenZeppelin Contracts 5.0 版本发布 Solidity Gas优化:高效的智能合约策略 智能合约安全的新最低测试标准:Fuzz / Invariant Test 智能合约的白名单技术 模糊测试利器 - Echidna 简介 智能合约设计模式:代理 离线授权 NFT EIP-4494:ERC721 -Permit

跟我学 Solidity : 变量的存储

Solidity 数据存储如何工作?

欢迎阅读“跟我学习 Solidity ”系列中的另一篇文章。正如我在上一篇文章中保证过,我们将看到Solidity中数据存储的工作方式。

以太坊虚拟机(EVM)

在讨论Solidity中的数据存储之前,我想介绍一些有关以太坊虚拟机的知识,以使事情更清楚。

EVM的内部工作原理:

flow chart showing the Ethereum EVM environment

EVM上下文(来源:fullstacks.org)

当我们安装以太坊客户端时,它附带了EVM,EVM是专门创建来运行智能合约的轻量级操作系统。 EVM是基于堆栈计算机的模型的体系结构,这意味着该指令集被设计为用于堆栈而不是寄存器。EVM 操作码的列表在黄皮书中进行了描述,可以在“ 以太坊虚拟机(EVM)操作码和指令参考”中找到。

代码执行步骤如下:当交易触发智能合约代码执行时,将实例化EVM,并在EVM的ROM中加载要调用的合约的代码。程序计数器设置为零,从合约帐户的存储器中加载存储器,存储器全部设置为零,并且所有块和环境变量都已设置。然后代码被执行。

数据存储位置

现在让我们回到memory关键字,如Solidity 文档中所述。从版本0.5.0开始,所有复杂类型都必须给出一个明确的数据存储位置,并且有三个数据位置:memory(内存)storage(存储)calldata(调用数据)

注意:唯一可以省略数据位置的地方是状态变量,因为状态变量始终存储在帐户的存储空间中。

  1. storage(存储)
  • storage中的数据被永久存储。其以键值形式存储。
  • storage中的数据写在区块链中(因此它们会更改状态),这就是为什么使用存储非常昂贵的原因。
  • 占用256位插槽的 gas成本为20,000。
  • 修改storage的值将花费5,000 gas 。
  • 清理存储插槽时(即将非零字节设置为零),将退还一定量的 gas 。
  • 将数据保存在256位大小(32字节为一个字)的存储字段中。即使未完全占用每个插槽,也会产生成本。
  1. memory(内存)
  • memory是一个字节数组,其插槽大小为256位(32个字节)。数据仅在函数执行期间存储,执行完之后,将其删除。它们不会保存到区块链中。
  • 读或写一个字(256位)需要3 gas 。
  • 为了避免给矿工带来太多工作,在进行22次读写操作后,之后的读写成本开始上升。
  1. calldata调用数据
  • calldata是一个不可修改的,非持久性的区域,用于存储函数参数,并且其行为基本上类似于memory
  • 外部函数的参数需要calldata,但也可用于其他变量。
  • 它避免了复制,并确保了数据不能被修改。
  • 带有calldata数据位置的数组和结构体也可以从函数中返回,但是不可以为这种类型赋值。

数据位置和赋值行为

如果你不想发生意外的行为,那么了解数据位置赋值的工作方式非常重要。

在赋值之间应用以下规则:

  • storagememory之间(或来自calldata)的赋值总是创建一个独立的副本。
  • memorymemory的赋值仅创建引用。这意味着对一个内存变量的更改在引用相同数据的所有其他内存变量中同样有效。
  • storage到本地storage变量的赋值也仅赋值一个引用。
  • 所有其他到storage的赋值总是被复制。这种情况的示例是赋值给状态变量或storage结构体类型的局部变量成员,即使局部变量本身只是一个引用。

让我们使用Remix debugger进行更详细的研究

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.7.0;

contract DataLocationTest {

    uint[] stateVar = [1,4,5];

    function foo() public{
        // case 1 : 从存储中加载到内存
        uint[] memory y = stateVar; // 复制 stateVar 到 y

        // case 2 : from memory to storage
        y[0] = 12;
        y[1] = 20;
        y[2] = 24;

        stateVar = y; // copy the content of y to stateVar

        // case 3 : from storage to storage
        uint[] storage z = stateVar; // z is a pointer to stateVar

        z[0] = 38;
        z[1] = 89;
        z[2] = 72;
    }

}

创建一个新文件,复制上面的代码,然后部署合约。 现在尝试调用函数foo。你将在控制台中看到交易的详细信息,并在其旁边有一个调试按钮。点击它。

Solidity Remix 调试

你现在应该看到如下所示的调试器区域:

Solidity debugger area screen with the arrow that allows you to step over the code highlighted in red

要单步执行(Step over)代码,请单击我以红色框中的箭头。

storagememory的赋值

你应该首先注意到,正如我们在EVM部分中提到的, 状态(Solidity State)部分加载了storage 的stateVar的内容,当然没有局部变量。

当你单步执行时,应该看到变量y出现在局部变量(Solidity locals)部分中。继续执行(step over),你会注意到,为了分配必要的内存空间,需要使用很多字节码

并从storage中加载每个字,然后将其复制到memory中。这意味着要支付更多的 gas ,因此从storagememory的赋值非常昂贵。

memorystorage的赋值

让我们研究第二种情况:从memorystorage的赋值。

当你修改完存储在memory中的副本并且想要将更改保存回storage时,可以使用它。它同样消耗大量的 gas 。如果我们用调试器步骤详细信息中的剩余 gas (remaining gas)来计算 gas 差值,则为17,083 gas 。该操作使用了四个SSTORE操作码:第一个用于存储数组大小(保持不变,消耗800个 gas ),另外三个用于更新数组的值(每个消耗了5,000个 gas )。

storagestorage的赋值

现在,让我们看一下情况三:从storagestorage的赋值。这次将创建一个新的局部变量,并包含与stateVar相同的内容。如果我们查看代码的执行过程,会注意到Solidity所做的,将包含数组长度的存储的第一个插槽地址入栈。根据文档,对于动态数组,插槽位置包含数组长度,用于计算包含数组数据的插槽位置。

来比较两者的 gas 成本:

第一种是将数据复制到memory,然后更新并复制回 storage, 使用21,629 gas ,

第二种是直接创建引用并更新状态, 使用 5,085 gas 。

那么很明显第二种方法是便宜得多。

但是,如果我们像这样直接更新状态变量呢,像这样:

stateVar[0] = 12;

也有可能但是,如果你要处理映射和嵌套数据类型(我们将在后面看到),则使用storage指针可以让代码更具可读性。

为了使本文简短,而不会给你太多信息,我决定在下一篇文章继续介绍复杂的变量。我希望本文对你有所帮助,并且像往常一样,如果你想了解更多信息,请继续关注即将发布的文章。

参考文献

  1. Storage vs. Memory vs. Stack in Solidity & Ethereum: https://dlt-repo.net/storage-vs-memory-vs-stack-in-solidity-ethereum/
  2. The Ethereum Virtual Machine: https://fullstacks.org/materials/ethereumbook/14_evm.html
  3. ETHEREUM VIRTUAL MACHINE: https://ethereum.org/en/developers/docs/evm/
  4. gas: https://ethereum.org/en/developers/docs/gas/

    本翻译由 Cell Network 赞助支持。

点赞 9
收藏 7
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

5 条评论

请先 登录 后评论