diff --git a/circuits b/circuits index 501e6bbaff7a..b7ec0b28356a 160000 --- a/circuits +++ b/circuits @@ -1 +1 @@ -Subproject commit 501e6bbaff7a9e4bd29cff7dd2ba65ae77648b4d +Subproject commit b7ec0b28356a1f1f96951dfc605b925adf33c2ff diff --git a/yarn-project/barretenberg.js/src/crypto/pedersen/pedersen.ts b/yarn-project/barretenberg.js/src/crypto/pedersen/pedersen.ts index 67c29e8a84c1..930aeb82aba6 100644 --- a/yarn-project/barretenberg.js/src/crypto/pedersen/pedersen.ts +++ b/yarn-project/barretenberg.js/src/crypto/pedersen/pedersen.ts @@ -13,7 +13,7 @@ export function pedersenCompress(wasm: BarretenbergWasm, lhs: Uint8Array, rhs: U // If not done already, precompute constants. wasm.call('pedersen__init'); if (lhs.length !== 32 || rhs.length !== 32) { - throw new Error('lhs and rhs must be equal to 32 bytes'); + throw new Error(`Pedersen lhs and rhs inputs must be 32 bytes (got ${lhs.length} and ${rhs.length} respectively)`); } wasm.writeMemory(0, lhs); wasm.writeMemory(32, rhs); diff --git a/yarn-project/circuits.js/src/structs/base_rollup.ts b/yarn-project/circuits.js/src/structs/base_rollup.ts index 91c67d6f9e43..fbd2a508f58b 100644 --- a/yarn-project/circuits.js/src/structs/base_rollup.ts +++ b/yarn-project/circuits.js/src/structs/base_rollup.ts @@ -20,6 +20,10 @@ export class NullifierLeafPreimage { toBuffer() { return serializeToBuffer(this.leafValue, this.nextValue, this.nextIndex); } + + static empty() { + return new NullifierLeafPreimage(Fr.ZERO, Fr.ZERO, 0); + } } export class AppendOnlyTreeSnapshot { diff --git a/yarn-project/circuits.js/src/structs/shared.ts b/yarn-project/circuits.js/src/structs/shared.ts index 029290f056ab..1e5ba604de58 100644 --- a/yarn-project/circuits.js/src/structs/shared.ts +++ b/yarn-project/circuits.js/src/structs/shared.ts @@ -27,6 +27,14 @@ export class MembershipWitness { .map(() => Fr.ZERO); return new MembershipWitness(pathSize, leafIndex, arr); } + + static fromBufferArray(leafIndex: number, siblingPath: Buffer[]) { + return new MembershipWitness( + siblingPath.length, + leafIndex, + siblingPath.map(x => Fr.fromBuffer(x)), + ); + } } export class AggregationObject { diff --git a/yarn-project/circuits.js/src/utils/serialize.ts b/yarn-project/circuits.js/src/utils/serialize.ts index 95fb9773e7e8..3190b61f0f70 100644 --- a/yarn-project/circuits.js/src/utils/serialize.ts +++ b/yarn-project/circuits.js/src/utils/serialize.ts @@ -175,6 +175,8 @@ export function toFriendlyJSON(obj: object): string { return '0x' + Buffer.from(value.data).toString('hex'); } else if (typeof value === 'bigint') { return value.toString(); + } else if ((value as { toFriendlyJSON: () => string }).toFriendlyJSON) { + return value.toFriendlyJSON(); } else { return value; } diff --git a/yarn-project/foundation/src/bigint-buffer/index.ts b/yarn-project/foundation/src/bigint-buffer/index.ts index b04faaaf8644..d4de49c9c928 100644 --- a/yarn-project/foundation/src/bigint-buffer/index.ts +++ b/yarn-project/foundation/src/bigint-buffer/index.ts @@ -47,5 +47,7 @@ export function toBufferLE(num: bigint, width: number): Buffer { */ export function toBufferBE(num: bigint, width: number): Buffer { const hex = num.toString(16); - return Buffer.from(hex.padStart(width * 2, '0').slice(0, width * 2), 'hex'); + const buffer = Buffer.from(hex.padStart(width * 2, '0').slice(0, width * 2), 'hex'); + if (buffer.length > width) throw new Error(`Number ${num.toString(16)} does not fit in ${width}`); + return buffer; } diff --git a/yarn-project/merkle-tree/src/indexed_tree/indexed_tree.test.ts b/yarn-project/merkle-tree/src/indexed_tree/indexed_tree.test.ts index 09cbb393793f..4556b7ab3c9e 100644 --- a/yarn-project/merkle-tree/src/indexed_tree/indexed_tree.test.ts +++ b/yarn-project/merkle-tree/src/indexed_tree/indexed_tree.test.ts @@ -14,11 +14,7 @@ const createFromName = async (levelUp: levelup.LevelUp, hasher: Hasher, name: st }; const createIndexedTreeLeaf = (value: number, nextIndex: number, nextValue: number) => { - return Buffer.concat([ - toBufferBE(BigInt(value), 32), - toBufferBE(BigInt(nextIndex), 32), - toBufferBE(BigInt(nextValue), 32), - ]); + return [toBufferBE(BigInt(value), 32), toBufferBE(BigInt(nextIndex), 32), toBufferBE(BigInt(nextValue), 32)]; }; const verifyCommittedState = async ( @@ -58,7 +54,7 @@ describe('IndexedMerkleTreeSpecific', () => { * nextVal 0 0 0 0 0 0 0 0. */ - const zeroTreeLeafHash = pedersen.hashToField(createIndexedTreeLeaf(0, 0, 0)); + const zeroTreeLeafHash = pedersen.compressInputs(createIndexedTreeLeaf(0, 0, 0)); const level1ZeroHash = pedersen.compress(zeroTreeLeafHash, zeroTreeLeafHash); const level2ZeroHash = pedersen.compress(level1ZeroHash, level1ZeroHash); let root = pedersen.compress(level2ZeroHash, level2ZeroHash); @@ -82,8 +78,8 @@ describe('IndexedMerkleTreeSpecific', () => { * nextIdx 1 0 0 0 0 0 0 0 * nextVal 30 0 0 0 0 0 0 0. */ - let index0Hash = pedersen.hashToField(createIndexedTreeLeaf(0, 1, 30)); - let index1Hash = pedersen.hashToField(createIndexedTreeLeaf(30, 0, 0)); + let index0Hash = pedersen.compressInputs(createIndexedTreeLeaf(0, 1, 30)); + let index1Hash = pedersen.compressInputs(createIndexedTreeLeaf(30, 0, 0)); let e10 = pedersen.compress(index0Hash, index1Hash); let e20 = pedersen.compress(e10, level1ZeroHash); root = pedersen.compress(e20, level2ZeroHash); @@ -106,8 +102,8 @@ describe('IndexedMerkleTreeSpecific', () => { * nextIdx 2 0 1 0 0 0 0 0 * nextVal 10 0 30 0 0 0 0 0. */ - index0Hash = pedersen.hashToField(createIndexedTreeLeaf(0, 2, 10)); - let index2Hash = pedersen.hashToField(createIndexedTreeLeaf(10, 1, 30)); + index0Hash = pedersen.compressInputs(createIndexedTreeLeaf(0, 2, 10)); + let index2Hash = pedersen.compressInputs(createIndexedTreeLeaf(10, 1, 30)); e10 = pedersen.compress(index0Hash, index1Hash); let e11 = pedersen.compress(index2Hash, zeroTreeLeafHash); e20 = pedersen.compress(e10, e11); @@ -132,8 +128,8 @@ describe('IndexedMerkleTreeSpecific', () => { * nextVal 10 0 20 30 0 0 0 0. */ e10 = pedersen.compress(index0Hash, index1Hash); - index2Hash = pedersen.hashToField(createIndexedTreeLeaf(10, 3, 20)); - const index3Hash = pedersen.hashToField(createIndexedTreeLeaf(20, 1, 30)); + index2Hash = pedersen.compressInputs(createIndexedTreeLeaf(10, 3, 20)); + const index3Hash = pedersen.compressInputs(createIndexedTreeLeaf(20, 1, 30)); e11 = pedersen.compress(index2Hash, index3Hash); e20 = pedersen.compress(e10, e11); root = pedersen.compress(e20, level2ZeroHash); @@ -156,8 +152,8 @@ describe('IndexedMerkleTreeSpecific', () => { * nextIdx 2 4 3 1 0 0 0 0 * nextVal 10 50 20 30 0 0 0 0. */ - index1Hash = pedersen.hashToField(createIndexedTreeLeaf(30, 4, 50)); - const index4Hash = pedersen.hashToField(createIndexedTreeLeaf(50, 0, 0)); + index1Hash = pedersen.compressInputs(createIndexedTreeLeaf(30, 4, 50)); + const index4Hash = pedersen.compressInputs(createIndexedTreeLeaf(50, 0, 0)); e10 = pedersen.compress(index0Hash, index1Hash); e20 = pedersen.compress(e10, e11); const e12 = pedersen.compress(index4Hash, zeroTreeLeafHash); diff --git a/yarn-project/merkle-tree/src/indexed_tree/indexed_tree.ts b/yarn-project/merkle-tree/src/indexed_tree/indexed_tree.ts index 71443dc580be..bfcbafe032c1 100644 --- a/yarn-project/merkle-tree/src/indexed_tree/indexed_tree.ts +++ b/yarn-project/merkle-tree/src/indexed_tree/indexed_tree.ts @@ -35,11 +35,7 @@ const encodeTreeValue = (leafData: LeafData) => { return Buffer.concat([valueAsBuffer, indexAsBuffer, nextValueAsBuffer]); }; -// TODO: Check which version of hash we need to match the cpp implementation const hashEncodedTreeValue = (leaf: LeafData, hasher: Hasher) => { - return hasher.hashToField( - Buffer.concat([leaf.value, leaf.nextIndex, leaf.nextValue].map(val => toBufferBE(val, 32))), - ); return hasher.compressInputs([leaf.value, leaf.nextIndex, leaf.nextValue].map(val => toBufferBE(val, 32))); }; @@ -74,12 +70,19 @@ export class IndexedTree implements MerkleTree { * @param hasher - A hasher used to compute hash paths. * @param name - A name of the tree. * @param depth - A depth of the tree. + * @param prefilledSize - {optional} A number of leaves that are prefilled with values. * @returns A promise with the new Merkle tree. */ - public static async new(db: LevelUp, hasher: Hasher, name: string, depth: number): Promise { + public static async new( + db: LevelUp, + hasher: Hasher, + name: string, + depth: number, + prefilledSize = 0, + ): Promise { const underlying = await StandardMerkleTree.new(db, hasher, name, depth, hashEncodedTreeValue(initialLeaf, hasher)); const tree = new IndexedTree(underlying, hasher, db); - await tree.init(); + await tree.init(prefilledSize); return tree; } @@ -97,6 +100,14 @@ export class IndexedTree implements MerkleTree { return tree; } + /** + * Returns an empty leaf of the tree. + * @returns An empty leaf. + */ + static initialLeaf(): LeafData { + return initialLeaf; + } + /** * Returns the root of the tree. * @returns The root of the tree. @@ -105,6 +116,14 @@ export class IndexedTree implements MerkleTree { return this.underlying.getRoot(includeUncommitted); } + /** + * Returns the depth of the tree. + * @returns The depth of the tree. + */ + public getDepth(): number { + return this.underlying.getDepth(); + } + /** * Returns the number of leaves in the tree. * @returns The number of leaves in the tree. @@ -152,6 +171,24 @@ export class IndexedTree implements MerkleTree { return await this.underlying.getSiblingPath(index, includeUncommitted); } + /** + * Exposes the underlying tree's update leaf method + * @param leaf - The hash to set at the leaf + * @param index - The index of the element + */ + public async updateLeaf(leaf: LeafData, index: bigint): Promise { + this.cachedLeaves[Number(index)] = leaf; + const encodedLeaf = hashEncodedTreeValue(leaf, this.hasher); + await this.underlying.updateLeaf(encodedLeaf, index); + } + + /** + * Special case which will force append zero into the tree by increasing its size + */ + private appendZero(): void { + this.underlying.forceAppendEmptyLeaf(); + } + /** * Appends the given leaf to the tree. * @param leaf - The leaf to append. @@ -159,6 +196,13 @@ export class IndexedTree implements MerkleTree { */ private async appendLeaf(leaf: Buffer): Promise { const newValue = toBigIntBE(leaf); + + // Special case when appending zero + if (newValue === 0n) { + this.appendZero(); + return; + } + const indexOfPrevious = this.findIndexOfPreviousValue(newValue, true); const previousLeafCopy = this.getLatestLeafDataCopy(indexOfPrevious.index, true); if (previousLeafCopy === undefined) { @@ -228,9 +272,13 @@ export class IndexedTree implements MerkleTree { /** * Saves the initial leaf to this object and saves it to a database. */ - private async init() { + private async init(initialSize = 1) { this.leaves.push(initialLeaf); await this.underlying.appendLeaves([hashEncodedTreeValue(initialLeaf, this.hasher)]); + + for (let i = 1; i < initialSize; i++) { + await this.appendLeaf(Buffer.from([i])); + } await this.commit(); } diff --git a/yarn-project/merkle-tree/src/merkle_tree.ts b/yarn-project/merkle-tree/src/merkle_tree.ts index acdc8d96aae5..6c7158ac010d 100644 --- a/yarn-project/merkle-tree/src/merkle_tree.ts +++ b/yarn-project/merkle-tree/src/merkle_tree.ts @@ -1,3 +1,4 @@ +import { LeafData } from './index.js'; import { SiblingPath } from './sibling_path/sibling_path.js'; /** @@ -35,6 +36,16 @@ export interface MerkleTree extends SiblingPathSource { * Commit pending updates to the tree */ commit(): Promise; + /** + * 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 | LeafData, index: bigint): Promise; + /** + * Returns the depth of the tree + */ + getDepth(): number; /** * Rollback pending update to the tree */ diff --git a/yarn-project/merkle-tree/src/standard_tree/standard_tree.ts b/yarn-project/merkle-tree/src/standard_tree/standard_tree.ts index 92ebae9d9842..eb89aa3a31c2 100644 --- a/yarn-project/merkle-tree/src/standard_tree/standard_tree.ts +++ b/yarn-project/merkle-tree/src/standard_tree/standard_tree.ts @@ -188,6 +188,13 @@ export class StandardMerkleTree implements MerkleTree { } } + /** + * Force increase the size of the tree + */ + public forceAppendEmptyLeaf() { + this.cachedSize = (this.cachedSize ?? this.size) + 1n; + } + /** * Commits the changes to the database. * @returns Empty promise. diff --git a/yarn-project/sequencer-client/src/block_builder/circuit_powered_block_builder.test.ts b/yarn-project/sequencer-client/src/block_builder/circuit_powered_block_builder.test.ts index fcf0c985098a..cba3a13aa7c2 100644 --- a/yarn-project/sequencer-client/src/block_builder/circuit_powered_block_builder.test.ts +++ b/yarn-project/sequencer-client/src/block_builder/circuit_powered_block_builder.test.ts @@ -159,7 +159,7 @@ describe('sequencer/circuit_block_builder', () => { expect(l2Block.number).toEqual(blockNumber); expect(proof).toEqual(emptyProof); - }); + }, 20000); it('builds an L2 block with empty txs using wasm circuits', async () => { const simulator = new WasmCircuitSimulator(wasm); @@ -197,7 +197,7 @@ describe('sequencer/circuit_block_builder', () => { expect(contractTreeAfter.root).not.toEqual(contractTreeBefore.root); expect(contractTreeAfter.root).toEqual(await expectsDb.getTreeInfo(MerkleTreeId.CONTRACT_TREE).then(t => t.root)); expect(contractTreeAfter.size).toEqual(4n); - }); + }, 10000); }); // Test subject class that exposes internal functions for testing diff --git a/yarn-project/sequencer-client/src/block_builder/circuit_powered_block_builder.ts b/yarn-project/sequencer-client/src/block_builder/circuit_powered_block_builder.ts index c84bdd21b5db..4eabd13bc1bb 100644 --- a/yarn-project/sequencer-client/src/block_builder/circuit_powered_block_builder.ts +++ b/yarn-project/sequencer-client/src/block_builder/circuit_powered_block_builder.ts @@ -11,6 +11,7 @@ import { PreviousKernelData, PreviousRollupData, PRIVATE_DATA_TREE_ROOTS_TREE_HEIGHT, + PRIVATE_DATA_TREE_HEIGHT, ROLLUP_VK_TREE_HEIGHT, RootRollupInputs, RootRollupPublicInputs, @@ -18,6 +19,7 @@ import { VK_TREE_HEIGHT, } from '@aztec/circuits.js'; import { Fr, createDebugLogger, toBigIntBE } from '@aztec/foundation'; +import { LeafData, SiblingPath } from '@aztec/merkle-tree'; import { Tx } from '@aztec/tx'; import { MerkleTreeId, MerkleTreeOperations } from '@aztec/world-state'; import flatMap from 'lodash.flatmap'; @@ -40,6 +42,24 @@ const FUTURE_NUM = 0; const DELETE_FR = new Fr(0n); const DELETE_NUM = 0; +/** + * All of the data required for the circuit compute and verify nullifiers + */ +export interface LowNullifierWitnessData { + /** + * Preimage of the low nullifier that proves non membership + */ + preimage: NullifierLeafPreimage; + /** + * Sibling path to prove membership of low nullifier + */ + siblingPath: SiblingPath; + /** + * The index of low nullifier + */ + index: bigint; +} + export class CircuitPoweredBlockBuilder { constructor( protected db: MerkleTreeOperations, @@ -178,8 +198,7 @@ export class CircuitPoweredBlockBuilder { await Promise.all([ this.validateTree(rollupOutput, MerkleTreeId.CONTRACT_TREE, 'Contract'), this.validateTree(rollupOutput, MerkleTreeId.DATA_TREE, 'PrivateData'), - // TODO: Wait for new implementation of nullifier tree to avoid mismatches here - // this.validateTree(rollupOutput, MerkleTreeId.NULLIFIER_TREE, 'Nullifier'), + this.validateTree(rollupOutput, MerkleTreeId.NULLIFIER_TREE, 'Nullifier'), ]); } @@ -381,6 +400,144 @@ export class CircuitPoweredBlockBuilder { return fullSiblingPath.data.slice(subtreeHeight).map(b => Fr.fromBuffer(b)); } + /** + * Each base rollup needs to provide non membership / inclusion proofs for each of the nullifier. + * This method will return membership proofs and perform partial node updates that will + * allow the circuit to incrementally update the tree and perform a batch insertion. + * + * This offers massive circuit performance savings over doing incremental insertions. + * + * A description of the algorithm can be found here: https://colab.research.google.com/drive/1A0gizduSi4FIiIJZ8OylwIpO9-OTqV-R + * + * WARNING: This function has side effects, it will insert values into the tree. + * + * Assumptions: + * 1. There are 8 nullifiers provided and they are either unique or empty. (denoted as 0) + * 2. If kc 0 has 1 nullifier, and kc 1 has 3 nullifiers the layout will assume to be the sparse + * nullifier layout: [kc0-0, 0, 0, 0, kc1-0, kc1-1, kc1-2, 0] + * + * TODO: this implementation will change once the zero value is changed from h(0,0,0). Changes incoming over the next sprint + * @param leaves Values to insert into the tree + * @returns + */ + public async performBaseRollupBatchInsertionProofs(leaves: Buffer[]): Promise { + // Keep track of the touched during batch insertion + const touchedNodes: Set = new Set(); + + // Return data + const lowNullifierWitnesses: LowNullifierWitnessData[] = []; + const dbInfo = await this.db.getTreeInfo(MerkleTreeId.NULLIFIER_TREE); + const startInsertionIndex: bigint = dbInfo.size; + let currInsertionIndex: bigint = startInsertionIndex; + + // Leaf data of hte leaves to be inserted + const insertionSubtree: NullifierLeafPreimage[] = []; + + // Low nullifier membership proof sibling paths + for (const leaf of leaves) { + const newValue = toBigIntBE(leaf); + const indexOfPrevious = await this.db.getPreviousValueIndex(MerkleTreeId.NULLIFIER_TREE, newValue); + + // NOTE: null values for nullfier leaves are being changed to 0n current impl is a hack + // Default value + const nullifierLeaf: NullifierLeafPreimage = new NullifierLeafPreimage(new Fr(newValue), new Fr(0n), 0); + if (touchedNodes.has(indexOfPrevious.index) || newValue === 0n) { + // If the node has already been touched, then we return an empty leaf and sibling path + const emptySP = new SiblingPath(); + emptySP.data = Array(dbInfo.depth).fill( + Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'), + ); + const witness: LowNullifierWitnessData = { + preimage: NullifierLeafPreimage.empty(), + index: 0n, + siblingPath: emptySP, + }; + lowNullifierWitnesses.push(witness); + } else { + // If the node has not been touched, we update its low nullifier pointer, but we do NOT insert it yet, inserting it now + // will alter non membership paths of the not yet inserted members + // Insertion is done at the end once updates have already occurred. + touchedNodes.add(indexOfPrevious.index); + + const lowNullifier = await this.db.getLeafData(MerkleTreeId.NULLIFIER_TREE, indexOfPrevious.index); + + // If no low nullifier can be found, abort - this means the nullifier is invalid + // in some way (it should not happen) + if (lowNullifier === undefined) { + return undefined; + } + + const lowNullifierPreimage = new NullifierLeafPreimage( + new Fr(lowNullifier.value), + new Fr(lowNullifier.nextValue), + Number(lowNullifier.nextIndex), + ); + + // Get sibling path for existence of the old leaf + const siblingPath = await this.db.getSiblingPath(MerkleTreeId.NULLIFIER_TREE, BigInt(indexOfPrevious.index)); + + // Update the running paths + const witness = { + preimage: lowNullifierPreimage, + index: BigInt(indexOfPrevious.index), + siblingPath: siblingPath, + }; + lowNullifierWitnesses.push(witness); + + // Update subtree insertion leaf from null data + nullifierLeaf.nextIndex = lowNullifierPreimage.nextIndex; + nullifierLeaf.nextValue = lowNullifierPreimage.nextValue; + + // Update the current low nullifier + lowNullifier.nextIndex = currInsertionIndex; + lowNullifier.nextValue = BigInt(newValue); + + // Update the old leaf in the tree + await this.db.updateLeaf(MerkleTreeId.NULLIFIER_TREE, lowNullifier, BigInt(indexOfPrevious.index)); + } + + // increment insertion index + currInsertionIndex++; + insertionSubtree.push(nullifierLeaf); + } + + // Create insertion subtree and forcefully insert in series + // Here we calculate the pointers for the inserted values, if they have not already been updated + for (let i = 0; i < leaves.length; i++) { + const newValue = new Fr(toBigIntBE(leaves[i])); + + if (newValue.isZero()) continue; + + // We have already fetched the new low nullifier for this leaf, so we can set its low nullifier + const lowNullifier = lowNullifierWitnesses[i].preimage; + // If the lowNullifier is 0, then we check the previous leaves for the low nullifier leaf + if (lowNullifier.leafValue.isZero() && lowNullifier.nextValue.isZero() && lowNullifier.nextIndex === 0) { + for (let j = 0; j < i; j++) { + if ( + (insertionSubtree[j].nextValue > newValue && insertionSubtree[j].leafValue < newValue) || + (insertionSubtree[j].nextValue.isZero() && insertionSubtree[j].nextIndex === 0) + ) { + insertionSubtree[j].nextIndex = Number(startInsertionIndex) + i; + insertionSubtree[j].nextValue = newValue; + } + } + } + } + + // For each calculated new leaf, we insert it into the tree at the next position + for (let i = 0; i < insertionSubtree.length; i++) { + const asLeafData: LeafData = { + value: insertionSubtree[i].leafValue.value, + nextValue: insertionSubtree[i].nextValue.value, + nextIndex: BigInt(insertionSubtree[i].nextIndex), + }; + + await this.db.updateLeaf(MerkleTreeId.NULLIFIER_TREE, asLeafData, startInsertionIndex + BigInt(i)); + } + + return lowNullifierWitnesses; + } + // Builds the base rollup inputs, updating the contract, nullifier, and data trees in the process protected async buildBaseRollupInput(tx1: Tx, tx2: Tx) { // Get trees info before any changes hit @@ -396,33 +553,21 @@ export class CircuitPoweredBlockBuilder { ); const newCommitments = flatMap([tx1, tx2], tx => tx.data.end.newCommitments.map(x => x.toBuffer())); - // console.log(`Contract root before insertion: `, await this.getTreeSnapshot(MerkleTreeId.CONTRACT_TREE).then(t => t.root.toBuffer().toString('hex'))) - // console.log(`New contracts to insert`, flatMap([tx1, tx2], tx => tx.data.end.newContracts.map(nc => [nc.contractAddress, nc.functionTreeRoot, nc.portalContractAddress].join('/'))).join(', ')) - // console.log(`Inserting new contracts hashes`, newContracts.map(c => c.toString('hex')).join(', ')) await this.db.appendLeaves(MerkleTreeId.CONTRACT_TREE, newContracts); - // console.log(`Contract root after insertion: `, await this.getTreeSnapshot(MerkleTreeId.CONTRACT_TREE).then(t => t.root)) - // console.log(`Data root before insertion: `, await this.getTreeSnapshot(MerkleTreeId.DATA_TREE).then(t => t.root)) - // console.log(`Inserting new data`, newCommitments.map(c => c.toString('hex')).join(', ')) await this.db.appendLeaves(MerkleTreeId.DATA_TREE, newCommitments); - // console.log(`Data root after insertion: `, await this.getTreeSnapshot(MerkleTreeId.DATA_TREE).then(t => t.root)) // Update the nullifier tree, capturing the low nullifier info for each individual operation const newNullifiers = [...tx1.data.end.newNullifiers, ...tx2.data.end.newNullifiers]; - const lowNullifierInfos = []; - // console.log( - // `Nullifier root before insertion: `, - // await this.getTreeSnapshot(MerkleTreeId.NULLIFIER_TREE).then(t => '0x' + t.root.toBuffer().toString('hex')), - // ); - // console.log(`Inserting new data`, newNullifiers.join(', ')); - for (const nullifier of newNullifiers) { - lowNullifierInfos.push(await this.getLowNullifierInfo(nullifier)); - await this.db.appendLeaves(MerkleTreeId.NULLIFIER_TREE, [nullifier.toBuffer()]); + + const nullifierWitnesses = await this.performBaseRollupBatchInsertionProofs(newNullifiers.map(fr => fr.toBuffer())); + if (nullifierWitnesses === undefined) { + throw new Error(`Could not craft nullifier batch insertion proofs`); } - // console.log( - // `Nullifier root after insertion: `, - // await this.getTreeSnapshot(MerkleTreeId.NULLIFIER_TREE).then(t => '0x' + t.root.toBuffer().toString('hex')), - // ); + // Extract witness objects from returned data + const lowNullifierMembershipWitnesses = nullifierWitnesses.map(w => + MembershipWitness.fromBufferArray(Number(w.index), w.siblingPath.data), + ); // Get the subtree sibling paths for the circuit const newCommitmentsSubtreeSiblingPath = await this.getSubtreeSiblingPath( @@ -446,8 +591,8 @@ export class CircuitPoweredBlockBuilder { newCommitmentsSubtreeSiblingPath, newContractsSubtreeSiblingPath, newNullifiersSubtreeSiblingPath, - lowNullifierLeafPreimages: lowNullifierInfos.map(i => i.leafPreimage), - lowNullifierMembershipWitness: lowNullifierInfos.map(i => i.witness), + lowNullifierLeafPreimages: nullifierWitnesses.map((w: LowNullifierWitnessData) => w.preimage), + lowNullifierMembershipWitness: lowNullifierMembershipWitnesses, kernelData: [this.getKernelDataFor(tx1), this.getKernelDataFor(tx2)], historicContractsTreeRootMembershipWitnesses: [ await this.getContractMembershipWitnessFor(tx1), diff --git a/yarn-project/sequencer-client/src/sequencer/index.ts b/yarn-project/sequencer-client/src/sequencer/index.ts index 69a46c77a3a8..7eb31272ab39 100644 --- a/yarn-project/sequencer-client/src/sequencer/index.ts +++ b/yarn-project/sequencer-client/src/sequencer/index.ts @@ -94,15 +94,15 @@ export class Sequencer { */ protected async work() { try { - // Update state when the previous block has been synched - const prevBlockSynched = await this.isBlockSynched(); - if (prevBlockSynched && this.state === SequencerState.PUBLISHING_BLOCK) { - this.log(`Block has been synched`); + // Update state when the previous block has been synced + const prevBlockSynced = await this.isBlockSynced(); + if (prevBlockSynced && this.state === SequencerState.PUBLISHING_BLOCK) { + this.log(`Block has been synced`); this.state = SequencerState.IDLE; } // Do not go forward with new block if the previous one has not been mined and processed - if (!prevBlockSynched) { + if (!prevBlockSynced) { return; } @@ -157,9 +157,9 @@ export class Sequencer { /** * Returns whether the previous block sent has been mined, and all dependencies have caught up with it. - * @returns Boolean indicating if our dependencies are synched to the latest block. + * @returns Boolean indicating if our dependencies are synced to the latest block. */ - protected async isBlockSynched() { + protected async isBlockSynced() { return ( (await this.worldState.status().then((s: WorldStateStatus) => s.syncedToL2Block)) >= this.lastBlockNumber && (await this.p2pClient.getStatus().then(s => s.syncedToL2Block)) >= this.lastBlockNumber diff --git a/yarn-project/world-state/src/merkle-tree/merkle_tree_operations_facade.ts b/yarn-project/world-state/src/merkle-tree/merkle_tree_operations_facade.ts index 7b25ebee0662..09e2289e2380 100644 --- a/yarn-project/world-state/src/merkle-tree/merkle_tree_operations_facade.ts +++ b/yarn-project/world-state/src/merkle-tree/merkle_tree_operations_facade.ts @@ -21,6 +21,9 @@ export class MerkleTreeOperationsFacade implements MerkleTreeOperations { ): Promise<{ index: number; alreadyPresent: boolean }> { return this.trees.getPreviousValueIndex(treeId, value, this.includeUncommitted); } + updateLeaf(treeId: MerkleTreeId.NULLIFIER_TREE, leaf: LeafData, index: bigint): Promise { + return this.trees.updateLeaf(treeId, leaf, index, this.includeUncommitted); + } getLeafData(treeId: MerkleTreeId.NULLIFIER_TREE, index: number): Promise { return this.trees.getLeafData(treeId, index, this.includeUncommitted); } diff --git a/yarn-project/world-state/src/world-state-db/index.ts b/yarn-project/world-state/src/world-state-db/index.ts index d13ea4db461e..fc5aa0a83b45 100644 --- a/yarn-project/world-state/src/world-state-db/index.ts +++ b/yarn-project/world-state/src/world-state-db/index.ts @@ -16,6 +16,13 @@ export enum MerkleTreeId { export type IndexedMerkleTreeId = MerkleTreeId.NULLIFIER_TREE; +/** + * The nullifier tree must be pre filled with the number of leaves that are added by one rollup. + * The tree must be initially padded as the pre-populated 0 index prevents efficient subtree insertion. + * Padding with some values solves this issue. + */ +export const INITIAL_NULLIFIER_TREE_SIZE = 8; + /** * Defines tree information. */ @@ -32,6 +39,11 @@ export interface TreeInfo { * The number of leaves in the tree. */ size: bigint; + + /** + * The depth of the tree. + */ + depth: number; } /** @@ -88,6 +100,13 @@ export interface MerkleTreeOperations { * @param index - The index of the leaf required */ getLeafData(treeId: IndexedMerkleTreeId, index: number): Promise; + /** + * Update the leaf data at the given index + * @param treeId - The tree for which leaf data should be edited + * @param leaf - The updated leaf value + * @param index - The index of the leaf to be updated + */ + updateLeaf(treeId: IndexedMerkleTreeId, leaf: LeafData, index: bigint): Promise; /** * Returns the index containing a leaf value * @param treeId - The tree for which the index should be returned diff --git a/yarn-project/world-state/src/world-state-db/merkle_trees.ts b/yarn-project/world-state/src/world-state-db/merkle_trees.ts index 32efefa31fe1..fc191150bbd5 100644 --- a/yarn-project/world-state/src/world-state-db/merkle_trees.ts +++ b/yarn-project/world-state/src/world-state-db/merkle_trees.ts @@ -9,8 +9,15 @@ import { BarretenbergWasm } from '@aztec/barretenberg.js/wasm'; import { SerialQueue } from '@aztec/foundation'; import { IndexedTree, LeafData, MerkleTree, Pedersen, SiblingPath, StandardMerkleTree } from '@aztec/merkle-tree'; import { default as levelup } from 'levelup'; -import { IndexedMerkleTreeId, MerkleTreeDb, MerkleTreeId, MerkleTreeOperations, TreeInfo } from './index.js'; import { MerkleTreeOperationsFacade } from '../merkle-tree/merkle_tree_operations_facade.js'; +import { + INITIAL_NULLIFIER_TREE_SIZE, + IndexedMerkleTreeId, + MerkleTreeDb, + MerkleTreeId, + MerkleTreeOperations, + TreeInfo, +} from './index.js'; /** * A convenience class for managing multiple merkle trees. @@ -44,6 +51,7 @@ export class MerkleTrees implements MerkleTreeDb { hasher, `${MerkleTreeId[MerkleTreeId.NULLIFIER_TREE]}`, NULLIFIER_TREE_HEIGHT, + INITIAL_NULLIFIER_TREE_SIZE, ); const dataTree = await StandardMerkleTree.new( this.db, @@ -191,6 +199,16 @@ export class MerkleTrees implements MerkleTreeDb { }); } + /** + * Updates a leaf in a tree at a given index. + * @param treeId - The ID of the tree + * @param leaf - The new leaf value + * @param index - The index to insert into + */ + public async updateLeaf(treeId: IndexedMerkleTreeId, leaf: LeafData, index: bigint): Promise { + return await this.synchronise(() => this.trees[treeId].updateLeaf(leaf, index)); + } + /** * Waits for all jobs to finish before executing the given function. * @param fn - The function to execute. @@ -210,6 +228,7 @@ export class MerkleTrees implements MerkleTreeDb { treeId, root: this.trees[treeId].getRoot(includeUncommitted), size: this.trees[treeId].getNumLeaves(includeUncommitted), + depth: this.trees[treeId].getDepth(), } as TreeInfo; return Promise.resolve(treeInfo); } diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 40e990a3917d..8b7ec6f06c53 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -206,6 +206,7 @@ __metadata: resolution: "@aztec/circuits.js@workspace:circuits.js" dependencies: "@aztec/foundation": "workspace:^" + "@aztec/merkle-tree": "workspace:^" "@jest/globals": ^29.4.3 "@types/detect-node": ^2.0.0 "@types/jest": ^29.4.0