From b5d51ebe8c1c9b0f4104f8f04995018bea2b701a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bene=C5=A1?= Date: Wed, 8 Jan 2025 09:30:07 -0600 Subject: [PATCH] feat: PXE db contract store (#10867) --- cspell.json | 3 + .../aztec-nr/aztec/src/note/note_interface.nr | 2 +- .../aztec-nr/aztec/src/oracle/mod.nr | 1 + .../aztec-nr/aztec/src/oracle/pxe_db.nr | 83 +++++++++++++++++++ .../contracts/test_contract/src/main.nr | 18 ++-- .../end-to-end/src/e2e_pxe_db.test.ts | 50 +++++++++++ .../pxe/src/database/kv_pxe_database.ts | 30 +++++++ yarn-project/pxe/src/database/pxe_database.ts | 18 ++++ .../src/database/pxe_database_test_suite.ts | 61 ++++++++++++++ .../pxe/src/simulator_oracle/index.ts | 22 +++++ yarn-project/simulator/src/acvm/acvm.ts | 21 ++++- .../simulator/src/acvm/oracle/oracle.ts | 34 ++++++++ .../simulator/src/acvm/oracle/typed_oracle.ts | 8 ++ .../simulator/src/client/db_oracle.ts | 20 ++++- .../simulator/src/client/view_data_oracle.ts | 20 +++++ yarn-project/txe/src/oracle/txe_oracle.ts | 39 ++++++++- .../txe/src/txe_service/txe_service.ts | 31 +++++++ 17 files changed, 450 insertions(+), 11 deletions(-) create mode 100644 noir-projects/aztec-nr/aztec/src/oracle/pxe_db.nr create mode 100644 yarn-project/end-to-end/src/e2e_pxe_db.test.ts diff --git a/cspell.json b/cspell.json index 9acd495666a..69fe7cf7dce 100644 --- a/cspell.json +++ b/cspell.json @@ -77,6 +77,7 @@ "demonomorphizes", "demonomorphizing", "deregistration", + "desynchronization", "devex", "devnet", "devs", @@ -176,6 +177,7 @@ "noirc", "noirup", "nullifer", + "Nullifiable", "offchain", "onchain", "opentelemetry", @@ -276,6 +278,7 @@ "unexclude", "unexcluded", "unfinalised", + "unnullify", "unprefixed", "unshift", "unshifted", diff --git a/noir-projects/aztec-nr/aztec/src/note/note_interface.nr b/noir-projects/aztec-nr/aztec/src/note/note_interface.nr index 9e96ac4edb8..d3f15cced88 100644 --- a/noir-projects/aztec-nr/aztec/src/note/note_interface.nr +++ b/noir-projects/aztec-nr/aztec/src/note/note_interface.nr @@ -1,6 +1,6 @@ use crate::context::PrivateContext; use crate::note::note_header::NoteHeader; -use dep::protocol_types::traits::{Empty, Serialize}; +use dep::protocol_types::traits::Empty; pub trait NoteProperties { fn properties() -> T; diff --git a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr index 7fbf9f64b8b..8e9d8e1399e 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr @@ -19,6 +19,7 @@ pub mod block_header; pub mod notes; pub mod storage; pub mod logs; +pub mod pxe_db; pub mod returns; // debug_log oracle is used by both noir-protocol-circuits and this crate and for this reason we just re-export it diff --git a/noir-projects/aztec-nr/aztec/src/oracle/pxe_db.nr b/noir-projects/aztec-nr/aztec/src/oracle/pxe_db.nr new file mode 100644 index 00000000000..0bcf6fe5a9e --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/oracle/pxe_db.nr @@ -0,0 +1,83 @@ +use protocol_types::{address::AztecAddress, traits::{Deserialize, Serialize}}; + +#[oracle(store)] +unconstrained fn store_oracle( + contract_address: AztecAddress, + key: Field, + values: [Field; N], +) {} + +/// Store a value of type T that implements Serialize in local PXE database. The data is scoped to the current +/// contract. If the data under the key already exists, it is overwritten. +pub unconstrained fn store(contract_address: AztecAddress, key: Field, value: T) +where + T: Serialize, +{ + let serialized = value.serialize(); + store_oracle(contract_address, key, serialized); +} + +/// Load data from local PXE database. We pass in `t_size` as a parameter to have the information of how many fields +/// we need to pad if the key does not exist (note that the actual response size is `t_size + 1` as the Option prefixes +/// the response with a boolean indicating if the data exists). +/// +/// Note that we need to return an Option<[Field; N]> as we cannot return an Option directly. This is because then +/// the shape of T would affect the expected oracle response (e.g. if we were returning a struct of 3 u32 values +/// then the expected response shape would be 3 single items. If instead we had a struct containing +/// `u32, [Field;10], u32`, then the expected shape would be single, array, single.). +#[oracle(load)] +unconstrained fn load_oracle( + contract_address: AztecAddress, + key: Field, + t_size: u32, +) -> Option<[Field; N]> {} + +/// Load a value of type T that implements Deserialize from local PXE database. The data is scoped to the current +/// contract. If the key does not exist, Option::none() is returned. +pub unconstrained fn load(contract_address: AztecAddress, key: Field) -> Option +where + T: Deserialize, +{ + let serialized_option = load_oracle::(contract_address, key, N); + serialized_option.map(|arr| Deserialize::deserialize(arr)) +} + +mod test { + use crate::{ + oracle::{pxe_db::{load, store}, random::random}, + test::{helpers::test_environment::TestEnvironment, mocks::mock_struct::MockStruct}, + }; + + #[test] + unconstrained fn stores_loads_and_overwrites_data() { + let env = TestEnvironment::new(); + + let contract_address = env.contract_address(); + let key = random(); + let value = MockStruct::new(5, 6); + store(contract_address, key, value); + + let loaded_value: MockStruct = load(contract_address, key).unwrap(); + + assert(loaded_value == value, "Stored and loaded values should be equal"); + + // Now we test that the value gets overwritten correctly. + let new_value = MockStruct::new(7, 8); + store(contract_address, key, new_value); + + let loaded_value: MockStruct = load(contract_address, key).unwrap(); + + assert(loaded_value == new_value, "Stored and loaded values should be equal"); + } + + #[test] + unconstrained fn load_non_existent_key() { + let env = TestEnvironment::new(); + + let contract_address = env.contract_address(); + let key = random(); + let loaded_value: Option = load(contract_address, key); + + assert(loaded_value == Option::none(), "Value should not exist"); + } +} diff --git a/noir-projects/noir-contracts/contracts/test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test_contract/src/main.nr index ed3918d1f68..e3394847bf2 100644 --- a/noir-projects/noir-contracts/contracts/test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test_contract/src/main.nr @@ -32,7 +32,8 @@ contract Test { note_getter::{get_notes, view_notes}, note_getter_options::NoteStatus, }, - oracle::random::random, + oracle::pxe_db, + test::mocks::mock_struct::MockStruct, utils::comparison::Comparator, }; use dep::token_portal_content_hash_lib::{ @@ -458,16 +459,21 @@ contract Test { constant.value } + unconstrained fn store_in_pxe_db(key: Field, arbitrary_struct: MockStruct) { + pxe_db::store(context.this_address(), key, arbitrary_struct); + } + + unconstrained fn load_from_pxe_db(key: Field) -> pub [Field; 2] { + let maybe_arbitrary_struct: Option = pxe_db::load(context.this_address(), key); + let arbitrary_struct = maybe_arbitrary_struct.unwrap_or(MockStruct::new(0, 0)); + arbitrary_struct.serialize() + } + #[private] fn test_nullifier_key_freshness(address: AztecAddress, public_nullifying_key: Point) { assert_eq(get_public_keys(address).npk_m.inner, public_nullifying_key); } - // Purely exists for testing - unconstrained fn get_random(kinda_seed: Field) -> pub Field { - kinda_seed * random() - } - pub struct DummyNote { amount: Field, secret_hash: Field, diff --git a/yarn-project/end-to-end/src/e2e_pxe_db.test.ts b/yarn-project/end-to-end/src/e2e_pxe_db.test.ts new file mode 100644 index 00000000000..8c8430085c7 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_pxe_db.test.ts @@ -0,0 +1,50 @@ +import { Fr, type Wallet } from '@aztec/aztec.js'; +import { TestContract } from '@aztec/noir-contracts.js/Test'; + +import { jest } from '@jest/globals'; + +import { setup } from './fixtures/utils.js'; + +const TIMEOUT = 120_000; + +// TODO(#10724): Nuke this once the linked issue is implemented (then the code will be well-tested). There is also +// a TXE test in `pxe_db.nr` but I decided to keep this ugly test around as it tests the PXE oracle callback handler +// (which is not tested by the TXE test). Dont't forget to remove `store_in_pxe_db` and `load_from_pxe_db` from +// the test contract when removing this test. +describe('PXE db', () => { + jest.setTimeout(TIMEOUT); + + let teardown: () => Promise; + + let testContract: TestContract; + + beforeAll(async () => { + let wallet: Wallet; + ({ teardown, wallet } = await setup(1)); + testContract = await TestContract.deploy(wallet).send().deployed(); + }); + + afterAll(() => teardown()); + + it('stores and loads data', async () => { + // In this test we feed arbitrary struct to a test contract, the test contract stores it in the PXE db and then + // we load it back. + const arbitraryStruct = { + a: Fr.random(), + b: Fr.random(), + }; + + const key = 6n; + await testContract.methods.store_in_pxe_db(key, arbitraryStruct).simulate(); + + // Now we try to load the data back from the PXE db. + const expectedReturnValue = [arbitraryStruct.a, arbitraryStruct.b].map(v => v.toBigInt()); + expect(await testContract.methods.load_from_pxe_db(key).simulate()).toEqual(expectedReturnValue); + }); + + it('handles non-existent data', async () => { + // In this test we try to load a key from the PXE db that does not exist. We should get an array of zeros. + const key = 7n; + expect(await testContract.methods.load_from_pxe_db(key).simulate()).toEqual([0n, 0n]); + }); +}); diff --git a/yarn-project/pxe/src/database/kv_pxe_database.ts b/yarn-project/pxe/src/database/kv_pxe_database.ts index ccff64cb229..4c73644ca97 100644 --- a/yarn-project/pxe/src/database/kv_pxe_database.ts +++ b/yarn-project/pxe/src/database/kv_pxe_database.ts @@ -12,6 +12,7 @@ import { type ContractArtifact, FunctionSelector, FunctionType } from '@aztec/fo import { toBufferBE } from '@aztec/foundation/bigint-buffer'; import { Fr } from '@aztec/foundation/fields'; import { toArray } from '@aztec/foundation/iterable'; +import { type LogFn, createDebugOnlyLogger } from '@aztec/foundation/log'; import { type AztecAsyncArray, type AztecAsyncKVStore, @@ -63,6 +64,11 @@ export class KVPxeDatabase implements PxeDatabase { #taggingSecretIndexesForSenders: AztecAsyncMap; #taggingSecretIndexesForRecipients: AztecAsyncMap; + // Arbitrary data stored by contracts. Key is computed as `${contractAddress}:${key}` + #contractStore: AztecAsyncMap; + + debug: LogFn; + protected constructor(private db: AztecAsyncKVStore) { this.#db = db; @@ -100,6 +106,10 @@ export class KVPxeDatabase implements PxeDatabase { this.#taggingSecretIndexesForSenders = db.openMap('tagging_secret_indexes_for_senders'); this.#taggingSecretIndexesForRecipients = db.openMap('tagging_secret_indexes_for_recipients'); + + this.#contractStore = db.openMap('contract_store'); + + this.debug = createDebugOnlyLogger('aztec:kv-pxe-database'); } public static async create(db: AztecAsyncKVStore): Promise { @@ -611,4 +621,24 @@ export class KVPxeDatabase implements PxeDatabase { await Promise.all(senders.map(sender => this.#taggingSecretIndexesForSenders.delete(sender))); }); } + + async store(contract: AztecAddress, key: Fr, values: Fr[]): Promise { + const dataKey = `${contract.toString()}:${key.toString()}`; + const dataBuffer = Buffer.concat(values.map(value => value.toBuffer())); + await this.#contractStore.set(dataKey, dataBuffer); + } + + async load(contract: AztecAddress, key: Fr): Promise { + const dataKey = `${contract.toString()}:${key.toString()}`; + const dataBuffer = await this.#contractStore.getAsync(dataKey); + if (!dataBuffer) { + this.debug(`Data not found for contract ${contract.toString()} and key ${key.toString()}`); + return null; + } + const values: Fr[] = []; + for (let i = 0; i < dataBuffer.length; i += Fr.SIZE_IN_BYTES) { + values.push(Fr.fromBuffer(dataBuffer.subarray(i, i + Fr.SIZE_IN_BYTES))); + } + return values; + } } diff --git a/yarn-project/pxe/src/database/pxe_database.ts b/yarn-project/pxe/src/database/pxe_database.ts index 8a8b2f8cd61..cc8496aaadf 100644 --- a/yarn-project/pxe/src/database/pxe_database.ts +++ b/yarn-project/pxe/src/database/pxe_database.ts @@ -213,4 +213,22 @@ export interface PxeDatabase extends ContractArtifactDatabase, ContractInstanceD * is also required to deal with chain reorgs. */ resetNoteSyncData(): Promise; + + /** + * Used by contracts during execution to store arbitrary data in the local PXE database. The data is siloed/scoped + * to a specific `contract`. + * @param contract - An address of a contract that is requesting to store the data. + * @param key - A field element representing the key to store the data under. + * @param values - An array of field elements representing the data to store. + */ + store(contract: AztecAddress, key: Fr, values: Fr[]): Promise; + + /** + * Used by contracts during execution to load arbitrary data from the local PXE database. The data is siloed/scoped + * to a specific `contract`. + * @param contract - An address of a contract that is requesting to load the data. + * @param key - A field element representing the key under which to load the data.. + * @returns An array of field elements representing the stored data or `null` if no data is stored under the key. + */ + load(contract: AztecAddress, key: Fr): Promise; } diff --git a/yarn-project/pxe/src/database/pxe_database_test_suite.ts b/yarn-project/pxe/src/database/pxe_database_test_suite.ts index 78c7cf2f110..1a1a586c78d 100644 --- a/yarn-project/pxe/src/database/pxe_database_test_suite.ts +++ b/yarn-project/pxe/src/database/pxe_database_test_suite.ts @@ -405,5 +405,66 @@ export function describePxeDatabase(getDatabase: () => PxeDatabase) { await expect(database.getContractInstance(address)).resolves.toEqual(instance); }); }); + + describe('contract store', () => { + let contract: AztecAddress; + + beforeEach(() => { + // Setup mock contract address + contract = AztecAddress.random(); + }); + + it('stores and loads a single value', async () => { + const key = new Fr(1); + const values = [new Fr(42)]; + + await database.store(contract, key, values); + const result = await database.load(contract, key); + expect(result).toEqual(values); + }); + + it('stores and loads multiple values', async () => { + const key = new Fr(1); + const values = [new Fr(42), new Fr(43), new Fr(44)]; + + await database.store(contract, key, values); + const result = await database.load(contract, key); + expect(result).toEqual(values); + }); + + it('overwrites existing values', async () => { + const key = new Fr(1); + const initialValues = [new Fr(42)]; + const newValues = [new Fr(100)]; + + await database.store(contract, key, initialValues); + await database.store(contract, key, newValues); + + const result = await database.load(contract, key); + expect(result).toEqual(newValues); + }); + + it('stores values for different contracts independently', async () => { + const anotherContract = AztecAddress.random(); + const key = new Fr(1); + const values1 = [new Fr(42)]; + const values2 = [new Fr(100)]; + + await database.store(contract, key, values1); + await database.store(anotherContract, key, values2); + + const result1 = await database.load(contract, key); + const result2 = await database.load(anotherContract, key); + + expect(result1).toEqual(values1); + expect(result2).toEqual(values2); + }); + + it('returns null for non-existent keys', async () => { + const key = Fr.random(); + const result = await database.load(contract, key); + expect(result).toBeNull(); + }); + }); }); } diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index 137b4137d95..61c578ec8ba 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -666,4 +666,26 @@ export class SimulatorOracle implements DBOracle { }); } } + + /** + * Used by contracts during execution to store arbitrary data in the local PXE database. The data is siloed/scoped + * to a specific `contract`. + * @param contract - An address of a contract that is requesting to store the data. + * @param key - A field element representing the key to store the data under. + * @param values - An array of field elements representing the data to store. + */ + store(contract: AztecAddress, key: Fr, values: Fr[]): Promise { + return this.db.store(contract, key, values); + } + + /** + * Used by contracts during execution to load arbitrary data from the local PXE database. The data is siloed/scoped + * to a specific `contract`. + * @param contract - An address of a contract that is requesting to load the data. + * @param key - A field element representing the key under which to load the data.. + * @returns An array of field elements representing the stored data or `null` if no data is stored under the key. + */ + load(contract: AztecAddress, key: Fr): Promise { + return this.db.load(contract, key); + } } diff --git a/yarn-project/simulator/src/acvm/acvm.ts b/yarn-project/simulator/src/acvm/acvm.ts index 600c4c7a8e5..7e0c18395cd 100644 --- a/yarn-project/simulator/src/acvm/acvm.ts +++ b/yarn-project/simulator/src/acvm/acvm.ts @@ -18,7 +18,15 @@ import { type ORACLE_NAMES } from './oracle/index.js'; */ type ACIRCallback = Record< ORACLE_NAMES, - (...args: ForeignCallInput[]) => void | Promise | ForeignCallOutput | Promise + ( + ...args: ForeignCallInput[] + ) => + | void + | Promise + | ForeignCallOutput + | ForeignCallOutput[] + | Promise + | Promise >; /** @@ -56,7 +64,16 @@ export async function acvm( } const result = await oracleFunction.call(callback, ...args); - return typeof result === 'undefined' ? [] : [result]; + + if (typeof result === 'undefined') { + return []; + } else if (result instanceof Array && !result.every(item => typeof item === 'string')) { + // We are dealing with a nested array which means that we do not need it wrap it in another array as to have + // the nested array structure it is already "wrapped". + return result; + } else { + return [result] as ForeignCallOutput[]; + } } catch (err) { let typedError: Error; if (err instanceof Error) { diff --git a/yarn-project/simulator/src/acvm/oracle/oracle.ts b/yarn-project/simulator/src/acvm/oracle/oracle.ts index 254b91fb3ab..9aa79e9a288 100644 --- a/yarn-project/simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/oracle.ts @@ -2,12 +2,15 @@ import { MerkleTreeId, UnencryptedL2Log } from '@aztec/circuit-types'; import { FunctionSelector, NoteSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr } from '@aztec/foundation/fields'; +import { createLogger } from '@aztec/foundation/log'; import { type ACVMField } from '../acvm_types.js'; import { frToBoolean, frToNumber, fromACVMField } from '../deserialize.js'; import { toACVMField } from '../serialize.js'; import { type TypedOracle } from './typed_oracle.js'; +const logger = createLogger('simulator:acvm:oracle'); + /** * A data source that has all the apis required by Aztec.nr. */ @@ -393,4 +396,35 @@ export class Oracle { async syncNotes() { await this.typedOracle.syncNotes(); } + + async store([contract]: ACVMField[], [key]: ACVMField[], values: ACVMField[]) { + const processedContract = AztecAddress.fromField(fromACVMField(contract)); + const processedKey = fromACVMField(key); + const processedValues = values.map(fromACVMField); + logger.debug(`Storing data for key ${processedKey} in contract ${processedContract}. Data: [${processedValues}]`); + await this.typedOracle.store(processedContract, processedKey, processedValues); + } + + /** + * Load data from pxe db. + * @param contract - The contract address. + * @param key - The key to load. + * @param tSize - The size of the serialized object to return. + * @returns The data found flag and the serialized object concatenated in one array. + */ + async load([contract]: ACVMField[], [key]: ACVMField[], [tSize]: ACVMField[]): Promise<(ACVMField | ACVMField[])[]> { + const processedContract = AztecAddress.fromField(fromACVMField(contract)); + const processedKey = fromACVMField(key); + const values = await this.typedOracle.load(processedContract, processedKey); + if (values === null) { + // No data was found so we set the data-found flag to 0 and we pad with zeros get the correct return size. + const processedTSize = frToNumber(fromACVMField(tSize)); + logger.debug(`No data found for key ${processedKey} in contract ${processedContract}`); + return [toACVMField(0), Array(processedTSize).fill(toACVMField(0))]; + } else { + // Data was found so we set the data-found flag to 1 and return it along with the data. + logger.debug(`Returning data for key ${processedKey} in contract ${processedContract}. Data: [${values}]`); + return [toACVMField(1), values.map(toACVMField)]; + } + } } diff --git a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts index d5c461bd3c2..21085bd6ab8 100644 --- a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts @@ -248,4 +248,12 @@ export abstract class TypedOracle { syncNotes(): Promise { throw new OracleMethodNotAvailableError('syncNotes'); } + + store(_contract: AztecAddress, _key: Fr, _values: Fr[]): Promise { + throw new OracleMethodNotAvailableError('store'); + } + + load(_contract: AztecAddress, _key: Fr): Promise { + throw new OracleMethodNotAvailableError('load'); + } } diff --git a/yarn-project/simulator/src/client/db_oracle.ts b/yarn-project/simulator/src/client/db_oracle.ts index d53022e49e2..65cd0108f38 100644 --- a/yarn-project/simulator/src/client/db_oracle.ts +++ b/yarn-project/simulator/src/client/db_oracle.ts @@ -224,7 +224,7 @@ export interface DBOracle extends CommitmentsDB { ): Promise; /** - * Synchronizes the logs tagged with the recipient's address and all the senders in the addressbook. + * Synchronizes the logs tagged with the recipient's address and all the senders in the address book. * Returns the unsynched logs and updates the indexes of the secrets used to tag them until there are no more logs to sync. * @param contractAddress - The address of the contract that the logs are tagged for * @param recipient - The address of the recipient @@ -247,4 +247,22 @@ export interface DBOracle extends CommitmentsDB { * Removes all of a contract's notes that have been nullified from the note database. */ removeNullifiedNotes(contractAddress: AztecAddress): Promise; + + /** + * Used by contracts during execution to store arbitrary data in the local PXE database. The data is siloed/scoped + * to a specific `contract`. + * @param contract - An address of a contract that is requesting to store the data. + * @param key - A field element representing the key to store the data under. + * @param values - An array of field elements representing the data to store. + */ + store(contract: AztecAddress, key: Fr, values: Fr[]): Promise; + + /** + * Used by contracts during execution to load arbitrary data from the local PXE database. The data is siloed/scoped + * to a specific `contract`. + * @param contract - An address of a contract that is requesting to load the data. + * @param key - A field element representing the key under which to load the data.. + * @returns An array of field elements representing the stored data or `null` if no data is stored under the key. + */ + load(contract: AztecAddress, key: Fr): Promise; } diff --git a/yarn-project/simulator/src/client/view_data_oracle.ts b/yarn-project/simulator/src/client/view_data_oracle.ts index 8f561371b4d..869ee86984e 100644 --- a/yarn-project/simulator/src/client/view_data_oracle.ts +++ b/yarn-project/simulator/src/client/view_data_oracle.ts @@ -325,4 +325,24 @@ export class ViewDataOracle extends TypedOracle { await this.db.removeNullifiedNotes(this.contractAddress); } + + public override store(contract: AztecAddress, key: Fr, values: Fr[]): Promise { + if (!contract.equals(this.contractAddress)) { + // TODO(#10727): instead of this check check that this.contractAddress is allowed to process notes for contract + throw new Error( + `Contract address ${contract} does not match the oracle's contract address ${this.contractAddress}`, + ); + } + return this.db.store(this.contractAddress, key, values); + } + + public override load(contract: AztecAddress, key: Fr): Promise { + if (!contract.equals(this.contractAddress)) { + // TODO(#10727): instead of this check check that this.contractAddress is allowed to process notes for contract + throw new Error( + `Contract address ${contract} does not match the oracle's contract address ${this.contractAddress}`, + ); + } + return this.db.load(this.contractAddress, key); + } } diff --git a/yarn-project/txe/src/oracle/txe_oracle.ts b/yarn-project/txe/src/oracle/txe_oracle.ts index 608263ea150..07965dbce20 100644 --- a/yarn-project/txe/src/oracle/txe_oracle.ts +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -58,7 +58,7 @@ import { import { AztecAddress } from '@aztec/foundation/aztec-address'; import { poseidon2Hash } from '@aztec/foundation/crypto'; import { Fr } from '@aztec/foundation/fields'; -import { type Logger, applyStringFormatting } from '@aztec/foundation/log'; +import { type LogFn, type Logger, applyStringFormatting, createDebugOnlyLogger } from '@aztec/foundation/log'; import { Timer } from '@aztec/foundation/timer'; import { type KeyStore } from '@aztec/key-store'; import { ContractDataOracle, SimulatorOracle, enrichPublicSimulationError } from '@aztec/pxe'; @@ -116,6 +116,8 @@ export class TXE implements TypedOracle { private node = new TXENode(this.blockNumber); + debug: LogFn; + constructor( private logger: Logger, private trees: MerkleTrees, @@ -129,6 +131,8 @@ export class TXE implements TypedOracle { // Default msg_sender (for entrypoints) is now Fr.max_value rather than 0 addr (see #7190 & #7404) this.msgSender = AztecAddress.fromField(Fr.MAX_FIELD_VALUE); this.simulatorOracle = new SimulatorOracle(this.contractDataOracle, txeDatabase, keyStore, this.node); + + this.debug = createDebugOnlyLogger('aztec:kv-pxe-database'); } // Utils @@ -1051,4 +1055,37 @@ export class TXE implements TypedOracle { return preimage.value; } + + /** + * Used by contracts during execution to store arbitrary data in the local PXE database. The data is siloed/scoped + * to a specific `contract`. + * @param contract - The contract address to store the data under. + * @param key - A field element representing the key to store the data under. + * @param values - An array of field elements representing the data to store. + */ + store(contract: AztecAddress, key: Fr, values: Fr[]): Promise { + if (!contract.equals(this.contractAddress)) { + // TODO(#10727): instead of this check check that this.contractAddress is allowed to process notes for contract + throw new Error( + `Contract address ${contract} does not match the oracle's contract address ${this.contractAddress}`, + ); + } + return this.txeDatabase.store(this.contractAddress, key, values); + } + + /** + * Used by contracts during execution to load arbitrary data from the local PXE database. The data is siloed/scoped + * to a specific `contract`. + * @param contract - The contract address to load the data from. + * @param key - A field element representing the key under which to load the data.. + * @returns An array of field elements representing the stored data or `null` if no data is stored under the key. + */ + load(contract: AztecAddress, key: Fr): Promise { + if (!contract.equals(this.contractAddress)) { + // TODO(#10727): instead of this check check that this.contractAddress is allowed to process notes for contract + this.debug(`Data not found for contract ${contract.toString()} and key ${key.toString()}`); + return Promise.resolve(null); + } + return this.txeDatabase.load(this.contractAddress, key); + } } diff --git a/yarn-project/txe/src/txe_service/txe_service.ts b/yarn-project/txe/src/txe_service/txe_service.ts index f372c0ac5ec..983f6f27df7 100644 --- a/yarn-project/txe/src/txe_service/txe_service.ts +++ b/yarn-project/txe/src/txe_service/txe_service.ts @@ -598,6 +598,37 @@ export class TXEService { return toForeignCallResult([]); } + async store(contract: ForeignCallSingle, key: ForeignCallSingle, values: ForeignCallArray) { + const processedContract = AztecAddress.fromField(fromSingle(contract)); + const processedKey = fromSingle(key); + const processedValues = fromArray(values); + await this.typedOracle.store(processedContract, processedKey, processedValues); + return toForeignCallResult([]); + } + + /** + * Load data from pxe db. + * @param contract - The contract address. + * @param key - The key to load. + * @param tSize - The size of the serialized object to return. + * @returns The data found flag and the serialized object concatenated in one array. + */ + async load(contract: ForeignCallSingle, key: ForeignCallSingle, tSize: ForeignCallSingle) { + const processedContract = AztecAddress.fromField(fromSingle(contract)); + const processedKey = fromSingle(key); + const values = await this.typedOracle.load(processedContract, processedKey); + // 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). + if (values === null) { + // No data was found so we set `some` to 0 and pad `value` with zeros get the correct return size. + const processedTSize = fromSingle(tSize).toNumber(); + return toForeignCallResult([toSingle(new Fr(0)), toArray(Array(processedTSize).fill(new Fr(0)))]); + } else { + // Data was found so we set `some` to 1 and return it along with `value`. + return toForeignCallResult([toSingle(new Fr(1)), toArray(values)]); + } + } + // AVM opcodes avmOpcodeEmitUnencryptedLog(_message: ForeignCallArray) {