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
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { AztecAddress } from '@aztec/aztec.js/addresses';
import type { ContractFunctionInteraction } from '@aztec/aztec.js/contracts';
import type { Wallet } from '@aztec/aztec.js/wallet';
import { ScopeTestContract } from '@aztec/noir-test-contracts.js/ScopeTest';

import { AutomineTestContext } from '../automine_test_context.js';

// Verifies that PXE note access and key-derivation are scoped per account: a different account
// cannot read another's notes or derive their nullifier hiding key. Uses a single node with
// AutomineSequencer and three accounts (alice, bob, charlie).
// AutomineSequencer and three accounts (alice, bob, charlie). The same isolation checks run for both
// the external-private and external-utility function variants, which differ only by a `_utility` suffix.
describe('automine/accounts/scope_isolation', () => {
let wallet: Wallet;
let accounts: AztecAddress[];
Expand All @@ -33,62 +35,44 @@ describe('automine/accounts/scope_isolation', () => {

afterAll(() => teardown());

// Tests for external private functions: read_note (scoped to owner) and get_nhk (scoped to key holder).
describe('external private', () => {
// Alice simulates read_note from her own scope; asserts the correct stored value is returned.
const variants: {
context: string;
readNote: (owner: AztecAddress) => ContractFunctionInteraction;
getNhk: (owner: AztecAddress) => ContractFunctionInteraction;
}[] = [
{
context: 'external private',
readNote: owner => contract.methods.read_note(owner),
getNhk: owner => contract.methods.get_nhk(owner),
},
{
context: 'external utility',
readNote: owner => contract.methods.read_note_utility(owner),
getNhk: owner => contract.methods.get_nhk_utility(owner),
},
];

describe.each(variants)('$context', ({ readNote, getNhk }) => {
// Alice reads her own note from her own scope; asserts the correct stored value is returned.
it('owner can read own notes', async () => {
const { result: value } = await contract.methods.read_note(alice).simulate({ from: alice });
const { result: value } = await readNote(alice).simulate({ from: alice });
expect(value).toEqual(ALICE_NOTE_VALUE);
});

// Bob attempts to read Alice's note from his scope; asserts simulation throws 'Failed to get a note'.
it('cannot read notes belonging to a different account', async () => {
await expect(contract.methods.read_note(alice).simulate({ from: bob })).rejects.toThrow('Failed to get a note');
await expect(readNote(alice).simulate({ from: bob })).rejects.toThrow('Failed to get a note');
});

// Bob attempts to derive Charlie's nullifier hiding key; asserts 'Key validation request denied'.
it('cannot access nullifier hiding key of a different account', async () => {
await expect(contract.methods.get_nhk(charlie).simulate({ from: bob })).rejects.toThrow(
'Key validation request denied',
);
await expect(getNhk(charlie).simulate({ from: bob })).rejects.toThrow('Key validation request denied');
});

// Both Alice and Bob read their own notes on the shared wallet; asserts each sees only their value.
it('each account can access their isolated state on a shared wallet', async () => {
const { result: aliceValue } = await contract.methods.read_note(alice).simulate({ from: alice });
const { result: bobValue } = await contract.methods.read_note(bob).simulate({ from: bob });

expect(aliceValue).toEqual(ALICE_NOTE_VALUE);
expect(bobValue).toEqual(BOB_NOTE_VALUE);
});
});

// Same isolation checks repeated for external utility functions (read_note_utility, get_nhk_utility).
describe('external utility', () => {
// Alice simulates read_note_utility from her own scope; asserts the correct stored value is returned.
it('owner can read own notes', async () => {
const { result: value } = await contract.methods.read_note_utility(alice).simulate({ from: alice });
expect(value).toEqual(ALICE_NOTE_VALUE);
});

// Bob attempts to read Alice's note via utility scope; asserts simulation throws 'Failed to get a note'.
it('cannot read notes belonging to a different account', async () => {
await expect(contract.methods.read_note_utility(alice).simulate({ from: bob })).rejects.toThrow(
'Failed to get a note',
);
});

// Bob attempts to derive Charlie's NHK via utility scope; asserts 'Key validation request denied'.
it('cannot access nullifier hiding key of a different account', async () => {
await expect(contract.methods.get_nhk_utility(charlie).simulate({ from: bob })).rejects.toThrow(
'Key validation request denied',
);
});

// Both Alice and Bob read via utility on the shared wallet; asserts each sees only their value.
it('each account can access their isolated state on a shared wallet', async () => {
const { result: aliceValue } = await contract.methods.read_note_utility(alice).simulate({ from: alice });
const { result: bobValue } = await contract.methods.read_note_utility(bob).simulate({ from: bob });
const { result: aliceValue } = await readNote(alice).simulate({ from: alice });
const { result: bobValue } = await readNote(bob).simulate({ from: bob });

expect(aliceValue).toEqual(ALICE_NOTE_VALUE);
expect(bobValue).toEqual(BOB_NOTE_VALUE);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Fr } from '@aztec/aztec.js/fields';
import { ImportTestContract } from '@aztec/noir-test-contracts.js/ImportTest';
import { TestContract } from '@aztec/noir-test-contracts.js/Test';

Expand Down Expand Up @@ -25,21 +26,37 @@ describe('automine/contracts/nested/importer', () => {
await t.teardown();
});

// Calls importerContract.call_no_args(testContract.address) and awaits inclusion.
// call_no_args routes a private call into Test.get_this_address; asserts the imported call returns the
// Test contract's own address, and that the tx is included on-chain.
it('calls a method no arguments', async () => {
logger.info(`Calling noargs on importer contract`);
const { result } = await importerContract.methods
.call_no_args(testContract.address)
.simulate({ from: defaultAccountAddress });
expect(result).toEqual(testContract.address);

await importerContract.methods.call_no_args(testContract.address).send({ from: defaultAccountAddress });
});

// Calls importerContract.call_public_fn(testContract.address) and awaits inclusion.
// call_public_fn enqueues Test.emit_nullifier_public(1); asserts the nullifier landed by checking that
// re-emitting the same nullifier on the Test contract is rejected as a duplicate.
it('calls a public function', async () => {
logger.info(`Calling public_fn on importer contract`);
await importerContract.methods.call_public_fn(testContract.address).send({ from: defaultAccountAddress });

await expect(
testContract.methods.emit_nullifier_public(new Fr(1)).simulate({ from: defaultAccountAddress }),
).rejects.toThrow(/duplicate nullifier/);
});

// Calls importerContract.pub_call_public_fn(testContract.address) and awaits inclusion.
// pub_call_public_fn calls Test.emit_nullifier_public(1) from a public function; asserts the nullifier
// landed by checking that re-emitting the same nullifier on the Test contract is rejected as a duplicate.
it('calls a public function from a public function', async () => {
logger.info(`Calling pub_public_fn on importer contract`);
await importerContract.methods.pub_call_public_fn(testContract.address).send({ from: defaultAccountAddress });

await expect(
testContract.methods.emit_nullifier_public(new Fr(1)).simulate({ from: defaultAccountAddress }),
).rejects.toThrow(/duplicate nullifier/);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import type { AztecAddress } from '@aztec/aztec.js/addresses';
import { BatchCall } from '@aztec/aztec.js/contracts';
import { Fr } from '@aztec/aztec.js/fields';
import { toBigIntBE } from '@aztec/foundation/bigint-buffer';
import { serializeToBuffer } from '@aztec/foundation/serialize';
import { ChildContract } from '@aztec/noir-test-contracts.js/Child';

import { AutomineTestContext } from '../../automine_test_context.js';

// Nested contract calls between Parent and Child. A single account runs a shared Parent/Child deployed in
// beforeAll via applyManualParentChild(); the public and enqueued-call suites redeploy a fresh Child per
// test because each asserts an absolute child storage value that only holds if the child starts at zero.
describe('automine/contracts/nested/manual_calls', () => {
const t = new AutomineTestContext();
let { wallet, parentContract, childContract, defaultAccountAddress, aztecNode } = t;

const getChildStoredValue = (child: { address: AztecAddress }) =>
aztecNode.getPublicStorageAt('latest', child.address, new Fr(1));

beforeAll(async () => {
await t.setup();
await t.applyManualParentChild();
({ wallet, parentContract, childContract, defaultAccountAddress, aztecNode } = t);
});

afterAll(async () => {
await t.teardown();
});

describe('private calls', () => {
// Routes a private call through the parent into child.value(0). Asserts the nested call returns the
// same preimage as calling child.value(0) directly, and that the tx is included on-chain.
it('performs a nested private call returning the child value', async () => {
const selector = await childContract.methods.value.selector();

const { result } = await parentContract.methods
.entry_point(childContract.address, selector)
.simulate({ from: defaultAccountAddress });
const { result: direct } = await childContract.methods.value(0n).simulate({ from: defaultAccountAddress });
expect(result).toEqual(direct);

await parentContract.methods.entry_point(childContract.address, selector).send({ from: defaultAccountAddress });
});
});

describe('public calls', () => {
let child: ChildContract;

beforeEach(async () => {
({ contract: child } = await ChildContract.deploy(wallet).send({ from: defaultAccountAddress }));
});

// Routes a public call through the parent into child.pub_inc_value(42); asserts the child's storage
// slot holds 42 afterwards, confirming the nested public call executed and wrote state.
it('performs public nested calls', async () => {
await parentContract.methods
.pub_entry_point(child.address, await child.methods.pub_inc_value.selector(), 42n)
.send({ from: defaultAccountAddress });
expect(await getChildStoredValue(child)).toEqual(new Fr(42n));
});

// Regression for https://github.com/AztecProtocol/aztec-packages/issues/640
// Calls pub_entry_point_twice so pub_inc_value runs twice in one tx; asserts storage is 84 (not 42).
it('reads fresh value after write within the same tx', async () => {
await parentContract.methods
.pub_entry_point_twice(child.address, await child.methods.pub_inc_value.selector(), 42n)
.send({ from: defaultAccountAddress });
expect(await getChildStoredValue(child)).toEqual(new Fr(84n));
});

// Regression for https://github.com/AztecProtocol/aztec-packages/issues/1645
// Executes a public call first and then a private call (which enqueues another public call)
// through the account contract, if the account entrypoint behaves properly, it will honor
// this order and not run the private call first which results in the public calls being inverted.
// Batches pub_set_value(20) and parent.enqueue(pub_set_value(40)); reads public logs to assert [20, 40].
it('executes public calls in expected order', async () => {
const pubSetValueSelector = await child.methods.pub_set_value.selector();
const actions = [
child.methods.pub_set_value(20n),
parentContract.methods.enqueue_call_to_child(child.address, pubSetValueSelector, 40n),
];

const { receipt: tx } = await new BatchCall(wallet, actions).send({ from: defaultAccountAddress });
const block = (await aztecNode.getBlock({ number: tx.blockNumber! }, { includeTransactions: true }))!;
const allPublicLogs = block.body.txEffects.flatMap(effect => effect.publicLogs);
const processedLogs = allPublicLogs.map(log => toBigIntBE(serializeToBuffer(log.getEmittedFields())));
expect(processedLogs).toEqual([20n, 40n]);
expect(await getChildStoredValue(child)).toEqual(new Fr(40n));
});
});

describe('enqueued public calls', () => {
let child: ChildContract;

beforeEach(async () => {
({ contract: child } = await ChildContract.deploy(wallet).send({ from: defaultAccountAddress }));
});

// Enqueues one pub_inc_value(42) call via the parent and asserts child storage equals 42.
it('enqueues a single public call', async () => {
await parentContract.methods
.enqueue_call_to_child(child.address, await child.methods.pub_inc_value.selector(), 42n)
.send({ from: defaultAccountAddress });
expect(await getChildStoredValue(child)).toEqual(new Fr(42n));
});

// Enqueues pub_inc_value(42) then pub_inc_value(43) via enqueue_call_to_child_twice; asserts 85.
it('enqueues multiple public calls', async () => {
await parentContract.methods
.enqueue_call_to_child_twice(child.address, await child.methods.pub_inc_value.selector(), 42n)
.send({ from: defaultAccountAddress });
expect(await getChildStoredValue(child)).toEqual(new Fr(85n));
});

// Calls enqueue_call_to_pub_entry_point which enqueues pub_entry_point → pub_inc_value; asserts 42.
it('enqueues a public call with nested public calls', async () => {
await parentContract.methods
.enqueue_call_to_pub_entry_point(child.address, await child.methods.pub_inc_value.selector(), 42n)
.send({ from: defaultAccountAddress });
expect(await getChildStoredValue(child)).toEqual(new Fr(42n));
});

// Calls enqueue_calls_to_pub_entry_point which enqueues pub_entry_point twice; asserts 85.
it('enqueues multiple public calls with nested public calls', async () => {
await parentContract.methods
.enqueue_calls_to_pub_entry_point(child.address, await child.methods.pub_inc_value.selector(), 42n)
.send({ from: defaultAccountAddress });
expect(await getChildStoredValue(child)).toEqual(new Fr(85n));
});
});
});

This file was deleted.

This file was deleted.

Loading
Loading