Skip to content

Commit 59fdfe7

Browse files
authored
refactor!: merging event emission functions (#16126)
## Summary This PR consolidates the event emission API by merging `emit_event_in_private_log` and `emit_event_as_offchain_message` into a single `emit_event_in_private` function with configurable delivery modes. ## Key Changes - **Unified API**: Replaced `PrivateLogContentConstraintsEnum` with `MessageDeliveryEnum` that provides clear delivery mode options - **Two Primary Modes**: - `CONSTRAINED_ONCHAIN`: Constrained encryption + tagging + private log stream (for smart contract decisions) - `UNCONSTRAINED_OFFCHAIN`: Unconstrained encryption + offchain delivery (for trusted/incentivized scenarios) - **Enhanced Documentation**: Comprehensive documentation explaining the three dimensions of message delivery (encryption constraints, delivery mechanism, tagging constraints) and their use cases - **Backward Compatibility**: Maintained `UNCONSTRAINED_ONCHAIN` mode for existing code (marked for future removal) ## Benefits - **Clearer Intent**: Developers can now choose delivery modes based on their specific security and trust requirements - **Simplified API**: Single function handles all private event emission scenarios - **Improved Security Model**: Explicit guidance on when to use each delivery mode ## Breaking Changes - `emit_event_in_private_log` → `emit_event_in_private` - `emit_event_as_offchain_message` → `emit_event_in_private` with `UNCONSTRAINED_OFFCHAIN` mode - `PrivateLogContent` → `MessageDelivery` All contract examples and tests have been updated to use the new API.
2 parents 5146518 + 0a136a7 commit 59fdfe7

File tree

13 files changed

+154
-96
lines changed

13 files changed

+154
-96
lines changed

cspell.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"chiplet",
6464
"cimg",
6565
"ciphertext",
66+
"ciphertexts",
6667
"clonedeep",
6768
"clonedeepwith",
6869
"cmd",
@@ -151,6 +152,7 @@
151152
"homomorphic",
152153
"ierc",
153154
"IGSE",
155+
"incentivized",
154156
"indexeddb",
155157
"interruptible",
156158
"IPFS",

docs/docs/migration_notes.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ As we prepare for a bigger `Wallet` interface refactor and the upcoming `WalletS
4444

4545
## [Aztec.nr]
4646

47+
### Private event emission API changes
48+
49+
The private event emission API has been significantly reworked to provide clearer semantics around message delivery guarantees. The key changes are:
50+
51+
1. `emit_event_in_private_log` has been renamed to `emit_event_in_private` and now takes a `delivery_mode` parameter instead of `constraints`
52+
2. `emit_event_as_offchain_message` has been removed in favor of using `emit_event_in_private` with `MessageDelivery.UNCONSTRAINED_OFFCHAIN`
53+
3. `PrivateLogContent` enum has been replaced with `MessageDelivery` enum with the following values:
54+
- `CONSTRAINED_ONCHAIN`: For on-chain delivery with cryptographic guarantees (replaces `CONSTRAINED_ENCRYPTION`)
55+
- `UNCONSTRAINED_OFFCHAIN`: For off-chain delivery without constraints
56+
- `UNCONSTRAINED_ONCHAIN`: For on-chain delivery without constraints (replaces `NO_CONSTRAINTS`)
57+
4758
### Contract functions can no longer be `pub` or `pub(crate)`
4859

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

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

Lines changed: 102 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::{
55
logs::{event::to_encrypted_private_event_message, utils::prefix_with_tag},
66
offchain_messages::emit_offchain_message,
77
},
8-
utils::remove_constraints::{remove_constraints, remove_constraints_if},
8+
utils::remove_constraints::remove_constraints_if,
99
};
1010
use dep::protocol_types::{
1111
address::AztecAddress,
@@ -14,50 +14,113 @@ use dep::protocol_types::{
1414
traits::{Serialize, ToField},
1515
};
1616

17-
pub struct PrivateLogContentConstraintsEnum {
18-
/// The contents of the log are entirely unconstrained, and could have any values.
19-
///
20-
/// Only use in scenarios where the recipient not receiving the message is an acceptable outcome (e.g. because the
21-
/// sender is somehow motivated to ensure the recipient learns of it).
22-
pub NO_CONSTRAINTS: u8,
23-
/// The contents of the log and its encryption are constrained. The tag (and therefore whether the recipient is
24-
/// actually able to find the message) is not.
17+
/// Specifies the configuration parameters for message delivery. There are two fundamental aspects to consider:
18+
///
19+
/// +----------------------------------------------------------------------------------------------------------+
20+
/// | 1. Delivery Mechanism |
21+
/// | - Messages can be delivered either on-chain or out-of-band |
22+
/// | - On-chain delivery uses the Aztec protocol's private log stream, submitted to L1 blobs and consuming DA |
23+
/// | - Out-of-band delivery is implemented by the application (e.g. storing ciphertexts in cloud storage) |
24+
/// | - Out-of-band delivery cannot have any cryptographic constraints since messages are never stored on-chain|
25+
/// +----------------------------------------------------------------------------------------------------------+
26+
///
27+
/// For on-chain delivery, we must also consider:
28+
///
29+
/// +----------------------------------------------------------------------------------------------------------+
30+
/// | 2. Message Encryption and Tagging |
31+
/// | - Messages can use either constrained or unconstrained encryption |
32+
/// | - Constrained encryption guarantees the ciphertext is formed correctly but costs more in constraints, |
33+
/// | which results in slower proving times |
34+
/// | - Unconstrained encryption trusts the sender but is cheaper constraint-wise and hence faster to prove |
35+
/// | - Tagging is an indexing mechanism that helps recipients locate their messages |
36+
/// | - If tagging is not performed correctly by the sender, the recipient will not be able to find the message|
37+
/// +----------------------------------------------------------------------------------------------------------+
38+
///
39+
/// For off-chain delivery, constrained encryption is not relevant since it doesn't provide any additional guarantees
40+
/// over unconstrained encryption and is slower to prove (requiring more constraints).
41+
///
42+
/// There are three available delivery modes described below.
43+
pub struct MessageDeliveryEnum {
44+
/// 1. Constrained On-chain
45+
/// - Uses constrained encryption and in the future constrained tagging (issue #14565) with on-chain delivery
46+
/// - Provides cryptographic guarantees that recipients can discover and decrypt messages (once #14565 is tackled)
47+
/// - Slowest proving times since encryption is constrained
48+
/// - Expensive since it consumes L1 blob space
49+
/// - Use when smart contracts need to make decisions based on message contents
50+
/// - Example 1: An escrow contract facilitating a private NFT sale that needs to verify payment before releasing
51+
/// the NFT to the buyer.
52+
/// - Example 2: An application with private configuration where changes must be broadcast to all participants.
53+
/// This ensures every user can access the latest configuration. Without notification of config changes,
54+
/// users would be unable to read updated variables and therefore blocked from using the application's
55+
/// functions. This pattern applies to all critical events that require universal broadcast.
2556
///
26-
/// Only use in scenarios where the recipient not receiving the message is an acceptable outcome (e.g. because the
27-
/// sender is somehow motivated to ensure the recipient learns of it).
28-
// TODO(#14565): This variant requires for tagging to also be constrained, as it is otherwise useless.
29-
pub CONSTRAINED_ENCRYPTION: u8,
57+
/// Safety: Despite being called CONSTRAINED_ONCHAIN, this delivery mode is currently NOT fully constrained.
58+
/// The tag prefixing is unconstrained, meaning a malicious sender could manipulate the tag to prevent
59+
/// recipient decryption. TODO(#14565): Implement proper constrained tag prefixing.
60+
pub CONSTRAINED_ONCHAIN: u8,
61+
62+
/// 2. Unconstrained On-chain
63+
/// - Uses unconstrained encryption and tagging with on-chain delivery
64+
/// - Faster proving times since no constraints are used for encryption
65+
/// - Expensive since it consumes L1 blob space
66+
/// - Suitable when recipients can verify message validity through other means
67+
/// - Use this if you don't need the cryptographic guarantees of constrained encryption and tagging but
68+
/// don't want to deal with setting up out-of-band delivery infrastructure as required by mode 3
69+
/// - Example: Depositing a privately-held NFT into an NFT-sale escrow contract. The buyers know the escrow
70+
/// contract's decryption keys, they receive the message on-chain and are willing to buy the NFT only if the NFT
71+
/// contained in the message is legitimate.
72+
pub UNCONSTRAINED_ONCHAIN: u8,
73+
74+
/// 3. Out-of-band
75+
/// - Uses unconstrained encryption with off-chain delivery
76+
/// - Lowest cost since no on-chain storage is needed and short proving times since no constraints are used
77+
/// for encryption
78+
/// - Suitable when recipients can verify message validity through other means
79+
/// - Requires setting up custom infrastructure for handling off-chain delivery (e.g. cloud storage)
80+
/// - Example: A payment app where a merchant receives the message off-chain and is willing to release the goods
81+
/// once he verifies that the payment is correct (i.e. can decrypt the message and verify that it contains
82+
/// a legitimate token note - note with note commitment in the note hash tree).
83+
pub UNCONSTRAINED_OFFCHAIN: u8,
3084
}
3185

32-
pub global PrivateLogContent: PrivateLogContentConstraintsEnum = PrivateLogContentConstraintsEnum {
33-
NO_CONSTRAINTS: 1,
34-
CONSTRAINED_ENCRYPTION: 2,
35-
// TODO: add constrained tagging and constrained handshaking
86+
pub global MessageDelivery: MessageDeliveryEnum = MessageDeliveryEnum {
87+
CONSTRAINED_ONCHAIN: 1,
88+
UNCONSTRAINED_ONCHAIN: 2,
89+
UNCONSTRAINED_OFFCHAIN: 3,
3690
};
3791

38-
/// Emits an event in a private log, encrypting it such that only `recipient` will learn of its contents. The log will
39-
/// be tagged using a shared secret between `sender` and `recipient`, so that `recipient` can efficiently find the log.
92+
/// Emits an event that can be delivered either via private logs or offchain messages, with configurable encryption and
93+
/// tagging constraints.
4094
///
41-
/// The `constraints` value determines what parts of this computation will be constrained. See the documentation for
42-
/// each value in `PrivateLogContentConstraintsEnum` to learn more about the different variants.
43-
pub fn emit_event_in_private_log<Event>(
95+
/// # Arguments
96+
/// * `event` - The event to emit
97+
/// * `context` - The private context to emit the event in
98+
/// * `recipient` - The address that should receive this event
99+
/// * `delivery_mode` - Controls encryption, tagging, and delivery constraints. Must be a compile-time constant.
100+
/// See `MessageDeliveryEnum` for details on the available modes.
101+
pub fn emit_event_in_private<Event>(
44102
event: Event,
45103
context: &mut PrivateContext,
46104
recipient: AztecAddress,
47-
constraints: u8,
105+
delivery_mode: u8,
48106
)
49107
where
50108
Event: EventInterface + Serialize,
51109
{
52-
// This function relies on `constraints` being a constant in order to reduce circuit constraints when unconstrained
53-
// usage is requested. If `constraints` were a runtime value then performance would suffer.
54-
assert_constant(constraints);
110+
// This function relies on `delivery_mode` being a constant in order to reduce circuit constraints when unconstrained
111+
// usage is requested. If `delivery_mode` were a runtime value then performance would suffer.
112+
assert_constant(delivery_mode);
113+
114+
// The following maps out the 3 dimensions across which we configure message delivery.
115+
let constrained_encryption = delivery_mode == MessageDelivery.CONSTRAINED_ONCHAIN;
116+
let emit_as_offchain_message = delivery_mode == MessageDelivery.UNCONSTRAINED_OFFCHAIN;
117+
// TODO(#14565): Add constrained tagging
118+
let _constrained_tagging = delivery_mode == MessageDelivery.CONSTRAINED_ONCHAIN;
55119

56120
let (ciphertext, randomness) = remove_constraints_if(
57-
constraints == PrivateLogContent.NO_CONSTRAINTS,
121+
!constrained_encryption,
58122
|| to_encrypted_private_event_message(event, recipient),
59123
);
60-
let log_content = prefix_with_tag(ciphertext, recipient);
61124

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

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

77-
/// Emits an event as an offchain message. Similar to private log emission but uses offchain message mechanism instead.
78-
///
79-
/// Unlike private log emission, encryption here is always unconstrained. This design choice stems from the nature of
80-
/// offchain messages - they lack guaranteed delivery, unlike private logs. Without delivery guarantees, smart
81-
/// contracts cannot make assumptions about a message being delivered, making constrained encryption unnecessary.
82-
/// However, message integrity remains protected through a cryptographic commitment stored in the nullifier tree,
83-
/// preventing tampering even in the absence of guaranteed delivery. See the description of the
84-
/// `messages::offchain_message::emit_offchain_message` function for more details on when a guaranteed delivery is
85-
/// valuable. If guaranteed delivery is required, the `emit_event_in_private_log` function should be used instead.
86-
pub fn emit_event_as_offchain_message<Event>(
87-
event: Event,
88-
context: &mut PrivateContext,
89-
recipient: AztecAddress,
90-
)
91-
where
92-
Event: EventInterface + Serialize,
93-
{
94-
// Safety: as explained above, this function places no constraints on the content of the message.
95-
let (message_ciphertext, randomness) =
96-
unsafe { remove_constraints(|| to_encrypted_private_event_message(event, recipient)) };
97-
98-
// We generate a cryptographic commitment to the event to ensure its authenticity during out-of-band delivery. Note
99-
// that the commitment is made from the (constrained) event content, and not the (unconstrained) ciphertext.
100-
// The nullifier tree is chosen over the note hash tree for this purpose since it provides a simpler mechanism
101-
// - nullifiers require no nonce, and events, being non-spendable, don't need the guarantee that a "spending"
102-
// nullifier can be computed.
103-
// TODO(#11571): with decryption happening in Noir we can now use the Packable trait instead.
104-
let serialized_event_with_randomness = [randomness].concat(event.serialize());
105-
let event_commitment = poseidon2_hash_with_separator(
106-
serialized_event_with_randomness,
107-
GENERATOR_INDEX__EVENT_COMMITMENT,
108-
);
109-
context.push_nullifier(event_commitment);
110-
111-
// Safety: Offchain effects are by definition unconstrained. They are emitted via an oracle
112-
// which we don't use for anything besides its side effects, therefore this is safe to call.
113-
unsafe { emit_offchain_message(message_ciphertext, recipient) };
146+
context.emit_private_log(log_content, log_content.len());
147+
}
114148
}
115149

150+
// TODO(benesjan): rename to emit_event_in_public
116151
pub fn emit_event_in_public_log<Event>(event: Event, context: &mut PublicContext)
117152
where
118153
Event: EventInterface + Serialize,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ pub unconstrained fn process_private_event_msg(
4646
GENERATOR_INDEX__EVENT_COMMITMENT,
4747
);
4848

49-
// Randomness was injected into the event payload in `emit_event_in_private_log` but we have already used it
49+
// Randomness was injected into the event payload in `emit_event_in_private` but we have already used it
5050
// to compute the event commitment, so we can safely discard it now.
5151
let serialized_event = array::subbvec(
5252
serialized_event_with_randomness,

noir-projects/aztec-nr/aztec/src/messages/logs/utils.nr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::oracle::notes::{
33
};
44
use dep::protocol_types::address::AztecAddress;
55

6+
// TODO(#14565): Add constrained tagging
67
pub(crate) fn prefix_with_tag<let L: u32>(
78
log_without_tag: [Field; L],
89
recipient: AztecAddress,

noir-projects/aztec-nr/aztec/src/messages/offchain_messages.nr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ pub global OFFCHAIN_MESSAGE_IDENTIFIER: Field =
3636
///
3737
/// * `message` - The message to emit.
3838
/// * `recipient` - The address of the recipient.
39+
// TODO(benesjan): Make this function constrained.
3940
pub unconstrained fn emit_offchain_message<T>(message: T, recipient: AztecAddress)
4041
where
4142
T: Serialize,

noir-projects/noir-contracts/contracts/app/simple_token_contract/src/main.nr

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub contract SimpleToken {
1515
use dep::aztec::{
1616
authwit::auth::compute_authwit_nullifier,
1717
context::{PrivateCallInterface, PrivateContext, PublicContext},
18-
event::event_interface::{emit_event_in_private_log, PrivateLogContent},
18+
event::event_interface::{emit_event_in_private, MessageDelivery},
1919
macros::{
2020
events::event,
2121
functions::{authorize_once, initializer, internal, private, public, utility, view},
@@ -153,11 +153,11 @@ pub contract SimpleToken {
153153
to,
154154
));
155155

156-
emit_event_in_private_log(
156+
emit_event_in_private(
157157
Transfer { from, to, amount },
158158
&mut context,
159159
to,
160-
PrivateLogContent.NO_CONSTRAINTS,
160+
MessageDelivery.UNCONSTRAINED_ONCHAIN,
161161
);
162162
}
163163

noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub contract Token {
2020

2121
use dep::aztec::{
2222
context::{PrivateCallInterface, PrivateContext, PublicContext},
23-
event::event_interface::{emit_event_in_private_log, PrivateLogContent},
23+
event::event_interface::{emit_event_in_private, MessageDelivery},
2424
macros::{
2525
events::event,
2626
functions::{authorize_once, initializer, internal, private, public, utility, view},
@@ -301,11 +301,11 @@ pub contract Token {
301301
// another person where the payment is considered to be successful when the other party successfully decrypts a
302302
// note).
303303
// docs:start:encrypted_unconstrained
304-
emit_event_in_private_log(
304+
emit_event_in_private(
305305
Transfer { from, to, amount },
306306
&mut context,
307307
to,
308-
PrivateLogContent.NO_CONSTRAINTS,
308+
MessageDelivery.UNCONSTRAINED_ONCHAIN,
309309
);
310310
// docs:end:encrypted_unconstrained
311311
}

noir-projects/noir-contracts/contracts/test/event_only_contract/src/main.nr

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use aztec::macros::aztec;
55
#[aztec]
66
contract EventOnly {
77
use aztec::{
8-
event::event_interface::{emit_event_in_private_log, PrivateLogContent},
8+
event::event_interface::{emit_event_in_private, MessageDelivery},
99
macros::{events::event, functions::private},
1010
};
1111

@@ -17,11 +17,11 @@ contract EventOnly {
1717
#[private]
1818
fn emit_event_for_msg_sender(value: Field) {
1919
let sender = context.msg_sender();
20-
emit_event_in_private_log(
20+
emit_event_in_private(
2121
TestEvent { value },
2222
&mut context,
2323
sender,
24-
PrivateLogContent.NO_CONSTRAINTS,
24+
MessageDelivery.UNCONSTRAINED_ONCHAIN,
2525
);
2626
}
2727
}

noir-projects/noir-contracts/contracts/test/offchain_effect_contract/src/main.nr

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use aztec::macros::aztec;
44
#[aztec]
55
contract OffchainEffect {
66
use aztec::{
7-
event::event_interface::emit_event_as_offchain_message,
7+
event::event_interface::{emit_event_in_private, MessageDelivery},
88
macros::{events::event, functions::{private, utility}, storage::storage},
99
messages::logs::note::encode_and_encrypt_note_and_emit_as_offchain_message,
1010
note::note_viewer_options::NoteViewerOptions,
@@ -51,7 +51,12 @@ contract OffchainEffect {
5151

5252
#[private]
5353
fn emit_event_as_offchain_message_for_msg_sender(a: u64, b: u64, c: u64) {
54-
emit_event_as_offchain_message(TestEvent { a, b, c }, &mut context, context.msg_sender());
54+
emit_event_in_private(
55+
TestEvent { a, b, c },
56+
&mut context,
57+
context.msg_sender(),
58+
MessageDelivery.UNCONSTRAINED_OFFCHAIN,
59+
);
5560
}
5661

5762
#[private]

0 commit comments

Comments
 (0)