@aztec/blob-client 0.0.1-commit.b468ad8 → 0.0.1-commit.b6e433891
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 +9 -3
- package/dest/client/http.d.ts.map +1 -1
- package/dest/client/http.js +102 -49
- package/dest/client/interface.d.ts +12 -1
- package/dest/client/interface.d.ts.map +1 -1
- 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 +132 -43
- package/src/client/interface.ts +11 -0
- 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
|
@@ -14,6 +14,10 @@ export declare class HttpBlobClient implements BlobClientInterface {
|
|
|
14
14
|
protected readonly fileStoreUploadClient: FileStoreBlobClient | undefined;
|
|
15
15
|
private disabled;
|
|
16
16
|
private healthcheckUploadIntervalId?;
|
|
17
|
+
/** Cached beacon genesis time (seconds since Unix epoch). Fetched once at startup. */
|
|
18
|
+
private beaconGenesisTime?;
|
|
19
|
+
/** Cached beacon slot duration in seconds. Fetched once at startup. */
|
|
20
|
+
private beaconSecondsPerSlot?;
|
|
17
21
|
constructor(config?: BlobClientConfig, opts?: {
|
|
18
22
|
logger?: Logger;
|
|
19
23
|
archiveClient?: BlobArchiveClient;
|
|
@@ -54,7 +58,7 @@ export declare class HttpBlobClient implements BlobClientInterface {
|
|
|
54
58
|
getBlobSidecar(blockHash: `0x${string}`, blobHashes: Buffer[], opts?: GetBlobSidecarOptions): Promise<Blob[]>;
|
|
55
59
|
private tryFileStores;
|
|
56
60
|
getBlobSidecarFrom(hostUrl: string, blockHashOrSlot: string | number, blobHashes?: Buffer[], l1ConsensusHostIndex?: number): Promise<Blob[]>;
|
|
57
|
-
getBlobsFromHost(hostUrl: string, blockHashOrSlot: string | number, l1ConsensusHostIndex?: number): Promise<BlobJson[]>;
|
|
61
|
+
getBlobsFromHost(hostUrl: string, blockHashOrSlot: string | number, l1ConsensusHostIndex?: number, blobHashes?: Buffer[]): Promise<BlobJson[]>;
|
|
58
62
|
private fetchBlobSidecars;
|
|
59
63
|
private getLatestSlotNumber;
|
|
60
64
|
private getSlotNumber;
|
|
@@ -64,16 +68,18 @@ export declare class HttpBlobClient implements BlobClientInterface {
|
|
|
64
68
|
canUpload(): boolean;
|
|
65
69
|
/**
|
|
66
70
|
* Start the blob client.
|
|
67
|
-
*
|
|
71
|
+
* Fetches and caches beacon genesis config for timestamp-based slot resolution,
|
|
72
|
+
* then uploads the initial healthcheck file (awaited) and starts periodic uploads.
|
|
68
73
|
*/
|
|
69
74
|
start(): Promise<void>;
|
|
70
75
|
/**
|
|
71
76
|
* Start periodic healthcheck upload to the file store to ensure it remains available even if accidentally deleted.
|
|
72
77
|
*/
|
|
73
78
|
private startPeriodicHealthcheckUpload;
|
|
79
|
+
private fetchBeaconConfig;
|
|
74
80
|
/**
|
|
75
81
|
* Stop the blob client, clearing any periodic tasks.
|
|
76
82
|
*/
|
|
77
83
|
stop(): void;
|
|
78
84
|
}
|
|
79
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
85
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaHR0cC5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2NsaWVudC9odHRwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxJQUFJLEVBQUUsS0FBSyxRQUFRLEVBQStCLE1BQU0saUJBQWlCLENBQUM7QUFHbkYsT0FBTyxFQUFFLEtBQUssTUFBTSxFQUFnQixNQUFNLHVCQUF1QixDQUFDO0FBT2xFLE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDakUsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSx1Q0FBdUMsQ0FBQztBQUVqRixPQUFPLEVBQUUsS0FBSyxnQkFBZ0IsRUFBOEIsTUFBTSxhQUFhLENBQUM7QUFDaEYsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVqRixxQkFBYSxjQUFlLFlBQVcsbUJBQW1CO0lBa0J0RCxPQUFPLENBQUMsUUFBUSxDQUFDLElBQUk7SUFqQnZCLFNBQVMsQ0FBQyxRQUFRLENBQUMsR0FBRyxFQUFFLE1BQU0sQ0FBQztJQUMvQixTQUFTLENBQUMsUUFBUSxDQUFDLE1BQU0sRUFBRSxnQkFBZ0IsQ0FBQztJQUM1QyxTQUFTLENBQUMsUUFBUSxDQUFDLGFBQWEsRUFBRSxpQkFBaUIsR0FBRyxTQUFTLENBQUM7SUFDaEUsU0FBUyxDQUFDLFFBQVEsQ0FBQyxLQUFLLEVBQUUsT0FBTyxLQUFLLENBQUM7SUFDdkMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxnQkFBZ0IsRUFBRSxtQkFBbUIsRUFBRSxDQUFDO0lBQzNELFNBQVMsQ0FBQyxRQUFRLENBQUMscUJBQXFCLEVBQUUsbUJBQW1CLEdBQUcsU0FBUyxDQUFDO0lBRTFFLE9BQU8sQ0FBQyxRQUFRLENBQVM7SUFDekIsT0FBTyxDQUFDLDJCQUEyQixDQUFDLENBQWlCO0lBRXJELHNGQUFzRjtJQUN0RixPQUFPLENBQUMsaUJBQWlCLENBQUMsQ0FBUztJQUNuQyx1RUFBdUU7SUFDdkUsT0FBTyxDQUFDLG9CQUFvQixDQUFDLENBQVM7SUFFdEMsWUFDRSxNQUFNLENBQUMsRUFBRSxnQkFBZ0IsRUFDUixJQUFJLEdBQUU7UUFDckIsTUFBTSxDQUFDLEVBQUUsTUFBTSxDQUFDO1FBQ2hCLGFBQWEsQ0FBQyxFQUFFLGlCQUFpQixDQUFDO1FBQ2xDLGdCQUFnQixDQUFDLEVBQUUsbUJBQW1CLEVBQUUsQ0FBQztRQUN6QyxxQkFBcUIsQ0FBQyxFQUFFLG1CQUFtQixDQUFDO1FBQzVDLHlFQUF5RTtRQUN6RSxjQUFjLENBQUMsRUFBRSxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsS0FBSyxJQUFJLENBQUM7S0FDckMsRUF3QlA7SUFFRDs7O09BR0c7SUFDSCxPQUFPLENBQUMsc0JBQXNCO0lBVTlCOzs7OztPQUtHO0lBQ0ksV0FBVyxDQUFDLEtBQUssRUFBRSxPQUFPLEdBQUcsSUFBSSxDQUd2QztJQUVZLFdBQVcsa0JBb0V2QjtJQUVZLG9CQUFvQixDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLENBbUJqRTtJQUVEOzs7Ozs7Ozs7Ozs7OztPQWNHO0lBQ1UsY0FBYyxDQUN6QixTQUFTLEVBQUUsS0FBSyxNQUFNLEVBQUUsRUFDeEIsVUFBVSxFQUFFLE1BQU0sRUFBRSxFQUNwQixJQUFJLENBQUMsRUFBRSxxQkFBcUIsR0FDM0IsT0FBTyxDQUFDLElBQUksRUFBRSxDQUFDLENBa0pqQjtZQVFhLGFBQWE7SUFzQ2Qsa0JBQWtCLENBQzdCLE9BQU8sRUFBRSxNQUFNLEVBQ2YsZUFBZSxFQUFFLE1BQU0sR0FBRyxNQUFNLEVBQ2hDLFVBQVUsR0FBRSxNQUFNLEVBQU8sRUFDekIsb0JBQW9CLENBQUMsRUFBRSxNQUFNLEdBQzVCLE9BQU8sQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUdqQjtJQUVZLGdCQUFnQixDQUMzQixPQUFPLEVBQUUsTUFBTSxFQUNmLGVBQWUsRUFBRSxNQUFNLEdBQUcsTUFBTSxFQUNoQyxvQkFBb0IsQ0FBQyxFQUFFLE1BQU0sRUFDN0IsVUFBVSxDQUFDLEVBQUUsTUFBTSxFQUFFLEdBQ3BCLE9BQU8sQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQXNDckI7SUFFRCxPQUFPLENBQUMsaUJBQWlCO1lBcUJYLG1CQUFtQjtZQW1DbkIsYUFBYTtJQTZFM0Isc0NBQXNDO0lBQy9CLGdCQUFnQixJQUFJLGlCQUFpQixHQUFHLFNBQVMsQ0FFdkQ7SUFFRCxpRUFBaUU7SUFDMUQsU0FBUyxJQUFJLE9BQU8sQ0FFMUI7SUFFRDs7OztPQUlHO0lBQ1UsS0FBSyxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FXbEM7SUFFRDs7T0FFRztJQUNILE9BQU8sQ0FBQyw4QkFBOEI7WUFnQnhCLGlCQUFpQjtJQTBDL0I7O09BRUc7SUFDSSxJQUFJLElBQUksSUFBSSxDQUtsQjtDQUNGIn0=
|
|
@@ -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;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"}
|
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';
|
|
@@ -17,6 +18,8 @@ export class HttpBlobClient {
|
|
|
17
18
|
fileStoreUploadClient;
|
|
18
19
|
disabled;
|
|
19
20
|
healthcheckUploadIntervalId;
|
|
21
|
+
/** Cached beacon genesis time (seconds since Unix epoch). Fetched once at startup. */ beaconGenesisTime;
|
|
22
|
+
/** Cached beacon slot duration in seconds. Fetched once at startup. */ beaconSecondsPerSlot;
|
|
20
23
|
constructor(config, opts = {}){
|
|
21
24
|
this.opts = opts;
|
|
22
25
|
this.disabled = false;
|
|
@@ -183,8 +186,8 @@ export class HttpBlobClient {
|
|
|
183
186
|
// Return the result, ignoring any undefined ones
|
|
184
187
|
const getFilledBlobs = ()=>resultBlobs.filter((b)=>b !== undefined);
|
|
185
188
|
// Helper to fill in results from fetched blobs
|
|
186
|
-
const fillResults = (fetchedBlobs)=>{
|
|
187
|
-
const blobs = processFetchedBlobs(fetchedBlobs, blobHashes, this.log);
|
|
189
|
+
const fillResults = async (fetchedBlobs)=>{
|
|
190
|
+
const blobs = await processFetchedBlobs(fetchedBlobs, blobHashes, this.log);
|
|
188
191
|
// Fill in any missing positions with matching blobs
|
|
189
192
|
for(let i = 0; i < blobHashes.length; i++){
|
|
190
193
|
if (resultBlobs[i] === undefined) {
|
|
@@ -220,7 +223,7 @@ export class HttpBlobClient {
|
|
|
220
223
|
...ctx
|
|
221
224
|
};
|
|
222
225
|
this.log.trace(`Attempting to get slot number for block hash`, consensusCtx);
|
|
223
|
-
const slotNumber = await this.getSlotNumber(blockHash);
|
|
226
|
+
const slotNumber = await this.getSlotNumber(blockHash, opts?.parentBeaconBlockRoot, opts?.l1BlockTimestamp);
|
|
224
227
|
this.log.debug(`Got slot number ${slotNumber} from consensus host for querying blobs`, consensusCtx);
|
|
225
228
|
if (slotNumber) {
|
|
226
229
|
let l1ConsensusHostUrl;
|
|
@@ -235,8 +238,8 @@ export class HttpBlobClient {
|
|
|
235
238
|
l1ConsensusHostUrl,
|
|
236
239
|
...ctx
|
|
237
240
|
});
|
|
238
|
-
const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex);
|
|
239
|
-
const result = fillResults(blobs);
|
|
241
|
+
const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex, getMissingBlobHashes());
|
|
242
|
+
const result = await fillResults(blobs);
|
|
240
243
|
this.log.debug(`Got ${blobs.length} blobs from consensus host (total: ${result.length}/${blobHashes.length})`, {
|
|
241
244
|
slotNumber,
|
|
242
245
|
l1ConsensusHostUrl,
|
|
@@ -279,7 +282,7 @@ export class HttpBlobClient {
|
|
|
279
282
|
this.log.debug('No blobs found from archive client', archiveCtx);
|
|
280
283
|
} else {
|
|
281
284
|
this.log.trace(`Got ${allBlobs.length} blobs from archive client before filtering`, archiveCtx);
|
|
282
|
-
const result = fillResults(allBlobs);
|
|
285
|
+
const result = await fillResults(allBlobs);
|
|
283
286
|
this.log.debug(`Got ${allBlobs.length} blobs from archive client (total: ${result.length}/${blobHashes.length})`, archiveCtx);
|
|
284
287
|
if (result.length === blobHashes.length) {
|
|
285
288
|
return returnWithCallback(result);
|
|
@@ -320,7 +323,7 @@ export class HttpBlobClient {
|
|
|
320
323
|
});
|
|
321
324
|
const blobs = await client.getBlobsByHashes(blobHashStrings);
|
|
322
325
|
if (blobs.length > 0) {
|
|
323
|
-
const result = fillResults(blobs);
|
|
326
|
+
const result = await fillResults(blobs);
|
|
324
327
|
this.log.debug(`Got ${blobs.length} blobs from filestore (total: ${result.length}/${ctx.blobHashes.length})`, {
|
|
325
328
|
url: client.getBaseUrl(),
|
|
326
329
|
...ctx
|
|
@@ -334,14 +337,14 @@ export class HttpBlobClient {
|
|
|
334
337
|
}
|
|
335
338
|
}
|
|
336
339
|
async getBlobSidecarFrom(hostUrl, blockHashOrSlot, blobHashes = [], l1ConsensusHostIndex) {
|
|
337
|
-
const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
|
|
338
|
-
return processFetchedBlobs(blobs, blobHashes, this.log).filter((b)=>b !== undefined);
|
|
340
|
+
const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
|
|
341
|
+
return (await processFetchedBlobs(blobs, blobHashes, this.log)).filter((b)=>b !== undefined);
|
|
339
342
|
}
|
|
340
|
-
async getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex) {
|
|
343
|
+
async getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes) {
|
|
341
344
|
try {
|
|
342
|
-
let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
|
|
345
|
+
let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
|
|
343
346
|
if (res.ok) {
|
|
344
|
-
return parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
347
|
+
return await parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
345
348
|
}
|
|
346
349
|
if (res.status === 404 && typeof blockHashOrSlot === 'number') {
|
|
347
350
|
const latestSlot = await this.getLatestSlotNumber(hostUrl, l1ConsensusHostIndex);
|
|
@@ -354,9 +357,9 @@ export class HttpBlobClient {
|
|
|
354
357
|
let currentSlot = blockHashOrSlot + 1;
|
|
355
358
|
while(res.status === 404 && maxRetries > 0 && latestSlot !== undefined && currentSlot <= latestSlot){
|
|
356
359
|
this.log.debug(`Trying slot ${currentSlot}`);
|
|
357
|
-
res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex);
|
|
360
|
+
res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex, blobHashes);
|
|
358
361
|
if (res.ok) {
|
|
359
|
-
return parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
362
|
+
return await parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
360
363
|
}
|
|
361
364
|
currentSlot++;
|
|
362
365
|
maxRetries--;
|
|
@@ -373,8 +376,15 @@ export class HttpBlobClient {
|
|
|
373
376
|
return [];
|
|
374
377
|
}
|
|
375
378
|
}
|
|
376
|
-
fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex) {
|
|
377
|
-
|
|
379
|
+
fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes) {
|
|
380
|
+
let baseUrl = `${hostUrl}/eth/v1/beacon/blobs/${blockHashOrSlot}`;
|
|
381
|
+
if (blobHashes && blobHashes.length > 0) {
|
|
382
|
+
const params = new URLSearchParams();
|
|
383
|
+
for (const hash of blobHashes){
|
|
384
|
+
params.append('versioned_hashes', `0x${hash.toString('hex')}`);
|
|
385
|
+
}
|
|
386
|
+
baseUrl += `?${params.toString()}`;
|
|
387
|
+
}
|
|
378
388
|
const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
|
|
379
389
|
this.log.debug(`Fetching blob sidecar for ${blockHashOrSlot}`, {
|
|
380
390
|
url,
|
|
@@ -420,36 +430,45 @@ export class HttpBlobClient {
|
|
|
420
430
|
*
|
|
421
431
|
* @param blockHash - The block hash
|
|
422
432
|
* @returns The slot number
|
|
423
|
-
*/ async getSlotNumber(blockHash) {
|
|
433
|
+
*/ async getSlotNumber(blockHash, parentBeaconBlockRoot, l1BlockTimestamp) {
|
|
424
434
|
const { l1ConsensusHostUrls, l1RpcUrls } = this.config;
|
|
425
435
|
if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
|
|
426
436
|
this.log.debug('No consensus host url configured');
|
|
427
437
|
return undefined;
|
|
428
438
|
}
|
|
429
|
-
if (
|
|
430
|
-
|
|
431
|
-
|
|
439
|
+
// Primary path: compute slot from timestamp if genesis config is cached (no network call needed)
|
|
440
|
+
if (l1BlockTimestamp !== undefined && this.beaconGenesisTime !== undefined && this.beaconSecondsPerSlot !== undefined) {
|
|
441
|
+
const slot = Number((l1BlockTimestamp - this.beaconGenesisTime) / BigInt(this.beaconSecondsPerSlot));
|
|
442
|
+
this.log.debug(`Computed slot ${slot} from L1 block timestamp`, {
|
|
443
|
+
l1BlockTimestamp
|
|
444
|
+
});
|
|
445
|
+
return slot;
|
|
432
446
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
params: [
|
|
444
|
-
blockHash,
|
|
445
|
-
/*tx flag*/ false
|
|
446
|
-
]
|
|
447
|
+
if (!parentBeaconBlockRoot) {
|
|
448
|
+
// parentBeaconBlockRoot not provided by caller — fetch it from the execution RPC
|
|
449
|
+
if (!l1RpcUrls || l1RpcUrls.length === 0) {
|
|
450
|
+
this.log.debug('No execution host url configured');
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
const client = createPublicClient({
|
|
454
|
+
transport: makeL1HttpTransport(l1RpcUrls, {
|
|
455
|
+
timeout: this.config.l1HttpTimeoutMS
|
|
456
|
+
})
|
|
447
457
|
});
|
|
448
|
-
|
|
449
|
-
|
|
458
|
+
try {
|
|
459
|
+
const res = await client.request({
|
|
460
|
+
method: 'eth_getBlockByHash',
|
|
461
|
+
params: [
|
|
462
|
+
blockHash,
|
|
463
|
+
/*tx flag*/ false
|
|
464
|
+
]
|
|
465
|
+
});
|
|
466
|
+
if (res.parentBeaconBlockRoot) {
|
|
467
|
+
parentBeaconBlockRoot = res.parentBeaconBlockRoot;
|
|
468
|
+
}
|
|
469
|
+
} catch (err) {
|
|
470
|
+
this.log.error(`Error getting parent beacon block root`, err);
|
|
450
471
|
}
|
|
451
|
-
} catch (err) {
|
|
452
|
-
this.log.error(`Error getting parent beacon block root`, err);
|
|
453
472
|
}
|
|
454
473
|
if (!parentBeaconBlockRoot) {
|
|
455
474
|
this.log.error(`No parent beacon block root found for block ${blockHash}`);
|
|
@@ -481,8 +500,10 @@ export class HttpBlobClient {
|
|
|
481
500
|
}
|
|
482
501
|
/**
|
|
483
502
|
* Start the blob client.
|
|
484
|
-
*
|
|
503
|
+
* Fetches and caches beacon genesis config for timestamp-based slot resolution,
|
|
504
|
+
* then uploads the initial healthcheck file (awaited) and starts periodic uploads.
|
|
485
505
|
*/ async start() {
|
|
506
|
+
await this.fetchBeaconConfig();
|
|
486
507
|
if (!this.fileStoreUploadClient) {
|
|
487
508
|
return;
|
|
488
509
|
}
|
|
@@ -501,6 +522,40 @@ export class HttpBlobClient {
|
|
|
501
522
|
}, intervalMs);
|
|
502
523
|
}
|
|
503
524
|
/**
|
|
525
|
+
* Fetches and caches beacon genesis time and slot duration from the first available consensus host.
|
|
526
|
+
* These static values enable timestamp-based slot resolution, eliminating the per-fetch headers call.
|
|
527
|
+
* Logs a warning and leaves fields undefined if all hosts fail, callers fall back gracefully.
|
|
528
|
+
*/ async fetchBeaconConfig() {
|
|
529
|
+
const { l1ConsensusHostUrls } = this.config;
|
|
530
|
+
if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
for(let i = 0; i < l1ConsensusHostUrls.length; i++){
|
|
534
|
+
try {
|
|
535
|
+
const { url: genesisUrl, ...genesisOptions } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrls[i]}/eth/v1/config/genesis`, this.config, i);
|
|
536
|
+
const { url: specUrl, ...specOptions } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrls[i]}/eth/v1/config/spec`, this.config, i);
|
|
537
|
+
const [genesisRes, specRes] = await Promise.all([
|
|
538
|
+
this.fetch(genesisUrl, genesisOptions),
|
|
539
|
+
this.fetch(specUrl, specOptions)
|
|
540
|
+
]);
|
|
541
|
+
if (genesisRes.ok && specRes.ok) {
|
|
542
|
+
const genesis = await genesisRes.json();
|
|
543
|
+
const spec = await specRes.json();
|
|
544
|
+
this.beaconGenesisTime = BigInt(genesis.data.genesisTime);
|
|
545
|
+
this.beaconSecondsPerSlot = parseInt(spec.data.secondsPerSlot);
|
|
546
|
+
this.log.debug(`Fetched beacon genesis config`, {
|
|
547
|
+
genesisTime: this.beaconGenesisTime,
|
|
548
|
+
secondsPerSlot: this.beaconSecondsPerSlot
|
|
549
|
+
});
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
} catch (err) {
|
|
553
|
+
this.log.warn(`Failed to fetch beacon config from host ${l1ConsensusHostUrls[i]}`, err);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
this.log.warn('Could not fetch beacon genesis config from any consensus host — will use headers call fallback');
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
504
559
|
* Stop the blob client, clearing any periodic tasks.
|
|
505
560
|
*/ stop() {
|
|
506
561
|
if (this.healthcheckUploadIntervalId) {
|
|
@@ -509,10 +564,9 @@ export class HttpBlobClient {
|
|
|
509
564
|
}
|
|
510
565
|
}
|
|
511
566
|
}
|
|
512
|
-
function parseBlobJsonsFromResponse(response, logger) {
|
|
567
|
+
async function parseBlobJsonsFromResponse(response, logger) {
|
|
513
568
|
try {
|
|
514
|
-
|
|
515
|
-
return blobs;
|
|
569
|
+
return await Promise.all(response.data.map(parseBlobJson));
|
|
516
570
|
} catch (err) {
|
|
517
571
|
logger.error(`Error parsing blob json from response`, err);
|
|
518
572
|
return [];
|
|
@@ -522,15 +576,14 @@ function parseBlobJsonsFromResponse(response, logger) {
|
|
|
522
576
|
// https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getBlobSidecars
|
|
523
577
|
// Here we attempt to parse the response data to Buffer, and check the lengths (via Blob's constructor), to avoid
|
|
524
578
|
// throwing an error down the line when calling Blob.fromJson().
|
|
525
|
-
function parseBlobJson(
|
|
526
|
-
const blobBuffer = Buffer.from(
|
|
527
|
-
const
|
|
528
|
-
const blob = new Blob(blobBuffer, commitmentBuffer);
|
|
579
|
+
async function parseBlobJson(rawHex) {
|
|
580
|
+
const blobBuffer = Buffer.from(rawHex.slice(2), 'hex');
|
|
581
|
+
const blob = await Blob.fromBlobBuffer(blobBuffer);
|
|
529
582
|
return blob.toJSON();
|
|
530
583
|
}
|
|
531
584
|
// Returns an array that maps each blob hash to the corresponding blob, or undefined if the blob is not found
|
|
532
585
|
// or the data does not match the commitment.
|
|
533
|
-
function processFetchedBlobs(blobs, blobHashes, logger) {
|
|
586
|
+
async function processFetchedBlobs(blobs, blobHashes, logger) {
|
|
534
587
|
const requestedBlobHashes = new Set(blobHashes.map(bufferToHex));
|
|
535
588
|
const hashToBlob = new Map();
|
|
536
589
|
for (const blobJson of blobs){
|
|
@@ -539,7 +592,7 @@ function processFetchedBlobs(blobs, blobHashes, logger) {
|
|
|
539
592
|
continue;
|
|
540
593
|
}
|
|
541
594
|
try {
|
|
542
|
-
const blob = Blob.fromJson(blobJson);
|
|
595
|
+
const blob = await Blob.fromJson(blobJson);
|
|
543
596
|
hashToBlob.set(hashHex, blob);
|
|
544
597
|
} catch (err) {
|
|
545
598
|
// If the above throws, it's likely that the blob commitment does not match the hash of the blob data.
|
|
@@ -10,6 +10,17 @@ export interface GetBlobSidecarOptions {
|
|
|
10
10
|
* - Near tip: FileStore first with no retries (data should exist), L1 consensus second (freshest data), then FileStore with retries, then archive (eg. blobscan)
|
|
11
11
|
*/
|
|
12
12
|
isHistoricalSync?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* The parent beacon block root for the L1 block containing the blobs.
|
|
15
|
+
* If provided, skips the eth_getBlockByHash execution RPC call inside getSlotNumber.
|
|
16
|
+
*/
|
|
17
|
+
parentBeaconBlockRoot?: string;
|
|
18
|
+
/**
|
|
19
|
+
* The timestamp of the L1 execution block containing the blobs.
|
|
20
|
+
* When provided alongside a cached beacon genesis config (fetched at startup), allows computing
|
|
21
|
+
* the beacon slot directly via timestamp math, skipping the beacon headers network call entirely.
|
|
22
|
+
*/
|
|
23
|
+
l1BlockTimestamp?: bigint;
|
|
13
24
|
}
|
|
14
25
|
export interface BlobClientInterface {
|
|
15
26
|
/** Sends the given blobs to the filestore, to be indexed by blob hash. */
|
|
@@ -25,4 +36,4 @@ export interface BlobClientInterface {
|
|
|
25
36
|
/** Returns true if this client can upload blobs to filestore. */
|
|
26
37
|
canUpload(): boolean;
|
|
27
38
|
}
|
|
28
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
39
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvY2xpZW50L2ludGVyZmFjZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssRUFBRSxJQUFJLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUU1Qzs7R0FFRztBQUNILE1BQU0sV0FBVyxxQkFBcUI7SUFDcEM7Ozs7O09BS0c7SUFDSCxnQkFBZ0IsQ0FBQyxFQUFFLE9BQU8sQ0FBQztJQUMzQjs7O09BR0c7SUFDSCxxQkFBcUIsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUMvQjs7OztPQUlHO0lBQ0gsZ0JBQWdCLENBQUMsRUFBRSxNQUFNLENBQUM7Q0FDM0I7QUFFRCxNQUFNLFdBQVcsbUJBQW1CO0lBQ2xDLDBFQUEwRTtJQUMxRSxvQkFBb0IsQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQ3RELHFFQUFxRTtJQUNyRSxjQUFjLENBQUMsT0FBTyxFQUFFLE1BQU0sRUFBRSxVQUFVLENBQUMsRUFBRSxNQUFNLEVBQUUsRUFBRSxJQUFJLENBQUMsRUFBRSxxQkFBcUIsR0FBRyxPQUFPLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUN0Ryw2RUFBNkU7SUFDN0UsS0FBSyxDQUFDLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQ3hCLG9GQUFvRjtJQUNwRixXQUFXLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQzdCLDBEQUEwRDtJQUMxRCxJQUFJLENBQUMsSUFBSSxJQUFJLENBQUM7SUFDZCxpRUFBaUU7SUFDakUsU0FBUyxJQUFJLE9BQU8sQ0FBQztDQUN0QiJ9
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../src/client/interface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAE5C;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../src/client/interface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAE5C;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,mBAAmB;IAClC,0EAA0E;IAC1E,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACtD,qEAAqE;IACrE,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IACtG,6EAA6E;IAC7E,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,oFAAoF;IACpF,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,0DAA0D;IAC1D,IAAI,CAAC,IAAI,IAAI,CAAC;IACd,iEAAiE;IACjE,SAAS,IAAI,OAAO,CAAC;CACtB"}
|
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.b6e433891",
|
|
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.b6e433891",
|
|
60
|
+
"@aztec/ethereum": "0.0.1-commit.b6e433891",
|
|
61
|
+
"@aztec/foundation": "0.0.1-commit.b6e433891",
|
|
62
|
+
"@aztec/kv-store": "0.0.1-commit.b6e433891",
|
|
63
|
+
"@aztec/stdlib": "0.0.1-commit.b6e433891",
|
|
64
|
+
"@aztec/telemetry-client": "0.0.1-commit.b6e433891",
|
|
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';
|
|
@@ -24,6 +25,11 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
24
25
|
private disabled = false;
|
|
25
26
|
private healthcheckUploadIntervalId?: NodeJS.Timeout;
|
|
26
27
|
|
|
28
|
+
/** Cached beacon genesis time (seconds since Unix epoch). Fetched once at startup. */
|
|
29
|
+
private beaconGenesisTime?: bigint;
|
|
30
|
+
/** Cached beacon slot duration in seconds. Fetched once at startup. */
|
|
31
|
+
private beaconSecondsPerSlot?: number;
|
|
32
|
+
|
|
27
33
|
constructor(
|
|
28
34
|
config?: BlobClientConfig,
|
|
29
35
|
private readonly opts: {
|
|
@@ -215,8 +221,8 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
215
221
|
const getFilledBlobs = (): Blob[] => resultBlobs.filter((b): b is Blob => b !== undefined);
|
|
216
222
|
|
|
217
223
|
// Helper to fill in results from fetched blobs
|
|
218
|
-
const fillResults = (fetchedBlobs: BlobJson[]): Blob[] => {
|
|
219
|
-
const blobs = processFetchedBlobs(fetchedBlobs, blobHashes, this.log);
|
|
224
|
+
const fillResults = async (fetchedBlobs: BlobJson[]): Promise<Blob[]> => {
|
|
225
|
+
const blobs = await processFetchedBlobs(fetchedBlobs, blobHashes, this.log);
|
|
220
226
|
// Fill in any missing positions with matching blobs
|
|
221
227
|
for (let i = 0; i < blobHashes.length; i++) {
|
|
222
228
|
if (resultBlobs[i] === undefined) {
|
|
@@ -251,7 +257,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
251
257
|
// The beacon api can query by slot number, so we get that first
|
|
252
258
|
const consensusCtx = { l1ConsensusHostUrls, ...ctx };
|
|
253
259
|
this.log.trace(`Attempting to get slot number for block hash`, consensusCtx);
|
|
254
|
-
const slotNumber = await this.getSlotNumber(blockHash);
|
|
260
|
+
const slotNumber = await this.getSlotNumber(blockHash, opts?.parentBeaconBlockRoot, opts?.l1BlockTimestamp);
|
|
255
261
|
this.log.debug(`Got slot number ${slotNumber} from consensus host for querying blobs`, consensusCtx);
|
|
256
262
|
|
|
257
263
|
if (slotNumber) {
|
|
@@ -268,8 +274,13 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
268
274
|
l1ConsensusHostUrl,
|
|
269
275
|
...ctx,
|
|
270
276
|
});
|
|
271
|
-
const blobs = await this.getBlobsFromHost(
|
|
272
|
-
|
|
277
|
+
const blobs = await this.getBlobsFromHost(
|
|
278
|
+
l1ConsensusHostUrl,
|
|
279
|
+
slotNumber,
|
|
280
|
+
l1ConsensusHostIndex,
|
|
281
|
+
getMissingBlobHashes(),
|
|
282
|
+
);
|
|
283
|
+
const result = await fillResults(blobs);
|
|
273
284
|
this.log.debug(
|
|
274
285
|
`Got ${blobs.length} blobs from consensus host (total: ${result.length}/${blobHashes.length})`,
|
|
275
286
|
{ slotNumber, l1ConsensusHostUrl, ...ctx },
|
|
@@ -312,7 +323,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
312
323
|
this.log.debug('No blobs found from archive client', archiveCtx);
|
|
313
324
|
} else {
|
|
314
325
|
this.log.trace(`Got ${allBlobs.length} blobs from archive client before filtering`, archiveCtx);
|
|
315
|
-
const result = fillResults(allBlobs);
|
|
326
|
+
const result = await fillResults(allBlobs);
|
|
316
327
|
this.log.debug(
|
|
317
328
|
`Got ${allBlobs.length} blobs from archive client (total: ${result.length}/${blobHashes.length})`,
|
|
318
329
|
archiveCtx,
|
|
@@ -345,7 +356,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
345
356
|
*/
|
|
346
357
|
private async tryFileStores(
|
|
347
358
|
getMissingBlobHashes: () => Buffer[],
|
|
348
|
-
fillResults: (blobs: BlobJson[]) => Blob[]
|
|
359
|
+
fillResults: (blobs: BlobJson[]) => Promise<Blob[]>,
|
|
349
360
|
ctx: { blockHash: string; blobHashes: string[] },
|
|
350
361
|
): Promise<void> {
|
|
351
362
|
// Shuffle clients for load distribution
|
|
@@ -366,7 +377,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
366
377
|
});
|
|
367
378
|
const blobs = await client.getBlobsByHashes(blobHashStrings);
|
|
368
379
|
if (blobs.length > 0) {
|
|
369
|
-
const result = fillResults(blobs);
|
|
380
|
+
const result = await fillResults(blobs);
|
|
370
381
|
this.log.debug(
|
|
371
382
|
`Got ${blobs.length} blobs from filestore (total: ${result.length}/${ctx.blobHashes.length})`,
|
|
372
383
|
{
|
|
@@ -387,19 +398,20 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
387
398
|
blobHashes: Buffer[] = [],
|
|
388
399
|
l1ConsensusHostIndex?: number,
|
|
389
400
|
): Promise<Blob[]> {
|
|
390
|
-
const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
|
|
391
|
-
return processFetchedBlobs(blobs, blobHashes, this.log).filter((b): b is Blob => b !== undefined);
|
|
401
|
+
const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
|
|
402
|
+
return (await processFetchedBlobs(blobs, blobHashes, this.log)).filter((b): b is Blob => b !== undefined);
|
|
392
403
|
}
|
|
393
404
|
|
|
394
405
|
public async getBlobsFromHost(
|
|
395
406
|
hostUrl: string,
|
|
396
407
|
blockHashOrSlot: string | number,
|
|
397
408
|
l1ConsensusHostIndex?: number,
|
|
409
|
+
blobHashes?: Buffer[],
|
|
398
410
|
): Promise<BlobJson[]> {
|
|
399
411
|
try {
|
|
400
|
-
let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
|
|
412
|
+
let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
|
|
401
413
|
if (res.ok) {
|
|
402
|
-
return parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
414
|
+
return await parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
403
415
|
}
|
|
404
416
|
|
|
405
417
|
if (res.status === 404 && typeof blockHashOrSlot === 'number') {
|
|
@@ -414,9 +426,9 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
414
426
|
let currentSlot = blockHashOrSlot + 1;
|
|
415
427
|
while (res.status === 404 && maxRetries > 0 && latestSlot !== undefined && currentSlot <= latestSlot) {
|
|
416
428
|
this.log.debug(`Trying slot ${currentSlot}`);
|
|
417
|
-
res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex);
|
|
429
|
+
res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex, blobHashes);
|
|
418
430
|
if (res.ok) {
|
|
419
|
-
return parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
431
|
+
return await parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
420
432
|
}
|
|
421
433
|
currentSlot++;
|
|
422
434
|
maxRetries--;
|
|
@@ -439,8 +451,17 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
439
451
|
hostUrl: string,
|
|
440
452
|
blockHashOrSlot: string | number,
|
|
441
453
|
l1ConsensusHostIndex?: number,
|
|
454
|
+
blobHashes?: Buffer[],
|
|
442
455
|
): Promise<Response> {
|
|
443
|
-
|
|
456
|
+
let baseUrl = `${hostUrl}/eth/v1/beacon/blobs/${blockHashOrSlot}`;
|
|
457
|
+
|
|
458
|
+
if (blobHashes && blobHashes.length > 0) {
|
|
459
|
+
const params = new URLSearchParams();
|
|
460
|
+
for (const hash of blobHashes) {
|
|
461
|
+
params.append('versioned_hashes', `0x${hash.toString('hex')}`);
|
|
462
|
+
}
|
|
463
|
+
baseUrl += `?${params.toString()}`;
|
|
464
|
+
}
|
|
444
465
|
|
|
445
466
|
const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
|
|
446
467
|
this.log.debug(`Fetching blob sidecar for ${blockHashOrSlot}`, { url, ...options });
|
|
@@ -482,34 +503,50 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
482
503
|
* @param blockHash - The block hash
|
|
483
504
|
* @returns The slot number
|
|
484
505
|
*/
|
|
485
|
-
private async getSlotNumber(
|
|
506
|
+
private async getSlotNumber(
|
|
507
|
+
blockHash: `0x${string}`,
|
|
508
|
+
parentBeaconBlockRoot?: string,
|
|
509
|
+
l1BlockTimestamp?: bigint,
|
|
510
|
+
): Promise<number | undefined> {
|
|
486
511
|
const { l1ConsensusHostUrls, l1RpcUrls } = this.config;
|
|
487
512
|
if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
|
|
488
513
|
this.log.debug('No consensus host url configured');
|
|
489
514
|
return undefined;
|
|
490
515
|
}
|
|
491
516
|
|
|
492
|
-
if (
|
|
493
|
-
|
|
494
|
-
|
|
517
|
+
// Primary path: compute slot from timestamp if genesis config is cached (no network call needed)
|
|
518
|
+
if (
|
|
519
|
+
l1BlockTimestamp !== undefined &&
|
|
520
|
+
this.beaconGenesisTime !== undefined &&
|
|
521
|
+
this.beaconSecondsPerSlot !== undefined
|
|
522
|
+
) {
|
|
523
|
+
const slot = Number((l1BlockTimestamp - this.beaconGenesisTime) / BigInt(this.beaconSecondsPerSlot));
|
|
524
|
+
this.log.debug(`Computed slot ${slot} from L1 block timestamp`, { l1BlockTimestamp });
|
|
525
|
+
return slot;
|
|
495
526
|
}
|
|
496
527
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
528
|
+
if (!parentBeaconBlockRoot) {
|
|
529
|
+
// parentBeaconBlockRoot not provided by caller — fetch it from the execution RPC
|
|
530
|
+
if (!l1RpcUrls || l1RpcUrls.length === 0) {
|
|
531
|
+
this.log.debug('No execution host url configured');
|
|
532
|
+
return undefined;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const client = createPublicClient({
|
|
536
|
+
transport: makeL1HttpTransport(l1RpcUrls, { timeout: this.config.l1HttpTimeoutMS }),
|
|
506
537
|
});
|
|
538
|
+
try {
|
|
539
|
+
const res: RpcBlock = await client.request({
|
|
540
|
+
method: 'eth_getBlockByHash',
|
|
541
|
+
params: [blockHash, /*tx flag*/ false],
|
|
542
|
+
});
|
|
507
543
|
|
|
508
|
-
|
|
509
|
-
|
|
544
|
+
if (res.parentBeaconBlockRoot) {
|
|
545
|
+
parentBeaconBlockRoot = res.parentBeaconBlockRoot;
|
|
546
|
+
}
|
|
547
|
+
} catch (err) {
|
|
548
|
+
this.log.error(`Error getting parent beacon block root`, err);
|
|
510
549
|
}
|
|
511
|
-
} catch (err) {
|
|
512
|
-
this.log.error(`Error getting parent beacon block root`, err);
|
|
513
550
|
}
|
|
514
551
|
|
|
515
552
|
if (!parentBeaconBlockRoot) {
|
|
@@ -555,9 +592,12 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
555
592
|
|
|
556
593
|
/**
|
|
557
594
|
* Start the blob client.
|
|
558
|
-
*
|
|
595
|
+
* Fetches and caches beacon genesis config for timestamp-based slot resolution,
|
|
596
|
+
* then uploads the initial healthcheck file (awaited) and starts periodic uploads.
|
|
559
597
|
*/
|
|
560
598
|
public async start(): Promise<void> {
|
|
599
|
+
await this.fetchBeaconConfig();
|
|
600
|
+
|
|
561
601
|
if (!this.fileStoreUploadClient) {
|
|
562
602
|
return;
|
|
563
603
|
}
|
|
@@ -582,6 +622,53 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
582
622
|
}, intervalMs);
|
|
583
623
|
}
|
|
584
624
|
|
|
625
|
+
/**
|
|
626
|
+
* Fetches and caches beacon genesis time and slot duration from the first available consensus host.
|
|
627
|
+
* These static values enable timestamp-based slot resolution, eliminating the per-fetch headers call.
|
|
628
|
+
* Logs a warning and leaves fields undefined if all hosts fail, callers fall back gracefully.
|
|
629
|
+
*/
|
|
630
|
+
private async fetchBeaconConfig(): Promise<void> {
|
|
631
|
+
const { l1ConsensusHostUrls } = this.config;
|
|
632
|
+
if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
for (let i = 0; i < l1ConsensusHostUrls.length; i++) {
|
|
637
|
+
try {
|
|
638
|
+
const { url: genesisUrl, ...genesisOptions } = getBeaconNodeFetchOptions(
|
|
639
|
+
`${l1ConsensusHostUrls[i]}/eth/v1/config/genesis`,
|
|
640
|
+
this.config,
|
|
641
|
+
i,
|
|
642
|
+
);
|
|
643
|
+
const { url: specUrl, ...specOptions } = getBeaconNodeFetchOptions(
|
|
644
|
+
`${l1ConsensusHostUrls[i]}/eth/v1/config/spec`,
|
|
645
|
+
this.config,
|
|
646
|
+
i,
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
const [genesisRes, specRes] = await Promise.all([
|
|
650
|
+
this.fetch(genesisUrl, genesisOptions),
|
|
651
|
+
this.fetch(specUrl, specOptions),
|
|
652
|
+
]);
|
|
653
|
+
|
|
654
|
+
if (genesisRes.ok && specRes.ok) {
|
|
655
|
+
const genesis = await genesisRes.json();
|
|
656
|
+
const spec = await specRes.json();
|
|
657
|
+
this.beaconGenesisTime = BigInt(genesis.data.genesisTime);
|
|
658
|
+
this.beaconSecondsPerSlot = parseInt(spec.data.secondsPerSlot);
|
|
659
|
+
this.log.debug(`Fetched beacon genesis config`, {
|
|
660
|
+
genesisTime: this.beaconGenesisTime,
|
|
661
|
+
secondsPerSlot: this.beaconSecondsPerSlot,
|
|
662
|
+
});
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
} catch (err) {
|
|
666
|
+
this.log.warn(`Failed to fetch beacon config from host ${l1ConsensusHostUrls[i]}`, err);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
this.log.warn('Could not fetch beacon genesis config from any consensus host — will use headers call fallback');
|
|
670
|
+
}
|
|
671
|
+
|
|
585
672
|
/**
|
|
586
673
|
* Stop the blob client, clearing any periodic tasks.
|
|
587
674
|
*/
|
|
@@ -593,10 +680,9 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
593
680
|
}
|
|
594
681
|
}
|
|
595
682
|
|
|
596
|
-
function parseBlobJsonsFromResponse(response: any, logger: Logger): BlobJson[] {
|
|
683
|
+
async function parseBlobJsonsFromResponse(response: any, logger: Logger): Promise<BlobJson[]> {
|
|
597
684
|
try {
|
|
598
|
-
|
|
599
|
-
return blobs;
|
|
685
|
+
return await Promise.all((response.data as string[]).map(parseBlobJson));
|
|
600
686
|
} catch (err) {
|
|
601
687
|
logger.error(`Error parsing blob json from response`, err);
|
|
602
688
|
return [];
|
|
@@ -607,16 +693,19 @@ function parseBlobJsonsFromResponse(response: any, logger: Logger): BlobJson[] {
|
|
|
607
693
|
// https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getBlobSidecars
|
|
608
694
|
// Here we attempt to parse the response data to Buffer, and check the lengths (via Blob's constructor), to avoid
|
|
609
695
|
// throwing an error down the line when calling Blob.fromJson().
|
|
610
|
-
function parseBlobJson(
|
|
611
|
-
const blobBuffer = Buffer.from(
|
|
612
|
-
const
|
|
613
|
-
const blob = new Blob(blobBuffer, commitmentBuffer);
|
|
696
|
+
async function parseBlobJson(rawHex: string): Promise<BlobJson> {
|
|
697
|
+
const blobBuffer = Buffer.from(rawHex.slice(2), 'hex');
|
|
698
|
+
const blob = await Blob.fromBlobBuffer(blobBuffer);
|
|
614
699
|
return blob.toJSON();
|
|
615
700
|
}
|
|
616
701
|
|
|
617
702
|
// Returns an array that maps each blob hash to the corresponding blob, or undefined if the blob is not found
|
|
618
703
|
// or the data does not match the commitment.
|
|
619
|
-
function processFetchedBlobs(
|
|
704
|
+
async function processFetchedBlobs(
|
|
705
|
+
blobs: BlobJson[],
|
|
706
|
+
blobHashes: Buffer[],
|
|
707
|
+
logger: Logger,
|
|
708
|
+
): Promise<(Blob | undefined)[]> {
|
|
620
709
|
const requestedBlobHashes = new Set<string>(blobHashes.map(bufferToHex));
|
|
621
710
|
const hashToBlob = new Map<string, Blob>();
|
|
622
711
|
for (const blobJson of blobs) {
|
|
@@ -626,7 +715,7 @@ function processFetchedBlobs(blobs: BlobJson[], blobHashes: Buffer[], logger: Lo
|
|
|
626
715
|
}
|
|
627
716
|
|
|
628
717
|
try {
|
|
629
|
-
const blob = Blob.fromJson(blobJson);
|
|
718
|
+
const blob = await Blob.fromJson(blobJson);
|
|
630
719
|
hashToBlob.set(hashHex, blob);
|
|
631
720
|
} catch (err) {
|
|
632
721
|
// If the above throws, it's likely that the blob commitment does not match the hash of the blob data.
|
package/src/client/interface.ts
CHANGED
|
@@ -11,6 +11,17 @@ export interface GetBlobSidecarOptions {
|
|
|
11
11
|
* - Near tip: FileStore first with no retries (data should exist), L1 consensus second (freshest data), then FileStore with retries, then archive (eg. blobscan)
|
|
12
12
|
*/
|
|
13
13
|
isHistoricalSync?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The parent beacon block root for the L1 block containing the blobs.
|
|
16
|
+
* If provided, skips the eth_getBlockByHash execution RPC call inside getSlotNumber.
|
|
17
|
+
*/
|
|
18
|
+
parentBeaconBlockRoot?: string;
|
|
19
|
+
/**
|
|
20
|
+
* The timestamp of the L1 execution block containing the blobs.
|
|
21
|
+
* When provided alongside a cached beacon genesis config (fetched at startup), allows computing
|
|
22
|
+
* the beacon slot directly via timestamp math, skipping the beacon headers network call entirely.
|
|
23
|
+
*/
|
|
24
|
+
l1BlockTimestamp?: bigint;
|
|
14
25
|
}
|
|
15
26
|
|
|
16
27
|
export interface BlobClientInterface {
|
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);
|