Skip to content

DO NOT MERGE: init & inner q's and todos#19720

Open
iAmMichaelConnor wants to merge 1 commit intonextfrom
mc/init-and-inner-audit-qs-and-todos
Open

DO NOT MERGE: init & inner q's and todos#19720
iAmMichaelConnor wants to merge 1 commit intonextfrom
mc/init-and-inner-audit-qs-and-todos

Conversation

@iAmMichaelConnor
Copy link
Contributor

Comments I wrote as I went, for discussion, and possibly for changes before external audit.

Comment on lines +339 to +358
///
/// It's technically possible for both to be set, since one function in the tx will span phases.
/// Of course, that function probably doesn't _need_ to set both, since it _knows_ the counter at which the phases switch.
///
/// MIKE: It's a shame that this introduces another thing which must be known before a single function
/// can be processed by a kernel, but is only learned later in the tx. (I still cling on to the idea
/// of state channels or collaborative proving of a single tx being possible. Any time something like
/// this is introduced it makes that harder to achieve). Obviously, there are other examples of this
/// pattern (such as the first nullifier hint), but I'd like to avoid more examples if we can.
///
/// Tentative idea:
/// Can we not more-simply track two items:
/// max_end_counter_of_all_functions_which_voiced_an_expectation_that_theyre_non_revertible
/// min_start_counter_of_all_functions_which_voiced_an_expectation_that_theyre_revertible
/// (not genuine naming suggestions, I'm just trying to explain the idea)
///
/// (it doesn't technically _have_ to be the explicit start and end counters, but it helps me explain my point)
/// Each time we process a function, we take the max between the function & the kernel's `max_...` value and the min of the `min_...` values.
/// Then we can simply check these against the min_revertible_side_effect counter in the tail circuit once known.
/// This removes the ugliness that each function needs to propagate its expectation of the `min_revertible_side_effect_counter`.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flagging this as a worthwhile talking point

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right both can be set! The comment is misleading (sorry!). It should be for checking a "counter" (not a function) is in a particular phase. For example, a function could be checking that it is in revertible phase by setting its start counter to expected_revertible_side_effect_counter. And in the same function it could be checking that a note hash it's reading is indeed non-revertible by setting the note hash's counter to expected_non_revertible_side_effect_counter.

Your idea works great if we only care about the phase a function is in, but it won't be able to allow checking the phase of a random counter. But I'm also not sure if that's useful at all. So also happy to change it to what you suggested!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dropped the ball on this one.

but it won't be able to allow checking the phase of a random counter. But I'm also not sure if that's useful at all

I hadn't considered the use case of checking a counter outside the current function's execution frame. Not sure if that's useful.
No time to do anything differently to what we have, now, unfortunately.

Comment on lines +85 to +95
// Q: I wonder if (future enhancement) we should hash every item in every claimed_length_array at the time the item is created.
// Some of these structs actually contain a lot of elements, and we're ROM-accessing each element when copying the arrays
// in every inner kernel iteration.
// PrivateLogData, KeyValidationRequestAndGenerator, PrivateCallRequest, PublicCallRequest are large.
// Also, the Scoped<Counted<>> wrappers add overhead too, so we could consider hashing those (unless we need that data in every kernel iteration).
// Then we can unpack the hashes as and when we need them in future circuits.
// E.g. when we pop a (hashed) private call off the stack, we can unpack the hash at that time.
// E.g. when we process key validation requests in the reset kernel, we can unpack the hash at that time.
// E.g. private log preimage data and public call request data can be passed to the mempool, much like we do with contract class log preimage data.
// This could save several thousand gates per kernel iteration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worthy of discussion (post alpha)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the databus doesn't add much value if we still pack and unpack eventually 🤣


impl Empty for BatchingBlobCommitment {
fn empty() -> Self {
// TODO(potential for bugs): this "empty" is not an all-zero witness, because the point_at_infinity
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is used at all, not even in tests. This was probably copy paste from another struct, and sometimes trait impl like this is not really needed but still implemented. I only removed those unused when I saw one, didn't do a deep clean everywhere and missed this one which should definitely be removed :(

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +339 to +358
///
/// It's technically possible for both to be set, since one function in the tx will span phases.
/// Of course, that function probably doesn't _need_ to set both, since it _knows_ the counter at which the phases switch.
///
/// MIKE: It's a shame that this introduces another thing which must be known before a single function
/// can be processed by a kernel, but is only learned later in the tx. (I still cling on to the idea
/// of state channels or collaborative proving of a single tx being possible. Any time something like
/// this is introduced it makes that harder to achieve). Obviously, there are other examples of this
/// pattern (such as the first nullifier hint), but I'd like to avoid more examples if we can.
///
/// Tentative idea:
/// Can we not more-simply track two items:
/// max_end_counter_of_all_functions_which_voiced_an_expectation_that_theyre_non_revertible
/// min_start_counter_of_all_functions_which_voiced_an_expectation_that_theyre_revertible
/// (not genuine naming suggestions, I'm just trying to explain the idea)
///
/// (it doesn't technically _have_ to be the explicit start and end counters, but it helps me explain my point)
/// Each time we process a function, we take the max between the function & the kernel's `max_...` value and the min of the `min_...` values.
/// Then we can simply check these against the min_revertible_side_effect counter in the tail circuit once known.
/// This removes the ugliness that each function needs to propagate its expectation of the `min_revertible_side_effect_counter`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right both can be set! The comment is misleading (sorry!). It should be for checking a "counter" (not a function) is in a particular phase. For example, a function could be checking that it is in revertible phase by setting its start counter to expected_revertible_side_effect_counter. And in the same function it could be checking that a note hash it's reading is indeed non-revertible by setting the note hash's counter to expected_non_revertible_side_effect_counter.

Your idea works great if we only care about the phase a function is in, but it won't be able to allow checking the phase of a random counter. But I'm also not sure if that's useful at all. So also happy to change it to what you suggested!

///
/// TODO: Move this to abis/public_data/storage/read.nr
/// OR... consider inlining it into the function which called it, because it's not being used anywhere else (unless there are plans to test it in isolation?)
/// TODO: possible duplicate of aztec-nr/aztec/src/history/public_storage.nr?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is! That's why I thought we could move it into public_data so that this could also be used in aztec-nr or contracts that want to read public data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good to me!

// A: We don't have to. The init and inner circuits add contract address to non-empty nullifiers.
// So we know we should silo it if the contract address is not empty.
//
// Note to self: possibly accidental sentinel value; giving the contract address `0` special meaning in the protocol?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is tech debt! We used to allow running the reset circuit that silos nullifiers or private logs more than once. And the protocol nullifier used to be already-siloed when added in the Init. We can remove the if-else and always silo it now. Good find!

}

// Claude says:
// Ambiguous points_to_infinity semantics: The function relies on points_to_infinity() to handle the max leaf case. In TestLeafPreimage, this returns true when next_value == 0. This creates ambiguity: is next_value: 0 an uninitialized leaf or the max leaf? The empty_low_leaf test conflates these cases.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah points_to_infinity does consider empty leaves to be leaves that point to infinity. I think it's fine because even though is_valid_low_leaf returns true for an empty leaf, it can't be used to prove non-membership. The actual value inserted to the tree is a hash of the leaf preimage, and the hash of an empty leaf preimage is not 0. So the uninitialized leaves in the tree are not really the empty leaves. They are essentially the same as other made up leaves that satisfy the is_valid_low_leaf check but don't exist.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oooops I was wrong. There's an if-else in the hash function for nullifier and public data leaf preimages. If the preimage is empty then its hash will be 0. So an empty leaf is actually a valid low leaf!!
Luckily when we insert nullifiers in private tx base we check that the low leaves are not empty.
But when we read the public data tree for contract upgrade, or for anything via the util in aztec-nr, we didn't check it. So it's possible to use an empty leaf as hint and ignore the upgraded contract class id.
WE HAVE A BUG!!!!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work, claude!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants