Skip to content

Commit 2c71b66

Browse files
authored
Merge 6cd407f into de2b775
2 parents de2b775 + 6cd407f commit 2c71b66

File tree

5 files changed

+87
-63
lines changed

5 files changed

+87
-63
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,29 @@
11
use crate::{
22
context::PrivateContext, note::{note_emission::NoteEmission, note_interface::NoteInterface},
3-
keys::{getters::get_current_public_keys, public_keys::{OvpkM, IvpkM}},
3+
keys::{getters::{get_current_public_keys, get_ovsk_app}, public_keys::{OvpkM, IvpkM}},
44
encrypted_logs::payload::compute_encrypted_note_log, oracle::logs_traits::LensForEncryptedLog
55
};
66
use dep::protocol_types::{hash::sha256_to_field, address::AztecAddress, abis::note_hash::NoteHash};
77

8-
unconstrained fn compute_unconstrained<Note, let N: u32, let NB: u32, let M: u32>(
9-
contract_address: AztecAddress,
10-
storage_slot: Field,
11-
ovsk_app: Field,
12-
ovpk: OvpkM,
13-
ivpk: IvpkM,
14-
recipient: AztecAddress,
15-
note: Note
16-
) -> ([u8; M], Field) where Note: NoteInterface<N, NB>, [Field; N]: LensForEncryptedLog<N, M> {
17-
compute(
18-
contract_address,
19-
storage_slot,
20-
ovsk_app,
21-
ovpk,
22-
ivpk,
23-
recipient,
24-
note
25-
)
26-
}
27-
28-
fn compute<Note, let N: u32, let NB: u32, let M: u32>(
29-
contract_address: AztecAddress,
30-
storage_slot: Field,
31-
ovsk_app: Field,
32-
ovpk: OvpkM,
33-
ivpk: IvpkM,
34-
recipient: AztecAddress,
35-
note: Note
36-
) -> ([u8; M], Field) where Note: NoteInterface<N, NB>, [Field; N]: LensForEncryptedLog<N, M> {
37-
let encrypted_log: [u8; M] = compute_encrypted_note_log(
38-
contract_address,
39-
storage_slot,
40-
ovsk_app,
41-
ovpk,
42-
ivpk,
43-
recipient,
44-
note
45-
);
46-
let log_hash = sha256_to_field(encrypted_log);
47-
(encrypted_log, log_hash)
48-
}
49-
50-
fn emit_with_keys<Note, let N: u32, let NB: u32, let M: u32>(
51-
context: &mut PrivateContext,
8+
fn compute_raw_note_log<Note, let N: u32, let NB: u32, let M: u32>(
9+
context: PrivateContext,
5210
note: Note,
11+
ovsk_app: Field,
5312
ovpk: OvpkM,
5413
ivpk: IvpkM,
55-
recipient: AztecAddress,
56-
inner_compute: fn(AztecAddress, Field, Field, OvpkM, IvpkM, AztecAddress, Note) -> ([u8; M], Field)
57-
) where Note: NoteInterface<N, NB>, [Field; N]: LensForEncryptedLog<N, M> {
14+
recipient: AztecAddress
15+
) -> (u32, [u8; M], Field) where Note: NoteInterface<N, NB>, [Field; N]: LensForEncryptedLog<N, M> {
5816
let note_header = note.get_header();
5917
let note_hash_counter = note_header.note_hash_counter;
6018
let storage_slot = note_header.storage_slot;
6119

20+
// TODO(#8589): use typesystem to skip this check when not needed
6221
let note_exists = context.note_hashes.storage.any(|n: NoteHash| n.counter == note_hash_counter);
6322
assert(note_exists, "Can only emit a note log for an existing note.");
6423

6524
let contract_address: AztecAddress = context.this_address();
66-
let ovsk_app: Field = context.request_ovsk_app(ovpk.hash());
6725

68-
let (encrypted_log, log_hash) = inner_compute(
26+
let encrypted_log: [u8; M] = compute_encrypted_note_log(
6927
contract_address,
7028
storage_slot,
7129
ovsk_app,
@@ -74,8 +32,20 @@ fn emit_with_keys<Note, let N: u32, let NB: u32, let M: u32>(
7432
recipient,
7533
note
7634
);
35+
let log_hash = sha256_to_field(encrypted_log);
36+
37+
(note_hash_counter, encrypted_log, log_hash)
38+
}
7739

78-
context.emit_raw_note_log(note_hash_counter, encrypted_log, log_hash);
40+
unconstrained fn compute_raw_note_log_unconstrained<Note, let N: u32, let NB: u32, let M: u32>(
41+
context: PrivateContext,
42+
note: Note,
43+
ovpk: OvpkM,
44+
ivpk: IvpkM,
45+
recipient: AztecAddress
46+
) -> (u32, [u8; M], Field) where Note: NoteInterface<N, NB>, [Field; N]: LensForEncryptedLog<N, M> {
47+
let ovsk_app = get_ovsk_app(ovpk.hash());
48+
compute_raw_note_log(context, note, ovsk_app, ovpk, ivpk, recipient)
7949
}
8050

8151
pub fn encode_and_encrypt_note<Note, let N: u32, let NB: u32, let M: u32>(
@@ -86,7 +56,10 @@ pub fn encode_and_encrypt_note<Note, let N: u32, let NB: u32, let M: u32>(
8656
| e: NoteEmission<Note> | {
8757
let ovpk = get_current_public_keys(context, ov).ovpk_m;
8858
let ivpk = get_current_public_keys(context, iv).ivpk_m;
89-
emit_with_keys(context, e.note, ovpk, ivpk, iv, compute);
59+
let ovsk_app: Field = context.request_ovsk_app(ovpk.hash());
60+
61+
let (note_hash_counter, encrypted_log, log_hash) = compute_raw_note_log(*context, e.note, ovsk_app, ovpk, ivpk, iv);
62+
context.emit_raw_note_log(note_hash_counter, encrypted_log, log_hash);
9063
}
9164
}
9265

@@ -96,9 +69,17 @@ pub fn encode_and_encrypt_note_unconstrained<Note, let N: u32, let NB: u32, let
9669
iv: AztecAddress
9770
) -> fn[(&mut PrivateContext, AztecAddress, AztecAddress)](NoteEmission<Note>) -> () where Note: NoteInterface<N, NB>, [Field; N]: LensForEncryptedLog<N, M> {
9871
| e: NoteEmission<Note> | {
72+
// Note: We could save a lot of gates by obtaining the following keys in an unconstrained context but this
73+
// function is currently not used anywhere so we are not optimizing it.
9974
let ovpk = get_current_public_keys(context, ov).ovpk_m;
10075
let ivpk = get_current_public_keys(context, iv).ivpk_m;
101-
emit_with_keys(context, e.note, ovpk, ivpk, iv, compute_unconstrained);
76+
77+
// See the comment in `encode_and_encrypt_note_with_keys_unconstrained` for why having note hash counter
78+
// and log hash unconstrained here is fine.
79+
let (note_hash_counter, encrypted_log, log_hash) = unsafe {
80+
compute_raw_note_log_unconstrained(*context, e.note, ovpk, ivpk, iv)
81+
};
82+
context.emit_raw_note_log(note_hash_counter, encrypted_log, log_hash);
10283
}
10384
}
10485

@@ -109,7 +90,10 @@ pub fn encode_and_encrypt_note_with_keys<Note, let N: u32, let NB: u32, let M: u
10990
recipient: AztecAddress
11091
) -> fn[(&mut PrivateContext, OvpkM, IvpkM, AztecAddress)](NoteEmission<Note>) -> () where Note: NoteInterface<N, NB>, [Field; N]: LensForEncryptedLog<N, M> {
11192
| e: NoteEmission<Note> | {
112-
emit_with_keys(context, e.note, ovpk, ivpk, recipient, compute);
93+
let ovsk_app: Field = context.request_ovsk_app(ovpk.hash());
94+
95+
let (note_hash_counter, encrypted_log, log_hash) = compute_raw_note_log(*context, e.note, ovsk_app, ovpk, ivpk, recipient);
96+
context.emit_raw_note_log(note_hash_counter, encrypted_log, log_hash);
11397
}
11498
}
11599

@@ -120,6 +104,28 @@ pub fn encode_and_encrypt_note_with_keys_unconstrained<Note, let N: u32, let NB:
120104
recipient: AztecAddress
121105
) -> fn[(&mut PrivateContext, OvpkM, IvpkM, AztecAddress)](NoteEmission<Note>) -> () where Note: NoteInterface<N, NB>, [Field; N]: LensForEncryptedLog<N, M> {
122106
| e: NoteEmission<Note> | {
123-
emit_with_keys(context, e.note, ovpk, ivpk, recipient, compute_unconstrained);
107+
// Having the log hash be unconstrained here is fine because the way this works is we send the log hash
108+
// to the kernel, and it gets included as part of its public inputs. Then we send the tx to the sequencer,
109+
// which includes the kernel proof and the log preimages. The sequencer computes the hashes of the logs
110+
// and checks that they are the ones in the public inputs of the kernel, and drops the tx otherwise (proposing
111+
// the block on L1 would later fail if it didn't because of txs effects hash mismatch).
112+
// So if we don't constrain the log hash, then a malicious sender can compute the correct log, submit a bad
113+
// log hash to the kernel, and then submit the bad log preimage to the sequencer. All checks will pass, but
114+
// the submitted log will not be the one that was computed by the app.
115+
// In the unconstrained case, we don't care about the log at all because we don't do anything with it,
116+
// and because it's unconstrained: it could be anything. So if a sender chooses to broadcast the tx with a log
117+
// that is different from the one that was used in the circuit, then they'll be able to, but they were already
118+
// able to change the log before anyway, so the end result is the same. It's important here that we do not
119+
// return the log from this function to the app, otherwise it could try to do stuff with it and then that might
120+
// be wrong.
121+
// Regarding the note hash counter, this is used for squashing. The kernel assumes that a given note can have
122+
// more than one log and removes all of the matching ones, so all a malicious sender could do is either: cause
123+
// for the log to be deleted when it shouldn't have (which is fine - they can already make the content be
124+
// whatever), or cause for the log to not be deleted when it should have (which is also fine - it'll be a log
125+
// for a note that doesn't exist).
126+
let (note_hash_counter, encrypted_log, log_hash) = unsafe {
127+
compute_raw_note_log_unconstrained(*context, e.note, ovpk, ivpk, recipient)
128+
};
129+
context.emit_raw_note_log(note_hash_counter, encrypted_log, log_hash);
124130
}
125131
}

noir-projects/aztec-nr/aztec/src/keys/getters/mod.nr

+15-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ use dep::protocol_types::{
55
use crate::{
66
context::{PrivateContext, UnconstrainedContext},
77
oracle::{keys::get_public_keys_and_partial_address, key_validation_request::get_key_validation_request},
8-
keys::{public_keys::{PublicKeys, PUBLIC_KEYS_LENGTH}, stored_keys::StoredKeys, constants::NULLIFIER_INDEX},
8+
keys::{
9+
public_keys::{PublicKeys, PUBLIC_KEYS_LENGTH}, stored_keys::StoredKeys,
10+
constants::{NULLIFIER_INDEX, OUTGOING_INDEX}
11+
},
912
state_vars::{public_mutable::PublicMutable, map::Map}
1013
};
1114

@@ -17,10 +20,21 @@ global KEY_REGISTRY_UPDATE_BLOCKS = 5;
1720

1821
global KEY_REGISTRY_STORAGE_SLOT = 1;
1922

23+
// A helper function that gets app-siloed nullifier secret key for a given `npk_m_hash`. This function is used
24+
// in unconstrained contexts only - in Note::compute_nullifier_without_context which in turn is called by
25+
// `compute_note_hash_and_optionally_a_nullifier` function that is used by the NoteProcessor. The safe alternative
26+
// is `request_nsk_app` function define on `PrivateContext`.
2027
unconstrained pub fn get_nsk_app(npk_m_hash: Field) -> Field {
2128
get_key_validation_request(npk_m_hash, NULLIFIER_INDEX).sk_app
2229
}
2330

31+
// A helper function that gets app-siloed outgoing viewing key for a given `ovpk_m_hash`. This function is used
32+
// in unconstrained contexts only - when computing unconstrained note logs. The safe alternative is `request_ovsk_app`
33+
// function defined on `PrivateContext`.
34+
unconstrained pub fn get_ovsk_app(ovpk_m_hash: Field) -> Field {
35+
get_key_validation_request(ovpk_m_hash, OUTGOING_INDEX).sk_app
36+
}
37+
2438
// Returns all current public keys for a given account, applying proper constraints to the context. We read all
2539
// keys at once since the constraints for reading them all are actually fewer than if we read them one at a time - any
2640
// read keys that are not required by the caller can simply be discarded.

noir-projects/noir-contracts/contracts/token_blacklist_contract/src/main.nr

+4-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ contract TokenBlacklist {
1313
use dep::aztec::{
1414
hash::compute_secret_hash,
1515
prelude::{AztecAddress, Map, NoteGetterOptions, PrivateSet, PublicMutable, SharedMutable},
16-
encrypted_logs::encrypted_note_emission::encode_and_encrypt_note, utils::comparison::Comparator
16+
encrypted_logs::encrypted_note_emission::{encode_and_encrypt_note, encode_and_encrypt_note_unconstrained},
17+
utils::comparison::Comparator
1718
};
1819

1920
use dep::authwit::{auth::{assert_current_call_valid_authwit, assert_current_call_valid_authwit_public}};
@@ -214,8 +215,8 @@ contract TokenBlacklist {
214215
}
215216

216217
let amount = U128::from_integer(amount);
217-
storage.balances.sub(from, amount).emit(encode_and_encrypt_note(&mut context, from, from));
218-
storage.balances.add(to, amount).emit(encode_and_encrypt_note(&mut context, from, to));
218+
storage.balances.sub(from, amount).emit(encode_and_encrypt_note_unconstrained(&mut context, from, from));
219+
storage.balances.add(to, amount).emit(encode_and_encrypt_note_unconstrained(&mut context, from, to));
219220
}
220221

221222
#[aztec(private)]

yarn-project/end-to-end/src/e2e_keys.test.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,8 @@ describe('Key Registry', () => {
6363
// There are some examples where the action is fully hidden though. One of those examples is shielding where you
6464
// instantly consume the note after creating it. In this case, the nullifier is never emitted and hence the action
6565
// is impossible to detect with this scheme.
66-
// Another example is a withdraw is withdrawing from DeFi and then immediately spending the funds. In this case,
67-
// we would need nsk_app and the contract address of the DeFi contract to detect the nullification of the initial
68-
// note.
66+
// Another example is withdrawing from DeFi and then immediately spending the funds. In this case, we would
67+
// need nsk_app and the contract address of the DeFi contract to detect the nullification of the initial note.
6968
it('nsk_app and contract address are enough to detect note nullification', async () => {
7069
const masterNullifierSecretKey = deriveMasterNullifierSecretKey(secret);
7170
const nskApp = computeAppNullifierSecretKey(masterNullifierSecretKey, testContract.address);

yarn-project/sequencer-client/src/tx_validator/metadata_validator.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ export class MetadataTxValidator<T extends AnyTx> implements TxValidator<T> {
5151
const maxBlockNumber = target.maxBlockNumber;
5252

5353
if (maxBlockNumber.isSome && maxBlockNumber.value < this.#globalVariables.blockNumber) {
54-
this.#log.warn(`Rejecting tx ${Tx.getHash(tx)} for low max block number`);
54+
this.#log.warn(
55+
`Rejecting tx ${Tx.getHash(tx)} for low max block number. Tx max block number: ${
56+
maxBlockNumber.value
57+
}, current block number: ${this.#globalVariables.blockNumber}.`,
58+
);
5559
return false;
5660
} else {
5761
return true;

0 commit comments

Comments
 (0)