Skip to content

feat: add optional additional scopes to wallet transaction API#20487

Open
nchamo wants to merge 4 commits intomerge-train/fairiesfrom
refactor/additional-scopes
Open

feat: add optional additional scopes to wallet transaction API#20487
nchamo wants to merge 4 commits intomerge-train/fairiesfrom
refactor/additional-scopes

Conversation

@nchamo
Copy link
Contributor

@nchamo nchamo commented Feb 13, 2026

Summary

Previously, PrivateExecutionOracle automatically expanded scopes when making nested private calls to registered account contracts. This behavior was pretty hidden and made it hard to reason about which addresses' private state was accessible during execution.

This PR removes the auto-expansion and instead introduces an explicit additionalScopes option on send(), simulate(), and deploy(). Callers now declare upfront which additional addresses' private state they need access to beyond the sender's.

@nchamo nchamo changed the title refactor: add explicit additionalScopes to wallet transaction API feat: add optional additional scopes to wallet transaction API Feb 13, 2026

#[external("private")]
fn test_recursively_create_notes(owner: AztecAddress, how_many_recursions: u64) {
fn test_recursively_create_notes(recipients: [AztecAddress; 10], how_many_recursions: u64) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We had to make these changes when we introduced scopes, because there was no way to inject multiple scopes. Since now there is, we can revert the change we made

@nchamo nchamo added the ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure label Feb 13, 2026
@nchamo nchamo self-assigned this Feb 13, 2026

isStaticCall = isStaticCall || this.callContext.isStaticCall;

// When scopes are set and the target contract is a registered account (has keys in the keyStore),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Die hack, die!

Copy link
Contributor

Choose a reason for hiding this comment

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

congrats :D

// When `from` is the zero address (e.g. when deploying a new account contract), we use only the
// additionalScopes if any, or an empty list which acts as deny-all: no notes are visible and
// no keys are accessible. Otherwise, we combine `from` with any additionalScopes, deduplicating.
protected scopesFor(from: AztecAddress, additionalScopes: AztecAddress[] = []): AztecAddress[] {
Copy link
Contributor

Choose a reason for hiding this comment

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

I find this name doesn't make match sense, maybe scopesFrom would make more sense.

And the comments are just a natural language description of the imperative steps the function does, which personally distracted me more than just reading the code. I would just remove the comment.

Copy link
Contributor

@mverzilli mverzilli left a comment

Choose a reason for hiding this comment

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

Looks good! I think it's missing migration notes, and some documentation in general


const batch = new BatchCall(wallet, calls);
const opts = await this.getSendMethodOpts(batch);
const additionalScopes = isStandardTokenContract(token) ? undefined : [token.address];
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does the non standard token need additional scopes?

const tx = await bobsDeployMethod.send({ from: AztecAddress.ZERO, wait: { returnReceipt: true } });
const tx = await bobsDeployMethod.send({
from: AztecAddress.ZERO,
additionalScopes: [bobsAddress],
Copy link
Contributor

Choose a reason for hiding this comment

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

This is the only thing that worries me. It makes 100% total sense that account contract self-deployments need this, but for such a common operation it's getting very verbose. Maybe we should consider this a higher level abstraction and provide some sane defaults

Copy link
Contributor

@Thunkar Thunkar left a comment

Choose a reason for hiding this comment

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

I'm fine with merging this as-is, but two important things remain:

  • Modify the capabilities manifest so simulations and transactions declare extra scopes might be required. Take into account the case where these scopes are not known in advance by the app, but are the same ones requested by the getAccounts capability
  • Talk about account contract self-deployments and how to add defaults (and the problems this caused in the past.

Copy link
Contributor

@benesjan benesjan left a comment

Choose a reason for hiding this comment

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

I am repeating myself in some of the comments but think it would be useful to put everywhere a comment on why the scopes are getting expanded

/** The fee options for the transaction. */
fee?: InteractionFeeOptions;
/**
* Additional addresses whose private state should be accessible during execution,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* Additional addresses whose private state should be accessible during execution,
* Additional addresses whose private state and keys should be accessible during execution,

fee?: InteractionFeeOptions;
/**
* Additional addresses whose private state should be accessible during execution,
* beyond the sender's
Copy link
Contributor

Choose a reason for hiding this comment

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

Think it would be useful to have here an example of why you would want this.

skipInstancePublication: !publicDeploy,
skipInitialization,
from,
additionalScopes: [address],
Copy link
Contributor

Choose a reason for hiding this comment

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

Would put here a comment regarding why we need to add the additional scope

await newAccountDeployMethod.send({ from: prefundedAccount.address });
await newAccountDeployMethod.send({
from: prefundedAccount.address,
additionalScopes: [newAccountManager.address],
Copy link
Contributor

Choose a reason for hiding this comment

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

Would put here a comment regarding why we need to add the additional scope

accountManagers.map(async x => {
const deployMethod = await x.getDeployMethod();
await deployMethod.send({ from: fundedAccount });
await deployMethod.send({ from: fundedAccount, additionalScopes: [x.address] });
Copy link
Contributor

Choose a reason for hiding this comment

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

Would put here a comment regarding why we need to add the additional scope

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants