@aztec/blob-client 0.0.1-commit.ffe5b04ea → 0.0.1-commit.fff30aa

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,10 +14,6 @@ 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?;
21
17
  constructor(config?: BlobClientConfig, opts?: {
22
18
  logger?: Logger;
23
19
  archiveClient?: BlobArchiveClient;
@@ -58,7 +54,7 @@ export declare class HttpBlobClient implements BlobClientInterface {
58
54
  getBlobSidecar(blockHash: `0x${string}`, blobHashes: Buffer[], opts?: GetBlobSidecarOptions): Promise<Blob[]>;
59
55
  private tryFileStores;
60
56
  getBlobSidecarFrom(hostUrl: string, blockHashOrSlot: string | number, blobHashes?: Buffer[], l1ConsensusHostIndex?: number): Promise<Blob[]>;
61
- getBlobsFromHost(hostUrl: string, blockHashOrSlot: string | number, l1ConsensusHostIndex?: number, blobHashes?: Buffer[]): Promise<BlobJson[]>;
57
+ getBlobsFromHost(hostUrl: string, blockHashOrSlot: string | number, l1ConsensusHostIndex?: number): Promise<BlobJson[]>;
62
58
  private fetchBlobSidecars;
63
59
  private getLatestSlotNumber;
64
60
  private getSlotNumber;
@@ -68,18 +64,16 @@ export declare class HttpBlobClient implements BlobClientInterface {
68
64
  canUpload(): boolean;
69
65
  /**
70
66
  * Start the blob client.
71
- * Fetches and caches beacon genesis config for timestamp-based slot resolution,
72
- * then uploads the initial healthcheck file (awaited) and starts periodic uploads.
67
+ * Uploads the initial healthcheck file (awaited) and starts periodic uploads.
73
68
  */
74
69
  start(): Promise<void>;
75
70
  /**
76
71
  * Start periodic healthcheck upload to the file store to ensure it remains available even if accidentally deleted.
77
72
  */
78
73
  private startPeriodicHealthcheckUpload;
79
- private fetchBeaconConfig;
80
74
  /**
81
75
  * Stop the blob client, clearing any periodic tasks.
82
76
  */
83
77
  stop(): void;
84
78
  }
85
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaHR0cC5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2NsaWVudC9odHRwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxJQUFJLEVBQUUsS0FBSyxRQUFRLEVBQStCLE1BQU0saUJBQWlCLENBQUM7QUFFbkYsT0FBTyxFQUFFLEtBQUssTUFBTSxFQUFnQixNQUFNLHVCQUF1QixDQUFDO0FBT2xFLE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDakUsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSx1Q0FBdUMsQ0FBQztBQUVqRixPQUFPLEVBQUUsS0FBSyxnQkFBZ0IsRUFBOEIsTUFBTSxhQUFhLENBQUM7QUFDaEYsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVqRixxQkFBYSxjQUFlLFlBQVcsbUJBQW1CO0lBa0J0RCxPQUFPLENBQUMsUUFBUSxDQUFDLElBQUk7SUFqQnZCLFNBQVMsQ0FBQyxRQUFRLENBQUMsR0FBRyxFQUFFLE1BQU0sQ0FBQztJQUMvQixTQUFTLENBQUMsUUFBUSxDQUFDLE1BQU0sRUFBRSxnQkFBZ0IsQ0FBQztJQUM1QyxTQUFTLENBQUMsUUFBUSxDQUFDLGFBQWEsRUFBRSxpQkFBaUIsR0FBRyxTQUFTLENBQUM7SUFDaEUsU0FBUyxDQUFDLFFBQVEsQ0FBQyxLQUFLLEVBQUUsT0FBTyxLQUFLLENBQUM7SUFDdkMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxnQkFBZ0IsRUFBRSxtQkFBbUIsRUFBRSxDQUFDO0lBQzNELFNBQVMsQ0FBQyxRQUFRLENBQUMscUJBQXFCLEVBQUUsbUJBQW1CLEdBQUcsU0FBUyxDQUFDO0lBRTFFLE9BQU8sQ0FBQyxRQUFRLENBQVM7SUFDekIsT0FBTyxDQUFDLDJCQUEyQixDQUFDLENBQWlCO0lBRXJELHNGQUFzRjtJQUN0RixPQUFPLENBQUMsaUJBQWlCLENBQUMsQ0FBUztJQUNuQyx1RUFBdUU7SUFDdkUsT0FBTyxDQUFDLG9CQUFvQixDQUFDLENBQVM7SUFFdEMsWUFDRSxNQUFNLENBQUMsRUFBRSxnQkFBZ0IsRUFDUixJQUFJLEdBQUU7UUFDckIsTUFBTSxDQUFDLEVBQUUsTUFBTSxDQUFDO1FBQ2hCLGFBQWEsQ0FBQyxFQUFFLGlCQUFpQixDQUFDO1FBQ2xDLGdCQUFnQixDQUFDLEVBQUUsbUJBQW1CLEVBQUUsQ0FBQztRQUN6QyxxQkFBcUIsQ0FBQyxFQUFFLG1CQUFtQixDQUFDO1FBQzVDLHlFQUF5RTtRQUN6RSxjQUFjLENBQUMsRUFBRSxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsS0FBSyxJQUFJLENBQUM7S0FDckMsRUF3QlA7SUFFRDs7O09BR0c7SUFDSCxPQUFPLENBQUMsc0JBQXNCO0lBVTlCOzs7OztPQUtHO0lBQ0ksV0FBVyxDQUFDLEtBQUssRUFBRSxPQUFPLEdBQUcsSUFBSSxDQUd2QztJQUVZLFdBQVcsa0JBb0V2QjtJQUVZLG9CQUFvQixDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLENBbUJqRTtJQUVEOzs7Ozs7Ozs7Ozs7OztPQWNHO0lBQ1UsY0FBYyxDQUN6QixTQUFTLEVBQUUsS0FBSyxNQUFNLEVBQUUsRUFDeEIsVUFBVSxFQUFFLE1BQU0sRUFBRSxFQUNwQixJQUFJLENBQUMsRUFBRSxxQkFBcUIsR0FDM0IsT0FBTyxDQUFDLElBQUksRUFBRSxDQUFDLENBa0pqQjtZQVFhLGFBQWE7SUFzQ2Qsa0JBQWtCLENBQzdCLE9BQU8sRUFBRSxNQUFNLEVBQ2YsZUFBZSxFQUFFLE1BQU0sR0FBRyxNQUFNLEVBQ2hDLFVBQVUsR0FBRSxNQUFNLEVBQU8sRUFDekIsb0JBQW9CLENBQUMsRUFBRSxNQUFNLEdBQzVCLE9BQU8sQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUdqQjtJQUVZLGdCQUFnQixDQUMzQixPQUFPLEVBQUUsTUFBTSxFQUNmLGVBQWUsRUFBRSxNQUFNLEdBQUcsTUFBTSxFQUNoQyxvQkFBb0IsQ0FBQyxFQUFFLE1BQU0sRUFDN0IsVUFBVSxDQUFDLEVBQUUsTUFBTSxFQUFFLEdBQ3BCLE9BQU8sQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQXNDckI7SUFFRCxPQUFPLENBQUMsaUJBQWlCO1lBcUJYLG1CQUFtQjtZQW1DbkIsYUFBYTtJQTZFM0Isc0NBQXNDO0lBQy9CLGdCQUFnQixJQUFJLGlCQUFpQixHQUFHLFNBQVMsQ0FFdkQ7SUFFRCxpRUFBaUU7SUFDMUQsU0FBUyxJQUFJLE9BQU8sQ0FFMUI7SUFFRDs7OztPQUlHO0lBQ1UsS0FBSyxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FXbEM7SUFFRDs7T0FFRztJQUNILE9BQU8sQ0FBQyw4QkFBOEI7WUFnQnhCLGlCQUFpQjtJQTBDL0I7O09BRUc7SUFDSSxJQUFJLElBQUksSUFBSSxDQUtsQjtDQUNGIn0=
79
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaHR0cC5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2NsaWVudC9odHRwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxJQUFJLEVBQUUsS0FBSyxRQUFRLEVBQStCLE1BQU0saUJBQWlCLENBQUM7QUFHbkYsT0FBTyxFQUFFLEtBQUssTUFBTSxFQUFnQixNQUFNLHVCQUF1QixDQUFDO0FBT2xFLE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDakUsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSx1Q0FBdUMsQ0FBQztBQUVqRixPQUFPLEVBQUUsS0FBSyxnQkFBZ0IsRUFBOEIsTUFBTSxhQUFhLENBQUM7QUFDaEYsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVqRixxQkFBYSxjQUFlLFlBQVcsbUJBQW1CO0lBYXRELE9BQU8sQ0FBQyxRQUFRLENBQUMsSUFBSTtJQVp2QixTQUFTLENBQUMsUUFBUSxDQUFDLEdBQUcsRUFBRSxNQUFNLENBQUM7SUFDL0IsU0FBUyxDQUFDLFFBQVEsQ0FBQyxNQUFNLEVBQUUsZ0JBQWdCLENBQUM7SUFDNUMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxhQUFhLEVBQUUsaUJBQWlCLEdBQUcsU0FBUyxDQUFDO0lBQ2hFLFNBQVMsQ0FBQyxRQUFRLENBQUMsS0FBSyxFQUFFLE9BQU8sS0FBSyxDQUFDO0lBQ3ZDLFNBQVMsQ0FBQyxRQUFRLENBQUMsZ0JBQWdCLEVBQUUsbUJBQW1CLEVBQUUsQ0FBQztJQUMzRCxTQUFTLENBQUMsUUFBUSxDQUFDLHFCQUFxQixFQUFFLG1CQUFtQixHQUFHLFNBQVMsQ0FBQztJQUUxRSxPQUFPLENBQUMsUUFBUSxDQUFTO0lBQ3pCLE9BQU8sQ0FBQywyQkFBMkIsQ0FBQyxDQUFpQjtJQUVyRCxZQUNFLE1BQU0sQ0FBQyxFQUFFLGdCQUFnQixFQUNSLElBQUksR0FBRTtRQUNyQixNQUFNLENBQUMsRUFBRSxNQUFNLENBQUM7UUFDaEIsYUFBYSxDQUFDLEVBQUUsaUJBQWlCLENBQUM7UUFDbEMsZ0JBQWdCLENBQUMsRUFBRSxtQkFBbUIsRUFBRSxDQUFDO1FBQ3pDLHFCQUFxQixDQUFDLEVBQUUsbUJBQW1CLENBQUM7UUFDNUMseUVBQXlFO1FBQ3pFLGNBQWMsQ0FBQyxFQUFFLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxLQUFLLElBQUksQ0FBQztLQUNyQyxFQXdCUDtJQUVEOzs7T0FHRztJQUNILE9BQU8sQ0FBQyxzQkFBc0I7SUFVOUI7Ozs7O09BS0c7SUFDSSxXQUFXLENBQUMsS0FBSyxFQUFFLE9BQU8sR0FBRyxJQUFJLENBR3ZDO0lBRVksV0FBVyxrQkF3R3ZCO0lBRVksb0JBQW9CLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxHQUFHLE9BQU8sQ0FBQyxPQUFPLENBQUMsQ0FtQmpFO0lBRUQ7Ozs7Ozs7Ozs7Ozs7O09BY0c7SUFDVSxjQUFjLENBQ3pCLFNBQVMsRUFBRSxLQUFLLE1BQU0sRUFBRSxFQUN4QixVQUFVLEVBQUUsTUFBTSxFQUFFLEVBQ3BCLElBQUksQ0FBQyxFQUFFLHFCQUFxQixHQUMzQixPQUFPLENBQUMsSUFBSSxFQUFFLENBQUMsQ0E2SWpCO1lBUWEsYUFBYTtJQXNDZCxrQkFBa0IsQ0FDN0IsT0FBTyxFQUFFLE1BQU0sRUFDZixlQUFlLEVBQUUsTUFBTSxHQUFHLE1BQU0sRUFDaEMsVUFBVSxHQUFFLE1BQU0sRUFBTyxFQUN6QixvQkFBb0IsQ0FBQyxFQUFFLE1BQU0sR0FDNUIsT0FBTyxDQUFDLElBQUksRUFBRSxDQUFDLENBR2pCO0lBRVksZ0JBQWdCLENBQzNCLE9BQU8sRUFBRSxNQUFNLEVBQ2YsZUFBZSxFQUFFLE1BQU0sR0FBRyxNQUFNLEVBQ2hDLG9CQUFvQixDQUFDLEVBQUUsTUFBTSxHQUM1QixPQUFPLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FzQ3JCO0lBRUQsT0FBTyxDQUFDLGlCQUFpQjtZQVlYLG1CQUFtQjtZQW1DbkIsYUFBYTtJQTZEM0Isc0NBQXNDO0lBQy9CLGdCQUFnQixJQUFJLGlCQUFpQixHQUFHLFNBQVMsQ0FFdkQ7SUFFRCxpRUFBaUU7SUFDMUQsU0FBUyxJQUFJLE9BQU8sQ0FFMUI7SUFFRDs7O09BR0c7SUFDVSxLQUFLLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQVNsQztJQUVEOztPQUVHO0lBQ0gsT0FBTyxDQUFDLDhCQUE4QjtJQVd0Qzs7T0FFRztJQUNJLElBQUksSUFBSSxJQUFJLENBS2xCO0NBQ0YifQ==
@@ -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;IAkBtD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAjBvB,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAC/B,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAC5C,SAAS,CAAC,QAAQ,CAAC,aAAa,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAChE,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC;IACvC,SAAS,CAAC,QAAQ,CAAC,gBAAgB,EAAE,mBAAmB,EAAE,CAAC;IAC3D,SAAS,CAAC,QAAQ,CAAC,qBAAqB,EAAE,mBAAmB,GAAG,SAAS,CAAC;IAE1E,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,2BAA2B,CAAC,CAAiB;IAErD,sFAAsF;IACtF,OAAO,CAAC,iBAAiB,CAAC,CAAS;IACnC,uEAAuE;IACvE,OAAO,CAAC,oBAAoB,CAAC,CAAS;IAEtC,YACE,MAAM,CAAC,EAAE,gBAAgB,EACR,IAAI,GAAE;QACrB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,aAAa,CAAC,EAAE,iBAAiB,CAAC;QAClC,gBAAgB,CAAC,EAAE,mBAAmB,EAAE,CAAC;QACzC,qBAAqB,CAAC,EAAE,mBAAmB,CAAC;QAC5C,yEAAyE;QACzE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;KACrC,EAwBP;IAED;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAU9B;;;;;OAKG;IACI,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAGvC;IAEY,WAAW,kBAoEvB;IAEY,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAmBjE;IAED;;;;;;;;;;;;;;OAcG;IACU,cAAc,CACzB,SAAS,EAAE,KAAK,MAAM,EAAE,EACxB,UAAU,EAAE,MAAM,EAAE,EACpB,IAAI,CAAC,EAAE,qBAAqB,GAC3B,OAAO,CAAC,IAAI,EAAE,CAAC,CAkJjB;YAQa,aAAa;IAsCd,kBAAkB,CAC7B,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GAAG,MAAM,EAChC,UAAU,GAAE,MAAM,EAAO,EACzB,oBAAoB,CAAC,EAAE,MAAM,GAC5B,OAAO,CAAC,IAAI,EAAE,CAAC,CAGjB;IAEY,gBAAgB,CAC3B,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GAAG,MAAM,EAChC,oBAAoB,CAAC,EAAE,MAAM,EAC7B,UAAU,CAAC,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,QAAQ,EAAE,CAAC,CAsCrB;IAED,OAAO,CAAC,iBAAiB;YAqBX,mBAAmB;YAmCnB,aAAa;IA6E3B,sCAAsC;IAC/B,gBAAgB,IAAI,iBAAiB,GAAG,SAAS,CAEvD;IAED,iEAAiE;IAC1D,SAAS,IAAI,OAAO,CAE1B;IAED;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAWlC;IAED;;OAEG;IACH,OAAO,CAAC,8BAA8B;YAgBxB,iBAAiB;IA0C/B;;OAEG;IACI,IAAI,IAAI,IAAI,CAKlB;CACF"}
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;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,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,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,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,8 +18,6 @@ export class HttpBlobClient {
17
18
  fileStoreUploadClient;
18
19
  disabled;
19
20
  healthcheckUploadIntervalId;
20
- /** Cached beacon genesis time (seconds since Unix epoch). Fetched once at startup. */ beaconGenesisTime;
21
- /** Cached beacon slot duration in seconds. Fetched once at startup. */ beaconSecondsPerSlot;
22
21
  constructor(config, opts = {}){
23
22
  this.opts = opts;
24
23
  this.disabled = false;
@@ -68,22 +67,50 @@ export class HttpBlobClient {
68
67
  l1ConsensusHostUrls,
69
68
  archiveUrl
70
69
  });
71
- let successfulSourceCount = 0;
70
+ let consensusSuperNodes = 0;
71
+ let consensusNonSuperNodes = 0;
72
+ let archiveSources = 0;
73
+ let blobSinks = 0;
72
74
  if (l1ConsensusHostUrls && l1ConsensusHostUrls.length > 0) {
73
75
  for(let l1ConsensusHostIndex = 0; l1ConsensusHostIndex < l1ConsensusHostUrls.length; l1ConsensusHostIndex++){
74
76
  const l1ConsensusHostUrl = l1ConsensusHostUrls[l1ConsensusHostIndex];
75
77
  try {
76
- const { url, ...options } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrl}/eth/v1/beacon/headers`, this.config, l1ConsensusHostIndex);
78
+ const { url, ...options } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrl}/eth/v1/beacon/headers/head`, this.config, l1ConsensusHostIndex);
77
79
  const res = await this.fetch(url, options);
78
- if (res.ok) {
79
- this.log.info(`L1 consensus host is reachable`, {
80
+ if (!res.ok) {
81
+ this.log.error(`Failure reaching L1 consensus host: ${res.statusText} (${res.status})`, {
80
82
  l1ConsensusHostUrl
81
83
  });
82
- successfulSourceCount++;
84
+ continue;
85
+ }
86
+ this.log.info(`L1 consensus host is reachable`, {
87
+ l1ConsensusHostUrl
88
+ });
89
+ // Check if the host serves blob sidecars (supernode/semi-supernode).
90
+ // Post-Fusaka (PeerDAS), non-supernode beacon nodes no longer serve the
91
+ // blob sidecar endpoint. A 200 response (even with an empty data array
92
+ // for a slot with no blobs) means the node supports serving blob sidecars.
93
+ const body = await res.json();
94
+ const headSlot = body?.data?.header?.message?.slot;
95
+ if (headSlot) {
96
+ const { url: blobUrl, ...blobOptions } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrl}/eth/v1/beacon/blobs/${headSlot}`, this.config, l1ConsensusHostIndex);
97
+ const blobRes = await this.fetch(blobUrl, blobOptions);
98
+ if (blobRes.ok) {
99
+ this.log.info(`L1 consensus host serves blob sidecars (supernode)`, {
100
+ l1ConsensusHostUrl
101
+ });
102
+ consensusSuperNodes++;
103
+ } else {
104
+ this.log.info(`L1 consensus host does not serve blob sidecars`, {
105
+ l1ConsensusHostUrl
106
+ });
107
+ consensusNonSuperNodes++;
108
+ }
83
109
  } else {
84
- this.log.error(`Failure reaching L1 consensus host: ${res.statusText} (${res.status})`, {
110
+ this.log.info(`L1 consensus host is reachable but could not determine head slot`, {
85
111
  l1ConsensusHostUrl
86
112
  });
113
+ consensusNonSuperNodes++;
87
114
  }
88
115
  } catch (err) {
89
116
  this.log.error(`Error reaching L1 consensus host`, err, {
@@ -91,8 +118,6 @@ export class HttpBlobClient {
91
118
  });
92
119
  }
93
120
  }
94
- } else {
95
- this.log.warn('No L1 consensus host urls configured');
96
121
  }
97
122
  if (this.archiveClient) {
98
123
  try {
@@ -101,14 +126,12 @@ export class HttpBlobClient {
101
126
  latest,
102
127
  archiveUrl
103
128
  });
104
- successfulSourceCount++;
129
+ archiveSources++;
105
130
  } catch (err) {
106
131
  this.log.error(`Error reaching archive client`, err, {
107
132
  archiveUrl
108
133
  });
109
134
  }
110
- } else {
111
- this.log.warn('No archive client configured');
112
135
  }
113
136
  if (this.fileStoreClients.length > 0) {
114
137
  for (const fileStoreClient of this.fileStoreClients){
@@ -118,7 +141,7 @@ export class HttpBlobClient {
118
141
  this.log.info(`FileStore is reachable`, {
119
142
  url: fileStoreClient.getBaseUrl()
120
143
  });
121
- successfulSourceCount++;
144
+ blobSinks++;
122
145
  } else {
123
146
  this.log.warn(`FileStore is not accessible`, {
124
147
  url: fileStoreClient.getBaseUrl()
@@ -131,12 +154,22 @@ export class HttpBlobClient {
131
154
  }
132
155
  }
133
156
  }
157
+ // Emit a single summary after validating all sources
158
+ const successfulSourceCount = consensusSuperNodes + archiveSources + blobSinks;
159
+ let summary = `Blob client running with consensusSuperNodes=${consensusSuperNodes} archiveSources=${archiveSources} blobSinks=${blobSinks}`;
160
+ if (consensusNonSuperNodes > 0) {
161
+ summary += `. ${consensusNonSuperNodes} consensus client(s) ignored because they are not running in supernode or semi-supernode mode`;
162
+ }
134
163
  if (successfulSourceCount === 0) {
135
164
  if (this.config.blobAllowEmptySources) {
136
- this.log.warn('No blob sources are reachable');
165
+ this.log.warn(summary);
137
166
  } else {
138
- throw new Error('No blob sources are reachable');
167
+ throw new Error(summary);
139
168
  }
169
+ } else if (consensusSuperNodes === 0) {
170
+ this.log.warn(summary);
171
+ } else {
172
+ this.log.info(summary);
140
173
  }
141
174
  }
142
175
  async sendBlobsToFilestore(blobs) {
@@ -222,7 +255,7 @@ export class HttpBlobClient {
222
255
  ...ctx
223
256
  };
224
257
  this.log.trace(`Attempting to get slot number for block hash`, consensusCtx);
225
- const slotNumber = await this.getSlotNumber(blockHash, opts?.parentBeaconBlockRoot, opts?.l1BlockTimestamp);
258
+ const slotNumber = await this.getSlotNumber(blockHash);
226
259
  this.log.debug(`Got slot number ${slotNumber} from consensus host for querying blobs`, consensusCtx);
227
260
  if (slotNumber) {
228
261
  let l1ConsensusHostUrl;
@@ -237,7 +270,7 @@ export class HttpBlobClient {
237
270
  l1ConsensusHostUrl,
238
271
  ...ctx
239
272
  });
240
- const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex, getMissingBlobHashes());
273
+ const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex);
241
274
  const result = await fillResults(blobs);
242
275
  this.log.debug(`Got ${blobs.length} blobs from consensus host (total: ${result.length}/${blobHashes.length})`, {
243
276
  slotNumber,
@@ -336,14 +369,14 @@ export class HttpBlobClient {
336
369
  }
337
370
  }
338
371
  async getBlobSidecarFrom(hostUrl, blockHashOrSlot, blobHashes = [], l1ConsensusHostIndex) {
339
- const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
372
+ const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
340
373
  return (await processFetchedBlobs(blobs, blobHashes, this.log)).filter((b)=>b !== undefined);
341
374
  }
342
- async getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes) {
375
+ async getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex) {
343
376
  try {
344
- let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
377
+ let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
345
378
  if (res.ok) {
346
- return await parseBlobJsonsFromResponse(await res.json(), this.log);
379
+ return parseBlobJsonsFromResponse(await res.json(), this.log);
347
380
  }
348
381
  if (res.status === 404 && typeof blockHashOrSlot === 'number') {
349
382
  const latestSlot = await this.getLatestSlotNumber(hostUrl, l1ConsensusHostIndex);
@@ -356,9 +389,9 @@ export class HttpBlobClient {
356
389
  let currentSlot = blockHashOrSlot + 1;
357
390
  while(res.status === 404 && maxRetries > 0 && latestSlot !== undefined && currentSlot <= latestSlot){
358
391
  this.log.debug(`Trying slot ${currentSlot}`);
359
- res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex, blobHashes);
392
+ res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex);
360
393
  if (res.ok) {
361
- return await parseBlobJsonsFromResponse(await res.json(), this.log);
394
+ return parseBlobJsonsFromResponse(await res.json(), this.log);
362
395
  }
363
396
  currentSlot++;
364
397
  maxRetries--;
@@ -375,15 +408,8 @@ export class HttpBlobClient {
375
408
  return [];
376
409
  }
377
410
  }
378
- fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes) {
379
- let baseUrl = `${hostUrl}/eth/v1/beacon/blobs/${blockHashOrSlot}`;
380
- if (blobHashes && blobHashes.length > 0) {
381
- const params = new URLSearchParams();
382
- for (const hash of blobHashes){
383
- params.append('versioned_hashes', `0x${hash.toString('hex')}`);
384
- }
385
- baseUrl += `?${params.toString()}`;
386
- }
411
+ fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex) {
412
+ const baseUrl = `${hostUrl}/eth/v1/beacon/blob_sidecars/${blockHashOrSlot}`;
387
413
  const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
388
414
  this.log.debug(`Fetching blob sidecar for ${blockHashOrSlot}`, {
389
415
  url,
@@ -429,45 +455,36 @@ export class HttpBlobClient {
429
455
  *
430
456
  * @param blockHash - The block hash
431
457
  * @returns The slot number
432
- */ async getSlotNumber(blockHash, parentBeaconBlockRoot, l1BlockTimestamp) {
458
+ */ async getSlotNumber(blockHash) {
433
459
  const { l1ConsensusHostUrls, l1RpcUrls } = this.config;
434
460
  if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
435
461
  this.log.debug('No consensus host url configured');
436
462
  return undefined;
437
463
  }
438
- // Primary path: compute slot from timestamp if genesis config is cached (no network call needed)
439
- if (l1BlockTimestamp !== undefined && this.beaconGenesisTime !== undefined && this.beaconSecondsPerSlot !== undefined) {
440
- const slot = Number((l1BlockTimestamp - this.beaconGenesisTime) / BigInt(this.beaconSecondsPerSlot));
441
- this.log.debug(`Computed slot ${slot} from L1 block timestamp`, {
442
- l1BlockTimestamp
443
- });
444
- return slot;
464
+ if (!l1RpcUrls || l1RpcUrls.length === 0) {
465
+ this.log.debug('No execution host url configured');
466
+ return undefined;
445
467
  }
446
- if (!parentBeaconBlockRoot) {
447
- // parentBeaconBlockRoot not provided by caller — fetch it from the execution RPC
448
- if (!l1RpcUrls || l1RpcUrls.length === 0) {
449
- this.log.debug('No execution host url configured');
450
- return undefined;
451
- }
452
- const client = createPublicClient({
453
- transport: fallback(l1RpcUrls.map((url)=>http(url, {
454
- batch: false
455
- })))
468
+ // Ping execution node to get the parentBeaconBlockRoot for this block
469
+ let parentBeaconBlockRoot;
470
+ const client = createPublicClient({
471
+ transport: makeL1HttpTransport(l1RpcUrls, {
472
+ timeout: this.config.l1HttpTimeoutMS
473
+ })
474
+ });
475
+ try {
476
+ const res = await client.request({
477
+ method: 'eth_getBlockByHash',
478
+ params: [
479
+ blockHash,
480
+ /*tx flag*/ false
481
+ ]
456
482
  });
457
- try {
458
- const res = await client.request({
459
- method: 'eth_getBlockByHash',
460
- params: [
461
- blockHash,
462
- /*tx flag*/ false
463
- ]
464
- });
465
- if (res.parentBeaconBlockRoot) {
466
- parentBeaconBlockRoot = res.parentBeaconBlockRoot;
467
- }
468
- } catch (err) {
469
- this.log.error(`Error getting parent beacon block root`, err);
483
+ if (res.parentBeaconBlockRoot) {
484
+ parentBeaconBlockRoot = res.parentBeaconBlockRoot;
470
485
  }
486
+ } catch (err) {
487
+ this.log.error(`Error getting parent beacon block root`, err);
471
488
  }
472
489
  if (!parentBeaconBlockRoot) {
473
490
  this.log.error(`No parent beacon block root found for block ${blockHash}`);
@@ -499,10 +516,8 @@ export class HttpBlobClient {
499
516
  }
500
517
  /**
501
518
  * Start the blob client.
502
- * Fetches and caches beacon genesis config for timestamp-based slot resolution,
503
- * then uploads the initial healthcheck file (awaited) and starts periodic uploads.
519
+ * Uploads the initial healthcheck file (awaited) and starts periodic uploads.
504
520
  */ async start() {
505
- await this.fetchBeaconConfig();
506
521
  if (!this.fileStoreUploadClient) {
507
522
  return;
508
523
  }
@@ -521,40 +536,6 @@ export class HttpBlobClient {
521
536
  }, intervalMs);
522
537
  }
523
538
  /**
524
- * Fetches and caches beacon genesis time and slot duration from the first available consensus host.
525
- * These static values enable timestamp-based slot resolution, eliminating the per-fetch headers call.
526
- * Logs a warning and leaves fields undefined if all hosts fail, callers fall back gracefully.
527
- */ async fetchBeaconConfig() {
528
- const { l1ConsensusHostUrls } = this.config;
529
- if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
530
- return;
531
- }
532
- for(let i = 0; i < l1ConsensusHostUrls.length; i++){
533
- try {
534
- const { url: genesisUrl, ...genesisOptions } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrls[i]}/eth/v1/config/genesis`, this.config, i);
535
- const { url: specUrl, ...specOptions } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrls[i]}/eth/v1/config/spec`, this.config, i);
536
- const [genesisRes, specRes] = await Promise.all([
537
- this.fetch(genesisUrl, genesisOptions),
538
- this.fetch(specUrl, specOptions)
539
- ]);
540
- if (genesisRes.ok && specRes.ok) {
541
- const genesis = await genesisRes.json();
542
- const spec = await specRes.json();
543
- this.beaconGenesisTime = BigInt(genesis.data.genesisTime);
544
- this.beaconSecondsPerSlot = parseInt(spec.data.secondsPerSlot);
545
- this.log.debug(`Fetched beacon genesis config`, {
546
- genesisTime: this.beaconGenesisTime,
547
- secondsPerSlot: this.beaconSecondsPerSlot
548
- });
549
- return;
550
- }
551
- } catch (err) {
552
- this.log.warn(`Failed to fetch beacon config from host ${l1ConsensusHostUrls[i]}`, err);
553
- }
554
- }
555
- this.log.warn('Could not fetch beacon genesis config from any consensus host — will use headers call fallback');
556
- }
557
- /**
558
539
  * Stop the blob client, clearing any periodic tasks.
559
540
  */ stop() {
560
541
  if (this.healthcheckUploadIntervalId) {
@@ -563,9 +544,10 @@ export class HttpBlobClient {
563
544
  }
564
545
  }
565
546
  }
566
- async function parseBlobJsonsFromResponse(response, logger) {
547
+ function parseBlobJsonsFromResponse(response, logger) {
567
548
  try {
568
- return await Promise.all(response.data.map(parseBlobJson));
549
+ const blobs = response.data.map(parseBlobJson);
550
+ return blobs;
569
551
  } catch (err) {
570
552
  logger.error(`Error parsing blob json from response`, err);
571
553
  return [];
@@ -575,9 +557,10 @@ async function parseBlobJsonsFromResponse(response, logger) {
575
557
  // https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getBlobSidecars
576
558
  // Here we attempt to parse the response data to Buffer, and check the lengths (via Blob's constructor), to avoid
577
559
  // throwing an error down the line when calling Blob.fromJson().
578
- async function parseBlobJson(rawHex) {
579
- const blobBuffer = Buffer.from(rawHex.slice(2), 'hex');
580
- const blob = await Blob.fromBlobBuffer(blobBuffer);
560
+ function parseBlobJson(data) {
561
+ const blobBuffer = Buffer.from(data.blob.slice(2), 'hex');
562
+ const commitmentBuffer = Buffer.from(data.kzg_commitment.slice(2), 'hex');
563
+ const blob = new Blob(blobBuffer, commitmentBuffer);
581
564
  return blob.toJSON();
582
565
  }
583
566
  // Returns an array that maps each blob hash to the corresponding blob, or undefined if the blob is not found
@@ -10,17 +10,6 @@ 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;
24
13
  }
25
14
  export interface BlobClientInterface {
26
15
  /** Sends the given blobs to the filestore, to be indexed by blob hash. */
@@ -36,4 +25,4 @@ export interface BlobClientInterface {
36
25
  /** Returns true if this client can upload blobs to filestore. */
37
26
  canUpload(): boolean;
38
27
  }
39
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvY2xpZW50L2ludGVyZmFjZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssRUFBRSxJQUFJLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUU1Qzs7R0FFRztBQUNILE1BQU0sV0FBVyxxQkFBcUI7SUFDcEM7Ozs7O09BS0c7SUFDSCxnQkFBZ0IsQ0FBQyxFQUFFLE9BQU8sQ0FBQztJQUMzQjs7O09BR0c7SUFDSCxxQkFBcUIsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUMvQjs7OztPQUlHO0lBQ0gsZ0JBQWdCLENBQUMsRUFBRSxNQUFNLENBQUM7Q0FDM0I7QUFFRCxNQUFNLFdBQVcsbUJBQW1CO0lBQ2xDLDBFQUEwRTtJQUMxRSxvQkFBb0IsQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQ3RELHFFQUFxRTtJQUNyRSxjQUFjLENBQUMsT0FBTyxFQUFFLE1BQU0sRUFBRSxVQUFVLENBQUMsRUFBRSxNQUFNLEVBQUUsRUFBRSxJQUFJLENBQUMsRUFBRSxxQkFBcUIsR0FBRyxPQUFPLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUN0Ryw2RUFBNkU7SUFDN0UsS0FBSyxDQUFDLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQ3hCLG9GQUFvRjtJQUNwRixXQUFXLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQzdCLDBEQUEwRDtJQUMxRCxJQUFJLENBQUMsSUFBSSxJQUFJLENBQUM7SUFDZCxpRUFBaUU7SUFDakUsU0FBUyxJQUFJLE9BQU8sQ0FBQztDQUN0QiJ9
28
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvY2xpZW50L2ludGVyZmFjZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssRUFBRSxJQUFJLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUU1Qzs7R0FFRztBQUNILE1BQU0sV0FBVyxxQkFBcUI7SUFDcEM7Ozs7O09BS0c7SUFDSCxnQkFBZ0IsQ0FBQyxFQUFFLE9BQU8sQ0FBQztDQUM1QjtBQUVELE1BQU0sV0FBVyxtQkFBbUI7SUFDbEMsMEVBQTBFO0lBQzFFLG9CQUFvQixDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLENBQUM7SUFDdEQscUVBQXFFO0lBQ3JFLGNBQWMsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLFVBQVUsQ0FBQyxFQUFFLE1BQU0sRUFBRSxFQUFFLElBQUksQ0FBQyxFQUFFLHFCQUFxQixHQUFHLE9BQU8sQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBQ3RHLDZFQUE2RTtJQUM3RSxLQUFLLENBQUMsSUFBSSxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDeEIsb0ZBQW9GO0lBQ3BGLFdBQVcsSUFBSSxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDN0IsMERBQTBEO0lBQzFELElBQUksQ0FBQyxJQUFJLElBQUksQ0FBQztJQUNkLGlFQUFpRTtJQUNqRSxTQUFTLElBQUksT0FBTyxDQUFDO0NBQ3RCIn0=
@@ -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;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"}
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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/blob-client",
3
- "version": "0.0.1-commit.ffe5b04ea",
3
+ "version": "0.0.1-commit.fff30aa",
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.ffe5b04ea",
60
- "@aztec/ethereum": "0.0.1-commit.ffe5b04ea",
61
- "@aztec/foundation": "0.0.1-commit.ffe5b04ea",
62
- "@aztec/kv-store": "0.0.1-commit.ffe5b04ea",
63
- "@aztec/stdlib": "0.0.1-commit.ffe5b04ea",
64
- "@aztec/telemetry-client": "0.0.1-commit.ffe5b04ea",
59
+ "@aztec/blob-lib": "0.0.1-commit.fff30aa",
60
+ "@aztec/ethereum": "0.0.1-commit.fff30aa",
61
+ "@aztec/foundation": "0.0.1-commit.fff30aa",
62
+ "@aztec/kv-store": "0.0.1-commit.fff30aa",
63
+ "@aztec/stdlib": "0.0.1-commit.fff30aa",
64
+ "@aztec/telemetry-client": "0.0.1-commit.fff30aa",
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,11 +25,6 @@ export class HttpBlobClient implements BlobClientInterface {
24
25
  private disabled = false;
25
26
  private healthcheckUploadIntervalId?: NodeJS.Timeout;
26
27
 
27
- /** Cached beacon genesis time (seconds since Unix epoch). Fetched once at startup. */
28
- private beaconGenesisTime?: bigint;
29
- /** Cached beacon slot duration in seconds. Fetched once at startup. */
30
- private beaconSecondsPerSlot?: number;
31
-
32
28
  constructor(
33
29
  config?: BlobClientConfig,
34
30
  private readonly opts: {
@@ -94,44 +90,68 @@ export class HttpBlobClient implements BlobClientInterface {
94
90
  const archiveUrl = this.archiveClient?.getBaseUrl();
95
91
  this.log.info(`Testing configured blob sources`, { l1ConsensusHostUrls, archiveUrl });
96
92
 
97
- let successfulSourceCount = 0;
93
+ let consensusSuperNodes = 0;
94
+ let consensusNonSuperNodes = 0;
95
+ let archiveSources = 0;
96
+ let blobSinks = 0;
98
97
 
99
98
  if (l1ConsensusHostUrls && l1ConsensusHostUrls.length > 0) {
100
99
  for (let l1ConsensusHostIndex = 0; l1ConsensusHostIndex < l1ConsensusHostUrls.length; l1ConsensusHostIndex++) {
101
100
  const l1ConsensusHostUrl = l1ConsensusHostUrls[l1ConsensusHostIndex];
102
101
  try {
103
102
  const { url, ...options } = getBeaconNodeFetchOptions(
104
- `${l1ConsensusHostUrl}/eth/v1/beacon/headers`,
103
+ `${l1ConsensusHostUrl}/eth/v1/beacon/headers/head`,
105
104
  this.config,
106
105
  l1ConsensusHostIndex,
107
106
  );
108
107
  const res = await this.fetch(url, options);
109
- if (res.ok) {
110
- this.log.info(`L1 consensus host is reachable`, { l1ConsensusHostUrl });
111
- successfulSourceCount++;
112
- } else {
108
+ if (!res.ok) {
113
109
  this.log.error(`Failure reaching L1 consensus host: ${res.statusText} (${res.status})`, {
114
110
  l1ConsensusHostUrl,
115
111
  });
112
+ continue;
113
+ }
114
+
115
+ this.log.info(`L1 consensus host is reachable`, { l1ConsensusHostUrl });
116
+
117
+ // Check if the host serves blob sidecars (supernode/semi-supernode).
118
+ // Post-Fusaka (PeerDAS), non-supernode beacon nodes no longer serve the
119
+ // blob sidecar endpoint. A 200 response (even with an empty data array
120
+ // for a slot with no blobs) means the node supports serving blob sidecars.
121
+ const body = await res.json();
122
+ const headSlot = body?.data?.header?.message?.slot;
123
+ if (headSlot) {
124
+ const { url: blobUrl, ...blobOptions } = getBeaconNodeFetchOptions(
125
+ `${l1ConsensusHostUrl}/eth/v1/beacon/blobs/${headSlot}`,
126
+ this.config,
127
+ l1ConsensusHostIndex,
128
+ );
129
+ const blobRes = await this.fetch(blobUrl, blobOptions);
130
+ if (blobRes.ok) {
131
+ this.log.info(`L1 consensus host serves blob sidecars (supernode)`, { l1ConsensusHostUrl });
132
+ consensusSuperNodes++;
133
+ } else {
134
+ this.log.info(`L1 consensus host does not serve blob sidecars`, { l1ConsensusHostUrl });
135
+ consensusNonSuperNodes++;
136
+ }
137
+ } else {
138
+ this.log.info(`L1 consensus host is reachable but could not determine head slot`, { l1ConsensusHostUrl });
139
+ consensusNonSuperNodes++;
116
140
  }
117
141
  } catch (err) {
118
142
  this.log.error(`Error reaching L1 consensus host`, err, { l1ConsensusHostUrl });
119
143
  }
120
144
  }
121
- } else {
122
- this.log.warn('No L1 consensus host urls configured');
123
145
  }
124
146
 
125
147
  if (this.archiveClient) {
126
148
  try {
127
149
  const latest = await this.archiveClient.getLatestBlock();
128
150
  this.log.info(`Archive client is reachable and synced to L1 block ${latest.number}`, { latest, archiveUrl });
129
- successfulSourceCount++;
151
+ archiveSources++;
130
152
  } catch (err) {
131
153
  this.log.error(`Error reaching archive client`, err, { archiveUrl });
132
154
  }
133
- } else {
134
- this.log.warn('No archive client configured');
135
155
  }
136
156
 
137
157
  if (this.fileStoreClients.length > 0) {
@@ -140,7 +160,7 @@ export class HttpBlobClient implements BlobClientInterface {
140
160
  const accessible = await fileStoreClient.testConnection();
141
161
  if (accessible) {
142
162
  this.log.info(`FileStore is reachable`, { url: fileStoreClient.getBaseUrl() });
143
- successfulSourceCount++;
163
+ blobSinks++;
144
164
  } else {
145
165
  this.log.warn(`FileStore is not accessible`, { url: fileStoreClient.getBaseUrl() });
146
166
  }
@@ -150,12 +170,24 @@ export class HttpBlobClient implements BlobClientInterface {
150
170
  }
151
171
  }
152
172
 
173
+ // Emit a single summary after validating all sources
174
+ const successfulSourceCount = consensusSuperNodes + archiveSources + blobSinks;
175
+
176
+ let summary = `Blob client running with consensusSuperNodes=${consensusSuperNodes} archiveSources=${archiveSources} blobSinks=${blobSinks}`;
177
+ if (consensusNonSuperNodes > 0) {
178
+ summary += `. ${consensusNonSuperNodes} consensus client(s) ignored because they are not running in supernode or semi-supernode mode`;
179
+ }
180
+
153
181
  if (successfulSourceCount === 0) {
154
182
  if (this.config.blobAllowEmptySources) {
155
- this.log.warn('No blob sources are reachable');
183
+ this.log.warn(summary);
156
184
  } else {
157
- throw new Error('No blob sources are reachable');
185
+ throw new Error(summary);
158
186
  }
187
+ } else if (consensusSuperNodes === 0) {
188
+ this.log.warn(summary);
189
+ } else {
190
+ this.log.info(summary);
159
191
  }
160
192
  }
161
193
 
@@ -256,7 +288,7 @@ export class HttpBlobClient implements BlobClientInterface {
256
288
  // The beacon api can query by slot number, so we get that first
257
289
  const consensusCtx = { l1ConsensusHostUrls, ...ctx };
258
290
  this.log.trace(`Attempting to get slot number for block hash`, consensusCtx);
259
- const slotNumber = await this.getSlotNumber(blockHash, opts?.parentBeaconBlockRoot, opts?.l1BlockTimestamp);
291
+ const slotNumber = await this.getSlotNumber(blockHash);
260
292
  this.log.debug(`Got slot number ${slotNumber} from consensus host for querying blobs`, consensusCtx);
261
293
 
262
294
  if (slotNumber) {
@@ -273,12 +305,7 @@ export class HttpBlobClient implements BlobClientInterface {
273
305
  l1ConsensusHostUrl,
274
306
  ...ctx,
275
307
  });
276
- const blobs = await this.getBlobsFromHost(
277
- l1ConsensusHostUrl,
278
- slotNumber,
279
- l1ConsensusHostIndex,
280
- getMissingBlobHashes(),
281
- );
308
+ const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex);
282
309
  const result = await fillResults(blobs);
283
310
  this.log.debug(
284
311
  `Got ${blobs.length} blobs from consensus host (total: ${result.length}/${blobHashes.length})`,
@@ -397,7 +424,7 @@ export class HttpBlobClient implements BlobClientInterface {
397
424
  blobHashes: Buffer[] = [],
398
425
  l1ConsensusHostIndex?: number,
399
426
  ): Promise<Blob[]> {
400
- const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
427
+ const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
401
428
  return (await processFetchedBlobs(blobs, blobHashes, this.log)).filter((b): b is Blob => b !== undefined);
402
429
  }
403
430
 
@@ -405,12 +432,11 @@ export class HttpBlobClient implements BlobClientInterface {
405
432
  hostUrl: string,
406
433
  blockHashOrSlot: string | number,
407
434
  l1ConsensusHostIndex?: number,
408
- blobHashes?: Buffer[],
409
435
  ): Promise<BlobJson[]> {
410
436
  try {
411
- let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex, blobHashes);
437
+ let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
412
438
  if (res.ok) {
413
- return await parseBlobJsonsFromResponse(await res.json(), this.log);
439
+ return parseBlobJsonsFromResponse(await res.json(), this.log);
414
440
  }
415
441
 
416
442
  if (res.status === 404 && typeof blockHashOrSlot === 'number') {
@@ -425,9 +451,9 @@ export class HttpBlobClient implements BlobClientInterface {
425
451
  let currentSlot = blockHashOrSlot + 1;
426
452
  while (res.status === 404 && maxRetries > 0 && latestSlot !== undefined && currentSlot <= latestSlot) {
427
453
  this.log.debug(`Trying slot ${currentSlot}`);
428
- res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex, blobHashes);
454
+ res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex);
429
455
  if (res.ok) {
430
- return await parseBlobJsonsFromResponse(await res.json(), this.log);
456
+ return parseBlobJsonsFromResponse(await res.json(), this.log);
431
457
  }
432
458
  currentSlot++;
433
459
  maxRetries--;
@@ -450,17 +476,8 @@ export class HttpBlobClient implements BlobClientInterface {
450
476
  hostUrl: string,
451
477
  blockHashOrSlot: string | number,
452
478
  l1ConsensusHostIndex?: number,
453
- blobHashes?: Buffer[],
454
479
  ): Promise<Response> {
455
- let baseUrl = `${hostUrl}/eth/v1/beacon/blobs/${blockHashOrSlot}`;
456
-
457
- if (blobHashes && blobHashes.length > 0) {
458
- const params = new URLSearchParams();
459
- for (const hash of blobHashes) {
460
- params.append('versioned_hashes', `0x${hash.toString('hex')}`);
461
- }
462
- baseUrl += `?${params.toString()}`;
463
- }
480
+ const baseUrl = `${hostUrl}/eth/v1/beacon/blob_sidecars/${blockHashOrSlot}`;
464
481
 
465
482
  const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
466
483
  this.log.debug(`Fetching blob sidecar for ${blockHashOrSlot}`, { url, ...options });
@@ -502,50 +519,34 @@ export class HttpBlobClient implements BlobClientInterface {
502
519
  * @param blockHash - The block hash
503
520
  * @returns The slot number
504
521
  */
505
- private async getSlotNumber(
506
- blockHash: `0x${string}`,
507
- parentBeaconBlockRoot?: string,
508
- l1BlockTimestamp?: bigint,
509
- ): Promise<number | undefined> {
522
+ private async getSlotNumber(blockHash: `0x${string}`): Promise<number | undefined> {
510
523
  const { l1ConsensusHostUrls, l1RpcUrls } = this.config;
511
524
  if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
512
525
  this.log.debug('No consensus host url configured');
513
526
  return undefined;
514
527
  }
515
528
 
516
- // Primary path: compute slot from timestamp if genesis config is cached (no network call needed)
517
- if (
518
- l1BlockTimestamp !== undefined &&
519
- this.beaconGenesisTime !== undefined &&
520
- this.beaconSecondsPerSlot !== undefined
521
- ) {
522
- const slot = Number((l1BlockTimestamp - this.beaconGenesisTime) / BigInt(this.beaconSecondsPerSlot));
523
- this.log.debug(`Computed slot ${slot} from L1 block timestamp`, { l1BlockTimestamp });
524
- return slot;
529
+ if (!l1RpcUrls || l1RpcUrls.length === 0) {
530
+ this.log.debug('No execution host url configured');
531
+ return undefined;
525
532
  }
526
533
 
527
- if (!parentBeaconBlockRoot) {
528
- // parentBeaconBlockRoot not provided by caller — fetch it from the execution RPC
529
- if (!l1RpcUrls || l1RpcUrls.length === 0) {
530
- this.log.debug('No execution host url configured');
531
- return undefined;
532
- }
533
-
534
- const client = createPublicClient({
535
- transport: fallback(l1RpcUrls.map(url => http(url, { batch: false }))),
534
+ // Ping execution node to get the parentBeaconBlockRoot for this block
535
+ let parentBeaconBlockRoot: string | undefined;
536
+ const client = createPublicClient({
537
+ transport: makeL1HttpTransport(l1RpcUrls, { timeout: this.config.l1HttpTimeoutMS }),
538
+ });
539
+ try {
540
+ const res: RpcBlock = await client.request({
541
+ method: 'eth_getBlockByHash',
542
+ params: [blockHash, /*tx flag*/ false],
536
543
  });
537
- try {
538
- const res: RpcBlock = await client.request({
539
- method: 'eth_getBlockByHash',
540
- params: [blockHash, /*tx flag*/ false],
541
- });
542
544
 
543
- if (res.parentBeaconBlockRoot) {
544
- parentBeaconBlockRoot = res.parentBeaconBlockRoot;
545
- }
546
- } catch (err) {
547
- this.log.error(`Error getting parent beacon block root`, err);
545
+ if (res.parentBeaconBlockRoot) {
546
+ parentBeaconBlockRoot = res.parentBeaconBlockRoot;
548
547
  }
548
+ } catch (err) {
549
+ this.log.error(`Error getting parent beacon block root`, err);
549
550
  }
550
551
 
551
552
  if (!parentBeaconBlockRoot) {
@@ -591,12 +592,9 @@ export class HttpBlobClient implements BlobClientInterface {
591
592
 
592
593
  /**
593
594
  * Start the blob client.
594
- * Fetches and caches beacon genesis config for timestamp-based slot resolution,
595
- * then uploads the initial healthcheck file (awaited) and starts periodic uploads.
595
+ * Uploads the initial healthcheck file (awaited) and starts periodic uploads.
596
596
  */
597
597
  public async start(): Promise<void> {
598
- await this.fetchBeaconConfig();
599
-
600
598
  if (!this.fileStoreUploadClient) {
601
599
  return;
602
600
  }
@@ -621,53 +619,6 @@ export class HttpBlobClient implements BlobClientInterface {
621
619
  }, intervalMs);
622
620
  }
623
621
 
624
- /**
625
- * Fetches and caches beacon genesis time and slot duration from the first available consensus host.
626
- * These static values enable timestamp-based slot resolution, eliminating the per-fetch headers call.
627
- * Logs a warning and leaves fields undefined if all hosts fail, callers fall back gracefully.
628
- */
629
- private async fetchBeaconConfig(): Promise<void> {
630
- const { l1ConsensusHostUrls } = this.config;
631
- if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
632
- return;
633
- }
634
-
635
- for (let i = 0; i < l1ConsensusHostUrls.length; i++) {
636
- try {
637
- const { url: genesisUrl, ...genesisOptions } = getBeaconNodeFetchOptions(
638
- `${l1ConsensusHostUrls[i]}/eth/v1/config/genesis`,
639
- this.config,
640
- i,
641
- );
642
- const { url: specUrl, ...specOptions } = getBeaconNodeFetchOptions(
643
- `${l1ConsensusHostUrls[i]}/eth/v1/config/spec`,
644
- this.config,
645
- i,
646
- );
647
-
648
- const [genesisRes, specRes] = await Promise.all([
649
- this.fetch(genesisUrl, genesisOptions),
650
- this.fetch(specUrl, specOptions),
651
- ]);
652
-
653
- if (genesisRes.ok && specRes.ok) {
654
- const genesis = await genesisRes.json();
655
- const spec = await specRes.json();
656
- this.beaconGenesisTime = BigInt(genesis.data.genesisTime);
657
- this.beaconSecondsPerSlot = parseInt(spec.data.secondsPerSlot);
658
- this.log.debug(`Fetched beacon genesis config`, {
659
- genesisTime: this.beaconGenesisTime,
660
- secondsPerSlot: this.beaconSecondsPerSlot,
661
- });
662
- return;
663
- }
664
- } catch (err) {
665
- this.log.warn(`Failed to fetch beacon config from host ${l1ConsensusHostUrls[i]}`, err);
666
- }
667
- }
668
- this.log.warn('Could not fetch beacon genesis config from any consensus host — will use headers call fallback');
669
- }
670
-
671
622
  /**
672
623
  * Stop the blob client, clearing any periodic tasks.
673
624
  */
@@ -679,9 +630,10 @@ export class HttpBlobClient implements BlobClientInterface {
679
630
  }
680
631
  }
681
632
 
682
- async function parseBlobJsonsFromResponse(response: any, logger: Logger): Promise<BlobJson[]> {
633
+ function parseBlobJsonsFromResponse(response: any, logger: Logger): BlobJson[] {
683
634
  try {
684
- return await Promise.all((response.data as string[]).map(parseBlobJson));
635
+ const blobs = response.data.map(parseBlobJson);
636
+ return blobs;
685
637
  } catch (err) {
686
638
  logger.error(`Error parsing blob json from response`, err);
687
639
  return [];
@@ -692,9 +644,10 @@ async function parseBlobJsonsFromResponse(response: any, logger: Logger): Promis
692
644
  // https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getBlobSidecars
693
645
  // Here we attempt to parse the response data to Buffer, and check the lengths (via Blob's constructor), to avoid
694
646
  // throwing an error down the line when calling Blob.fromJson().
695
- async function parseBlobJson(rawHex: string): Promise<BlobJson> {
696
- const blobBuffer = Buffer.from(rawHex.slice(2), 'hex');
697
- const blob = await Blob.fromBlobBuffer(blobBuffer);
647
+ function parseBlobJson(data: any): BlobJson {
648
+ const blobBuffer = Buffer.from(data.blob.slice(2), 'hex');
649
+ const commitmentBuffer = Buffer.from(data.kzg_commitment.slice(2), 'hex');
650
+ const blob = new Blob(blobBuffer, commitmentBuffer);
698
651
  return blob.toJSON();
699
652
  }
700
653
 
@@ -11,17 +11,6 @@ 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;
25
14
  }
26
15
 
27
16
  export interface BlobClientInterface {