Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
5a3d077
refactor(aztec-nr)!: move messages::message_delivery to messages::del…
vezenovm Jun 4, 2026
29664d5
fix: regenerate yarn-project/yarn.lock for noir 1.0.0-beta.22 (v5 base)
AztecBot Jun 4, 2026
0a7dc40
Merge branch 'merge-train/fairies-v5' into mv/move-message-delivery-m…
vezenovm Jun 4, 2026
956993c
feat(aztec-nr): extend OnchainDelivery builder for secret origin
vezenovm Jun 3, 2026
c9aff0b
refactor(aztec-nr): hide OnchainDeliveryMode constructors behind the …
vezenovm Jun 4, 2026
989948d
docs'
vezenovm Jun 4, 2026
95e5137
typo
vezenovm Jun 4, 2026
f3e91b3
try static_assert
vezenovm Jun 4, 2026
befb4ea
chore: re-pin HandshakeRegistry standard contract on the fairies-v5 base
vezenovm Jun 5, 2026
51cd497
default to handshake registry standard contract and do not panic on m…
vezenovm Jun 5, 2026
1636dc6
test: cover constrained note-log survival in pending_note_hashes squa…
vezenovm Jun 5, 2026
5269722
comment
vezenovm Jun 5, 2026
33c69d4
Merge branch 'merge-train/fairies-v5' into mv/move-message-delivery-m…
vezenovm Jun 5, 2026
248696b
fix(aztec-nr): repair doc link to renamed delivery module
vezenovm Jun 5, 2026
d2c9a4e
Merge branch 'merge-train/fairies-v5' into mv/move-message-delivery-m…
vezenovm Jun 5, 2026
46aa057
Merge remote-tracking branch 'origin/mv/move-message-delivery-module'…
vezenovm Jun 5, 2026
e110055
merge
vezenovm Jun 5, 2026
224871d
option OnchainDeliveryMode
vezenovm Jun 5, 2026
2687058
docs: restore constrained delivery warning
vezenovm Jun 5, 2026
87a6034
docs: use exact constrained delivery warning
vezenovm Jun 5, 2026
78db3d3
restore exact warning
vezenovm Jun 5, 2026
13808b4
rename
vezenovm Jun 5, 2026
7c2f8f0
rename
vezenovm Jun 5, 2026
c8edad1
comment
vezenovm Jun 5, 2026
4a19b4c
Apply suggestion from @vezenovm
vezenovm Jun 5, 2026
422ec8e
Apply suggestion from @nchamo
vezenovm Jun 5, 2026
71e305a
Apply suggestion from @nchamo
vezenovm Jun 5, 2026
54e0907
Apply suggestion from @nchamo
vezenovm Jun 5, 2026
e6f1b12
refactor(aztec-nr): replace secret origin delivery config
vezenovm Jun 5, 2026
a889141
refactor(aztec-nr): simplify onchain delivery builder
vezenovm Jun 5, 2026
fd212c7
conflict
vezenovm Jun 5, 2026
70e6e64
validate in deserialize
vezenovm Jun 8, 2026
be865bc
split OnchainDelivery type
vezenovm Jun 8, 2026
4d2a9dc
small comment
vezenovm Jun 8, 2026
658142b
simplfiy comments
vezenovm Jun 8, 2026
a744124
comments
vezenovm Jun 8, 2026
33acb61
.
vezenovm Jun 8, 2026
d2ccc25
pr reviews, cleanup of API, tightened comments
vezenovm Jun 9, 2026
876ef91
consolidate tests
vezenovm Jun 9, 2026
c95ebcb
comment
vezenovm Jun 9, 2026
fc38308
DeliveryMode
vezenovm Jun 9, 2026
cf38f47
consolidate constructors
vezenovm Jun 9, 2026
5a91cfb
be more explicit about mocking enums, use equality checks rather than…
vezenovm Jun 9, 2026
a2b7989
cleanup
vezenovm Jun 9, 2026
b562b4d
split up delivery module root file into submodules
vezenovm Jun 9, 2026
c54bda8
improve vis
vezenovm Jun 9, 2026
a2afd82
Merge branch 'merge-train/fairies-v5' into mv/f-697-general-delivery-…
vezenovm Jun 9, 2026
e46cd06
rename module to mode
vezenovm Jun 9, 2026
ba2c977
pr cleanup, resolve inside delivery root module, and typos
vezenovm Jun 10, 2026
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
414 changes: 414 additions & 0 deletions noir-projects/aztec-nr/aztec/src/messages/delivery/builder.nr

Large diffs are not rendered by default.

343 changes: 86 additions & 257 deletions noir-projects/aztec-nr/aztec/src/messages/delivery/mod.nr

Large diffs are not rendered by default.

131 changes: 131 additions & 0 deletions noir-projects/aztec-nr/aztec/src/messages/delivery/mode.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
use crate::protocol::{traits::{Deserialize, Serialize, ToField}, utils::reader::Reader};

/// Noir does not support enums, so this wrapper models delivery variants while keeping raw discriminants private.
pub(crate) struct DeliveryMode {
inner: u8,
}

impl DeliveryMode {
pub(crate) fn offchain() -> Self {
Self { inner: 1 }
}

pub(crate) fn onchain_unconstrained() -> Self {
OnchainDeliveryMode::onchain_unconstrained().into()
}

pub(crate) fn onchain_constrained() -> Self {
OnchainDeliveryMode::onchain_constrained().into()
}

pub(crate) fn assert_is_constant(self) {
assert_constant(self.inner);
}
}

impl Eq for DeliveryMode {
fn eq(self, other: Self) -> bool {
self.inner == other.inner
}
}

/// The mode of an on-chain message delivery: unconstrained or constrained tagging.
///
/// This is kept separate from the private [`DeliveryMode`] wrapper because it is used in external ABIs and therefore
/// owns the serialization and validation for the on-chain-only subset.
///
/// Consumers derive modes through the delivery API, e.g.
/// `let mode: OnchainDeliveryMode = MessageDelivery::onchain_unconstrained().into()`.
#[derive(Serialize)]
pub struct OnchainDeliveryMode {
inner: u8,
}

impl OnchainDeliveryMode {
/// On-chain delivery without constrained encryption/tagging.
///
/// `unconstrained` alone is a reserved Noir keyword.
pub(crate) fn onchain_unconstrained() -> Self {
Self { inner: 2 }
}

/// On-chain delivery with constrained encryption/tagging.
pub(crate) fn onchain_constrained() -> Self {
Self { inner: 3 }
}

fn from_u8(inner: u8) -> Self {
let mode = Self { inner };
assert(mode.is_valid(), "unrecognized delivery mode");
mode
}

/// Whether `self` is one of the valid on-chain delivery modes.
fn is_valid(self) -> bool {
(self == Self::onchain_unconstrained()) | (self == Self::onchain_constrained())
}
}

impl Deserialize for OnchainDeliveryMode {
let N: u32 = <u8 as Deserialize>::N;

fn deserialize(fields: [Field; Self::N]) -> Self {
Self::from_u8(<u8 as Deserialize>::deserialize(fields))
}

fn stream_deserialize<let K: u32>(reader: &mut Reader<K>) -> Self {
Self::from_u8(reader.read() as u8)
}
}

impl Eq for OnchainDeliveryMode {
fn eq(self, other: Self) -> bool {
self.inner == other.inner
}
}

impl ToField for OnchainDeliveryMode {
fn to_field(self) -> Field {
self.inner as Field
}
}

impl From<OnchainDeliveryMode> for DeliveryMode {
fn from(mode: OnchainDeliveryMode) -> DeliveryMode {
DeliveryMode { inner: mode.inner }
}
}

mod test {
use crate::protocol::traits::{Deserialize, Serialize};
use super::{DeliveryMode, OnchainDeliveryMode};

#[test]
fn onchain_delivery_modes_are_valid() {
assert(OnchainDeliveryMode::onchain_unconstrained().is_valid());
assert(OnchainDeliveryMode::onchain_constrained().is_valid());
}

#[test]
fn mode_roundtrips_through_serialization() {
let unconstrained_mode = OnchainDeliveryMode::onchain_unconstrained();
let constrained_mode = OnchainDeliveryMode::onchain_constrained();

assert(OnchainDeliveryMode::deserialize(unconstrained_mode.serialize()) == unconstrained_mode);
assert(OnchainDeliveryMode::deserialize(constrained_mode.serialize()) == constrained_mode);
}

#[test(should_fail_with = "unrecognized delivery mode")]
fn deserializing_invalid_mode_fails() {
let _ = OnchainDeliveryMode::deserialize([99]);
}

#[test]
fn delivery_mode_onchain_constructors_match_onchain_delivery_modes() {
let unconstrained_delivery_mode: DeliveryMode = OnchainDeliveryMode::onchain_unconstrained().into();
let constrained_delivery_mode: DeliveryMode = OnchainDeliveryMode::onchain_constrained().into();

assert(DeliveryMode::onchain_unconstrained() == unconstrained_delivery_mode);
assert(DeliveryMode::onchain_constrained() == constrained_delivery_mode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/// Noir does not support enums, so this wrapper models tag-secret derivation variants.
pub(crate) struct TagSecretDerivation {
inner: u8,
}

impl TagSecretDerivation {
pub(crate) fn wallet_default() -> Self {
Self { inner: 0 }
}

pub(crate) fn address_secret() -> Self {
Self { inner: 1 }
}

pub(crate) fn non_interactive_handshake() -> Self {
Self { inner: 2 }
}

pub(crate) fn assert_is_constant(self) {
assert_constant(self.inner);
}
}

impl Eq for TagSecretDerivation {
fn eq(self, other: Self) -> bool {
self.inner == other.inner
}
}
4 changes: 2 additions & 2 deletions noir-projects/aztec-nr/aztec/src/messages/logs/utils.nr
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::messages::delivery::ONCHAIN_UNCONSTRAINED;
use crate::messages::delivery::MessageDelivery;
use crate::oracle::{notes::{get_app_tagging_secret, get_next_tagging_index, get_sender_for_tags}, random::random};
use crate::protocol::{address::AztecAddress, hash::poseidon2_hash};

Expand Down Expand Up @@ -26,7 +26,7 @@ pub(crate) fn compute_discovery_tag(recipient: AztecAddress, sender_override: Op
random()
},
|secret| {
let index = get_next_tagging_index(secret, ONCHAIN_UNCONSTRAINED);
let index = get_next_tagging_index(secret, MessageDelivery::onchain_unconstrained());
Comment thread
vezenovm marked this conversation as resolved.
poseidon2_hash([secret, index as Field])
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ use crate::protocol::traits::Serialize;
/// A tagging secret the app supplies explicitly to `get_pending_tagged_logs`.
///
/// These are searched alongside the secrets PXE derives or stores internally, and exist for secrets PXE cannot
/// enumerate on its own (e.g. handshake-derived ones). `mode` is a delivery mode constant:
/// [`ONCHAIN_UNCONSTRAINED`](crate::messages::delivery::ONCHAIN_UNCONSTRAINED) or
/// [`ONCHAIN_CONSTRAINED`](crate::messages::delivery::ONCHAIN_CONSTRAINED).
/// enumerate on its own (e.g. handshake-derived ones). `mode` is an on-chain delivery mode serialized as a field.
#[derive(Serialize)]
#[allow(dead_code)]
pub(crate) struct ProvidedSecret {
Expand Down
16 changes: 10 additions & 6 deletions noir-projects/aztec-nr/aztec/src/oracle/notes.nr
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::messages::delivery::OnchainDeliveryMode;
use crate::note::{HintedNote, note_interface::NoteType};

use crate::protocol::{address::AztecAddress, traits::Packable};
Expand Down Expand Up @@ -192,19 +193,22 @@ unconstrained fn get_app_tagging_secret_oracle(_sender: AztecAddress, _recipient
///
/// The caller already holds the tagging `secret`, whether obtained from [`get_app_tagging_secret`] or computed by the
/// contract, and only needs the simulator to hand out a fresh per-secret, per-mode index; the caller computes the tag
/// itself. `mode` is one of [`ONCHAIN_UNCONSTRAINED`](crate::messages::delivery::ONCHAIN_UNCONSTRAINED) /
/// [`ONCHAIN_CONSTRAINED`](crate::messages::delivery::ONCHAIN_CONSTRAINED) and determines the per-mode index
/// counter. Offchain messages are not tagged, so only the onchain delivery variants should reach this oracle.
/// itself. `mode` is converted into the [`OnchainDeliveryMode`] selecting the per-mode index counter, so callers can
/// pass either a concrete on-chain delivery builder or an explicit mode. Offchain messages are not tagged, so only the
/// onchain delivery variants can be used by this oracle.
///
/// The simulator persists the index increment only if the tagged log is found in an actual block; a reverting
/// transaction can otherwise cause the sender to skip indices and later produce notes that are not found by the
/// recipient.
pub unconstrained fn get_next_tagging_index(secret: Field, mode: u8) -> u32 {
get_next_tagging_index_oracle(secret, mode)
pub unconstrained fn get_next_tagging_index<M>(secret: Field, mode: M) -> u32
where
M: Into<OnchainDeliveryMode>,
{
get_next_tagging_index_oracle(secret, mode.into())
}

#[oracle(aztec_prv_getNextTaggingIndex)]
unconstrained fn get_next_tagging_index_oracle(_secret: Field, _mode: u8) -> u32 {}
unconstrained fn get_next_tagging_index_oracle(_secret: Field, _mode: OnchainDeliveryMode) -> u32 {}

/// Gets the sender for tags.
///
Expand Down
2 changes: 1 addition & 1 deletion noir-projects/aztec-nr/aztec/src/standard_addresses.nr
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ pub global STANDARD_PUBLIC_CHECKS_ADDRESS: AztecAddress = AztecAddress::from_fie
);

pub global STANDARD_HANDSHAKE_REGISTRY_ADDRESS: AztecAddress = AztecAddress::from_field(
0x30280ec85136f131f9c34a9ac5c1390363c9207930c47eb93ddfd83f3601b5a1,
0x2491f3a3cdfb66c89b1ea0d1d96fde4ea3e64cd48f122b81efeebf7ec24ce24d,
);
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ pub global STANDARD_PUBLIC_CHECKS_ADDRESS: AztecAddress = AztecAddress::from_fie
);

pub global STANDARD_HANDSHAKE_REGISTRY_ADDRESS: AztecAddress = AztecAddress::from_field(
0x30280ec85136f131f9c34a9ac5c1390363c9207930c47eb93ddfd83f3601b5a1,
0x2491f3a3cdfb66c89b1ea0d1d96fde4ea3e64cd48f122b81efeebf7ec24ce24d,
);
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,24 @@ pub contract HandshakeRegistry {
keys::{ecdh_shared_secret::derive_ecdh_shared_secret, ephemeral::generate_positive_ephemeral_key_pair},
macros::{functions::external, storage::storage},
messages::{
delivery::{MessageDelivery, ONCHAIN_CONSTRAINED, ONCHAIN_UNCONSTRAINED},
delivery::{MessageDelivery, OnchainDeliveryMode},
encryption::{aes128::AES128, message_encryption::MessageEncryption},
},
protocol::{
address::AztecAddress,
constants::DOM_SEP__NON_INTERACTIVE_HANDSHAKE_LOG_TAG,
hash::compute_log_tag,
point::EmbeddedCurvePoint,
traits::{Deserialize, Packable, Serialize, ToField},
traits::{Deserialize, Serialize, ToField},
},
state_vars::{Map, Owned, PrivateMutable},
};

/// A handshake discovered during sync: the sender's ephemeral public key and the delivery mode.
#[derive(Deserialize, Eq, Packable, Serialize)]
#[derive(Deserialize, Eq, Serialize)]
pub struct DiscoveredHandshake {
pub eph_pk: EmbeddedCurvePoint,
pub mode: u8,
pub mode: OnchainDeliveryMode,
Comment thread
vezenovm marked this conversation as resolved.
}

#[derive(Deserialize, Serialize)]
Expand All @@ -60,14 +60,15 @@ pub contract HandshakeRegistry {
struct Storage<Context> {
/// One current [`HandshakeNote`] per `(recipient, sender, mode)` tuple. Re-handshaking for the same tuple
/// replaces the prior sender-owned note, so only the latest handshake remains valid for that mode.
handshakes: Map<AztecAddress, Map<u8, Owned<PrivateMutable<HandshakeNote, Context>, Context>, Context>, Context>,
handshakes: Map<AztecAddress, Map<OnchainDeliveryMode, Owned<PrivateMutable<HandshakeNote, Context>, Context>, Context>, Context>,
Comment thread
vezenovm marked this conversation as resolved.
}

/// Performs a non-interactive handshake from `sender` to `recipient` and returns the app-siloed shared secret
/// for the calling contract.
///
/// `mode` sets the delivery mode for messages tagged with this handshake ([`ONCHAIN_UNCONSTRAINED`] or
/// [`ONCHAIN_CONSTRAINED`]); the handshake note itself is always stored onchain.
/// `mode` sets the delivery mode for messages tagged with this handshake
/// ([`MessageDelivery::onchain_unconstrained`] or [`MessageDelivery::onchain_constrained`]); the handshake note
/// itself is always stored onchain.
///
/// Generates a fresh ephemeral key pair `(eph_sk, eph_pk)`, computes the raw ECDH shared secret point
/// `S = eph_sk * recipient_address_point`, and produces three effects:
Expand All @@ -88,20 +89,17 @@ pub contract HandshakeRegistry {
/// protect, and a fallback would insert a permanent note recording a handshake with an invalid recipient,
/// polluting registry state.
#[external("private")]
fn non_interactive_handshake(sender: AztecAddress, recipient: AztecAddress, mode: u8) -> Field {
assert((mode == ONCHAIN_UNCONSTRAINED) | (mode == ONCHAIN_CONSTRAINED), "unrecognized delivery mode");

fn non_interactive_handshake(sender: AztecAddress, recipient: AztecAddress, mode: OnchainDeliveryMode) -> Field {
let recipient_point = recipient.to_address_point().expect(f"recipient address is not on the curve");

let (eph_sk, eph_pk) = generate_positive_ephemeral_key_pair();
let s_raw = derive_ecdh_shared_secret(eph_sk, recipient_point.inner);

let note = HandshakeNote::new(s_raw, NON_INTERACTIVE_HANDSHAKE, recipient);

// Delivery is `ONCHAIN_UNCONSTRAINED`. The recipient is not involved in this note's delivery. They
// discover the handshake via the encrypted log emitted below, not via the sender's note.
// We use onchain unconstrained delivery rather than `OFFCHAIN` so the note is
// discoverable via normal PXE sync.
// The recipient is not involved in this note's delivery: they discover the handshake via the encrypted log
// emitted below, not via the sender's note. We deliver onchain unconstrained rather than offchain so the
// note is discoverable via normal PXE sync.
self.storage.handshakes.at(recipient).at(mode).at(sender).initialize_or_replace(|_| note).deliver(
MessageDelivery::onchain_unconstrained().with_sender(sender),
);
Expand All @@ -112,7 +110,7 @@ pub contract HandshakeRegistry {
);

let ciphertext = AES128::encrypt(
[eph_pk.x, mode as Field],
[eph_pk.x, mode.to_field()],
recipient,
self.context.this_address(),
);
Expand All @@ -133,9 +131,12 @@ pub contract HandshakeRegistry {
/// If no stored handshake for `(sender, recipient, mode)` silos to `app_siloed_secret` under the calling
/// contract's address.
#[external("private")]
fn validate_handshake(sender: AztecAddress, recipient: AztecAddress, mode: u8, app_siloed_secret: Field) {
assert((mode == ONCHAIN_UNCONSTRAINED) | (mode == ONCHAIN_CONSTRAINED), "unrecognized delivery mode");

fn validate_handshake(
sender: AztecAddress,
recipient: AztecAddress,
mode: OnchainDeliveryMode,
app_siloed_secret: Field,
) {
let caller = self.msg_sender();
let replacement_note_message = self.storage.handshakes.at(recipient).at(mode).at(sender).get_note();
let note = replacement_note_message.get_note();
Expand All @@ -160,14 +161,12 @@ pub contract HandshakeRegistry {
unconstrained fn get_app_siloed_secret(
sender: AztecAddress,
recipient: AztecAddress,
mode: u8,
mode: OnchainDeliveryMode,
// TODO(F-671): replace with `self.msg_sender()` once utility context exposes it. The
// explicit param forces hooks to also gate on the caller; otherwise any contract that
// can authorize (target, selector) can read another caller's siloed secret.
caller: AztecAddress,
) -> Option<Field> {
assert((mode == ONCHAIN_UNCONSTRAINED) | (mode == ONCHAIN_CONSTRAINED), "unrecognized delivery mode");

let handshake = self.storage.handshakes.at(recipient).at(mode).at(sender);

if handshake.is_initialized() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use aztec::{
context::UtilityContext,
ephemeral::EphemeralArray,
messages::{
delivery::OnchainDeliveryMode,
discovery::{ComputeNoteHash, ComputeNoteNullifier, CustomMessageHandler, do_sync_state},
encryption::{aes128::AES128, message_encryption::MessageEncryption},
processing::{
Expand All @@ -18,7 +19,7 @@ use aztec::{
address::AztecAddress,
constants::DOM_SEP__NON_INTERACTIVE_HANDSHAKE_LOG_TAG,
hash::{compute_log_tag, sha256_to_field},
traits::ToField,
traits::{Deserialize, ToField},
},
utils::point::point_from_x_coord_and_sign,
};
Expand Down Expand Up @@ -71,7 +72,10 @@ pub(crate) unconstrained fn handshake_registry_sync(
let _ = AES128::decrypt(ciphertext, scope, contract_address)
.and_then(|pt| {
point_from_x_coord_and_sign(pt.get(0), true).map(|pk| {
DiscoveredHandshake { eph_pk: pk, mode: pt.get(1) as u8 }
DiscoveredHandshake {
eph_pk: pk,
mode: OnchainDeliveryMode::deserialize([pt.get(1)]),
}
})
})
.map(|h| handshakes.push(h));
Expand Down
Loading
Loading