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
}