Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
a8e71dc
test(end-to-end): onchain message delivery harness (mode x secret-sou…
vezenovm Jun 29, 2026
082a4ec
refactor: rename ConstrainedDeliveryTest contract to OnchainDeliveryTest
vezenovm Jun 29, 2026
7d9d17a
Merge branch 'merge-train/fairies-v5' into mv/e2e-onchain-delivery-ha…
vezenovm Jun 29, 2026
458ec6c
fmt
vezenovm Jun 29, 2026
6d7d6df
chore(end-to-end): restore #24281 timing_env testEnvironment
vezenovm Jun 29, 2026
f6f432e
test cleanup
vezenovm Jun 29, 2026
7a75fed
test(end-to-end): pin rejected-source and cross-mode handshake reuse
vezenovm Jun 29, 2026
648c633
package json field
vezenovm Jun 29, 2026
c9e93cd
more cleanup
vezenovm Jun 29, 2026
50d6635
Merge branch 'merge-train/fairies-v5' into mv/e2e-onchain-delivery-ha…
vezenovm Jun 29, 2026
e4d7905
lint
vezenovm Jun 29, 2026
c68be78
Merge remote-tracking branch 'origin/mv/e2e-onchain-delivery-harness'…
vezenovm Jun 29, 2026
47ebeea
test(end-to-end): pin onchain-delivery discovery boundaries
vezenovm Jun 29, 2026
2bcdacb
more tests and improved harness
vezenovm Jun 29, 2026
aa23c9a
Merge branch 'merge-train/fairies-v5' into mv/e2e-onchain-delivery-ha…
vezenovm Jun 29, 2026
27dd6b1
cleanup
vezenovm Jun 29, 2026
4c8289a
test(end-to-end): pin reverse cross-mode handshake reuse
vezenovm Jun 29, 2026
27ad174
improve comments
vezenovm Jun 29, 2026
5210aaa
test cleanup
vezenovm Jun 30, 2026
f5c8a4e
move tests
vezenovm Jun 30, 2026
4e06e36
cleanup log test
vezenovm Jun 30, 2026
922952a
fix(pxe): remove unused recipientTaggingStore var in log_service.test.ts
vezenovm Jun 30, 2026
9e769aa
fix(end-to-end): un-nest the arbitrary-secret rejection test
vezenovm Jul 1, 2026
481dac6
docs(end-to-end): explain why the arbitrary-secret test stays top-level
vezenovm Jul 1, 2026
d6ae4b1
fix(end-to-end): address onchain-delivery review feedback
vezenovm Jul 1, 2026
dfc1ff1
Merge branch 'merge-train/fairies-v5' into mv/e2e-onchain-delivery-ha…
vezenovm Jul 1, 2026
eea90a9
fix(end-to-end): assert handshake-reuse hook calls instead of throwing
vezenovm Jul 1, 2026
b547463
fix(pxe): update log_service.test.ts to renamed AppTaggingSecret.comp…
vezenovm Jul 1, 2026
4ec5ce0
Merge branch 'merge-train/fairies-v5' into mv/e2e-onchain-delivery-ha…
vezenovm Jul 1, 2026
fe1d77d
Merge branch 'merge-train/fairies-v5' into mv/e2e-onchain-delivery-ha…
vezenovm Jul 2, 2026
09392c9
fix(ci): wait for dockerd to finish removing a stale container before…
vezenovm Jul 2, 2026
64e43e4
empty
vezenovm Jul 2, 2026
bb65ad0
Merge remote-tracking branch 'origin/mv/e2e-onchain-delivery-harness'…
vezenovm Jul 2, 2026
23ed748
.
vezenovm Jul 2, 2026
5861355
.
vezenovm Jul 2, 2026
cb3fd61
Merge branch 'merge-train/fairies-v5' into mv/e2e-onchain-delivery-ha…
vezenovm Jul 2, 2026
0184e78
fix(pxe): pass derived account privacy keys to addAccount in log_serv…
vezenovm Jul 2, 2026
59e55dd
fix(wallet-sdk): use derived contract address in registerContract
AztecBot Jul 2, 2026
e5d2b58
Merge branch 'merge-train/fairies-v5' into mv/e2e-onchain-delivery-ha…
vezenovm Jul 6, 2026
c524be7
fix(e2e): stop emitting every nested automine test cmd twice
AztecBot Jul 6, 2026
007c178
revert(ci): drop docker_isolate polling loop; dedup makes it unnecessary
AztecBot Jul 6, 2026
05f7619
Merge branch 'merge-train/fairies-v5' into mv/e2e-onchain-delivery-ha…
vezenovm Jul 6, 2026
65c37db
Apply suggestion from @vezenovm
vezenovm Jul 6, 2026
14a6b73
Apply suggestions from code review
vezenovm Jul 6, 2026
5b73de6
Update yarn-project/end-to-end/src/automine/delivery/constrained.test.ts
vezenovm Jul 6, 2026
a67c64f
Update yarn-project/end-to-end/src/automine/delivery/constrained.test.ts
vezenovm Jul 6, 2026
77acbbb
Update yarn-project/end-to-end/src/automine/delivery/handshake_reuse.…
vezenovm Jul 6, 2026
dd8a1b8
Update yarn-project/end-to-end/src/automine/delivery/handshake_reuse.…
vezenovm Jul 6, 2026
8609059
comments cleanup
vezenovm Jul 6, 2026
cdd254f
merge conflicts w/ fairies-v5
vezenovm Jul 6, 2026
25dff72
Pass signing
vezenovm Jul 6, 2026
06fd3cc
Apply suggestion from @vezenovm
vezenovm Jul 6, 2026
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
2 changes: 1 addition & 1 deletion noir-projects/noir-contracts/Nargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ members = [
"contracts/test/benchmarking_contract",
"contracts/test/calldata_limit_test_contract",
"contracts/test/child_contract",
"contracts/test/constrained_delivery_test_contract",
"contracts/test/onchain_delivery_test_contract",
"contracts/test/counter/counter_contract",
"contracts/test/custom_message_contract",
"contracts/test/custom_sync_state_contract",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "constrained_delivery_test_contract"
name = "onchain_delivery_test_contract"
authors = [""]
compiler_version = ">=0.25.0"
type = "contract"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
//! Thin wrappers around constrained delivery for TXE tests.
//! Thin wrappers around onchain message delivery for tests.
use aztec::macros::aztec;

mod test;

#[aztec]
pub contract ConstrainedDeliveryTest {
pub contract OnchainDeliveryTest {
use aztec::{
macros::{events::event, functions::external, storage::storage},
messages::delivery::MessageDelivery,
Expand Down Expand Up @@ -56,6 +56,16 @@ pub contract ConstrainedDeliveryTest {
self.emit(DeliveryEvent { value }).deliver_to(recipient, MessageDelivery::onchain_constrained());
}

#[external("private")]
fn emit_note_unconstrained(recipient: AztecAddress, value: Field) {
self.storage.notes.at(recipient).insert(FieldNote { value }).deliver(MessageDelivery::onchain_unconstrained());
}

#[external("private")]
fn emit_event_unconstrained(recipient: AztecAddress, value: Field) {
self.emit(DeliveryEvent { value }).deliver_to(recipient, MessageDelivery::onchain_unconstrained());
}

#[external("private")]
fn emit_two_events(recipient: AztecAddress) {
self.emit(DeliveryEvent { value: 1 }).deliver_to(recipient, MessageDelivery::onchain_constrained());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! bootstrap the app-siloed handshake secret, reserve the next per-secret index, constrain that index, emit the
//! sequence nullifier, then emit the tagged private log. The malformed-log test lands a constrained tag without its
//! sequence nullifier so the next real delivery must reject the broken sequence.
use crate::ConstrainedDeliveryTest;
use crate::OnchainDeliveryTest;

use aztec::{
protocol::address::AztecAddress,
Expand All @@ -24,7 +24,7 @@ unconstrained fn setup() -> (TestEnvironment, AztecAddress, AztecAddress, AztecA
.without_initializer();
assert_eq(registry_address, STANDARD_HANDSHAKE_REGISTRY_ADDRESS);

let test_address = env.deploy("ConstrainedDeliveryTest").without_initializer();
let test_address = env.deploy("OnchainDeliveryTest").without_initializer();

(env, registry_address, test_address, sender, recipient)
}
Expand All @@ -38,7 +38,7 @@ unconstrained fn authorizing(registry_address: AztecAddress) -> CallPrivateOptio
#[test]
unconstrained fn rehandshake_replaces_registry_secret_for_future_delivery() {
let (env, registry_address, test_address, sender, recipient) = setup();
let test_contract = ConstrainedDeliveryTest::at(test_address);
let test_contract = OnchainDeliveryTest::at(test_address);
let registry = HandshakeRegistry::at(registry_address);

env.call_private_opts(sender, authorizing(registry_address), test_contract.emit_event(recipient, 1));
Expand Down Expand Up @@ -69,7 +69,7 @@ unconstrained fn rehandshake_replaces_registry_secret_for_future_delivery() {
#[test(should_fail_with = "reading an unknown nullifier")]
unconstrained fn fails_at_index_above_zero_without_prior_nullifier() {
let (env, registry_address, test_address, sender, recipient) = setup();
let test_contract = ConstrainedDeliveryTest::at(test_address);
let test_contract = OnchainDeliveryTest::at(test_address);
let registry = HandshakeRegistry::at(registry_address);

let _ = env.call_private(sender, registry.non_interactive_handshake(sender, recipient));
Expand Down
3 changes: 3 additions & 0 deletions yarn-project/end-to-end/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ function test_cmds {

local tests=(
# List all standalone and nested tests, except for the ones listed above.
# Keep these globs non-overlapping: docker_isolate derives the container name from the test path, so a
# duplicated path emits two sibling invocations racing on the same name — you'll see docker "Conflict,
# name already in use" errors, or one copy's docker rm -f killing the other's live container mid-run.
src/automine/*.test.ts
src/automine/!(simulation)/**/*.test.ts
src/automine/simulation/!(avm_simulator).test.ts
Expand Down

@vezenovm vezenovm Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Renamed this file to keep constrained specific tests (unconstrained secret rejection + concurrency/batching)

Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
import { type InitialAccountData, generateSchnorrAccounts } from '@aztec/accounts/testing';
import { NO_FROM } from '@aztec/aztec.js/account';
import type { AztecAddress } from '@aztec/aztec.js/addresses';
import { BatchCall } from '@aztec/aztec.js/contracts';
import type { AztecNode } from '@aztec/aztec.js/node';
import type { Wallet } from '@aztec/aztec.js/wallet';
import { BlockNumber } from '@aztec/foundation/branded-types';
import { Point } from '@aztec/foundation/curves/grumpkin';
import { HandshakeRegistryContract } from '@aztec/noir-contracts.js/HandshakeRegistry';
import {
ConstrainedDeliveryTestContract,
type DeliveryEvent,
} from '@aztec/noir-test-contracts.js/ConstrainedDeliveryTest';
import { OnchainDeliveryTestContract } from '@aztec/noir-test-contracts.js/OnchainDeliveryTest';
import { STANDARD_HANDSHAKE_REGISTRY_ADDRESS } from '@aztec/standard-contracts/handshake-registry/constants';
import type { AztecNodeDebug } from '@aztec/stdlib/interfaces/client';

import { jest } from '@jest/globals';

import { AUTOMINE_E2E_OPTS } from './fixtures/fixtures.js';
import { ensureHandshakeRegistryPublished, setup, setupPXEAndGetWallet } from './fixtures/setup.js';
import { TestWallet } from './test-wallet/test_wallet.js';
import { AUTOMINE_E2E_OPTS } from '../../fixtures/fixtures.js';
import { ensureHandshakeRegistryPublished, setup } from '../../fixtures/setup.js';

describe('constrained delivery', () => {
// Delivery-method-specific tests that don't fit the generic (strategy, mode) matrix in `onchain.test.ts`
describe('delivery/constrained', () => {
jest.setTimeout(300_000);

let teardown: () => Promise<void>;
Expand All @@ -30,7 +23,7 @@ describe('constrained delivery', () => {
let batchRecipient2: AztecAddress;
let batchRecipient3: AztecAddress;
let batchRecipient4: AztecAddress;
let contract: ConstrainedDeliveryTestContract;
let contract: OnchainDeliveryTestContract;
let registry: HandshakeRegistryContract;

beforeAll(async () => {
Expand All @@ -41,7 +34,7 @@ describe('constrained delivery', () => {
} = await setup(6, { ...AUTOMINE_E2E_OPTS }));

await ensureHandshakeRegistryPublished(wallet, sender);
({ contract } = await ConstrainedDeliveryTestContract.deploy(wallet).send({ from: sender }));
({ contract } = await OnchainDeliveryTestContract.deploy(wallet).send({ from: sender }));
registry = HandshakeRegistryContract.at(STANDARD_HANDSHAKE_REGISTRY_ADDRESS, wallet);
});

Expand Down Expand Up @@ -159,109 +152,32 @@ describe('constrained delivery', () => {
});
});

// A constrained message sent by an account on one PXE is discovered and read by a recipient whose account lives on a
// separate PXE, with no shared in-memory state: PXE B finds the message purely from on-chain logs plus the
// HandshakeRegistry. The two PXEs share one node, following the e2e_2_pxes pattern.
describe('cross-PXE constrained delivery', () => {
// This test builds its own PXE via setup() rather than reusing the wallet from the describe block above, because
// it needs a resolveTaggingSecretStrategy hook that only exists as a PXE-creation-time option.
describe('delivery/constrained: rejects unsound sources', () => {
jest.setTimeout(300_000);

let aztecNode: AztecNode & AztecNodeDebug;
let walletSender: TestWallet;
let walletRecipient: TestWallet;
let sender: AztecAddress;
let recipient: AztecAddress;
let additionallyFundedAccounts: InitialAccountData[];
let contractSender: ConstrainedDeliveryTestContract;
let teardownSender: () => Promise<void>;
let teardownRecipient: () => Promise<void>;

beforeAll(async () => {
// PXE A holds the sender. The recipient is funded at genesis here but created and deployed on PXE B below.
({
aztecNode,
additionallyFundedAccounts,
wallet: walletSender,
accounts: [sender],
teardown: teardownSender,
} = await setup(1, {
it('rejects a constrained send backed by an arbitrary secret', async () => {
const {
teardown,
wallet,
accounts: [sender, recipient],
} = await setup(2, {
...AUTOMINE_E2E_OPTS,
additionallyFundedAccounts: await generateSchnorrAccounts(1, 'schnorr'),
}));

// PXE B holds the recipient on the same node; the recipient account's keys live only here.
({ wallet: walletRecipient, teardown: teardownRecipient } = await setupPXEAndGetWallet(
aztecNode,
aztecNode,
{},
undefined,
'pxe-b',
));
const recipientAccount = await walletRecipient.createSchnorrAccount(
additionallyFundedAccounts[0].secret,
additionallyFundedAccounts[0].salt,
additionallyFundedAccounts[0].signingKey,
);
await (await recipientAccount.getDeployMethod()).send({ from: NO_FROM });
recipient = recipientAccount.address;

await ensureHandshakeRegistryPublished(walletSender, sender);
const { contract: deployed, instance } = await ConstrainedDeliveryTestContract.deploy(walletSender).send({
from: sender,
});
contractSender = deployed;

await ensureHandshakeRegistryPublished(walletRecipient, recipient);
await walletRecipient.registerContract(instance, ConstrainedDeliveryTestContract.artifact);
});

afterAll(async () => {
await teardownRecipient();
await teardownSender();
});

it('delivers multiple constrained events from PXE A that PXE B discovers', async () => {
// Distinct values prove PXE B decrypts each message's content, not merely that a tagged log arrived; delivering
// several on one sequence proves both the bootstrap send (index 0) and the reused-handshake sends are
// discovered. Constrained sends to one pair are strictly ordered, so they go one tx at a time.
const eventValues = [10n, 20n, 30n];
const blockNumbers: number[] = [];
for (const value of eventValues) {
const { receipt } = await contractSender.methods.emit_event(recipient, value).send({ from: sender });
blockNumbers.push(receipt.blockNumber!);
}

await walletRecipient.sync();

const events = await walletRecipient.getPrivateEvents<DeliveryEvent>(
ConstrainedDeliveryTestContract.events.DeliveryEvent,
{
contractAddress: contractSender.address,
fromBlock: BlockNumber(Math.min(...blockNumbers)),
toBlock: BlockNumber(Math.max(...blockNumbers) + 1),
scopes: [recipient],
pxeCreationOptions: {
hooks: {
resolveTaggingSecretStrategy: async () => ({ type: 'arbitrary-secret', secret: await Point.random() }),
},
},
);

const discovered = events.map(e => e.event.value);
expect(discovered.length).toBe(eventValues.length);
for (const value of eventValues) {
expect(discovered).toContainEqual(value);
}
});

it('delivers multiple constrained notes from PXE A that PXE B reads back', async () => {
// Same recipient and handshake as the events above, so these notes land at the following sequence indices.
const noteValues = [40n, 50n, 60n];
for (const value of noteValues) {
await contractSender.methods.emit_note(recipient, value).send({ from: sender });
});
try {
await ensureHandshakeRegistryPublished(wallet, sender);
const { contract } = await OnchainDeliveryTestContract.deploy(wallet).send({ from: sender });
await expect(contract.methods.emit_event(recipient, 1).send({ from: sender })).rejects.toThrow(
'an unconstrained tagging secret cannot back constrained delivery',
);
} finally {
await teardown();
}

await walletRecipient.sync();

const contractRecipient = ConstrainedDeliveryTestContract.at(contractSender.address, walletRecipient);
// Count proves every delivered note was discovered; the sum of distinct values proves each was decrypted.
const { result } = await contractRecipient.methods.get_note_values(recipient).simulate({ from: recipient });
const values: bigint[] = result.storage.slice(0, Number(result.len));
expect(values).toEqual(noteValues);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { AppTaggingSecretKind } from '@aztec/stdlib/logs';

import { buildMessageDeliveryTest } from './onchain_delivery_harness.js';

// Pins mode-agnostic handshake reuse: a handshake bootstrapped under one delivery mode must be reused when the other
// mode sends next.
describe('handshake_reuse', () => {
const forwardHookCalls: AppTaggingSecretKind[] = [];
buildMessageDeliveryTest({
strategy: 'non-interactive handshake',
mode: { events: 'constrained', notes: 'unconstrained' },
senderHook: ({ deliveryMode }) => {
forwardHookCalls.push(deliveryMode);
return Promise.resolve({ type: 'non-interactive-handshake' });
},
additionalTests: () => {
it('the strategy hook fires exactly once, to bootstrap the handshake on the first constrained send', () => {
expect(forwardHookCalls).toHaveLength(1);
expect(forwardHookCalls[0]).toBe(AppTaggingSecretKind.CONSTRAINED);
});
},
});

// Reverse direction also pins the constrained sequence to a fresh index 0 after an unconstrained bootstrap; a leaked
// unconstrained counter would make the first constrained send require a predecessor nullifier that does not exist.
const reverseHookCalls: AppTaggingSecretKind[] = [];
buildMessageDeliveryTest({
strategy: 'non-interactive handshake',
mode: { events: 'unconstrained', notes: 'constrained' },
senderHook: ({ deliveryMode }) => {
reverseHookCalls.push(deliveryMode);
return Promise.resolve({ type: 'non-interactive-handshake' });
},
additionalTests: () => {
it('the strategy hook fires exactly once, to bootstrap the handshake on the first unconstrained send', () => {
expect(reverseHookCalls).toHaveLength(1);
expect(reverseHookCalls[0]).toBe(AppTaggingSecretKind.UNCONSTRAINED);
});
},
});
});
47 changes: 47 additions & 0 deletions yarn-project/end-to-end/src/automine/delivery/onchain.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Point } from '@aztec/foundation/curves/grumpkin';

import { buildMessageDeliveryTest } from './onchain_delivery_harness.js';

describe('onchain delivery', () => {
Comment thread
vezenovm marked this conversation as resolved.
let arbitrarySecret: Point;

beforeAll(async () => {
arbitrarySecret = await Point.random();
});

buildMessageDeliveryTest({
strategy: 'non-interactive handshake',
mode: 'constrained',
senderHook: () => Promise.resolve({ type: 'non-interactive-handshake' }),
});

buildMessageDeliveryTest({
strategy: 'non-interactive handshake',
mode: 'unconstrained',
senderHook: () => Promise.resolve({ type: 'non-interactive-handshake' }),
});

buildMessageDeliveryTest({
strategy: 'arbitrary secret',
mode: 'unconstrained',
senderHook: () => Promise.resolve({ type: 'arbitrary-secret', secret: arbitrarySecret }),
recipientRegistration: async (recipientWallet, recipientAddress) => {
await recipientWallet.registerTaggingSecretSource({
kind: 'arbitrary-secret',
recipient: recipientAddress,
secret: arbitrarySecret,
});
},
Comment thread
vezenovm marked this conversation as resolved.
});

// With the recipient registering the sender,
// the recipient PXE reconstructs the address-derived tag and discovers the delivery.
buildMessageDeliveryTest({
strategy: 'address-derived',
mode: 'unconstrained',
senderHook: () => Promise.resolve({ type: 'address-derived' }),
recipientRegistration: async (recipientWallet, _recipientAddress, senderAddress) => {
await recipientWallet.registerSender(senderAddress);
},
});
});
Loading
Loading