Skip to content

feat(pxe): add execution hooks for authorizing cross-contract utility calls#23007

Merged
nchamo merged 5 commits into
merge-train/fairiesfrom
feat/execution-hooks
May 7, 2026
Merged

feat(pxe): add execution hooks for authorizing cross-contract utility calls#23007
nchamo merged 5 commits into
merge-train/fairiesfrom
feat/execution-hooks

Conversation

@nchamo

@nchamo nchamo commented May 6, 2026

Copy link
Copy Markdown
Contributor

Why we are doing this

Utility functions run in simulation and have access to private state, so allowing unrestricted cross-contract utility calls is a security risk — a contract could call into another contract's utility functions to read its secrets. We need a way to let wallets/PXE integrators control which cross-contract utility calls are allowed.

What we're doing

Adds an ExecutionHooks interface to PXECreationOptions. The idea is that hooks are a generic extension point for the PXE execution pipeline — we expect more to be added over time. The first one is authorizeUtilityCall.

authorizeUtilityCall is called before any cross-contract utility call. It gets passed the caller, target, selector, args, and whether the call originated from a private or utility context, and returns a boolean. The default denies everything, so existing behavior is unchanged.

To discuss

  • Naming: hooks vs callbacks — "hooks" feels right for TS/JS but open to other ideas
  • Required vs optional fields: currently all fields on ExecutionHooks are required and callers spread DEFAULT_EXECUTION_HOOKS to partially override. Alternative is making fields optional with defaults applied at call sites. Open to feedback

Future work

  • TXE should have a way to configure allowed cross-contract utility calls so test contracts don't have to disable the check globally

Fixes F-29

@nchamo nchamo added the ci-draft Run CI on draft PRs. label May 6, 2026
@nchamo nchamo self-assigned this May 6, 2026
@nchamo nchamo added the ci-draft Run CI on draft PRs. label May 6, 2026
@nchamo nchamo marked this pull request as ready for review May 6, 2026 19:53

@Thunkar Thunkar 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.

Like the approach! Would be great to add the plumbing on EmbeddedWallet and showcase how it works on the ecosystem repo

Comment thread yarn-project/pxe/src/hooks/authorize_utility_call.ts Outdated

@benesjan benesjan 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.

Looks nice but what's the reasoning for the flow to be so different from authwits? (am I correct in assuming that it's easy of implementation?)

During authwits we run simulation and harvest the authwit requests with the approach Grego implemented while here we are asking for permissions during the real thing.

It feels somewhat incorrect that we are choosing 2 completely different approaches when we are dealing with a somewhat similar thing (permissions to run stuff).

Comment thread yarn-project/pxe/src/hooks/execution_hooks.ts Outdated
Comment on lines +937 to +938
`Cross-contract utility call denied: ${this.contractAddress} attempted to call ` +
`${targetContractAddress}:${functionSelector} (${targetArtifact.name}).`,

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 think this should get its own error doc link

Comment thread yarn-project/pxe/src/hooks/authorize_utility_call.ts Outdated

@mverzilli mverzilli 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.

Love it! I'm just gonna wear my gregohat now and suggest that you use this in some way in go-swap, because I'm curious how this will translate to UX

@nventuro

nventuro commented May 7, 2026

Copy link
Copy Markdown
Contributor

Looks nice but what's the reasoning for the flow to be so different from authwits? (am I correct in assuming that it's easy of implementation?)

During authwits we run simulation and harvest the authwit requests with the approach Grego implemented while here we are asking for permissions during the real thing.

It feels somewhat incorrect that we are choosing 2 completely different approaches when we are dealing with a somewhat similar thing (permissions to run stuff).

An authwit requested requires human intervention. The person needs to make a decision (sign an authwit) which a) likely cannot be automated (it may require access to a hardware wallet, a signer that is located elsewhere. etc.), b) should not be automated (as it results in the user possibly spending assets or mutating their state), and c) for which the user does not have the full picture yet - they don't know what is going to happen once they sign.

If you implemented authwits via callbacks, then this would result in the simulation being repeatedly halted while the user is asked if they want to proceed, to which they'll have to option but to say yes, because there's no useful information for them to review - they can't even see the full transaction yet as it is still executing. This has been referrred to as 'authwits on the fly', and has been discarded for these very reasons.

We therefore implemented authwits via a different mechanism: since PXE knows about authwits (as they are a standard), it can cleanly reason about how to do a drop-in replacement for them via a kerneless simulation in which it simply records requets and lies to contracts pretending the request was accepted. The user can now be prompted once for all signatures needed and they can see the full transaction effects before signing. "If you sign this, this is what will happen".


Execution of utility functions is fundamentally different. PXE cannot predict what a utility function should return, so it has no way of mocking the result. Additionally, the decision on whether to execute a utility function is not about ownership but about privacy: a call made by a bad contract might result in some information being revealed. And even if we ignored all that, there would be nothing to be gained by gathering more information about the tx execution: a call is ok or not by itself (i.e. 'is the caller bad' or 'is this contract ok to call by anyone'), so trying to proceed before asking the user results in no benefit.

Finally, in many (most?) cases the wallet can decide whether a specific call is safe or not and authorize or reject it. Examples include lists of trusted/audited contracts (like the standard registry, widely used applications, etc.), and contracts the user has already trusted in the past. The callback is not expected to make its way all the way to the user, but rather for the wallet to resolve it itself in most cases. But we do need a callback because neither app nor wallet can predict ahead of time which contracts will be invoked during execution, so we need for a way to ask for permission after it has begun.

@nchamo nchamo merged commit 365da36 into merge-train/fairies May 7, 2026
14 checks passed
@nchamo nchamo deleted the feat/execution-hooks branch May 7, 2026 18:31
@AztecBot

AztecBot commented May 7, 2026

Copy link
Copy Markdown
Collaborator

❌ Failed to cherry-pick to v4-next due to conflicts. (🤖) View backport run.

AztecBot added a commit that referenced this pull request May 7, 2026
… calls (#23007)

Cherry-pick of PR #23007 with .rej files for the two hunks that failed
to apply against backport-to-v4-next-staging:

- yarn-project/end-to-end/src/fixtures/setup.ts.rej
  Hunk anchored on `skipInitialSequencer?: boolean;` field that does
  not exist on this branch.

- yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts.rej
  Hunk context shifted by an extra `log: this.logger,` line present on
  this branch. (Note: the target file is unmodified by this commit
  because its only hunk was rejected.)

This commit will not compile on its own; the next commit resolves the
.rej files.
AztecBot added a commit that referenced this pull request May 7, 2026
- yarn-project/end-to-end/src/fixtures/setup.ts:
  Insert pxeCreationOptions field at the equivalent position
  (end of SetupOptions, after walletMinFeePadding) since
  skipInitialSequencer doesn't exist on this branch.

- yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts:
  Add 'hooks: this.hooks,' to the args passed when constructing
  the child PrivateExecutionOracle, between simulator and
  l2TipsStore (as in #23007). this.hooks resolves via inherited
  protected field on UtilityExecutionOracle.
nchamo pushed a commit that referenced this pull request May 7, 2026
… calls (#23007)

Cherry-pick of PR #23007 with .rej files for the two hunks that failed
to apply against backport-to-v4-next-staging:

- yarn-project/end-to-end/src/fixtures/setup.ts.rej
  Hunk anchored on `skipInitialSequencer?: boolean;` field that does
  not exist on this branch.

- yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts.rej
  Hunk context shifted by an extra `log: this.logger,` line present on
  this branch. (Note: the target file is unmodified by this commit
  because its only hunk was rejected.)

This commit will not compile on its own; the next commit resolves the
.rej files.
nchamo pushed a commit that referenced this pull request May 7, 2026
- yarn-project/end-to-end/src/fixtures/setup.ts:
  Insert pxeCreationOptions field at the equivalent position
  (end of SetupOptions, after walletMinFeePadding) since
  skipInitialSequencer doesn't exist on this branch.

- yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts:
  Add 'hooks: this.hooks,' to the args passed when constructing
  the child PrivateExecutionOracle, between simulator and
  l2TipsStore (as in #23007). this.hooks resolves via inherited
  protected field on UtilityExecutionOracle.
nchamo added a commit that referenced this pull request May 7, 2026
…s-contract utility calls (#23066)

Backport of #23007 (`feat(pxe): add execution hooks for authorizing
cross-contract utility calls`, by @nchamo) to
`backport-to-v4-next-staging`.

The auto-cherry-pick (run
[25514836711](https://github.com/AztecProtocol/aztec-packages/actions/runs/25514836711))
failed with two rejected hunks. Restructured into two commits so the
failure and the fix are reviewable independently:

**1. `feat(pxe): add execution hooks for authorizing cross-contract
utility calls (#23007)`**
The cherry-pick attempt: applies the PR diff with `git apply --verbose
--reject`. All clean hunks land, and the two failed hunks are recorded
as `.rej` sidecar files committed alongside their target. Does not
compile on its own.

- `yarn-project/end-to-end/src/fixtures/setup.ts.rej` — anchor
`skipInitialSequencer?: boolean;` doesn't exist on this branch.
-
`yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts.rej`
— context shifted by an extra `log: this.logger,` line on this branch.
(Target file is unmodified by this commit because its only hunk
rejected.)

**2. `fix: resolve cherry-pick conflicts from #23007`**
Deletes the two `.rej` files and applies the equivalent edits manually:

- `setup.ts` — insert `pxeCreationOptions?: PXECreationOptions;` at the
end of `SetupOptions` (after `walletMinFeePadding`).
- `private_execution_oracle.ts` — insert `hooks: this.hooks,` between
`simulator: this.simulator,` and `l2TipsStore: this.l2TipsStore,`.
`this.hooks` resolves via the `protected readonly hooks: ExecutionHooks
| undefined` field that this PR adds on the parent class
`UtilityExecutionOracle`.

No semantic deviations from the source PR.
@benesjan

Copy link
Copy Markdown
Contributor

That makes sense. Thanks for the thorough explanation @nventuro

BTW feels like that comment would be a good basis for some docs

rangozd pushed a commit to rangozd/aztec-packages that referenced this pull request May 16, 2026
BEGIN_COMMIT_OVERRIDE
feat: allow setting additional scopes in nr tests (AztecProtocol#22968)
feat(pxe): add execution hooks for authorizing cross-contract utility
calls (AztecProtocol#23007)
END_COMMIT_OVERRIDE
danielntmd pushed a commit to danielntmd/aztec-packages that referenced this pull request Jun 4, 2026
The 4 `toMatchObject` assertions in the `authorizeUtilityCall hook` block
compared `lastRequest.functionSelector` against `contractB.methods.pow_utility.selector()`,
which is async (returns `Promise<FunctionSelector>` via
`FunctionSelector.fromNameAndParameters`, async since AztecProtocol#23007). An un-awaited
Promise has no own enumerable properties, so `toMatchObject` treated the
expected `functionSelector` as a vacuously-satisfied empty object and never
actually checked the selector -- the assertions silently passed regardless of
the real value.

Awaiting `.selector()` makes the assertions compare against the resolved
`FunctionSelector`. Verified: a negative control with a deliberately wrong
selector fails only after the await is added, and the full suite passes 10/10
with the correct selector.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants