@aztec/blob-client 0.0.1-commit.f504929 → 0.0.1-commit.f650c0a5c

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.
@@ -1,4 +1,5 @@
1
1
  import { type Logger } from '@aztec/foundation/log';
2
+ import { type HttpFileStoreOptions } from '@aztec/stdlib/file-store';
2
3
  import { FileStoreBlobClient } from './filestore_blob_client.js';
3
4
  /**
4
5
  * Metadata required to construct the base path for blob storage.
@@ -25,8 +26,8 @@ export declare function makeBlobBasePath(metadata: BlobFileStoreMetadata): strin
25
26
  * @param logger - Optional logger
26
27
  * @returns A FileStoreBlobClient for reading blobs, or undefined if storeUrl is undefined
27
28
  */
28
- export declare function createReadOnlyFileStoreBlobClient(storeUrl: string, metadata: BlobFileStoreMetadata, logger?: Logger): Promise<FileStoreBlobClient>;
29
- export declare function createReadOnlyFileStoreBlobClient(storeUrl: string | undefined, metadata: BlobFileStoreMetadata, logger?: Logger): Promise<FileStoreBlobClient | undefined>;
29
+ export declare function createReadOnlyFileStoreBlobClient(storeUrl: string, metadata: BlobFileStoreMetadata, logger?: Logger, httpOptions?: HttpFileStoreOptions): Promise<FileStoreBlobClient>;
30
+ export declare function createReadOnlyFileStoreBlobClient(storeUrl: string | undefined, metadata: BlobFileStoreMetadata, logger?: Logger, httpOptions?: HttpFileStoreOptions): Promise<FileStoreBlobClient | undefined>;
30
31
  /**
31
32
  * Creates multiple read-only FileStoreBlobClients from an array of URLs.
32
33
  *
@@ -35,7 +36,7 @@ export declare function createReadOnlyFileStoreBlobClient(storeUrl: string | und
35
36
  * @param logger - Optional logger
36
37
  * @returns Array of FileStoreBlobClients (excludes any that failed to create)
37
38
  */
38
- export declare function createReadOnlyFileStoreBlobClients(storeUrls: string[] | undefined, metadata: BlobFileStoreMetadata, logger?: Logger): Promise<FileStoreBlobClient[]>;
39
+ export declare function createReadOnlyFileStoreBlobClients(storeUrls: string[] | undefined, metadata: BlobFileStoreMetadata, logger?: Logger, httpOptions?: HttpFileStoreOptions): Promise<FileStoreBlobClient[]>;
39
40
  /**
40
41
  * Creates a writable FileStoreBlobClient for uploading blobs.
41
42
  * Note: https:// URLs are not supported for upload, only s3://, gs://, or file://.
@@ -47,4 +48,4 @@ export declare function createReadOnlyFileStoreBlobClients(storeUrls: string[] |
47
48
  */
48
49
  export declare function createWritableFileStoreBlobClient(storeUrl: string, metadata: BlobFileStoreMetadata, logger?: Logger): Promise<FileStoreBlobClient>;
49
50
  export declare function createWritableFileStoreBlobClient(storeUrl: string | undefined, metadata: BlobFileStoreMetadata, logger?: Logger): Promise<FileStoreBlobClient | undefined>;
50
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmFjdG9yeS5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2ZpbGVzdG9yZS9mYWN0b3J5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxLQUFLLE1BQU0sRUFBZ0IsTUFBTSx1QkFBdUIsQ0FBQztBQVFsRSxPQUFPLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQztBQUVqRTs7O0dBR0c7QUFDSCxNQUFNLFdBQVcscUJBQXFCO0lBQ3BDLHNCQUFzQjtJQUN0QixTQUFTLEVBQUUsTUFBTSxDQUFDO0lBQ2xCLHlCQUF5QjtJQUN6QixhQUFhLEVBQUUsTUFBTSxDQUFDO0lBQ3RCLDhEQUE4RDtJQUM5RCxhQUFhLEVBQUUsTUFBTSxDQUFDO0NBQ3ZCO0FBRUQ7OztHQUdHO0FBQ0gsd0JBQWdCLGdCQUFnQixDQUFDLFFBQVEsRUFBRSxxQkFBcUIsR0FBRyxNQUFNLENBS3hFO0FBRUQ7Ozs7Ozs7R0FPRztBQUNILHdCQUFzQixpQ0FBaUMsQ0FDckQsUUFBUSxFQUFFLE1BQU0sRUFDaEIsUUFBUSxFQUFFLHFCQUFxQixFQUMvQixNQUFNLENBQUMsRUFBRSxNQUFNLEdBQ2QsT0FBTyxDQUFDLG1CQUFtQixDQUFDLENBQUM7QUFDaEMsd0JBQXNCLGlDQUFpQyxDQUNyRCxRQUFRLEVBQUUsTUFBTSxHQUFHLFNBQVMsRUFDNUIsUUFBUSxFQUFFLHFCQUFxQixFQUMvQixNQUFNLENBQUMsRUFBRSxNQUFNLEdBQ2QsT0FBTyxDQUFDLG1CQUFtQixHQUFHLFNBQVMsQ0FBQyxDQUFDO0FBbUI1Qzs7Ozs7OztHQU9HO0FBQ0gsd0JBQXNCLGtDQUFrQyxDQUN0RCxTQUFTLEVBQUUsTUFBTSxFQUFFLEdBQUcsU0FBUyxFQUMvQixRQUFRLEVBQUUscUJBQXFCLEVBQy9CLE1BQU0sQ0FBQyxFQUFFLE1BQU0sR0FDZCxPQUFPLENBQUMsbUJBQW1CLEVBQUUsQ0FBQyxDQW9CaEM7QUFFRDs7Ozs7Ozs7R0FRRztBQUNILHdCQUFzQixpQ0FBaUMsQ0FDckQsUUFBUSxFQUFFLE1BQU0sRUFDaEIsUUFBUSxFQUFFLHFCQUFxQixFQUMvQixNQUFNLENBQUMsRUFBRSxNQUFNLEdBQ2QsT0FBTyxDQUFDLG1CQUFtQixDQUFDLENBQUM7QUFDaEMsd0JBQXNCLGlDQUFpQyxDQUNyRCxRQUFRLEVBQUUsTUFBTSxHQUFHLFNBQVMsRUFDNUIsUUFBUSxFQUFFLHFCQUFxQixFQUMvQixNQUFNLENBQUMsRUFBRSxNQUFNLEdBQ2QsT0FBTyxDQUFDLG1CQUFtQixHQUFHLFNBQVMsQ0FBQyxDQUFDIn0=
51
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmFjdG9yeS5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2ZpbGVzdG9yZS9mYWN0b3J5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxLQUFLLE1BQU0sRUFBZ0IsTUFBTSx1QkFBdUIsQ0FBQztBQUNsRSxPQUFPLEVBRUwsS0FBSyxvQkFBb0IsRUFJMUIsTUFBTSwwQkFBMEIsQ0FBQztBQUVsQyxPQUFPLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQztBQUVqRTs7O0dBR0c7QUFDSCxNQUFNLFdBQVcscUJBQXFCO0lBQ3BDLHNCQUFzQjtJQUN0QixTQUFTLEVBQUUsTUFBTSxDQUFDO0lBQ2xCLHlCQUF5QjtJQUN6QixhQUFhLEVBQUUsTUFBTSxDQUFDO0lBQ3RCLDhEQUE4RDtJQUM5RCxhQUFhLEVBQUUsTUFBTSxDQUFDO0NBQ3ZCO0FBRUQ7OztHQUdHO0FBQ0gsd0JBQWdCLGdCQUFnQixDQUFDLFFBQVEsRUFBRSxxQkFBcUIsR0FBRyxNQUFNLENBS3hFO0FBRUQ7Ozs7Ozs7R0FPRztBQUNILHdCQUFzQixpQ0FBaUMsQ0FDckQsUUFBUSxFQUFFLE1BQU0sRUFDaEIsUUFBUSxFQUFFLHFCQUFxQixFQUMvQixNQUFNLENBQUMsRUFBRSxNQUFNLEVBQ2YsV0FBVyxDQUFDLEVBQUUsb0JBQW9CLEdBQ2pDLE9BQU8sQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDO0FBQ2hDLHdCQUFzQixpQ0FBaUMsQ0FDckQsUUFBUSxFQUFFLE1BQU0sR0FBRyxTQUFTLEVBQzVCLFFBQVEsRUFBRSxxQkFBcUIsRUFDL0IsTUFBTSxDQUFDLEVBQUUsTUFBTSxFQUNmLFdBQVcsQ0FBQyxFQUFFLG9CQUFvQixHQUNqQyxPQUFPLENBQUMsbUJBQW1CLEdBQUcsU0FBUyxDQUFDLENBQUM7QUFvQjVDOzs7Ozs7O0dBT0c7QUFDSCx3QkFBc0Isa0NBQWtDLENBQ3RELFNBQVMsRUFBRSxNQUFNLEVBQUUsR0FBRyxTQUFTLEVBQy9CLFFBQVEsRUFBRSxxQkFBcUIsRUFDL0IsTUFBTSxDQUFDLEVBQUUsTUFBTSxFQUNmLFdBQVcsQ0FBQyxFQUFFLG9CQUFvQixHQUNqQyxPQUFPLENBQUMsbUJBQW1CLEVBQUUsQ0FBQyxDQW9CaEM7QUFFRDs7Ozs7Ozs7R0FRRztBQUNILHdCQUFzQixpQ0FBaUMsQ0FDckQsUUFBUSxFQUFFLE1BQU0sRUFDaEIsUUFBUSxFQUFFLHFCQUFxQixFQUMvQixNQUFNLENBQUMsRUFBRSxNQUFNLEdBQ2QsT0FBTyxDQUFDLG1CQUFtQixDQUFDLENBQUM7QUFDaEMsd0JBQXNCLGlDQUFpQyxDQUNyRCxRQUFRLEVBQUUsTUFBTSxHQUFHLFNBQVMsRUFDNUIsUUFBUSxFQUFFLHFCQUFxQixFQUMvQixNQUFNLENBQUMsRUFBRSxNQUFNLEdBQ2QsT0FBTyxDQUFDLG1CQUFtQixHQUFHLFNBQVMsQ0FBQyxDQUFDIn0=
@@ -1 +1 @@
1
- {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/filestore/factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,uBAAuB,CAAC;AAQlE,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEjE;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,sBAAsB;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,yBAAyB;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,MAAM,CAKxE;AAED;;;;;;;GAOG;AACH,wBAAsB,iCAAiC,CACrD,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,qBAAqB,EAC/B,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAChC,wBAAsB,iCAAiC,CACrD,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,QAAQ,EAAE,qBAAqB,EAC/B,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAAC;AAmB5C;;;;;;;GAOG;AACH,wBAAsB,kCAAkC,CACtD,SAAS,EAAE,MAAM,EAAE,GAAG,SAAS,EAC/B,QAAQ,EAAE,qBAAqB,EAC/B,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAoBhC;AAED;;;;;;;;GAQG;AACH,wBAAsB,iCAAiC,CACrD,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,qBAAqB,EAC/B,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAChC,wBAAsB,iCAAiC,CACrD,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,QAAQ,EAAE,qBAAqB,EAC/B,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAAC"}
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/filestore/factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAEL,KAAK,oBAAoB,EAI1B,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEjE;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,sBAAsB;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,yBAAyB;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,MAAM,CAKxE;AAED;;;;;;;GAOG;AACH,wBAAsB,iCAAiC,CACrD,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,qBAAqB,EAC/B,MAAM,CAAC,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,oBAAoB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAChC,wBAAsB,iCAAiC,CACrD,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,QAAQ,EAAE,qBAAqB,EAC/B,MAAM,CAAC,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,oBAAoB,GACjC,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAAC;AAoB5C;;;;;;;GAOG;AACH,wBAAsB,kCAAkC,CACtD,SAAS,EAAE,MAAM,EAAE,GAAG,SAAS,EAC/B,QAAQ,EAAE,qBAAqB,EAC/B,MAAM,CAAC,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,oBAAoB,GACjC,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAoBhC;AAED;;;;;;;;GAQG;AACH,wBAAsB,iCAAiC,CACrD,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,qBAAqB,EAC/B,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAChC,wBAAsB,iCAAiC,CACrD,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,QAAQ,EAAE,qBAAqB,EAC/B,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAAC"}
@@ -10,7 +10,7 @@ import { FileStoreBlobClient } from './filestore_blob_client.js';
10
10
  const normalizedAddress = rollupAddress.toLowerCase().replace(/^0x/, '');
11
11
  return `aztec-${l1ChainId}-${rollupVersion}-0x${normalizedAddress}`;
12
12
  }
13
- export async function createReadOnlyFileStoreBlobClient(storeUrl, metadata, logger) {
13
+ export async function createReadOnlyFileStoreBlobClient(storeUrl, metadata, logger, httpOptions) {
14
14
  if (!storeUrl) {
15
15
  return undefined;
16
16
  }
@@ -20,7 +20,7 @@ export async function createReadOnlyFileStoreBlobClient(storeUrl, metadata, logg
20
20
  storeUrl,
21
21
  basePath
22
22
  });
23
- const store = await createReadOnlyFileStore(storeUrl, log);
23
+ const store = await createReadOnlyFileStore(storeUrl, log, httpOptions);
24
24
  return new FileStoreBlobClient(store, basePath, log);
25
25
  }
26
26
  /**
@@ -30,7 +30,7 @@ export async function createReadOnlyFileStoreBlobClient(storeUrl, metadata, logg
30
30
  * @param metadata - Chain metadata for constructing the base path
31
31
  * @param logger - Optional logger
32
32
  * @returns Array of FileStoreBlobClients (excludes any that failed to create)
33
- */ export async function createReadOnlyFileStoreBlobClients(storeUrls, metadata, logger) {
33
+ */ export async function createReadOnlyFileStoreBlobClients(storeUrls, metadata, logger, httpOptions) {
34
34
  if (!storeUrls || storeUrls.length === 0) {
35
35
  return [];
36
36
  }
@@ -38,7 +38,7 @@ export async function createReadOnlyFileStoreBlobClient(storeUrl, metadata, logg
38
38
  const clients = [];
39
39
  for (const storeUrl of storeUrls){
40
40
  try {
41
- const client = await createReadOnlyFileStoreBlobClient(storeUrl, metadata, log);
41
+ const client = await createReadOnlyFileStoreBlobClient(storeUrl, metadata, log, httpOptions);
42
42
  if (client) {
43
43
  clients.push(client);
44
44
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/blob-client",
3
- "version": "0.0.1-commit.f504929",
3
+ "version": "0.0.1-commit.f650c0a5c",
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.f504929",
60
- "@aztec/ethereum": "0.0.1-commit.f504929",
61
- "@aztec/foundation": "0.0.1-commit.f504929",
62
- "@aztec/kv-store": "0.0.1-commit.f504929",
63
- "@aztec/stdlib": "0.0.1-commit.f504929",
64
- "@aztec/telemetry-client": "0.0.1-commit.f504929",
59
+ "@aztec/blob-lib": "0.0.1-commit.f650c0a5c",
60
+ "@aztec/ethereum": "0.0.1-commit.f650c0a5c",
61
+ "@aztec/foundation": "0.0.1-commit.f650c0a5c",
62
+ "@aztec/kv-store": "0.0.1-commit.f650c0a5c",
63
+ "@aztec/stdlib": "0.0.1-commit.f650c0a5c",
64
+ "@aztec/telemetry-client": "0.0.1-commit.f650c0a5c",
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,15 @@ 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;
62
+
63
+ /** Whether to prefer filestores over consensus clients when fetching blobs. Default: false (consensus first). */
64
+ blobPreferFilestores?: boolean;
65
+
66
+ /** Timeout in ms for HTTP requests to the blob file store. Default: 10000 (10s). */
67
+ blobFileStoreTimeoutMs?: number;
58
68
  }
59
69
 
60
70
  export const blobClientConfigMapping: ConfigMappingsType<BlobClientConfig> = {
@@ -108,6 +118,21 @@ export const blobClientConfigMapping: ConfigMappingsType<BlobClientConfig> = {
108
118
  description: 'Interval in minutes for uploading healthcheck file to file store (default: 60 = 1 hour)',
109
119
  parseEnv: (val: string | undefined) => (val ? +val : undefined),
110
120
  },
121
+ l1HttpTimeoutMS: {
122
+ env: 'ETHEREUM_HTTP_TIMEOUT_MS',
123
+ description: 'Timeout for HTTP requests to the L1 RPC node in ms.',
124
+ ...optionalNumberConfigHelper(),
125
+ },
126
+ blobPreferFilestores: {
127
+ env: 'BLOB_PREFER_FILESTORES',
128
+ description: 'Whether to prefer filestores over consensus clients when fetching blobs. Default: false.',
129
+ ...booleanConfigHelper(false),
130
+ },
131
+ blobFileStoreTimeoutMs: {
132
+ env: 'BLOB_FILE_STORE_TIMEOUT_MS',
133
+ description: 'Timeout in ms for HTTP requests to the blob file store. Default: 10000 (10s).',
134
+ ...optionalNumberConfigHelper(),
135
+ },
111
136
  ...blobArchiveApiConfigMappings,
112
137
  };
113
138
 
@@ -76,8 +76,15 @@ export async function createBlobClientWithFileStores(
76
76
  rollupAddress: config.l1Contracts.rollupAddress.toString(),
77
77
  };
78
78
 
79
+ // Disable internal retries for blob file stores — retry logic is handled by HttpBlobClient.
80
+ // Set a configurable timeout (default 10s) to avoid hanging on slow stores.
81
+ const httpOptions = {
82
+ retryBackoff: [] as number[],
83
+ timeoutMs: config.blobFileStoreTimeoutMs ?? 10_000,
84
+ };
85
+
79
86
  const [fileStoreClients, fileStoreUploadClient] = await Promise.all([
80
- createReadOnlyFileStoreBlobClients(config.blobFileStoreUrls, fileStoreMetadata, log),
87
+ createReadOnlyFileStoreBlobClients(config.blobFileStoreUrls, fileStoreMetadata, log, httpOptions),
81
88
  createWritableFileStoreBlobClient(config.blobFileStoreUploadUrl, fileStoreMetadata, log),
82
89
  ]);
83
90
 
@@ -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';
@@ -29,6 +30,9 @@ export class HttpBlobClient implements BlobClientInterface {
29
30
  /** Cached beacon slot duration in seconds. Fetched once at startup. */
30
31
  private beaconSecondsPerSlot?: number;
31
32
 
33
+ /** Indexes of consensus hosts that serve blob sidecars (supernodes). Populated by testSources(). */
34
+ private superNodeHostIndexes?: Set<number>;
35
+
32
36
  constructor(
33
37
  config?: BlobClientConfig,
34
38
  private readonly opts: {
@@ -94,44 +98,75 @@ export class HttpBlobClient implements BlobClientInterface {
94
98
  const archiveUrl = this.archiveClient?.getBaseUrl();
95
99
  this.log.info(`Testing configured blob sources`, { l1ConsensusHostUrls, archiveUrl });
96
100
 
97
- let successfulSourceCount = 0;
101
+ let consensusSuperNodes = 0;
102
+ let consensusNonSuperNodes = 0;
103
+ let archiveSources = 0;
104
+ let blobSinks = 0;
105
+
106
+ const detectedSuperNodes = new Set<number>();
98
107
 
99
108
  if (l1ConsensusHostUrls && l1ConsensusHostUrls.length > 0) {
100
109
  for (let l1ConsensusHostIndex = 0; l1ConsensusHostIndex < l1ConsensusHostUrls.length; l1ConsensusHostIndex++) {
101
110
  const l1ConsensusHostUrl = l1ConsensusHostUrls[l1ConsensusHostIndex];
102
111
  try {
103
112
  const { url, ...options } = getBeaconNodeFetchOptions(
104
- `${l1ConsensusHostUrl}/eth/v1/beacon/headers`,
113
+ `${l1ConsensusHostUrl}/eth/v1/beacon/headers/head`,
105
114
  this.config,
106
115
  l1ConsensusHostIndex,
107
116
  );
108
117
  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 {
118
+ if (!res.ok) {
113
119
  this.log.error(`Failure reaching L1 consensus host: ${res.statusText} (${res.status})`, {
114
120
  l1ConsensusHostUrl,
115
121
  });
122
+ continue;
123
+ }
124
+
125
+ this.log.info(`L1 consensus host is reachable`, { l1ConsensusHostUrl });
126
+
127
+ // Check if the host serves blob sidecars (supernode/semi-supernode).
128
+ // Post-Fusaka (PeerDAS), non-supernode beacon nodes no longer serve the
129
+ // blob sidecar endpoint. A 200 response (even with an empty data array
130
+ // for a slot with no blobs) means the node supports serving blob sidecars.
131
+ const body = await res.json();
132
+ const headSlot = body?.data?.header?.message?.slot;
133
+ if (headSlot) {
134
+ const { url: blobUrl, ...blobOptions } = getBeaconNodeFetchOptions(
135
+ `${l1ConsensusHostUrl}/eth/v1/beacon/blobs/${headSlot}`,
136
+ this.config,
137
+ l1ConsensusHostIndex,
138
+ );
139
+ const blobRes = await this.fetch(blobUrl, blobOptions);
140
+ if (blobRes.ok) {
141
+ this.log.info(`L1 consensus host serves blob sidecars (supernode)`, { l1ConsensusHostUrl });
142
+ detectedSuperNodes.add(l1ConsensusHostIndex);
143
+ consensusSuperNodes++;
144
+ } else {
145
+ this.log.info(`L1 consensus host does not serve blob sidecars, skipping for blob fetching`, {
146
+ l1ConsensusHostUrl,
147
+ });
148
+ consensusNonSuperNodes++;
149
+ }
150
+ } else {
151
+ this.log.info(`L1 consensus host is reachable but could not determine head slot`, { l1ConsensusHostUrl });
152
+ consensusNonSuperNodes++;
116
153
  }
117
154
  } catch (err) {
118
155
  this.log.error(`Error reaching L1 consensus host`, err, { l1ConsensusHostUrl });
119
156
  }
120
157
  }
121
- } else {
122
- this.log.warn('No L1 consensus host urls configured');
123
158
  }
124
159
 
160
+ this.superNodeHostIndexes = detectedSuperNodes;
161
+
125
162
  if (this.archiveClient) {
126
163
  try {
127
164
  const latest = await this.archiveClient.getLatestBlock();
128
165
  this.log.info(`Archive client is reachable and synced to L1 block ${latest.number}`, { latest, archiveUrl });
129
- successfulSourceCount++;
166
+ archiveSources++;
130
167
  } catch (err) {
131
168
  this.log.error(`Error reaching archive client`, err, { archiveUrl });
132
169
  }
133
- } else {
134
- this.log.warn('No archive client configured');
135
170
  }
136
171
 
137
172
  if (this.fileStoreClients.length > 0) {
@@ -140,7 +175,7 @@ export class HttpBlobClient implements BlobClientInterface {
140
175
  const accessible = await fileStoreClient.testConnection();
141
176
  if (accessible) {
142
177
  this.log.info(`FileStore is reachable`, { url: fileStoreClient.getBaseUrl() });
143
- successfulSourceCount++;
178
+ blobSinks++;
144
179
  } else {
145
180
  this.log.warn(`FileStore is not accessible`, { url: fileStoreClient.getBaseUrl() });
146
181
  }
@@ -150,12 +185,24 @@ export class HttpBlobClient implements BlobClientInterface {
150
185
  }
151
186
  }
152
187
 
188
+ // Emit a single summary after validating all sources
189
+ const successfulSourceCount = consensusSuperNodes + archiveSources + blobSinks;
190
+
191
+ let summary = `Blob client running with consensusSuperNodes=${consensusSuperNodes} archiveSources=${archiveSources} blobSinks=${blobSinks}`;
192
+ if (consensusNonSuperNodes > 0) {
193
+ summary += `. ${consensusNonSuperNodes} consensus client(s) ignored because they are not running in supernode or semi-supernode mode`;
194
+ }
195
+
153
196
  if (successfulSourceCount === 0) {
154
197
  if (this.config.blobAllowEmptySources) {
155
- this.log.warn('No blob sources are reachable');
198
+ this.log.warn(summary);
156
199
  } else {
157
- throw new Error('No blob sources are reachable');
200
+ throw new Error(summary);
158
201
  }
202
+ } else if (consensusSuperNodes === 0) {
203
+ this.log.warn(summary);
204
+ } else {
205
+ this.log.info(summary);
159
206
  }
160
207
  }
161
208
 
@@ -181,18 +228,15 @@ export class HttpBlobClient implements BlobClientInterface {
181
228
  }
182
229
 
183
230
  /**
184
- * Get the blob sidecar
185
- *
186
- * If requesting from the blob client, we send the blobkHash
187
- * If requesting from the beacon node, we send the slot number
231
+ * Get the blob sidecar.
188
232
  *
189
- * Source ordering depends on sync state:
190
- * - Historical sync: blob client FileStore L1 consensus Archive
191
- * - Near tip sync: blob client → FileStore → L1 consensus → FileStore (with retries) → Archive (eg blobscan)
233
+ * Alternates between two primary sources (consensus and filestore) in a retry loop,
234
+ * then falls back to archive if blobs are still missing. The order of the primary
235
+ * sources is configurable via `blobPreferFilestores`.
192
236
  *
193
237
  * @param blockHash - The block hash
194
238
  * @param blobHashes - The blob hashes to fetch
195
- * @param opts - Options including isHistoricalSync flag
239
+ * @param opts - Options for slot resolution
196
240
  * @returns The blobs
197
241
  */
198
242
  public async getBlobSidecar(
@@ -205,12 +249,11 @@ export class HttpBlobClient implements BlobClientInterface {
205
249
  return [];
206
250
  }
207
251
 
208
- const isHistoricalSync = opts?.isHistoricalSync ?? false;
209
252
  // Accumulate blobs across sources, preserving order and handling duplicates
210
253
  // resultBlobs[i] will contain the blob for blobHashes[i], or undefined if not yet found
211
254
  const resultBlobs: (Blob | undefined)[] = new Array(blobHashes.length).fill(undefined);
212
255
 
213
- // Helper to get missing blob hashes that we still need to fetch
256
+ // Helper to get missing blob hashes that we still need to fetch
214
257
  const getMissingBlobHashes = (): Buffer[] =>
215
258
  blobHashes
216
259
  .map((bh, i) => (resultBlobs[i] === undefined ? bh : undefined))
@@ -239,84 +282,60 @@ export class HttpBlobClient implements BlobClientInterface {
239
282
  return blobs;
240
283
  };
241
284
 
242
- const { l1ConsensusHostUrls } = this.config;
243
-
244
285
  const ctx = { blockHash, blobHashes: blobHashes.map(bufferToHex) };
245
286
 
246
- // Try filestore (quick, no retries) - useful for both historical and near-tip sync
247
- if (this.fileStoreClients.length > 0 && getMissingBlobHashes().length > 0) {
248
- await this.tryFileStores(getMissingBlobHashes, fillResults, ctx);
249
- if (getMissingBlobHashes().length === 0) {
250
- return returnWithCallback(getFilledBlobs());
287
+ // Lazily resolve the slot number only resolved when consensus hosts are actually tried.
288
+ let slotNumber: number | undefined;
289
+ let slotResolved = false;
290
+ const getSlotNumber = async (): Promise<number | undefined> => {
291
+ if (!slotResolved) {
292
+ slotNumber = await this.resolveSlotNumber(blockHash, opts);
293
+ slotResolved = true;
251
294
  }
252
- }
295
+ return slotNumber;
296
+ };
253
297
 
254
- const missingAfterSink = getMissingBlobHashes();
255
- if (missingAfterSink.length > 0 && l1ConsensusHostUrls && l1ConsensusHostUrls.length > 0) {
256
- // The beacon api can query by slot number, so we get that first
257
- const consensusCtx = { l1ConsensusHostUrls, ...ctx };
258
- this.log.trace(`Attempting to get slot number for block hash`, consensusCtx);
259
- const slotNumber = await this.getSlotNumber(blockHash, opts?.parentBeaconBlockRoot, opts?.l1BlockTimestamp);
260
- this.log.debug(`Got slot number ${slotNumber} from consensus host for querying blobs`, consensusCtx);
261
-
262
- if (slotNumber) {
263
- let l1ConsensusHostUrl: string;
264
- for (let l1ConsensusHostIndex = 0; l1ConsensusHostIndex < l1ConsensusHostUrls.length; l1ConsensusHostIndex++) {
265
- const missingHashes = getMissingBlobHashes();
266
- if (missingHashes.length === 0) {
267
- break;
268
- }
298
+ // Build the two source-try functions. The order depends on the config.
299
+ const tryConsensus = () => this.tryConsensusHosts(getSlotNumber, getMissingBlobHashes, fillResults, ctx);
300
+ const tryFilestores = () => this.tryFileStores(getMissingBlobHashes, fillResults, ctx);
269
301
 
270
- l1ConsensusHostUrl = l1ConsensusHostUrls[l1ConsensusHostIndex];
271
- this.log.trace(`Attempting to get ${missingHashes.length} blobs from consensus host`, {
272
- slotNumber,
273
- l1ConsensusHostUrl,
274
- ...ctx,
275
- });
276
- const blobs = await this.getBlobsFromHost(
277
- l1ConsensusHostUrl,
278
- slotNumber,
279
- l1ConsensusHostIndex,
280
- getMissingBlobHashes(),
281
- );
282
- const result = await fillResults(blobs);
283
- this.log.debug(
284
- `Got ${blobs.length} blobs from consensus host (total: ${result.length}/${blobHashes.length})`,
285
- { slotNumber, l1ConsensusHostUrl, ...ctx },
286
- );
287
- if (result.length === blobHashes.length) {
288
- return returnWithCallback(result);
289
- }
290
- }
291
- }
292
- }
302
+ const preferFilestores = this.config.blobPreferFilestores ?? false;
303
+ const [trySourceA, trySourceB] = preferFilestores ? [tryFilestores, tryConsensus] : [tryConsensus, tryFilestores];
293
304
 
294
- // For near-tip sync, retry filestores with backoff (eventual consistency)
295
- // This handles the case where blobs are still being uploaded by other validators
296
- if (!isHistoricalSync && this.fileStoreClients.length > 0 && getMissingBlobHashes().length > 0) {
297
- try {
298
- await retry(
299
- async () => {
300
- await this.tryFileStores(getMissingBlobHashes, fillResults, ctx);
301
- if (getMissingBlobHashes().length > 0) {
302
- throw new Error('Still missing blobs from filestores');
303
- }
304
- },
305
- 'filestore blob retrieval',
306
- makeBackoff([1, 1, 2]),
307
- this.log,
308
- true, // failSilently - expected to fail during eventual consistency
309
- );
310
- return returnWithCallback(getFilledBlobs());
311
- } catch {
312
- // Exhausted retries, continue to archive fallback
313
- }
305
+ // Historical sync: blobs should already exist, use shorter backoff for transient errors.
306
+ // Near-tip sync: blobs may still be uploading, use longer backoff for eventual consistency.
307
+ const isHistoricalSync = opts?.isHistoricalSync ?? false;
308
+ const backoff = isHistoricalSync ? [1, 1] : [1, 1, 1, 2, 2];
309
+
310
+ // Retry loop: alternate between the two primary sources with backoff.
311
+ try {
312
+ await retry(
313
+ async () => {
314
+ if (getMissingBlobHashes().length > 0) {
315
+ await trySourceA();
316
+ }
317
+ if (getMissingBlobHashes().length > 0) {
318
+ await trySourceB();
319
+ }
320
+ if (getMissingBlobHashes().length > 0) {
321
+ throw new Error('Still missing blobs after trying all primary sources');
322
+ }
323
+ },
324
+ 'blob retrieval',
325
+ makeBackoff(backoff),
326
+ this.log,
327
+ true, // failSilently — expected during eventual consistency
328
+ );
329
+ return returnWithCallback(getFilledBlobs());
330
+ } catch {
331
+ // Exhausted retries, continue to archive fallback
314
332
  }
315
333
 
316
- const missingAfterConsensus = getMissingBlobHashes();
317
- if (missingAfterConsensus.length > 0 && this.archiveClient) {
334
+ // Archive fallback
335
+ const missingAfterPrimary = getMissingBlobHashes();
336
+ if (missingAfterPrimary.length > 0 && this.archiveClient) {
318
337
  const archiveCtx = { archiveUrl: this.archiveClient.getBaseUrl(), ...ctx };
319
- this.log.trace(`Attempting to get ${missingAfterConsensus.length} blobs from archive`, archiveCtx);
338
+ this.log.trace(`Attempting to get ${missingAfterPrimary.length} blobs from archive`, archiveCtx);
320
339
  const allBlobs = await this.archiveClient.getBlobsFromBlock(blockHash);
321
340
  if (!allBlobs) {
322
341
  this.log.debug('No blobs found from archive client', archiveCtx);
@@ -338,7 +357,7 @@ export class HttpBlobClient implements BlobClientInterface {
338
357
  this.log.warn(
339
358
  `Failed to fetch all blobs for ${blockHash} from all blob sources (got ${result.length}/${blobHashes.length})`,
340
359
  {
341
- l1ConsensusHostUrls,
360
+ l1ConsensusHostUrls: this.config.l1ConsensusHostUrls,
342
361
  archiveUrl: this.archiveClient?.getBaseUrl(),
343
362
  fileStoreUrls: this.fileStoreClients.map(c => c.getBaseUrl()),
344
363
  },
@@ -347,6 +366,71 @@ export class HttpBlobClient implements BlobClientInterface {
347
366
  return returnWithCallback(result);
348
367
  }
349
368
 
369
+ /** Resolves the beacon slot number for the given block hash. Returns undefined if no consensus hosts. */
370
+ private resolveSlotNumber(
371
+ blockHash: `0x${string}`,
372
+ opts?: GetBlobSidecarOptions,
373
+ ): Promise<number | undefined> | undefined {
374
+ const { l1ConsensusHostUrls } = this.config;
375
+ if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
376
+ return undefined;
377
+ }
378
+ // If no supernodes, no point resolving the slot
379
+ if (this.superNodeHostIndexes && this.superNodeHostIndexes.size === 0) {
380
+ return undefined;
381
+ }
382
+ return this.getSlotNumber(blockHash, opts?.parentBeaconBlockRoot, opts?.l1BlockTimestamp);
383
+ }
384
+
385
+ /**
386
+ * Try all supernode consensus hosts for blob sidecars.
387
+ * Skips hosts that were detected as non-supernodes during testSources().
388
+ */
389
+ private async tryConsensusHosts(
390
+ getSlotNumber: () => Promise<number | undefined>,
391
+ getMissingBlobHashes: () => Buffer[],
392
+ fillResults: (blobs: BlobJson[]) => Promise<Blob[]>,
393
+ ctx: { blockHash: string; blobHashes: string[] },
394
+ ): Promise<void> {
395
+ const { l1ConsensusHostUrls } = this.config;
396
+ if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
397
+ return;
398
+ }
399
+
400
+ const slotNumber = await getSlotNumber();
401
+ if (!slotNumber) {
402
+ return;
403
+ }
404
+
405
+ for (let l1ConsensusHostIndex = 0; l1ConsensusHostIndex < l1ConsensusHostUrls.length; l1ConsensusHostIndex++) {
406
+ const missingHashes = getMissingBlobHashes();
407
+ if (missingHashes.length === 0) {
408
+ break;
409
+ }
410
+
411
+ // Skip non-supernode hosts if we've already detected supernodes
412
+ if (this.superNodeHostIndexes && !this.superNodeHostIndexes.has(l1ConsensusHostIndex)) {
413
+ this.log.trace(`Skipping non-supernode consensus host`, {
414
+ l1ConsensusHostUrl: l1ConsensusHostUrls[l1ConsensusHostIndex],
415
+ });
416
+ continue;
417
+ }
418
+
419
+ const l1ConsensusHostUrl = l1ConsensusHostUrls[l1ConsensusHostIndex];
420
+ this.log.trace(`Attempting to get ${missingHashes.length} blobs from consensus host`, {
421
+ slotNumber,
422
+ l1ConsensusHostUrl,
423
+ ...ctx,
424
+ });
425
+ const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex, missingHashes);
426
+ const result = await fillResults(blobs);
427
+ this.log.debug(
428
+ `Got ${blobs.length} blobs from consensus host (total: ${result.length}/${ctx.blobHashes.length})`,
429
+ { slotNumber, l1ConsensusHostUrl, ...ctx },
430
+ );
431
+ }
432
+ }
433
+
350
434
  /**
351
435
  * Try all filestores once (shuffled for load distribution).
352
436
  * @param getMissingBlobHashes - Function to get remaining blob hashes to fetch
@@ -462,16 +546,17 @@ export class HttpBlobClient implements BlobClientInterface {
462
546
  baseUrl += `?${params.toString()}`;
463
547
  }
464
548
 
465
- const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
466
- this.log.debug(`Fetching blob sidecar for ${blockHashOrSlot}`, { url, ...options });
467
- return this.fetch(url, options);
549
+ const { url, logSafeUrl, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
550
+ this.log.debug(`Fetching blob sidecar for ${blockHashOrSlot}`, { url: logSafeUrl, ...options });
551
+ // No retry here — this is called inside the main retry loop in getBlobSidecar
552
+ return fetch(url, options);
468
553
  }
469
554
 
470
555
  private async getLatestSlotNumber(hostUrl: string, l1ConsensusHostIndex?: number): Promise<number | undefined> {
471
556
  try {
472
557
  const baseUrl = `${hostUrl}/eth/v1/beacon/headers/head`;
473
- const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
474
- this.log.debug(`Fetching latest slot number`, { url, ...options });
558
+ const { url, logSafeUrl, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
559
+ this.log.debug(`Fetching latest slot number`, { url: logSafeUrl, ...options });
475
560
  const res = await this.fetch(url, options);
476
561
  if (res.ok) {
477
562
  const body = await res.json();
@@ -532,7 +617,7 @@ export class HttpBlobClient implements BlobClientInterface {
532
617
  }
533
618
 
534
619
  const client = createPublicClient({
535
- transport: fallback(l1RpcUrls.map(url => http(url, { batch: false }))),
620
+ transport: makeL1HttpTransport(l1RpcUrls, { timeout: this.config.l1HttpTimeoutMS }),
536
621
  });
537
622
  try {
538
623
  const res: RpcBlock = await client.request({
@@ -734,12 +819,16 @@ function getBeaconNodeFetchOptions(url: string, config: BlobClientConfig, l1Cons
734
819
  l1ConsensusHostApiKeyHeaders[l1ConsensusHostIndex];
735
820
 
736
821
  let formattedUrl = url;
822
+ let logSafeUrl = url;
737
823
  if (l1ConsensusHostApiKey && l1ConsensusHostApiKey.getValue() !== '' && !l1ConsensusHostApiKeyHeader) {
738
- formattedUrl += `${formattedUrl.includes('?') ? '&' : '?'}key=${l1ConsensusHostApiKey.getValue()}`;
824
+ const separator = formattedUrl.includes('?') ? '&' : '?';
825
+ formattedUrl += `${separator}key=${l1ConsensusHostApiKey.getValue()}`;
826
+ logSafeUrl += `${separator}key=[REDACTED]`;
739
827
  }
740
828
 
741
829
  return {
742
830
  url: formattedUrl,
831
+ logSafeUrl,
743
832
  ...(l1ConsensusHostApiKey &&
744
833
  l1ConsensusHostApiKeyHeader && {
745
834
  headers: {
@@ -6,9 +6,7 @@ import type { Blob } from '@aztec/blob-lib';
6
6
  export interface GetBlobSidecarOptions {
7
7
  /**
8
8
  * True if the archiver is catching up (historical sync), false if near tip.
9
- * This affects source ordering:
10
- * - Historical: FileStore first (data should exist), then L1 consensus, then archive (eg. blobscan)
11
- * - Near tip: FileStore first with no retries (data should exist), L1 consensus second (freshest data), then FileStore with retries, then archive (eg. blobscan)
9
+ * Historical sync uses a shorter retry backoff since blobs should already exist.
12
10
  */
13
11
  isHistoricalSync?: boolean;
14
12
  /**