Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 5 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,11 @@ 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](../standard-contracts/index.md) (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
187 changes: 187 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,187 @@
use crate::protocol::point::EmbeddedCurvePoint;
use crate::protocol::traits::{Deserialize, Packable, 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, Packable, Serialize)]
pub struct DiscoveredHandshake {
pub eph_pk: EmbeddedCurvePoint,
pub mode: u8,
Comment thread
nchamo marked this conversation as resolved.
Outdated
}
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,
}

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 [`crate::oracle::message_processing::get_pending_tagged_logs`] searches for logs tagged
/// with these secrets.
///
/// Returns an empty array when `contract_address` is the registry itself, to prevent infinite recursion during the
/// registry's own sync.
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);

if contract_address != STANDARD_HANDSHAKE_REGISTRY_ADDRESS {
let eph_pks: EphemeralArray<EmbeddedCurvePoint> = EphemeralArray::empty_at(HANDSHAKE_EPH_PKS_SLOT);
let modes: EphemeralArray<Field> = 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 as Field);
}

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::protocol::{address::AztecAddress, traits::Serialize};
use crate::standard_addresses::STANDARD_HANDSHAKE_REGISTRY_ADDRESS;
use crate::test::helpers::test_environment::TestEnvironment;
use crate::utils::point::point_from_x_coord;
use std::test::OracleMock;

#[test]
unconstrained fn get_handshake_secrets_skips_registry_itself() {
let mut env = TestEnvironment::new();
let scope = env.create_light_account();

env.utility_context_at(STANDARD_HANDSHAKE_REGISTRY_ADDRESS, |_| {
let call_mock = OracleMock::mock("aztec_utl_callUtilityFunction");

let _ = get_handshake_secrets(STANDARD_HANDSHAKE_REGISTRY_ADDRESS, scope);

assert_eq(call_mock.times_called(), 0);
});
}

#[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: 2 });
items.push(DiscoveredHandshake { eph_pk: pk_b, mode: 3 });
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, 2);
assert_eq(secrets.get(1).secret, secret_b);
assert_eq(secrets.get(1).mode, 3);
});
}

#[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: 2 });
page_1_items.push(DiscoveredHandshake { eph_pk: pk_b, mode: 3 });
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: 2 });
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, 2);
assert_eq(secrets.get(1).secret, 222);
assert_eq(secrets.get(1).mode, 3);
assert_eq(secrets.get(2).secret, 333);
assert_eq(secrets.get(2).mode, 2);
});
}

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);
}
}
2 changes: 2 additions & 0 deletions noir-projects/aztec-nr/aztec/src/messages/delivery/mod.nr
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod handshake;

use crate::{
context::PrivateContext,
messages::{
Expand Down
21 changes: 11 additions & 10 deletions noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::logging::{aztecnr_debug_log, aztecnr_debug_log_format, aztecnr_warn_log_format};
use crate::messages::delivery::handshake::get_handshake_secrets;
use crate::protocol::address::AztecAddress;
use crate::protocol::hash::sha256_to_field;

pub(crate) mod nonce_discovery;
pub(crate) mod partial_notes;
Expand All @@ -9,24 +9,19 @@ pub mod private_notes;
pub mod process_message;

use crate::{
ephemeral::EphemeralArray,
messages::{
discovery::process_message::process_message_ciphertext,
encoding::MAX_MESSAGE_CONTENT_LEN,
logs::note::MAX_NOTE_PACKED_LEN,
processing::{
MessageContext, offchain::OffchainInboxSync, OffchainMessageWithContext,
pending_tagged_log::PendingTaggedLog, provided_secret::ProvidedSecret,
validate_and_store_enqueued_notes_and_events,
pending_tagged_log::PendingTaggedLog, validate_and_store_enqueued_notes_and_events,
},
},
oracle::message_processing,
utils::array,
};

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

pub struct NoteHashAndNullifier {
/// The result of [`crate::note::note_interface::NoteHash::compute_note_hash`].
pub note_hash: Field,
Expand Down Expand Up @@ -145,8 +140,7 @@ pub unconstrained fn do_sync_state(

// First we process all private logs, which can contain different kinds of messages e.g. private notes, partial
// notes, private events, etc.
// TODO(F-588): populate with tagging secrets for constrained delivery
let provided_secrets = EphemeralArray::<ProvidedSecret>::empty_at(PROVIDED_SECRETS_ARRAY_BASE_SLOT);
let provided_secrets = get_handshake_secrets(contract_address, scope);
let logs = message_processing::get_pending_tagged_logs(scope, provided_secrets);
logs.for_each(|_i, pending_tagged_log: PendingTaggedLog| {
if pending_tagged_log.log.len() == 0 {
Expand Down Expand Up @@ -201,12 +195,14 @@ pub unconstrained fn do_sync_state(
mod test {
use crate::ephemeral::EphemeralArray;
use crate::messages::{
delivery::handshake::HandshakePage,
discovery::{CustomMessageHandler, do_sync_state},
logs::note::MAX_NOTE_PACKED_LEN,
processing::{offchain::OffchainInboxSync, pending_tagged_log::PendingTaggedLog},
};
use crate::protocol::address::AztecAddress;
use crate::protocol::{address::AztecAddress, traits::Serialize};
use crate::test::helpers::test_environment::TestEnvironment;
use std::test::OracleMock;

#[test]
unconstrained fn do_sync_state_does_not_panic_on_empty_logs() {
Expand All @@ -216,6 +212,11 @@ mod test {
let contract_address = AztecAddress { inner: 0xdeadbeef };

env.utility_context_at(contract_address, |_| {
// get_handshake_secrets calls the HandshakeRegistry during sync; return an empty page so
// no tagging secrets are produced.
let empty_page = HandshakePage { items: BoundedVec::new(), total_count: 0 };
let _ = OracleMock::mock("aztec_utl_callUtilityFunction").returns(empty_page.serialize());

// Mock the oracle call to return a known base slot, then populate an ephemeral
// array at that slot so do_sync_state processes a non-empty log list.
let base_slot = 42;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use crate::protocol::traits::Serialize;
use crate::protocol::traits::{Deserialize, 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).
#[derive(Serialize)]
#[derive(Deserialize, Serialize)]
#[allow(dead_code)]
pub(crate) struct ProvidedSecret {
pub secret: Field,
Expand Down
Loading
Loading