@aztec/blob-client 0.0.1-commit.8afd444 → 0.0.1-commit.8c0b8ff
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/dest/blobstore/blob_store_test_suite.js +9 -9
- package/dest/client/config.d.ts +3 -1
- package/dest/client/config.d.ts.map +1 -1
- package/dest/client/config.js +6 -1
- package/dest/client/http.d.ts +1 -1
- package/dest/client/http.d.ts.map +1 -1
- package/dest/client/http.js +61 -26
- package/dest/client/tests.js +3 -3
- package/package.json +7 -7
- package/src/blobstore/blob_store_test_suite.ts +9 -9
- package/src/client/config.ts +9 -0
- package/src/client/http.ts +66 -25
- package/src/client/tests.ts +2 -2
|
@@ -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
|
@@ -40,6 +40,8 @@ export interface BlobClientConfig extends BlobArchiveApiConfig {
|
|
|
40
40
|
* Interval in minutes for uploading healthcheck file to file store (default: 60 = 1 hour)
|
|
41
41
|
*/
|
|
42
42
|
blobHealthcheckUploadIntervalMinutes?: number;
|
|
43
|
+
/** Timeout for HTTP requests to the L1 RPC node in ms. */
|
|
44
|
+
l1HttpTimeoutMS?: number;
|
|
43
45
|
}
|
|
44
46
|
export declare const blobClientConfigMapping: ConfigMappingsType<BlobClientConfig>;
|
|
45
47
|
/**
|
|
@@ -51,4 +53,4 @@ export declare function getBlobClientConfigFromEnv(): BlobClientConfig;
|
|
|
51
53
|
* Returns whether the given blob client config has any remote sources defined.
|
|
52
54
|
*/
|
|
53
55
|
export declare function hasRemoteBlobSources(config?: BlobClientConfig): boolean;
|
|
54
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
56
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmlnLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvY2xpZW50L2NvbmZpZy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQ0wsS0FBSyxrQkFBa0IsRUFDdkIsV0FBVyxFQUlaLE1BQU0sMEJBQTBCLENBQUM7QUFFbEMsT0FBTyxFQUFFLEtBQUssb0JBQW9CLEVBQWdDLE1BQU0sc0JBQXNCLENBQUM7QUFFL0Y7O0dBRUc7QUFDSCxNQUFNLFdBQVcsZ0JBQWlCLFNBQVEsb0JBQW9CO0lBQzVEOztPQUVHO0lBQ0gsU0FBUyxDQUFDLEVBQUUsTUFBTSxFQUFFLENBQUM7SUFFckI7O09BRUc7SUFDSCxtQkFBbUIsQ0FBQyxFQUFFLE1BQU0sRUFBRSxDQUFDO0lBRS9COztPQUVHO0lBQ0gsc0JBQXNCLENBQUMsRUFBRSxXQUFXLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztJQUUvQzs7T0FFRztJQUNILDRCQUE0QixDQUFDLEVBQUUsTUFBTSxFQUFFLENBQUM7SUFFeEM7O09BRUc7SUFDSCxpQkFBaUIsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUUzQjs7T0FFRztJQUNILHFCQUFxQixDQUFDLEVBQUUsT0FBTyxDQUFDO0lBRWhDOztPQUVHO0lBQ0gsaUJBQWlCLENBQUMsRUFBRSxNQUFNLEVBQUUsQ0FBQztJQUU3Qjs7T0FFRztJQUNILHNCQUFzQixDQUFDLEVBQUUsTUFBTSxDQUFDO0lBRWhDOztPQUVHO0lBQ0gsb0NBQW9DLENBQUMsRUFBRSxNQUFNLENBQUM7SUFFOUMsMERBQTBEO0lBQzFELGVBQWUsQ0FBQyxFQUFFLE1BQU0sQ0FBQztDQUMxQjtBQUVELGVBQU8sTUFBTSx1QkFBdUIsRUFBRSxrQkFBa0IsQ0FBQyxnQkFBZ0IsQ0F5RHhFLENBQUM7QUFFRjs7O0dBR0c7QUFDSCx3QkFBZ0IsMEJBQTBCLElBQUksZ0JBQWdCLENBRTdEO0FBRUQ7O0dBRUc7QUFDSCx3QkFBZ0Isb0JBQW9CLENBQUMsTUFBTSxHQUFFLGdCQUFxQixHQUFHLE9BQU8sQ0FFM0UifQ==
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/client/config.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,kBAAkB,EACvB,WAAW,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/client/config.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,kBAAkB,EACvB,WAAW,EAIZ,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;IAE9C,0DAA0D;IAC1D,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,eAAO,MAAM,uBAAuB,EAAE,kBAAkB,CAAC,gBAAgB,CAyDxE,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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SecretValue, booleanConfigHelper, getConfigFromMappings } from '@aztec/foundation/config';
|
|
1
|
+
import { SecretValue, booleanConfigHelper, getConfigFromMappings, optionalNumberConfigHelper } from '@aztec/foundation/config';
|
|
2
2
|
import { blobArchiveApiConfigMappings } from '../archive/config.js';
|
|
3
3
|
export const blobClientConfigMapping = {
|
|
4
4
|
l1RpcUrls: {
|
|
@@ -45,6 +45,11 @@ export const blobClientConfigMapping = {
|
|
|
45
45
|
description: 'Interval in minutes for uploading healthcheck file to file store (default: 60 = 1 hour)',
|
|
46
46
|
parseEnv: (val)=>val ? +val : undefined
|
|
47
47
|
},
|
|
48
|
+
l1HttpTimeoutMS: {
|
|
49
|
+
env: 'ETHEREUM_HTTP_TIMEOUT_MS',
|
|
50
|
+
description: 'Timeout for HTTP requests to the L1 RPC node in ms.',
|
|
51
|
+
...optionalNumberConfigHelper()
|
|
52
|
+
},
|
|
48
53
|
...blobArchiveApiConfigMappings
|
|
49
54
|
};
|
|
50
55
|
/**
|
package/dest/client/http.d.ts
CHANGED
|
@@ -76,4 +76,4 @@ export declare class HttpBlobClient implements BlobClientInterface {
|
|
|
76
76
|
*/
|
|
77
77
|
stop(): void;
|
|
78
78
|
}
|
|
79
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
79
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaHR0cC5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2NsaWVudC9odHRwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxJQUFJLEVBQUUsS0FBSyxRQUFRLEVBQStCLE1BQU0saUJBQWlCLENBQUM7QUFHbkYsT0FBTyxFQUFFLEtBQUssTUFBTSxFQUFnQixNQUFNLHVCQUF1QixDQUFDO0FBT2xFLE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDakUsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSx1Q0FBdUMsQ0FBQztBQUVqRixPQUFPLEVBQUUsS0FBSyxnQkFBZ0IsRUFBOEIsTUFBTSxhQUFhLENBQUM7QUFDaEYsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVqRixxQkFBYSxjQUFlLFlBQVcsbUJBQW1CO0lBYXRELE9BQU8sQ0FBQyxRQUFRLENBQUMsSUFBSTtJQVp2QixTQUFTLENBQUMsUUFBUSxDQUFDLEdBQUcsRUFBRSxNQUFNLENBQUM7SUFDL0IsU0FBUyxDQUFDLFFBQVEsQ0FBQyxNQUFNLEVBQUUsZ0JBQWdCLENBQUM7SUFDNUMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxhQUFhLEVBQUUsaUJBQWlCLEdBQUcsU0FBUyxDQUFDO0lBQ2hFLFNBQVMsQ0FBQyxRQUFRLENBQUMsS0FBSyxFQUFFLE9BQU8sS0FBSyxDQUFDO0lBQ3ZDLFNBQVMsQ0FBQyxRQUFRLENBQUMsZ0JBQWdCLEVBQUUsbUJBQW1CLEVBQUUsQ0FBQztJQUMzRCxTQUFTLENBQUMsUUFBUSxDQUFDLHFCQUFxQixFQUFFLG1CQUFtQixHQUFHLFNBQVMsQ0FBQztJQUUxRSxPQUFPLENBQUMsUUFBUSxDQUFTO0lBQ3pCLE9BQU8sQ0FBQywyQkFBMkIsQ0FBQyxDQUFpQjtJQUVyRCxZQUNFLE1BQU0sQ0FBQyxFQUFFLGdCQUFnQixFQUNSLElBQUksR0FBRTtRQUNyQixNQUFNLENBQUMsRUFBRSxNQUFNLENBQUM7UUFDaEIsYUFBYSxDQUFDLEVBQUUsaUJBQWlCLENBQUM7UUFDbEMsZ0JBQWdCLENBQUMsRUFBRSxtQkFBbUIsRUFBRSxDQUFDO1FBQ3pDLHFCQUFxQixDQUFDLEVBQUUsbUJBQW1CLENBQUM7UUFDNUMseUVBQXlFO1FBQ3pFLGNBQWMsQ0FBQyxFQUFFLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxLQUFLLElBQUksQ0FBQztLQUNyQyxFQXdCUDtJQUVEOzs7T0FHRztJQUNILE9BQU8sQ0FBQyxzQkFBc0I7SUFVOUI7Ozs7O09BS0c7SUFDSSxXQUFXLENBQUMsS0FBSyxFQUFFLE9BQU8sR0FBRyxJQUFJLENBR3ZDO0lBRVksV0FBVyxrQkF3R3ZCO0lBRVksb0JBQW9CLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxHQUFHLE9BQU8sQ0FBQyxPQUFPLENBQUMsQ0FtQmpFO0lBRUQ7Ozs7Ozs7Ozs7Ozs7O09BY0c7SUFDVSxjQUFjLENBQ3pCLFNBQVMsRUFBRSxLQUFLLE1BQU0sRUFBRSxFQUN4QixVQUFVLEVBQUUsTUFBTSxFQUFFLEVBQ3BCLElBQUksQ0FBQyxFQUFFLHFCQUFxQixHQUMzQixPQUFPLENBQUMsSUFBSSxFQUFFLENBQUMsQ0E2SWpCO1lBUWEsYUFBYTtJQXNDZCxrQkFBa0IsQ0FDN0IsT0FBTyxFQUFFLE1BQU0sRUFDZixlQUFlLEVBQUUsTUFBTSxHQUFHLE1BQU0sRUFDaEMsVUFBVSxHQUFFLE1BQU0sRUFBTyxFQUN6QixvQkFBb0IsQ0FBQyxFQUFFLE1BQU0sR0FDNUIsT0FBTyxDQUFDLElBQUksRUFBRSxDQUFDLENBR2pCO0lBRVksZ0JBQWdCLENBQzNCLE9BQU8sRUFBRSxNQUFNLEVBQ2YsZUFBZSxFQUFFLE1BQU0sR0FBRyxNQUFNLEVBQ2hDLG9CQUFvQixDQUFDLEVBQUUsTUFBTSxHQUM1QixPQUFPLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FzQ3JCO0lBRUQsT0FBTyxDQUFDLGlCQUFpQjtZQVlYLG1CQUFtQjtZQW1DbkIsYUFBYTtJQTZEM0Isc0NBQXNDO0lBQy9CLGdCQUFnQixJQUFJLGlCQUFpQixHQUFHLFNBQVMsQ0FFdkQ7SUFFRCxpRUFBaUU7SUFDMUQsU0FBUyxJQUFJLE9BQU8sQ0FFMUI7SUFFRDs7O09BR0c7SUFDVSxLQUFLLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQVNsQztJQUVEOztPQUVHO0lBQ0gsT0FBTyxDQUFDLDhCQUE4QjtJQVd0Qzs7T0FFRztJQUNJLElBQUksSUFBSSxJQUFJLENBS2xCO0NBQ0YifQ==
|
|
@@ -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;
|
|
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;AAGnF,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,uBAAuB,CAAC;AAOlE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAEjF,OAAO,EAAE,KAAK,gBAAgB,EAA8B,MAAM,aAAa,CAAC;AAChF,OAAO,KAAK,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAEjF,qBAAa,cAAe,YAAW,mBAAmB;IAatD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAZvB,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAC/B,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAC5C,SAAS,CAAC,QAAQ,CAAC,aAAa,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAChE,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC;IACvC,SAAS,CAAC,QAAQ,CAAC,gBAAgB,EAAE,mBAAmB,EAAE,CAAC;IAC3D,SAAS,CAAC,QAAQ,CAAC,qBAAqB,EAAE,mBAAmB,GAAG,SAAS,CAAC;IAE1E,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,2BAA2B,CAAC,CAAiB;IAErD,YACE,MAAM,CAAC,EAAE,gBAAgB,EACR,IAAI,GAAE;QACrB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,aAAa,CAAC,EAAE,iBAAiB,CAAC;QAClC,gBAAgB,CAAC,EAAE,mBAAmB,EAAE,CAAC;QACzC,qBAAqB,CAAC,EAAE,mBAAmB,CAAC;QAC5C,yEAAyE;QACzE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;KACrC,EAwBP;IAED;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAU9B;;;;;OAKG;IACI,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAGvC;IAEY,WAAW,kBAwGvB;IAEY,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAmBjE;IAED;;;;;;;;;;;;;;OAcG;IACU,cAAc,CACzB,SAAS,EAAE,KAAK,MAAM,EAAE,EACxB,UAAU,EAAE,MAAM,EAAE,EACpB,IAAI,CAAC,EAAE,qBAAqB,GAC3B,OAAO,CAAC,IAAI,EAAE,CAAC,CA6IjB;YAQa,aAAa;IAsCd,kBAAkB,CAC7B,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GAAG,MAAM,EAChC,UAAU,GAAE,MAAM,EAAO,EACzB,oBAAoB,CAAC,EAAE,MAAM,GAC5B,OAAO,CAAC,IAAI,EAAE,CAAC,CAGjB;IAEY,gBAAgB,CAC3B,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GAAG,MAAM,EAChC,oBAAoB,CAAC,EAAE,MAAM,GAC5B,OAAO,CAAC,QAAQ,EAAE,CAAC,CAsCrB;IAED,OAAO,CAAC,iBAAiB;YAYX,mBAAmB;YAmCnB,aAAa;IA6D3B,sCAAsC;IAC/B,gBAAgB,IAAI,iBAAiB,GAAG,SAAS,CAEvD;IAED,iEAAiE;IAC1D,SAAS,IAAI,OAAO,CAE1B;IAED;;;OAGG;IACU,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CASlC;IAED;;OAEG;IACH,OAAO,CAAC,8BAA8B;IAWtC;;OAEG;IACI,IAAI,IAAI,IAAI,CAKlB;CACF"}
|
package/dest/client/http.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Blob, computeEthVersionedBlobHash } from '@aztec/blob-lib';
|
|
2
|
+
import { makeL1HttpTransport } from '@aztec/ethereum/client';
|
|
2
3
|
import { shuffle } from '@aztec/foundation/array';
|
|
3
4
|
import { createLogger } from '@aztec/foundation/log';
|
|
4
5
|
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
5
6
|
import { bufferToHex, hexToBuffer } from '@aztec/foundation/string';
|
|
6
|
-
import { createPublicClient
|
|
7
|
+
import { createPublicClient } from 'viem';
|
|
7
8
|
import { createBlobArchiveClient } from '../archive/factory.js';
|
|
8
9
|
import { DEFAULT_HEALTHCHECK_UPLOAD_INTERVAL_MINUTES } from '../filestore/healthcheck.js';
|
|
9
10
|
import { getBlobClientConfigFromEnv } from './config.js';
|
|
@@ -66,22 +67,50 @@ export class HttpBlobClient {
|
|
|
66
67
|
l1ConsensusHostUrls,
|
|
67
68
|
archiveUrl
|
|
68
69
|
});
|
|
69
|
-
let
|
|
70
|
+
let consensusSuperNodes = 0;
|
|
71
|
+
let consensusNonSuperNodes = 0;
|
|
72
|
+
let archiveSources = 0;
|
|
73
|
+
let blobSinks = 0;
|
|
70
74
|
if (l1ConsensusHostUrls && l1ConsensusHostUrls.length > 0) {
|
|
71
75
|
for(let l1ConsensusHostIndex = 0; l1ConsensusHostIndex < l1ConsensusHostUrls.length; l1ConsensusHostIndex++){
|
|
72
76
|
const l1ConsensusHostUrl = l1ConsensusHostUrls[l1ConsensusHostIndex];
|
|
73
77
|
try {
|
|
74
|
-
const { url, ...options } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrl}/eth/v1/beacon/headers`, this.config, l1ConsensusHostIndex);
|
|
78
|
+
const { url, ...options } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrl}/eth/v1/beacon/headers/head`, this.config, l1ConsensusHostIndex);
|
|
75
79
|
const res = await this.fetch(url, options);
|
|
76
|
-
if (res.ok) {
|
|
77
|
-
this.log.
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
this.log.error(`Failure reaching L1 consensus host: ${res.statusText} (${res.status})`, {
|
|
78
82
|
l1ConsensusHostUrl
|
|
79
83
|
});
|
|
80
|
-
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
this.log.info(`L1 consensus host is reachable`, {
|
|
87
|
+
l1ConsensusHostUrl
|
|
88
|
+
});
|
|
89
|
+
// Check if the host serves blob sidecars (supernode/semi-supernode).
|
|
90
|
+
// Post-Fusaka (PeerDAS), non-supernode beacon nodes no longer serve the
|
|
91
|
+
// blob sidecar endpoint. A 200 response (even with an empty data array
|
|
92
|
+
// for a slot with no blobs) means the node supports serving blob sidecars.
|
|
93
|
+
const body = await res.json();
|
|
94
|
+
const headSlot = body?.data?.header?.message?.slot;
|
|
95
|
+
if (headSlot) {
|
|
96
|
+
const { url: blobUrl, ...blobOptions } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrl}/eth/v1/beacon/blobs/${headSlot}`, this.config, l1ConsensusHostIndex);
|
|
97
|
+
const blobRes = await this.fetch(blobUrl, blobOptions);
|
|
98
|
+
if (blobRes.ok) {
|
|
99
|
+
this.log.info(`L1 consensus host serves blob sidecars (supernode)`, {
|
|
100
|
+
l1ConsensusHostUrl
|
|
101
|
+
});
|
|
102
|
+
consensusSuperNodes++;
|
|
103
|
+
} else {
|
|
104
|
+
this.log.info(`L1 consensus host does not serve blob sidecars`, {
|
|
105
|
+
l1ConsensusHostUrl
|
|
106
|
+
});
|
|
107
|
+
consensusNonSuperNodes++;
|
|
108
|
+
}
|
|
81
109
|
} else {
|
|
82
|
-
this.log.
|
|
110
|
+
this.log.info(`L1 consensus host is reachable but could not determine head slot`, {
|
|
83
111
|
l1ConsensusHostUrl
|
|
84
112
|
});
|
|
113
|
+
consensusNonSuperNodes++;
|
|
85
114
|
}
|
|
86
115
|
} catch (err) {
|
|
87
116
|
this.log.error(`Error reaching L1 consensus host`, err, {
|
|
@@ -89,8 +118,6 @@ export class HttpBlobClient {
|
|
|
89
118
|
});
|
|
90
119
|
}
|
|
91
120
|
}
|
|
92
|
-
} else {
|
|
93
|
-
this.log.warn('No L1 consensus host urls configured');
|
|
94
121
|
}
|
|
95
122
|
if (this.archiveClient) {
|
|
96
123
|
try {
|
|
@@ -99,14 +126,12 @@ export class HttpBlobClient {
|
|
|
99
126
|
latest,
|
|
100
127
|
archiveUrl
|
|
101
128
|
});
|
|
102
|
-
|
|
129
|
+
archiveSources++;
|
|
103
130
|
} catch (err) {
|
|
104
131
|
this.log.error(`Error reaching archive client`, err, {
|
|
105
132
|
archiveUrl
|
|
106
133
|
});
|
|
107
134
|
}
|
|
108
|
-
} else {
|
|
109
|
-
this.log.warn('No archive client configured');
|
|
110
135
|
}
|
|
111
136
|
if (this.fileStoreClients.length > 0) {
|
|
112
137
|
for (const fileStoreClient of this.fileStoreClients){
|
|
@@ -116,7 +141,7 @@ export class HttpBlobClient {
|
|
|
116
141
|
this.log.info(`FileStore is reachable`, {
|
|
117
142
|
url: fileStoreClient.getBaseUrl()
|
|
118
143
|
});
|
|
119
|
-
|
|
144
|
+
blobSinks++;
|
|
120
145
|
} else {
|
|
121
146
|
this.log.warn(`FileStore is not accessible`, {
|
|
122
147
|
url: fileStoreClient.getBaseUrl()
|
|
@@ -129,12 +154,22 @@ export class HttpBlobClient {
|
|
|
129
154
|
}
|
|
130
155
|
}
|
|
131
156
|
}
|
|
157
|
+
// Emit a single summary after validating all sources
|
|
158
|
+
const successfulSourceCount = consensusSuperNodes + archiveSources + blobSinks;
|
|
159
|
+
let summary = `Blob client running with consensusSuperNodes=${consensusSuperNodes} archiveSources=${archiveSources} blobSinks=${blobSinks}`;
|
|
160
|
+
if (consensusNonSuperNodes > 0) {
|
|
161
|
+
summary += `. ${consensusNonSuperNodes} consensus client(s) ignored because they are not running in supernode or semi-supernode mode`;
|
|
162
|
+
}
|
|
132
163
|
if (successfulSourceCount === 0) {
|
|
133
164
|
if (this.config.blobAllowEmptySources) {
|
|
134
|
-
this.log.warn(
|
|
165
|
+
this.log.warn(summary);
|
|
135
166
|
} else {
|
|
136
|
-
throw new Error(
|
|
167
|
+
throw new Error(summary);
|
|
137
168
|
}
|
|
169
|
+
} else if (consensusSuperNodes === 0) {
|
|
170
|
+
this.log.warn(summary);
|
|
171
|
+
} else {
|
|
172
|
+
this.log.info(summary);
|
|
138
173
|
}
|
|
139
174
|
}
|
|
140
175
|
async sendBlobsToFilestore(blobs) {
|
|
@@ -183,8 +218,8 @@ export class HttpBlobClient {
|
|
|
183
218
|
// Return the result, ignoring any undefined ones
|
|
184
219
|
const getFilledBlobs = ()=>resultBlobs.filter((b)=>b !== undefined);
|
|
185
220
|
// Helper to fill in results from fetched blobs
|
|
186
|
-
const fillResults = (fetchedBlobs)=>{
|
|
187
|
-
const blobs = processFetchedBlobs(fetchedBlobs, blobHashes, this.log);
|
|
221
|
+
const fillResults = async (fetchedBlobs)=>{
|
|
222
|
+
const blobs = await processFetchedBlobs(fetchedBlobs, blobHashes, this.log);
|
|
188
223
|
// Fill in any missing positions with matching blobs
|
|
189
224
|
for(let i = 0; i < blobHashes.length; i++){
|
|
190
225
|
if (resultBlobs[i] === undefined) {
|
|
@@ -236,7 +271,7 @@ export class HttpBlobClient {
|
|
|
236
271
|
...ctx
|
|
237
272
|
});
|
|
238
273
|
const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex);
|
|
239
|
-
const result = fillResults(blobs);
|
|
274
|
+
const result = await fillResults(blobs);
|
|
240
275
|
this.log.debug(`Got ${blobs.length} blobs from consensus host (total: ${result.length}/${blobHashes.length})`, {
|
|
241
276
|
slotNumber,
|
|
242
277
|
l1ConsensusHostUrl,
|
|
@@ -279,7 +314,7 @@ export class HttpBlobClient {
|
|
|
279
314
|
this.log.debug('No blobs found from archive client', archiveCtx);
|
|
280
315
|
} else {
|
|
281
316
|
this.log.trace(`Got ${allBlobs.length} blobs from archive client before filtering`, archiveCtx);
|
|
282
|
-
const result = fillResults(allBlobs);
|
|
317
|
+
const result = await fillResults(allBlobs);
|
|
283
318
|
this.log.debug(`Got ${allBlobs.length} blobs from archive client (total: ${result.length}/${blobHashes.length})`, archiveCtx);
|
|
284
319
|
if (result.length === blobHashes.length) {
|
|
285
320
|
return returnWithCallback(result);
|
|
@@ -320,7 +355,7 @@ export class HttpBlobClient {
|
|
|
320
355
|
});
|
|
321
356
|
const blobs = await client.getBlobsByHashes(blobHashStrings);
|
|
322
357
|
if (blobs.length > 0) {
|
|
323
|
-
const result = fillResults(blobs);
|
|
358
|
+
const result = await fillResults(blobs);
|
|
324
359
|
this.log.debug(`Got ${blobs.length} blobs from filestore (total: ${result.length}/${ctx.blobHashes.length})`, {
|
|
325
360
|
url: client.getBaseUrl(),
|
|
326
361
|
...ctx
|
|
@@ -335,7 +370,7 @@ export class HttpBlobClient {
|
|
|
335
370
|
}
|
|
336
371
|
async getBlobSidecarFrom(hostUrl, blockHashOrSlot, blobHashes = [], l1ConsensusHostIndex) {
|
|
337
372
|
const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
|
|
338
|
-
return processFetchedBlobs(blobs, blobHashes, this.log).filter((b)=>b !== undefined);
|
|
373
|
+
return (await processFetchedBlobs(blobs, blobHashes, this.log)).filter((b)=>b !== undefined);
|
|
339
374
|
}
|
|
340
375
|
async getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex) {
|
|
341
376
|
try {
|
|
@@ -433,9 +468,9 @@ export class HttpBlobClient {
|
|
|
433
468
|
// Ping execution node to get the parentBeaconBlockRoot for this block
|
|
434
469
|
let parentBeaconBlockRoot;
|
|
435
470
|
const client = createPublicClient({
|
|
436
|
-
transport:
|
|
437
|
-
|
|
438
|
-
|
|
471
|
+
transport: makeL1HttpTransport(l1RpcUrls, {
|
|
472
|
+
timeout: this.config.l1HttpTimeoutMS
|
|
473
|
+
})
|
|
439
474
|
});
|
|
440
475
|
try {
|
|
441
476
|
const res = await client.request({
|
|
@@ -530,7 +565,7 @@ function parseBlobJson(data) {
|
|
|
530
565
|
}
|
|
531
566
|
// Returns an array that maps each blob hash to the corresponding blob, or undefined if the blob is not found
|
|
532
567
|
// or the data does not match the commitment.
|
|
533
|
-
function processFetchedBlobs(blobs, blobHashes, logger) {
|
|
568
|
+
async function processFetchedBlobs(blobs, blobHashes, logger) {
|
|
534
569
|
const requestedBlobHashes = new Set(blobHashes.map(bufferToHex));
|
|
535
570
|
const hashToBlob = new Map();
|
|
536
571
|
for (const blobJson of blobs){
|
|
@@ -539,7 +574,7 @@ function processFetchedBlobs(blobs, blobHashes, logger) {
|
|
|
539
574
|
continue;
|
|
540
575
|
}
|
|
541
576
|
try {
|
|
542
|
-
const blob = Blob.fromJson(blobJson);
|
|
577
|
+
const blob = await Blob.fromJson(blobJson);
|
|
543
578
|
hashToBlob.set(hashHex, blob);
|
|
544
579
|
} catch (err) {
|
|
545
580
|
// If the above throws, it's likely that the blob commitment does not match the hash of the blob data.
|
package/dest/client/tests.js
CHANGED
|
@@ -17,7 +17,7 @@ import { makeRandomBlob } from '@aztec/blob-lib/testing';
|
|
|
17
17
|
await cleanup();
|
|
18
18
|
});
|
|
19
19
|
it('should send and retrieve blobs by hash', async ()=>{
|
|
20
|
-
const blob = makeRandomBlob(5);
|
|
20
|
+
const blob = await makeRandomBlob(5);
|
|
21
21
|
const blobHash = blob.getEthVersionedBlobHash();
|
|
22
22
|
await client.sendBlobsToFilestore([
|
|
23
23
|
blob
|
|
@@ -29,9 +29,9 @@ import { makeRandomBlob } from '@aztec/blob-lib/testing';
|
|
|
29
29
|
expect(retrievedBlobs[0]).toEqual(blob);
|
|
30
30
|
});
|
|
31
31
|
it('should handle multiple blobs', async ()=>{
|
|
32
|
-
const blobs = Array.from({
|
|
32
|
+
const blobs = await Promise.all(Array.from({
|
|
33
33
|
length: 3
|
|
34
|
-
}, ()=>makeRandomBlob(7));
|
|
34
|
+
}, ()=>makeRandomBlob(7)));
|
|
35
35
|
const blobHashes = blobs.map((blob)=>blob.getEthVersionedBlobHash());
|
|
36
36
|
await client.sendBlobsToFilestore(blobs);
|
|
37
37
|
const retrievedBlobs = await client.getBlobSidecar(blockId, blobHashes);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/blob-client",
|
|
3
|
-
"version": "0.0.1-commit.
|
|
3
|
+
"version": "0.0.1-commit.8c0b8ff",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": "./dest/client/bin/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -56,12 +56,12 @@
|
|
|
56
56
|
]
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@aztec/blob-lib": "0.0.1-commit.
|
|
60
|
-
"@aztec/ethereum": "0.0.1-commit.
|
|
61
|
-
"@aztec/foundation": "0.0.1-commit.
|
|
62
|
-
"@aztec/kv-store": "0.0.1-commit.
|
|
63
|
-
"@aztec/stdlib": "0.0.1-commit.
|
|
64
|
-
"@aztec/telemetry-client": "0.0.1-commit.
|
|
59
|
+
"@aztec/blob-lib": "0.0.1-commit.8c0b8ff",
|
|
60
|
+
"@aztec/ethereum": "0.0.1-commit.8c0b8ff",
|
|
61
|
+
"@aztec/foundation": "0.0.1-commit.8c0b8ff",
|
|
62
|
+
"@aztec/kv-store": "0.0.1-commit.8c0b8ff",
|
|
63
|
+
"@aztec/stdlib": "0.0.1-commit.8c0b8ff",
|
|
64
|
+
"@aztec/telemetry-client": "0.0.1-commit.8c0b8ff",
|
|
65
65
|
"express": "^4.21.2",
|
|
66
66
|
"snappy": "^7.2.2",
|
|
67
67
|
"source-map-support": "^0.5.21",
|
|
@@ -13,7 +13,7 @@ export function describeBlobStore(getBlobStore: () => Promise<BlobStore>) {
|
|
|
13
13
|
it('should store and retrieve a blob by hash', async () => {
|
|
14
14
|
// Create a test blob with random fields
|
|
15
15
|
const testFields = [Fr.random(), Fr.random(), Fr.random()];
|
|
16
|
-
const blob = Blob.fromFields(testFields);
|
|
16
|
+
const blob = await Blob.fromFields(testFields);
|
|
17
17
|
const blobHash = blob.getEthVersionedBlobHash();
|
|
18
18
|
|
|
19
19
|
// Store the blob
|
|
@@ -29,8 +29,8 @@ export function describeBlobStore(getBlobStore: () => Promise<BlobStore>) {
|
|
|
29
29
|
|
|
30
30
|
it('should handle multiple blobs stored and retrieved by their hashes', async () => {
|
|
31
31
|
// Create two different blobs
|
|
32
|
-
const blob1 = Blob.fromFields([Fr.random(), Fr.random()]);
|
|
33
|
-
const blob2 = Blob.fromFields([Fr.random(), Fr.random(), Fr.random()]);
|
|
32
|
+
const blob1 = await Blob.fromFields([Fr.random(), Fr.random()]);
|
|
33
|
+
const blob2 = await Blob.fromFields([Fr.random(), Fr.random(), Fr.random()]);
|
|
34
34
|
|
|
35
35
|
const blobHash1 = blob1.getEthVersionedBlobHash();
|
|
36
36
|
const blobHash2 = blob2.getEthVersionedBlobHash();
|
|
@@ -57,9 +57,9 @@ export function describeBlobStore(getBlobStore: () => Promise<BlobStore>) {
|
|
|
57
57
|
|
|
58
58
|
it('should handle retrieving subset of stored blobs', async () => {
|
|
59
59
|
// Store multiple blobs
|
|
60
|
-
const blob1 = Blob.fromFields([Fr.random()]);
|
|
61
|
-
const blob2 = Blob.fromFields([Fr.random()]);
|
|
62
|
-
const blob3 = Blob.fromFields([Fr.random()]);
|
|
60
|
+
const blob1 = await Blob.fromFields([Fr.random()]);
|
|
61
|
+
const blob2 = await Blob.fromFields([Fr.random()]);
|
|
62
|
+
const blob3 = await Blob.fromFields([Fr.random()]);
|
|
63
63
|
|
|
64
64
|
await blobStore.addBlobs([blob1, blob2, blob3]);
|
|
65
65
|
|
|
@@ -75,7 +75,7 @@ export function describeBlobStore(getBlobStore: () => Promise<BlobStore>) {
|
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
it('should handle duplicate blob hashes in request', async () => {
|
|
78
|
-
const blob = Blob.fromFields([Fr.random()]);
|
|
78
|
+
const blob = await Blob.fromFields([Fr.random()]);
|
|
79
79
|
const blobHash = blob.getEthVersionedBlobHash();
|
|
80
80
|
|
|
81
81
|
await blobStore.addBlobs([blob]);
|
|
@@ -91,8 +91,8 @@ export function describeBlobStore(getBlobStore: () => Promise<BlobStore>) {
|
|
|
91
91
|
it('should overwrite blob when storing with same hash', async () => {
|
|
92
92
|
// Create two blobs that will have the same hash (same content)
|
|
93
93
|
const fields = [Fr.random(), Fr.random()];
|
|
94
|
-
const blob1 = Blob.fromFields(fields);
|
|
95
|
-
const blob2 = Blob.fromFields(fields);
|
|
94
|
+
const blob1 = await Blob.fromFields(fields);
|
|
95
|
+
const blob2 = await Blob.fromFields(fields);
|
|
96
96
|
|
|
97
97
|
const blobHash = blob1.getEthVersionedBlobHash();
|
|
98
98
|
|
package/src/client/config.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
SecretValue,
|
|
4
4
|
booleanConfigHelper,
|
|
5
5
|
getConfigFromMappings,
|
|
6
|
+
optionalNumberConfigHelper,
|
|
6
7
|
} from '@aztec/foundation/config';
|
|
7
8
|
|
|
8
9
|
import { type BlobArchiveApiConfig, blobArchiveApiConfigMappings } from '../archive/config.js';
|
|
@@ -55,6 +56,9 @@ export interface BlobClientConfig extends BlobArchiveApiConfig {
|
|
|
55
56
|
* Interval in minutes for uploading healthcheck file to file store (default: 60 = 1 hour)
|
|
56
57
|
*/
|
|
57
58
|
blobHealthcheckUploadIntervalMinutes?: number;
|
|
59
|
+
|
|
60
|
+
/** Timeout for HTTP requests to the L1 RPC node in ms. */
|
|
61
|
+
l1HttpTimeoutMS?: number;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
export const blobClientConfigMapping: ConfigMappingsType<BlobClientConfig> = {
|
|
@@ -108,6 +112,11 @@ export const blobClientConfigMapping: ConfigMappingsType<BlobClientConfig> = {
|
|
|
108
112
|
description: 'Interval in minutes for uploading healthcheck file to file store (default: 60 = 1 hour)',
|
|
109
113
|
parseEnv: (val: string | undefined) => (val ? +val : undefined),
|
|
110
114
|
},
|
|
115
|
+
l1HttpTimeoutMS: {
|
|
116
|
+
env: 'ETHEREUM_HTTP_TIMEOUT_MS',
|
|
117
|
+
description: 'Timeout for HTTP requests to the L1 RPC node in ms.',
|
|
118
|
+
...optionalNumberConfigHelper(),
|
|
119
|
+
},
|
|
111
120
|
...blobArchiveApiConfigMappings,
|
|
112
121
|
};
|
|
113
122
|
|
package/src/client/http.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Blob, type BlobJson, computeEthVersionedBlobHash } from '@aztec/blob-lib';
|
|
2
|
+
import { makeL1HttpTransport } from '@aztec/ethereum/client';
|
|
2
3
|
import { shuffle } from '@aztec/foundation/array';
|
|
3
4
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
4
5
|
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
5
6
|
import { bufferToHex, hexToBuffer } from '@aztec/foundation/string';
|
|
6
7
|
|
|
7
|
-
import { type RpcBlock, createPublicClient
|
|
8
|
+
import { type RpcBlock, createPublicClient } from 'viem';
|
|
8
9
|
|
|
9
10
|
import { createBlobArchiveClient } from '../archive/factory.js';
|
|
10
11
|
import type { BlobArchiveClient } from '../archive/interface.js';
|
|
@@ -89,44 +90,68 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
89
90
|
const archiveUrl = this.archiveClient?.getBaseUrl();
|
|
90
91
|
this.log.info(`Testing configured blob sources`, { l1ConsensusHostUrls, archiveUrl });
|
|
91
92
|
|
|
92
|
-
let
|
|
93
|
+
let consensusSuperNodes = 0;
|
|
94
|
+
let consensusNonSuperNodes = 0;
|
|
95
|
+
let archiveSources = 0;
|
|
96
|
+
let blobSinks = 0;
|
|
93
97
|
|
|
94
98
|
if (l1ConsensusHostUrls && l1ConsensusHostUrls.length > 0) {
|
|
95
99
|
for (let l1ConsensusHostIndex = 0; l1ConsensusHostIndex < l1ConsensusHostUrls.length; l1ConsensusHostIndex++) {
|
|
96
100
|
const l1ConsensusHostUrl = l1ConsensusHostUrls[l1ConsensusHostIndex];
|
|
97
101
|
try {
|
|
98
102
|
const { url, ...options } = getBeaconNodeFetchOptions(
|
|
99
|
-
`${l1ConsensusHostUrl}/eth/v1/beacon/headers`,
|
|
103
|
+
`${l1ConsensusHostUrl}/eth/v1/beacon/headers/head`,
|
|
100
104
|
this.config,
|
|
101
105
|
l1ConsensusHostIndex,
|
|
102
106
|
);
|
|
103
107
|
const res = await this.fetch(url, options);
|
|
104
|
-
if (res.ok) {
|
|
105
|
-
this.log.info(`L1 consensus host is reachable`, { l1ConsensusHostUrl });
|
|
106
|
-
successfulSourceCount++;
|
|
107
|
-
} else {
|
|
108
|
+
if (!res.ok) {
|
|
108
109
|
this.log.error(`Failure reaching L1 consensus host: ${res.statusText} (${res.status})`, {
|
|
109
110
|
l1ConsensusHostUrl,
|
|
110
111
|
});
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.log.info(`L1 consensus host is reachable`, { l1ConsensusHostUrl });
|
|
116
|
+
|
|
117
|
+
// Check if the host serves blob sidecars (supernode/semi-supernode).
|
|
118
|
+
// Post-Fusaka (PeerDAS), non-supernode beacon nodes no longer serve the
|
|
119
|
+
// blob sidecar endpoint. A 200 response (even with an empty data array
|
|
120
|
+
// for a slot with no blobs) means the node supports serving blob sidecars.
|
|
121
|
+
const body = await res.json();
|
|
122
|
+
const headSlot = body?.data?.header?.message?.slot;
|
|
123
|
+
if (headSlot) {
|
|
124
|
+
const { url: blobUrl, ...blobOptions } = getBeaconNodeFetchOptions(
|
|
125
|
+
`${l1ConsensusHostUrl}/eth/v1/beacon/blobs/${headSlot}`,
|
|
126
|
+
this.config,
|
|
127
|
+
l1ConsensusHostIndex,
|
|
128
|
+
);
|
|
129
|
+
const blobRes = await this.fetch(blobUrl, blobOptions);
|
|
130
|
+
if (blobRes.ok) {
|
|
131
|
+
this.log.info(`L1 consensus host serves blob sidecars (supernode)`, { l1ConsensusHostUrl });
|
|
132
|
+
consensusSuperNodes++;
|
|
133
|
+
} else {
|
|
134
|
+
this.log.info(`L1 consensus host does not serve blob sidecars`, { l1ConsensusHostUrl });
|
|
135
|
+
consensusNonSuperNodes++;
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
this.log.info(`L1 consensus host is reachable but could not determine head slot`, { l1ConsensusHostUrl });
|
|
139
|
+
consensusNonSuperNodes++;
|
|
111
140
|
}
|
|
112
141
|
} catch (err) {
|
|
113
142
|
this.log.error(`Error reaching L1 consensus host`, err, { l1ConsensusHostUrl });
|
|
114
143
|
}
|
|
115
144
|
}
|
|
116
|
-
} else {
|
|
117
|
-
this.log.warn('No L1 consensus host urls configured');
|
|
118
145
|
}
|
|
119
146
|
|
|
120
147
|
if (this.archiveClient) {
|
|
121
148
|
try {
|
|
122
149
|
const latest = await this.archiveClient.getLatestBlock();
|
|
123
150
|
this.log.info(`Archive client is reachable and synced to L1 block ${latest.number}`, { latest, archiveUrl });
|
|
124
|
-
|
|
151
|
+
archiveSources++;
|
|
125
152
|
} catch (err) {
|
|
126
153
|
this.log.error(`Error reaching archive client`, err, { archiveUrl });
|
|
127
154
|
}
|
|
128
|
-
} else {
|
|
129
|
-
this.log.warn('No archive client configured');
|
|
130
155
|
}
|
|
131
156
|
|
|
132
157
|
if (this.fileStoreClients.length > 0) {
|
|
@@ -135,7 +160,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
135
160
|
const accessible = await fileStoreClient.testConnection();
|
|
136
161
|
if (accessible) {
|
|
137
162
|
this.log.info(`FileStore is reachable`, { url: fileStoreClient.getBaseUrl() });
|
|
138
|
-
|
|
163
|
+
blobSinks++;
|
|
139
164
|
} else {
|
|
140
165
|
this.log.warn(`FileStore is not accessible`, { url: fileStoreClient.getBaseUrl() });
|
|
141
166
|
}
|
|
@@ -145,12 +170,24 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
145
170
|
}
|
|
146
171
|
}
|
|
147
172
|
|
|
173
|
+
// Emit a single summary after validating all sources
|
|
174
|
+
const successfulSourceCount = consensusSuperNodes + archiveSources + blobSinks;
|
|
175
|
+
|
|
176
|
+
let summary = `Blob client running with consensusSuperNodes=${consensusSuperNodes} archiveSources=${archiveSources} blobSinks=${blobSinks}`;
|
|
177
|
+
if (consensusNonSuperNodes > 0) {
|
|
178
|
+
summary += `. ${consensusNonSuperNodes} consensus client(s) ignored because they are not running in supernode or semi-supernode mode`;
|
|
179
|
+
}
|
|
180
|
+
|
|
148
181
|
if (successfulSourceCount === 0) {
|
|
149
182
|
if (this.config.blobAllowEmptySources) {
|
|
150
|
-
this.log.warn(
|
|
183
|
+
this.log.warn(summary);
|
|
151
184
|
} else {
|
|
152
|
-
throw new Error(
|
|
185
|
+
throw new Error(summary);
|
|
153
186
|
}
|
|
187
|
+
} else if (consensusSuperNodes === 0) {
|
|
188
|
+
this.log.warn(summary);
|
|
189
|
+
} else {
|
|
190
|
+
this.log.info(summary);
|
|
154
191
|
}
|
|
155
192
|
}
|
|
156
193
|
|
|
@@ -215,8 +252,8 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
215
252
|
const getFilledBlobs = (): Blob[] => resultBlobs.filter((b): b is Blob => b !== undefined);
|
|
216
253
|
|
|
217
254
|
// Helper to fill in results from fetched blobs
|
|
218
|
-
const fillResults = (fetchedBlobs: BlobJson[]): Blob[] => {
|
|
219
|
-
const blobs = processFetchedBlobs(fetchedBlobs, blobHashes, this.log);
|
|
255
|
+
const fillResults = async (fetchedBlobs: BlobJson[]): Promise<Blob[]> => {
|
|
256
|
+
const blobs = await processFetchedBlobs(fetchedBlobs, blobHashes, this.log);
|
|
220
257
|
// Fill in any missing positions with matching blobs
|
|
221
258
|
for (let i = 0; i < blobHashes.length; i++) {
|
|
222
259
|
if (resultBlobs[i] === undefined) {
|
|
@@ -269,7 +306,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
269
306
|
...ctx,
|
|
270
307
|
});
|
|
271
308
|
const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex);
|
|
272
|
-
const result = fillResults(blobs);
|
|
309
|
+
const result = await fillResults(blobs);
|
|
273
310
|
this.log.debug(
|
|
274
311
|
`Got ${blobs.length} blobs from consensus host (total: ${result.length}/${blobHashes.length})`,
|
|
275
312
|
{ slotNumber, l1ConsensusHostUrl, ...ctx },
|
|
@@ -312,7 +349,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
312
349
|
this.log.debug('No blobs found from archive client', archiveCtx);
|
|
313
350
|
} else {
|
|
314
351
|
this.log.trace(`Got ${allBlobs.length} blobs from archive client before filtering`, archiveCtx);
|
|
315
|
-
const result = fillResults(allBlobs);
|
|
352
|
+
const result = await fillResults(allBlobs);
|
|
316
353
|
this.log.debug(
|
|
317
354
|
`Got ${allBlobs.length} blobs from archive client (total: ${result.length}/${blobHashes.length})`,
|
|
318
355
|
archiveCtx,
|
|
@@ -345,7 +382,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
345
382
|
*/
|
|
346
383
|
private async tryFileStores(
|
|
347
384
|
getMissingBlobHashes: () => Buffer[],
|
|
348
|
-
fillResults: (blobs: BlobJson[]) => Blob[]
|
|
385
|
+
fillResults: (blobs: BlobJson[]) => Promise<Blob[]>,
|
|
349
386
|
ctx: { blockHash: string; blobHashes: string[] },
|
|
350
387
|
): Promise<void> {
|
|
351
388
|
// Shuffle clients for load distribution
|
|
@@ -366,7 +403,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
366
403
|
});
|
|
367
404
|
const blobs = await client.getBlobsByHashes(blobHashStrings);
|
|
368
405
|
if (blobs.length > 0) {
|
|
369
|
-
const result = fillResults(blobs);
|
|
406
|
+
const result = await fillResults(blobs);
|
|
370
407
|
this.log.debug(
|
|
371
408
|
`Got ${blobs.length} blobs from filestore (total: ${result.length}/${ctx.blobHashes.length})`,
|
|
372
409
|
{
|
|
@@ -388,7 +425,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
388
425
|
l1ConsensusHostIndex?: number,
|
|
389
426
|
): Promise<Blob[]> {
|
|
390
427
|
const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
|
|
391
|
-
return processFetchedBlobs(blobs, blobHashes, this.log).filter((b): b is Blob => b !== undefined);
|
|
428
|
+
return (await processFetchedBlobs(blobs, blobHashes, this.log)).filter((b): b is Blob => b !== undefined);
|
|
392
429
|
}
|
|
393
430
|
|
|
394
431
|
public async getBlobsFromHost(
|
|
@@ -497,7 +534,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
497
534
|
// Ping execution node to get the parentBeaconBlockRoot for this block
|
|
498
535
|
let parentBeaconBlockRoot: string | undefined;
|
|
499
536
|
const client = createPublicClient({
|
|
500
|
-
transport:
|
|
537
|
+
transport: makeL1HttpTransport(l1RpcUrls, { timeout: this.config.l1HttpTimeoutMS }),
|
|
501
538
|
});
|
|
502
539
|
try {
|
|
503
540
|
const res: RpcBlock = await client.request({
|
|
@@ -616,7 +653,11 @@ function parseBlobJson(data: any): BlobJson {
|
|
|
616
653
|
|
|
617
654
|
// Returns an array that maps each blob hash to the corresponding blob, or undefined if the blob is not found
|
|
618
655
|
// or the data does not match the commitment.
|
|
619
|
-
function processFetchedBlobs(
|
|
656
|
+
async function processFetchedBlobs(
|
|
657
|
+
blobs: BlobJson[],
|
|
658
|
+
blobHashes: Buffer[],
|
|
659
|
+
logger: Logger,
|
|
660
|
+
): Promise<(Blob | undefined)[]> {
|
|
620
661
|
const requestedBlobHashes = new Set<string>(blobHashes.map(bufferToHex));
|
|
621
662
|
const hashToBlob = new Map<string, Blob>();
|
|
622
663
|
for (const blobJson of blobs) {
|
|
@@ -626,7 +667,7 @@ function processFetchedBlobs(blobs: BlobJson[], blobHashes: Buffer[], logger: Lo
|
|
|
626
667
|
}
|
|
627
668
|
|
|
628
669
|
try {
|
|
629
|
-
const blob = Blob.fromJson(blobJson);
|
|
670
|
+
const blob = await Blob.fromJson(blobJson);
|
|
630
671
|
hashToBlob.set(hashHex, blob);
|
|
631
672
|
} catch (err) {
|
|
632
673
|
// If the above throws, it's likely that the blob commitment does not match the hash of the blob data.
|
package/src/client/tests.ts
CHANGED
|
@@ -28,7 +28,7 @@ export function runBlobClientTests(
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
it('should send and retrieve blobs by hash', async () => {
|
|
31
|
-
const blob = makeRandomBlob(5);
|
|
31
|
+
const blob = await makeRandomBlob(5);
|
|
32
32
|
const blobHash = blob.getEthVersionedBlobHash();
|
|
33
33
|
|
|
34
34
|
await client.sendBlobsToFilestore([blob]);
|
|
@@ -39,7 +39,7 @@ export function runBlobClientTests(
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
it('should handle multiple blobs', async () => {
|
|
42
|
-
const blobs = Array.from({ length: 3 }, () => makeRandomBlob(7));
|
|
42
|
+
const blobs = await Promise.all(Array.from({ length: 3 }, () => makeRandomBlob(7)));
|
|
43
43
|
const blobHashes = blobs.map(blob => blob.getEthVersionedBlobHash());
|
|
44
44
|
|
|
45
45
|
await client.sendBlobsToFilestore(blobs);
|