Skip to content
Open
58 changes: 18 additions & 40 deletions yarn-project/kv-store/src/stores/l2_tips_store.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
import { type BlockHash, type L2BlockTag, L2TipsStoreBase } from '@aztec/stdlib/block';
import { PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
import { type BlockHash, type CheckpointId, type L2BlockTag, L2TipsStoreBase } from '@aztec/stdlib/block';

import type { AztecAsyncMap } from '../interfaces/map.js';
import type { AztecAsyncKVStore } from '../interfaces/store.js';

/** Serialized form of a per-tip checkpoint id stored in the KV store. */
type StoredCheckpointId = { number: number; hash: string };

/**
* Persistent implementation of L2 tips store backed by a KV store.
* Used by nodes that need to persist chain state across restarts.
*/
export class L2TipsKVStore extends L2TipsStoreBase {
private readonly l2TipsStore: AztecAsyncMap<L2BlockTag, BlockNumber>;
private readonly l2TipCheckpointsStore: AztecAsyncMap<L2BlockTag, StoredCheckpointId>;
private readonly l2BlockHashesStore: AztecAsyncMap<BlockNumber, string>;
private readonly l2BlockNumberToCheckpointNumberStore: AztecAsyncMap<BlockNumber, CheckpointNumber>;
private readonly l2CheckpointStore: AztecAsyncMap<CheckpointNumber, Buffer>;

constructor(
private store: AztecAsyncKVStore,
Expand All @@ -22,11 +23,8 @@ export class L2TipsKVStore extends L2TipsStoreBase {
) {
super(initialBlockHash);
this.l2TipsStore = store.openMap([namespace, 'l2_tips'].join('_'));
this.l2TipCheckpointsStore = store.openMap([namespace, 'l2_tip_checkpoints'].join('_'));
this.l2BlockHashesStore = store.openMap([namespace, 'l2_block_hashes'].join('_'));
this.l2BlockNumberToCheckpointNumberStore = store.openMap(
[namespace, 'l2_block_number_to_checkpoint_number'].join('_'),
);
this.l2CheckpointStore = store.openMap([namespace, 'l2_checkpoint_store'].join('_'));
}

protected getTip(tag: L2BlockTag): Promise<BlockNumber | undefined> {
Expand All @@ -37,6 +35,18 @@ export class L2TipsKVStore extends L2TipsStoreBase {
return this.l2TipsStore.set(tag, blockNumber);
}

protected async getTipCheckpoint(tag: L2BlockTag): Promise<CheckpointId | undefined> {
const stored = await this.l2TipCheckpointsStore.getAsync(tag);
if (stored === undefined) {
return undefined;
}
return { number: CheckpointNumber(stored.number), hash: stored.hash };
}

protected setTipCheckpoint(tag: L2BlockTag, checkpoint: CheckpointId): Promise<void> {
return this.l2TipCheckpointsStore.set(tag, { number: checkpoint.number, hash: checkpoint.hash });
}

protected getStoredBlockHash(blockNumber: BlockNumber): Promise<string | undefined> {
return this.l2BlockHashesStore.getAsync(blockNumber);
}
Expand All @@ -51,38 +61,6 @@ export class L2TipsKVStore extends L2TipsStoreBase {
}
}

protected getCheckpointNumberForBlock(blockNumber: BlockNumber): Promise<CheckpointNumber | undefined> {
return this.l2BlockNumberToCheckpointNumberStore.getAsync(blockNumber);
}

protected setCheckpointNumberForBlock(blockNumber: BlockNumber, checkpointNumber: CheckpointNumber): Promise<void> {
return this.l2BlockNumberToCheckpointNumberStore.set(blockNumber, checkpointNumber);
}

protected async deleteBlockToCheckpointBefore(blockNumber: BlockNumber): Promise<void> {
for await (const key of this.l2BlockNumberToCheckpointNumberStore.keysAsync({ end: blockNumber })) {
await this.l2BlockNumberToCheckpointNumberStore.delete(key);
}
}

protected async getCheckpoint(checkpointNumber: CheckpointNumber): Promise<PublishedCheckpoint | undefined> {
const buffer = await this.l2CheckpointStore.getAsync(checkpointNumber);
if (!buffer) {
return undefined;
}
return PublishedCheckpoint.fromBuffer(buffer);
}

protected saveCheckpointData(checkpoint: PublishedCheckpoint): Promise<void> {
return this.l2CheckpointStore.set(checkpoint.checkpoint.number, checkpoint.toBuffer());
}

protected async deleteCheckpointsBefore(checkpointNumber: CheckpointNumber): Promise<void> {
for await (const key of this.l2CheckpointStore.keysAsync({ end: checkpointNumber })) {
await this.l2CheckpointStore.delete(key);
}
}

protected runInTransaction<T>(fn: () => Promise<T>): Promise<T> {
return this.store.transactionAsync(fn);
}
Expand Down
6 changes: 4 additions & 2 deletions yarn-project/p2p/src/client/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,10 @@ export async function createP2PClient(
}

const bindings = logger.getBindings();
// Schema version 3: tx proofs are stored in a separate map from the tx data (see TxPoolV2Impl).
const store = deps.store ?? (await createStore(P2P_STORE_NAME, 3, config, bindings));
// Schema version 4: L2 tips store resolves checkpoint tips from per-tip ids in l2_tip_checkpoints; the
// block->checkpoint mapping and checkpoint maps were dropped. Bumped to wipe stores whose tips predate
// per-tip ids, which would otherwise make getL2Tips throw on every read.
const store = deps.store ?? (await createStore(P2P_STORE_NAME, 4, config, bindings));
const archive = await createStore(P2P_ARCHIVE_STORE_NAME, 1, config, bindings);
const peerStore = await createStore(P2P_PEER_STORE_NAME, 1, config, bindings);
const attestationStore = await createStore(P2P_ATTESTATION_STORE_NAME, 2, config, bindings);
Expand Down
6 changes: 0 additions & 6 deletions yarn-project/p2p/src/client/p2p_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,10 +335,6 @@ describe('P2P Client', () => {
await expect(client.getL2Tips()).resolves.toEqual({
proposed: { number: BlockNumber(100), hash: expect.any(String) },
checkpointed: { block: { number: BlockNumber(100), hash: expect.any(String) }, checkpoint: anyCheckpoint },
proposedCheckpoint: {
block: { number: BlockNumber(100), hash: expect.any(String) },
checkpoint: anyCheckpoint,
},
proven: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: anyCheckpoint },
finalized: { block: { number: BlockNumber(50), hash: expect.any(String) }, checkpoint: anyCheckpoint },
});
Expand All @@ -349,7 +345,6 @@ describe('P2P Client', () => {

await expect(client.getL2Tips()).resolves.toEqual({
proposed: { number: BlockNumber(90), hash: expect.any(String) },
proposedCheckpoint: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: anyCheckpoint },
checkpointed: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: anyCheckpoint },
proven: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: anyCheckpoint },
finalized: { block: { number: BlockNumber(50), hash: expect.any(String) }, checkpoint: anyCheckpoint },
Expand All @@ -362,7 +357,6 @@ describe('P2P Client', () => {

await expect(client.getL2Tips()).resolves.toEqual({
proposed: { number: BlockNumber(92), hash: expect.any(String) },
proposedCheckpoint: { block: { number: BlockNumber(92), hash: expect.any(String) }, checkpoint: anyCheckpoint },
checkpointed: { block: { number: BlockNumber(92), hash: expect.any(String) }, checkpoint: anyCheckpoint },
proven: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: anyCheckpoint },
finalized: { block: { number: BlockNumber(50), hash: expect.any(String) }, checkpoint: anyCheckpoint },
Expand Down
6 changes: 3 additions & 3 deletions yarn-project/p2p/src/client/p2p_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import {
type L2BlockSource,
L2BlockStream,
type L2BlockStreamEvent,
type L2Tips,
type L2TipsStore,
type LocalL2Tips,
} from '@aztec/stdlib/block';
import type { ContractDataSource } from '@aztec/stdlib/contract';
import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
Expand Down Expand Up @@ -148,7 +148,7 @@ export class P2PClient extends WithTracer implements P2P {
this.p2pService.updateConfig(config);
}

public getL2Tips(): Promise<L2Tips> {
public getL2Tips(): Promise<LocalL2Tips> {
return this.l2Tips.getL2Tips();
}

Expand All @@ -174,7 +174,7 @@ export class P2PClient extends WithTracer implements P2P {
break;
case 'chain-pruned':
this.txCollection.stopCollectingForBlocksAfter(event.block.number);
await this.handlePruneL2Blocks(event.block, event.checkpoint);
await this.handlePruneL2Blocks(event.block, event.checkpointed.checkpoint);
break;
case 'chain-checkpointed':
break;
Expand Down
23 changes: 20 additions & 3 deletions yarn-project/prover-node/src/prover-node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ describe('ProverNode', () => {

// ---------------- event dispatch ----------------

/** Builds an L2TipId (block + checkpoint id) for block/checkpoint number `n`. */
const makeTipId = (n: number) => ({
block: { number: BlockNumber(n), hash: `0x0${n}` },
checkpoint: { number: CheckpointNumber(n), hash: `0x0${n}` },
});

it('dispatches chain-checkpointed to handleCheckpointEvent', async () => {
setupNotFullyProven();
const checkpoint = makeCheckpoint(1, 1, 1);
Expand All @@ -100,8 +106,9 @@ describe('ProverNode', () => {
// No registered checkpoints — nothing to prune.
await proverNode.handleBlockStreamEvent({
type: 'chain-pruned',
checkpoint: { number: CheckpointNumber(0), hash: '0x00' },
block: { number: BlockNumber(0), hash: '0x00' },
checkpointed: makeTipId(0),
proven: makeTipId(0),
});
expect(sessionManager.onPrune).not.toHaveBeenCalled();

Expand All @@ -115,8 +122,9 @@ describe('ProverNode', () => {

await proverNode.handleBlockStreamEvent({
type: 'chain-pruned',
checkpoint: { number: CheckpointNumber(1), hash: '0x01' },
block: { number: BlockNumber(1), hash: '0x01' },
checkpointed: makeTipId(1),
proven: makeTipId(1),
});
expect(sessionManager.onPrune).toHaveBeenCalledWith([EpochNumber(2)]);
});
Expand All @@ -125,6 +133,7 @@ describe('ProverNode', () => {
await proverNode.handleBlockStreamEvent({
type: 'chain-proven',
block: { number: BlockNumber(7), hash: '0x07' },
checkpoint: { number: CheckpointNumber(7), hash: '0x07' },
});
expect(publishingService.onChainProven).toHaveBeenCalledWith(BlockNumber(7));
});
Expand All @@ -149,6 +158,7 @@ describe('ProverNode', () => {
await proverNode.handleBlockStreamEvent({
type: 'chain-finalized',
block: { number: BlockNumber(1), hash: '0x01' },
checkpoint: { number: CheckpointNumber(1), hash: '0x01' },
});

expect(cache.get(txHash)).toBeUndefined();
Expand All @@ -164,6 +174,7 @@ describe('ProverNode', () => {
await proverNode.handleBlockStreamEvent({
type: 'chain-finalized',
block: { number: BlockNumber(1), hash: '0x01' },
checkpoint: { number: CheckpointNumber(1), hash: '0x01' },
});
expect(reapSpy.mock.calls.length).toBe(3);
reapSpy.mockClear();
Expand All @@ -172,6 +183,7 @@ describe('ProverNode', () => {
await proverNode.handleBlockStreamEvent({
type: 'chain-finalized',
block: { number: BlockNumber(1), hash: '0x01' },
checkpoint: { number: CheckpointNumber(1), hash: '0x01' },
});
expect(reapSpy).not.toHaveBeenCalled();
});
Expand All @@ -183,6 +195,7 @@ describe('ProverNode', () => {
await proverNode.handleBlockStreamEvent({
type: 'chain-finalized',
block: { number: BlockNumber(1), hash: '0x01' },
checkpoint: { number: CheckpointNumber(1), hash: '0x01' },
});
expect(reapSpy).not.toHaveBeenCalled();
});
Expand Down Expand Up @@ -318,6 +331,7 @@ describe('ProverNode', () => {
await proverNode.handleBlockStreamEvent({
type: 'chain-finalized',
block: { number: BlockNumber(1), hash: '0x01' },
checkpoint: { number: CheckpointNumber(1), hash: '0x01' },
});

expect(reapSpy).not.toHaveBeenCalled();
Expand All @@ -338,6 +352,7 @@ describe('ProverNode', () => {
await proverNode.handleBlockStreamEvent({
type: 'chain-finalized',
block: { number: BlockNumber(1), hash: '0x01' },
checkpoint: { number: CheckpointNumber(1), hash: '0x01' },
});

expect(reapSpy.mock.calls.map(([e]) => Number(e))).toEqual([0, 1, 2]);
Expand Down Expand Up @@ -372,8 +387,9 @@ describe('ProverNode', () => {
sessionManager.onPrune.mockClear();
await proverNode.handleBlockStreamEvent({
type: 'chain-pruned',
checkpoint: { number: CheckpointNumber(0), hash: '0x00' },
block: { number: BlockNumber(0), hash: '0x00' },
checkpointed: makeTipId(0),
proven: makeTipId(0),
});
expect(sessionManager.onPrune).toHaveBeenCalledTimes(1);
expect(sessionManager.onPrune).toHaveBeenCalledWith([EpochNumber(3)]);
Expand Down Expand Up @@ -594,6 +610,7 @@ describe('ProverNode', () => {
header: { slotNumber: SlotNumber(slot) },
archive: { root: archiveRoot },
blocks: [{ number: blockNumber, header: { hash: () => Promise.resolve('0x01') } }],
hash: () => new Fr(checkpointNumber),
} as unknown as Checkpoint;
}

Expand Down
2 changes: 1 addition & 1 deletion yarn-project/prover-node/src/prover-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
await this.handleCheckpointEvent(event.checkpoint);
break;
case 'chain-pruned':
await this.handlePruneEvent(event.checkpoint);
await this.handlePruneEvent(event.checkpointed.checkpoint);
break;
case 'chain-proven':
this.publishingService?.onChainProven(BlockNumber(event.block.number));
Expand Down
39 changes: 35 additions & 4 deletions yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,14 @@ describe('BlockSynchronizer', () => {
await synchronizer.handleBlockStreamEvent({
type: 'chain-pruned',
block: makeL2BlockId(reorgBlock.number, reorgResponse.hash.toString()),
checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()),
checkpointed: {
block: makeL2BlockId(BlockNumber.ZERO, GENESIS_BLOCK_HEADER_HASH.toString()),
checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()),
},
proven: {
block: makeL2BlockId(BlockNumber.ZERO, GENESIS_BLOCK_HEADER_HASH.toString()),
checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()),
},
});

// The anchor block should be updated to the reorg block header.
Expand Down Expand Up @@ -230,7 +237,14 @@ describe('BlockSynchronizer', () => {
await synchronizer.handleBlockStreamEvent({
type: 'chain-pruned',
block: block3,
checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()),
checkpointed: {
block: makeL2BlockId(BlockNumber.ZERO, GENESIS_BLOCK_HEADER_HASH.toString()),
checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()),
},
proven: {
block: makeL2BlockId(BlockNumber.ZERO, GENESIS_BLOCK_HEADER_HASH.toString()),
checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()),
},
});

// Rows at blocks 4 and 5 must be gone.
Expand Down Expand Up @@ -265,6 +279,7 @@ describe('BlockSynchronizer', () => {
await synchronizer.handleBlockStreamEvent({
type: 'chain-finalized',
block: block9,
checkpoint: makeL2CheckpointId(CheckpointNumber(1), Fr.random().toString()),
});

// Finalization is a no-op for storage under delete-on-prune, every row at and below the tip survives.
Expand Down Expand Up @@ -304,7 +319,14 @@ describe('BlockSynchronizer', () => {
await synchronizer.handleBlockStreamEvent({
type: 'chain-pruned',
block: block1,
checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()),
checkpointed: {
block: makeL2BlockId(BlockNumber.ZERO, GENESIS_BLOCK_HEADER_HASH.toString()),
checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()),
},
proven: {
block: makeL2BlockId(BlockNumber.ZERO, GENESIS_BLOCK_HEADER_HASH.toString()),
checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()),
},
});

// Blocks 2 and 3 deleted.
Expand Down Expand Up @@ -408,6 +430,7 @@ describe('BlockSynchronizer', () => {
await synchronizer.handleBlockStreamEvent({
type: 'chain-proven',
block: { number: BlockNumber(5), hash: '0x789' },
checkpoint: { number: CheckpointNumber(1), hash: '0x789c' },
});

const obtainedHeader = await anchorBlockStore.getBlockHeader();
Expand All @@ -428,6 +451,7 @@ describe('BlockSynchronizer', () => {
await synchronizer.handleBlockStreamEvent({
type: 'chain-finalized',
block: { number: BlockNumber(10), hash: '0xabc' },
checkpoint: { number: CheckpointNumber(2), hash: '0xabcc' },
});

const obtainedHeader = await anchorBlockStore.getBlockHeader();
Expand All @@ -445,7 +469,14 @@ describe('BlockSynchronizer', () => {
await synchronizer.handleBlockStreamEvent({
type: 'chain-pruned',
block: { number: BlockNumber(3), hash: '0x3' },
checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()),
checkpointed: {
block: makeL2BlockId(BlockNumber.ZERO, GENESIS_BLOCK_HEADER_HASH.toString()),
checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()),
},
proven: {
block: makeL2BlockId(BlockNumber.ZERO, GENESIS_BLOCK_HEADER_HASH.toString()),
checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()),
},
});

// Anchor should be unchanged
Expand Down
Loading
Loading