Skip to content
Merged
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
2 changes: 2 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"chiplet",
"cimg",
"ciphertext",
"ciphertexts",
"clonedeep",
"clonedeepwith",
"cmd",
Expand Down Expand Up @@ -151,6 +152,7 @@
"homomorphic",
"ierc",
"IGSE",
"incentivized",
"indexeddb",
"interruptible",
"IPFS",
Expand Down
11 changes: 11 additions & 0 deletions docs/docs/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ As we prepare for a bigger `Wallet` interface refactor and the upcoming `WalletS

## [Aztec.nr]

### Private event emission API changes

The private event emission API has been significantly reworked to provide clearer semantics around message delivery guarantees. The key changes are:

1. `emit_event_in_private_log` has been renamed to `emit_event_in_private` and now takes a `delivery_mode` parameter instead of `constraints`
2. `emit_event_as_offchain_message` has been removed in favor of using `emit_event_in_private` with `MessageDelivery.UNCONSTRAINED_OFFCHAIN`
3. `PrivateLogContent` enum has been replaced with `MessageDelivery` enum with the following values:
- `CONSTRAINED_ONCHAIN`: For on-chain delivery with cryptographic guarantees (replaces `CONSTRAINED_ENCRYPTION`)
- `UNCONSTRAINED_OFFCHAIN`: For off-chain delivery without constraints
- `UNCONSTRAINED_ONCHAIN`: For on-chain delivery without constraints (replaces `NO_CONSTRAINTS`)

### Contract functions can no longer be `pub` or `pub(crate)`

With the latest changes to `TestEnvironment`, making contract functions have public visibility is no longer required given the new `call_public` and `simulate_utility` functions. To avoid accidental direct invocation, and to reduce confusion with the autogenerated interfaces, we're forbidding them being public.
Expand Down
169 changes: 102 additions & 67 deletions noir-projects/aztec-nr/aztec/src/event/event_interface.nr
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
logs::{event::to_encrypted_private_event_message, utils::prefix_with_tag},
offchain_messages::emit_offchain_message,
},
utils::remove_constraints::{remove_constraints, remove_constraints_if},
utils::remove_constraints::remove_constraints_if,
};
use dep::protocol_types::{
address::AztecAddress,
Expand All @@ -14,50 +14,113 @@ use dep::protocol_types::{
traits::{Serialize, ToField},
};

pub struct PrivateLogContentConstraintsEnum {
/// The contents of the log are entirely unconstrained, and could have any values.
///
/// Only use in scenarios where the recipient not receiving the message is an acceptable outcome (e.g. because the
/// sender is somehow motivated to ensure the recipient learns of it).
pub NO_CONSTRAINTS: u8,
/// The contents of the log and its encryption are constrained. The tag (and therefore whether the recipient is
/// actually able to find the message) is not.
/// Specifies the configuration parameters for message delivery. There are two fundamental aspects to consider:
///
/// +----------------------------------------------------------------------------------------------------------+
/// | 1. Delivery Mechanism |
/// | - Messages can be delivered either on-chain or out-of-band |
/// | - On-chain delivery uses the Aztec protocol's private log stream, submitted to L1 blobs and consuming DA |
/// | - Out-of-band delivery is implemented by the application (e.g. storing ciphertexts in cloud storage) |
/// | - Out-of-band delivery cannot have any cryptographic constraints since messages are never stored on-chain|
/// +----------------------------------------------------------------------------------------------------------+
///
/// For on-chain delivery, we must also consider:
///
/// +----------------------------------------------------------------------------------------------------------+
/// | 2. Message Encryption and Tagging |
/// | - Messages can use either constrained or unconstrained encryption |
/// | - Constrained encryption guarantees the ciphertext is formed correctly but costs more in constraints, |
/// | which results in slower proving times |
/// | - Unconstrained encryption trusts the sender but is cheaper constraint-wise and hence faster to prove |
/// | - Tagging is an indexing mechanism that helps recipients locate their messages |
/// | - If tagging is not performed correctly by the sender, the recipient will not be able to find the message|
/// +----------------------------------------------------------------------------------------------------------+
///
/// For off-chain delivery, constrained encryption is not relevant since it doesn't provide any additional guarantees
/// over unconstrained encryption and is slower to prove (requiring more constraints).
///
/// There are three available delivery modes described below.
pub struct MessageDeliveryEnum {
/// 1. Constrained On-chain
/// - Uses constrained encryption and in the future constrained tagging (issue #14565) with on-chain delivery
/// - Provides cryptographic guarantees that recipients can discover and decrypt messages (once #14565 is tackled)
/// - Slowest proving times since encryption is constrained
/// - Expensive since it consumes L1 blob space
/// - Use when smart contracts need to make decisions based on message contents
/// - Example 1: An escrow contract facilitating a private NFT sale that needs to verify payment before releasing
/// the NFT to the buyer.
/// - Example 2: An application with private configuration where changes must be broadcast to all participants.
/// This ensures every user can access the latest configuration. Without notification of config changes,
/// users would be unable to read updated variables and therefore blocked from using the application's
/// functions. This pattern applies to all critical events that require universal broadcast.
///
/// Only use in scenarios where the recipient not receiving the message is an acceptable outcome (e.g. because the
/// sender is somehow motivated to ensure the recipient learns of it).
// TODO(#14565): This variant requires for tagging to also be constrained, as it is otherwise useless.
pub CONSTRAINED_ENCRYPTION: u8,
/// Safety: Despite being called CONSTRAINED_ONCHAIN, this delivery mode is currently NOT fully constrained.
/// The tag prefixing is unconstrained, meaning a malicious sender could manipulate the tag to prevent
/// recipient decryption. TODO(#14565): Implement proper constrained tag prefixing.
pub CONSTRAINED_ONCHAIN: u8,

/// 2. Unconstrained On-chain
/// - Uses unconstrained encryption and tagging with on-chain delivery
/// - Faster proving times since no constraints are used for encryption
/// - Expensive since it consumes L1 blob space
/// - Suitable when recipients can verify message validity through other means
/// - Use this if you don't need the cryptographic guarantees of constrained encryption and tagging but
/// don't want to deal with setting up out-of-band delivery infrastructure as required by mode 3
/// - Example: Depositing a privately-held NFT into an NFT-sale escrow contract. The buyers know the escrow
/// contract's decryption keys, they receive the message on-chain and are willing to buy the NFT only if the NFT
/// contained in the message is legitimate.
pub UNCONSTRAINED_ONCHAIN: u8,

/// 3. Out-of-band
/// - Uses unconstrained encryption with off-chain delivery
/// - Lowest cost since no on-chain storage is needed and short proving times since no constraints are used
/// for encryption
/// - Suitable when recipients can verify message validity through other means
/// - Requires setting up custom infrastructure for handling off-chain delivery (e.g. cloud storage)
/// - Example: A payment app where a merchant receives the message off-chain and is willing to release the goods
/// once he verifies that the payment is correct (i.e. can decrypt the message and verify that it contains
/// a legitimate token note - note with note commitment in the note hash tree).
pub UNCONSTRAINED_OFFCHAIN: u8,
}

pub global PrivateLogContent: PrivateLogContentConstraintsEnum = PrivateLogContentConstraintsEnum {
NO_CONSTRAINTS: 1,
CONSTRAINED_ENCRYPTION: 2,
// TODO: add constrained tagging and constrained handshaking
pub global MessageDelivery: MessageDeliveryEnum = MessageDeliveryEnum {
CONSTRAINED_ONCHAIN: 1,
UNCONSTRAINED_ONCHAIN: 2,
UNCONSTRAINED_OFFCHAIN: 3,
};

/// Emits an event in a private log, encrypting it such that only `recipient` will learn of its contents. The log will
/// be tagged using a shared secret between `sender` and `recipient`, so that `recipient` can efficiently find the log.
/// Emits an event that can be delivered either via private logs or offchain messages, with configurable encryption and
/// tagging constraints.
///
/// The `constraints` value determines what parts of this computation will be constrained. See the documentation for
/// each value in `PrivateLogContentConstraintsEnum` to learn more about the different variants.
pub fn emit_event_in_private_log<Event>(
/// # Arguments
/// * `event` - The event to emit
/// * `context` - The private context to emit the event in
/// * `recipient` - The address that should receive this event
/// * `delivery_mode` - Controls encryption, tagging, and delivery constraints. Must be a compile-time constant.
/// See `MessageDeliveryEnum` for details on the available modes.
pub fn emit_event_in_private<Event>(
event: Event,
context: &mut PrivateContext,
recipient: AztecAddress,
constraints: u8,
delivery_mode: u8,
)
where
Event: EventInterface + Serialize,
{
// This function relies on `constraints` being a constant in order to reduce circuit constraints when unconstrained
// usage is requested. If `constraints` were a runtime value then performance would suffer.
assert_constant(constraints);
// 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 then performance would suffer.
assert_constant(delivery_mode);

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

let (ciphertext, randomness) = remove_constraints_if(
constraints == PrivateLogContent.NO_CONSTRAINTS,
!constrained_encryption,
|| to_encrypted_private_event_message(event, recipient),
);
let log_content = prefix_with_tag(ciphertext, recipient);

// We generate a cryptographic commitment to the event to ensure its authenticity during out-of-band delivery.
// The nullifier tree is chosen over the note hash tree for this purpose since it provides a simpler mechanism
Expand All @@ -71,48 +134,20 @@ where
);
context.push_nullifier(event_commitment);

context.emit_private_log(log_content, log_content.len());
}
if emit_as_offchain_message {
// Safety: Offchain messages are by definition unconstrained. They are emitted via the `emit_offchain_effect`
// oracle which we don't use for anything besides its side effects, therefore this is safe to call.
unsafe { emit_offchain_message(ciphertext, recipient) };
} else {
// Safety: Currently unsafe. See description of CONSTRAINED_ONCHAIN in MessageDeliveryEnum.
// TODO(#14565): Implement proper constrained tag prefixing to make this truly CONSTRAINED_ONCHAIN
let log_content = prefix_with_tag(ciphertext, recipient);

/// Emits an event as an offchain message. Similar to private log emission but uses offchain message mechanism instead.
///
/// Unlike private log emission, encryption here is always unconstrained. This design choice stems from the nature of
/// offchain messages - they lack guaranteed delivery, unlike private logs. Without delivery guarantees, smart
/// contracts cannot make assumptions about a message being delivered, making constrained encryption unnecessary.
/// However, message integrity remains protected through a cryptographic commitment stored in the nullifier tree,
/// preventing tampering even in the absence of guaranteed delivery. See the description of the
/// `messages::offchain_message::emit_offchain_message` function for more details on when a guaranteed delivery is
/// valuable. If guaranteed delivery is required, the `emit_event_in_private_log` function should be used instead.
pub fn emit_event_as_offchain_message<Event>(
event: Event,
context: &mut PrivateContext,
recipient: AztecAddress,
)
where
Event: EventInterface + Serialize,
{
// Safety: as explained above, this function places no constraints on the content of the message.
let (message_ciphertext, randomness) =
unsafe { remove_constraints(|| to_encrypted_private_event_message(event, recipient)) };

// We generate a cryptographic commitment to the event to ensure its authenticity during out-of-band delivery. Note
// that the commitment is made from the (constrained) event content, and not the (unconstrained) ciphertext.
// The nullifier tree is chosen over the note hash tree for this purpose since it provides a simpler mechanism
// - nullifiers require no nonce, and events, being non-spendable, don't need the guarantee that a "spending"
// nullifier can be computed.
// TODO(#11571): with decryption happening in Noir we can now use the Packable trait instead.
let serialized_event_with_randomness = [randomness].concat(event.serialize());
let event_commitment = poseidon2_hash_with_separator(
serialized_event_with_randomness,
GENERATOR_INDEX__EVENT_COMMITMENT,
);
context.push_nullifier(event_commitment);

// Safety: Offchain effects are by definition unconstrained. They are emitted via an oracle
// which we don't use for anything besides its side effects, therefore this is safe to call.
unsafe { emit_offchain_message(message_ciphertext, recipient) };
context.emit_private_log(log_content, log_content.len());
}
}

// TODO(benesjan): rename to emit_event_in_public
pub fn emit_event_in_public_log<Event>(event: Event, context: &mut PublicContext)
where
Event: EventInterface + Serialize,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub unconstrained fn process_private_event_msg(
GENERATOR_INDEX__EVENT_COMMITMENT,
);

// Randomness was injected into the event payload in `emit_event_in_private_log` but we have already used it
// Randomness was injected into the event payload in `emit_event_in_private` but we have already used it
// to compute the event commitment, so we can safely discard it now.
let serialized_event = array::subbvec(
serialized_event_with_randomness,
Expand Down
1 change: 1 addition & 0 deletions noir-projects/aztec-nr/aztec/src/messages/logs/utils.nr
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::oracle::notes::{
};
use dep::protocol_types::address::AztecAddress;

// TODO(#14565): Add constrained tagging
pub(crate) fn prefix_with_tag<let L: u32>(
log_without_tag: [Field; L],
recipient: AztecAddress,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub global OFFCHAIN_MESSAGE_IDENTIFIER: Field =
///
/// * `message` - The message to emit.
/// * `recipient` - The address of the recipient.
// TODO(benesjan): Make this function constrained.
pub unconstrained fn emit_offchain_message<T>(message: T, recipient: AztecAddress)
where
T: Serialize,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub contract SimpleToken {
use dep::aztec::{
authwit::auth::compute_authwit_nullifier,
context::{PrivateCallInterface, PrivateContext, PublicContext},
event::event_interface::{emit_event_in_private_log, PrivateLogContent},
event::event_interface::{emit_event_in_private, MessageDelivery},
macros::{
events::event,
functions::{authorize_once, initializer, internal, private, public, utility, view},
Expand Down Expand Up @@ -153,11 +153,11 @@ pub contract SimpleToken {
to,
));

emit_event_in_private_log(
emit_event_in_private(
Transfer { from, to, amount },
&mut context,
to,
PrivateLogContent.NO_CONSTRAINTS,
MessageDelivery.UNCONSTRAINED_ONCHAIN,
Comment thread
benesjan marked this conversation as resolved.
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub contract Token {

use dep::aztec::{
context::{PrivateCallInterface, PrivateContext, PublicContext},
event::event_interface::{emit_event_in_private_log, PrivateLogContent},
event::event_interface::{emit_event_in_private, MessageDelivery},
macros::{
events::event,
functions::{authorize_once, initializer, internal, private, public, utility, view},
Expand Down Expand Up @@ -301,11 +301,11 @@ pub contract Token {
// another person where the payment is considered to be successful when the other party successfully decrypts a
// note).
// docs:start:encrypted_unconstrained
emit_event_in_private_log(
emit_event_in_private(
Transfer { from, to, amount },
&mut context,
to,
PrivateLogContent.NO_CONSTRAINTS,
MessageDelivery.UNCONSTRAINED_ONCHAIN,
);
// docs:end:encrypted_unconstrained
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use aztec::macros::aztec;
#[aztec]
contract EventOnly {
use aztec::{
event::event_interface::{emit_event_in_private_log, PrivateLogContent},
event::event_interface::{emit_event_in_private, MessageDelivery},
macros::{events::event, functions::private},
};

Expand All @@ -17,11 +17,11 @@ contract EventOnly {
#[private]
fn emit_event_for_msg_sender(value: Field) {
let sender = context.msg_sender();
emit_event_in_private_log(
emit_event_in_private(
TestEvent { value },
&mut context,
sender,
PrivateLogContent.NO_CONSTRAINTS,
MessageDelivery.UNCONSTRAINED_ONCHAIN,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use aztec::macros::aztec;
#[aztec]
contract OffchainEffect {
use aztec::{
event::event_interface::emit_event_as_offchain_message,
event::event_interface::{emit_event_in_private, MessageDelivery},
macros::{events::event, functions::{private, utility}, storage::storage},
messages::logs::note::encode_and_encrypt_note_and_emit_as_offchain_message,
note::note_viewer_options::NoteViewerOptions,
Expand Down Expand Up @@ -51,7 +51,12 @@ contract OffchainEffect {

#[private]
fn emit_event_as_offchain_message_for_msg_sender(a: u64, b: u64, c: u64) {
emit_event_as_offchain_message(TestEvent { a, b, c }, &mut context, context.msg_sender());
emit_event_in_private(
TestEvent { a, b, c },
&mut context,
context.msg_sender(),
MessageDelivery.UNCONSTRAINED_OFFCHAIN,
);
}

#[private]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub contract Test {
use dep::aztec::note::constants::MAX_NOTES_PER_PAGE;

use dep::aztec::{
event::event_interface::{emit_event_in_private_log, PrivateLogContent},
event::event_interface::{emit_event_in_private, MessageDelivery},
hash::{ArgsHasher, pedersen_hash},
history::note_inclusion::ProveNoteInclusion,
macros::{
Expand Down Expand Up @@ -340,7 +340,12 @@ pub contract Test {
value4: fields[4],
};

emit_event_in_private_log(event, &mut context, owner, PrivateLogContent.NO_CONSTRAINTS);
emit_event_in_private(
event,
&mut context,
owner,
MessageDelivery.UNCONSTRAINED_ONCHAIN,
);

// this contract has reached max number of functions, so using this one fn
// to test nested and non nested encrypted logs
Expand Down
Loading
Loading