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