Skip to content

Commit

Permalink
feat: Light block builder (#8662)
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.

Replaces the orchestator-based block builder on the sequencer. In a
32-tx block with a simple public function per tx, this reduces block
building time from 38 to 18 seconds.

```
aztec:sequencer [VERBOSE] Assembled block 5 (txEffectsHash: 00a201e4acd5f1d58d8369d385f3fa0a7c10c84c1a95c15c75844603660f5ff3) eventName=l2-block-built duration=18983.888042002916 publicProcessDuration=18955.1897890009 rollupCircuitsDuration=18983.70012800023 txCount=32 blockNumber=5 blockTimestamp=1726864978 noteEncryptedLogLength=256 noteEncryptedLogCount=0 encryptedLogLength=256 encryptedLogCount=0 unencryptedLogCount=0 unencryptedLogSize=384 +19s
```
```
aztec:sequencer [VERBOSE] Assembled block 5 (txEffectsHash: 00414283d876d3b97871a671bc18660222211c7baa9eb106da5215b998752320) eventName=l2-block-built duration=38610.87693199888 publicProcessDuration=19141.498885001987 rollupCircuitsDuration=38610.68504599854 txCount=32 blockNumber=5 blockTimestamp=1726865139 noteEncryptedLogLength=256 noteEncryptedLogCount=0 encryptedLogLength=256 encryptedLogCount=0 unencryptedLogCount=0 unencryptedLogSize=384 +39s
```
  • Loading branch information
spalladino authored Sep 23, 2024
1 parent a9e412b commit 1e922a5
Show file tree
Hide file tree
Showing 24 changed files with 821 additions and 234 deletions.
1 change: 1 addition & 0 deletions yarn-project/circuit-types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"./jest": "./dest/jest/index.js",
"./interfaces": "./dest/interfaces/index.js",
"./log_id": "./dest/logs/log_id.js",
"./test": "./dest/test/index.js",
"./tx_hash": "./dest/tx/tx_hash.js"
},
"typedocOptions": {
Expand Down
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 { computeUnbalancedMerkleRoot } 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 computeUnbalancedMerkleRoot(leaves, emptyTxEffectHash);
}

get noteEncryptedLogs(): EncryptedNoteL2BlockL2Logs {
Expand Down
19 changes: 19 additions & 0 deletions yarn-project/circuit-types/src/merkle_tree_id.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import {
ARCHIVE_HEIGHT,
ARCHIVE_TREE_ID,
L1_TO_L2_MESSAGE_TREE_ID,
L1_TO_L2_MSG_TREE_HEIGHT,
NOTE_HASH_TREE_HEIGHT,
NOTE_HASH_TREE_ID,
NULLIFIER_TREE_HEIGHT,
NULLIFIER_TREE_ID,
PUBLIC_DATA_TREE_HEIGHT,
PUBLIC_DATA_TREE_ID,
} from '@aztec/circuits.js';

Expand All @@ -21,3 +26,17 @@ export enum MerkleTreeId {
export const merkleTreeIds = () => {
return Object.values(MerkleTreeId).filter((v): v is MerkleTreeId => !isNaN(Number(v)));
};

const TREE_HEIGHTS = {
[MerkleTreeId.NOTE_HASH_TREE]: NOTE_HASH_TREE_HEIGHT,
[MerkleTreeId.ARCHIVE]: ARCHIVE_HEIGHT,
[MerkleTreeId.L1_TO_L2_MESSAGE_TREE]: L1_TO_L2_MSG_TREE_HEIGHT,
[MerkleTreeId.NULLIFIER_TREE]: NULLIFIER_TREE_HEIGHT,
[MerkleTreeId.PUBLIC_DATA_TREE]: PUBLIC_DATA_TREE_HEIGHT,
} as const;

export type TreeHeights = typeof TREE_HEIGHTS;

export function getTreeHeight<TID extends MerkleTreeId>(treeId: TID): TreeHeights[TID] {
return TREE_HEIGHTS[treeId];
}
63 changes: 63 additions & 0 deletions yarn-project/circuit-types/src/test/factories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { type MerkleTreeOperations, makeProcessedTx, mockTx } from '@aztec/circuit-types';
import {
Fr,
GasSettings,
type Header,
KernelCircuitPublicInputs,
LogHash,
MAX_L2_TO_L1_MSGS_PER_TX,
MAX_NOTE_HASHES_PER_TX,
MAX_NULLIFIERS_PER_TX,
MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX,
PublicDataUpdateRequest,
ScopedLogHash,
} from '@aztec/circuits.js';
import { makeScopedL2ToL1Message } from '@aztec/circuits.js/testing';
import { makeTuple } from '@aztec/foundation/array';

/** Makes a bloated processed tx for testing purposes. */
export function makeBloatedProcessedTx(
historicalHeaderOrDb: Header | MerkleTreeOperations,
vkRoot: Fr,
seed = 0x1,
overrides: { inclusionFee?: Fr } = {},
) {
seed *= MAX_NULLIFIERS_PER_TX; // Ensure no clashing given incremental seeds
const tx = mockTx(seed);
const kernelOutput = KernelCircuitPublicInputs.empty();
kernelOutput.constants.vkTreeRoot = vkRoot;
kernelOutput.constants.historicalHeader =
'getInitialHeader' in historicalHeaderOrDb ? historicalHeaderOrDb.getInitialHeader() : historicalHeaderOrDb;
kernelOutput.end.publicDataUpdateRequests = makeTuple(
MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX,
i => new PublicDataUpdateRequest(new Fr(i), new Fr(i + 10), i + 20),
seed + 0x500,
);
kernelOutput.end.publicDataUpdateRequests = makeTuple(
MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX,
i => new PublicDataUpdateRequest(new Fr(i), new Fr(i + 10), i + 20),
seed + 0x600,
);

kernelOutput.constants.txContext.gasSettings = GasSettings.default({ inclusionFee: overrides.inclusionFee });

const processedTx = makeProcessedTx(tx, kernelOutput, []);

processedTx.data.end.noteHashes = makeTuple(MAX_NOTE_HASHES_PER_TX, i => new Fr(i), seed + 0x100);
processedTx.data.end.nullifiers = makeTuple(MAX_NULLIFIERS_PER_TX, i => new Fr(i), seed + 0x100000);

processedTx.data.end.nullifiers[tx.data.forPublic!.end.nullifiers.length - 1] = Fr.zero();

processedTx.data.end.l2ToL1Msgs = makeTuple(MAX_L2_TO_L1_MSGS_PER_TX, makeScopedL2ToL1Message, seed + 0x300);
processedTx.noteEncryptedLogs.unrollLogs().forEach((log, i) => {
processedTx.data.end.noteEncryptedLogsHashes[i] = new LogHash(Fr.fromBuffer(log.hash()), 0, new Fr(log.length));
});
processedTx.encryptedLogs.unrollLogs().forEach((log, i) => {
processedTx.data.end.encryptedLogsHashes[i] = new ScopedLogHash(
new LogHash(Fr.fromBuffer(log.hash()), 0, new Fr(log.length)),
log.maskedContractAddress,
);
});

return processedTx;
}
1 change: 1 addition & 0 deletions yarn-project/circuit-types/src/test/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './factories.js';
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
4 changes: 2 additions & 2 deletions yarn-project/circuits.js/src/merkle/merkle_tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class MerkleTree {
}

/** Returns a nice string representation of the tree, useful for debugging purposes. */
public drawTree() {
public drawTree(elemSize = 8) {
const levels: string[][] = [];
const tree = this.nodes;
const maxRowSize = Math.ceil(tree.length / 2);
Expand All @@ -58,7 +58,7 @@ export class MerkleTree {
levels.push(
tree
.slice(rowOffset, rowOffset + rowSize)
.map(n => n.toString('hex').slice(0, 8) + ' '.repeat((paddingSize - 1) * 9)),
.map(n => n.toString('hex').slice(0, elemSize) + ' '.repeat((paddingSize - 1) * (elemSize + 1))),
);
rowOffset += rowSize;
paddingSize <<= 1;
Expand Down
5 changes: 3 additions & 2 deletions yarn-project/circuits.js/src/structs/gas_settings.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { compact } from '@aztec/foundation/collection';
import { Fr } from '@aztec/foundation/fields';
import { BufferReader, FieldReader, serializeToBuffer, serializeToFields } from '@aztec/foundation/serialize';
import { type FieldsOf } from '@aztec/foundation/types';
Expand Down Expand Up @@ -66,13 +67,13 @@ export class GasSettings {
}

/** Default gas settings to use when user has not provided them. */
static default(overrides?: Partial<FieldsOf<GasSettings>>) {
static default(overrides: Partial<FieldsOf<GasSettings>> = {}) {
return GasSettings.from({
gasLimits: { l2Gas: DEFAULT_GAS_LIMIT, daGas: DEFAULT_GAS_LIMIT },
teardownGasLimits: { l2Gas: DEFAULT_TEARDOWN_GAS_LIMIT, daGas: DEFAULT_TEARDOWN_GAS_LIMIT },
maxFeesPerGas: { feePerL2Gas: new Fr(DEFAULT_MAX_FEE_PER_GAS), feePerDaGas: new Fr(DEFAULT_MAX_FEE_PER_GAS) },
inclusionFee: new Fr(DEFAULT_INCLUSION_FEE),
...overrides,
...compact(overrides),
});
}

Expand Down
33 changes: 17 additions & 16 deletions yarn-project/circuits.js/src/tests/factories.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type FieldsOf, makeHalfFullTuple, makeTuple } from '@aztec/foundation/array';
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { toBufferBE } from '@aztec/foundation/bigint-buffer';
import { compact } from '@aztec/foundation/collection';
import { EthAddress } from '@aztec/foundation/eth-address';
import { type Bufferable } from '@aztec/foundation/serialize';
import {
Expand Down Expand Up @@ -789,21 +790,18 @@ export function makePrivateCircuitPublicInputs(seed = 0): PrivateCircuitPublicIn
});
}

export function makeGlobalVariables(
seed = 1,
blockNumber: number | undefined = undefined,
slotNumber: number | undefined = undefined,
): GlobalVariables {
return new GlobalVariables(
new Fr(seed),
new Fr(seed + 1),
new Fr(blockNumber ?? seed + 2),
new Fr(slotNumber ?? seed + 3),
new Fr(seed + 4),
EthAddress.fromField(new Fr(seed + 5)),
AztecAddress.fromField(new Fr(seed + 6)),
new GasFees(new Fr(seed + 7), new Fr(seed + 8)),
);
export function makeGlobalVariables(seed = 1, overrides: Partial<FieldsOf<GlobalVariables>> = {}): GlobalVariables {
return GlobalVariables.from({
chainId: new Fr(seed),
version: new Fr(seed + 1),
blockNumber: new Fr(seed + 2),
slotNumber: new Fr(seed + 3),
timestamp: new Fr(seed + 4),
coinbase: EthAddress.fromField(new Fr(seed + 5)),
feeRecipient: AztecAddress.fromField(new Fr(seed + 6)),
gasFees: new GasFees(new Fr(seed + 7), new Fr(seed + 8)),
...compact(overrides),
});
}

export function makeGasFees(seed = 1) {
Expand Down Expand Up @@ -1076,7 +1074,10 @@ export function makeHeader(
makeAppendOnlyTreeSnapshot(seed + 0x100),
makeContentCommitment(seed + 0x200, txsEffectsHash),
makeStateReference(seed + 0x600),
makeGlobalVariables((seed += 0x700), blockNumber, slotNumber),
makeGlobalVariables((seed += 0x700), {
...(blockNumber ? { blockNumber: new Fr(blockNumber) } : {}),
...(slotNumber ? { slotNumber: new Fr(slotNumber) } : {}),
}),
fr(seed + 0x800),
);
}
Expand Down
32 changes: 30 additions & 2 deletions yarn-project/end-to-end/src/e2e_block_building.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from '@aztec/aztec.js';
import { times } from '@aztec/foundation/collection';
import { poseidon2HashWithSeparator } from '@aztec/foundation/crypto';
import { StatefulTestContractArtifact } from '@aztec/noir-contracts.js';
import { StatefulTestContract, StatefulTestContractArtifact } from '@aztec/noir-contracts.js';
import { TestContract } from '@aztec/noir-contracts.js/Test';
import { TokenContract } from '@aztec/noir-contracts.js/Token';

Expand Down Expand Up @@ -89,7 +89,35 @@ describe('e2e_block_building', () => {
expect(areDeployed).toEqual(times(TX_COUNT, () => true));
});

it.skip('can call public function from different tx in same block', async () => {
it('assembles a block with multiple txs with public fns', async () => {
// First deploy the contract
const ownerAddress = owner.getCompleteAddress().address;
const contract = await StatefulTestContract.deploy(owner, ownerAddress, ownerAddress, 1).send().deployed();

// Assemble N contract deployment txs
// We need to create them sequentially since we cannot have parallel calls to a circuit
const TX_COUNT = 8;
await aztecNode.setConfig({ minTxsPerBlock: TX_COUNT });

const methods = times(TX_COUNT, i => contract.methods.increment_public_value(ownerAddress, i));
for (let i = 0; i < TX_COUNT; i++) {
await methods[i].create({});
await methods[i].prove({});
}

// Send them simultaneously to be picked up by the sequencer
const txs = await Promise.all(methods.map(method => method.send()));
logger.info(`Txs sent with hashes: `);
for (const tx of txs) {
logger.info(` ${await tx.getTxHash()}`);
}

// Await txs to be mined and assert they are all mined on the same block
const receipts = await Promise.all(txs.map(tx => tx.wait()));
expect(receipts.map(r => r.blockNumber)).toEqual(times(TX_COUNT, () => receipts[0].blockNumber));
});

it.skip('can call public function from different tx in same block as deployed', async () => {
// Ensure both txs will land on the same block
await aztecNode.setConfig({ minTxsPerBlock: 2 });

Expand Down
2 changes: 1 addition & 1 deletion yarn-project/foundation/src/serialize/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ export function toFriendlyJSON(obj: object): string {
).toFriendlyJSON
) {
return value.toFriendlyJSON();
} else if (value && value.type && ['Fr', 'Fq', 'AztecAddress'].includes(value.type)) {
} else if (value && value.type && ['Fr', 'Fq', 'AztecAddress', 'EthAddress'].includes(value.type)) {
return value.value;
} else {
return value;
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 './unbalanced_merkle_root.js';

/**
* A leaf of an indexed merkle tree.
*/
Expand Down
Loading

0 comments on commit 1e922a5

Please sign in to comment.