owned this note
owned this note
Published
Linked with GitHub
# Stuck and exited keys reporting to CSM gas estimation
### What makes up an expected gas cost?
- intrinsic transaction gas cost;
- calldata cost;
- gas for a function call execution;
### Intrinsic gas cost
Is fixed and is 21000 gas.
### Calldata cost
It depends on a size and structure of the calldata. As of time of writing calldata costs 4 gas per byte equal to 0, and
16 gas for the others. Both functions have the same interface:
```solidity
function SIGNATURE(bytes calldata ids, bytes calldata counts) {}
```
In both functions `ids` and `counts` are sequences of 8 byte sizes items.
In the worst case scenario a calldata for a single NO-count entry will take 2 * 8 * 16 = 256 gas (excluding a cost for
ABI encoded pointers to the parameters).
:::warning
It is 16 bytes per entry in `counts` at the moment, but it seems to be decreased since there is 2^64 limit of keys
count for a node operator in the module. It should not change overall numbers somehow drastically.
:::
### Function call execution
The both functions read the data from the calldata directly, so non-linear gas cost growth with regard to memory
expansion isn't expected. In addition the functions aren't depend on external calls. Given that we can simply
extrapolate single entry call cost to get the cost for multiple entries.
The results for single entry call for both items are the following as of time of writing:
```console
$ forge test --mt '(updateExitedValidatorsCount|updateStuckValidatorsCount)_NonZero' --gas-report
```
| src/CSModule.sol:CSModule contract | |
|------------------------------------|----------|
| Function Name | max |
| updateExitedValidatorsCount | 48004 |
| updateStuckValidatorsCount | 24824 |
The `updateExitedValidatorsCount` consumes more gas because it updates the module-level counters as well as recomputes
counters for a single node operator. It biases the linear gas estimation a little as will be shown below.
A synthetic test shows the gas usage for 1000 entries being reported:
:::spoiler Different estimations
Plain foundry gas report.
| Function Name | max |
|------------------------------------|----------|
| updateExitedValidatorsCount | 24539488 |
| updateStuckValidatorsCount | 23215610 |
Using emo-eth/forge-gas-metering:
| Function Name | usage |
|------------------------------------|----------|
| updateExitedValidatorsCount | 25176162 |
| updateStuckValidatorsCount | 23368977 |
Using a call on a fork (100 entries)
| Function Name | usage |
|------------------------------------|----------|
| updateExitedValidatorsCount | 3764331 |
| updateStuckValidatorsCount | 2763446 |
:::
Using a call on a fork (1000 entries)
| Function Name | usage |
|------------------------------------|----------|
| updateExitedValidatorsCount |~37194373 |
| updateStuckValidatorsCount | 27372938 |
Which gives as the following estimations per reported entry:
:::warning
Outdated!
:::
| Function Name | max |
|------------------------------------|----------|
| updateExitedValidatorsCount | 24539 |
| updateStuckValidatorsCount | 23215 |
:::warning
NB! The function execution gas cost doesn't include calldata gas cost and intrinsic gas cost as far as I know.
:::
### Conclusion
Given the calculations above we can expect the following numbers for gas usage:
| Function name / entries | 1 | 10 | 100 | 1000 | 2700 | 5000 |
|-----------------------------|--------|---------|-----------|------------|------------|-------------|
| updateExitedValidatorsCount | 69'260 | 268'950 | 2'500'500 | 24'816'000 | 66'967'500 | 123'996'000 |
| updateStuckValidatorsCount | 46'080 | 255'710 | 2'368'100 | 23'492'000 | 63'392'700 | 117'376'000 |
### Script
```javascript
// $ yarn add viem # only once
// $ make deploy-local
// $ node test.js
import { createPublicClient, http, getContract, parseEther } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
import crypto from "crypto";
import CSModule from "./out/CSModule.sol/CSModule.json" assert { type: "json" };
import deploy from "./out/deploy-mainnet.json" assert { type: "json" };
const client = createPublicClient({
chain: mainnet,
transport: http("http://127.0.0.1:8545"),
});
const account = privateKeyToAccount(process.env.DEPLOYER_PRIVATE_KEY);
const csm = getContract({
address: deploy.CSModule,
abi: CSModule.abi,
client,
});
async function prepareNodeOperatorsSet(o = 1, k = 1) {
// Create tons of node operators.
for (let i = 0; i < o; i++) {
await csm.write.addNodeOperatorETH(
[k, toHex(randomUint8Array(48 * k)), toHex(randomUint8Array(96 * k))],
{
value: parseEther(`${2 * k}`),
address: csm.address,
account,
},
);
await csm.write.updateTargetValidatorsLimits([i, true, k], {
address: csm.address,
account,
});
await csm.write.vetKeys([i, k], {
address: csm.address,
account,
});
}
// Move validators to the deposited state.
// 100 keys -> 7'785'804 of gas
for (let i = 0; i < o * k; i += 100) {
await csm.write.obtainDepositData([100, ""], {
address: csm.address,
account,
});
}
}
const n = 100;
let gas;
const cur = await csm.read.getNodeOperatorsCount({
address: csm.address,
client,
});
if (cur == 0) {
await prepareNodeOperatorsSet(n, 1);
}
gas = await csm.estimateGas.updateStuckValidatorsCount(
[
`0x${[...Array(n).keys()]
.map((e) => e.toString(16).padStart(16, 0))
.join("")}`,
`0x${"00000000000000000000000000000001".repeat(n)}`,
],
{
address: csm.address,
account,
},
);
console.log("updateStuckValidatorsCount", gas);
gas = await csm.estimateGas.updateExitedValidatorsCount(
[
`0x${[...Array(n).keys()]
.map((e) => e.toString(16).padStart(16, 0))
.join("")}`,
`0x${"00000000000000000000000000000001".repeat(n)}`,
],
{
address: csm.address,
account,
},
);
console.log("updateExitedValidatorsCount", gas);
function randomUint8Array(length) {
return crypto.getRandomValues(new Uint8Array(length));
}
function toHex(t) {
return "0x" + Buffer.from(t).toString("hex");
}
```