Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5fef66a
feat(aztec-nr): wire handshake secret discovery into contract sync
nchamo Jun 8, 2026
bcca6af
chore(aztec-nr): remove dead_code allow on ProvidedSecret
nchamo Jun 8, 2026
1bab207
fix(aztec-nr): resolve broken doc links in handshake.nr
nchamo Jun 8, 2026
d972444
chore(txe): bump split chunk size limit for HandshakeRegistry
nchamo Jun 8, 2026
26a34f2
refactor(aztec-nr): split do_sync_state to break address cycle
nchamo Jun 8, 2026
5e302ba
fix(end-to-end): publish HandshakeRegistry in common e2e setup
nchamo Jun 8, 2026
4326d74
fix(aztec-nr): use struct notation for AztecAddress in selector
nchamo Jun 8, 2026
9346f38
Merge branch 'merge-train/fairies-v5' into nchamo/f-588-constrained-d…
nchamo Jun 8, 2026
d60f406
fix(docs): remove broken standard-contracts link in debugging.md
nchamo Jun 8, 2026
dad3a20
fix(noir-projects): update contract snapshot for sync_state_with_secr…
nchamo Jun 8, 2026
4582042
fix(pxe): register HandshakeRegistry in private execution tests
nchamo Jun 8, 2026
c57a73e
chore(pxe): format private execution test
nchamo Jun 8, 2026
679f46a
fix(pxe): skip instance lookup for HandshakeRegistry utility calls
nchamo Jun 8, 2026
6418f4f
fix(txe): preload HandshakeRegistry in shared contract store
nchamo Jun 8, 2026
efafea2
Merge branch 'merge-train/fairies-v5' into nchamo/f-588-constrained-d…
nchamo Jun 9, 2026
c1c5040
refactor(pxe): register handshake registry in PXE.create
nchamo Jun 10, 2026
120b230
fix(pxe): use lazy import for handshake registry to avoid bundle bloat
nchamo Jun 10, 2026
b602d4d
refactor(txe): lazy-load handshake registry to reduce chunk size
nchamo Jun 10, 2026
e078993
refactor(pxe): set preloaded contract defaults at each layer
nchamo Jun 10, 2026
8eca57f
fix(standard-contracts): add import attribute to lazy JSON imports fo…
nchamo Jun 10, 2026
6099d59
Merge branch 'merge-train/fairies-v5' into nchamo/f-588-constrained-d…
nchamo Jun 10, 2026
1a0e300
refactor(aztec-nr): use OnchainDeliveryMode throughout handshake types
nchamo Jun 10, 2026
d6858d6
chore(aztec-nr): regenerate standard contract addresses
nchamo Jun 10, 2026
0bfce77
chore(aztec-nr): revert standard contract addresses to base branch va…
nchamo Jun 10, 2026
78068e0
chore: retrigger CI
nchamo 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
5 changes: 4 additions & 1 deletion docs/docs-developers/docs/aztec-nr/debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ LOG_LEVEL=verbose aztec start --local-network

#### Cross-contract utility call denied

Utility functions execute on the user's device and have access to private state. A cross-contract utility call made by a malicious or compromised contract could leak private information to an untrusted contract. PXE therefore denies all cross-contract utility calls by default and requires explicit authorization via an execution hook.
Utility functions execute on the user's device and have access to private state. A cross-contract utility call made by
a malicious or compromised contract could leak private information to an untrusted contract. PXE therefore denies cross-
contract utility calls by default and requires explicit authorization via an execution hook. Calls to standard contracts
(such as the HandshakeRegistry, which is queried during every contract's sync) are always automatically authorized.

When a contract executes a utility function that calls into a different contract, PXE asks an **execution hook** whether the call should be allowed. If no hook is configured, or the hook denies the request, you will see:

Expand Down
20 changes: 5 additions & 15 deletions noir-projects/aztec-nr/aztec/src/messages/delivery/builder.nr
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use super::tag_secret_derivation::TagSecretDerivation;
/// ## Construction
///
/// The fields are private and there is no public constructor: a `MessageDelivery` can only be produced by a
/// [`MessageDeliveryBuilder`] that enforces valid configurations, so invalid field combinations cannot be
/// [`MessageDeliveryBuilder`] that enforces valid configurations, so invalid field combinations cannot be
/// represented to the consumer.
pub struct MessageDelivery {
mode: DeliveryMode,
Expand Down Expand Up @@ -223,10 +223,7 @@ pub struct OnchainUnconstrainedDelivery {

impl OnchainUnconstrainedDelivery {
fn new() -> Self {
Self {
tag_secret_derivation: TagSecretDerivation::wallet_default(),
sender_override: Option::none(),
}
Self { tag_secret_derivation: TagSecretDerivation::wallet_default(), sender_override: Option::none() }
}

/// Overrides the sender address used for discovery tag derivation.
Expand Down Expand Up @@ -285,10 +282,7 @@ pub struct OnchainConstrainedDelivery {

impl OnchainConstrainedDelivery {
fn new() -> Self {
Self {
tag_secret_derivation: TagSecretDerivation::wallet_default(),
sender_override: Option::none(),
}
Self { tag_secret_derivation: TagSecretDerivation::wallet_default(), sender_override: Option::none() }
}

/// Overrides the sender address used for discovery tag derivation.
Expand Down Expand Up @@ -370,12 +364,8 @@ mod test {
let constrained_delivery =
MessageDelivery::onchain_constrained().via_non_interactive_handshake().build_message_delivery();

assert(
unconstrained_delivery.tag_secret_derivation() == TagSecretDerivation::non_interactive_handshake(),
);
assert(
constrained_delivery.tag_secret_derivation() == TagSecretDerivation::non_interactive_handshake(),
);
assert(unconstrained_delivery.tag_secret_derivation() == TagSecretDerivation::non_interactive_handshake());
Comment thread
vezenovm marked this conversation as resolved.
assert(constrained_delivery.tag_secret_derivation() == TagSecretDerivation::non_interactive_handshake());
}

#[test]
Expand Down
172 changes: 172 additions & 0 deletions noir-projects/aztec-nr/aztec/src/messages/delivery/handshake.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use crate::messages::delivery::OnchainDeliveryMode;
use crate::protocol::point::EmbeddedCurvePoint;
use crate::protocol::traits::{Deserialize, Serialize};

use crate::{
ephemeral::EphemeralArray,
messages::processing::provided_secret::ProvidedSecret,
oracle::{call_utility_function::call_utility_function, shared_secret::get_shared_secrets},
protocol::{
abis::function_selector::FunctionSelector, address::AztecAddress, hash::sha256_to_field, traits::ToField,
},
standard_addresses::STANDARD_HANDSHAKE_REGISTRY_ADDRESS,
};

/// Page size for handshake discovery pagination.
pub global MAX_HANDSHAKES_PER_PAGE: u32 = 32;

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

/// A paginated response of discovered handshakes.
#[derive(Deserialize, Serialize)]
pub struct HandshakePage {
pub items: BoundedVec<DiscoveredHandshake, MAX_HANDSHAKES_PER_PAGE>,
pub total_count: u32,
}

pub(crate) global PROVIDED_SECRETS_ARRAY_BASE_SLOT: Field =
sha256_to_field("AZTEC_NR::PROVIDED_SECRETS_ARRAY_BASE_SLOT".as_bytes());

global HANDSHAKE_EPH_PKS_SLOT: Field = sha256_to_field("AZTEC_NR::HANDSHAKE_EPH_PKS_SLOT".as_bytes());

global HANDSHAKE_MODES_SLOT: Field = sha256_to_field("AZTEC_NR::HANDSHAKE_MODES_SLOT".as_bytes());

/// Fetches discovered handshakes from the HandshakeRegistry and derives app-siloed tagging secrets for each,
/// returning them so that [`get_pending_tagged_logs`](crate::oracle::message_processing::get_pending_tagged_logs)
/// searches for logs tagged with these secrets.
pub(crate) unconstrained fn get_handshake_secrets(
contract_address: AztecAddress,
scope: AztecAddress,
) -> EphemeralArray<ProvidedSecret> {
let provided_secrets = EphemeralArray::<ProvidedSecret>::empty_at(PROVIDED_SECRETS_ARRAY_BASE_SLOT);

let eph_pks: EphemeralArray<EmbeddedCurvePoint> = EphemeralArray::empty_at(HANDSHAKE_EPH_PKS_SLOT);
let modes: EphemeralArray<OnchainDeliveryMode> = EphemeralArray::empty_at(HANDSHAKE_MODES_SLOT);

let mut page_offset: u32 = 0;
let mut has_more = true;
while has_more {
let page = fetch_handshake_page(scope, page_offset);

for j in 0..page.items.len() {
let handshake = page.items.get(j);
eph_pks.push(handshake.eph_pk);
modes.push(handshake.mode);
}

page_offset += page.items.len();
has_more = page_offset < page.total_count;
}

if eph_pks.len() > 0 {
let secrets = get_shared_secrets(scope, eph_pks, contract_address);
for j in 0..secrets.len() {
provided_secrets.push(ProvidedSecret { secret: secrets.get(j), mode: modes.get(j) });
}
}

provided_secrets
}

/// Calls the HandshakeRegistry's `get_handshakes` utility function and deserializes the response.
unconstrained fn fetch_handshake_page(recipient: AztecAddress, page_offset: u32) -> HandshakePage {
let selector = comptime { FunctionSelector::from_signature("get_handshakes((Field),u32)") };
let args: [Field; 2] = [recipient.to_field(), page_offset as Field];
let response = call_utility_function(STANDARD_HANDSHAKE_REGISTRY_ADDRESS, selector, args);
HandshakePage::deserialize(response)
}

mod test {
use crate::ephemeral::EphemeralArray;
use crate::messages::delivery::handshake::{
DiscoveredHandshake, get_handshake_secrets, HandshakePage, MAX_HANDSHAKES_PER_PAGE,
};
use crate::messages::delivery::OnchainDeliveryMode;
use crate::protocol::{address::AztecAddress, traits::Serialize};
use crate::test::helpers::test_environment::TestEnvironment;
use crate::utils::point::point_from_x_coord;
use std::test::OracleMock;

global UNCONSTRAINED: OnchainDeliveryMode = OnchainDeliveryMode::onchain_unconstrained();
global CONSTRAINED: OnchainDeliveryMode = OnchainDeliveryMode::onchain_constrained();

#[test]
unconstrained fn get_handshake_secrets_returns_secrets_from_single_page() {
let mut env = TestEnvironment::new();
let scope = env.create_light_account();
let contract_address = AztecAddress { inner: 0xdeadbeef };

env.utility_context_at(contract_address, |_| {
let pk_a = point_from_x_coord(1).unwrap();
let pk_b = point_from_x_coord(2).unwrap();

let mut items: BoundedVec<DiscoveredHandshake, MAX_HANDSHAKES_PER_PAGE> = BoundedVec::new();
items.push(DiscoveredHandshake { eph_pk: pk_a, mode: UNCONSTRAINED });
items.push(DiscoveredHandshake { eph_pk: pk_b, mode: CONSTRAINED });
let page = HandshakePage { items, total_count: 2 };
let _ = OracleMock::mock("aztec_utl_callUtilityFunction").returns(page.serialize());

let secret_a: Field = 111;
let secret_b: Field = 222;
mock_get_shared_secrets([secret_a, secret_b]);

let secrets = get_handshake_secrets(contract_address, scope);

assert_eq(secrets.len(), 2);
assert_eq(secrets.get(0).secret, secret_a);
assert_eq(secrets.get(0).mode, UNCONSTRAINED);
assert_eq(secrets.get(1).secret, secret_b);
assert_eq(secrets.get(1).mode, CONSTRAINED);
});
}

#[test]
unconstrained fn get_handshake_secrets_fetches_multiple_pages() {
let mut env = TestEnvironment::new();
let scope = env.create_light_account();
let contract_address = AztecAddress { inner: 0xdeadbeef };

env.utility_context_at(contract_address, |_| {
let pk_a = point_from_x_coord(1).unwrap();
let pk_b = point_from_x_coord(2).unwrap();
let pk_c = point_from_x_coord(8).unwrap();

let mut page_1_items: BoundedVec<DiscoveredHandshake, MAX_HANDSHAKES_PER_PAGE> = BoundedVec::new();
page_1_items.push(DiscoveredHandshake { eph_pk: pk_a, mode: UNCONSTRAINED });
page_1_items.push(DiscoveredHandshake { eph_pk: pk_b, mode: CONSTRAINED });
let page_1 = HandshakePage { items: page_1_items, total_count: 3 };

let mut page_2_items: BoundedVec<DiscoveredHandshake, MAX_HANDSHAKES_PER_PAGE> = BoundedVec::new();
page_2_items.push(DiscoveredHandshake { eph_pk: pk_c, mode: UNCONSTRAINED });
let page_2 = HandshakePage { items: page_2_items, total_count: 3 };

let _ = OracleMock::mock("aztec_utl_callUtilityFunction").returns(page_1.serialize()).times(1);
let _ = OracleMock::mock("aztec_utl_callUtilityFunction").returns(page_2.serialize()).times(1);

mock_get_shared_secrets([111, 222, 333]);

let secrets = get_handshake_secrets(contract_address, scope);
assert_eq(secrets.len(), 3);
assert_eq(secrets.get(0).secret, 111);
assert_eq(secrets.get(0).mode, UNCONSTRAINED);
assert_eq(secrets.get(1).secret, 222);
assert_eq(secrets.get(1).mode, CONSTRAINED);
assert_eq(secrets.get(2).secret, 333);
assert_eq(secrets.get(2).mode, UNCONSTRAINED);
});
}

unconstrained fn mock_get_shared_secrets<let N: u32>(response_values: [Field; N]) {
let response_slot: Field = 99;
let response_array: EphemeralArray<Field> = EphemeralArray::at(response_slot);
for value in response_values {
response_array.push(value);
}
let _ = OracleMock::mock("aztec_utl_getSharedSecrets").returns(response_slot);
}
}
30 changes: 21 additions & 9 deletions noir-projects/aztec-nr/aztec/src/messages/delivery/mod.nr
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
mod builder;
mod mode;
mod tag_secret_derivation;

pub mod handshake;

use crate::{
context::PrivateContext,
messages::{
Expand All @@ -11,10 +17,6 @@ use crate::protocol::{address::AztecAddress, constants::DOM_SEP__UNCONSTRAINED_M
use mode::DeliveryMode;
use tag_secret_derivation::TagSecretDerivation;

mod builder;
mod mode;
mod tag_secret_derivation;

pub use builder::{
MessageDelivery, MessageDeliveryBuilder, OffchainDelivery, OnchainConstrainedDelivery, OnchainUnconstrainedDelivery,
};
Expand Down Expand Up @@ -133,31 +135,41 @@ fn resolve_tag_secret_derivation(
}

mod test {
use super::{resolve_tag_secret_derivation, DeliveryMode, TagSecretDerivation};
use super::{DeliveryMode, resolve_tag_secret_derivation, TagSecretDerivation};

#[test]
fn wallet_default_resolves_for_delivery_mode() {
assert(
resolve_tag_secret_derivation(DeliveryMode::onchain_unconstrained(), TagSecretDerivation::wallet_default())
resolve_tag_secret_derivation(
DeliveryMode::onchain_unconstrained(),
TagSecretDerivation::wallet_default(),
)
== TagSecretDerivation::address_secret(),
);
assert(
resolve_tag_secret_derivation(DeliveryMode::onchain_constrained(), TagSecretDerivation::wallet_default())
resolve_tag_secret_derivation(
DeliveryMode::onchain_constrained(),
TagSecretDerivation::wallet_default(),
)
== TagSecretDerivation::non_interactive_handshake(),
);
}

#[test]
fn explicit_tag_secret_derivation_is_preserved() {
assert(
resolve_tag_secret_derivation(DeliveryMode::onchain_unconstrained(), TagSecretDerivation::address_secret())
resolve_tag_secret_derivation(
DeliveryMode::onchain_unconstrained(),
TagSecretDerivation::address_secret(),
)
== TagSecretDerivation::address_secret(),
);
assert(
resolve_tag_secret_derivation(
DeliveryMode::onchain_constrained(),
TagSecretDerivation::non_interactive_handshake(),
) == TagSecretDerivation::non_interactive_handshake(),
)
== TagSecretDerivation::non_interactive_handshake(),
);
}
}
Loading
Loading