diff --git a/noir-projects/aztec-nr/aztec/src/oracle/capsules.nr b/noir-projects/aztec-nr/aztec/src/oracle/capsules.nr index 93b3b07d67b5..c54db467c9af 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/capsules.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/capsules.nr @@ -7,7 +7,8 @@ where T: Serialize, { let serialized = value.serialize(); - store_oracle(contract_address, slot, serialized); + // TODO(@mverzilli): bubble scope param up, for now none=global scope + store_oracle(contract_address, slot, serialized, Option::none()); } /// Returns data previously stored via `storeCapsule` in the per-contract non-volatile database. Returns Option::none() @@ -16,13 +17,20 @@ pub unconstrained fn load(contract_address: AztecAddress, slot: Field) -> Opt where T: Deserialize, { - let serialized_option = load_oracle(contract_address, slot, ::N); + // TODO(@mverzilli): bubble scope param up, for now none=global scope + let serialized_option = load_oracle( + contract_address, + slot, + ::N, + Option::none(), + ); serialized_option.map(|arr| Deserialize::deserialize(arr)) } /// Deletes data in the per-contract non-volatile database. Does nothing if no data was present. pub unconstrained fn delete(contract_address: AztecAddress, slot: Field) { - delete_oracle(contract_address, slot); + // TODO(@mverzilli): bubble scope param up, for now none=global scope + delete_oracle(contract_address, slot, Option::none()); } /// Copies a number of contiguous entries in the per-contract non-volatile database. This allows for efficient data @@ -30,11 +38,23 @@ pub unconstrained fn delete(contract_address: AztecAddress, slot: Field) { /// destination regions (which will result in the overlapped source values being overwritten). All copied slots must /// exist in the database (i.e. have been stored and not deleted) pub unconstrained fn copy(contract_address: AztecAddress, src_slot: Field, dst_slot: Field, num_entries: u32) { - copy_oracle(contract_address, src_slot, dst_slot, num_entries); + // TODO(@mverzilli): bubble scope param up, for now none=global scope + copy_oracle( + contract_address, + src_slot, + dst_slot, + num_entries, + Option::none(), + ); } #[oracle(aztec_utl_storeCapsule)] -unconstrained fn store_oracle(contract_address: AztecAddress, slot: Field, values: [Field; N]) {} +unconstrained fn store_oracle( + contract_address: AztecAddress, + slot: Field, + values: [Field; N], + scope: Option, +) {} /// We need to pass in `array_len` (the value of N) as a parameter to tell the oracle how many fields the response must /// have. @@ -48,13 +68,20 @@ unconstrained fn load_oracle( contract_address: AztecAddress, slot: Field, array_len: u32, + scope: Option, ) -> Option<[Field; N]> {} #[oracle(aztec_utl_deleteCapsule)] -unconstrained fn delete_oracle(contract_address: AztecAddress, slot: Field) {} +unconstrained fn delete_oracle(contract_address: AztecAddress, slot: Field, scope: Option) {} #[oracle(aztec_utl_copyCapsule)] -unconstrained fn copy_oracle(contract_address: AztecAddress, src_slot: Field, dst_slot: Field, num_entries: u32) {} +unconstrained fn copy_oracle( + contract_address: AztecAddress, + src_slot: Field, + dst_slot: Field, + num_entries: u32, + scope: Option, +) {} mod test { // These tests are sort of redundant since we already test the oracle implementation directly in TypeScript, but diff --git a/noir-projects/aztec-nr/aztec/src/oracle/version.nr b/noir-projects/aztec-nr/aztec/src/oracle/version.nr index f16367db9d0a..db1f5d94ef02 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/version.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/version.nr @@ -4,7 +4,7 @@ /// /// @dev Whenever a contract function or Noir test is run, the `aztec_utl_assertCompatibleOracleVersion` oracle is /// called and if the oracle version is incompatible an error is thrown. -pub global ORACLE_VERSION: Field = 16; +pub global ORACLE_VERSION: Field = 17; /// Asserts that the version of the oracle is compatible with the version expected by the contract. pub fn assert_compatible_oracle_version() { diff --git a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/capsules.nr b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/capsules.nr index 762b984311b2..e8d8bdaa0b2e 100644 --- a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/capsules.nr +++ b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/capsules.nr @@ -7,7 +7,7 @@ where T: Serialize, { let serialized = value.serialize(); - store_oracle(contract_address, slot, serialized); + store_oracle(contract_address, slot, serialized, Option::none()); } /// Returns data previously stored via `storeCapsule` in the per-contract non-volatile database. Returns Option::none() @@ -16,13 +16,13 @@ pub unconstrained fn load(contract_address: AztecAddress, slot: Field) -> Opt where T: Deserialize, { - let serialized_option = load_oracle(contract_address, slot, ::N); + let serialized_option = load_oracle(contract_address, slot, ::N, Option::none()); serialized_option.map(|arr| Deserialize::deserialize(arr)) } /// Deletes data in the per-contract non-volatile database. Does nothing if no data was present. pub unconstrained fn delete(contract_address: AztecAddress, slot: Field) { - delete_oracle(contract_address, slot); + delete_oracle(contract_address, slot, Option::none()); } /// Copies a number of contiguous entries in the per-contract non-volatile database. This allows for efficient data @@ -35,7 +35,7 @@ pub unconstrained fn copy( dst_slot: Field, num_entries: u32, ) { - copy_oracle(contract_address, src_slot, dst_slot, num_entries); + copy_oracle(contract_address, src_slot, dst_slot, num_entries, Option::none()); } #[oracle(aztec_utl_storeCapsule)] @@ -43,6 +43,7 @@ unconstrained fn store_oracle( contract_address: AztecAddress, slot: Field, values: [Field; N], + scope: Option, ) {} /// We need to pass in `array_len` (the value of N) as a parameter to tell the oracle how many fields the response must @@ -57,10 +58,11 @@ unconstrained fn load_oracle( contract_address: AztecAddress, slot: Field, array_len: u32, + scope: Option, ) -> Option<[Field; N]> {} #[oracle(aztec_utl_deleteCapsule)] -unconstrained fn delete_oracle(contract_address: AztecAddress, slot: Field) {} +unconstrained fn delete_oracle(contract_address: AztecAddress, slot: Field, scope: Option) {} #[oracle(aztec_utl_copyCapsule)] unconstrained fn copy_oracle( @@ -68,4 +70,5 @@ unconstrained fn copy_oracle( src_slot: Field, dst_slot: Field, num_entries: u32, + scope: Option, ) {} diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts index e468114f2b4b..3ae2a1341b64 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts @@ -137,10 +137,16 @@ export interface IUtilityExecutionOracle { messageContextRequestsArrayBaseSlot: Fr, messageContextResponsesArrayBaseSlot: Fr, ): Promise; - storeCapsule(contractAddress: AztecAddress, key: Fr, capsule: Fr[]): Promise; - loadCapsule(contractAddress: AztecAddress, key: Fr): Promise; - deleteCapsule(contractAddress: AztecAddress, key: Fr): Promise; - copyCapsule(contractAddress: AztecAddress, srcKey: Fr, dstKey: Fr, numEntries: number): Promise; + storeCapsule(contractAddress: AztecAddress, key: Fr, capsule: Fr[], scope?: AztecAddress): void; + loadCapsule(contractAddress: AztecAddress, key: Fr, scope?: AztecAddress): Promise; + deleteCapsule(contractAddress: AztecAddress, key: Fr, scope?: AztecAddress): void; + copyCapsule( + contractAddress: AztecAddress, + srcKey: Fr, + dstKey: Fr, + numEntries: number, + scope?: AztecAddress, + ): Promise; aes128Decrypt(ciphertext: Buffer, iv: Buffer, symKey: Buffer): Promise; getSharedSecret(address: AztecAddress, ephPk: Point): Promise; emitOffchainEffect(data: Fr[]): Promise; diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts index d8c7297824db..a09dbf14e013 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts @@ -23,7 +23,10 @@ export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback { contractAddress: ACVMField[], slot: ACVMField[], tSize: ACVMField[], - ): Promise<(ACVMField | ACVMField[])[]> => oracle.aztec_utl_loadCapsule(contractAddress, slot, tSize), + ): Promise<(ACVMField | ACVMField[])[]> => + // Last two params represent an Option == None, which means the capsule is contract-global, + // matching legacy behavior. + oracle.aztec_utl_loadCapsule(contractAddress, slot, tSize, [new Fr(0).toString()], [new Fr(0).toString()]), privateStoreInExecutionCache: (values: ACVMField[], hash: ACVMField[]): Promise => oracle.aztec_prv_storeInExecutionCache(values, hash), privateLoadFromExecutionCache: (returnsHash: ACVMField[]): Promise => @@ -65,15 +68,30 @@ export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback { contractAddress: ACVMField[], slot: ACVMField[], capsule: ACVMField[], - ): Promise => oracle.aztec_utl_storeCapsule(contractAddress, slot, capsule), + ): Promise => + // Last two params represent an Option == None, which means the capsule is contract-global, + // matching legacy behavior. + oracle.aztec_utl_storeCapsule(contractAddress, slot, capsule, [new Fr(0).toString()], [new Fr(0).toString()]), utilityCopyCapsule: ( contractAddress: ACVMField[], srcSlot: ACVMField[], dstSlot: ACVMField[], numEntries: ACVMField[], - ): Promise => oracle.aztec_utl_copyCapsule(contractAddress, srcSlot, dstSlot, numEntries), + ): Promise => + // Last two params represent an Option == None, which means the capsule is contract-global, + // matching legacy behavior. + oracle.aztec_utl_copyCapsule( + contractAddress, + srcSlot, + dstSlot, + numEntries, + [new Fr(0).toString()], + [new Fr(0).toString()], + ), utilityDeleteCapsule: (contractAddress: ACVMField[], slot: ACVMField[]): Promise => - oracle.aztec_utl_deleteCapsule(contractAddress, slot), + // Last two params represent an Option == None, which means the capsule is contract-global, + // matching legacy behavior. + oracle.aztec_utl_deleteCapsule(contractAddress, slot, [new Fr(0).toString()], [new Fr(0).toString()]), utilityAes128Decrypt: ( ciphertextBVecStorage: ACVMField[], ciphertextLength: ACVMField[], diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts index 40ab39c54fa4..1d5318c39f74 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts @@ -25,6 +25,10 @@ export class UnavailableOracleError extends Error { } } +function optionalAddressFromAcvmFields(scopeSome: ACVMField, scopeValue: ACVMField) { + return Fr.fromString(scopeSome).toNumber() === 1 ? AztecAddress.fromField(Fr.fromString(scopeValue)) : undefined; +} + /** * A data source that has all the apis required by Aztec.nr. */ @@ -544,17 +548,21 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_utl_storeCapsule( + aztec_utl_storeCapsule( [contractAddress]: ACVMField[], [slot]: ACVMField[], capsule: ACVMField[], + [scopeSome]: ACVMField[], + [scopeValue]: ACVMField[], ): Promise { - await this.handlerAsUtility().storeCapsule( + const scope = optionalAddressFromAcvmFields(scopeSome, scopeValue); + this.handlerAsUtility().storeCapsule( AztecAddress.fromField(Fr.fromString(contractAddress)), Fr.fromString(slot), capsule.map(Fr.fromString), + scope, ); - return []; + return Promise.resolve([]); } // eslint-disable-next-line camelcase @@ -562,10 +570,14 @@ export class Oracle { [contractAddress]: ACVMField[], [slot]: ACVMField[], [tSize]: ACVMField[], + [scopeSome]: ACVMField[], + [scopeValue]: ACVMField[], ): Promise<(ACVMField | ACVMField[])[]> { + const scope = optionalAddressFromAcvmFields(scopeSome, scopeValue); const values = await this.handlerAsUtility().loadCapsule( AztecAddress.fromField(Fr.fromString(contractAddress)), Fr.fromString(slot), + scope, ); // We are going to return a Noir Option struct to represent the possibility of null values. Options are a struct @@ -580,12 +592,19 @@ export class Oracle { } // eslint-disable-next-line camelcase - async aztec_utl_deleteCapsule([contractAddress]: ACVMField[], [slot]: ACVMField[]): Promise { - await this.handlerAsUtility().deleteCapsule( + aztec_utl_deleteCapsule( + [contractAddress]: ACVMField[], + [slot]: ACVMField[], + [scopeSome]: ACVMField[], + [scopeValue]: ACVMField[], + ): Promise { + const scope = optionalAddressFromAcvmFields(scopeSome, scopeValue); + this.handlerAsUtility().deleteCapsule( AztecAddress.fromField(Fr.fromString(contractAddress)), Fr.fromString(slot), + scope, ); - return []; + return Promise.resolve([]); } // eslint-disable-next-line camelcase @@ -594,12 +613,16 @@ export class Oracle { [srcSlot]: ACVMField[], [dstSlot]: ACVMField[], [numEntries]: ACVMField[], + [scopeSome]: ACVMField[], + [scopeValue]: ACVMField[], ): Promise { + const scope = optionalAddressFromAcvmFields(scopeSome, scopeValue); await this.handlerAsUtility().copyCapsule( AztecAddress.fromField(Fr.fromString(contractAddress)), Fr.fromString(srcSlot), Fr.fromString(dstSlot), Fr.fromString(numEntries).toNumber(), + scope, ); return []; } diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts index 02ae9fa46ac0..849dc531a203 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts @@ -12,7 +12,7 @@ import type { AztecNode } from '@aztec/stdlib/interfaces/server'; import { deriveKeys } from '@aztec/stdlib/keys'; import { Note, NoteDao } from '@aztec/stdlib/note'; import { makeL2Tips } from '@aztec/stdlib/testing'; -import { BlockHeader, GlobalVariables, TxHash } from '@aztec/stdlib/tx'; +import { BlockHeader, Capsule, GlobalVariables, TxHash } from '@aztec/stdlib/tx'; import { mock } from 'jest-mock-extended'; import type { _MockProxy } from 'jest-mock-extended/lib/Mock.js'; @@ -245,6 +245,73 @@ describe('Utility Execution test suite', () => { }); }); + describe('capsules', () => { + it('forwards scope to the capsule store', async () => { + const scope = await AztecAddress.random(); + const slot = Fr.random(); + const srcSlot = Fr.random(); + const dstSlot = Fr.random(); + const capsule = [Fr.random()]; + + capsuleStore.loadCapsule.mockResolvedValueOnce(capsule); + + utilityExecutionOracle.storeCapsule(contractAddress, slot, capsule, scope); + await utilityExecutionOracle.loadCapsule(contractAddress, slot, scope); + utilityExecutionOracle.deleteCapsule(contractAddress, slot, scope); + await utilityExecutionOracle.copyCapsule(contractAddress, srcSlot, dstSlot, 1, scope); + + expect(capsuleStore.storeCapsule).toHaveBeenCalledWith(contractAddress, slot, capsule, 'test-job-id', scope); + expect(capsuleStore.loadCapsule).toHaveBeenCalledWith(contractAddress, slot, 'test-job-id', scope); + expect(capsuleStore.deleteCapsule).toHaveBeenCalledWith(contractAddress, slot, 'test-job-id', scope); + expect(capsuleStore.copyCapsule).toHaveBeenCalledWith( + contractAddress, + srcSlot, + dstSlot, + 1, + 'test-job-id', + scope, + ); + }); + + it('loads transient capsules by scope', async () => { + const scope = await AztecAddress.random(); + const slot = Fr.random(); + const transientGlobal = [Fr.random()]; + const transientScoped = [Fr.random()]; + const persisted = [Fr.random()]; + + utilityExecutionOracle = new UtilityExecutionOracle({ + contractAddress, + authWitnesses: [], + capsules: [ + new Capsule(contractAddress, slot, transientGlobal), + new Capsule(contractAddress, slot, transientScoped, scope), + ], + anchorBlockHeader, + contractStore, + noteStore, + keyStore, + addressStore, + aztecNode, + recipientTaggingStore, + senderAddressBookStore, + capsuleStore, + privateEventStore, + messageContextService, + jobId: 'test-job-id', + scopes: 'ALL_SCOPES', + }); + + capsuleStore.loadCapsule.mockResolvedValueOnce(persisted); + + expect(await utilityExecutionOracle.loadCapsule(contractAddress, slot)).toEqual(transientGlobal); + expect(await utilityExecutionOracle.loadCapsule(contractAddress, slot, AztecAddress.ZERO)).toEqual( + transientGlobal, + ); + expect(await utilityExecutionOracle.loadCapsule(contractAddress, slot, scope)).toEqual(transientScoped); + }); + }); + describe('utilityResolveMessageContexts', () => { const requestSlot = Fr.random(); const responseSlot = Fr.random(); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index ce774d4076d7..48a14a711198 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -613,42 +613,51 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra } } - public storeCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[]): Promise { + public storeCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[], scope?: AztecAddress): void { if (!contractAddress.equals(this.contractAddress)) { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - this.capsuleStore.storeCapsule(this.contractAddress, slot, capsule, this.jobId); - return Promise.resolve(); + this.capsuleStore.storeCapsule(contractAddress, slot, capsule, this.jobId, scope); } - public async loadCapsule(contractAddress: AztecAddress, slot: Fr): Promise { + public async loadCapsule(contractAddress: AztecAddress, slot: Fr, scope?: AztecAddress): Promise { if (!contractAddress.equals(this.contractAddress)) { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - return ( - // TODO(#12425): On the following line, the pertinent capsule gets overshadowed by the transient one. Tackle this. - this.capsules.find(c => c.contractAddress.equals(contractAddress) && c.storageSlot.equals(slot))?.data ?? - (await this.capsuleStore.loadCapsule(this.contractAddress, slot, this.jobId)) - ); + const normalizedScope = scope ?? AztecAddress.ZERO; + const maybeTransientCapsule = this.capsules.find( + c => + c.contractAddress.equals(contractAddress) && + c.storageSlot.equals(slot) && + (c.scope ?? AztecAddress.ZERO).equals(normalizedScope), + )?.data; + + // TODO(#12425): On the following line, the pertinent capsule gets overshadowed by the transient one. Tackle this. + return maybeTransientCapsule ?? (await this.capsuleStore.loadCapsule(contractAddress, slot, this.jobId, scope)); } - public deleteCapsule(contractAddress: AztecAddress, slot: Fr): Promise { + public deleteCapsule(contractAddress: AztecAddress, slot: Fr, scope?: AztecAddress): void { if (!contractAddress.equals(this.contractAddress)) { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - this.capsuleStore.deleteCapsule(this.contractAddress, slot, this.jobId); - return Promise.resolve(); + this.capsuleStore.deleteCapsule(contractAddress, slot, this.jobId, scope); } - public copyCapsule(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): Promise { + public copyCapsule( + contractAddress: AztecAddress, + srcSlot: Fr, + dstSlot: Fr, + numEntries: number, + scope?: AztecAddress, + ): Promise { if (!contractAddress.equals(this.contractAddress)) { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - return this.capsuleStore.copyCapsule(this.contractAddress, srcSlot, dstSlot, numEntries, this.jobId); + return this.capsuleStore.copyCapsule(contractAddress, srcSlot, dstSlot, numEntries, this.jobId, scope); } // TODO(#11849): consider replacing this oracle with a pure Noir implementation of aes decryption. diff --git a/yarn-project/pxe/src/oracle_version.ts b/yarn-project/pxe/src/oracle_version.ts index f3069e1bf869..9837031291c4 100644 --- a/yarn-project/pxe/src/oracle_version.ts +++ b/yarn-project/pxe/src/oracle_version.ts @@ -4,9 +4,9 @@ /// /// @dev Whenever a contract function or Noir test is run, the `aztec_utl_assertCompatibleOracleVersion` oracle is called /// and if the oracle version is incompatible an error is thrown. -export const ORACLE_VERSION = 16; +export const ORACLE_VERSION = 17; /// This hash is computed as by hashing the Oracle interface and it is used to detect when the Oracle interface changes, /// which in turn implies that you need to update the ORACLE_VERSION constant in this file and in /// `noir-projects/aztec-nr/aztec/src/oracle/version.nr`. -export const ORACLE_INTERFACE_HASH = '73ccb2a24bc9fe7514108be9ff98d7ca8734bc316fb7c1ec4329d1d32f412a55'; +export const ORACLE_INTERFACE_HASH = 'e3d3af6b1b843213d9789f82aa08e70732124f84d42378b4a19921763be1528b'; diff --git a/yarn-project/pxe/src/storage/capsule_store/capsule_store.test.ts b/yarn-project/pxe/src/storage/capsule_store/capsule_store.test.ts index 51217891caee..9f94338d6ee8 100644 --- a/yarn-project/pxe/src/storage/capsule_store/capsule_store.test.ts +++ b/yarn-project/pxe/src/storage/capsule_store/capsule_store.test.ts @@ -66,6 +66,31 @@ describe('capsule data provider', () => { expect(result2).toEqual(values2); }); + it('stores values for different scopes independently', async () => { + const scopeA = await AztecAddress.random(); + const scopeB = await AztecAddress.random(); + const slot = new Fr(1); + const valuesA = [new Fr(42)]; + const valuesB = [new Fr(100)]; + + capsuleStore.storeCapsule(contract, slot, valuesA, 'test', scopeA); + capsuleStore.storeCapsule(contract, slot, valuesB, 'test', scopeB); + + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scopeA)).toEqual(valuesA); + expect(await capsuleStore.loadCapsule(contract, slot, 'test', scopeB)).toEqual(valuesB); + expect(await capsuleStore.loadCapsule(contract, slot, 'test')).toBeNull(); + }); + + it('treats the global scope as the zero address', async () => { + const slot = new Fr(1); + const values = [new Fr(42)]; + + capsuleStore.storeCapsule(contract, slot, values, 'test'); + + expect(await capsuleStore.loadCapsule(contract, slot, 'test')).toEqual(values); + expect(await capsuleStore.loadCapsule(contract, slot, 'test', AztecAddress.ZERO)).toEqual(values); + }); + it('returns null for non-existent slots', async () => { const slot = Fr.random(); const result = await capsuleStore.loadCapsule(contract, slot, 'test'); @@ -176,6 +201,19 @@ describe('capsule data provider', () => { 'Attempted to copy empty slot', ); }); + + it('copies values within a scope only', async () => { + const scope = await AztecAddress.random(); + const src = new Fr(1); + const dst = new Fr(5); + const values = [new Fr(42)]; + + capsuleStore.storeCapsule(contract, src, values, 'test', scope); + await capsuleStore.copyCapsule(contract, src, dst, 1, 'test', scope); + + expect(await capsuleStore.loadCapsule(contract, dst, 'test', scope)).toEqual(values); + expect(await capsuleStore.loadCapsule(contract, dst, 'test')).toBeNull(); + }); }); describe('arrays', () => { diff --git a/yarn-project/pxe/src/storage/capsule_store/capsule_store.ts b/yarn-project/pxe/src/storage/capsule_store/capsule_store.ts index 0b847c06df52..a9fbf9ce9073 100644 --- a/yarn-project/pxe/src/storage/capsule_store/capsule_store.ts +++ b/yarn-project/pxe/src/storage/capsule_store/capsule_store.ts @@ -1,7 +1,7 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { type Logger, createLogger } from '@aztec/foundation/log'; import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store'; -import type { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { StagedStore } from '../../job_coordinator/job_coordinator.js'; @@ -10,11 +10,12 @@ export class CapsuleStore implements StagedStore { #store: AztecAsyncKVStore; - // Arbitrary data stored by contracts. Key is computed as `${contractAddress}:${key}` + // Arbitrary data stored by contracts. Key is computed as `${contractAddress}:${scope}:${key}`, using the zero + // address for the global scope. #capsules: AztecAsyncMap; - // jobId => `${contractAddress}:${key}` => capsule data - // when `#stagedCapsules.get('some-job-id').get('${some-contract-address:some-key') === null`, + // jobId => `${contractAddress}:${scope}:${key}` => capsule data + // when `#stagedCapsules.get('some-job-id').get('${some-contract-address}:${some-scope}:${some-key}') === null`, // it signals that the capsule was deleted during the job, so it needs to be deleted on commit #stagedCapsules: Map>; @@ -134,8 +135,8 @@ export class CapsuleStore implements StagedStore { * to public contract storage in that it's indexed by the contract address and storage slot but instead of the global * network state it's backed by local PXE db. */ - storeCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[], jobId: string) { - const dbSlotKey = dbSlotToKey(contractAddress, slot); + storeCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[], jobId: string, scope?: AztecAddress) { + const dbSlotKey = dbSlotToKey(contractAddress, slot, scope); // A store overrides any pre-existing data on the slot this.#setOnStage(jobId, dbSlotKey, Buffer.concat(capsule.map(value => value.toBuffer()))); @@ -147,8 +148,13 @@ export class CapsuleStore implements StagedStore { * @param slot - The slot in the database to read. * @returns The stored data or `null` if no data is stored under the slot. */ - async loadCapsule(contractAddress: AztecAddress, slot: Fr, jobId: string): Promise { - const dataBuffer = await this.#getFromStage(jobId, dbSlotToKey(contractAddress, slot)); + async loadCapsule( + contractAddress: AztecAddress, + slot: Fr, + jobId: string, + scope?: AztecAddress, + ): Promise { + const dataBuffer = await this.#getFromStage(jobId, dbSlotToKey(contractAddress, slot, scope)); if (!dataBuffer) { this.logger.trace(`Data not found for contract ${contractAddress.toString()} and slot ${slot.toString()}`); return null; @@ -165,9 +171,9 @@ export class CapsuleStore implements StagedStore { * @param contractAddress - The contract address under which the data is scoped. * @param slot - The slot in the database to delete. */ - deleteCapsule(contractAddress: AztecAddress, slot: Fr, jobId: string) { + deleteCapsule(contractAddress: AztecAddress, slot: Fr, jobId: string, scope?: AztecAddress) { // When we commit this, we will interpret null as a deletion, so we'll propagate the delete to the KV store - this.#deleteOnStage(jobId, dbSlotToKey(contractAddress, slot)); + this.#deleteOnStage(jobId, dbSlotToKey(contractAddress, slot, scope)); } /** @@ -187,6 +193,7 @@ export class CapsuleStore implements StagedStore { dstSlot: Fr, numEntries: number, jobId: string, + scope?: AztecAddress, ): Promise { // This transactional context gives us "copy atomicity": // there shouldn't be concurrent writes to what's being copied here. @@ -203,8 +210,8 @@ export class CapsuleStore implements StagedStore { } for (const i of indexes) { - const currentSrcSlot = dbSlotToKey(contractAddress, srcSlot.add(new Fr(i))); - const currentDstSlot = dbSlotToKey(contractAddress, dstSlot.add(new Fr(i))); + const currentSrcSlot = dbSlotToKey(contractAddress, srcSlot.add(new Fr(i)), scope); + const currentDstSlot = dbSlotToKey(contractAddress, dstSlot.add(new Fr(i)), scope); const toCopy = await this.#getFromStage(jobId, currentSrcSlot); if (!toCopy) { @@ -306,8 +313,8 @@ export class CapsuleStore implements StagedStore { } } -function dbSlotToKey(contractAddress: AztecAddress, slot: Fr): string { - return `${contractAddress.toString()}:${slot.toString()}`; +function dbSlotToKey(contractAddress: AztecAddress, slot: Fr, scope?: AztecAddress): string { + return [contractAddress.toString(), (scope ?? AztecAddress.ZERO).toString(), slot.toString()].join(':'); } function arraySlot(baseSlot: Fr, index: number) { diff --git a/yarn-project/pxe/src/storage/metadata.ts b/yarn-project/pxe/src/storage/metadata.ts index 826f90735b91..aa63404894b5 100644 --- a/yarn-project/pxe/src/storage/metadata.ts +++ b/yarn-project/pxe/src/storage/metadata.ts @@ -1 +1 @@ -export const PXE_DATA_SCHEMA_VERSION = 4; +export const PXE_DATA_SCHEMA_VERSION = 5; diff --git a/yarn-project/stdlib/src/tx/capsule.ts b/yarn-project/stdlib/src/tx/capsule.ts index 8efeee24983f..eaff72c8c56a 100644 --- a/yarn-project/stdlib/src/tx/capsule.ts +++ b/yarn-project/stdlib/src/tx/capsule.ts @@ -19,6 +19,8 @@ export class Capsule { public readonly storageSlot: Fr, /** Data passed to the contract */ public readonly data: Fr[], + /** Optional namespace for the capsule contents */ + public readonly scope?: AztecAddress, ) {} static get schema() { @@ -30,12 +32,18 @@ export class Capsule { } toBuffer() { - return serializeToBuffer(this.contractAddress, this.storageSlot, new Vector(this.data)); + return this.scope + ? serializeToBuffer(this.contractAddress, this.storageSlot, new Vector(this.data), true, this.scope) + : serializeToBuffer(this.contractAddress, this.storageSlot, new Vector(this.data), false); } static fromBuffer(buffer: Buffer | BufferReader): Capsule { const reader = BufferReader.asReader(buffer); - return new Capsule(AztecAddress.fromBuffer(reader), Fr.fromBuffer(reader), reader.readVector(Fr)); + const contractAddress = AztecAddress.fromBuffer(reader); + const storageSlot = Fr.fromBuffer(reader); + const data = reader.readVector(Fr); + const hasScope = reader.readBoolean(); + return new Capsule(contractAddress, storageSlot, data, hasScope ? AztecAddress.fromBuffer(reader) : undefined); } toString() { diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index 1d60cd154451..d9a8e0745770 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -810,31 +810,61 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - async aztec_utl_storeCapsule( + aztec_utl_storeCapsule( foreignContractAddress: ForeignCallSingle, foreignSlot: ForeignCallSingle, foreignCapsule: ForeignCallArray, + ) { + return this.aztec_utl_storeCapsuleV2(foreignContractAddress, foreignSlot, foreignCapsule); + } + + // eslint-disable-next-line camelcase + aztec_utl_storeCapsuleV2( + foreignContractAddress: ForeignCallSingle, + foreignSlot: ForeignCallSingle, + foreignCapsule: ForeignCallArray, + foreignScopeIsSome?: ForeignCallSingle, + foreignScopeValue?: ForeignCallSingle, ) { const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); const slot = fromSingle(foreignSlot); const capsule = fromArray(foreignCapsule); + const scope = + foreignScopeIsSome && fromSingle(foreignScopeIsSome).toBool() + ? AztecAddress.fromField(fromSingle(foreignScopeValue!)) + : undefined; - await this.handlerAsUtility().storeCapsule(contractAddress, slot, capsule); + this.handlerAsUtility().storeCapsule(contractAddress, slot, capsule, scope); return toForeignCallResult([]); } // eslint-disable-next-line camelcase - async aztec_utl_loadCapsule( + aztec_utl_loadCapsule( + foreignContractAddress: ForeignCallSingle, + foreignSlot: ForeignCallSingle, + foreignTSize: ForeignCallSingle, + ) { + return this.aztec_utl_loadCapsuleV2(foreignContractAddress, foreignSlot, foreignTSize); + } + + // eslint-disable-next-line camelcase + async aztec_utl_loadCapsuleV2( foreignContractAddress: ForeignCallSingle, foreignSlot: ForeignCallSingle, foreignTSize: ForeignCallSingle, + foreignScopeIsSome?: ForeignCallSingle, + foreignScopeValue?: ForeignCallSingle, ) { const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); const slot = fromSingle(foreignSlot); const tSize = fromSingle(foreignTSize).toNumber(); + const scope = + foreignScopeIsSome && fromSingle(foreignScopeIsSome).toBool() + ? AztecAddress.fromField(fromSingle(foreignScopeValue!)) + : undefined; - const values = await this.handlerAsUtility().loadCapsule(contractAddress, slot); + const values = await this.handlerAsUtility().loadCapsule(contractAddress, slot, scope); // We are going to return a Noir Option struct to represent the possibility of null values. Options are a struct // with two fields: `some` (a boolean) and `value` (a field array in this case). @@ -848,28 +878,58 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - async aztec_utl_deleteCapsule(foreignContractAddress: ForeignCallSingle, foreignSlot: ForeignCallSingle) { + aztec_utl_deleteCapsule(foreignContractAddress: ForeignCallSingle, foreignSlot: ForeignCallSingle) { + return this.aztec_utl_deleteCapsuleV2(foreignContractAddress, foreignSlot); + } + + // eslint-disable-next-line camelcase + aztec_utl_deleteCapsuleV2( + foreignContractAddress: ForeignCallSingle, + foreignSlot: ForeignCallSingle, + foreignScopeIsSome?: ForeignCallSingle, + foreignScopeValue?: ForeignCallSingle, + ) { const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); const slot = fromSingle(foreignSlot); + const scope = + foreignScopeIsSome && fromSingle(foreignScopeIsSome).toBool() + ? AztecAddress.fromField(fromSingle(foreignScopeValue!)) + : undefined; - await this.handlerAsUtility().deleteCapsule(contractAddress, slot); + this.handlerAsUtility().deleteCapsule(contractAddress, slot, scope); return toForeignCallResult([]); } // eslint-disable-next-line camelcase - async aztec_utl_copyCapsule( + aztec_utl_copyCapsule( + foreignContractAddress: ForeignCallSingle, + foreignSrcSlot: ForeignCallSingle, + foreignDstSlot: ForeignCallSingle, + foreignNumEntries: ForeignCallSingle, + ) { + return this.aztec_utl_copyCapsuleV2(foreignContractAddress, foreignSrcSlot, foreignDstSlot, foreignNumEntries); + } + + // eslint-disable-next-line camelcase + async aztec_utl_copyCapsuleV2( foreignContractAddress: ForeignCallSingle, foreignSrcSlot: ForeignCallSingle, foreignDstSlot: ForeignCallSingle, foreignNumEntries: ForeignCallSingle, + foreignScopeIsSome?: ForeignCallSingle, + foreignScopeValue?: ForeignCallSingle, ) { const contractAddress = AztecAddress.fromField(fromSingle(foreignContractAddress)); const srcSlot = fromSingle(foreignSrcSlot); const dstSlot = fromSingle(foreignDstSlot); const numEntries = fromSingle(foreignNumEntries).toNumber(); + const scope = + foreignScopeIsSome && fromSingle(foreignScopeIsSome).toBool() + ? AztecAddress.fromField(fromSingle(foreignScopeValue!)) + : undefined; - await this.handlerAsUtility().copyCapsule(contractAddress, srcSlot, dstSlot, numEntries); + await this.handlerAsUtility().copyCapsule(contractAddress, srcSlot, dstSlot, numEntries, scope); return toForeignCallResult([]); }