Skip to content

Commit ac545ff

Browse files
committed
initial integration into private msg delivery flow and tests for constrained tags
1 parent 0afe779 commit ac545ff

16 files changed

Lines changed: 210 additions & 69 deletions

File tree

noir-projects/aztec-nr/aztec/src/event/event_message.nr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ where
5858
Option::none(),
5959
recipient,
6060
delivery_mode,
61+
Option::none(),
6162
);
6263
}
6364
}

noir-projects/aztec-nr/aztec/src/macros/aztec.nr

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,11 @@ pub comptime fn aztec(m: Module, args: [AztecConfig]) -> Quoted {
157157
Option::none()
158158
};
159159

160-
let sync_state_fn_and_abi_export =
161-
generate_sync_state(process_custom_message_option, offchain_inbox_sync_option, custom_sync_handler);
160+
let sync_state_fn_and_abi_export = generate_sync_state(
161+
process_custom_message_option,
162+
offchain_inbox_sync_option,
163+
custom_sync_handler,
164+
);
162165

163166
if m.functions().any(|f| f.name() == quote { offchain_receive }) {
164167
panic(

noir-projects/aztec-nr/aztec/src/messages/delivery/constrained_delivery.nr

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ use crate::oracle::{call_utility_function::call_utility_function, notes::get_nex
77
use crate::protocol::{
88
abis::function_selector::FunctionSelector,
99
address::AztecAddress,
10-
constants::DOM_SEP__CONSTRAINED_MSG_NULLIFIER,
11-
hash::poseidon2_hash_with_separator,
10+
constants::{DOM_SEP__CONSTRAINED_MSG_LOG_TAG, DOM_SEP__CONSTRAINED_MSG_NULLIFIER},
11+
hash::{compute_log_tag, poseidon2_hash, poseidon2_hash_with_separator},
1212
traits::{Deserialize, ToField},
1313
};
1414

@@ -32,7 +32,8 @@ use crate::protocol::{
3232
/// note.
3333
/// - `index > 0`: asserts the prior nullifier
3434
/// `poseidon2_hash_with_separator([sender, recipient, secret, index - 1], DOM_SEP__CONSTRAINED_MSG_NULLIFIER)`
35-
/// exists via [`compute_nullifier_existence_request`](crate::nullifier::utils::compute_nullifier_existence_request).
35+
/// exists via
36+
/// [`compute_nullifier_existence_request`](crate::nullifier::utils::compute_nullifier_existence_request).
3637
/// The chain transitively constrains back to the index-0 `validate_handshake`. The caller is expected
3738
/// to emit the matching nullifier alongside each constrained send so subsequent calls can prove the
3839
/// chain.
@@ -47,9 +48,7 @@ pub fn calculate_secret_and_index(
4748
// Safety: the returned `Option<Field>` is untrusted and is constrained below before being returned to the
4849
// caller, either by performing a new handshake or by constraining the prior handshake's existence.
4950
let maybe_secret: Option<Field> = unsafe {
50-
let selector = comptime {
51-
FunctionSelector::from_signature("get_app_siloed_secret((Field),(Field),(Field))")
52-
};
51+
let selector = comptime { FunctionSelector::from_signature("get_app_siloed_secret((Field),(Field),(Field))") };
5352
let returns = call_utility_function(
5453
registry,
5554
selector,
@@ -64,12 +63,9 @@ pub fn calculate_secret_and_index(
6463
// `validate_handshake` is needed.
6564
// TODO(F-660): dispatch to `perform_handshake(sender, recipient, handshake_type)` once interactive
6665
// handshakes are supported.
67-
let selector = comptime {
68-
FunctionSelector::from_signature("non_interactive_handshake((Field),(Field))")
69-
};
70-
let secret: Field = context
71-
.call_private_function(registry, selector, [sender.to_field(), recipient.to_field()])
72-
.get_preimage();
66+
let selector = comptime { FunctionSelector::from_signature("non_interactive_handshake((Field),(Field))") };
67+
let secret: Field =
68+
context.call_private_function(registry, selector, [sender.to_field(), recipient.to_field()]).get_preimage();
7369
(secret, 0)
7470
} else {
7571
let secret = maybe_secret.unwrap_unchecked();
@@ -79,23 +75,51 @@ pub fn calculate_secret_and_index(
7975
let index = unsafe { get_next_constrained_index(secret) };
8076

8177
if index == 0 {
82-
let selector = comptime {
83-
FunctionSelector::from_signature("validate_handshake((Field),(Field),Field)")
84-
};
85-
let _ = context.call_private_function(
86-
registry,
87-
selector,
88-
[sender.to_field(), recipient.to_field(), secret],
89-
);
78+
let selector = comptime { FunctionSelector::from_signature("validate_handshake((Field),(Field),Field)") };
79+
let _ =
80+
context.call_private_function(registry, selector, [sender.to_field(), recipient.to_field(), secret]);
9081
} else {
9182
let prev_nullifier = poseidon2_hash_with_separator(
9283
[sender.to_field(), recipient.to_field(), secret, (index - 1) as Field],
9384
DOM_SEP__CONSTRAINED_MSG_NULLIFIER,
9485
);
95-
// TODO(F-670): exercise the index > 0 path end-to-end once send_constrained_msg emits nullifiers.
9686
context.assert_nullifier_exists(compute_nullifier_existence_request(prev_nullifier, caller));
9787
}
9888

9989
(secret, index)
10090
}
10191
}
92+
93+
/// Computes the constrained-delivery discovery log tag for an `(app_siloed_secret, index)` pair.
94+
///
95+
/// Collapses the secret and index into a single raw tag and domain-separates it with
96+
/// `DOM_SEP__CONSTRAINED_MSG_LOG_TAG`. This mirrors the recipient-side derivation in the PXE (`siloedTagFor`): the
97+
/// recipient recomputes the same tag to discover the message, and the protocol silos the emitted log's first field by
98+
/// the emitting contract address, completing the match.
99+
pub fn compute_constrained_log_tag(app_siloed_secret: Field, index: u32) -> Field {
100+
compute_log_tag(poseidon2_hash([app_siloed_secret, index as Field]), DOM_SEP__CONSTRAINED_MSG_LOG_TAG)
101+
}
102+
103+
/// Emits the per-send chain nullifier for a constrained message and returns its discovery log tag.
104+
///
105+
/// Used by [`crate::messages::delivery::do_private_message_delivery`] on the constrained-delivery path. It wraps
106+
/// [`calculate_secret_and_index`] to resolve the `(app_siloed_secret, index)` pair, then:
107+
/// 1. Emits the chain nullifier. A subsequent send at `index + 1` proves this nullifier exists, transitively constraining
108+
/// the chain back to the `index == 0` handshake validation in [`calculate_secret_and_index`].
109+
/// 2. Returns the log tag. The recipient recomputes the same tag from the handshake secret to discover the message.
110+
pub(crate) fn emit_nullifier_and_compute_constrained_tag(
111+
context: &mut PrivateContext,
112+
registry: AztecAddress,
113+
sender: AztecAddress,
114+
recipient: AztecAddress,
115+
) -> Field {
116+
let (secret, index) = calculate_secret_and_index(context, registry, sender, recipient);
117+
118+
let nullifier = poseidon2_hash_with_separator(
119+
[sender.to_field(), recipient.to_field(), secret, index as Field],
120+
DOM_SEP__CONSTRAINED_MSG_NULLIFIER,
121+
);
122+
context.push_nullifier_unsafe(nullifier);
123+
124+
compute_constrained_log_tag(secret, index)
125+
}

noir-projects/aztec-nr/aztec/src/messages/delivery/mod.nr

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -182,19 +182,36 @@ pub struct MessageDeliveryEnum {
182182
pub global MessageDelivery: MessageDeliveryEnum =
183183
MessageDeliveryEnum { OFFCHAIN: 1, ONCHAIN_UNCONSTRAINED: 2, ONCHAIN_CONSTRAINED: 3 };
184184

185+
/// Handshake inputs that opt a message delivery into constrained tagging.
186+
///
187+
/// Pass `Some` to [`do_private_message_delivery`] alongside [`MessageDeliveryEnum::ONCHAIN_CONSTRAINED`] to derive
188+
/// the log tag from a handshake-registry secret and emit the per-send chain nullifier (see
189+
/// [`crate::messages::delivery::constrained_delivery`]). Pass `None` for the wallet-driven unconstrained tag.
190+
pub struct ConstrainedDelivery {
191+
/// Handshake registry to resolve the app-siloed shared secret from.
192+
pub registry: AztecAddress,
193+
/// Account the message is sent on behalf of, bound into both the secret/index resolution and the chain nullifier.
194+
pub sender: AztecAddress,
195+
}
196+
185197
/// Performs private delivery of a message to `recipient` according to `delivery_mode`.
186198
///
187199
/// The message is encoded into plaintext and then encrypted for `recipient`. This function takes a _function_ that
188200
/// returns the plaintext instead of taking the plaintext directly in order to not waste constraints encoding the
189201
/// message in scenarios where the plaintext will be encrypted with unconstrained encryption.
190202
///
191203
/// `maybe_note_hash_counter` is only relevant for on-chain delivery modes (i.e. via protocol logs): if a newly created
192-
/// note hash's side effect counter is passed, then the log will be squashed alongside the note should its nullifier be
193-
/// emitted in the current transaction. This is typically only used for note messages: since the note will not actually
194-
/// be created, there is no point in delivering the message.
204+
/// note hash's side effect counter is passed and constrained tagging is not requested, then the log will be squashed
205+
/// alongside the note should its nullifier be emitted in the current transaction. Constrained-tagged logs are not tied
206+
/// to note squashing because recipient discovery scans those tags sequentially.
195207
///
196208
/// `delivery_mode` must be one of [`MessageDeliveryEnum`].
197209
///
210+
/// `maybe_constrained_delivery` opts into constrained tagging: when `Some` (only valid with
211+
/// [`MessageDeliveryEnum::ONCHAIN_CONSTRAINED`]) the log tag is derived from a handshake-registry secret and a chain
212+
/// nullifier is emitted. When `None` the wallet-driven unconstrained tag is used. Its some-ness must be a
213+
/// compile-time constant.
214+
///
198215
/// ## Privacy
199216
///
200217
/// The emitted log always has the same length regardless of `MESSAGE_PLAINTEXT_LEN`, because all message ciphertexts
@@ -205,17 +222,25 @@ pub fn do_private_message_delivery<Env, let MESSAGE_PLAINTEXT_LEN: u32>(
205222
maybe_note_hash_counter: Option<u32>,
206223
recipient: AztecAddress,
207224
delivery_mode: u8,
225+
maybe_constrained_delivery: Option<ConstrainedDelivery>,
208226
) {
209227
// This function relies on `delivery_mode` being a constant in order to reduce circuit constraints when
210228
// unconstrained usage is requested. If `delivery_mode` were a runtime value the compiler would be unable to
211229
// perform dead-code elimination.
212230
assert_constant(delivery_mode);
213231

214-
// The following maps out the 3 dimensions across which we configure message delivery.
232+
// The following maps out the dimensions across which we configure message delivery.
215233
let constrained_encryption = delivery_mode == MessageDelivery.ONCHAIN_CONSTRAINED;
216234
let deliver_as_offchain_message = delivery_mode == MessageDelivery.OFFCHAIN;
217-
// TODO(#14565): Add constrained tagging
218-
let _constrained_tagging = delivery_mode == MessageDelivery.ONCHAIN_CONSTRAINED;
235+
236+
// Constrained tagging is opt-in via `maybe_constrained_delivery`. Its some-ness must be a compile-time constant.
237+
assert_constant(maybe_constrained_delivery.is_some());
238+
let constrained_tagging = maybe_constrained_delivery.is_some();
239+
if constrained_tagging {
240+
// Constrained tagging requires the constrained encryption that `ONCHAIN_CONSTRAINED` selects, otherwise the
241+
// sender could pair a correct tag with forge-able (unconstrained) ciphertext.
242+
assert(constrained_encryption, "constrained tagging requires ONCHAIN_CONSTRAINED delivery");
243+
}
219244

220245
let contract_address = context.this_address();
221246

@@ -227,20 +252,33 @@ pub fn do_private_message_delivery<Env, let MESSAGE_PLAINTEXT_LEN: u32>(
227252
if deliver_as_offchain_message {
228253
deliver_offchain_message(ciphertext, recipient);
229254
} else {
230-
// TODO(#14565): constrained tagging is not yet implemented. Both modes currently use the unconstrained
231-
// domain separator because the discovery tag always comes from an oracle. Once constrained tagging lands,
232-
// this should branch on `constrained_tagging` to select the appropriate separator.
233-
let discovery_tag = compute_discovery_tag(recipient);
234-
let log_tag = compute_log_tag(discovery_tag, DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG);
255+
let log_tag = if constrained_tagging {
256+
let delivery_info = maybe_constrained_delivery.unwrap_unchecked();
257+
constrained_delivery::emit_nullifier_and_compute_constrained_tag(
258+
context,
259+
delivery_info.registry,
260+
delivery_info.sender,
261+
recipient,
262+
)
263+
} else {
264+
// TODO(#14565): Thread constrained tagging through high-level note/event delivery APIs so that
265+
// `ONCHAIN_CONSTRAINED` callers supply handshake info instead of taking this fallback.
266+
// `ONCHAIN_UNCONSTRAINED` deliveries are expected to derive the wallet-driven tag here.
267+
let discovery_tag = compute_discovery_tag(recipient);
268+
compute_log_tag(discovery_tag, DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG)
269+
};
235270

236271
// We forbid this value not being constant to avoid predicating the context calls below, which might result in
237272
// the context's arrays having unknown compile time write indices and hence dramatically increasing constraints
238273
// when accessing them. In practice this restriction is not a problem as we always know at compile time whether
239274
// we're emitting a note or non-note message.
240275
assert_constant(maybe_note_hash_counter.is_some());
241276

277+
// TODO(F-664): Add e2e coverage for constrained note delivery through kernel squashing and recipient sync.
278+
let squashable_note_log = maybe_note_hash_counter.is_some() & !constrained_tagging;
279+
242280
let log = BoundedVec::from_array(ciphertext);
243-
if maybe_note_hash_counter.is_some() {
281+
if squashable_note_log {
244282
// We associate the log with the note's side effect counter, so that if the note ends up being squashed in
245283
// the current transaction, the log will be removed as well.
246284
context.emit_raw_note_log_unsafe(log_tag, log, maybe_note_hash_counter.unwrap());

noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,12 @@ pub type CustomMessageHandler = unconstrained fn(
112112
/// this handler instead of [`do_sync_state`]. It receives all of the same parameters, so it can run custom logic
113113
/// (e.g. fetching and decrypting custom logs) before, after, or instead of calling [`do_sync_state`].
114114
pub type CustomSyncHandler = unconstrained fn(
115-
/* contract_address */ AztecAddress,
116-
/* compute_note_hash */ ComputeNoteHash,
117-
/* compute_note_nullifier */ ComputeNoteNullifier,
118-
/* process_custom_message */ Option<CustomMessageHandler>,
119-
/* offchain_inbox_sync */ Option<OffchainInboxSync>,
120-
/* scope */ AztecAddress,
121-
);
115+
/* contract_address */AztecAddress,
116+
/* compute_note_hash */ ComputeNoteHash,
117+
/* compute_note_nullifier */ ComputeNoteNullifier,
118+
/* process_custom_message */ Option<CustomMessageHandler>,
119+
/* offchain_inbox_sync */ Option<OffchainInboxSync>,
120+
/* scope */ AztecAddress);
122121

123122
/// Synchronizes the contract's private state with the network.
124123
///

noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,7 @@ pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs(
108108
// searching for this tagged log when performing message discovery in the future until we either find it or
109109
// the entry is somehow removed from the array.
110110
} else {
111-
assert(
112-
num_logs == 1,
113-
f"Expected at most 1 completion log per partial note, got {num_logs}",
114-
);
111+
assert(num_logs == 1, f"Expected at most 1 completion log per partial note, got {num_logs}");
115112

116113
aztecnr_debug_log_format!("Completion log found for partial note with tag {}")([
117114
pending_partial_note.note_completion_log_tag,
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
//! Compatibility wrapper for the legacy `messages::message_delivery` path.
22

33
pub use crate::messages::delivery::{
4-
MessageDelivery,
5-
MessageDeliveryEnum,
6-
do_private_message_delivery,
4+
ConstrainedDelivery, do_private_message_delivery, MessageDelivery, MessageDeliveryEnum,
75
};

noir-projects/aztec-nr/aztec/src/messages/processing/log_retrieval_request.nr

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ pub(crate) struct LogSourceEnum {
66
pub PUBLIC_AND_PRIVATE: Field,
77
}
88

9-
pub(crate) global LogSource: LogSourceEnum =
10-
LogSourceEnum { PRIVATE: 0, PUBLIC: 1, PUBLIC_AND_PRIVATE: 2 };
9+
pub(crate) global LogSource: LogSourceEnum = LogSourceEnum { PRIVATE: 0, PUBLIC: 1, PUBLIC_AND_PRIVATE: 2 };
1110

1211
/// A request for the `bulk_retrieve_logs` oracle to fetch all logs matching a tag.
1312
#[derive(Serialize)]

noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@ use crate::{
1919
discovery::partial_notes::DeliveredPendingPartialNote,
2020
encoding::MESSAGE_CIPHERTEXT_LEN,
2121
logs::{event::MAX_EVENT_SERIALIZED_LEN, note::MAX_NOTE_PACKED_LEN},
22-
processing::{
23-
log_retrieval_request::LogRetrievalRequest,
24-
log_retrieval_response::LogRetrievalResponse,
25-
},
22+
processing::{log_retrieval_request::LogRetrievalRequest, log_retrieval_response::LogRetrievalResponse},
2623
},
2724
oracle::message_processing,
2825
};

noir-projects/aztec-nr/aztec/src/note/note_message.nr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ where
8181
Option::some(self.new_note.note_hash_counter),
8282
recipient,
8383
delivery_mode,
84+
Option::none(),
8485
);
8586
}
8687

0 commit comments

Comments
 (0)