Skip to content

feat(aztec-nr): Helper to validate the app-siloed secret and index for constrained tagging#23569

Closed
vezenovm wants to merge 28 commits into
merge-train/fairiesfrom
maxim/f-669-aztec-nr-calculate_secret_and_index-helper-for-constrained
Closed

feat(aztec-nr): Helper to validate the app-siloed secret and index for constrained tagging#23569
vezenovm wants to merge 28 commits into
merge-train/fairiesfrom
maxim/f-669-aztec-nr-calculate_secret_and_index-helper-for-constrained

Conversation

@vezenovm

@vezenovm vezenovm commented May 26, 2026

Copy link
Copy Markdown
Contributor

Fixes F-699

Stacks on #23359

  • New noir-projects/aztec-nr/aztec/src/messages/delivery/ (to match messages/discovery/)
  • Moved MessageDelivery into noir-projects/aztec-nr/aztec/src/messages/delivery/mod.nr
  • New calculate_secret_and_index helper inside noir-projects/aztec-nr/aztec/src/messages/delivery/constrained_delivery.nr
  • New noir-projects/noir-contracts/contracts/test/constrained_delivery_test_contract with test module. Some tests have been left for the F-670 follow-up.

vezenovm and others added 17 commits May 14, 2026 10:52
…eNote, Context>, Context>, Context> and additional test
…hake_registry_contract/src/main.nr

Co-authored-by: Nicolas Chamo <nicolas@chamo.com.ar>
… dom sep

The new DOM_SEP__CONSTRAINED_MSG_LOG_TAG domain separator added in this PR
adds one assert_dom_sep_matches_derived call, which pushes to both seen_u32
and seen_values. constants_tests::hashed_values_match_derived sized the
tester at <71, 64>, so the extra push overflowed the BoundedVec
("push out of bounds" at bounded_vec.nr:186). Bump to <72, 65> to match the
actual 72 value / 65 u32 hashed entries.
…next_app_tag_as_sender-for-constrained' into maxim/f-668-aztec-nr-extend-get_next_app_tag_as_sender-for-constrained
@vezenovm vezenovm added the ci-draft Run CI on draft PRs. label May 26, 2026
…for-constrained' into maxim/f-669-aztec-nr-calculate_secret_and_index-helper-for-constrained
Comment thread barretenberg/cpp/src/barretenberg/aztec/aztec_constants.hpp Outdated
vezenovm added 3 commits May 26, 2026 16:50
…ecret_and_index-helper-for-constrained' into maxim/f-669-aztec-nr-calculate_secret_and_index-helper-for-constrained
Base automatically changed from maxim/f-668-aztec-nr-extend-get_next_app_tag_as_sender-for-constrained to merge-train/fairies May 28, 2026 16:09
// the `Some` branch. Without this, a later constrained message under the same secret restarts at index 0
// and collides on `(secret, 0)`.
// Safety: this only advances a PXE-side counter; the returned index is constrained to 0 below.
let index = unsafe { get_next_constrained_tagging_index(secret) };

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this call the new bootstrap_seeds_index_counter_for_same_tx_reuse test will fail. We could do this inside of the handshake function as well.

@vezenovm vezenovm marked this pull request as ready for review May 28, 2026 20:19
@vezenovm vezenovm requested a review from nchamo May 28, 2026 20:19
@vezenovm vezenovm removed the ci-draft Run CI on draft PRs. label May 28, 2026

@nchamo nchamo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are so close!

@@ -0,0 +1,115 @@
pub mod constrained_delivery;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite follow why you moved the file here in this PR and also dropped all the docs. Did you change something else? I don't quite see it

I'm ok with moving the file, but maybe we could have a follow up PR where we move the file and directly avoid the compatibility wrapper

/// Resolves the app-siloed shared secret and next per-secret index for a constrained send.
///
/// Wraps the registry calls needed by every constrained-delivery app: query the handshake registry for an
/// existing app-siloed secret, bootstrap a fresh handshake if there isn't one, and constrain the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// existing app-siloed secret, bootstrap a fresh handshake if there isn't one, and constrain the
/// existing app-siloed secret, bootstrap a fresh handshake if there isn't one, and constrain the

// Safety: the returned `Option<Field>` is untrusted and is constrained below before being returned to the
// caller, either by performing a new handshake or by constraining the prior handshake's existence.
let maybe_secret: Option<Field> = unsafe {
let selector = comptime {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we somehow avoid the selector and to something like Registry::at(address).... Like we do for tokens. I'm concerned about a possible change in the function signature that would drift from this

Comment on lines +75 to +78
// Reserve index 0 for the freshly bootstrapped secret so the PXE seeds its per-secret counter, mirroring
// the `Some` branch. Without this, a later constrained message under the same secret restarts at index 0
// and collides on `(secret, 0)`.
// Safety: this only advances a PXE-side counter; the returned index is constrained to 0 below.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels off. Calling the oracle so that the PXE counter can be started? Shouldn't the oracle fetch for logs when starting and start at the correct index? What if the messages started in another PXE, and the index should be 2 when get_next_constrained_tagging_index is first called, would be handle that correctly?

Comment on lines +99 to +102
let prev_nullifier = poseidon2_hash_with_separator(
[sender.to_field(), recipient.to_field(), secret, (index - 1) as Field],
DOM_SEP__CONSTRAINED_MSG_NULLIFIER,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably have a helper function in this file for this, since we'll need to re-use it when we actually send a message

///
/// Wraps the registry calls needed by every constrained-delivery app: query the handshake registry for an
/// existing app-siloed secret, bootstrap a fresh handshake if there isn't one, and constrain the
/// oracle-supplied secret. The returned `(secret, index)` pair is the input for the caller's tag derivation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be an index?

Suggested change
/// oracle-supplied secret. The returned `(secret, index)` pair is the input for the caller's tag derivation
/// oracle-supplied tagging index. The returned `(secret, index)` pair is the input for the caller's tag derivation

Comment on lines +21 to +38
///
/// ## Implementation Details
/// 1. Calls `HandshakeRegistry::get_app_siloed_secret` offchain (untrusted hint).
/// 2. On `None`, calls `HandshakeRegistry::non_interactive_handshake(sender, recipient)` to bootstrap and
/// returns `(secret, 0)`. The constrained return value of the private call is the source of truth for the
/// secret.
/// 3. On `Some(secret)`, reads the per-secret index hint via [`get_next_constrained_tagging_index`] (also untrusted),
/// then constrains:
/// - `index == 0`: calls `HandshakeRegistry::validate_handshake` to bind the secret to the registry's
/// current note. `PrivateMutable::get_note` inside `validate_handshake` nullifies the current handshake
/// note and emits a fresh one, so this branch already produces a per-call nullifier on the registry
/// note.
/// - `index > 0`: asserts the prior nullifier
/// `poseidon2_hash_with_separator([sender, recipient, secret, index - 1], DOM_SEP__CONSTRAINED_MSG_NULLIFIER)`
/// exists via [`compute_nullifier_existence_request`](crate::nullifier::utils::compute_nullifier_existence_request).
/// The chain transitively constrains back to the index-0 `validate_handshake`. The caller is expected
/// to emit the matching nullifier alongside each constrained send so subsequent calls can prove the
/// chain.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would skip this. I think that either the code is clear enough or, if not, we should make it clearer.

Suggested change
///
/// ## Implementation Details
/// 1. Calls `HandshakeRegistry::get_app_siloed_secret` offchain (untrusted hint).
/// 2. On `None`, calls `HandshakeRegistry::non_interactive_handshake(sender, recipient)` to bootstrap and
/// returns `(secret, 0)`. The constrained return value of the private call is the source of truth for the
/// secret.
/// 3. On `Some(secret)`, reads the per-secret index hint via [`get_next_constrained_tagging_index`] (also untrusted),
/// then constrains:
/// - `index == 0`: calls `HandshakeRegistry::validate_handshake` to bind the secret to the registry's
/// current note. `PrivateMutable::get_note` inside `validate_handshake` nullifies the current handshake
/// note and emits a fresh one, so this branch already produces a per-call nullifier on the registry
/// note.
/// - `index > 0`: asserts the prior nullifier
/// `poseidon2_hash_with_separator([sender, recipient, secret, index - 1], DOM_SEP__CONSTRAINED_MSG_NULLIFIER)`
/// exists via [`compute_nullifier_existence_request`](crate::nullifier::utils::compute_nullifier_existence_request).
/// The chain transitively constrains back to the index-0 `validate_handshake`. The caller is expected
/// to emit the matching nullifier alongside each constrained send so subsequent calls can prove the
/// chain.

Comment on lines +47 to +48
// Safety: the returned `Option<Field>` is untrusted and is constrained below before being returned to the
// caller, either by performing a new handshake or by constraining the prior handshake's existence.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about something like this?

// Safety: when the utility call returns no secret, we attempt to create a fresh handshake. Creating
  // a duplicate fails, so a forged empty response cannot hide an existing handshake. When the utility
  // call returns a secret, it is validated against the registry's stored handshake.

traits::{Deserialize, ToField},
};

/// Resolves the app-siloed shared secret and next per-secret index for a constrained send.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning, boss-man likes the first line to be 10 words max

Comment on lines +1 to +10
//! Tests for `calculate_secret_and_index`.
//!
//! TODO(F-670): Coverage here is limited to the bootstrap branch and the `Some(secret)` branch with `index == 0` (which
//! constrains via `HandshakeRegistry::validate_handshake`). The `index > 0` branch of the helper is reachable
//! only after the per-secret index has been advanced in the PXE, which requires a tx to land a private log
//! tagged with `compute_log_tag(poseidon2_hash([secret, index]), DOM_SEP__CONSTRAINED_MSG_LOG_TAG)` AND emit
//! the chained constrained-message nullifier. Both are produced by the forthcoming `emit_constrained_message`
//! helper, so positive and negative `index > 0` tests live with that helper. Add
//! `advances_index_above_zero_when_prior_message_was_emitted` and
//! `fails_at_index_above_zero_without_prior_nullifier` alongside `emit_constrained_message`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you mock the get_next_constrained_tagging_index oracle and test it here? Yes, for the full e2e version we would need more code and PXE, but we could test that branch by mocking the oracle, right?

@vezenovm

Copy link
Copy Markdown
Contributor Author

Closing as we have fully migrated over to #23866

@vezenovm vezenovm closed this Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants