Unzen Fork
Unzen is an upcoming fork that introduces zk gas as a protocol-level budget for proving work. Instead of limiting blocks only by ordinary EVM gas, Unzen adds a separate weighted gas metric that reflects how expensive different opcodes and precompiles are to prove.
The objective is to cap proving time per block with a protocol constant, BLOCK_ZK_GAS_LIMIT, so a block cannot be valid if it would require more proving work than the protocol budget allows.
For the source protocol details, see the upstream ZK Gas specification.
EVM Version: Osaka
Unzen targets the Osaka EVM version. This will make the Cancun, Prague, and Osaka execution-layer features available to contracts.
The main contract-facing additions are:
- Cancun opcodes:
TSTORE,TLOAD,MCOPY,BLOBHASH, andBLOBBASEFEE - Prague precompiles: BLS12-381 operations at addresses
0x0bthrough0x11 - Osaka opcode:
CLZ(0x1e), exposed by Solidity inline assembly asclz - Osaka precompile:
P256VERIFYfor secp256r1 signature verification at address0x100
Block-Level ZK Gas Limit
Unzen defines BLOCK_ZK_GAS_LIMIT as the maximum cumulative zk gas allowed in a block.
The initial specification sets:
| Constant | Value |
|---|---|
BLOCK_ZK_GAS_LIMIT | 100,000,000 |
All transactions in a block are metered, including system and anchor transactions.
If cumulative zk gas exceeds the block limit during execution:
- the transaction that crossed the limit is aborted
- all state changes from that transaction are discarded
- transactions that completed before it are kept
- all remaining transactions in the block are skipped
This makes the zk gas limit a block validity constraint rather than a mempool policy or local prover preference.
Weighted Opcode Metering
zk gas is calculated by multiplying normal EVM gas spent by a proving-cost multiplier.
For ordinary opcodes, Unzen meters each opcode execution as:
zk_gas += step_gas * opcode_multiplier[opcode]step_gas is the gas charged by the EVM for a single opcode, including static cost and dynamic costs such as memory expansion or storage access charges.
The multiplier table is derived from per-opcode proving-time benchmarks. Expensive operations receive higher multipliers. For example, mulmod, div, mod, keccak256, and call receive larger weights than simple stack or memory operations.
Unlisted opcodes default to max(uint16) as a fail-safe, making missing multiplier entries immediately exceed practical limits.
Precompile Metering
Precompiles require explicit metering because they do not execute through the normal opcode loop.
When a CALL-family opcode invokes an active precompile:
- the CALL-family opcode is metered for opcode-side spawn overhead
- the precompile itself is metered separately
- the precompile multiplier is indexed by the low byte of the precompile address
The precompile charge is:
precompile_zk_gas = precompile_gas_used * precompile_multiplier[address]precompile_gas_used is the gas charged by the precompile's own gas schedule, excluding the CALL-family opcode's separate costs.
This keeps heavyweight precompiles such as modexp, point evaluation, BLAKE2F, and pairing operations represented in the block's proving budget.
Spawn Opcode Estimation
Spawn opcodes need special handling because they can create child execution frames and pre-charge parent gas with a child execution budget.
If Unzen used the observed parent opcode gas delta directly, the same child execution path could be counted twice:
- once through the parent's forwarded gas budget
- once through the child frame's actual opcode and precompile execution
Unzen avoids this by using fixed raw-gas estimates for spawn opcodes whenever a child frame is created or a precompile is invoked.
| Opcode | Estimated raw gas |
|---|---|
CALL | 12,500 |
CALLCODE | 12,500 |
DELEGATECALL | 3,500 |
STATICCALL | 3,500 |
CREATE | 37,000 |
CREATE2 | 44,500 |
Child-frame execution is still metered normally. The fixed parent-side estimate only replaces the parent opcode's observed gas delta so the forwarded child budget is not double counted.