0

I'm working on a code which requires getting the smallest unique number from a number of bets placed by players. I created an isolated code snippet to demonstrate it, it's on gist as well.

My issue that updateResults() costs a lot of gas. I understand it's primarily because it's writing a lot into maps and arrays in storage (through itMaps). It wouldn't need to be so but I couldn't figure out how to do that in memory.

So suggestions on how to reduce gas usage of updateResult() are appreciated. Whether it's a completely different approach or optimising variable scopes only.

Note: I understand that this won't scale for a higher number of bets because it iterates through an array. The current plan to address that is to calculate results in smaller chunks/batches for the future. Also, bets need to be encrypted but I removed that complexity for this question.

Current gas consumption

addBet()

  • First bet: 102,854
  • consecutive bets: 87,854
  • replace bet: 27,164

updateResults()

  • 0 bet: 48,138
  • 1 bet: 101,340
  • 2 bets / no winner: 145,564
  • 3 bets / no winner: 153,732
  • 2 bets / with winner: 150,816
  • 3 bets / with winner: 222,835

using 0.4.11+commit.68ef5810.Emscripten.clang


pragma solidity ^0.4.8;

import "github.com/szerintedmi/solidity-itMapsLib/itMapsLib.sol";

contract GasTest  {
    using itMaps for itMaps.itMapUintUint;
    using itMaps for itMaps.itMapUintAddress;
    using itMaps for itMaps.itMapAddressUint;
    using itMaps for itMaps.itMapUintBool;
    using GasTestLib for GasTestLib.Game;

    GasTestLib.Game game;

    function addBet(uint number) returns (bool overwrittenBet) {
        if (game.winningAddress != address(0)) {
            // it's a first bet so clear winner from previous round
            game.winningAddress = address(0);
            game.smallestNumber = 0;
        }
        return game.im_bets.insert(msg.sender,number); 
    }

    function updateResults() returns (uint numberOfUnrevealedOrInvalidBets) {
        return game._updateResults();
    }

    function getResults() constant returns (address winningAddress, uint smallestNumber) {
        return (game.winningAddress, game.smallestNumber);
    }
}

library GasTestLib {

  /* TODO: hove to structrure this? what data/logic shall we move here? */

  using itMaps for itMaps.itMapAddressUint;
  using itMaps for itMaps.itMapUintAddress;
  using itMaps for itMaps.itMapUintBool;

  struct Game {

      // playeraddress => bet number , bet number is 0 until revealed
      itMaps.itMapAddressUint im_bets;
      // callback queryId => player address : used for retrieving playerAddress at __callback
      uint smallestNumber;
      address winningAddress;
      ResultCalcHelper resultCalcHelper;
  }

  struct ResultCalcHelper {
    /* temporary structure to calculate results - can't be in memory because can't create mappings and dynamic arrays in memory
    could it be done without storage ??  */
    itMaps.itMapUintAddress im_seenOnce; // Key:betnumber -> Value:PlayerAddress
    itMaps.itMapUintBool im_seenMultiple; // Key:betnumber => Value:seen (bool)
                                          // mapping(uint=>bool) m_seenMultiple; would be enough to calc results
                                          // but it needs to be itmap to be able to destroy after round.
  }

  function _updateResults(Game storage self) internal returns (uint numberOfUnrevealedOrInvalidBets) {
    // This function needs to be updated to run in batches of bets to allow 
    //  higher number of bets evaluated without hitting the gas limit
      ResultCalcHelper storage _resultCalcHelper = self.resultCalcHelper;
      uint numberOfBets = self.im_bets.size() ;
      uint numberToCheck;

      // collect unique betnumbers in seenOnce from all bets (im_bets)
      for(uint i = 0; i < numberOfBets  ; i++) {
          numberToCheck = self.im_bets.getValueByIndex(i); // CHECK: does it overwrite value in im_bets?
          if(numberToCheck > 0) { // if this bid has been already revealed and valid...
            if (_resultCalcHelper.im_seenMultiple.contains(numberToCheck) ) {
              continue;
            }
            if (_resultCalcHelper.im_seenOnce.contains(numberToCheck)) {
              _resultCalcHelper.im_seenOnce.remove(numberToCheck);
              _resultCalcHelper.im_seenMultiple.insert(numberToCheck, true);
            } else {
              // first occurence, add to seenOnce
              _resultCalcHelper.im_seenOnce.insert( numberToCheck, self.im_bets.getKeyByIndex(i));
            }
          } else {
              numberOfUnrevealedOrInvalidBets++ ;
          } // numberToCheck
      } // for

      // find smallestNumber in seenOnce
      self.winningAddress = address(0);
      self.smallestNumber = 0;
      uint seenOnceCount = _resultCalcHelper.im_seenOnce.size();
      for( i=0; i < seenOnceCount; i++) {
        numberToCheck = _resultCalcHelper.im_seenOnce.getKeyByIndex(i);
        if (numberToCheck < self.smallestNumber || self.smallestNumber == 0) {
          self.smallestNumber = numberToCheck;
          self.winningAddress = _resultCalcHelper.im_seenOnce.getValueByIndex(i);
        }
      }
      // Clean up
      // CHECK: is it the best way? ie. shall we just set array lengthto zero? (im_seenOnce.clear())
      //    https://ethereum.stackexchange.com/questions/14017/solidity-how-could-i-apply-delete-to-complete-storage-ref-with-one-call
      _resultCalcHelper.im_seenOnce.destroy();
      _resultCalcHelper.im_seenMultiple.destroy();
      self.im_bets.destroy();
      return numberOfUnrevealedOrInvalidBets;
  } // updateResults

}
szerte
  • 1,231
  • 1
  • 14
  • 32

1 Answers1

1

you can replace if conditions in the code using modifiers as follows

function addBet(uint number) returns (bool overwrittenBet) {
    if (game.winningAddress != address(0)) {
        // it's a first bet so clear winner from previous round
        game.winningAddress = address(0);
        game.smallestNumber = 0;
    }
    return game.im_bets.insert(msg.sender,number); 
}

take the following as example

function addBet(uint number) checkCondition(game.winningAddress) returns (bool oerwrittenBet){
      game.winningAddress = address(0);
      game.smallestNumber = 0;
      return game.im_bets.insert(msg.sender,number);
}

modifier checkCondition(winningAddress){
     require(winningAddress == address(0))
     _;
}

for more information look into http://solidity.readthedocs.io/en/develop/common-patterns.html

pikachu
  • 100
  • 2
  • It actually increased the gas consumption for addBet eg. for first bet it went up to 113,003 from 102,854 – szerte Jun 06 '17 at 10:47