Kernel Tracking
Gas and fees are tracked throughout the kernel circuits to ensure that users are charged correctly for their transactions.
Private Kernel Circuits Overview
On the private side, the ordering of the circuits is:
- PrivateKernelInit
- PrivateKernelInner
- PrivateKernelTail or PrivateKernelTailToPublic
The structs are (irrelevant fields omitted):
Private Context Initialization
Whenever a private function is run, it has a PrivateContext
associated with it, which is initialized in part from a PrivateContextInputs
object.
The gas settings that users specify become part of the values in the TxContext
within the PrivateContextInputs
of the entrypoint. These values are copied to the PrivateCircuitPublicInputs
.
The same TxContext
is provided as part of the TxRequest
in the PrivateKernelInitCircuitPrivateInputs
. This is done to ensure that the TxContext
in the PrivateCallData
(what was executed) matches the TxContext
in the TxRequest
(users' intent).
Private Kernel Init
The PrivateKernelInit circuit takes in a PrivateCallData
and a TxRequest
and outputs a PrivateKernelCircuitPublicInputs
.
It must:
- check that the
TxContext
provided as in theTxRequest
input matches theTxContext
in thePrivateCallData
- copy the
TxContext
from theTxRequest
to thePrivateKernelCircuitPublicInputs.constants.tx_context
- copy the
Header
from thePrivateCircuitPublicInputs
to thePrivateKernelCircuitPublicInputs.constants.historical_header
- set the min_revertible_side_effect_counter if it is present in the
PrivateCallData
- set the
fee_payer
if theis_fee_payer
flag is set in thePrivateCircuitPublicInputs
- set the
public_teardown_function_hash
if it is present in thePrivateCircuitPublicInputs
- set the
combined_constant_data.global_variables
to zero, since these are not yet known during private execution
Private Kernel Inner
The PrivateKernelInner circuit takes in a PrivateKernelData
and a PrivateCallData
and ultimately outputs a PrivateKernelCircuitPublicInputs
.
It must:
- set the
fee_payer
if theis_fee_payer
flag is set in thePrivateCircuitPublicInputs
(and is not set in the inputPrivateKernelData
) - set the
public_teardown_function_hash
if it is present in thePrivateCircuitPublicInputs
(and is not set in the inputPrivateKernelData
) - copy the constants from the
PrivateKernelData
to thePrivateKernelCircuitPublicInputs.constants
Private Kernel Tail
The PrivateKernelTail circuit takes in a PrivateKernelData
and outputs a KernelCircuitPublicInputs
(see diagram below).
This is only used when there are no enqueued public functions or public teardown functions.
It must:
- check that there are no enqueued public functions or public teardown function
- compute the gas used
- this will only include DA gas and any gas specified in the
teardown_gas_allocations
- this will only include DA gas and any gas specified in the
- ensure the gas used is less than the gas limits
- ensure that
fee_payer
is set, and set it in theKernelCircuitPublicInputs
- copy the constants from the
PrivateKernelData
to theKernelCircuitPublicInputs.constants
Transactions without a public component can safely set their teardown gas allocations to zero. They are included as part of the gas computation in the private kernel tail for consistency (limits always include teardown gas allocations) and future-compatibility if we have a need for private teardown functions.
Private Kernel Tail to Public
The PrivateKernelTailToPublic circuit takes in a PrivateKernelData
and outputs a PublicKernelCircuitPublicInputs
(see diagram below).
This is only used when there are enqueued public functions or a public teardown function.
It must:
- check that there are enqueued public functions or a public teardown function
- partition the side effects produced during private execution into revertible and non-revertible sets of
PublicAccumulatedData
- compute gas used for the revertible and non-revertible. Both sets can have a DA component, but the revertible set will also include the teardown gas allocations the user specified (if any). This ensures that the user effectively pre-pays for the gas consumed in teardown.
- ensure the gas used (across revertible and non-revertible) is less than the gas limits
- ensure that
fee_payer
is set, and set it in thePublicKernelCircuitPublicInputs
- set the
public_teardown_call_request
in thePublicKernelCircuitPublicInputs
- copy the constants from the
PrivateKernelData
to thePublicKernelCircuitPublicInputs.constants
Mempool/Node Validation
A Tx
broadcasted to the network has:
Tx {
/**
* Output of the private kernel circuit for this tx.
*/
data: PrivateKernelTailCircuitPublicInputs,
/**
* Proof from the private kernel circuit.
*/
proof: Proof,
/**
* Encrypted logs generated by the tx.
*/
encryptedLogs: EncryptedTxL2Logs,
/**
* Unencrypted logs generated by the tx.
*/
unencryptedLogs: UnencryptedTxL2Logs,
/**
* Enqueued public functions from the private circuit to be run by the sequencer.
* Preimages of the public call stack entries from the private kernel circuit output.
*/
enqueuedPublicFunctionCalls: PublicCallRequest[],
/**
* Public teardown function from the private circuit to be run by the sequencer.
* Preimage of the public teardown function hash from the private kernel circuit output.
*/
publicTeardownFunctionCall: PublicCallRequest,
}
Where the PrivateKernelTailCircuitPublicInputs
may be destined for the base rollup (if there is no public component), or the public kernel circuits (if there is a public component).
Regardless, it has a fee_payer
set.
When a node receives a transaction, it must check that:
- the
fee_payer
is set - the
fee_payer
has a balance of FPA greater than the computed transaction fee if the transaction has no public component - the
fee_payer
has a balance of FPA greater than the computed max transaction fee if the transaction has a public component
See other validity conditions.
Public Kernel Circuits
On the public side, the order of the circuits is:
- PublicKernelSetup
- PublicKernelAppLogic
- PublicKernelTeardown
- PublicKernelTail
The structs are (irrelevant fields omitted):
Public Context Initialization
Whenever a public function is run, it has a PublicContext
associated with it, which is initialized in part from a PublicContextInputs
object.
The sequencer must provide information including the current gas_fees
, the current gas_left
, and the transaction_fee
, but we cannot trust these values to be correct: we must compute the correct values in the public kernel circuits, and validate that the sequencer provided the correct values.
Further, the sequencer is only obligated to provide the transaction_fee
to the teardown function, as that is the only point at which the transaction fee can be known.
Public Circuit Public Inputs
The "outputs" of the public functions are coming from the public VM.
Therefore, once we verify that the start_gas_left
which the sequencer provided is correct, we can trust the end_gas_left
that the public VM reports.
Further, we can trust that the transaction_fee
the public VM reported is the one which was made available to the public functions during teardown (though we must verify that the sequencer provided the correct value).
The PublicCircuitPublicInputs
include the global_variables
as injected via the PublicContextInputs
. The first public kernel circuit to run, regardless of whether it is a setup, app, or teardown kernel, is responsible for setting its constant_data.global_variables
equal to these. All subsequent public kernel circuit runs must verify that the global_variables
from the PublicCircuitPublicInputs
match the ones from the previously set constant_data.global_variables
.
Public Kernel Setup
The PublicKernelSetup circuit takes in a PublicKernelData
and a PublicCallData
and outputs a PublicKernelCircuitPublicInputs
.
It must assert that the revert_code
in the PublicCircuitPublicInputs
is equal to zero.
It must assert that the public_call.call_stack_item.public_inputs.global_variables.gas_fees
are valid according to the update rules defined.
It must compute the gas used in the PublicKernelData
provided, and verify that the gas_limits
in the PublicKernelData
's TxContext
minus the computed gas_used
is equal to the start_gas_left
specified on the PublicCircuitPublicInputs
.
This ensures that the public VM was provided with the correct starting gas values.
It must update the gas used in end_non_revertible
as:
# assume the previous PublicKernelCircuitPublicInputs was copied to circuit_outputs
pub fn update_non_revertible_gas_used(public_call: PublicCallData, circuit_outputs: &mut PublicKernelCircuitPublicInputsBuilder) {
let tx_gas_limits = circuit_outputs.constants.tx_context.gas_settings.gas_limits;
let call_gas_left = public_call.call_stack_item.public_inputs.end_gas_left;
let accum_end_gas_used = circuit_outputs.end.gas_used;
circuit_outputs.end_non_revertible.gas_used = tx_gas_limits
.sub(call_gas_left)
.sub(accum_end_gas_used);
}
Within the AVM, users may specify gas limits for each public function call. This does not apply to the "top-level" enqueued call: they all pull from the same global gas limit, and there is no way to "catch" an "out of gas" at this top-level apart from reverting.
Public Kernel App Logic
The PublicKernelAppLogic circuit takes in a PublicKernelData
and a PublicCallData
and outputs a PublicKernelCircuitPublicInputs
.
It must perform the same computation as the PublicKernelSetup regarding verification of the start_gas_left
and the gas_fees
.
It must check the revert_code
in the PublicCircuitPublicInputs
.
If the revert_code
is zero
Instead of updating end_non_revertible
, it must update end
as:
# assume the previous PublicKernelCircuitPublicInputs was copied to circuit_outputs
pub fn update_revertible_gas_used(public_call: PublicCallData, circuit_outputs: &mut PublicKernelCircuitPublicInputsBuilder) {
let tx_gas_limits = circuit_outputs.constants.tx_context.gas_settings.gas_limits;
let call_gas_left = public_call.call_stack_item.public_inputs.end_gas_left;
let accum_end_non_revertible_gas_used = circuit_outputs.end_non_revertible.gas_used;
circuit_outputs.end.gas_used = tx_gas_limits
.sub(call_gas_left)
.sub(accum_end_non_revertible_gas_used);
}
If the revert_code
is non-zero
All side effects from the revertible
set are discarded.
It consumes all the gas left:
# assume the previous PublicKernelCircuitPublicInputs was copied to circuit_outputs
pub fn update_revertible_gas_used(public_call: PublicCallData, circuit_outputs: &mut PublicKernelCircuitPublicInputsBuilder) {
let tx_gas_limits = circuit_outputs.constants.tx_context.gas_settings.gas_limits;
let accum_end_non_revertible_gas_used = circuit_outputs.end_non_revertible.gas_used;
circuit_outputs.end.gas_used = tx_gas_limits
.sub(accum_end_non_revertible_gas_used);
}
It sets the revert_code
in PublicKernelCircuitPublicInputs
to 1
.
Recall in the Private Kernel Tail to Public circuit, the gas allocated for the public teardown function was included in the end
gas used. This ensures that we have gas available for teardown even though app logic consumed all gas.
Consuming all gas left in the event of revert creates incentives for the sequencer to arrange transactions such that they revert, which is suboptimal. Future improvements will likely address this by only consuming the gas that was actually used, even in the event of revert.
Public Kernel Teardown
The PublicKernelTeardown circuit takes in a PublicKernelData
and a PublicCallData
and outputs a PublicKernelCircuitPublicInputs
.
It must perform the same computation as the PublicKernelSetup regarding verification of the gas_fees
.
It must assert that the start_gas_left
is equal to the PublicKernelData's public_inputs.constants.tx_context.gas_settings.teardown_gas_allocations
It must also compute the gas used in the PublicKernelData
provided, and the transaction fee using this computed value, then verify that the transaction_fee
in the PublicCircuitPublicInputs
is equal to the computed transaction fee.
This ensures that the public VM was provided with the correct transaction fee, and that teardown did not exceed the gas limits.
Handling reverts
Teardown is attempted even if the app logic failed.
The teardown kernel can see if the app logic failed by checking if revert_code
in the PublicKernelCircuitPublicInputs
is set to 1
.
It also has access to the revert_code
reported by the AVM of the current call within PublicCircuitPublicInputs
.
The interplay between these two revert_code
s is as follows:
Kernel revert_code | current AVM revert_code | Resulting Kernel revert_code |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
0 | 1 | 2 |
1 | 1 | 3 |
2 or 3 | (any) | (unchanged) |
Rollup Kernel Circuits
The base rollup kernel circuit takes in a KernelData
, which contains a KernelCircuitPublicInputs
, which it uses to compute the transaction_fee
.
Additionally, it verifies that the max fees per gas specified by the user are greater than the current block's fees per gas. It also verifies the constant_data.global_variables.gas_fees
are correct.
After the public data writes specific to this transaction have been processed, and a new tree root is produced, the kernel circuit injects an additional public data write based upon that root which deducts the transaction fee from the fee_payer
's balance.
The calculated trasaction fee is set as output on the base rollup as accumulated_fees
. Each subsequent merge rollup circuit sums this value from both of its inputs. The root rollup circuit then uses this value to set the total_fees
in the Header
.