Skip to content

Commit

Permalink
feat: Light block builder
Browse files Browse the repository at this point in the history
Adds a block builder that assembles an L2Block out of a set of processed
txs without relying on base, merge, block root, or parity circuits, and
works using only ts code. Keeps the same interface as the current block
builder for drop-in replacement within the orchestrator.
  • Loading branch information
spalladino committed Sep 19, 2024
1 parent 87e0a17 commit 54bde06
Show file tree
Hide file tree
Showing 8 changed files with 297 additions and 119 deletions.
47 changes: 3 additions & 44 deletions yarn-project/circuit-types/src/body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import {
TxEffect,
UnencryptedL2BlockL2Logs,
} from '@aztec/circuit-types';
import { padArrayEnd } from '@aztec/foundation/collection';
import { sha256Trunc } from '@aztec/foundation/crypto';
import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize';
import { computeVariableMerkleRoot } from '@aztec/foundation/trees';

import { inspect } from 'util';

Expand Down Expand Up @@ -52,49 +51,9 @@ export class Body {
* @returns The txs effects hash.
*/
getTxsEffectsHash() {
// Adapted from proving-state.ts -> findMergeLevel and unbalanced_tree.ts
// Calculates the tree upwards layer by layer until we reach the root
// The L1 calculation instead computes the tree from right to left (slightly cheaper gas)
// TODO: A more thorough investigation of which method is cheaper, then use that method everywhere
const computeRoot = (leaves: Buffer[]): Buffer => {
const depth = Math.ceil(Math.log2(leaves.length));
let [layerWidth, nodeToShift] =
leaves.length & 1 ? [leaves.length - 1, leaves[leaves.length - 1]] : [leaves.length, Buffer.alloc(0)];
// Allocate this layer's leaves and init the next layer up
let thisLayer = leaves.slice(0, layerWidth);
let nextLayer = [];
for (let i = 0; i < depth; i++) {
for (let j = 0; j < layerWidth; j += 2) {
// Store the hash of each pair one layer up
nextLayer[j / 2] = sha256Trunc(Buffer.concat([thisLayer[j], thisLayer[j + 1]]));
}
layerWidth /= 2;
if (layerWidth & 1) {
if (nodeToShift.length) {
// If the next layer has odd length, and we have a node that needs to be shifted up, add it here
nextLayer.push(nodeToShift);
layerWidth += 1;
nodeToShift = Buffer.alloc(0);
} else {
// If we don't have a node waiting to be shifted, store the next layer's final node to be shifted
layerWidth -= 1;
nodeToShift = nextLayer[layerWidth];
}
}
// reset the layers
thisLayer = nextLayer;
nextLayer = [];
}
// return the root
return thisLayer[0];
};

const emptyTxEffectHash = TxEffect.empty().hash();
let leaves: Buffer[] = this.txEffects.map(txEffect => txEffect.hash());
if (leaves.length < 2) {
leaves = padArrayEnd(leaves, emptyTxEffectHash, 2);
}
return computeRoot(leaves);
const leaves: Buffer[] = this.txEffects.map(txEffect => txEffect.hash());
return computeVariableMerkleRoot(leaves, emptyTxEffectHash);
}

get noteEncryptedLogs(): EncryptedNoteL2BlockL2Logs {
Expand Down
53 changes: 29 additions & 24 deletions yarn-project/circuit-types/src/tx_effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,33 +146,10 @@ export class TxEffect {
*/
hash() {
const padBuffer = (buf: Buffer, length: number) => Buffer.concat([buf, Buffer.alloc(length - buf.length)]);
// Below follows computeTxOutHash in TxsDecoder.sol and new_sha in variable_merkle_tree.nr
// TODO(#7218): Revert to fixed height tree for outbox
const computeTxOutHash = (l2ToL1Msgs: Fr[]) => {
if (l2ToL1Msgs.length == 0) {
return Buffer.alloc(32);
}
const depth = l2ToL1Msgs.length == 1 ? 1 : Math.ceil(Math.log2(l2ToL1Msgs.length));
let thisLayer = padArrayEnd(
l2ToL1Msgs.map(msg => msg.toBuffer()),
Buffer.alloc(32),
2 ** depth,
);
let nextLayer = [];
for (let i = 0; i < depth; i++) {
for (let j = 0; j < thisLayer.length; j += 2) {
// Store the hash of each pair one layer up
nextLayer[j / 2] = sha256Trunc(Buffer.concat([thisLayer[j], thisLayer[j + 1]]));
}
thisLayer = nextLayer;
nextLayer = [];
}
return thisLayer[0];
};

const noteHashesBuffer = padBuffer(serializeToBuffer(this.noteHashes), Fr.SIZE_IN_BYTES * MAX_NOTE_HASHES_PER_TX);
const nullifiersBuffer = padBuffer(serializeToBuffer(this.nullifiers), Fr.SIZE_IN_BYTES * MAX_NULLIFIERS_PER_TX);
const outHashBuffer = computeTxOutHash(this.l2ToL1Msgs);
const outHashBuffer = this.txOutHash();
const publicDataWritesBuffer = padBuffer(
serializeToBuffer(this.publicDataWrites),
PublicDataWrite.SIZE_IN_BYTES * MAX_TOTAL_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX,
Expand Down Expand Up @@ -200,6 +177,34 @@ export class TxEffect {
return sha256Trunc(inputValue);
}

/**
* Computes txOutHash of this tx effect.
* TODO(#7218): Revert to fixed height tree for outbox
* @dev Follows computeTxOutHash in TxsDecoder.sol and new_sha in variable_merkle_tree.nr
*/
txOutHash() {
const { l2ToL1Msgs } = this;
if (l2ToL1Msgs.length == 0) {
return Buffer.alloc(32);
}
const depth = l2ToL1Msgs.length == 1 ? 1 : Math.ceil(Math.log2(l2ToL1Msgs.length));
let thisLayer = padArrayEnd(
l2ToL1Msgs.map(msg => msg.toBuffer()),
Buffer.alloc(32),
2 ** depth,
);
let nextLayer = [];
for (let i = 0; i < depth; i++) {
for (let j = 0; j < thisLayer.length; j += 2) {
// Store the hash of each pair one layer up
nextLayer[j / 2] = sha256Trunc(Buffer.concat([thisLayer[j], thisLayer[j + 1]]));
}
thisLayer = nextLayer;
nextLayer = [];
}
return thisLayer[0];
}

static random(
numPrivateCallsPerTx = 2,
numPublicCallsPerTx = 3,
Expand Down
2 changes: 2 additions & 0 deletions yarn-project/foundation/src/trees/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './variable_merkle_root.js';

/**
* A leaf of an indexed merkle tree.
*/
Expand Down
50 changes: 50 additions & 0 deletions yarn-project/foundation/src/trees/variable_merkle_root.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { padArrayEnd } from '@aztec/foundation/collection';
import { sha256Trunc } from '@aztec/foundation/crypto';

/**
* Adapted from proving-state.ts -> findMergeLevel and unbalanced_tree.ts
* Calculates the tree upwards layer by layer until we reach the root
* The L1 calculation instead computes the tree from right to left (slightly cheaper gas)
* TODO: A more thorough investigation of which method is cheaper, then use that method everywhere
*/
export function computeVariableMerkleRoot(leaves: Buffer[], emptyLeaf?: Buffer, hasher = sha256Trunc): Buffer {
// pad leaves to 2
if (leaves.length < 2) {
if (emptyLeaf === undefined) {
throw new Error('Cannot compute a Merkle root with less than 2 leaves');
} else {
leaves = padArrayEnd(leaves, emptyLeaf, 2);
}
}

const depth = Math.ceil(Math.log2(leaves.length));
let [layerWidth, nodeToShift] =
leaves.length & 1 ? [leaves.length - 1, leaves[leaves.length - 1]] : [leaves.length, Buffer.alloc(0)];
// Allocate this layer's leaves and init the next layer up
let thisLayer = leaves.slice(0, layerWidth);
let nextLayer = [];
for (let i = 0; i < depth; i++) {
for (let j = 0; j < layerWidth; j += 2) {
// Store the hash of each pair one layer up
nextLayer[j / 2] = hasher(Buffer.concat([thisLayer[j], thisLayer[j + 1]]));
}
layerWidth /= 2;
if (layerWidth & 1) {
if (nodeToShift.length) {
// If the next layer has odd length, and we have a node that needs to be shifted up, add it here
nextLayer.push(nodeToShift);
layerWidth += 1;
nodeToShift = Buffer.alloc(0);
} else {
// If we don't have a node waiting to be shifted, store the next layer's final node to be shifted
layerWidth -= 1;
nodeToShift = nextLayer[layerWidth];
}
}
// reset the layers
thisLayer = nextLayer;
nextLayer = [];
}
// return the root
return thisLayer[0];
}
5 changes: 3 additions & 2 deletions yarn-project/prover-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"exports": {
".": "./dest/index.js",
"./prover-agent": "./dest/prover-agent/index.js",
"./orchestrator": "./dest/orchestrator/index.js"
"./orchestrator": "./dest/orchestrator/index.js",
"./helpers": "./dest/orchestrator/block-building-helpers.js"
},
"typedocOptions": {
"entryPoints": [
Expand Down Expand Up @@ -95,4 +96,4 @@
"engines": {
"node": ">=18"
}
}
}
54 changes: 5 additions & 49 deletions yarn-project/sequencer-client/src/block_builder/index.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,7 @@
import { TestCircuitProver } from '@aztec/bb-prover';
import {
type BlockSimulator,
type MerkleTreeOperations,
type ProcessedTx,
type ProvingTicket,
type SimulationBlockResult,
} from '@aztec/circuit-types';
import { type Fr, type GlobalVariables } from '@aztec/circuits.js';
import { ProvingOrchestrator } from '@aztec/prover-client/orchestrator';
import { type SimulationProvider } from '@aztec/simulator';
import { type TelemetryClient } from '@aztec/telemetry-client';
import { NoopTelemetryClient } from '@aztec/telemetry-client/noop';
import { type BlockSimulator, type MerkleTreeOperations } from '@aztec/circuit-types';

/**
* Implements a block simulator using a test circuit prover under the hood, which just simulates circuits and outputs empty proofs.
* This class is temporary and should die once we switch from tx effects to tx objects submissions, since sequencers won't have
* the need to create L2 block headers to submit to L1. When we do that, we should also remove the references to the
* prover-client and bb-prover packages from this package.
*/
export class BlockBuilder implements BlockSimulator {
private orchestrator: ProvingOrchestrator;
constructor(db: MerkleTreeOperations, simulationProvider: SimulationProvider, telemetry: TelemetryClient) {
const testProver = new TestCircuitProver(telemetry, simulationProvider);
this.orchestrator = new ProvingOrchestrator(db, testProver, telemetry);
}

startNewBlock(numTxs: number, globalVariables: GlobalVariables, l1ToL2Messages: Fr[]): Promise<ProvingTicket> {
return this.orchestrator.startNewBlock(numTxs, globalVariables, l1ToL2Messages);
}
cancelBlock(): void {
this.orchestrator.cancelBlock();
}
finaliseBlock(): Promise<SimulationBlockResult> {
return this.orchestrator.finaliseBlock();
}
setBlockCompleted(): Promise<void> {
return this.orchestrator.setBlockCompleted();
}
addNewTx(tx: ProcessedTx): Promise<void> {
return this.orchestrator.addNewTx(tx);
}
}

export class BlockBuilderFactory {
constructor(private simulationProvider: SimulationProvider, private telemetry?: TelemetryClient) {}

create(db: MerkleTreeOperations): BlockSimulator {
return new BlockBuilder(db, this.simulationProvider, this.telemetry ?? new NoopTelemetryClient());
}
export * from './orchestrator.js';
export * from './light.js';
export interface BlockBuilderFactory {
create(db: MerkleTreeOperations): BlockSimulator;
}
Loading

0 comments on commit 54bde06

Please sign in to comment.