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
5 changes: 3 additions & 2 deletions .github/workflows/aztec-cli-acceptance-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ jobs:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
run: |
export CI=1
./ci3/slack_notify "#team-fairies" \
"Aztec CLI Acceptance Test passed for version ${VERSION} :white_check_mark:"
./ci3/slack_notify \
"Aztec CLI Acceptance Test passed for version ${VERSION} :white_check_mark:" \
"#team-fairies"
- name: Notify Slack and dispatch ClaudeBox on failure
if: failure() && github.event_name != 'workflow_dispatch'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ impl<Storage, CallSelf, CallSelfStatic, CallInternal> ContractSelfPublic<Storage
///
/// Public event emission is achieved by emitting public transaction logs. A total of `N+1` fields are emitted,
/// where `N` is the serialization length of the event.
pub fn emit<Event>(&mut self, event: Event)
pub unconstrained fn emit<Event>(&mut self, event: Event)
where
Event: EventInterface + Serialize,
{
Expand Down
3 changes: 2 additions & 1 deletion noir-projects/aztec-nr/aztec/src/event/event_emission.nr
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ where
}

/// Equivalent to `self.emit(event)`: see [`crate::contract_self::ContractSelfPublic::emit`].
pub fn emit_event_in_public<Event>(context: PublicContext, event: Event)
#[inline_never]
pub unconstrained fn emit_event_in_public<Event>(context: PublicContext, event: Event)
where
Event: EventInterface + Serialize,
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,23 @@

use aztec::macros::aztec;

mod pow_note;
mod test;

#[aztec]
pub contract NestedUtility {
use aztec::macros::functions::external;
use aztec::protocol::address::AztecAddress;
use crate::pow_note::PowNote;
use aztec::macros::{functions::external, storage::storage};
use aztec::{
messages::message_delivery::MessageDelivery,
protocol::address::AztecAddress,
state_vars::{Owned, PrivateMutable},
};

#[storage]
struct Storage<Context> {
pow_args: Owned<PrivateMutable<PowNote, Context>, Context>,
}

#[external("utility")]
unconstrained fn pow_utility(x: Field, n: u32) -> Field {
Expand All @@ -24,6 +35,28 @@ pub contract NestedUtility {
self.call(NestedUtility::at(target).pow_utility(x, n))
}

/// Stores the base and exponent for pow in a private note.
#[external("private")]
fn set_pow_args(x: Field, n: Field) {
let owner = self.msg_sender();
self.storage.pow_args.at(owner).initialize_or_replace(|_| PowNote { x, n }).deliver(
MessageDelivery.ONCHAIN_CONSTRAINED,
);
}

/// Reads x and n from storage and computes x^n.
#[external("utility")]
unconstrained fn pow_from_storage(owner: AztecAddress) -> Field {
let note = self.storage.pow_args.at(owner).view_note();
self.call_self.pow_utility(note.x, note.n as u32)
}

/// Cross-contract version: calls pow_from_storage on the target contract.
#[external("utility")]
unconstrained fn delegate_pow_from_storage(target: AztecAddress, owner: AztecAddress) -> Field {
self.call(NestedUtility::at(target).pow_from_storage(owner))
}

#[external("private")]
fn pow_private(x: Field, n: u32) -> Field {
// Safety: this is a test contract; the unconstrained result is returned directly
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use aztec::{macros::notes::note, protocol::traits::{Deserialize, Packable, Serialize}};

#[derive(Deserialize, Eq, Packable, Serialize)]
#[note]
pub struct PowNote {
pub x: Field,
pub n: Field,
}
7 changes: 6 additions & 1 deletion playground/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ const chunkSizeValidator = (limits: ChunkSizeLimit[]): Plugin => {
configResolved(resolvedConfig) {
config = resolvedConfig;
},
closeBundle() {
// `writeBundle` is documented to fire AFTER the output bundle has been
// written to disk, whereas `closeBundle` (which we used previously) is the
// last hook to run and can fire before any chunks have been flushed in
// current vite/rollup versions — manifesting as ENOENT on `scandir 'dist'`
// for a build that otherwise transformed all modules cleanly.
writeBundle() {
const outDir = this.meta?.watchMode ? null : 'dist';
if (!outDir) return; // Skip in watch mode

Expand Down
14 changes: 14 additions & 0 deletions yarn-project/end-to-end/src/e2e_nested_utility_calls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,18 @@ describe('authorizeUtilityCall hook', () => {
callerContext: 'private',
});
});

it('syncs target contract notes on cross-contract utility call', async () => {
hookAllows = true;

// Store x=2, n=10 as private notes on contract B.
await contractB.methods.set_pow_args(2n, 10n).send({ from: defaultAccountAddress });

// Cross-contract call from A → B: B must be synced before the nested utility call
// so that B's notes (set above) are discovered.
const { result: crossContractResult } = await contractA.methods
.delegate_pow_from_storage(contractB.address, defaultAccountAddress)
.simulate({ from: defaultAccountAddress });
expect(crossContractResult).toEqual(2n ** 10n);
});
});
15 changes: 15 additions & 0 deletions yarn-project/kv-store/browser-stubs/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,21 @@ BufferPolyfill.from = function (value, encodingOrOffset, length) {
);
};

BufferPolyfill.compare = function (a, b) {
if (!ArrayBuffer.isView(a) || !ArrayBuffer.isView(b)) {
throw new TypeError('Arguments must be Buffers or Uint8Arrays');
}
const x = new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
const y = new Uint8Array(b.buffer, b.byteOffset, b.byteLength);
const len = Math.min(x.length, y.length);
for (let i = 0; i < len; i++) {
if (x[i] !== y[i]) {
return x[i] < y[i] ? -1 : 1;
}
}
return x.length < y.length ? -1 : x.length > y.length ? 1 : 0;
};

BufferPolyfill.alloc = function (size, fill, encoding) {
const buf = new BufferPolyfill(size);
if (fill !== undefined) {
Expand Down
10 changes: 10 additions & 0 deletions yarn-project/kv-store/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@
"./stores": "./dest/stores/index.js",
"./config": "./dest/config.js"
},
"imports": {
"#msgpackr": {
"browser": "msgpackr/index-no-eval",
"default": "msgpackr"
},
"#ordered-binary": {
"browser": "./dest/sqlite-opfs/internal/ordered-binary-browser.js",
"default": "ordered-binary"
}
},
"scripts": {
"build": "yarn clean && ../scripts/tsc.sh",
"build:dev": "../scripts/tsc.sh --watch",
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/kv-store/src/sqlite-opfs/array.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Encoder } from 'msgpackr';
import { Encoder } from '#msgpackr';
import { toBufferKey } from '#ordered-binary';
import { hash } from 'ohash';
import { toBufferKey } from 'ordered-binary';

import type { AztecAsyncArray } from '../interfaces/array.js';
import type { Value } from '../interfaces/common.js';
Expand Down
43 changes: 25 additions & 18 deletions yarn-project/kv-store/src/sqlite-opfs/encrypted_store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { describe, expect, it } from 'vitest';

import { mockLogger } from '../interfaces/utils.js';
import { SqliteEncryptionError } from './errors.js';
import { AztecSQLiteOPFSStore } from './store.js';

function randomKey(): Uint8Array {
Expand All @@ -24,16 +25,16 @@ function cloneKey(k: Uint8Array): Uint8Array {
describe('AztecSQLiteOPFSStore with encryption', () => {
it('rejects keys that are not 32 bytes', async () => {
const short = new Uint8Array(16);
await expect(AztecSQLiteOPFSStore.open(mockLogger, undefined, true, undefined, short)).rejects.toThrow(
/encryptionKey must be 32 bytes/,
);
const promise = AztecSQLiteOPFSStore.open(mockLogger, undefined, true, undefined, short);
await expect(promise).rejects.toThrow(SqliteEncryptionError);
await expect(promise).rejects.toMatchObject({ code: 'invalid_key_length' });
});

it('rejects encryption on ephemeral (:memory:) stores', async () => {
const key = randomKey();
await expect(AztecSQLiteOPFSStore.open(mockLogger, undefined, true, undefined, key)).rejects.toThrow(
/not supported for ephemeral/,
);
const promise = AztecSQLiteOPFSStore.open(mockLogger, undefined, true, undefined, key);
await expect(promise).rejects.toThrow(SqliteEncryptionError);
await expect(promise).rejects.toMatchObject({ code: 'encryption_not_supported_for_ephemeral' });
});

it('round-trips values with the same key (persistent)', async () => {
Expand All @@ -51,37 +52,43 @@ describe('AztecSQLiteOPFSStore with encryption', () => {
await b.delete();
});

it('fails to open with the wrong key', async () => {
it('fails to open with the wrong key (throws SqliteEncryptionError, code=decrypt_failed)', async () => {
const key = randomKey();
const name = `test-wrong-${Date.now()}`;
const dir = `/test-wrong-pool-${Date.now()}`;
const a = await AztecSQLiteOPFSStore.open(mockLogger, name, false, dir, cloneKey(key));
await a.openMap<string, string>('m').set('k1', 'v1');
await a.close();

await expect(async () => {
const b = await AztecSQLiteOPFSStore.open(mockLogger, name, false, dir, randomKey());
await b.openMap<string, string>('m').getAsync('k1');
await b.close();
}).rejects.toThrow();
let caught: unknown;
try {
await AztecSQLiteOPFSStore.open(mockLogger, name, false, dir, randomKey());
} catch (err) {
caught = err;
}
expect(caught).toBeInstanceOf(SqliteEncryptionError);
expect((caught as SqliteEncryptionError).code).toBe('decrypt_failed');

const c = await AztecSQLiteOPFSStore.open(mockLogger, name, false, dir, cloneKey(key));
await c.delete();
});

it('fails to open an encrypted DB without a key', async () => {
it('fails to open an encrypted DB without a key (throws SqliteEncryptionError, code=decrypt_failed)', async () => {
const key = randomKey();
const name = `test-nokey-${Date.now()}`;
const dir = `/test-nokey-pool-${Date.now()}`;
const a = await AztecSQLiteOPFSStore.open(mockLogger, name, false, dir, cloneKey(key));
await a.openMap<string, string>('m').set('k1', 'v1');
await a.close();

await expect(async () => {
const b = await AztecSQLiteOPFSStore.open(mockLogger, name, false, dir);
await b.openMap<string, string>('m').getAsync('k1');
await b.close();
}).rejects.toThrow();
let caught: unknown;
try {
await AztecSQLiteOPFSStore.open(mockLogger, name, false, dir);
} catch (err) {
caught = err;
}
expect(caught).toBeInstanceOf(SqliteEncryptionError);
expect((caught as SqliteEncryptionError).code).toBe('decrypt_failed');

const c = await AztecSQLiteOPFSStore.open(mockLogger, name, false, dir, cloneKey(key));
await c.delete();
Expand Down
44 changes: 44 additions & 0 deletions yarn-project/kv-store/src/sqlite-opfs/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Typed error surface for sqlite3mc-backed page-level encryption failures.
*
* Three concrete failure modes are surfaced:
*
* - `invalid_key_length`: caller-side pre-flight (key not 32 bytes).
* - `encryption_not_supported_for_ephemeral`: caller-side pre-flight (encryption was requested on an ephemeral
* `:memory:` store, which sqlite3mc does not support).
* - `decrypt_failed`: runtime failure raised when sqlite3mc cannot decode page 1 of an existing database. Covers
* both "wrong key supplied" and "no key supplied to an encrypted DB".
*/
export type SqliteEncryptionErrorCode =
| 'invalid_key_length'
| 'encryption_not_supported_for_ephemeral'
| 'decrypt_failed';

/**
* Error thrown by sqlite-opfs when an encryption operation fails.
**/
export class SqliteEncryptionError extends Error {
readonly code: SqliteEncryptionErrorCode;

constructor(code: SqliteEncryptionErrorCode, message: string, opts?: { cause?: unknown }) {
super(message, opts?.cause !== undefined ? { cause: opts.cause } : undefined);
this.name = 'SqliteEncryptionError';
this.code = code;
}
}

/**
* Strings raised by sqlite3mc when page 1 cannot be decoded.
**/
const SQLITE3MC_DECRYPT_ERROR_PATTERNS: readonly RegExp[] = [
/file is not a database/i,
/file is encrypted or is not a database/i,
];

/**
* Returns `true` if `message` matches one of the known sqlite3mc decrypt-failure
* strings.
**/
export function isDecryptFailureMessage(message: string): boolean {
return SQLITE3MC_DECRYPT_ERROR_PATTERNS.some(p => p.test(message));
}
2 changes: 2 additions & 0 deletions yarn-project/kv-store/src/sqlite-opfs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { initStoreForRollupAndSchemaVersion } from '../utils.js';
import { AztecSQLiteOPFSStore } from './store.js';

export { AztecSQLiteOPFSStore } from './store.js';
export { SqliteEncryptionError } from './errors.js';
export type { SqliteEncryptionErrorCode } from './errors.js';

export async function createStore(
name: string,
Expand Down
Loading
Loading