Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion noir-projects/aztec-nr/aztec/src/event/event_message.nr
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::{
event::{event_emission::NewEvent, event_interface::EventInterface},
messages::{
logs::event::encode_private_event_message,
message_delivery::{do_private_message_delivery, MessageDelivery},
message_delivery::{ConstrainedDelivery, do_private_message_delivery, MessageDelivery},
},
};
use crate::protocol::{address::AztecAddress, traits::Serialize};
Expand Down Expand Up @@ -61,6 +61,28 @@ where
Option::none(),
recipient,
delivery_mode,
Option::none(),
);
}

/// Delivers the event message to a `recipient` using constrained onchain tagging.
///
/// `registry` is the handshake registry used to resolve the shared secret and `sender` is the account the
/// message is sent on behalf of.
pub fn deliver_to_constrained(self, registry: AztecAddress, sender: AztecAddress, recipient: AztecAddress) {
// Technical note: we're about to call a closure that needs access to `new_event`, but we can't pass `self` to
// it because the closure might execute in unconstrained mode, and since `self` contains a mutable reference to
// `context` this would cause for a mutable reference to cross the constrained-unconstrained barrier, which is
// not allowed. As a workaround, we create a variable without the context and capture that instead.
let new_event = self.new_event;

do_private_message_delivery(
self.context,
|| encode_private_event_message(new_event.event, new_event.randomness),
Option::none(),
recipient,
MessageDelivery::onchain_constrained(),
Option::some(ConstrainedDelivery { registry, sender }),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use crate::oracle::{call_utility_function::call_utility_function, notes::get_nex
use crate::protocol::{
abis::function_selector::FunctionSelector,
address::AztecAddress,
constants::DOM_SEP__CONSTRAINED_MSG_NULLIFIER,
hash::poseidon2_hash_with_separator,
constants::{DOM_SEP__CONSTRAINED_MSG_LOG_TAG, DOM_SEP__CONSTRAINED_MSG_NULLIFIER},
hash::{compute_log_tag, poseidon2_hash, poseidon2_hash_with_separator},
traits::{Deserialize, ToField},
};

Expand All @@ -32,7 +32,8 @@ use crate::protocol::{
/// note.
/// - `index > 0`: asserts the prior nullifier
/// `poseidon2_hash_with_separator([sender, recipient, secret, index - 1], DOM_SEP__CONSTRAINED_MSG_NULLIFIER)`
/// exists via [`compute_nullifier_existence_request`](crate::nullifier::utils::compute_nullifier_existence_request).
/// exists via
/// [`compute_nullifier_existence_request`](crate::nullifier::utils::compute_nullifier_existence_request).
/// The chain transitively constrains back to the index-0 `validate_handshake`. The caller is expected
/// to emit the matching nullifier alongside each constrained send so subsequent calls can prove the
/// chain.
Expand All @@ -47,9 +48,7 @@ pub fn calculate_secret_and_index(
// Safety: the returned `Option<Field>` is untrusted and is constrained below before being returned to the
// caller, either by performing a new handshake or by constraining the prior handshake's existence.
let maybe_secret: Option<Field> = unsafe {
let selector = comptime {
FunctionSelector::from_signature("get_app_siloed_secret((Field),(Field),(Field))")
};
let selector = comptime { FunctionSelector::from_signature("get_app_siloed_secret((Field),(Field),(Field))") };
let returns = call_utility_function(
registry,
selector,
Expand Down Expand Up @@ -87,23 +86,51 @@ pub fn calculate_secret_and_index(
let index = unsafe { get_next_constrained_tagging_index(secret) };

if index == 0 {
let selector = comptime {
FunctionSelector::from_signature("validate_handshake((Field),(Field),Field)")
};
let _ = context.call_private_function(
registry,
selector,
[sender.to_field(), recipient.to_field(), secret],
);
let selector = comptime { FunctionSelector::from_signature("validate_handshake((Field),(Field),Field)") };
let _ =
context.call_private_function(registry, selector, [sender.to_field(), recipient.to_field(), secret]);
} else {
let prev_nullifier = poseidon2_hash_with_separator(
[sender.to_field(), recipient.to_field(), secret, (index - 1) as Field],
DOM_SEP__CONSTRAINED_MSG_NULLIFIER,
);
// TODO(F-670): exercise the index > 0 path end-to-end once send_constrained_msg emits nullifiers.
context.assert_nullifier_exists(compute_nullifier_existence_request(prev_nullifier, caller));
}

(secret, index)
}
}

/// Computes the constrained-delivery discovery log tag for an `(app_siloed_secret, index)` pair.
///
/// Collapses the secret and index into a single raw tag and domain-separates it with
/// `DOM_SEP__CONSTRAINED_MSG_LOG_TAG`. This mirrors the recipient-side derivation in the PXE (`siloedTagFor`): the
/// recipient recomputes the same tag to discover the message, and the protocol silos the emitted log's first field by
/// the emitting contract address, completing the match.
pub fn compute_constrained_log_tag(app_siloed_secret: Field, index: u32) -> Field {
compute_log_tag(poseidon2_hash([app_siloed_secret, index as Field]), DOM_SEP__CONSTRAINED_MSG_LOG_TAG)
}

/// Emits the per-send chain nullifier for a constrained message and returns its discovery log tag.
///
/// Used by [`crate::messages::delivery::do_private_message_delivery`] on the constrained-delivery path. It wraps
/// [`calculate_secret_and_index`] to resolve the `(app_siloed_secret, index)` pair, then:
/// 1. Emits the chain nullifier. A subsequent send at `index + 1` proves this nullifier exists, transitively constraining
/// the chain back to the `index == 0` handshake validation in [`calculate_secret_and_index`].
/// 2. Returns the log tag. The recipient recomputes the same tag from the handshake secret to discover the message.
pub(crate) fn emit_nullifier_and_compute_constrained_tag(
context: &mut PrivateContext,
registry: AztecAddress,
sender: AztecAddress,
recipient: AztecAddress,
) -> Field {
let (secret, index) = calculate_secret_and_index(context, registry, sender, recipient);

let nullifier = poseidon2_hash_with_separator(
[sender.to_field(), recipient.to_field(), secret, index as Field],
DOM_SEP__CONSTRAINED_MSG_NULLIFIER,
);
context.push_nullifier_unsafe(nullifier);

compute_constrained_log_tag(secret, index)
}
62 changes: 50 additions & 12 deletions noir-projects/aztec-nr/aztec/src/messages/delivery/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,36 @@ impl MessageDelivery {
}
}

/// Handshake inputs that opt a message delivery into constrained tagging.
///
/// Pass `Some` to [`do_private_message_delivery`] alongside [`MessageDelivery::onchain_constrained`] to derive
/// the log tag from a handshake-registry secret and emit the per-send chain nullifier (see
/// [`crate::messages::delivery::constrained_delivery`]). Pass `None` for the wallet-driven unconstrained tag.
pub struct ConstrainedDelivery {
/// Handshake registry to resolve the app-siloed shared secret from.
pub registry: AztecAddress,
/// Account the message is sent on behalf of, bound into both the secret/index resolution and the chain nullifier.
pub sender: AztecAddress,
}

/// Performs private delivery of a message to `recipient` according to `delivery_mode`.
///
/// The message is encoded into plaintext and then encrypted for `recipient`. This function takes a _function_ that
/// returns the plaintext instead of taking the plaintext directly in order to not waste constraints encoding the
/// message in scenarios where the plaintext will be encrypted with unconstrained encryption.
///
/// `maybe_note_hash_counter` is only relevant for on-chain delivery modes (i.e. via protocol logs): if a newly created
/// note hash's side effect counter is passed, then the log will be squashed alongside the note should its nullifier be
/// emitted in the current transaction. This is typically only used for note messages: since the note will not actually
/// be created, there is no point in delivering the message.
/// note hash's side effect counter is passed and constrained tagging is not requested, then the log will be squashed
/// alongside the note should its nullifier be emitted in the current transaction. Constrained-tagged logs are not tied
/// to note squashing because recipient discovery scans those tags sequentially.
///
/// `delivery_mode` must be a [`MessageDelivery`] value.
///
/// `maybe_constrained_delivery` opts into constrained tagging: when `Some` (only valid with
/// [`MessageDelivery::onchain_constrained`]) the log tag is derived from a handshake-registry secret and a chain
/// nullifier is emitted. When `None` the wallet-driven unconstrained tag is used. Its some-ness must be a
/// compile-time constant.
///
/// ## Privacy
///
/// The emitted log always has the same length regardless of `MESSAGE_PLAINTEXT_LEN`, because all message ciphertexts
Expand All @@ -69,17 +86,25 @@ pub fn do_private_message_delivery<Env, let MESSAGE_PLAINTEXT_LEN: u32>(
maybe_note_hash_counter: Option<u32>,
recipient: AztecAddress,
delivery_mode: MessageDelivery,
maybe_constrained_delivery: Option<ConstrainedDelivery>,
) {
// This function relies on `delivery_mode` being a constant in order to reduce circuit constraints when
// unconstrained usage is requested. If `delivery_mode` were a runtime value the compiler would be unable to
// perform dead-code elimination.
assert_constant(delivery_mode);

// The following maps out the 3 dimensions across which we configure message delivery.
// The following maps out the dimensions across which we configure message delivery.
let constrained_encryption = delivery_mode == MessageDelivery::onchain_constrained();
let deliver_as_offchain_message = delivery_mode == MessageDelivery::offchain();
// TODO(#14565): Add constrained tagging
let _constrained_tagging = delivery_mode == MessageDelivery::onchain_constrained();

// Constrained tagging is opt-in via `maybe_constrained_delivery`. Its some-ness must be a compile-time constant.
assert_constant(maybe_constrained_delivery.is_some());
let constrained_tagging = maybe_constrained_delivery.is_some();
if constrained_tagging {
// Constrained tagging requires the constrained encryption that `ONCHAIN_CONSTRAINED` selects, otherwise the
// sender could pair a correct tag with forge-able (unconstrained) ciphertext.
assert(constrained_encryption, "constrained tagging requires ONCHAIN_CONSTRAINED delivery");
}

let contract_address = context.this_address();

Expand All @@ -91,20 +116,33 @@ pub fn do_private_message_delivery<Env, let MESSAGE_PLAINTEXT_LEN: u32>(
if deliver_as_offchain_message {
deliver_offchain_message(ciphertext, recipient);
} else {
// TODO(#14565): constrained tagging is not yet implemented. Both modes currently use the unconstrained
// domain separator because the discovery tag always comes from an oracle. Once constrained tagging lands,
// this should branch on `constrained_tagging` to select the appropriate separator.
let discovery_tag = compute_discovery_tag(recipient);
let log_tag = compute_log_tag(discovery_tag, DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG);
let log_tag = if constrained_tagging {
let delivery_info = maybe_constrained_delivery.unwrap_unchecked();
constrained_delivery::emit_nullifier_and_compute_constrained_tag(
context,
delivery_info.registry,
delivery_info.sender,
recipient,
)
} else {
// High-level constrained delivery APIs supply handshake info instead of taking this fallback.
// `ONCHAIN_UNCONSTRAINED` deliveries and legacy `ONCHAIN_CONSTRAINED` calls derive the wallet-driven tag
// here.
let discovery_tag = compute_discovery_tag(recipient);
compute_log_tag(discovery_tag, DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG)
};

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

// TODO(F-664): Add e2e coverage for constrained note delivery through kernel squashing and recipient sync.
let squashable_note_log = maybe_note_hash_counter.is_some() & !constrained_tagging;

let log = BoundedVec::from_array(ciphertext);
if maybe_note_hash_counter.is_some() {
if squashable_note_log {
// We associate the log with the note's side effect counter, so that if the note ends up being squashed in
// the current transaction, the log will be removed as well.
context.emit_raw_note_log_unsafe(log_tag, log, maybe_note_hash_counter.unwrap());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Compatibility wrapper for the legacy `messages::message_delivery` path.

pub use crate::messages::delivery::{
ConstrainedDelivery,
MessageDelivery,
do_private_message_delivery,
};
56 changes: 54 additions & 2 deletions noir-projects/aztec-nr/aztec/src/note/note_message.nr
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::{
context::PrivateContext,
messages::{
logs::note::encode_private_note_message,
message_delivery::{do_private_message_delivery, MessageDelivery},
message_delivery::{ConstrainedDelivery, do_private_message_delivery, MessageDelivery},
},
note::{lifecycle::NewNote, note_interface::NoteType},
};
Expand Down Expand Up @@ -52,6 +52,14 @@ where
self.deliver_to(self.new_note.owner, delivery_mode);
}

/// Delivers the note message to its owner using constrained on-chain tagging.
///
/// `registry` is the handshake registry used to resolve the shared secret and `sender` is the account the
/// message is sent on behalf of.
pub fn deliver_constrained(self, registry: AztecAddress, sender: AztecAddress) {
self.deliver_to_constrained(registry, sender, self.new_note.owner);
}

/// Same as [`deliver`](NoteMessage::deliver), except the message gets delivered to an arbitrary `recipient`
/// instead
/// of the note owner.
Expand All @@ -71,6 +79,7 @@ where
// it because the closure might execute in unconstrained mode, and since `self` contains a mutable reference to
// `context` this would cause for a mutable reference to cross the constrained-unconstrained barrier, which is
// not allowed. As a workaround, we create a variable without the context and capture that instead.
let note_hash_counter = self.new_note.note_hash_counter;
let new_note = self.new_note;

do_private_message_delivery(
Expand All @@ -81,9 +90,35 @@ where
new_note.storage_slot,
new_note.randomness,
),
Option::some(self.new_note.note_hash_counter),
Option::some(note_hash_counter),
recipient,
delivery_mode,
Option::none(),
);
}

/// Same as [`deliver_constrained`](NoteMessage::deliver_constrained), except the message gets delivered to an
/// arbitrary `recipient` instead of the note owner.
pub fn deliver_to_constrained(self, registry: AztecAddress, sender: AztecAddress, recipient: 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.

These deliver_to_constrained methods were added to not break the pre-existing APIs. We should utilize the builder pattern here once #23619 lands.

// Technical note: we're about to call a closure that needs access to `new_note`, but we can't pass `self` to
// it because the closure might execute in unconstrained mode, and since `self` contains a mutable reference to
// `context` this would cause for a mutable reference to cross the constrained-unconstrained barrier, which is
// not allowed. As a workaround, we create a variable without the context and capture that instead.
let note_hash_counter = self.new_note.note_hash_counter;
let new_note = self.new_note;

do_private_message_delivery(
self.context,
|| encode_private_note_message(
new_note.note,
new_note.owner,
new_note.storage_slot,
new_note.randomness,
),
Option::some(note_hash_counter),
recipient,
MessageDelivery::onchain_constrained(),
Option::some(ConstrainedDelivery { registry, sender }),
);
}

Expand Down Expand Up @@ -137,6 +172,14 @@ where
}
}

/// Same as [`NoteMessage::deliver_constrained`], except the message will only be delivered if it actually exists.
pub fn deliver_constrained(self, registry: AztecAddress, sender: AztecAddress) {
if self.maybe_new_note.is_some() {
let note_message = NoteMessage::new(self.maybe_new_note.unwrap_unchecked(), self.context);
note_message.deliver_constrained(registry, sender);
}
}

/// Same as [`NoteMessage::deliver_to`], except the message will only be delivered if it actually exists.
///
/// Messages delivered using [`crate::messages::message_delivery::MessageDelivery::onchain_constrained()`] will
Expand All @@ -147,6 +190,15 @@ where
}
}

/// Same as [`NoteMessage::deliver_to_constrained`], except the message will only be delivered if it actually
/// exists.
pub fn deliver_to_constrained(self, registry: AztecAddress, sender: AztecAddress, recipient: AztecAddress) {
if self.maybe_new_note.is_some() {
let note_message = NoteMessage::new(self.maybe_new_note.unwrap_unchecked(), self.context);
note_message.deliver_to_constrained(registry, sender, recipient);
}
}

/// Returns the note contained in the message.
pub fn get_note(self) -> Option<Note> {
self.maybe_new_note.map(|new_note| new_note.note)
Expand Down
1 change: 1 addition & 0 deletions noir-projects/aztec-nr/uint-note/src/uint_note.nr
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ impl UintNote {
Option::none(),
recipient,
MessageDelivery::onchain_unconstrained(),
Option::none(),
);

let partial_note = PartialUintNote { commitment };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ impl NFTNote {
Option::none(),
recipient,
MessageDelivery::onchain_unconstrained(),
Option::none(),
);

let partial_note = PartialNFTNote { commitment };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ type = "contract"

[dependencies]
aztec = { path = "../../../../aztec-nr/aztec" }
balance_set = { path = "../../../../aztec-nr/balance-set" }
field_note = { path = "../../../../aztec-nr/field-note" }
handshake_registry_contract = { path = "../../message_discovery/handshake_registry_contract" }
Loading
Loading