When optimizing for cost, there’s a law of diminishing returns. So if you want to save money, it’s important to understand where your biggest costs come from.
(These tips apply to Ethereum. We learned them the hard way as we were creating the smart contract for Storm4.)
Tip 1 — Storage is expensive
When you store data in the blockchain you’re storing it forever. It’s not going to disappear next month if you don’t pay your storage bill. This means the Ethereum network wants you to pay upfront for a lifetime of storage. In other words, storage isn’t cheap.
This is highlighted if you look at the costs per op-code for the EVM (ethereum virtual machine). You can see that most ops (ADD, SUB, XOR, etc) cost around 3 gas. Expensive ops like a SHA3 hash cost maybe 40–50 gas. But storing data has a starting cost of 20,000 gas!
A single SSTORE op (a write to persistent storage on the blockchain) can only store up to 32 bytes, and costs 20,000 gas per op.
Consider what must be stored, and ask yourself if there’s a way to reduce its size.
Sometimes this involves thinking outside the blockchain. For example, imagine you’re trying to store a blob of data. Does the data itself need to be stored on the blockchain? Or can you instead store a “proof” of the data? For example, you could store a hash of the data on the blockchain, and store the data itself somewhere else that’s publicly readable.
Tip 2 — Mappings are often cheaper than arrays
A mapping (otherwise known as a hash-map, hash-table, dictionary, etc) is often cheaper than an array. I had assumed it would be the other way around. But I was wrong.
When I think of a mapping, I think about storing both the key & value. But in Ethereum mappings, only the value is stored, NOT the key. Confused ? Yeah, I was too. There’s a great write-up that explains how it works:
The 10,000 foot overview is this: (paraphrased from linked articles)
- The EVM storage for a contract is like a near-infinite ticker tape, and each slot of the tape holds 32 bytes.
- The length of the tape is 2²⁵⁶, or ~10⁷⁷ storage slots per contract.
- This is unimaginably big. In fact, the number of particles in the observable universe is 10⁸⁰.
- So the key is hashed (along with some other stuff) to get a unique slot on the ticker tape for it’s storage.
- We don’t have to worry about collisions because the address space is so ridiculously big.
In other words, storing a new item in a mapping can be a single SSTORE op! This in in contract to storing a new item in an array, which requires the same SSTORE op to write the item, plus an additional write to update the length of the array.
Tip 3 — Transactions are expensive
There is a base cost to perform a transaction: 21,000 gas.
Note: A “transaction” is an invocation into Ethereum that potentially modifies the blockchain. Similar to a read-write transaction in a database system. You have to pay for these. In contrast, a “call” is an invocation that cannot modify the blockchain. It’s the equivalent of a read-only transaction in a database system. These are free.
This means it’s considerably cheaper to batch multiple operations into a single transaction.
If the flow of your application supports batching operations, then updating your smart contract to make it possible could save you a lot of operations & money over the long run.
Tip 4 — Transaction parameters aren’t packed
How many bytes of data are required to invoke your method ? (i.e. when your function parameters are serialized, how many bytes does it take up ?) It’s important to know because you have to pay for each byte using a formula:
- Gtxdatazero = 4 gas (paid for every zero byte of parameter data for a transaction)
- Gtxdatanonzero = 68 gas (paid for every non-zero byte of parameter data for a transaction)
For example, consider the following function:
function setUserData(bytes32 userID, bytes32 userData)
That’s 64 bytes of (presumably) non-zero data: 64 * 68 = 4,352 gas
Now for the tricky question. Consider the following function:
function addMerkleTreeRoot(bytes32 merkleTreeRoot, bytes20[] userIDs)
What’s the cost of the transaction data to invoke this method with 10 userID’s ? The answer surprised me. (I’m going to ignore some other complexity here in order to focus on the lesson at hand.)
It turns out that the userIDs
parameter is NOT packed! In other words, each value in the array (bytes20
) goes into it’s own 32 byte slot. And you have to pay for the unused 12 bytes.
- merkleTreeRoot = 32 * 68 = 2,176 gas
- userIDs = (10 * 20 * 68) + (10 * 12 * 4) = 14,080 gas
This is rather wasteful. But it can be fixed. Instead you can pass in a “packed array”:
function addMerkleTreeRoot(bytes32 merkleTreeRoot, bytes userIDsPacked)
You can then unpack the values within the contract:
uint numUserIDs = userIDsPacked.length / 20;
for (uint i = 0; i < numUserIDs; i++)
{
bytes20 userID;
assembly {
userID := mload(add(userIDsPacked, add(32, mul(20, i))))
}
// ...
}
Tip 5— You can re-broadcast a transaction with a different gasPrice
There’s a bunch of idiots out there who spend a lot of time talking about what the “current gasPrice” is, and what gasPrice you must use. Ignore these people like the plague.
The fact is:
- you can initially broadcast your transaction with gasPrice A
- and then later replace that transaction with a new transaction using gasPrice B (assuming A hasn’t been mined already)
So if you can write code, you can play the market.
- Start by broadcasting your transaction with a low gasPrice
- Check back later to see if it’s been mined
- If not mined, re-broadcast the same transaction (i.e. with the same nonce value), but with a slightly higher gasPrice
- Slowly ramp up your gasPrice using whatever formula you’d like
The reality is that the gasPrice rises and falls throughout the day/week depending on many factors. And one of these factors is certainly how many other transactions are pending. But ultimately it comes down the the miners. When a particular miner wins, they get to select which transactions to mine. There’s a large pool of pending transactions, and they only have room for so many. So obviously they’re going to pick the transactions that will give them the most money.
If you broadcast a transaction with a low gasPrice during a busy period, you might not make the cut. But an hour later, that same transaction could easily get picked.
If you absolutely MUST get your transaction mined this instant, then you’re going to have to pay the market rate at that moment. This is no different than any other market. But if that’s NOT the case, then there’s no reason why you shouldn’t play the game. All it takes is a little bit of thought & a little bit of code.
Source: Crypto New Media