@aztec/blob-client 0.0.1-commit.993d52e → 0.0.1-commit.9ee6fcc6

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.
@@ -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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmlnLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvY2xpZW50L2NvbmZpZy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQ0wsS0FBSyxrQkFBa0IsRUFDdkIsV0FBVyxFQUdaLE1BQU0sMEJBQTBCLENBQUM7QUFFbEMsT0FBTyxFQUFFLEtBQUssb0JBQW9CLEVBQWdDLE1BQU0sc0JBQXNCLENBQUM7QUFFL0Y7O0dBRUc7QUFDSCxNQUFNLFdBQVcsZ0JBQWlCLFNBQVEsb0JBQW9CO0lBQzVEOztPQUVHO0lBQ0gsU0FBUyxDQUFDLEVBQUUsTUFBTSxFQUFFLENBQUM7SUFFckI7O09BRUc7SUFDSCxtQkFBbUIsQ0FBQyxFQUFFLE1BQU0sRUFBRSxDQUFDO0lBRS9COztPQUVHO0lBQ0gsc0JBQXNCLENBQUMsRUFBRSxXQUFXLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztJQUUvQzs7T0FFRztJQUNILDRCQUE0QixDQUFDLEVBQUUsTUFBTSxFQUFFLENBQUM7SUFFeEM7O09BRUc7SUFDSCxpQkFBaUIsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUUzQjs7T0FFRztJQUNILHFCQUFxQixDQUFDLEVBQUUsT0FBTyxDQUFDO0lBRWhDOztPQUVHO0lBQ0gsaUJBQWlCLENBQUMsRUFBRSxNQUFNLEVBQUUsQ0FBQztJQUU3Qjs7T0FFRztJQUNILHNCQUFzQixDQUFDLEVBQUUsTUFBTSxDQUFDO0lBRWhDOztPQUVHO0lBQ0gsb0NBQW9DLENBQUMsRUFBRSxNQUFNLENBQUM7Q0FDL0M7QUFFRCxlQUFPLE1BQU0sdUJBQXVCLEVBQUUsa0JBQWtCLENBQUMsZ0JBQWdCLENBb0R4RSxDQUFDO0FBRUY7OztHQUdHO0FBQ0gsd0JBQWdCLDBCQUEwQixJQUFJLGdCQUFnQixDQUU3RDtBQUVEOztHQUVHO0FBQ0gsd0JBQWdCLG9CQUFvQixDQUFDLE1BQU0sR0FBRSxnQkFBcUIsR0FBRyxPQUFPLENBRTNFIn0=
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,EAGZ,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;CAC/C;AAED,eAAO,MAAM,uBAAuB,EAAE,kBAAkB,CAAC,gBAAgB,CAoDxE,CAAC;AAEF;;;GAGG;AACH,wBAAgB,0BAA0B,IAAI,gBAAgB,CAE7D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,GAAE,gBAAqB,GAAG,OAAO,CAE3E"}
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"}
@@ -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
  /**
@@ -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
- * Uploads the initial healthcheck file (awaited) and starts periodic uploads.
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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaHR0cC5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2NsaWVudC9odHRwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxJQUFJLEVBQUUsS0FBSyxRQUFRLEVBQStCLE1BQU0saUJBQWlCLENBQUM7QUFFbkYsT0FBTyxFQUFFLEtBQUssTUFBTSxFQUFnQixNQUFNLHVCQUF1QixDQUFDO0FBT2xFLE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDakUsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSx1Q0FBdUMsQ0FBQztBQUVqRixPQUFPLEVBQUUsS0FBSyxnQkFBZ0IsRUFBOEIsTUFBTSxhQUFhLENBQUM7QUFDaEYsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVqRixxQkFBYSxjQUFlLFlBQVcsbUJBQW1CO0lBYXRELE9BQU8sQ0FBQyxRQUFRLENBQUMsSUFBSTtJQVp2QixTQUFTLENBQUMsUUFBUSxDQUFDLEdBQUcsRUFBRSxNQUFNLENBQUM7SUFDL0IsU0FBUyxDQUFDLFFBQVEsQ0FBQyxNQUFNLEVBQUUsZ0JBQWdCLENBQUM7SUFDNUMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxhQUFhLEVBQUUsaUJBQWlCLEdBQUcsU0FBUyxDQUFDO0lBQ2hFLFNBQVMsQ0FBQyxRQUFRLENBQUMsS0FBSyxFQUFFLE9BQU8sS0FBSyxDQUFDO0lBQ3ZDLFNBQVMsQ0FBQyxRQUFRLENBQUMsZ0JBQWdCLEVBQUUsbUJBQW1CLEVBQUUsQ0FBQztJQUMzRCxTQUFTLENBQUMsUUFBUSxDQUFDLHFCQUFxQixFQUFFLG1CQUFtQixHQUFHLFNBQVMsQ0FBQztJQUUxRSxPQUFPLENBQUMsUUFBUSxDQUFTO0lBQ3pCLE9BQU8sQ0FBQywyQkFBMkIsQ0FBQyxDQUFpQjtJQUVyRCxZQUNFLE1BQU0sQ0FBQyxFQUFFLGdCQUFnQixFQUNSLElBQUksR0FBRTtRQUNyQixNQUFNLENBQUMsRUFBRSxNQUFNLENBQUM7UUFDaEIsYUFBYSxDQUFDLEVBQUUsaUJBQWlCLENBQUM7UUFDbEMsZ0JBQWdCLENBQUMsRUFBRSxtQkFBbUIsRUFBRSxDQUFDO1FBQ3pDLHFCQUFxQixDQUFDLEVBQUUsbUJBQW1CLENBQUM7UUFDNUMseUVBQXlFO1FBQ3pFLGNBQWMsQ0FBQyxFQUFFLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxLQUFLLElBQUksQ0FBQztLQUNyQyxFQXdCUDtJQUVEOzs7T0FHRztJQUNILE9BQU8sQ0FBQyxzQkFBc0I7SUFVOUI7Ozs7O09BS0c7SUFDSSxXQUFXLENBQUMsS0FBSyxFQUFFLE9BQU8sR0FBRyxJQUFJLENBR3ZDO0lBRVksV0FBVyxrQkFvRXZCO0lBRVksb0JBQW9CLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxHQUFHLE9BQU8sQ0FBQyxPQUFPLENBQUMsQ0FtQmpFO0lBRUQ7Ozs7Ozs7Ozs7Ozs7O09BY0c7SUFDVSxjQUFjLENBQ3pCLFNBQVMsRUFBRSxLQUFLLE1BQU0sRUFBRSxFQUN4QixVQUFVLEVBQUUsTUFBTSxFQUFFLEVBQ3BCLElBQUksQ0FBQyxFQUFFLHFCQUFxQixHQUMzQixPQUFPLENBQUMsSUFBSSxFQUFFLENBQUMsQ0E2SWpCO1lBUWEsYUFBYTtJQXNDZCxrQkFBa0IsQ0FDN0IsT0FBTyxFQUFFLE1BQU0sRUFDZixlQUFlLEVBQUUsTUFBTSxHQUFHLE1BQU0sRUFDaEMsVUFBVSxHQUFFLE1BQU0sRUFBTyxFQUN6QixvQkFBb0IsQ0FBQyxFQUFFLE1BQU0sR0FDNUIsT0FBTyxDQUFDLElBQUksRUFBRSxDQUFDLENBR2pCO0lBRVksZ0JBQWdCLENBQzNCLE9BQU8sRUFBRSxNQUFNLEVBQ2YsZUFBZSxFQUFFLE1BQU0sR0FBRyxNQUFNLEVBQ2hDLG9CQUFvQixDQUFDLEVBQUUsTUFBTSxHQUM1QixPQUFPLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FzQ3JCO0lBRUQsT0FBTyxDQUFDLGlCQUFpQjtZQVlYLG1CQUFtQjtZQW1DbkIsYUFBYTtJQTZEM0Isc0NBQXNDO0lBQy9CLGdCQUFnQixJQUFJLGlCQUFpQixHQUFHLFNBQVMsQ0FFdkQ7SUFFRCxpRUFBaUU7SUFDMUQsU0FBUyxJQUFJLE9BQU8sQ0FFMUI7SUFFRDs7O09BR0c7SUFDVSxLQUFLLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQVNsQztJQUVEOztPQUVHO0lBQ0gsT0FBTyxDQUFDLDhCQUE4QjtJQVd0Qzs7T0FFRztJQUNJLElBQUksSUFBSSxJQUFJLENBS2xCO0NBQ0YifQ==
85
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaHR0cC5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2NsaWVudC9odHRwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxJQUFJLEVBQUUsS0FBSyxRQUFRLEVBQStCLE1BQU0saUJBQWlCLENBQUM7QUFHbkYsT0FBTyxFQUFFLEtBQUssTUFBTSxFQUFnQixNQUFNLHVCQUF1QixDQUFDO0FBT2xFLE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDakUsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSx1Q0FBdUMsQ0FBQztBQUVqRixPQUFPLEVBQUUsS0FBSyxnQkFBZ0IsRUFBOEIsTUFBTSxhQUFhLENBQUM7QUFDaEYsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVqRixxQkFBYSxjQUFlLFlBQVcsbUJBQW1CO0lBa0J0RCxPQUFPLENBQUMsUUFBUSxDQUFDLElBQUk7SUFqQnZCLFNBQVMsQ0FBQyxRQUFRLENBQUMsR0FBRyxFQUFFLE1BQU0sQ0FBQztJQUMvQixTQUFTLENBQUMsUUFBUSxDQUFDLE1BQU0sRUFBRSxnQkFBZ0IsQ0FBQztJQUM1QyxTQUFTLENBQUMsUUFBUSxDQUFDLGFBQWEsRUFBRSxpQkFBaUIsR0FBRyxTQUFTLENBQUM7SUFDaEUsU0FBUyxDQUFDLFFBQVEsQ0FBQyxLQUFLLEVBQUUsT0FBTyxLQUFLLENBQUM7SUFDdkMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxnQkFBZ0IsRUFBRSxtQkFBbUIsRUFBRSxDQUFDO0lBQzNELFNBQVMsQ0FBQyxRQUFRLENBQUMscUJBQXFCLEVBQUUsbUJBQW1CLEdBQUcsU0FBUyxDQUFDO0lBRTFFLE9BQU8sQ0FBQyxRQUFRLENBQVM7SUFDekIsT0FBTyxDQUFDLDJCQUEyQixDQUFDLENBQWlCO0lBRXJELHNGQUFzRjtJQUN0RixPQUFPLENBQUMsaUJBQWlCLENBQUMsQ0FBUztJQUNuQyx1RUFBdUU7SUFDdkUsT0FBTyxDQUFDLG9CQUFvQixDQUFDLENBQVM7SUFFdEMsWUFDRSxNQUFNLENBQUMsRUFBRSxnQkFBZ0IsRUFDUixJQUFJLEdBQUU7UUFDckIsTUFBTSxDQUFDLEVBQUUsTUFBTSxDQUFDO1FBQ2hCLGFBQWEsQ0FBQyxFQUFFLGlCQUFpQixDQUFDO1FBQ2xDLGdCQUFnQixDQUFDLEVBQUUsbUJBQW1CLEVBQUUsQ0FBQztRQUN6QyxxQkFBcUIsQ0FBQyxFQUFFLG1CQUFtQixDQUFDO1FBQzVDLHlFQUF5RTtRQUN6RSxjQUFjLENBQUMsRUFBRSxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsS0FBSyxJQUFJLENBQUM7S0FDckMsRUF3QlA7SUFFRDs7O09BR0c7SUFDSCxPQUFPLENBQUMsc0JBQXNCO0lBVTlCOzs7OztPQUtHO0lBQ0ksV0FBVyxDQUFDLEtBQUssRUFBRSxPQUFPLEdBQUcsSUFBSSxDQUd2QztJQUVZLFdBQVcsa0JBb0V2QjtJQUVZLG9CQUFvQixDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLENBbUJqRTtJQUVEOzs7Ozs7Ozs7Ozs7OztPQWNHO0lBQ1UsY0FBYyxDQUN6QixTQUFTLEVBQUUsS0FBSyxNQUFNLEVBQUUsRUFDeEIsVUFBVSxFQUFFLE1BQU0sRUFBRSxFQUNwQixJQUFJLENBQUMsRUFBRSxxQkFBcUIsR0FDM0IsT0FBTyxDQUFDLElBQUksRUFBRSxDQUFDLENBa0pqQjtZQVFhLGFBQWE7SUFzQ2Qsa0JBQWtCLENBQzdCLE9BQU8sRUFBRSxNQUFNLEVBQ2YsZUFBZSxFQUFFLE1BQU0sR0FBRyxNQUFNLEVBQ2hDLFVBQVUsR0FBRSxNQUFNLEVBQU8sRUFDekIsb0JBQW9CLENBQUMsRUFBRSxNQUFNLEdBQzVCLE9BQU8sQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUdqQjtJQUVZLGdCQUFnQixDQUMzQixPQUFPLEVBQUUsTUFBTSxFQUNmLGVBQWUsRUFBRSxNQUFNLEdBQUcsTUFBTSxFQUNoQyxvQkFBb0IsQ0FBQyxFQUFFLE1BQU0sRUFDN0IsVUFBVSxDQUFDLEVBQUUsTUFBTSxFQUFFLEdBQ3BCLE9BQU8sQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQXNDckI7SUFFRCxPQUFPLENBQUMsaUJBQWlCO1lBcUJYLG1CQUFtQjtZQW1DbkIsYUFBYTtJQTZFM0Isc0NBQXNDO0lBQy9CLGdCQUFnQixJQUFJLGlCQUFpQixHQUFHLFNBQVMsQ0FFdkQ7SUFFRCxpRUFBaUU7SUFDMUQsU0FBUyxJQUFJLE9BQU8sQ0FFMUI7SUFFRDs7OztPQUlHO0lBQ1UsS0FBSyxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FXbEM7SUFFRDs7T0FFRztJQUNILE9BQU8sQ0FBQyw4QkFBOEI7WUFnQnhCLGlCQUFpQjtJQTBDL0I7O09BRUc7SUFDSSxJQUFJLElBQUksSUFBSSxDQUtsQjtDQUNGIn0=
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/client/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,KAAK,QAAQ,EAA+B,MAAM,iBAAiB,CAAC;AAEnF,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,uBAAuB,CAAC;AAOlE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAEjF,OAAO,EAAE,KAAK,gBAAgB,EAA8B,MAAM,aAAa,CAAC;AAChF,OAAO,KAAK,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAEjF,qBAAa,cAAe,YAAW,mBAAmB;IAatD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAZvB,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAC/B,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAC5C,SAAS,CAAC,QAAQ,CAAC,aAAa,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAChE,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC;IACvC,SAAS,CAAC,QAAQ,CAAC,gBAAgB,EAAE,mBAAmB,EAAE,CAAC;IAC3D,SAAS,CAAC,QAAQ,CAAC,qBAAqB,EAAE,mBAAmB,GAAG,SAAS,CAAC;IAE1E,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,2BAA2B,CAAC,CAAiB;IAErD,YACE,MAAM,CAAC,EAAE,gBAAgB,EACR,IAAI,GAAE;QACrB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,aAAa,CAAC,EAAE,iBAAiB,CAAC;QAClC,gBAAgB,CAAC,EAAE,mBAAmB,EAAE,CAAC;QACzC,qBAAqB,CAAC,EAAE,mBAAmB,CAAC;QAC5C,yEAAyE;QACzE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;KACrC,EAwBP;IAED;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAU9B;;;;;OAKG;IACI,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAGvC;IAEY,WAAW,kBAoEvB;IAEY,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAmBjE;IAED;;;;;;;;;;;;;;OAcG;IACU,cAAc,CACzB,SAAS,EAAE,KAAK,MAAM,EAAE,EACxB,UAAU,EAAE,MAAM,EAAE,EACpB,IAAI,CAAC,EAAE,qBAAqB,GAC3B,OAAO,CAAC,IAAI,EAAE,CAAC,CA6IjB;YAQa,aAAa;IAsCd,kBAAkB,CAC7B,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GAAG,MAAM,EAChC,UAAU,GAAE,MAAM,EAAO,EACzB,oBAAoB,CAAC,EAAE,MAAM,GAC5B,OAAO,CAAC,IAAI,EAAE,CAAC,CAGjB;IAEY,gBAAgB,CAC3B,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GAAG,MAAM,EAChC,oBAAoB,CAAC,EAAE,MAAM,GAC5B,OAAO,CAAC,QAAQ,EAAE,CAAC,CAsCrB;IAED,OAAO,CAAC,iBAAiB;YAYX,mBAAmB;YAmCnB,aAAa;IA6D3B,sCAAsC;IAC/B,gBAAgB,IAAI,iBAAiB,GAAG,SAAS,CAEvD;IAED,iEAAiE;IAC1D,SAAS,IAAI,OAAO,CAE1B;IAED;;;OAGG;IACU,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CASlC;IAED;;OAEG;IACH,OAAO,CAAC,8BAA8B;IAWtC;;OAEG;IACI,IAAI,IAAI,IAAI,CAKlB;CACF"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/client/http.ts"],"names":[],"mappings":"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"}
@@ -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, fallback, http } from 'viem';
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;
@@ -220,7 +223,7 @@ export class HttpBlobClient {
220
223
  ...ctx
221
224
  };
222
225
  this.log.trace(`Attempting to get slot number for block hash`, consensusCtx);
223
- const slotNumber = await this.getSlotNumber(blockHash);
226
+ const slotNumber = await this.getSlotNumber(blockHash, opts?.parentBeaconBlockRoot, opts?.l1BlockTimestamp);
224
227
  this.log.debug(`Got slot number ${slotNumber} from consensus host for querying blobs`, consensusCtx);
225
228
  if (slotNumber) {
226
229
  let l1ConsensusHostUrl;
@@ -235,7 +238,7 @@ export class HttpBlobClient {
235
238
  l1ConsensusHostUrl,
236
239
  ...ctx
237
240
  });
238
- const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex);
241
+ const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex, getMissingBlobHashes());
239
242
  const result = await fillResults(blobs);
240
243
  this.log.debug(`Got ${blobs.length} blobs from consensus host (total: ${result.length}/${blobHashes.length})`, {
241
244
  slotNumber,
@@ -334,14 +337,14 @@ export class HttpBlobClient {
334
337
  }
335
338
  }
336
339
  async getBlobSidecarFrom(hostUrl, blockHashOrSlot, blobHashes = [], l1ConsensusHostIndex) {
337
- const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
340
+ const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
338
341
  return (await processFetchedBlobs(blobs, blobHashes, this.log)).filter((b)=>b !== undefined);
339
342
  }
340
- async getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex) {
343
+ async getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes) {
341
344
  try {
342
- let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
345
+ let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
343
346
  if (res.ok) {
344
- return parseBlobJsonsFromResponse(await res.json(), this.log);
347
+ return await parseBlobJsonsFromResponse(await res.json(), this.log);
345
348
  }
346
349
  if (res.status === 404 && typeof blockHashOrSlot === 'number') {
347
350
  const latestSlot = await this.getLatestSlotNumber(hostUrl, l1ConsensusHostIndex);
@@ -354,9 +357,9 @@ export class HttpBlobClient {
354
357
  let currentSlot = blockHashOrSlot + 1;
355
358
  while(res.status === 404 && maxRetries > 0 && latestSlot !== undefined && currentSlot <= latestSlot){
356
359
  this.log.debug(`Trying slot ${currentSlot}`);
357
- res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex);
360
+ res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex, blobHashes);
358
361
  if (res.ok) {
359
- return parseBlobJsonsFromResponse(await res.json(), this.log);
362
+ return await parseBlobJsonsFromResponse(await res.json(), this.log);
360
363
  }
361
364
  currentSlot++;
362
365
  maxRetries--;
@@ -373,8 +376,15 @@ export class HttpBlobClient {
373
376
  return [];
374
377
  }
375
378
  }
376
- fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex) {
377
- const baseUrl = `${hostUrl}/eth/v1/beacon/blob_sidecars/${blockHashOrSlot}`;
379
+ fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes) {
380
+ let baseUrl = `${hostUrl}/eth/v1/beacon/blobs/${blockHashOrSlot}`;
381
+ if (blobHashes && blobHashes.length > 0) {
382
+ const params = new URLSearchParams();
383
+ for (const hash of blobHashes){
384
+ params.append('versioned_hashes', `0x${hash.toString('hex')}`);
385
+ }
386
+ baseUrl += `?${params.toString()}`;
387
+ }
378
388
  const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
379
389
  this.log.debug(`Fetching blob sidecar for ${blockHashOrSlot}`, {
380
390
  url,
@@ -420,36 +430,45 @@ export class HttpBlobClient {
420
430
  *
421
431
  * @param blockHash - The block hash
422
432
  * @returns The slot number
423
- */ async getSlotNumber(blockHash) {
433
+ */ async getSlotNumber(blockHash, parentBeaconBlockRoot, l1BlockTimestamp) {
424
434
  const { l1ConsensusHostUrls, l1RpcUrls } = this.config;
425
435
  if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
426
436
  this.log.debug('No consensus host url configured');
427
437
  return undefined;
428
438
  }
429
- if (!l1RpcUrls || l1RpcUrls.length === 0) {
430
- this.log.debug('No execution host url configured');
431
- return undefined;
439
+ // Primary path: compute slot from timestamp if genesis config is cached (no network call needed)
440
+ if (l1BlockTimestamp !== undefined && this.beaconGenesisTime !== undefined && this.beaconSecondsPerSlot !== undefined) {
441
+ const slot = Number((l1BlockTimestamp - this.beaconGenesisTime) / BigInt(this.beaconSecondsPerSlot));
442
+ this.log.debug(`Computed slot ${slot} from L1 block timestamp`, {
443
+ l1BlockTimestamp
444
+ });
445
+ return slot;
432
446
  }
433
- // Ping execution node to get the parentBeaconBlockRoot for this block
434
- let parentBeaconBlockRoot;
435
- const client = createPublicClient({
436
- transport: fallback(l1RpcUrls.map((url)=>http(url, {
437
- batch: false
438
- })))
439
- });
440
- try {
441
- const res = await client.request({
442
- method: 'eth_getBlockByHash',
443
- params: [
444
- blockHash,
445
- /*tx flag*/ false
446
- ]
447
+ if (!parentBeaconBlockRoot) {
448
+ // parentBeaconBlockRoot not provided by caller — fetch it from the execution RPC
449
+ if (!l1RpcUrls || l1RpcUrls.length === 0) {
450
+ this.log.debug('No execution host url configured');
451
+ return undefined;
452
+ }
453
+ const client = createPublicClient({
454
+ transport: makeL1HttpTransport(l1RpcUrls, {
455
+ timeout: this.config.l1HttpTimeoutMS
456
+ })
447
457
  });
448
- if (res.parentBeaconBlockRoot) {
449
- parentBeaconBlockRoot = res.parentBeaconBlockRoot;
458
+ try {
459
+ const res = await client.request({
460
+ method: 'eth_getBlockByHash',
461
+ params: [
462
+ blockHash,
463
+ /*tx flag*/ false
464
+ ]
465
+ });
466
+ if (res.parentBeaconBlockRoot) {
467
+ parentBeaconBlockRoot = res.parentBeaconBlockRoot;
468
+ }
469
+ } catch (err) {
470
+ this.log.error(`Error getting parent beacon block root`, err);
450
471
  }
451
- } catch (err) {
452
- this.log.error(`Error getting parent beacon block root`, err);
453
472
  }
454
473
  if (!parentBeaconBlockRoot) {
455
474
  this.log.error(`No parent beacon block root found for block ${blockHash}`);
@@ -481,8 +500,10 @@ export class HttpBlobClient {
481
500
  }
482
501
  /**
483
502
  * Start the blob client.
484
- * Uploads the initial healthcheck file (awaited) and starts periodic uploads.
503
+ * Fetches and caches beacon genesis config for timestamp-based slot resolution,
504
+ * then uploads the initial healthcheck file (awaited) and starts periodic uploads.
485
505
  */ async start() {
506
+ await this.fetchBeaconConfig();
486
507
  if (!this.fileStoreUploadClient) {
487
508
  return;
488
509
  }
@@ -501,6 +522,40 @@ export class HttpBlobClient {
501
522
  }, intervalMs);
502
523
  }
503
524
  /**
525
+ * Fetches and caches beacon genesis time and slot duration from the first available consensus host.
526
+ * These static values enable timestamp-based slot resolution, eliminating the per-fetch headers call.
527
+ * Logs a warning and leaves fields undefined if all hosts fail, callers fall back gracefully.
528
+ */ async fetchBeaconConfig() {
529
+ const { l1ConsensusHostUrls } = this.config;
530
+ if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
531
+ return;
532
+ }
533
+ for(let i = 0; i < l1ConsensusHostUrls.length; i++){
534
+ try {
535
+ const { url: genesisUrl, ...genesisOptions } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrls[i]}/eth/v1/config/genesis`, this.config, i);
536
+ const { url: specUrl, ...specOptions } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrls[i]}/eth/v1/config/spec`, this.config, i);
537
+ const [genesisRes, specRes] = await Promise.all([
538
+ this.fetch(genesisUrl, genesisOptions),
539
+ this.fetch(specUrl, specOptions)
540
+ ]);
541
+ if (genesisRes.ok && specRes.ok) {
542
+ const genesis = await genesisRes.json();
543
+ const spec = await specRes.json();
544
+ this.beaconGenesisTime = BigInt(genesis.data.genesisTime);
545
+ this.beaconSecondsPerSlot = parseInt(spec.data.secondsPerSlot);
546
+ this.log.debug(`Fetched beacon genesis config`, {
547
+ genesisTime: this.beaconGenesisTime,
548
+ secondsPerSlot: this.beaconSecondsPerSlot
549
+ });
550
+ return;
551
+ }
552
+ } catch (err) {
553
+ this.log.warn(`Failed to fetch beacon config from host ${l1ConsensusHostUrls[i]}`, err);
554
+ }
555
+ }
556
+ this.log.warn('Could not fetch beacon genesis config from any consensus host — will use headers call fallback');
557
+ }
558
+ /**
504
559
  * Stop the blob client, clearing any periodic tasks.
505
560
  */ stop() {
506
561
  if (this.healthcheckUploadIntervalId) {
@@ -509,10 +564,9 @@ export class HttpBlobClient {
509
564
  }
510
565
  }
511
566
  }
512
- function parseBlobJsonsFromResponse(response, logger) {
567
+ async function parseBlobJsonsFromResponse(response, logger) {
513
568
  try {
514
- const blobs = response.data.map(parseBlobJson);
515
- return blobs;
569
+ return await Promise.all(response.data.map(parseBlobJson));
516
570
  } catch (err) {
517
571
  logger.error(`Error parsing blob json from response`, err);
518
572
  return [];
@@ -522,10 +576,9 @@ function parseBlobJsonsFromResponse(response, logger) {
522
576
  // https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getBlobSidecars
523
577
  // Here we attempt to parse the response data to Buffer, and check the lengths (via Blob's constructor), to avoid
524
578
  // throwing an error down the line when calling Blob.fromJson().
525
- function parseBlobJson(data) {
526
- const blobBuffer = Buffer.from(data.blob.slice(2), 'hex');
527
- const commitmentBuffer = Buffer.from(data.kzg_commitment.slice(2), 'hex');
528
- const blob = new Blob(blobBuffer, commitmentBuffer);
579
+ async function parseBlobJson(rawHex) {
580
+ const blobBuffer = Buffer.from(rawHex.slice(2), 'hex');
581
+ const blob = await Blob.fromBlobBuffer(blobBuffer);
529
582
  return blob.toJSON();
530
583
  }
531
584
  // Returns an array that maps each blob hash to the corresponding blob, or undefined if the blob is not found
@@ -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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvY2xpZW50L2ludGVyZmFjZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssRUFBRSxJQUFJLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUU1Qzs7R0FFRztBQUNILE1BQU0sV0FBVyxxQkFBcUI7SUFDcEM7Ozs7O09BS0c7SUFDSCxnQkFBZ0IsQ0FBQyxFQUFFLE9BQU8sQ0FBQztDQUM1QjtBQUVELE1BQU0sV0FBVyxtQkFBbUI7SUFDbEMsMEVBQTBFO0lBQzFFLG9CQUFvQixDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLENBQUM7SUFDdEQscUVBQXFFO0lBQ3JFLGNBQWMsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLFVBQVUsQ0FBQyxFQUFFLE1BQU0sRUFBRSxFQUFFLElBQUksQ0FBQyxFQUFFLHFCQUFxQixHQUFHLE9BQU8sQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBQ3RHLDZFQUE2RTtJQUM3RSxLQUFLLENBQUMsSUFBSSxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDeEIsb0ZBQW9GO0lBQ3BGLFdBQVcsSUFBSSxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDN0IsMERBQTBEO0lBQzFELElBQUksQ0FBQyxJQUFJLElBQUksQ0FBQztJQUNkLGlFQUFpRTtJQUNqRSxTQUFTLElBQUksT0FBTyxDQUFDO0NBQ3RCIn0=
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;CAC5B;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"}
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.993d52e",
3
+ "version": "0.0.1-commit.9ee6fcc6",
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.993d52e",
60
- "@aztec/ethereum": "0.0.1-commit.993d52e",
61
- "@aztec/foundation": "0.0.1-commit.993d52e",
62
- "@aztec/kv-store": "0.0.1-commit.993d52e",
63
- "@aztec/stdlib": "0.0.1-commit.993d52e",
64
- "@aztec/telemetry-client": "0.0.1-commit.993d52e",
59
+ "@aztec/blob-lib": "0.0.1-commit.9ee6fcc6",
60
+ "@aztec/ethereum": "0.0.1-commit.9ee6fcc6",
61
+ "@aztec/foundation": "0.0.1-commit.9ee6fcc6",
62
+ "@aztec/kv-store": "0.0.1-commit.9ee6fcc6",
63
+ "@aztec/stdlib": "0.0.1-commit.9ee6fcc6",
64
+ "@aztec/telemetry-client": "0.0.1-commit.9ee6fcc6",
65
65
  "express": "^4.21.2",
66
66
  "snappy": "^7.2.2",
67
67
  "source-map-support": "^0.5.21",
@@ -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
 
@@ -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, fallback, http } from 'viem';
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: {
@@ -251,7 +257,7 @@ export class HttpBlobClient implements BlobClientInterface {
251
257
  // The beacon api can query by slot number, so we get that first
252
258
  const consensusCtx = { l1ConsensusHostUrls, ...ctx };
253
259
  this.log.trace(`Attempting to get slot number for block hash`, consensusCtx);
254
- const slotNumber = await this.getSlotNumber(blockHash);
260
+ const slotNumber = await this.getSlotNumber(blockHash, opts?.parentBeaconBlockRoot, opts?.l1BlockTimestamp);
255
261
  this.log.debug(`Got slot number ${slotNumber} from consensus host for querying blobs`, consensusCtx);
256
262
 
257
263
  if (slotNumber) {
@@ -268,7 +274,12 @@ export class HttpBlobClient implements BlobClientInterface {
268
274
  l1ConsensusHostUrl,
269
275
  ...ctx,
270
276
  });
271
- const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex);
277
+ const blobs = await this.getBlobsFromHost(
278
+ l1ConsensusHostUrl,
279
+ slotNumber,
280
+ l1ConsensusHostIndex,
281
+ getMissingBlobHashes(),
282
+ );
272
283
  const result = await fillResults(blobs);
273
284
  this.log.debug(
274
285
  `Got ${blobs.length} blobs from consensus host (total: ${result.length}/${blobHashes.length})`,
@@ -387,7 +398,7 @@ export class HttpBlobClient implements BlobClientInterface {
387
398
  blobHashes: Buffer[] = [],
388
399
  l1ConsensusHostIndex?: number,
389
400
  ): Promise<Blob[]> {
390
- const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
401
+ const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
391
402
  return (await processFetchedBlobs(blobs, blobHashes, this.log)).filter((b): b is Blob => b !== undefined);
392
403
  }
393
404
 
@@ -395,11 +406,12 @@ export class HttpBlobClient implements BlobClientInterface {
395
406
  hostUrl: string,
396
407
  blockHashOrSlot: string | number,
397
408
  l1ConsensusHostIndex?: number,
409
+ blobHashes?: Buffer[],
398
410
  ): Promise<BlobJson[]> {
399
411
  try {
400
- let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
412
+ let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
401
413
  if (res.ok) {
402
- return parseBlobJsonsFromResponse(await res.json(), this.log);
414
+ return await parseBlobJsonsFromResponse(await res.json(), this.log);
403
415
  }
404
416
 
405
417
  if (res.status === 404 && typeof blockHashOrSlot === 'number') {
@@ -414,9 +426,9 @@ export class HttpBlobClient implements BlobClientInterface {
414
426
  let currentSlot = blockHashOrSlot + 1;
415
427
  while (res.status === 404 && maxRetries > 0 && latestSlot !== undefined && currentSlot <= latestSlot) {
416
428
  this.log.debug(`Trying slot ${currentSlot}`);
417
- res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex);
429
+ res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex, blobHashes);
418
430
  if (res.ok) {
419
- return parseBlobJsonsFromResponse(await res.json(), this.log);
431
+ return await parseBlobJsonsFromResponse(await res.json(), this.log);
420
432
  }
421
433
  currentSlot++;
422
434
  maxRetries--;
@@ -439,8 +451,17 @@ export class HttpBlobClient implements BlobClientInterface {
439
451
  hostUrl: string,
440
452
  blockHashOrSlot: string | number,
441
453
  l1ConsensusHostIndex?: number,
454
+ blobHashes?: Buffer[],
442
455
  ): Promise<Response> {
443
- const baseUrl = `${hostUrl}/eth/v1/beacon/blob_sidecars/${blockHashOrSlot}`;
456
+ let baseUrl = `${hostUrl}/eth/v1/beacon/blobs/${blockHashOrSlot}`;
457
+
458
+ if (blobHashes && blobHashes.length > 0) {
459
+ const params = new URLSearchParams();
460
+ for (const hash of blobHashes) {
461
+ params.append('versioned_hashes', `0x${hash.toString('hex')}`);
462
+ }
463
+ baseUrl += `?${params.toString()}`;
464
+ }
444
465
 
445
466
  const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
446
467
  this.log.debug(`Fetching blob sidecar for ${blockHashOrSlot}`, { url, ...options });
@@ -482,34 +503,50 @@ export class HttpBlobClient implements BlobClientInterface {
482
503
  * @param blockHash - The block hash
483
504
  * @returns The slot number
484
505
  */
485
- private async getSlotNumber(blockHash: `0x${string}`): Promise<number | undefined> {
506
+ private async getSlotNumber(
507
+ blockHash: `0x${string}`,
508
+ parentBeaconBlockRoot?: string,
509
+ l1BlockTimestamp?: bigint,
510
+ ): Promise<number | undefined> {
486
511
  const { l1ConsensusHostUrls, l1RpcUrls } = this.config;
487
512
  if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
488
513
  this.log.debug('No consensus host url configured');
489
514
  return undefined;
490
515
  }
491
516
 
492
- if (!l1RpcUrls || l1RpcUrls.length === 0) {
493
- this.log.debug('No execution host url configured');
494
- return undefined;
517
+ // Primary path: compute slot from timestamp if genesis config is cached (no network call needed)
518
+ if (
519
+ l1BlockTimestamp !== undefined &&
520
+ this.beaconGenesisTime !== undefined &&
521
+ this.beaconSecondsPerSlot !== undefined
522
+ ) {
523
+ const slot = Number((l1BlockTimestamp - this.beaconGenesisTime) / BigInt(this.beaconSecondsPerSlot));
524
+ this.log.debug(`Computed slot ${slot} from L1 block timestamp`, { l1BlockTimestamp });
525
+ return slot;
495
526
  }
496
527
 
497
- // Ping execution node to get the parentBeaconBlockRoot for this block
498
- let parentBeaconBlockRoot: string | undefined;
499
- const client = createPublicClient({
500
- transport: fallback(l1RpcUrls.map(url => http(url, { batch: false }))),
501
- });
502
- try {
503
- const res: RpcBlock = await client.request({
504
- method: 'eth_getBlockByHash',
505
- params: [blockHash, /*tx flag*/ false],
528
+ if (!parentBeaconBlockRoot) {
529
+ // parentBeaconBlockRoot not provided by caller — fetch it from the execution RPC
530
+ if (!l1RpcUrls || l1RpcUrls.length === 0) {
531
+ this.log.debug('No execution host url configured');
532
+ return undefined;
533
+ }
534
+
535
+ const client = createPublicClient({
536
+ transport: makeL1HttpTransport(l1RpcUrls, { timeout: this.config.l1HttpTimeoutMS }),
506
537
  });
538
+ try {
539
+ const res: RpcBlock = await client.request({
540
+ method: 'eth_getBlockByHash',
541
+ params: [blockHash, /*tx flag*/ false],
542
+ });
507
543
 
508
- if (res.parentBeaconBlockRoot) {
509
- parentBeaconBlockRoot = res.parentBeaconBlockRoot;
544
+ if (res.parentBeaconBlockRoot) {
545
+ parentBeaconBlockRoot = res.parentBeaconBlockRoot;
546
+ }
547
+ } catch (err) {
548
+ this.log.error(`Error getting parent beacon block root`, err);
510
549
  }
511
- } catch (err) {
512
- this.log.error(`Error getting parent beacon block root`, err);
513
550
  }
514
551
 
515
552
  if (!parentBeaconBlockRoot) {
@@ -555,9 +592,12 @@ export class HttpBlobClient implements BlobClientInterface {
555
592
 
556
593
  /**
557
594
  * Start the blob client.
558
- * Uploads the initial healthcheck file (awaited) and starts periodic uploads.
595
+ * Fetches and caches beacon genesis config for timestamp-based slot resolution,
596
+ * then uploads the initial healthcheck file (awaited) and starts periodic uploads.
559
597
  */
560
598
  public async start(): Promise<void> {
599
+ await this.fetchBeaconConfig();
600
+
561
601
  if (!this.fileStoreUploadClient) {
562
602
  return;
563
603
  }
@@ -582,6 +622,53 @@ export class HttpBlobClient implements BlobClientInterface {
582
622
  }, intervalMs);
583
623
  }
584
624
 
625
+ /**
626
+ * Fetches and caches beacon genesis time and slot duration from the first available consensus host.
627
+ * These static values enable timestamp-based slot resolution, eliminating the per-fetch headers call.
628
+ * Logs a warning and leaves fields undefined if all hosts fail, callers fall back gracefully.
629
+ */
630
+ private async fetchBeaconConfig(): Promise<void> {
631
+ const { l1ConsensusHostUrls } = this.config;
632
+ if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
633
+ return;
634
+ }
635
+
636
+ for (let i = 0; i < l1ConsensusHostUrls.length; i++) {
637
+ try {
638
+ const { url: genesisUrl, ...genesisOptions } = getBeaconNodeFetchOptions(
639
+ `${l1ConsensusHostUrls[i]}/eth/v1/config/genesis`,
640
+ this.config,
641
+ i,
642
+ );
643
+ const { url: specUrl, ...specOptions } = getBeaconNodeFetchOptions(
644
+ `${l1ConsensusHostUrls[i]}/eth/v1/config/spec`,
645
+ this.config,
646
+ i,
647
+ );
648
+
649
+ const [genesisRes, specRes] = await Promise.all([
650
+ this.fetch(genesisUrl, genesisOptions),
651
+ this.fetch(specUrl, specOptions),
652
+ ]);
653
+
654
+ if (genesisRes.ok && specRes.ok) {
655
+ const genesis = await genesisRes.json();
656
+ const spec = await specRes.json();
657
+ this.beaconGenesisTime = BigInt(genesis.data.genesisTime);
658
+ this.beaconSecondsPerSlot = parseInt(spec.data.secondsPerSlot);
659
+ this.log.debug(`Fetched beacon genesis config`, {
660
+ genesisTime: this.beaconGenesisTime,
661
+ secondsPerSlot: this.beaconSecondsPerSlot,
662
+ });
663
+ return;
664
+ }
665
+ } catch (err) {
666
+ this.log.warn(`Failed to fetch beacon config from host ${l1ConsensusHostUrls[i]}`, err);
667
+ }
668
+ }
669
+ this.log.warn('Could not fetch beacon genesis config from any consensus host — will use headers call fallback');
670
+ }
671
+
585
672
  /**
586
673
  * Stop the blob client, clearing any periodic tasks.
587
674
  */
@@ -593,10 +680,9 @@ export class HttpBlobClient implements BlobClientInterface {
593
680
  }
594
681
  }
595
682
 
596
- function parseBlobJsonsFromResponse(response: any, logger: Logger): BlobJson[] {
683
+ async function parseBlobJsonsFromResponse(response: any, logger: Logger): Promise<BlobJson[]> {
597
684
  try {
598
- const blobs = response.data.map(parseBlobJson);
599
- return blobs;
685
+ return await Promise.all((response.data as string[]).map(parseBlobJson));
600
686
  } catch (err) {
601
687
  logger.error(`Error parsing blob json from response`, err);
602
688
  return [];
@@ -607,10 +693,9 @@ function parseBlobJsonsFromResponse(response: any, logger: Logger): BlobJson[] {
607
693
  // https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getBlobSidecars
608
694
  // Here we attempt to parse the response data to Buffer, and check the lengths (via Blob's constructor), to avoid
609
695
  // throwing an error down the line when calling Blob.fromJson().
610
- function parseBlobJson(data: any): BlobJson {
611
- const blobBuffer = Buffer.from(data.blob.slice(2), 'hex');
612
- const commitmentBuffer = Buffer.from(data.kzg_commitment.slice(2), 'hex');
613
- const blob = new Blob(blobBuffer, commitmentBuffer);
696
+ async function parseBlobJson(rawHex: string): Promise<BlobJson> {
697
+ const blobBuffer = Buffer.from(rawHex.slice(2), 'hex');
698
+ const blob = await Blob.fromBlobBuffer(blobBuffer);
614
699
  return blob.toJSON();
615
700
  }
616
701
 
@@ -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 {