Transient Storage

发布于:2025-07-21 ⋅ 阅读:(19) ⋅ 点赞:(0)

本节是《Solidity by Example》的中文翻译与深入讲解,专为零基础或刚接触区块链开发的小白朋友打造。我们将通过“示例 + 解说 + 提示”的方式,带你逐步理解每一段 Solidity 代码的实际用途与背后的逻辑。

Solidity 是以太坊等智能合约平台使用的主要编程语言,就像写网页要用 HTML 和 JavaScript,写智能合约就需要会 Solidity。

如果你从没写过区块链代码也没关系,只要你了解一点点编程概念,比如“变量”“函数”“条件判断”,我们就能从最简单的例子开始,一步步建立你的 Solidity 编程思维。

Transient Storage

瞬态存储
瞬态存储中的数据在交易结束后会被清除。

  • 什么是瞬态存储(Transient Storage)?

    瞬态存储(Transient Storage)是 Solidity 在以太坊 Cancun 升级(EIP-1153)中引入的一种新数据存储位置,介于storage(永久存储)和memory(函数级临时存储)之间。

    • 特点:
      • 数据存储在交易(transaction)期间,交易结束后自动清除。
      • 使用 tstore(存储)和 tload(读取)操作,通过内联汇编(assembly)访问。
      • storage 操作(如 sstore/sload)更节省 Gas,因为数据不持久化到区块链。
    • 比喻:
      • 瞬态存储像会议室的临时白板,会议(交易)结束后自动擦除。
      • 相比之下,storage 像刻在石碑上的记录(永久且昂贵),memory 像便签纸(函数结束即丢弃)。
  • 用途:

    • 临时状态:适合需要跨函数调用但仅在交易内有效的状态(如锁状态)。
    • 重入保护:在重入保护(reentrancy guard)中替代 storage,降低 Gas 成本。
    • 优化 Gas:瞬态存储的操作(tstore/tload)比 storage 操作便宜,适合高频临时数据操作。
// SPDX-License-Identifier: MIT
// 使用 MIT 许可证,允许自由使用、修改和分发代码。

pragma solidity ^0.8.26;
// 指定 Solidity 编译器版本,必须为 0.8.26 或更高(但低于 0.9.0)。

// Make sure EVM version and VM set to Cancun
// 确保 EVM 版本和虚拟机设置为 Cancun(支持 EIP-1153 的瞬态存储)。

// Storage - data is stored on the blockchain
// Memory - data is cleared out after a function call
// Transient storage - data is cleared out after a transaction
// 存储(Storage) - 数据存储在区块链上,永久存在。
// 内存(Memory) - 数据在函数调用后清除。
// 瞬态存储(Transient Storage) - 数据在交易结束后清除。

interface ITest {
    function val() external view returns (uint256);
    function test() external;
}
// 定义一个接口 ITest,包含两个函数:
// - val():视图函数,返回 uint256 类型的值(读取状态,免费)。
// - test():外部函数,执行测试逻辑(可能修改状态,消耗 Gas)。
// 接口用于跨合约调用,测试存储行为。

contract Callback {
    uint256 public val;
    // 声明一个公共的状态变量 val,存储在 storage(区块链上),记录测试值。
    // public 自动生成 getter 函数(val()),消耗 Gas 修改。

    fallback() external {
        val = ITest(msg.sender).val();
        // 定义一个 fallback 函数,在接收未定义函数调用时触发。
        // 调用调用者的 val() 函数(通过 ITest 接口),获取其状态并存储到本合约的 val。
        // msg.sender 是触发调用的合约地址(如 TestStorage 或 TestTransientStorage)。
    }

    function test(address target) external {
        ITest(target).test();
        // 定义一个公共函数 test,接受目标合约地址 target。
        // 通过 ITest 接口调用目标合约的 test() 函数,触发目标合约逻辑。
        // 若目标合约调用本合约的 fallback,val 会被更新。
    }
}

contract TestStorage {
    uint256 public val;
    // 声明一个公共的状态变量 val,存储在 storage(区块链上),记录测试值。

    function test() public {
        val = 123;
        // 将 storage 变量 val 设置为 123,修改区块链状态,消耗 Gas(约 20,000+)。
        bytes memory b = "";
        // 创建一个空的 memory 字节数组 b,临时存储,不消耗 Gas。
        msg.sender.call(b);
        // 调用 msg.sender(调用者,通常是 Callback 合约)的 fallback 函数,传递空字节数组 b。
        // 触发 Callback 的 fallback,Callback 会读取本合约的 val(123)。
    }
}

contract TestTransientStorage {
    bytes32 constant SLOT = 0;
    // 定义一个常量 SLOT(值为 0),表示瞬态存储的槽位。
    // 瞬态存储使用固定槽位(bytes32 类型)来存储数据。

    function test() public {
        assembly {
            tstore(SLOT, 321)
            // 使用内联汇编的 tstore 操作,将值 321 存储到瞬态存储的 SLOT(槽位 0)。
            // 瞬态存储仅在交易期间有效,交易结束后清除。
            // tstore 比 sstore(storage 操作)更节省 Gas。
        }
        bytes memory b = "";
        // 创建一个空的 memory 字节数组 b,临时存储,不消耗 Gas。
        msg.sender.call(b);
        // 调用 msg.sender(调用者,通常是 Callback 合约)的 fallback 函数,传递空字节数组 b。
        // 触发 Callback 的 fallback,Callback 会读取本合约的 val(瞬态存储中的 321)。
    }

    function val() public view returns (uint256 v) {
        assembly {
            v := tload(SLOT)
            // 使用内联汇编的 tload 操作,读取瞬态存储 SLOT(槽位 0)的值。
            // 返回瞬态存储中的值(交易内为 321,交易结束后为 0)。
        }
    }
}

contract MaliciousCallback {
    uint256 public count = 0;
    // 声明一个公共的状态变量 count,存储在 storage,记录重入次数。

    fallback() external {
        count++;
        // 在 fallback 函数中增加 count,表示被调用一次。
        ITest(msg.sender).test();
        // 尝试重新调用调用者的 test() 函数,模拟重入攻击。
    }

    function attack(address _target) external {
        // 定义一个公共函数 attack,接受目标合约地址 _target。
        ITest(_target).test();
        // 调用目标合约的 test() 函数,触发重入攻击。
        // 若目标合约未防止重入,MaliciousCallback 的 fallback 会反复调用 test()。
    }
}

contract ReentrancyGuard {
    bool private locked;
    // 声明一个私有状态变量 locked,存储在 storage,表示锁状态(true 表示锁定,false 表示未锁定)。
    // private 防止外部直接访问。

    modifier lock() {
        require(!locked);
        // 检查 locked 是否为 false,若为 true 则抛出错误,防止重入。
        locked = true;
        // 设置 locked 为 true,锁定合约。
        _;
        // 执行函数主体。
        locked = false;
        // 函数结束后,设置 locked 为 false,释放锁。
    }

    // 27587 gas
    function test() public lock {
        // 定义一个公共函数 test,使用 lock 修饰符防止重入。
        // 注释表明此函数消耗约 27,587 Gas(因 storage 操作)。
        bytes memory b = "";
        // 创建一个空的 memory 字节数组 b,临时存储。
        msg.sender.call(b);
        // 调用 msg.sender 的 fallback 函数,传递空字节数组 b。
        // 若调用者是 MaliciousCallback,lock 修饰符会防止重入。
    }
}

contract ReentrancyGuardTransient {
    bytes32 constant SLOT = 0;
    // 定义一个常量 SLOT(值为 0),表示瞬态存储的槽位。

    modifier lock() {
        assembly {
            if tload(SLOT) { revert(0, 0) }
            // 检查瞬态存储 SLOT 的值,若非 0(已锁定),抛出错误,防止重入。
            tstore(SLOT, 1)
            // 设置瞬态存储 SLOT 的值为 1,表示锁定。
        }
        _;
        // 执行函数主体。
        assembly {
            tstore(SLOT, 0)
            // 函数结束后,设置瞬态存储 SLOT 为 0,释放锁(交易结束会自动清除)。
        }
    }

    // 4909 gas
    function test() external lock {
        // 定义一个外部函数 test,使用 lock 修饰符防止重入。
        // 注释表明此函数消耗约 4,909 Gas(因瞬态存储操作比 storage 便宜)。
        bytes memory b = "";
        // 创建一个空的 memory 字节数组 b,临时存储。
        msg.sender.call(b);
        // 调用 msg.sender 的 fallback 函数,传递空字节数组 b。
        // 若调用者是 MaliciousCallback,lock 修饰符会防止重入。
    }
}

代码整体说明

代码包含多个合约,展示瞬态存储(transient storage)与传统存储(storage)的对比,以及在重入保护中的应用:

  1. 接口 ITest:定义测试函数签名,用于跨合约调用。
  2. 合约 Callback:测试普通存储和瞬态存储的差异,通过 fallback 捕获调用者的状态。
  3. 合约 TestStorage:使用普通存储(storage)保存数据,展示持久化行为。
  4. 合约 TestTransientStorage:使用瞬态存储(tstore/tload)保存数据,展示交易后清除行为。
  5. 合约 MaliciousCallback:模拟重入攻击,测试重入保护。
  6. 合约 ReentrancyGuard:使用 storage 实现重入保护,展示高 Gas 成本。
  7. 合约 ReentrancyGuardTransient:使用瞬态存储实现重入保护,展示低 Gas 成本。

瞬态存储的本质

  • 瞬态存储(transient storage)是一种在交易期间存储数据的机制,交易结束后数据自动清除。
  • 比喻:
    • 瞬态存储像会议室的临时白板,会议(交易)结束后自动擦除,记录成本低。
    • Storage 像刻在石碑上的记录,永久但昂贵(高 Gas)。
    • Memory 像便签纸,函数结束即丢弃,免费但无法跨函数调用。
  • 核心特点:
    • 生命周期:仅在交易内有效,结束后清除(无需手动清理)。
    • 操作方式:通过内联汇编的 tstore(存储)和 tload(读取)访问,使用槽位(bytes32)存储数据。
    • 低 Gas 成本tstoretloadsstoresloadstorage 操作)便宜,适合临时状态。
  • 优势:
    • 节省 Gas:瞬态存储操作成本低(如 ReentrancyGuardTransient 的 4,909 Gas 对比 ReentrancyGuard 的 27,587 Gas)。
    • 自动清理:交易结束后数据自动清除,无需手动重置。
    • 跨函数调用:数据可在交易内的多次调用中共享(不像 memory 局限于单函数)。

代码功能

  • 接口 ITest
    • 定义 val()test() 函数,用于跨合约调用,测试存储行为。
  • 合约 Callback
    • 接收目标合约的 val() 返回值,存储到 storage 变量 val,展示持久化存储。
  • 合约 TestStorage
    • 使用 storage 变量 val 存储 123,调用 Callbackfallback,持久保存数据。
  • 合约 TestTransientStorage
    • 使用瞬态存储(tstore/tload)存储 321,调用 Callbackfallback,数据在交易结束后清除。
  • 合约 MaliciousCallback
    • 模拟重入攻击,通过 fallback 反复调用目标合约的 test()
  • 合约 ReentrancyGuard
    • 使用 storage 变量 locked 实现重入保护,消耗高 Gas(27,587)。
  • 合约 ReentrancyGuardTransient
    • 使用瞬态存储(槽位 0)实现重入保护,消耗低 Gas(4,909)。

瞬态存储的注意事项

  • 生命周期:
    • 瞬态存储仅在交易内有效,交易结束后自动清零。
    • 不适合持久数据存储(如用户余额)。
  • 操作方式:
    • 仅通过内联汇编(tstore/tload)访问,需熟悉汇编语法。
    • 槽位(bytes32)需明确定义,避免冲突。
  • Gas 成本:
    • tstoretloadsstoresload 便宜,适合高频临时操作。
    • 仍需注意交易内多次操作的累积成本。
  • 兼容性:
    • 瞬态存储依赖 Cancun 升级(EIP-1153),需确保 EVM 支持。
    • 测试时使用 Hardhat 或 Remix 的 Cancun 环境。
  • 安全性:
    • 检查槽位状态(tload),防止逻辑错误。
    • 避免在关键逻辑中依赖瞬态存储的持久性(会丢失)。

网站公告

今日签到

点亮在社区的每一天
去签到