Skip to content

Commit f069ba7

Browse files
authored
feat(pxe): origin block number timestamp log oracle in log retrieval (#24398)
1 parent 0df2e0d commit f069ba7

18 files changed

Lines changed: 319 additions & 35 deletions

File tree

noir-projects/aztec-nr/aztec/src/messages/delivery/handshake.nr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ pub(crate) unconstrained fn get_existing_app_siloed_handshake_secrets(
8181
/// Establishes a non-interactive handshake for `(sender, recipient)` and returns its app-siloed secrets.
8282
///
8383
/// The registry inserts a fresh handshake note and returns the app-siloed secrets. The constrained return value is the
84-
/// source of truth for the secrets, so constrained delivery anchoring a freshly bootstrapped handshake needs no separate
85-
/// `validate_handshake`.
84+
/// source of truth for the secrets, so constrained delivery anchoring a freshly bootstrapped handshake needs no
85+
/// separate `validate_handshake`.
8686
///
8787
/// Any handshake already registered for the pair is overwritten.
8888
pub(crate) fn create_non_interactive_handshake(

noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ pub unconstrained fn get_logs_by_tag(
5454
get_logs_by_tag_oracle(requests)
5555
}
5656

57+
// TODO: PXE also exposes `getLogsByTagV2`, whose response additionally carries the origin block number and timestamp
58+
// of each log. Switch this oracle (and `LogRetrievalResponse`) over to it once a consumer needs that context.
5759
#[oracle(aztec_utl_getLogsByTag)]
5860
unconstrained fn get_logs_by_tag_oracle(
5961
requests: EphemeralArray<LogRetrievalRequest>,

noir-projects/aztec-nr/aztec/src/oracle/version.nr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
/// immediately if AZTEC_NR_MINOR > PXE_MINOR because if a contract is updated to use a newer Aztec.nr dependency
1212
/// without actually using any of the new oracles then there is no reason to throw.
1313
pub global ORACLE_VERSION_MAJOR: Field = 30;
14-
pub global ORACLE_VERSION_MINOR: Field = 2;
14+
pub global ORACLE_VERSION_MINOR: Field = 3;
1515

1616
/// Asserts that the version of the oracle is compatible with the version expected by the contract.
1717
pub fn assert_compatible_oracle_version() {

noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ use crate::{
1515
DOM_SEP__ECDH_SUBKEY, DOM_SEP__EVENT_COMMITMENT, DOM_SEP__EVENT_LOG_TAG, DOM_SEP__FBSK_M,
1616
DOM_SEP__FUNCTION_ARGS, DOM_SEP__INITIALIZATION_NULLIFIER, DOM_SEP__INITIALIZER,
1717
DOM_SEP__IVSK_M, DOM_SEP__MERKLE_HASH, DOM_SEP__MESSAGE_NULLIFIER, DOM_SEP__MSSK_M,
18-
DOM_SEP__NHK_M,
19-
DOM_SEP__NON_INTERACTIVE_HANDSHAKE_LOG_TAG, DOM_SEP__NOTE_COMPLETION_LOG_TAG,
20-
DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_HASH_NONCE, DOM_SEP__NOTE_NULLIFIER,
21-
DOM_SEP__NULLIFIER_MERKLE, DOM_SEP__OVSK_M, DOM_SEP__PARTIAL_ADDRESS,
22-
DOM_SEP__PARTIAL_NOTE_COMMITMENT, DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT,
23-
DOM_SEP__PRIVATE_FUNCTION_LEAF, DOM_SEP__PRIVATE_INITIALIZATION_NULLIFIER,
24-
DOM_SEP__PRIVATE_LOG_FIRST_FIELD, DOM_SEP__PRIVATE_TX_HASH, DOM_SEP__PROTOCOL_CONTRACTS,
25-
DOM_SEP__PUBLIC_BYTECODE, DOM_SEP__PUBLIC_CALLDATA, DOM_SEP__PUBLIC_DATA_MERKLE,
18+
DOM_SEP__NHK_M, DOM_SEP__NON_INTERACTIVE_HANDSHAKE_LOG_TAG,
19+
DOM_SEP__NOTE_COMPLETION_LOG_TAG, DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_HASH_NONCE,
20+
DOM_SEP__NOTE_NULLIFIER, DOM_SEP__NULLIFIER_MERKLE, DOM_SEP__OVSK_M,
21+
DOM_SEP__PARTIAL_ADDRESS, DOM_SEP__PARTIAL_NOTE_COMMITMENT,
22+
DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, DOM_SEP__PRIVATE_FUNCTION_LEAF,
23+
DOM_SEP__PRIVATE_INITIALIZATION_NULLIFIER, DOM_SEP__PRIVATE_LOG_FIRST_FIELD,
24+
DOM_SEP__PRIVATE_TX_HASH, DOM_SEP__PROTOCOL_CONTRACTS, DOM_SEP__PUBLIC_BYTECODE,
25+
DOM_SEP__PUBLIC_CALLDATA, DOM_SEP__PUBLIC_DATA_MERKLE,
2626
DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER, DOM_SEP__PUBLIC_KEYS_HASH,
2727
DOM_SEP__PUBLIC_LEAF_SLOT, DOM_SEP__PUBLIC_STORAGE_MAP_SLOT, DOM_SEP__PUBLIC_TX_HASH,
2828
DOM_SEP__RETRIEVED_BYTECODES_MERKLE, DOM_SEP__SALTED_INITIALIZATION_HASH,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export { TxResolverService } from '../messages/tx_resolver_service.js';
5353
export { UtilityExecutionOracle } from './oracle/utility_execution_oracle.js';
5454
export { PrivateExecutionOracle } from './oracle/private_execution_oracle.js';
5555
export { buildACIRCallback, UnavailableOracleError } from './oracle/acir_callback.js';
56+
export { LEGACY_ORACLE_REGISTRY } from './oracle/legacy_oracle_registry.js';
5657
export { executePrivateFunction, extractPrivateCircuitPublicInputs } from './oracle/private_execution.js';
5758
export { generateSimulatedProvingResult } from './contract_function_simulator.js';
5859
export { packAsHintedNote } from './oracle/note_packing_utils.js';

yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_response.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import type { BlockNumber } from '@aztec/foundation/branded-types';
12
import type { Fr } from '@aztec/foundation/curves/bn254';
23
import type { TxHash } from '@aztec/stdlib/tx';
4+
import type { UInt64 } from '@aztec/stdlib/types';
35

46
/**
57
* Intermediate struct used to perform batch log retrieval by PXE. The `utilityBulkRetrieveLogs` oracle stores values of this
@@ -10,4 +12,6 @@ export type LogRetrievalResponse = {
1012
txHash: TxHash;
1113
uniqueNoteHashesInTx: Fr[];
1214
firstNullifierInTx: Fr;
15+
blockNumber: BlockNumber;
16+
blockTimestamp: UInt64;
1317
};

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

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import type { ACIRCallback, ACVMField } from '@aztec/simulator/client';
22

33
import { ORACLE_VERSION_MAJOR, ORACLE_VERSION_MINOR } from '../../oracle_version.js';
44
import type { IMiscOracle, IPrivateExecutionOracle, IUtilityExecutionOracle } from './interfaces.js';
5-
import { ORACLE_REGISTRY } from './oracle_registry.js';
5+
import { LEGACY_ORACLE_REGISTRY, type LegacyOracleEntry } from './legacy_oracle_registry.js';
6+
import { type NamedValue, ORACLE_REGISTRY, type OracleRegistryEntry, makeEntry } from './oracle_registry.js';
67

78
export class UnavailableOracleError extends Error {
89
constructor(oracleName: string) {
@@ -17,26 +18,71 @@ export class UnavailableOracleError extends Error {
1718
* `aztec_{scope}_{methodName}` convention to resolve the handler, and calling the method directly. Unknown oracle
1819
* names produce enhanced error messages based on the contract's oracle version.
1920
*/
20-
export function buildACIRCallback(handler: OracleHandler): ACIRCallback {
21+
export function buildACIRCallback(
22+
handler: OracleHandler,
23+
registries: {
24+
real?: Record<string, OracleRegistryEntry>;
25+
legacy?: Record<string, LegacyOracleEntry>;
26+
} = {},
27+
): ACIRCallback {
28+
const { real = ORACLE_REGISTRY, legacy: legacyRegistry = LEGACY_ORACLE_REGISTRY } = registries;
2129
const target = {} as ACIRCallback;
22-
for (const [oracleKey, entry] of Object.entries(ORACLE_REGISTRY)) {
23-
const match = oracleKey.match(/^aztec_(\w+?)_(.+)$/);
24-
if (!match) {
25-
throw new Error(`Oracle "${oracleKey}" does not follow the aztec_{scope}_{method} convention`);
26-
}
27-
const [, scope, methodName] = match;
30+
for (const [oracleKey, entry] of Object.entries(real)) {
31+
const { scope, methodName } = parseOracleName(oracleKey, 'Oracle');
2832
target[oracleKey] = async (...inputs: ACVMField[][]) => {
2933
assertHandlerSupportsScope(handler, scope);
3034
const named = entry.deserializeParams(inputs);
31-
const positional = named.map(p => p.value);
35+
const positional = named.map((p: NamedValue) => p.value);
3236
const result = await (handler as any)[methodName](...positional);
3337
return entry.serializeReturn(result);
3438
};
3539
}
3640

37-
return new Proxy(target, {
41+
// Legacy oracle names: served for contracts compiled against a retired oracle version. Each reuses the current
42+
// handler of its `modernOracle` and reshapes the wire (params and/or return) back to what the old bytecode expects.
43+
for (const [legacyKey, legacy] of Object.entries(legacyRegistry)) {
44+
const { scope } = parseOracleName(legacyKey, 'Legacy oracle');
45+
if (legacyKey in target) {
46+
throw new Error(`Legacy oracle "${legacyKey}" collides with a live oracle of the same name in the registry`);
47+
}
48+
const modernEntry = real[legacy.modernOracle];
49+
const { methodName } = parseOracleName(legacy.modernOracle, 'Oracle');
50+
// Override only the side whose wire changed; inherit the other from the modern entry.
51+
const paramOverride = legacy.params;
52+
const paramSource = paramOverride ? makeEntry({ params: [...paramOverride.legacyType] }) : modernEntry;
53+
const returnOverride = legacy.returnType;
54+
const returnSource = returnOverride ? makeEntry({ returnType: returnOverride.legacyType }) : modernEntry;
55+
target[legacyKey] = async (...inputs: ACVMField[][]) => {
56+
assertHandlerSupportsScope(handler, scope);
57+
const legacyArgs = paramSource.deserializeParams(inputs).map(p => p.value);
58+
const positional = paramOverride ? paramOverride.mapping(legacyArgs) : legacyArgs;
59+
const result = await (handler as any)[methodName](...positional);
60+
return returnSource.serializeReturn(returnOverride ? returnOverride.mapping(result) : result);
61+
};
62+
}
63+
64+
return new Proxy(target, makeUnknownOracleTrap(handler));
65+
}
66+
67+
/** Parses an `aztec_{scope}_{method}` oracle name into its parts, throwing if it doesn't follow the convention. */
68+
function parseOracleName(key: string, label: string): { scope: string; methodName: string } {
69+
const match = key.match(/^aztec_(\w+?)_(.+)$/);
70+
if (!match) {
71+
throw new Error(`${label} "${key}" does not follow the aztec_{scope}_{method} convention`);
72+
}
73+
return { scope: match[1], methodName: match[2] };
74+
}
75+
76+
/**
77+
* Proxy trap for the callback table: a known oracle name passes through; an unknown one throws a diagnostic keyed on
78+
* the contract's oracle version (version unknown, contract newer than this environment, or a same-version mismatch).
79+
*/
80+
function makeUnknownOracleTrap(handler: OracleHandler): ProxyHandler<ACIRCallback> {
81+
return {
3882
get(obj, prop: string) {
39-
if (prop in obj) {
83+
// Own-property check only: `in` would match inherited `Object.prototype` keys (e.g. `constructor`, `toString`)
84+
// and return the built-in instead of falling through to the unknown-oracle diagnostic below.
85+
if (Object.hasOwn(obj, prop)) {
4086
return (obj as Record<string, unknown>)[prop];
4187
}
4288

@@ -76,7 +122,7 @@ export function buildACIRCallback(handler: OracleHandler): ACIRCallback {
76122
}
77123
};
78124
},
79-
});
125+
};
80126
}
81127

82128
type OracleHandler = IMiscOracle & (IUtilityExecutionOracle | IPrivateExecutionOracle);
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/* eslint-disable camelcase */
2+
import { Fr } from '@aztec/foundation/curves/bn254';
3+
import { toACVMField } from '@aztec/simulator/client';
4+
5+
import { buildACIRCallback } from './acir_callback.js';
6+
import type { LegacyOracleEntry } from './legacy_oracle_registry.js';
7+
import { FIELD, U32 } from './oracle_registry.js';
8+
9+
type Handler = Parameters<typeof buildACIRCallback>[0];
10+
11+
describe('legacy oracle dispatch', () => {
12+
it('adapts the return wire: handler runs, result is mapped, then serialized through the legacy type', async () => {
13+
// Fixture scenario: the return override maps the handler's current result to the legacy value the old bytecode
14+
// expects, then serializes it through the legacy type.
15+
const handler = { isMisc: true, getRandomField: () => Promise.resolve(new Fr(41)) } as Handler;
16+
17+
const legacyRegistry: Record<string, LegacyOracleEntry> = {
18+
aztec_misc_legacyReturn: {
19+
modernOracle: 'aztec_misc_getRandomField',
20+
returnType: { legacyType: FIELD, mapping: (result: Fr) => new Fr(result.toBigInt() + 1n) },
21+
},
22+
};
23+
24+
const callback = buildACIRCallback(handler, { legacy: legacyRegistry });
25+
26+
// Handler produces 41; the override maps it to the legacy value (41 + 1) the old bytecode expects.
27+
const wire = await callback['aztec_misc_legacyReturn']();
28+
29+
expect(wire).toEqual([toACVMField(new Fr(42))]);
30+
});
31+
32+
it('adapts the param wire: legacy args are deserialized, mapped, then passed to the modern handler', async () => {
33+
// Fixture scenario: the retired wire carried a single `major` field, but the current handler signature is
34+
// (major, minor). The param override deserializes that one-field wire and reshapes it into the modern arg tuple,
35+
// defaulting the `minor` the old bytecode never sent.
36+
const DEFAULTED_MINOR = 0;
37+
38+
let handlerArgs: unknown[] | undefined;
39+
const handler = {
40+
isMisc: true,
41+
assertCompatibleOracleVersion: (...args: unknown[]) => {
42+
handlerArgs = args;
43+
},
44+
} as Handler;
45+
46+
const legacyRegistry: Record<string, LegacyOracleEntry> = {
47+
aztec_misc_legacyParams: {
48+
modernOracle: 'aztec_misc_assertCompatibleOracleVersion',
49+
params: {
50+
legacyType: [{ name: 'major', type: U32 }],
51+
mapping: ([major]: number[]) => [major, DEFAULTED_MINOR],
52+
},
53+
},
54+
};
55+
56+
const callback = buildACIRCallback(handler, { legacy: legacyRegistry });
57+
58+
// Old bytecode sends one field (major = 5); the handler must still receive the full (major, minor) tuple.
59+
await callback['aztec_misc_legacyParams']([toACVMField(new Fr(5))]);
60+
61+
expect(handlerArgs).toEqual([5, DEFAULTED_MINOR]);
62+
});
63+
64+
it('rejects a legacy name that collides with a live oracle', () => {
65+
const handler = { isMisc: true, getRandomField: () => Promise.resolve(new Fr(0)) } as Handler;
66+
const legacyRegistry: Record<string, LegacyOracleEntry> = {
67+
aztec_misc_getRandomField: {
68+
modernOracle: 'aztec_misc_getRandomField',
69+
returnType: { legacyType: FIELD, mapping: (result: Fr) => result },
70+
},
71+
};
72+
73+
expect(() => buildACIRCallback(handler, { legacy: legacyRegistry })).toThrow('collides with a live oracle');
74+
});
75+
});
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/* eslint-disable camelcase */
2+
import { MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_CIPHERTEXT_LEN } from '@aztec/constants';
3+
4+
import type { EphemeralArray } from '../noir-structs/ephemeral_array.js';
5+
import type { LogRetrievalResponse } from '../noir-structs/log_retrieval_response.js';
6+
import {
7+
type InferDeserializedParams,
8+
ORACLE_REGISTRY,
9+
type OracleRegistryEntry,
10+
type ParamTypes,
11+
type RegistryParam,
12+
} from './oracle_registry.js';
13+
import {
14+
EPHEMERAL_ARRAY,
15+
FIELD,
16+
FIXED_BOUNDED_VEC,
17+
STRUCT,
18+
TX_HASH,
19+
type TypeMapping,
20+
} from './oracle_type_mappings.js';
21+
22+
const LEGACY_LOG_RETRIEVAL_RESPONSE: TypeMapping<LegacyLogRetrievalResponse> = STRUCT([
23+
{ name: 'logPayload', type: FIXED_BOUNDED_VEC(FIELD, PRIVATE_LOG_CIPHERTEXT_LEN) },
24+
{ name: 'txHash', type: TX_HASH },
25+
{ name: 'uniqueNoteHashesInTx', type: FIXED_BOUNDED_VEC(FIELD, MAX_NOTE_HASHES_PER_TX) },
26+
{ name: 'firstNullifierInTx', type: FIELD },
27+
]);
28+
29+
/**
30+
* Wire shapes that already-deployed contracts still call by their original oracle name, keyed by that retired name.
31+
* New Aztec.nr calls the current name in `ORACLE_REGISTRY`; old bytecode keeps calling the name compiled into it and
32+
* is served from here, so versioning an oracle's wire (e.g. adding return fields) stops being a breaking change.
33+
*
34+
* Append-only. Drop an entry only once the `ORACLE_VERSION_MAJOR` that introduced its successor is retired, since
35+
* older contracts can no longer run against this environment.
36+
*/
37+
export const LEGACY_ORACLE_REGISTRY: Record<string, LegacyOracleEntry> = {
38+
aztec_utl_getLogsByTag: legacyOracle({
39+
modernOracle: 'aztec_utl_getLogsByTagV2',
40+
returnType: {
41+
legacyType: EPHEMERAL_ARRAY(EPHEMERAL_ARRAY(LEGACY_LOG_RETRIEVAL_RESPONSE)),
42+
// We can map this directly, since the new type is a superset of the old type
43+
mapping: result => result as unknown as EphemeralArray<EphemeralArray<LegacyLogRetrievalResponse>>,
44+
},
45+
}),
46+
};
47+
48+
type LegacyLogRetrievalResponse = Pick<
49+
LogRetrievalResponse,
50+
'logPayload' | 'txHash' | 'uniqueNoteHashesInTx' | 'firstNullifierInTx'
51+
>;
52+
53+
type Registry = typeof ORACLE_REGISTRY;
54+
55+
/** The handler return value type of a modern oracle entry. */
56+
type ReturnValueOf<K extends keyof Registry> = Registry[K] extends OracleRegistryEntry<any, infer R> ? R : never;
57+
58+
/** The modern handler's positional argument tuple, derived from the oracle's declared params. */
59+
type HandlerArgsOf<K extends keyof Registry> = ParamTypes<ReturnType<Registry[K]['deserializeParams']>>;
60+
61+
/**
62+
* A legacy oracle adapter, stored under the retired oracle name that already-deployed contracts still call. It reuses
63+
* the handler of `modernOracle` and overrides only the wire side(s) that changed; an omitted side falls back to that
64+
* modern entry.
65+
*/
66+
export interface LegacyOracleEntry {
67+
/** The current oracle whose handler this reuses and whose param/return wire it inherits by default. */
68+
modernOracle: keyof Registry;
69+
/**
70+
* Old param wire. `legacyType` deserializes the old wire; `mapping` bridges the deserialized legacy args to the
71+
* modern handler's args. Omit when the param wire is unchanged.
72+
*/
73+
params?: { legacyType: readonly RegistryParam[]; mapping: (legacyArgs: any) => readonly unknown[] };
74+
/**
75+
* Old return wire. `legacyType` serializes the subset the old contract reads; `mapping` bridges the handler's
76+
* current result to that subset's value type. Omit when the return wire is unchanged.
77+
*/
78+
returnType?: { legacyType: TypeMapping; mapping: (result: any) => unknown };
79+
}
80+
81+
function legacyOracle<K extends keyof Registry, W = never, const TLegacyParams extends RegistryParam[] = []>(entry: {
82+
modernOracle: K;
83+
params?: {
84+
legacyType: [...TLegacyParams];
85+
mapping: (legacyArgs: ParamTypes<InferDeserializedParams<TLegacyParams>>) => HandlerArgsOf<K>;
86+
};
87+
returnType?: { legacyType: TypeMapping<W>; mapping: (result: ReturnValueOf<K>) => W };
88+
}): LegacyOracleEntry {
89+
return entry;
90+
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ export const ORACLE_REGISTRY = {
241241
],
242242
}),
243243

244-
aztec_utl_getLogsByTag: makeEntry({
244+
aztec_utl_getLogsByTagV2: makeEntry({
245245
params: [{ name: 'requests', type: EPHEMERAL_ARRAY(LOG_RETRIEVAL_REQUEST) }],
246246
returnType: EPHEMERAL_ARRAY(EPHEMERAL_ARRAY(LOG_RETRIEVAL_RESPONSE)),
247247
}),
@@ -643,7 +643,7 @@ export function makeEntry<const TParams extends RegistryParam[] = [], TReturnVal
643643
}
644644

645645
/** A named oracle parameter with its TypeMapping. */
646-
interface RegistryParam<TName extends string = string, T = any> {
646+
export interface RegistryParam<TName extends string = string, T = any> {
647647
name: TName;
648648
type: TypeMapping<T>;
649649
}
@@ -669,7 +669,7 @@ export type ParamTypes<T extends readonly NamedValue[]> = {
669669
* @example `InferDeserializedParams<[RegistryParam<'addr', AztecAddress>, RegistryParam<'slot', Fr>]>`
670670
* → `[NamedValue<'addr', AztecAddress>, NamedValue<'slot', Fr>]`
671671
*/
672-
type InferDeserializedParams<T extends RegistryParam[]> = {
672+
export type InferDeserializedParams<T extends RegistryParam[]> = {
673673
[K in keyof T]: T[K] extends RegistryParam<infer N, infer V> ? NamedValue<N, V> : never;
674674
};
675675

0 commit comments

Comments
 (0)