Skip to content

Commit

Permalink
refactor: append-only merkle tree generics (#5355)
Browse files Browse the repository at this point in the history
Give Merkle Trees generic type arguments for the leaves they store.

---------

Co-authored-by: Santiago Palladino <santiago@aztecprotocol.com>
  • Loading branch information
alexghr and spalladino authored Mar 26, 2024
1 parent ae5126a commit ef7bf79
Show file tree
Hide file tree
Showing 42 changed files with 477 additions and 209 deletions.
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"erc",
"falsey",
"fargate",
"Fieldeable",
"filestat",
"flatmap",
"foundryup",
Expand Down
11 changes: 9 additions & 2 deletions yarn-project/aztec-node/src/aztec-node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,8 +458,15 @@ export class AztecNodeService implements AztecNode {

const treeHeight = Math.ceil(Math.log2(l2ToL1Messages.length));
// The root of this tree is the out_hash calculated in Noir => we truncate to match Noir's SHA
const tree = new StandardTree(openTmpStore(true), new SHA256Trunc(), 'temp_outhash_sibling_path', treeHeight);
await tree.appendLeaves(l2ToL1Messages.map(l2ToL1Msg => l2ToL1Msg.toBuffer()));
const tree = new StandardTree(
openTmpStore(true),
new SHA256Trunc(),
'temp_outhash_sibling_path',
treeHeight,
0n,
Fr,
);
await tree.appendLeaves(l2ToL1Messages);

return [indexOfL2ToL1Message, await tree.getSiblingPath(indexOfL2ToL1Message, true)];
}
Expand Down
10 changes: 5 additions & 5 deletions yarn-project/end-to-end/src/e2e_blacklist_token_contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,14 @@ describe('e2e_blacklist_token_contract', () => {

let tokenSim: TokenSimulator;

let slowUpdateTreeSimulator: SparseTree;
let slowUpdateTreeSimulator: SparseTree<Fr>;

let cheatCodes: CheatCodes;

const getMembershipProof = async (index: bigint, includeUncommitted: boolean) => {
return {
index,
value: Fr.fromBuffer(slowUpdateTreeSimulator.getLeafValue(index, includeUncommitted)!),
value: slowUpdateTreeSimulator.getLeafValue(index, includeUncommitted)!,
// eslint-disable-next-line camelcase
sibling_path: (await slowUpdateTreeSimulator.getSiblingPath(index, includeUncommitted)).toFields(),
};
Expand Down Expand Up @@ -101,9 +101,9 @@ describe('e2e_blacklist_token_contract', () => {
await wallets[accountIndex].addNote(extendedNote);
};

const updateSlowTree = async (tree: SparseTree, wallet: Wallet, index: AztecAddress, value: bigint) => {
const updateSlowTree = async (tree: SparseTree<Fr>, wallet: Wallet, index: AztecAddress, value: bigint) => {
await wallet.addCapsule(getUpdateCapsule(await getUpdateProof(value, index.toBigInt())));
await tree.updateLeaf(new Fr(value).toBuffer(), index.toBigInt());
await tree.updateLeaf(new Fr(value), index.toBigInt());
};

beforeAll(async () => {
Expand All @@ -113,7 +113,7 @@ describe('e2e_blacklist_token_contract', () => {
slowTree = await SlowTreeContract.deploy(wallets[0]).send().deployed();

const depth = 254;
slowUpdateTreeSimulator = await newTree(SparseTree, openTmpStore(), new Pedersen(), 'test', depth);
slowUpdateTreeSimulator = await newTree(SparseTree, openTmpStore(), new Pedersen(), 'test', Fr, depth);

// Add account[0] as admin
await updateSlowTree(slowUpdateTreeSimulator, wallets[0], accounts[0].address, 4n);
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
setup,
} from './fixtures/utils.js';

jest.setTimeout(1_000_000);
jest.setTimeout(100_000);

const TOKEN_NAME = 'BananaCoin';
const TOKEN_SYMBOL = 'BC';
Expand Down
8 changes: 4 additions & 4 deletions yarn-project/end-to-end/src/e2e_slow_tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ describe('e2e_slow_tree', () => {

it('Messing around with noir slow tree', async () => {
const depth = 254;
const slowUpdateTreeSimulator = await newTree(SparseTree, openTmpStore(), new Pedersen(), 'test', depth);
const slowUpdateTreeSimulator = await newTree(SparseTree, openTmpStore(), new Pedersen(), 'test', Fr, depth);
const getMembershipProof = async (index: bigint, includeUncommitted: boolean) => {
return {
index,
value: Fr.fromBuffer(slowUpdateTreeSimulator.getLeafValue(index, includeUncommitted)!),
value: slowUpdateTreeSimulator.getLeafValue(index, includeUncommitted)!,
// eslint-disable-next-line camelcase
sibling_path: (await slowUpdateTreeSimulator.getSiblingPath(index, includeUncommitted)).toFields(),
};
Expand Down Expand Up @@ -102,7 +102,7 @@ describe('e2e_slow_tree', () => {
.update_at_public(await getUpdateProof(1n, key))
.send()
.wait();
await slowUpdateTreeSimulator.updateLeaf(new Fr(1).toBuffer(), key);
await slowUpdateTreeSimulator.updateLeaf(new Fr(1), key);

// Update below.
_root = {
Expand Down Expand Up @@ -140,7 +140,7 @@ describe('e2e_slow_tree', () => {
const t2 = computeNextChange(BigInt(await cheatCodes.eth.timestamp()));
await wallet.addCapsule(getUpdateCapsule(await getUpdateProof(4n, key)));
await contract.methods.update_at_private(key, 4n).send().wait();
await slowUpdateTreeSimulator.updateLeaf(new Fr(4).toBuffer(), key);
await slowUpdateTreeSimulator.updateLeaf(new Fr(4), key);
_root = {
before: Fr.fromBuffer(slowUpdateTreeSimulator.getRoot(false)).toBigInt(),
after: Fr.fromBuffer(slowUpdateTreeSimulator.getRoot(true)).toBigInt(),
Expand Down
11 changes: 9 additions & 2 deletions yarn-project/end-to-end/src/integration_l1_publisher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,8 +428,15 @@ describe('L1Publisher integration', () => {

const treeHeight = Math.ceil(Math.log2(newL2ToL1MsgsArray.length));

const tree = new StandardTree(openTmpStore(true), new SHA256Trunc(), 'temp_outhash_sibling_path', treeHeight);
await tree.appendLeaves(newL2ToL1MsgsArray.map(l2ToL1Msg => l2ToL1Msg.toBuffer()));
const tree = new StandardTree(
openTmpStore(true),
new SHA256Trunc(),
'temp_outhash_sibling_path',
treeHeight,
0n,
Fr,
);
await tree.appendLeaves(newL2ToL1MsgsArray);

const expectedRoot = tree.getRoot(true);
const [actualRoot] = await outbox.read.roots([block.header.globalVariables.blockNumber.toBigInt()]);
Expand Down
11 changes: 11 additions & 0 deletions yarn-project/foundation/src/serialize/buffer_reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,14 @@ export class BufferReader {
return this.buffer.length;
}
}

/**
* A deserializer
*/
export interface FromBuffer<T> {
/**
* Deserializes an object from a buffer
* @param buffer - The buffer to deserialize.
*/
fromBuffer(buffer: Buffer): T;
}
10 changes: 7 additions & 3 deletions yarn-project/merkle-tree/src/interfaces/append_only_tree.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { TreeSnapshotBuilder } from '../snapshots/snapshot_builder.js';
import { Bufferable } from '@aztec/foundation/serialize';

import { TreeSnapshot, TreeSnapshotBuilder } from '../snapshots/snapshot_builder.js';
import { MerkleTree } from './merkle_tree.js';

/**
* A Merkle tree that supports only appending leaves and not updating existing leaves.
*/
export interface AppendOnlyTree extends MerkleTree, TreeSnapshotBuilder {
export interface AppendOnlyTree<T extends Bufferable = Buffer>
extends MerkleTree<T>,
TreeSnapshotBuilder<TreeSnapshot<T>> {
/**
* Appends a set of leaf values to the tree.
* @param leaves - The set of leaves to be appended.
*/
appendLeaves(leaves: Buffer[]): Promise<void>;
appendLeaves(leaves: T[]): Promise<void>;
}
7 changes: 6 additions & 1 deletion yarn-project/merkle-tree/src/interfaces/indexed_tree.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { SiblingPath } from '@aztec/circuit-types';
import { IndexedTreeLeaf, IndexedTreeLeafPreimage } from '@aztec/foundation/trees';

import { IndexedTreeSnapshot, TreeSnapshot, TreeSnapshotBuilder } from '../snapshots/snapshot_builder.js';
import { AppendOnlyTree } from './append_only_tree.js';
import { MerkleTree } from './merkle_tree.js';

/**
* Factory for creating leaf preimages.
Expand Down Expand Up @@ -73,7 +75,10 @@ export interface BatchInsertionResult<TreeHeight extends number, SubtreeSiblingP
/**
* Indexed merkle tree.
*/
export interface IndexedTree extends AppendOnlyTree {
export interface IndexedTree
extends MerkleTree<Buffer>,
TreeSnapshotBuilder<IndexedTreeSnapshot>,
Omit<AppendOnlyTree<Buffer>, keyof TreeSnapshotBuilder<TreeSnapshot<Buffer>>> {
/**
* Finds the index of the largest leaf whose value is less than or equal to the provided value.
* @param newValue - The new value to be inserted into the tree.
Expand Down
9 changes: 5 additions & 4 deletions yarn-project/merkle-tree/src/interfaces/merkle_tree.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SiblingPath } from '@aztec/circuit-types';
import { Bufferable } from '@aztec/foundation/serialize';

/**
* Defines the interface for a source of sibling paths.
Expand All @@ -15,7 +16,7 @@ export interface SiblingPathSource {
/**
* Defines the interface for a Merkle tree.
*/
export interface MerkleTree extends SiblingPathSource {
export interface MerkleTree<T extends Bufferable = Buffer> extends SiblingPathSource {
/**
* Returns the current root of the tree.
* @param includeUncommitted - Set to true to include uncommitted updates in the calculated root.
Expand Down Expand Up @@ -48,15 +49,15 @@ export interface MerkleTree extends SiblingPathSource {
* @param index - The index of the leaf value to be returned.
* @param includeUncommitted - Set to true to include uncommitted updates in the data set.
*/
getLeafValue(index: bigint, includeUncommitted: boolean): Buffer | undefined;
getLeafValue(index: bigint, includeUncommitted: boolean): T | undefined;

/**
* Returns the index of a leaf given its value, or undefined if no leaf with that value is found.
* @param leaf - The leaf value to look for.
* @param includeUncommitted - Indicates whether to include uncommitted data.
* @returns The index of the first leaf found with a given value (undefined if not found).
*/
findLeafIndex(leaf: Buffer, includeUncommitted: boolean): bigint | undefined;
findLeafIndex(leaf: T, includeUncommitted: boolean): bigint | undefined;

/**
* Returns the first index containing a leaf value after `startIndex`.
Expand All @@ -65,5 +66,5 @@ export interface MerkleTree extends SiblingPathSource {
* @param includeUncommitted - Indicates whether to include uncommitted data.
* @returns The index of the first leaf found with a given value (undefined if not found).
*/
findLeafIndexAfter(leaf: Buffer, startIndex: bigint, includeUncommitted: boolean): bigint | undefined;
findLeafIndexAfter(leaf: T, startIndex: bigint, includeUncommitted: boolean): bigint | undefined;
}
10 changes: 7 additions & 3 deletions yarn-project/merkle-tree/src/interfaces/update_only_tree.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { TreeSnapshotBuilder } from '../snapshots/snapshot_builder.js';
import { Bufferable } from '@aztec/foundation/serialize';

import { TreeSnapshot, TreeSnapshotBuilder } from '../snapshots/snapshot_builder.js';
import { MerkleTree } from './merkle_tree.js';

/**
* A Merkle tree that supports updates at arbitrary indices but not appending.
*/
export interface UpdateOnlyTree extends MerkleTree, TreeSnapshotBuilder {
export interface UpdateOnlyTree<T extends Bufferable = Buffer>
extends MerkleTree<T>,
TreeSnapshotBuilder<TreeSnapshot<T>> {
/**
* Updates a leaf at a given index in the tree.
* @param leaf - The leaf value to be updated.
* @param index - The leaf to be updated.
*/
updateLeaf(leaf: Buffer, index: bigint): Promise<void>;
updateLeaf(leaf: T, index: bigint): Promise<void>;
}
16 changes: 13 additions & 3 deletions yarn-project/merkle-tree/src/load_tree.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Bufferable, FromBuffer } from '@aztec/foundation/serialize';
import { AztecKVStore } from '@aztec/kv-store';
import { Hasher } from '@aztec/types/interfaces';

Expand All @@ -11,13 +12,22 @@ import { TreeBase, getTreeMeta } from './tree_base.js';
* @param name - Name of the tree.
* @returns The newly created tree.
*/
export function loadTree<T extends TreeBase>(
c: new (store: AztecKVStore, hasher: Hasher, name: string, depth: number, size: bigint, root: Buffer) => T,
export function loadTree<T extends TreeBase<Bufferable>, D extends FromBuffer<Bufferable>>(
c: new (
store: AztecKVStore,
hasher: Hasher,
name: string,
depth: number,
size: bigint,
deserializer: D,
root: Buffer,
) => T,
store: AztecKVStore,
hasher: Hasher,
name: string,
deserializer: D,
): Promise<T> {
const { root, depth, size } = getTreeMeta(store, name);
const tree = new c(store, hasher, name, depth, size, root);
const tree = new c(store, hasher, name, depth, size, deserializer, root);
return Promise.resolve(tree);
}
8 changes: 5 additions & 3 deletions yarn-project/merkle-tree/src/new_tree.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Bufferable, FromBuffer } from '@aztec/foundation/serialize';
import { AztecKVStore } from '@aztec/kv-store';
import { Hasher } from '@aztec/types/interfaces';

Expand All @@ -13,15 +14,16 @@ import { TreeBase } from './tree_base.js';
* @param prefilledSize - A number of leaves that are prefilled with values.
* @returns The newly created tree.
*/
export async function newTree<T extends TreeBase>(
c: new (store: AztecKVStore, hasher: Hasher, name: string, depth: number, size: bigint) => T,
export async function newTree<T extends TreeBase<Bufferable>, D extends FromBuffer<Bufferable>>(
c: new (store: AztecKVStore, hasher: Hasher, name: string, depth: number, size: bigint, deserializer: D) => T,
store: AztecKVStore,
hasher: Hasher,
name: string,
deserializer: D,
depth: number,
prefilledSize = 1,
): Promise<T> {
const tree = new c(store, hasher, name, depth, 0n);
const tree = new c(store, hasher, name, depth, 0n, deserializer);
await tree.init(prefilledSize);
return tree;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { randomBytes } from '@aztec/foundation/crypto';
import { FromBuffer } from '@aztec/foundation/serialize';
import { AztecKVStore } from '@aztec/kv-store';
import { openTmpStore } from '@aztec/kv-store/utils';

Expand All @@ -8,14 +9,15 @@ import { describeSnapshotBuilderTestSuite } from './snapshot_builder_test_suite.

describe('AppendOnlySnapshot', () => {
let tree: StandardTree;
let snapshotBuilder: AppendOnlySnapshotBuilder;
let snapshotBuilder: AppendOnlySnapshotBuilder<Buffer>;
let db: AztecKVStore;

beforeEach(async () => {
db = openTmpStore();
const hasher = new Pedersen();
tree = await newTree(StandardTree, db, hasher, 'test', 4);
snapshotBuilder = new AppendOnlySnapshotBuilder(db, tree, hasher);
const deserializer: FromBuffer<Buffer> = { fromBuffer: b => b };
tree = await newTree(StandardTree, db, hasher, 'test', deserializer, 4);
snapshotBuilder = new AppendOnlySnapshotBuilder(db, tree, hasher, deserializer);
});

describeSnapshotBuilderTestSuite(
Expand Down
Loading

0 comments on commit ef7bf79

Please sign in to comment.