Skip to content

Commit b090d22

Browse files
author
AztecBot
committed
Merge branch 'next' into merge-train/barretenberg
2 parents 8b88095 + 3a0fe12 commit b090d22

File tree

4 files changed

+214
-16
lines changed

4 files changed

+214
-16
lines changed

yarn-project/foundation/src/config/env_var.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ export type EnvVar =
176176
| 'PROVER_TEST_VERIFICATION_DELAY_MS'
177177
| 'PXE_L2_BLOCK_BATCH_SIZE'
178178
| 'PXE_PROVER_ENABLED'
179+
| 'PXE_SYNC_CHAIN_TIP'
179180
| 'RPC_MAX_BATCH_SIZE'
180181
| 'RPC_MAX_BODY_SIZE'
181182
| 'RPC_SIMULATE_PUBLIC_MAX_GAS_LIMIT'

yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts

Lines changed: 146 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
22
import { timesParallel } from '@aztec/foundation/collection';
33
import { Fr } from '@aztec/foundation/curves/bn254';
4+
import type { AztecAsyncKVStore } from '@aztec/kv-store';
45
import { openTmpStore } from '@aztec/kv-store/lmdb-v2';
56
import { L2TipsKVStore } from '@aztec/kv-store/stores';
67
import { GENESIS_CHECKPOINT_HEADER_HASH, L2Block, L2BlockHash, type L2BlockStream } from '@aztec/stdlib/block';
8+
import { Checkpoint, L1PublishedData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
79
import type { AztecNode } from '@aztec/stdlib/interfaces/client';
810

911
import { jest } from '@jest/globals';
1012
import { type MockProxy, mock } from 'jest-mock-extended';
1113

14+
import type { BlockSynchronizerConfig } from '../config/index.js';
1215
import { AnchorBlockStore } from '../storage/anchor_block_store/anchor_block_store.js';
1316
import { NoteStore } from '../storage/note_store/note_store.js';
1417
import { PrivateEventStore } from '../storage/private_event_store/private_event_store.js';
1518
import { BlockSynchronizer } from './block_synchronizer.js';
1619

1720
describe('BlockSynchronizer', () => {
1821
let synchronizer: BlockSynchronizer;
22+
let store: AztecAsyncKVStore;
1923
let tipsStore: L2TipsKVStore;
2024
let anchorBlockStore: AnchorBlockStore;
2125
let noteStore: NoteStore;
@@ -29,15 +33,19 @@ describe('BlockSynchronizer', () => {
2933
}
3034
};
3135

36+
const createSynchronizer = (config: Partial<BlockSynchronizerConfig> = {}) => {
37+
return new TestSynchronizer(aztecNode, store, anchorBlockStore, noteStore, privateEventStore, tipsStore, config);
38+
};
39+
3240
beforeEach(async () => {
33-
const store = await openTmpStore('test');
41+
store = await openTmpStore('test');
3442
blockStream = mock<L2BlockStream>();
3543
aztecNode = mock<AztecNode>();
3644
tipsStore = new L2TipsKVStore(store, 'pxe');
3745
anchorBlockStore = new AnchorBlockStore(store);
3846
noteStore = new NoteStore(store);
3947
privateEventStore = new PrivateEventStore(store);
40-
synchronizer = new TestSynchronizer(aztecNode, store, anchorBlockStore, noteStore, privateEventStore, tipsStore);
48+
synchronizer = createSynchronizer();
4149
});
4250

4351
it('sets header from latest block', async () => {
@@ -95,4 +103,140 @@ describe('BlockSynchronizer', () => {
95103

96104
expect(rollback).toHaveBeenCalledWith(3, 4);
97105
});
106+
107+
describe('syncChainTip config', () => {
108+
it('updates anchor on blocks-added when syncChainTip is proposed (default)', async () => {
109+
synchronizer = createSynchronizer({ syncChainTip: 'proposed' });
110+
const block = await L2Block.random(BlockNumber(1));
111+
await synchronizer.handleBlockStreamEvent({ type: 'blocks-added', blocks: [block] });
112+
113+
const obtainedHeader = await anchorBlockStore.getBlockHeader();
114+
expect(obtainedHeader.equals(block.header)).toBe(true);
115+
});
116+
117+
it('does not update anchor on blocks-added when syncChainTip is checkpointed', async () => {
118+
synchronizer = createSynchronizer({ syncChainTip: 'checkpointed' });
119+
120+
// First set a known anchor
121+
const initialBlock = await L2Block.random(BlockNumber(0));
122+
await anchorBlockStore.setHeader(initialBlock.header);
123+
124+
// blocks-added should NOT update the anchor
125+
const newBlock = await L2Block.random(BlockNumber(1));
126+
await synchronizer.handleBlockStreamEvent({ type: 'blocks-added', blocks: [newBlock] });
127+
128+
const obtainedHeader = await anchorBlockStore.getBlockHeader();
129+
expect(obtainedHeader.equals(initialBlock.header)).toBe(true);
130+
});
131+
132+
it('updates anchor on chain-checkpointed when syncChainTip is checkpointed', async () => {
133+
synchronizer = createSynchronizer({ syncChainTip: 'checkpointed' });
134+
135+
// Set initial anchor
136+
const initialBlock = await L2Block.random(BlockNumber(0));
137+
await anchorBlockStore.setHeader(initialBlock.header);
138+
139+
// Create a checkpoint with a block
140+
const checkpointBlock = await L2Block.random(BlockNumber(1));
141+
const checkpoint = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 1 });
142+
// Replace the random block with our known block
143+
checkpoint.blocks[0] = checkpointBlock;
144+
145+
const publishedCheckpoint = new PublishedCheckpoint(checkpoint, L1PublishedData.random(), []);
146+
147+
await synchronizer.handleBlockStreamEvent({
148+
type: 'chain-checkpointed',
149+
checkpoint: publishedCheckpoint,
150+
block: { number: BlockNumber(1), hash: '0x456' },
151+
});
152+
153+
const obtainedHeader = await anchorBlockStore.getBlockHeader();
154+
expect(obtainedHeader.equals(checkpointBlock.header)).toBe(true);
155+
});
156+
157+
it('does not update anchor on chain-checkpointed when syncChainTip is proposed', async () => {
158+
synchronizer = createSynchronizer({ syncChainTip: 'proposed' });
159+
160+
// Set initial anchor via blocks-added
161+
const initialBlock = await L2Block.random(BlockNumber(1));
162+
await synchronizer.handleBlockStreamEvent({ type: 'blocks-added', blocks: [initialBlock] });
163+
164+
// Create a different checkpoint
165+
const checkpoint = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 1 });
166+
const publishedCheckpoint = new PublishedCheckpoint(checkpoint, L1PublishedData.random(), []);
167+
168+
await synchronizer.handleBlockStreamEvent({
169+
type: 'chain-checkpointed',
170+
checkpoint: publishedCheckpoint,
171+
block: { number: BlockNumber(1), hash: '0x456' },
172+
});
173+
174+
// Anchor should still be the initial block, not the checkpoint block
175+
const obtainedHeader = await anchorBlockStore.getBlockHeader();
176+
expect(obtainedHeader.equals(initialBlock.header)).toBe(true);
177+
});
178+
179+
it('updates anchor on chain-proven when syncChainTip is proven', async () => {
180+
synchronizer = createSynchronizer({ syncChainTip: 'proven' });
181+
182+
// Set initial anchor
183+
const initialBlock = await L2Block.random(BlockNumber(0));
184+
await anchorBlockStore.setHeader(initialBlock.header);
185+
186+
// Mock node to return block header
187+
const provenBlock = await L2Block.random(BlockNumber(5));
188+
aztecNode.getBlockHeader.mockResolvedValue(provenBlock.header);
189+
190+
await synchronizer.handleBlockStreamEvent({
191+
type: 'chain-proven',
192+
block: { number: BlockNumber(5), hash: '0x789' },
193+
});
194+
195+
const obtainedHeader = await anchorBlockStore.getBlockHeader();
196+
expect(obtainedHeader.equals(provenBlock.header)).toBe(true);
197+
});
198+
199+
it('updates anchor on chain-finalized when syncChainTip is finalized', async () => {
200+
synchronizer = createSynchronizer({ syncChainTip: 'finalized' });
201+
202+
// Set initial anchor
203+
const initialBlock = await L2Block.random(BlockNumber(0));
204+
await anchorBlockStore.setHeader(initialBlock.header);
205+
206+
// Mock node to return block header
207+
const finalizedBlock = await L2Block.random(BlockNumber(10));
208+
aztecNode.getBlockHeader.mockResolvedValue(finalizedBlock.header);
209+
210+
await synchronizer.handleBlockStreamEvent({
211+
type: 'chain-finalized',
212+
block: { number: BlockNumber(10), hash: '0xabc' },
213+
});
214+
215+
const obtainedHeader = await anchorBlockStore.getBlockHeader();
216+
expect(obtainedHeader.equals(finalizedBlock.header)).toBe(true);
217+
});
218+
219+
it('ignores prune event when anchor is already at or below prune point', async () => {
220+
synchronizer = createSynchronizer({ syncChainTip: 'checkpointed' });
221+
222+
// Set anchor to block 2
223+
const anchorBlock = await L2Block.random(BlockNumber(2));
224+
await anchorBlockStore.setHeader(anchorBlock.header);
225+
226+
const rollback = jest.spyOn(noteStore, 'rollback').mockImplementation(() => Promise.resolve());
227+
228+
// Prune to block 3 (above anchor) - should be ignored
229+
await synchronizer.handleBlockStreamEvent({
230+
type: 'chain-pruned',
231+
block: { number: BlockNumber(3), hash: '0x3' },
232+
checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_CHECKPOINT_HEADER_HASH.toString() },
233+
});
234+
235+
expect(rollback).not.toHaveBeenCalled();
236+
237+
// Anchor should be unchanged
238+
const obtainedHeader = await anchorBlockStore.getBlockHeader();
239+
expect(obtainedHeader.equals(anchorBlock.header)).toBe(true);
240+
});
241+
});
98242
});

yarn-project/pxe/src/block_synchronizer/block_synchronizer.ts

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import {
99
type L2BlockStreamEventHandler,
1010
} from '@aztec/stdlib/block';
1111
import type { AztecNode } from '@aztec/stdlib/interfaces/client';
12+
import type { BlockHeader } from '@aztec/stdlib/tx';
1213

13-
import type { PXEConfig } from '../config/index.js';
14+
import type { BlockSynchronizerConfig } from '../config/index.js';
1415
import type { AnchorBlockStore } from '../storage/anchor_block_store/anchor_block_store.js';
1516
import type { NoteStore } from '../storage/note_store/note_store.js';
1617
import type { PrivateEventStore } from '../storage/private_event_store/private_event_store.js';
@@ -32,7 +33,7 @@ export class BlockSynchronizer implements L2BlockStreamEventHandler {
3233
private noteStore: NoteStore,
3334
private privateEventStore: PrivateEventStore,
3435
private l2TipsStore: L2TipsKVStore,
35-
config: Partial<Pick<PXEConfig, 'l2BlockBatchSize'>> = {},
36+
private config: Partial<BlockSynchronizerConfig> = {},
3637
loggerOrSuffix?: string | Logger,
3738
) {
3839
this.log =
@@ -42,7 +43,7 @@ export class BlockSynchronizer implements L2BlockStreamEventHandler {
4243
this.blockStream = this.createBlockStream(config);
4344
}
4445

45-
protected createBlockStream(config: Partial<Pick<PXEConfig, 'l2BlockBatchSize'>>) {
46+
protected createBlockStream(config: Partial<BlockSynchronizerConfig>): L2BlockStream {
4647
return new L2BlockStream(this.node, this.l2TipsStore, this, createLogger('pxe:block_stream'), {
4748
batchSize: config.l2BlockBatchSize,
4849
// Skipping finalized blocks makes us sync much faster - we only need to download blocks other than the latest one
@@ -57,19 +58,51 @@ export class BlockSynchronizer implements L2BlockStreamEventHandler {
5758

5859
switch (event.type) {
5960
case 'blocks-added': {
60-
const lastBlock = event.blocks.at(-1)!;
61-
this.log.verbose(`Updated pxe last block to ${lastBlock.number}`, {
62-
blockHash: lastBlock.hash(),
63-
archive: lastBlock.archive.root.toString(),
64-
header: lastBlock.header.toInspect(),
65-
});
66-
await this.anchorBlockStore.setHeader(lastBlock.header);
61+
if (this.config.syncChainTip === undefined || this.config.syncChainTip === 'proposed') {
62+
const lastBlock = event.blocks.at(-1)!;
63+
await this.updateAnchorBlockHeader(lastBlock.header);
64+
}
65+
break;
66+
}
67+
case 'chain-checkpointed': {
68+
if (this.config.syncChainTip === 'checkpointed') {
69+
// Get the last block header from the checkpoint
70+
const lastBlock = event.checkpoint.checkpoint.blocks.at(-1)!;
71+
await this.updateAnchorBlockHeader(lastBlock.header);
72+
}
73+
break;
74+
}
75+
case 'chain-proven': {
76+
if (this.config.syncChainTip === 'proven') {
77+
const blockHeader = await this.node.getBlockHeader(BlockNumber(event.block.number));
78+
if (blockHeader) {
79+
await this.updateAnchorBlockHeader(blockHeader);
80+
}
81+
}
82+
break;
83+
}
84+
case 'chain-finalized': {
85+
if (this.config.syncChainTip === 'finalized') {
86+
const blockHeader = await this.node.getBlockHeader(BlockNumber(event.block.number));
87+
if (blockHeader) {
88+
await this.updateAnchorBlockHeader(blockHeader);
89+
}
90+
}
6791
break;
6892
}
6993
case 'chain-pruned': {
94+
const currentAnchorBlockHeader = await this.anchorBlockStore.getBlockHeader();
95+
const currentAnchorBlockNumber = currentAnchorBlockHeader.getBlockNumber();
96+
if (currentAnchorBlockNumber <= event.block.number) {
97+
this.log.verbose(
98+
`Ignoring prune event to block ${event.block.number} greater than current anchor block ${currentAnchorBlockNumber}`,
99+
{ pruneEvent: event, currentAnchorBlockHeader: currentAnchorBlockHeader.toInspect() },
100+
);
101+
return;
102+
}
103+
70104
this.log.warn(`Pruning data after block ${event.block.number} due to reorg`);
71105

72-
const oldAnchorBlockNumber = (await this.anchorBlockStore.getBlockHeader()).getBlockNumber();
73106
// Note that the following is not necessarily the anchor block that will be used in the transaction - if
74107
// the chain has already moved past the reorg, we'll also see blocks-added events that will push the anchor
75108
// forward.
@@ -83,15 +116,21 @@ export class BlockSynchronizer implements L2BlockStreamEventHandler {
83116

84117
// Operations are wrapped in a single transaction to ensure atomicity.
85118
await this.store.transactionAsync(async () => {
86-
await this.noteStore.rollback(event.block.number, oldAnchorBlockNumber);
87-
await this.privateEventStore.rollback(event.block.number, oldAnchorBlockNumber);
88-
await this.anchorBlockStore.setHeader(newAnchorBlockHeader);
119+
await this.noteStore.rollback(event.block.number, currentAnchorBlockNumber);
120+
await this.privateEventStore.rollback(event.block.number, currentAnchorBlockNumber);
121+
await this.updateAnchorBlockHeader(newAnchorBlockHeader);
89122
});
90123
break;
91124
}
92125
}
93126
}
94127

128+
/** Updates the anchor block header to the target block */
129+
private async updateAnchorBlockHeader(blockHeader: BlockHeader) {
130+
this.log.verbose(`Updated pxe last block to ${blockHeader.getBlockNumber()}`, blockHeader.toInspect());
131+
await this.anchorBlockStore.setHeader(blockHeader);
132+
}
133+
95134
/**
96135
* Syncs PXE and the node by downloading the metadata of the latest blocks, allowing simulations to use
97136
* recent data (e.g. notes), and handling any reorgs that might have occurred.

yarn-project/pxe/src/config/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export interface KernelProverConfig {
2424
export interface BlockSynchronizerConfig {
2525
/** Maximum amount of blocks to pull from the stream in one request when synchronizing */
2626
l2BlockBatchSize: number;
27+
/** Which chain tip to sync to (proposed, checkpointed, proven, finalized) */
28+
syncChainTip?: 'proposed' | 'checkpointed' | 'proven' | 'finalized';
2729
}
2830

2931
export type PXEConfig = KernelProverConfig & DataStoreConfig & ChainConfig & BlockSynchronizerConfig;
@@ -53,6 +55,18 @@ export const pxeConfigMappings: ConfigMappingsType<PXEConfig> = {
5355
description: 'Enable real proofs',
5456
...booleanConfigHelper(true),
5557
},
58+
syncChainTip: {
59+
env: 'PXE_SYNC_CHAIN_TIP',
60+
description: 'Which chain tip to sync to (proposed, checkpointed, proven, finalized)',
61+
defaultValue: 'proposed',
62+
parseEnv: (val: string) => {
63+
const allowedValues = ['proposed', 'checkpointed', 'proven', 'finalized'];
64+
if (allowedValues.includes(val)) {
65+
return val;
66+
}
67+
throw new Error(`Invalid value for PXE_SYNC_CHAIN_TIP: ${val}. Allowed values are: ${allowedValues.join(', ')}`);
68+
},
69+
},
5670
};
5771

5872
/**

0 commit comments

Comments
 (0)