@aztec/blob-client 0.0.1-commit.03f7ef2 → 0.0.1-commit.04852196a
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/README.md +10 -1
- package/dest/archive/config.js +1 -1
- package/dest/archive/instrumentation.d.ts +1 -1
- package/dest/archive/instrumentation.d.ts.map +1 -1
- package/dest/archive/instrumentation.js +13 -13
- package/dest/blobstore/blob_store_test_suite.js +9 -9
- package/dest/client/config.d.ts +5 -1
- package/dest/client/config.d.ts.map +1 -1
- package/dest/client/config.js +5 -0
- package/dest/client/factory.d.ts +3 -2
- package/dest/client/factory.d.ts.map +1 -1
- package/dest/client/factory.js +5 -2
- package/dest/client/http.d.ts +24 -2
- package/dest/client/http.d.ts.map +1 -1
- package/dest/client/http.js +133 -47
- package/dest/client/interface.d.ts +18 -1
- package/dest/client/interface.d.ts.map +1 -1
- package/dest/client/local.d.ts +3 -1
- package/dest/client/local.d.ts.map +1 -1
- package/dest/client/local.js +3 -0
- package/dest/client/tests.js +3 -3
- package/dest/filestore/filestore_blob_client.d.ts +14 -2
- package/dest/filestore/filestore_blob_client.d.ts.map +1 -1
- package/dest/filestore/filestore_blob_client.js +29 -5
- package/dest/filestore/healthcheck.d.ts +5 -0
- package/dest/filestore/healthcheck.d.ts.map +1 -0
- package/dest/filestore/healthcheck.js +3 -0
- package/package.json +8 -8
- package/src/archive/config.ts +1 -1
- package/src/archive/instrumentation.ts +22 -13
- package/src/blobstore/blob_store_test_suite.ts +9 -9
- package/src/client/config.ts +10 -0
- package/src/client/factory.ts +7 -2
- package/src/client/http.ts +175 -41
- package/src/client/interface.ts +17 -0
- package/src/client/local.ts +5 -0
- package/src/client/tests.ts +2 -2
- package/src/filestore/filestore_blob_client.ts +33 -5
- package/src/filestore/healthcheck.ts +5 -0
package/src/client/http.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { type RpcBlock, createPublicClient, fallback, http } from 'viem';
|
|
|
9
9
|
import { createBlobArchiveClient } from '../archive/factory.js';
|
|
10
10
|
import type { BlobArchiveClient } from '../archive/interface.js';
|
|
11
11
|
import type { FileStoreBlobClient } from '../filestore/filestore_blob_client.js';
|
|
12
|
+
import { DEFAULT_HEALTHCHECK_UPLOAD_INTERVAL_MINUTES } from '../filestore/healthcheck.js';
|
|
12
13
|
import { type BlobClientConfig, getBlobClientConfigFromEnv } from './config.js';
|
|
13
14
|
import type { BlobClientInterface, GetBlobSidecarOptions } from './interface.js';
|
|
14
15
|
|
|
@@ -21,6 +22,12 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
21
22
|
protected readonly fileStoreUploadClient: FileStoreBlobClient | undefined;
|
|
22
23
|
|
|
23
24
|
private disabled = false;
|
|
25
|
+
private healthcheckUploadIntervalId?: NodeJS.Timeout;
|
|
26
|
+
|
|
27
|
+
/** Cached beacon genesis time (seconds since Unix epoch). Fetched once at startup. */
|
|
28
|
+
private beaconGenesisTime?: bigint;
|
|
29
|
+
/** Cached beacon slot duration in seconds. Fetched once at startup. */
|
|
30
|
+
private beaconSecondsPerSlot?: number;
|
|
24
31
|
|
|
25
32
|
constructor(
|
|
26
33
|
config?: BlobClientConfig,
|
|
@@ -213,8 +220,8 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
213
220
|
const getFilledBlobs = (): Blob[] => resultBlobs.filter((b): b is Blob => b !== undefined);
|
|
214
221
|
|
|
215
222
|
// Helper to fill in results from fetched blobs
|
|
216
|
-
const fillResults = (fetchedBlobs: BlobJson[]): Blob[] => {
|
|
217
|
-
const blobs = processFetchedBlobs(fetchedBlobs, blobHashes, this.log);
|
|
223
|
+
const fillResults = async (fetchedBlobs: BlobJson[]): Promise<Blob[]> => {
|
|
224
|
+
const blobs = await processFetchedBlobs(fetchedBlobs, blobHashes, this.log);
|
|
218
225
|
// Fill in any missing positions with matching blobs
|
|
219
226
|
for (let i = 0; i < blobHashes.length; i++) {
|
|
220
227
|
if (resultBlobs[i] === undefined) {
|
|
@@ -249,7 +256,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
249
256
|
// The beacon api can query by slot number, so we get that first
|
|
250
257
|
const consensusCtx = { l1ConsensusHostUrls, ...ctx };
|
|
251
258
|
this.log.trace(`Attempting to get slot number for block hash`, consensusCtx);
|
|
252
|
-
const slotNumber = await this.getSlotNumber(blockHash);
|
|
259
|
+
const slotNumber = await this.getSlotNumber(blockHash, opts?.parentBeaconBlockRoot, opts?.l1BlockTimestamp);
|
|
253
260
|
this.log.debug(`Got slot number ${slotNumber} from consensus host for querying blobs`, consensusCtx);
|
|
254
261
|
|
|
255
262
|
if (slotNumber) {
|
|
@@ -266,8 +273,13 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
266
273
|
l1ConsensusHostUrl,
|
|
267
274
|
...ctx,
|
|
268
275
|
});
|
|
269
|
-
const blobs = await this.getBlobsFromHost(
|
|
270
|
-
|
|
276
|
+
const blobs = await this.getBlobsFromHost(
|
|
277
|
+
l1ConsensusHostUrl,
|
|
278
|
+
slotNumber,
|
|
279
|
+
l1ConsensusHostIndex,
|
|
280
|
+
getMissingBlobHashes(),
|
|
281
|
+
);
|
|
282
|
+
const result = await fillResults(blobs);
|
|
271
283
|
this.log.debug(
|
|
272
284
|
`Got ${blobs.length} blobs from consensus host (total: ${result.length}/${blobHashes.length})`,
|
|
273
285
|
{ slotNumber, l1ConsensusHostUrl, ...ctx },
|
|
@@ -310,7 +322,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
310
322
|
this.log.debug('No blobs found from archive client', archiveCtx);
|
|
311
323
|
} else {
|
|
312
324
|
this.log.trace(`Got ${allBlobs.length} blobs from archive client before filtering`, archiveCtx);
|
|
313
|
-
const result = fillResults(allBlobs);
|
|
325
|
+
const result = await fillResults(allBlobs);
|
|
314
326
|
this.log.debug(
|
|
315
327
|
`Got ${allBlobs.length} blobs from archive client (total: ${result.length}/${blobHashes.length})`,
|
|
316
328
|
archiveCtx,
|
|
@@ -343,7 +355,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
343
355
|
*/
|
|
344
356
|
private async tryFileStores(
|
|
345
357
|
getMissingBlobHashes: () => Buffer[],
|
|
346
|
-
fillResults: (blobs: BlobJson[]) => Blob[]
|
|
358
|
+
fillResults: (blobs: BlobJson[]) => Promise<Blob[]>,
|
|
347
359
|
ctx: { blockHash: string; blobHashes: string[] },
|
|
348
360
|
): Promise<void> {
|
|
349
361
|
// Shuffle clients for load distribution
|
|
@@ -364,7 +376,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
364
376
|
});
|
|
365
377
|
const blobs = await client.getBlobsByHashes(blobHashStrings);
|
|
366
378
|
if (blobs.length > 0) {
|
|
367
|
-
const result = fillResults(blobs);
|
|
379
|
+
const result = await fillResults(blobs);
|
|
368
380
|
this.log.debug(
|
|
369
381
|
`Got ${blobs.length} blobs from filestore (total: ${result.length}/${ctx.blobHashes.length})`,
|
|
370
382
|
{
|
|
@@ -385,19 +397,20 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
385
397
|
blobHashes: Buffer[] = [],
|
|
386
398
|
l1ConsensusHostIndex?: number,
|
|
387
399
|
): Promise<Blob[]> {
|
|
388
|
-
const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
|
|
389
|
-
return processFetchedBlobs(blobs, blobHashes, this.log).filter((b): b is Blob => b !== undefined);
|
|
400
|
+
const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
|
|
401
|
+
return (await processFetchedBlobs(blobs, blobHashes, this.log)).filter((b): b is Blob => b !== undefined);
|
|
390
402
|
}
|
|
391
403
|
|
|
392
404
|
public async getBlobsFromHost(
|
|
393
405
|
hostUrl: string,
|
|
394
406
|
blockHashOrSlot: string | number,
|
|
395
407
|
l1ConsensusHostIndex?: number,
|
|
408
|
+
blobHashes?: Buffer[],
|
|
396
409
|
): Promise<BlobJson[]> {
|
|
397
410
|
try {
|
|
398
|
-
let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
|
|
411
|
+
let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
|
|
399
412
|
if (res.ok) {
|
|
400
|
-
return parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
413
|
+
return await parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
401
414
|
}
|
|
402
415
|
|
|
403
416
|
if (res.status === 404 && typeof blockHashOrSlot === 'number') {
|
|
@@ -412,9 +425,9 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
412
425
|
let currentSlot = blockHashOrSlot + 1;
|
|
413
426
|
while (res.status === 404 && maxRetries > 0 && latestSlot !== undefined && currentSlot <= latestSlot) {
|
|
414
427
|
this.log.debug(`Trying slot ${currentSlot}`);
|
|
415
|
-
res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex);
|
|
428
|
+
res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex, blobHashes);
|
|
416
429
|
if (res.ok) {
|
|
417
|
-
return parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
430
|
+
return await parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
418
431
|
}
|
|
419
432
|
currentSlot++;
|
|
420
433
|
maxRetries--;
|
|
@@ -437,8 +450,17 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
437
450
|
hostUrl: string,
|
|
438
451
|
blockHashOrSlot: string | number,
|
|
439
452
|
l1ConsensusHostIndex?: number,
|
|
453
|
+
blobHashes?: Buffer[],
|
|
440
454
|
): Promise<Response> {
|
|
441
|
-
|
|
455
|
+
let baseUrl = `${hostUrl}/eth/v1/beacon/blobs/${blockHashOrSlot}`;
|
|
456
|
+
|
|
457
|
+
if (blobHashes && blobHashes.length > 0) {
|
|
458
|
+
const params = new URLSearchParams();
|
|
459
|
+
for (const hash of blobHashes) {
|
|
460
|
+
params.append('versioned_hashes', `0x${hash.toString('hex')}`);
|
|
461
|
+
}
|
|
462
|
+
baseUrl += `?${params.toString()}`;
|
|
463
|
+
}
|
|
442
464
|
|
|
443
465
|
const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
|
|
444
466
|
this.log.debug(`Fetching blob sidecar for ${blockHashOrSlot}`, { url, ...options });
|
|
@@ -480,34 +502,50 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
480
502
|
* @param blockHash - The block hash
|
|
481
503
|
* @returns The slot number
|
|
482
504
|
*/
|
|
483
|
-
private async getSlotNumber(
|
|
505
|
+
private async getSlotNumber(
|
|
506
|
+
blockHash: `0x${string}`,
|
|
507
|
+
parentBeaconBlockRoot?: string,
|
|
508
|
+
l1BlockTimestamp?: bigint,
|
|
509
|
+
): Promise<number | undefined> {
|
|
484
510
|
const { l1ConsensusHostUrls, l1RpcUrls } = this.config;
|
|
485
511
|
if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
|
|
486
512
|
this.log.debug('No consensus host url configured');
|
|
487
513
|
return undefined;
|
|
488
514
|
}
|
|
489
515
|
|
|
490
|
-
if (
|
|
491
|
-
|
|
492
|
-
|
|
516
|
+
// Primary path: compute slot from timestamp if genesis config is cached (no network call needed)
|
|
517
|
+
if (
|
|
518
|
+
l1BlockTimestamp !== undefined &&
|
|
519
|
+
this.beaconGenesisTime !== undefined &&
|
|
520
|
+
this.beaconSecondsPerSlot !== undefined
|
|
521
|
+
) {
|
|
522
|
+
const slot = Number((l1BlockTimestamp - this.beaconGenesisTime) / BigInt(this.beaconSecondsPerSlot));
|
|
523
|
+
this.log.debug(`Computed slot ${slot} from L1 block timestamp`, { l1BlockTimestamp });
|
|
524
|
+
return slot;
|
|
493
525
|
}
|
|
494
526
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
527
|
+
if (!parentBeaconBlockRoot) {
|
|
528
|
+
// parentBeaconBlockRoot not provided by caller — fetch it from the execution RPC
|
|
529
|
+
if (!l1RpcUrls || l1RpcUrls.length === 0) {
|
|
530
|
+
this.log.debug('No execution host url configured');
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const client = createPublicClient({
|
|
535
|
+
transport: fallback(l1RpcUrls.map(url => http(url, { batch: false }))),
|
|
504
536
|
});
|
|
537
|
+
try {
|
|
538
|
+
const res: RpcBlock = await client.request({
|
|
539
|
+
method: 'eth_getBlockByHash',
|
|
540
|
+
params: [blockHash, /*tx flag*/ false],
|
|
541
|
+
});
|
|
505
542
|
|
|
506
|
-
|
|
507
|
-
|
|
543
|
+
if (res.parentBeaconBlockRoot) {
|
|
544
|
+
parentBeaconBlockRoot = res.parentBeaconBlockRoot;
|
|
545
|
+
}
|
|
546
|
+
} catch (err) {
|
|
547
|
+
this.log.error(`Error getting parent beacon block root`, err);
|
|
508
548
|
}
|
|
509
|
-
} catch (err) {
|
|
510
|
-
this.log.error(`Error getting parent beacon block root`, err);
|
|
511
549
|
}
|
|
512
550
|
|
|
513
551
|
if (!parentBeaconBlockRoot) {
|
|
@@ -545,12 +583,105 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
545
583
|
public getArchiveClient(): BlobArchiveClient | undefined {
|
|
546
584
|
return this.archiveClient;
|
|
547
585
|
}
|
|
586
|
+
|
|
587
|
+
/** Returns true if this client can upload blobs to filestore. */
|
|
588
|
+
public canUpload(): boolean {
|
|
589
|
+
return this.fileStoreUploadClient !== undefined;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Start the blob client.
|
|
594
|
+
* Fetches and caches beacon genesis config for timestamp-based slot resolution,
|
|
595
|
+
* then uploads the initial healthcheck file (awaited) and starts periodic uploads.
|
|
596
|
+
*/
|
|
597
|
+
public async start(): Promise<void> {
|
|
598
|
+
await this.fetchBeaconConfig();
|
|
599
|
+
|
|
600
|
+
if (!this.fileStoreUploadClient) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
await this.fileStoreUploadClient.uploadHealthcheck();
|
|
605
|
+
this.log.debug('Initial healthcheck file uploaded');
|
|
606
|
+
|
|
607
|
+
this.startPeriodicHealthcheckUpload();
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Start periodic healthcheck upload to the file store to ensure it remains available even if accidentally deleted.
|
|
612
|
+
*/
|
|
613
|
+
private startPeriodicHealthcheckUpload(): void {
|
|
614
|
+
const intervalMs =
|
|
615
|
+
(this.config.blobHealthcheckUploadIntervalMinutes ?? DEFAULT_HEALTHCHECK_UPLOAD_INTERVAL_MINUTES) * 60 * 1000;
|
|
616
|
+
|
|
617
|
+
this.healthcheckUploadIntervalId = setInterval(() => {
|
|
618
|
+
void this.fileStoreUploadClient!.uploadHealthcheck().catch(err => {
|
|
619
|
+
this.log.warn('Failed to upload periodic healthcheck file', err);
|
|
620
|
+
});
|
|
621
|
+
}, intervalMs);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Fetches and caches beacon genesis time and slot duration from the first available consensus host.
|
|
626
|
+
* These static values enable timestamp-based slot resolution, eliminating the per-fetch headers call.
|
|
627
|
+
* Logs a warning and leaves fields undefined if all hosts fail, callers fall back gracefully.
|
|
628
|
+
*/
|
|
629
|
+
private async fetchBeaconConfig(): Promise<void> {
|
|
630
|
+
const { l1ConsensusHostUrls } = this.config;
|
|
631
|
+
if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
for (let i = 0; i < l1ConsensusHostUrls.length; i++) {
|
|
636
|
+
try {
|
|
637
|
+
const { url: genesisUrl, ...genesisOptions } = getBeaconNodeFetchOptions(
|
|
638
|
+
`${l1ConsensusHostUrls[i]}/eth/v1/config/genesis`,
|
|
639
|
+
this.config,
|
|
640
|
+
i,
|
|
641
|
+
);
|
|
642
|
+
const { url: specUrl, ...specOptions } = getBeaconNodeFetchOptions(
|
|
643
|
+
`${l1ConsensusHostUrls[i]}/eth/v1/config/spec`,
|
|
644
|
+
this.config,
|
|
645
|
+
i,
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
const [genesisRes, specRes] = await Promise.all([
|
|
649
|
+
this.fetch(genesisUrl, genesisOptions),
|
|
650
|
+
this.fetch(specUrl, specOptions),
|
|
651
|
+
]);
|
|
652
|
+
|
|
653
|
+
if (genesisRes.ok && specRes.ok) {
|
|
654
|
+
const genesis = await genesisRes.json();
|
|
655
|
+
const spec = await specRes.json();
|
|
656
|
+
this.beaconGenesisTime = BigInt(genesis.data.genesisTime);
|
|
657
|
+
this.beaconSecondsPerSlot = parseInt(spec.data.secondsPerSlot);
|
|
658
|
+
this.log.debug(`Fetched beacon genesis config`, {
|
|
659
|
+
genesisTime: this.beaconGenesisTime,
|
|
660
|
+
secondsPerSlot: this.beaconSecondsPerSlot,
|
|
661
|
+
});
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
} catch (err) {
|
|
665
|
+
this.log.warn(`Failed to fetch beacon config from host ${l1ConsensusHostUrls[i]}`, err);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
this.log.warn('Could not fetch beacon genesis config from any consensus host — will use headers call fallback');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Stop the blob client, clearing any periodic tasks.
|
|
673
|
+
*/
|
|
674
|
+
public stop(): void {
|
|
675
|
+
if (this.healthcheckUploadIntervalId) {
|
|
676
|
+
clearInterval(this.healthcheckUploadIntervalId);
|
|
677
|
+
this.healthcheckUploadIntervalId = undefined;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
548
680
|
}
|
|
549
681
|
|
|
550
|
-
function parseBlobJsonsFromResponse(response: any, logger: Logger): BlobJson[] {
|
|
682
|
+
async function parseBlobJsonsFromResponse(response: any, logger: Logger): Promise<BlobJson[]> {
|
|
551
683
|
try {
|
|
552
|
-
|
|
553
|
-
return blobs;
|
|
684
|
+
return await Promise.all((response.data as string[]).map(parseBlobJson));
|
|
554
685
|
} catch (err) {
|
|
555
686
|
logger.error(`Error parsing blob json from response`, err);
|
|
556
687
|
return [];
|
|
@@ -561,16 +692,19 @@ function parseBlobJsonsFromResponse(response: any, logger: Logger): BlobJson[] {
|
|
|
561
692
|
// https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getBlobSidecars
|
|
562
693
|
// Here we attempt to parse the response data to Buffer, and check the lengths (via Blob's constructor), to avoid
|
|
563
694
|
// throwing an error down the line when calling Blob.fromJson().
|
|
564
|
-
function parseBlobJson(
|
|
565
|
-
const blobBuffer = Buffer.from(
|
|
566
|
-
const
|
|
567
|
-
const blob = new Blob(blobBuffer, commitmentBuffer);
|
|
695
|
+
async function parseBlobJson(rawHex: string): Promise<BlobJson> {
|
|
696
|
+
const blobBuffer = Buffer.from(rawHex.slice(2), 'hex');
|
|
697
|
+
const blob = await Blob.fromBlobBuffer(blobBuffer);
|
|
568
698
|
return blob.toJSON();
|
|
569
699
|
}
|
|
570
700
|
|
|
571
701
|
// Returns an array that maps each blob hash to the corresponding blob, or undefined if the blob is not found
|
|
572
702
|
// or the data does not match the commitment.
|
|
573
|
-
function processFetchedBlobs(
|
|
703
|
+
async function processFetchedBlobs(
|
|
704
|
+
blobs: BlobJson[],
|
|
705
|
+
blobHashes: Buffer[],
|
|
706
|
+
logger: Logger,
|
|
707
|
+
): Promise<(Blob | undefined)[]> {
|
|
574
708
|
const requestedBlobHashes = new Set<string>(blobHashes.map(bufferToHex));
|
|
575
709
|
const hashToBlob = new Map<string, Blob>();
|
|
576
710
|
for (const blobJson of blobs) {
|
|
@@ -580,7 +714,7 @@ function processFetchedBlobs(blobs: BlobJson[], blobHashes: Buffer[], logger: Lo
|
|
|
580
714
|
}
|
|
581
715
|
|
|
582
716
|
try {
|
|
583
|
-
const blob = Blob.fromJson(blobJson);
|
|
717
|
+
const blob = await Blob.fromJson(blobJson);
|
|
584
718
|
hashToBlob.set(hashHex, blob);
|
|
585
719
|
} catch (err) {
|
|
586
720
|
// If the above throws, it's likely that the blob commitment does not match the hash of the blob data.
|
package/src/client/interface.ts
CHANGED
|
@@ -11,6 +11,17 @@ export interface GetBlobSidecarOptions {
|
|
|
11
11
|
* - Near tip: FileStore first with no retries (data should exist), L1 consensus second (freshest data), then FileStore with retries, then archive (eg. blobscan)
|
|
12
12
|
*/
|
|
13
13
|
isHistoricalSync?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The parent beacon block root for the L1 block containing the blobs.
|
|
16
|
+
* If provided, skips the eth_getBlockByHash execution RPC call inside getSlotNumber.
|
|
17
|
+
*/
|
|
18
|
+
parentBeaconBlockRoot?: string;
|
|
19
|
+
/**
|
|
20
|
+
* The timestamp of the L1 execution block containing the blobs.
|
|
21
|
+
* When provided alongside a cached beacon genesis config (fetched at startup), allows computing
|
|
22
|
+
* the beacon slot directly via timestamp math, skipping the beacon headers network call entirely.
|
|
23
|
+
*/
|
|
24
|
+
l1BlockTimestamp?: bigint;
|
|
14
25
|
}
|
|
15
26
|
|
|
16
27
|
export interface BlobClientInterface {
|
|
@@ -18,6 +29,12 @@ export interface BlobClientInterface {
|
|
|
18
29
|
sendBlobsToFilestore(blobs: Blob[]): Promise<boolean>;
|
|
19
30
|
/** Fetches the given blob sidecars by block hash and blob hashes. */
|
|
20
31
|
getBlobSidecar(blockId: string, blobHashes?: Buffer[], opts?: GetBlobSidecarOptions): Promise<Blob[]>;
|
|
32
|
+
/** Starts the blob client (e.g., uploads healthcheck file if not exists). */
|
|
33
|
+
start?(): Promise<void>;
|
|
21
34
|
/** Tests all configured blob sources and logs whether they are reachable or not. */
|
|
22
35
|
testSources(): Promise<void>;
|
|
36
|
+
/** Stops the blob client, clearing any periodic tasks. */
|
|
37
|
+
stop?(): void;
|
|
38
|
+
/** Returns true if this client can upload blobs to filestore. */
|
|
39
|
+
canUpload(): boolean;
|
|
23
40
|
}
|
package/src/client/local.ts
CHANGED
|
@@ -22,4 +22,9 @@ export class LocalBlobClient implements BlobClientInterface {
|
|
|
22
22
|
public getBlobSidecar(_blockId: string, blobHashes: Buffer[], _opts?: GetBlobSidecarOptions): Promise<Blob[]> {
|
|
23
23
|
return this.blobStore.getBlobsByHashes(blobHashes);
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
/** Returns true if this client can upload blobs. Always true for local client. */
|
|
27
|
+
public canUpload(): boolean {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
25
30
|
}
|
package/src/client/tests.ts
CHANGED
|
@@ -28,7 +28,7 @@ export function runBlobClientTests(
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
it('should send and retrieve blobs by hash', async () => {
|
|
31
|
-
const blob = makeRandomBlob(5);
|
|
31
|
+
const blob = await makeRandomBlob(5);
|
|
32
32
|
const blobHash = blob.getEthVersionedBlobHash();
|
|
33
33
|
|
|
34
34
|
await client.sendBlobsToFilestore([blob]);
|
|
@@ -39,7 +39,7 @@ export function runBlobClientTests(
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
it('should handle multiple blobs', async () => {
|
|
42
|
-
const blobs = Array.from({ length: 3 }, () => makeRandomBlob(7));
|
|
42
|
+
const blobs = await Promise.all(Array.from({ length: 3 }, () => makeRandomBlob(7)));
|
|
43
43
|
const blobHashes = blobs.map(blob => blob.getEthVersionedBlobHash());
|
|
44
44
|
|
|
45
45
|
await client.sendBlobsToFilestore(blobs);
|
|
@@ -3,6 +3,7 @@ import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
|
3
3
|
import type { FileStore, ReadOnlyFileStore } from '@aztec/stdlib/file-store';
|
|
4
4
|
|
|
5
5
|
import { inboundTransform, outboundTransform } from '../encoding/index.js';
|
|
6
|
+
import { HEALTHCHECK_CONTENT, HEALTHCHECK_FILENAME } from './healthcheck.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* A blob client that uses a FileStore (S3/GCS/local) as the data source.
|
|
@@ -27,6 +28,14 @@ export class FileStoreBlobClient {
|
|
|
27
28
|
return `${this.basePath}/blobs/${versionedBlobHash}.data`;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Get the path for the healthcheck file.
|
|
33
|
+
* Format: basePath/.healthcheck
|
|
34
|
+
*/
|
|
35
|
+
private healthcheckPath(): string {
|
|
36
|
+
return `${this.basePath}/${HEALTHCHECK_FILENAME}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
/**
|
|
31
40
|
* Fetch blobs by their versioned hashes.
|
|
32
41
|
* @param blobHashes - Array of versioned blob hashes (0x-prefixed hex strings)
|
|
@@ -104,12 +113,31 @@ export class FileStoreBlobClient {
|
|
|
104
113
|
}
|
|
105
114
|
|
|
106
115
|
/**
|
|
107
|
-
* Test if the filestore connection is working.
|
|
116
|
+
* Test if the filestore connection is working by checking for healthcheck file.
|
|
117
|
+
* The healthcheck file is uploaded periodically by writable clients via HttpBlobClient.start().
|
|
118
|
+
* This provides a uniform connection test across all store types (S3/GCS/Local/HTTP).
|
|
119
|
+
*/
|
|
120
|
+
async testConnection(): Promise<boolean> {
|
|
121
|
+
try {
|
|
122
|
+
return await this.store.exists(this.healthcheckPath());
|
|
123
|
+
} catch (err: any) {
|
|
124
|
+
this.log.warn(`Connection test failed: ${err?.message ?? String(err)}`);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Upload the healthcheck file if it doesn't already exist.
|
|
131
|
+
* This enables read-only clients (HTTP) to verify connectivity.
|
|
108
132
|
*/
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
133
|
+
async uploadHealthcheck(): Promise<void> {
|
|
134
|
+
if (!this.isWritable()) {
|
|
135
|
+
this.log.trace('Cannot upload healthcheck: store is read-only');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const path = this.healthcheckPath();
|
|
139
|
+
await (this.store as FileStore).save(path, Buffer.from(HEALTHCHECK_CONTENT));
|
|
140
|
+
this.log.debug(`Uploaded healthcheck file to ${path}`);
|
|
113
141
|
}
|
|
114
142
|
|
|
115
143
|
/**
|