@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.
Files changed (39) hide show
  1. package/README.md +10 -1
  2. package/dest/archive/config.js +1 -1
  3. package/dest/archive/instrumentation.d.ts +1 -1
  4. package/dest/archive/instrumentation.d.ts.map +1 -1
  5. package/dest/archive/instrumentation.js +13 -13
  6. package/dest/blobstore/blob_store_test_suite.js +9 -9
  7. package/dest/client/config.d.ts +5 -1
  8. package/dest/client/config.d.ts.map +1 -1
  9. package/dest/client/config.js +5 -0
  10. package/dest/client/factory.d.ts +3 -2
  11. package/dest/client/factory.d.ts.map +1 -1
  12. package/dest/client/factory.js +5 -2
  13. package/dest/client/http.d.ts +24 -2
  14. package/dest/client/http.d.ts.map +1 -1
  15. package/dest/client/http.js +133 -47
  16. package/dest/client/interface.d.ts +18 -1
  17. package/dest/client/interface.d.ts.map +1 -1
  18. package/dest/client/local.d.ts +3 -1
  19. package/dest/client/local.d.ts.map +1 -1
  20. package/dest/client/local.js +3 -0
  21. package/dest/client/tests.js +3 -3
  22. package/dest/filestore/filestore_blob_client.d.ts +14 -2
  23. package/dest/filestore/filestore_blob_client.d.ts.map +1 -1
  24. package/dest/filestore/filestore_blob_client.js +29 -5
  25. package/dest/filestore/healthcheck.d.ts +5 -0
  26. package/dest/filestore/healthcheck.d.ts.map +1 -0
  27. package/dest/filestore/healthcheck.js +3 -0
  28. package/package.json +8 -8
  29. package/src/archive/config.ts +1 -1
  30. package/src/archive/instrumentation.ts +22 -13
  31. package/src/blobstore/blob_store_test_suite.ts +9 -9
  32. package/src/client/config.ts +10 -0
  33. package/src/client/factory.ts +7 -2
  34. package/src/client/http.ts +175 -41
  35. package/src/client/interface.ts +17 -0
  36. package/src/client/local.ts +5 -0
  37. package/src/client/tests.ts +2 -2
  38. package/src/filestore/filestore_blob_client.ts +33 -5
  39. package/src/filestore/healthcheck.ts +5 -0
package/README.md CHANGED
@@ -31,9 +31,18 @@ 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
+ ### File Store Connectivity Testing
38
+
39
+ All file stores (S3, GCS, HTTP, local) test connectivity by checking if a well-known healthcheck file (`.healthcheck`) exists. This approach was chosen because:
40
+
41
+ 1. **HTTP compatibility**: For HTTP-based file stores, requesting a known file is the only reliable way to verify connectivity
42
+ 2. **Uniform behavior**: Using the same healthcheck mechanism across all store types ensures consistent behavior and simplifies testing
43
+
44
+ When uploading is enabled, the sequencer uploads the healthcheck file on startup and then periodically re-uploads it (by default every 60 minutes) to ensure it remains available. This guards against accidental deletion, storage pruning, or other failures that might remove the file.
45
+
37
46
  ### Example Usage
38
47
 
39
48
  ```typescript
@@ -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([
@@ -36,6 +36,10 @@ export interface BlobClientConfig extends BlobArchiveApiConfig {
36
36
  * URL for uploading blobs to filestore (s3://, gs://, file://)
37
37
  */
38
38
  blobFileStoreUploadUrl?: string;
39
+ /**
40
+ * Interval in minutes for uploading healthcheck file to file store (default: 60 = 1 hour)
41
+ */
42
+ blobHealthcheckUploadIntervalMinutes?: number;
39
43
  }
40
44
  export declare const blobClientConfigMapping: ConfigMappingsType<BlobClientConfig>;
41
45
  /**
@@ -47,4 +51,4 @@ export declare function getBlobClientConfigFromEnv(): BlobClientConfig;
47
51
  * Returns whether the given blob client config has any remote sources defined.
48
52
  */
49
53
  export declare function hasRemoteBlobSources(config?: BlobClientConfig): boolean;
50
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmlnLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvY2xpZW50L2NvbmZpZy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQ0wsS0FBSyxrQkFBa0IsRUFDdkIsV0FBVyxFQUdaLE1BQU0sMEJBQTBCLENBQUM7QUFFbEMsT0FBTyxFQUFFLEtBQUssb0JBQW9CLEVBQWdDLE1BQU0sc0JBQXNCLENBQUM7QUFFL0Y7O0dBRUc7QUFDSCxNQUFNLFdBQVcsZ0JBQWlCLFNBQVEsb0JBQW9CO0lBQzVEOztPQUVHO0lBQ0gsU0FBUyxDQUFDLEVBQUUsTUFBTSxFQUFFLENBQUM7SUFFckI7O09BRUc7SUFDSCxtQkFBbUIsQ0FBQyxFQUFFLE1BQU0sRUFBRSxDQUFDO0lBRS9COztPQUVHO0lBQ0gsc0JBQXNCLENBQUMsRUFBRSxXQUFXLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztJQUUvQzs7T0FFRztJQUNILDRCQUE0QixDQUFDLEVBQUUsTUFBTSxFQUFFLENBQUM7SUFFeEM7O09BRUc7SUFDSCxpQkFBaUIsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUUzQjs7T0FFRztJQUNILHFCQUFxQixDQUFDLEVBQUUsT0FBTyxDQUFDO0lBRWhDOztPQUVHO0lBQ0gsaUJBQWlCLENBQUMsRUFBRSxNQUFNLEVBQUUsQ0FBQztJQUU3Qjs7T0FFRztJQUNILHNCQUFzQixDQUFDLEVBQUUsTUFBTSxDQUFDO0NBQ2pDO0FBRUQsZUFBTyxNQUFNLHVCQUF1QixFQUFFLGtCQUFrQixDQUFDLGdCQUFnQixDQStDeEUsQ0FBQztBQUVGOzs7R0FHRztBQUNILHdCQUFnQiwwQkFBMEIsSUFBSSxnQkFBZ0IsQ0FFN0Q7QUFFRDs7R0FFRztBQUNILHdCQUFnQixvQkFBb0IsQ0FBQyxNQUFNLEdBQUUsZ0JBQXFCLEdBQUcsT0FBTyxDQUUzRSJ9
54
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmlnLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvY2xpZW50L2NvbmZpZy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQ0wsS0FBSyxrQkFBa0IsRUFDdkIsV0FBVyxFQUdaLE1BQU0sMEJBQTBCLENBQUM7QUFFbEMsT0FBTyxFQUFFLEtBQUssb0JBQW9CLEVBQWdDLE1BQU0sc0JBQXNCLENBQUM7QUFFL0Y7O0dBRUc7QUFDSCxNQUFNLFdBQVcsZ0JBQWlCLFNBQVEsb0JBQW9CO0lBQzVEOztPQUVHO0lBQ0gsU0FBUyxDQUFDLEVBQUUsTUFBTSxFQUFFLENBQUM7SUFFckI7O09BRUc7SUFDSCxtQkFBbUIsQ0FBQyxFQUFFLE1BQU0sRUFBRSxDQUFDO0lBRS9COztPQUVHO0lBQ0gsc0JBQXNCLENBQUMsRUFBRSxXQUFXLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztJQUUvQzs7T0FFRztJQUNILDRCQUE0QixDQUFDLEVBQUUsTUFBTSxFQUFFLENBQUM7SUFFeEM7O09BRUc7SUFDSCxpQkFBaUIsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUUzQjs7T0FFRztJQUNILHFCQUFxQixDQUFDLEVBQUUsT0FBTyxDQUFDO0lBRWhDOztPQUVHO0lBQ0gsaUJBQWlCLENBQUMsRUFBRSxNQUFNLEVBQUUsQ0FBQztJQUU3Qjs7T0FFRztJQUNILHNCQUFzQixDQUFDLEVBQUUsTUFBTSxDQUFDO0lBRWhDOztPQUVHO0lBQ0gsb0NBQW9DLENBQUMsRUFBRSxNQUFNLENBQUM7Q0FDL0M7QUFFRCxlQUFPLE1BQU0sdUJBQXVCLEVBQUUsa0JBQWtCLENBQUMsZ0JBQWdCLENBb0R4RSxDQUFDO0FBRUY7OztHQUdHO0FBQ0gsd0JBQWdCLDBCQUEwQixJQUFJLGdCQUFnQixDQUU3RDtBQUVEOztHQUVHO0FBQ0gsd0JBQWdCLG9CQUFvQixDQUFDLE1BQU0sR0FBRSxnQkFBcUIsR0FBRyxPQUFPLENBRTNFIn0=
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/client/config.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,kBAAkB,EACvB,WAAW,EAGZ,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAE,KAAK,oBAAoB,EAAgC,MAAM,sBAAsB,CAAC;AAE/F;;GAEG;AACH,MAAM,WAAW,gBAAiB,SAAQ,oBAAoB;IAC5D;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IAErB;;OAEG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE/B;;OAEG;IACH,sBAAsB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;IAE/C;;OAEG;IACH,4BAA4B,CAAC,EAAE,MAAM,EAAE,CAAC;IAExC;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B;;OAEG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAEhC;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE7B;;OAEG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,eAAO,MAAM,uBAAuB,EAAE,kBAAkB,CAAC,gBAAgB,CA+CxE,CAAC;AAEF;;;GAGG;AACH,wBAAgB,0BAA0B,IAAI,gBAAgB,CAE7D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,GAAE,gBAAqB,GAAG,OAAO,CAE3E"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/client/config.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,kBAAkB,EACvB,WAAW,EAGZ,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAE,KAAK,oBAAoB,EAAgC,MAAM,sBAAsB,CAAC;AAE/F;;GAEG;AACH,MAAM,WAAW,gBAAiB,SAAQ,oBAAoB;IAC5D;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IAErB;;OAEG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE/B;;OAEG;IACH,sBAAsB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;IAE/C;;OAEG;IACH,4BAA4B,CAAC,EAAE,MAAM,EAAE,CAAC;IAExC;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B;;OAEG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAEhC;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE7B;;OAEG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC;;OAEG;IACH,oCAAoC,CAAC,EAAE,MAAM,CAAC;CAC/C;AAED,eAAO,MAAM,uBAAuB,EAAE,kBAAkB,CAAC,gBAAgB,CAoDxE,CAAC;AAEF;;;GAGG;AACH,wBAAgB,0BAA0B,IAAI,gBAAgB,CAE7D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,GAAE,gBAAqB,GAAG,OAAO,CAE3E"}
@@ -40,6 +40,11 @@ export const blobClientConfigMapping = {
40
40
  env: 'BLOB_FILE_STORE_UPLOAD_URL',
41
41
  description: 'URL for uploading blobs to filestore (s3://, gs://, file://)'
42
42
  },
43
+ blobHealthcheckUploadIntervalMinutes: {
44
+ env: 'BLOB_HEALTHCHECK_UPLOAD_INTERVAL_MINUTES',
45
+ description: 'Interval in minutes for uploading healthcheck file to file store (default: 60 = 1 hour)',
46
+ parseEnv: (val)=>val ? +val : undefined
47
+ },
43
48
  ...blobArchiveApiConfigMappings
44
49
  };
45
50
  /**
@@ -30,10 +30,11 @@ export interface BlobClientWithFileStoresConfig extends BlobClientConfig {
30
30
  * 2. Creating read-only FileStore clients
31
31
  * 3. Creating a writable FileStore client for uploads
32
32
  * 4. Creating the BlobClient with these dependencies
33
+ * 5. Starting the client (uploads initial healthcheck file if upload client is configured)
33
34
  *
34
35
  * @param config - Configuration containing blob client settings and chain metadata
35
36
  * @param logger - Optional logger for the blob client
36
- * @returns A BlobClientInterface configured with file store support
37
+ * @returns A BlobClientInterface configured with file store support, already started
37
38
  */
38
39
  export declare function createBlobClientWithFileStores(config: BlobClientWithFileStoresConfig, logger?: Logger): Promise<BlobClientInterface>;
39
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmFjdG9yeS5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2NsaWVudC9mYWN0b3J5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxLQUFLLE1BQU0sRUFBZ0IsTUFBTSx1QkFBdUIsQ0FBQztBQVFsRSxPQUFPLEtBQUssRUFBRSxtQkFBbUIsRUFBRSxNQUFNLHVDQUF1QyxDQUFDO0FBQ2pGLE9BQU8sRUFBRSxLQUFLLGdCQUFnQixFQUF3QixNQUFNLGFBQWEsQ0FBQztBQUUxRSxPQUFPLEtBQUssRUFBRSxtQkFBbUIsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBRzFELE1BQU0sV0FBVyxvQkFBb0I7SUFDbkMsTUFBTSxDQUFDLEVBQUUsTUFBTSxDQUFDO0lBQ2hCLDBDQUEwQztJQUMxQyxnQkFBZ0IsQ0FBQyxFQUFFLG1CQUFtQixFQUFFLENBQUM7SUFDekMsMkNBQTJDO0lBQzNDLHFCQUFxQixDQUFDLEVBQUUsbUJBQW1CLENBQUM7Q0FDN0M7QUFFRCx3QkFBZ0IsZ0JBQWdCLENBQUMsTUFBTSxDQUFDLEVBQUUsZ0JBQWdCLEVBQUUsSUFBSSxDQUFDLEVBQUUsb0JBQW9CLEdBQUcsbUJBQW1CLENBbUI1RztBQUVEOzs7R0FHRztBQUNILE1BQU0sV0FBVyw4QkFBK0IsU0FBUSxnQkFBZ0I7SUFDdEUsU0FBUyxFQUFFLE1BQU0sQ0FBQztJQUNsQixhQUFhLEVBQUUsTUFBTSxDQUFDO0lBQ3RCLFdBQVcsRUFBRTtRQUFFLGFBQWEsRUFBRTtZQUFFLFFBQVEsSUFBSSxNQUFNLENBQUE7U0FBRSxDQUFBO0tBQUUsQ0FBQztDQUN4RDtBQUVEOzs7Ozs7Ozs7OztHQVdHO0FBQ0gsd0JBQXNCLDhCQUE4QixDQUNsRCxNQUFNLEVBQUUsOEJBQThCLEVBQ3RDLE1BQU0sQ0FBQyxFQUFFLE1BQU0sR0FDZCxPQUFPLENBQUMsbUJBQW1CLENBQUMsQ0FtQjlCIn0=
40
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmFjdG9yeS5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2NsaWVudC9mYWN0b3J5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxLQUFLLE1BQU0sRUFBZ0IsTUFBTSx1QkFBdUIsQ0FBQztBQVFsRSxPQUFPLEtBQUssRUFBRSxtQkFBbUIsRUFBRSxNQUFNLHVDQUF1QyxDQUFDO0FBQ2pGLE9BQU8sRUFBRSxLQUFLLGdCQUFnQixFQUF3QixNQUFNLGFBQWEsQ0FBQztBQUUxRSxPQUFPLEtBQUssRUFBRSxtQkFBbUIsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBRzFELE1BQU0sV0FBVyxvQkFBb0I7SUFDbkMsTUFBTSxDQUFDLEVBQUUsTUFBTSxDQUFDO0lBQ2hCLDBDQUEwQztJQUMxQyxnQkFBZ0IsQ0FBQyxFQUFFLG1CQUFtQixFQUFFLENBQUM7SUFDekMsMkNBQTJDO0lBQzNDLHFCQUFxQixDQUFDLEVBQUUsbUJBQW1CLENBQUM7Q0FDN0M7QUFFRCx3QkFBZ0IsZ0JBQWdCLENBQUMsTUFBTSxDQUFDLEVBQUUsZ0JBQWdCLEVBQUUsSUFBSSxDQUFDLEVBQUUsb0JBQW9CLEdBQUcsbUJBQW1CLENBbUI1RztBQUVEOzs7R0FHRztBQUNILE1BQU0sV0FBVyw4QkFBK0IsU0FBUSxnQkFBZ0I7SUFDdEUsU0FBUyxFQUFFLE1BQU0sQ0FBQztJQUNsQixhQUFhLEVBQUUsTUFBTSxDQUFDO0lBQ3RCLFdBQVcsRUFBRTtRQUFFLGFBQWEsRUFBRTtZQUFFLFFBQVEsSUFBSSxNQUFNLENBQUE7U0FBRSxDQUFBO0tBQUUsQ0FBQztDQUN4RDtBQUVEOzs7Ozs7Ozs7Ozs7R0FZRztBQUNILHdCQUFzQiw4QkFBOEIsQ0FDbEQsTUFBTSxFQUFFLDhCQUE4QixFQUN0QyxNQUFNLENBQUMsRUFBRSxNQUFNLEdBQ2QsT0FBTyxDQUFDLG1CQUFtQixDQUFDLENBdUI5QiJ9
@@ -1 +1 @@
1
- {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/client/factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,uBAAuB,CAAC;AAQlE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AACjF,OAAO,EAAE,KAAK,gBAAgB,EAAwB,MAAM,aAAa,CAAC;AAE1E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAG1D,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,gBAAgB,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACzC,2CAA2C;IAC3C,qBAAqB,CAAC,EAAE,mBAAmB,CAAC;CAC7C;AAED,wBAAgB,gBAAgB,CAAC,MAAM,CAAC,EAAE,gBAAgB,EAAE,IAAI,CAAC,EAAE,oBAAoB,GAAG,mBAAmB,CAmB5G;AAED;;;GAGG;AACH,MAAM,WAAW,8BAA+B,SAAQ,gBAAgB;IACtE,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE;QAAE,aAAa,EAAE;YAAE,QAAQ,IAAI,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;CACxD;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,8BAA8B,EACtC,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,mBAAmB,CAAC,CAmB9B"}
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/client/factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,uBAAuB,CAAC;AAQlE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AACjF,OAAO,EAAE,KAAK,gBAAgB,EAAwB,MAAM,aAAa,CAAC;AAE1E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAG1D,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,gBAAgB,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACzC,2CAA2C;IAC3C,qBAAqB,CAAC,EAAE,mBAAmB,CAAC;CAC7C;AAED,wBAAgB,gBAAgB,CAAC,MAAM,CAAC,EAAE,gBAAgB,EAAE,IAAI,CAAC,EAAE,oBAAoB,GAAG,mBAAmB,CAmB5G;AAED;;;GAGG;AACH,MAAM,WAAW,8BAA+B,SAAQ,gBAAgB;IACtE,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE;QAAE,aAAa,EAAE;YAAE,QAAQ,IAAI,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;CACxD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,8BAA8B,EACtC,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,mBAAmB,CAAC,CAuB9B"}
@@ -30,10 +30,11 @@ export function createBlobClient(config, deps) {
30
30
  * 2. Creating read-only FileStore clients
31
31
  * 3. Creating a writable FileStore client for uploads
32
32
  * 4. Creating the BlobClient with these dependencies
33
+ * 5. Starting the client (uploads initial healthcheck file if upload client is configured)
33
34
  *
34
35
  * @param config - Configuration containing blob client settings and chain metadata
35
36
  * @param logger - Optional logger for the blob client
36
- * @returns A BlobClientInterface configured with file store support
37
+ * @returns A BlobClientInterface configured with file store support, already started
37
38
  */ export async function createBlobClientWithFileStores(config, logger) {
38
39
  const log = logger ?? createLogger('blob-client');
39
40
  const fileStoreMetadata = {
@@ -45,9 +46,11 @@ export function createBlobClient(config, deps) {
45
46
  createReadOnlyFileStoreBlobClients(config.blobFileStoreUrls, fileStoreMetadata, log),
46
47
  createWritableFileStoreBlobClient(config.blobFileStoreUploadUrl, fileStoreMetadata, log)
47
48
  ]);
48
- return createBlobClient(config, {
49
+ const client = createBlobClient(config, {
49
50
  logger: log,
50
51
  fileStoreClients,
51
52
  fileStoreUploadClient
52
53
  });
54
+ await client.start?.();
55
+ return client;
53
56
  }
@@ -13,6 +13,11 @@ export declare class HttpBlobClient implements BlobClientInterface {
13
13
  protected readonly fileStoreClients: FileStoreBlobClient[];
14
14
  protected readonly fileStoreUploadClient: FileStoreBlobClient | undefined;
15
15
  private disabled;
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?;
16
21
  constructor(config?: BlobClientConfig, opts?: {
17
22
  logger?: Logger;
18
23
  archiveClient?: BlobArchiveClient;
@@ -53,11 +58,28 @@ export declare class HttpBlobClient implements BlobClientInterface {
53
58
  getBlobSidecar(blockHash: `0x${string}`, blobHashes: Buffer[], opts?: GetBlobSidecarOptions): Promise<Blob[]>;
54
59
  private tryFileStores;
55
60
  getBlobSidecarFrom(hostUrl: string, blockHashOrSlot: string | number, blobHashes?: Buffer[], l1ConsensusHostIndex?: number): Promise<Blob[]>;
56
- getBlobsFromHost(hostUrl: string, blockHashOrSlot: string | number, l1ConsensusHostIndex?: number): Promise<BlobJson[]>;
61
+ getBlobsFromHost(hostUrl: string, blockHashOrSlot: string | number, l1ConsensusHostIndex?: number, blobHashes?: Buffer[]): Promise<BlobJson[]>;
57
62
  private fetchBlobSidecars;
58
63
  private getLatestSlotNumber;
59
64
  private getSlotNumber;
60
65
  /** @internal - exposed for testing */
61
66
  getArchiveClient(): BlobArchiveClient | undefined;
67
+ /** Returns true if this client can upload blobs to filestore. */
68
+ canUpload(): boolean;
69
+ /**
70
+ * Start the blob client.
71
+ * Fetches and caches beacon genesis config for timestamp-based slot resolution,
72
+ * then uploads the initial healthcheck file (awaited) and starts periodic uploads.
73
+ */
74
+ start(): Promise<void>;
75
+ /**
76
+ * Start periodic healthcheck upload to the file store to ensure it remains available even if accidentally deleted.
77
+ */
78
+ private startPeriodicHealthcheckUpload;
79
+ private fetchBeaconConfig;
80
+ /**
81
+ * Stop the blob client, clearing any periodic tasks.
82
+ */
83
+ stop(): void;
62
84
  }
63
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaHR0cC5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2NsaWVudC9odHRwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxJQUFJLEVBQUUsS0FBSyxRQUFRLEVBQStCLE1BQU0saUJBQWlCLENBQUM7QUFFbkYsT0FBTyxFQUFFLEtBQUssTUFBTSxFQUFnQixNQUFNLHVCQUF1QixDQUFDO0FBT2xFLE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDakUsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSx1Q0FBdUMsQ0FBQztBQUNqRixPQUFPLEVBQUUsS0FBSyxnQkFBZ0IsRUFBOEIsTUFBTSxhQUFhLENBQUM7QUFDaEYsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVqRixxQkFBYSxjQUFlLFlBQVcsbUJBQW1CO0lBWXRELE9BQU8sQ0FBQyxRQUFRLENBQUMsSUFBSTtJQVh2QixTQUFTLENBQUMsUUFBUSxDQUFDLEdBQUcsRUFBRSxNQUFNLENBQUM7SUFDL0IsU0FBUyxDQUFDLFFBQVEsQ0FBQyxNQUFNLEVBQUUsZ0JBQWdCLENBQUM7SUFDNUMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxhQUFhLEVBQUUsaUJBQWlCLEdBQUcsU0FBUyxDQUFDO0lBQ2hFLFNBQVMsQ0FBQyxRQUFRLENBQUMsS0FBSyxFQUFFLE9BQU8sS0FBSyxDQUFDO0lBQ3ZDLFNBQVMsQ0FBQyxRQUFRLENBQUMsZ0JBQWdCLEVBQUUsbUJBQW1CLEVBQUUsQ0FBQztJQUMzRCxTQUFTLENBQUMsUUFBUSxDQUFDLHFCQUFxQixFQUFFLG1CQUFtQixHQUFHLFNBQVMsQ0FBQztJQUUxRSxPQUFPLENBQUMsUUFBUSxDQUFTO0lBRXpCLFlBQ0UsTUFBTSxDQUFDLEVBQUUsZ0JBQWdCLEVBQ1IsSUFBSSxHQUFFO1FBQ3JCLE1BQU0sQ0FBQyxFQUFFLE1BQU0sQ0FBQztRQUNoQixhQUFhLENBQUMsRUFBRSxpQkFBaUIsQ0FBQztRQUNsQyxnQkFBZ0IsQ0FBQyxFQUFFLG1CQUFtQixFQUFFLENBQUM7UUFDekMscUJBQXFCLENBQUMsRUFBRSxtQkFBbUIsQ0FBQztRQUM1Qyx5RUFBeUU7UUFDekUsY0FBYyxDQUFDLEVBQUUsQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEtBQUssSUFBSSxDQUFDO0tBQ3JDLEVBd0JQO0lBRUQ7OztPQUdHO0lBQ0gsT0FBTyxDQUFDLHNCQUFzQjtJQVU5Qjs7Ozs7T0FLRztJQUNJLFdBQVcsQ0FBQyxLQUFLLEVBQUUsT0FBTyxHQUFHLElBQUksQ0FHdkM7SUFFWSxXQUFXLGtCQW9FdkI7SUFFWSxvQkFBb0IsQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQW1CakU7SUFFRDs7Ozs7Ozs7Ozs7Ozs7T0FjRztJQUNVLGNBQWMsQ0FDekIsU0FBUyxFQUFFLEtBQUssTUFBTSxFQUFFLEVBQ3hCLFVBQVUsRUFBRSxNQUFNLEVBQUUsRUFDcEIsSUFBSSxDQUFDLEVBQUUscUJBQXFCLEdBQzNCLE9BQU8sQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQTZJakI7WUFRYSxhQUFhO0lBc0NkLGtCQUFrQixDQUM3QixPQUFPLEVBQUUsTUFBTSxFQUNmLGVBQWUsRUFBRSxNQUFNLEdBQUcsTUFBTSxFQUNoQyxVQUFVLEdBQUUsTUFBTSxFQUFPLEVBQ3pCLG9CQUFvQixDQUFDLEVBQUUsTUFBTSxHQUM1QixPQUFPLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FHakI7SUFFWSxnQkFBZ0IsQ0FDM0IsT0FBTyxFQUFFLE1BQU0sRUFDZixlQUFlLEVBQUUsTUFBTSxHQUFHLE1BQU0sRUFDaEMsb0JBQW9CLENBQUMsRUFBRSxNQUFNLEdBQzVCLE9BQU8sQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQXNDckI7SUFFRCxPQUFPLENBQUMsaUJBQWlCO1lBWVgsbUJBQW1CO1lBbUNuQixhQUFhO0lBNkQzQixzQ0FBc0M7SUFDL0IsZ0JBQWdCLElBQUksaUJBQWlCLEdBQUcsU0FBUyxDQUV2RDtDQUNGIn0=
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;AACjF,OAAO,EAAE,KAAK,gBAAgB,EAA8B,MAAM,aAAa,CAAC;AAChF,OAAO,KAAK,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAEjF,qBAAa,cAAe,YAAW,mBAAmB;IAYtD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAXvB,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;IAEzB,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;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"}
@@ -5,6 +5,7 @@ import { makeBackoff, retry } from '@aztec/foundation/retry';
5
5
  import { bufferToHex, hexToBuffer } from '@aztec/foundation/string';
6
6
  import { createPublicClient, fallback, http } from 'viem';
7
7
  import { createBlobArchiveClient } from '../archive/factory.js';
8
+ import { DEFAULT_HEALTHCHECK_UPLOAD_INTERVAL_MINUTES } from '../filestore/healthcheck.js';
8
9
  import { getBlobClientConfigFromEnv } from './config.js';
9
10
  export class HttpBlobClient {
10
11
  opts;
@@ -15,6 +16,9 @@ export class HttpBlobClient {
15
16
  fileStoreClients;
16
17
  fileStoreUploadClient;
17
18
  disabled;
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;
18
22
  constructor(config, opts = {}){
19
23
  this.opts = opts;
20
24
  this.disabled = false;
@@ -181,8 +185,8 @@ export class HttpBlobClient {
181
185
  // Return the result, ignoring any undefined ones
182
186
  const getFilledBlobs = ()=>resultBlobs.filter((b)=>b !== undefined);
183
187
  // Helper to fill in results from fetched blobs
184
- const fillResults = (fetchedBlobs)=>{
185
- const blobs = processFetchedBlobs(fetchedBlobs, blobHashes, this.log);
188
+ const fillResults = async (fetchedBlobs)=>{
189
+ const blobs = await processFetchedBlobs(fetchedBlobs, blobHashes, this.log);
186
190
  // Fill in any missing positions with matching blobs
187
191
  for(let i = 0; i < blobHashes.length; i++){
188
192
  if (resultBlobs[i] === undefined) {
@@ -218,7 +222,7 @@ export class HttpBlobClient {
218
222
  ...ctx
219
223
  };
220
224
  this.log.trace(`Attempting to get slot number for block hash`, consensusCtx);
221
- const slotNumber = await this.getSlotNumber(blockHash);
225
+ const slotNumber = await this.getSlotNumber(blockHash, opts?.parentBeaconBlockRoot, opts?.l1BlockTimestamp);
222
226
  this.log.debug(`Got slot number ${slotNumber} from consensus host for querying blobs`, consensusCtx);
223
227
  if (slotNumber) {
224
228
  let l1ConsensusHostUrl;
@@ -233,8 +237,8 @@ export class HttpBlobClient {
233
237
  l1ConsensusHostUrl,
234
238
  ...ctx
235
239
  });
236
- const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex);
237
- const result = fillResults(blobs);
240
+ const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex, getMissingBlobHashes());
241
+ const result = await fillResults(blobs);
238
242
  this.log.debug(`Got ${blobs.length} blobs from consensus host (total: ${result.length}/${blobHashes.length})`, {
239
243
  slotNumber,
240
244
  l1ConsensusHostUrl,
@@ -277,7 +281,7 @@ export class HttpBlobClient {
277
281
  this.log.debug('No blobs found from archive client', archiveCtx);
278
282
  } else {
279
283
  this.log.trace(`Got ${allBlobs.length} blobs from archive client before filtering`, archiveCtx);
280
- const result = fillResults(allBlobs);
284
+ const result = await fillResults(allBlobs);
281
285
  this.log.debug(`Got ${allBlobs.length} blobs from archive client (total: ${result.length}/${blobHashes.length})`, archiveCtx);
282
286
  if (result.length === blobHashes.length) {
283
287
  return returnWithCallback(result);
@@ -318,7 +322,7 @@ export class HttpBlobClient {
318
322
  });
319
323
  const blobs = await client.getBlobsByHashes(blobHashStrings);
320
324
  if (blobs.length > 0) {
321
- const result = fillResults(blobs);
325
+ const result = await fillResults(blobs);
322
326
  this.log.debug(`Got ${blobs.length} blobs from filestore (total: ${result.length}/${ctx.blobHashes.length})`, {
323
327
  url: client.getBaseUrl(),
324
328
  ...ctx
@@ -332,14 +336,14 @@ export class HttpBlobClient {
332
336
  }
333
337
  }
334
338
  async getBlobSidecarFrom(hostUrl, blockHashOrSlot, blobHashes = [], l1ConsensusHostIndex) {
335
- const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
336
- 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);
337
341
  }
338
- async getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex) {
342
+ async getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes) {
339
343
  try {
340
- let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
344
+ let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
341
345
  if (res.ok) {
342
- return parseBlobJsonsFromResponse(await res.json(), this.log);
346
+ return await parseBlobJsonsFromResponse(await res.json(), this.log);
343
347
  }
344
348
  if (res.status === 404 && typeof blockHashOrSlot === 'number') {
345
349
  const latestSlot = await this.getLatestSlotNumber(hostUrl, l1ConsensusHostIndex);
@@ -352,9 +356,9 @@ export class HttpBlobClient {
352
356
  let currentSlot = blockHashOrSlot + 1;
353
357
  while(res.status === 404 && maxRetries > 0 && latestSlot !== undefined && currentSlot <= latestSlot){
354
358
  this.log.debug(`Trying slot ${currentSlot}`);
355
- res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex);
359
+ res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex, blobHashes);
356
360
  if (res.ok) {
357
- return parseBlobJsonsFromResponse(await res.json(), this.log);
361
+ return await parseBlobJsonsFromResponse(await res.json(), this.log);
358
362
  }
359
363
  currentSlot++;
360
364
  maxRetries--;
@@ -371,8 +375,15 @@ export class HttpBlobClient {
371
375
  return [];
372
376
  }
373
377
  }
374
- fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex) {
375
- 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
+ }
376
387
  const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
377
388
  this.log.debug(`Fetching blob sidecar for ${blockHashOrSlot}`, {
378
389
  url,
@@ -418,36 +429,45 @@ export class HttpBlobClient {
418
429
  *
419
430
  * @param blockHash - The block hash
420
431
  * @returns The slot number
421
- */ async getSlotNumber(blockHash) {
432
+ */ async getSlotNumber(blockHash, parentBeaconBlockRoot, l1BlockTimestamp) {
422
433
  const { l1ConsensusHostUrls, l1RpcUrls } = this.config;
423
434
  if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
424
435
  this.log.debug('No consensus host url configured');
425
436
  return undefined;
426
437
  }
427
- if (!l1RpcUrls || l1RpcUrls.length === 0) {
428
- this.log.debug('No execution host url configured');
429
- 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;
430
445
  }
431
- // Ping execution node to get the parentBeaconBlockRoot for this block
432
- let parentBeaconBlockRoot;
433
- const client = createPublicClient({
434
- transport: fallback(l1RpcUrls.map((url)=>http(url, {
435
- batch: false
436
- })))
437
- });
438
- try {
439
- const res = await client.request({
440
- method: 'eth_getBlockByHash',
441
- params: [
442
- blockHash,
443
- /*tx flag*/ false
444
- ]
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
+ })))
445
456
  });
446
- if (res.parentBeaconBlockRoot) {
447
- 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);
448
470
  }
449
- } catch (err) {
450
- this.log.error(`Error getting parent beacon block root`, err);
451
471
  }
452
472
  if (!parentBeaconBlockRoot) {
453
473
  this.log.error(`No parent beacon block root found for block ${blockHash}`);
@@ -474,11 +494,78 @@ export class HttpBlobClient {
474
494
  /** @internal - exposed for testing */ getArchiveClient() {
475
495
  return this.archiveClient;
476
496
  }
497
+ /** Returns true if this client can upload blobs to filestore. */ canUpload() {
498
+ return this.fileStoreUploadClient !== undefined;
499
+ }
500
+ /**
501
+ * Start the blob client.
502
+ * Fetches and caches beacon genesis config for timestamp-based slot resolution,
503
+ * then uploads the initial healthcheck file (awaited) and starts periodic uploads.
504
+ */ async start() {
505
+ await this.fetchBeaconConfig();
506
+ if (!this.fileStoreUploadClient) {
507
+ return;
508
+ }
509
+ await this.fileStoreUploadClient.uploadHealthcheck();
510
+ this.log.debug('Initial healthcheck file uploaded');
511
+ this.startPeriodicHealthcheckUpload();
512
+ }
513
+ /**
514
+ * Start periodic healthcheck upload to the file store to ensure it remains available even if accidentally deleted.
515
+ */ startPeriodicHealthcheckUpload() {
516
+ const intervalMs = (this.config.blobHealthcheckUploadIntervalMinutes ?? DEFAULT_HEALTHCHECK_UPLOAD_INTERVAL_MINUTES) * 60 * 1000;
517
+ this.healthcheckUploadIntervalId = setInterval(()=>{
518
+ void this.fileStoreUploadClient.uploadHealthcheck().catch((err)=>{
519
+ this.log.warn('Failed to upload periodic healthcheck file', err);
520
+ });
521
+ }, intervalMs);
522
+ }
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
+ /**
558
+ * Stop the blob client, clearing any periodic tasks.
559
+ */ stop() {
560
+ if (this.healthcheckUploadIntervalId) {
561
+ clearInterval(this.healthcheckUploadIntervalId);
562
+ this.healthcheckUploadIntervalId = undefined;
563
+ }
564
+ }
477
565
  }
478
- function parseBlobJsonsFromResponse(response, logger) {
566
+ async function parseBlobJsonsFromResponse(response, logger) {
479
567
  try {
480
- const blobs = response.data.map(parseBlobJson);
481
- return blobs;
568
+ return await Promise.all(response.data.map(parseBlobJson));
482
569
  } catch (err) {
483
570
  logger.error(`Error parsing blob json from response`, err);
484
571
  return [];
@@ -488,15 +575,14 @@ function parseBlobJsonsFromResponse(response, logger) {
488
575
  // https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getBlobSidecars
489
576
  // Here we attempt to parse the response data to Buffer, and check the lengths (via Blob's constructor), to avoid
490
577
  // throwing an error down the line when calling Blob.fromJson().
491
- function parseBlobJson(data) {
492
- const blobBuffer = Buffer.from(data.blob.slice(2), 'hex');
493
- const commitmentBuffer = Buffer.from(data.kzg_commitment.slice(2), 'hex');
494
- 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);
495
581
  return blob.toJSON();
496
582
  }
497
583
  // Returns an array that maps each blob hash to the corresponding blob, or undefined if the blob is not found
498
584
  // or the data does not match the commitment.
499
- function processFetchedBlobs(blobs, blobHashes, logger) {
585
+ async function processFetchedBlobs(blobs, blobHashes, logger) {
500
586
  const requestedBlobHashes = new Set(blobHashes.map(bufferToHex));
501
587
  const hashToBlob = new Map();
502
588
  for (const blobJson of blobs){
@@ -505,7 +591,7 @@ function processFetchedBlobs(blobs, blobHashes, logger) {
505
591
  continue;
506
592
  }
507
593
  try {
508
- const blob = Blob.fromJson(blobJson);
594
+ const blob = await Blob.fromJson(blobJson);
509
595
  hashToBlob.set(hashHex, blob);
510
596
  } catch (err) {
511
597
  // If the above throws, it's likely that the blob commitment does not match the hash of the blob data.
@@ -10,13 +10,30 @@ 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. */
16
27
  sendBlobsToFilestore(blobs: Blob[]): Promise<boolean>;
17
28
  /** Fetches the given blob sidecars by block hash and blob hashes. */
18
29
  getBlobSidecar(blockId: string, blobHashes?: Buffer[], opts?: GetBlobSidecarOptions): Promise<Blob[]>;
30
+ /** Starts the blob client (e.g., uploads healthcheck file if not exists). */
31
+ start?(): Promise<void>;
19
32
  /** Tests all configured blob sources and logs whether they are reachable or not. */
20
33
  testSources(): Promise<void>;
34
+ /** Stops the blob client, clearing any periodic tasks. */
35
+ stop?(): void;
36
+ /** Returns true if this client can upload blobs to filestore. */
37
+ canUpload(): boolean;
21
38
  }
22
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvY2xpZW50L2ludGVyZmFjZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssRUFBRSxJQUFJLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUU1Qzs7R0FFRztBQUNILE1BQU0sV0FBVyxxQkFBcUI7SUFDcEM7Ozs7O09BS0c7SUFDSCxnQkFBZ0IsQ0FBQyxFQUFFLE9BQU8sQ0FBQztDQUM1QjtBQUVELE1BQU0sV0FBVyxtQkFBbUI7SUFDbEMsMEVBQTBFO0lBQzFFLG9CQUFvQixDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLENBQUM7SUFDdEQscUVBQXFFO0lBQ3JFLGNBQWMsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLFVBQVUsQ0FBQyxFQUFFLE1BQU0sRUFBRSxFQUFFLElBQUksQ0FBQyxFQUFFLHFCQUFxQixHQUFHLE9BQU8sQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBQ3RHLG9GQUFvRjtJQUNwRixXQUFXLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO0NBQzlCIn0=
39
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvY2xpZW50L2ludGVyZmFjZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssRUFBRSxJQUFJLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUU1Qzs7R0FFRztBQUNILE1BQU0sV0FBVyxxQkFBcUI7SUFDcEM7Ozs7O09BS0c7SUFDSCxnQkFBZ0IsQ0FBQyxFQUFFLE9BQU8sQ0FBQztJQUMzQjs7O09BR0c7SUFDSCxxQkFBcUIsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUMvQjs7OztPQUlHO0lBQ0gsZ0JBQWdCLENBQUMsRUFBRSxNQUFNLENBQUM7Q0FDM0I7QUFFRCxNQUFNLFdBQVcsbUJBQW1CO0lBQ2xDLDBFQUEwRTtJQUMxRSxvQkFBb0IsQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQ3RELHFFQUFxRTtJQUNyRSxjQUFjLENBQUMsT0FBTyxFQUFFLE1BQU0sRUFBRSxVQUFVLENBQUMsRUFBRSxNQUFNLEVBQUUsRUFBRSxJQUFJLENBQUMsRUFBRSxxQkFBcUIsR0FBRyxPQUFPLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUN0Ryw2RUFBNkU7SUFDN0UsS0FBSyxDQUFDLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQ3hCLG9GQUFvRjtJQUNwRixXQUFXLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQzdCLDBEQUEwRDtJQUMxRCxJQUFJLENBQUMsSUFBSSxJQUFJLENBQUM7SUFDZCxpRUFBaUU7SUFDakUsU0FBUyxJQUFJLE9BQU8sQ0FBQztDQUN0QiJ9