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

語言: CN / TW / HK

自從智慧合約被引入以來,基於智慧合約的系統複雜性增加了很多。它從簡單的投票合約、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解決方案相結合將是明智的選擇。

Source: http://medium.com/better-programming/issues-of-returning-arrays-of-dynamic-size-in-solidity-smart-contracts-dd1e54424235

關於

ChinaDeFi– ChinaDeFi.com 是一個研究驅動的DeFi創新組織,同時我們也是區塊鏈開發團隊。每天從全球超過500個優質資訊源的近900篇內容中,尋找思考更具深度、梳理更為系統的內容,以最快的速度同步到中國市場提供決策輔助材料。

Layer 2道友– 歡迎對Layer 2感興趣的區塊鏈技術愛好者、研究分析人與Gavin(微信: chinadefi)聯絡,共同探討Layer 2帶來的落地機遇。敬請關注我們的微信公眾號 “去中心化金融社群”