Skip to content

Commit 0a5df39

Browse files
nventuroaztec-bot
authored andcommitted
fix: tagging secrets not being scoped by sender (#24772)
This fixes a scoping bug where an account's ivsk would be used to produce an ECDH shared secret even if that account was not in scope. The base wallet class was adjusted to put these in scope when an explicit sender is selected for tagged messages. (cherry picked from commit 4994006)
1 parent 0f0577c commit 0a5df39

7 files changed

Lines changed: 68 additions & 20 deletions

File tree

yarn-project/cli-wallet/src/utils/wallet.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,10 @@ export class CLIWallet extends BaseWallet {
145145
increasedFee: InteractionFeeOptions,
146146
): Promise<TxProvingResult> {
147147
const cancellationTxRequest = await this.createCancellationTxExecutionRequest(from, txNonce, increasedFee);
148-
return await this.pxe.proveTx(cancellationTxRequest, { scopes: this.scopesFrom(from), senderForTags: from });
148+
return await this.pxe.proveTx(cancellationTxRequest, {
149+
scopes: this.scopesFrom(from, [], undefined),
150+
senderForTags: from,
151+
});
149152
}
150153

151154
override async getAccountFromAddress(address: AztecAddress) {
@@ -292,7 +295,7 @@ export class CLIWallet extends BaseWallet {
292295
opts: SimulateViaEntrypointOptions,
293296
): Promise<TxSimulationResultWithAppOffset> {
294297
const { from, feeOptions, additionalScopes, sendMessagesAs } = opts;
295-
const scopes = this.scopesFrom(from, additionalScopes);
298+
const scopes = this.scopesFrom(from, additionalScopes ?? [], sendMessagesAs);
296299
const feeExecutionPayload = await feeOptions.walletFeePaymentMethod?.getExecutionPayload();
297300
const finalExecutionPayload = feeExecutionPayload
298301
? mergeExecutionPayloads([feeExecutionPayload, executionPayload])

yarn-project/end-to-end/src/test-wallet/test_wallet.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ export class TestWallet extends BaseWallet {
321321
opts: SimulateViaEntrypointOptions,
322322
): Promise<TxSimulationResultWithAppOffset> {
323323
const { from, feeOptions, additionalScopes, skipTxValidation, skipFeeEnforcement, sendMessagesAs } = opts;
324-
const scopes = this.scopesFrom(from, additionalScopes);
324+
const scopes = this.scopesFrom(from, additionalScopes ?? [], sendMessagesAs);
325325
const skipKernels = this.simulationMode !== 'full';
326326
const useOverride = this.simulationMode === 'kernelless-override';
327327

@@ -387,7 +387,7 @@ export class TestWallet extends BaseWallet {
387387
});
388388
const txRequest = await this.createTxExecutionRequestFromPayloadAndFee(exec, opts.from, fee);
389389
const txProvingResult = await this.pxe.proveTx(txRequest, {
390-
scopes: this.scopesFrom(opts.from, opts.additionalScopes),
390+
scopes: this.scopesFrom(opts.from, opts.additionalScopes ?? [], opts.sendMessagesAs),
391391
senderForTags: this.senderForTagsFrom(opts.from, opts.sendMessagesAs),
392392
});
393393
return new ProvenTx(

yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { MAX_PROCESSABLE_L2_GAS, MAX_TX_DA_GAS } from '@aztec/constants';
22
import { Fr } from '@aztec/foundation/curves/bn254';
3-
import { Point } from '@aztec/foundation/curves/grumpkin';
3+
import { GrumpkinScalar, Point } from '@aztec/foundation/curves/grumpkin';
44
import type { KeyStore } from '@aztec/key-store';
55
import { WASMSimulator } from '@aztec/simulator/client';
66
import { FunctionSelector } from '@aztec/stdlib/abi';
77
import type { AuthWitness } from '@aztec/stdlib/auth-witness';
88
import { AztecAddress } from '@aztec/stdlib/aztec-address';
99
import type { L2TipsProvider } from '@aztec/stdlib/block';
10+
import { CompleteAddress } from '@aztec/stdlib/contract';
1011
import { Gas, GasFees, GasSettings } from '@aztec/stdlib/gas';
1112
import type { AztecNode } from '@aztec/stdlib/interfaces/server';
1213
import { AppTaggingSecret, AppTaggingSecretKind } from '@aztec/stdlib/logs';
@@ -80,6 +81,35 @@ describe('PrivateExecutionOracle', () => {
8081
});
8182
});
8283

84+
describe('getAppTaggingSecret', () => {
85+
it('rejects a sender outside the execution scopes', async () => {
86+
const inScopeSender = await AztecAddress.random();
87+
const outOfScopeSender = await AztecAddress.random();
88+
const recipient = await AztecAddress.random();
89+
const oracle = makeOracle({ scopes: [inScopeSender] });
90+
91+
await expect(oracle.getAppTaggingSecret(outOfScopeSender, recipient)).rejects.toThrow(
92+
'is not in the allowed scopes list',
93+
);
94+
});
95+
96+
it('returns a secret for a sender inside the execution scopes', async () => {
97+
const senderCompleteAddress = await CompleteAddress.random();
98+
const sender = senderCompleteAddress.address;
99+
const recipient = (await CompleteAddress.random()).address;
100+
101+
const addressStore = mock<AddressStore>();
102+
addressStore.getCompleteAddress.mockResolvedValue(senderCompleteAddress);
103+
const keyStore = mock<KeyStore>();
104+
keyStore.getMasterIncomingViewingSecretKey.mockResolvedValue(GrumpkinScalar.random());
105+
106+
const oracle = makeOracle({ scopes: [sender], addressStore, keyStore });
107+
108+
const result = await oracle.getAppTaggingSecret(sender, recipient);
109+
expect(result.isSome()).toBe(true);
110+
});
111+
});
112+
83113
describe('resolveTaggingStrategy', () => {
84114
let sender: AztecAddress;
85115
let recipient: AztecAddress;

yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import type {
3535
TaggingSecretStrategy,
3636
} from '../../hooks/resolve_tagging_secret_strategy.js';
3737
import { NoteService } from '../../notes/note_service.js';
38+
import { assertAllowedScope } from '../../storage/allowed_scopes.js';
3839
import type { SenderTaggingStore } from '../../storage/tagging_store/sender_tagging_store.js';
3940
import { syncSenderTaggingIndexes } from '../../tagging/index.js';
4041
import type { ExecutionNoteCache } from '../execution_note_cache.js';
@@ -321,7 +322,17 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP
321322
* @returns The app tagging secret, or `None` if the recipient is invalid.
322323
*/
323324
public async getAppTaggingSecret(sender: AztecAddress, recipient: AztecAddress): Promise<Option<Fr>> {
324-
const extendedSecret = await this.#calculateAppTaggingSecret(this.contractAddress, sender, recipient);
325+
assertAllowedScope(sender, this.scopes);
326+
327+
const senderCompleteAddress = await this.getCompleteAddressOrFail(sender);
328+
const senderIvsk = await this.keyStore.getMasterIncomingViewingSecretKey(sender);
329+
const extendedSecret = await AppTaggingSecret.computeViaEcdh(
330+
senderCompleteAddress,
331+
senderIvsk,
332+
recipient,
333+
this.contractAddress,
334+
recipient,
335+
);
325336

326337
if (!extendedSecret) {
327338
this.logger.warn(`Computing a tagging secret for invalid recipient ${recipient} - returning no secret`, {
@@ -354,12 +365,6 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP
354365
return index;
355366
}
356367

357-
async #calculateAppTaggingSecret(contractAddress: AztecAddress, sender: AztecAddress, recipient: AztecAddress) {
358-
const senderCompleteAddress = await this.getCompleteAddressOrFail(sender);
359-
const senderIvsk = await this.keyStore.getMasterIncomingViewingSecretKey(sender);
360-
return AppTaggingSecret.computeViaEcdh(senderCompleteAddress, senderIvsk, recipient, contractAddress, recipient);
361-
}
362-
363368
async #getIndexToUseForSecret(secret: AppTaggingSecret): Promise<number> {
364369
// If we have the tagging index in the cache, we use it. If not we obtain it from the execution data provider.
365370
const lastUsedIndexInTx = this.taggingIndexCache.getLastUsedIndex(secret);

yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,15 @@ export abstract class BaseWallet implements Wallet {
125125
protected log = createLogger('wallet-sdk:base_wallet'),
126126
) {}
127127

128-
protected scopesFrom(from: AztecAddress | NoFrom, additionalScopes: AztecAddress[] = []): AztecAddress[] {
129-
const allScopes = from === NO_FROM ? additionalScopes : [from, ...additionalScopes];
128+
protected scopesFrom(
129+
from: AztecAddress | NoFrom,
130+
additionalScopes: AztecAddress[],
131+
sendMessagesAs: AztecAddress | undefined,
132+
): AztecAddress[] {
133+
// The sendMessagesAs account must be in scope so that its tagging secrets can be accessed.
134+
const tagSenderScopes = sendMessagesAs ? [sendMessagesAs] : [];
135+
const baseScopes = from === NO_FROM ? [] : [from];
136+
const allScopes = [...baseScopes, ...additionalScopes, ...tagSenderScopes];
130137
const scopeSet = new Set(allScopes.map(address => address.toString()));
131138
return [...scopeSet].map(AztecAddress.fromStringUnsafe);
132139
}
@@ -402,7 +409,7 @@ export abstract class BaseWallet implements Wallet {
402409
simulatePublic: true,
403410
skipTxValidation: opts.skipTxValidation,
404411
skipFeeEnforcement: opts.skipFeeEnforcement,
405-
scopes: this.scopesFrom(opts.from, opts.additionalScopes),
412+
scopes: this.scopesFrom(opts.from, opts.additionalScopes ?? [], opts.sendMessagesAs),
406413
senderForTags: this.senderForTagsFrom(opts.from, opts.sendMessagesAs),
407414
overrides: opts.overrides,
408415
});
@@ -498,7 +505,7 @@ export abstract class BaseWallet implements Wallet {
498505
return this.pxe.profileTx(txRequest, {
499506
profileMode: opts.profileMode,
500507
skipProofGeneration: opts.skipProofGeneration ?? true,
501-
scopes: this.scopesFrom(opts.from, opts.additionalScopes),
508+
scopes: this.scopesFrom(opts.from, opts.additionalScopes ?? [], opts.sendMessagesAs),
502509
senderForTags: this.senderForTagsFrom(opts.from, opts.sendMessagesAs),
503510
});
504511
}
@@ -515,7 +522,7 @@ export abstract class BaseWallet implements Wallet {
515522
});
516523
const txRequest = await this.createTxExecutionRequestFromPayloadAndFee(executionPayload, opts.from, feeOptions);
517524
const provenTx = await this.pxe.proveTx(txRequest, {
518-
scopes: this.scopesFrom(opts.from, opts.additionalScopes),
525+
scopes: this.scopesFrom(opts.from, opts.additionalScopes ?? [], opts.sendMessagesAs),
519526
senderForTags: this.senderForTagsFrom(opts.from, opts.sendMessagesAs),
520527
});
521528
const offchainOutput = extractOffchainOutput(

yarn-project/wallets/src/embedded/embedded_wallet.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe('EmbeddedWallet', () => {
4242
});
4343

4444
describe('sendTx', () => {
45-
it('passes sendMessagesAs as senderForTags to PXE simulation', async () => {
45+
it('passes sendMessagesAs as senderForTags and includes it in scopes for PXE simulation', async () => {
4646
getPredictedMinFees.mockResolvedValue([new GasFees(2, 2)]);
4747
getNodeInfo.mockResolvedValue({
4848
l1ChainId: 1,
@@ -69,7 +69,10 @@ describe('EmbeddedWallet', () => {
6969

7070
expect(simulateTx).toHaveBeenCalledWith(
7171
expect.anything(),
72-
expect.objectContaining({ senderForTags: sendMessagesAs }),
72+
expect.objectContaining({
73+
senderForTags: sendMessagesAs,
74+
scopes: expect.arrayContaining([sendMessagesAs]),
75+
}),
7376
);
7477
});
7578

yarn-project/wallets/src/embedded/embedded_wallet.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ export class EmbeddedWallet extends BaseWallet {
330330
opts: SimulateViaEntrypointOptions,
331331
): Promise<TxSimulationResultWithAppOffset> {
332332
const { from, feeOptions, additionalScopes, skipTxValidation, skipFeeEnforcement, sendMessagesAs } = opts;
333-
const scopes = this.scopesFrom(from, additionalScopes);
333+
const scopes = this.scopesFrom(from, additionalScopes ?? [], sendMessagesAs);
334334

335335
const feeExecutionPayload = await feeOptions.walletFeePaymentMethod?.getExecutionPayload();
336336
const finalExecutionPayload = feeExecutionPayload

0 commit comments

Comments
 (0)