Skip to content

Commit 26d2643

Browse files
sklppy88benesjan
andauthored
feat: add api for inclusion proof of outgoing message in block #4562 (#4899)
Resolves #4562. --------- Co-authored-by: Jan Beneš <janbenes1234@gmail.com>
1 parent b327254 commit 26d2643

10 files changed

Lines changed: 231 additions & 7 deletions

File tree

.circleci/config.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,20 @@ jobs:
868868
aztec_manifest_key: end-to-end
869869
<<: *defaults_e2e_test
870870

871+
872+
e2e-outbox:
873+
docker:
874+
- image: aztecprotocol/alpine-build-image
875+
resource_class: small
876+
steps:
877+
- *checkout
878+
- *setup_env
879+
- run:
880+
name: "Test"
881+
command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_outbox.test.ts
882+
aztec_manifest_key: end-to-end
883+
<<: *defaults_e2e_test
884+
871885
uniswap-trade-on-l1-from-l2:
872886
steps:
873887
- *checkout
@@ -1399,6 +1413,7 @@ workflows:
13991413
- e2e-inclusion-proofs-contract: *e2e_test
14001414
- e2e-pending-note-hashes-contract: *e2e_test
14011415
- e2e-ordering: *e2e_test
1416+
- e2e-outbox: *e2e_test
14021417
- e2e-counter: *e2e_test
14031418
- e2e-private-voting: *e2e_test
14041419
- uniswap-trade-on-l1-from-l2: *e2e_test
@@ -1463,6 +1478,7 @@ workflows:
14631478
- e2e-inclusion-proofs-contract
14641479
- e2e-pending-note-hashes-contract
14651480
- e2e-ordering
1481+
- e2e-outbox
14661482
- e2e-counter
14671483
- e2e-private-voting
14681484
- uniswap-trade-on-l1-from-l2

yarn-project/aztec-node/src/aztec-node/server.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
Header,
3333
INITIAL_L2_BLOCK_NUM,
3434
L1_TO_L2_MSG_TREE_HEIGHT,
35+
L2_TO_L1_MESSAGE_LENGTH,
3536
NOTE_HASH_TREE_HEIGHT,
3637
NULLIFIER_TREE_HEIGHT,
3738
NullifierLeafPreimage,
@@ -44,7 +45,8 @@ import { AztecAddress } from '@aztec/foundation/aztec-address';
4445
import { createDebugLogger } from '@aztec/foundation/log';
4546
import { AztecKVStore } from '@aztec/kv-store';
4647
import { AztecLmdbStore } from '@aztec/kv-store/lmdb';
47-
import { initStoreForRollup } from '@aztec/kv-store/utils';
48+
import { initStoreForRollup, openTmpStore } from '@aztec/kv-store/utils';
49+
import { SHA256, StandardTree } from '@aztec/merkle-tree';
4850
import { AztecKVTxPool, P2P, createP2PClient } from '@aztec/p2p';
4951
import {
5052
GlobalVariableBuilder,
@@ -113,7 +115,7 @@ export class AztecNodeService implements AztecNode {
113115
const log = createDebugLogger('aztec:node');
114116
const storeLog = createDebugLogger('aztec:node:lmdb');
115117
const store = await initStoreForRollup(
116-
AztecLmdbStore.open(config.dataDirectory, storeLog),
118+
AztecLmdbStore.open(config.dataDirectory, false, storeLog),
117119
config.l1Contracts.rollupAddress,
118120
storeLog,
119121
);
@@ -426,6 +428,47 @@ export class AztecNodeService implements AztecNode {
426428
return committedDb.getSiblingPath(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, leafIndex);
427429
}
428430

431+
/**
432+
* Returns the index of a l2ToL1Message in a ephemeral l2 to l1 data tree as well as its sibling path.
433+
* @remarks This tree is considered ephemeral because it is created on-demand by: taking all the l2ToL1 messages
434+
* in a single block, and then using them to make a variable depth append-only tree with these messages as leaves.
435+
* The tree is discarded immediately after calculating what we need from it.
436+
* @param blockNumber - The block number at which to get the data.
437+
* @param l2ToL1Message - The l2ToL1Message get the index / sibling path for.
438+
* @returns A tuple of the index and the sibling path of the L2ToL1Message.
439+
*/
440+
public async getL2ToL1MessageIndexAndSiblingPath(
441+
blockNumber: number | 'latest',
442+
l2ToL1Message: Fr,
443+
): Promise<[number, SiblingPath<number>]> {
444+
const block = await this.blockSource.getBlock(blockNumber === 'latest' ? await this.getBlockNumber() : blockNumber);
445+
446+
if (block === undefined) {
447+
throw new Error('Block is not defined');
448+
}
449+
450+
const l2ToL1Messages = block.body.txEffects.flatMap(txEffect => txEffect.l2ToL1Msgs);
451+
452+
if (l2ToL1Messages.length !== L2_TO_L1_MESSAGE_LENGTH * block.body.txEffects.length) {
453+
throw new Error('L2 to L1 Messages are not padded');
454+
}
455+
456+
const indexOfL2ToL1Message = l2ToL1Messages.findIndex(l2ToL1MessageInBlock =>
457+
l2ToL1MessageInBlock.equals(l2ToL1Message),
458+
);
459+
460+
if (indexOfL2ToL1Message === -1) {
461+
throw new Error('The L2ToL1Message you are trying to prove inclusion of does not exist');
462+
}
463+
464+
const treeHeight = Math.ceil(Math.log2(l2ToL1Messages.length));
465+
466+
const tree = new StandardTree(openTmpStore(true), new SHA256(), 'temp_outhash_sibling_path', treeHeight);
467+
await tree.appendLeaves(l2ToL1Messages.map(l2ToL1Msg => l2ToL1Msg.toBuffer()));
468+
469+
return [indexOfL2ToL1Message, await tree.getSiblingPath(BigInt(indexOfL2ToL1Message), true)];
470+
}
471+
429472
/**
430473
* Returns a sibling path for a leaf in the committed blocks tree.
431474
* @param blockNumber - The block number at which to get the data.

yarn-project/aztec.js/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export {
115115
merkleTreeIds,
116116
mockTx,
117117
Comparator,
118+
SiblingPath,
118119
} from '@aztec/circuit-types';
119120
export { NodeInfo } from '@aztec/types/interfaces';
120121

yarn-project/aztec/src/cli/cmds/start_archiver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const startArchiver = async (options: any, signalHandlers: (() => Promise
2424

2525
const storeLog = createDebugLogger('aztec:archiver:lmdb');
2626
const store = await initStoreForRollup(
27-
AztecLmdbStore.open(archiverConfig.dataDirectory, storeLog),
27+
AztecLmdbStore.open(archiverConfig.dataDirectory, false, storeLog),
2828
archiverConfig.l1Contracts.rollupAddress,
2929
storeLog,
3030
);

yarn-project/circuit-types/src/interfaces/aztec-node.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,20 @@ export interface AztecNode {
9393
leafIndex: bigint,
9494
): Promise<SiblingPath<typeof L1_TO_L2_MSG_TREE_HEIGHT>>;
9595

96+
/**
97+
* Returns the index of a l2ToL1Message in a ephemeral l2 to l1 data tree as well as its sibling path.
98+
* @remarks This tree is considered ephemeral because it is created on-demand by: taking all the l2ToL1 messages
99+
* in a single block, and then using them to make a variable depth append-only tree with these messages as leaves.
100+
* The tree is discarded immediately after calculating what we need from it.
101+
* @param blockNumber - The block number at which to get the data.
102+
* @param l2ToL1Message - The l2ToL1Message get the index / sibling path for.
103+
* @returns A tuple of the index and the sibling path of the L2ToL1Message.
104+
*/
105+
getL2ToL1MessageIndexAndSiblingPath(
106+
blockNumber: number | 'latest',
107+
l2ToL1Message: Fr,
108+
): Promise<[number, SiblingPath<number>]>;
109+
96110
/**
97111
* Returns a sibling path for a leaf in the committed historic blocks tree.
98112
* @param blockNumber - The block number at which to get the data.
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import {
2+
AccountWalletWithPrivateKey,
3+
AztecNode,
4+
BatchCall,
5+
DeployL1Contracts,
6+
EthAddress,
7+
Fr,
8+
SiblingPath,
9+
sha256,
10+
} from '@aztec/aztec.js';
11+
import { SHA256 } from '@aztec/merkle-tree';
12+
import { TestContract } from '@aztec/noir-contracts.js';
13+
14+
import { beforeEach, describe, expect, it } from '@jest/globals';
15+
16+
import { setup } from './fixtures/utils.js';
17+
18+
// @remark - This does not test the Outbox Contract yet. All this test does is create L2 to L1 messages in a block,
19+
// verify their existence, and produce a sibling path that is also checked for validity against the circuit produced
20+
// out_hash in the header.
21+
describe('E2E Outbox Tests', () => {
22+
let teardown: () => void;
23+
let aztecNode: AztecNode;
24+
const merkleSha256 = new SHA256();
25+
let contract: TestContract;
26+
let wallets: AccountWalletWithPrivateKey[];
27+
let deployL1ContractsValues: DeployL1Contracts;
28+
29+
beforeEach(async () => {
30+
({ teardown, aztecNode, wallets, deployL1ContractsValues } = await setup(1));
31+
32+
const receipt = await TestContract.deploy(wallets[0]).send({ contractAddressSalt: Fr.ZERO }).wait();
33+
contract = receipt.contract;
34+
}, 100_000);
35+
36+
afterAll(() => teardown());
37+
38+
it('Inserts a new transaction with two out messages, and verifies sibling paths of both the new messages', async () => {
39+
const [[recipient1, content1], [recipient2, content2]] = [
40+
[EthAddress.random(), Fr.random()],
41+
[EthAddress.random(), Fr.random()],
42+
];
43+
44+
// We can't put any more l2 to L1 messages here There are a max of 2 L2 to L1 messages per transaction
45+
const call = new BatchCall(wallets[0], [
46+
contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content1, recipient1).request(),
47+
contract.methods.create_l2_to_l1_message_arbitrary_recipient_private(content2, recipient2).request(),
48+
]);
49+
50+
// TODO (#5104): When able to guarantee multiple txs in a single block, make this populate a full tree. Right now we are
51+
// unable to do this because in CI, for some reason, the tx's are handled in different blocks, so it is impossible
52+
// to make a full tree of L2 -> L1 messages as we are only able to set one tx's worth of L1 -> L2 messages in a block (2 messages out of 4)
53+
const txReceipt = await call.send().wait();
54+
55+
const block = await aztecNode.getBlock(txReceipt.blockNumber!);
56+
57+
const l2ToL1Messages = block?.body.txEffects.flatMap(txEffect => txEffect.l2ToL1Msgs);
58+
59+
expect(l2ToL1Messages?.map(l2ToL1Message => l2ToL1Message.toString())).toStrictEqual(
60+
[makeL2ToL1Message(recipient2, content2), makeL2ToL1Message(recipient1, content1), Fr.ZERO, Fr.ZERO].map(
61+
expectedL2ToL1Message => expectedL2ToL1Message.toString(),
62+
),
63+
);
64+
65+
// For each individual message, we are using our node API to grab the index and sibling path. We expect
66+
// the index to match the order of the block we obtained earlier. We also then use this sibling path to hash up to the root,
67+
// verifying that the expected root obtained through the message and the sibling path match the actual root
68+
// that was returned by the circuits in the header as out_hash.
69+
const [index, siblingPath] = await aztecNode.getL2ToL1MessageIndexAndSiblingPath(
70+
txReceipt.blockNumber!,
71+
l2ToL1Messages![0],
72+
);
73+
expect(siblingPath.pathSize).toBe(2);
74+
expect(index).toBe(0);
75+
const expectedRoot = calculateExpectedRoot(l2ToL1Messages![0], siblingPath as SiblingPath<2>, index);
76+
expect(expectedRoot.toString('hex')).toEqual(block?.header.contentCommitment.outHash.toString('hex'));
77+
78+
const [index2, siblingPath2] = await aztecNode.getL2ToL1MessageIndexAndSiblingPath(
79+
txReceipt.blockNumber!,
80+
l2ToL1Messages![1],
81+
);
82+
expect(siblingPath2.pathSize).toBe(2);
83+
expect(index2).toBe(1);
84+
const expectedRoot2 = calculateExpectedRoot(l2ToL1Messages![1], siblingPath2 as SiblingPath<2>, index2);
85+
expect(expectedRoot2.toString('hex')).toEqual(block?.header.contentCommitment.outHash.toString('hex'));
86+
}, 360_000);
87+
88+
function calculateExpectedRoot(l2ToL1Message: Fr, siblingPath: SiblingPath<2>, index: number): Buffer {
89+
const firstLayerInput: [Buffer, Buffer] =
90+
index & 0x1
91+
? [siblingPath.toBufferArray()[0], l2ToL1Message.toBuffer()]
92+
: [l2ToL1Message.toBuffer(), siblingPath.toBufferArray()[0]];
93+
const firstLayer = merkleSha256.hash(...firstLayerInput);
94+
index /= 2;
95+
const secondLayerInput: [Buffer, Buffer] =
96+
index & 0x1 ? [siblingPath.toBufferArray()[1], firstLayer] : [firstLayer, siblingPath.toBufferArray()[1]];
97+
return merkleSha256.hash(...secondLayerInput);
98+
}
99+
100+
function makeL2ToL1Message(recipient: EthAddress, content: Fr = Fr.ZERO): Fr {
101+
const leaf = Fr.fromBufferReduce(
102+
sha256(
103+
Buffer.concat([
104+
contract.address.toBuffer(),
105+
new Fr(1).toBuffer(), // aztec version
106+
recipient.toBuffer32(),
107+
new Fr(deployL1ContractsValues.publicClient.chain.id).toBuffer(), // chain id
108+
content.toBuffer(),
109+
]),
110+
),
111+
);
112+
113+
return leaf;
114+
}
115+
});

yarn-project/kv-store/src/lmdb/store.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,20 @@ export class AztecLmdbStore implements AztecKVStore {
4545
* different rollup instances.
4646
*
4747
* @param path - A path on the disk to store the database. Optional
48+
* @param ephemeral - true if the store should only exist in memory and not automatically be flushed to disk. Optional
4849
* @param log - A logger to use. Optional
4950
* @returns The store
5051
*/
51-
static open(path?: string, log = createDebugLogger('aztec:kv-store:lmdb')): AztecLmdbStore {
52+
static open(
53+
path?: string,
54+
ephemeral: boolean = false,
55+
log = createDebugLogger('aztec:kv-store:lmdb'),
56+
): AztecLmdbStore {
5257
log.info(`Opening LMDB database at ${path || 'temporary location'}`);
53-
const rootDb = open({ path });
58+
const rootDb = open({
59+
path,
60+
noSync: ephemeral,
61+
});
5462
return new AztecLmdbStore(rootDb);
5563
}
5664

yarn-project/kv-store/src/utils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ export async function initStoreForRollup<T extends AztecKVStore>(
3434

3535
/**
3636
* Opens a temporary store for testing purposes.
37+
* @param ephemeral - true if the store should only exist in memory and not automatically be flushed to disk. Optional
3738
* @returns A new store
3839
*/
39-
export function openTmpStore(): AztecKVStore {
40-
return AztecLmdbStore.open();
40+
export function openTmpStore(ephemeral: boolean = false): AztecKVStore {
41+
return AztecLmdbStore.open(undefined, ephemeral);
4142
}

yarn-project/merkle-tree/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from './interfaces/indexed_tree.js';
33
export * from './interfaces/merkle_tree.js';
44
export * from './interfaces/update_only_tree.js';
55
export * from './pedersen.js';
6+
export * from './sha_256.js';
67
export * from './sparse_tree/sparse_tree.js';
78
export { StandardIndexedTree } from './standard_indexed_tree/standard_indexed_tree.js';
89
export { StandardIndexedTreeWithAppend } from './standard_indexed_tree/test/standard_indexed_tree_with_append.js';
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { sha256 } from '@aztec/foundation/crypto';
2+
import { Hasher } from '@aztec/types/interfaces';
3+
4+
/**
5+
* A helper class encapsulating SHA256 hash functionality.
6+
* @deprecated Don't call SHA256 directly in production code. Instead, create suitably-named functions for specific
7+
* purposes.
8+
*/
9+
export class SHA256 implements Hasher {
10+
/*
11+
* @deprecated Don't call SHA256 directly in production code. Instead, create suitably-named functions for specific
12+
* purposes.
13+
*/
14+
public hash(lhs: Uint8Array, rhs: Uint8Array): Buffer {
15+
return sha256(Buffer.concat([Buffer.from(lhs), Buffer.from(rhs)]));
16+
}
17+
18+
/*
19+
* @deprecated Don't call SHA256 directly in production code. Instead, create suitably-named functions for specific
20+
* purposes.
21+
*/
22+
public hashInputs(inputs: Buffer[]): Buffer {
23+
return sha256(Buffer.concat(inputs));
24+
}
25+
}

0 commit comments

Comments
 (0)