@aztec/pxe 5.2.0 → 5.3.0-nightly.20260818
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/config/index.d.ts +15 -2
- package/dest/config/index.d.ts.map +1 -1
- package/dest/config/index.js +5 -0
- package/dest/config/package_info.js +1 -1
- package/dest/contract/contract_call_graph.d.ts +52 -0
- package/dest/contract/contract_call_graph.d.ts.map +1 -0
- package/dest/contract/contract_call_graph.js +91 -0
- package/dest/contract/contract_sync_service.d.ts +47 -12
- package/dest/contract/contract_sync_service.d.ts.map +1 -1
- package/dest/contract/contract_sync_service.js +125 -31
- package/dest/contract_function_simulator/oracle/private_execution_oracle.d.ts +1 -1
- package/dest/contract_function_simulator/oracle/private_execution_oracle.d.ts.map +1 -1
- package/dest/contract_function_simulator/oracle/private_execution_oracle.js +12 -1
- package/dest/contract_function_simulator/oracle/utility_execution_oracle.d.ts +1 -1
- package/dest/contract_function_simulator/oracle/utility_execution_oracle.d.ts.map +1 -1
- package/dest/contract_function_simulator/oracle/utility_execution_oracle.js +12 -1
- package/dest/debug/pxe_debug_utils.d.ts +1 -1
- package/dest/debug/pxe_debug_utils.d.ts.map +1 -1
- package/dest/debug/pxe_debug_utils.js +9 -1
- package/dest/job_coordinator/job_coordinator.d.ts +10 -1
- package/dest/job_coordinator/job_coordinator.d.ts.map +1 -1
- package/dest/job_coordinator/job_coordinator.js +19 -0
- package/dest/pxe.d.ts +1 -1
- package/dest/pxe.d.ts.map +1 -1
- package/dest/pxe.js +28 -4
- package/package.json +17 -17
- package/src/config/index.ts +25 -1
- package/src/config/package_info.ts +1 -1
- package/src/contract/contract_call_graph.ts +113 -0
- package/src/contract/contract_sync_service.ts +221 -58
- package/src/contract_function_simulator/oracle/private_execution_oracle.ts +9 -8
- package/src/contract_function_simulator/oracle/utility_execution_oracle.ts +9 -8
- package/src/debug/pxe_debug_utils.ts +7 -6
- package/src/job_coordinator/job_coordinator.ts +30 -0
- package/src/pxe.ts +20 -16
|
@@ -2,15 +2,17 @@ import type { Logger } from '@aztec/foundation/log';
|
|
|
2
2
|
import { allToCompletion } from '@aztec/foundation/promise';
|
|
3
3
|
import { Semaphore } from '@aztec/foundation/queue';
|
|
4
4
|
import { isProtocolContract } from '@aztec/protocol-contracts';
|
|
5
|
-
import type
|
|
5
|
+
import { type FunctionCall, FunctionSelector } from '@aztec/stdlib/abi';
|
|
6
6
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
7
7
|
import type { AztecNode } from '@aztec/stdlib/interfaces/client';
|
|
8
8
|
import type { BlockHeader } from '@aztec/stdlib/tx';
|
|
9
9
|
|
|
10
|
+
import type { ContractSyncConfig } from '../config/index.js';
|
|
10
11
|
import type { StagedStore } from '../job_coordinator/job_coordinator.js';
|
|
11
12
|
import { NoteService } from '../notes/note_service.js';
|
|
12
13
|
import type { ContractStore } from '../storage/contract_store/contract_store.js';
|
|
13
14
|
import type { NoteStore } from '../storage/note_store/note_store.js';
|
|
15
|
+
import { type CallKey, ContractCallGraph, type ContractFunction, toCallKey } from './contract_call_graph.js';
|
|
14
16
|
import type { ContractClassService } from './contract_class_service.js';
|
|
15
17
|
import { syncScope } from './helpers.js';
|
|
16
18
|
|
|
@@ -20,6 +22,12 @@ import { syncScope } from './helpers.js';
|
|
|
20
22
|
*/
|
|
21
23
|
export const MAX_CONCURRENT_SCOPE_SYNCS = 5;
|
|
22
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Selector of the macro-generated `sync_state` utility function, which is the same for every contract.
|
|
27
|
+
* Pinned against a compiled artifact in tests, so a macro signature change fails there.
|
|
28
|
+
*/
|
|
29
|
+
export const SYNC_STATE_SELECTOR = FunctionSelector.fromString('0x418ef5da');
|
|
30
|
+
|
|
23
31
|
/**
|
|
24
32
|
* Service for syncing the private state of contracts. It uses a cache to avoid redundant sync operations - the cache
|
|
25
33
|
* is wiped when the anchor block changes.
|
|
@@ -30,9 +38,14 @@ export class ContractSyncService implements StagedStore {
|
|
|
30
38
|
readonly storeName = 'contract_sync';
|
|
31
39
|
|
|
32
40
|
// Tracks contracts synced since last wipe. The cache is keyed per individual scope address
|
|
33
|
-
// (`contractAddress:scopeAddress`)
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
// (`contractAddress:scopeAddress`). The value is a promise that resolves when the contract is synced.
|
|
42
|
+
private readonly syncedContracts: Map<SyncKey, Promise<void>> = new Map();
|
|
43
|
+
|
|
44
|
+
// Per-job speculation state, dropped when the job commits or discards.
|
|
45
|
+
private readonly speculationByJob: Map<JobId, JobSpeculation> = new Map();
|
|
46
|
+
|
|
47
|
+
// Predicts a function's callees from the calls observed in past jobs, driving speculative sync.
|
|
48
|
+
private readonly callGraph: ContractCallGraph;
|
|
36
49
|
|
|
37
50
|
constructor(
|
|
38
51
|
private aztecNode: AztecNode,
|
|
@@ -40,38 +53,63 @@ export class ContractSyncService implements StagedStore {
|
|
|
40
53
|
private contractClassService: ContractClassService,
|
|
41
54
|
private noteStore: NoteStore,
|
|
42
55
|
private log: Logger,
|
|
43
|
-
|
|
56
|
+
{ concurrentContractSyncEnabled }: ContractSyncConfig,
|
|
57
|
+
) {
|
|
58
|
+
this.callGraph = new ContractCallGraph(concurrentContractSyncEnabled);
|
|
59
|
+
}
|
|
44
60
|
|
|
45
61
|
/**
|
|
46
62
|
* Ensures a contract's private state is synchronized.
|
|
47
63
|
* Uses a cache to avoid redundant sync operations - the cache is wiped when the anchor block changes.
|
|
48
|
-
* @param contractAddress - The address of the contract to sync.
|
|
49
|
-
* @param functionToInvokeAfterSync - The function selector that will be called after sync (used to validate it's
|
|
50
|
-
* not sync_state itself).
|
|
51
|
-
* @param utilityExecutor - Executor function for running the sync_state utility function.
|
|
52
|
-
* @param scopes - Access scopes to pass through to the utility executor (affects whose account's private state is discovered).
|
|
53
64
|
*/
|
|
54
|
-
async ensureContractSynced(
|
|
55
|
-
|
|
56
|
-
functionToInvokeAfterSync
|
|
57
|
-
utilityExecutor
|
|
58
|
-
anchorBlockHeader
|
|
59
|
-
jobId
|
|
60
|
-
scopes
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
functionToInvokeAfterSync,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
async ensureContractSynced({
|
|
66
|
+
contract,
|
|
67
|
+
functionToInvokeAfterSync,
|
|
68
|
+
utilityExecutor,
|
|
69
|
+
anchorBlockHeader,
|
|
70
|
+
jobId,
|
|
71
|
+
scopes,
|
|
72
|
+
triggeredBy,
|
|
73
|
+
}: ContractSyncRequest): Promise<void> {
|
|
74
|
+
// A call is recorded only when both functions are known: the invoked callee and the caller that triggered it.
|
|
75
|
+
if (functionToInvokeAfterSync && triggeredBy) {
|
|
76
|
+
this.callGraph.recordCall({
|
|
77
|
+
jobId,
|
|
78
|
+
caller: triggeredBy,
|
|
79
|
+
callee: { address: contract, selector: functionToInvokeAfterSync },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await this.#startSyncIfNeeded(
|
|
84
|
+
contract,
|
|
85
|
+
functionToInvokeAfterSync,
|
|
86
|
+
utilityExecutor,
|
|
87
|
+
anchorBlockHeader,
|
|
88
|
+
jobId,
|
|
89
|
+
scopes,
|
|
72
90
|
);
|
|
91
|
+
}
|
|
73
92
|
|
|
74
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Waits until every speculative sync the job fired has finished, then rejects if any failed, so the job discards
|
|
95
|
+
* instead of committing. This is needed because a sync that fails midway can leave partial staged writes, and a
|
|
96
|
+
* speculative failure might not be surfaced by any request.
|
|
97
|
+
*/
|
|
98
|
+
async settle(jobId: JobId): Promise<void> {
|
|
99
|
+
// A speculative sync's execution can fire more speculative syncs mid-drain, so loop until no new promises
|
|
100
|
+
// appear, and only escalate once nothing is still writing.
|
|
101
|
+
const { syncs } = this.#speculationForJob(jobId);
|
|
102
|
+
const failures: unknown[] = [];
|
|
103
|
+
while (syncs.length > 0) {
|
|
104
|
+
const results = await Promise.allSettled(syncs.splice(0));
|
|
105
|
+
failures.push(...results.filter(result => result.status === 'rejected').map(result => result.reason));
|
|
106
|
+
}
|
|
107
|
+
if (failures.length > 0) {
|
|
108
|
+
throw new AggregateError(
|
|
109
|
+
failures,
|
|
110
|
+
'Speculative syncs failed, so the job must discard its staged writes instead of committing',
|
|
111
|
+
);
|
|
112
|
+
}
|
|
75
113
|
}
|
|
76
114
|
|
|
77
115
|
/** Clears sync cache entries for the given scopes of a contract. */
|
|
@@ -88,14 +126,18 @@ export class ContractSyncService implements StagedStore {
|
|
|
88
126
|
this.syncedContracts.clear();
|
|
89
127
|
}
|
|
90
128
|
|
|
91
|
-
commit(
|
|
129
|
+
commit(jobId: JobId): Promise<void> {
|
|
130
|
+
this.callGraph.commitJob(jobId);
|
|
131
|
+
this.speculationByJob.delete(jobId);
|
|
92
132
|
return Promise.resolve();
|
|
93
133
|
}
|
|
94
134
|
|
|
95
|
-
discardStaged(
|
|
135
|
+
discardStaged(jobId: JobId): Promise<void> {
|
|
96
136
|
// We clear the synced contracts cache here because, when the job is discarded, any associated database writes from
|
|
97
137
|
// the sync are also undone.
|
|
98
138
|
this.syncedContracts.clear();
|
|
139
|
+
this.callGraph.discardJob(jobId);
|
|
140
|
+
this.speculationByJob.delete(jobId);
|
|
99
141
|
return Promise.resolve();
|
|
100
142
|
}
|
|
101
143
|
|
|
@@ -103,38 +145,113 @@ export class ContractSyncService implements StagedStore {
|
|
|
103
145
|
* For each unsynced scope, creates a promise that waits on:
|
|
104
146
|
* 1. Note nullifier sync (shared, batched across all unsynced scopes).
|
|
105
147
|
* 2. Per-scope sync (individual, semaphore-bounded).
|
|
148
|
+
* When concurrent contract sync is enabled, the predicted direct callees of the invoked function and of the
|
|
149
|
+
* contract's `sync_state` start speculatively too, once the contract's own syncs have started (see
|
|
150
|
+
* {@link #speculativelySync}).
|
|
151
|
+
* @returns A promise that resolves once every requested scope is synced, including syncs already in flight from
|
|
152
|
+
* concurrent calls. Speculative syncs are not included: those are only awaited by a later request that needs
|
|
153
|
+
* their contract, or by the job's {@link settle}.
|
|
106
154
|
*/
|
|
107
|
-
#startSyncIfNeeded(
|
|
155
|
+
async #startSyncIfNeeded(
|
|
108
156
|
contractAddress: AztecAddress,
|
|
157
|
+
functionToInvokeAfterSync: FunctionSelector | null,
|
|
158
|
+
utilityExecutor: (call: FunctionCall, scopes: AztecAddress[]) => Promise<any>,
|
|
159
|
+
anchorBlockHeader: BlockHeader,
|
|
160
|
+
jobId: JobId,
|
|
109
161
|
scopes: AztecAddress[],
|
|
162
|
+
): Promise<void> {
|
|
163
|
+
const scopesToSync = scopes.filter(scope => !this.syncedContracts.has(toKey(contractAddress, scope)));
|
|
164
|
+
if (scopesToSync.length > 0) {
|
|
165
|
+
this.log.debug(`Syncing contract ${contractAddress} for ${scopesToSync.length} scope(s)`);
|
|
166
|
+
|
|
167
|
+
const syncNullifiersPromise = this.#syncNoteNullifiers(contractAddress, anchorBlockHeader, jobId, scopesToSync);
|
|
168
|
+
|
|
169
|
+
// We build a new semaphore for each sync call, so it rate-limits the scopes within that single call. We do
|
|
170
|
+
// this so that if these scope syncs trigger nested syncs, the nested ones can execute without causing a deadlock.
|
|
171
|
+
const syncSlot = new Semaphore(MAX_CONCURRENT_SCOPE_SYNCS);
|
|
172
|
+
|
|
173
|
+
for (const scope of scopesToSync) {
|
|
174
|
+
const key = toKey(contractAddress, scope);
|
|
175
|
+
const syncScopePromise = runBounded(syncSlot, () =>
|
|
176
|
+
syncScope(
|
|
177
|
+
contractAddress,
|
|
178
|
+
this.contractStore,
|
|
179
|
+
this.contractClassService,
|
|
180
|
+
anchorBlockHeader,
|
|
181
|
+
functionToInvokeAfterSync,
|
|
182
|
+
utilityExecutor,
|
|
183
|
+
scope,
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
// This cached promise is what every later request for this scope awaits, and both branches write staged data,
|
|
187
|
+
// so it must run both to completion even when one fails.
|
|
188
|
+
const promise = allToCompletion([syncNullifiersPromise, syncScopePromise])
|
|
189
|
+
.then(() => {})
|
|
190
|
+
.catch(err => {
|
|
191
|
+
this.syncedContracts.delete(key);
|
|
192
|
+
throw err;
|
|
193
|
+
});
|
|
194
|
+
this.syncedContracts.set(key, promise);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// `sync_state` itself calls other contracts (e.g. most contract syncs query the handshake registry), so its
|
|
199
|
+
// predicted callees start syncing alongside the contract's own syncs.
|
|
200
|
+
this.#speculativelySync(contractAddress, SYNC_STATE_SELECTOR, utilityExecutor, anchorBlockHeader, jobId, scopes);
|
|
201
|
+
this.#speculativelySync(
|
|
202
|
+
contractAddress,
|
|
203
|
+
functionToInvokeAfterSync,
|
|
204
|
+
utilityExecutor,
|
|
205
|
+
anchorBlockHeader,
|
|
206
|
+
jobId,
|
|
207
|
+
scopes,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
await this.#awaitSync(contractAddress, scopes);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Starts the syncs of the contracts the given function is predicted to call (see {@link ContractCallGraph} for how
|
|
215
|
+
* predictions are learned). Each started sync speculates from its own function in turn, so the whole predicted call
|
|
216
|
+
* tree syncs in parallel with the contract instead of one contract at a time as execution reaches it.
|
|
217
|
+
*
|
|
218
|
+
* A wrong prediction is cheap: the extra node requests are batched into round trips the job already makes, and the
|
|
219
|
+
* synced data simply goes unused.
|
|
220
|
+
*/
|
|
221
|
+
#speculativelySync(
|
|
222
|
+
contractAddress: AztecAddress,
|
|
223
|
+
functionToInvokeAfterSync: FunctionSelector | null,
|
|
224
|
+
utilityExecutor: (call: FunctionCall, scopes: AztecAddress[]) => Promise<any>,
|
|
110
225
|
anchorBlockHeader: BlockHeader,
|
|
111
|
-
jobId:
|
|
112
|
-
|
|
226
|
+
jobId: JobId,
|
|
227
|
+
scopes: AztecAddress[],
|
|
113
228
|
): void {
|
|
114
|
-
|
|
115
|
-
if (
|
|
229
|
+
// Without a function there is no key to predict from (the request is a direct read).
|
|
230
|
+
if (!functionToInvokeAfterSync) {
|
|
116
231
|
return;
|
|
117
232
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
233
|
+
const speculation = this.#speculationForJob(jobId);
|
|
234
|
+
const caller: ContractFunction = { address: contractAddress, selector: functionToInvokeAfterSync };
|
|
235
|
+
for (const callee of this.callGraph.predictDirectCallees(caller)) {
|
|
236
|
+
// The job's set of already-speculated functions stops the recursion when the predicted graph has a cycle.
|
|
237
|
+
if (speculation.speculated.has(toCallKey(callee))) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
speculation.speculated.add(toCallKey(callee));
|
|
241
|
+
const syncPromise = this.#startSyncIfNeeded(
|
|
242
|
+
callee.address,
|
|
243
|
+
callee.selector,
|
|
244
|
+
utilityExecutor,
|
|
245
|
+
anchorBlockHeader,
|
|
246
|
+
jobId,
|
|
247
|
+
scopes,
|
|
248
|
+
);
|
|
249
|
+
speculation.syncs.push(syncPromise);
|
|
250
|
+
// `settle` only escalates these failures at the end of the job: catch here so one does not become an unhandled
|
|
251
|
+
// rejection before then, and log it.
|
|
252
|
+
syncPromise.catch(err => {
|
|
253
|
+
this.log.warn(`Speculative sync of ${callee.address} failed`, { jobId, error: err?.message });
|
|
254
|
+
});
|
|
138
255
|
}
|
|
139
256
|
}
|
|
140
257
|
|
|
@@ -142,7 +259,7 @@ export class ContractSyncService implements StagedStore {
|
|
|
142
259
|
async #syncNoteNullifiers(
|
|
143
260
|
contractAddress: AztecAddress,
|
|
144
261
|
anchorBlockHeader: BlockHeader,
|
|
145
|
-
jobId:
|
|
262
|
+
jobId: JobId,
|
|
146
263
|
scopes: AztecAddress[],
|
|
147
264
|
): Promise<void> {
|
|
148
265
|
// Protocol contracts don't have private state to sync
|
|
@@ -155,6 +272,15 @@ export class ContractSyncService implements StagedStore {
|
|
|
155
272
|
await noteService.syncNoteNullifiers(contractAddress, scopes);
|
|
156
273
|
}
|
|
157
274
|
|
|
275
|
+
#speculationForJob(jobId: JobId): JobSpeculation {
|
|
276
|
+
let speculation = this.speculationByJob.get(jobId);
|
|
277
|
+
if (!speculation) {
|
|
278
|
+
speculation = { speculated: new Set(), syncs: [] };
|
|
279
|
+
this.speculationByJob.set(jobId, speculation);
|
|
280
|
+
}
|
|
281
|
+
return speculation;
|
|
282
|
+
}
|
|
283
|
+
|
|
158
284
|
/** Collects all relevant scope promises (including in-flight ones from concurrent calls) and awaits them. */
|
|
159
285
|
async #awaitSync(contractAddress: AztecAddress, scopes: AztecAddress[]): Promise<void> {
|
|
160
286
|
const promises = scopes
|
|
@@ -164,7 +290,44 @@ export class ContractSyncService implements StagedStore {
|
|
|
164
290
|
}
|
|
165
291
|
}
|
|
166
292
|
|
|
167
|
-
|
|
293
|
+
/** A request to synchronize a contract's private state. */
|
|
294
|
+
type ContractSyncRequest = {
|
|
295
|
+
/** The contract to sync. */
|
|
296
|
+
contract: AztecAddress;
|
|
297
|
+
/**
|
|
298
|
+
* The function that will be invoked after the sync, or null when nothing will be invoked (e.g. reading
|
|
299
|
+
* notes/events directly).
|
|
300
|
+
*/
|
|
301
|
+
functionToInvokeAfterSync: FunctionSelector | null;
|
|
302
|
+
/** Executes a utility function call under the given scopes. Syncs run each contract's sync_state through it. */
|
|
303
|
+
utilityExecutor: (call: FunctionCall, scopes: AztecAddress[]) => Promise<any>;
|
|
304
|
+
/** The anchor block to sync at. */
|
|
305
|
+
anchorBlockHeader: BlockHeader;
|
|
306
|
+
/** The job requesting the sync. */
|
|
307
|
+
jobId: JobId;
|
|
308
|
+
/** Access scopes to pass through to the utility executor (affects whose account's private state is discovered). */
|
|
309
|
+
scopes: AztecAddress[];
|
|
310
|
+
/**
|
|
311
|
+
* The function whose execution triggered this sync request, or undefined when the request is a job's top-level use
|
|
312
|
+
* (an entry call or a direct read) rather than a nested call.
|
|
313
|
+
*/
|
|
314
|
+
triggeredBy: ContractFunction | undefined;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
type JobId = string;
|
|
318
|
+
|
|
319
|
+
/** A job's speculation state. */
|
|
320
|
+
type JobSpeculation = {
|
|
321
|
+
/** Functions prediction already ran for, so the recursion stops on cycles in the predicted graph. */
|
|
322
|
+
speculated: Set<CallKey>;
|
|
323
|
+
/** Every sync fired by prediction, awaited by {@link settle} before the job commits or discards. */
|
|
324
|
+
syncs: Promise<void>[];
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/** Key of a contract's sync cache entry for a single scope: `contractAddress:scopeAddress`. */
|
|
328
|
+
type SyncKey = `0x${string}:0x${string}`;
|
|
329
|
+
|
|
330
|
+
function toKey(contract: AztecAddress, scope: AztecAddress): SyncKey {
|
|
168
331
|
return `${contract.toString()}:${scope.toString()}`;
|
|
169
332
|
}
|
|
170
333
|
|
|
@@ -661,14 +661,15 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP
|
|
|
661
661
|
|
|
662
662
|
isStaticCall = isStaticCall || this.callContext.isStaticCall;
|
|
663
663
|
|
|
664
|
-
await this.contractSyncService.ensureContractSynced(
|
|
665
|
-
targetContractAddress,
|
|
666
|
-
functionSelector,
|
|
667
|
-
this.utilityExecutor,
|
|
668
|
-
this.anchorBlockHeader,
|
|
669
|
-
this.jobId,
|
|
670
|
-
this.scopes,
|
|
671
|
-
|
|
664
|
+
await this.contractSyncService.ensureContractSynced({
|
|
665
|
+
contract: targetContractAddress,
|
|
666
|
+
functionToInvokeAfterSync: functionSelector,
|
|
667
|
+
utilityExecutor: this.utilityExecutor,
|
|
668
|
+
anchorBlockHeader: this.anchorBlockHeader,
|
|
669
|
+
jobId: this.jobId,
|
|
670
|
+
scopes: this.scopes,
|
|
671
|
+
triggeredBy: { address: this.callContext.contractAddress, selector: this.callContext.functionSelector },
|
|
672
|
+
});
|
|
672
673
|
|
|
673
674
|
const targetArtifact = await this.anchoredContractData.getFunctionArtifactWithDebugMetadata(
|
|
674
675
|
targetContractAddress,
|
|
@@ -1056,14 +1056,15 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra
|
|
|
1056
1056
|
}
|
|
1057
1057
|
}
|
|
1058
1058
|
|
|
1059
|
-
await this.contractSyncService.ensureContractSynced(
|
|
1060
|
-
targetContractAddress,
|
|
1061
|
-
functionSelector,
|
|
1062
|
-
this.utilityExecutor,
|
|
1063
|
-
this.anchorBlockHeader,
|
|
1064
|
-
this.jobId,
|
|
1065
|
-
this.scopes,
|
|
1066
|
-
|
|
1059
|
+
await this.contractSyncService.ensureContractSynced({
|
|
1060
|
+
contract: targetContractAddress,
|
|
1061
|
+
functionToInvokeAfterSync: functionSelector,
|
|
1062
|
+
utilityExecutor: this.utilityExecutor,
|
|
1063
|
+
anchorBlockHeader: this.anchorBlockHeader,
|
|
1064
|
+
jobId: this.jobId,
|
|
1065
|
+
scopes: this.scopes,
|
|
1066
|
+
triggeredBy: { address: this.contractAddress, selector: this.callContext.functionSelector },
|
|
1067
|
+
});
|
|
1067
1068
|
}
|
|
1068
1069
|
|
|
1069
1070
|
this.logger.debug(
|
|
@@ -69,15 +69,16 @@ export class PXEDebugUtils {
|
|
|
69
69
|
|
|
70
70
|
const contractFunctionSimulator = this.#getSimulatorForTx();
|
|
71
71
|
|
|
72
|
-
await this.contractSyncService.ensureContractSynced(
|
|
73
|
-
filter.contractAddress,
|
|
74
|
-
null,
|
|
75
|
-
async (privateSyncCall, execScopes) =>
|
|
72
|
+
await this.contractSyncService.ensureContractSynced({
|
|
73
|
+
contract: filter.contractAddress,
|
|
74
|
+
functionToInvokeAfterSync: null,
|
|
75
|
+
utilityExecutor: async (privateSyncCall, execScopes) =>
|
|
76
76
|
await this.#executeUtility(contractFunctionSimulator, privateSyncCall, [], execScopes, jobId),
|
|
77
77
|
anchorBlockHeader,
|
|
78
78
|
jobId,
|
|
79
|
-
filter.scopes,
|
|
80
|
-
|
|
79
|
+
scopes: filter.scopes,
|
|
80
|
+
triggeredBy: undefined,
|
|
81
|
+
});
|
|
81
82
|
|
|
82
83
|
return this.noteStore.getNotes(filter, jobId);
|
|
83
84
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from '@aztec/foundation/crypto/random';
|
|
2
2
|
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
|
|
3
|
+
import { allToCompletion } from '@aztec/foundation/promise';
|
|
3
4
|
import type { AztecAsyncKVStore } from '@aztec/kv-store';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -24,6 +25,16 @@ export interface StagedStore {
|
|
|
24
25
|
* @param jobId - The job identifier
|
|
25
26
|
*/
|
|
26
27
|
discardStaged(jobId: string): Promise<void>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A store may have pending work that must finish before the job's staged writes are committed or discarded, yet
|
|
31
|
+
* commits run inside a transaction that cannot wait for it. Such stores implement this method: it is called before
|
|
32
|
+
* every commit and discard, outside the transaction. If settling fails, the commit is cancelled, but a discard
|
|
33
|
+
* proceeds.
|
|
34
|
+
*
|
|
35
|
+
* @param jobId - The job identifier
|
|
36
|
+
*/
|
|
37
|
+
settle?(jobId: string): Promise<void>;
|
|
27
38
|
}
|
|
28
39
|
|
|
29
40
|
/**
|
|
@@ -108,6 +119,9 @@ export class JobCoordinator {
|
|
|
108
119
|
|
|
109
120
|
this.log.debug(`Committing job ${jobId}`);
|
|
110
121
|
|
|
122
|
+
// Settling must stay outside the transaction: it can take arbitrarily long.
|
|
123
|
+
await allToCompletion([...this.#stores.values()].map(store => store.settle?.(jobId)));
|
|
124
|
+
|
|
111
125
|
// Commit all stores atomically in a single transaction.
|
|
112
126
|
// Each store's commit is a no-op if it has no staged data (but that's up to each store to handle).
|
|
113
127
|
await this.kvStore.transactionAsync(async () => {
|
|
@@ -133,6 +147,8 @@ export class JobCoordinator {
|
|
|
133
147
|
|
|
134
148
|
this.log.debug(`Aborting job ${jobId}`);
|
|
135
149
|
|
|
150
|
+
await this.#settleStoresLoggingFailures(jobId);
|
|
151
|
+
|
|
136
152
|
for (const store of this.#stores.values()) {
|
|
137
153
|
await store.discardStaged(jobId);
|
|
138
154
|
}
|
|
@@ -147,4 +163,18 @@ export class JobCoordinator {
|
|
|
147
163
|
hasJobInProgress(): boolean {
|
|
148
164
|
return this.#currentJobId !== undefined;
|
|
149
165
|
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Settles every store, logging failures instead of propagating them. The abort must run to completion no matter what,
|
|
169
|
+
* so a store that fails to settle cannot stop the others from discarding or mask the error that aborted the job.
|
|
170
|
+
*/
|
|
171
|
+
async #settleStoresLoggingFailures(jobId: string): Promise<void> {
|
|
172
|
+
await allToCompletion(
|
|
173
|
+
[...this.#stores.values()].map(store =>
|
|
174
|
+
store.settle?.(jobId).catch(err => {
|
|
175
|
+
this.log.warn(`Store ${store.storeName} failed to settle while aborting job ${jobId}`, { jobId, err });
|
|
176
|
+
}),
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
150
180
|
}
|
package/src/pxe.ts
CHANGED
|
@@ -327,6 +327,7 @@ export class PXE {
|
|
|
327
327
|
contractClassService,
|
|
328
328
|
noteStore,
|
|
329
329
|
createLogger('pxe:contract_sync', bindings),
|
|
330
|
+
config,
|
|
330
331
|
);
|
|
331
332
|
const txResolver = new TxResolverService(readCachedNode);
|
|
332
333
|
|
|
@@ -549,15 +550,16 @@ export class PXE {
|
|
|
549
550
|
const { origin: contractAddress, functionSelector } = txRequest;
|
|
550
551
|
|
|
551
552
|
try {
|
|
552
|
-
await this.contractSyncService.ensureContractSynced(
|
|
553
|
-
contractAddress,
|
|
554
|
-
functionSelector,
|
|
555
|
-
(privateSyncCall, execScopes) =>
|
|
553
|
+
await this.contractSyncService.ensureContractSynced({
|
|
554
|
+
contract: contractAddress,
|
|
555
|
+
functionToInvokeAfterSync: functionSelector,
|
|
556
|
+
utilityExecutor: (privateSyncCall, execScopes) =>
|
|
556
557
|
this.#executeUtility(contractFunctionSimulator, privateSyncCall, [], execScopes, jobId),
|
|
557
558
|
anchorBlockHeader,
|
|
558
559
|
jobId,
|
|
559
560
|
scopes,
|
|
560
|
-
|
|
561
|
+
triggeredBy: undefined,
|
|
562
|
+
});
|
|
561
563
|
|
|
562
564
|
const result = await contractFunctionSimulator.run(txRequest, {
|
|
563
565
|
anchorBlockHeader,
|
|
@@ -1381,15 +1383,16 @@ export class PXE {
|
|
|
1381
1383
|
const contractFunctionSimulator = this.#getSimulatorForTx();
|
|
1382
1384
|
|
|
1383
1385
|
const anchorBlockHeader = await this.anchorBlockStore.getBlockHeader();
|
|
1384
|
-
await this.contractSyncService.ensureContractSynced(
|
|
1385
|
-
call.to,
|
|
1386
|
-
call.selector,
|
|
1387
|
-
(privateSyncCall, execScopes) =>
|
|
1386
|
+
await this.contractSyncService.ensureContractSynced({
|
|
1387
|
+
contract: call.to,
|
|
1388
|
+
functionToInvokeAfterSync: call.selector,
|
|
1389
|
+
utilityExecutor: (privateSyncCall, execScopes) =>
|
|
1388
1390
|
this.#executeUtility(contractFunctionSimulator, privateSyncCall, [], execScopes, jobId),
|
|
1389
1391
|
anchorBlockHeader,
|
|
1390
1392
|
jobId,
|
|
1391
1393
|
scopes,
|
|
1392
|
-
|
|
1394
|
+
triggeredBy: undefined,
|
|
1395
|
+
});
|
|
1393
1396
|
|
|
1394
1397
|
const { result: executionResult, offchainEffects } = await this.#executeUtility(
|
|
1395
1398
|
contractFunctionSimulator,
|
|
@@ -1459,15 +1462,16 @@ export class PXE {
|
|
|
1459
1462
|
|
|
1460
1463
|
const contractFunctionSimulator = this.#getSimulatorForTx();
|
|
1461
1464
|
|
|
1462
|
-
await this.contractSyncService.ensureContractSynced(
|
|
1463
|
-
filter.contractAddress,
|
|
1464
|
-
null,
|
|
1465
|
-
async (privateSyncCall, execScopes) =>
|
|
1465
|
+
await this.contractSyncService.ensureContractSynced({
|
|
1466
|
+
contract: filter.contractAddress,
|
|
1467
|
+
functionToInvokeAfterSync: null,
|
|
1468
|
+
utilityExecutor: async (privateSyncCall, execScopes) =>
|
|
1466
1469
|
await this.#executeUtility(contractFunctionSimulator, privateSyncCall, [], execScopes, jobId),
|
|
1467
1470
|
anchorBlockHeader,
|
|
1468
1471
|
jobId,
|
|
1469
|
-
filter.scopes,
|
|
1470
|
-
|
|
1472
|
+
scopes: filter.scopes,
|
|
1473
|
+
triggeredBy: undefined,
|
|
1474
|
+
});
|
|
1471
1475
|
});
|
|
1472
1476
|
|
|
1473
1477
|
// anchorBlockNumber is set during the job and fixed to whatever it is after a block sync
|