-
Notifications
You must be signed in to change notification settings - Fork 612
feat(pxe): resolve tagging secret source via wallet hook #24040
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nchamo
wants to merge
17
commits into
merge-train/fairies-v5
Choose a base branch
from
nchamo/f-699-pxewallet-get_delivery_privacy_preference-oracle
base: merge-train/fairies-v5
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
df57245
feat(pxe): add get_delivery_privacy_preference oracle
nchamo 26917d5
fix(pxe): remove duplicate hooks re-export from server entrypoint
nchamo 36c849f
refactor(pxe): address review feedback on delivery privacy preference
nchamo ea9bf23
Merge origin/merge-train/fairies-v5 into nchamo/f-699-pxewallet-get_d…
nchamo f5d14ac
docs: clarify delivery preference is consulted only when establishing…
nchamo 2ea81d0
docs: explain the cross-contract risk authorizeUtilityCall guards aga…
nchamo 4454c2c
feat(pxe): resolve tagging secret source via wallet hook
nchamo 1c6c807
Merge origin/merge-train/fairies-v5 into nchamo/f-699-pxewallet-get_d…
nchamo f04873c
test(aztec-nr): wire oracle tests for resolve_tagging_secret
nchamo d550918
fix(pxe): refresh oracle interface hash after merge
nchamo 5fc1e29
fix(txe): refresh TXE oracle interface hash after merge
nchamo a9d6a84
docs: use onchain to satisfy cspell forbidden word
nchamo a55134f
Merge origin/merge-train/fairies-v5 into nchamo/f-699-pxewallet-get_d…
nchamo 19ba78d
fix(txe): refresh TXE oracle interface hash after merge
nchamo 3e5e021
docs(aztec-nr): clarify tagging secret handshake reuse
nchamo f00d8eb
docs: clarify tagging secret source in note delivery/discovery
nchamo 48a022f
docs: fix off-chain spelling to satisfy cspell
nchamo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 86 additions & 0 deletions
86
docs/docs-developers/docs/foundational-topics/pxe/execution_hooks.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| --- | ||
| title: Execution hooks | ||
| sidebar_position: 3 | ||
| tags: [pxe, wallets] | ||
| description: How wallets use PXE execution hooks to apply custom policies during client-side simulation. | ||
| --- | ||
|
|
||
| Execution hooks are callbacks that the PXE invokes during client-side simulation when an operation needs a decision from the wallet. They let the wallet apply its own policies before execution proceeds, such as prompting the user, consulting a dynamic allowlist, or inspecting call arguments. Every hook is optional, and when a hook is absent the PXE applies a safe default. | ||
|
nchamo marked this conversation as resolved.
Outdated
|
||
|
|
||
| ## Configuring hooks | ||
|
|
||
| Pass a `hooks` object when creating the PXE: | ||
|
|
||
| ```typescript | ||
| import { createPXE } from "@aztec/pxe/server"; | ||
| import { DeliveryPrivacyPreference } from "@aztec/pxe/config"; | ||
|
|
||
| const pxe = await createPXE(node, config, { | ||
| hooks: { | ||
| // Allow calls to a known helper contract, deny everything else. | ||
| authorizeUtilityCall: async (request) => { | ||
| return request.target.equals(trustedHelper) | ||
| ? { authorized: true } | ||
| : { authorized: false, reason: "Unknown target" }; | ||
| }, | ||
| // Accept the privacy leak of on-the-fly handshakes so messages reach recipients that haven't registered the sender. | ||
| getDeliveryPrivacyPreference: async () => DeliveryPrivacyPreference.BEST_EFFORT, | ||
|
nchamo marked this conversation as resolved.
Outdated
|
||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| ## `authorizeUtilityCall` | ||
|
|
||
| Called whenever a utility function makes a cross-contract call. A call made by a malicious contract could leak private information, so the hook lets the wallet decide, per call, whether to allow it. A static allowlist would not work here because neither the app nor the wallet can predict ahead of time which contracts will be invoked during execution: permission must be asked after execution has begun. Calls to standard contracts (such as the HandshakeRegistry, which is queried during every contract's sync) bypass this hook and are always authorized. | ||
|
nchamo marked this conversation as resolved.
|
||
|
|
||
| Unlike [authentication witnesses (authwits)](../../aztec-js/how_to_use_authwit.md), the hook is invoked live, while execution is underway. Authwits can be recorded during simulation and signed once at the end, but the PXE cannot predict what a utility call would return, so it must ask before continuing. Most of the time the wallet can answer on its own, for example against a list of audited or previously trusted contracts, without involving the user. | ||
|
nchamo marked this conversation as resolved.
Outdated
|
||
|
|
||
| ### In production | ||
|
nchamo marked this conversation as resolved.
Outdated
|
||
|
|
||
| Pass an `authorizeUtilityCall` hook when [creating the PXE](#configuring-hooks). It receives a `UtilityCallAuthorizationRequest` with the caller and target addresses, their contract class IDs, the function selector, the function name, the arguments, and the caller context (`'private'`, `'private view'`, or `'utility'`). Return `{ authorized: true }` to allow the call, or `{ authorized: false, reason: '...' }` to deny it with a message. | ||
|
|
||
| When the hook is absent, cross-contract utility calls are denied. See [Cross-contract utility call denied](../../aztec-nr/debugging.md#cross-contract-utility-call-denied) for the resulting error. | ||
|
|
||
| ### In Noir tests | ||
|
|
||
| The hook only exists on a real PXE. When testing cross-contract utility calls in the Noir test environment (TXE), use `with_authorized_utility_call_targets` on your call options: | ||
|
nchamo marked this conversation as resolved.
Outdated
|
||
|
|
||
| ```rust | ||
| // For private calls: | ||
| env.call_private_opts( | ||
| account, | ||
| CallPrivateOptions::new().with_authorized_utility_call_targets([target_address]), | ||
|
nchamo marked this conversation as resolved.
|
||
| MyContract::at(caller).some_private_fn(), | ||
| ); | ||
|
|
||
| // For private view calls: | ||
| env.view_private_opts( | ||
| account, | ||
| ViewPrivateOptions::new().with_authorized_utility_call_targets([target_address]), | ||
| MyContract::at(caller).some_view_fn(), | ||
| ); | ||
|
|
||
| // For utility calls: | ||
| env.execute_utility_opts( | ||
| ExecuteUtilityOptions::new().with_authorized_utility_call_targets([target_address]), | ||
| MyContract::at(caller).some_utility_fn(), | ||
| ); | ||
| ``` | ||
|
|
||
| ## `getDeliveryPrivacyPreference` | ||
|
|
||
| Called when message delivery needs a tagging secret and the executing contract has not pinned a tag-secret derivation. The hook lets the wallet choose between maximum privacy and delivery that requires no sender-recipient coordination; see [Delivery privacy preference](../../aztec-nr/framework-description/note_delivery.md#delivery-privacy-preference) for the trade-offs and the defaults in each environment. | ||
|
|
||
| ### In production | ||
|
|
||
| Pass a `getDeliveryPrivacyPreference` hook when [creating the PXE](#configuring-hooks). It receives a `DeliveryPrivacyPreferenceRequest` with the executing contract's address and the message's sender, recipient, and delivery mode (`'constrained'` or `'unconstrained'`), so a wallet can apply per-application or per-recipient policies, or surface the decision to the user, instead of returning a fixed value. | ||
|
|
||
| When the hook is absent, the PXE assumes `DeliveryPrivacyPreference.MAX_PRIVACY`, so privacy is never weakened without the wallet opting in. | ||
|
|
||
| ### In Noir tests | ||
|
|
||
| The hook only exists on a real PXE. In the Noir test environment (TXE), which defaults to max privacy like a bare PXE, set the preference on the test environment; it affects message delivery in subsequent private executions: | ||
|
nchamo marked this conversation as resolved.
Outdated
|
||
|
|
||
| ```rust | ||
| env.set_delivery_privacy_preference(DeliveryPrivacyPreference::best_effort()); | ||
|
nchamo marked this conversation as resolved.
Outdated
|
||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
98 changes: 98 additions & 0 deletions
98
noir-projects/aztec-nr/aztec/src/messages/delivery/privacy_preference.nr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| use crate::protocol::{traits::Deserialize, utils::reader::Reader}; | ||
|
|
||
| /// A wallet-owned policy that selects the tag-secret derivation used by message delivery when the contract does not | ||
| /// pin one. | ||
| /// | ||
| /// Max privacy never leaks information and instead relies on sender-recipient coordination for delivery. Best effort | ||
| /// accepts a privacy leak so that delivery requires no sender-recipient coordination at all. | ||
| /// | ||
| /// ## Who configures what | ||
| /// | ||
| /// Each party involved in message delivery owns a different knob: | ||
| /// | ||
| /// - **Contracts** choose a delivery mode and can optionally pin a tag-secret derivation via the `via_*` methods on | ||
| /// the [`MessageDelivery`](crate::messages::delivery::MessageDelivery) builders (see | ||
| /// [`OnchainUnconstrainedDelivery`](crate::messages::delivery::OnchainUnconstrainedDelivery) and | ||
| /// [`OnchainConstrainedDelivery`](crate::messages::delivery::OnchainConstrainedDelivery)). When they pin nothing, | ||
| /// the decision is delegated to the wallet, which is the recommended default. | ||
| /// - **Wallets** answer that delegation with this preference, reported through the | ||
| /// [`get_delivery_privacy_preference`](crate::oracle::delivery_privacy_preference::get_delivery_privacy_preference) | ||
| /// oracle. | ||
| /// | ||
| /// ## Privacy | ||
| /// | ||
| /// The two variants trade off as follows: | ||
| /// | ||
| /// - [`max_privacy`](Self::max_privacy): nothing that could link sender and recipient is published, and delivery | ||
| /// relies on sender-recipient coordination. Unconstrained delivery uses the address-pair (ECDH) secret, which is | ||
| /// derived locally and leaves no on-chain trace, but the recipient only finds the message if they registered the | ||
| /// sender. Constrained delivery requires an interactive handshake with the recipient, and fails when none exists. | ||
| /// - [`best_effort`](Self::best_effort): tags are derived from a non-interactive handshake, reusing an existing one | ||
| /// or establishing it on the fly, letting the recipient discover the message without any prior sender-recipient | ||
| /// coordination. Establishing it publishes an on-chain handshake that reveals information about the recipient. | ||
| pub struct DeliveryPrivacyPreference { | ||
|
nchamo marked this conversation as resolved.
Outdated
|
||
| inner: u8, | ||
| } | ||
|
|
||
| impl DeliveryPrivacyPreference { | ||
| /// Never leak information to obtain a tagging secret. | ||
| /// | ||
| /// Delivery relies on sender-recipient coordination instead: unconstrained messages are only found by recipients | ||
| /// who registered the sender, and constrained delivery requires an interactive handshake. | ||
| pub fn max_privacy() -> Self { | ||
| Self { inner: 1 } | ||
| } | ||
|
|
||
| /// Accept a privacy leak to deliver without sender-recipient coordination. | ||
| /// | ||
| /// Tags are derived from a non-interactive handshake, reused when one exists and otherwise established on the | ||
| /// fly. The handshake is published on-chain and reveals information about the recipient. In exchange, the | ||
| /// recipient discovers the message without coordinating with the sender in advance. | ||
| pub fn best_effort() -> Self { | ||
| Self { inner: 2 } | ||
| } | ||
|
|
||
| /// Validates a raw discriminant, as deserialization must always reject unknown values. | ||
| fn from_u8(inner: u8) -> Self { | ||
| let preference = Self { inner }; | ||
| assert( | ||
| (preference == Self::max_privacy()) | (preference == Self::best_effort()), | ||
| "unrecognized delivery privacy preference", | ||
|
nchamo marked this conversation as resolved.
Outdated
|
||
| ); | ||
| preference | ||
| } | ||
| } | ||
|
|
||
| impl Deserialize for DeliveryPrivacyPreference { | ||
|
nchamo marked this conversation as resolved.
Outdated
|
||
| 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 DeliveryPrivacyPreference { | ||
|
nchamo marked this conversation as resolved.
Outdated
|
||
| fn eq(self, other: Self) -> bool { | ||
| self.inner == other.inner | ||
| } | ||
| } | ||
|
|
||
| mod test { | ||
| use crate::protocol::traits::Deserialize; | ||
| use super::DeliveryPrivacyPreference; | ||
|
|
||
| #[test] | ||
| fn deserialize_roundtrips_valid_preferences() { | ||
| assert(DeliveryPrivacyPreference::deserialize([1]) == DeliveryPrivacyPreference::max_privacy()); | ||
| assert(DeliveryPrivacyPreference::deserialize([2]) == DeliveryPrivacyPreference::best_effort()); | ||
|
nchamo marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| #[test(should_fail_with = "unrecognized delivery privacy preference")] | ||
| fn deserializing_invalid_preference_fails() { | ||
| let _ = DeliveryPrivacyPreference::deserialize([99]); | ||
| } | ||
| } | ||
55 changes: 55 additions & 0 deletions
55
noir-projects/aztec-nr/aztec/src/oracle/delivery_privacy_preference.nr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| use crate::messages::delivery::{DeliveryPrivacyPreference, OnchainDeliveryMode}; | ||
| use crate::protocol::address::AztecAddress; | ||
|
|
||
| /// Returns the wallet's [`DeliveryPrivacyPreference`] for a message sent from `sender` to `recipient` with the given | ||
| /// delivery `mode`. | ||
| pub(crate) unconstrained fn get_delivery_privacy_preference( | ||
| sender: AztecAddress, | ||
| recipient: AztecAddress, | ||
| mode: OnchainDeliveryMode, | ||
| ) -> DeliveryPrivacyPreference { | ||
| get_delivery_privacy_preference_oracle(sender, recipient, mode) | ||
| } | ||
|
|
||
| #[oracle(aztec_prv_getDeliveryPrivacyPreference)] | ||
| unconstrained fn get_delivery_privacy_preference_oracle( | ||
| _sender: AztecAddress, | ||
| _recipient: AztecAddress, | ||
| _mode: OnchainDeliveryMode, | ||
| ) -> DeliveryPrivacyPreference {} | ||
|
|
||
| mod test { | ||
|
nchamo marked this conversation as resolved.
Outdated
|
||
| use crate::messages::delivery::{DeliveryPrivacyPreference, OnchainDeliveryMode}; | ||
| use crate::protocol::{address::AztecAddress, traits::FromField}; | ||
| use crate::test::helpers::test_environment::TestEnvironment; | ||
| use super::get_delivery_privacy_preference; | ||
|
|
||
| #[test] | ||
| unconstrained fn defaults_to_max_privacy() { | ||
| let env = TestEnvironment::new(); | ||
|
|
||
| env.private_context(|_| { | ||
| let preference = get_delivery_privacy_preference( | ||
| AztecAddress::from_field(1), | ||
| AztecAddress::from_field(2), | ||
| OnchainDeliveryMode::onchain_unconstrained(), | ||
| ); | ||
| assert_eq(preference, DeliveryPrivacyPreference::max_privacy()); | ||
| }); | ||
| } | ||
|
|
||
| #[test] | ||
| unconstrained fn returns_the_preference_set_in_the_test_environment() { | ||
| let env = TestEnvironment::new(); | ||
| env.set_delivery_privacy_preference(DeliveryPrivacyPreference::best_effort()); | ||
|
|
||
| env.private_context(|_| { | ||
| let preference = get_delivery_privacy_preference( | ||
| AztecAddress::from_field(1), | ||
| AztecAddress::from_field(2), | ||
| OnchainDeliveryMode::onchain_constrained(), | ||
| ); | ||
| assert_eq(preference, DeliveryPrivacyPreference::best_effort()); | ||
| }); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.