Skip to content

Commit d11f720

Browse files
authored
feat: caching block hash (#19138)
For [my other work](https://github.com/AztecProtocol/aztec-packages/pull/19030/changes) I need retrieval of a block header hash to be fast. In this PR I ensure this is the case by caching the hash.
2 parents 3a2ae02 + 73dc7bc commit d11f720

File tree

9 files changed

+59
-76
lines changed

9 files changed

+59
-76
lines changed

yarn-project/archiver/src/archiver/archiver_store_test_suite.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -709,8 +709,8 @@ export function describeArchiverDataStore(
709709
// getBlock should work for both checkpointed and uncheckpointed blocks
710710
expect((await store.getBlock(1))?.number).toBe(1);
711711
expect((await store.getBlock(2))?.number).toBe(2);
712-
expect(await store.getBlock(3)).toEqual(block3);
713-
expect(await store.getBlock(4)).toEqual(block4);
712+
expect((await store.getBlock(3))?.equals(block3)).toBe(true);
713+
expect((await store.getBlock(4))?.equals(block4)).toBe(true);
714714
expect(await store.getBlock(5)).toBeUndefined();
715715

716716
const block5 = await L2BlockNew.random(BlockNumber(5), {
@@ -723,13 +723,13 @@ export function describeArchiverDataStore(
723723
// Verify the uncheckpointed blocks have correct data
724724
const retrieved3 = await store.getBlock(3);
725725
expect(retrieved3!.number).toBe(3);
726-
expect(retrieved3).toEqual(block3);
726+
expect(retrieved3!.equals(block3)).toBe(true);
727727
const retrieved4 = await store.getBlock(4);
728728
expect(retrieved4!.number).toBe(4);
729-
expect(retrieved4).toEqual(block4);
729+
expect(retrieved4!.equals(block4)).toBe(true);
730730
const retrieved5 = await store.getBlock(5);
731731
expect(retrieved5!.number).toBe(5);
732-
expect(retrieved5).toEqual(block5);
732+
expect(retrieved5!.equals(block5)).toBe(true);
733733
});
734734

735735
it('getBlockByHash retrieves uncheckpointed blocks', async () => {
@@ -750,10 +750,10 @@ export function describeArchiverDataStore(
750750
const hash2 = await block2.header.hash();
751751

752752
const retrieved1 = await store.getBlockByHash(hash1);
753-
expect(retrieved1).toEqual(block1);
753+
expect(retrieved1!.equals(block1)).toBe(true);
754754

755755
const retrieved2 = await store.getBlockByHash(hash2);
756-
expect(retrieved2).toEqual(block2);
756+
expect(retrieved2!.equals(block2)).toBe(true);
757757
});
758758

759759
it('getBlockByArchive retrieves uncheckpointed blocks', async () => {
@@ -774,10 +774,10 @@ export function describeArchiverDataStore(
774774
const archive2 = block2.archive.root;
775775

776776
const retrieved1 = await store.getBlockByArchive(archive1);
777-
expect(retrieved1).toEqual(block1);
777+
expect(retrieved1!.equals(block1)).toBe(true);
778778

779779
const retrieved2 = await store.getBlockByArchive(archive2);
780-
expect(retrieved2).toEqual(block2);
780+
expect(retrieved2!.equals(block2)).toBe(true);
781781
});
782782

783783
it('getCheckpointedBlock returns undefined for uncheckpointed blocks', async () => {
@@ -811,8 +811,8 @@ export function describeArchiverDataStore(
811811
expect(await store.getCheckpointedBlock(4)).toBeUndefined();
812812

813813
// But getBlock should work for all blocks
814-
expect(await store.getBlock(3)).toEqual(block3);
815-
expect(await store.getBlock(4)).toEqual(block4);
814+
expect((await store.getBlock(3))?.equals(block3)).toBe(true);
815+
expect((await store.getBlock(4))?.equals(block4)).toBe(true);
816816
});
817817

818818
it('getCheckpointedBlockByHash returns undefined for uncheckpointed blocks', async () => {
@@ -829,7 +829,7 @@ export function describeArchiverDataStore(
829829
expect(await store.getCheckpointedBlockByHash(hash)).toBeUndefined();
830830

831831
// But getBlockByHash should work
832-
expect(await store.getBlockByHash(hash)).toEqual(block1);
832+
expect((await store.getBlockByHash(hash))?.equals(block1)).toBe(true);
833833
});
834834

835835
it('getCheckpointedBlockByArchive returns undefined for uncheckpointed blocks', async () => {
@@ -846,7 +846,7 @@ export function describeArchiverDataStore(
846846
expect(await store.getCheckpointedBlockByArchive(archive)).toBeUndefined();
847847

848848
// But getBlockByArchive should work
849-
expect(await store.getBlockByArchive(archive)).toEqual(block1);
849+
expect((await store.getBlockByArchive(archive))?.equals(block1)).toBe(true);
850850
});
851851

852852
it('checkpoint adopts previously added uncheckpointed blocks', async () => {
@@ -1064,8 +1064,8 @@ export function describeArchiverDataStore(
10641064
await expect(store.addBlocks([block3, block4])).resolves.toBe(true);
10651065

10661066
// Verify blocks were added
1067-
expect(await store.getBlock(3)).toEqual(block3);
1068-
expect(await store.getBlock(4)).toEqual(block4);
1067+
expect((await store.getBlock(3))?.equals(block3)).toBe(true);
1068+
expect((await store.getBlock(4))?.equals(block4)).toBe(true);
10691069
});
10701070

10711071
it('allows blocks for the initial checkpoint when store is empty', async () => {
@@ -1083,8 +1083,8 @@ export function describeArchiverDataStore(
10831083
await expect(store.addBlocks([block1, block2])).resolves.toBe(true);
10841084

10851085
// Verify blocks were added
1086-
expect(await store.getBlock(1)).toEqual(block1);
1087-
expect(await store.getBlock(2)).toEqual(block2);
1086+
expect((await store.getBlock(1))?.equals(block1)).toBe(true);
1087+
expect((await store.getBlock(2))?.equals(block2)).toBe(true);
10881088
expect(await store.getLatestBlockNumber()).toBe(2);
10891089
});
10901090

yarn-project/archiver/src/archiver/kv_archiver_store/block_store.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,6 @@ export class BlockStore {
669669
body,
670670
CheckpointNumber(blockStorage.checkpointNumber!),
671671
blockStorage.indexWithinCheckpoint,
672-
Fr.fromBuffer(blockHash),
673672
);
674673

675674
if (block.number !== blockNumber) {

yarn-project/constants/src/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export * from './constants.gen.js';
2121
// eslint-disable-next-line import/export
2222
export const INITIAL_L2_BLOCK_NUM: BlockNumber = BlockNumber(INITIAL_L2_BLOCK_NUM_RAW);
2323

24-
/** The initial L2 checkpoint number (typed as CheckpointNumber). This is the first checkpont number in the Aztec L2 chain. */
24+
/** The initial L2 checkpoint number (typed as CheckpointNumber). This is the first checkpoint number in the Aztec L2 chain. */
2525
// Shadow the export from constants.gen above
2626
// eslint-disable-next-line import/export
2727
export const INITIAL_L2_CHECKPOINT_NUM: CheckpointNumber = CheckpointNumber(INITIAL_CHECKPOINT_NUM_RAW);

yarn-project/prover-client/src/block-factory/light.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ describe('LightBlockBuilder', () => {
125125

126126
const expectedHeader = await buildExpectedHeader(txs, l1ToL2Messages);
127127

128-
expect(header).toEqual(expectedHeader);
128+
expect(header.equals(expectedHeader)).toBe(true);
129129
});
130130

131131
it('builds a 3 tx header', async () => {
@@ -137,7 +137,7 @@ describe('LightBlockBuilder', () => {
137137
return Promise.resolve([merge, rollupOutputs[2]]);
138138
});
139139

140-
expect(header).toEqual(expectedHeader);
140+
expect(header.equals(expectedHeader)).toBe(true);
141141
});
142142

143143
it('builds a 4 tx header', async () => {
@@ -150,7 +150,7 @@ describe('LightBlockBuilder', () => {
150150
return [mergeLeft, mergeRight];
151151
});
152152

153-
expect(header).toEqual(expectedHeader);
153+
expect(header.equals(expectedHeader)).toBe(true);
154154
});
155155

156156
it('builds a 4 tx header with no l1 to l2 messages', async () => {
@@ -164,7 +164,7 @@ describe('LightBlockBuilder', () => {
164164
return [mergeLeft, mergeRight];
165165
});
166166

167-
expect(header).toEqual(expectedHeader);
167+
expect(header.equals(expectedHeader)).toBe(true);
168168
});
169169

170170
it('builds a 5 tx header', async () => {
@@ -178,7 +178,7 @@ describe('LightBlockBuilder', () => {
178178
return [merge20, rollupOutputs[4]];
179179
});
180180

181-
expect(header).toEqual(expectedHeader);
181+
expect(header.equals(expectedHeader)).toBe(true);
182182
});
183183

184184
it('builds a single tx header', async () => {
@@ -187,7 +187,7 @@ describe('LightBlockBuilder', () => {
187187

188188
const expectedHeader = await buildExpectedHeader(txs, l1ToL2Messages);
189189

190-
expect(header).toEqual(expectedHeader);
190+
expect(header.equals(expectedHeader)).toBe(true);
191191
});
192192

193193
it('builds an empty header', async () => {
@@ -196,7 +196,7 @@ describe('LightBlockBuilder', () => {
196196

197197
const expectedHeader = await buildExpectedHeader(txs, l1ToL2Messages);
198198

199-
expect(header).toEqual(expectedHeader);
199+
expect(header.equals(expectedHeader)).toBe(true);
200200
});
201201

202202
const makeTx = (i: number) => {

yarn-project/simulator/src/public/fixtures/utils.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,7 @@ export async function createTxForPublicCalls(
132132
: Gas.empty();
133133
const gasSettings = new GasSettings(gasLimits, teardownGasLimits, maxFeesPerGas, GasFees.empty());
134134
const txContext = new TxContext(Fr.zero(), Fr.zero(), gasSettings);
135-
const header = BlockHeader.empty();
136-
header.globalVariables = globals;
135+
const header = BlockHeader.empty({ globalVariables: globals });
137136
const constantData = new TxConstantData(header, txContext, Fr.zero(), Fr.zero());
138137
const includeByTimestamp = 0n; // Not used in the simulator.
139138

yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.test.ts

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,9 @@ import {
55
GAS_ESTIMATION_TEARDOWN_DA_GAS_LIMIT,
66
GAS_ESTIMATION_TEARDOWN_L2_GAS_LIMIT,
77
NULLIFIER_SUBTREE_HEIGHT,
8-
PUBLIC_DATA_TREE_HEIGHT,
98
} from '@aztec/constants';
109
import { Fr } from '@aztec/foundation/curves/bn254';
1110
import { EthAddress } from '@aztec/foundation/eth-address';
12-
import type { AztecKVStore } from '@aztec/kv-store';
13-
import { openTmpStore } from '@aztec/kv-store/lmdb';
14-
import { type AppendOnlyTree, Poseidon, StandardTree, newTree } from '@aztec/merkle-tree';
1511
import { ProtocolContractAddress } from '@aztec/protocol-contracts';
1612
import { computeFeePayerBalanceStorageSlot } from '@aztec/protocol-contracts/fee-juice';
1713
import { PublicDataWrite, PublicSimulatorConfig, PublicTxResult, RevertCode } from '@aztec/stdlib/avm';
@@ -24,8 +20,8 @@ import type { MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server'
2420
import { countAccumulatedItems } from '@aztec/stdlib/kernel';
2521
import { L2ToL1Message, ScopedL2ToL1Message } from '@aztec/stdlib/messaging';
2622
import { fr, mockTx } from '@aztec/stdlib/testing';
27-
import { AppendOnlyTreeSnapshot, MerkleTreeId, PublicDataTreeLeaf } from '@aztec/stdlib/trees';
28-
import { BlockHeader, GlobalVariables, PartialStateReference, StateReference } from '@aztec/stdlib/tx';
23+
import { MerkleTreeId, PublicDataTreeLeaf } from '@aztec/stdlib/trees';
24+
import { GlobalVariables } from '@aztec/stdlib/tx';
2925
import { NativeWorldStateService } from '@aztec/world-state';
3026

3127
import { jest } from '@jest/globals';
@@ -63,10 +59,7 @@ describe('public_tx_simulator', () => {
6359
let merkleTreesCopy: MerkleTreeWriteOperations;
6460
let contractsDB: PublicContractsDB;
6561

66-
let publicDataTree: AppendOnlyTree<Fr>;
67-
6862
let worldStateService: NativeWorldStateService;
69-
let treeStore: AztecKVStore;
7063
let simulator: PublicTxSimulator;
7164
let simulateInternal: jest.SpiedFunction<
7265
(
@@ -271,35 +264,11 @@ describe('public_tx_simulator', () => {
271264
merkleTreesCopy = await worldStateService.fork();
272265
contractsDB = new PublicContractsDB(mock<ContractDataSource>());
273266

274-
treeStore = openTmpStore();
275-
276-
publicDataTree = await newTree(
277-
StandardTree,
278-
treeStore,
279-
new Poseidon(),
280-
'PublicData',
281-
Fr,
282-
PUBLIC_DATA_TREE_HEIGHT,
283-
1, // Add a default low leaf for the public data hints to be proved against.
284-
);
285-
const snap = new AppendOnlyTreeSnapshot(
286-
Fr.fromBuffer(publicDataTree.getRoot(true)),
287-
Number(publicDataTree.getNumLeaves(true)),
288-
);
289-
const header = BlockHeader.empty();
290-
const stateReference = new StateReference(
291-
header.state.l1ToL2MessageTree,
292-
new PartialStateReference(header.state.partial.noteHashTree, header.state.partial.nullifierTree, snap),
293-
);
294-
// Clone the whole state because somewhere down the line (AbstractPhaseManager) the public data root is modified in the referenced header directly :/
295-
header.state = StateReference.fromBuffer(stateReference.toBuffer());
296-
297267
simulator = createSimulator({ skipFeeEnforcement: true });
298268
}, 30_000);
299269

300270
afterEach(async () => {
301271
await worldStateService.close();
302-
await treeStore.delete();
303272
});
304273

305274
it('runs a tx with enqueued public calls in setup phase only', async () => {

yarn-project/stdlib/src/block/l2_block_new.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ export class L2BlockNew {
2727
public checkpointNumber: CheckpointNumber,
2828
/** Index of the block within the checkpoint. */
2929
public indexWithinCheckpoint: number,
30-
private blockHash: Fr | undefined = undefined,
3130
) {}
3231

3332
get number(): BlockNumber {
@@ -80,11 +79,23 @@ export class L2BlockNew {
8079
* Returns the block's hash (hash of block header).
8180
* @returns The block's hash.
8281
*/
83-
public async hash(): Promise<Fr> {
84-
if (this.blockHash === undefined) {
85-
this.blockHash = await this.header.hash();
86-
}
87-
return this.blockHash;
82+
public hash(): Promise<Fr> {
83+
return this.header.hash();
84+
}
85+
86+
/**
87+
* Checks if this block equals another block.
88+
* @param other - The other block to compare with.
89+
* @returns True if both blocks are equal.
90+
*/
91+
public equals(other: this): boolean {
92+
return (
93+
this.archive.equals(other.archive) &&
94+
this.header.equals(other.header) &&
95+
this.body.equals(other.body) &&
96+
this.checkpointNumber === other.checkpointNumber &&
97+
this.indexWithinCheckpoint === other.indexWithinCheckpoint
98+
);
8899
}
89100

90101
public toBlobFields(): Fr[] {
@@ -185,7 +196,6 @@ export class L2BlockNew {
185196

186197
toBlockInfo(): L2BlockInfo {
187198
return {
188-
blockHash: this.blockHash,
189199
archive: this.archive.root,
190200
lastArchive: this.header.lastArchive.root,
191201
blockNumber: this.number,

yarn-project/stdlib/src/tx/block_header.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,24 @@ import { StateReference } from './state_reference.js';
1717

1818
/** A header of an L2 block. */
1919
export class BlockHeader {
20+
private _cachedHash?: Promise<Fr>;
21+
2022
constructor(
2123
/** Snapshot of archive before the block is applied. */
22-
public lastArchive: AppendOnlyTreeSnapshot,
24+
public readonly lastArchive: AppendOnlyTreeSnapshot,
2325
/** State reference. */
24-
public state: StateReference,
26+
public readonly state: StateReference,
2527
/**
2628
* Hash of the sponge blob after the tx effects of this block has been applied.
2729
* May contain tx effects from the previous blocks in the same checkpoint.
2830
*/
29-
public spongeBlobHash: Fr,
31+
public readonly spongeBlobHash: Fr,
3032
/** Global variables of an L2 block. */
31-
public globalVariables: GlobalVariables,
33+
public readonly globalVariables: GlobalVariables,
3234
/** Total fees in the block, computed by the root rollup circuit */
33-
public totalFees: Fr,
35+
public readonly totalFees: Fr,
3436
/** Total mana used in the block, computed by the root rollup circuit */
35-
public totalManaUsed: Fr,
37+
public readonly totalManaUsed: Fr,
3638
) {}
3739

3840
static get schema(): ZodFor<BlockHeader> {
@@ -160,7 +162,10 @@ export class BlockHeader {
160162
}
161163

162164
hash(): Promise<Fr> {
163-
return poseidon2HashWithSeparator(this.toFields(), GeneratorIndex.BLOCK_HASH);
165+
if (!this._cachedHash) {
166+
this._cachedHash = poseidon2HashWithSeparator(this.toFields(), GeneratorIndex.BLOCK_HASH);
167+
}
168+
return this._cachedHash;
164169
}
165170

166171
static random(overrides: Partial<FieldsOf<BlockHeader>> & Partial<FieldsOf<GlobalVariables>> = {}): BlockHeader {

yarn-project/world-state/src/test/utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
} from '@aztec/stdlib/interfaces/server';
1717
import { mockCheckpointAndMessages, mockL1ToL2Messages } from '@aztec/stdlib/testing';
1818
import { AppendOnlyTreeSnapshot, MerkleTreeId } from '@aztec/stdlib/trees';
19+
import { BlockHeader } from '@aztec/stdlib/tx';
1920

2021
import type { NativeWorldStateService } from '../native/native_world_state.js';
2122

@@ -59,7 +60,7 @@ export async function updateBlockState(block: L2BlockNew, l1ToL2Messages: Fr[],
5960
await Promise.all([publicDataInsert, nullifierInsert, noteHashInsert, messageInsert]);
6061

6162
const state = await fork.getStateReference();
62-
block.header.state = state;
63+
block.header = BlockHeader.from({ ...block.header, state });
6364
await fork.updateArchive(block.header);
6465

6566
const archiveState = await fork.getTreeInfo(MerkleTreeId.ARCHIVE);

0 commit comments

Comments
 (0)