@aztec/aztec-node 5.3.0-nightly.20260821 → 5.3.0-nightly.20260823
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/aztec-node/server.d.ts +2 -1
- package/dest/aztec-node/server.d.ts.map +1 -1
- package/dest/aztec-node/server.js +28 -5
- package/dest/factory.d.ts +1 -1
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +2 -0
- package/dest/modules/node_block_provider.d.ts +4 -2
- package/dest/modules/node_block_provider.d.ts.map +1 -1
- package/dest/modules/node_block_provider.js +6 -4
- package/dest/modules/node_world_state_queries.d.ts +7 -4
- package/dest/modules/node_world_state_queries.d.ts.map +1 -1
- package/dest/modules/node_world_state_queries.js +33 -33
- package/dest/modules/unseen_block_hold_off.d.ts +52 -0
- package/dest/modules/unseen_block_hold_off.d.ts.map +1 -0
- package/dest/modules/unseen_block_hold_off.js +150 -0
- package/package.json +28 -28
- package/src/aztec-node/server.ts +33 -5
- package/src/factory.ts +2 -0
- package/src/modules/node_block_provider.ts +8 -4
- package/src/modules/node_world_state_queries.ts +38 -36
- package/src/modules/unseen_block_hold_off.ts +199 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
2
|
+
import { InterruptibleSleep } from '@aztec/foundation/sleep';
|
|
3
|
+
import { Timer } from '@aztec/foundation/timer';
|
|
4
|
+
import { L2BlockSourceEvents, getBlockSourceEmitter, inspectBlockParameter } from '@aztec/stdlib/block';
|
|
5
|
+
/**
|
|
6
|
+
* Longest a held request sleeps before re-reading the block source anyway. Held requests are woken by the source
|
|
7
|
+
* reporting an update, so this only bounds how long one waits when it misses the update that added its block: the
|
|
8
|
+
* source can commit between a request's read and its sleep, and a request that is not sleeping yet is not woken.
|
|
9
|
+
*/ const MAX_SLEEP_MS = 1000;
|
|
10
|
+
/** Max requests held simultaneously. Beyond this, misses fail fast as if the hold-off were disabled. */ export const MAX_CONCURRENT_HOLDS = 100;
|
|
11
|
+
/**
|
|
12
|
+
* Resolves RPC block anchors against the block source, briefly holding a request whose anchor the node is about to
|
|
13
|
+
* see instead of failing it immediately.
|
|
14
|
+
*
|
|
15
|
+
* Behind a load balancer a client can sync to block N+1 through one node and then anchor follow-up queries against
|
|
16
|
+
* another node that is still at block N. Failing those queries aborts a whole client flow over a skew that resolves
|
|
17
|
+
* in under a block time, so a miss on an anchor that plausibly lies just ahead of the tip is retried for a bounded
|
|
18
|
+
* budget. Everything else — a tag, a number far past the tip, a budget of zero, or too many requests already held —
|
|
19
|
+
* resolves exactly as the bare block source would.
|
|
20
|
+
*
|
|
21
|
+
* Held requests do not poll at a rate of their own choosing: they all sleep on one wake-up that the block source
|
|
22
|
+
* triggers when it reports it moved, so the source is read when there is something new to read and, failing that, at
|
|
23
|
+
* most once per {@link MAX_SLEEP_MS}. A source that reports no updates cannot wake them, so it never holds anything
|
|
24
|
+
* off.
|
|
25
|
+
*/ export class UnseenBlockHoldOff {
|
|
26
|
+
blockSource;
|
|
27
|
+
config;
|
|
28
|
+
activeHolds;
|
|
29
|
+
log;
|
|
30
|
+
/** Shared by every held request; interrupted on a source update. Undefined for a source that reports none. */ wakeup;
|
|
31
|
+
constructor(blockSource, config, log){
|
|
32
|
+
this.blockSource = blockSource;
|
|
33
|
+
this.config = config;
|
|
34
|
+
this.activeHolds = 0;
|
|
35
|
+
this.log = log ?? createLogger('node:unseen-block-hold-off');
|
|
36
|
+
const emitter = getBlockSourceEmitter(blockSource);
|
|
37
|
+
if (emitter === undefined) {
|
|
38
|
+
this.log.verbose(`Block source reports no updates, queries for unseen blocks will not be held off`);
|
|
39
|
+
} else {
|
|
40
|
+
const wakeup = new InterruptibleSleep();
|
|
41
|
+
this.wakeup = wakeup;
|
|
42
|
+
emitter.on(L2BlockSourceEvents.L2BlockSourceUpdated, ()=>wakeup.interrupt());
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Number of requests currently held waiting for their anchor block. Exposed for tests and diagnostics. */ get holds() {
|
|
46
|
+
return this.activeHolds;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolves `query` to block metadata, holding off briefly when it references a block the node is about to see.
|
|
50
|
+
* Returns undefined on a miss, so callers keep whatever behavior they had before (throwing or returning
|
|
51
|
+
* undefined) — the hold-off only delays that outcome.
|
|
52
|
+
*/ getBlockData(query, opts = {}) {
|
|
53
|
+
return this.#readWithHoldOff(query, (q)=>this.blockSource.getBlockData(q), opts);
|
|
54
|
+
}
|
|
55
|
+
/** Resolves `query` to a full block with its transactions, holding off as {@link getBlockData} does. */ getBlock(query, opts = {}) {
|
|
56
|
+
return this.#readWithHoldOff(query, (q)=>this.blockSource.getBlock(q), opts);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Reads `query` through `read`, and on a miss waits for the block it names before reading once more. Subject to the
|
|
60
|
+
* concurrent-hold cap: once it is saturated a miss resolves without waiting, as it would with the hold-off
|
|
61
|
+
* disabled.
|
|
62
|
+
*/ async #readWithHoldOff(query, read, opts) {
|
|
63
|
+
const value = await read(query);
|
|
64
|
+
if (value !== undefined || opts.holdOff === false) {
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
const arrived = await this.#waitForBlock(query, await this.#resolveWaitBudgetMs(query));
|
|
68
|
+
return arrived ? await read(query) : undefined;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Waits for the block `query` names to show up on the block source, for at most `waitMs`, and reports whether it
|
|
72
|
+
* did. Returns false without waiting when the source reports no updates, when the budget is empty, or when the
|
|
73
|
+
* concurrent-hold cap is saturated, so a miss fails as fast as it would with the hold-off disabled.
|
|
74
|
+
*
|
|
75
|
+
* Arrival is checked on block metadata rather than through the caller's read, so a held request costs a metadata
|
|
76
|
+
* read per wake-up whatever it asked for: a held `getBlock` would otherwise reconstruct a whole block with its
|
|
77
|
+
* transactions on every wake-up only to find it is not the one it is waiting for.
|
|
78
|
+
*
|
|
79
|
+
* A budget is approximate: the deadline is only re-checked after a sleep, so the actual wait can exceed it by the
|
|
80
|
+
* read's own latency.
|
|
81
|
+
*/ async #waitForBlock(query, waitMs) {
|
|
82
|
+
const wakeup = this.wakeup;
|
|
83
|
+
if (wakeup === undefined || !Number.isFinite(waitMs) || waitMs <= 0) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const blockParameter = inspectBlockParameter(query);
|
|
87
|
+
if (this.activeHolds >= MAX_CONCURRENT_HOLDS) {
|
|
88
|
+
this.log.verbose(`Not holding off query for unseen block, too many requests already held`, {
|
|
89
|
+
blockParameter,
|
|
90
|
+
holds: this.activeHolds
|
|
91
|
+
});
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
this.activeHolds++;
|
|
95
|
+
const timer = new Timer();
|
|
96
|
+
try {
|
|
97
|
+
this.log.verbose(`Holding off query for unseen block`, {
|
|
98
|
+
blockParameter,
|
|
99
|
+
waitMs,
|
|
100
|
+
holds: this.activeHolds
|
|
101
|
+
});
|
|
102
|
+
while(timer.ms() < waitMs){
|
|
103
|
+
await wakeup.sleep(Math.min(waitMs - timer.ms(), MAX_SLEEP_MS));
|
|
104
|
+
const data = await this.blockSource.getBlockData(query);
|
|
105
|
+
if (data !== undefined) {
|
|
106
|
+
this.log.verbose(`Unseen block arrived after ${timer.ms()}ms`, {
|
|
107
|
+
blockParameter,
|
|
108
|
+
blockNumber: data.header.getBlockNumber(),
|
|
109
|
+
elapsedMs: timer.ms()
|
|
110
|
+
});
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
this.log.verbose(`Gave up waiting for unseen block after ${timer.ms()}ms`, {
|
|
115
|
+
blockParameter,
|
|
116
|
+
waitMs,
|
|
117
|
+
elapsedMs: timer.ms()
|
|
118
|
+
});
|
|
119
|
+
return false;
|
|
120
|
+
} finally{
|
|
121
|
+
this.activeHolds--;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Budget for waiting on a missing anchor, decided once on entry rather than per wake-up. A tag always resolves
|
|
126
|
+
* against the current tip, so a tag miss is never a skew. A number is only worth waiting on when it is the very
|
|
127
|
+
* next block: further ahead the client is not merely one block in front, and at or below the tip the block was
|
|
128
|
+
* pruned or reorged away. A hash or archive root carries no height, so "one ahead" and "reorged away" are
|
|
129
|
+
* indistinguishable and both get the shorter hash budget.
|
|
130
|
+
*
|
|
131
|
+
* The genesis block hash is the one hash never worth waiting on: a client anchors on it before it has synced any
|
|
132
|
+
* block (as a PXE does for its first tagged-log queries), and the block is synthetic, so a source that does not
|
|
133
|
+
* answer for it now never will.
|
|
134
|
+
*/ async #resolveWaitBudgetMs(query) {
|
|
135
|
+
if ('tag' in query) {
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
if ('number' in query) {
|
|
139
|
+
const tip = await this.blockSource.getBlockNumber();
|
|
140
|
+
return query.number === tip + 1 ? this.config.byNumberWaitMs : 0;
|
|
141
|
+
}
|
|
142
|
+
if ('hash' in query && this.#isGenesisBlockHash(query.hash)) {
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
return this.config.byHashWaitMs;
|
|
146
|
+
}
|
|
147
|
+
/** True when `hash` names the synthetic genesis block, which never arrives and so is never waited for. */ #isGenesisBlockHash(hash) {
|
|
148
|
+
return hash.equals(this.blockSource.getGenesisBlockHash());
|
|
149
|
+
}
|
|
150
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/aztec-node",
|
|
3
|
-
"version": "5.3.0-nightly.
|
|
3
|
+
"version": "5.3.0-nightly.20260823",
|
|
4
4
|
"main": "dest/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -65,33 +65,33 @@
|
|
|
65
65
|
]
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@aztec/archiver": "5.3.0-nightly.
|
|
69
|
-
"@aztec/bb-prover": "5.3.0-nightly.
|
|
70
|
-
"@aztec/bb.js": "5.3.0-nightly.
|
|
71
|
-
"@aztec/blob-client": "5.3.0-nightly.
|
|
72
|
-
"@aztec/blob-lib": "5.3.0-nightly.
|
|
73
|
-
"@aztec/constants": "5.3.0-nightly.
|
|
74
|
-
"@aztec/epoch-cache": "5.3.0-nightly.
|
|
75
|
-
"@aztec/ethereum": "5.3.0-nightly.
|
|
76
|
-
"@aztec/foundation": "5.3.0-nightly.
|
|
77
|
-
"@aztec/kv-store": "5.3.0-nightly.
|
|
78
|
-
"@aztec/l1-artifacts": "5.3.0-nightly.
|
|
79
|
-
"@aztec/node-keystore": "5.3.0-nightly.
|
|
80
|
-
"@aztec/node-lib": "5.3.0-nightly.
|
|
81
|
-
"@aztec/noir-protocol-circuits-types": "5.3.0-nightly.
|
|
82
|
-
"@aztec/p2p": "5.3.0-nightly.
|
|
83
|
-
"@aztec/protocol-contracts": "5.3.0-nightly.
|
|
84
|
-
"@aztec/prover-client": "5.3.0-nightly.
|
|
85
|
-
"@aztec/prover-node": "5.3.0-nightly.
|
|
86
|
-
"@aztec/sequencer-client": "5.3.0-nightly.
|
|
87
|
-
"@aztec/simulator": "5.3.0-nightly.
|
|
88
|
-
"@aztec/slasher": "5.3.0-nightly.
|
|
89
|
-
"@aztec/standard-contracts": "5.3.0-nightly.
|
|
90
|
-
"@aztec/stdlib": "5.3.0-nightly.
|
|
91
|
-
"@aztec/telemetry-client": "5.3.0-nightly.
|
|
92
|
-
"@aztec/validator-client": "5.3.0-nightly.
|
|
93
|
-
"@aztec/validator-ha-signer": "5.3.0-nightly.
|
|
94
|
-
"@aztec/world-state": "5.3.0-nightly.
|
|
68
|
+
"@aztec/archiver": "5.3.0-nightly.20260823",
|
|
69
|
+
"@aztec/bb-prover": "5.3.0-nightly.20260823",
|
|
70
|
+
"@aztec/bb.js": "5.3.0-nightly.20260823",
|
|
71
|
+
"@aztec/blob-client": "5.3.0-nightly.20260823",
|
|
72
|
+
"@aztec/blob-lib": "5.3.0-nightly.20260823",
|
|
73
|
+
"@aztec/constants": "5.3.0-nightly.20260823",
|
|
74
|
+
"@aztec/epoch-cache": "5.3.0-nightly.20260823",
|
|
75
|
+
"@aztec/ethereum": "5.3.0-nightly.20260823",
|
|
76
|
+
"@aztec/foundation": "5.3.0-nightly.20260823",
|
|
77
|
+
"@aztec/kv-store": "5.3.0-nightly.20260823",
|
|
78
|
+
"@aztec/l1-artifacts": "5.3.0-nightly.20260823",
|
|
79
|
+
"@aztec/node-keystore": "5.3.0-nightly.20260823",
|
|
80
|
+
"@aztec/node-lib": "5.3.0-nightly.20260823",
|
|
81
|
+
"@aztec/noir-protocol-circuits-types": "5.3.0-nightly.20260823",
|
|
82
|
+
"@aztec/p2p": "5.3.0-nightly.20260823",
|
|
83
|
+
"@aztec/protocol-contracts": "5.3.0-nightly.20260823",
|
|
84
|
+
"@aztec/prover-client": "5.3.0-nightly.20260823",
|
|
85
|
+
"@aztec/prover-node": "5.3.0-nightly.20260823",
|
|
86
|
+
"@aztec/sequencer-client": "5.3.0-nightly.20260823",
|
|
87
|
+
"@aztec/simulator": "5.3.0-nightly.20260823",
|
|
88
|
+
"@aztec/slasher": "5.3.0-nightly.20260823",
|
|
89
|
+
"@aztec/standard-contracts": "5.3.0-nightly.20260823",
|
|
90
|
+
"@aztec/stdlib": "5.3.0-nightly.20260823",
|
|
91
|
+
"@aztec/telemetry-client": "5.3.0-nightly.20260823",
|
|
92
|
+
"@aztec/validator-client": "5.3.0-nightly.20260823",
|
|
93
|
+
"@aztec/validator-ha-signer": "5.3.0-nightly.20260823",
|
|
94
|
+
"@aztec/world-state": "5.3.0-nightly.20260823",
|
|
95
95
|
"koa": "^2.16.1",
|
|
96
96
|
"koa-router": "^13.1.1",
|
|
97
97
|
"tslib": "^2.4.0",
|
package/src/aztec-node/server.ts
CHANGED
|
@@ -111,6 +111,7 @@ import { NodeKeystoreAdapter, ValidatorClient } from '@aztec/validator-client';
|
|
|
111
111
|
import { NodeBlockProvider } from '../modules/node_block_provider.js';
|
|
112
112
|
import { NodeTxReceiptBuilder } from '../modules/node_tx_receipt.js';
|
|
113
113
|
import { NodeWorldStateQueries } from '../modules/node_world_state_queries.js';
|
|
114
|
+
import { UnseenBlockHoldOff } from '../modules/unseen_block_hold_off.js';
|
|
114
115
|
import { Sentinel } from '../sentinel/sentinel.js';
|
|
115
116
|
import type { AztecNodeConfig } from './config.js';
|
|
116
117
|
import { NodeMetrics } from './node_metrics.js';
|
|
@@ -164,6 +165,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
|
|
|
164
165
|
private readonly nodePublicCallsSimulator: NodePublicCallsSimulator;
|
|
165
166
|
private readonly worldStateQueries: NodeWorldStateQueries;
|
|
166
167
|
private readonly blockProvider: NodeBlockProvider;
|
|
168
|
+
private readonly unseenBlockHoldOff: UnseenBlockHoldOff;
|
|
167
169
|
private readonly txReceiptBuilder: NodeTxReceiptBuilder;
|
|
168
170
|
|
|
169
171
|
public readonly tracer: Tracer;
|
|
@@ -246,14 +248,25 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
|
|
|
246
248
|
log: this.log.createChild('public-calls-simulator'),
|
|
247
249
|
});
|
|
248
250
|
|
|
251
|
+
// Shared by every block-anchored read so the concurrent-hold cap applies across all of them.
|
|
252
|
+
this.unseenBlockHoldOff = new UnseenBlockHoldOff(
|
|
253
|
+
this.blockSource,
|
|
254
|
+
{
|
|
255
|
+
byNumberWaitMs: this.config.rpcUnseenBlockByNumberWaitMs ?? 2 * this.config.blockDurationMs,
|
|
256
|
+
byHashWaitMs: this.config.rpcUnseenBlockByHashWaitMs,
|
|
257
|
+
},
|
|
258
|
+
this.log.createChild('unseen-block-hold-off'),
|
|
259
|
+
);
|
|
260
|
+
|
|
249
261
|
this.worldStateQueries = new NodeWorldStateQueries({
|
|
250
262
|
worldStateSynchronizer: this.worldStateSynchronizer,
|
|
251
263
|
blockSource: this.blockSource,
|
|
252
264
|
l1ToL2MessageSource: this.l1ToL2MessageSource,
|
|
265
|
+
holdOff: this.unseenBlockHoldOff,
|
|
253
266
|
log: this.log.createChild('world-state-queries'),
|
|
254
267
|
});
|
|
255
268
|
|
|
256
|
-
this.blockProvider = new NodeBlockProvider(this.blockSource);
|
|
269
|
+
this.blockProvider = new NodeBlockProvider(this.blockSource, this.unseenBlockHoldOff);
|
|
257
270
|
|
|
258
271
|
this.txReceiptBuilder = new NodeTxReceiptBuilder({
|
|
259
272
|
p2pClient: this.p2pClient,
|
|
@@ -501,12 +514,26 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
|
|
|
501
514
|
return this.contractDataSource.getContract(address, blockData.header.globalVariables.timestamp);
|
|
502
515
|
}
|
|
503
516
|
|
|
504
|
-
public getPrivateLogsByTags(query: PrivateLogsQuery): Promise<LogResult[][]> {
|
|
505
|
-
|
|
517
|
+
public async getPrivateLogsByTags(query: PrivateLogsQuery): Promise<LogResult[][]> {
|
|
518
|
+
await this.#awaitLogsReferenceBlock(query.referenceBlock);
|
|
519
|
+
return await this.logsSource.getPrivateLogsByTags(query);
|
|
506
520
|
}
|
|
507
521
|
|
|
508
|
-
public getPublicLogsByTags(query: PublicLogsQuery): Promise<LogResult[][]> {
|
|
509
|
-
|
|
522
|
+
public async getPublicLogsByTags(query: PublicLogsQuery): Promise<LogResult[][]> {
|
|
523
|
+
await this.#awaitLogsReferenceBlock(query.referenceBlock);
|
|
524
|
+
return await this.logsSource.getPublicLogsByTags(query);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Waits briefly for a logs query's reorg-safety anchor when the node has not seen that block yet, so a client
|
|
529
|
+
* that synced one block ahead through another node is not failed over a transient skew. The result is discarded:
|
|
530
|
+
* the log store's own in-transaction anchor check stays authoritative and throws as before if the block never
|
|
531
|
+
* arrives.
|
|
532
|
+
*/
|
|
533
|
+
async #awaitLogsReferenceBlock(referenceBlock: BlockHash | undefined): Promise<void> {
|
|
534
|
+
if (referenceBlock !== undefined) {
|
|
535
|
+
await this.unseenBlockHoldOff.getBlockData({ hash: referenceBlock });
|
|
536
|
+
}
|
|
510
537
|
}
|
|
511
538
|
|
|
512
539
|
/**
|
|
@@ -571,6 +598,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
|
|
|
571
598
|
await tryStop(this.automineSequencer);
|
|
572
599
|
await tryStop(this.proverNode);
|
|
573
600
|
await tryStop(this.p2pClient);
|
|
601
|
+
await tryStop(this.feeProvider);
|
|
574
602
|
await tryStop(this.worldStateSynchronizer);
|
|
575
603
|
await tryStop(this.blockSource);
|
|
576
604
|
await tryStop(this.blobClient);
|
package/src/factory.ts
CHANGED
|
@@ -251,6 +251,8 @@ export async function createAztecNodeService(
|
|
|
251
251
|
|
|
252
252
|
const globalVariableBuilder = new GlobalVariableBuilder(publicClient, globalVariableBuilderConfig);
|
|
253
253
|
const feeProvider = new FeeProviderImpl(dateProvider, publicClient, globalVariableBuilderConfig);
|
|
254
|
+
await feeProvider.start();
|
|
255
|
+
started.push(feeProvider);
|
|
254
256
|
|
|
255
257
|
const collectOffenses = !config.disableValidator || config.enableOffenseCollection;
|
|
256
258
|
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
projectProposedToCheckpointResponse,
|
|
20
20
|
} from '../aztec-node/block_response_helpers.js';
|
|
21
21
|
import { normalizeBlockParameter, resolveCheckpointParameter } from './block_parameter.js';
|
|
22
|
+
import type { UnseenBlockHoldOff } from './unseen_block_hold_off.js';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Serves the node's block and checkpoint read queries, assembling RPC responses (optionally including
|
|
@@ -26,7 +27,10 @@ import { normalizeBlockParameter, resolveCheckpointParameter } from './block_par
|
|
|
26
27
|
* `AztecNodeService` to keep `server.ts` smaller.
|
|
27
28
|
*/
|
|
28
29
|
export class NodeBlockProvider {
|
|
29
|
-
constructor(
|
|
30
|
+
constructor(
|
|
31
|
+
private readonly blockSource: L2BlockSource,
|
|
32
|
+
private readonly holdOff: UnseenBlockHoldOff,
|
|
33
|
+
) {}
|
|
30
34
|
|
|
31
35
|
public async getBlock<Opts extends BlockIncludeOptions = {}>(
|
|
32
36
|
param: BlockParameter,
|
|
@@ -37,14 +41,14 @@ export class NodeBlockProvider {
|
|
|
37
41
|
const wantContext = !!options.includeL1PublishInfo || !!options.includeAttestations;
|
|
38
42
|
|
|
39
43
|
if (wantTxs) {
|
|
40
|
-
const block = await this.
|
|
44
|
+
const block = await this.holdOff.getBlock(query);
|
|
41
45
|
if (!block) {
|
|
42
46
|
return undefined;
|
|
43
47
|
}
|
|
44
48
|
const ctx = wantContext ? await this.#getCheckpointContext(block.checkpointNumber) : undefined;
|
|
45
49
|
return (await blockResponseFromL2Block(block, options, ctx)) as BlockResponse<Opts>;
|
|
46
50
|
}
|
|
47
|
-
const data = await this.
|
|
51
|
+
const data = await this.holdOff.getBlockData(query);
|
|
48
52
|
if (!data) {
|
|
49
53
|
return undefined;
|
|
50
54
|
}
|
|
@@ -54,7 +58,7 @@ export class NodeBlockProvider {
|
|
|
54
58
|
|
|
55
59
|
public getBlockData(param: BlockParameter): Promise<BlockData | undefined> {
|
|
56
60
|
const query = normalizeBlockParameter(param);
|
|
57
|
-
return this.
|
|
61
|
+
return this.holdOff.getBlockData(query);
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
public async getBlocks<Opts extends BlocksIncludeOptions = {}>(
|
|
@@ -28,6 +28,7 @@ import type { TxHash } from '@aztec/stdlib/tx';
|
|
|
28
28
|
import { WorldStateSynchronizerError } from '@aztec/world-state';
|
|
29
29
|
|
|
30
30
|
import { normalizeBlockParameter } from './block_parameter.js';
|
|
31
|
+
import type { UnseenBlockHoldOff, UnseenBlockHoldOffOptions } from './unseen_block_hold_off.js';
|
|
31
32
|
|
|
32
33
|
/** Attempts at resolving a query and syncing world state to it before giving up (see {@link NodeWorldStateQueries.getWorldState}). */
|
|
33
34
|
const WORLD_STATE_SYNC_ATTEMPTS = 3;
|
|
@@ -40,6 +41,7 @@ export interface NodeWorldStateQueriesDeps {
|
|
|
40
41
|
worldStateSynchronizer: WorldStateSynchronizer;
|
|
41
42
|
blockSource: L2BlockSource;
|
|
42
43
|
l1ToL2MessageSource: L1ToL2MessageSource;
|
|
44
|
+
holdOff: UnseenBlockHoldOff;
|
|
43
45
|
log?: Logger;
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -52,12 +54,14 @@ export class NodeWorldStateQueries {
|
|
|
52
54
|
private readonly worldStateSynchronizer: WorldStateSynchronizer;
|
|
53
55
|
private readonly blockSource: L2BlockSource;
|
|
54
56
|
private readonly l1ToL2MessageSource: L1ToL2MessageSource;
|
|
57
|
+
private readonly holdOff: UnseenBlockHoldOff;
|
|
55
58
|
private readonly log: Logger;
|
|
56
59
|
|
|
57
60
|
constructor(deps: NodeWorldStateQueriesDeps) {
|
|
58
61
|
this.worldStateSynchronizer = deps.worldStateSynchronizer;
|
|
59
62
|
this.blockSource = deps.blockSource;
|
|
60
63
|
this.l1ToL2MessageSource = deps.l1ToL2MessageSource;
|
|
64
|
+
this.holdOff = deps.holdOff;
|
|
61
65
|
this.log = deps.log ?? createLogger('node:world-state-queries');
|
|
62
66
|
}
|
|
63
67
|
|
|
@@ -137,7 +141,9 @@ export class NodeWorldStateQueries {
|
|
|
137
141
|
// The Noir circuit checks the archive membership proof against `anchor_block_header.last_archive.root`,
|
|
138
142
|
// which is the archive tree root BEFORE the anchor block was added (i.e. the state after block N-1).
|
|
139
143
|
// So we need the world state at block N-1, not block N, to produce a sibling path matching that root.
|
|
140
|
-
const referenceBlockNumber = await this.#
|
|
144
|
+
const { blockNumber: referenceBlockNumber } = await this.#resolveBlockNumberAndHash(
|
|
145
|
+
normalizeBlockParameter(referenceBlock),
|
|
146
|
+
);
|
|
141
147
|
if (referenceBlockNumber === BlockNumber.ZERO) {
|
|
142
148
|
// Block 0 (the initial block) has an empty archive, so no membership witness can exist.
|
|
143
149
|
return undefined;
|
|
@@ -282,9 +288,9 @@ export class NodeWorldStateQueries {
|
|
|
282
288
|
* Returns an instance of MerkleTreeOperations having first ensured the world state is synced to the requested
|
|
283
289
|
* block on the correct fork. Every query variant is resolved to a concrete (block number, block hash), which is
|
|
284
290
|
* threaded through both the sync and the snapshot read so a reorg that replaced the block at that height is
|
|
285
|
-
* detected rather than served silently. Transient failures — a prune landing between resolution and sync, or
|
|
286
|
-
* fork flip caught at either the sync or the snapshot stage — are retried a few times, re-resolving the query
|
|
287
|
-
* against the updated chain each time; terminal failures —
|
|
291
|
+
* detected rather than served silently. Transient sync failures — a prune landing between resolution and sync, or
|
|
292
|
+
* a fork flip caught at either the sync or the snapshot stage — are retried a few times, re-resolving the query
|
|
293
|
+
* against the updated chain each time; terminal failures — a query that resolves to no block, or a block whose
|
|
288
294
|
* history world state has pruned away — are thrown immediately.
|
|
289
295
|
* @param block - The block parameter (block number, block hash, or tag) at which to get the data.
|
|
290
296
|
* @returns An instance of a committed MerkleTreeOperations
|
|
@@ -292,9 +298,22 @@ export class NodeWorldStateQueries {
|
|
|
292
298
|
public async getWorldState(block: BlockParameter) {
|
|
293
299
|
const query = normalizeBlockParameter(block);
|
|
294
300
|
|
|
301
|
+
// User requests 'latest on the current fork', so the committed db is returned unverified
|
|
302
|
+
if ('tag' in query && query.tag === 'proposed') {
|
|
303
|
+
this.log.debug(`Using committed db for latest block`);
|
|
304
|
+
await this.worldStateSynchronizer.syncImmediate();
|
|
305
|
+
return this.worldStateSynchronizer.getCommitted();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Resolve the query against the block source BEFORE syncing, so the sync can be driven to a concrete
|
|
309
|
+
// (number, hash). Resolving after the sync races the block source: the resolved tip can advance past what world
|
|
310
|
+
// state synced while the sync is in flight. Resolving here rather than inside the retry loop also keeps a
|
|
311
|
+
// retry from re-entering the hold-off, which would multiply the wait a client experiences by the attempt count.
|
|
312
|
+
let resolved = await this.#resolveBlockNumberAndHash(query);
|
|
313
|
+
|
|
295
314
|
for (let attempt = 1; ; attempt++) {
|
|
296
315
|
try {
|
|
297
|
-
return await this.#resolveWorldState(
|
|
316
|
+
return await this.#resolveWorldState(resolved);
|
|
298
317
|
} catch (err) {
|
|
299
318
|
if (attempt >= WORLD_STATE_SYNC_ATTEMPTS || !(err instanceof WorldStateSynchronizerError)) {
|
|
300
319
|
throw err;
|
|
@@ -304,27 +323,17 @@ export class NodeWorldStateQueries {
|
|
|
304
323
|
block: inspectBlockParameter(block),
|
|
305
324
|
});
|
|
306
325
|
await sleep(WORLD_STATE_SYNC_RETRY_DELAY_MS);
|
|
326
|
+
resolved = await this.#resolveBlockNumberAndHash(query, { holdOff: false });
|
|
307
327
|
}
|
|
308
328
|
}
|
|
309
329
|
}
|
|
310
330
|
|
|
311
331
|
/**
|
|
312
|
-
*
|
|
313
|
-
* the
|
|
332
|
+
* Syncs world state to the resolved fork and returns the fork-verified snapshot at that block. Passing the hash
|
|
333
|
+
* makes the sync reorg-aware — it barriers until the archive-tree commit for that block has landed and verifies
|
|
334
|
+
* it matches the requested fork, throwing otherwise.
|
|
314
335
|
*/
|
|
315
|
-
async #resolveWorldState(
|
|
316
|
-
// User requests 'latest on the current fork', so the committed db is returned unverified
|
|
317
|
-
if ('tag' in query && query.tag === 'proposed') {
|
|
318
|
-
this.log.debug(`Using committed db for latest block`);
|
|
319
|
-
await this.worldStateSynchronizer.syncImmediate();
|
|
320
|
-
return this.worldStateSynchronizer.getCommitted();
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Resolve the query against the block source BEFORE syncing to a concrete (number, hash), and drive the sync to
|
|
324
|
-
// that exact fork. Resolving after the sync races the block source: the resolved tip can advance past what world
|
|
325
|
-
// state synced while the sync is in flight. Passing the hash makes the sync reorg-aware — it barriers until the
|
|
326
|
-
// archive-tree commit for that block has landed and verifies it matches the requested fork, throwing otherwise.
|
|
327
|
-
const { blockNumber, blockHash } = await this.#resolveBlockNumberAndHash(query);
|
|
336
|
+
async #resolveWorldState({ blockNumber, blockHash }: { blockNumber: BlockNumber; blockHash: BlockHash }) {
|
|
328
337
|
const blockSyncedTo = await this.worldStateSynchronizer.syncImmediate(blockNumber, blockHash);
|
|
329
338
|
|
|
330
339
|
// The fork could flip between it returning and the snapshot being read, so getVerifiedSnapshot pins the
|
|
@@ -333,32 +342,25 @@ export class NodeWorldStateQueries {
|
|
|
333
342
|
return await this.worldStateSynchronizer.getVerifiedSnapshot(blockNumber, blockHash);
|
|
334
343
|
}
|
|
335
344
|
|
|
336
|
-
/**
|
|
345
|
+
/**
|
|
346
|
+
* Resolves any {@link BlockParameter} variant to its concrete `(blockNumber, blockHash)`, holding the query
|
|
347
|
+
* briefly (unless the caller opts out) when it references a block the node is about to see.
|
|
348
|
+
*/
|
|
337
349
|
async #resolveBlockNumberAndHash(
|
|
338
350
|
query: NormalizedBlockParameter,
|
|
351
|
+
opts: UnseenBlockHoldOffOptions = {},
|
|
339
352
|
): Promise<{ blockNumber: BlockNumber; blockHash: BlockHash }> {
|
|
340
|
-
const blockData = await this.
|
|
353
|
+
const blockData = await this.holdOff.getBlockData(query, opts);
|
|
341
354
|
if (blockData === undefined) {
|
|
342
355
|
this.#throwOnUndefinedBlockData(query);
|
|
343
356
|
}
|
|
344
357
|
return { blockNumber: blockData.header.getBlockNumber(), blockHash: blockData.blockHash };
|
|
345
358
|
}
|
|
346
359
|
|
|
347
|
-
/** Resolves any {@link BlockParameter} variant to a concrete block number. */
|
|
348
|
-
async #resolveBlockNumber(block: BlockParameter): Promise<BlockNumber> {
|
|
349
|
-
const blockQuery = normalizeBlockParameter(block);
|
|
350
|
-
const blockNumber = await this.blockSource.getBlockNumber(blockQuery);
|
|
351
|
-
if (blockNumber === undefined) {
|
|
352
|
-
this.#throwOnUndefinedBlockData(blockQuery);
|
|
353
|
-
}
|
|
354
|
-
return blockNumber;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
360
|
/**
|
|
358
|
-
* Hash and archive misses
|
|
359
|
-
* the block may have been pruned
|
|
360
|
-
*
|
|
361
|
-
* current chain.
|
|
361
|
+
* Hash and archive misses report an unknown block (likely a reorg); tag and number misses report a transient
|
|
362
|
+
* condition — the block may have been pruned, or may simply not have arrived yet — and are distinguished to the
|
|
363
|
+
* caller as a {@link WorldStateSynchronizerError}.
|
|
362
364
|
*/
|
|
363
365
|
#throwOnUndefinedBlockData(query: NormalizedBlockParameter): never {
|
|
364
366
|
if ('hash' in query) {
|