Solidity

洗牌數組/映射

  • August 21, 2022

是否可以打亂地址的數組或映射?我嘗試了這個解決方案,但它總是返回相同的結果

for (uint256 i = 0; i < arrayAddress.length; i++) {
           uint256 n = i + uint256(keccak256(abi.encodePacked(block.timestamp))) % (arrayAddress.length - i);
           address temp = arrayAddress[n];
           arrayAddress[n] = arrayAddress[i];
           arrayAddress[i] = temp;
       }

我為此創建了一個簡單的解決方案。我使用數字而不是地址來保持簡單和易於視覺化。

  1. 使用block.timestampandblock.number時,我們需要小心,因為如果您使用這些值的函式(或交易)沒有修改狀態,那麼block.timestampandblock.number將是最後一個修改狀態的區塊的時間戳和區塊號. 只有當您的函式修改狀態block.timestampblock.number實際引用不同的值時。這可能是您多次執行函式時看到相同結果的原因,因為block.timestamp總是相同的(如果您沒有進行任何其他修改狀態的事務)並且keccak256函式正在創建相同的雜湊,因此結果相同.
  2. 也許不需要keccak256在循環的每次迭代中計算散列。for我們可以計算一個雜湊值,然後從中讀取每個字節,並將其用作值來使用%我們需要的運算符創建索引。除非我們的數組長度超過 256 個元素,否則我們可以讀取 2 個字節,這將為我們提供大約 65536 個值,依此類推。我建議這樣做是因為如果我們的數組永遠不會超過 256 個元素,假設我們的數組有 256 個元素,我們只需要計算keccak256散列大約 256 / 32 次,即 8 次,而不是更糟case 256 次,效率更高。
  3. 在循環中修改狀態可能不是一個好主意,我們可能會得到一個 out of gas 異常。為了解決這個問題,我們可以將狀態數據複製為memory,對其進行修改,然後在循環結束後再次將其保存到儲存中。
  4. 如果你的函式不修改狀態,那麼你不能依賴block.timestampor block.number。然後,您需要以某種方式為您的keccak256雜湊提供“種子”以使其唯一。可能會向您的函式發送一個額外的參數、一個隨機數等,以便您使用它而不是block.timestamp.

我寫了一些程式碼並在下面放了一些註釋和結果,以便您查看、使用和修改它:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.16;

// Autor: Jeremy Then

contract Shuffle {

   uint[] public uints = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20];

   function getInts() public view returns(uint[] memory) {
       return uints;
   }

   // This shuffle function works by creating a keccak256 hash of the current block.timestamp + a counter
   // to produce a different hash every time we need one.
   // Then, we use each byte as our value to generate the index we are going to swap the ith element with
   // by using the modulo (%) operator.
   // A keccak256 hash only has 32 bytes, so, in case the array we are trying to shuffle is bigger than
   // 32 elements, then we create a different keccak256 hash with the counter, and start taking each
   // byte of this new hash one by one to derive the index to swap the ith element with..
   // This function asumes that the array to shuffle is no bigger than 255 elements. If so,
   // then we would need to read 2 bytes instead of one. Because 1 byte only provides us 256 (2^8) values
   // to use as index, but 2 bytes would provide us with 65536 (2^16) values to use as index, which 
   // we then cut it down to the range of our array using the % operator.
   function shuffle() public returns(uint[] memory) {

       // Making a copy of the uints array, since modifying it from storage is really expensive
       // and we could get a out of gas exception
       uint[] memory uintsCopy = uints;

       uint counter = 0;
       uint j = 0;
       bytes32 b32 = keccak256(abi.encodePacked(block.timestamp + counter));
       uint length = uintsCopy.length;

       for (uint256 i = 0; i < uintsCopy.length; i++) {

           if(j > 31) {
               b32 = keccak256(abi.encodePacked(block.timestamp + ++counter));
               j = 0;
           }

           uint8 value = uint8(b32[j++]);

           uint256 n = value % length;

           uint temp = uintsCopy[n];
           uintsCopy[n] = uintsCopy[i];
           uintsCopy[i] = temp;
       }

       // Now, modifying the state uints array, once as a whole.
       uints = uintsCopy;

       return uintsCopy;
       
   }

   // Since this function does not modify the state and does not pay gas,
   // the `block.timestamp` will be the timestamp of the last block that modified the state
   // of this contract.
   // If another function is called that cause a state change, then that is an actual
   // transaction that was mined, paid gas and then block.timestamp will be the timestamp of the
   // block of that last transaction.
   function getTimestamp() public view returns(uint) {
       return block.timestamp;
   }

   // Since this function does not modify the state and does not pay gas,
   // the `block.number` will be the block number of the last block that modified the state
   // of this contract
   function getBlocknumber() public view returns(uint) {
       return block.number;
   }
   
}

// Running the shuffle function multiple times, it produced shuffled values like these:

// 15,12,5,2,19,10,16,1,3,11,20,9,7,13,8,6,18,4,17,14
// 11,1,4,9,16,3,17,10,14,19,2,6,13,8,7,15,12,5,18,20
// 7,1,17,6,16,18,20,2,15,5,10,11,3,13,14,12,19,8,9,4
// 18,4,1,6,11,14,10,20,7,2,3,5,13,12,16,8,15,19,17,9

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