feat: log decryption in Noir#12596
Conversation
| plaintext_bytes | ||
| } | ||
|
|
||
| /// Computes an encrypted log for an event using AES-128 encryption in CBC mode. |
There was a problem hiding this comment.
Sneaked this unrelated change in. I needed it to orient myself in the structure of the log and AI is perfect for generating these code docs.
| plaintext_bytes | ||
| } | ||
|
|
||
| /// Computes an encrypted log for a note using AES-128 encryption in CBC mode. |
There was a problem hiding this comment.
Sneaked this unrelated change in. I needed it to orient myself in the structure of the log and AI is perfect for generating these code docs.
| // (all the "real" log fields contain at most 31 bytes because of the way we convert the bytes to fields). | ||
| // Safety: randomness cannot be constrained. | ||
| final_log[i] = unsafe { random() }; | ||
| final_log[i] = unsafe { random_with_max_byte_size::<31>() }; |
There was a problem hiding this comment.
Same as in the event.nr above - this fixes privacy leak.
| [address]: ACVMField[], | ||
| [ephPKField0]: ACVMField[], | ||
| [ephPKField1]: ACVMField[], | ||
| [ephPKField2]: ACVMField[], |
There was a problem hiding this comment.
When I had the ephPk input defined only as "ephPk: ACVMField[]" then ephPk was only 1 Field instead of 3. Does the reviewer if it's really necessary to list the components directly here? It's ugly.
There was a problem hiding this comment.
I'm not sure I follow. It's a struct with 3 values so it gets converted to three acvm fields no?
There was a problem hiding this comment.
If I put there ephPk: ACVField[] instead of the 3 components I got just one field.
| pub unconstrained fn get_shared_secret_oracle( | ||
| address: AztecAddress, | ||
| ephPk: Point, | ||
| ) -> [Field; POINT_LENGTH] {} |
There was a problem hiding this comment.
When I directly returned Point from the oracle, I got an error with num destination slots. I see that there is not other case that returns a multi-field struct from oracle and e.g. ingetIndexedTaggingSecretAsSender we also return an array of fields and then deserialize. Is it really just unsupported? I would expect Noir to manage to map the struct fields. Is this just a tech debt.
There was a problem hiding this comment.
BoundedVec is an example of a multi-field struct that is being returned by the decryption oracle.
pub struct BoundedVec<T, let MaxLen: u32> {
storage: [T; MaxLen],
len: u32,
}There was a problem hiding this comment.
Yes, this is the same as the decrypt oracle
There was a problem hiding this comment.
Good point. Will investigate what did I do wrong here. Thanks
| } | ||
|
|
||
| plaintext_bytes | ||
| fields_to_be_bytes_32(fields) |
There was a problem hiding this comment.
Did this change here and in the note.nr because when decrypting we use be_bytes_32_to_fields. And those 2 functions are guaranteed to match.
| // (This process is then reversed when processing the log in `do_process_log`) | ||
| let final_plaintext_bytes = fields_to_be_bytes_32(final_plaintext); | ||
|
|
||
| encrypt_log( |
There was a problem hiding this comment.
The encrypt_log function originally didn't exist but I separated it from compute_log because that is against what I wanted to unit test my decryption functionality.
| fields[i + 2] = packed_note[i]; | ||
| } | ||
|
|
||
| fields |
There was a problem hiding this comment.
I return fields here instead of bytes. This is because that is the natural representation and I think it makes sense to convert it to bytes before calling the aes function.
There was a problem hiding this comment.
I left it as bytes, because we should be packing the note_type_id into much less than a field. Plus, this function is compute_note_plaintext_for_this_strategy and this strategy is specifically for AES, so bytes is the natural output.
There was a problem hiding this comment.
We eventually need to settle on what standard format aztec-nr uses for logs once it considers non-AES flows. In my mind the only sane choice is a field array (since that is what noir deals with, that is what the logs are, and that is the only thing that has no overhead), so this is more beginning to nudge things in that direction. Ultimately the note type id will share the field with some other values, likely in what is today the 'combined type id'.
|
|
||
| // AES 128 operates on bytes, not fields, so we need to convert the fields to bytes. | ||
| // (This process is then reversed when processing the log in `do_process_log`) | ||
| let final_plaintext_bytes = fields_to_be_bytes_32(final_plaintext); |
There was a problem hiding this comment.
This is new here. As for the event I now use this method to have a guraanteed match between encryption and decryption.
| } | ||
|
|
||
| #[private] | ||
| fn emit_encrypted_logs_nested(value: Field, owner: AztecAddress, sender: AztecAddress) { |
There was a problem hiding this comment.
Nuked this function because it was used in a test that was no longer viable to have because of no longer having decryption in Typescript and it was also unnecessary. Will describe details in the test.
| block.number, | ||
| /* isFromPublic */ false, | ||
| log.toBuffer(), | ||
| log.toFields(), |
There was a problem hiding this comment.
TxScopedL2Log now represents the log as fields and not as a buffer. I did this change because Fields are the natural representation and it results in less conversions in the code.
nventuro
left a comment
There was a problem hiding this comment.
Was not able to go through the entire thing yet, leaving some preliminary comments
| @@ -1,2 +1,2 @@ | |||
| export * from './encrypted_log_payload.js'; | |||
| export * from './l1_note_payload.js'; | |||
| export * from './shared_secret_derivation.js'; | |||
There was a problem hiding this comment.
Can't we get rid of this entire directory?
There was a problem hiding this comment.
We cannot because there is still l1_event_payload that uses this stuff.
I was thinking about this and I think we should prioritize moving the event decryption to Noir as well once this PR is merged because I currently have it all loaded in my brain and it would allows us to finally purge a lot of this code.
It would require modifying the process_log function such that it stores the logs in capsules and then exposing some event getter on the relevant contracts so it would be quite a bit of work though.
| const symKeyBuffer = fromUintArray(symKey, 8); | ||
|
|
||
| const plaintext = await this.typedOracle.aes128Decrypt(ciphertext, ivBuffer, symKeyBuffer); | ||
| return bufferToBoundedVec(plaintext, ciphertextBVecStorage.length); |
There was a problem hiding this comment.
Is this the first oracle that returns a boundedvec?
| [address]: ACVMField[], | ||
| [ephPKField0]: ACVMField[], | ||
| [ephPKField1]: ACVMField[], | ||
| [ephPKField2]: ACVMField[], |
There was a problem hiding this comment.
I'm not sure I follow. It's a struct with 3 values so it gets converted to three acvm fields no?
| // TODO: I forgot to add a corresponding function here, when I introduced an oracle method to txe_oracle.ts. The compiler didn't throw an error, so it took me a while to learn of the existence of this file, and that I need to implement this function here. Isn't there a way to programmatically identify that this is missing, given the existence of a txe_oracle method? | ||
| async aes128Decrypt(ciphertext: ForeignCallArray, iv: ForeignCallArray, symKey: ForeignCallArray) { | ||
| const ciphertextBuffer = fromUintArray(ciphertext, 8); | ||
| // TODO: I forgot to add a corresponding function here, when I introduced an oracle method to txe_oracle.ts. |
8011dc0 to
054534e
Compare
iAmMichaelConnor
left a comment
There was a problem hiding this comment.
I've left some comments :)
| @@ -0,0 +1,352 @@ | |||
| use crate::{ | |||
There was a problem hiding this comment.
I'm surprised git hasn't figured out that this file is a git mv of a file. It's showing as a deleted file, then a new file. How did you move around the files? VScode usually correctly does a git mv.
It's very hard to compare the diff as a result.
There was a problem hiding this comment.
I pointed out in the code where I did changes. Sorry the diff is 💩 but feel like I have an excuse here since I could not really separate this into a different PR because of the branch situation being tricky when I started the PR.
| } | ||
|
|
||
| /// Just like `random`, but returns a field that fits in `N` bytes. | ||
| pub unconstrained fn random_with_max_byte_size<let N: u32>() -> Field { |
There was a problem hiding this comment.
This probably shouldn't live here; oracle files should focus on the oracle funtionality. A util which makes use of an oracle should live elsewhere.
Re the function name: it's not returning a max byte size, it'll return a value which is this byte size. random_bytes might be clearer?
There was a problem hiding this comment.
Nuked the function in 65377a1 and I just use get_random_bytes there instead.
| pub unconstrained fn get_shared_secret_oracle( | ||
| address: AztecAddress, | ||
| ephPk: Point, | ||
| ) -> [Field; POINT_LENGTH] {} |
There was a problem hiding this comment.
BoundedVec is an example of a multi-field struct that is being returned by the decryption oracle.
pub struct BoundedVec<T, let MaxLen: u32> {
storage: [T; MaxLen],
len: u32,
}| /// equation for y. | ||
| /// @param x - The x coordinate of the point | ||
| /// @param sign - The "sign" of the y coordinate - determines whether y <= (Fr.MODULUS - 1) / 2 | ||
| pub fn point_from_x_coord_and_sign(x: Field, sign: bool) -> Point { |
There was a problem hiding this comment.
We should probably rename the point_from_x_coord to point_from_x_coord_unsafe. It's only being used in tests.
There was a problem hiding this comment.
It's not unsafe per se but the the name is bad because you cannot recover a point just from x_coordinate. You can however recover a point corresponding to an address just from x_coordinate.
So I guess it would be better to instead just rename it to address_point_from_x_coord. Will take a note to do this in a separate PR.
| fields[i + 2] = packed_note[i]; | ||
| } | ||
|
|
||
| fields |
There was a problem hiding this comment.
I left it as bytes, because we should be packing the note_type_id into much less than a field. Plus, this function is compute_note_plaintext_for_this_strategy and this strategy is specifically for AES, so bytes is the natural output.
| { | ||
| let final_plaintext = compute_note_plaintext_for_this_strategy(note, storage_slot); | ||
|
|
||
| encrypt_log(context.this_address(), final_plaintext, recipient, sender) |
There was a problem hiding this comment.
Why was this function extracted into its own file?
One of my goals when I refactored encryption was for everything to be in one long, flattened file, so that it was easier to see the whole unravelled log assembly process, whilst we did all our other refactoring. Please can we revert this particular change?
The encrypt_log function that's been extracted has a name which isn't descriptive of what the function does: it computes a log (so should live in this compute_log function) (Or actually, it assembles a log, so maybe compute_log should be renamed to assemble_log). A "log" can't be encrypted; a plaintext can be encrypted. And the extracted encrypt_log function also computes a header and encrypts that and then concatenates data according to this log assembly strategy.
There was a problem hiding this comment.
I needed to unit test it against the decryption functionality and during decryption we don't spit out a note but a log plaintext (see process_log function). This is not decided yet but the process_log contract function will most likely process event logs as well and there we will need 1 decryption functionality shared between events and notes. For this reason changing the process_log function to not destructure the log plaintext there seemed like a bad direction to go down.
Agree that the name could be improved but I think it can probably wait for when we handle the events.
| final_log | ||
| } | ||
|
|
||
| pub unconstrained fn decrypt_log( |
There was a problem hiding this comment.
I mentioned in another comment that the encrypted_log fn shouldn't be extracted from the compute_log function.
This function does the process in reverse, so it doesn't just "decrypt" it also decodes the various components of the log. process_log might be a better name (because it's literally processing the log according to this log assembly/disassembly strategy).
There was a problem hiding this comment.
I'm not sure we'd be able to keep that model, given that we'll need to agree on some common encoding that all schemes must follow (or else abstract both encryption and encoding). Given that, the primitive being 'encrypt/decrypt' a field array makes sense (i.e. potato encoding).
593052c to
bebc037
Compare
| } | ||
|
|
||
| /// Returns as many random bytes as specified through N. | ||
| pub unconstrained fn get_random_bytes<let N: u32>() -> [u8; N] { |
There was a problem hiding this comment.
As Mike pointed out, we should keep oracle folder for oracles. For this reason I moved this function to utils/random.nr (didn't do any changes to it).
| /// equation for y. | ||
| /// @param x - The x coordinate of the point | ||
| /// @param sign - The "sign" of the y coordinate - determines whether y <= (Fr.MODULUS - 1) / 2 | ||
| pub fn point_from_x_coord_and_sign(x: Field, sign: bool) -> Point { |
| // IMPORTANT: This test has a misleading name and behavior. When TxScopedL2Log was updated to work with | ||
| // PrivateLog and PublicLog types directly, the `isFromPublic` getter was initially left unimplemented. | ||
| // At that time, this test appeared to pass - not because it was correctly filtering logs by contract address, | ||
| // but because `isFromPublic` was returning undefined (falsey). As a result, the test is actually filtering | ||
| // based on the log type (public vs private) rather than the intended contract address check. |
There was a problem hiding this comment.
Is it working correctly now?
There was a problem hiding this comment.
It does not. But felt like it's not a scope of this PR to rework that so I just put there this warning that it's fucked up.
There was a problem hiding this comment.
Please create a tracking issue or address it then
| final_log | ||
| } | ||
|
|
||
| pub unconstrained fn decrypt_log( |
There was a problem hiding this comment.
I'm not sure we'd be able to keep that model, given that we'll need to agree on some common encoding that all schemes must follow (or else abstract both encryption and encoding). Given that, the primitive being 'encrypt/decrypt' a field array makes sense (i.e. potato encoding).
Co-authored-by: Nicolás Venturo <nicolas.venturo@gmail.com>
…trategies/default_aes128/note/note.nr Co-authored-by: Nicolás Venturo <nicolas.venturo@gmail.com>
Co-authored-by: Nicolás Venturo <nicolas.venturo@gmail.com>
Co-authored-by: Nicolás Venturo <nicolas.venturo@gmail.com>
Co-authored-by: Nicolás Venturo <nicolas.venturo@gmail.com>
c419b8c to
f09095b
Compare

Fixes #10723
Fixes #11634