Skip to content

feat: log decryption in Noir#12596

Merged
benesjan merged 20 commits into
masterfrom
janb/log_decryption_in_noir
Mar 14, 2025
Merged

feat: log decryption in Noir#12596
benesjan merged 20 commits into
masterfrom
janb/log_decryption_in_noir

Conversation

@benesjan

@benesjan benesjan commented Mar 8, 2025

Copy link
Copy Markdown
Contributor

Fixes #10723
Fixes #11634

@benesjan benesjan marked this pull request as draft March 8, 2025 18:13
plaintext_bytes
}

/// Computes an encrypted log for an event using AES-128 encryption in CBC mode.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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>() };

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as in the event.nr above - this fixes privacy leak.

[address]: ACVMField[],
[ephPKField0]: ACVMField[],
[ephPKField1]: ACVMField[],
[ephPKField2]: ACVMField[],

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure I follow. It's a struct with 3 values so it gets converted to three acvm fields no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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] {}

@benesjan benesjan Mar 11, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@iAmMichaelConnor iAmMichaelConnor Mar 13, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, this is the same as the decrypt oracle

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point. Will investigate what did I do wrong here. Thanks

Comment thread noir-projects/aztec-nr/aztec/src/discovery/mod.nr
}

plaintext_bytes
fields_to_be_bytes_32(fields)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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(),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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 nventuro left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Was not able to go through the entire thing yet, leaving some preliminary comments

Comment thread noir-projects/aztec-nr/aztec/src/discovery/mod.nr
Comment thread noir-projects/aztec-nr/aztec/src/discovery/mod.nr Outdated
Comment thread noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr Outdated
Comment thread noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr Outdated
Comment thread yarn-project/simulator/src/private/execution_data_provider.ts Outdated
@@ -1,2 +1,2 @@
export * from './encrypted_log_payload.js';
export * from './l1_note_payload.js';
export * from './shared_secret_derivation.js';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can't we get rid of this entire directory?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this the first oracle that returns a boundedvec?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes

[address]: ACVMField[],
[ephPKField0]: ACVMField[],
[ephPKField1]: ACVMField[],
[ephPKField2]: ACVMField[],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure I follow. It's a struct with 3 values so it gets converted to three acvm fields no?

Comment thread yarn-project/stdlib/src/logs/tx_scoped_l2_log.ts Outdated
// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Base automatically changed from nv/redo-partial to master March 12, 2025 18:25
@benesjan benesjan force-pushed the janb/log_decryption_in_noir branch 2 times, most recently from 8011dc0 to 054534e Compare March 12, 2025 19:34
@benesjan benesjan marked this pull request as ready for review March 12, 2025 19:34

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@iAmMichaelConnor iAmMichaelConnor left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I've left some comments :)

@@ -0,0 +1,352 @@
use crate::{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@benesjan benesjan Mar 13, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point. Moved it in 0298842

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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] {}

@iAmMichaelConnor iAmMichaelConnor Mar 13, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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,
}

Comment thread noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr
/// 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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should probably rename the point_from_x_coord to point_from_x_coord_unsafe. It's only being used in tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is it unsafe?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@benesjan benesjan Mar 13, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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).

@benesjan benesjan marked this pull request as draft March 13, 2025 18:55
@benesjan benesjan force-pushed the janb/log_decryption_in_noir branch from 593052c to bebc037 Compare March 13, 2025 19:55
@benesjan benesjan marked this pull request as ready for review March 13, 2025 19:56
@benesjan benesjan marked this pull request as draft March 13, 2025 20:32
@benesjan benesjan marked this pull request as ready for review March 13, 2025 21:40
@benesjan benesjan requested a review from nventuro March 13, 2025 22:03
@benesjan benesjan marked this pull request as draft March 14, 2025 15:14
@benesjan benesjan marked this pull request as ready for review March 14, 2025 15:35
}

/// Returns as many random bytes as specified through N.
pub unconstrained fn get_random_bytes<let N: u32>() -> [u8; N] {

@benesjan benesjan Mar 14, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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).

Comment thread noir-projects/aztec-nr/aztec/src/discovery/mod.nr
Comment thread noir-projects/aztec-nr/aztec/src/discovery/mod.nr
Comment thread noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr
Comment thread noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr Outdated
Comment thread noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr Outdated
Comment thread noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr Outdated
Comment thread noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr
/// 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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is it unsafe?

Comment on lines +430 to +434
// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is it working correctly now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please create a tracking issue or address it then

final_log
}

pub unconstrained fn decrypt_log(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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).

@benesjan benesjan force-pushed the janb/log_decryption_in_noir branch from c419b8c to f09095b Compare March 14, 2025 19:29
@benesjan benesjan enabled auto-merge (squash) March 14, 2025 19:29
@benesjan benesjan merged commit 238d57c into master Mar 14, 2025
@benesjan benesjan deleted the janb/log_decryption_in_noir branch March 14, 2025 20:00
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.

Determine the maximum log plaintext length Decrypt logs inside aztec-nr

3 participants