diff --git a/circuits b/circuits index 7679e98cb4e5..eeb138136492 160000 --- a/circuits +++ b/circuits @@ -1 +1 @@ -Subproject commit 7679e98cb4e5af7be6855fb39eb7f37e26ebe717 +Subproject commit eeb13813649213a10514fc0045c9309bee7c90eb diff --git a/yarn-project/aztec.js/Dockerfile b/yarn-project/aztec.js/Dockerfile index 614f06b17991..2e4ade811397 100644 --- a/yarn-project/aztec.js/Dockerfile +++ b/yarn-project/aztec.js/Dockerfile @@ -1,6 +1,6 @@ FROM 278380418400.dkr.ecr.eu-west-2.amazonaws.com/yarn-project-base AS builder COPY . . -RUN cd aztec-rpc && yarn build +RUN cd aztec-js && yarn build WORKDIR /usr/src/yarn-project/aztec.js RUN yarn build && yarn formatting && yarn test diff --git a/yarn-project/circuits.js/src/structs/__snapshots__/base_rollup.test.ts.snap b/yarn-project/circuits.js/src/structs/__snapshots__/base_rollup.test.ts.snap index c6ea9571b44d..7f25306ddff8 100644 --- a/yarn-project/circuits.js/src/structs/__snapshots__/base_rollup.test.ts.snap +++ b/yarn-project/circuits.js/src/structs/__snapshots__/base_rollup.test.ts.snap @@ -115,7 +115,7 @@ portal_contract_address: 0x707070707070707070707070707070707070707 is_private: 1 proof: [ 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 ] -vk: 0x12ca00 +vk: 0x12c940 vk_index: 42 vk_path: [ 0x1000 0x1001 0x1002 ] public_inputs: end: @@ -231,7 +231,7 @@ portal_contract_address: 0x707070707070707070707070707070707070707 is_private: 1 proof: [ 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 ] -vk: 0x135540 +vk: 0x135480 vk_index: 42 vk_path: [ 0x1000 0x1001 0x1002 ] ] @@ -284,11 +284,11 @@ sibling_path: [ 0x2006 0x2007 0x2008 0x2009 0x200a 0x200b 0x200c 0x200d ] sibling_path: [ 0x2007 0x2008 0x2009 0x200a 0x200b 0x200c 0x200d 0x200e ] ] new_commitments_subtree_sibling_path: -[ 0x3000 0x3001 0x3002 0x3003 0x3004 0x3005 0x3006 0x3007 ] +[ 0x3000 0x3001 0x3002 0x3003 0x3004 ] new_nullifiers_subtree_sibling_path: -[ 0x4000 0x4001 0x4002 0x4003 0x4004 0x4005 0x4006 0x4007 ] +[ 0x4000 0x4001 0x4002 0x4003 0x4004 ] new_contracts_subtree_sibling_path: -[ 0x5000 0x5001 0x5002 0x5003 ] +[ 0x5000 0x5001 0x5002 ] historic_private_data_tree_root_membership_witnesses: [ leaf_index: 6000 sibling_path: [ 0x6000 0x6001 0x6002 0x6003 0x6004 0x6005 0x6006 0x6007 ] diff --git a/yarn-project/circuits.js/src/structs/base_rollup.test.ts b/yarn-project/circuits.js/src/structs/base_rollup.test.ts index 3366c6ab435c..e58c805f87f0 100644 --- a/yarn-project/circuits.js/src/structs/base_rollup.test.ts +++ b/yarn-project/circuits.js/src/structs/base_rollup.test.ts @@ -39,9 +39,18 @@ describe('structs/base_rollup', () => { MembershipWitness.mock(NULLIFIER_TREE_HEIGHT, x), ); - const newCommitmentsSubtreeSiblingPath = range(PRIVATE_DATA_TREE_HEIGHT, 0x3000).map(x => fr(x)); - const newNullifiersSubtreeSiblingPath = range(NULLIFIER_TREE_HEIGHT, 0x4000).map(x => fr(x)); - const newContractsSubtreeSiblingPath = range(CONTRACT_TREE_HEIGHT, 0x5000).map(x => fr(x)); + const newCommitmentsSubtreeSiblingPath = range( + PRIVATE_DATA_TREE_HEIGHT - BaseRollupInputs.PRIVATE_DATA_SUBTREE_HEIGHT, + 0x3000, + ).map(x => fr(x)); + const newNullifiersSubtreeSiblingPath = range( + NULLIFIER_TREE_HEIGHT - BaseRollupInputs.NULLIFIER_SUBTREE_HEIGHT, + 0x4000, + ).map(x => fr(x)); + const newContractsSubtreeSiblingPath = range( + CONTRACT_TREE_HEIGHT - BaseRollupInputs.CONTRACT_SUBTREE_HEIGHT, + 0x5000, + ).map(x => fr(x)); const historicPrivateDataTreeRootMembershipWitnesses: BaseRollupInputs['historicPrivateDataTreeRootMembershipWitnesses'] = [ diff --git a/yarn-project/circuits.js/src/structs/base_rollup.ts b/yarn-project/circuits.js/src/structs/base_rollup.ts index 5642539fe2fe..d707376e5f84 100644 --- a/yarn-project/circuits.js/src/structs/base_rollup.ts +++ b/yarn-project/circuits.js/src/structs/base_rollup.ts @@ -4,6 +4,8 @@ import { serializeToBuffer } from '../utils/serialize.js'; import { CONTRACT_TREE_HEIGHT, CONTRACT_TREE_ROOTS_TREE_HEIGHT, + KERNEL_NEW_COMMITMENTS_LENGTH, + KERNEL_NEW_CONTRACTS_LENGTH, KERNEL_NEW_NULLIFIERS_LENGTH, NULLIFIER_TREE_HEIGHT, PRIVATE_DATA_TREE_HEIGHT, @@ -85,6 +87,10 @@ export class ConstantBaseRollupData { * Inputs to the base rollup circuit */ export class BaseRollupInputs { + public static PRIVATE_DATA_SUBTREE_HEIGHT = Math.log2(KERNEL_NEW_COMMITMENTS_LENGTH * 2); + public static CONTRACT_SUBTREE_HEIGHT = Math.log2(KERNEL_NEW_CONTRACTS_LENGTH * 2); + public static NULLIFIER_SUBTREE_HEIGHT = Math.log2(KERNEL_NEW_NULLIFIERS_LENGTH * 2); + constructor( public kernelData: [PreviousKernelData, PreviousKernelData], @@ -112,9 +118,21 @@ export class BaseRollupInputs { ) { assertLength(this, 'lowNullifierLeafPreimages', 2 * KERNEL_NEW_NULLIFIERS_LENGTH); assertLength(this, 'lowNullifierMembershipWitness', 2 * KERNEL_NEW_NULLIFIERS_LENGTH); - assertLength(this, 'newCommitmentsSubtreeSiblingPath', PRIVATE_DATA_TREE_HEIGHT); - assertLength(this, 'newNullifiersSubtreeSiblingPath', NULLIFIER_TREE_HEIGHT); - assertLength(this, 'newContractsSubtreeSiblingPath', CONTRACT_TREE_HEIGHT); + assertLength( + this, + 'newCommitmentsSubtreeSiblingPath', + PRIVATE_DATA_TREE_HEIGHT - BaseRollupInputs.PRIVATE_DATA_SUBTREE_HEIGHT, + ); + assertLength( + this, + 'newNullifiersSubtreeSiblingPath', + NULLIFIER_TREE_HEIGHT - BaseRollupInputs.NULLIFIER_SUBTREE_HEIGHT, + ); + assertLength( + this, + 'newContractsSubtreeSiblingPath', + CONTRACT_TREE_HEIGHT - BaseRollupInputs.CONTRACT_SUBTREE_HEIGHT, + ); } static from(fields: FieldsOf): BaseRollupInputs { @@ -155,7 +173,7 @@ export class BaseRollupPublicInputs { public endPrivateDataTreeSnapshot: AppendOnlyTreeSnapshot, public startNullifierTreeSnapshot: AppendOnlyTreeSnapshot, - public endNullifierTreeSnapshots: AppendOnlyTreeSnapshot, + public endNullifierTreeSnapshot: AppendOnlyTreeSnapshot, public startContractTreeSnapshot: AppendOnlyTreeSnapshot, public endContractTreeSnapshot: AppendOnlyTreeSnapshot, @@ -196,7 +214,7 @@ export class BaseRollupPublicInputs { this.endPrivateDataTreeSnapshot, this.startNullifierTreeSnapshot, - this.endNullifierTreeSnapshots, + this.endNullifierTreeSnapshot, this.startContractTreeSnapshot, this.endContractTreeSnapshot, diff --git a/yarn-project/circuits.js/src/structs/call_context.ts b/yarn-project/circuits.js/src/structs/call_context.ts index 16676997fd42..d4fd5045c6cf 100644 --- a/yarn-project/circuits.js/src/structs/call_context.ts +++ b/yarn-project/circuits.js/src/structs/call_context.ts @@ -23,7 +23,7 @@ export class CallContext { return serializeToBuffer( this.msgSender, this.storageContractAddress, - this.portalContractAddress.toBuffer(), + this.portalContractAddress, this.isDelegateCall, this.isStaticCall, this.isContractDeployment, diff --git a/yarn-project/circuits.js/src/structs/root_rollup.ts b/yarn-project/circuits.js/src/structs/root_rollup.ts index 2fb590fefbf0..7466dcd59742 100644 --- a/yarn-project/circuits.js/src/structs/root_rollup.ts +++ b/yarn-project/circuits.js/src/structs/root_rollup.ts @@ -2,7 +2,7 @@ import { Fr } from '@aztec/foundation'; import { assertLength, FieldsOf } from '../utils/jsUtils.js'; import { serializeToBuffer } from '../utils/serialize.js'; import { AppendOnlyTreeSnapshot } from './base_rollup.js'; -import { CONTRACT_TREE_HEIGHT, NULLIFIER_TREE_HEIGHT, PRIVATE_DATA_TREE_HEIGHT } from './constants.js'; +import { CONTRACT_TREE_ROOTS_TREE_HEIGHT, PRIVATE_DATA_TREE_HEIGHT } from './constants.js'; import { PreviousRollupData } from './merge_rollup.js'; import { AggregationObject } from './shared.js'; @@ -10,31 +10,32 @@ export class RootRollupInputs { constructor( public previousRollupData: [PreviousRollupData, PreviousRollupData], - public startPrivateDataTreeSnapshot: AppendOnlyTreeSnapshot, - // Note: the start_nullifier_tree_snapshot is contained within previous_rollup_data[0].public_inputs.start_nullifier_tree_snapshot. - public startContractTreeSnapshot: AppendOnlyTreeSnapshot, - - // For inserting the new subtrees into their respective trees: - // Note: the insertion leaf index can be derived from the above snapshots' `next_available_leaf_index` values. - public newCommitmentsSubtreeSiblingPath: Fr[], - public newNullifiersSubtreeSiblingPath: Fr[], - public newContractsSubtreeSiblingPath: Fr[], + public newHistoricPrivateDataTreeRootSiblingPath: Fr[], + public newHistoricContractDataTreeRootSiblingPath: Fr[], ) { - assertLength(this, 'newCommitmentsSubtreeSiblingPath', PRIVATE_DATA_TREE_HEIGHT); - assertLength(this, 'newNullifiersSubtreeSiblingPath', NULLIFIER_TREE_HEIGHT); - assertLength(this, 'newContractsSubtreeSiblingPath', CONTRACT_TREE_HEIGHT); + assertLength(this, 'newHistoricPrivateDataTreeRootSiblingPath', PRIVATE_DATA_TREE_HEIGHT); + assertLength(this, 'newHistoricContractDataTreeRootSiblingPath', CONTRACT_TREE_ROOTS_TREE_HEIGHT); } toBuffer() { return serializeToBuffer( this.previousRollupData, - this.startPrivateDataTreeSnapshot, - this.startContractTreeSnapshot, - this.newCommitmentsSubtreeSiblingPath, - this.newNullifiersSubtreeSiblingPath, - this.newContractsSubtreeSiblingPath, + this.newHistoricPrivateDataTreeRootSiblingPath, + this.newHistoricContractDataTreeRootSiblingPath, ); } + + static from(fields: FieldsOf): RootRollupInputs { + return new RootRollupInputs(...RootRollupInputs.getFields(fields)); + } + + static getFields(fields: FieldsOf) { + return [ + fields.previousRollupData, + fields.newHistoricPrivateDataTreeRootSiblingPath, + fields.newHistoricContractDataTreeRootSiblingPath, + ] as const; + } } export class RootRollupPublicInputs { diff --git a/yarn-project/circuits.js/src/structs/shared.ts b/yarn-project/circuits.js/src/structs/shared.ts index 93df6125969f..986a56b87389 100644 --- a/yarn-project/circuits.js/src/structs/shared.ts +++ b/yarn-project/circuits.js/src/structs/shared.ts @@ -1,11 +1,10 @@ import { BufferReader } from '@aztec/foundation'; import { Fq, Fr } from '@aztec/foundation/fields'; -import { assertLength, checkLength, range } from '../utils/jsUtils.js'; +import { assertLength, range } from '../utils/jsUtils.js'; import { Bufferable, serializeToBuffer } from '../utils/serialize.js'; - export class MembershipWitness { constructor(pathSize: N, public leafIndex: UInt32, public siblingPath: Fr[]) { - checkLength(this.siblingPath, pathSize, 'MembershipWitness.siblingPath'); + assertLength(this, 'siblingPath', pathSize); } toBuffer() { diff --git a/yarn-project/circuits.js/src/tests/factories.ts b/yarn-project/circuits.js/src/tests/factories.ts index e31620a2a826..c0b4d27b101c 100644 --- a/yarn-project/circuits.js/src/tests/factories.ts +++ b/yarn-project/circuits.js/src/tests/factories.ts @@ -1,5 +1,5 @@ -import { EthAddress, Fq, Fr, AztecAddress } from '@aztec/foundation'; -import { CallContext, PrivateCircuitPublicInputs } from '../index.js'; +import { AztecAddress, EthAddress, Fq, Fr } from '@aztec/foundation'; +import { CallContext, PrivateCircuitPublicInputs, RootRollupPublicInputs } from '../index.js'; import { AppendOnlyTreeSnapshot, BaseRollupPublicInputs, ConstantBaseRollupData } from '../structs/base_rollup.js'; import { ARGS_LENGTH, @@ -38,9 +38,9 @@ import { AffineElement, AggregationObject, ComposerType, + EcdsaSignature, MembershipWitness, UInt8Vector, - EcdsaSignature, } from '../structs/shared.js'; import { ContractDeploymentData, SignedTxRequest, TxContext, TxRequest } from '../structs/tx.js'; import { CommitmentMap, G1AffineElement, VerificationKey } from '../structs/verification_key.js'; @@ -246,10 +246,31 @@ export function makeBaseRollupPublicInputs(seed = 0) { makeAppendOnlyTreeSnapshot(seed + 0x600), makeAppendOnlyTreeSnapshot(seed + 0x700), makeAppendOnlyTreeSnapshot(seed + 0x800), - range(2, seed + 0x901).map(fr) as [Fr, Fr], + [fr(seed + 0x901), fr(seed + 0x902)], ); } +export function makeRootRollupPublicInputs(seed = 0) { + return RootRollupPublicInputs.from({ + startContractTreeSnapshot: makeAppendOnlyTreeSnapshot(seed + 0x100), + startNullifierTreeSnapshot: makeAppendOnlyTreeSnapshot(seed + 0x200), + startPrivateDataTreeSnapshot: makeAppendOnlyTreeSnapshot(seed + 0x300), + startTreeOfHistoricContractTreeRootsSnapshot: makeAppendOnlyTreeSnapshot(seed + 0x400), + startTreeOfHistoricPrivateDataTreeRootsSnapshot: makeAppendOnlyTreeSnapshot(seed + 0x500), + endContractTreeSnapshot: makeAppendOnlyTreeSnapshot(seed + 0x600), + endNullifierTreeSnapshot: makeAppendOnlyTreeSnapshot(seed + 0x700), + endPrivateDataTreeSnapshot: makeAppendOnlyTreeSnapshot(seed + 0x800), + endTreeOfHistoricContractTreeRootsSnapshot: makeAppendOnlyTreeSnapshot(seed + 0x900), + endTreeOfHistoricPrivateDataTreeRootsSnapshot: makeAppendOnlyTreeSnapshot(seed + 0x1000), + endAggregationObject: makeAggregationObject(seed + 0x1100), + newCommitmentsHash: fr(seed + 0x1200), + newContractDataHash: fr(seed + 0x1200), + newL1MsgsHash: fr(seed + 0x1200), + newNullifiersHash: fr(seed + 0x1200), + proverContributionsHash: fr(seed + 0x1200), + }); +} + /** * Test only. Easy to identify big endian field serialize. * @param num - The number. diff --git a/yarn-project/circuits.js/src/utils/jsUtils.ts b/yarn-project/circuits.js/src/utils/jsUtils.ts index 33c815e49410..10e55fd6071d 100644 --- a/yarn-project/circuits.js/src/utils/jsUtils.ts +++ b/yarn-project/circuits.js/src/utils/jsUtils.ts @@ -28,18 +28,6 @@ export function assertLength Buffer; + } | { /** * Serialize to a buffer. @@ -113,6 +119,10 @@ export type Bufferable = } | Bufferable[]; +function isSerializableToBuffer32(obj: Object): obj is { toBuffer32: () => Buffer } { + return !!(obj as { toBuffer32: () => Buffer }).toBuffer32; +} + /** * Serializes a list of objects contiguously for calling into wasm. * @param objs - Objects to serialize. @@ -134,6 +144,8 @@ export function serializeToBufferArray(...objs: Bufferable[]): Buffer[] { } else if (typeof obj === 'string') { ret.push(numToUInt32BE(obj.length)); ret.push(Buffer.from(obj)); + } else if (isSerializableToBuffer32(obj)) { + ret.push(obj.toBuffer32()); } else { ret.push(obj.toBuffer()); } diff --git a/yarn-project/foundation/src/fields/fields.ts b/yarn-project/foundation/src/fields/fields.ts index bda995797185..20f53bd27c50 100644 --- a/yarn-project/foundation/src/fields/fields.ts +++ b/yarn-project/foundation/src/fields/fields.ts @@ -27,6 +27,10 @@ export class Fr { toBuffer() { return toBufferBE(this.value, Fr.SIZE_IN_BYTES); } + + toString() { + return '0x' + this.value.toString(16); + } } export class Fq { @@ -53,4 +57,8 @@ export class Fq { toBuffer() { return toBufferBE(this.value, Fq.SIZE_IN_BYTES); } + + toString() { + return '0x' + this.value.toString(16); + } } diff --git a/yarn-project/sequencer-client/package.json b/yarn-project/sequencer-client/package.json index 1979ebbe55b7..b69e8002f1e4 100644 --- a/yarn-project/sequencer-client/package.json +++ b/yarn-project/sequencer-client/package.json @@ -37,9 +37,11 @@ "@aztec/foundation": "workspace:^", "@aztec/l1-contracts": "workspace:^", "@aztec/l2-block": "workspace:^", + "@aztec/merkle-tree": "workspace:^", "@aztec/p2p": "workspace:^", "@aztec/tx": "workspace:^", "@aztec/world-state": "workspace:^", + "lodash.flatmap": "^4.5.0", "lodash.times": "^4.3.2", "tslib": "^2.4.0" }, @@ -47,6 +49,7 @@ "@jest/globals": "^29.4.3", "@rushstack/eslint-patch": "^1.1.4", "@types/jest": "^29.4.0", + "@types/lodash.flatmap": "^4.5.7", "@types/lodash.times": "^4.3.7", "@types/node": "^18.7.23", "concurrently": "^7.6.0", diff --git a/yarn-project/sequencer-client/src/deps/tx.ts b/yarn-project/sequencer-client/src/deps/tx.ts index f95009dd9200..f7c5c69893bc 100644 --- a/yarn-project/sequencer-client/src/deps/tx.ts +++ b/yarn-project/sequencer-client/src/deps/tx.ts @@ -1,4 +1,7 @@ +import { BarretenbergWasm } from '@aztec/barretenberg.js/wasm'; +import { pedersenCompressInputs } from '@aztec/barretenberg.js/crypto'; import { + AccumulatedData, AffineElement, AggregationObject, AztecAddress, @@ -21,9 +24,8 @@ import { OptionallyRevealedData, PrivateKernelPublicInputs, TxContext, + UInt8Vector, } from '@aztec/circuits.js'; -import { AccumulatedData } from '@aztec/circuits.js'; -import { UInt8Vector } from '@aztec/circuits.js'; import { Tx } from '@aztec/tx'; import times from 'lodash.times'; @@ -39,11 +41,11 @@ function makeEmptyEthAddress() { return new EthAddress(Buffer.alloc(20, 0)); } -export function makeEmptyNewContractData(): NewContractData { +function makeEmptyNewContractData(): NewContractData { return new NewContractData(AztecAddress.ZERO, makeEmptyEthAddress(), frZero()); } -export function makeEmptyAggregationObject(): AggregationObject { +function makeEmptyAggregationObject(): AggregationObject { return new AggregationObject( new AffineElement(fqZero(), fqZero()), new AffineElement(fqZero(), fqZero()), @@ -52,20 +54,20 @@ export function makeEmptyAggregationObject(): AggregationObject { ); } -export function makeEmptyTxContext(): TxContext { +function makeEmptyTxContext(): TxContext { const deploymentData = new ContractDeploymentData(frZero(), frZero(), frZero(), makeEmptyEthAddress()); return new TxContext(false, false, true, deploymentData); } -export function makeEmptyOldTreeRoots(): OldTreeRoots { +function makeEmptyOldTreeRoots(): OldTreeRoots { return new OldTreeRoots(frZero(), frZero(), frZero(), frZero()); } -export function makeEmptyConstantData(): ConstantData { +function makeEmptyConstantData(): ConstantData { return new ConstantData(makeEmptyOldTreeRoots(), makeEmptyTxContext()); } -export function makeEmptyOptionallyRevealedData(): OptionallyRevealedData { +function makeEmptyOptionallyRevealedData(): OptionallyRevealedData { return new OptionallyRevealedData( frZero(), new FunctionData(0, true, true), @@ -79,7 +81,7 @@ export function makeEmptyOptionallyRevealedData(): OptionallyRevealedData { ); } -export function makeEmptyAccumulatedData(): AccumulatedData { +function makeEmptyAccumulatedData(): AccumulatedData { return new AccumulatedData( makeEmptyAggregationObject(), frZero(), @@ -106,5 +108,13 @@ function makeEmptyUnverifiedData() { } export function makeEmptyTx(): Tx { - return new Tx(makeEmptyPrivateKernelPublicInputs(), makeEmptyProof(), makeEmptyUnverifiedData()); + const isEmpty = true; + return new Tx(makeEmptyPrivateKernelPublicInputs(), makeEmptyProof(), makeEmptyUnverifiedData(), isEmpty); +} + +export function hashNewContractData(wasm: BarretenbergWasm, cd: NewContractData) { + return pedersenCompressInputs( + wasm, + [cd.contractAddress, cd.portalContractAddress, cd.functionTreeRoot].map(x => x.toBuffer()), + ); } diff --git a/yarn-project/sequencer-client/src/prover/mock.ts b/yarn-project/sequencer-client/src/prover/empty.ts similarity index 69% rename from yarn-project/sequencer-client/src/prover/mock.ts rename to yarn-project/sequencer-client/src/prover/empty.ts index 17c92a5dd6ea..9406035c3b74 100644 --- a/yarn-project/sequencer-client/src/prover/mock.ts +++ b/yarn-project/sequencer-client/src/prover/empty.ts @@ -11,14 +11,16 @@ import { Prover } from './index.js'; /* eslint-disable */ -export class MockProver implements Prover { +const EMPTY_PROOF_SIZE = 42; + +export class EmptyProver implements Prover { async getBaseRollupProof(input: BaseRollupInputs, publicInputs: BaseRollupPublicInputs): Promise { - return new UInt8Vector(Buffer.alloc(0)); + return new UInt8Vector(Buffer.alloc(EMPTY_PROOF_SIZE, 0)); } async getMergeRollupProof(input: MergeRollupInputs, publicInputs: MergeRollupPublicInputs): Promise { - return new UInt8Vector(Buffer.alloc(0)); + return new UInt8Vector(Buffer.alloc(EMPTY_PROOF_SIZE, 0)); } async getRootRollupProof(input: RootRollupInputs, publicInputs: RootRollupPublicInputs): Promise { - return new UInt8Vector(Buffer.alloc(0)); + return new UInt8Vector(Buffer.alloc(EMPTY_PROOF_SIZE, 0)); } } diff --git a/yarn-project/sequencer-client/src/sequencer/circuit_powered_block_builder.test.ts b/yarn-project/sequencer-client/src/sequencer/circuit_powered_block_builder.test.ts new file mode 100644 index 000000000000..ac7333197dbe --- /dev/null +++ b/yarn-project/sequencer-client/src/sequencer/circuit_powered_block_builder.test.ts @@ -0,0 +1,169 @@ +import { inspectTree, MerkleTreeDb, MerkleTreeId, MerkleTrees } from '@aztec/world-state'; +import { mock, MockProxy } from 'jest-mock-extended'; +import { Prover } from '../prover/index.js'; +import { Simulator } from '../simulator/index.js'; +import { CircuitPoweredBlockBuilder } from './circuit_powered_block_builder.js'; +import { VerificationKeys, getVerificationKeys } from './vks.js'; +import { default as memdown } from 'memdown'; +import { default as levelup } from 'levelup'; +import { + AppendOnlyTreeSnapshot, + BaseRollupInputs, + BaseRollupPublicInputs, + Fr, + RootRollupPublicInputs, + UInt8Vector, +} from '@aztec/circuits.js'; +import { Tx } from '@aztec/tx'; +import { + makeBaseRollupPublicInputs, + makePrivateKernelPublicInputs, + makeRootRollupPublicInputs, +} from '@aztec/circuits.js/factories'; +import { hashNewContractData, makeEmptyTx } from '../deps/tx.js'; +import flatMap from 'lodash.flatmap'; +import { BarretenbergWasm } from '@aztec/barretenberg.js/wasm'; + +/* eslint-disable @typescript-eslint/ban-ts-comment */ +// @ts-ignore +export const createMemDown = () => memdown(); + +describe('sequencer/circuit_block_builder', () => { + let builder: TestSubject; + let builderDb: MerkleTreeDb; + let expectsDb: MerkleTreeDb; + let vks: VerificationKeys; + let simulator: MockProxy; + let prover: MockProxy; + + let blockNumber: number; + let baseRollupOutputLeft: BaseRollupPublicInputs; + let baseRollupOutputRight: BaseRollupPublicInputs; + let rootRollupOutput: RootRollupPublicInputs; + + let wasm: BarretenbergWasm; + + const emptyProof = new UInt8Vector(Buffer.alloc(32, 0)); + + beforeAll(async () => { + wasm = new BarretenbergWasm(); + await wasm.init(); + }); + + beforeEach(async () => { + blockNumber = 3; + builderDb = await MerkleTrees.new(levelup(createMemDown())); + expectsDb = await MerkleTrees.new(levelup(createMemDown())); + vks = getVerificationKeys(); + simulator = mock(); + prover = mock(); + builder = new TestSubject(builderDb, blockNumber, vks, simulator, prover, wasm); + + // Populate root trees with first roots from the empty trees + // TODO: Should this be responsibility of the MerkleTreeDb init? + await updateRootTrees(); + await builder.updateRootTrees(); + + // Create mock outputs for simualator + baseRollupOutputLeft = makeBaseRollupPublicInputs(); + baseRollupOutputRight = makeBaseRollupPublicInputs(); + rootRollupOutput = makeRootRollupPublicInputs(); + + // Set up mocks + prover.getBaseRollupProof.mockResolvedValue(emptyProof); + prover.getRootRollupProof.mockResolvedValue(emptyProof); + simulator.baseRollupCircuit + .mockResolvedValueOnce(baseRollupOutputLeft) + .mockResolvedValueOnce(baseRollupOutputRight); + simulator.rootRollupCircuit.mockResolvedValue(rootRollupOutput); + }); + + const updateRootTrees = async () => { + for (const [newTree, rootTree] of [ + [MerkleTreeId.DATA_TREE, MerkleTreeId.DATA_TREE_ROOTS_TREE], + [MerkleTreeId.CONTRACT_TREE, MerkleTreeId.CONTRACT_TREE_ROOTS_TREE], + ] as const) { + if (rootTree === MerkleTreeId.CONTRACT_TREE_ROOTS_TREE) { + await inspectTree(expectsDb, rootTree); + } + const newTreeInfo = await expectsDb.getTreeInfo(newTree); + await expectsDb.appendLeaves(rootTree, [newTreeInfo.root]); + } + }; + + // Updates the expectedDb trees based on the new commitments, contracts, and nullifiers from these txs + const updateExpectedTreesFromTxs = async (txs: Tx[]) => { + for (const [tree, leaves] of [ + [MerkleTreeId.DATA_TREE, flatMap(txs, tx => tx.data.end.newCommitments.map(l => l.toBuffer()))], + [MerkleTreeId.CONTRACT_TREE, flatMap(txs, tx => tx.data.end.newContracts.map(n => hashNewContractData(wasm, n)))], + [MerkleTreeId.NULLIFIER_TREE, flatMap(txs, tx => tx.data.end.newNullifiers.map(l => l.toBuffer()))], + ] as const) { + await expectsDb.appendLeaves(tree, leaves); + } + }; + + const getTreeSnapshot = async (tree: MerkleTreeId) => { + const treeInfo = await expectsDb.getTreeInfo(tree); + return new AppendOnlyTreeSnapshot(Fr.fromBuffer(treeInfo.root), Number(treeInfo.size)); + }; + + it('builds an L2 block', async () => { + // Assemble a fake transaction, we'll tweak some fields below + const tx = new Tx(makePrivateKernelPublicInputs(), emptyProof); + const txsLeft = [tx, makeEmptyTx()]; + const txsRight = [makeEmptyTx(), makeEmptyTx()]; + const txs = [...txsLeft, ...txsRight]; + + // Set tree roots to proper values in the tx + for (const [name, id] of [ + ['privateDataTreeRoot', MerkleTreeId.DATA_TREE], + ['contractTreeRoot', MerkleTreeId.CONTRACT_TREE], + ['nullifierTreeRoot', MerkleTreeId.NULLIFIER_TREE], + ] as const) { + tx.data.constants.oldTreeRoots[name] = Fr.fromBuffer((await builderDb.getTreeInfo(id)).root); + } + + // Calculate what would be the tree roots after the txs from the first base rollup land and update mock circuit output + await updateExpectedTreesFromTxs(txsLeft); + baseRollupOutputLeft.endContractTreeSnapshot = await getTreeSnapshot(MerkleTreeId.CONTRACT_TREE); + baseRollupOutputLeft.endNullifierTreeSnapshot = await getTreeSnapshot(MerkleTreeId.NULLIFIER_TREE); + baseRollupOutputLeft.endPrivateDataTreeSnapshot = await getTreeSnapshot(MerkleTreeId.DATA_TREE); + + await inspectTree(expectsDb, MerkleTreeId.CONTRACT_TREE); + + // Same for the two txs on the right + await updateExpectedTreesFromTxs(txsRight); + baseRollupOutputRight.endContractTreeSnapshot = await getTreeSnapshot(MerkleTreeId.CONTRACT_TREE); + baseRollupOutputRight.endNullifierTreeSnapshot = await getTreeSnapshot(MerkleTreeId.NULLIFIER_TREE); + baseRollupOutputRight.endPrivateDataTreeSnapshot = await getTreeSnapshot(MerkleTreeId.DATA_TREE); + + // And update the root trees now to create proper output to the root rollup circuit + await updateRootTrees(); + rootRollupOutput.endContractTreeSnapshot = await getTreeSnapshot(MerkleTreeId.CONTRACT_TREE); + rootRollupOutput.endNullifierTreeSnapshot = await getTreeSnapshot(MerkleTreeId.NULLIFIER_TREE); + rootRollupOutput.endPrivateDataTreeSnapshot = await getTreeSnapshot(MerkleTreeId.DATA_TREE); + rootRollupOutput.endTreeOfHistoricContractTreeRootsSnapshot = await getTreeSnapshot( + MerkleTreeId.CONTRACT_TREE_ROOTS_TREE, + ); + rootRollupOutput.endTreeOfHistoricPrivateDataTreeRootsSnapshot = await getTreeSnapshot( + MerkleTreeId.DATA_TREE_ROOTS_TREE, + ); + + // Actually build a block! + const [l2block, proof] = await builder.buildL2Block(tx); + + expect(l2block.number).toEqual(blockNumber); + expect(proof).toEqual(emptyProof); + }); +}); + +// Test subject class that exposes internal functions for testing +class TestSubject extends CircuitPoweredBlockBuilder { + public buildBaseRollupInput(tx1: Tx, tx2: Tx): Promise { + return super.buildBaseRollupInput(tx1, tx2); + } + + public updateRootTrees(): Promise { + return super.updateRootTrees(); + } +} diff --git a/yarn-project/sequencer-client/src/sequencer/circuit_powered_block_builder.ts b/yarn-project/sequencer-client/src/sequencer/circuit_powered_block_builder.ts index d9e93f297d4a..170a5851991c 100644 --- a/yarn-project/sequencer-client/src/sequencer/circuit_powered_block_builder.ts +++ b/yarn-project/sequencer-client/src/sequencer/circuit_powered_block_builder.ts @@ -1,4 +1,6 @@ +import { createDebugLogger } from '@aztec/foundation'; import { ContractData, L2Block } from '@aztec/archiver'; +import { BarretenbergWasm } from '@aztec/barretenberg.js/wasm'; import { AppendOnlyTreeSnapshot, BaseRollupInputs, @@ -14,17 +16,20 @@ import { PRIVATE_DATA_TREE_ROOTS_TREE_HEIGHT, RootRollupInputs, RootRollupPublicInputs, + UInt8Vector, VK_TREE_HEIGHT, } from '@aztec/circuits.js'; +import { toBigIntBE } from '@aztec/foundation'; import { Tx } from '@aztec/tx'; -import { MerkleTreeId, MerkleTreeOperations } from '@aztec/world-state'; -import { makeEmptyTx } from '../deps/tx.js'; +import { MerkleTreeDb, MerkleTreeId } from '@aztec/world-state'; +import flatMap from 'lodash.flatmap'; +import times from 'lodash.times'; +import { hashNewContractData, makeEmptyTx } from '../deps/tx.js'; import { Proof, Prover } from '../prover/index.js'; import { Simulator } from '../simulator/index.js'; import { VerificationKeys } from './vks.js'; -// REFACTOR: Move this somewhere generic, and do something less horrible without going through hex strings. -const frToBigInt = (fr: Fr) => fr.value; +const frToBigInt = (fr: Fr) => toBigIntBE(fr.toBuffer()); const bigintToFr = (num: bigint) => new Fr(num); const bigintToNum = (num: bigint) => Number(num); @@ -35,18 +40,19 @@ const FUTURE_NUM = 0; // Denotes fields that should be deleted const DELETE_FR = new Fr(0n); const DELETE_ANY: any = {}; -const TODO_ANY: any = {}; export class CircuitPoweredBlockBuilder { constructor( - private db: MerkleTreeOperations, - private nextRollupId: number, - private vks: VerificationKeys, - private simulator: Simulator, - private prover: Prover, + protected db: MerkleTreeDb, + protected nextRollupId: number, + protected vks: VerificationKeys, + protected simulator: Simulator, + protected prover: Prover, + protected wasm: BarretenbergWasm, + protected debug = createDebugLogger('aztec:sequencer'), ) {} - public async buildL2Block(tx: Tx) { + public async buildL2Block(tx: Tx): Promise<[L2Block, UInt8Vector]> { const [ startPrivateDataTreeSnapshot, startNullifierTreeSnapshot, @@ -63,7 +69,9 @@ export class CircuitPoweredBlockBuilder { ].map(tree => this.getTreeSnapshot(tree)), ); - const [circuitsOutput] = await this.runCircuits(tx); + // We fill the tx batch with empty txs, we process only one tx at a time for now + const txs = [tx, makeEmptyTx(), makeEmptyTx(), makeEmptyTx()]; + const [circuitsOutput, proof] = await this.runCircuits(txs); const { endPrivateDataTreeSnapshot, @@ -73,6 +81,16 @@ export class CircuitPoweredBlockBuilder { endTreeOfHistoricContractTreeRootsSnapshot, } = circuitsOutput; + // Collect all new nullifiers, commitments, and contracts from all txs in this block + const newNullifiers = flatMap(txs, tx => tx.data.end.newNullifiers); + const newCommitments = flatMap(txs, tx => tx.data.end.newCommitments); + const newContracts = flatMap(txs, tx => tx.data.end.newContracts).map(cd => + Fr.fromBuffer(hashNewContractData(this.wasm, cd)), + ); + const newContractData = flatMap(txs, tx => tx.data.end.newContracts).map( + n => new ContractData(n.contractAddress, n.portalContractAddress), + ); + const l2block = L2Block.fromFields({ number: this.nextRollupId, startPrivateDataTreeSnapshot, @@ -85,46 +103,139 @@ export class CircuitPoweredBlockBuilder { endTreeOfHistoricPrivateDataTreeRootsSnapshot, startTreeOfHistoricContractTreeRootsSnapshot, endTreeOfHistoricContractTreeRootsSnapshot, - newCommitments: tx.data.end.newCommitments, - newNullifiers: tx.data.end.newNullifiers, - newContracts: tx.data.end.newContracts.map(x => x.functionTreeRoot), - newContractData: tx.data.end.newContracts.map(n => new ContractData(n.contractAddress, n.portalContractAddress)), + newCommitments, + newNullifiers, + newContracts, + newContractData, }); - return l2block; + + return [l2block, proof]; } - private async getTreeSnapshot(id: MerkleTreeId): Promise { + protected async getTreeSnapshot(id: MerkleTreeId): Promise { const treeInfo = await this.db.getTreeInfo(id); return new AppendOnlyTreeSnapshot(Fr.fromBuffer(treeInfo.root), Number(treeInfo.size)); } - private async runCircuits(tx: Tx): Promise<[RootRollupPublicInputs, Proof]> { - const emptyTx = makeEmptyTx(); + protected async runCircuits(txs: Tx[]): Promise<[RootRollupPublicInputs, Proof]> { + const [tx1, tx2, tx3, tx4] = txs; - const [, baseRollupOutputLeft, baseRollupProofLeft] = await this.baseRollupCircuit(tx, emptyTx); + // Simulate both base rollup circuits, updating the data, contract, and nullifier trees in the process + this.debug(`Running left base rollup simulator`); + const [baseRollupInputLeft, baseRollupOutputLeft] = await this.baseRollupCircuit(tx1, tx2); + this.debug(`Running right base rollup simulator`); + const [baseRollupInputRight, baseRollupOutputRight] = await this.baseRollupCircuit(tx3, tx4); - const [, baseRollupOutputRight, baseRollupProofRight] = await this.baseRollupCircuit(emptyTx, emptyTx); + // Get the proofs for them in parallel (faked for now) + this.debug(`Running base rollup circuit provers`); + const [baseRollupProofLeft, baseRollupProofRight] = await Promise.all([ + this.prover.getBaseRollupProof(baseRollupInputLeft, baseRollupOutputLeft), + this.prover.getBaseRollupProof(baseRollupInputRight, baseRollupOutputRight), + ]); + // Get the input for the root rollup circuit based on the base rollup ones + this.debug(`Producing root rollup inputs`); const rootInput = await this.getRootRollupInput( baseRollupOutputLeft, baseRollupProofLeft, baseRollupOutputRight, baseRollupProofRight, ); + + // Simulate and get proof for the root circuit + this.debug(`Running root rollup simulator`); const rootOutput = await this.simulator.rootRollupCircuit(rootInput); + this.debug(`Running root rollup circuit prover`); const rootProof = await this.prover.getRootRollupProof(rootInput, rootOutput); + // Update the root trees with the latest data and contract tree roots, + // and validate them against the output of the root circuit simulation + this.debug(`Updating and validating root trees`); + await this.updateRootTrees(); + await this.validateRootOutput(rootOutput); + return [rootOutput, rootProof]; } - private async baseRollupCircuit(tx1: Tx, tx2: Tx) { - const rollupInput = await this.getBaseRollupInput(tx1, tx2); + protected async baseRollupCircuit(tx1: Tx, tx2: Tx) { + const rollupInput = await this.buildBaseRollupInput(tx1, tx2); const rollupOutput = await this.simulator.baseRollupCircuit(rollupInput); - const rollupProof = await this.prover.getBaseRollupProof(rollupInput, rollupOutput); - return [rollupInput, rollupOutput, rollupProof] as const; + await this.validateTrees(rollupOutput); + return [rollupInput, rollupOutput] as const; + } + + // Updates our roots trees with the new generated trees after the rollup updates + protected async updateRootTrees() { + for (const [newTree, rootTree] of [ + [MerkleTreeId.DATA_TREE, MerkleTreeId.DATA_TREE_ROOTS_TREE], + [MerkleTreeId.CONTRACT_TREE, MerkleTreeId.CONTRACT_TREE_ROOTS_TREE], + ] as const) { + const newTreeInfo = await this.db.getTreeInfo(newTree); + await this.db.appendLeaves(rootTree, [newTreeInfo.root]); + } } - private async getRootRollupInput( + // Validate that the new roots we calculated from manual insertions match the outputs of the simulation + protected async validateTrees(rollupOutput: BaseRollupPublicInputs | RootRollupPublicInputs) { + await Promise.all([ + this.validateTree(rollupOutput, MerkleTreeId.CONTRACT_TREE, 'Contract'), + this.validateTree(rollupOutput, MerkleTreeId.DATA_TREE, 'PrivateData'), + this.validateTree(rollupOutput, MerkleTreeId.NULLIFIER_TREE, 'Nullifier'), + ]); + } + + // Validate that the roots of all local trees match the output of the root circuit simulation + protected async validateRootOutput(rootOutput: RootRollupPublicInputs) { + await Promise.all([ + this.validateTrees(rootOutput), + this.validateRootTree(rootOutput, MerkleTreeId.CONTRACT_TREE_ROOTS_TREE, 'Contract'), + this.validateRootTree(rootOutput, MerkleTreeId.DATA_TREE_ROOTS_TREE, 'PrivateData'), + ]); + } + + // Helper for validating a roots tree against a circuit simulation output + protected async validateRootTree( + rootOutput: RootRollupPublicInputs, + treeId: MerkleTreeId, + name: 'Contract' | 'PrivateData', + ) { + const localTree = await this.getTreeSnapshot(treeId); + const simulatedTree = rootOutput[`endTreeOfHistoric${name}TreeRootsSnapshot`]; + this.validateSimulatedTree(localTree, simulatedTree, name, `Roots ${name}`); + } + + // Helper for validating a non-roots tree against a circuit simulation output + protected async validateTree( + output: BaseRollupPublicInputs | RootRollupPublicInputs, + treeId: MerkleTreeId, + name: 'PrivateData' | 'Contract' | 'Nullifier', + ) { + const localTree = await this.getTreeSnapshot(treeId); + const simulatedTree = output[`end${name}TreeSnapshot`]; + this.validateSimulatedTree(localTree, simulatedTree, name); + } + + // Helper for comparing two trees snapshots + protected validateSimulatedTree( + localTree: AppendOnlyTreeSnapshot, + simulatedTree: AppendOnlyTreeSnapshot, + name: string, + label?: string, + ) { + if (!simulatedTree.root.toBuffer().equals(localTree.root.toBuffer())) { + throw new Error(`${label ?? name} tree root mismatch (local ${localTree.root}, simulated ${simulatedTree.root})`); + } + if (simulatedTree.nextAvailableLeafIndex !== localTree.nextAvailableLeafIndex) { + throw new Error( + `${label ?? name} tree next available leaf index mismatch (local ${ + localTree.nextAvailableLeafIndex + }, simulated ${simulatedTree.nextAvailableLeafIndex})`, + ); + } + } + + // Builds the inputs for the root rollup circuit, without making any changes to trees + protected async getRootRollupInput( rollupOutputLeft: BaseRollupPublicInputs, rollupProofLeft: Proof, rollupOutputRight: BaseRollupPublicInputs, @@ -135,17 +246,27 @@ export class CircuitPoweredBlockBuilder { this.getPreviousRollupDataFromBaseRollup(rollupOutputRight, rollupProofRight), ]; - return new RootRollupInputs( - previousRollupData, - await this.getTreeSnapshot(MerkleTreeId.DATA_TREE), - await this.getTreeSnapshot(MerkleTreeId.CONTRACT_TREE), - TODO_ANY, - TODO_ANY, - TODO_ANY, + const getRootTreeSiblingPath = async (treeId: MerkleTreeId) => { + // TODO: Synchronize these operations into the tree db to avoid race conditions + const { size } = await this.db.getTreeInfo(treeId); + // TODO: Check for off-by-one errors + const path = await this.db.getSiblingPath(treeId, size); + return path.data.map(b => Fr.fromBuffer(b)); + }; + + const newHistoricContractDataTreeRootSiblingPath = await getRootTreeSiblingPath( + MerkleTreeId.CONTRACT_TREE_ROOTS_TREE, ); + const newHistoricPrivateDataTreeRootSiblingPath = await getRootTreeSiblingPath(MerkleTreeId.DATA_TREE_ROOTS_TREE); + + return RootRollupInputs.from({ + previousRollupData, + newHistoricContractDataTreeRootSiblingPath, + newHistoricPrivateDataTreeRootSiblingPath, + }); } - private getPreviousRollupDataFromBaseRollup(rollupOutput: BaseRollupPublicInputs, rollupProof: Proof) { + protected getPreviousRollupDataFromBaseRollup(rollupOutput: BaseRollupPublicInputs, rollupProof: Proof) { return new PreviousRollupData( rollupOutput, rollupProof, @@ -157,7 +278,7 @@ export class CircuitPoweredBlockBuilder { ); } - private getKernelDataFor(tx: Tx) { + protected getKernelDataFor(tx: Tx) { return new PreviousKernelData( tx.data, tx.proof, @@ -172,13 +293,18 @@ export class CircuitPoweredBlockBuilder { } // Scan a tree searching for a specific value and return a membership witness proof for it - private async getMembershipWitnessFor( + protected async getMembershipWitnessFor( value: Fr, treeId: MerkleTreeId, height: N, ): Promise> { + // If this is an empty tx, then just return zeroes + if (value.value === 0n) return this.makeEmptyMembershipWitness(height); + const index = await this.db.findLeafIndex(treeId, value.toBuffer()); - if (!index) throw new Error(`Leaf with value ${value} not found in tree ${treeId}`); + if (index === undefined) { + throw new Error(`Leaf with value ${value} not found in tree ${treeId}`); + } const path = await this.db.getSiblingPath(treeId, index); // TODO: Check conversion from bigint to number return new MembershipWitness( @@ -188,7 +314,23 @@ export class CircuitPoweredBlockBuilder { ); } - private async getConstantBaseRollupData(): Promise { + protected getContractMembershipWitnessFor(tx: Tx) { + return this.getMembershipWitnessFor( + tx.data.constants.oldTreeRoots.contractTreeRoot, + MerkleTreeId.CONTRACT_TREE_ROOTS_TREE, + CONTRACT_TREE_ROOTS_TREE_HEIGHT, + ); + } + + protected getDataMembershipWitnessFor(tx: Tx) { + return this.getMembershipWitnessFor( + tx.data.constants.oldTreeRoots.privateDataTreeRoot, + MerkleTreeId.DATA_TREE_ROOTS_TREE, + PRIVATE_DATA_TREE_ROOTS_TREE_HEIGHT, + ); + } + + protected async getConstantBaseRollupData(): Promise { return ConstantBaseRollupData.from({ baseRollupVkHash: DELETE_FR, mergeRollupVkHash: DELETE_FR, @@ -200,12 +342,22 @@ export class CircuitPoweredBlockBuilder { }); } - private async getLowNullifierInfo(nullifier: Fr) { + protected async getLowNullifierInfo(nullifier: Fr) { + // Return empty nullifier info for an empty tx + if (nullifier.value === 0n) { + return { + index: 0, + leafPreimage: new NullifierLeafPreimage(new Fr(0n), new Fr(0n), 0), + witness: this.makeEmptyMembershipWitness(NULLIFIER_TREE_HEIGHT), + }; + } + const tree = MerkleTreeId.NULLIFIER_TREE; const prevValueIndex = await this.db.getPreviousValueIndex(tree, frToBigInt(nullifier)); const prevValueInfo = this.db.getLeafData(tree, prevValueIndex.index); if (!prevValueInfo) throw new Error(`Nullifier tree should have one initial leaf`); const prevValueSiblingPath = await this.db.getSiblingPath(tree, BigInt(prevValueIndex.index)); + return { index: prevValueIndex, leafPreimage: new NullifierLeafPreimage( @@ -221,41 +373,81 @@ export class CircuitPoweredBlockBuilder { }; } - private async getBaseRollupInput(tx1: Tx, tx2: Tx) { - // Concatenate the new nullifiers of each tx being rolled up - // and get the previous node for each of them, along with the sibling path - const lowNullifierInfos = await Promise.all( - [...tx1.data.end.newNullifiers, ...tx2.data.end.newNullifiers].map(fr => this.getLowNullifierInfo(fr)), + protected async getSubtreeSiblingPath(treeId: MerkleTreeId, subtreeHeight: number): Promise { + // Get sibling path to the last leaf we inserted + const lastLeafIndex = (await this.db.getTreeInfo(treeId).then(t => t.size)) - 1n; + const fullSiblingPath = await this.db.getSiblingPath(treeId, lastLeafIndex); + + // Drop the first subtreeHeight items since we only care about the path to the subtree root + return fullSiblingPath.data.slice(subtreeHeight).map(b => Fr.fromBuffer(b)); + } + + // 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 + const constants = await this.getConstantBaseRollupData(); + const startNullifierTreeSnapshot = await this.getTreeSnapshot(MerkleTreeId.NULLIFIER_TREE); + const startContractTreeSnapshot = await this.getTreeSnapshot(MerkleTreeId.CONTRACT_TREE); + const startPrivateDateTreeSnapshot = await this.getTreeSnapshot(MerkleTreeId.DATA_TREE); + + // Update the contract and data trees with the new items being inserted to get the new roots + // that will be used by the next iteration of the base rollup circuit + const newContracts = flatMap([tx1, tx2], tx => + tx.data.end.newContracts.map(cd => hashNewContractData(this.wasm, cd)), ); + const newCommitments = flatMap([tx1, tx2], tx => tx.data.end.newCommitments.map(x => x.toBuffer())); + await this.db.appendLeaves(MerkleTreeId.CONTRACT_TREE, newContracts); + await this.db.appendLeaves(MerkleTreeId.DATA_TREE, newCommitments); - const getContractMembershipWitnessFor = (tx: Tx) => - this.getMembershipWitnessFor( - tx.data.constants.oldTreeRoots.contractTreeRoot, - MerkleTreeId.CONTRACT_TREE_ROOTS_TREE, - CONTRACT_TREE_ROOTS_TREE_HEIGHT, - ); + // 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 = []; + for (const nullifier of newNullifiers) { + lowNullifierInfos.push(await this.getLowNullifierInfo(nullifier)); + await this.db.appendLeaves(MerkleTreeId.NULLIFIER_TREE, [nullifier.toBuffer()]); + } - const getDataMembershipWitnessFor = (tx: Tx) => - this.getMembershipWitnessFor( - tx.data.constants.oldTreeRoots.privateDataTreeRoot, - MerkleTreeId.DATA_TREE_ROOTS_TREE, - PRIVATE_DATA_TREE_ROOTS_TREE_HEIGHT, - ); + // Get the subtree sibling paths for the circuit + const newCommitmentsSubtreeSiblingPath = await this.getSubtreeSiblingPath( + MerkleTreeId.DATA_TREE, + BaseRollupInputs.PRIVATE_DATA_SUBTREE_HEIGHT, + ); + const newContractsSubtreeSiblingPath = await this.getSubtreeSiblingPath( + MerkleTreeId.CONTRACT_TREE, + BaseRollupInputs.CONTRACT_SUBTREE_HEIGHT, + ); + const newNullifiersSubtreeSiblingPath = await this.getSubtreeSiblingPath( + MerkleTreeId.NULLIFIER_TREE, + BaseRollupInputs.NULLIFIER_SUBTREE_HEIGHT, + ); return BaseRollupInputs.from({ - constants: await this.getConstantBaseRollupData(), - startNullifierTreeSnapshot: await this.getTreeSnapshot(MerkleTreeId.NULLIFIER_TREE), + constants, + startNullifierTreeSnapshot, + startContractTreeSnapshot, + startPrivateDateTreeSnapshot, + newCommitmentsSubtreeSiblingPath, + newContractsSubtreeSiblingPath, + newNullifiersSubtreeSiblingPath, lowNullifierLeafPreimages: lowNullifierInfos.map(i => i.leafPreimage), lowNullifierMembershipWitness: lowNullifierInfos.map(i => i.witness), kernelData: [this.getKernelDataFor(tx1), this.getKernelDataFor(tx2)], historicContractsTreeRootMembershipWitnesses: [ - await getContractMembershipWitnessFor(tx1), - await getContractMembershipWitnessFor(tx2), + await this.getContractMembershipWitnessFor(tx1), + await this.getContractMembershipWitnessFor(tx2), ], historicPrivateDataTreeRootMembershipWitnesses: [ - await getDataMembershipWitnessFor(tx1), - await getDataMembershipWitnessFor(tx2), + await this.getDataMembershipWitnessFor(tx1), + await this.getDataMembershipWitnessFor(tx2), ], } as BaseRollupInputs); } + + protected makeEmptyMembershipWitness(height: N) { + return new MembershipWitness( + height, + 0, + times(height, () => new Fr(0n)), + ); + } } diff --git a/yarn-project/tx/src/tx.ts b/yarn-project/tx/src/tx.ts index 26492fa06349..003ead84e845 100644 --- a/yarn-project/tx/src/tx.ts +++ b/yarn-project/tx/src/tx.ts @@ -22,11 +22,13 @@ export class Tx { * @param data - Tx inputs. * @param proof - Tx proof. * @param unverifiedData - Information not needed to verify the tx (e.g. encrypted note pre-images etc.) + * @param isEmpty - Whether this is a placeholder empty tx. */ constructor( public readonly data: PrivateKernelPublicInputs, public readonly proof: UInt8Vector, public readonly unverifiedData: Buffer, + public readonly isEmpty = false, ) {} /** diff --git a/yarn-project/world-state/src/synchroniser/server_world_state_synchroniser.ts b/yarn-project/world-state/src/synchroniser/server_world_state_synchroniser.ts index b9a1852e53ae..1067f21c207b 100644 --- a/yarn-project/world-state/src/synchroniser/server_world_state_synchroniser.ts +++ b/yarn-project/world-state/src/synchroniser/server_world_state_synchroniser.ts @@ -27,6 +27,10 @@ export class ServerWorldStateSynchroniser implements WorldStateSynchroniser { this.l2BlockDownloader = new L2BlockDownloader(l2BlockSource, 1000, 100); } + public getLeafValue(treeId: MerkleTreeId, index: bigint): Promise { + return this.merkleTreeDb.getLeafValue(treeId, index); + } + public getPreviousValueIndex( treeId: MerkleTreeId.NULLIFIER_TREE, value: bigint, 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 0236445049e6..481a48450133 100644 --- a/yarn-project/world-state/src/world-state-db/index.ts +++ b/yarn-project/world-state/src/world-state-db/index.ts @@ -47,6 +47,7 @@ export interface MerkleTreeOperations { ): Promise<{ index: number; alreadyPresent: boolean }>; getLeafData(treeId: IndexedMerkleTreeId, index: number): LeafData | undefined; findLeafIndex(treeId: MerkleTreeId, value: Buffer): Promise; + getLeafValue(treeId: MerkleTreeId, index: bigint): Promise; } /** @@ -56,3 +57,17 @@ export interface MerkleTreeDb extends MerkleTreeOperations { commit(): Promise; rollback(): Promise; } + +/** + * Outputs a tree leaves to console.log for debugging purposes. + */ +export async function inspectTree(db: MerkleTreeOperations, treeId: MerkleTreeId) { + const info = await db.getTreeInfo(treeId); + let output = [`Tree id=${treeId} size=${info.size} root=0x${info.root.toString('hex')}`]; + for (let i = 0; i < info.size; i++) { + output.push( + ` Leaf ${i}: ${await db.getLeafValue(treeId, BigInt(i)).then(x => x?.toString('hex') ?? '[undefined]')}`, + ); + } + console.log(output.join('\n')); +} diff --git a/yarn-project/world-state/src/world-state-db/memory_world_state_db.ts b/yarn-project/world-state/src/world-state-db/memory_world_state_db.ts index 048cfda5f33a..8d54290739cd 100644 --- a/yarn-project/world-state/src/world-state-db/memory_world_state_db.ts +++ b/yarn-project/world-state/src/world-state-db/memory_world_state_db.ts @@ -87,6 +87,10 @@ export class MerkleTrees implements MerkleTreeDb { return await this.synchronise(() => this._getTreeInfo(treeId)); } + public async getLeafValue(treeId: MerkleTreeId, index: bigint): Promise { + return await this.synchronise(() => this.trees[treeId].getLeafValue(index)); + } + /** * Gets the sibling path for a leaf in a tree. * @param treeId - The ID of the tree. diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 8ef7b12639bb..b608472236a9 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -493,17 +493,20 @@ __metadata: "@aztec/foundation": "workspace:^" "@aztec/l1-contracts": "workspace:^" "@aztec/l2-block": "workspace:^" + "@aztec/merkle-tree": "workspace:^" "@aztec/p2p": "workspace:^" "@aztec/tx": "workspace:^" "@aztec/world-state": "workspace:^" "@jest/globals": ^29.4.3 "@rushstack/eslint-patch": ^1.1.4 "@types/jest": ^29.4.0 + "@types/lodash.flatmap": ^4.5.7 "@types/lodash.times": ^4.3.7 "@types/node": ^18.7.23 concurrently: ^7.6.0 jest: ^28.1.3 jest-mock-extended: ^3.0.3 + lodash.flatmap: ^4.5.0 lodash.times: ^4.3.2 prettier: ^2.8.7 ts-jest: ^28.0.7 @@ -2279,6 +2282,15 @@ __metadata: languageName: node linkType: hard +"@types/lodash.flatmap@npm:^4.5.7": + version: 4.5.7 + resolution: "@types/lodash.flatmap@npm:4.5.7" + dependencies: + "@types/lodash": "*" + checksum: 2bcccdd30bcb06a400b37a2b731f194f6b11ae49467a98828d86e25dbd10055f8b033c7741755a41de7bc50f49e387ecec952f9871863f77c79582e891945486 + languageName: node + linkType: hard + "@types/lodash.times@npm:^4.3.7": version: 4.3.7 resolution: "@types/lodash.times@npm:4.3.7" @@ -6141,6 +6153,13 @@ __metadata: languageName: node linkType: hard +"lodash.flatmap@npm:^4.5.0": + version: 4.5.0 + resolution: "lodash.flatmap@npm:4.5.0" + checksum: c01a47d32e99f8fce75409f0a4a9bd12fbb2d3a46519a0dde14deedb1e527b5ddccc2bf997705c67bdecb915f47749e8a9ffefa7a91c41f0c448e06348ec81c7 + languageName: node + linkType: hard + "lodash.memoize@npm:4.x": version: 4.1.2 resolution: "lodash.memoize@npm:4.1.2"