@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.
@@ -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,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;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":"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"}
@@ -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;
@@ -66,22 +69,50 @@ export class HttpBlobClient {
66
69
  l1ConsensusHostUrls,
67
70
  archiveUrl
68
71
  });
69
- let successfulSourceCount = 0;
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.info(`L1 consensus host is reachable`, {
82
+ if (!res.ok) {
83
+ this.log.error(`Failure reaching L1 consensus host: ${res.statusText} (${res.status})`, {
78
84
  l1ConsensusHostUrl
79
85
  });
80
- successfulSourceCount++;
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.error(`Failure reaching L1 consensus host: ${res.statusText} (${res.status})`, {
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
- successfulSourceCount++;
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
- successfulSourceCount++;
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('No blob sources are reachable');
167
+ this.log.warn(summary);
135
168
  } else {
136
- throw new Error('No blob sources are reachable');
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
- const baseUrl = `${hostUrl}/eth/v1/beacon/blob_sidecars/${blockHashOrSlot}`;
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 (!l1RpcUrls || l1RpcUrls.length === 0) {
430
- this.log.debug('No execution host url configured');
431
- return undefined;
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
- // 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
- ]
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
- if (res.parentBeaconBlockRoot) {
449
- parentBeaconBlockRoot = res.parentBeaconBlockRoot;
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
- * Uploads the initial healthcheck file (awaited) and starts periodic uploads.
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
- const blobs = response.data.map(parseBlobJson);
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(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);
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,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.cd76b27",
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.cd76b27",
60
- "@aztec/ethereum": "0.0.1-commit.cd76b27",
61
- "@aztec/foundation": "0.0.1-commit.cd76b27",
62
- "@aztec/kv-store": "0.0.1-commit.cd76b27",
63
- "@aztec/stdlib": "0.0.1-commit.cd76b27",
64
- "@aztec/telemetry-client": "0.0.1-commit.cd76b27",
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",
@@ -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: {
@@ -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 successfulSourceCount = 0;
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
- successfulSourceCount++;
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
- successfulSourceCount++;
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('No blob sources are reachable');
188
+ this.log.warn(summary);
151
189
  } else {
152
- throw new Error('No blob sources are reachable');
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(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex);
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
- const baseUrl = `${hostUrl}/eth/v1/beacon/blob_sidecars/${blockHashOrSlot}`;
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(blockHash: `0x${string}`): Promise<number | undefined> {
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 (!l1RpcUrls || l1RpcUrls.length === 0) {
493
- this.log.debug('No execution host url configured');
494
- return undefined;
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
- // 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],
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
- if (res.parentBeaconBlockRoot) {
509
- parentBeaconBlockRoot = res.parentBeaconBlockRoot;
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
- * Uploads the initial healthcheck file (awaited) and starts periodic uploads.
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
- const blobs = response.data.map(parseBlobJson);
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(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);
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
 
@@ -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 {