@aztec/blob-client 0.0.1-commit.cd76b27 → 0.0.1-commit.ce4f8c4f2
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/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 +142 -55
- package/dest/client/interface.d.ts +12 -1
- package/dest/client/interface.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/client/config.ts +9 -0
- package/src/client/http.ts +169 -48
- package/src/client/interface.ts +11 -0
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":"AAAA,OAAO,EAAE,IAAI,EAAE,KAAK,QAAQ,EAA+B,MAAM,iBAAiB,CAAC;AAGnF,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,uBAAuB,CAAC;AAOlE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAEjF,OAAO,EAAE,KAAK,gBAAgB,EAA8B,MAAM,aAAa,CAAC;AAChF,OAAO,KAAK,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAEjF,qBAAa,cAAe,YAAW,mBAAmB;IAkBtD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAjBvB,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAC/B,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAC5C,SAAS,CAAC,QAAQ,CAAC,aAAa,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAChE,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC;IACvC,SAAS,CAAC,QAAQ,CAAC,gBAAgB,EAAE,mBAAmB,EAAE,CAAC;IAC3D,SAAS,CAAC,QAAQ,CAAC,qBAAqB,EAAE,mBAAmB,GAAG,SAAS,CAAC;IAE1E,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,2BAA2B,CAAC,CAAiB;IAErD,sFAAsF;IACtF,OAAO,CAAC,iBAAiB,CAAC,CAAS;IACnC,uEAAuE;IACvE,OAAO,CAAC,oBAAoB,CAAC,CAAS;IAEtC,YACE,MAAM,CAAC,EAAE,gBAAgB,EACR,IAAI,GAAE;QACrB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,aAAa,CAAC,EAAE,iBAAiB,CAAC;QAClC,gBAAgB,CAAC,EAAE,mBAAmB,EAAE,CAAC;QACzC,qBAAqB,CAAC,EAAE,mBAAmB,CAAC;QAC5C,yEAAyE;QACzE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;KACrC,EAwBP;IAED;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAU9B;;;;;OAKG;IACI,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAGvC;IAEY,WAAW,kBAwGvB;IAEY,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAmBjE;IAED;;;;;;;;;;;;;;OAcG;IACU,cAAc,CACzB,SAAS,EAAE,KAAK,MAAM,EAAE,EACxB,UAAU,EAAE,MAAM,EAAE,EACpB,IAAI,CAAC,EAAE,qBAAqB,GAC3B,OAAO,CAAC,IAAI,EAAE,CAAC,CAkJjB;YAQa,aAAa;IAsCd,kBAAkB,CAC7B,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GAAG,MAAM,EAChC,UAAU,GAAE,MAAM,EAAO,EACzB,oBAAoB,CAAC,EAAE,MAAM,GAC5B,OAAO,CAAC,IAAI,EAAE,CAAC,CAGjB;IAEY,gBAAgB,CAC3B,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GAAG,MAAM,EAChC,oBAAoB,CAAC,EAAE,MAAM,EAC7B,UAAU,CAAC,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,QAAQ,EAAE,CAAC,CAsCrB;IAED,OAAO,CAAC,iBAAiB;YAqBX,mBAAmB;YAmCnB,aAAa;IA6E3B,sCAAsC;IAC/B,gBAAgB,IAAI,iBAAiB,GAAG,SAAS,CAEvD;IAED,iEAAiE;IAC1D,SAAS,IAAI,OAAO,CAE1B;IAED;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAWlC;IAED;;OAEG;IACH,OAAO,CAAC,8BAA8B;YAgBxB,iBAAiB;IA0C/B;;OAEG;IACI,IAAI,IAAI,IAAI,CAKlB;CACF"}
|
package/dest/client/http.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Blob, computeEthVersionedBlobHash } from '@aztec/blob-lib';
|
|
2
|
+
import { makeL1HttpTransport } from '@aztec/ethereum/client';
|
|
2
3
|
import { shuffle } from '@aztec/foundation/array';
|
|
3
4
|
import { createLogger } from '@aztec/foundation/log';
|
|
4
5
|
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
5
6
|
import { bufferToHex, hexToBuffer } from '@aztec/foundation/string';
|
|
6
|
-
import { createPublicClient
|
|
7
|
+
import { createPublicClient } from 'viem';
|
|
7
8
|
import { createBlobArchiveClient } from '../archive/factory.js';
|
|
8
9
|
import { DEFAULT_HEALTHCHECK_UPLOAD_INTERVAL_MINUTES } from '../filestore/healthcheck.js';
|
|
9
10
|
import { getBlobClientConfigFromEnv } from './config.js';
|
|
@@ -17,6 +18,8 @@ export class HttpBlobClient {
|
|
|
17
18
|
fileStoreUploadClient;
|
|
18
19
|
disabled;
|
|
19
20
|
healthcheckUploadIntervalId;
|
|
21
|
+
/** Cached beacon genesis time (seconds since Unix epoch). Fetched once at startup. */ beaconGenesisTime;
|
|
22
|
+
/** Cached beacon slot duration in seconds. Fetched once at startup. */ beaconSecondsPerSlot;
|
|
20
23
|
constructor(config, opts = {}){
|
|
21
24
|
this.opts = opts;
|
|
22
25
|
this.disabled = false;
|
|
@@ -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) {
|
|
@@ -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,7 +272,7 @@ export class HttpBlobClient {
|
|
|
235
272
|
l1ConsensusHostUrl,
|
|
236
273
|
...ctx
|
|
237
274
|
});
|
|
238
|
-
const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex);
|
|
275
|
+
const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex, getMissingBlobHashes());
|
|
239
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,
|
|
@@ -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);
|
|
374
|
+
const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
|
|
338
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,10 +610,9 @@ 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
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/blob-client",
|
|
3
|
-
"version": "0.0.1-commit.
|
|
3
|
+
"version": "0.0.1-commit.ce4f8c4f2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": "./dest/client/bin/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -56,12 +56,12 @@
|
|
|
56
56
|
]
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@aztec/blob-lib": "0.0.1-commit.
|
|
60
|
-
"@aztec/ethereum": "0.0.1-commit.
|
|
61
|
-
"@aztec/foundation": "0.0.1-commit.
|
|
62
|
-
"@aztec/kv-store": "0.0.1-commit.
|
|
63
|
-
"@aztec/stdlib": "0.0.1-commit.
|
|
64
|
-
"@aztec/telemetry-client": "0.0.1-commit.
|
|
59
|
+
"@aztec/blob-lib": "0.0.1-commit.ce4f8c4f2",
|
|
60
|
+
"@aztec/ethereum": "0.0.1-commit.ce4f8c4f2",
|
|
61
|
+
"@aztec/foundation": "0.0.1-commit.ce4f8c4f2",
|
|
62
|
+
"@aztec/kv-store": "0.0.1-commit.ce4f8c4f2",
|
|
63
|
+
"@aztec/stdlib": "0.0.1-commit.ce4f8c4f2",
|
|
64
|
+
"@aztec/telemetry-client": "0.0.1-commit.ce4f8c4f2",
|
|
65
65
|
"express": "^4.21.2",
|
|
66
66
|
"snappy": "^7.2.2",
|
|
67
67
|
"source-map-support": "^0.5.21",
|
package/src/client/config.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
SecretValue,
|
|
4
4
|
booleanConfigHelper,
|
|
5
5
|
getConfigFromMappings,
|
|
6
|
+
optionalNumberConfigHelper,
|
|
6
7
|
} from '@aztec/foundation/config';
|
|
7
8
|
|
|
8
9
|
import { type BlobArchiveApiConfig, blobArchiveApiConfigMappings } from '../archive/config.js';
|
|
@@ -55,6 +56,9 @@ export interface BlobClientConfig extends BlobArchiveApiConfig {
|
|
|
55
56
|
* Interval in minutes for uploading healthcheck file to file store (default: 60 = 1 hour)
|
|
56
57
|
*/
|
|
57
58
|
blobHealthcheckUploadIntervalMinutes?: number;
|
|
59
|
+
|
|
60
|
+
/** Timeout for HTTP requests to the L1 RPC node in ms. */
|
|
61
|
+
l1HttpTimeoutMS?: number;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
export const blobClientConfigMapping: ConfigMappingsType<BlobClientConfig> = {
|
|
@@ -108,6 +112,11 @@ export const blobClientConfigMapping: ConfigMappingsType<BlobClientConfig> = {
|
|
|
108
112
|
description: 'Interval in minutes for uploading healthcheck file to file store (default: 60 = 1 hour)',
|
|
109
113
|
parseEnv: (val: string | undefined) => (val ? +val : undefined),
|
|
110
114
|
},
|
|
115
|
+
l1HttpTimeoutMS: {
|
|
116
|
+
env: 'ETHEREUM_HTTP_TIMEOUT_MS',
|
|
117
|
+
description: 'Timeout for HTTP requests to the L1 RPC node in ms.',
|
|
118
|
+
...optionalNumberConfigHelper(),
|
|
119
|
+
},
|
|
111
120
|
...blobArchiveApiConfigMappings,
|
|
112
121
|
};
|
|
113
122
|
|
package/src/client/http.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Blob, type BlobJson, computeEthVersionedBlobHash } from '@aztec/blob-lib';
|
|
2
|
+
import { makeL1HttpTransport } from '@aztec/ethereum/client';
|
|
2
3
|
import { shuffle } from '@aztec/foundation/array';
|
|
3
4
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
4
5
|
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
5
6
|
import { bufferToHex, hexToBuffer } from '@aztec/foundation/string';
|
|
6
7
|
|
|
7
|
-
import { type RpcBlock, createPublicClient
|
|
8
|
+
import { type RpcBlock, createPublicClient } from 'viem';
|
|
8
9
|
|
|
9
10
|
import { createBlobArchiveClient } from '../archive/factory.js';
|
|
10
11
|
import type { BlobArchiveClient } from '../archive/interface.js';
|
|
@@ -24,6 +25,11 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
24
25
|
private disabled = false;
|
|
25
26
|
private healthcheckUploadIntervalId?: NodeJS.Timeout;
|
|
26
27
|
|
|
28
|
+
/** Cached beacon genesis time (seconds since Unix epoch). Fetched once at startup. */
|
|
29
|
+
private beaconGenesisTime?: bigint;
|
|
30
|
+
/** Cached beacon slot duration in seconds. Fetched once at startup. */
|
|
31
|
+
private beaconSecondsPerSlot?: number;
|
|
32
|
+
|
|
27
33
|
constructor(
|
|
28
34
|
config?: BlobClientConfig,
|
|
29
35
|
private readonly opts: {
|
|
@@ -89,44 +95,68 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
89
95
|
const archiveUrl = this.archiveClient?.getBaseUrl();
|
|
90
96
|
this.log.info(`Testing configured blob sources`, { l1ConsensusHostUrls, archiveUrl });
|
|
91
97
|
|
|
92
|
-
let
|
|
98
|
+
let consensusSuperNodes = 0;
|
|
99
|
+
let consensusNonSuperNodes = 0;
|
|
100
|
+
let archiveSources = 0;
|
|
101
|
+
let blobSinks = 0;
|
|
93
102
|
|
|
94
103
|
if (l1ConsensusHostUrls && l1ConsensusHostUrls.length > 0) {
|
|
95
104
|
for (let l1ConsensusHostIndex = 0; l1ConsensusHostIndex < l1ConsensusHostUrls.length; l1ConsensusHostIndex++) {
|
|
96
105
|
const l1ConsensusHostUrl = l1ConsensusHostUrls[l1ConsensusHostIndex];
|
|
97
106
|
try {
|
|
98
107
|
const { url, ...options } = getBeaconNodeFetchOptions(
|
|
99
|
-
`${l1ConsensusHostUrl}/eth/v1/beacon/headers`,
|
|
108
|
+
`${l1ConsensusHostUrl}/eth/v1/beacon/headers/head`,
|
|
100
109
|
this.config,
|
|
101
110
|
l1ConsensusHostIndex,
|
|
102
111
|
);
|
|
103
112
|
const res = await this.fetch(url, options);
|
|
104
|
-
if (res.ok) {
|
|
105
|
-
this.log.info(`L1 consensus host is reachable`, { l1ConsensusHostUrl });
|
|
106
|
-
successfulSourceCount++;
|
|
107
|
-
} else {
|
|
113
|
+
if (!res.ok) {
|
|
108
114
|
this.log.error(`Failure reaching L1 consensus host: ${res.statusText} (${res.status})`, {
|
|
109
115
|
l1ConsensusHostUrl,
|
|
110
116
|
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.log.info(`L1 consensus host is reachable`, { l1ConsensusHostUrl });
|
|
121
|
+
|
|
122
|
+
// Check if the host serves blob sidecars (supernode/semi-supernode).
|
|
123
|
+
// Post-Fusaka (PeerDAS), non-supernode beacon nodes no longer serve the
|
|
124
|
+
// blob sidecar endpoint. A 200 response (even with an empty data array
|
|
125
|
+
// for a slot with no blobs) means the node supports serving blob sidecars.
|
|
126
|
+
const body = await res.json();
|
|
127
|
+
const headSlot = body?.data?.header?.message?.slot;
|
|
128
|
+
if (headSlot) {
|
|
129
|
+
const { url: blobUrl, ...blobOptions } = getBeaconNodeFetchOptions(
|
|
130
|
+
`${l1ConsensusHostUrl}/eth/v1/beacon/blobs/${headSlot}`,
|
|
131
|
+
this.config,
|
|
132
|
+
l1ConsensusHostIndex,
|
|
133
|
+
);
|
|
134
|
+
const blobRes = await this.fetch(blobUrl, blobOptions);
|
|
135
|
+
if (blobRes.ok) {
|
|
136
|
+
this.log.info(`L1 consensus host serves blob sidecars (supernode)`, { l1ConsensusHostUrl });
|
|
137
|
+
consensusSuperNodes++;
|
|
138
|
+
} else {
|
|
139
|
+
this.log.info(`L1 consensus host does not serve blob sidecars`, { l1ConsensusHostUrl });
|
|
140
|
+
consensusNonSuperNodes++;
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
this.log.info(`L1 consensus host is reachable but could not determine head slot`, { l1ConsensusHostUrl });
|
|
144
|
+
consensusNonSuperNodes++;
|
|
111
145
|
}
|
|
112
146
|
} catch (err) {
|
|
113
147
|
this.log.error(`Error reaching L1 consensus host`, err, { l1ConsensusHostUrl });
|
|
114
148
|
}
|
|
115
149
|
}
|
|
116
|
-
} else {
|
|
117
|
-
this.log.warn('No L1 consensus host urls configured');
|
|
118
150
|
}
|
|
119
151
|
|
|
120
152
|
if (this.archiveClient) {
|
|
121
153
|
try {
|
|
122
154
|
const latest = await this.archiveClient.getLatestBlock();
|
|
123
155
|
this.log.info(`Archive client is reachable and synced to L1 block ${latest.number}`, { latest, archiveUrl });
|
|
124
|
-
|
|
156
|
+
archiveSources++;
|
|
125
157
|
} catch (err) {
|
|
126
158
|
this.log.error(`Error reaching archive client`, err, { archiveUrl });
|
|
127
159
|
}
|
|
128
|
-
} else {
|
|
129
|
-
this.log.warn('No archive client configured');
|
|
130
160
|
}
|
|
131
161
|
|
|
132
162
|
if (this.fileStoreClients.length > 0) {
|
|
@@ -135,7 +165,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
135
165
|
const accessible = await fileStoreClient.testConnection();
|
|
136
166
|
if (accessible) {
|
|
137
167
|
this.log.info(`FileStore is reachable`, { url: fileStoreClient.getBaseUrl() });
|
|
138
|
-
|
|
168
|
+
blobSinks++;
|
|
139
169
|
} else {
|
|
140
170
|
this.log.warn(`FileStore is not accessible`, { url: fileStoreClient.getBaseUrl() });
|
|
141
171
|
}
|
|
@@ -145,12 +175,24 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
145
175
|
}
|
|
146
176
|
}
|
|
147
177
|
|
|
178
|
+
// Emit a single summary after validating all sources
|
|
179
|
+
const successfulSourceCount = consensusSuperNodes + archiveSources + blobSinks;
|
|
180
|
+
|
|
181
|
+
let summary = `Blob client running with consensusSuperNodes=${consensusSuperNodes} archiveSources=${archiveSources} blobSinks=${blobSinks}`;
|
|
182
|
+
if (consensusNonSuperNodes > 0) {
|
|
183
|
+
summary += `. ${consensusNonSuperNodes} consensus client(s) ignored because they are not running in supernode or semi-supernode mode`;
|
|
184
|
+
}
|
|
185
|
+
|
|
148
186
|
if (successfulSourceCount === 0) {
|
|
149
187
|
if (this.config.blobAllowEmptySources) {
|
|
150
|
-
this.log.warn(
|
|
188
|
+
this.log.warn(summary);
|
|
151
189
|
} else {
|
|
152
|
-
throw new Error(
|
|
190
|
+
throw new Error(summary);
|
|
153
191
|
}
|
|
192
|
+
} else if (consensusSuperNodes === 0) {
|
|
193
|
+
this.log.warn(summary);
|
|
194
|
+
} else {
|
|
195
|
+
this.log.info(summary);
|
|
154
196
|
}
|
|
155
197
|
}
|
|
156
198
|
|
|
@@ -251,7 +293,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
251
293
|
// The beacon api can query by slot number, so we get that first
|
|
252
294
|
const consensusCtx = { l1ConsensusHostUrls, ...ctx };
|
|
253
295
|
this.log.trace(`Attempting to get slot number for block hash`, consensusCtx);
|
|
254
|
-
const slotNumber = await this.getSlotNumber(blockHash);
|
|
296
|
+
const slotNumber = await this.getSlotNumber(blockHash, opts?.parentBeaconBlockRoot, opts?.l1BlockTimestamp);
|
|
255
297
|
this.log.debug(`Got slot number ${slotNumber} from consensus host for querying blobs`, consensusCtx);
|
|
256
298
|
|
|
257
299
|
if (slotNumber) {
|
|
@@ -268,7 +310,12 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
268
310
|
l1ConsensusHostUrl,
|
|
269
311
|
...ctx,
|
|
270
312
|
});
|
|
271
|
-
const blobs = await this.getBlobsFromHost(
|
|
313
|
+
const blobs = await this.getBlobsFromHost(
|
|
314
|
+
l1ConsensusHostUrl,
|
|
315
|
+
slotNumber,
|
|
316
|
+
l1ConsensusHostIndex,
|
|
317
|
+
getMissingBlobHashes(),
|
|
318
|
+
);
|
|
272
319
|
const result = await fillResults(blobs);
|
|
273
320
|
this.log.debug(
|
|
274
321
|
`Got ${blobs.length} blobs from consensus host (total: ${result.length}/${blobHashes.length})`,
|
|
@@ -387,7 +434,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
387
434
|
blobHashes: Buffer[] = [],
|
|
388
435
|
l1ConsensusHostIndex?: number,
|
|
389
436
|
): Promise<Blob[]> {
|
|
390
|
-
const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
|
|
437
|
+
const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
|
|
391
438
|
return (await processFetchedBlobs(blobs, blobHashes, this.log)).filter((b): b is Blob => b !== undefined);
|
|
392
439
|
}
|
|
393
440
|
|
|
@@ -395,11 +442,12 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
395
442
|
hostUrl: string,
|
|
396
443
|
blockHashOrSlot: string | number,
|
|
397
444
|
l1ConsensusHostIndex?: number,
|
|
445
|
+
blobHashes?: Buffer[],
|
|
398
446
|
): Promise<BlobJson[]> {
|
|
399
447
|
try {
|
|
400
|
-
let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
|
|
448
|
+
let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
|
|
401
449
|
if (res.ok) {
|
|
402
|
-
return parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
450
|
+
return await parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
403
451
|
}
|
|
404
452
|
|
|
405
453
|
if (res.status === 404 && typeof blockHashOrSlot === 'number') {
|
|
@@ -414,9 +462,9 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
414
462
|
let currentSlot = blockHashOrSlot + 1;
|
|
415
463
|
while (res.status === 404 && maxRetries > 0 && latestSlot !== undefined && currentSlot <= latestSlot) {
|
|
416
464
|
this.log.debug(`Trying slot ${currentSlot}`);
|
|
417
|
-
res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex);
|
|
465
|
+
res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex, blobHashes);
|
|
418
466
|
if (res.ok) {
|
|
419
|
-
return parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
467
|
+
return await parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
420
468
|
}
|
|
421
469
|
currentSlot++;
|
|
422
470
|
maxRetries--;
|
|
@@ -439,8 +487,17 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
439
487
|
hostUrl: string,
|
|
440
488
|
blockHashOrSlot: string | number,
|
|
441
489
|
l1ConsensusHostIndex?: number,
|
|
490
|
+
blobHashes?: Buffer[],
|
|
442
491
|
): Promise<Response> {
|
|
443
|
-
|
|
492
|
+
let baseUrl = `${hostUrl}/eth/v1/beacon/blobs/${blockHashOrSlot}`;
|
|
493
|
+
|
|
494
|
+
if (blobHashes && blobHashes.length > 0) {
|
|
495
|
+
const params = new URLSearchParams();
|
|
496
|
+
for (const hash of blobHashes) {
|
|
497
|
+
params.append('versioned_hashes', `0x${hash.toString('hex')}`);
|
|
498
|
+
}
|
|
499
|
+
baseUrl += `?${params.toString()}`;
|
|
500
|
+
}
|
|
444
501
|
|
|
445
502
|
const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
|
|
446
503
|
this.log.debug(`Fetching blob sidecar for ${blockHashOrSlot}`, { url, ...options });
|
|
@@ -482,34 +539,50 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
482
539
|
* @param blockHash - The block hash
|
|
483
540
|
* @returns The slot number
|
|
484
541
|
*/
|
|
485
|
-
private async getSlotNumber(
|
|
542
|
+
private async getSlotNumber(
|
|
543
|
+
blockHash: `0x${string}`,
|
|
544
|
+
parentBeaconBlockRoot?: string,
|
|
545
|
+
l1BlockTimestamp?: bigint,
|
|
546
|
+
): Promise<number | undefined> {
|
|
486
547
|
const { l1ConsensusHostUrls, l1RpcUrls } = this.config;
|
|
487
548
|
if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
|
|
488
549
|
this.log.debug('No consensus host url configured');
|
|
489
550
|
return undefined;
|
|
490
551
|
}
|
|
491
552
|
|
|
492
|
-
if (
|
|
493
|
-
|
|
494
|
-
|
|
553
|
+
// Primary path: compute slot from timestamp if genesis config is cached (no network call needed)
|
|
554
|
+
if (
|
|
555
|
+
l1BlockTimestamp !== undefined &&
|
|
556
|
+
this.beaconGenesisTime !== undefined &&
|
|
557
|
+
this.beaconSecondsPerSlot !== undefined
|
|
558
|
+
) {
|
|
559
|
+
const slot = Number((l1BlockTimestamp - this.beaconGenesisTime) / BigInt(this.beaconSecondsPerSlot));
|
|
560
|
+
this.log.debug(`Computed slot ${slot} from L1 block timestamp`, { l1BlockTimestamp });
|
|
561
|
+
return slot;
|
|
495
562
|
}
|
|
496
563
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
564
|
+
if (!parentBeaconBlockRoot) {
|
|
565
|
+
// parentBeaconBlockRoot not provided by caller — fetch it from the execution RPC
|
|
566
|
+
if (!l1RpcUrls || l1RpcUrls.length === 0) {
|
|
567
|
+
this.log.debug('No execution host url configured');
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const client = createPublicClient({
|
|
572
|
+
transport: makeL1HttpTransport(l1RpcUrls, { timeout: this.config.l1HttpTimeoutMS }),
|
|
506
573
|
});
|
|
574
|
+
try {
|
|
575
|
+
const res: RpcBlock = await client.request({
|
|
576
|
+
method: 'eth_getBlockByHash',
|
|
577
|
+
params: [blockHash, /*tx flag*/ false],
|
|
578
|
+
});
|
|
507
579
|
|
|
508
|
-
|
|
509
|
-
|
|
580
|
+
if (res.parentBeaconBlockRoot) {
|
|
581
|
+
parentBeaconBlockRoot = res.parentBeaconBlockRoot;
|
|
582
|
+
}
|
|
583
|
+
} catch (err) {
|
|
584
|
+
this.log.error(`Error getting parent beacon block root`, err);
|
|
510
585
|
}
|
|
511
|
-
} catch (err) {
|
|
512
|
-
this.log.error(`Error getting parent beacon block root`, err);
|
|
513
586
|
}
|
|
514
587
|
|
|
515
588
|
if (!parentBeaconBlockRoot) {
|
|
@@ -555,9 +628,12 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
555
628
|
|
|
556
629
|
/**
|
|
557
630
|
* Start the blob client.
|
|
558
|
-
*
|
|
631
|
+
* Fetches and caches beacon genesis config for timestamp-based slot resolution,
|
|
632
|
+
* then uploads the initial healthcheck file (awaited) and starts periodic uploads.
|
|
559
633
|
*/
|
|
560
634
|
public async start(): Promise<void> {
|
|
635
|
+
await this.fetchBeaconConfig();
|
|
636
|
+
|
|
561
637
|
if (!this.fileStoreUploadClient) {
|
|
562
638
|
return;
|
|
563
639
|
}
|
|
@@ -582,6 +658,53 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
582
658
|
}, intervalMs);
|
|
583
659
|
}
|
|
584
660
|
|
|
661
|
+
/**
|
|
662
|
+
* Fetches and caches beacon genesis time and slot duration from the first available consensus host.
|
|
663
|
+
* These static values enable timestamp-based slot resolution, eliminating the per-fetch headers call.
|
|
664
|
+
* Logs a warning and leaves fields undefined if all hosts fail, callers fall back gracefully.
|
|
665
|
+
*/
|
|
666
|
+
private async fetchBeaconConfig(): Promise<void> {
|
|
667
|
+
const { l1ConsensusHostUrls } = this.config;
|
|
668
|
+
if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
for (let i = 0; i < l1ConsensusHostUrls.length; i++) {
|
|
673
|
+
try {
|
|
674
|
+
const { url: genesisUrl, ...genesisOptions } = getBeaconNodeFetchOptions(
|
|
675
|
+
`${l1ConsensusHostUrls[i]}/eth/v1/config/genesis`,
|
|
676
|
+
this.config,
|
|
677
|
+
i,
|
|
678
|
+
);
|
|
679
|
+
const { url: specUrl, ...specOptions } = getBeaconNodeFetchOptions(
|
|
680
|
+
`${l1ConsensusHostUrls[i]}/eth/v1/config/spec`,
|
|
681
|
+
this.config,
|
|
682
|
+
i,
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
const [genesisRes, specRes] = await Promise.all([
|
|
686
|
+
this.fetch(genesisUrl, genesisOptions),
|
|
687
|
+
this.fetch(specUrl, specOptions),
|
|
688
|
+
]);
|
|
689
|
+
|
|
690
|
+
if (genesisRes.ok && specRes.ok) {
|
|
691
|
+
const genesis = await genesisRes.json();
|
|
692
|
+
const spec = await specRes.json();
|
|
693
|
+
this.beaconGenesisTime = BigInt(genesis.data.genesisTime);
|
|
694
|
+
this.beaconSecondsPerSlot = parseInt(spec.data.secondsPerSlot);
|
|
695
|
+
this.log.debug(`Fetched beacon genesis config`, {
|
|
696
|
+
genesisTime: this.beaconGenesisTime,
|
|
697
|
+
secondsPerSlot: this.beaconSecondsPerSlot,
|
|
698
|
+
});
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
} catch (err) {
|
|
702
|
+
this.log.warn(`Failed to fetch beacon config from host ${l1ConsensusHostUrls[i]}`, err);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
this.log.warn('Could not fetch beacon genesis config from any consensus host — will use headers call fallback');
|
|
706
|
+
}
|
|
707
|
+
|
|
585
708
|
/**
|
|
586
709
|
* Stop the blob client, clearing any periodic tasks.
|
|
587
710
|
*/
|
|
@@ -593,10 +716,9 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
593
716
|
}
|
|
594
717
|
}
|
|
595
718
|
|
|
596
|
-
function parseBlobJsonsFromResponse(response: any, logger: Logger): BlobJson[] {
|
|
719
|
+
async function parseBlobJsonsFromResponse(response: any, logger: Logger): Promise<BlobJson[]> {
|
|
597
720
|
try {
|
|
598
|
-
|
|
599
|
-
return blobs;
|
|
721
|
+
return await Promise.all((response.data as string[]).map(parseBlobJson));
|
|
600
722
|
} catch (err) {
|
|
601
723
|
logger.error(`Error parsing blob json from response`, err);
|
|
602
724
|
return [];
|
|
@@ -607,10 +729,9 @@ function parseBlobJsonsFromResponse(response: any, logger: Logger): BlobJson[] {
|
|
|
607
729
|
// https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getBlobSidecars
|
|
608
730
|
// Here we attempt to parse the response data to Buffer, and check the lengths (via Blob's constructor), to avoid
|
|
609
731
|
// 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);
|
|
732
|
+
async function parseBlobJson(rawHex: string): Promise<BlobJson> {
|
|
733
|
+
const blobBuffer = Buffer.from(rawHex.slice(2), 'hex');
|
|
734
|
+
const blob = await Blob.fromBlobBuffer(blobBuffer);
|
|
614
735
|
return blob.toJSON();
|
|
615
736
|
}
|
|
616
737
|
|
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 {
|