@dusk-network/app-core 0.4.1 → 0.5.0

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.
@@ -90,7 +90,11 @@ export declare const L2_WINDOW: 100000;
90
90
  /** Log searches one run of `syncWithdrawals` makes, the head's included, by default. */
91
91
  export declare const L2_RUN_REQUESTS: 8;
92
92
 
93
- /** DuskEVM blocks under the head counted as final where the RPC has no "finalized" block: about an hour. */
93
+ /**
94
+ * DuskEVM blocks under the head counted as final where the RPC has no
95
+ * "finalized" block: about an hour. The blocks a run reads within that
96
+ * margin under it are read again once they're under a later run's.
97
+ */
94
98
  export declare const UNFINALIZED_MARGIN: 1800;
95
99
 
96
100
  /** Pages of an address's transactions `explorerWithdrawals` reads at most, by default. */
@@ -360,9 +364,9 @@ export type WithdrawalsOptions = {
360
364
  /** How many of the newest games a proof is looked for among: 64 by default, and for anything but a positive whole number. */
361
365
  gameSearchDepth?: number | bigint;
362
366
  /**
363
- * Where the deployment's fixed facts are kept: each game's hub, each game
364
- * type's hub, each hub's clock limit, and each withdrawal's receipt once
365
- * its DuskEVM block is final.
367
+ * Where the deployment's fixed facts are kept: each game's hub and each
368
+ * game type's hub, once the factory names one, each hub's clock limit, and
369
+ * each withdrawal's receipt once its DuskEVM block is final.
366
370
  */
367
371
  cache?: FactCache;
368
372
  /** The clock, in milliseconds. */
@@ -463,6 +467,13 @@ export type FoundWithdrawal = {
463
467
  export type WithdrawalScan = {
464
468
  /** The final blocks covered, as ranges, highest first. */
465
469
  covered: number[][];
470
+ /**
471
+ * Where the RPC has no finalized block: the blocks covered within
472
+ * `UNFINALIZED_MARGIN` under the head a run read them under, as ranges,
473
+ * highest first, the newest 16, read again once they're under a later
474
+ * run's margin. Older ones are left to the passes over all the blocks.
475
+ */
476
+ open?: number[][];
466
477
  /** What was found, by hash. */
467
478
  entries: Record<string, FoundWithdrawal>;
468
479
  /** The final head the last run read under. */
@@ -489,7 +500,14 @@ export type WithdrawalScan = {
489
500
  * take though in half its requests at most (while older blocks are still to
490
501
  * be read, only once the final head has moved), and carries on below; under
491
502
  * a final head lower than the last run's, what was covered and found above
492
- * it is dropped, and read again. Once all the final blocks are
503
+ * it is dropped, and read again. Where the RPC has no finalized block, the
504
+ * final head is the latest less `UNFINALIZED_MARGIN`, and the blocks a run
505
+ * reads within that margin under it are read again once they're under a
506
+ * later run's (`open`): among the newest blocks a run reads again, or, once
507
+ * all the final blocks are covered, before the passes over them, with the
508
+ * requests a run has left. A deeper reorg there is found, and older blocks
509
+ * are read as they would be without it.
510
+ * Once all the final blocks are
493
511
  * covered, they're read again from the newest down, a pass at a time with
494
512
  * the requests a run has left: the first an hour later, each after as long
495
513
  * again as it has been since. A withdrawal is found once: one read again
@@ -133,6 +133,10 @@ const NONCE_COUNT = (1n << 240n) - 1n;
133
133
  // since.
134
134
  const L2_OVERLAP = 50_000;
135
135
  const L2_RECHECK_MS = 60 * 60_000;
136
+ // The ranges of blocks read only under a head the RPC didn't call final that
137
+ // a sync keeps to read again, the newest: older ones are left to its passes
138
+ // over all the blocks.
139
+ const L2_OPEN_RANGES = 16;
136
140
  // Answers a read is sent again for, a quarter second, then half a second later.
137
141
  const RETRY_STATUSES = new Set([429, 502, 503, 504]);
138
142
  const ATTEMPTS = 3;
@@ -1024,14 +1028,21 @@ function createReads({ cache, contracts, drivers, fetch, nodeUrl }) {
1024
1028
  };
1025
1029
 
1026
1030
  /**
1027
- * The hub that holds a game type's games.
1031
+ * The hub that holds a game type's games. None, and nothing kept, for a
1032
+ * type the factory names no hub for yet.
1028
1033
  *
1029
1034
  * @param {number} gameType
1030
1035
  * @returns {Promise<string>}
1031
1036
  */
1032
1037
  const hubOfType = (gameType) =>
1033
1038
  known(cache, inflight, `type:${contracts.disputeGameFactory}:${gameType}`, () =>
1034
- factory("virtualGameHubId", gameType).then(contractIdHex)
1039
+ factory("virtualGameHubId", gameType).then((value) => {
1040
+ const hubId = contractIdHex(value);
1041
+
1042
+ if (/^0+$/.test(hubId)) throw new Error("The factory names no hub for the game type");
1043
+
1044
+ return hubId;
1045
+ })
1035
1046
  );
1036
1047
 
1037
1048
  /**
@@ -2230,22 +2241,26 @@ function withdrawalFromLog(log) {
2230
2241
  const tooWide = (error) => /range|limit|too many|exceed/i.test(asError(error).message);
2231
2242
 
2232
2243
  /**
2233
- * The highest DuskEVM block that can't be reorged away, and the requests it
2234
- * took.
2244
+ * The highest DuskEVM block that can't be reorged away, the requests it
2245
+ * took, and the margin under it within which the blocks a run reads are
2246
+ * read again on a later one: none under the RPC's finalized block. Where
2247
+ * the RPC has none, the head is the latest less a margin, and the blocks
2248
+ * read within the margin under it are read again, as a deeper reorg could
2249
+ * still change them.
2235
2250
  *
2236
2251
  * @param {(method: string, params?: unknown[]) => Promise<any>} rpc
2237
- * @returns {Promise<{ head: number, requests: number }>}
2252
+ * @returns {Promise<{ head: number, margin: number, requests: number }>}
2238
2253
  */
2239
2254
  async function finalHead(rpc) {
2240
2255
  try {
2241
2256
  const block = await rpc("eth_getBlockByNumber", ["finalized", false]);
2242
2257
 
2243
- if (block?.number) return { head: Number(BigInt(block.number)), requests: 1 };
2258
+ if (block?.number) return { head: Number(BigInt(block.number)), margin: 0, requests: 1 };
2244
2259
  } catch {
2245
2260
  // An RPC without "finalized": the latest block, less a margin.
2246
2261
  }
2247
2262
 
2248
- return { head: Math.max(0, Number(BigInt(await rpc("eth_blockNumber", []))) - UNFINALIZED_MARGIN), requests: 2 };
2263
+ return { head: Math.max(0, Number(BigInt(await rpc("eth_blockNumber", []))) - UNFINALIZED_MARGIN), margin: UNFINALIZED_MARGIN, requests: 2 };
2249
2264
  }
2250
2265
 
2251
2266
  /**
@@ -2267,6 +2282,39 @@ function recheckOf(value, target) {
2267
2282
  return { at: kept.at, from: kept.from === null ? null : Math.min(kept.from, target), since: kept.since };
2268
2283
  }
2269
2284
 
2285
+ /**
2286
+ * The blocks a state kept as read only under a head the RPC didn't call
2287
+ * final, as far as they're still covered, as ranges, highest first; none
2288
+ * for anything else.
2289
+ *
2290
+ * @param {unknown} value
2291
+ * @param {number[][]} covered
2292
+ * @returns {number[][]}
2293
+ */
2294
+ function openOf(value, covered) {
2295
+ const kept = Array.isArray(value) ? value.filter((range) => Array.isArray(range) && Number.isSafeInteger(range[0]) && Number.isSafeInteger(range[1]) && range[0] <= range[1]) : [];
2296
+ /** @type {number[][]} */
2297
+ let out = [];
2298
+
2299
+ for (const [a, b] of kept) {
2300
+ for (const [c, d] of covered) {
2301
+ if (Math.max(a, c) <= Math.min(b, d)) out = addCoverage(out, Math.max(a, c), Math.min(b, d));
2302
+ }
2303
+ }
2304
+
2305
+ return out;
2306
+ }
2307
+
2308
+ /**
2309
+ * Ranges of blocks, highest first, without blocks `lo` to `hi`.
2310
+ *
2311
+ * @param {number[][]} ranges
2312
+ * @param {number} lo
2313
+ * @param {number} hi
2314
+ * @returns {number[][]}
2315
+ */
2316
+ const without = (ranges, lo, hi) => ranges.flatMap(([a, b]) => [...(b > hi ? [[Math.max(a, hi + 1), b]] : []), ...(a < lo ? [[a, Math.min(b, lo - 1)]] : [])]);
2317
+
2270
2318
  /**
2271
2319
  * @param {{ address: string, duskEvm: DuskEvmClient, network?: NetworkRef, bridge?: string, state?: WithdrawalScan | null, maxRequests?: number, now?: () => number }} options
2272
2320
  * @returns {Promise<{ found: FoundWithdrawal[], requests: number, state: WithdrawalScan }>}
@@ -2279,7 +2327,7 @@ export async function syncWithdrawals({ address, bridge, duskEvm, maxRequests =
2279
2327
  if (!contract) throw new TypeError("No DuskEVM bridge to read");
2280
2328
 
2281
2329
  const rpc = evmRpc(duskEvm, net);
2282
- const { head: target, requests: asked } = await finalHead(rpc);
2330
+ const { head: target, margin, requests: asked } = await finalHead(rpc);
2283
2331
  const time = now();
2284
2332
  // Coverage kept above the final head, as an older run read the latest, is
2285
2333
  // read again, and what was found there is dropped with it: it may have
@@ -2293,6 +2341,11 @@ export async function syncWithdrawals({ address, bridge, duskEvm, maxRequests =
2293
2341
  let requests = asked;
2294
2342
  let recheck = recheckOf(state?.recheck, target);
2295
2343
  const topics = [ETH_BRIDGE_INITIATED_TOPIC, pad(sender)];
2344
+ // Blocks read only under a head the RPC didn't call final, within its
2345
+ // margin, are read again once they're under a run's margin too: a deeper
2346
+ // reorg could still change them. Those kept above the final head go with
2347
+ // its coverage.
2348
+ let open = openOf(state?.open, covered);
2296
2349
 
2297
2350
  /**
2298
2351
  * The address's withdrawals in blocks `lo` to `hi`, those blocks covered;
@@ -2331,6 +2384,10 @@ export async function syncWithdrawals({ address, bridge, duskEvm, maxRequests =
2331
2384
  }
2332
2385
 
2333
2386
  covered = addCoverage(covered, lo, hi);
2387
+ // Read under this run's margin, they're as final as its head; within it,
2388
+ // under a head the RPC didn't call final, they're open.
2389
+ open = without(open, lo, hi);
2390
+ if (margin && hi > target - margin) open = addCoverage(open, Math.max(lo, target - margin + 1), hi);
2334
2391
 
2335
2392
  return true;
2336
2393
  };
@@ -2363,8 +2420,19 @@ export async function syncWithdrawals({ address, bridge, duskEvm, maxRequests =
2363
2420
  continue;
2364
2421
  }
2365
2422
 
2366
- // All the final blocks covered: they're read again, a pass at a time,
2367
- // from the newest down, with what a run has left.
2423
+ // All the final blocks covered: those read only under a head the RPC
2424
+ // didn't call final, and under this run's margin now, are read again
2425
+ // first, the newest first. Then all of them, a pass at a time, from the
2426
+ // newest down, with what a run has left.
2427
+ const settling = open.find(([a]) => a <= target - margin);
2428
+
2429
+ if (settling) {
2430
+ const b = Math.min(settling[1], target - margin);
2431
+
2432
+ await read(Math.max(settling[0], b - window + 1), b);
2433
+ continue;
2434
+ }
2435
+
2368
2436
  if (recheck === null) recheck = { at: time + L2_RECHECK_MS, from: null, since: time };
2369
2437
  if (recheck.from === null && time < recheck.at) break;
2370
2438
 
@@ -2378,7 +2446,7 @@ export async function syncWithdrawals({ address, bridge, duskEvm, maxRequests =
2378
2446
 
2379
2447
  if (recheck === null && !topGap(covered, target)) recheck = { at: time + L2_RECHECK_MS, from: null, since: time };
2380
2448
 
2381
- return { found, requests, state: { covered, entries, ...(recheck ? { recheck } : {}), target, updatedAt: now(), window } };
2449
+ return { found, requests, state: { covered, entries, ...(open.length ? { open: open.slice(0, L2_OPEN_RANGES) } : {}), ...(recheck ? { recheck } : {}), target, updatedAt: now(), window } };
2382
2450
  }
2383
2451
 
2384
2452
  /**