diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index beb2d4b81dc1..ebbb84faeb20 100644 --- a/noir-projects/aztec-nr/aztec/src/lib.nr +++ b/noir-projects/aztec-nr/aztec/src/lib.nr @@ -10,6 +10,7 @@ mod note; mod event; mod oracle; mod state_vars; +mod pxe_db; mod prelude; mod encrypted_logs; mod unencrypted_logs; diff --git a/noir-projects/aztec-nr/aztec/src/oracle/pxe_db.nr b/noir-projects/aztec-nr/aztec/src/oracle/pxe_db.nr index 0bcf6fe5a9e4..df96b00a9b07 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/pxe_db.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/pxe_db.nr @@ -1,83 +1,249 @@ 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) +/// Stores arbitrary information in a per-contract non-volatile database, which can later be retrieved with `load`. If +/// data was already stored at this slot, it is overwrriten. +pub unconstrained fn store(contract_address: AztecAddress, slot: Field, value: T) where T: Serialize, { let serialized = value.serialize(); - store_oracle(contract_address, key, serialized); + store_oracle(contract_address, slot, 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 +/// Returns data previously stored via `dbStore` in the per-contract non-volatile database. Returns Option::none() if +/// nothing was stored at the given slot. +pub unconstrained fn load(contract_address: AztecAddress, slot: Field) -> Option where T: Deserialize, { - let serialized_option = load_oracle::(contract_address, key, N); + let serialized_option = load_oracle::(contract_address, slot, N); 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); +} + +/// Copies a number of contiguous entries in the per-contract non-volatile database. This allows for efficient data +/// structures by avoiding repeated calls to `dbLoad` and `dbStore`. +/// Supports overlapping source and 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); +} + +#[oracle(dbStore)] +unconstrained fn store_oracle( + contract_address: AztecAddress, + slot: Field, + values: [Field; N], +) {} + +/// 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. +/// +/// Note that the oracle returns an Option<[Field; N]> because we cannot return an Option directly. That would +/// require for the oracle resolver to know the shape of T (e.g. if T were a struct of 3 u32 values then the expected +/// response shape would be 3 single items, whereas it were a struct containing `u32, [Field;10], u32` then the expected +/// shape would be single, array, single.). Instead, we return the serialization and deserialize in Noir. +#[oracle(dbLoad)] +unconstrained fn load_oracle( + contract_address: AztecAddress, + slot: Field, + array_len: u32, +) -> Option<[Field; N]> {} + +#[oracle(dbDelete)] +unconstrained fn delete_oracle(contract_address: AztecAddress, slot: Field) {} + +#[oracle(dbCopy)] +unconstrained fn copy_oracle( + contract_address: AztecAddress, + src_slot: Field, + dst_slot: Field, + num_entries: u32, +) {} + mod test { + // These tests are sort of redundant since we already test the oracle implementation directly in TypeScript, but + // they are cheap regardless and help ensure both that the TXE implementation works accordingly and that the Noir + // oracles are hooked up correctly. + use crate::{ - oracle::{pxe_db::{load, store}, random::random}, + oracle::pxe_db::{copy, delete, load, store}, test::{helpers::test_environment::TestEnvironment, mocks::mock_struct::MockStruct}, }; + use protocol_types::{address::AztecAddress, traits::{FromField, ToField}}; - #[test] - unconstrained fn stores_loads_and_overwrites_data() { + unconstrained fn setup() -> AztecAddress { let env = TestEnvironment::new(); + env.contract_address() + } + + global SLOT: Field = 1; + + #[test] + unconstrained fn stores_and_loads() { + let contract_address = setup(); - let contract_address = env.contract_address(); - let key = random(); let value = MockStruct::new(5, 6); - store(contract_address, key, value); + store(contract_address, SLOT, value); + + assert_eq(load(contract_address, SLOT).unwrap(), value); + } - let loaded_value: MockStruct = load(contract_address, key).unwrap(); + #[test] + unconstrained fn store_overwrites() { + let contract_address = setup(); - assert(loaded_value == value, "Stored and loaded values should be equal"); + let value = MockStruct::new(5, 6); + store(contract_address, SLOT, value); - // Now we test that the value gets overwritten correctly. let new_value = MockStruct::new(7, 8); - store(contract_address, key, new_value); + store(contract_address, SLOT, new_value); - let loaded_value: MockStruct = load(contract_address, key).unwrap(); + assert_eq(load(contract_address, SLOT).unwrap(), new_value); + } + + #[test] + unconstrained fn loads_empty_slot() { + let contract_address = setup(); - assert(loaded_value == new_value, "Stored and loaded values should be equal"); + let loaded_value: Option = load(contract_address, SLOT); + assert_eq(loaded_value, Option::none()); } #[test] - unconstrained fn load_non_existent_key() { - let env = TestEnvironment::new(); + unconstrained fn deletes_stored_value() { + let contract_address = setup(); + + let value = MockStruct::new(5, 6); + store(contract_address, SLOT, value); + delete(contract_address, SLOT); + + let loaded_value: Option = load(contract_address, SLOT); + assert_eq(loaded_value, Option::none()); + } + + #[test] + unconstrained fn deletes_empty_slot() { + let contract_address = setup(); + + delete(contract_address, SLOT); + let loaded_value: Option = load(contract_address, SLOT); + assert_eq(loaded_value, Option::none()); + } + + #[test] + unconstrained fn copies_non_overlapping_values() { + let contract_address = setup(); + + let src = 5; + + let values = [MockStruct::new(5, 6), MockStruct::new(7, 8), MockStruct::new(9, 10)]; + store(contract_address, src, values[0]); + store(contract_address, src + 1, values[1]); + store(contract_address, src + 2, values[2]); + + let dst = 10; + copy(contract_address, src, dst, 3); + + assert_eq(load(contract_address, dst).unwrap(), values[0]); + assert_eq(load(contract_address, dst + 1).unwrap(), values[1]); + assert_eq(load(contract_address, dst + 2).unwrap(), values[2]); + } + + #[test] + unconstrained fn copies_overlapping_values_with_src_ahead() { + let contract_address = setup(); + + let src = 1; + + let values = [MockStruct::new(5, 6), MockStruct::new(7, 8), MockStruct::new(9, 10)]; + store(contract_address, src, values[0]); + store(contract_address, src + 1, values[1]); + store(contract_address, src + 2, values[2]); + + let dst = 2; + copy(contract_address, src, dst, 3); + + assert_eq(load(contract_address, dst).unwrap(), values[0]); + assert_eq(load(contract_address, dst + 1).unwrap(), values[1]); + assert_eq(load(contract_address, dst + 2).unwrap(), values[2]); + + // src[1] and src[2] should have been overwritten since they are also dst[0] and dst[1] + assert_eq(load(contract_address, src).unwrap(), values[0]); // src[0] (unchanged) + assert_eq(load(contract_address, src + 1).unwrap(), values[0]); // dst[0] + assert_eq(load(contract_address, src + 2).unwrap(), values[1]); // dst[1] + } + + #[test] + unconstrained fn copies_overlapping_values_with_dst_ahead() { + let contract_address = setup(); + + let src = 2; + + let values = [MockStruct::new(5, 6), MockStruct::new(7, 8), MockStruct::new(9, 10)]; + store(contract_address, src, values[0]); + store(contract_address, src + 1, values[1]); + store(contract_address, src + 2, values[2]); + + let dst = 1; + copy(contract_address, src, dst, 3); + + assert_eq(load(contract_address, dst).unwrap(), values[0]); + assert_eq(load(contract_address, dst + 1).unwrap(), values[1]); + assert_eq(load(contract_address, dst + 2).unwrap(), values[2]); + + // src[0] and src[1] should have been overwritten since they are also dst[1] and dst[2] + assert_eq(load(contract_address, src).unwrap(), values[1]); // dst[1] + assert_eq(load(contract_address, src + 1).unwrap(), values[2]); // dst[2] + assert_eq(load(contract_address, src + 2).unwrap(), values[2]); // src[2] (unchanged) + } + + #[test(should_fail_with = "copy empty slot")] + unconstrained fn cannot_copy_empty_values() { + let contract_address = setup(); + + copy(contract_address, SLOT, SLOT, 1); + } + + #[test(should_fail_with = "not allowed to access")] + unconstrained fn cannot_store_other_contract() { + let contract_address = setup(); + let other_contract_address = AztecAddress::from_field(contract_address.to_field() + 1); + + let value = MockStruct::new(5, 6); + store(other_contract_address, SLOT, value); + } + + #[test(should_fail_with = "not allowed to access")] + unconstrained fn cannot_load_other_contract() { + let contract_address = setup(); + let other_contract_address = AztecAddress::from_field(contract_address.to_field() + 1); + + let _: Option = load(other_contract_address, SLOT); + } + + #[test(should_fail_with = "not allowed to access")] + unconstrained fn cannot_delete_other_contract() { + let contract_address = setup(); + let other_contract_address = AztecAddress::from_field(contract_address.to_field() + 1); + + delete(other_contract_address, SLOT); + } - let contract_address = env.contract_address(); - let key = random(); - let loaded_value: Option = load(contract_address, key); + #[test(should_fail_with = "not allowed to access")] + unconstrained fn cannot_copy_other_contract() { + let contract_address = setup(); + let other_contract_address = AztecAddress::from_field(contract_address.to_field() + 1); - assert(loaded_value == Option::none(), "Value should not exist"); + copy(other_contract_address, SLOT, SLOT, 0); } } diff --git a/noir-projects/aztec-nr/aztec/src/pxe_db/mod.nr b/noir-projects/aztec-nr/aztec/src/pxe_db/mod.nr new file mode 100644 index 000000000000..99a1cf6e040e --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/pxe_db/mod.nr @@ -0,0 +1,181 @@ +use crate::oracle::pxe_db; +use protocol_types::{address::AztecAddress, traits::{Deserialize, Serialize}}; + +/// A dynamically sized array backed by PXE's non-volatile database. Values are persisted until deleted, so they can be +/// e.g. stored during simulation of a transaction and later retrieved during witness generation. All values are scoped +/// per contract address, so external contracts cannot access them. +pub struct DBArray { + contract_address: AztecAddress, + /// An array's base slot is the slot in PXE's database in which the array length is stored. Array elements are + /// stored contiguously in the following slots, so e.g. if the base slot is 5, then the length is stored at slot 5, + /// the first element (index 0) at slot 6, the second (index 1) at slot 7, and so on. + base_slot: Field, +} + +impl DBArray +where + T: Serialize + Deserialize, +{ + /// Returns a DBArray connected to a contact's PXE database at a base database slot. Array elements are be stored at + /// contiguous slots following the base slot, so there should be sufficient space between array base slots to + /// accomodate elements. A reasonable strategy is to make the base slot a hash of a unique value. + pub unconstrained fn at(contract_address: AztecAddress, base_slot: Field) -> Self { + Self { contract_address, base_slot } + } + + /// Returns the number of elements stored in the array. + pub unconstrained fn len(self) -> u32 { + // An uninitialized array defaults to a length of 0. + pxe_db::load(self.contract_address, self.base_slot).unwrap_or(0) as u32 + } + + /// Stores a value at the end of the array. + pub unconstrained fn push(self, value: T) { + let current_length = self.len(); + + // The slot corresponding to the index `current_length` is the first slot immediately after the end of the + // array, which is where we want to place the new value. + pxe_db::store(self.contract_address, self.slot_at(current_length), value); + + // Then we simply update the length. + let new_length = current_length + 1; + pxe_db::store(self.contract_address, self.base_slot, new_length); + } + + /// Retrieves the value stored in the array at `index`. Throws if the index is out of bounds. + pub unconstrained fn get(self, index: u32) -> T { + assert(index < self.len(), "Attempted to read past the length of a DBArray"); + + pxe_db::load(self.contract_address, self.slot_at(index)).unwrap() + } + + /// Deletes the value stored in the array at `index`. Throws if the index is out of bounds. + pub unconstrained fn remove(self, index: u32) { + let current_length = self.len(); + assert(index < current_length, "Attempted to delete past the length of a DBArray"); + + // In order to be able to remove elements at arbitrary indices, we need to shift the entire contents of the + // array past the removed element one slot backward so that we don't end up with a gap and preserve the + // contiguous slots. We can skip this when deleting the last element however. + if index != current_length - 1 { + // The souce and destination regions overlap, but `copy` supports this. + pxe_db::copy( + self.contract_address, + self.slot_at(index + 1), + self.slot_at(index), + current_length - index - 1, + ); + } + + // We can now delete the last element (which has either been copied to the slot immediately before it, or was + // the element we meant to delete in the first place) and update the length. + pxe_db::delete(self.contract_address, self.slot_at(current_length - 1)); + pxe_db::store(self.contract_address, self.base_slot, current_length - 1); + } + + unconstrained fn slot_at(self, index: u32) -> Field { + // Elements are stored immediately after the base slot, so we add 1 to it to compute the slot for the first + // element. + self.base_slot + 1 + index as Field + } +} + +mod test { + use crate::test::helpers::test_environment::TestEnvironment; + use super::DBArray; + use protocol_types::address::AztecAddress; + + global SLOT: Field = 1230; + + unconstrained fn setup() -> AztecAddress { + TestEnvironment::new().unkonstrained().this_address() + } + + #[test] + unconstrained fn empty_array() { + let contract_address = setup(); + + let array: DBArray = DBArray::at(contract_address, SLOT); + assert_eq(array.len(), 0); + } + + #[test(should_fail_with = "Attempted to read past the length of a DBArray")] + unconstrained fn empty_array_read() { + let contract_address = setup(); + + let array = DBArray::at(contract_address, SLOT); + let _: Field = array.get(0); + } + + #[test] + unconstrained fn array_push() { + let contract_address = setup(); + + let array = DBArray::at(contract_address, SLOT); + array.push(5); + + assert_eq(array.len(), 1); + assert_eq(array.get(0), 5); + } + + #[test(should_fail_with = "Attempted to read past the length of a DBArray")] + unconstrained fn read_past_len() { + let contract_address = setup(); + + let array = DBArray::at(contract_address, SLOT); + array.push(5); + + let _ = array.get(1); + } + + #[test] + unconstrained fn array_remove_last() { + let contract_address = setup(); + + let array = DBArray::at(contract_address, SLOT); + + array.push(5); + array.remove(0); + + assert_eq(array.len(), 0); + } + + #[test] + unconstrained fn array_remove_some() { + let contract_address = setup(); + + let array = DBArray::at(contract_address, SLOT); + + array.push(7); + array.push(8); + array.push(9); + + assert_eq(array.len(), 3); + assert_eq(array.get(0), 7); + assert_eq(array.get(1), 8); + assert_eq(array.get(2), 9); + + array.remove(1); + + assert_eq(array.len(), 2); + assert_eq(array.get(0), 7); + assert_eq(array.get(1), 9); + } + + #[test] + unconstrained fn array_remove_all() { + let contract_address = setup(); + + let array = DBArray::at(contract_address, SLOT); + + array.push(7); + array.push(8); + array.push(9); + + array.remove(1); + array.remove(1); + array.remove(0); + + assert_eq(array.len(), 0); + } +} diff --git a/noir-projects/aztec-nr/aztec/src/test/mocks/mock_struct.nr b/noir-projects/aztec-nr/aztec/src/test/mocks/mock_struct.nr index 7c48a73260f2..2adfcda5a5ba 100644 --- a/noir-projects/aztec-nr/aztec/src/test/mocks/mock_struct.nr +++ b/noir-projects/aztec-nr/aztec/src/test/mocks/mock_struct.nr @@ -6,7 +6,7 @@ pub(crate) struct MockStruct { } impl MockStruct { - fn new(a: Field, b: Field) -> Self { + pub(crate) fn new(a: Field, b: Field) -> Self { Self { a, b } } } diff --git a/yarn-project/pxe/src/database/kv_pxe_database.ts b/yarn-project/pxe/src/database/kv_pxe_database.ts index 4c73644ca975..a4f3a9129bcb 100644 --- a/yarn-project/pxe/src/database/kv_pxe_database.ts +++ b/yarn-project/pxe/src/database/kv_pxe_database.ts @@ -622,17 +622,17 @@ export class KVPxeDatabase implements PxeDatabase { }); } - 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 dbStore(contractAddress: AztecAddress, slot: Fr, values: Fr[]): Promise { + await this.#contractStore.set( + dbSlotToKey(contractAddress, slot), + Buffer.concat(values.map(value => value.toBuffer())), + ); } - async load(contract: AztecAddress, key: Fr): Promise { - const dataKey = `${contract.toString()}:${key.toString()}`; - const dataBuffer = await this.#contractStore.getAsync(dataKey); + async dbLoad(contractAddress: AztecAddress, slot: Fr): Promise { + const dataBuffer = await this.#contractStore.getAsync(dbSlotToKey(contractAddress, slot)); if (!dataBuffer) { - this.debug(`Data not found for contract ${contract.toString()} and key ${key.toString()}`); + this.debug(`Data not found for contract ${contractAddress.toString()} and slot ${slot.toString()}`); return null; } const values: Fr[] = []; @@ -641,4 +641,36 @@ export class KVPxeDatabase implements PxeDatabase { } return values; } + + async dbDelete(contractAddress: AztecAddress, slot: Fr): Promise { + await this.#contractStore.delete(dbSlotToKey(contractAddress, slot)); + } + + async dbCopy(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): Promise { + // In order to support overlaping source and destination regions we need to check the relative positions of source + // and destination. If destination is ahead of source, then by the time we overwrite source elements using forward + // indexes we'll have already read those. On the contrary, if source is ahead of destination we need to use backward + // indexes to avoid reading elements that've been overwritten. + + const indexes = Array.from(Array(numEntries).keys()); + if (srcSlot.lt(dstSlot)) { + indexes.reverse(); + } + + for (const i of indexes) { + const currentSrcSlot = dbSlotToKey(contractAddress, srcSlot.add(new Fr(i))); + const currentDstSlot = dbSlotToKey(contractAddress, dstSlot.add(new Fr(i))); + + const toCopy = await this.#contractStore.getAsync(currentSrcSlot); + if (!toCopy) { + throw new Error(`Attempted to copy empty slot ${currentSrcSlot} for contract ${contractAddress.toString()}`); + } + + await this.#contractStore.set(currentDstSlot, toCopy); + } + } +} + +function dbSlotToKey(contractAddress: AztecAddress, slot: Fr): string { + return `${contractAddress.toString()}:${slot.toString()}`; } diff --git a/yarn-project/pxe/src/database/pxe_database.ts b/yarn-project/pxe/src/database/pxe_database.ts index cc8496aaadfc..e508891d1c75 100644 --- a/yarn-project/pxe/src/database/pxe_database.ts +++ b/yarn-project/pxe/src/database/pxe_database.ts @@ -215,20 +215,39 @@ export interface PxeDatabase extends ContractArtifactDatabase, ContractInstanceD 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. + * Stores arbitrary information in a per-contract non-volatile database, which can later be retrieved with `dbLoad`. + * If data was already stored at this slot, it is overwrriten. + * @param contractAddress - The contract address to scope the data under. + * @param slot - The slot in the database in which to store the value. Slots need not be contiguous. + * @param values - The data to store. */ - store(contract: AztecAddress, key: Fr, values: Fr[]): Promise; + dbStore(contractAddress: AztecAddress, slot: 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. + * Returns data previously stored via `dbStore` in the per-contract non-volatile database. + * @param contractAddress - The contract address under which the data is scoped. + * @param slot - The slot in the database to read. + * @returns The stored data or `null` if no data is stored under the slot. */ - load(contract: AztecAddress, key: Fr): Promise; + dbLoad(contractAddress: AztecAddress, slot: Fr): Promise; + + /** + * Deletes data in the per-contract non-volatile database. Does nothing if no data was present. + * @param contractAddress - The contract address under which the data is scoped. + * @param slot - The slot in the database to delete. + */ + dbDelete(contractAddress: AztecAddress, slot: Fr): Promise; + + /** + * Copies a number of contiguous entries in the per-contract non-volatile database. This allows for efficient data + * structures by avoiding repeated calls to `dbLoad` and `dbStore`. + * Supports overlapping source and 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) + * + * @param contractAddress - The contract address under which the data is scoped. + * @param srcSlot - The first slot to copy from. + * @param dstSlot - The first slot to copy to. + * @param numEntries - The number of entries to copy. + */ + dbCopy(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): 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 1a1a586c78d2..a15a56bdf3a1 100644 --- a/yarn-project/pxe/src/database/pxe_database_test_suite.ts +++ b/yarn-project/pxe/src/database/pxe_database_test_suite.ts @@ -406,7 +406,7 @@ export function describePxeDatabase(getDatabase: () => PxeDatabase) { }); }); - describe('contract store', () => { + describe('contract non-volatile database', () => { let contract: AztecAddress; beforeEach(() => { @@ -415,56 +415,155 @@ export function describePxeDatabase(getDatabase: () => PxeDatabase) { }); it('stores and loads a single value', async () => { - const key = new Fr(1); + const slot = new Fr(1); const values = [new Fr(42)]; - await database.store(contract, key, values); - const result = await database.load(contract, key); + await database.dbStore(contract, slot, values); + const result = await database.dbLoad(contract, slot); expect(result).toEqual(values); }); it('stores and loads multiple values', async () => { - const key = new Fr(1); + const slot = 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); + await database.dbStore(contract, slot, values); + const result = await database.dbLoad(contract, slot); expect(result).toEqual(values); }); it('overwrites existing values', async () => { - const key = new Fr(1); + const slot = 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); + await database.dbStore(contract, slot, initialValues); + await database.dbStore(contract, slot, newValues); - const result = await database.load(contract, key); + const result = await database.dbLoad(contract, slot); expect(result).toEqual(newValues); }); it('stores values for different contracts independently', async () => { const anotherContract = AztecAddress.random(); - const key = new Fr(1); + const slot = 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); + await database.dbStore(contract, slot, values1); + await database.dbStore(anotherContract, slot, values2); - const result1 = await database.load(contract, key); - const result2 = await database.load(anotherContract, key); + const result1 = await database.dbLoad(contract, slot); + const result2 = await database.dbLoad(anotherContract, slot); 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); + it('returns null for non-existent slots', async () => { + const slot = Fr.random(); + const result = await database.dbLoad(contract, slot); expect(result).toBeNull(); }); + + it('deletes a slot', async () => { + const slot = new Fr(1); + const values = [new Fr(42)]; + + await database.dbStore(contract, slot, values); + await database.dbDelete(contract, slot); + + expect(await database.dbLoad(contract, slot)).toBeNull(); + }); + + it('deletes an empty slot', async () => { + const slot = new Fr(1); + await database.dbDelete(contract, slot); + + expect(await database.dbLoad(contract, slot)).toBeNull(); + }); + + it('copies a single value', async () => { + const slot = new Fr(1); + const values = [new Fr(42)]; + + await database.dbStore(contract, slot, values); + + const dstSlot = new Fr(5); + await database.dbCopy(contract, slot, dstSlot, 1); + + expect(await database.dbLoad(contract, dstSlot)).toEqual(values); + }); + + it('copies multiple non-overlapping values', async () => { + const src = new Fr(1); + const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; + + await database.dbStore(contract, src, valuesArray[0]); + await database.dbStore(contract, src.add(new Fr(1)), valuesArray[1]); + await database.dbStore(contract, src.add(new Fr(2)), valuesArray[2]); + + const dst = new Fr(5); + await database.dbCopy(contract, src, dst, 3); + + expect(await database.dbLoad(contract, dst)).toEqual(valuesArray[0]); + expect(await database.dbLoad(contract, dst.add(new Fr(1)))).toEqual(valuesArray[1]); + expect(await database.dbLoad(contract, dst.add(new Fr(2)))).toEqual(valuesArray[2]); + }); + + it('copies overlapping values with src ahead', async () => { + const src = new Fr(1); + const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; + + await database.dbStore(contract, src, valuesArray[0]); + await database.dbStore(contract, src.add(new Fr(1)), valuesArray[1]); + await database.dbStore(contract, src.add(new Fr(2)), valuesArray[2]); + + const dst = new Fr(2); + await database.dbCopy(contract, src, dst, 3); + + expect(await database.dbLoad(contract, dst)).toEqual(valuesArray[0]); + expect(await database.dbLoad(contract, dst.add(new Fr(1)))).toEqual(valuesArray[1]); + expect(await database.dbLoad(contract, dst.add(new Fr(2)))).toEqual(valuesArray[2]); + + // Slots 2 and 3 (src[1] and src[2]) should have been overwritten since they are also dst[0] and dst[1] + expect(await database.dbLoad(contract, src)).toEqual(valuesArray[0]); // src[0] (unchanged) + expect(await database.dbLoad(contract, src.add(new Fr(1)))).toEqual(valuesArray[0]); // dst[0] + expect(await database.dbLoad(contract, src.add(new Fr(2)))).toEqual(valuesArray[1]); // dst[1] + }); + + it('copies overlapping values with dst ahead', async () => { + const src = new Fr(5); + const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; + + await database.dbStore(contract, src, valuesArray[0]); + await database.dbStore(contract, src.add(new Fr(1)), valuesArray[1]); + await database.dbStore(contract, src.add(new Fr(2)), valuesArray[2]); + + const dst = new Fr(4); + await database.dbCopy(contract, src, dst, 3); + + expect(await database.dbLoad(contract, dst)).toEqual(valuesArray[0]); + expect(await database.dbLoad(contract, dst.add(new Fr(1)))).toEqual(valuesArray[1]); + expect(await database.dbLoad(contract, dst.add(new Fr(2)))).toEqual(valuesArray[2]); + + // Slots 5 and 6 (src[0] and src[1]) should have been overwritten since they are also dst[1] and dst[2] + expect(await database.dbLoad(contract, src)).toEqual(valuesArray[1]); // dst[1] + expect(await database.dbLoad(contract, src.add(new Fr(1)))).toEqual(valuesArray[2]); // dst[2] + expect(await database.dbLoad(contract, src.add(new Fr(2)))).toEqual(valuesArray[2]); // src[2] (unchanged) + }); + + it('copying fails if any value is empty', async () => { + const src = new Fr(1); + const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; + + await database.dbStore(contract, src, valuesArray[0]); + // We skip src[1] + await database.dbStore(contract, src.add(new Fr(2)), valuesArray[2]); + + const dst = new Fr(5); + await expect(database.dbCopy(contract, src, dst, 3)).rejects.toThrow('Attempted to copy empty slot'); + }); }); }); } diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index 247e2496c0cd..ba80dc2940d7 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -804,26 +804,20 @@ 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); + dbStore(contractAddress: AztecAddress, slot: Fr, values: Fr[]): Promise { + return this.db.dbStore(contractAddress, slot, 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); + dbLoad(contractAddress: AztecAddress, slot: Fr): Promise { + return this.db.dbLoad(contractAddress, slot); + } + + dbDelete(contractAddress: AztecAddress, slot: Fr): Promise { + return this.db.dbDelete(contractAddress, slot); + } + + dbCopy(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): Promise { + return this.db.dbCopy(contractAddress, srcSlot, dstSlot, numEntries); } } diff --git a/yarn-project/simulator/src/acvm/oracle/oracle.ts b/yarn-project/simulator/src/acvm/oracle/oracle.ts index 3b0332619ba4..5351def9eb4d 100644 --- a/yarn-project/simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/oracle.ts @@ -2,15 +2,12 @@ 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, fromBoundedVec } 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. */ @@ -412,34 +409,50 @@ export class Oracle { return toACVMField(true); } - 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); + async dbStore([contractAddress]: ACVMField[], [slot]: ACVMField[], values: ACVMField[]) { + await this.typedOracle.dbStore( + AztecAddress.fromField(fromACVMField(contractAddress)), + fromACVMField(slot), + values.map(fromACVMField), + ); + } + + async dbLoad( + [contractAddress]: ACVMField[], + [slot]: ACVMField[], + [tSize]: ACVMField[], + ): Promise<(ACVMField | ACVMField[])[]> { + const values = await this.typedOracle.dbLoad( + AztecAddress.fromField(fromACVMField(contractAddress)), + fromACVMField(slot), + ); + + // 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 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))]; + // No data was found so we set `some` to 0 and pad `value` with zeros get the correct return size. + return [toACVMField(0), Array(frToNumber(fromACVMField(tSize))).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}]`); + // Data was found so we set `some` to 1 and return it along with `value`. return [toACVMField(1), values.map(toACVMField)]; } } + + async dbDelete([contractAddress]: ACVMField[], [slot]: ACVMField[]) { + await this.typedOracle.dbDelete(AztecAddress.fromField(fromACVMField(contractAddress)), fromACVMField(slot)); + } + + async dbCopy( + [contractAddress]: ACVMField[], + [srcSlot]: ACVMField[], + [dstSlot]: ACVMField[], + [numEntries]: ACVMField[], + ) { + await this.typedOracle.dbCopy( + AztecAddress.fromField(fromACVMField(contractAddress)), + fromACVMField(srcSlot), + fromACVMField(dstSlot), + frToNumber(fromACVMField(numEntries)), + ); + } } diff --git a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts index c5db2cfa9a74..e9fbf9d5dc70 100644 --- a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts @@ -250,11 +250,19 @@ export abstract class TypedOracle { throw new OracleMethodNotAvailableError('deliverNote'); } - store(_contract: AztecAddress, _key: Fr, _values: Fr[]): Promise { - throw new OracleMethodNotAvailableError('store'); + dbStore(_contractAddress: AztecAddress, _key: Fr, _values: Fr[]): Promise { + throw new OracleMethodNotAvailableError('dbStore'); } - load(_contract: AztecAddress, _key: Fr): Promise { - throw new OracleMethodNotAvailableError('load'); + dbLoad(_contractAddress: AztecAddress, _key: Fr): Promise { + throw new OracleMethodNotAvailableError('dbLoad'); + } + + dbDelete(_contractAddress: AztecAddress, _key: Fr): Promise { + throw new OracleMethodNotAvailableError('dbDelete'); + } + + dbCopy(_contractAddress: AztecAddress, _srcKey: Fr, _dstKey: Fr, _numEntries: number): Promise { + throw new OracleMethodNotAvailableError('dbCopy'); } } diff --git a/yarn-project/simulator/src/client/db_oracle.ts b/yarn-project/simulator/src/client/db_oracle.ts index 5f564eab62f5..594af643bccb 100644 --- a/yarn-project/simulator/src/client/db_oracle.ts +++ b/yarn-project/simulator/src/client/db_oracle.ts @@ -263,20 +263,39 @@ export interface DBOracle extends CommitmentsDB { 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. + * Stores arbitrary information in a per-contract non-volatile database, which can later be retrieved with `dbLoad`. + * * If data was already stored at this slot, it is overwrriten. + * @param contractAddress - The contract address to scope the data under. + * @param slot - The slot in the database in which to store the value. Slots need not be contiguous. + * @param values - The data to store. */ - store(contract: AztecAddress, key: Fr, values: Fr[]): Promise; + dbStore(contractAddress: AztecAddress, slot: 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. + * Returns data previously stored via `dbStore` in the per-contract non-volatile database. + * @param contractAddress - The contract address under which the data is scoped. + * @param slot - The slot in the database to read. + * @returns The stored data or `null` if no data is stored under the slot. */ - load(contract: AztecAddress, key: Fr): Promise; + dbLoad(contractAddress: AztecAddress, slot: Fr): Promise; + + /** + * Deletes data in the per-contract non-volatile database. Does nothing if no data was present. + * @param contractAddress - The contract address under which the data is scoped. + * @param slot - The slot in the database to delete. + */ + dbDelete(contractAddress: AztecAddress, slot: Fr): Promise; + + /** + * Copies a number of contiguous entries in the per-contract non-volatile database. This allows for efficient data + * structures by avoiding repeated calls to `dbLoad` and `dbStore`. + * Supports overlapping source and 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) + * + * @param contractAddress - The contract address under which the data is scoped. + * @param srcSlot - The first slot to copy from. + * @param dstSlot - The first slot to copy to. + * @param numEntries - The number of entries to copy. + */ + dbCopy(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): Promise; } diff --git a/yarn-project/simulator/src/client/view_data_oracle.ts b/yarn-project/simulator/src/client/view_data_oracle.ts index 7f6d10bacfc0..64f2e2c5cecf 100644 --- a/yarn-project/simulator/src/client/view_data_oracle.ts +++ b/yarn-project/simulator/src/client/view_data_oracle.ts @@ -328,23 +328,35 @@ export class ViewDataOracle extends TypedOracle { await this.db.deliverNote(contractAddress, storageSlot, nonce, content, noteHash, nullifier, txHash, recipient); } - 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}`, - ); + public override dbStore(contractAddress: AztecAddress, slot: Fr, values: Fr[]): 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.db.store(this.contractAddress, key, values); + return this.db.dbStore(this.contractAddress, slot, 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}`, - ); + public override dbLoad(contractAddress: AztecAddress, slot: Fr): 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.db.dbLoad(this.contractAddress, slot); + } + + public override dbDelete(contractAddress: AztecAddress, slot: Fr): 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.db.dbDelete(this.contractAddress, slot); + } + + public override dbCopy(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): 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.db.load(this.contractAddress, key); + return this.db.dbCopy(this.contractAddress, srcSlot, dstSlot, numEntries); } } diff --git a/yarn-project/txe/src/oracle/txe_oracle.ts b/yarn-project/txe/src/oracle/txe_oracle.ts index 10b843150da2..b338e756b65d 100644 --- a/yarn-project/txe/src/oracle/txe_oracle.ts +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -1072,36 +1072,35 @@ 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}`, - ); + dbStore(contractAddress: AztecAddress, slot: Fr, values: Fr[]): 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.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.dbStore(this.contractAddress, slot, values); + } + + dbLoad(contractAddress: AztecAddress, slot: Fr): 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.txeDatabase.dbLoad(this.contractAddress, slot); + } + + dbDelete(contractAddress: AztecAddress, slot: Fr): 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.txeDatabase.dbDelete(this.contractAddress, slot); + } + + dbCopy(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): 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.txeDatabase.load(this.contractAddress, key); + return this.txeDatabase.dbCopy(this.contractAddress, srcSlot, dstSlot, numEntries); } } diff --git a/yarn-project/txe/src/txe_service/txe_service.ts b/yarn-project/txe/src/txe_service/txe_service.ts index 9b27a2b5fe92..e2ce58282226 100644 --- a/yarn-project/txe/src/txe_service/txe_service.ts +++ b/yarn-project/txe/src/txe_service/txe_service.ts @@ -588,37 +588,49 @@ 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); + async dbStore(contractAddress: ForeignCallSingle, slot: ForeignCallSingle, values: ForeignCallArray) { + await this.typedOracle.dbStore( + AztecAddress.fromField(fromSingle(contractAddress)), + fromSingle(slot), + fromArray(values), + ); 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); + async dbLoad(contractAddress: ForeignCallSingle, slot: ForeignCallSingle, tSize: ForeignCallSingle) { + const values = await this.typedOracle.dbLoad(AztecAddress.fromField(fromSingle(contractAddress)), fromSingle(slot)); // 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)))]); + return toForeignCallResult([toSingle(new Fr(0)), toArray(Array(fromSingle(tSize).toNumber()).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)]); } } + async dbDelete(contractAddress: ForeignCallSingle, slot: ForeignCallSingle) { + await this.typedOracle.dbDelete(AztecAddress.fromField(fromSingle(contractAddress)), fromSingle(slot)); + return toForeignCallResult([]); + } + + async dbCopy( + contractAddress: ForeignCallSingle, + srcSlot: ForeignCallSingle, + dstSlot: ForeignCallSingle, + numEntries: ForeignCallSingle, + ) { + await this.typedOracle.dbCopy( + AztecAddress.fromField(fromSingle(contractAddress)), + fromSingle(srcSlot), + fromSingle(dstSlot), + fromSingle(numEntries).toNumber(), + ); + + return toForeignCallResult([]); + } + // AVM opcodes avmOpcodeEmitUnencryptedLog(_message: ForeignCallArray) {