A Cure For Gas – Optimising an Ethereum Contract

By Dr. Andrew Le Gear

In the previous blog [here] we described an attempt at creating a notarisation solution using Ethereum, which concluded that storage on the Ethereum blockchain was simply too expensive to make the business viable. However, there was still hope!  We continued to alter the original smart contract and eventually arrived at a far cheaper solution while maintaining the same business semantics. 

 

Expensive Contract 

First, let’s revisit the “Expensive Contract” (below). This contract had several problems: 

  1. Every new notarised conversation required a new contract. 
  2. Notarising the conversation was dependent on the size of the conversation being stored. 
  3. Adding to a notarised conversation increased the gas cost as the conversation grew. 
  4. Notarisation does not necessarily imply “public,” so storing the conversation on the blockchain in this manner would result in privacy issues. 

In total, a single notarisation of 256 characters was costing 814567 gas to create and execute. [ https://rinkeby.etherscan.io/address/0x76f3d9840c1c5a6714b1f8f38f803cb378bf08ec ] 

pragma solidity ^0.4.19; contract horizonMortalContract { /* Define variable owner of the type address */ address owner; /* This function is executed at initialization and sets the owner of the contract */ function horizonMortalContract() public { owner = msg.sender; } function kill() public { if (msg.sender == owner) selfdestruct(owner); } } contract horizonNotariser is horizonMortalContract { /* Define variable to hold the notarised conversation of the type string */ string conversation; /* add another message to the notarised conversation */ function notariseMessage(string _message) public { if (msg.sender == owner) { bytes memory _bytesMessage = bytes(_message); bytes memory _bytesConversation = bytes(conversation); string memory _concatenated = new string(_bytesMessage.length + _bytesConversation.length); bytes memory _bytesConcatenated = bytes(_concatenated); uint _currentIndex = 0; for (uint i = 0; i < _bytesConversation.length; i++) _bytesConcatenated[_currentIndex++] = _bytesConversation[i]; for (i = 0; i < _bytesMessage.length; i++) _bytesConcatenated[_currentIndex++] = _bytesMessage[i]; conversation = string(_bytesConcatenated); } } /* Retrieve the current state of the conversation */ function getConversation() public constant returns (string) { if (msg.sender == owner) { return conversation; } else { return ''; } } }

Expensive Contract

 

One Contract to Rule Them All 

By creating a contract for each notarisation in the previous example, we incurred a whopping 534436 gas to simply create the contract.  That amounts to 65% of the total cost of the notarisation.  Far better would be to have a single contract that managed the notarisations of tens of thousands of conversations, thus spreading the cost of creating the contract over many conversations.  The “Consolidated Contract” below implements this. [ https://rinkeby.etherscan.io/address/0xb4029a3c3ef4f92ff8bd6b31d76925586753baa5 ]

While the gas used to create the contract is higher at 771096, having it spread over 60000 notarisations makes the cost negligible (~12 gas per conversation).  This gives us an immediate 65% gas saving 

pragma solidity ^0.4.19; contract horizonMortalContract { /* Define variable owner of the type address */ address owner; /* This function is executed at initialization and sets the owner of the contract */ function horizonMortalContract() public { owner = msg.sender; } function kill() public { if (msg.sender == owner) selfdestruct(owner); } } contract horizonNotariser1 is horizonMortalContract { /* Define variable to hold the notarised conversation of the type string */ string[60000] conversation; uint256 usageCount = 0; /* add another message to the notarised conversation */ function notariseMessage(string _message,uint256 index) public { if (msg.sender == owner) { bytes memory _bytesMessage = bytes(_message); bytes memory _bytesConversation = bytes(conversation[index]); string memory _concatenated = new string(_bytesMessage.length + _bytesConversation.length); bytes memory _bytesConcatenated = bytes(_concatenated); uint _currentIndex = 0; for (uint i = 0; i &lt; _bytesConversation.length; i++) _bytesConcatenated[_currentIndex++] = _bytesConversation[i]; for (i = 0; i &lt; _bytesMessage.length; i++) _bytesConcatenated[_currentIndex++] = _bytesMessage[i]; conversation[index] = string(_bytesConcatenated); usageCount++; } } /* Retrieve index for a given message */ function getIndex(string _message) public constant returns (uint256) { if (msg.sender == owner) { for(uint256 i = 0 ; i &lt; 60000 ; i++) { if(keccak256(conversation[i])==keccak256(_message)) { return i; } } } return 0; } /* Retrieve the current conversation */ function getConversation(uint256 index) public constant returns (string) { if (msg.sender == owner) { return conversation[index]; } else { string empty; return empty; } } /* For maintenance purposes for us to check how many conversations are remaining before we need a new contract on the blockchain */ function getRemaining() public constant returns (uint256) { if (msg.sender == owner) { return (60000-usageCount); } else { return 0; } } }

Consolidated Contract 

The “Consolidated Contract” introduces problems of its own, however.  Notably we are now introducing an array index that must be tracked in storage on the blockchain and queried from the blockchain.  Significantly, also, we are still storing costly variable length strings that limits the practical size of the notarisation we can perform and exposes privacy concerns for notarised conversations. 

 

Hashed Contract  

As alluded to at the end of the previous blog [here], we could quite feasibly store a hash of the conversation on the blockchain.  This would neatly retain the semantics of notarisation while also guaranteeing privacy for the notarised conversation. Additionally, storing a fixed width 32-byte SHA-256 hash will give us a fixed price for storage on the blockchain that is cheaper for any conversation greater than 32 characters in length – which is basically every meaningful conversation you want to notarise.  The “Hashed Contract” below achieves all this [ https://rinkeby.etherscan.io/address/0xa05863a466fd05e44a018289284b4ffca49845b2 ]. 

pragma solidity ^0.4.19; contract horizonMortalContract { /* Define variable owner of the type address */ address owner; /* This function is executed at initialization and sets the owner of the contract */ function horizonMortalContract() public { owner = msg.sender; } function kill() public { if (msg.sender == owner) selfdestruct(owner); } } contract horizonNotariser2 is horizonMortalContract { /* Define variable to hold the notarised conversation hash */ bytes32[60000] conversationHash; uint256 usageCount = 0; /* update the hash of the conversation */ function notariseConversationHash(bytes32 _conversationHash, uint256 index) public { conversationHash[index] = _conversationHash; usageCount++; } /* Retrieve index for a given hash */ function getIndex(bytes32 _hash) public constant returns (uint256) { if (msg.sender == owner) { for(uint256 i = 0 ; i &lt; 60000 ; i++) { if(conversationHash[i]==_hash) { return i; } } } return 0; } /* Retrieve the current hash of the conversation */ function getConversation(uint256 index) public constant returns (bytes32) { if (msg.sender == owner) { return conversationHash[index]; } else { bytes32 empty; return empty; } } /* For maintenance purposes for us to check how many hashes are remaining before we need a new contract on the blockchain */ function getRemaining() public constant returns (uint256) { if (msg.sender == owner) { return (60000-usageCount); } else { return 0; } } }

Hashed Contract 

Removing dynamic types from the contract significantly reduces the cost of creation to 343666, but spread over the 60k conversations the impact of this reduction is not so obviously felt.  However, executing notarisation now falls to 63112 gas.  This is a further 77% cost saving on the previous contract. 

 

Finishing Touches 

Although the biggest gains have now been had, there are a few final optimisations we can make to get the best out of our contract: 

  • Track the index and remaining slots external to the contract. 
  • Check a slot is empty before using it to avoid bad actor scenarios and not wastefully execute SSTORE EVM codes when we don’t need to. 
  • Store the owner as a creation time constant in the contract. 
  • Use literals instead of constants when comparing to 0x0. 
  • Remove the inheritance, including the base contract in the main contract. 

The fully optimised contract below, “Fully Optimised,” includes these changes [ https://rinkeby.etherscan.io/address/0x093ec63a50aaa5b254a4da7e95c85229200c8940  ]. Creating this contract falls to 222030 – a further 35% saving in creation costs.  Execution of this notarisation is 43173 a further 32% saving.  This is about as low as we could theoretically go without making the contract meaningless.  ~21000 alone is incurred for a transfer. The remainder is mostly accounted for by an SSTORE EVM byte code of the hash in a variable on the blockchain. In total, compared to the “Expensive Contract” that we began with we have optimised gas usage by 95%! In dollar terms, at the current ETH<>USD rate of $858 and fast gas price of 8 Gwei this has brought our costs from $5.59 to $0.30 [as of 23/2/2018]

If you like the code, why not follow us on Twitter, LinkedIn and Facebook

pragma solidity ^0.4.17; contract horizonNotariser { /* Define variable owner of the type address */ address constant owner = 0x5c1bbad1246b7812027588e602b5e161273ad152; // your transaction account here // bytes32 constant zero = 0x0000000000000000000000000000000000000000000000000000000000000000; // extra 9 gas if you used this constant($1/10000) function kill() public { if (msg.sender == owner) selfdestruct(owner); } /* sha-256 hash of the conversation being notarised */ bytes32[60000] conversationHash; /* update the hash of the conversation */ function notariseConversationHash(bytes32 _conversationHash, uint256 index) public { if (msg.sender == owner &amp;&amp; conversationHash[index]==0x0000000000000000000000000000000000000000000000000000000000000000) { // to prevent bad actor conversationHash[index] = _conversationHash; } } /* Retrieve the current hash of the conversation */ function getConversation(uint256 index) public constant returns (bytes32) { if (msg.sender == owner) { return conversationHash[index]; } return 0x0000000000000000000000000000000000000000000000000000000000000000; } }

Fully Optimized 

Try It

Why not visit https://www.notar.ie/ and use our app (either Globex Call or Talketh) to try our notarisation solution!



Get Globex Call!



Notar.ie



Get Talketh!