Skip to content

Commit 755877c

Browse files
authored
refactor(pxe): introduce typed oracle registry for serialization (#23516)
1 parent df48ae2 commit 755877c

14 files changed

Lines changed: 1640 additions & 633 deletions

yarn-project/pxe/src/contract_function_simulator/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ export { Oracle } from './oracle/oracle.js';
1111
export { executePrivateFunction, extractPrivateCircuitPublicInputs } from './oracle/private_execution.js';
1212
export { generateSimulatedProvingResult } from './contract_function_simulator.js';
1313
export { packAsHintedNote } from './oracle/note_packing_utils.js';
14+
export { BoundedVec } from './noir-structs/bounded_vec.js';
15+
export { Option } from './noir-structs/option.js';
1416
export { UtilityContext } from './noir-structs/utility_context.js';
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* TypeScript counterpart of Noir's `BoundedVec<T, MaxLen>`.
3+
*
4+
* Carries the actual `data` plus wire-format metadata (`maxLength`, `elementSize`) so the ACVM
5+
* serializer can pad the storage slot to exactly `maxLength * elementSize` fields.
6+
*/
7+
export class BoundedVec<T> {
8+
private constructor(
9+
public readonly data: T[],
10+
public readonly maxLength: number,
11+
public readonly elementSize: number,
12+
) {}
13+
14+
/**
15+
* Construct a BoundedVec with data.
16+
*
17+
* @param data - Actual elements. Length must be `<= maxLength`.
18+
* @param maxLength - Maximum capacity declared at the Noir call site.
19+
* The storage slot is padded to this many elements.
20+
* @param elementSize - Number of Fr fields each element contributes when serialized.
21+
* `1` for scalar elements (u8, Field) — this is the default.
22+
* `> 1` for compound elements (e.g. a packed note that spans multiple fields).
23+
*
24+
* @example A bounded vec of bytes (elementSize defaults to 1):
25+
* ```ts
26+
* BoundedVec.from({ data: plaintext, maxLength: ciphertext.maxLength })
27+
* ```
28+
*
29+
* @example A bounded vec of packed notes, each spanning `packedHintedNoteLength` fields:
30+
* ```ts
31+
* BoundedVec.from({ data: notes, maxLength: maxNotes, elementSize: packedHintedNoteLength })
32+
* ```
33+
*/
34+
static from<T>({
35+
data,
36+
maxLength,
37+
elementSize = 1,
38+
}: {
39+
data: T[];
40+
maxLength: number;
41+
elementSize?: number;
42+
}): BoundedVec<T> {
43+
return new BoundedVec<T>(data, maxLength, elementSize);
44+
}
45+
46+
/**
47+
* Construct an empty BoundedVec, typically used as a shape template for `Option.empty(...)`.
48+
*
49+
* @param maxLength - Maximum capacity declared at the Noir call site.
50+
* @param elementSize - Number of Fr fields each element contributes when serialized (default 1).
51+
*/
52+
static empty<T>({ maxLength, elementSize = 1 }: { maxLength: number; elementSize?: number }): BoundedVec<T> {
53+
return new BoundedVec<T>([], maxLength, elementSize);
54+
}
55+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* TypeScript counterpart of Noir's `Option<T>`.
3+
*
4+
* Wraps a value that may or may not be present. Use {@link Option.some} to wrap a present value and
5+
* {@link Option.none} for an absent one. The type guards {@link isSome} and {@link isNone} narrow
6+
* `value` in conditional branches.
7+
*/
8+
export class Option<T> {
9+
private constructor(
10+
public readonly value: T | undefined,
11+
public readonly template: T | undefined,
12+
) {}
13+
14+
/**
15+
* Wrap a present value.
16+
*
17+
* @example
18+
* ```ts
19+
* return Option.some(values);
20+
* ```
21+
*/
22+
static some<T>(value: T): Option<T> {
23+
return new Option<T>(value, undefined);
24+
}
25+
26+
/**
27+
* Construct an absent Option.
28+
*
29+
* When serialized back to ACVM, the `None` case must produce the same number of fields as `Some`.
30+
* For types whose wire size varies per call site (`BoundedVec`, `FixedArray`), pass a `template` so the
31+
* serializer knows how many zero fields to emit. Omit the template when the Option will not be
32+
* re-serialized (e.g. deserialized input params).
33+
*
34+
* @param template - A representative empty `T` whose serialization determines the zero-filled wire format.
35+
*
36+
* @example None for a fixed-size type:
37+
* ```ts
38+
* return Option.none(AztecAddress.ZERO);
39+
* ```
40+
*
41+
* @example None for a dynamic-size type:
42+
* ```ts
43+
* return Option.none(BoundedVec.empty<number>({ maxLength: ciphertext.maxLength }));
44+
* ```
45+
*/
46+
static none<T>(template?: T): Option<T> {
47+
return new Option<T>(undefined, template);
48+
}
49+
50+
/**
51+
* Type guard: narrows `value` to `T` in the truthy branch.
52+
*
53+
* @example
54+
* ```ts
55+
* const opt = await handler.getSenderForTags();
56+
* if (opt.isSome()) {
57+
* console.log(opt.value); // narrowed to T
58+
* }
59+
* ```
60+
*/
61+
isSome(): this is Option<T> & { value: T } {
62+
return this.value !== undefined;
63+
}
64+
65+
/** Type guard: narrows `value` to `undefined` in the truthy branch. */
66+
isNone(): this is Option<T> & { value: undefined } {
67+
return this.value === undefined;
68+
}
69+
}

yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import type { ARCHIVE_HEIGHT, L1_TO_L2_MSG_TREE_HEIGHT, NOTE_HASH_TREE_HEIGHT } from '@aztec/constants';
22
import type { BlockNumber } from '@aztec/foundation/branded-types';
3-
import { Fr } from '@aztec/foundation/curves/bn254';
4-
import { MembershipWitness } from '@aztec/foundation/trees';
3+
import type { Fr } from '@aztec/foundation/curves/bn254';
4+
import type { MembershipWitness } from '@aztec/foundation/trees';
55
import type { FunctionSelector, NoteSelector } from '@aztec/stdlib/abi';
66
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
7-
import { BlockHash } from '@aztec/stdlib/block';
7+
import type { BlockHash } from '@aztec/stdlib/block';
88
import type { ContractInstance, PartialAddress } from '@aztec/stdlib/contract';
99
import type { KeyValidationRequest } from '@aztec/stdlib/kernel';
1010
import type { PublicKeys } from '@aztec/stdlib/keys';
1111
import type { ContractClassLog, Tag } from '@aztec/stdlib/logs';
1212
import type { Note, NoteStatus } from '@aztec/stdlib/note';
13-
import { type NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees';
13+
import type { NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees';
1414
import type { BlockHeader, TxEffect, TxHash } from '@aztec/stdlib/tx';
1515

16+
import type { BoundedVec } from '../noir-structs/bounded_vec.js';
17+
import type { Option } from '../noir-structs/option.js';
1618
import type { UtilityContext } from '../noir-structs/utility_context.js';
1719
import type { MessageLoadOracleInputs } from './message_load_oracle_inputs.js';
1820

@@ -74,17 +76,17 @@ export interface IUtilityExecutionOracle {
7476
getBlockHashMembershipWitness(
7577
anchorBlockHash: BlockHash,
7678
blockHash: BlockHash,
77-
): Promise<MembershipWitness<typeof ARCHIVE_HEIGHT> | undefined>;
79+
): Promise<Option<MembershipWitness<typeof ARCHIVE_HEIGHT>>>;
7880
getNullifierMembershipWitness(anchorBlockHash: BlockHash, nullifier: Fr): Promise<NullifierMembershipWitness>;
7981
getPublicDataWitness(anchorBlockHash: BlockHash, leafSlot: Fr): Promise<PublicDataWitness>;
8082
getLowNullifierMembershipWitness(anchorBlockHash: BlockHash, nullifier: Fr): Promise<NullifierMembershipWitness>;
8183
getBlockHeader(blockNumber: BlockNumber): Promise<BlockHeader>;
8284
getPublicKeysAndPartialAddress(
8385
account: AztecAddress,
84-
): Promise<{ publicKeys: PublicKeys; partialAddress: PartialAddress } | undefined>;
86+
): Promise<Option<{ publicKeys: PublicKeys; partialAddress: PartialAddress }>>;
8587
getAuthWitness(messageHash: Fr): Promise<Fr[]>;
8688
getNotes(
87-
owner: AztecAddress | undefined,
89+
owner: Option<AztecAddress>,
8890
storageSlot: Fr,
8991
numSelects: number,
9092
selectByIndexes: number[],
@@ -99,7 +101,9 @@ export interface IUtilityExecutionOracle {
99101
limit: number,
100102
offset: number,
101103
status: NoteStatus,
102-
): Promise<NoteData[]>;
104+
maxNotes: number,
105+
packedHintedNoteLength: number,
106+
): Promise<BoundedVec<NoteData>>;
103107
doesNullifierExist(innerNullifier: Fr): Promise<boolean>;
104108
getL1ToL2MembershipWitness(
105109
contractAddress: AztecAddress,
@@ -122,9 +126,9 @@ export interface IUtilityExecutionOracle {
122126
): Promise<void>;
123127
getLogsByTag(requestArrayBaseSlot: Fr): Promise<Fr>;
124128
getMessageContextsByTxHash(requestArrayBaseSlot: Fr): Promise<Fr>;
125-
getTxEffect(txHash: TxHash): Promise<TxEffect | null>;
129+
getTxEffect(txHash: TxHash): Promise<Option<TxEffect>>;
126130
setCapsule(contractAddress: AztecAddress, key: Fr, capsule: Fr[], scope: AztecAddress): void;
127-
getCapsule(contractAddress: AztecAddress, key: Fr, scope: AztecAddress): Promise<Fr[] | null>;
131+
getCapsule(contractAddress: AztecAddress, key: Fr, tSize: number, scope: AztecAddress): Promise<Option<Fr[]>>;
128132
deleteCapsule(contractAddress: AztecAddress, key: Fr, scope: AztecAddress): void;
129133
copyCapsule(
130134
contractAddress: AztecAddress,
@@ -133,9 +137,9 @@ export interface IUtilityExecutionOracle {
133137
numEntries: number,
134138
scope: AztecAddress,
135139
): Promise<void>;
136-
decryptAes128(ciphertext: Buffer, iv: Buffer, symKey: Buffer): Promise<Buffer | undefined>;
140+
decryptAes128(ciphertext: BoundedVec<number>, iv: Buffer, symKey: Buffer): Promise<Option<BoundedVec<number>>>;
137141
getSharedSecrets(address: AztecAddress, ephPksSlot: Fr, contractAddress: AztecAddress): Promise<Fr>;
138-
setContractSyncCacheInvalid(contractAddress: AztecAddress, scopes: AztecAddress[]): void;
142+
setContractSyncCacheInvalid(contractAddress: AztecAddress, scopes: BoundedVec<AztecAddress>): void;
139143
emitOffchainEffect(data: Fr[]): Promise<void>;
140144
callUtilityFunction(
141145
targetContractAddress: AztecAddress,
@@ -190,7 +194,7 @@ export interface IPrivateExecutionOracle {
190194
assertValidPublicCalldata(calldataHash: Fr): Promise<void>;
191195
notifyRevertiblePhaseStart(minRevertibleSideEffectCounter: number): Promise<void>;
192196
isExecutionInRevertiblePhase(sideEffectCounter: number): Promise<boolean>;
193-
getSenderForTags(): Promise<AztecAddress | undefined>;
197+
getSenderForTags(): Promise<Option<AztecAddress>>;
194198
setSenderForTags(senderForTags: AztecAddress): Promise<void>;
195199
getNextAppTagAsSender(sender: AztecAddress, recipient: AztecAddress): Promise<Tag>;
196200
}

yarn-project/pxe/src/contract_function_simulator/oracle/note_packing_utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function packAsHintedNote({
4848
noteNonce: Fr;
4949
isPending: boolean;
5050
note: Note;
51-
}) {
51+
}): Fr[] {
5252
// If the note is pending it means it has a non-zero note hash counter associated with it.
5353
const nonZeroNoteHashCounter = isPending;
5454

@@ -58,8 +58,8 @@ export function packAsHintedNote({
5858
// Pack in order: note, contract_address, owner, randomness, storage_slot, metadata (stage, maybe_note_nonce)
5959
return [
6060
...note.items,
61-
contractAddress,
62-
owner,
61+
contractAddress.toField(),
62+
owner.toField(),
6363
randomness,
6464
storageSlot,
6565
new Fr(noteMetadata.stage),

0 commit comments

Comments
 (0)