Gas and limits
This page collects the numbers: what each operation costs in gas, how the project stops those costs creeping up, how large each contract is allowed to be, and every fixed limit written into the contracts. All figures come from files in the repository; none are estimates.
The gas snapshot
blockchain/gas-snapshot.json holds the gas used by each tracked operation, measured on Hardhat's in-process network by blockchain/test/gas/measure-operations.ts. The figures are transaction gas (receipt.gasUsed), including the 21,000 base cost and calldata.
| Operation | Gas | What is measured |
|---|---|---|
addIssuer | 75,343 | Admin registers an issuer with a short name |
registerType | 150,590 | Issuer registers its first custom type ("Workshop", "A short workshop") |
issue | 238,790 | Issuer issues a credential with no expiry, built-in type 1 |
issueExpiring | 238,917 | The same with a validUntil |
suspend | 44,519 | Issuer suspends an active credential (reason 5) |
reinstate | 44,682 | Issuer reinstates it |
supersede | 76,397 | Issuer supersedes it with another active credential |
revoke | 42,265 | Issuer revokes an active credential |
adminHold | 66,189 | Admin suspends a credential (sets the admin hold) |
issueReplacing | 284,333 | Issue and supersede the old credential in one transaction |
supersedeToRegistry | 69,555 | Issuer points a credential at a replacement in another set |
redeem | 137,402 | Student wallet redeems an invitation |
rebind | 103,590 | A new wallet takes over the enrolment |
revokeInvitations | 34,767 | Issuer cancels outstanding invitations |
unbind | 36,203 | Issuer removes the binding |
setTypeActive | 32,070 | Issuer switches its custom type off |
pause | 46,965 | Guardian pauses the set |
unpause | 27,101 | Admin unpauses |
suspendIssuer | 52,190 | Guardian suspends an issuer (setIssuerSuspended) |
removeIssuer | 29,502 | Admin removes an issuer |
accessAdmitFirst | 279,791 | Holder admits the first member (creates the Semaphore group) |
accessAdmitLater | 201,669 | Holder admits the second member |
accessProve | 330,178 | A member proves access (group of three, tree depth 2) |
accessRemove | 138,405 | Holder removes the third member |
retireIssuer, setIssuerName, bindCredentialRegistry, role changes and the timelock are not in the snapshot. Gas depends on input sizes (names, signatures), so other inputs give other figures. Nothing in v0.3 grows with stored data: blockchain/test/v03/BoundedWork.test.ts measures every write with 1, 100 and 1,000 items already stored and requires the gas to stay flat.
The same replay also records calldata size and zero bytes per operation (calldataStats in blockchain/scripts/lib/network-cost.ts), which the network evaluation tooling (npm run network:measure, npm run network:report) uses to compare chains.
How regressions are caught
| Rule | Source |
|---|---|
Tolerance is 10 percent either way (GAS_TOLERANCE = 0.1) | blockchain/scripts/lib/gas-snapshot.ts |
| A regression above +10 percent or a missing operation always fails | blockchain/test/gas/GasSnapshot.test.ts |
| An improvement beyond 10 percent or a new operation fails in CI ("out of date") so a later regression cannot hide behind the gain; locally it is only logged | Same |
The snapshot is refreshed locally with npm run gas:snapshot (GAS_SNAPSHOT_WRITE=true) and committed; writing it in CI is refused | Same |
| The check is skipped under coverage, where instrumented code costs far more | Same |
npm run gas:check runs this test alone. npm run gas runs the whole suite with hardhat-gas-reporter (REPORT_GAS=true) and writes gas-report.txt, which CI uploads as an artifact. Details are on Invariants and gas.
Contract size budgets
Ethereum refuses to deploy runtime code above 24,576 bytes (EIP-170). The project sets a tighter budget for new contracts to keep headroom for later revisions.
| Group | Budget | Checked by |
|---|---|---|
Contracts compiled directly in blockchain/contracts/ (that is AcademicRecords) | 24,576 bytes (EIP_170_LIMIT) | npm run sizes (blockchain/scripts/check-sizes.ts), in CI |
Contracts compiled directly in blockchain/contracts/v03/ and blockchain/contracts/zk/ | 20,000 bytes (V03_BUDGET) | Same; also ContractSize.test.ts and AccessGate.test.ts |
Runtime sizes from the local Hardhat build of the current sources (optimizer on, 200 runs, viaIR):
| Contract | Runtime bytes |
|---|---|
AcademicRecords | 15,732 |
RecordStorage (library) | 5,588 |
StudentManagement | 2,861 |
IssuerRegistry | 6,484 |
CredentialRegistry | 4,761 |
StatusRegistry | 7,696 |
HolderRegistry | 4,762 |
RecordTypeRegistry | 3,510 |
CertichainTimelock | 5,448 |
VersionAnnouncer | 4,875 |
CertichainAccessGate | 4,719 |
npm run sizes prints the same figures after a compile.
Every hard limit
v0.3 registries
| Limit | Value | Constant | Contract |
|---|---|---|---|
| Issuer name length | 1 to 128 bytes | MAX_NAME_BYTES | IssuerRegistry |
| Key lineage depth | 8 hops | MAX_LINEAGE_HOPS | IssuerRegistry |
issuedAt ahead of block time | 5 minutes | MAX_FUTURE_DRIFT | CredentialRegistry |
| Anchor layout version | 1 | SCHEMA_VERSION | CredentialRegistry |
| Nonce reuse | Once per issuer | _usedNonces | CredentialRegistry |
| Reserved status reasons | 8 to 999 refused in issuer calls | RESERVED_FROM, ISSUER_DEFINED_FROM | StatusRegistry |
| Invitation lifetime | Expiry at most 30 days after the redeeming block | MAX_INVITATION_TTL | HolderRegistry |
| Enrolments per wallet | One per issuer | _enrolments | HolderRegistry |
| Built-in record types | Ids 0 to 31 | LAST_BUILT_IN_TYPE | RecordTypeRegistry |
| Reserved record types | Ids 32 to 999 | (gap before FIRST_CUSTOM_TYPE) | RecordTypeRegistry |
| First custom record type | 1000 | FIRST_CUSTOM_TYPE | RecordTypeRegistry |
| Record type name | 1 to 64 bytes | MAX_NAME_BYTES | RecordTypeRegistry |
| Record type description | At most 256 bytes | MAX_DESCRIPTION_BYTES | RecordTypeRegistry |
| Set label | 1 to 32 bytes | MAX_LABEL_BYTES | VersionAnnouncer |
| Admin transfer delay | Chosen at deployment; scripts default to 3 days | DEFAULT_ADMIN_DELAY_SEC (script) | IssuerRegistry, StatusRegistry |
Access gate
| Limit | Value | Constant |
|---|---|---|
| Members per credential, over its whole life | 64 | MAX_MEMBERS |
| Tree depth served | 6 | MAX_DEPTH_SERVED |
| Epoch length | 1 day | EPOCH_SECONDS |
| Accepted epochs | Current, previous and next | _checkProof |
| Old group root still accepted | 10 minutes after it changes | GROUP_ROOT_VALIDITY |
| Receipts per member | One per credential per epoch (nullifier) | _spent |
Timelock (deploy script)
| Limit | Value | Constant |
|---|---|---|
| Minimum delay on a public chain | 1 hour | MIN_DELAY_FLOOR_SEC |
| Maximum delay | 30 days | MAX_DELAY_SEC |
Legacy v0.2
| Limit | Value | Where |
|---|---|---|
| Sharees per record | 32 | MAX_SHAREES_PER_RECORD in RecordStorage |
| Contract size | EIP-170 | check-sizes.ts |
The v0.2 lookups by student wallet and the per-student delete loop over stored records, so their cost grows with data; the 32 sharee cap bounds the work per record.
Source files
blockchain/gas-snapshot.jsonblockchain/test/gas/measure-operations.ts,blockchain/test/gas/GasSnapshot.test.tsblockchain/scripts/lib/gas-snapshot.ts,blockchain/scripts/lib/network-cost.tsblockchain/scripts/check-sizes.ts,blockchain/scripts/lib/contract-size.tsblockchain/hardhat.config.ts,blockchain/scripts/lib/solidity-profiles.ts
Tests
blockchain/test/gas/GasSnapshot.test.ts,OperationRecords.test.ts,gas-snapshot-lib.test.ts,network-cost-lib.test.ts,network-fees-file.test.tsblockchain/test/v03/ContractSize.test.ts,blockchain/test/v03/BoundedWork.test.ts