2018年3月9日,史蒂夫馬克思html
以太坊智能合約使用一種不常見的存儲模式,這種模式一般會讓新開發人員感到困惑。在這篇文章中,我將描述該存儲模型並解釋Solidity編程語言如何使用它。編程
每一個在以太坊虛擬機(EVM)中運行的智能合約的狀態都在鏈上永久地存儲着。這個存儲能夠被認爲是每一個智能合約都保存着一個很是大的數組,初始化爲全0。數組中的每一個值都是32字節寬,而且有2^256個這樣的值。智能合約能夠在任何位置讀取或寫入數值。這就是存儲接口的大小。
我鼓勵你堅持「天文數組」的思考模式,但要注意,這不是組成以太坊網絡的物理計算機的實際存儲方式。存儲數組空間實際上很是稀疏,由於不須要存儲零。將32字節密鑰映射到32字節值的鍵/值存儲將很好地完成這項工做。一個不存在的鍵被簡單地定義爲映射到零值。數組
因爲零不佔用任何空間,所以能夠經過將值設置爲零來回收存儲空間。當您將一個值更改成零時,智能合約中內置的返還gas機制被激活。安全
在這個存模型中,到底是怎麼樣存儲的呢?對於具備固定大小的已知變量,在內存中給予它們保留空間是合理的。Solidity編程語言就是這樣作的。網絡
contract StorageTest { uint256 a; uint256[2] b; struct Entry { uint256 id; uint256 value; } Entry c; }
在上面的代碼中:app
這些下標位置是在編譯時肯定的,嚴格基於變量出如今合同代碼中的順序。編程語言
使用保留下標的方法適用於存儲固定大小的狀態變量,但不適用於動態數組和映射(mapping
),由於沒法知道須要保留多少個槽。函數
若是您想將計算機RAM或硬盤驅動器做爲比喻,您可能會但願有一個「分配」步驟來查找可用空間,而後執行「釋放」步驟,將該空間放回可用存儲池中。佈局
可是這是沒必要要的,由於智能合約存儲是一個天文數字級別的規模。存儲器中有2^256個位置可供選擇,大約是已知可觀察宇宙中的原子數。您能夠隨意選擇存儲位置,而不會遇到碰撞。您選擇的位置相隔太遠以致於您能夠在每一個位置存儲儘量多的數據,而無需進入下一個位置。ui
固然,隨機選擇地點不會頗有幫助,由於您沒法再次查找數據。Solidity改成使用散列函數來統一併可重複計算動態大小值的位置。
動態數組須要一個地方來存儲它的大小以及它的元素。
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; }
在上面的代碼中,動態大小的數組d存在下標5的位置,可是存儲的惟一數據是數組的大小。數組d中的值從下標的散列值hash(5)開始連續存儲。
下面的Solidity函數計算動態數組元素的位置:
function arrLocation(uint256 slot, uint256 index, uint256 elementSize) public pure returns (uint256) { return uint256(keccak256(slot)) + (index * elementSize); }
一個映射mapping須要有效的方法來找到與給定的鍵相對應的位置。計算鍵的哈希值是一個好的開始,但必須注意確保不一樣的mappings產生不一樣的位置。
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; mapping(uint256 => uint256) f; }
在上面的代碼中,e的「位置」 是下標6,f的位置是下標7,但實際上沒有任何內容存儲在這些位置。(不知道多長鬚要存儲,而且獨立的值須要位於其餘地方。)
要在映射中查找特定值的位置,鍵和映射存儲的下標會一塊兒進行哈希運算。
如下Solidity函數計算值的位置:
function mapLocation(uint256 slot, uint256 key) public pure returns (uint256) { return uint256(keccak256(key, slot)); }
請注意,當keccak256
函數有多個參數時,在哈希運算以前先將這些參數鏈接在一塊兒。因爲下標和鍵都是哈希函數的輸入,所以不一樣mappings之間不會發生衝突。
動態大小的數組和mappings能夠遞歸地嵌套在一塊兒。當發生這種狀況時,經過遞歸地應用上面定義的計算來找到值的位置。這聽起來比它更復雜。
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 }
要找到這些複雜類型中的項目,咱們可使用上面定義的函數。要找到g123:
// first find arr = g[123] arrLoc = mapLocation(8, 123); // g is at slot 8 // then find arr[0] itemLoc = arrLocation(arrLoc, 0, 1);
要找到h2:
// first find map = h[2] mapLoc = arrLocation(9, 2, 1); // h is at slot 9 // then find map[456] itemLoc = mapLocation(mapLoc, 456);
下表顯示瞭如何計算不一樣類型的存儲位置。「下標」是指在編譯時遇到狀態變量時的下一個可用下標,而點表示二進制串聯:
類 | 聲明 | 值 | 位置 |
---|---|---|---|
簡單的變量 | T v | v | v的下標 |
固定大小的數組 | T[10] v | v[n] | (v's slot)+ n *(T的大小) |
動態數組 | T[] v | v[n] | keccak256(v's slot)+ n *(T的大小) |
v.length | v的下標 | ||
製圖 | mapping(T1 => T2) v | v[key] | keccak256(key。(v's slot)) |
若是您想了解更多信息,我推薦如下資源: