@aztec/blob-client 0.0.1-commit.3469e52 → 0.0.1-commit.35158ae7e
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/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 +150 -63
- 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/config.ts +9 -0
- package/src/client/http.ts +182 -57
- 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/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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaHR0cC5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2NsaWVudC9odHRwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxJQUFJLEVBQUUsS0FBSyxRQUFRLEVBQStCLE1BQU0saUJBQWlCLENBQUM7QUFHbkYsT0FBTyxFQUFFLEtBQUssTUFBTSxFQUFnQixNQUFNLHVCQUF1QixDQUFDO0FBT2xFLE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDakUsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSx1Q0FBdUMsQ0FBQztBQUVqRixPQUFPLEVBQUUsS0FBSyxnQkFBZ0IsRUFBOEIsTUFBTSxhQUFhLENBQUM7QUFDaEYsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVqRixxQkFBYSxjQUFlLFlBQVcsbUJBQW1CO0lBa0J0RCxPQUFPLENBQUMsUUFBUSxDQUFDLElBQUk7SUFqQnZCLFNBQVMsQ0FBQyxRQUFRLENBQUMsR0FBRyxFQUFFLE1BQU0sQ0FBQztJQUMvQixTQUFTLENBQUMsUUFBUSxDQUFDLE1BQU0sRUFBRSxnQkFBZ0IsQ0FBQztJQUM1QyxTQUFTLENBQUMsUUFBUSxDQUFDLGFBQWEsRUFBRSxpQkFBaUIsR0FBRyxTQUFTLENBQUM7SUFDaEUsU0FBUyxDQUFDLFFBQVEsQ0FBQyxLQUFLLEVBQUUsT0FBTyxLQUFLLENBQUM7SUFDdkMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxnQkFBZ0IsRUFBRSxtQkFBbUIsRUFBRSxDQUFDO0lBQzNELFNBQVMsQ0FBQyxRQUFRLENBQUMscUJBQXFCLEVBQUUsbUJBQW1CLEdBQUcsU0FBUyxDQUFDO0lBRTFFLE9BQU8sQ0FBQyxRQUFRLENBQVM7SUFDekIsT0FBTyxDQUFDLDJCQUEyQixDQUFDLENBQWlCO0lBRXJELHNGQUFzRjtJQUN0RixPQUFPLENBQUMsaUJBQWlCLENBQUMsQ0FBUztJQUNuQyx1RUFBdUU7SUFDdkUsT0FBTyxDQUFDLG9CQUFvQixDQUFDLENBQVM7SUFFdEMsWUFDRSxNQUFNLENBQUMsRUFBRSxnQkFBZ0IsRUFDUixJQUFJLEdBQUU7UUFDckIsTUFBTSxDQUFDLEVBQUUsTUFBTSxDQUFDO1FBQ2hCLGFBQWEsQ0FBQyxFQUFFLGlCQUFpQixDQUFDO1FBQ2xDLGdCQUFnQixDQUFDLEVBQUUsbUJBQW1CLEVBQUUsQ0FBQztRQUN6QyxxQkFBcUIsQ0FBQyxFQUFFLG1CQUFtQixDQUFDO1FBQzVDLHlFQUF5RTtRQUN6RSxjQUFjLENBQUMsRUFBRSxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsS0FBSyxJQUFJLENBQUM7S0FDckMsRUF3QlA7SUFFRDs7O09BR0c7SUFDSCxPQUFPLENBQUMsc0JBQXNCO0lBVTlCOzs7OztPQUtHO0lBQ0ksV0FBVyxDQUFDLEtBQUssRUFBRSxPQUFPLEdBQUcsSUFBSSxDQUd2QztJQUVZLFdBQVcsa0JBd0d2QjtJQUVZLG9CQUFvQixDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLENBbUJqRTtJQUVEOzs7Ozs7Ozs7Ozs7OztPQWNHO0lBQ1UsY0FBYyxDQUN6QixTQUFTLEVBQUUsS0FBSyxNQUFNLEVBQUUsRUFDeEIsVUFBVSxFQUFFLE1BQU0sRUFBRSxFQUNwQixJQUFJLENBQUMsRUFBRSxxQkFBcUIsR0FDM0IsT0FBTyxDQUFDLElBQUksRUFBRSxDQUFDLENBa0pqQjtZQVFhLGFBQWE7SUFzQ2Qsa0JBQWtCLENBQzdCLE9BQU8sRUFBRSxNQUFNLEVBQ2YsZUFBZSxFQUFFLE1BQU0sR0FBRyxNQUFNLEVBQ2hDLFVBQVUsR0FBRSxNQUFNLEVBQU8sRUFDekIsb0JBQW9CLENBQUMsRUFBRSxNQUFNLEdBQzVCLE9BQU8sQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUdqQjtJQUVZLGdCQUFnQixDQUMzQixPQUFPLEVBQUUsTUFBTSxFQUNmLGVBQWUsRUFBRSxNQUFNLEdBQUcsTUFBTSxFQUNoQyxvQkFBb0IsQ0FBQyxFQUFFLE1BQU0sRUFDN0IsVUFBVSxDQUFDLEVBQUUsTUFBTSxFQUFFLEdBQ3BCLE9BQU8sQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQXNDckI7SUFFRCxPQUFPLENBQUMsaUJBQWlCO1lBcUJYLG1CQUFtQjtZQW1DbkIsYUFBYTtJQTZFM0Isc0NBQXNDO0lBQy9CLGdCQUFnQixJQUFJLGlCQUFpQixHQUFHLFNBQVMsQ0FFdkQ7SUFFRCxpRUFBaUU7SUFDMUQsU0FBUyxJQUFJLE9BQU8sQ0FFMUI7SUFFRDs7OztPQUlHO0lBQ1UsS0FBSyxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FXbEM7SUFFRDs7T0FFRztJQUNILE9BQU8sQ0FBQyw4QkFBOEI7WUFnQnhCLGlCQUFpQjtJQTBDL0I7O09BRUc7SUFDSSxJQUFJLElBQUksSUFBSSxDQUtsQjtDQUNGIn0=
|
|
@@ -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":"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"}
|
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;
|
|
@@ -66,22 +69,50 @@ export class HttpBlobClient {
|
|
|
66
69
|
l1ConsensusHostUrls,
|
|
67
70
|
archiveUrl
|
|
68
71
|
});
|
|
69
|
-
let
|
|
72
|
+
let consensusSuperNodes = 0;
|
|
73
|
+
let consensusNonSuperNodes = 0;
|
|
74
|
+
let archiveSources = 0;
|
|
75
|
+
let blobSinks = 0;
|
|
70
76
|
if (l1ConsensusHostUrls && l1ConsensusHostUrls.length > 0) {
|
|
71
77
|
for(let l1ConsensusHostIndex = 0; l1ConsensusHostIndex < l1ConsensusHostUrls.length; l1ConsensusHostIndex++){
|
|
72
78
|
const l1ConsensusHostUrl = l1ConsensusHostUrls[l1ConsensusHostIndex];
|
|
73
79
|
try {
|
|
74
|
-
const { url, ...options } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrl}/eth/v1/beacon/headers`, this.config, l1ConsensusHostIndex);
|
|
80
|
+
const { url, ...options } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrl}/eth/v1/beacon/headers/head`, this.config, l1ConsensusHostIndex);
|
|
75
81
|
const res = await this.fetch(url, options);
|
|
76
|
-
if (res.ok) {
|
|
77
|
-
this.log.
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
this.log.error(`Failure reaching L1 consensus host: ${res.statusText} (${res.status})`, {
|
|
78
84
|
l1ConsensusHostUrl
|
|
79
85
|
});
|
|
80
|
-
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
this.log.info(`L1 consensus host is reachable`, {
|
|
89
|
+
l1ConsensusHostUrl
|
|
90
|
+
});
|
|
91
|
+
// Check if the host serves blob sidecars (supernode/semi-supernode).
|
|
92
|
+
// Post-Fusaka (PeerDAS), non-supernode beacon nodes no longer serve the
|
|
93
|
+
// blob sidecar endpoint. A 200 response (even with an empty data array
|
|
94
|
+
// for a slot with no blobs) means the node supports serving blob sidecars.
|
|
95
|
+
const body = await res.json();
|
|
96
|
+
const headSlot = body?.data?.header?.message?.slot;
|
|
97
|
+
if (headSlot) {
|
|
98
|
+
const { url: blobUrl, ...blobOptions } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrl}/eth/v1/beacon/blobs/${headSlot}`, this.config, l1ConsensusHostIndex);
|
|
99
|
+
const blobRes = await this.fetch(blobUrl, blobOptions);
|
|
100
|
+
if (blobRes.ok) {
|
|
101
|
+
this.log.info(`L1 consensus host serves blob sidecars (supernode)`, {
|
|
102
|
+
l1ConsensusHostUrl
|
|
103
|
+
});
|
|
104
|
+
consensusSuperNodes++;
|
|
105
|
+
} else {
|
|
106
|
+
this.log.info(`L1 consensus host does not serve blob sidecars`, {
|
|
107
|
+
l1ConsensusHostUrl
|
|
108
|
+
});
|
|
109
|
+
consensusNonSuperNodes++;
|
|
110
|
+
}
|
|
81
111
|
} else {
|
|
82
|
-
this.log.
|
|
112
|
+
this.log.info(`L1 consensus host is reachable but could not determine head slot`, {
|
|
83
113
|
l1ConsensusHostUrl
|
|
84
114
|
});
|
|
115
|
+
consensusNonSuperNodes++;
|
|
85
116
|
}
|
|
86
117
|
} catch (err) {
|
|
87
118
|
this.log.error(`Error reaching L1 consensus host`, err, {
|
|
@@ -89,8 +120,6 @@ export class HttpBlobClient {
|
|
|
89
120
|
});
|
|
90
121
|
}
|
|
91
122
|
}
|
|
92
|
-
} else {
|
|
93
|
-
this.log.warn('No L1 consensus host urls configured');
|
|
94
123
|
}
|
|
95
124
|
if (this.archiveClient) {
|
|
96
125
|
try {
|
|
@@ -99,14 +128,12 @@ export class HttpBlobClient {
|
|
|
99
128
|
latest,
|
|
100
129
|
archiveUrl
|
|
101
130
|
});
|
|
102
|
-
|
|
131
|
+
archiveSources++;
|
|
103
132
|
} catch (err) {
|
|
104
133
|
this.log.error(`Error reaching archive client`, err, {
|
|
105
134
|
archiveUrl
|
|
106
135
|
});
|
|
107
136
|
}
|
|
108
|
-
} else {
|
|
109
|
-
this.log.warn('No archive client configured');
|
|
110
137
|
}
|
|
111
138
|
if (this.fileStoreClients.length > 0) {
|
|
112
139
|
for (const fileStoreClient of this.fileStoreClients){
|
|
@@ -116,7 +143,7 @@ export class HttpBlobClient {
|
|
|
116
143
|
this.log.info(`FileStore is reachable`, {
|
|
117
144
|
url: fileStoreClient.getBaseUrl()
|
|
118
145
|
});
|
|
119
|
-
|
|
146
|
+
blobSinks++;
|
|
120
147
|
} else {
|
|
121
148
|
this.log.warn(`FileStore is not accessible`, {
|
|
122
149
|
url: fileStoreClient.getBaseUrl()
|
|
@@ -129,12 +156,22 @@ export class HttpBlobClient {
|
|
|
129
156
|
}
|
|
130
157
|
}
|
|
131
158
|
}
|
|
159
|
+
// Emit a single summary after validating all sources
|
|
160
|
+
const successfulSourceCount = consensusSuperNodes + archiveSources + blobSinks;
|
|
161
|
+
let summary = `Blob client running with consensusSuperNodes=${consensusSuperNodes} archiveSources=${archiveSources} blobSinks=${blobSinks}`;
|
|
162
|
+
if (consensusNonSuperNodes > 0) {
|
|
163
|
+
summary += `. ${consensusNonSuperNodes} consensus client(s) ignored because they are not running in supernode or semi-supernode mode`;
|
|
164
|
+
}
|
|
132
165
|
if (successfulSourceCount === 0) {
|
|
133
166
|
if (this.config.blobAllowEmptySources) {
|
|
134
|
-
this.log.warn(
|
|
167
|
+
this.log.warn(summary);
|
|
135
168
|
} else {
|
|
136
|
-
throw new Error(
|
|
169
|
+
throw new Error(summary);
|
|
137
170
|
}
|
|
171
|
+
} else if (consensusSuperNodes === 0) {
|
|
172
|
+
this.log.warn(summary);
|
|
173
|
+
} else {
|
|
174
|
+
this.log.info(summary);
|
|
138
175
|
}
|
|
139
176
|
}
|
|
140
177
|
async sendBlobsToFilestore(blobs) {
|
|
@@ -183,8 +220,8 @@ export class HttpBlobClient {
|
|
|
183
220
|
// Return the result, ignoring any undefined ones
|
|
184
221
|
const getFilledBlobs = ()=>resultBlobs.filter((b)=>b !== undefined);
|
|
185
222
|
// Helper to fill in results from fetched blobs
|
|
186
|
-
const fillResults = (fetchedBlobs)=>{
|
|
187
|
-
const blobs = processFetchedBlobs(fetchedBlobs, blobHashes, this.log);
|
|
223
|
+
const fillResults = async (fetchedBlobs)=>{
|
|
224
|
+
const blobs = await processFetchedBlobs(fetchedBlobs, blobHashes, this.log);
|
|
188
225
|
// Fill in any missing positions with matching blobs
|
|
189
226
|
for(let i = 0; i < blobHashes.length; i++){
|
|
190
227
|
if (resultBlobs[i] === undefined) {
|
|
@@ -220,7 +257,7 @@ export class HttpBlobClient {
|
|
|
220
257
|
...ctx
|
|
221
258
|
};
|
|
222
259
|
this.log.trace(`Attempting to get slot number for block hash`, consensusCtx);
|
|
223
|
-
const slotNumber = await this.getSlotNumber(blockHash);
|
|
260
|
+
const slotNumber = await this.getSlotNumber(blockHash, opts?.parentBeaconBlockRoot, opts?.l1BlockTimestamp);
|
|
224
261
|
this.log.debug(`Got slot number ${slotNumber} from consensus host for querying blobs`, consensusCtx);
|
|
225
262
|
if (slotNumber) {
|
|
226
263
|
let l1ConsensusHostUrl;
|
|
@@ -235,8 +272,8 @@ export class HttpBlobClient {
|
|
|
235
272
|
l1ConsensusHostUrl,
|
|
236
273
|
...ctx
|
|
237
274
|
});
|
|
238
|
-
const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex);
|
|
239
|
-
const result = fillResults(blobs);
|
|
275
|
+
const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex, getMissingBlobHashes());
|
|
276
|
+
const result = await fillResults(blobs);
|
|
240
277
|
this.log.debug(`Got ${blobs.length} blobs from consensus host (total: ${result.length}/${blobHashes.length})`, {
|
|
241
278
|
slotNumber,
|
|
242
279
|
l1ConsensusHostUrl,
|
|
@@ -279,7 +316,7 @@ export class HttpBlobClient {
|
|
|
279
316
|
this.log.debug('No blobs found from archive client', archiveCtx);
|
|
280
317
|
} else {
|
|
281
318
|
this.log.trace(`Got ${allBlobs.length} blobs from archive client before filtering`, archiveCtx);
|
|
282
|
-
const result = fillResults(allBlobs);
|
|
319
|
+
const result = await fillResults(allBlobs);
|
|
283
320
|
this.log.debug(`Got ${allBlobs.length} blobs from archive client (total: ${result.length}/${blobHashes.length})`, archiveCtx);
|
|
284
321
|
if (result.length === blobHashes.length) {
|
|
285
322
|
return returnWithCallback(result);
|
|
@@ -320,7 +357,7 @@ export class HttpBlobClient {
|
|
|
320
357
|
});
|
|
321
358
|
const blobs = await client.getBlobsByHashes(blobHashStrings);
|
|
322
359
|
if (blobs.length > 0) {
|
|
323
|
-
const result = fillResults(blobs);
|
|
360
|
+
const result = await fillResults(blobs);
|
|
324
361
|
this.log.debug(`Got ${blobs.length} blobs from filestore (total: ${result.length}/${ctx.blobHashes.length})`, {
|
|
325
362
|
url: client.getBaseUrl(),
|
|
326
363
|
...ctx
|
|
@@ -334,14 +371,14 @@ export class HttpBlobClient {
|
|
|
334
371
|
}
|
|
335
372
|
}
|
|
336
373
|
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);
|
|
374
|
+
const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
|
|
375
|
+
return (await processFetchedBlobs(blobs, blobHashes, this.log)).filter((b)=>b !== undefined);
|
|
339
376
|
}
|
|
340
|
-
async getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex) {
|
|
377
|
+
async getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes) {
|
|
341
378
|
try {
|
|
342
|
-
let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
|
|
379
|
+
let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
|
|
343
380
|
if (res.ok) {
|
|
344
|
-
return parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
381
|
+
return await parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
345
382
|
}
|
|
346
383
|
if (res.status === 404 && typeof blockHashOrSlot === 'number') {
|
|
347
384
|
const latestSlot = await this.getLatestSlotNumber(hostUrl, l1ConsensusHostIndex);
|
|
@@ -354,9 +391,9 @@ export class HttpBlobClient {
|
|
|
354
391
|
let currentSlot = blockHashOrSlot + 1;
|
|
355
392
|
while(res.status === 404 && maxRetries > 0 && latestSlot !== undefined && currentSlot <= latestSlot){
|
|
356
393
|
this.log.debug(`Trying slot ${currentSlot}`);
|
|
357
|
-
res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex);
|
|
394
|
+
res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex, blobHashes);
|
|
358
395
|
if (res.ok) {
|
|
359
|
-
return parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
396
|
+
return await parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
360
397
|
}
|
|
361
398
|
currentSlot++;
|
|
362
399
|
maxRetries--;
|
|
@@ -373,8 +410,15 @@ export class HttpBlobClient {
|
|
|
373
410
|
return [];
|
|
374
411
|
}
|
|
375
412
|
}
|
|
376
|
-
fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex) {
|
|
377
|
-
|
|
413
|
+
fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes) {
|
|
414
|
+
let baseUrl = `${hostUrl}/eth/v1/beacon/blobs/${blockHashOrSlot}`;
|
|
415
|
+
if (blobHashes && blobHashes.length > 0) {
|
|
416
|
+
const params = new URLSearchParams();
|
|
417
|
+
for (const hash of blobHashes){
|
|
418
|
+
params.append('versioned_hashes', `0x${hash.toString('hex')}`);
|
|
419
|
+
}
|
|
420
|
+
baseUrl += `?${params.toString()}`;
|
|
421
|
+
}
|
|
378
422
|
const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
|
|
379
423
|
this.log.debug(`Fetching blob sidecar for ${blockHashOrSlot}`, {
|
|
380
424
|
url,
|
|
@@ -420,36 +464,45 @@ export class HttpBlobClient {
|
|
|
420
464
|
*
|
|
421
465
|
* @param blockHash - The block hash
|
|
422
466
|
* @returns The slot number
|
|
423
|
-
*/ async getSlotNumber(blockHash) {
|
|
467
|
+
*/ async getSlotNumber(blockHash, parentBeaconBlockRoot, l1BlockTimestamp) {
|
|
424
468
|
const { l1ConsensusHostUrls, l1RpcUrls } = this.config;
|
|
425
469
|
if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
|
|
426
470
|
this.log.debug('No consensus host url configured');
|
|
427
471
|
return undefined;
|
|
428
472
|
}
|
|
429
|
-
if (
|
|
430
|
-
|
|
431
|
-
|
|
473
|
+
// Primary path: compute slot from timestamp if genesis config is cached (no network call needed)
|
|
474
|
+
if (l1BlockTimestamp !== undefined && this.beaconGenesisTime !== undefined && this.beaconSecondsPerSlot !== undefined) {
|
|
475
|
+
const slot = Number((l1BlockTimestamp - this.beaconGenesisTime) / BigInt(this.beaconSecondsPerSlot));
|
|
476
|
+
this.log.debug(`Computed slot ${slot} from L1 block timestamp`, {
|
|
477
|
+
l1BlockTimestamp
|
|
478
|
+
});
|
|
479
|
+
return slot;
|
|
432
480
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
params: [
|
|
444
|
-
blockHash,
|
|
445
|
-
/*tx flag*/ false
|
|
446
|
-
]
|
|
481
|
+
if (!parentBeaconBlockRoot) {
|
|
482
|
+
// parentBeaconBlockRoot not provided by caller — fetch it from the execution RPC
|
|
483
|
+
if (!l1RpcUrls || l1RpcUrls.length === 0) {
|
|
484
|
+
this.log.debug('No execution host url configured');
|
|
485
|
+
return undefined;
|
|
486
|
+
}
|
|
487
|
+
const client = createPublicClient({
|
|
488
|
+
transport: makeL1HttpTransport(l1RpcUrls, {
|
|
489
|
+
timeout: this.config.l1HttpTimeoutMS
|
|
490
|
+
})
|
|
447
491
|
});
|
|
448
|
-
|
|
449
|
-
|
|
492
|
+
try {
|
|
493
|
+
const res = await client.request({
|
|
494
|
+
method: 'eth_getBlockByHash',
|
|
495
|
+
params: [
|
|
496
|
+
blockHash,
|
|
497
|
+
/*tx flag*/ false
|
|
498
|
+
]
|
|
499
|
+
});
|
|
500
|
+
if (res.parentBeaconBlockRoot) {
|
|
501
|
+
parentBeaconBlockRoot = res.parentBeaconBlockRoot;
|
|
502
|
+
}
|
|
503
|
+
} catch (err) {
|
|
504
|
+
this.log.error(`Error getting parent beacon block root`, err);
|
|
450
505
|
}
|
|
451
|
-
} catch (err) {
|
|
452
|
-
this.log.error(`Error getting parent beacon block root`, err);
|
|
453
506
|
}
|
|
454
507
|
if (!parentBeaconBlockRoot) {
|
|
455
508
|
this.log.error(`No parent beacon block root found for block ${blockHash}`);
|
|
@@ -481,8 +534,10 @@ export class HttpBlobClient {
|
|
|
481
534
|
}
|
|
482
535
|
/**
|
|
483
536
|
* Start the blob client.
|
|
484
|
-
*
|
|
537
|
+
* Fetches and caches beacon genesis config for timestamp-based slot resolution,
|
|
538
|
+
* then uploads the initial healthcheck file (awaited) and starts periodic uploads.
|
|
485
539
|
*/ async start() {
|
|
540
|
+
await this.fetchBeaconConfig();
|
|
486
541
|
if (!this.fileStoreUploadClient) {
|
|
487
542
|
return;
|
|
488
543
|
}
|
|
@@ -501,6 +556,40 @@ export class HttpBlobClient {
|
|
|
501
556
|
}, intervalMs);
|
|
502
557
|
}
|
|
503
558
|
/**
|
|
559
|
+
* Fetches and caches beacon genesis time and slot duration from the first available consensus host.
|
|
560
|
+
* These static values enable timestamp-based slot resolution, eliminating the per-fetch headers call.
|
|
561
|
+
* Logs a warning and leaves fields undefined if all hosts fail, callers fall back gracefully.
|
|
562
|
+
*/ async fetchBeaconConfig() {
|
|
563
|
+
const { l1ConsensusHostUrls } = this.config;
|
|
564
|
+
if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
for(let i = 0; i < l1ConsensusHostUrls.length; i++){
|
|
568
|
+
try {
|
|
569
|
+
const { url: genesisUrl, ...genesisOptions } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrls[i]}/eth/v1/config/genesis`, this.config, i);
|
|
570
|
+
const { url: specUrl, ...specOptions } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrls[i]}/eth/v1/config/spec`, this.config, i);
|
|
571
|
+
const [genesisRes, specRes] = await Promise.all([
|
|
572
|
+
this.fetch(genesisUrl, genesisOptions),
|
|
573
|
+
this.fetch(specUrl, specOptions)
|
|
574
|
+
]);
|
|
575
|
+
if (genesisRes.ok && specRes.ok) {
|
|
576
|
+
const genesis = await genesisRes.json();
|
|
577
|
+
const spec = await specRes.json();
|
|
578
|
+
this.beaconGenesisTime = BigInt(genesis.data.genesisTime);
|
|
579
|
+
this.beaconSecondsPerSlot = parseInt(spec.data.secondsPerSlot);
|
|
580
|
+
this.log.debug(`Fetched beacon genesis config`, {
|
|
581
|
+
genesisTime: this.beaconGenesisTime,
|
|
582
|
+
secondsPerSlot: this.beaconSecondsPerSlot
|
|
583
|
+
});
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
} catch (err) {
|
|
587
|
+
this.log.warn(`Failed to fetch beacon config from host ${l1ConsensusHostUrls[i]}`, err);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
this.log.warn('Could not fetch beacon genesis config from any consensus host — will use headers call fallback');
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
504
593
|
* Stop the blob client, clearing any periodic tasks.
|
|
505
594
|
*/ stop() {
|
|
506
595
|
if (this.healthcheckUploadIntervalId) {
|
|
@@ -509,10 +598,9 @@ export class HttpBlobClient {
|
|
|
509
598
|
}
|
|
510
599
|
}
|
|
511
600
|
}
|
|
512
|
-
function parseBlobJsonsFromResponse(response, logger) {
|
|
601
|
+
async function parseBlobJsonsFromResponse(response, logger) {
|
|
513
602
|
try {
|
|
514
|
-
|
|
515
|
-
return blobs;
|
|
603
|
+
return await Promise.all(response.data.map(parseBlobJson));
|
|
516
604
|
} catch (err) {
|
|
517
605
|
logger.error(`Error parsing blob json from response`, err);
|
|
518
606
|
return [];
|
|
@@ -522,15 +610,14 @@ function parseBlobJsonsFromResponse(response, logger) {
|
|
|
522
610
|
// https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getBlobSidecars
|
|
523
611
|
// Here we attempt to parse the response data to Buffer, and check the lengths (via Blob's constructor), to avoid
|
|
524
612
|
// 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);
|
|
613
|
+
async function parseBlobJson(rawHex) {
|
|
614
|
+
const blobBuffer = Buffer.from(rawHex.slice(2), 'hex');
|
|
615
|
+
const blob = await Blob.fromBlobBuffer(blobBuffer);
|
|
529
616
|
return blob.toJSON();
|
|
530
617
|
}
|
|
531
618
|
// Returns an array that maps each blob hash to the corresponding blob, or undefined if the blob is not found
|
|
532
619
|
// or the data does not match the commitment.
|
|
533
|
-
function processFetchedBlobs(blobs, blobHashes, logger) {
|
|
620
|
+
async function processFetchedBlobs(blobs, blobHashes, logger) {
|
|
534
621
|
const requestedBlobHashes = new Set(blobHashes.map(bufferToHex));
|
|
535
622
|
const hashToBlob = new Map();
|
|
536
623
|
for (const blobJson of blobs){
|
|
@@ -539,7 +626,7 @@ function processFetchedBlobs(blobs, blobHashes, logger) {
|
|
|
539
626
|
continue;
|
|
540
627
|
}
|
|
541
628
|
try {
|
|
542
|
-
const blob = Blob.fromJson(blobJson);
|
|
629
|
+
const blob = await Blob.fromJson(blobJson);
|
|
543
630
|
hashToBlob.set(hashHex, blob);
|
|
544
631
|
} catch (err) {
|
|
545
632
|
// 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
|