Skip to content

Commit 37db75e

Browse files
authored
refactor(PXE): clean up block sync (#19026)
Block sync is currently handled through collaboration of `Synchronizer`, `SyncDataProvider`, and even `PXEOracleInterface#getAnchorBlock`. ## Current problems - The names aren't very intuitive (`Synchronizer` and `SyncDataProvider` only handle chain state data at the end of the day). - Any code running inside `ContractFunctionSimulator` can call `getAnchorBlock` whenever it pleases. This works currently because calls to `Synchronizer` are carefully orchestrated from PXE. But it seems error prone, obscure, and it gives the false impression that function simulation happens on live block data when it actually depends on a static anchor. - There are multiple ways throughout the app to get the anchor block: `Synchronizer#getSynchedBlock` (unused), `PXEOracleInterface#getAnchorBlock` (unnecessary, inappropriate), `SyncDataProvider#getBlockNumber` (redundant), and `SyncDataProvider#getBlockHeader` (the one that should be the source of truth) - Many checks are relying on block number when they should rely on block hash. ## Refactor (note: adjusted naming after discussing with @benesjan) - Rename `Synchronizer` => `BlockSynchronizer` - Rename `SyncDataProvider` => `AnchorBlockDataProvider` - Remove `AnchorBlockDataProvider#getBlockNumber`. `AnchorBlockDataProvider#getBlockHeader` remains the only way to consume current block information for the rest of the app. To get the block number, there's a simple `getBlockNumber` accessor in `BlockHeader`. - Remove `ExecutionDataProvider#getAnchorBlock`. Instead, explicitly pass anchor block headers as params to functions in "the realm" of `ContractFunctionSimulator` (`run`, `runUtility`, `varifyCurrentClassId`, etc). ## Out of scope of PR > - Many checks are relying on block number when they should rely on block hash. This should be tackled on a per use case/feature basis in future PRs, though relying on `getBlockHeader` solely seems aligned in the right direction. Closes F-225
2 parents f204ea2 + d393ba0 commit 37db75e

30 files changed

+221
-189
lines changed

yarn-project/pxe/src/synchronizer/synchronizer.test.ts renamed to yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,21 @@ import { randomPublishedL2Block } from '@aztec/stdlib/testing';
99
import { jest } from '@jest/globals';
1010
import { type MockProxy, mock } from 'jest-mock-extended';
1111

12+
import { AnchorBlockDataProvider } from '../storage/anchor_block_data_provider/anchor_block_data_provider.js';
1213
import { NoteDataProvider } from '../storage/note_data_provider/note_data_provider.js';
13-
import { SyncDataProvider } from '../storage/sync_data_provider/sync_data_provider.js';
1414
import { TaggingDataProvider } from '../storage/tagging_data_provider/tagging_data_provider.js';
15-
import { Synchronizer } from './synchronizer.js';
15+
import { BlockSynchronizer } from './block_synchronizer.js';
1616

17-
describe('Synchronizer', () => {
18-
let synchronizer: Synchronizer;
17+
describe('BlockSynchronizer', () => {
18+
let synchronizer: BlockSynchronizer;
1919
let tipsStore: L2TipsKVStore;
20-
let syncDataProvider: SyncDataProvider;
20+
let anchorBlockDataProvider: AnchorBlockDataProvider;
2121
let noteDataProvider: NoteDataProvider;
2222
let taggingDataProvider: TaggingDataProvider;
2323
let aztecNode: MockProxy<AztecNode>;
2424
let blockStream: MockProxy<L2BlockStream>;
2525

26-
const TestSynchronizer = class extends Synchronizer {
26+
const TestSynchronizer = class extends BlockSynchronizer {
2727
protected override createBlockStream(): L2BlockStream {
2828
return blockStream;
2929
}
@@ -34,17 +34,23 @@ describe('Synchronizer', () => {
3434
blockStream = mock<L2BlockStream>();
3535
aztecNode = mock<AztecNode>();
3636
tipsStore = new L2TipsKVStore(store, 'pxe');
37-
syncDataProvider = new SyncDataProvider(store);
37+
anchorBlockDataProvider = new AnchorBlockDataProvider(store);
3838
noteDataProvider = await NoteDataProvider.create(store);
3939
taggingDataProvider = new TaggingDataProvider(store);
40-
synchronizer = new TestSynchronizer(aztecNode, syncDataProvider, noteDataProvider, taggingDataProvider, tipsStore);
40+
synchronizer = new TestSynchronizer(
41+
aztecNode,
42+
anchorBlockDataProvider,
43+
noteDataProvider,
44+
taggingDataProvider,
45+
tipsStore,
46+
);
4147
});
4248

4349
it('sets header from latest block', async () => {
4450
const block = await randomPublishedL2Block(1);
4551
await synchronizer.handleBlockStreamEvent({ type: 'blocks-added', blocks: [block] });
4652

47-
const obtainedHeader = await syncDataProvider.getBlockHeader();
53+
const obtainedHeader = await anchorBlockDataProvider.getBlockHeader();
4854
expect(obtainedHeader).toEqual(block.block.getBlockHeader());
4955
});
5056

yarn-project/pxe/src/synchronizer/synchronizer.ts renamed to yarn-project/pxe/src/block_synchronizer/block_synchronizer.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,23 @@ import { L2BlockStream, type L2BlockStreamEvent, type L2BlockStreamEventHandler
55
import type { AztecNode } from '@aztec/stdlib/interfaces/client';
66

77
import type { PXEConfig } from '../config/index.js';
8+
import type { AnchorBlockDataProvider } from '../storage/anchor_block_data_provider/anchor_block_data_provider.js';
89
import type { NoteDataProvider } from '../storage/note_data_provider/note_data_provider.js';
9-
import type { SyncDataProvider } from '../storage/sync_data_provider/sync_data_provider.js';
1010
import type { TaggingDataProvider } from '../storage/tagging_data_provider/tagging_data_provider.js';
1111

1212
/**
13-
* The Synchronizer class orchestrates synchronization between the PXE and Aztec node, maintaining an up-to-date
13+
* The BlockSynchronizer class orchestrates synchronization between PXE and Aztec node, maintaining an up-to-date
1414
* view of the L2 chain state. It handles block header retrieval, chain reorganizations, and provides an interface
1515
* for querying sync status.
1616
*/
17-
export class Synchronizer implements L2BlockStreamEventHandler {
17+
export class BlockSynchronizer implements L2BlockStreamEventHandler {
1818
private log: Logger;
1919
private isSyncing: Promise<void> | undefined;
2020
protected readonly blockStream: L2BlockStream;
2121

2222
constructor(
2323
private node: AztecNode,
24-
private syncDataProvider: SyncDataProvider,
24+
private anchorBlockDataProvider: AnchorBlockDataProvider,
2525
private noteDataProvider: NoteDataProvider,
2626
private taggingDataProvider: TaggingDataProvider,
2727
private l2TipsStore: L2TipsKVStore,
@@ -30,7 +30,7 @@ export class Synchronizer implements L2BlockStreamEventHandler {
3030
) {
3131
this.log =
3232
!loggerOrSuffix || typeof loggerOrSuffix === 'string'
33-
? createLogger(loggerOrSuffix ? `pxe:synchronizer:${loggerOrSuffix}` : `pxe:synchronizer`)
33+
? createLogger(loggerOrSuffix ? `pxe:block_synchronizer:${loggerOrSuffix}` : `pxe:block_synchronizer`)
3434
: loggerOrSuffix;
3535
this.blockStream = this.createBlockStream(config);
3636
}
@@ -56,13 +56,13 @@ export class Synchronizer implements L2BlockStreamEventHandler {
5656
archive: lastBlock.archive.root.toString(),
5757
header: lastBlock.header.toInspect(),
5858
});
59-
await this.syncDataProvider.setHeader(lastBlock.getBlockHeader());
59+
await this.anchorBlockDataProvider.setHeader(lastBlock.getBlockHeader());
6060
break;
6161
}
6262
case 'chain-pruned': {
6363
this.log.warn(`Pruning data after block ${event.block.number} due to reorg`);
6464
// We first unnullify and then remove so that unnullified notes that were created after the block number end up deleted.
65-
const lastSynchedBlockNumber = await this.syncDataProvider.getBlockNumber();
65+
const lastSynchedBlockNumber = (await this.anchorBlockDataProvider.getBlockHeader()).getBlockNumber();
6666
await this.noteDataProvider.rollbackNotesAndNullifiers(event.block.number, lastSynchedBlockNumber);
6767
// Remove all note tagging indexes to force a full resync. This is suboptimal, but unless we track the
6868
// block number in which each index is used it's all we can do.
@@ -72,7 +72,7 @@ export class Synchronizer implements L2BlockStreamEventHandler {
7272
if (!newHeader) {
7373
this.log.error(`Block header not found for block number ${event.block.number} during chain prune`);
7474
} else {
75-
await this.syncDataProvider.setHeader(newHeader);
75+
await this.anchorBlockDataProvider.setHeader(newHeader);
7676
}
7777
break;
7878
}
@@ -82,6 +82,11 @@ export class Synchronizer implements L2BlockStreamEventHandler {
8282
/**
8383
* Syncs PXE and the node by downloading the metadata of the latest blocks, allowing simulations to use
8484
* recent data (e.g. notes), and handling any reorgs that might have occurred.
85+
*
86+
* Note this BlockSynchronizer is designed to let its users control when a synchronization is run,
87+
* so this component doesn't proactively stay up to date with the blockchain.
88+
*
89+
* We do this so PXE can ensure data consistency.
8590
*/
8691
public async sync() {
8792
if (this.isSyncing !== undefined) {
@@ -104,18 +109,14 @@ export class Synchronizer implements L2BlockStreamEventHandler {
104109
let currentHeader;
105110

106111
try {
107-
currentHeader = await this.syncDataProvider.getBlockHeader();
112+
currentHeader = await this.anchorBlockDataProvider.getBlockHeader();
108113
} catch {
109114
this.log.debug('Header is not set, requesting from the node');
110115
}
111116
if (!currentHeader) {
112117
// REFACTOR: We should know the header of the genesis block without having to request it from the node.
113-
await this.syncDataProvider.setHeader((await this.node.getBlockHeader(BlockNumber.ZERO))!);
118+
await this.anchorBlockDataProvider.setHeader((await this.node.getBlockHeader(BlockNumber.ZERO))!);
114119
}
115120
await this.blockStream.sync();
116121
}
117-
118-
public getSynchedBlockNumber() {
119-
return this.syncDataProvider.getBlockNumber();
120-
}
121122
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './block_synchronizer.js';

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ export interface KernelProverConfig {
1919
}
2020

2121
/**
22-
* Configuration settings for the synchronizer.
22+
* Configuration settings for the block synchronizer.
2323
*/
24-
export interface SynchronizerConfig {
24+
export interface BlockSynchronizerConfig {
2525
/** Maximum amount of blocks to pull from the stream in one request when synchronizing */
2626
l2BlockBatchSize: number;
2727
}
2828

29-
export type PXEConfig = KernelProverConfig & DataStoreConfig & ChainConfig & SynchronizerConfig;
29+
export type PXEConfig = KernelProverConfig & DataStoreConfig & ChainConfig & BlockSynchronizerConfig;
3030

3131
export type CliPXEOptions = {
3232
/** Custom Aztec Node URL to connect to */

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import { PrivateLog } from '@aztec/stdlib/logs';
5959
import { ScopedL2ToL1Message } from '@aztec/stdlib/messaging';
6060
import { ChonkProof } from '@aztec/stdlib/proofs';
6161
import {
62+
BlockHeader,
6263
CallContext,
6364
HashedValues,
6465
PrivateExecutionResult,
@@ -98,6 +99,7 @@ export class ContractFunctionSimulator {
9899
* @param contractAddress - The address of the contract (should match request.origin)
99100
* @param msgSender - The address calling the function. This can be replaced to simulate a call from another contract
100101
* or a specific account.
102+
* @param anchorBlockHeader - The block header to use as base state for this run.
101103
* @param senderForTags - The address that is used as a tagging sender when emitting private logs. Returned from
102104
* the `privateGetSenderForTags` oracle.
103105
* @param scopes - The accounts whose notes we can access in this call. Currently optional and will default to all.
@@ -108,13 +110,13 @@ export class ContractFunctionSimulator {
108110
contractAddress: AztecAddress,
109111
selector: FunctionSelector,
110112
msgSender = AztecAddress.fromField(Fr.MAX_FIELD_VALUE),
113+
anchorBlockHeader: BlockHeader,
111114
senderForTags?: AztecAddress,
112115
scopes?: AztecAddress[],
113116
): Promise<PrivateExecutionResult> {
114117
const simulatorSetupTimer = new Timer();
115-
const anchorBlockHeader = await this.executionDataProvider.getAnchorBlockHeader();
116118

117-
await verifyCurrentClassId(contractAddress, this.executionDataProvider);
119+
await verifyCurrentClassId(contractAddress, this.executionDataProvider, anchorBlockHeader);
118120

119121
const entryPointArtifact = await this.executionDataProvider.getFunctionArtifact(contractAddress, selector);
120122

@@ -213,20 +215,34 @@ export class ContractFunctionSimulator {
213215
* Runs a utility function.
214216
* @param call - The function call to execute.
215217
* @param authwits - Authentication witnesses required for the function call.
218+
* @param anchorBlockHeader - The block header to use as base state for this run.
216219
* @param scopes - Optional array of account addresses whose notes can be accessed in this call. Defaults to all
217220
* accounts if not specified.
218221
* @returns A return value of the utility function in a form as returned by the simulator (Noir fields)
219222
*/
220-
public async runUtility(call: FunctionCall, authwits: AuthWitness[], scopes?: AztecAddress[]): Promise<Fr[]> {
221-
await verifyCurrentClassId(call.to, this.executionDataProvider);
223+
public async runUtility(
224+
call: FunctionCall,
225+
authwits: AuthWitness[],
226+
anchorBlockHeader: BlockHeader,
227+
scopes?: AztecAddress[],
228+
): Promise<Fr[]> {
229+
await verifyCurrentClassId(call.to, this.executionDataProvider, anchorBlockHeader);
222230

223231
const entryPointArtifact = await this.executionDataProvider.getFunctionArtifact(call.to, call.selector);
224232

225233
if (entryPointArtifact.functionType !== FunctionType.UTILITY) {
226234
throw new Error(`Cannot run ${entryPointArtifact.functionType} function as utility`);
227235
}
228236

229-
const oracle = new UtilityExecutionOracle(call.to, authwits, [], this.executionDataProvider, undefined, scopes);
237+
const oracle = new UtilityExecutionOracle(
238+
call.to,
239+
authwits,
240+
[],
241+
anchorBlockHeader,
242+
this.executionDataProvider,
243+
undefined,
244+
scopes,
245+
);
230246

231247
try {
232248
this.log.verbose(`Executing utility function ${entryPointArtifact.name}`, {

yarn-project/pxe/src/contract_function_simulator/execution_data_provider.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { KeyValidationRequest } from '@aztec/stdlib/kernel';
1010
import type { DirectionalAppTaggingSecret } from '@aztec/stdlib/logs';
1111
import type { NoteStatus } from '@aztec/stdlib/note';
1212
import { type MerkleTreeId, type NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees';
13-
import type { BlockHeader, NodeStats } from '@aztec/stdlib/tx';
13+
import type { NodeStats } from '@aztec/stdlib/tx';
1414

1515
import type { NoteData } from './oracle/interfaces.js';
1616
import type { MessageLoadOracleInputs } from './oracle/message_load_oracle_inputs.js';
@@ -138,14 +138,6 @@ export interface ExecutionDataProvider {
138138
secret: Fr,
139139
): Promise<MessageLoadOracleInputs<typeof L1_TO_L2_MSG_TREE_HEIGHT>>;
140140

141-
/**
142-
* Retrieve the latest block header synchronized by the execution data provider. This block header is referred
143-
* to as the anchor block header in Aztec terminology and it defines the state that is used during private function
144-
* execution.
145-
* @returns The anchor block header.
146-
*/
147-
getAnchorBlockHeader(): Promise<BlockHeader>;
148-
149141
/**
150142
* Fetches the index and sibling path of a leaf at a given block from a given tree.
151143
* @param blockNumber - The block number at which to get the membership witness.

yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export interface IMiscOracle {
6363
export interface IUtilityExecutionOracle {
6464
isUtility: true;
6565

66-
utilityGetUtilityContext(): Promise<UtilityContext>;
66+
utilityGetUtilityContext(): UtilityContext;
6767
utilityGetKeyValidationRequest(pkMHash: Fr): Promise<KeyValidationRequest>;
6868
utilityGetContractInstance(address: AztecAddress): Promise<ContractInstance>;
6969
utilityGetMembershipWitness(blockNumber: BlockNumber, treeId: MerkleTreeId, leafValue: Fr): Promise<Fr[] | undefined>;

yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,9 @@ export class Oracle {
112112
return [values.map(toACVMField)];
113113
}
114114

115-
async utilityGetUtilityContext(): Promise<(ACVMField | ACVMField[])[]> {
116-
const context = await this.handlerAsUtility().utilityGetUtilityContext();
117-
return context.toNoirRepresentation();
115+
utilityGetUtilityContext(): Promise<(ACVMField | ACVMField[])[]> {
116+
const context = this.handlerAsUtility().utilityGetUtilityContext();
117+
return Promise.resolve(context.toNoirRepresentation());
118118
}
119119

120120
async utilityGetKeyValidationRequest([pkMHash]: ACVMField[]): Promise<ACVMField[]> {

yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ describe('Oracle Version Check test suite', () => {
2525
// Mock basic oracle responses
2626
executionDataProvider.getPublicStorageAt.mockResolvedValue(Fr.ZERO);
2727
executionDataProvider.loadCapsule.mockImplementation((_, __) => Promise.resolve(null));
28-
executionDataProvider.getAnchorBlockHeader.mockResolvedValue(BlockHeader.empty());
2928
executionDataProvider.getContractInstance.mockResolvedValue({
3029
currentContractClassId: new Fr(42),
3130
originalContractClassId: new Fr(42),
@@ -68,7 +67,7 @@ describe('Oracle Version Check test suite', () => {
6867
// Call the private function with arbitrary message sender and sender for tags
6968
const msgSender = await AztecAddress.random();
7069
const senderForTags = await AztecAddress.random();
71-
await acirSimulator.run(txRequest, contractAddress, selector, msgSender, senderForTags);
70+
await acirSimulator.run(txRequest, contractAddress, selector, msgSender, BlockHeader.random(), senderForTags);
7271

7372
expect(executionDataProvider.assertCompatibleOracleVersion).toHaveBeenCalledTimes(1);
7473
}, 30_000);
@@ -97,7 +96,7 @@ describe('Oracle Version Check test suite', () => {
9796
};
9897

9998
// Call the utility function
100-
await acirSimulator.runUtility(execRequest, [], []);
99+
await acirSimulator.runUtility(execRequest, [], BlockHeader.random(), []);
101100

102101
expect(executionDataProvider.assertCompatibleOracleVersion).toHaveBeenCalledTimes(1);
103102
}, 30_000);

0 commit comments

Comments
 (0)