Skip to content

Commit e893675

Browse files
authored
feat: serialize synchronize and simulateTx calls by the pxe via SerialQueue (AztecProtocol#3817)
Serialize calls by the PXE to: - synchronize with the aztec node - simulate transactions by use of a SerialQueue Synchronization is greedy, meaning it will sync as much as possible while its job is processing.
1 parent 30e47a0 commit e893675

File tree

4 files changed

+96
-46
lines changed

4 files changed

+96
-46
lines changed

yarn-project/foundation/src/fifo/memory_fifo.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,9 @@ export class MemoryFifo<T> {
9191

9292
/**
9393
* Process items from the queue using a provided handler function.
94-
* The function iterates over items in the queue, invoking the handler for each item until the queue is empty or flushing.
94+
* The function iterates over items in the queue, invoking the handler for each item until the queue is empty and flushing.
9595
* If the handler throws an error, it will be caught and logged as 'Queue handler exception:', but the iteration will continue.
96-
* The process function returns a promise that resolves when there are no more items in the queue or the queue is flushing.
96+
* The process function returns a promise that resolves when there are no more items in the queue and the queue is flushing.
9797
*
9898
* @param handler - A function that takes an item of type T and returns a Promise<void> after processing the item.
9999
* @returns A Promise<void> that resolves when the queue is finished processing.

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

+42-21
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ import {
1818
PublicCallRequest,
1919
} from '@aztec/circuits.js';
2020
import { computeCommitmentNonce, siloNullifier } from '@aztec/circuits.js/abis';
21-
import { encodeArguments } from '@aztec/foundation/abi';
21+
import { DecodedReturn, encodeArguments } from '@aztec/foundation/abi';
2222
import { padArrayEnd } from '@aztec/foundation/collection';
2323
import { Fr } from '@aztec/foundation/fields';
24+
import { SerialQueue } from '@aztec/foundation/fifo';
2425
import { DebugLogger, createDebugLogger } from '@aztec/foundation/log';
2526
import { NoirWasmVersion } from '@aztec/noir-compiler/versions';
2627
import {
@@ -70,6 +71,9 @@ export class PXEService implements PXE {
7071
private simulator: AcirSimulator;
7172
private log: DebugLogger;
7273
private sandboxVersion: string;
74+
// serialize synchronizer and calls to simulateTx.
75+
// ensures that state is not changed while simulating
76+
private jobQueue = new SerialQueue();
7377

7478
constructor(
7579
private keyStore: KeyStore,
@@ -79,7 +83,7 @@ export class PXEService implements PXE {
7983
logSuffix?: string,
8084
) {
8185
this.log = createDebugLogger(logSuffix ? `aztec:pxe_service_${logSuffix}` : `aztec:pxe_service`);
82-
this.synchronizer = new Synchronizer(node, db, logSuffix);
86+
this.synchronizer = new Synchronizer(node, db, this.jobQueue, logSuffix);
8387
this.contractDataOracle = new ContractDataOracle(db, node);
8488
this.simulator = getAcirSimulator(db, node, keyStore, this.contractDataOracle);
8589

@@ -93,7 +97,11 @@ export class PXEService implements PXE {
9397
*/
9498
public async start() {
9599
const { l2BlockPollingIntervalMS } = this.config;
96-
await this.synchronizer.start(1, l2BlockPollingIntervalMS);
100+
this.synchronizer.start(1, l2BlockPollingIntervalMS);
101+
this.jobQueue.start();
102+
this.log.info('Started Job Queue');
103+
await this.jobQueue.syncPoint();
104+
this.log.info('Synced Job Queue');
97105
await this.restoreNoteProcessors();
98106
const info = await this.getNodeInfo();
99107
this.log.info(`Started PXE connected to chain ${info.chainId} version ${info.protocolVersion}`);
@@ -121,8 +129,10 @@ export class PXEService implements PXE {
121129
* @returns A Promise resolving once the server has been stopped successfully.
122130
*/
123131
public async stop() {
132+
await this.jobQueue.cancel();
133+
this.log.info('Cancelled Job Queue');
124134
await this.synchronizer.stop();
125-
this.log.info('Stopped');
135+
this.log.info('Stopped Synchronizer');
126136
}
127137

128138
/** Returns an estimate of the db size in bytes. */
@@ -336,18 +346,21 @@ export class PXEService implements PXE {
336346
throw new Error(`Unspecified internal are not allowed`);
337347
}
338348

339-
// We get the contract address from origin, since contract deployments are signalled as origin from their own address
340-
// TODO: Is this ok? Should it be changed to be from ZERO?
341-
const deployedContractAddress = txRequest.txContext.isContractDeploymentTx ? txRequest.origin : undefined;
342-
const newContract = deployedContractAddress ? await this.db.getContract(deployedContractAddress) : undefined;
349+
// all simulations must be serialized w.r.t. the synchronizer
350+
return await this.jobQueue.put(async () => {
351+
// We get the contract address from origin, since contract deployments are signalled as origin from their own address
352+
// TODO: Is this ok? Should it be changed to be from ZERO?
353+
const deployedContractAddress = txRequest.txContext.isContractDeploymentTx ? txRequest.origin : undefined;
354+
const newContract = deployedContractAddress ? await this.db.getContract(deployedContractAddress) : undefined;
343355

344-
const tx = await this.#simulateAndProve(txRequest, newContract);
345-
if (simulatePublic) {
346-
await this.#simulatePublicCalls(tx);
347-
}
348-
this.log.info(`Executed local simulation for ${await tx.getTxHash()}`);
356+
const tx = await this.#simulateAndProve(txRequest, newContract);
357+
if (simulatePublic) {
358+
await this.#simulatePublicCalls(tx);
359+
}
360+
this.log.info(`Executed local simulation for ${await tx.getTxHash()}`);
349361

350-
return tx;
362+
return tx;
363+
});
351364
}
352365

353366
public async sendTx(tx: Tx): Promise<TxHash> {
@@ -360,13 +373,21 @@ export class PXEService implements PXE {
360373
return txHash;
361374
}
362375

363-
public async viewTx(functionName: string, args: any[], to: AztecAddress, _from?: AztecAddress) {
364-
// TODO - Should check if `from` has the permission to call the view function.
365-
const functionCall = await this.#getFunctionCall(functionName, args, to);
366-
const executionResult = await this.#simulateUnconstrained(functionCall);
367-
368-
// TODO - Return typed result based on the function artifact.
369-
return executionResult;
376+
public async viewTx(
377+
functionName: string,
378+
args: any[],
379+
to: AztecAddress,
380+
_from?: AztecAddress,
381+
): Promise<DecodedReturn> {
382+
// all simulations must be serialized w.r.t. the synchronizer
383+
return await this.jobQueue.put(async () => {
384+
// TODO - Should check if `from` has the permission to call the view function.
385+
const functionCall = await this.#getFunctionCall(functionName, args, to);
386+
const executionResult = await this.#simulateUnconstrained(functionCall);
387+
388+
// TODO - Return typed result based on the function artifact.
389+
return executionResult;
390+
});
370391
}
371392

372393
public async getTxReceipt(txHash: TxHash): Promise<TxReceipt> {

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { BlockHeader, CompleteAddress, EthAddress, Fr, GrumpkinScalar } from '@aztec/circuits.js';
22
import { Grumpkin } from '@aztec/circuits.js/barretenberg';
3+
import { SerialQueue } from '@aztec/foundation/fifo';
34
import { TestKeyStore } from '@aztec/key-store';
45
import { AztecLmdbStore } from '@aztec/kv-store';
56
import { AztecNode, INITIAL_L2_BLOCK_NUM, L2Block, MerkleTreeId } from '@aztec/types';
@@ -17,6 +18,7 @@ describe('Synchronizer', () => {
1718
let synchronizer: TestSynchronizer;
1819
let roots: Record<MerkleTreeId, Fr>;
1920
let blockHeader: BlockHeader;
21+
let jobQueue: SerialQueue;
2022

2123
beforeEach(async () => {
2224
blockHeader = BlockHeader.random();
@@ -31,7 +33,8 @@ describe('Synchronizer', () => {
3133

3234
aztecNode = mock<AztecNode>();
3335
database = new KVPxeDatabase(await AztecLmdbStore.create(EthAddress.random()));
34-
synchronizer = new TestSynchronizer(aztecNode, database);
36+
jobQueue = new SerialQueue();
37+
synchronizer = new TestSynchronizer(aztecNode, database, jobQueue);
3538
});
3639

3740
it('sets tree roots from aztec node on initial sync', async () => {
@@ -128,7 +131,7 @@ class TestSynchronizer extends Synchronizer {
128131
return super.initialSync();
129132
}
130133

131-
public workNoteProcessorCatchUp(): Promise<void> {
134+
public workNoteProcessorCatchUp(): Promise<boolean> {
132135
return super.workNoteProcessorCatchUp();
133136
}
134137
}

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

+47-21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { AztecAddress, BlockHeader, Fr, PublicKey } from '@aztec/circuits.js';
22
import { computeGlobalsHash } from '@aztec/circuits.js/abis';
3+
import { SerialQueue } from '@aztec/foundation/fifo';
34
import { DebugLogger, createDebugLogger } from '@aztec/foundation/log';
45
import { InterruptibleSleep } from '@aztec/foundation/sleep';
56
import { AztecNode, INITIAL_L2_BLOCK_NUM, KeyStore, L2BlockContext, L2BlockL2Logs, LogType } from '@aztec/types';
@@ -24,7 +25,7 @@ export class Synchronizer {
2425
private log: DebugLogger;
2526
private noteProcessorsToCatchUp: NoteProcessor[] = [];
2627

27-
constructor(private node: AztecNode, private db: PxeDatabase, logSuffix = '') {
28+
constructor(private node: AztecNode, private db: PxeDatabase, private jobQueue: SerialQueue, logSuffix = '') {
2829
this.log = createDebugLogger(logSuffix ? `aztec:pxe_synchronizer_${logSuffix}` : 'aztec:pxe_synchronizer');
2930
}
3031

@@ -36,23 +37,35 @@ export class Synchronizer {
3637
* @param limit - The maximum number of encrypted, unencrypted logs and blocks to fetch in each iteration.
3738
* @param retryInterval - The time interval (in ms) to wait before retrying if no data is available.
3839
*/
39-
public async start(limit = 1, retryInterval = 1000) {
40+
public start(limit = 1, retryInterval = 1000) {
4041
if (this.running) {
4142
return;
4243
}
4344
this.running = true;
4445

45-
await this.initialSync();
46+
this.jobQueue
47+
.put(() => this.initialSync())
48+
.catch(err => {
49+
this.log.error(`Error in synchronizer initial sync`, err);
50+
this.running = false;
51+
throw err;
52+
});
4653

4754
const run = async () => {
4855
while (this.running) {
49-
if (this.noteProcessorsToCatchUp.length > 0) {
50-
// There is a note processor that needs to catch up. We hijack the main loop to catch up the note processor.
51-
await this.workNoteProcessorCatchUp(limit, retryInterval);
52-
} else {
53-
// No note processor needs to catch up. We continue with the normal flow.
54-
await this.work(limit, retryInterval);
55-
}
56+
await this.jobQueue.put(async () => {
57+
let moreWork = true;
58+
while (moreWork && this.running) {
59+
if (this.noteProcessorsToCatchUp.length > 0) {
60+
// There is a note processor that needs to catch up. We hijack the main loop to catch up the note processor.
61+
moreWork = await this.workNoteProcessorCatchUp(limit);
62+
} else {
63+
// No note processor needs to catch up. We continue with the normal flow.
64+
moreWork = await this.work(limit);
65+
}
66+
}
67+
});
68+
await this.interruptibleSleep.sleep(retryInterval);
5669
}
5770
};
5871

@@ -70,26 +83,29 @@ export class Synchronizer {
7083
await this.db.setBlockData(latestBlockNumber, latestBlockHeader);
7184
}
7285

73-
protected async work(limit = 1, retryInterval = 1000): Promise<void> {
86+
/**
87+
* Fetches encrypted logs and blocks from the Aztec node and processes them for all note processors.
88+
*
89+
* @param limit - The maximum number of encrypted, unencrypted logs and blocks to fetch in each iteration.
90+
* @returns true if there could be more work, false if we're caught up or there was an error.
91+
*/
92+
protected async work(limit = 1): Promise<boolean> {
7493
const from = this.getSynchedBlockNumber() + 1;
7594
try {
7695
let encryptedLogs = await this.node.getLogs(from, limit, LogType.ENCRYPTED);
7796
if (!encryptedLogs.length) {
78-
await this.interruptibleSleep.sleep(retryInterval);
79-
return;
97+
return false;
8098
}
8199

82100
let unencryptedLogs = await this.node.getLogs(from, limit, LogType.UNENCRYPTED);
83101
if (!unencryptedLogs.length) {
84-
await this.interruptibleSleep.sleep(retryInterval);
85-
return;
102+
return false;
86103
}
87104

88105
// Note: If less than `limit` encrypted logs is returned, then we fetch only that number of blocks.
89106
const blocks = await this.node.getBlocks(from, encryptedLogs.length);
90107
if (!blocks.length) {
91-
await this.interruptibleSleep.sleep(retryInterval);
92-
return;
108+
return false;
93109
}
94110

95111
if (blocks.length !== encryptedLogs.length) {
@@ -120,21 +136,30 @@ export class Synchronizer {
120136
for (const noteProcessor of this.noteProcessors) {
121137
await noteProcessor.process(blockContexts, encryptedLogs);
122138
}
139+
return true;
123140
} catch (err) {
124141
this.log.error(`Error in synchronizer work`, err);
125-
await this.interruptibleSleep.sleep(retryInterval);
142+
return false;
126143
}
127144
}
128145

129-
protected async workNoteProcessorCatchUp(limit = 1, retryInterval = 1000): Promise<void> {
146+
/**
147+
* Catch up a note processor that is lagging behind the main sync,
148+
* e.g. because we just added a new account.
149+
*
150+
* @param limit - the maximum number of encrypted, unencrypted logs and blocks to fetch in each iteration.
151+
* @returns true if there could be more work, false if we're caught up or there was an error.
152+
*/
153+
protected async workNoteProcessorCatchUp(limit = 1): Promise<boolean> {
130154
const noteProcessor = this.noteProcessorsToCatchUp[0];
131155
const toBlockNumber = this.getSynchedBlockNumber();
132156

133157
if (noteProcessor.status.syncedToBlock >= toBlockNumber) {
134158
// Note processor already synched, nothing to do
135159
this.noteProcessorsToCatchUp.shift();
136160
this.noteProcessors.push(noteProcessor);
137-
return;
161+
// could be more work if there are more note processors to catch up
162+
return true;
138163
}
139164

140165
const from = noteProcessor.status.syncedToBlock + 1;
@@ -184,9 +209,10 @@ export class Synchronizer {
184209
this.noteProcessorsToCatchUp.shift();
185210
this.noteProcessors.push(noteProcessor);
186211
}
212+
return true;
187213
} catch (err) {
188214
this.log.error(`Error in synchronizer workNoteProcessorCatchUp`, err);
189-
await this.interruptibleSleep.sleep(retryInterval);
215+
return false;
190216
}
191217
}
192218

0 commit comments

Comments
 (0)