智慧合約:關於返回動態陣列的問題

自從智慧合約被引入以來,基於智慧合約的系統複雜性增加了很多。它從簡單的投票合約、ERC20代幣開始,發展到像Uniswap、Aragon等複雜的架構。
最近,我所在的Custom App(開發工作室)正在開發一款Minter Guru dApp,而我正在研究系統架構和智慧合約。根據Minter Guru的理念,我們必須將盡可能多的邏輯轉移到智慧合約上。
在實現read(從CRUD模式)函式時,我們遇到了返回動態大小陣列的問題,該陣列可能包含數百甚至數千個元素。因此,我們研究了實現此類函式的侷限性。
在本文中,我們將討論這些限制、處理它們的方法,以及“用智慧合約實現Web2後端”方法來構建應用程式的可行性。
為了更好地理解,你應該熟悉以太坊區塊鏈基礎知識(或任何其他EVM相容的區塊鏈,如Polygon或BSC),Solidity和Hardhat。所有關於如何執行它的例子和說明的程式碼都可以在我們的GitHub庫中找到。
gas限制
我們的第一個想法是,理論上,我們可以達到gas的極限。
在EVM中,每個函式呼叫都需要gas。由於呼叫不會消耗真正的gas,所以我們可以設定任何gas限制,但是由於EVM實現的最大gas限制等於uint64型別的最大值,所以就是18446744073709551615。
讓我們通過一個簡單的合約例子來檢查一下實現這個限制的速度。我們將使用一個簡單的合約uint256[]儲存變數,推送n個值到陣列函式和getter方法。此外,我們需要兩個指令碼來部署合約,並測試所需的gas限制。我們將使用Hardhat作為我們的本地網路並與合約進行互動。
消耗的gas取決於陣列的大小,因為EVM會將陣列資料複製到記憶體中。因此,我們將研究gas消耗和陣列大小(以位元組為單位)之間的依賴關係。為了簡單起見,我們使用uint256[]陣列。對於其他資料型別,以位元組為單位的編碼陣列大小的計算有所不同。根據Solidity,Enc(uint256[])=Enc(array.size)(Enc(array[0],…,array[array.size-1]))。因此,陣列的位元組大小等於size(uint256)+size(uint256) array.size=32+32 array.size。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.9; contract ReturnLimitTest { uint256[] private array; function addToArray(uint256 count) external { for (uint256 i = 0; i < count; i++) { array.push(array.length); } } function getArray() external view returns (uint256[] memory) { return array; } }
import * as hre from "hardhat"; import {program} from "commander"; import {ReturnLimitTest__factory} from "../typechain-types"; async function main() { program.option("-instance, --instance <string>") program.parse(); const BN = hre.ethers.BigNumber; const accounts = await hre.ethers.getSigners(); const factory = new ReturnLimitTest__factory(accounts[0]); const instance = factory.attach(program.opts().instance); console.log("array size,estimated gas") for (let i = 0; i < 10000000000000000; i++) { const estimatedGas = await instance.estimateGas.getArray({ gasLimit: BN.from("18446744073709551615") }); await instance.addToArray(1000); console.log((i * 1000).toString() + ',' + estimatedGas.toString()); } } main().catch((error) => { console.error(error); process.exitCode = 1; });
在執行這個指令碼時,我們看到for迴圈的迭代開始變得異常緩慢。第10次迭代花了10秒以上,第50次花了4分鐘以上。

根據某些陣列大小估計的gas
】
估計的gas依賴於陣列大小
在圖表中,我們可以看到估計的gas與陣列大小呈線性關係,所以讓我們建立外推來估計陣列的理論大小。我們已經使用谷歌電子表格和線性迴歸完成了它。

估計gas外推
可以看到,理論上的uint256陣列尺寸限制大約等於1.8e+17千位元組(163709千位元組),這實際上是無法實現的。其中執行時間就是一個大問題。
執行時間
首先,我們要提到的是,我們正在用Node.js實現的Hardhat網路進行測試,所以它的效能會比在以太坊節點中使用的Golang實現要差。但是,與EVM實現語言相比,EVM的設計對智慧合約功能的實現影響更大。
進行測試的機器引數:
- CPU – 11代Intel®Core™i7-1165G7 @ 2.80GHz × 8
- Ram – 16GB
- 作業系統- Ubuntu 22.04 LTS
- 磁碟 – 512gb SSD
現在修改我們的測試指令碼來測量執行時間。我們只需要改變測試指令碼中的for迴圈來記錄執行時間。下面是程式碼:
console.log("array size,estimated gas,execution time") for (let i = 0; i < 10000000000000000; i++) { const start = Date.now(); const estimatedGas = await instance.estimateGas.getList({ gasLimit: BN.from("18446744073709551615") }); const finish = Date.now(); await instance.addToList(1000); console.log((i * 1000).toString() + ',' + estimatedGas.toString() + ',' + (finish - start).toString()); }

現在我們可以看到,高執行時間的問題很快就出現了。對於320 KB的資料,12秒的執行時間太長了,所以時間成為我們需要克服的主要問題。
分頁
沒有一種神奇的方法可以在合理的時間內獲得大的陣列。但是,如果你的應用程式在某一時刻不需要完整的陣列,你可以像在Web2中那樣使用分頁模式。然而,如果是這樣,則表明存在架構問題,這是智慧合約系統或經典Web2應用程式無法解決的。
分頁通常被開發人員用來限制一個API呼叫中返回資料的大小。這種方法減少了延遲,也減少了為瀏覽器和移動應用程式呈現的UI元素的數量。
分頁的實現包括以下步驟:
- 在陣列上定義排序
- 將頁碼和頁面大小新增到API方法(本例中的合約函式)。你的新方法應該返回陣列的一部分:array[page size:min((page+1) size, array.length)]
- 將陣列中元素的總數量新增到響應中,就可以在客戶端上找到頁面數量。
現在修改我們的測試智慧合約,如下圖所示:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.9; contract ReturnLimitTestWithPagination { uint256[] private array; function addToArray(uint256 count) external { for (uint256 i = 0; i < count; i++) { array.push(array.length); } } function getArray(uint256 page, uint256 pageSize) external view returns (uint256[] memory, uint256) { require(pageSize > 0, "page size must be positive"); require(page == 0 || page*pageSize <= array.length, "out of bounds"); uint256 actualSize = pageSize; if ((page+1)*pageSize > array.length) { actualSize = array.length - page*pageSize; } uint256[] memory res = new uint256[](actualSize); for (uint256 i = 0; i < actualSize; i++) { res[i] = array[page*pageSize+i]; } return (res, array.length); } }
測試指令碼。我們還將節省每個獲取頁面呼叫的執行時間,以表明它對任何頁面保持不變。這是程式碼:
import * as hre from "hardhat"; import {program} from "commander"; import {ReturnLimitTestWithPagination__factory} from "../typechain-types"; async function main() { program.option("-instance, --instance <string>") program.parse(); const accounts = await hre.ethers.getSigners(); const factory = new ReturnLimitTestWithPagination__factory(accounts[0]); const instance = factory.attach(program.opts().instance); await instance.addToArray(1023); console.log("array size,execution time,one call execution times") for (let i = 1; i < 10000000000000000; i++) { const start = Date.now(); const oneCall = []; for (let j = 0; j < i; j++) { const start = Date.now(); await instance.estimateGas.getArray(j, 1024); oneCall.push((Date.now() - start)); } const finish = Date.now(); await instance.addToArray(1024); console.log((i * 1024 * 32).toString() + ',' + (finish - start).toString() + ',' + oneCall.join(";")); } } main().catch((error) => { console.error(error); process.exitCode = 1; });
讓我們看看結果。同樣,完整的結果可以在我們的儲存庫中找到。此外,為了減少表的大小,我們將只顯示get頁面呼叫的平均和最大執行時間:

按頁獲取陣列的執行時間
所以現在我們可以在合理的時間內得到陣列的一部分。
最後一個細節:如果想要遍歷可以頻繁修改狀態的陣列,可以在呼叫函式時使用固定的區塊號。這保證了狀態在每一頁呼叫中都保持不變。使用Hardhat合約繫結,可以這樣做:
await instance.getArray(0, 1024, { blockTag: 1234, // block number, hash, or tag (eg. latest) });
結論
所以,有兩個要點:
- 返回大陣列的函式的主要問題是呼叫執行時間。
- 通過分頁,我們可以使呼叫時間變得合理,並可以獲取部分資料。如果應用程式的for邏輯可以同時處理陣列的一部分,就可以使用該模式。
我們已經用最簡單的getter邏輯完成了所有的測試,但是在你的應用程式中,getter邏輯可能要複雜得多,因此在設計架構時,應該小心地將所有內容轉移到智慧合約。
在部署到主網後,你可能會遇到無法解決的問題(如果你的合約不可升級)。如果你覺得你的系統太複雜,無法將其全部放入智慧合約中,那麼將智慧合約與經典的Web2解決方案相結合將是明智的選擇。
關於
ChinaDeFi– ChinaDeFi.com 是一個研究驅動的DeFi創新組織,同時我們也是區塊鏈開發團隊。每天從全球超過500個優質資訊源的近900篇內容中,尋找思考更具深度、梳理更為系統的內容,以最快的速度同步到中國市場提供決策輔助材料。
Layer 2道友– 歡迎對Layer 2感興趣的區塊鏈技術愛好者、研究分析人與Gavin(微信: chinadefi)聯絡,共同探討Layer 2帶來的落地機遇。敬請關注我們的微信公眾號 “去中心化金融社群” 。

- 資訊:加密貨幣的機構採用現狀
- 資訊:MetaMask的整合為使用者解鎖了去中心化社交
- 警惕:新的以太坊合併 Metamask 網路釣魚!
- 總結:Web3使用者體驗的四個層
- BTC:薩爾瓦多的這一年
- 資訊:2個地址控制著 40% 的以太坊交易
- 比較:以太坊 PoS 和 PoW 安全性
- 以太坊:合併後的一天內,ETHW Core 將繼續進行 ETH PoW 分叉
- SWEAT:乘著合併的風,再一個Move-To-Earn專案的推出
- EIP-3523:半同質代幣介紹
- Coinbase:Celer Bridge攻擊分析
- Uniswap:合併前需要注意的事項
- Composable:XCVM 是如何抽象跨鏈開發者體驗的?
- 基礎:在 Solidity 中探索 ERC20標準
- 以太坊:合併後,對MEV的影響
- 空投:苦等Arbitrum空投的人,都轉向了哪裡?
- 以太坊:合併(Merge)之後還有很多升級(Surge、 Verge、Purge、 Splurge)
- Polygon:去中心化社交媒體是未來?還是不必要的利基?
- Arbitrum:怎麼獲得Arbitrum空投?
- 智慧合約:關於返回動態陣列的問題