Skip to content

Commit 4994006

Browse files
authored
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.
1 parent d2db074 commit 4994006

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) {
@@ -311,7 +314,7 @@ export class CLIWallet extends BaseWallet {
311314
opts: SimulateViaEntrypointOptions,
312315
): Promise<TxSimulationResultWithAppOffset> {
313316
const { from, feeOptions, additionalScopes, sendMessagesAs } = opts;
314-
const scopes = this.scopesFrom(from, additionalScopes);
317+
const scopes = this.scopesFrom(from, additionalScopes ?? [], sendMessagesAs);
315318
const feeExecutionPayload = await feeOptions.walletFeePaymentMethod?.getExecutionPayload();
316319
const finalExecutionPayload = feeExecutionPayload
317320
? 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
@@ -318,7 +318,7 @@ export class TestWallet extends BaseWallet {
318318
opts: SimulateViaEntrypointOptions,
319319
): Promise<TxSimulationResultWithAppOffset> {
320320
const { from, feeOptions, additionalScopes, skipTxValidation, skipFeeEnforcement, sendMessagesAs } = opts;
321-
const scopes = this.scopesFrom(from, additionalScopes);
321+
const scopes = this.scopesFrom(from, additionalScopes ?? [], sendMessagesAs);
322322
const skipKernels = this.simulationMode !== 'full';
323323
const useOverride = this.simulationMode === 'kernelless-override';
324324

@@ -384,7 +384,7 @@ export class TestWallet extends BaseWallet {
384384
});
385385
const txRequest = await this.createTxExecutionRequestFromPayloadAndFee(exec, opts.from, fee);
386386
const txProvingResult = await this.pxe.proveTx(txRequest, {
387-
scopes: this.scopesFrom(opts.from, opts.additionalScopes),
387+
scopes: this.scopesFrom(opts.from, opts.additionalScopes ?? [], opts.sendMessagesAs),
388388
senderForTags: this.senderForTagsFrom(opts.from, opts.sendMessagesAs),
389389
});
390390
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
@@ -36,6 +36,7 @@ import {
3636
type TaggingSecretStrategy,
3737
} from '../../hooks/resolve_tagging_secret_strategy.js';
3838
import { NoteService } from '../../notes/note_service.js';
39+
import { assertAllowedScope } from '../../storage/allowed_scopes.js';
3940
import type { SenderTaggingStore } from '../../storage/tagging_store/sender_tagging_store.js';
4041
import { syncSenderTaggingIndexes } from '../../tagging/index.js';
4142
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
@@ -329,7 +329,7 @@ export class EmbeddedWallet extends BaseWallet {
329329
opts: SimulateViaEntrypointOptions,
330330
): Promise<TxSimulationResultWithAppOffset> {
331331
const { from, feeOptions, additionalScopes, skipTxValidation, skipFeeEnforcement, sendMessagesAs } = opts;
332-
const scopes = this.scopesFrom(from, additionalScopes);
332+
const scopes = this.scopesFrom(from, additionalScopes ?? [], sendMessagesAs);
333333

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

0 commit comments

Comments
 (0)