Ethereum 存儲類型及 Storage 位置

林瑋宸 Albert Lin
10 min readFeb 15, 2021

--

前一陣子參加 Paradigm CTF 2021 發現了自己能力真的非常不足,能解出的題目非常少。其中有一題 Bank 的題目就有運用到 Ethereum 在 Storage 的儲存觀念。所以上網找了一下相關的資訊,於是整理一下在這當成筆記。若你對於 Storage 儲存也有些疑惑的話,希望這一篇可以幫助到你。

Ethereum Data Location

Ethereum 在存變數的時候可以分成 Storage 和 Memory。其中 Memory 裡面包含了 Calldata 和 Stack 不同的資料結構。

Storage

  • 在合約建立的時候就建好了 。
  • 永久儲存在區塊鏈中的變數。
  • 是以 key/value 方式儲存。

Memory

  • 暫時儲存變數,當外部呼叫合約函式之後就會銷毀。
  • 除了 memory 還包含兩種型態的儲存數據位置 (Calldata and Stack)。
  • 是以 array 的方式儲存。

Calldata (Memory)

  • read only。
  • 用來存函數參數。
  • 外部函式的參數會被強制指定成 calldata。
  • 用起來效果跟 memory 差不多。

Stack (Memory)

  • EVM 是使用 stack 的 virtual machine。
  • 每一個元素所佔的空間為 256 bit (32 bytes)。
  • Stack 長度最高為 1024。
  • 是存 local variable。

若以消耗的 Gas 來看的話

  • Storage 開銷最大。
  • Memory 因為是暫存,開銷小。
  • Stack 免費但有數量限制(16個變數)。
  • Calldata 包含了這次呼叫所帶的 data (func sig + parameter),計算時需要增加 n * 16(已修正感謝陳品大大的提醒) 的 gas 費用。

PS: 若發生 CompilerError: Stack too deep, try removing local variables 錯誤,常用的處理方法有:

  • 把變數封裝到 struct。
  • 使用 memory 關鍵字(他就會使用 memory 存,而非 stack)。

Ethereum 變數型態的儲存位置

Solidity 變數基本上可以分成兩種:Value Type 和 Reference Type。差別在於賦值或傳遞參數的時是複製值還是傳遞 Reference。有點像常見的
Call by value / Call by reference 的概念。 Reference Type 會有一個額外的屬性:數據位置,用以表示是存在 Memory 中還是 Storage 中。

Value Types:
- Booleans
- Integers
- Fixed Point Numbers
- Fixed-size byte arrays
- Rational and Integer Literals
- String literals
- Hexadecimal literals
- Enums
- Function Types
- Address
- Address Literals
Reference Type:
- bytes
- string
- Array
- Struct

不同儲存空間的傳遞方式

Pass by Value

  • Memory 和 Storage 之間的給值
  • state variable 之間的給值

Pass by Reference

  • Storage state variable to Storage local variable
  • Memory reference type to Memory reference type

注意:不能將 memory 賦值給 local variable

pragma solidity ^0.4.0;

contract C {
uint[] x; // storage

// memoryArray 的存储位置是 memory
function f(uint[] memoryArray) public {
x = memoryArray; // 將整個array copy 到 storage 中,可行
var y = x; // 分配一个指针(其中 y 的數據存储位置是 storage)可行
y[7]; // 返回第 8 个元素,可行
y.length = 2; // 通過 y 修改 x,可行
delete x; // 清除 array ,同時修改 y,可行
// 下面的就不可行了;需要在 storage 中创建新的未命名的临时数组,
// 但 storage 是"静态"分配的:
y = memoryArray; // 不可行
// 下面这一行也不可行,因为这会"重置"指针,
// 但并没有可以让它指向的合适的存储位置。
delete y; // 不可行

g(x); // 调用 g 函数,同时移交对 x 的引用
h(x); // 调用 h 函数,同时在 memory 中创建一个独立的临时拷贝
}

function g(uint[] storage storageArray) internal {}
function h(uint[] memoryArray) public {}

Storage 變數具體的儲存位置

  • Ethereum Storage 是以一個 SLOT 一個 SLOT 為單位
  • 每一個 Storage 都是從 SLOT0 開始
  • 每一個 SLOT 都是存放 32 byte
  • 基本型態只會佔本身所需的空間,ex. address: 20 bytes, uint256: 32 byte
  • 若 SLOT 中還有空間 compiler 會盡量去塞滿。若變數大小超過 SLOT 則會選下一個新的 SLOT 去放。架設 state variable 為 byte4 var1 , byte4 var2 and uint var3var1andvar2會放到 SLOT0, var3則會放到 SLOT1

定位固定大小的值

contract StorageTest {
uint256 a;
uint256[2] b;
struct Entry {
uint256 id;
uint256 value;
}
Entry c;
}

a 在 SLOT0。b 在 SLOT1 和 SLOT2。因為 array length 為 2,每一個元素各佔一個 SLOT。c 在 SLOT3 和 SLOT4。SLOT3 放 id 和 SLOT4 放 value。

Dynamic Array

contract StorageTest {
uint256 a; // slot 0
uint256[2] b; // slots 1-2
struct Entry {
uint256 id;
uint256 value;
}
Entry c; // slot 3-4
Entry[] d; // slot 5
}

dynamic array 會在 SLOT5 放整個 array length。真正的資料是放在 keccak256(5) 的位置開始放。5 是指 SLOT5 的意思,array 資料是從 keccak256(5) 位置依序存放的。

計算 dynamic array 某個 index 的存放位置

function getArrayLocation(
uint256 slot,
uint256 index,
uint256 elementSize
) public pure returns (uint256) {
return
uint256(keccak256(abi.encodePacked(slot))) + (index * elementSize);
}

Map

contract StorageTest {
uint256 a; // slot 0
uint256[2] b; // slots 1-2

struct Entry {
uint256 id;
uint256 value;
}
Entry c; // slots 3-4
Entry[] d; // slot 5 for length, keccak256(5)+ for data

mapping(uint256 => uint256) e; // slot 6
mapping(uint256 => uint256) f; // slot 7
}

Map 一樣會佔一個 SLOT 但真正的 data 是存在另外的地方。真正的 data 是存在 keccak256( key, slot_number)。slot_number, key 表示把 key 後面接 slot_number 連在一起做hash。 ex. e[123] 的儲存位置 = keccak256(123, 6)。

計算 map 某個 key 的存放位置

function getMapLocation(uint256 slot, uint256 key)
public
pure
returns (uint256)
{
return uint256(keccak256(abi.encodePacked(key, slot)));
}

複合型態

contract StorageTest {
uint256 a; // slot 0
uint256[2] b; // slots 1-2

struct Entry {
uint256 id;
uint256 value;
}
Entry c; // slots 3-4
Entry[] d; // slot 5 for length, keccak256(5)+ for data

mapping(uint256 => uint256) e; // slot 6, data at h(k . 6)
mapping(uint256 => uint256) f; // slot 7, data at h(k . 7)
mapping(uint256 => uint256[]) g; // slot 8
mapping(uint256 => uint256)[] h; // slot 9
}

若要找出 g[123][2] 的儲存位置

  1. 先找出 g[123] 的位置。
  2. g SLOT 是 8。
  3. mapLoc = mapLocation(8, 123) = keccak256(123, 8)。

再去找g[123][2] 的位置

  1. arrLocation(mapLoc, 2, 1) = uint256(keccak256(mapLoc)) + (2 * 1)。

若要找出 h[2][456] 的儲存位置

  1. 先找出 h[2] 的位置
  2. arrLoc = arrLocation(9, 2, 1) = uint256(keccak256(9)) + (2 * 1)。map 佔 1 slot

再找出 h[2][456] 的位置

  1. mapLocation(arrLoc, 456) = keccak256(456, arrLoc)

以上的資訊我都是在網路上找到並加以理解整理出來。若有興趣想要看原本的資訊,我會把連結放在 Reference 中,歡迎大家去看。Storage 的存放位置理解沒有很難,但仍需要一點時間去消化。一開始我也看得霧煞煞,後來慢慢地理解它的運作方式。希望這篇可以幫助大家更理解 ethereum 。我也持續在學習中,有什麼地方理解錯誤的麻煩大家給予糾正。感謝!

補充 Struct 的 storage layout

Struct 也會根據裡面的型態盡量去塞在同一個 Slot,但比較特別的是塞進去變數順序會是從右到左。舉例

contract StructLayout {
struct TestStruct {
uint256 a; // slot 0
uint184 b; // slot 1
uint128 c; // slot 2 128-255 from left
uint64 d; // slot 3 65-127 from left
bytes4 e; // slot 4 32-64 from left
uint32 f; // slot 5 0-31 from left
}
TestStruct s;
}

a 型態是 uint256,所以佔據整個 slot0。 b 型態是 uint184,但因為下一個 c 是 uint184,無法同時塞到 slot1,所以 slot1 只有存放 b。剩下的 c, d, e, f 都被塞到的 slot2。順序會是從右到左,所以最右邊的會是存放 c,而最左邊的則是存放 f。

Reference

以太坊存储类型(memory, storage)及变量存储详解
Layout of State Variables in Storage

--

--