Skip to content

Commit

Permalink
feat: plumb fee payer (#6286)
Browse files Browse the repository at this point in the history
## Implemented

[See
spec](https://docs.aztec.network/protocol-specs/gas-and-fees/specifying-gas-fee-info).

A boolean flag `is_fee_payer` in the PrivateCircuitPublicInputs. The
private kernel circuits will check this flag for every call stack item.

When a call stack item is found with `is_fee_payer` set, the kernel
circuit will set `fee_payer` in its PrivateKernelCircuitPublicInputs to
be the callStackItem.contractAddress.

This is subsequently passed through the PublicKernelCircuitPublicInputs
to the KernelCircuitPublicInputs.

If a transaction attempts to set fee_payer multiple times, the
transaction will be considered invalid.

## Deviations

Whereas in the spec we said we would use `contract_address`, we have
updated that to use `storage_contract_address` because it correctly
handles delegate calls: if `is_fee_payer` gets set during delegate call,
the "delegator" should be the `fee_payer`, not the "delegatee".

## Remaining

Actually setting `fee_payer` in a contract, and making assertions.
(#5920)

If the fee_payer is not set, the transaction will be considered invalid.
(#6343)

## Elsewhere

Unit tests on SerDe
(#6339)
  • Loading branch information
just-mitch committed May 13, 2024
1 parent 2633cfc commit 1f8fd1c
Show file tree
Hide file tree
Showing 42 changed files with 337 additions and 61 deletions.
6 changes: 3 additions & 3 deletions docs/docs/protocol-specs/gas-and-fees/fee-schedule.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The [transaction fee](./specifying-gas-fee-info.md#transaction-fee) is comprised of a DA component, an L2 component, and an inclusion fee. The DA and L2 components are calculated by multiplying the gas consumed in each dimension by the respective `feePerGas` value. The inclusion fee is a fixed cost associated with the transaction, which is used to cover the cost of verifying the encompassing rollup proof on L1.

# DA Gas
## DA Gas

DA gas is consumed to cover the costs associated with publishing data associated with a transaction.

Expand Down Expand Up @@ -48,7 +48,7 @@ da_gas_used = FIXED_DA_GAS +
A side effect of the above calculation is that all transactions will have a non-zero `transaction_fee`.
:::

# L2 Gas
## L2 Gas

L2 gas is consumed to cover the costs associated with executing the public VM, proving the public VM circuit, and proving the public kernel circuit.

Expand Down Expand Up @@ -77,7 +77,7 @@ In the current implementation, private execution does not consume L2 gas. This w
- possibly emitting logs (due to validation checks)
:::

# Max Inclusion Fee
## Max Inclusion Fee

Each transaction, and each block, has inescapable overhead costs associated with it which are not directly related to the amount of data or computation performed.

Expand Down
8 changes: 4 additions & 4 deletions docs/docs/protocol-specs/gas-and-fees/kernel-tracking.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ title: 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
## Private Kernel Circuits Overview

On the private side, the ordering of the circuits is:

Expand Down Expand Up @@ -213,7 +213,7 @@ It must:
- set the `public_teardown_call_request` in the `PublicKernelCircuitPublicInputs`
- copy the constants from the `PrivateKernelData` to the `PublicKernelCircuitPublicInputs.constants`

# Mempool/Node Validation
## Mempool/Node Validation

A `Tx` broadcasted to the network has:

Expand Down Expand Up @@ -260,7 +260,7 @@ When a node receives a transaction, it must check that:

See other [validity conditions](../transactions/validity.md).

# Public Kernel Circuits
## Public Kernel Circuits

On the public side, the order of the circuits is:

Expand Down Expand Up @@ -538,7 +538,7 @@ The interplay between these two `revert_code`s is as follows:
| 1 | 1 | 3 |
| 2 or 3 | (any) | (unchanged) |

# Base Rollup Kernel Circuit
## Base Rollup Kernel Circuit

The base rollup kernel circuit takes in a `KernelData`, which contains a `KernelCircuitPublicInputs`, which it uses to compute the `transaction_fee`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ title: Published Gas & Fee Data

When a block is published to L1, it includes information about the gas and fees at a block-level, and at a transaction-level.

# Block-level Data
## Block-level Data

The block header contains a `GlobalVariables`, which contains a `GasFees` object. This object contains the following fields:
- `feePerDaGas`: The fee in [FPA](./fee-payment-asset.md) per unit of DA gas consumed for transactions in the block.
Expand All @@ -22,7 +22,7 @@ In the future, these values may be updated dynamically based on network conditio
Should we move to a 1559-style fee market with block-level gas targets, there is an interesting point where gas "used" presently includes the entire [`teardown_gas_allocation`](./specifying-gas-fee-info.md) regardless of how much of that allocation was spent. In the future, if this becomes a concern, we can update our accounting to reflect the true gas used for the purposes of updating the `GasFees` object, though the user will be charged the full `teardown_gas_allocation` regardless.
:::

# Transaction-level Data
## Transaction-level Data

The transaction data which is published to L1 is a `TxEffects` object, which includes
- `transaction_fee`: the fee paid by the transaction in FPA
Expand Down
14 changes: 7 additions & 7 deletions docs/docs/protocol-specs/gas-and-fees/specifying-gas-fee-info.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ GasSettings --> GasFees
All fees are denominated in the [Fee Payment Asset (FPA)](./fee-payment-asset.md).
:::

# Gas Dimensions and Max Inclusion Fee
## Gas Dimensions and Max Inclusion Fee

Transactions are metered for their gas consumption across two dimensions:

Expand All @@ -58,15 +58,15 @@ Separately, every transaction has overhead costs associated with it, e.g. verify
See the [Fee Schedule](./fee-schedule.md) for a detailed breakdown of costs associated with different actions.


# `gasLimits` and `teardownGasLimits`
## `gasLimits` and `teardownGasLimits`

Transactions can optionally have a "teardown" phase as part of their public execution, during which the "transaction fee" is available to public functions. This is useful to transactions/contracts that need to compute a "refund", e.g. contracts that facilitate [fee abstraction](./tx-setup-and-teardown.md).

Because the transaction fee must be known at the time teardown is executed, transactions must effectively "prepay" for the teardown phase. Thus, the `teardownGasLimits` are portions of the `gasLimits` that are reserved for the teardown phase.

For example, if a transaction has `gasLimits` of 1000 DA gas and 2000 L2 gas, and `teardownGasLimits` of 100 DA gas and 200 L2 gas, then the transaction will be able to consume 900 DA gas and 1800 L2 gas during the main execution phase, but 100 DA gas and 200 L2 gas **will be consumed** to cover the teardown phase: even if teardown does not consume that much gas, the transaction will still be charged for it; even if the transaction does not have a teardown phase, the gas will still be consumed.

# `maxFeesPerGas` and `feePerGas`
## `maxFeesPerGas` and `feePerGas`

The `maxFeesPerGas` field specifies the maximum fees that the user is willing to pay per gas unit consumed in each dimension.

Expand Down Expand Up @@ -97,7 +97,7 @@ A transaction cannot be executed if the `maxFeesPerGas` is less than the `feePer

The `feePerGas` is presently held constant at `1` for both dimensions, but may be updated in future protocol versions.

# Transaction Fee
## Transaction Fee

The transaction fee is calculated as:

Expand All @@ -111,7 +111,7 @@ Why is the "max" inclusion fee charged? We're working on a mechanism that will a

See more on how the "gas consumed" values are calculated in the [Fee Schedule](./fee-schedule.md).

# Maximum Transaction Fee
## Maximum Transaction Fee

The final transaction fee cannot be calculated until all public function execution is complete. However, a maximum theoretical fee can be calculated as:

Expand All @@ -121,15 +121,15 @@ maxTransactionFee = maxInclusionFee + (gasLimits.daGas * maxFeesPerDaGas) + (gas

This is useful for imposing [validity conditions](./kernel-tracking.md#mempoolnode-validation).

# `fee_payer`
## `fee_payer`

The `fee_payer` is the entity that pays the transaction fee.

It is effectively set in private by the contract that calls `context.set_as_fee_payer()`.

This manifests as a boolean flag `is_fee_payer` in the `PrivateCircuitPublicInputs`. The private kernel circuits will check this flag for every call stack item.

When a call stack item is found with `is_fee_payer` set, the kernel circuit will set `fee_payer` in its `PrivateKernelCircuitPublicInputs` to be the `callStackItem.contractAddress`.
When a call stack item is found with `is_fee_payer` set, the kernel circuit will set `fee_payer` in its `PrivateKernelCircuitPublicInputs` to be the `callContext.storageContractAddress`.

This is subsequently passed through the `PublicKernelCircuitPublicInputs` to the `KernelCircuitPublicInputs`.

Expand Down
10 changes: 5 additions & 5 deletions docs/docs/protocol-specs/gas-and-fees/tx-setup-and-teardown.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The public teardown phase is the only phase where the final transaction fee is a

In the base rollup, the kernel circuit injects a public data write that levies the transaction fee on the `fee_payer`.

# An example: Fee Abstraction
## An example: Fee Abstraction

Consider a user, Alice, who does not have FPA but wishes to interact with the network. Suppose she has a private balance of a fictitious asset "BananaCoin" that supports public and private balances.

Expand All @@ -58,11 +58,11 @@ Suppose there is a Fee Payment Contract (FPC) that has been deployed by another

This illustrates the utility of the various phases. In particular, we see why the setup phase must not be revertible: if Alice's public app logic fails, the FPC is still going to pay the fee in the base rollup; if public setup were revertible, the transfer of Alice's BananaCoin would revert so the FPC would be losing money.

# Sequencer Whitelisting
## Sequencer Whitelisting

Because a transaction is invalid if it fails in the public setup phase, sequencers are taking a risk by processing them. To mitigate this risk, it is expected that sequencers will only process transactions that use public functions that they have whitelisted.

# Defining Setup
## Defining Setup

The private function that is executed first is referred to as the "entrypoint".

Expand All @@ -76,15 +76,15 @@ Execution of the entrypoint is always verified/processed by the `PrivateKernelIn

It is only the `PrivateKernelInit` circuit that looks at the `min_revertible_side_effect_counter` as reported by `PrivateCirclePublicInputs`, and thus it is only the entrypoint that can effectively call `context.end_setup()`.

# Defining Teardown
## Defining Teardown

At any point during private execution, a contract may call `context.set_public_teardown_function` to specify a public function that will be called during the public teardown phase. This function takes the same arguments as `context.call_public_function`, but does not have a side effect counter associated with it.

Similar to `call_public_function`, this results in the hash of a `PublicCallStackItem` being set on `PrivateCircuitPublicInputs` as `public_teardown_function_hash`.

The private kernel circuits will verify that this hash is set at most once.

# Interpreting the `min_revertible_side_effect_counter`
## Interpreting the `min_revertible_side_effect_counter`

Notes, nullifiers, and logs are examples of side effects that are partitioned into setup and app logic.

Expand Down
2 changes: 1 addition & 1 deletion l1-contracts/src/core/libraries/ConstantsGen.sol
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ library Constants {
uint256 internal constant TX_REQUEST_LENGTH = 2 + TX_CONTEXT_LENGTH + FUNCTION_DATA_LENGTH;
uint256 internal constant HEADER_LENGTH = APPEND_ONLY_TREE_SNAPSHOT_LENGTH
+ CONTENT_COMMITMENT_LENGTH + STATE_REFERENCE_LENGTH + GLOBAL_VARIABLES_LENGTH;
uint256 internal constant PRIVATE_CIRCUIT_PUBLIC_INPUTS_LENGTH = CALL_CONTEXT_LENGTH + 3
uint256 internal constant PRIVATE_CIRCUIT_PUBLIC_INPUTS_LENGTH = CALL_CONTEXT_LENGTH + 4
+ MAX_BLOCK_NUMBER_LENGTH + (READ_REQUEST_LENGTH * MAX_NOTE_HASH_READ_REQUESTS_PER_CALL)
+ (READ_REQUEST_LENGTH * MAX_NULLIFIER_READ_REQUESTS_PER_CALL)
+ (NULLIFIER_KEY_VALIDATION_REQUEST_LENGTH * MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_CALL)
Expand Down
4 changes: 4 additions & 0 deletions noir-projects/aztec-nr/aztec/src/context/private_context.nr
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ struct PrivateContext {
side_effect_counter: u32,

min_revertible_side_effect_counter: u32,
is_fee_payer: bool,

args_hash: Field,
return_hash: Field,
Expand Down Expand Up @@ -115,6 +116,7 @@ impl PrivateContext {
inputs,
side_effect_counter,
min_revertible_side_effect_counter,
is_fee_payer: false,
args_hash,
return_hash: 0,
max_block_number: MaxBlockNumber::empty(),
Expand Down Expand Up @@ -159,6 +161,7 @@ impl PrivateContext {
args_hash: self.args_hash,
returns_hash: self.return_hash,
min_revertible_side_effect_counter: self.min_revertible_side_effect_counter,
is_fee_payer: self.is_fee_payer,
max_block_number: self.max_block_number,
note_hash_read_requests: self.note_hash_read_requests.storage,
nullifier_read_requests: self.nullifier_read_requests.storage,
Expand Down Expand Up @@ -608,6 +611,7 @@ impl Empty for PrivateContext {
inputs: PrivateContextInputs::empty(),
side_effect_counter: 0 as u32,
min_revertible_side_effect_counter: 0 as u32,
is_fee_payer: false,
args_hash: 0,
return_hash: 0,
max_block_number: MaxBlockNumber::empty(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ impl KernelCircuitPublicInputsComposer {

self.propagate_sorted_arrays();

self.propagate_fee_payer();

// TODO: Should be done in a reset circuit.
self.squash_transient_data();

Expand Down Expand Up @@ -230,6 +232,10 @@ impl KernelCircuitPublicInputsComposer {
self.public_inputs.public_teardown_call_request = self.previous_kernel.public_inputs.public_teardown_call_request;
}

fn propagate_fee_payer(&mut self) {
self.public_inputs.fee_payer = self.previous_kernel.public_inputs.fee_payer;
}

fn squash_transient_data(&mut self) {
verify_squashed_transient_note_hashes_and_nullifiers(
self.public_inputs.end.new_note_hashes.storage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ impl PrivateKernelCircuitPublicInputsComposer {

public_inputs.constants = previous_kernel_public_inputs.constants;
public_inputs.min_revertible_side_effect_counter = previous_kernel_public_inputs.min_revertible_side_effect_counter;
public_inputs.fee_payer = previous_kernel_public_inputs.fee_payer;
public_inputs.public_teardown_call_request = previous_kernel_public_inputs.public_teardown_call_request;

let start = previous_kernel_public_inputs.validation_requests;
public_inputs.validation_requests.max_block_number = start.for_rollup.max_block_number;
Expand All @@ -71,8 +73,6 @@ impl PrivateKernelCircuitPublicInputsComposer {
let _call_request = public_inputs.end.private_call_stack.pop();
public_inputs.end.public_call_stack = array_to_bounded_vec(start.public_call_stack);

public_inputs.public_teardown_call_request = previous_kernel_public_inputs.public_teardown_call_request;

PrivateKernelCircuitPublicInputsComposer { public_inputs }
}

Expand Down Expand Up @@ -105,6 +105,7 @@ impl PrivateKernelCircuitPublicInputsComposer {
self.propagate_private_call_requests(source);
self.propagate_public_call_requests(source);
self.propagate_public_teardown_call_request(source);
self.propagate_fee_payer(source);

*self
}
Expand Down Expand Up @@ -220,4 +221,11 @@ impl PrivateKernelCircuitPublicInputsComposer {
self.public_inputs.public_teardown_call_request = call_request;
}
}

fn propagate_fee_payer(&mut self, source: DataSource) {
if (source.private_call_public_inputs.is_fee_payer) {
assert(self.public_inputs.fee_payer.is_zero(), "Cannot overwrite non-empty fee_payer");
self.public_inputs.fee_payer = source.storage_contract_address;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,20 @@ mod tests {
request.contract_address, builder.private_call.public_inputs.call_context.storage_contract_address
);
}

#[test]
unconstrained fn propagate_fee_payer() {
let mut builder = PrivateKernelInitInputsBuilder::new();
let fee_payer = builder.private_call.public_inputs.call_context.storage_contract_address;
builder.private_call.public_inputs.is_fee_payer = true;

let public_inputs = builder.execute();
assert_eq(public_inputs.fee_payer, fee_payer);

// Check that the fee payer is not set if is_fee_payer is false
let mut builder = PrivateKernelInitInputsBuilder::new();
assert_eq(builder.private_call.public_inputs.is_fee_payer, false);
let public_inputs = builder.execute();
assert_eq(public_inputs.fee_payer, AztecAddress::empty());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -248,4 +248,37 @@ mod tests {
assert_eq(public_inputs.end.encrypted_logs_hashes[1].value, encrypted_logs_hash);
assert_eq(public_inputs.end.unencrypted_logs_hashes[1].value, unencrypted_logs_hash);
}

#[test]
unconstrained fn propagate_fee_payer() {
let mut builder = PrivateKernelInnerInputsBuilder::new();
let fee_payer = builder.private_call.public_inputs.call_context.storage_contract_address;
builder.private_call.public_inputs.is_fee_payer = true;
let public_inputs = builder.execute();
assert_eq(public_inputs.fee_payer, fee_payer);

// Check that the fee payer is not set if is_fee_payer is false
let mut builder = PrivateKernelInnerInputsBuilder::new();
assert_eq(builder.private_call.public_inputs.is_fee_payer, false);
let public_inputs = builder.execute();
assert_eq(public_inputs.fee_payer, AztecAddress::empty());

// Check that we carry forward if the fee payer is already set
let mut builder = PrivateKernelInnerInputsBuilder::new();
let fee_payer = AztecAddress::from_field(123);
builder.previous_kernel.fee_payer = fee_payer;
let public_inputs = builder.execute();
assert_eq(public_inputs.fee_payer, fee_payer);
}

#[test(should_fail_with="Cannot overwrite non-empty fee_payer")]
unconstrained fn does_not_overwrite_fee_payer() {
let mut builder = PrivateKernelInnerInputsBuilder::new();
let original_fee_payer = AztecAddress::from_field(123);

builder.private_call.public_inputs.is_fee_payer = true;
builder.previous_kernel.fee_payer = original_fee_payer;

builder.failed();
}
}
Loading

0 comments on commit 1f8fd1c

Please sign in to comment.