for loop in Gravity (write about the one in checkValidatorSignatures)
Loop optimizations can be applied on many loops in the code. Let's take the for loop in the checkValidatorSignatures function for example.
for (uint256 i = 0; i < _currentValidators.length; i++) {
// If v is set to 0, this signifies that it was not possible to get a signature from this validator and we skip evaluation
// (In a valid signature, it is either 27 or 28)
if (_v[i] != 0) {
// Check that the current validator has signed off on the hash
require(
verifySig(_currentValidators[i], _theHash, _v[i], _r[i], _s[i]),
"Validator signature does not match."
);
// Sum up cumulative power
cumulativePower = cumulativePower + _currentPowers[i];
// Break early to avoid wasting gas
if (cumulativePower > _powerThreshold) {
break;
}
}
}
we can apply multiple optimizations to this loop:
Variables in solidity are already initialized to their default value, and initializing them to the same value actually costs more gas. So for example in the loop above, the code can be optimized using uint i; instead of uint i = 0;.
Use ++i instead of i++ to save some gas spent in every iteration.
Save the array length in a local variable before the loop instead of accessing it in every iteration.
Save _v[i] in a local variable instead of accessing the i element of the array twice in every iteration.
so after applying all these changes, the code will look something like this:
uint length = _currentValidators.length;
for (uint256 i; i < length; ++i) {
// If v is set to 0, this signifies that it was not possible to get a signature from this validator and we skip evaluation
// (In a valid signature, it is either 27 or 28)
uint8 v = _v[i];
if (v != 0) {
// Check that the current validator has signed off on the hash
require(
verifySig(_currentValidators[i], _theHash, v, _r[i], _s[i]),
"Validator signature does not match."
);
// Sum up cumulative power
cumulativePower = cumulativePower + _currentPowers[i];
// Break early to avoid wasting gas
if (cumulativePower > _powerThreshold) {
break;
}
}
}
Optimizations of this kind can be done to other loops in the code, and will reduce their cost.
Redundant initializing of cumulativePower in the checkValidatorSignatures function (uint256 cumulativePower = 0; - line 231)
Use the calldata modifier instead of memory when passing arrays as function parameters to reduce the gas cost of the function call
Combine the checkValidatorSignatures and isOrchestrator functions to avoid 2 loops (the new function can be a different function from checkValidatorSignatures to avoid checking it whenever the isOrchestrator check is not needed)
Save the length in the updateValset function instead of accessing it 4 times (every access is 3 memory touches). This can be done also in the submitBatch and submitLogicCall functions.
Use return instead if break in the checkValidatorSignatures function to prevent checking the same condition twice (once in the if statement inside the loop and the other one in the require)
function checkValidatorSignatures(
// The current validator set and their powers
address[] memory _currentValidators,
uint256[] memory _currentPowers,
// The current validator's signatures
uint8[] memory _v,
bytes32[] memory _r,
bytes32[] memory _s,
// This is what we are checking they have signed
bytes32 _theHash,
uint256 _powerThreshold
) private pure {
uint256 cumulativePower = 0;
for (uint256 i = 0; i < _currentValidators.length; i++) {
// If v is set to 0, this signifies that it was not possible to get a signature from this validator and we skip evaluation
// (In a valid signature, it is either 27 or 28)
if (_v[i] != 0) {
// Check that the current validator has signed off on the hash
require(
verifySig(_currentValidators[i], _theHash, _v[i], _r[i], _s[i]),
"Validator signature does not match."
);
// Sum up cumulative power
cumulativePower = cumulativePower + _currentPowers[i];
// Break early to avoid wasting gas
if (cumulativePower > _powerThreshold) {
break; // use return instead
}
}
}
// Check that there was enough power
require(
cumulativePower > _powerThreshold,
"Submitted validator set signatures do not have enough power."
);
// Success
}
Redundant initialization of state_lastValsetNonce to zero - uint256 public state_lastValsetNonce = 0;
Use type(uint256).max instead of the storage variable MAX_UINT in CosmosToken to save gas (this optimization saves an SLOAD and reduces the gas of that operation)
Use a custom function that uses ++ (prefix) instead of state_lastEventNonce = state_lastEventNonce.add(1)
Save an SLOAD by saving the state_gravityId variable in a local variable in the updateValset function
old code:
// Check that the supplied current validator set matches the saved checkpoint
require(
makeCheckpoint(_currentValset, state_gravityId) == state_lastValsetCheckpoint,
"Supplied current validators and powers do not match checkpoint."
);
require(
isOrchestrator(_currentValset, msg.sender),
"The sender of the transaction is not validated orchestrator"
);
// Check that enough current validators have signed off on the new validator set
bytes32 newCheckpoint = makeCheckpoint(_newValset, state_gravityId);
new code:
bytes32 gravityId = state_gravityId;
// Check that the supplied current validator set matches the saved checkpoint
require(
makeCheckpoint(_currentValset, gravityId) == state_lastValsetCheckpoint,
"Supplied current validators and powers do not match checkpoint."
);
require(
isOrchestrator(_currentValset, msg.sender),
"The sender of the transaction is not validated orchestrator"
);
// Check that enough current validators have signed off on the new validator set
bytes32 newCheckpoint = makeCheckpoint(_newValset, gravityId);
Gas Optimizations
for loop in Gravity (write about the one in
checkValidatorSignatures
)Loop optimizations can be applied on many loops in the code. Let's take the for loop in the
checkValidatorSignatures
function for example.we can apply multiple optimizations to this loop:
uint i;
instead ofuint i = 0;
._v[i]
in a local variable instead of accessing the i element of the array twice in every iteration.so after applying all these changes, the code will look something like this:
Optimizations of this kind can be done to other loops in the code, and will reduce their cost.
Redundant initializing of cumulativePower in the
checkValidatorSignatures
function (uint256 cumulativePower = 0;
- line 231)Use the calldata modifier instead of memory when passing arrays as function parameters to reduce the gas cost of the function call
Combine the
checkValidatorSignatures
andisOrchestrator
functions to avoid 2 loops (the new function can be a different function fromcheckValidatorSignatures
to avoid checking it whenever theisOrchestrator
check is not needed)Save the length in the
updateValset
function instead of accessing it 4 times (every access is 3 memory touches). This can be done also in thesubmitBatch
andsubmitLogicCall
functions.old code:
new code:
Save 2 SLOADs in updateValset function by saving the state_lastEventNonce variable
old code:
new code:
Use return instead if break in the
checkValidatorSignatures
function to prevent checking the same condition twice (once in the if statement inside the loop and the other one in the require)Redundant initialization of
state_lastValsetNonce
to zero -uint256 public state_lastValsetNonce = 0;
Use
type(uint256).max
instead of the storage variableMAX_UINT
inCosmosToken
to save gas (this optimization saves an SLOAD and reduces the gas of that operation)Use a custom function that uses ++ (prefix) instead of
state_lastEventNonce = state_lastEventNonce.add(1)
Save an SLOAD by saving the
state_gravityId
variable in a local variable in theupdateValset
functionold code:
new code: