Skip to content

Commit fd509a3

Browse files
docs: some tail circuit doc comments (#20449)
Doc comments for the tail circuit Oh, and I changed a name of a function (and hence the file that it lived in).
2 parents f3420e0 + 521d581 commit fd509a3

File tree

9 files changed

+62
-39
lines changed

9 files changed

+62
-39
lines changed

cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@
374374
"unshift",
375375
"unshifted",
376376
"unsiloed",
377+
"unsquashable",
377378
"unsynched",
378379
"unvalidated",
379380
"unzipit",

noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/components/previous_kernel_for_tail_validator.nr

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
mod validate_accumulated_items;
2-
mod validate_no_transient_data;
2+
mod validate_unsquashable_note_hash_nullifier_ordering;
33
mod validate_siloed_values;
44

55
use types::{
@@ -8,8 +8,8 @@ use types::{
88
traits::Empty,
99
};
1010
use validate_accumulated_items::validate_accumulated_items;
11-
use validate_no_transient_data::validate_no_transient_data;
1211
use validate_siloed_values::validate_siloed_values;
12+
use validate_unsquashable_note_hash_nullifier_ordering::validate_unsquashable_note_hash_nullifier_ordering;
1313

1414
/// Validate the previous kernel for a private tail, either to_rollup or to_public.
1515
/// `is_for_public` should be known at compile time.
@@ -31,10 +31,14 @@ pub fn validate_previous_kernel_for_tail(
3131
"min_revertible_side_effect_counter must not be 0 for tail_to_public",
3232
);
3333
} else {
34-
// The `min_revertible_side_effect_counter` could be anything for a pure private tx, as it's not used in the
35-
// tail circuit or any circuits before the tail.
34+
// For a private-only tx, `min_revertible_side_effect_counter` can be 0 or non-zero - it doesn't
35+
// affect correctness. The reset circuit uses 0 as the split counter for private-only txs, so the
36+
// revertible/non-revertible boundary is never applied regardless of this value.
3637
}
3738

39+
// Here, we finally reconcile that the "claimed" revertible counter (as hinted by all app circuits
40+
// and kernel iterations of this tx) actually reconciles with the min_revertible_side_effect counter
41+
// that was set by one of the app circuits of this tx.
3842
assert_eq(
3943
previous_kernel_public_inputs.claimed_revertible_counter,
4044
min_revertible_side_effect_counter,
@@ -64,7 +68,7 @@ pub fn validate_previous_kernel_for_tail(
6468
// --- fee_payer ---
6569

6670
let fee_payer = previous_kernel_public_inputs.fee_payer;
67-
// TODO: use assert_not_empty after Noir #9002.
71+
6872
assert(!fee_payer.is_empty(), "Fee payer can't be empty");
6973

7074
// --- expiration_timestamp ---
@@ -91,6 +95,8 @@ pub fn validate_previous_kernel_for_tail(
9195
let first_nullifier = previous_kernel_public_inputs.end.nullifiers.array[0];
9296
assert_eq(
9397
claimed_first_nullifier,
98+
// Note: we assert that this first_nullifier is siloed within `validate_siloed_values` below,
99+
// by asserting that the contract_address is 0.
94100
first_nullifier.innermost().value,
95101
"First nullifier claim was not satisfied",
96102
);
@@ -117,9 +123,10 @@ pub fn validate_previous_kernel_for_tail(
117123
validate_siloed_values(note_hashes, "note hashes");
118124
validate_siloed_values(nullifiers, "nullifiers");
119125
validate_siloed_values(private_logs, "private logs");
126+
120127
l2_to_l1_msgs.for_each(|msg| msg.innermost().recipient.validate());
121128

122-
validate_no_transient_data(previous_kernel_public_inputs);
129+
validate_unsquashable_note_hash_nullifier_ordering(previous_kernel_public_inputs);
123130

124131
for private_log in private_logs.array {
125132
assert(

noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/components/previous_kernel_for_tail_validator/validate_accumulated_items.nr

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@ pub fn validate_accumulated_items(
5656
// We require the entire contract class log hash to be empty because the contract address is exposed to the public.
5757
assert_trimmed_array(contract_class_logs_hashes, |log_hash| log_hash.is_empty());
5858

59-
// For `public_call_requests`, we only need to ensure that the trimmed items are nullish. Items emitted from private
60-
// functions are guaranteed to be non-empty - their `msg_sender` is either the caller's contract address (which
59+
// For `public_call_requests`, we only need to ensure that the trimmed items are nullish (i.e.
60+
// we don't need to ensure denseness here). Items emitted from private functions are _guaranteed_
61+
// to be non-empty because their `msg_sender` is either the caller's contract address (which
6162
// cannot be 0) or `NULL_MSG_SENDER_CONTRACT_ADDRESS`.
6263
// See `private_call_data_validator.nr > validate_public_call_request` for details.
6364
if is_for_public {

noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/components/previous_kernel_for_tail_validator/validate_siloed_values.nr

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use types::{address::AztecAddress, side_effect::Scoped, utils::arrays::ClaimedLengthArray};
22

33
/// Ensure that the data has been properly siloed in the reset circuit.
4-
/// Since the siloing is always performed from index 0, we only need to check the last item (within the claimed length).
4+
/// Since the siloing is always performed from index 0 within the reset circuit, we only need to
5+
/// check the last item (within the claimed length).
56
/// If the last item is siloed, then the entire array is siloed.
6-
/// A siloed item has a zero contract address, and unsiloed item must not have a zero contract address.
7+
/// A siloed item has a zero contract address, and an unsiloed item must not have a zero contract address.
78
/// See `reset_output_validator.nr` for how siloing is performed.
89
pub fn validate_siloed_values<T, let N: u32, let S: u32>(
910
array: ClaimedLengthArray<Scoped<T>, N>,

noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/components/previous_kernel_for_tail_validator/validate_no_transient_data.nr renamed to noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/components/previous_kernel_for_tail_validator/validate_unsquashable_note_hash_nullifier_ordering.nr

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,27 @@ use types::{
33
constants::MAX_NULLIFIERS_PER_TX, side_effect::Ordered, utils::arrays::find_first_index,
44
};
55

6-
// This is mainly for ensuring that for any nullifier that links to a note hash,
7-
// it is created _after_ the note hash.
8-
// This is enforced for transient data when they are squashed in the reset circuit.
9-
// But if a pair is not transient, their counters will be checked here.
10-
// Why would we have a (nullifier, pending note) pair that is non-transient?
11-
// When a pending note hash is non-revertible and its nullifier is revertible, we can't
12-
// squash them, but we still need to perform this check on their counters.
13-
// A nice side effect of this check is that it also makes sure all the transient data is squashed:
14-
// In aztec-nr, if a contract is emitting a nullifier for a non-revertible note
15-
// hash, or if it doesn't want to squash the note hash at all (to keep a full record
16-
// of what had happened, for example), it could set the nullifier.note_hash to be
17-
// the _siloed_ note hash (or not set it at all).
18-
// When we run this function (in the tail), because the non-squashed note hashes
19-
// are already siloed in the reset circuit, the nullifiers that map to non-transient
20-
// note hashes will match up with those _siloed_ note hashes. But for nullifiers
21-
// that should already have been squashed against a transient (not siloed) note
22-
// hash, they won't be able to find a match.
23-
pub fn validate_no_transient_data(previous_kernel_public_inputs: PrivateKernelCircuitPublicInputs) {
6+
/// This is mainly for ensuring that for any nullifier that links to a note hash,
7+
/// it is created _after_ the note hash.
8+
/// This is enforced for transient data when they are squashed in the reset circuit.
9+
/// But if a pair is not transient, their counters will be checked here.
10+
/// Why would we have a (nullifier, pending note) pair that is non-transient?
11+
/// When a pending note hash is non-revertible and its nullifier is revertible, we can't
12+
/// squash them, but we still need to perform this check on their counters.
13+
///
14+
/// A nice side effect of this check is that it also makes sure all the transient data is squashed:
15+
/// In aztec-nr, if a contract is emitting a nullifier for a non-revertible note
16+
/// hash, or if it doesn't want to squash the note hash at all (to keep a full record
17+
/// of what had happened, for example), it could set the nullifier.note_hash to be
18+
/// the _siloed_ note hash (or not set it at all).
19+
/// When we run this function (in the tail), because the non-squashed note hashes
20+
/// are already siloed in the reset circuit, the nullifiers that map to non-transient
21+
/// note hashes will match up with those _siloed_ note hashes. But for nullifiers
22+
/// that should already have been squashed against a transient (not siloed) note
23+
/// hash, they won't be able to find a match.
24+
pub fn validate_unsquashable_note_hash_nullifier_ordering(
25+
previous_kernel_public_inputs: PrivateKernelCircuitPublicInputs,
26+
) {
2427
// Safety: the below hints are constrained by the following methods. See private_kernel_inner for use.
2528
let note_hash_index_for_each_nullifier =
2629
unsafe { get_note_hash_index_for_each_nullifier(previous_kernel_public_inputs) };
@@ -41,7 +44,18 @@ pub fn validate_no_transient_data(previous_kernel_public_inputs: PrivateKernelCi
4144
note_hash.counter() < nullifier.counter(),
4245
"Cannot link a note hash emitted after a nullifier",
4346
);
44-
// No need to verify logs linked to a note hash are squashed.
47+
// Note: we don't need to assert that the contract_addresses of the note_hash and
48+
// nullifier match, because by this point (after the final reset circuit) they are all
49+
// siloed, and hence their contract_addresses will be 0.
50+
51+
// Note: A nullifier's note_hash field could reference a siloed note_hash from a
52+
// different contract. This is harmless: the note_hash field is discarded in
53+
// expose_to_public() and never reaches the rollup. Cross-contract squashing is
54+
// prevented by the reset circuit's contract_address check in
55+
// validate_squashable_note_hash_nullifier_pair. The only effect of the link here
56+
// is a counter-ordering check, which has no security implication.
57+
58+
// Note: No need to verify logs linked to a note hash are squashed.
4559
// When a note hash is squashed, all associated logs are guaranteed to be removed.
4660
// See reset/transient_data/transient_data_validator.nr for details.
4761
}
@@ -56,14 +70,13 @@ pub fn validate_no_transient_data(previous_kernel_public_inputs: PrivateKernelCi
5670
// For each nullifier, this array gives the index of that note_hash in the new note_hashes array.
5771
//
5872
// note_hashes: [ C0, C1, C2, C3, C4, C5, C6, C7]
59-
// nullifiers: [N(C5), N, N(C3), N, N, 0, 0, 0] <-- Notes C5 and C3 are transient notes.
73+
// nullifiers: [N(C5), N, N(C3), N, N, 0, 0, 0] <-- E.g. N(C5) is the nullifier pointing to C5.
6074
//
61-
// this: [ 5, _, 3, _, _, _, _, _] <-- the index of the transient note for each nullifier
75+
// this: [ 5, _, 3, _, _, _, _, _] <-- the index of the linked note for each nullifier
6276
//
6377
unconstrained fn get_note_hash_index_for_each_nullifier(
6478
previous_kernel: PrivateKernelCircuitPublicInputs,
6579
) -> [u32; MAX_NULLIFIERS_PER_TX] {
66-
// Q: there's no "null" value, so how do we distinguish between "null" and transient note_hash index 0?
6780
let mut note_hash_index_for_each_nullifier = [0; MAX_NULLIFIERS_PER_TX];
6881
let note_hashes = previous_kernel.end.note_hashes;
6982
let nullifiers = previous_kernel.end.nullifiers;

noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/components/tail_output_validator.nr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ impl TailOutputValidator {
117117
}
118118

119119
fn validate_gas_used(self) {
120-
let gas_used = meter_gas_used(self.previous_kernel, false /* is_for_public */);
120+
let gas_used = meter_gas_used(self.previous_kernel, false);
121121
assert_eq(self.output.gas_used, gas_used, "incorrect metered gas used");
122122

123123
let limits = self.previous_kernel.constants.tx_context.gas_settings.gas_limits;

noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/components/tail_output_validator/validate_expiration_timestamp.nr

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use types::{abis::global_variables::GlobalVariables, constants::MAX_TX_LIFETIME};
22

3+
/// We enforce in the tail circuit that the `include_by_timestamp` is at most MAX_TX_LIFETIME after the
4+
/// tx's anchor timestamp. This restriction was motivated by wanting to prune network nodes'
5+
/// mempools of all txs that are longer than a day old.
36
pub fn validate_expiration_timestamp(expiration_timestamp: u64, global_variables: GlobalVariables) {
47
let max_timestamp = global_variables.timestamp + MAX_TX_LIFETIME;
58
assert(expiration_timestamp <= max_timestamp, "expiration_timestamp exceeds max tx lifetime");

noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/private_kernel_tail.nr

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,11 @@ pub fn execute(
5858
) -> PrivateToRollupKernelCircuitPublicInputs {
5959
// Validate inputs.
6060
if !::std::runtime::is_unconstrained() {
61-
inputs.previous_kernel.verify(true /* is_last_kernel */);
61+
inputs.previous_kernel.verify(true);
6262
inputs.previous_kernel.validate_vk_in_vk_tree(ALLOWED_PREVIOUS_CIRCUITS);
6363
}
6464

65-
validate_previous_kernel_for_tail(
66-
inputs.previous_kernel.public_inputs,
67-
false, /* is_for_public */
68-
);
65+
validate_previous_kernel_for_tail(inputs.previous_kernel.public_inputs, false);
6966

7067
// Generate output.
7168
// Safety: The output is validated below by TailOutputValidator.

noir-projects/noir-protocol-circuits/crates/types/src/constants.nr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ pub global INITIAL_L2_BLOCK_NUM: Field = 1;
163163
pub global FIELDS_PER_BLOB: u32 = 4096;
164164
pub global BLOBS_PER_CHECKPOINT: u32 = 6;
165165
pub global MAX_CHECKPOINTS_PER_EPOCH: u32 = 32;
166-
pub global MAX_TX_LIFETIME: u64 = 86400; // 1 day
166+
pub global MAX_TX_LIFETIME: u64 = 86400; // 1 day. Arbitrarily chosen.
167167
// The genesis values are taken from world_state.test.cpp > WorldStateTest.GetInitialTreeInfoForAllTrees
168168
pub global GENESIS_BLOCK_HEADER_HASH: Field =
169169
0x2ff681dd7730c7b9e5650c70afa57ee81377792dfc95d98c11817b8c761ff965;

0 commit comments

Comments
 (0)