Skip to content
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
19 changes: 19 additions & 0 deletions docs/docs-developers/docs/foundational-topics/pxe/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,25 @@ The keystore securely stores cryptographic keys for registered accounts, includi

Oracles are pieces of data that are injected into a smart contract function from the client side. Learn more about [how oracles work](../../aztec-nr/framework-description/advanced/protocol_oracles.md).

## Oracle versioning

The set of oracles that the PXE exposes to private and utility functions is versioned, so that contracts can declare which oracles they expect to be available. Every contract compiled with `Aztec.nr` records the oracle version it was built against, and the PXE checks this version before executing any oracle call.

The version uses two components, `major.minor`, with the following compatibility rules:

- **`major`** must match exactly. A major bump is a breaking change — oracles were removed or their signatures changed — and a PXE on a different major cannot safely run the contract.
- **`minor`** indicates additive changes (new oracles). The PXE uses a best-effort approach here: a contract compiled against a higher `minor` than the PXE supports is still allowed to run, and an error is only thrown if the contract actually invokes an oracle the PXE does not know about. In practice, a contract built with a newer Aztec.nr may not use any of the newly added oracles at all, in which case it runs fine on an older PXE.

The canonical version constants live in the PXE (`ORACLE_VERSION_MAJOR` / `ORACLE_VERSION_MINOR` in `yarn-project/pxe/src/oracle_version.ts`) and in Aztec.nr (`noir-projects/aztec-nr/aztec/src/oracle/version.nr`). The two are kept in lockstep as part of each release.

### Resolving a version mismatch

If you see an error like _"Oracle '…' not found. … The contract was compiled with Aztec.nr oracle version X.Y, but this private execution environment only supports up to A.B"_, the contract uses one or more oracles from a newer Aztec.nr than your PXE supports.

To fix it, upgrade the software that ships the PXE (sandbox, wallet, or whatever embeds `@aztec/pxe`) to a release whose Aztec.nr version is at least as new as the one the contract was compiled with.

If the PXE reports a version that _should_ include every oracle the contract needs but an oracle is still missing, that is a contract bug rather than a version problem and you should likely report it to the app developer.

Comment on lines +90 to +108
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not sure this fits in a foundational topics doc. However we do need something to point at and we need to merge this soon so maybe we could track to move this somewhere else in a Linear issue?

## For developers

To learn how to develop on top of the PXE, refer to these guides:
Expand Down
2 changes: 1 addition & 1 deletion docs/netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -802,7 +802,7 @@
[[redirects]]
# PXE: incompatible oracle version between contract and PXE
from = "/errors/8"
to = "/developers/docs/foundational-topics/pxe"
to = "/developers/docs/foundational-topics/pxe#oracle-versioning"

[[redirects]]
# CLI: aztec dep version in Nargo.toml does not match the CLI version
Expand Down
33 changes: 20 additions & 13 deletions noir-projects/aztec-nr/aztec/src/oracle/version.nr
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
/// The ORACLE_VERSION constant is used to check that the oracle interface is in sync between PXE and Aztec.nr. We need
/// to version the oracle interface to ensure that developers get a reasonable error message if they use incompatible
/// versions of Aztec.nr and PXE. The TypeScript counterpart is in `oracle_version.ts`.
/// The oracle version constants are used to check that the oracle interface is in sync between PXE and Aztec.nr.
/// We version the oracle interface as `major.minor` where:
/// - `major` = backward-breaking changes (must match exactly between PXE and Aztec.nr)
/// - `minor` = oracle additions (non-breaking; PXE minor >= contract minor)
///
/// The TypeScript counterparts are in `oracle_version.ts`.
///
/// @dev Whenever a contract function or Noir test is run, the `aztec_utl_assertCompatibleOracleVersion` oracle is
/// called and if the oracle version is incompatible an error is thrown.
pub global ORACLE_VERSION: Field = 22;
/// called. If the major version is incompatible, an error is thrown immediately. The minor version is recorded by
/// the PXE and used to provide helpful error messages if a contract calls an oracle that doesn't exist. We don't throw
/// immediately if AZTEC_NR_MINOR > PXE_MINOR because if a contract is updated to use a newer Aztec.nr dependency
/// without actually using any of the new oracles then there is no reason to throw.
pub global ORACLE_VERSION_MAJOR: Field = 22;
pub global ORACLE_VERSION_MINOR: Field = 0;

/// Asserts that the version of the oracle is compatible with the version expected by the contract.
pub fn assert_compatible_oracle_version() {
Expand All @@ -16,23 +23,23 @@ pub fn assert_compatible_oracle_version() {
}

unconstrained fn assert_compatible_oracle_version_wrapper() {
assert_compatible_oracle_version_oracle(ORACLE_VERSION);
assert_compatible_oracle_version_oracle(ORACLE_VERSION_MAJOR, ORACLE_VERSION_MINOR);
}

#[oracle(aztec_utl_assertCompatibleOracleVersion)]
unconstrained fn assert_compatible_oracle_version_oracle(version: Field) {}
#[oracle(aztec_utl_assertCompatibleOracleVersionV2)]
Comment thread
benesjan marked this conversation as resolved.
unconstrained fn assert_compatible_oracle_version_oracle(major: Field, minor: Field) {}

mod test {
use super::{assert_compatible_oracle_version_oracle, ORACLE_VERSION};
use super::{assert_compatible_oracle_version_oracle, ORACLE_VERSION_MAJOR, ORACLE_VERSION_MINOR};

#[test]
unconstrained fn compatible_oracle_version() {
assert_compatible_oracle_version_oracle(ORACLE_VERSION);
assert_compatible_oracle_version_oracle(ORACLE_VERSION_MAJOR, ORACLE_VERSION_MINOR);
}

#[test(should_fail_with = "Incompatible aztec cli version:")]
unconstrained fn incompatible_oracle_version() {
let arbitrary_incorrect_version = 318183437;
assert_compatible_oracle_version_oracle(arbitrary_incorrect_version);
unconstrained fn incompatible_oracle_version_major() {
let arbitrary_incorrect_major = 318183437;
assert_compatible_oracle_version_oracle(arbitrary_incorrect_major, ORACLE_VERSION_MINOR);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::{oracle::version::ORACLE_VERSION, test::helpers::test_environment::TestEnvironment};
use crate::{
oracle::version::{ORACLE_VERSION_MAJOR, ORACLE_VERSION_MINOR},
test::helpers::test_environment::TestEnvironment,
};
use std::test::OracleMock;

#[test]
Expand Down Expand Up @@ -39,10 +42,10 @@ unconstrained fn private_public_and_utility_context_share_default_chain_id() {

#[test]
unconstrained fn oracle_version_is_checked_upon_env_creation() {
let mock = OracleMock::mock("aztec_utl_assertCompatibleOracleVersion");
let mock = OracleMock::mock("aztec_utl_assertCompatibleOracleVersionV2");

let _env = TestEnvironment::new();

assert_eq(mock.times_called(), 1);
assert_eq(mock.get_last_params::<Field>(), ORACLE_VERSION);
assert_eq(mock.get_last_params::<(Field, Field)>(), (ORACLE_VERSION_MAJOR, ORACLE_VERSION_MINOR));
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
/// The ORACLE_VERSION constant is used to check that the oracle interface is in sync between PXE and Aztec.nr. We need
Comment thread
benesjan marked this conversation as resolved.
/// to version the oracle interface to ensure that developers get a reasonable error message if they use incompatible
/// versions of Aztec.nr and PXE. The TypeScript counterpart is in `oracle_version.ts`.
/// The oracle version constants are used to check that the oracle interface is in sync between PXE and Aztec.nr.
/// We version the oracle interface as `major.minor` where:
/// - `major` = backward-breaking changes (must match exactly between PXE and Aztec.nr)
/// - `minor` = oracle additions (non-breaking; PXE minor >= contract minor)
///
/// The TypeScript counterparts are in `oracle_version.ts`.
///
/// @dev Whenever a contract function or Noir test is run, the `aztec_utl_assertCompatibleOracleVersion` oracle is
/// called and if the oracle version is incompatible an error is thrown.
pub global ORACLE_VERSION: Field = 22;
/// called. If the major version is incompatible, an error is thrown immediately. The minor version is recorded by
/// the PXE and used to provide helpful error messages if a contract calls an oracle that doesn't exist. We don't throw
/// immediately if AZTEC_NR_MINOR > PXE_MINOR because if a contract is updated to use a newer Aztec.nr dependency
/// without actually using any of the new oracles then there is no reason to throw.
pub global ORACLE_VERSION_MAJOR: Field = 22;
pub global ORACLE_VERSION_MINOR: Field = 0;

/// Asserts that the version of the oracle is compatible with the version expected by the contract.
pub fn assert_compatible_oracle_version() {
Expand All @@ -16,23 +23,23 @@ pub fn assert_compatible_oracle_version() {
}

unconstrained fn assert_compatible_oracle_version_wrapper() {
assert_compatible_oracle_version_oracle(ORACLE_VERSION);
assert_compatible_oracle_version_oracle(ORACLE_VERSION_MAJOR, ORACLE_VERSION_MINOR);
}

#[oracle(aztec_utl_assertCompatibleOracleVersion)]
unconstrained fn assert_compatible_oracle_version_oracle(version: Field) {}
#[oracle(aztec_utl_assertCompatibleOracleVersionV2)]
unconstrained fn assert_compatible_oracle_version_oracle(major: Field, minor: Field) {}

mod test {
use super::{assert_compatible_oracle_version_oracle, ORACLE_VERSION};
use super::{assert_compatible_oracle_version_oracle, ORACLE_VERSION_MAJOR, ORACLE_VERSION_MINOR};

#[test]
unconstrained fn compatible_oracle_version() {
assert_compatible_oracle_version_oracle(ORACLE_VERSION);
assert_compatible_oracle_version_oracle(ORACLE_VERSION_MAJOR, ORACLE_VERSION_MINOR);
}

#[test(should_fail_with = "Incompatible aztec cli version:")]
unconstrained fn incompatible_oracle_version() {
let arbitrary_incorrect_version = 318183437;
assert_compatible_oracle_version_oracle(arbitrary_incorrect_version);
unconstrained fn incompatible_oracle_version_major() {
let arbitrary_incorrect_major = 318183437;
assert_compatible_oracle_version_oracle(arbitrary_incorrect_major, ORACLE_VERSION_MINOR);
}
}
8 changes: 4 additions & 4 deletions yarn-project/pxe/src/bin/check_oracle_version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import { ORACLE_INTERFACE_HASH } from '../oracle_version.js';
*
* The Oracle interface needs to be versioned to ensure compatibility between Aztec.nr and PXE. This function computes
* a hash of the Oracle interface and compares it against a known hash. If they don't match, it means the interface has
* changed and the ORACLE_VERSION constant needs to be incremented and the ORACLE_INTERFACE_HASH constant needs to be
* updated.
* changed and the oracle version needs to be bumped:
* - If the change is backward-breaking (e.g. removing/renaming an oracle), bump ORACLE_VERSION_MAJOR.
* - If the change is an oracle addition (non-breaking), bump ORACLE_VERSION_MINOR.
*
* TODO(#16581): The following only takes into consideration changes to the oracles defined in Oracle.ts and omits TXE
* oracles. Ensure this checks TXE oracles as well. This hasn't been implemented yet since we don't have a clean TXE
Expand All @@ -26,9 +27,8 @@ function assertOracleInterfaceMatches(): void {
// We use keccak256 here just because we already have it in the dependencies.
const oracleInterfaceHash = keccak256String(oracleInterfaceSignature);
if (oracleInterfaceHash !== ORACLE_INTERFACE_HASH) {
// This check exists only to notify you when you need to update the ORACLE_VERSION constant.
throw new Error(
`The Oracle interface has changed, which implies a breaking change in the aztec.nr/PXE oracle interface. Update ORACLE_INTERFACE_HASH to ${oracleInterfaceHash} and bump ORACLE_VERSION in pxe/src/oracle_version.ts.`,
`The Oracle interface has changed. Update ORACLE_INTERFACE_HASH to ${oracleInterfaceHash} in pxe/src/oracle_version.ts and bump the oracle version (ORACLE_VERSION_MAJOR for breaking changes, ORACLE_VERSION_MINOR for oracle additions).`,
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export interface IMiscOracle {
isMisc: true;

getRandomField(): Fr;
assertCompatibleOracleVersion(version: number): void;
assertCompatibleOracleVersion(major: number, minor: number): void;
log(level: number, message: string, fields: Fr[]): Promise<void>;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import type { Oracle } from './oracle.js';
* TODO(F-416): Remove these aliases on v5 when protocol contracts are redeployed.
*/
export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback {
// If you are about to add a new mapping ensure that the old oracle name is different from the new one - this can
// commonly occur when only the args are getting modified.
Comment thread
benesjan marked this conversation as resolved.
return {
// Simple prefix renames (privateXxx/utilityXxx → aztec_prv_/aztec_utl_)
utilityLog: (
Expand All @@ -19,7 +21,11 @@ export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback {
fields: ACVMField[],
): Promise<ACVMField[]> => oracle.aztec_utl_log(level, message, _ignoredFieldsSize, fields),
utilityAssertCompatibleOracleVersion: (version: ACVMField[]): Promise<ACVMField[]> =>
oracle.aztec_utl_assertCompatibleOracleVersion(version),
oracle.aztec_utl_assertCompatibleOracleVersionV2(version, [toACVMField(0)]),
// Old 1-arg oracle before minor/major split. Maps to V2 with minor=0.
// eslint-disable-next-line camelcase
aztec_utl_assertCompatibleOracleVersion: (version: ACVMField[]): Promise<ACVMField[]> =>
oracle.aztec_utl_assertCompatibleOracleVersionV2(version, [toACVMField(0)]),
utilityLoadCapsule: (
contractAddress: ACVMField[],
slot: ACVMField[],
Expand Down
62 changes: 59 additions & 3 deletions yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { AztecAddress } from '@aztec/stdlib/aztec-address';
import { BlockHash } from '@aztec/stdlib/block';
import { ContractClassLog, ContractClassLogFields } from '@aztec/stdlib/logs';

import { ORACLE_VERSION_MAJOR, ORACLE_VERSION_MINOR } from '../../oracle_version.js';
import type { IMiscOracle, IPrivateExecutionOracle, IUtilityExecutionOracle } from './interfaces.js';
import { buildLegacyOracleCallbacks } from './legacy_oracle_mappings.js';
import { packAsHintedNote } from './note_packing_utils.js';
Expand Down Expand Up @@ -111,12 +112,67 @@ export class Oracle {
return acc;
}, {} as ACIRCallback);

return { ...callback, ...buildLegacyOracleCallbacks(this) };
const allCallbacks = { ...callback, ...buildLegacyOracleCallbacks(this) };

// Wrap in a Proxy to intercept access to missing oracle names and provide enhanced error messages when the
// contract's minor version is higher than the PXE's (i.e. the contract expects oracles that were added in a newer
// minor version).
const handler = this.handler;
return new Proxy(allCallbacks, {
Comment thread
benesjan marked this conversation as resolved.
get(target, prop: string) {
if (prop in target) {
return target[prop];
}
// Return a function that throws with an enhanced error message if applicable
return () => {
type NonOracleFunctionGetContractOracleVersion = {
nonOracleFunctionGetContractOracleVersion(): { major: number; minor: number } | undefined;
};

let contractVersion = undefined;
if ('nonOracleFunctionGetContractOracleVersion' in handler) {
contractVersion = (
handler as unknown as NonOracleFunctionGetContractOracleVersion
).nonOracleFunctionGetContractOracleVersion();
}
if (!contractVersion) {
throw new Error(
`Oracle '${prop}' not found and the contract's oracle version is unknown (the version check oracle ` +
`was not called before '${prop}'). This usually means the contract was not compiled with the ` +
`#[aztec] macro, which injects the version check as the first oracle call in every private/utility ` +
`external function. If you're using a custom entry point, ensure assert_compatible_oracle_version() ` +
`is called before any other oracle calls. See https://docs.aztec.network/errors/8`,
);
} else if (contractVersion.minor > ORACLE_VERSION_MINOR) {
throw new Error(
`Oracle '${prop}' not found.` +
` This usually means the contract requires a newer private execution environment than you have.` +
` Upgrade your private execution environment to a compatible version. The contract was compiled with` +
` Aztec.nr oracle version ${contractVersion.major}.${contractVersion.minor}, but this private` +
` execution environment only supports up to ${ORACLE_VERSION_MAJOR}.${ORACLE_VERSION_MINOR}.` +
` See https://docs.aztec.network/errors/8`,
);
} else {
throw new Error(
`Oracle '${prop}' not found.` +
` The contract's oracle version (${contractVersion.major}.${contractVersion.minor}) is compatible` +
` with this private execution environment (${ORACLE_VERSION_MAJOR}.${ORACLE_VERSION_MINOR}), so all` +
` standard oracles should be available. This could mean the contract was compiled against a modified` +
` version of Aztec.nr, or that it references an oracle that does not exist.` +
` See https://docs.aztec.network/errors/8`,
);
Comment thread
benesjan marked this conversation as resolved.
}
};
},
});
}

// eslint-disable-next-line camelcase
aztec_utl_assertCompatibleOracleVersion([version]: ACVMField[]) {
this.handlerAsMisc().assertCompatibleOracleVersion(Fr.fromString(version).toNumber());
aztec_utl_assertCompatibleOracleVersionV2([major]: ACVMField[], [minor]: ACVMField[]) {
this.handlerAsMisc().assertCompatibleOracleVersion(
Fr.fromString(major).toNumber(),
Fr.fromString(minor).toNumber(),
);
return Promise.resolve([]);
}

Expand Down
Loading
Loading