Skip to content

Commit

Permalink
feat: introduce max_block_number (#5251)
Browse files Browse the repository at this point in the history
Part of #4761.

This adds a new validity condition to transactions called
`max_block_number`, causing them to fail if the current block is larger
than a requested max block. This can be used to construct proofs that
are only valid if included before a certain block (which is exactly how
SharedMutableStorage/SlowJoe/SlowUpdatesTree2.0 works).

---

I made `max_block_number` an `Option<u32>` both to not have to include a
initial value equal to the largest block, and also to avoid issues that
arise from abuse of `std::unsafe::zeroed`. Many parts of the stack
assume a (mostly) zeroed transaction is a valid one, but a
`max_block_number` value of 0 is not useful. With `Option`, a zeroed
value means no max block number was requested (`is_none()` returns
true), and this entire issue is avoided.

This property is initially set to `is_none()`, meaning there's no max
block number constraint. The `PrivateContext` now has a
`request_max_block_number` function that can be used to add constraints.
Each time a lower max block number is seen it replaces the current one.
The private kernel aggregates these across private calls and ends up
with the smallest one.

This value is stored in a new struct called `RollupValidationRequests`,
an extension from @LeilaWang's work in
#5236. These are
validation requests accumulated during private and public execution that
are forwarded to the rollup for it to check. Currently we only have
`max_block_number`, but there may be more. Note that we currently have a
slight duplication in the public kernal tail public inputs, but this is
expected to be sorted out very soon as this struct is refactored.

---

Note that in the end to end tests we're only testing that the sequencer
drops the transaction, but not that the base rollup rejects this
transaction (this is only tested in the rollup circuit unit tests).
Testing this would require bypassing the sequencer tx validation logic
and manually building a block, but this is a fairly involved endeavor
and one that our infrastructure does not currently easily support. I'm
still looking into a way to add this test.
  • Loading branch information
nventuro authored Mar 22, 2024
1 parent eea711f commit 6573173
Show file tree
Hide file tree
Showing 55 changed files with 783 additions and 58 deletions.
12 changes: 12 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,18 @@ jobs:
aztec_manifest_key: end-to-end
<<: *defaults_e2e_test

e2e-max-block-number:
docker:
- image: aztecprotocol/alpine-build-image
resource_class: small
steps:
- *checkout
- *setup_env
- run:
name: "Test"
command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_max_block_number.test.ts
aztec_manifest_key: end-to-end

e2e-multiple-accounts-1-enc-key:
steps:
- *checkout
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Function Context
## title: Function Context
---

# The Function Context
Expand Down Expand Up @@ -94,6 +94,14 @@ The return values are a set of values that are returned from an applications exe

return_values : BoundedVec<Field, RETURN_VALUES_LENGTH>,

## Max Block Number

Some data structures impose time constraints, e.g. they may make it so that a value can only be changed after a certain delay. Interacting with these in private involves creating proofs that are only valid as long as they are included before a certain future point in time. To achieve this, the `request_max_block_number` function can be used to set this property:

#include_code max-block-number /noir-projects/aztec-nr/aztec/src/context/private_context.nr rust

A transaction that requests a maximum block number will never be included in a block with a block number larger than the requested value, since it would be considered invalid. This can also be used to make transactions automatically expire after some time if not included.

### Read Requests

<!-- TODO(maddiaa): leaving as todo until their is further clarification around their implementation in the protocol -->
Expand Down
5 changes: 3 additions & 2 deletions l1-contracts/src/core/libraries/ConstantsGen.sol
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,12 @@ library Constants {
uint256 internal constant HEADER_LENGTH = 20;
uint256 internal constant L1_TO_L2_MESSAGE_LENGTH = 6;
uint256 internal constant L2_TO_L1_MESSAGE_LENGTH = 2;
uint256 internal constant MAX_BLOCK_NUMBER_LENGTH = 2;
uint256 internal constant NULLIFIER_KEY_VALIDATION_REQUEST_LENGTH = 4;
uint256 internal constant NULLIFIER_KEY_VALIDATION_REQUEST_CONTEXT_LENGTH = 5;
uint256 internal constant PARTIAL_STATE_REFERENCE_LENGTH = 6;
uint256 internal constant PRIVATE_CALL_STACK_ITEM_LENGTH = 208;
uint256 internal constant PRIVATE_CIRCUIT_PUBLIC_INPUTS_LENGTH = 205;
uint256 internal constant PRIVATE_CALL_STACK_ITEM_LENGTH = 210;
uint256 internal constant PRIVATE_CIRCUIT_PUBLIC_INPUTS_LENGTH = 207;
uint256 internal constant PUBLIC_CIRCUIT_PUBLIC_INPUTS_LENGTH = 198;
uint256 internal constant STATE_REFERENCE_LENGTH = 8;
uint256 internal constant TX_CONTEXT_DATA_LENGTH = 4;
Expand Down
12 changes: 11 additions & 1 deletion noir-projects/aztec-nr/aztec/src/context/private_context.nr
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
use dep::protocol_types::{
abis::{
call_context::CallContext, function_data::FunctionData, function_selector::FunctionSelector,
nullifier_key_validation_request::NullifierKeyValidationRequest,
max_block_number::MaxBlockNumber, nullifier_key_validation_request::NullifierKeyValidationRequest,
private_call_stack_item::PrivateCallStackItem,
private_circuit_public_inputs::PrivateCircuitPublicInputs,
public_call_stack_item::PublicCallStackItem,
Expand Down Expand Up @@ -46,6 +46,8 @@ struct PrivateContext {
args_hash : Field,
return_values : BoundedVec<Field, RETURN_VALUES_LENGTH>,

max_block_number: MaxBlockNumber,

note_hash_read_requests: BoundedVec<SideEffect, MAX_NOTE_HASH_READ_REQUESTS_PER_CALL>,
nullifier_read_requests: BoundedVec<ReadRequest, MAX_NULLIFIER_READ_REQUESTS_PER_CALL>,
nullifier_key_validation_requests: BoundedVec<NullifierKeyValidationRequest, MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_CALL>,
Expand Down Expand Up @@ -129,6 +131,7 @@ impl PrivateContext {
min_revertible_side_effect_counter,
args_hash,
return_values: BoundedVec::new(),
max_block_number: MaxBlockNumber::default(),
note_hash_read_requests: BoundedVec::new(),
nullifier_read_requests: BoundedVec::new(),
nullifier_key_validation_requests: BoundedVec::new(),
Expand Down Expand Up @@ -163,6 +166,7 @@ impl PrivateContext {
args_hash: self.args_hash,
return_values: self.return_values.storage,
min_revertible_side_effect_counter: self.min_revertible_side_effect_counter,
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,
nullifier_key_validation_requests: self.nullifier_key_validation_requests.storage,
Expand All @@ -188,6 +192,12 @@ impl PrivateContext {
self.min_revertible_side_effect_counter = self.side_effect_counter;
}

// docs:start:max-block-number
pub fn request_max_block_number(&mut self, max_block_number: u32) {
// docs:end:max-block-number
self.max_block_number = MaxBlockNumber::min_with_u32(self.max_block_number, max_block_number);
}

pub fn push_note_hash_read_request(&mut self, note_hash: Field) {
let side_effect = SideEffect { value: note_hash, counter: self.side_effect_counter };
self.note_hash_read_requests.push(side_effect);
Expand Down
23 changes: 21 additions & 2 deletions noir-projects/noir-contracts/contracts/test_contract/src/main.nr
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// A contract used for testing a random hodgepodge of small features from simulator and end-to-end tests.
contract Test {
use dep::aztec::prelude::{
AztecAddress, EthAddress, NoteHeader, NoteGetterOptions, NoteViewerOptions, PrivateContext,
PrivateImmutable, PrivateSet
AztecAddress, EthAddress, FunctionSelector, NoteHeader, NoteGetterOptions, NoteViewerOptions,
PrivateContext, PrivateImmutable, PrivateSet
};

use dep::aztec::protocol_types::{
Expand Down Expand Up @@ -63,6 +63,25 @@ contract Test {
context.this_address()
}

#[aztec(private)]
fn request_max_block_number(max_block_number: u32, enqueue_public_call: bool) {
// docs:start:request-max-block-number
context.request_max_block_number(max_block_number);
// docs:end:request-max-block-number

if enqueue_public_call {
let _ = context.call_public_function(
context.this_address(),
FunctionSelector::from_signature("dummy_public_call()"),
[]
);
}
}

#[aztec(public)]
#[aztec(internal)]
fn dummy_public_call() {}

#[aztec(private)]
fn call_create_note(value: Field, owner: AztecAddress, storage_slot: Field) {
assert(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ use dep::types::{
abis::{
call_request::CallRequest, accumulated_data::CombinedAccumulatedData,
kernel_circuit_public_inputs::PrivateKernelCircuitPublicInputsBuilder,
membership_witness::NoteHashReadRequestMembershipWitness,
max_block_number::MaxBlockNumber, membership_witness::NoteHashReadRequestMembershipWitness,
private_circuit_public_inputs::PrivateCircuitPublicInputs,
private_kernel::private_call_data::PrivateCallData, kernel_data::{PrivateKernelInnerData},
private_kernel::private_call_data::PrivateCallData, kernel_data::PrivateKernelInnerData,
side_effect::{SideEffect, SideEffectLinkedToNoteHash}
},
address::{AztecAddress, EthAddress, PartialAddress, compute_initialization_hash},
Expand Down Expand Up @@ -92,6 +92,7 @@ pub fn initialize_end_values(
public_inputs.min_revertible_side_effect_counter = previous_kernel.public_inputs.min_revertible_side_effect_counter;

let start = previous_kernel.public_inputs.validation_requests;
public_inputs.validation_requests.max_block_number = start.for_rollup.max_block_number;
public_inputs.validation_requests.note_hash_read_requests = array_to_bounded_vec(start.note_hash_read_requests);
public_inputs.validation_requests.nullifier_read_requests = array_to_bounded_vec(start.nullifier_read_requests);
public_inputs.validation_requests.nullifier_key_validation_requests = array_to_bounded_vec(start.nullifier_key_validation_requests);
Expand Down Expand Up @@ -180,6 +181,9 @@ pub fn update_end_values(

let storage_contract_address = private_call_public_inputs.call_context.storage_contract_address;

// Update the max block number if the private call requested a lower one.
public_inputs.validation_requests.max_block_number = MaxBlockNumber::min(public_inputs.validation_requests.max_block_number, private_call_public_inputs.max_block_number);

// Transient read requests and witnesses are accumulated in public_inputs.end
// We silo the read requests (domain separation per contract address)
let read_requests = private_call_public_inputs.note_hash_read_requests;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,23 @@ mod tests {
builder.failed();
}

#[test]
fn default_max_block_number() {
let mut builder = PrivateKernelInitInputsBuilder::new();
let public_inputs = builder.execute();

assert(public_inputs.validation_requests.for_rollup.max_block_number.is_none());
}

#[test]
fn propagate_max_block_number_request() {
let mut builder = PrivateKernelInitInputsBuilder::new();
builder.private_call.request_max_block_number(42);
let public_inputs = builder.execute();

assert_eq(public_inputs.validation_requests.for_rollup.max_block_number.unwrap(), 42);
}

#[test]
fn native_no_note_hash_read_requests_works() {
let mut builder = PrivateKernelInitInputsBuilder::new();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ mod tests {
use dep::types::{
abis::{
kernel_circuit_public_inputs::PrivateKernelInnerCircuitPublicInputs,
side_effect::{SideEffect, SideEffectLinkedToNoteHash}
max_block_number::MaxBlockNumber, side_effect::{SideEffect, SideEffectLinkedToNoteHash}
},
address::{AztecAddress, EthAddress}, hash::compute_logs_hash,
messaging::l2_to_l1_message::L2ToL1Message, utils::{arrays::array_length},
Expand Down Expand Up @@ -550,6 +550,36 @@ mod tests {
builder.failed();
}

#[test]
fn propagate_previous_kernel_max_block_number() {
let mut builder = PrivateKernelInnerInputsBuilder::new();
builder.previous_kernel.validation_requests.max_block_number = MaxBlockNumber::new(13);
let public_inputs = builder.execute();

assert_eq(public_inputs.validation_requests.for_rollup.max_block_number.unwrap(), 13);
}

#[test]
fn propagate_max_block_number_request() {
let mut builder = PrivateKernelInnerInputsBuilder::new();
builder.private_call.request_max_block_number(42);
let public_inputs = builder.execute();

assert_eq(public_inputs.validation_requests.for_rollup.max_block_number.unwrap(), 42);
}

#[test]
fn ignore_larger_max_block_number() {
let mut builder = PrivateKernelInnerInputsBuilder::new();
builder.previous_kernel.validation_requests.max_block_number = MaxBlockNumber::new(13);
// A private call requesting a larger max_block_number should not change the current one as that constraint is
// already satisfied.
builder.private_call.request_max_block_number(42);
let public_inputs = builder.execute();

assert_eq(public_inputs.validation_requests.for_rollup.max_block_number.unwrap(), 13);
}

#[test]
fn native_no_note_hash_read_requests_works() {
let mut builder = PrivateKernelInnerInputsBuilder::new();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ mod tests {
use dep::types::{
abis::{
kernel_circuit_public_inputs::PrivateKernelTailCircuitPublicInputs,
side_effect::{SideEffect, SideEffectLinkedToNoteHash, Ordered}
max_block_number::MaxBlockNumber, side_effect::{SideEffect, SideEffectLinkedToNoteHash, Ordered}
},
hash::compute_unique_siloed_note_hashes, tests::kernel_data_builder::PreviousKernelDataBuilder,
utils::{arrays::{array_eq, array_length}}, traits::{Empty, is_empty, is_empty_array}
Expand Down Expand Up @@ -452,6 +452,15 @@ mod tests {
builder.failed();
}

#[test]
fn propagate_previous_kernel_max_block_number() {
let mut builder = PrivateKernelTailInputsBuilder::new();
builder.previous_kernel.validation_requests.max_block_number = MaxBlockNumber::new(13);
let public_inputs = builder.execute();

assert_eq(public_inputs.rollup_validation_requests.max_block_number.unwrap(), 13);
}

#[test]
unconstrained fn one_pending_nullifier_read_request() {
let mut builder = PrivateKernelTailInputsBuilder::new();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ pub fn initialize_emitted_end_values(
circuit_outputs.end_non_revertible.new_nullifiers = array_to_bounded_vec(start_non_revertible.new_nullifiers);
circuit_outputs.end_non_revertible.public_data_update_requests = array_to_bounded_vec(start_non_revertible.public_data_update_requests);

// TODO - should be propagated only in initialize_end_values() and clear them in the tail circuit.
// TODO - should be propagated only in initialize_end_values() and clear them in the tail circuit. The
// max_block_number must be propagated to the rollup however as a RollupValidationRequest.
let start = previous_kernel.public_inputs.validation_requests;
circuit_outputs.validation_requests.max_block_number = start.for_rollup.max_block_number;
circuit_outputs.validation_requests.public_data_reads = array_to_bounded_vec(start.public_data_reads);
}

Expand All @@ -99,6 +101,7 @@ pub fn initialize_end_values(
circuit_outputs.end_non_revertible.public_call_stack = array_to_bounded_vec(start_non_revertible.public_call_stack);

let start = previous_kernel.public_inputs.validation_requests;
circuit_outputs.validation_requests.max_block_number = previous_kernel.public_inputs.validation_requests.for_rollup.max_block_number;
circuit_outputs.validation_requests.nullifier_read_requests = array_to_bounded_vec(start.nullifier_read_requests);
circuit_outputs.validation_requests.nullifier_non_existent_read_requests = array_to_bounded_vec(start.nullifier_non_existent_read_requests);
}
Expand Down Expand Up @@ -158,6 +161,7 @@ fn validate_call_requests<N>(
}

pub fn update_validation_requests(public_call: PublicCallData, circuit_outputs: &mut PublicKernelCircuitPublicInputsBuilder) {
// Note that the public kernel cannot modify the max block number value - it simply forwards it to the rollup
propagate_nullifier_read_requests(public_call, circuit_outputs);
propagate_nullifier_non_existent_read_requests(public_call, circuit_outputs);
propagate_valid_public_data_reads(public_call, circuit_outputs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ mod tests {
use dep::types::{
abis::{
call_request::CallRequest, function_selector::FunctionSelector,
kernel_circuit_public_inputs::PublicKernelCircuitPublicInputs, public_data_read::PublicDataRead,
public_data_update_request::PublicDataUpdateRequest, public_call_data::PublicCallData,
read_request::ReadRequest
kernel_circuit_public_inputs::PublicKernelCircuitPublicInputs, max_block_number::MaxBlockNumber,
public_data_read::PublicDataRead, public_data_update_request::PublicDataUpdateRequest,
public_call_data::PublicCallData, read_request::ReadRequest
},
address::{AztecAddress, EthAddress}, contract_class_id::ContractClassId,
contrakt::storage_read::StorageRead, hash::compute_logs_hash,
Expand Down Expand Up @@ -357,6 +357,8 @@ mod tests {
fn circuit_outputs_should_be_correctly_populated_with_previous_private_kernel() {
let mut builder = PublicKernelSetupCircuitPrivateInputsBuilder::new();

builder.previous_kernel.validation_requests.max_block_number = MaxBlockNumber::new(13);

builder.public_call.append_public_call_requests_for_regular_calls(2);
let storage = builder.public_call.public_call_stack.storage;

Expand Down Expand Up @@ -384,6 +386,7 @@ mod tests {

let public_inputs = kernel.public_kernel_setup();

assert_eq(public_inputs.validation_requests.for_rollup.max_block_number.unwrap(), 13);
assert_eq_call_requests(public_inputs.end.private_call_stack, []);
assert_eq_call_requests(
public_inputs.end_non_revertible.public_call_stack,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ impl BaseRollupInputs {
== self.constants.global_variables.version, "kernel version does not match the rollup version"
);

let rollup_validation_requests = self.kernel_data.public_inputs.rollup_validation_requests;

// Verify the max block number
// TODO #5345: why is block_number a Field and not u32?
if rollup_validation_requests.max_block_number.is_some() {
assert(
self.constants.global_variables.block_number as u32
<= rollup_validation_requests.max_block_number.unwrap_unchecked(), "kernel max_block_number is smaller than block number"
);
}

let commitments_tree_subroot = self.calculate_commitments_subtree();

let empty_commitments_subtree_root = calculate_empty_tree_root(NOTE_HASH_SUBTREE_HEIGHT);
Expand Down Expand Up @@ -1005,6 +1016,30 @@ mod tests {
builder.fails();
}

#[test(should_fail_with = "kernel max_block_number is smaller than block number")]
unconstrained fn constants_dont_satisfy_smaller_max_block_number() {
let mut builder = BaseRollupInputsBuilder::new();
builder.constants.global_variables.block_number = 42;
builder.kernel_data.set_max_block_number(5);
builder.fails();
}

#[test]
unconstrained fn constants_satisfy_equal_max_block_number() {
let mut builder = BaseRollupInputsBuilder::new();
builder.constants.global_variables.block_number = 42;
builder.kernel_data.set_max_block_number(42);
builder.succeeds();
}

#[test]
unconstrained fn constants_satisfy_larger_max_block_number() {
let mut builder = BaseRollupInputsBuilder::new();
builder.constants.global_variables.block_number = 42;
builder.kernel_data.set_max_block_number(4294967295);
builder.succeeds();
}

#[test]
unconstrained fn subtree_height_is_0() {
let outputs = BaseRollupInputsBuilder::new().execute();
Expand Down
2 changes: 2 additions & 0 deletions noir-projects/noir-protocol-circuits/crates/types/src/abis.nr
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ mod public_data_update_request;
mod accumulated_data;
mod validation_requests;

mod max_block_number;

mod private_kernel;
mod kernel_circuit_public_inputs;
mod kernel_data;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ impl PrivateKernelCircuitPublicInputsBuilder {
end_non_revertible,
end,
constants: self.constants,
rollup_validation_requests: self.validation_requests.to_rollup(),
needs_setup: end_non_revertible.needs_setup(),
needs_app_logic: end.needs_app_logic(),
needs_teardown: end_non_revertible.needs_teardown()
Expand Down
Loading

0 comments on commit 6573173

Please sign in to comment.