@aztec/blob-client 0.0.1-commit.fcb71a6 → 0.0.1-commit.ffe5b04ea

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 CHANGED
@@ -31,7 +31,7 @@ URL for uploading blobs to a file store.
31
31
  **L1 Consensus Host URLs** (`L1_CONSENSUS_HOST_URLS`):
32
32
  Beacon node URLs for fetching recent blobs directly from L1.
33
33
 
34
- **Archive API URL** (`BLOB_SINK_ARCHIVE_API_URL`):
34
+ **Archive API URL** (`BLOB_ARCHIVE_API_URL`):
35
35
  Blobscan or similar archive API for historical blob data.
36
36
 
37
37
  ### File Store Connectivity Testing
@@ -2,7 +2,7 @@ import { l1ReaderConfigMappings } from '@aztec/ethereum/l1-reader';
2
2
  import { pickConfigMappings } from '@aztec/foundation/config';
3
3
  export const blobArchiveApiConfigMappings = {
4
4
  archiveApiUrl: {
5
- env: 'BLOB_SINK_ARCHIVE_API_URL',
5
+ env: 'BLOB_ARCHIVE_API_URL',
6
6
  description: 'The URL of the archive API'
7
7
  },
8
8
  ...pickConfigMappings(l1ReaderConfigMappings, [
@@ -8,4 +8,4 @@ export declare class BlobArchiveClientInstrumentation {
8
8
  incRequest(type: 'blocks' | 'blobs', status: number): void;
9
9
  incRetrievedBlobs(count: number): void;
10
10
  }
11
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5zdHJ1bWVudGF0aW9uLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYXJjaGl2ZS9pbnN0cnVtZW50YXRpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUF1QixLQUFLLGVBQWUsRUFBaUMsTUFBTSx5QkFBeUIsQ0FBQztBQUVuSCxxQkFBYSxnQ0FBZ0M7SUFPekMsT0FBTyxDQUFDLFFBQVE7SUFObEIsT0FBTyxDQUFDLG1CQUFtQixDQUFnQjtJQUMzQyxPQUFPLENBQUMsa0JBQWtCLENBQWdCO0lBQzFDLE9BQU8sQ0FBQyxjQUFjLENBQWdCO0lBRXRDLFlBQ0UsTUFBTSxFQUFFLGVBQWUsRUFDZixRQUFRLEVBQUUsTUFBTSxFQUN4QixJQUFJLEVBQUUsTUFBTSxFQWlCYjtJQUVELFVBQVUsQ0FBQyxJQUFJLEVBQUUsUUFBUSxHQUFHLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxRQU1sRDtJQUVELGlCQUFpQixDQUFDLEtBQUssRUFBRSxNQUFNLFFBRTlCO0NBQ0YifQ==
11
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5zdHJ1bWVudGF0aW9uLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYXJjaGl2ZS9pbnN0cnVtZW50YXRpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUdMLEtBQUssZUFBZSxFQUdyQixNQUFNLHlCQUF5QixDQUFDO0FBRWpDLHFCQUFhLGdDQUFnQztJQU96QyxPQUFPLENBQUMsUUFBUTtJQU5sQixPQUFPLENBQUMsbUJBQW1CLENBQWdCO0lBQzNDLE9BQU8sQ0FBQyxrQkFBa0IsQ0FBZ0I7SUFDMUMsT0FBTyxDQUFDLGNBQWMsQ0FBZ0I7SUFFdEMsWUFDRSxNQUFNLEVBQUUsZUFBZSxFQUNmLFFBQVEsRUFBRSxNQUFNLEVBQ3hCLElBQUksRUFBRSxNQUFNLEVBb0JiO0lBRUQsVUFBVSxDQUFDLElBQUksRUFBRSxRQUFRLEdBQUcsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLFFBTWxEO0lBRUQsaUJBQWlCLENBQUMsS0FBSyxFQUFFLE1BQU0sUUFFOUI7Q0FDRiJ9
@@ -1 +1 @@
1
- {"version":3,"file":"instrumentation.d.ts","sourceRoot":"","sources":["../../src/archive/instrumentation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,eAAe,EAAiC,MAAM,yBAAyB,CAAC;AAEnH,qBAAa,gCAAgC;IAOzC,OAAO,CAAC,QAAQ;IANlB,OAAO,CAAC,mBAAmB,CAAgB;IAC3C,OAAO,CAAC,kBAAkB,CAAgB;IAC1C,OAAO,CAAC,cAAc,CAAgB;IAEtC,YACE,MAAM,EAAE,eAAe,EACf,QAAQ,EAAE,MAAM,EACxB,IAAI,EAAE,MAAM,EAiBb;IAED,UAAU,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,EAAE,MAAM,EAAE,MAAM,QAMlD;IAED,iBAAiB,CAAC,KAAK,EAAE,MAAM,QAE9B;CACF"}
1
+ {"version":3,"file":"instrumentation.d.ts","sourceRoot":"","sources":["../../src/archive/instrumentation.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,eAAe,EAGrB,MAAM,yBAAyB,CAAC;AAEjC,qBAAa,gCAAgC;IAOzC,OAAO,CAAC,QAAQ;IANlB,OAAO,CAAC,mBAAmB,CAAgB;IAC3C,OAAO,CAAC,kBAAkB,CAAgB;IAC1C,OAAO,CAAC,cAAc,CAAgB;IAEtC,YACE,MAAM,EAAE,eAAe,EACf,QAAQ,EAAE,MAAM,EACxB,IAAI,EAAE,MAAM,EAoBb;IAED,UAAU,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,EAAE,MAAM,EAAE,MAAM,QAMlD;IAED,iBAAiB,CAAC,KAAK,EAAE,MAAM,QAE9B;CACF"}
@@ -1,4 +1,4 @@
1
- import { Attributes, Metrics, ValueType } from '@aztec/telemetry-client';
1
+ import { Attributes, Metrics, createUpDownCounterWithDefault } from '@aztec/telemetry-client';
2
2
  export class BlobArchiveClientInstrumentation {
3
3
  httpHost;
4
4
  blockRequestCounter;
@@ -7,18 +7,18 @@ export class BlobArchiveClientInstrumentation {
7
7
  constructor(client, httpHost, name){
8
8
  this.httpHost = httpHost;
9
9
  const meter = client.getMeter(name);
10
- this.blockRequestCounter = meter.createUpDownCounter(Metrics.BLOB_SINK_ARCHIVE_BLOCK_REQUEST_COUNT, {
11
- description: 'Number of requests made to retrieve blocks from the blob archive',
12
- valueType: ValueType.INT
13
- });
14
- this.blobRequestCounter = meter.createUpDownCounter(Metrics.BLOB_SINK_ARCHIVE_BLOB_REQUEST_COUNT, {
15
- description: 'Number of requests made to retrieve blobs from the blob archive',
16
- valueType: ValueType.INT
17
- });
18
- this.retrievedBlobs = meter.createUpDownCounter(Metrics.BLOB_SINK_ARCHIVE_BLOB_COUNT, {
19
- description: 'Number of blobs retrieved from the blob archive',
20
- valueType: ValueType.INT
21
- });
10
+ const requestAttrs = {
11
+ [Attributes.HTTP_RESPONSE_STATUS_CODE]: [
12
+ 200,
13
+ 404
14
+ ],
15
+ [Attributes.HTTP_REQUEST_HOST]: [
16
+ httpHost
17
+ ]
18
+ };
19
+ this.blockRequestCounter = createUpDownCounterWithDefault(meter, Metrics.BLOB_SINK_ARCHIVE_BLOCK_REQUEST_COUNT, requestAttrs);
20
+ this.blobRequestCounter = createUpDownCounterWithDefault(meter, Metrics.BLOB_SINK_ARCHIVE_BLOB_REQUEST_COUNT, requestAttrs);
21
+ this.retrievedBlobs = createUpDownCounterWithDefault(meter, Metrics.BLOB_SINK_ARCHIVE_BLOB_COUNT);
22
22
  }
23
23
  incRequest(type, status) {
24
24
  const counter = type === 'blocks' ? this.blockRequestCounter : this.blobRequestCounter;
@@ -12,7 +12,7 @@ export function describeBlobStore(getBlobStore) {
12
12
  Fr.random(),
13
13
  Fr.random()
14
14
  ];
15
- const blob = Blob.fromFields(testFields);
15
+ const blob = await Blob.fromFields(testFields);
16
16
  const blobHash = blob.getEthVersionedBlobHash();
17
17
  // Store the blob
18
18
  await blobStore.addBlobs([
@@ -28,11 +28,11 @@ export function describeBlobStore(getBlobStore) {
28
28
  });
29
29
  it('should handle multiple blobs stored and retrieved by their hashes', async ()=>{
30
30
  // Create two different blobs
31
- const blob1 = Blob.fromFields([
31
+ const blob1 = await Blob.fromFields([
32
32
  Fr.random(),
33
33
  Fr.random()
34
34
  ]);
35
- const blob2 = Blob.fromFields([
35
+ const blob2 = await Blob.fromFields([
36
36
  Fr.random(),
37
37
  Fr.random(),
38
38
  Fr.random()
@@ -64,13 +64,13 @@ export function describeBlobStore(getBlobStore) {
64
64
  });
65
65
  it('should handle retrieving subset of stored blobs', async ()=>{
66
66
  // Store multiple blobs
67
- const blob1 = Blob.fromFields([
67
+ const blob1 = await Blob.fromFields([
68
68
  Fr.random()
69
69
  ]);
70
- const blob2 = Blob.fromFields([
70
+ const blob2 = await Blob.fromFields([
71
71
  Fr.random()
72
72
  ]);
73
- const blob3 = Blob.fromFields([
73
+ const blob3 = await Blob.fromFields([
74
74
  Fr.random()
75
75
  ]);
76
76
  await blobStore.addBlobs([
@@ -90,7 +90,7 @@ export function describeBlobStore(getBlobStore) {
90
90
  expect(retrievedBlobs[1]).toEqual(blob3);
91
91
  });
92
92
  it('should handle duplicate blob hashes in request', async ()=>{
93
- const blob = Blob.fromFields([
93
+ const blob = await Blob.fromFields([
94
94
  Fr.random()
95
95
  ]);
96
96
  const blobHash = blob.getEthVersionedBlobHash();
@@ -112,8 +112,8 @@ export function describeBlobStore(getBlobStore) {
112
112
  Fr.random(),
113
113
  Fr.random()
114
114
  ];
115
- const blob1 = Blob.fromFields(fields);
116
- const blob2 = Blob.fromFields(fields);
115
+ const blob1 = await Blob.fromFields(fields);
116
+ const blob2 = await Blob.fromFields(fields);
117
117
  const blobHash = blob1.getEthVersionedBlobHash();
118
118
  // Store first blob
119
119
  await blobStore.addBlobs([
@@ -14,6 +14,10 @@ export declare class HttpBlobClient implements BlobClientInterface {
14
14
  protected readonly fileStoreUploadClient: FileStoreBlobClient | undefined;
15
15
  private disabled;
16
16
  private healthcheckUploadIntervalId?;
17
+ /** Cached beacon genesis time (seconds since Unix epoch). Fetched once at startup. */
18
+ private beaconGenesisTime?;
19
+ /** Cached beacon slot duration in seconds. Fetched once at startup. */
20
+ private beaconSecondsPerSlot?;
17
21
  constructor(config?: BlobClientConfig, opts?: {
18
22
  logger?: Logger;
19
23
  archiveClient?: BlobArchiveClient;
@@ -54,24 +58,28 @@ export declare class HttpBlobClient implements BlobClientInterface {
54
58
  getBlobSidecar(blockHash: `0x${string}`, blobHashes: Buffer[], opts?: GetBlobSidecarOptions): Promise<Blob[]>;
55
59
  private tryFileStores;
56
60
  getBlobSidecarFrom(hostUrl: string, blockHashOrSlot: string | number, blobHashes?: Buffer[], l1ConsensusHostIndex?: number): Promise<Blob[]>;
57
- getBlobsFromHost(hostUrl: string, blockHashOrSlot: string | number, l1ConsensusHostIndex?: number): Promise<BlobJson[]>;
61
+ getBlobsFromHost(hostUrl: string, blockHashOrSlot: string | number, l1ConsensusHostIndex?: number, blobHashes?: Buffer[]): Promise<BlobJson[]>;
58
62
  private fetchBlobSidecars;
59
63
  private getLatestSlotNumber;
60
64
  private getSlotNumber;
61
65
  /** @internal - exposed for testing */
62
66
  getArchiveClient(): BlobArchiveClient | undefined;
67
+ /** Returns true if this client can upload blobs to filestore. */
68
+ canUpload(): boolean;
63
69
  /**
64
70
  * Start the blob client.
65
- * Uploads the initial healthcheck file (awaited) and starts periodic uploads.
71
+ * Fetches and caches beacon genesis config for timestamp-based slot resolution,
72
+ * then uploads the initial healthcheck file (awaited) and starts periodic uploads.
66
73
  */
67
74
  start(): Promise<void>;
68
75
  /**
69
76
  * Start periodic healthcheck upload to the file store to ensure it remains available even if accidentally deleted.
70
77
  */
71
78
  private startPeriodicHealthcheckUpload;
79
+ private fetchBeaconConfig;
72
80
  /**
73
81
  * Stop the blob client, clearing any periodic tasks.
74
82
  */
75
83
  stop(): void;
76
84
  }
77
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaHR0cC5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2NsaWVudC9odHRwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxJQUFJLEVBQUUsS0FBSyxRQUFRLEVBQStCLE1BQU0saUJBQWlCLENBQUM7QUFFbkYsT0FBTyxFQUFFLEtBQUssTUFBTSxFQUFnQixNQUFNLHVCQUF1QixDQUFDO0FBT2xFLE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDakUsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSx1Q0FBdUMsQ0FBQztBQUVqRixPQUFPLEVBQUUsS0FBSyxnQkFBZ0IsRUFBOEIsTUFBTSxhQUFhLENBQUM7QUFDaEYsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVqRixxQkFBYSxjQUFlLFlBQVcsbUJBQW1CO0lBYXRELE9BQU8sQ0FBQyxRQUFRLENBQUMsSUFBSTtJQVp2QixTQUFTLENBQUMsUUFBUSxDQUFDLEdBQUcsRUFBRSxNQUFNLENBQUM7SUFDL0IsU0FBUyxDQUFDLFFBQVEsQ0FBQyxNQUFNLEVBQUUsZ0JBQWdCLENBQUM7SUFDNUMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxhQUFhLEVBQUUsaUJBQWlCLEdBQUcsU0FBUyxDQUFDO0lBQ2hFLFNBQVMsQ0FBQyxRQUFRLENBQUMsS0FBSyxFQUFFLE9BQU8sS0FBSyxDQUFDO0lBQ3ZDLFNBQVMsQ0FBQyxRQUFRLENBQUMsZ0JBQWdCLEVBQUUsbUJBQW1CLEVBQUUsQ0FBQztJQUMzRCxTQUFTLENBQUMsUUFBUSxDQUFDLHFCQUFxQixFQUFFLG1CQUFtQixHQUFHLFNBQVMsQ0FBQztJQUUxRSxPQUFPLENBQUMsUUFBUSxDQUFTO0lBQ3pCLE9BQU8sQ0FBQywyQkFBMkIsQ0FBQyxDQUFpQjtJQUVyRCxZQUNFLE1BQU0sQ0FBQyxFQUFFLGdCQUFnQixFQUNSLElBQUksR0FBRTtRQUNyQixNQUFNLENBQUMsRUFBRSxNQUFNLENBQUM7UUFDaEIsYUFBYSxDQUFDLEVBQUUsaUJBQWlCLENBQUM7UUFDbEMsZ0JBQWdCLENBQUMsRUFBRSxtQkFBbUIsRUFBRSxDQUFDO1FBQ3pDLHFCQUFxQixDQUFDLEVBQUUsbUJBQW1CLENBQUM7UUFDNUMseUVBQXlFO1FBQ3pFLGNBQWMsQ0FBQyxFQUFFLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxLQUFLLElBQUksQ0FBQztLQUNyQyxFQXdCUDtJQUVEOzs7T0FHRztJQUNILE9BQU8sQ0FBQyxzQkFBc0I7SUFVOUI7Ozs7O09BS0c7SUFDSSxXQUFXLENBQUMsS0FBSyxFQUFFLE9BQU8sR0FBRyxJQUFJLENBR3ZDO0lBRVksV0FBVyxrQkFvRXZCO0lBRVksb0JBQW9CLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxHQUFHLE9BQU8sQ0FBQyxPQUFPLENBQUMsQ0FtQmpFO0lBRUQ7Ozs7Ozs7Ozs7Ozs7O09BY0c7SUFDVSxjQUFjLENBQ3pCLFNBQVMsRUFBRSxLQUFLLE1BQU0sRUFBRSxFQUN4QixVQUFVLEVBQUUsTUFBTSxFQUFFLEVBQ3BCLElBQUksQ0FBQyxFQUFFLHFCQUFxQixHQUMzQixPQUFPLENBQUMsSUFBSSxFQUFFLENBQUMsQ0E2SWpCO1lBUWEsYUFBYTtJQXNDZCxrQkFBa0IsQ0FDN0IsT0FBTyxFQUFFLE1BQU0sRUFDZixlQUFlLEVBQUUsTUFBTSxHQUFHLE1BQU0sRUFDaEMsVUFBVSxHQUFFLE1BQU0sRUFBTyxFQUN6QixvQkFBb0IsQ0FBQyxFQUFFLE1BQU0sR0FDNUIsT0FBTyxDQUFDLElBQUksRUFBRSxDQUFDLENBR2pCO0lBRVksZ0JBQWdCLENBQzNCLE9BQU8sRUFBRSxNQUFNLEVBQ2YsZUFBZSxFQUFFLE1BQU0sR0FBRyxNQUFNLEVBQ2hDLG9CQUFvQixDQUFDLEVBQUUsTUFBTSxHQUM1QixPQUFPLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FzQ3JCO0lBRUQsT0FBTyxDQUFDLGlCQUFpQjtZQVlYLG1CQUFtQjtZQW1DbkIsYUFBYTtJQTZEM0Isc0NBQXNDO0lBQy9CLGdCQUFnQixJQUFJLGlCQUFpQixHQUFHLFNBQVMsQ0FFdkQ7SUFFRDs7O09BR0c7SUFDVSxLQUFLLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQVNsQztJQUVEOztPQUVHO0lBQ0gsT0FBTyxDQUFDLDhCQUE4QjtJQVd0Qzs7T0FFRztJQUNJLElBQUksSUFBSSxJQUFJLENBS2xCO0NBQ0YifQ==
85
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaHR0cC5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2NsaWVudC9odHRwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxJQUFJLEVBQUUsS0FBSyxRQUFRLEVBQStCLE1BQU0saUJBQWlCLENBQUM7QUFFbkYsT0FBTyxFQUFFLEtBQUssTUFBTSxFQUFnQixNQUFNLHVCQUF1QixDQUFDO0FBT2xFLE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDakUsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSx1Q0FBdUMsQ0FBQztBQUVqRixPQUFPLEVBQUUsS0FBSyxnQkFBZ0IsRUFBOEIsTUFBTSxhQUFhLENBQUM7QUFDaEYsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVqRixxQkFBYSxjQUFlLFlBQVcsbUJBQW1CO0lBa0J0RCxPQUFPLENBQUMsUUFBUSxDQUFDLElBQUk7SUFqQnZCLFNBQVMsQ0FBQyxRQUFRLENBQUMsR0FBRyxFQUFFLE1BQU0sQ0FBQztJQUMvQixTQUFTLENBQUMsUUFBUSxDQUFDLE1BQU0sRUFBRSxnQkFBZ0IsQ0FBQztJQUM1QyxTQUFTLENBQUMsUUFBUSxDQUFDLGFBQWEsRUFBRSxpQkFBaUIsR0FBRyxTQUFTLENBQUM7SUFDaEUsU0FBUyxDQUFDLFFBQVEsQ0FBQyxLQUFLLEVBQUUsT0FBTyxLQUFLLENBQUM7SUFDdkMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxnQkFBZ0IsRUFBRSxtQkFBbUIsRUFBRSxDQUFDO0lBQzNELFNBQVMsQ0FBQyxRQUFRLENBQUMscUJBQXFCLEVBQUUsbUJBQW1CLEdBQUcsU0FBUyxDQUFDO0lBRTFFLE9BQU8sQ0FBQyxRQUFRLENBQVM7SUFDekIsT0FBTyxDQUFDLDJCQUEyQixDQUFDLENBQWlCO0lBRXJELHNGQUFzRjtJQUN0RixPQUFPLENBQUMsaUJBQWlCLENBQUMsQ0FBUztJQUNuQyx1RUFBdUU7SUFDdkUsT0FBTyxDQUFDLG9CQUFvQixDQUFDLENBQVM7SUFFdEMsWUFDRSxNQUFNLENBQUMsRUFBRSxnQkFBZ0IsRUFDUixJQUFJLEdBQUU7UUFDckIsTUFBTSxDQUFDLEVBQUUsTUFBTSxDQUFDO1FBQ2hCLGFBQWEsQ0FBQyxFQUFFLGlCQUFpQixDQUFDO1FBQ2xDLGdCQUFnQixDQUFDLEVBQUUsbUJBQW1CLEVBQUUsQ0FBQztRQUN6QyxxQkFBcUIsQ0FBQyxFQUFFLG1CQUFtQixDQUFDO1FBQzVDLHlFQUF5RTtRQUN6RSxjQUFjLENBQUMsRUFBRSxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsS0FBSyxJQUFJLENBQUM7S0FDckMsRUF3QlA7SUFFRDs7O09BR0c7SUFDSCxPQUFPLENBQUMsc0JBQXNCO0lBVTlCOzs7OztPQUtHO0lBQ0ksV0FBVyxDQUFDLEtBQUssRUFBRSxPQUFPLEdBQUcsSUFBSSxDQUd2QztJQUVZLFdBQVcsa0JBb0V2QjtJQUVZLG9CQUFvQixDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLENBbUJqRTtJQUVEOzs7Ozs7Ozs7Ozs7OztPQWNHO0lBQ1UsY0FBYyxDQUN6QixTQUFTLEVBQUUsS0FBSyxNQUFNLEVBQUUsRUFDeEIsVUFBVSxFQUFFLE1BQU0sRUFBRSxFQUNwQixJQUFJLENBQUMsRUFBRSxxQkFBcUIsR0FDM0IsT0FBTyxDQUFDLElBQUksRUFBRSxDQUFDLENBa0pqQjtZQVFhLGFBQWE7SUFzQ2Qsa0JBQWtCLENBQzdCLE9BQU8sRUFBRSxNQUFNLEVBQ2YsZUFBZSxFQUFFLE1BQU0sR0FBRyxNQUFNLEVBQ2hDLFVBQVUsR0FBRSxNQUFNLEVBQU8sRUFDekIsb0JBQW9CLENBQUMsRUFBRSxNQUFNLEdBQzVCLE9BQU8sQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUdqQjtJQUVZLGdCQUFnQixDQUMzQixPQUFPLEVBQUUsTUFBTSxFQUNmLGVBQWUsRUFBRSxNQUFNLEdBQUcsTUFBTSxFQUNoQyxvQkFBb0IsQ0FBQyxFQUFFLE1BQU0sRUFDN0IsVUFBVSxDQUFDLEVBQUUsTUFBTSxFQUFFLEdBQ3BCLE9BQU8sQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQXNDckI7SUFFRCxPQUFPLENBQUMsaUJBQWlCO1lBcUJYLG1CQUFtQjtZQW1DbkIsYUFBYTtJQTZFM0Isc0NBQXNDO0lBQy9CLGdCQUFnQixJQUFJLGlCQUFpQixHQUFHLFNBQVMsQ0FFdkQ7SUFFRCxpRUFBaUU7SUFDMUQsU0FBUyxJQUFJLE9BQU8sQ0FFMUI7SUFFRDs7OztPQUlHO0lBQ1UsS0FBSyxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FXbEM7SUFFRDs7T0FFRztJQUNILE9BQU8sQ0FBQyw4QkFBOEI7WUFnQnhCLGlCQUFpQjtJQTBDL0I7O09BRUc7SUFDSSxJQUFJLElBQUksSUFBSSxDQUtsQjtDQUNGIn0=
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/client/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,KAAK,QAAQ,EAA+B,MAAM,iBAAiB,CAAC;AAEnF,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,uBAAuB,CAAC;AAOlE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAEjF,OAAO,EAAE,KAAK,gBAAgB,EAA8B,MAAM,aAAa,CAAC;AAChF,OAAO,KAAK,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAEjF,qBAAa,cAAe,YAAW,mBAAmB;IAatD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAZvB,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAC/B,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAC5C,SAAS,CAAC,QAAQ,CAAC,aAAa,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAChE,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC;IACvC,SAAS,CAAC,QAAQ,CAAC,gBAAgB,EAAE,mBAAmB,EAAE,CAAC;IAC3D,SAAS,CAAC,QAAQ,CAAC,qBAAqB,EAAE,mBAAmB,GAAG,SAAS,CAAC;IAE1E,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,2BAA2B,CAAC,CAAiB;IAErD,YACE,MAAM,CAAC,EAAE,gBAAgB,EACR,IAAI,GAAE;QACrB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,aAAa,CAAC,EAAE,iBAAiB,CAAC;QAClC,gBAAgB,CAAC,EAAE,mBAAmB,EAAE,CAAC;QACzC,qBAAqB,CAAC,EAAE,mBAAmB,CAAC;QAC5C,yEAAyE;QACzE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;KACrC,EAwBP;IAED;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAU9B;;;;;OAKG;IACI,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAGvC;IAEY,WAAW,kBAoEvB;IAEY,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAmBjE;IAED;;;;;;;;;;;;;;OAcG;IACU,cAAc,CACzB,SAAS,EAAE,KAAK,MAAM,EAAE,EACxB,UAAU,EAAE,MAAM,EAAE,EACpB,IAAI,CAAC,EAAE,qBAAqB,GAC3B,OAAO,CAAC,IAAI,EAAE,CAAC,CA6IjB;YAQa,aAAa;IAsCd,kBAAkB,CAC7B,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GAAG,MAAM,EAChC,UAAU,GAAE,MAAM,EAAO,EACzB,oBAAoB,CAAC,EAAE,MAAM,GAC5B,OAAO,CAAC,IAAI,EAAE,CAAC,CAGjB;IAEY,gBAAgB,CAC3B,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GAAG,MAAM,EAChC,oBAAoB,CAAC,EAAE,MAAM,GAC5B,OAAO,CAAC,QAAQ,EAAE,CAAC,CAsCrB;IAED,OAAO,CAAC,iBAAiB;YAYX,mBAAmB;YAmCnB,aAAa;IA6D3B,sCAAsC;IAC/B,gBAAgB,IAAI,iBAAiB,GAAG,SAAS,CAEvD;IAED;;;OAGG;IACU,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CASlC;IAED;;OAEG;IACH,OAAO,CAAC,8BAA8B;IAWtC;;OAEG;IACI,IAAI,IAAI,IAAI,CAKlB;CACF"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/client/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,KAAK,QAAQ,EAA+B,MAAM,iBAAiB,CAAC;AAEnF,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,uBAAuB,CAAC;AAOlE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAEjF,OAAO,EAAE,KAAK,gBAAgB,EAA8B,MAAM,aAAa,CAAC;AAChF,OAAO,KAAK,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAEjF,qBAAa,cAAe,YAAW,mBAAmB;IAkBtD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAjBvB,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAC/B,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAC5C,SAAS,CAAC,QAAQ,CAAC,aAAa,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAChE,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC;IACvC,SAAS,CAAC,QAAQ,CAAC,gBAAgB,EAAE,mBAAmB,EAAE,CAAC;IAC3D,SAAS,CAAC,QAAQ,CAAC,qBAAqB,EAAE,mBAAmB,GAAG,SAAS,CAAC;IAE1E,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,2BAA2B,CAAC,CAAiB;IAErD,sFAAsF;IACtF,OAAO,CAAC,iBAAiB,CAAC,CAAS;IACnC,uEAAuE;IACvE,OAAO,CAAC,oBAAoB,CAAC,CAAS;IAEtC,YACE,MAAM,CAAC,EAAE,gBAAgB,EACR,IAAI,GAAE;QACrB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,aAAa,CAAC,EAAE,iBAAiB,CAAC;QAClC,gBAAgB,CAAC,EAAE,mBAAmB,EAAE,CAAC;QACzC,qBAAqB,CAAC,EAAE,mBAAmB,CAAC;QAC5C,yEAAyE;QACzE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;KACrC,EAwBP;IAED;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAU9B;;;;;OAKG;IACI,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAGvC;IAEY,WAAW,kBAoEvB;IAEY,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAmBjE;IAED;;;;;;;;;;;;;;OAcG;IACU,cAAc,CACzB,SAAS,EAAE,KAAK,MAAM,EAAE,EACxB,UAAU,EAAE,MAAM,EAAE,EACpB,IAAI,CAAC,EAAE,qBAAqB,GAC3B,OAAO,CAAC,IAAI,EAAE,CAAC,CAkJjB;YAQa,aAAa;IAsCd,kBAAkB,CAC7B,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GAAG,MAAM,EAChC,UAAU,GAAE,MAAM,EAAO,EACzB,oBAAoB,CAAC,EAAE,MAAM,GAC5B,OAAO,CAAC,IAAI,EAAE,CAAC,CAGjB;IAEY,gBAAgB,CAC3B,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GAAG,MAAM,EAChC,oBAAoB,CAAC,EAAE,MAAM,EAC7B,UAAU,CAAC,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,QAAQ,EAAE,CAAC,CAsCrB;IAED,OAAO,CAAC,iBAAiB;YAqBX,mBAAmB;YAmCnB,aAAa;IA6E3B,sCAAsC;IAC/B,gBAAgB,IAAI,iBAAiB,GAAG,SAAS,CAEvD;IAED,iEAAiE;IAC1D,SAAS,IAAI,OAAO,CAE1B;IAED;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAWlC;IAED;;OAEG;IACH,OAAO,CAAC,8BAA8B;YAgBxB,iBAAiB;IA0C/B;;OAEG;IACI,IAAI,IAAI,IAAI,CAKlB;CACF"}
@@ -17,6 +17,8 @@ export class HttpBlobClient {
17
17
  fileStoreUploadClient;
18
18
  disabled;
19
19
  healthcheckUploadIntervalId;
20
+ /** Cached beacon genesis time (seconds since Unix epoch). Fetched once at startup. */ beaconGenesisTime;
21
+ /** Cached beacon slot duration in seconds. Fetched once at startup. */ beaconSecondsPerSlot;
20
22
  constructor(config, opts = {}){
21
23
  this.opts = opts;
22
24
  this.disabled = false;
@@ -183,8 +185,8 @@ export class HttpBlobClient {
183
185
  // Return the result, ignoring any undefined ones
184
186
  const getFilledBlobs = ()=>resultBlobs.filter((b)=>b !== undefined);
185
187
  // Helper to fill in results from fetched blobs
186
- const fillResults = (fetchedBlobs)=>{
187
- const blobs = processFetchedBlobs(fetchedBlobs, blobHashes, this.log);
188
+ const fillResults = async (fetchedBlobs)=>{
189
+ const blobs = await processFetchedBlobs(fetchedBlobs, blobHashes, this.log);
188
190
  // Fill in any missing positions with matching blobs
189
191
  for(let i = 0; i < blobHashes.length; i++){
190
192
  if (resultBlobs[i] === undefined) {
@@ -220,7 +222,7 @@ export class HttpBlobClient {
220
222
  ...ctx
221
223
  };
222
224
  this.log.trace(`Attempting to get slot number for block hash`, consensusCtx);
223
- const slotNumber = await this.getSlotNumber(blockHash);
225
+ const slotNumber = await this.getSlotNumber(blockHash, opts?.parentBeaconBlockRoot, opts?.l1BlockTimestamp);
224
226
  this.log.debug(`Got slot number ${slotNumber} from consensus host for querying blobs`, consensusCtx);
225
227
  if (slotNumber) {
226
228
  let l1ConsensusHostUrl;
@@ -235,8 +237,8 @@ export class HttpBlobClient {
235
237
  l1ConsensusHostUrl,
236
238
  ...ctx
237
239
  });
238
- const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex);
239
- const result = fillResults(blobs);
240
+ const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex, getMissingBlobHashes());
241
+ const result = await fillResults(blobs);
240
242
  this.log.debug(`Got ${blobs.length} blobs from consensus host (total: ${result.length}/${blobHashes.length})`, {
241
243
  slotNumber,
242
244
  l1ConsensusHostUrl,
@@ -279,7 +281,7 @@ export class HttpBlobClient {
279
281
  this.log.debug('No blobs found from archive client', archiveCtx);
280
282
  } else {
281
283
  this.log.trace(`Got ${allBlobs.length} blobs from archive client before filtering`, archiveCtx);
282
- const result = fillResults(allBlobs);
284
+ const result = await fillResults(allBlobs);
283
285
  this.log.debug(`Got ${allBlobs.length} blobs from archive client (total: ${result.length}/${blobHashes.length})`, archiveCtx);
284
286
  if (result.length === blobHashes.length) {
285
287
  return returnWithCallback(result);
@@ -320,7 +322,7 @@ export class HttpBlobClient {
320
322
  });
321
323
  const blobs = await client.getBlobsByHashes(blobHashStrings);
322
324
  if (blobs.length > 0) {
323
- const result = fillResults(blobs);
325
+ const result = await fillResults(blobs);
324
326
  this.log.debug(`Got ${blobs.length} blobs from filestore (total: ${result.length}/${ctx.blobHashes.length})`, {
325
327
  url: client.getBaseUrl(),
326
328
  ...ctx
@@ -334,14 +336,14 @@ export class HttpBlobClient {
334
336
  }
335
337
  }
336
338
  async getBlobSidecarFrom(hostUrl, blockHashOrSlot, blobHashes = [], l1ConsensusHostIndex) {
337
- const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
338
- return processFetchedBlobs(blobs, blobHashes, this.log).filter((b)=>b !== undefined);
339
+ const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
340
+ return (await processFetchedBlobs(blobs, blobHashes, this.log)).filter((b)=>b !== undefined);
339
341
  }
340
- async getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex) {
342
+ async getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes) {
341
343
  try {
342
- let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
344
+ let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
343
345
  if (res.ok) {
344
- return parseBlobJsonsFromResponse(await res.json(), this.log);
346
+ return await parseBlobJsonsFromResponse(await res.json(), this.log);
345
347
  }
346
348
  if (res.status === 404 && typeof blockHashOrSlot === 'number') {
347
349
  const latestSlot = await this.getLatestSlotNumber(hostUrl, l1ConsensusHostIndex);
@@ -354,9 +356,9 @@ export class HttpBlobClient {
354
356
  let currentSlot = blockHashOrSlot + 1;
355
357
  while(res.status === 404 && maxRetries > 0 && latestSlot !== undefined && currentSlot <= latestSlot){
356
358
  this.log.debug(`Trying slot ${currentSlot}`);
357
- res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex);
359
+ res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex, blobHashes);
358
360
  if (res.ok) {
359
- return parseBlobJsonsFromResponse(await res.json(), this.log);
361
+ return await parseBlobJsonsFromResponse(await res.json(), this.log);
360
362
  }
361
363
  currentSlot++;
362
364
  maxRetries--;
@@ -373,8 +375,15 @@ export class HttpBlobClient {
373
375
  return [];
374
376
  }
375
377
  }
376
- fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex) {
377
- const baseUrl = `${hostUrl}/eth/v1/beacon/blob_sidecars/${blockHashOrSlot}`;
378
+ fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes) {
379
+ let baseUrl = `${hostUrl}/eth/v1/beacon/blobs/${blockHashOrSlot}`;
380
+ if (blobHashes && blobHashes.length > 0) {
381
+ const params = new URLSearchParams();
382
+ for (const hash of blobHashes){
383
+ params.append('versioned_hashes', `0x${hash.toString('hex')}`);
384
+ }
385
+ baseUrl += `?${params.toString()}`;
386
+ }
378
387
  const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
379
388
  this.log.debug(`Fetching blob sidecar for ${blockHashOrSlot}`, {
380
389
  url,
@@ -420,36 +429,45 @@ export class HttpBlobClient {
420
429
  *
421
430
  * @param blockHash - The block hash
422
431
  * @returns The slot number
423
- */ async getSlotNumber(blockHash) {
432
+ */ async getSlotNumber(blockHash, parentBeaconBlockRoot, l1BlockTimestamp) {
424
433
  const { l1ConsensusHostUrls, l1RpcUrls } = this.config;
425
434
  if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
426
435
  this.log.debug('No consensus host url configured');
427
436
  return undefined;
428
437
  }
429
- if (!l1RpcUrls || l1RpcUrls.length === 0) {
430
- this.log.debug('No execution host url configured');
431
- return undefined;
438
+ // Primary path: compute slot from timestamp if genesis config is cached (no network call needed)
439
+ if (l1BlockTimestamp !== undefined && this.beaconGenesisTime !== undefined && this.beaconSecondsPerSlot !== undefined) {
440
+ const slot = Number((l1BlockTimestamp - this.beaconGenesisTime) / BigInt(this.beaconSecondsPerSlot));
441
+ this.log.debug(`Computed slot ${slot} from L1 block timestamp`, {
442
+ l1BlockTimestamp
443
+ });
444
+ return slot;
432
445
  }
433
- // Ping execution node to get the parentBeaconBlockRoot for this block
434
- let parentBeaconBlockRoot;
435
- const client = createPublicClient({
436
- transport: fallback(l1RpcUrls.map((url)=>http(url, {
437
- batch: false
438
- })))
439
- });
440
- try {
441
- const res = await client.request({
442
- method: 'eth_getBlockByHash',
443
- params: [
444
- blockHash,
445
- /*tx flag*/ false
446
- ]
446
+ if (!parentBeaconBlockRoot) {
447
+ // parentBeaconBlockRoot not provided by caller — fetch it from the execution RPC
448
+ if (!l1RpcUrls || l1RpcUrls.length === 0) {
449
+ this.log.debug('No execution host url configured');
450
+ return undefined;
451
+ }
452
+ const client = createPublicClient({
453
+ transport: fallback(l1RpcUrls.map((url)=>http(url, {
454
+ batch: false
455
+ })))
447
456
  });
448
- if (res.parentBeaconBlockRoot) {
449
- parentBeaconBlockRoot = res.parentBeaconBlockRoot;
457
+ try {
458
+ const res = await client.request({
459
+ method: 'eth_getBlockByHash',
460
+ params: [
461
+ blockHash,
462
+ /*tx flag*/ false
463
+ ]
464
+ });
465
+ if (res.parentBeaconBlockRoot) {
466
+ parentBeaconBlockRoot = res.parentBeaconBlockRoot;
467
+ }
468
+ } catch (err) {
469
+ this.log.error(`Error getting parent beacon block root`, err);
450
470
  }
451
- } catch (err) {
452
- this.log.error(`Error getting parent beacon block root`, err);
453
471
  }
454
472
  if (!parentBeaconBlockRoot) {
455
473
  this.log.error(`No parent beacon block root found for block ${blockHash}`);
@@ -476,10 +494,15 @@ export class HttpBlobClient {
476
494
  /** @internal - exposed for testing */ getArchiveClient() {
477
495
  return this.archiveClient;
478
496
  }
497
+ /** Returns true if this client can upload blobs to filestore. */ canUpload() {
498
+ return this.fileStoreUploadClient !== undefined;
499
+ }
479
500
  /**
480
501
  * Start the blob client.
481
- * Uploads the initial healthcheck file (awaited) and starts periodic uploads.
502
+ * Fetches and caches beacon genesis config for timestamp-based slot resolution,
503
+ * then uploads the initial healthcheck file (awaited) and starts periodic uploads.
482
504
  */ async start() {
505
+ await this.fetchBeaconConfig();
483
506
  if (!this.fileStoreUploadClient) {
484
507
  return;
485
508
  }
@@ -498,6 +521,40 @@ export class HttpBlobClient {
498
521
  }, intervalMs);
499
522
  }
500
523
  /**
524
+ * Fetches and caches beacon genesis time and slot duration from the first available consensus host.
525
+ * These static values enable timestamp-based slot resolution, eliminating the per-fetch headers call.
526
+ * Logs a warning and leaves fields undefined if all hosts fail, callers fall back gracefully.
527
+ */ async fetchBeaconConfig() {
528
+ const { l1ConsensusHostUrls } = this.config;
529
+ if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
530
+ return;
531
+ }
532
+ for(let i = 0; i < l1ConsensusHostUrls.length; i++){
533
+ try {
534
+ const { url: genesisUrl, ...genesisOptions } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrls[i]}/eth/v1/config/genesis`, this.config, i);
535
+ const { url: specUrl, ...specOptions } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrls[i]}/eth/v1/config/spec`, this.config, i);
536
+ const [genesisRes, specRes] = await Promise.all([
537
+ this.fetch(genesisUrl, genesisOptions),
538
+ this.fetch(specUrl, specOptions)
539
+ ]);
540
+ if (genesisRes.ok && specRes.ok) {
541
+ const genesis = await genesisRes.json();
542
+ const spec = await specRes.json();
543
+ this.beaconGenesisTime = BigInt(genesis.data.genesisTime);
544
+ this.beaconSecondsPerSlot = parseInt(spec.data.secondsPerSlot);
545
+ this.log.debug(`Fetched beacon genesis config`, {
546
+ genesisTime: this.beaconGenesisTime,
547
+ secondsPerSlot: this.beaconSecondsPerSlot
548
+ });
549
+ return;
550
+ }
551
+ } catch (err) {
552
+ this.log.warn(`Failed to fetch beacon config from host ${l1ConsensusHostUrls[i]}`, err);
553
+ }
554
+ }
555
+ this.log.warn('Could not fetch beacon genesis config from any consensus host — will use headers call fallback');
556
+ }
557
+ /**
501
558
  * Stop the blob client, clearing any periodic tasks.
502
559
  */ stop() {
503
560
  if (this.healthcheckUploadIntervalId) {
@@ -506,10 +563,9 @@ export class HttpBlobClient {
506
563
  }
507
564
  }
508
565
  }
509
- function parseBlobJsonsFromResponse(response, logger) {
566
+ async function parseBlobJsonsFromResponse(response, logger) {
510
567
  try {
511
- const blobs = response.data.map(parseBlobJson);
512
- return blobs;
568
+ return await Promise.all(response.data.map(parseBlobJson));
513
569
  } catch (err) {
514
570
  logger.error(`Error parsing blob json from response`, err);
515
571
  return [];
@@ -519,15 +575,14 @@ function parseBlobJsonsFromResponse(response, logger) {
519
575
  // https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getBlobSidecars
520
576
  // Here we attempt to parse the response data to Buffer, and check the lengths (via Blob's constructor), to avoid
521
577
  // throwing an error down the line when calling Blob.fromJson().
522
- function parseBlobJson(data) {
523
- const blobBuffer = Buffer.from(data.blob.slice(2), 'hex');
524
- const commitmentBuffer = Buffer.from(data.kzg_commitment.slice(2), 'hex');
525
- const blob = new Blob(blobBuffer, commitmentBuffer);
578
+ async function parseBlobJson(rawHex) {
579
+ const blobBuffer = Buffer.from(rawHex.slice(2), 'hex');
580
+ const blob = await Blob.fromBlobBuffer(blobBuffer);
526
581
  return blob.toJSON();
527
582
  }
528
583
  // Returns an array that maps each blob hash to the corresponding blob, or undefined if the blob is not found
529
584
  // or the data does not match the commitment.
530
- function processFetchedBlobs(blobs, blobHashes, logger) {
585
+ async function processFetchedBlobs(blobs, blobHashes, logger) {
531
586
  const requestedBlobHashes = new Set(blobHashes.map(bufferToHex));
532
587
  const hashToBlob = new Map();
533
588
  for (const blobJson of blobs){
@@ -536,7 +591,7 @@ function processFetchedBlobs(blobs, blobHashes, logger) {
536
591
  continue;
537
592
  }
538
593
  try {
539
- const blob = Blob.fromJson(blobJson);
594
+ const blob = await Blob.fromJson(blobJson);
540
595
  hashToBlob.set(hashHex, blob);
541
596
  } catch (err) {
542
597
  // If the above throws, it's likely that the blob commitment does not match the hash of the blob data.
@@ -10,6 +10,17 @@ export interface GetBlobSidecarOptions {
10
10
  * - Near tip: FileStore first with no retries (data should exist), L1 consensus second (freshest data), then FileStore with retries, then archive (eg. blobscan)
11
11
  */
12
12
  isHistoricalSync?: boolean;
13
+ /**
14
+ * The parent beacon block root for the L1 block containing the blobs.
15
+ * If provided, skips the eth_getBlockByHash execution RPC call inside getSlotNumber.
16
+ */
17
+ parentBeaconBlockRoot?: string;
18
+ /**
19
+ * The timestamp of the L1 execution block containing the blobs.
20
+ * When provided alongside a cached beacon genesis config (fetched at startup), allows computing
21
+ * the beacon slot directly via timestamp math, skipping the beacon headers network call entirely.
22
+ */
23
+ l1BlockTimestamp?: bigint;
13
24
  }
14
25
  export interface BlobClientInterface {
15
26
  /** Sends the given blobs to the filestore, to be indexed by blob hash. */
@@ -22,5 +33,7 @@ export interface BlobClientInterface {
22
33
  testSources(): Promise<void>;
23
34
  /** Stops the blob client, clearing any periodic tasks. */
24
35
  stop?(): void;
36
+ /** Returns true if this client can upload blobs to filestore. */
37
+ canUpload(): boolean;
25
38
  }
26
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvY2xpZW50L2ludGVyZmFjZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssRUFBRSxJQUFJLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUU1Qzs7R0FFRztBQUNILE1BQU0sV0FBVyxxQkFBcUI7SUFDcEM7Ozs7O09BS0c7SUFDSCxnQkFBZ0IsQ0FBQyxFQUFFLE9BQU8sQ0FBQztDQUM1QjtBQUVELE1BQU0sV0FBVyxtQkFBbUI7SUFDbEMsMEVBQTBFO0lBQzFFLG9CQUFvQixDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLENBQUM7SUFDdEQscUVBQXFFO0lBQ3JFLGNBQWMsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLFVBQVUsQ0FBQyxFQUFFLE1BQU0sRUFBRSxFQUFFLElBQUksQ0FBQyxFQUFFLHFCQUFxQixHQUFHLE9BQU8sQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBQ3RHLDZFQUE2RTtJQUM3RSxLQUFLLENBQUMsSUFBSSxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDeEIsb0ZBQW9GO0lBQ3BGLFdBQVcsSUFBSSxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDN0IsMERBQTBEO0lBQzFELElBQUksQ0FBQyxJQUFJLElBQUksQ0FBQztDQUNmIn0=
39
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvY2xpZW50L2ludGVyZmFjZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssRUFBRSxJQUFJLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUU1Qzs7R0FFRztBQUNILE1BQU0sV0FBVyxxQkFBcUI7SUFDcEM7Ozs7O09BS0c7SUFDSCxnQkFBZ0IsQ0FBQyxFQUFFLE9BQU8sQ0FBQztJQUMzQjs7O09BR0c7SUFDSCxxQkFBcUIsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUMvQjs7OztPQUlHO0lBQ0gsZ0JBQWdCLENBQUMsRUFBRSxNQUFNLENBQUM7Q0FDM0I7QUFFRCxNQUFNLFdBQVcsbUJBQW1CO0lBQ2xDLDBFQUEwRTtJQUMxRSxvQkFBb0IsQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQ3RELHFFQUFxRTtJQUNyRSxjQUFjLENBQUMsT0FBTyxFQUFFLE1BQU0sRUFBRSxVQUFVLENBQUMsRUFBRSxNQUFNLEVBQUUsRUFBRSxJQUFJLENBQUMsRUFBRSxxQkFBcUIsR0FBRyxPQUFPLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUN0Ryw2RUFBNkU7SUFDN0UsS0FBSyxDQUFDLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQ3hCLG9GQUFvRjtJQUNwRixXQUFXLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQzdCLDBEQUEwRDtJQUMxRCxJQUFJLENBQUMsSUFBSSxJQUFJLENBQUM7SUFDZCxpRUFBaUU7SUFDakUsU0FBUyxJQUFJLE9BQU8sQ0FBQztDQUN0QiJ9
@@ -1 +1 @@
1
- {"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../src/client/interface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAE5C;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,0EAA0E;IAC1E,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACtD,qEAAqE;IACrE,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IACtG,6EAA6E;IAC7E,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,oFAAoF;IACpF,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,0DAA0D;IAC1D,IAAI,CAAC,IAAI,IAAI,CAAC;CACf"}
1
+ {"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../src/client/interface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAE5C;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,mBAAmB;IAClC,0EAA0E;IAC1E,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACtD,qEAAqE;IACrE,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IACtG,6EAA6E;IAC7E,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,oFAAoF;IACpF,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,0DAA0D;IAC1D,IAAI,CAAC,IAAI,IAAI,CAAC;IACd,iEAAiE;IACjE,SAAS,IAAI,OAAO,CAAC;CACtB"}
@@ -7,5 +7,7 @@ export declare class LocalBlobClient implements BlobClientInterface {
7
7
  testSources(): Promise<void>;
8
8
  sendBlobsToFilestore(blobs: Blob[]): Promise<boolean>;
9
9
  getBlobSidecar(_blockId: string, blobHashes: Buffer[], _opts?: GetBlobSidecarOptions): Promise<Blob[]>;
10
+ /** Returns true if this client can upload blobs. Always true for local client. */
11
+ canUpload(): boolean;
10
12
  }
11
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibG9jYWwuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9jbGllbnQvbG9jYWwudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLEVBQUUsSUFBSSxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFFNUMsT0FBTyxLQUFLLEVBQUUsU0FBUyxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDdkQsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVqRixxQkFBYSxlQUFnQixZQUFXLG1CQUFtQjtJQUN6RCxPQUFPLENBQUMsUUFBUSxDQUFDLFNBQVMsQ0FBWTtJQUV0QyxZQUFZLFNBQVMsRUFBRSxTQUFTLEVBRS9CO0lBRU0sV0FBVyxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FFbEM7SUFFWSxvQkFBb0IsQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUdqRTtJQUVNLGNBQWMsQ0FBQyxRQUFRLEVBQUUsTUFBTSxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUsRUFBRSxLQUFLLENBQUMsRUFBRSxxQkFBcUIsR0FBRyxPQUFPLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FFNUc7Q0FDRiJ9
13
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibG9jYWwuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9jbGllbnQvbG9jYWwudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLEVBQUUsSUFBSSxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFFNUMsT0FBTyxLQUFLLEVBQUUsU0FBUyxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDdkQsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVqRixxQkFBYSxlQUFnQixZQUFXLG1CQUFtQjtJQUN6RCxPQUFPLENBQUMsUUFBUSxDQUFDLFNBQVMsQ0FBWTtJQUV0QyxZQUFZLFNBQVMsRUFBRSxTQUFTLEVBRS9CO0lBRU0sV0FBVyxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FFbEM7SUFFWSxvQkFBb0IsQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUdqRTtJQUVNLGNBQWMsQ0FBQyxRQUFRLEVBQUUsTUFBTSxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUsRUFBRSxLQUFLLENBQUMsRUFBRSxxQkFBcUIsR0FBRyxPQUFPLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FFNUc7SUFFRCxrRkFBa0Y7SUFDM0UsU0FBUyxJQUFJLE9BQU8sQ0FFMUI7Q0FDRiJ9
@@ -1 +1 @@
1
- {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/client/local.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAEjF,qBAAa,eAAgB,YAAW,mBAAmB;IACzD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IAEtC,YAAY,SAAS,EAAE,SAAS,EAE/B;IAEM,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAElC;IAEY,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAGjE;IAEM,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,KAAK,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAE5G;CACF"}
1
+ {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/client/local.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAEjF,qBAAa,eAAgB,YAAW,mBAAmB;IACzD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IAEtC,YAAY,SAAS,EAAE,SAAS,EAE/B;IAEM,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAElC;IAEY,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAGjE;IAEM,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,KAAK,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAE5G;IAED,kFAAkF;IAC3E,SAAS,IAAI,OAAO,CAE1B;CACF"}
@@ -13,4 +13,7 @@ export class LocalBlobClient {
13
13
  getBlobSidecar(_blockId, blobHashes, _opts) {
14
14
  return this.blobStore.getBlobsByHashes(blobHashes);
15
15
  }
16
+ /** Returns true if this client can upload blobs. Always true for local client. */ canUpload() {
17
+ return true;
18
+ }
16
19
  }
@@ -17,7 +17,7 @@ import { makeRandomBlob } from '@aztec/blob-lib/testing';
17
17
  await cleanup();
18
18
  });
19
19
  it('should send and retrieve blobs by hash', async ()=>{
20
- const blob = makeRandomBlob(5);
20
+ const blob = await makeRandomBlob(5);
21
21
  const blobHash = blob.getEthVersionedBlobHash();
22
22
  await client.sendBlobsToFilestore([
23
23
  blob
@@ -29,9 +29,9 @@ import { makeRandomBlob } from '@aztec/blob-lib/testing';
29
29
  expect(retrievedBlobs[0]).toEqual(blob);
30
30
  });
31
31
  it('should handle multiple blobs', async ()=>{
32
- const blobs = Array.from({
32
+ const blobs = await Promise.all(Array.from({
33
33
  length: 3
34
- }, ()=>makeRandomBlob(7));
34
+ }, ()=>makeRandomBlob(7)));
35
35
  const blobHashes = blobs.map((blob)=>blob.getEthVersionedBlobHash());
36
36
  await client.sendBlobsToFilestore(blobs);
37
37
  const retrievedBlobs = await client.getBlobSidecar(blockId, blobHashes);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/blob-client",
3
- "version": "0.0.1-commit.fcb71a6",
3
+ "version": "0.0.1-commit.ffe5b04ea",
4
4
  "type": "module",
5
5
  "bin": "./dest/client/bin/index.js",
6
6
  "exports": {
@@ -56,12 +56,12 @@
56
56
  ]
57
57
  },
58
58
  "dependencies": {
59
- "@aztec/blob-lib": "0.0.1-commit.fcb71a6",
60
- "@aztec/ethereum": "0.0.1-commit.fcb71a6",
61
- "@aztec/foundation": "0.0.1-commit.fcb71a6",
62
- "@aztec/kv-store": "0.0.1-commit.fcb71a6",
63
- "@aztec/stdlib": "0.0.1-commit.fcb71a6",
64
- "@aztec/telemetry-client": "0.0.1-commit.fcb71a6",
59
+ "@aztec/blob-lib": "0.0.1-commit.ffe5b04ea",
60
+ "@aztec/ethereum": "0.0.1-commit.ffe5b04ea",
61
+ "@aztec/foundation": "0.0.1-commit.ffe5b04ea",
62
+ "@aztec/kv-store": "0.0.1-commit.ffe5b04ea",
63
+ "@aztec/stdlib": "0.0.1-commit.ffe5b04ea",
64
+ "@aztec/telemetry-client": "0.0.1-commit.ffe5b04ea",
65
65
  "express": "^4.21.2",
66
66
  "snappy": "^7.2.2",
67
67
  "source-map-support": "^0.5.21",
@@ -75,7 +75,7 @@
75
75
  "@types/node": "^22.15.17",
76
76
  "@types/source-map-support": "^0.5.10",
77
77
  "@types/supertest": "^6.0.2",
78
- "@typescript/native-preview": "7.0.0-dev.20251126.1",
78
+ "@typescript/native-preview": "7.0.0-dev.20260113.1",
79
79
  "jest": "^30.0.0",
80
80
  "jest-mock-extended": "^4.0.0",
81
81
  "supertest": "^7.0.0",
@@ -7,7 +7,7 @@ export type BlobArchiveApiConfig = {
7
7
 
8
8
  export const blobArchiveApiConfigMappings: ConfigMappingsType<BlobArchiveApiConfig> = {
9
9
  archiveApiUrl: {
10
- env: 'BLOB_SINK_ARCHIVE_API_URL',
10
+ env: 'BLOB_ARCHIVE_API_URL',
11
11
  description: 'The URL of the archive API',
12
12
  },
13
13
  ...pickConfigMappings(l1ReaderConfigMappings, ['l1ChainId']),
@@ -1,4 +1,10 @@
1
- import { Attributes, Metrics, type TelemetryClient, type UpDownCounter, ValueType } from '@aztec/telemetry-client';
1
+ import {
2
+ Attributes,
3
+ Metrics,
4
+ type TelemetryClient,
5
+ type UpDownCounter,
6
+ createUpDownCounterWithDefault,
7
+ } from '@aztec/telemetry-client';
2
8
 
3
9
  export class BlobArchiveClientInstrumentation {
4
10
  private blockRequestCounter: UpDownCounter;
@@ -11,20 +17,23 @@ export class BlobArchiveClientInstrumentation {
11
17
  name: string,
12
18
  ) {
13
19
  const meter = client.getMeter(name);
14
- this.blockRequestCounter = meter.createUpDownCounter(Metrics.BLOB_SINK_ARCHIVE_BLOCK_REQUEST_COUNT, {
15
- description: 'Number of requests made to retrieve blocks from the blob archive',
16
- valueType: ValueType.INT,
17
- });
20
+ const requestAttrs = {
21
+ [Attributes.HTTP_RESPONSE_STATUS_CODE]: [200, 404],
22
+ [Attributes.HTTP_REQUEST_HOST]: [httpHost],
23
+ };
24
+ this.blockRequestCounter = createUpDownCounterWithDefault(
25
+ meter,
26
+ Metrics.BLOB_SINK_ARCHIVE_BLOCK_REQUEST_COUNT,
27
+ requestAttrs,
28
+ );
18
29
 
19
- this.blobRequestCounter = meter.createUpDownCounter(Metrics.BLOB_SINK_ARCHIVE_BLOB_REQUEST_COUNT, {
20
- description: 'Number of requests made to retrieve blobs from the blob archive',
21
- valueType: ValueType.INT,
22
- });
30
+ this.blobRequestCounter = createUpDownCounterWithDefault(
31
+ meter,
32
+ Metrics.BLOB_SINK_ARCHIVE_BLOB_REQUEST_COUNT,
33
+ requestAttrs,
34
+ );
23
35
 
24
- this.retrievedBlobs = meter.createUpDownCounter(Metrics.BLOB_SINK_ARCHIVE_BLOB_COUNT, {
25
- description: 'Number of blobs retrieved from the blob archive',
26
- valueType: ValueType.INT,
27
- });
36
+ this.retrievedBlobs = createUpDownCounterWithDefault(meter, Metrics.BLOB_SINK_ARCHIVE_BLOB_COUNT);
28
37
  }
29
38
 
30
39
  incRequest(type: 'blocks' | 'blobs', status: number) {
@@ -13,7 +13,7 @@ export function describeBlobStore(getBlobStore: () => Promise<BlobStore>) {
13
13
  it('should store and retrieve a blob by hash', async () => {
14
14
  // Create a test blob with random fields
15
15
  const testFields = [Fr.random(), Fr.random(), Fr.random()];
16
- const blob = Blob.fromFields(testFields);
16
+ const blob = await Blob.fromFields(testFields);
17
17
  const blobHash = blob.getEthVersionedBlobHash();
18
18
 
19
19
  // Store the blob
@@ -29,8 +29,8 @@ export function describeBlobStore(getBlobStore: () => Promise<BlobStore>) {
29
29
 
30
30
  it('should handle multiple blobs stored and retrieved by their hashes', async () => {
31
31
  // Create two different blobs
32
- const blob1 = Blob.fromFields([Fr.random(), Fr.random()]);
33
- const blob2 = Blob.fromFields([Fr.random(), Fr.random(), Fr.random()]);
32
+ const blob1 = await Blob.fromFields([Fr.random(), Fr.random()]);
33
+ const blob2 = await Blob.fromFields([Fr.random(), Fr.random(), Fr.random()]);
34
34
 
35
35
  const blobHash1 = blob1.getEthVersionedBlobHash();
36
36
  const blobHash2 = blob2.getEthVersionedBlobHash();
@@ -57,9 +57,9 @@ export function describeBlobStore(getBlobStore: () => Promise<BlobStore>) {
57
57
 
58
58
  it('should handle retrieving subset of stored blobs', async () => {
59
59
  // Store multiple blobs
60
- const blob1 = Blob.fromFields([Fr.random()]);
61
- const blob2 = Blob.fromFields([Fr.random()]);
62
- const blob3 = Blob.fromFields([Fr.random()]);
60
+ const blob1 = await Blob.fromFields([Fr.random()]);
61
+ const blob2 = await Blob.fromFields([Fr.random()]);
62
+ const blob3 = await Blob.fromFields([Fr.random()]);
63
63
 
64
64
  await blobStore.addBlobs([blob1, blob2, blob3]);
65
65
 
@@ -75,7 +75,7 @@ export function describeBlobStore(getBlobStore: () => Promise<BlobStore>) {
75
75
  });
76
76
 
77
77
  it('should handle duplicate blob hashes in request', async () => {
78
- const blob = Blob.fromFields([Fr.random()]);
78
+ const blob = await Blob.fromFields([Fr.random()]);
79
79
  const blobHash = blob.getEthVersionedBlobHash();
80
80
 
81
81
  await blobStore.addBlobs([blob]);
@@ -91,8 +91,8 @@ export function describeBlobStore(getBlobStore: () => Promise<BlobStore>) {
91
91
  it('should overwrite blob when storing with same hash', async () => {
92
92
  // Create two blobs that will have the same hash (same content)
93
93
  const fields = [Fr.random(), Fr.random()];
94
- const blob1 = Blob.fromFields(fields);
95
- const blob2 = Blob.fromFields(fields);
94
+ const blob1 = await Blob.fromFields(fields);
95
+ const blob2 = await Blob.fromFields(fields);
96
96
 
97
97
  const blobHash = blob1.getEthVersionedBlobHash();
98
98
 
@@ -24,6 +24,11 @@ export class HttpBlobClient implements BlobClientInterface {
24
24
  private disabled = false;
25
25
  private healthcheckUploadIntervalId?: NodeJS.Timeout;
26
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;
31
+
27
32
  constructor(
28
33
  config?: BlobClientConfig,
29
34
  private readonly opts: {
@@ -215,8 +220,8 @@ export class HttpBlobClient implements BlobClientInterface {
215
220
  const getFilledBlobs = (): Blob[] => resultBlobs.filter((b): b is Blob => b !== undefined);
216
221
 
217
222
  // Helper to fill in results from fetched blobs
218
- const fillResults = (fetchedBlobs: BlobJson[]): Blob[] => {
219
- const blobs = processFetchedBlobs(fetchedBlobs, blobHashes, this.log);
223
+ const fillResults = async (fetchedBlobs: BlobJson[]): Promise<Blob[]> => {
224
+ const blobs = await processFetchedBlobs(fetchedBlobs, blobHashes, this.log);
220
225
  // Fill in any missing positions with matching blobs
221
226
  for (let i = 0; i < blobHashes.length; i++) {
222
227
  if (resultBlobs[i] === undefined) {
@@ -251,7 +256,7 @@ export class HttpBlobClient implements BlobClientInterface {
251
256
  // The beacon api can query by slot number, so we get that first
252
257
  const consensusCtx = { l1ConsensusHostUrls, ...ctx };
253
258
  this.log.trace(`Attempting to get slot number for block hash`, consensusCtx);
254
- const slotNumber = await this.getSlotNumber(blockHash);
259
+ const slotNumber = await this.getSlotNumber(blockHash, opts?.parentBeaconBlockRoot, opts?.l1BlockTimestamp);
255
260
  this.log.debug(`Got slot number ${slotNumber} from consensus host for querying blobs`, consensusCtx);
256
261
 
257
262
  if (slotNumber) {
@@ -268,8 +273,13 @@ export class HttpBlobClient implements BlobClientInterface {
268
273
  l1ConsensusHostUrl,
269
274
  ...ctx,
270
275
  });
271
- const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex);
272
- const result = fillResults(blobs);
276
+ const blobs = await this.getBlobsFromHost(
277
+ l1ConsensusHostUrl,
278
+ slotNumber,
279
+ l1ConsensusHostIndex,
280
+ getMissingBlobHashes(),
281
+ );
282
+ const result = await fillResults(blobs);
273
283
  this.log.debug(
274
284
  `Got ${blobs.length} blobs from consensus host (total: ${result.length}/${blobHashes.length})`,
275
285
  { slotNumber, l1ConsensusHostUrl, ...ctx },
@@ -312,7 +322,7 @@ export class HttpBlobClient implements BlobClientInterface {
312
322
  this.log.debug('No blobs found from archive client', archiveCtx);
313
323
  } else {
314
324
  this.log.trace(`Got ${allBlobs.length} blobs from archive client before filtering`, archiveCtx);
315
- const result = fillResults(allBlobs);
325
+ const result = await fillResults(allBlobs);
316
326
  this.log.debug(
317
327
  `Got ${allBlobs.length} blobs from archive client (total: ${result.length}/${blobHashes.length})`,
318
328
  archiveCtx,
@@ -345,7 +355,7 @@ export class HttpBlobClient implements BlobClientInterface {
345
355
  */
346
356
  private async tryFileStores(
347
357
  getMissingBlobHashes: () => Buffer[],
348
- fillResults: (blobs: BlobJson[]) => Blob[],
358
+ fillResults: (blobs: BlobJson[]) => Promise<Blob[]>,
349
359
  ctx: { blockHash: string; blobHashes: string[] },
350
360
  ): Promise<void> {
351
361
  // Shuffle clients for load distribution
@@ -366,7 +376,7 @@ export class HttpBlobClient implements BlobClientInterface {
366
376
  });
367
377
  const blobs = await client.getBlobsByHashes(blobHashStrings);
368
378
  if (blobs.length > 0) {
369
- const result = fillResults(blobs);
379
+ const result = await fillResults(blobs);
370
380
  this.log.debug(
371
381
  `Got ${blobs.length} blobs from filestore (total: ${result.length}/${ctx.blobHashes.length})`,
372
382
  {
@@ -387,19 +397,20 @@ export class HttpBlobClient implements BlobClientInterface {
387
397
  blobHashes: Buffer[] = [],
388
398
  l1ConsensusHostIndex?: number,
389
399
  ): Promise<Blob[]> {
390
- const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
391
- 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);
392
402
  }
393
403
 
394
404
  public async getBlobsFromHost(
395
405
  hostUrl: string,
396
406
  blockHashOrSlot: string | number,
397
407
  l1ConsensusHostIndex?: number,
408
+ blobHashes?: Buffer[],
398
409
  ): Promise<BlobJson[]> {
399
410
  try {
400
- let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
411
+ let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
401
412
  if (res.ok) {
402
- return parseBlobJsonsFromResponse(await res.json(), this.log);
413
+ return await parseBlobJsonsFromResponse(await res.json(), this.log);
403
414
  }
404
415
 
405
416
  if (res.status === 404 && typeof blockHashOrSlot === 'number') {
@@ -414,9 +425,9 @@ export class HttpBlobClient implements BlobClientInterface {
414
425
  let currentSlot = blockHashOrSlot + 1;
415
426
  while (res.status === 404 && maxRetries > 0 && latestSlot !== undefined && currentSlot <= latestSlot) {
416
427
  this.log.debug(`Trying slot ${currentSlot}`);
417
- res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex);
428
+ res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex, blobHashes);
418
429
  if (res.ok) {
419
- return parseBlobJsonsFromResponse(await res.json(), this.log);
430
+ return await parseBlobJsonsFromResponse(await res.json(), this.log);
420
431
  }
421
432
  currentSlot++;
422
433
  maxRetries--;
@@ -439,8 +450,17 @@ export class HttpBlobClient implements BlobClientInterface {
439
450
  hostUrl: string,
440
451
  blockHashOrSlot: string | number,
441
452
  l1ConsensusHostIndex?: number,
453
+ blobHashes?: Buffer[],
442
454
  ): Promise<Response> {
443
- const baseUrl = `${hostUrl}/eth/v1/beacon/blob_sidecars/${blockHashOrSlot}`;
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
+ }
444
464
 
445
465
  const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
446
466
  this.log.debug(`Fetching blob sidecar for ${blockHashOrSlot}`, { url, ...options });
@@ -482,34 +502,50 @@ export class HttpBlobClient implements BlobClientInterface {
482
502
  * @param blockHash - The block hash
483
503
  * @returns The slot number
484
504
  */
485
- private async getSlotNumber(blockHash: `0x${string}`): Promise<number | undefined> {
505
+ private async getSlotNumber(
506
+ blockHash: `0x${string}`,
507
+ parentBeaconBlockRoot?: string,
508
+ l1BlockTimestamp?: bigint,
509
+ ): Promise<number | undefined> {
486
510
  const { l1ConsensusHostUrls, l1RpcUrls } = this.config;
487
511
  if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
488
512
  this.log.debug('No consensus host url configured');
489
513
  return undefined;
490
514
  }
491
515
 
492
- if (!l1RpcUrls || l1RpcUrls.length === 0) {
493
- this.log.debug('No execution host url configured');
494
- return undefined;
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;
495
525
  }
496
526
 
497
- // Ping execution node to get the parentBeaconBlockRoot for this block
498
- let parentBeaconBlockRoot: string | undefined;
499
- const client = createPublicClient({
500
- transport: fallback(l1RpcUrls.map(url => http(url, { batch: false }))),
501
- });
502
- try {
503
- const res: RpcBlock = await client.request({
504
- method: 'eth_getBlockByHash',
505
- params: [blockHash, /*tx flag*/ false],
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 }))),
506
536
  });
537
+ try {
538
+ const res: RpcBlock = await client.request({
539
+ method: 'eth_getBlockByHash',
540
+ params: [blockHash, /*tx flag*/ false],
541
+ });
507
542
 
508
- if (res.parentBeaconBlockRoot) {
509
- parentBeaconBlockRoot = res.parentBeaconBlockRoot;
543
+ if (res.parentBeaconBlockRoot) {
544
+ parentBeaconBlockRoot = res.parentBeaconBlockRoot;
545
+ }
546
+ } catch (err) {
547
+ this.log.error(`Error getting parent beacon block root`, err);
510
548
  }
511
- } catch (err) {
512
- this.log.error(`Error getting parent beacon block root`, err);
513
549
  }
514
550
 
515
551
  if (!parentBeaconBlockRoot) {
@@ -548,11 +584,19 @@ export class HttpBlobClient implements BlobClientInterface {
548
584
  return this.archiveClient;
549
585
  }
550
586
 
587
+ /** Returns true if this client can upload blobs to filestore. */
588
+ public canUpload(): boolean {
589
+ return this.fileStoreUploadClient !== undefined;
590
+ }
591
+
551
592
  /**
552
593
  * Start the blob client.
553
- * Uploads the initial healthcheck file (awaited) and starts periodic uploads.
594
+ * Fetches and caches beacon genesis config for timestamp-based slot resolution,
595
+ * then uploads the initial healthcheck file (awaited) and starts periodic uploads.
554
596
  */
555
597
  public async start(): Promise<void> {
598
+ await this.fetchBeaconConfig();
599
+
556
600
  if (!this.fileStoreUploadClient) {
557
601
  return;
558
602
  }
@@ -577,6 +621,53 @@ export class HttpBlobClient implements BlobClientInterface {
577
621
  }, intervalMs);
578
622
  }
579
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
+
580
671
  /**
581
672
  * Stop the blob client, clearing any periodic tasks.
582
673
  */
@@ -588,10 +679,9 @@ export class HttpBlobClient implements BlobClientInterface {
588
679
  }
589
680
  }
590
681
 
591
- function parseBlobJsonsFromResponse(response: any, logger: Logger): BlobJson[] {
682
+ async function parseBlobJsonsFromResponse(response: any, logger: Logger): Promise<BlobJson[]> {
592
683
  try {
593
- const blobs = response.data.map(parseBlobJson);
594
- return blobs;
684
+ return await Promise.all((response.data as string[]).map(parseBlobJson));
595
685
  } catch (err) {
596
686
  logger.error(`Error parsing blob json from response`, err);
597
687
  return [];
@@ -602,16 +692,19 @@ function parseBlobJsonsFromResponse(response: any, logger: Logger): BlobJson[] {
602
692
  // https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getBlobSidecars
603
693
  // Here we attempt to parse the response data to Buffer, and check the lengths (via Blob's constructor), to avoid
604
694
  // throwing an error down the line when calling Blob.fromJson().
605
- function parseBlobJson(data: any): BlobJson {
606
- const blobBuffer = Buffer.from(data.blob.slice(2), 'hex');
607
- const commitmentBuffer = Buffer.from(data.kzg_commitment.slice(2), 'hex');
608
- 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);
609
698
  return blob.toJSON();
610
699
  }
611
700
 
612
701
  // Returns an array that maps each blob hash to the corresponding blob, or undefined if the blob is not found
613
702
  // or the data does not match the commitment.
614
- function processFetchedBlobs(blobs: BlobJson[], blobHashes: Buffer[], logger: Logger): (Blob | undefined)[] {
703
+ async function processFetchedBlobs(
704
+ blobs: BlobJson[],
705
+ blobHashes: Buffer[],
706
+ logger: Logger,
707
+ ): Promise<(Blob | undefined)[]> {
615
708
  const requestedBlobHashes = new Set<string>(blobHashes.map(bufferToHex));
616
709
  const hashToBlob = new Map<string, Blob>();
617
710
  for (const blobJson of blobs) {
@@ -621,7 +714,7 @@ function processFetchedBlobs(blobs: BlobJson[], blobHashes: Buffer[], logger: Lo
621
714
  }
622
715
 
623
716
  try {
624
- const blob = Blob.fromJson(blobJson);
717
+ const blob = await Blob.fromJson(blobJson);
625
718
  hashToBlob.set(hashHex, blob);
626
719
  } catch (err) {
627
720
  // If the above throws, it's likely that the blob commitment does not match the hash of the blob data.
@@ -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 {
@@ -24,4 +35,6 @@ export interface BlobClientInterface {
24
35
  testSources(): Promise<void>;
25
36
  /** Stops the blob client, clearing any periodic tasks. */
26
37
  stop?(): void;
38
+ /** Returns true if this client can upload blobs to filestore. */
39
+ canUpload(): boolean;
27
40
  }
@@ -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
  }
@@ -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);