Solidity

在完成整個循環之前,在數組中搜尋較低的值會返回“Out of GAS”

  • September 6, 2022

我開發了一個將數據儲存在結構數組中的智能合約。另一個函式經常呼叫該函式來了解所有帳戶getLowerLastDate()的下級LastDate和相關性。Wallet

問題是當Acounts數組有大約 500 條記錄時(只是。恐怕因為可能有數百萬條),事務被塊還原out of GAS

我不確定如何解決這個區塊鏈障礙,因為我需要知道LastDate正確的智能合約操作的下限。

任何想法或提示表示讚賞。

struct StructAccount { 
 uint256 Index; 
 address Wallet; 
 uint256 Balance; 
 uint256 Time; 
 uint256 RegDate;
 uint256 LastDate;
 }  

StructAccount[] public Accounts;


function getLowerLastDate() public view returns (uint, address) {
 uint    lowerDate = block.timestamp;
 uint    Key = 0;
 for (uint i = 0; i < Accounts.length; i++) {
   if(Accounts[i].LastDate != 0){
       if( Accounts[i].LastDate <= lowerDate ){
           lowerDate = Accounts[i].LastDate;
           Key = i;
       }
   }
 }
 Accounts[Key].LastDate = 0; //reset and search the next lower LastDate
 return (lowerDate, Accounts[Key].Wallet) ;
}

我不確定你是如何創建每個StructAccount的,但你可以創建一個名為 的新全域變數lowestLastDate,並且每次創建一個StructAccount時,將它與 的值進行比較lowestLastDate,如果它較低,則將其設為 的新值lowestLastDate

每當您想要訪問較低 LastDate 的值時,您都可以從變數中獲得它。這使您可以在 O(1) 時間內訪問最低的遲到日期,但將花費更多的氣體來初始化每個StructAccount

我想我可以幫你解決這個問題。

您可以嘗試的第一種方法,也是最簡單的一種,是製作Accounts數組的本地/記憶體副本。因為在循環中讀取和讀取儲存會消耗大量氣體。

檢查常見操作碼(如SLOAD, SSTORE, MLOAD, )的 gas 成本MSTORE

所以,我會像這樣重構你的程式碼:

contract Contract {
   struct StructAccount { 
       uint256 Index; 
       address Wallet; 
       uint256 Balance; 
       uint256 Time; 
       uint256 RegDate;
       uint256 LastDate;
   }  

   StructAccount[] public Accounts;

   function getLowerLastDate() public returns (uint, address) {
       StructAccount[] memory accountsCopy = Accounts;
       uint lowerDate = block.timestamp;
       uint Key = 0;
       for (uint i = 0; i < accountsCopy.length; i++) {
           if(accountsCopy[i].LastDate != 0){
               if( accountsCopy[i].LastDate <= lowerDate ){
                   lowerDate = accountsCopy[i].LastDate;
                   Key = i;
               }
           }
       }

       // We can leave this line as is, since we need to update the state Accounts array at least once here.
       Accounts[Key].LastDate = 0; //reset and search the next lower LastDate
       return (lowerDate, accountsCopy[Key].Wallet);
   }
}

請注意我是如何在數組的記憶體中製作副本的。這樣,從記憶體中讀取比從儲存中讀取更便宜。

此外,執行以下操作是完全有效的。複製一個狀態數組,對其進行操作,然後將其分配回狀態數組,一次。

contract Contract {

   uint256[] public numbers;

   uint256 public counter;

   function addNumber() public {
       numbers.push(counter++);
   }

   function doOperation() public {

       uint256[] memory counterCopy = numbers;

       for(uint256 i = 0; i < counterCopy.length; i++) {
           counterCopy[i] = counterCopy[i] * 2;
       }

       numbers = counterCopy; // This is valid.

   }

}

現在,當您擁有如此多的記錄(數百萬?)時,這可能無法解決您未來的所有問題。如您所見,遍歷具有如此多記錄的數組並不理想,甚至在記憶體中也不理想。這將非常昂貴,而且無論如何您都可能會遇到氣體不足的異常。

這給我們帶來了第二種方法:

每次添加/更新時記錄StructAccount最低的實例。LastDate

以功能為例addStructAccount(我不知道這是否是您添加帳戶的方式,但這只是您可以做的一個範例)。請注意,我一直在檢查新帳戶是否具有較低的 LastDate,如果是,我將structWithLowestLastDate狀態變數替換為它。我們也可以在更新任何帳戶時執行此操作。

contract Contract {

   struct StructAccount { 
       uint256 Index; 
       address Wallet; 
       uint256 Balance; 
       uint256 Time; 
       uint256 RegDate;
       uint256 LastDate;
   }

   StructAccount public structWithLowestLastDate;

   StructAccount[] public Accounts;

   function addStructAccount(uint256 index, address wallet, uint256 balance, uint256 time, uint256 regDate, uint256 lastDate) public {

       StructAccount memory account = StructAccount(index, wallet, balance, time, regDate, lastDate);

       // Keeping the account with the lowest LastData in a storage variable of its own, this way we don't need to look for it in the array
       if(lastDate < structWithLowestLastDate.LastDate) {
           structWithLowestLastDate = account;
       }

       // Adding the account to the storage array as usual. 
       Accounts.push(account);

   }

   function getLowerLastDate() public returns (uint, address) {
       StructAccount[] memory accountsCopy = Accounts;
       uint lowerDate = block.timestamp;
       uint Key = 0;
       for (uint i = 0; i < accountsCopy.length; i++) {
           if(accountsCopy[i].LastDate != 0){
               if( accountsCopy[i].LastDate <= lowerDate ){
                   lowerDate = accountsCopy[i].LastDate;
                   Key = i;
               }
           }
       }

       // We can leave this line as is, since we need to update the state Accounts array at least once here.
       Accounts[Key].LastDate = 0; //reset and search the next lower LastDate
       return (lowerDate, accountsCopy[Key].Wallet);
   }
}

也許這種方法對您也不起作用,因為我看到當getLowerLastDate()您重置它時,如果您再次呼叫它,您期望找到具有最低 LastDate 的下一個帳戶。

為此,您可能希望實現 a minHeap,因此您始終將較低的 LastDate 帳戶作為第一個元素,並且您可以peek()在恆定時間內或及時使用pop()O(log n)

https://www.digitalocean.com/community/tutorials/min-heap-binary-tree

如果你要在智能合約中有這麼多記錄,也許你可以考慮將這些數據移動到正常後端/數據庫,如果它不需要去中心化等,或者移動到 IPFS。

或者,也許您可以有邏輯從您不再需要的數組中刪除數據。這樣,您可以將數組保持在合理的大小。

我將嘗試minHeap在 Solidity 中實現一個,看看它是如何工作的……

好吧,我在這裡找到了一個最大堆實現:https ://github.com/Dev43/heap-solidity

您可以輕鬆地將其轉換為最小堆。

引用自:https://ethereum.stackexchange.com/questions/135121