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(noir): better NoteGetterOptions. #1695

Merged
merged 9 commits into from
Aug 22, 2023
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
12 changes: 10 additions & 2 deletions yarn-project/acir-simulator/src/client/client_execution_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ export class ClientTxExecutionContext {
*
* @param contractAddress - The contract address.
* @param storageSlot - The storage slot.
* @param numSelects - The number of valid selects in selectBy and selectValues.
* @param selectBy - An array of indices of the fields to selects.
* @param selectValues - The values to match.
* @param sortBy - An array of indices of the fields to sort.
* @param sortOrder - The order of the corresponding index in sortBy. (1: DESC, 2: ASC, 0: Do nothing)
* @param limit - The number of notes to retrieve per query.
Expand All @@ -118,6 +121,9 @@ export class ClientTxExecutionContext {
public async getNotes(
contractAddress: AztecAddress,
storageSlot: ACVMField,
numSelects: number,
selectBy: ACVMField[],
selectValues: ACVMField[],
sortBy: ACVMField[],
sortOrder: ACVMField[],
limit: number,
Expand All @@ -136,8 +142,10 @@ export class ClientTxExecutionContext {

// Nullified pending notes are already removed from the list.
const notes = pickNotes([...dbNotesFiltered, ...pendingNotes], {
sortBy: sortBy.map(field => +field),
sortOrder: sortOrder.map(field => +field),
selects: selectBy
.slice(0, numSelects)
.map((fieldIndex, i) => ({ index: +fieldIndex, value: fromACVMField(selectValues[i]) })),
sorts: sortBy.map((fieldIndex, i) => ({ index: +fieldIndex, order: +sortOrder[i] })),
limit,
offset,
});
Expand Down
145 changes: 125 additions & 20 deletions yarn-project/acir-simulator/src/client/pick_notes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Fr } from '@aztec/foundation/fields';
import { SortOrder, pickNotes } from './pick_notes.js';

describe('getNotes', () => {
const expectSortedNotes = (notes: { preimage: Fr[] }[], ...expected: [number, bigint[]][]) => {
const expectNotesFields = (notes: { preimage: Fr[] }[], ...expected: [number, bigint[]][]) => {
expect(notes.length).toBe(expected[0][1].length);
expected.forEach(([fieldIndex, fields]) => {
for (let i = 0; i < notes.length; ++i) {
Expand All @@ -12,6 +12,13 @@ describe('getNotes', () => {
});
};

const expectNotes = (notes: { preimage: Fr[] }[], expected: bigint[][]) => {
expect(notes.length).toBe(expected.length);
notes.forEach((note, i) => {
expect(note.preimage.map(p => p.value)).toEqual(expected[i]);
});
};

const createNote = (preimage: bigint[]) => ({
preimage: preimage.map(f => new Fr(f)),
});
Expand All @@ -28,31 +35,41 @@ describe('getNotes', () => {

// Sort 1st field in ascending order.
{
const options = { sortBy: [1], sortOrder: [SortOrder.ASC] };
const options = { sorts: [{ index: 1, order: SortOrder.ASC }] };
const result = pickNotes(notes, options);
expectSortedNotes(result, [1, [0n, 1n, 5n, 5n, 5n, 6n]]);
expectNotesFields(result, [1, [0n, 1n, 5n, 5n, 5n, 6n]]);
}

// Sort 1st field in descending order.
{
const options = { sortBy: [1] };
const options = { sorts: [{ index: 1, order: SortOrder.DESC }] };
const result = pickNotes(notes, options);
expectSortedNotes(result, [1, [6n, 5n, 5n, 5n, 1n, 0n]], [0, [7n, 4n, 6n, 6n, 2n, 0n]]);
expectNotesFields(result, [1, [6n, 5n, 5n, 5n, 1n, 0n]], [0, [7n, 4n, 6n, 6n, 2n, 0n]]);
}

// Sort 1st and 0th fields in descending order.
{
const options = { sortBy: [1, 0] };
const options = {
sorts: [
{ index: 1, order: SortOrder.DESC },
{ index: 0, order: SortOrder.DESC },
],
};
const result = pickNotes(notes, options);
expectSortedNotes(result, [1, [6n, 5n, 5n, 5n, 1n, 0n]], [0, [7n, 6n, 6n, 4n, 2n, 0n]]);
expectNotesFields(result, [1, [6n, 5n, 5n, 5n, 1n, 0n]], [0, [7n, 6n, 6n, 4n, 2n, 0n]]);
}

// Sort 1st field in descending order
// Then 0th field in ascending order
{
const options = { sortBy: [1, 0], sortOrder: [SortOrder.DESC, SortOrder.ASC] };
const options = {
sorts: [
{ index: 1, order: SortOrder.DESC },
{ index: 0, order: SortOrder.ASC },
],
};
const result = pickNotes(notes, options);
expectSortedNotes(
expectNotesFields(
result,
[1, [6n, 5n, 5n, 5n, 1n, 0n]],
[0, [7n, 4n, 6n, 6n, 2n, 0n]],
Expand All @@ -64,9 +81,15 @@ describe('getNotes', () => {
// Then 0th field in ascending order
// Then 2nd field in descending order.
{
const options = { sortBy: [1, 0, 2], sortOrder: [SortOrder.DESC, SortOrder.ASC, SortOrder.DESC] };
const options = {
sorts: [
{ index: 1, order: SortOrder.DESC },
{ index: 0, order: SortOrder.ASC },
{ index: 2, order: SortOrder.DESC },
],
};
const result = pickNotes(notes, options);
expectSortedNotes(
expectNotesFields(
result,
[1, [6n, 5n, 5n, 5n, 1n, 0n]],
[0, [7n, 4n, 6n, 6n, 2n, 0n]],
Expand All @@ -78,32 +101,114 @@ describe('getNotes', () => {
it('should get sorted notes in a range', () => {
const notes = [createNote([2n]), createNote([8n]), createNote([6n]), createNote([5n]), createNote([0n])];

const sortBy = [0];
const sorts = [{ index: 0, order: SortOrder.DESC }];
// Sorted values: [8n, 6n, 5n, 2n, 0n]

{
const options = { sortBy, limit: 3 };
const options = { sorts, limit: 3 };
const result = pickNotes(notes, options);
expectSortedNotes(result, [0, [8n, 6n, 5n]]);
expectNotesFields(result, [0, [8n, 6n, 5n]]);
}

{
const options = { sortBy, limit: 3, offset: 1 };
const options = { sorts, limit: 3, offset: 1 };
const result = pickNotes(notes, options);
expectSortedNotes(result, [0, [6n, 5n, 2n]]);
expectNotesFields(result, [0, [6n, 5n, 2n]]);
}

{
const options = { sortBy, limit: 3, offset: 4 };
const options = { sorts, limit: 3, offset: 4 };
const result = pickNotes(notes, options);
expectSortedNotes(result, [0, [0n]]);
expectNotesFields(result, [0, [0n]]);
}
});

it('should not change order if sortOrder is NADA', () => {
const notes = [createNote([2n]), createNote([8n]), createNote([6n]), createNote([5n]), createNote([0n])];
const options = { sortBy: [0], sortOrder: [SortOrder.NADA] };
const options = { sorts: [{ index: 0, order: SortOrder.NADA }] };
const result = pickNotes(notes, options);
expectNotesFields(result, [0, [2n, 8n, 6n, 5n, 0n]]);
});

it('should get notes that have the required fields', () => {
const notes = [
createNote([2n, 1n, 3n]),
createNote([1n, 2n, 3n]),
createNote([3n, 2n, 0n]),
createNote([2n, 2n, 0n]),
createNote([2n, 3n, 3n]),
];

{
const options = { selects: [{ index: 0, value: new Fr(2n) }] };
const result = pickNotes(notes, options);
expectNotes(result, [
[2n, 1n, 3n],
[2n, 2n, 0n],
[2n, 3n, 3n],
]);
}

{
const options = {
selects: [
{ index: 0, value: new Fr(2n) },
{ index: 2, value: new Fr(3n) },
],
};
const result = pickNotes(notes, options);
expectNotes(result, [
[2n, 1n, 3n],
[2n, 3n, 3n],
]);
}

{
const options = {
selects: [
{ index: 1, value: new Fr(2n) },
{ index: 2, value: new Fr(3n) },
],
};
const result = pickNotes(notes, options);
expectNotes(result, [[1n, 2n, 3n]]);
}

{
const options = { selects: [{ index: 1, value: new Fr(5n) }] };
const result = pickNotes(notes, options);
expectNotes(result, []);
}

{
const options = {
selects: [
{ index: 0, value: new Fr(2n) },
{ index: 1, value: new Fr(5n) },
],
};
const result = pickNotes(notes, options);
expectNotes(result, []);
}
});

it('should get sorted matching notes', () => {
const notes = [
createNote([2n, 1n, 3n]),
createNote([4n, 5n, 8n]),
createNote([7n, 6n, 8n]),
createNote([6n, 5n, 2n]),
createNote([0n, 0n, 8n]),
createNote([6n, 5n, 8n]),
];

const options = { selects: [{ index: 2, value: new Fr(8n) }], sorts: [{ index: 1, order: SortOrder.ASC }] };
const result = pickNotes(notes, options);
expectSortedNotes(result, [0, [2n, 8n, 6n, 5n, 0n]]);
expectNotes(result, [
[0n, 0n, 8n],
[4n, 5n, 8n],
[6n, 5n, 8n],
[7n, 6n, 8n],
]);
});
});
56 changes: 43 additions & 13 deletions yarn-project/acir-simulator/src/client/pick_notes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import { Fr } from '@aztec/foundation/fields';

/**
* Configuration for selecting values.
*/
export interface Select {
/**
* Index of the field to select and match.
*/
index: number;
/**
* Required value of the field.
*/
value: Fr;
}

/**
* The order to sort an array.
*/
Expand All @@ -10,19 +24,33 @@ export enum SortOrder {
}

/**
* Options for selecting items from the database.
* Configuration for sorting values.
*/
export interface Sort {
/**
* Index of the field to sort.
*/
index: number;
/**
* Order to sort the field.
*/
order: SortOrder;
}

/**
* Options for picking items from an array of BasicNoteData.
*/
interface GetOptions {
/**
* An array of indices of the fields to sort.
* Configurations for selecting items.
* Default: empty array.
*/
sortBy?: number[];
selects?: Select[];
/**
* The order of the corresponding index in sortBy. (1: DESC, 2: ASC, 0: Do nothing)
* Configurations for sorting items.
* Default: empty array.
*/
sortOrder?: SortOrder[];
sorts?: Sort[];
/**
* The number of items to retrieve per query.
* Default: 0. No limit.
Expand All @@ -45,16 +73,18 @@ interface BasicNoteData {
preimage: Fr[];
}

const sortNotes = (a: Fr[], b: Fr[], sortBy: number[], sortOrder: number[], level = 0): number => {
const index = sortBy[level];
if (sortBy[level] === undefined) return 0;
const selectNotes = <T extends BasicNoteData>(notes: T[], selects: Select[]): T[] =>
notes.filter(note => selects.every(({ index, value }) => note.preimage[index]?.equals(value)));

const sortNotes = (a: Fr[], b: Fr[], sorts: Sort[], level = 0): number => {
if (sorts[level] === undefined) return 0;

const order = sortOrder[level] ?? 1; // Default: Descending.
const { index, order } = sorts[level];
if (order === 0) return 0;

const dir = order === 1 ? [-1, 1] : [1, -1];
return a[index].value === b[index].value
? sortNotes(a, b, sortBy, sortOrder, level + 1)
? sortNotes(a, b, sorts, level + 1)
: a[index].value > b[index].value
? dir[0]
: dir[1];
Expand All @@ -65,9 +95,9 @@ const sortNotes = (a: Fr[], b: Fr[], sortBy: number[], sortOrder: number[], leve
*/
export function pickNotes<T extends BasicNoteData>(
notes: T[],
{ sortBy = [], sortOrder = [], limit = 0, offset = 0 }: GetOptions,
{ selects = [], sorts = [], limit = 0, offset = 0 }: GetOptions,
) {
return notes
.sort((a, b) => sortNotes(a.preimage, b.preimage, sortBy, sortOrder))
return selectNotes(notes, selects)
.sort((a, b) => sortNotes(a.preimage, b.preimage, sorts))
.slice(offset, limit ? offset + limit : undefined);
}
15 changes: 13 additions & 2 deletions yarn-project/acir-simulator/src/client/private_execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,19 @@ export class PrivateFunctionExecution {
const { publicKey, partialAddress } = await this.context.db.getCompleteAddress(address);
return [publicKey.x, publicKey.y, partialAddress].map(toACVMField);
},
getNotes: ([slot], sortBy, sortOrder, [limit], [offset], [returnSize]) =>
this.context.getNotes(this.contractAddress, slot, sortBy, sortOrder, +limit, +offset, +returnSize),
getNotes: ([slot], [numSelects], selectBy, selectValues, sortBy, sortOrder, [limit], [offset], [returnSize]) =>
this.context.getNotes(
this.contractAddress,
slot,
+numSelects,
selectBy,
selectValues,
sortBy,
sortOrder,
+limit,
+offset,
+returnSize,
),
getRandomField: () => Promise.resolve(toACVMField(Fr.random())),
notifyCreatedNote: ([storageSlot], preimage, [innerNoteHash]) => {
this.context.pushNewNote(
Expand Down
15 changes: 13 additions & 2 deletions yarn-project/acir-simulator/src/client/unconstrained_execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,19 @@ export class UnconstrainedFunctionExecution {
const { publicKey, partialAddress } = await this.context.db.getCompleteAddress(address);
return [publicKey.x, publicKey.y, partialAddress].map(toACVMField);
},
getNotes: ([slot], sortBy, sortOrder, [limit], [offset], [returnSize]) =>
this.context.getNotes(this.contractAddress, slot, sortBy, sortOrder, +limit, +offset, +returnSize),
getNotes: ([slot], [numSelects], selectBy, selectValues, sortBy, sortOrder, [limit], [offset], [returnSize]) =>
this.context.getNotes(
this.contractAddress,
slot,
+numSelects,
selectBy,
selectValues,
sortBy,
sortOrder,
+limit,
+offset,
+returnSize,
),
getRandomField: () => Promise.resolve(toACVMField(Fr.random())),
debugLog: (...params) => {
this.log(oracleDebugCallToFormattedStr(params));
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/aztec.js/src/abis/ecdsa_account_contract.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ contract EasyPrivateToken {
let balances = storage.balances;

// Return the sum of all notes in the set.
balance_utils::get_balance(balances.at(owner).storage_slot)
balance_utils::get_balance(balances.at(owner).set)
}

// Computes note hash and nullifier.
Expand Down
Loading