Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: PXE db contract store #10867

Merged
merged 28 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"demonomorphizes",
"demonomorphizing",
"deregistration",
"desynchronization",
"devex",
"devnet",
"devs",
Expand Down Expand Up @@ -176,6 +177,7 @@
"noirc",
"noirup",
"nullifer",
"Nullifiable",
"offchain",
"onchain",
"opentelemetry",
Expand Down Expand Up @@ -276,6 +278,7 @@
"unexclude",
"unexcluded",
"unfinalised",
"unnullify",
"unprefixed",
"unshift",
"unshifted",
Expand Down
2 changes: 1 addition & 1 deletion noir-projects/aztec-nr/aztec/src/note/note_interface.nr
Original file line number Diff line number Diff line change
@@ -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<T> {
fn properties() -> T;
Expand Down
1 change: 1 addition & 0 deletions noir-projects/aztec-nr/aztec/src/oracle/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
83 changes: 83 additions & 0 deletions noir-projects/aztec-nr/aztec/src/oracle/pxe_db.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use protocol_types::{address::AztecAddress, traits::{Deserialize, Serialize}};

#[oracle(store)]
unconstrained fn store_oracle<let N: u32>(
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<T, let N: u32>(contract_address: AztecAddress, key: Field, value: T)
where
T: Serialize<N>,
{
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<T> 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)]
Comment on lines +20 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very clear, thanks for the writeup.

unconstrained fn load_oracle<let N: u32>(
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<T, let N: u32>(contract_address: AztecAddress, key: Field) -> Option<T>
where
T: Deserialize<N>,
{
let serialized_option = load_oracle::<N>(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<MockStruct> = load(contract_address, key);

assert(loaded_value == Option::none(), "Value should not exist");
}
}
18 changes: 12 additions & 6 deletions noir-projects/noir-contracts/contracts/test_contract/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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<MockStruct> = 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 {
benesjan marked this conversation as resolved.
Show resolved Hide resolved
kinda_seed * random()
}

pub struct DummyNote {
amount: Field,
secret_hash: Field,
Expand Down
50 changes: 50 additions & 0 deletions yarn-project/end-to-end/src/e2e_pxe_db.test.ts
Original file line number Diff line number Diff line change
@@ -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<void>;

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]);
});
});
30 changes: 30 additions & 0 deletions yarn-project/pxe/src/database/kv_pxe_database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -63,6 +64,11 @@ export class KVPxeDatabase implements PxeDatabase {
#taggingSecretIndexesForSenders: AztecAsyncMap<string, number>;
#taggingSecretIndexesForRecipients: AztecAsyncMap<string, number>;

// Arbitrary data stored by contracts. Key is computed as `${contractAddress}:${key}`
#contractStore: AztecAsyncMap<string, Buffer>;

debug: LogFn;

protected constructor(private db: AztecAsyncKVStore) {
this.#db = db;

Expand Down Expand Up @@ -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<KVPxeDatabase> {
Expand Down Expand Up @@ -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<void> {
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<Fr[] | null> {
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;
}
}
18 changes: 18 additions & 0 deletions yarn-project/pxe/src/database/pxe_database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,4 +213,22 @@ export interface PxeDatabase extends ContractArtifactDatabase, ContractInstanceD
* is also required to deal with chain reorgs.
*/
resetNoteSyncData(): Promise<void>;

/**
* 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<void>;

/**
* 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<Fr[] | null>;
}
61 changes: 61 additions & 0 deletions yarn-project/pxe/src/database/pxe_database_test_suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
}
22 changes: 22 additions & 0 deletions yarn-project/pxe/src/simulator_oracle/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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<Fr[] | null> {
return this.db.load(contract, key);
}
}
Loading
Loading