@aztec/blob-client 0.0.1-commit.03f7ef2 → 0.0.1-commit.04852196a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/dest/archive/config.js +1 -1
- package/dest/archive/instrumentation.d.ts +1 -1
- package/dest/archive/instrumentation.d.ts.map +1 -1
- package/dest/archive/instrumentation.js +13 -13
- package/dest/blobstore/blob_store_test_suite.js +9 -9
- package/dest/client/config.d.ts +5 -1
- package/dest/client/config.d.ts.map +1 -1
- package/dest/client/config.js +5 -0
- package/dest/client/factory.d.ts +3 -2
- package/dest/client/factory.d.ts.map +1 -1
- package/dest/client/factory.js +5 -2
- package/dest/client/http.d.ts +24 -2
- package/dest/client/http.d.ts.map +1 -1
- package/dest/client/http.js +133 -47
- package/dest/client/interface.d.ts +18 -1
- package/dest/client/interface.d.ts.map +1 -1
- package/dest/client/local.d.ts +3 -1
- package/dest/client/local.d.ts.map +1 -1
- package/dest/client/local.js +3 -0
- package/dest/client/tests.js +3 -3
- package/dest/filestore/filestore_blob_client.d.ts +14 -2
- package/dest/filestore/filestore_blob_client.d.ts.map +1 -1
- package/dest/filestore/filestore_blob_client.js +29 -5
- package/dest/filestore/healthcheck.d.ts +5 -0
- package/dest/filestore/healthcheck.d.ts.map +1 -0
- package/dest/filestore/healthcheck.js +3 -0
- package/package.json +8 -8
- package/src/archive/config.ts +1 -1
- package/src/archive/instrumentation.ts +22 -13
- package/src/blobstore/blob_store_test_suite.ts +9 -9
- package/src/client/config.ts +10 -0
- package/src/client/factory.ts +7 -2
- package/src/client/http.ts +175 -41
- package/src/client/interface.ts +17 -0
- package/src/client/local.ts +5 -0
- package/src/client/tests.ts +2 -2
- package/src/filestore/filestore_blob_client.ts +33 -5
- package/src/filestore/healthcheck.ts +5 -0
package/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** (`
|
|
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
|
package/dest/archive/config.js
CHANGED
|
@@ -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: '
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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([
|
package/dest/client/config.d.ts
CHANGED
|
@@ -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,
|
|
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;
|
|
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"}
|
package/dest/client/config.js
CHANGED
|
@@ -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
|
/**
|
package/dest/client/factory.d.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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"}
|
package/dest/client/factory.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dest/client/http.d.ts
CHANGED
|
@@ -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,
|
|
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;
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/client/http.ts"],"names":[],"mappings":"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"}
|
package/dest/client/http.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
447
|
-
|
|
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
|
-
|
|
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(
|
|
492
|
-
const blobBuffer = Buffer.from(
|
|
493
|
-
const
|
|
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,
|
|
39
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvY2xpZW50L2ludGVyZmFjZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssRUFBRSxJQUFJLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUU1Qzs7R0FFRztBQUNILE1BQU0sV0FBVyxxQkFBcUI7SUFDcEM7Ozs7O09BS0c7SUFDSCxnQkFBZ0IsQ0FBQyxFQUFFLE9BQU8sQ0FBQztJQUMzQjs7O09BR0c7SUFDSCxxQkFBcUIsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUMvQjs7OztPQUlHO0lBQ0gsZ0JBQWdCLENBQUMsRUFBRSxNQUFNLENBQUM7Q0FDM0I7QUFFRCxNQUFNLFdBQVcsbUJBQW1CO0lBQ2xDLDBFQUEwRTtJQUMxRSxvQkFBb0IsQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQ3RELHFFQUFxRTtJQUNyRSxjQUFjLENBQUMsT0FBTyxFQUFFLE1BQU0sRUFBRSxVQUFVLENBQUMsRUFBRSxNQUFNLEVBQUUsRUFBRSxJQUFJLENBQUMsRUFBRSxxQkFBcUIsR0FBRyxPQUFPLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUN0Ryw2RUFBNkU7SUFDN0UsS0FBSyxDQUFDLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQ3hCLG9GQUFvRjtJQUNwRixXQUFXLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQzdCLDBEQUEwRDtJQUMxRCxJQUFJLENBQUMsSUFBSSxJQUFJLENBQUM7SUFDZCxpRUFBaUU7SUFDakUsU0FBUyxJQUFJLE9BQU8sQ0FBQztDQUN0QiJ9
|