Skip to content

Commit 3785717

Browse files
authored
feat(aztec-nr)!: domain-separated tags on log emission (#21910) [v4-next backport] (#22031)
2 parents 5a5b915 + df9eda1 commit 3785717

28 files changed

Lines changed: 375 additions & 224 deletions

File tree

docs/docs-developers/docs/resources/migration_notes.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,42 @@ Aztec is in active development. Each version may introduce breaking changes that
99

1010
## TBD
1111

12+
### [Aztec.nr] Domain-separated tags on log emission
13+
14+
All logs emitted through the Aztec.nr framework now include a domain-separated tag at `fields[0]`. Each log category uses its own domain separator via `compute_log_tag(raw_tag, dom_sep)`:
15+
16+
- **Events** (`DOM_SEP__EVENT_LOG_TAG`): the event type ID is the raw tag.
17+
- **Message delivery** (`DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG`): the discovery tag is the raw tag.
18+
- **Partial note completion logs** (`DOM_SEP__NOTE_COMPLETION_LOG_TAG`): the partial note's `commitment` field is the raw tag.
19+
20+
The low-level emit methods now take `tag` as an explicit first parameter and have been renamed with an `_unsafe` suffix. Previously the tag was included as `log[0]` — it has now been extracted into its own parameter, and `log` no longer contains it:
21+
22+
```diff
23+
- context.emit_private_log(log, length);
24+
+ context.emit_private_log_unsafe(tag, log, length);
25+
- context.emit_raw_note_log(log, length, note_hash_counter);
26+
+ context.emit_raw_note_log_unsafe(tag, log, length, note_hash_counter);
27+
- context.emit_public_log(log);
28+
+ context.emit_public_log_unsafe(tag, log);
29+
```
30+
31+
Prefer the higher-level APIs (`emit` for events, `MessageDelivery` for messages) which handle tagging automatically.
32+
33+
### [Aztec.nr] Public events no longer include the event type selector at the end of the payload
34+
35+
`emit_event_in_public` previously appended the event type selector as the last field. It now prepends a domain-separated tag at `fields[0]` instead. The payload after the tag contains only the serialized event fields.
36+
37+
If you were reading public event directly from node logs (i.e. via `node.getPublicLogs` and not via `wallet.getPublicEvents`), update your parsing:
38+
39+
```diff
40+
- // Old: fields = [serialized_event..., event_type_selector]
41+
- const selector = EventSelector.fromField(fields[fields.length - 1]);
42+
- const event = decodeFromAbi([abiType], fields);
43+
+ // New: fields = [domain_separated_tag, serialized_event...]
44+
+ const eventFields = log.getEmittedFieldsWithoutTag();
45+
+ const event = decodeFromAbi([abiType], eventFields);
46+
```
47+
1248
### [Aztec.nr] Capsule operations are now addressed by scope
1349

1450
All capsule operations (`store`, `load`, `delete`, `copy`) and `CapsuleArray` now require a `scope: AztecAddress` parameter. This scopes capsule storage by address, providing isolation between different accounts within the same PXE.
@@ -62,7 +98,6 @@ The `CustomMessageHandler` function type now receives an additional `scope: Azte
6298
```
6399

64100
**Impact**: Contracts that implement a custom message handler must update the function signature.
65-
66101
### [aztec.js] `DeployMethod.send()` always returns `{ contract, receipt, instance }`
67102

68103
The `returnReceipt` option in deploy wait options has been removed. `DeployMethod.send()` now always returns an object with `contract`, `receipt`, and `instance` at the top level, provided the user waits for the transaction to be included.

noir-projects/aztec-nr/aztec/src/context/private_context.nr

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ use crate::protocol::{
3737
MAX_KEY_VALIDATION_REQUESTS_PER_CALL, MAX_L2_TO_L1_MSGS_PER_CALL, MAX_NOTE_HASH_READ_REQUESTS_PER_CALL,
3838
MAX_NOTE_HASHES_PER_CALL, MAX_NULLIFIER_READ_REQUESTS_PER_CALL, MAX_NULLIFIERS_PER_CALL,
3939
MAX_PRIVATE_CALL_STACK_LENGTH_PER_CALL, MAX_PRIVATE_LOGS_PER_CALL, MAX_TX_LIFETIME,
40-
NULL_MSG_SENDER_CONTRACT_ADDRESS, PRIVATE_LOG_SIZE_IN_FIELDS,
40+
NULL_MSG_SENDER_CONTRACT_ADDRESS, PRIVATE_LOG_CIPHERTEXT_LEN,
4141
},
4242
hash::poseidon2_hash,
4343
messaging::l2_to_l1_message::L2ToL1Message,
@@ -878,22 +878,33 @@ impl PrivateContext {
878878
/// about _which_ function has been executed. A tx which leaks such information does not contribute to the privacy
879879
/// set of the network.
880880
///
881-
/// * Unlike `emit_raw_note_log`, this log is not tied to any specific note
881+
/// * Unlike `emit_raw_note_log_unsafe`, this log is not tied to any specific note
882882
///
883883
/// # Arguments
884+
/// * `tag` - A tag placed at `fields[0]` of the emitted log. Used by recipients and nodes to identify and
885+
/// filter for relevant logs without scanning all of them.
884886
/// * `log` - The log data that will be publicly broadcast (so make sure it's already been encrypted before you
885-
/// call this function). Private logs are bounded in size (PRIVATE_LOG_SIZE_IN_FIELDS), to encourage all logs from
886-
/// all smart contracts look identical.
887-
/// * `length` - The actual length of the `log` (measured in number of Fields). Although the input log has a max
888-
/// size of PRIVATE_LOG_SIZE_IN_FIELDS, the latter values of the array might all be 0's for small logs. This
889-
/// `length` should reflect the trimmed length of the array. The protocol's kernel circuits can then append random
890-
/// fields as "padding" after the `length`, so that the logs of this smart contract look indistinguishable from
891-
/// (the same length as) the logs of all other applications. It's up to wallets how much padding to apply, so
892-
/// ideally all wallets should agree on standards for this.
893-
pub fn emit_private_log(&mut self, log: [Field; PRIVATE_LOG_SIZE_IN_FIELDS], length: u32) {
887+
/// call this function). Private logs are bounded in size (`PRIVATE_LOG_CIPHERTEXT_LEN`), to encourage all logs
888+
/// from all smart contracts look identical.
889+
/// * `length` - The actual length of `log` (measured in number of Fields). Although the input log has a max
890+
/// size of `PRIVATE_LOG_CIPHERTEXT_LEN`, the latter values of the array might all be 0's for small logs. This
891+
/// `length` should reflect the trimmed length of the array. The protocol's kernel circuits can then append
892+
/// random fields as "padding" after the `length`, so that the logs of this smart contract look
893+
/// indistinguishable from (the same length as) the logs of all other applications. It's up to wallets how much
894+
/// padding to apply, so ideally all wallets should agree on standards for this.
895+
///
896+
/// ## Safety
897+
///
898+
/// The `tag` should be domain-separated (e.g. via [`crate::protocol::hash::compute_log_tag`]) to prevent
899+
/// collisions between logs from different sources. Without domain separation, two unrelated log types that
900+
/// happen to share a raw tag value become indistinguishable. Prefer the higher-level APIs
901+
/// ([`crate::messages::message_delivery::MessageDelivery`] for messages, `self.emit(event)` for events) which
902+
/// handle tagging automatically.
903+
pub fn emit_private_log_unsafe(&mut self, tag: Field, log: [Field; PRIVATE_LOG_CIPHERTEXT_LEN], length: u32) {
894904
let counter = self.next_counter();
895-
let private_log = PrivateLogData { log: PrivateLog::new(log, length), note_hash_counter: 0 }.count(counter);
896-
self.private_logs.push(private_log);
905+
let full_log = [tag].concat(log);
906+
self.private_logs.push(PrivateLogData { log: PrivateLog::new(full_log, length + 1), note_hash_counter: 0 }
907+
.count(counter));
897908
}
898909

899910
// TODO: rename.
@@ -903,20 +914,31 @@ impl PrivateContext {
903914
/// This linkage is important in case the note gets squashed (due to being read later in this same tx), since we
904915
/// can then squash the log as well.
905916
///
906-
/// See `emit_private_log` for more info about private log emission.
917+
/// See `emit_private_log_unsafe` for more info about private log emission.
907918
///
908919
/// # Arguments
920+
/// * `tag` - A tag placed at `fields[0]`. See `emit_private_log_unsafe`.
909921
/// * `log` - The log data as an array of Field elements
910922
/// * `length` - The actual length of the `log` (measured in number of Fields).
911923
/// * `note_hash_counter` - The side-effect counter that was assigned to the new note_hash when it was pushed to
912924
/// this `PrivateContext`.
913925
///
914926
/// Important: If your application logic requires the log to always be emitted regardless of note squashing,
915-
/// consider using `emit_private_log` instead, or emitting additional events.
927+
/// consider using `emit_private_log_unsafe` instead, or emitting additional events.
916928
///
917-
pub fn emit_raw_note_log(&mut self, log: [Field; PRIVATE_LOG_SIZE_IN_FIELDS], length: u32, note_hash_counter: u32) {
929+
/// ## Safety
930+
///
931+
/// Same as [`PrivateContext::emit_private_log_unsafe`]: the `tag` should be domain-separated.
932+
pub fn emit_raw_note_log_unsafe(
933+
&mut self,
934+
tag: Field,
935+
log: [Field; PRIVATE_LOG_CIPHERTEXT_LEN],
936+
length: u32,
937+
note_hash_counter: u32,
938+
) {
918939
let counter = self.next_counter();
919-
let private_log = PrivateLogData { log: PrivateLog::new(log, length), note_hash_counter };
940+
let full_log = [tag].concat(log);
941+
let private_log = PrivateLogData { log: PrivateLog::new(full_log, length + 1), note_hash_counter };
920942
self.private_logs.push(private_log.count(counter));
921943
}
922944

noir-projects/aztec-nr/aztec/src/context/public_context.nr

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::protocol::{
1111
address::{AztecAddress, EthAddress},
1212
constants::{MAX_U32_VALUE, NULL_MSG_SENDER_CONTRACT_ADDRESS},
1313
traits::{Empty, FromField, Packable, Serialize, ToField},
14+
utils::writer::Writer,
1415
};
1516

1617
/// # PublicContext
@@ -96,14 +97,27 @@ impl PublicContext {
9697
/// Emits a _public_ log that will be visible onchain to everyone.
9798
///
9899
/// # Arguments
99-
/// * `log` - The data to log, must implement Serialize trait
100+
/// * `tag` - A tag placed at `fields[0]` of the emitted log. Nodes index logs by this value, allowing
101+
/// clients to efficiently query for matching logs without scanning all of them.
102+
/// * `log` - The data to log, must implement Serialize trait.
100103
///
101-
pub fn emit_public_log<T>(_self: Self, log: T)
104+
/// ## Safety
105+
///
106+
/// The `tag` should be domain-separated (e.g. via [`crate::protocol::hash::compute_log_tag`]) to prevent
107+
/// collisions between logs from different sources. Without domain separation, two unrelated log types that
108+
/// happen to share a raw tag value become indistinguishable. Prefer `self.emit(event)` for events, which
109+
/// handles tagging automatically.
110+
pub fn emit_public_log_unsafe<T>(_self: Self, tag: Field, log: T)
102111
where
103112
T: Serialize,
104113
{
114+
// We use a Writer to serialize the log directly after the tag, avoiding an extra O(n) copy that would
115+
// result from serializing first and then prepending the tag.
116+
let mut writer: Writer<1 + <T as Serialize>::N> = Writer::new();
117+
writer.write(tag);
118+
Serialize::stream_serialize(log, &mut writer);
105119
// Safety: AVM opcodes are constrained by the AVM itself
106-
unsafe { avm::emit_public_log(Serialize::serialize(log).as_vector()) };
120+
unsafe { avm::emit_public_log(writer.finish().as_vector()) };
107121
}
108122

109123
/// Checks if a given note hash exists in the note hash tree at a particular leaf_index.

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

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::{
33
event::{event_interface::{compute_private_event_commitment, EventInterface}, EventMessage},
44
oracle::random::random,
55
};
6-
use crate::protocol::traits::{Serialize, ToField};
6+
use crate::protocol::{constants::DOM_SEP__EVENT_LOG_TAG, hash::compute_log_tag, traits::{Serialize, ToField}};
77

88
/// An event that was emitted in the current contract call.
99
pub struct NewEvent<Event> {
@@ -39,17 +39,11 @@ pub fn emit_event_in_public<Event>(context: PublicContext, event: Event)
3939
where
4040
Event: EventInterface + Serialize,
4141
{
42-
let mut log_content = [0; <Event as Serialize>::N + 1];
43-
44-
let serialized_event = event.serialize();
45-
for i in 0..serialized_event.len() {
46-
log_content[i] = serialized_event[i];
47-
}
48-
49-
// We put the selector in the "last" place, to avoid reading or assigning to an expression in an index
50-
//
51-
// TODO(F-224): change this order.
52-
log_content[serialized_event.len()] = Event::get_event_type_id().to_field();
53-
54-
context.emit_public_log(log_content);
42+
// We prepend a domain-separated tag derived from the event type ID so that clients can filter for specific
43+
// events without scanning all public logs.
44+
let log_tag = compute_log_tag(
45+
Event::get_event_type_id().to_field(),
46+
DOM_SEP__EVENT_LOG_TAG,
47+
);
48+
context.emit_public_log_unsafe(log_tag, event);
5549
}

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

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
use crate::{
22
messages::{
33
encoding::{encode_message, MAX_MESSAGE_CONTENT_LEN, MESSAGE_EXPANDED_METADATA_LEN},
4-
encryption::{aes128::AES128, message_encryption::MessageEncryption},
5-
logs::utils::prefix_with_tag,
64
msg_type::PARTIAL_NOTE_PRIVATE_MSG_TYPE_ID,
75
},
86
note::note_interface::NoteType,
97
utils::array,
108
};
11-
use crate::protocol::{
12-
address::AztecAddress,
13-
constants::PRIVATE_LOG_SIZE_IN_FIELDS,
14-
traits::{FromField, Packable, ToField},
15-
};
9+
use crate::protocol::{address::AztecAddress, traits::{FromField, Packable, ToField}};
1610

1711
/// The number of fields in a private note message content that are not the note's packed representation.
1812
pub(crate) global PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_RESERVED_FIELDS_LEN: u32 = 3;
@@ -25,29 +19,6 @@ pub(crate) global PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_NOTE_COMPLETION_LOG_TAG_IND
2519
pub global MAX_PARTIAL_NOTE_PRIVATE_PACKED_LEN: u32 =
2620
MAX_MESSAGE_CONTENT_LEN - PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_RESERVED_FIELDS_LEN;
2721

28-
// TODO(#16881): once partial notes support delivery via an offchain message we will most likely want to remove this.
29-
pub fn compute_partial_note_private_content_log<PartialNotePrivateContent>(
30-
partial_note_private_content: PartialNotePrivateContent,
31-
owner: AztecAddress,
32-
randomness: Field,
33-
recipient: AztecAddress,
34-
note_completion_log_tag: Field,
35-
contract_address: AztecAddress,
36-
) -> [Field; PRIVATE_LOG_SIZE_IN_FIELDS]
37-
where
38-
PartialNotePrivateContent: NoteType + Packable,
39-
{
40-
let message_plaintext = encode_partial_note_private_message(
41-
partial_note_private_content,
42-
owner,
43-
randomness,
44-
note_completion_log_tag,
45-
);
46-
let message_ciphertext = AES128::encrypt(message_plaintext, recipient, contract_address);
47-
48-
prefix_with_tag(message_ciphertext, recipient)
49-
}
50-
5122
/// Creates the plaintext for a partial note private message (i.e. one of type [`PARTIAL_NOTE_PRIVATE_MSG_TYPE_ID`]).
5223
///
5324
/// This plaintext is meant to be decoded via [`decode_partial_note_private_message`].

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

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,63 +2,44 @@ use crate::oracle::notes::{get_next_app_tag_as_sender, get_sender_for_tags};
22
use crate::protocol::address::AztecAddress;
33

44
// TODO(#14565): Add constrained tagging
5-
pub(crate) fn prefix_with_tag<let L: u32>(log_without_tag: [Field; L], recipient: AztecAddress) -> [Field; L + 1] {
5+
/// Returns the next discovery tag for a private log sent to `recipient`.
6+
///
7+
/// Private logs are encrypted, so the recipient cannot tell which logs are meant for it just by looking at them.
8+
/// To solve this, sender and recipient derive a shared secret from their keys, and from that secret they produce a
9+
/// sequence of one-time tags (tag_0, tag_1, ...). The recipient scans for these tags because it can compute the same
10+
/// sequence. This function returns the next raw (not domain-separated) tag in the sequence.
11+
pub(crate) fn compute_discovery_tag(recipient: AztecAddress) -> Field {
612
// Safety: we assume that the sender wants for the recipient to find the tagged note, and therefore that they will
713
// cooperate and use the correct tag. Usage of a bad tag will result in the recipient not being able to find the
814
// note automatically.
9-
let tag = unsafe {
15+
unsafe {
1016
let sender = get_sender_for_tags().expect(
1117
f"Sender for tags is not set when emitting a private log. Set it by calling `set_sender_for_tags(...)`.",
1218
);
1319
get_next_app_tag_as_sender(sender, recipient)
14-
};
15-
16-
let mut log_with_tag = [0; L + 1];
17-
18-
log_with_tag[0] = tag;
19-
for i in 0..log_without_tag.len() {
20-
log_with_tag[i + 1] = log_without_tag[i];
2120
}
22-
23-
log_with_tag
2421
}
2522

2623
mod test {
2724
use crate::protocol::{address::AztecAddress, traits::FromField};
28-
use super::prefix_with_tag;
25+
use super::compute_discovery_tag;
2926
use std::test::OracleMock;
3027

31-
#[test(should_fail)]
28+
#[test(should_fail_with = "Sender for tags is not set")]
3229
unconstrained fn no_tag_sender() {
3330
let recipient = AztecAddress::from_field(2);
34-
35-
let expected_tag = 42;
36-
37-
// Mock the tagging oracles - note aztec_prv_getSenderForTags returns none
3831
let _ = OracleMock::mock("aztec_prv_getSenderForTags").returns(Option::<AztecAddress>::none());
39-
let _ = OracleMock::mock("aztec_prv_getNextAppTagAsSender").returns(expected_tag);
40-
41-
let log_without_tag = [1, 2, 3];
42-
let _ = prefix_with_tag(log_without_tag, recipient);
32+
let _ = OracleMock::mock("aztec_prv_getNextAppTagAsSender").returns(42);
33+
let _ = compute_discovery_tag(recipient);
4334
}
4435

4536
#[test]
46-
unconstrained fn prefixing_with_tag() {
37+
unconstrained fn returns_oracle_tag() {
4738
let sender = AztecAddress::from_field(1);
4839
let recipient = AztecAddress::from_field(2);
49-
5040
let expected_tag = 42;
51-
52-
// Mock the tagging oracles
5341
let _ = OracleMock::mock("aztec_prv_getSenderForTags").returns(Option::some(sender));
5442
let _ = OracleMock::mock("aztec_prv_getNextAppTagAsSender").returns(expected_tag);
55-
56-
let log_without_tag = [1, 2, 3];
57-
let log_with_tag = prefix_with_tag(log_without_tag, recipient);
58-
59-
let expected_result = [expected_tag, 1, 2, 3];
60-
61-
// Check tag was prefixed correctly
62-
assert_eq(log_with_tag, expected_result, "Tag was not prefixed correctly");
43+
assert_eq(compute_discovery_tag(recipient), expected_tag);
6344
}
6445
}

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ use crate::{
22
context::PrivateContext,
33
messages::{
44
encryption::{aes128::AES128, message_encryption::MessageEncryption},
5-
logs::utils::prefix_with_tag,
5+
logs::utils::compute_discovery_tag,
66
offchain_messages::deliver_offchain_message,
77
},
88
utils::remove_constraints::remove_constraints_if,
99
};
10-
use crate::protocol::address::AztecAddress;
10+
use crate::protocol::{address::AztecAddress, constants::DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG, hash::compute_log_tag};
1111

1212
/// Placeholder struct until Noir adds `enum` support.
1313
///
@@ -219,9 +219,11 @@ pub fn do_private_message_delivery<Env, let MESSAGE_PLAINTEXT_LEN: u32>(
219219
if deliver_as_offchain_message {
220220
deliver_offchain_message(ciphertext, recipient);
221221
} else {
222-
// Safety: Currently unsafe. See description of ONCHAIN_CONSTRAINED in MessageDeliveryEnum. TODO(#14565):
223-
// Implement proper constrained tag prefixing to make this truly ONCHAIN_CONSTRAINED
224-
let log_content = prefix_with_tag(ciphertext, recipient);
222+
// TODO(#14565): constrained tagging is not yet implemented. Both modes currently use the unconstrained
223+
// domain separator because the discovery tag always comes from an oracle. Once constrained tagging lands,
224+
// this should branch on `constrained_tagging` to select the appropriate separator.
225+
let discovery_tag = compute_discovery_tag(recipient);
226+
let log_tag = compute_log_tag(discovery_tag, DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG);
225227

226228
// We forbid this value not being constant to avoid predicating the context calls below, which might result in
227229
// the context's arrays having unknown compile time write indices and hence dramatically increasing constraints
@@ -235,9 +237,9 @@ pub fn do_private_message_delivery<Env, let MESSAGE_PLAINTEXT_LEN: u32>(
235237
//
236238
// Note that the log always has the same length regardless of `MESSAGE_PLAINTEXT_LEN`, because all message
237239
// ciphertexts also have the same length. This prevents accidental privacy leakage via the log length.
238-
context.emit_raw_note_log(log_content, log_content.len(), maybe_note_hash_counter.unwrap());
240+
context.emit_raw_note_log_unsafe(log_tag, ciphertext, ciphertext.len(), maybe_note_hash_counter.unwrap());
239241
} else {
240-
context.emit_private_log(log_content, log_content.len());
242+
context.emit_private_log_unsafe(log_tag, ciphertext, ciphertext.len());
241243
}
242244
}
243245
}

0 commit comments

Comments
 (0)