@aztec/blob-client 0.0.1-commit.7035c9bd6 → 0.0.1-commit.71324e566
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/client/config.d.ts +5 -1
- package/dest/client/config.d.ts.map +1 -1
- package/dest/client/config.js +12 -2
- package/dest/client/factory.d.ts +1 -1
- package/dest/client/factory.d.ts.map +1 -1
- package/dest/client/factory.js +7 -1
- package/dest/client/http.d.ts +11 -9
- package/dest/client/http.d.ts.map +1 -1
- package/dest/client/http.js +172 -94
- package/dest/client/interface.d.ts +2 -4
- package/dest/client/interface.d.ts.map +1 -1
- package/dest/filestore/factory.d.ts +5 -4
- package/dest/filestore/factory.d.ts.map +1 -1
- package/dest/filestore/factory.js +4 -4
- package/package.json +7 -7
- package/src/client/config.ts +18 -2
- package/src/client/factory.ts +8 -1
- package/src/client/http.ts +188 -100
- package/src/client/interface.ts +1 -3
- package/src/filestore/factory.ts +7 -2
|
@@ -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,
|
|
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;
|
|
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.
|
|
3
|
+
"version": "0.0.1-commit.71324e566",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": "./dest/client/bin/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -56,12 +56,12 @@
|
|
|
56
56
|
]
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@aztec/blob-lib": "0.0.1-commit.
|
|
60
|
-
"@aztec/ethereum": "0.0.1-commit.
|
|
61
|
-
"@aztec/foundation": "0.0.1-commit.
|
|
62
|
-
"@aztec/kv-store": "0.0.1-commit.
|
|
63
|
-
"@aztec/stdlib": "0.0.1-commit.
|
|
64
|
-
"@aztec/telemetry-client": "0.0.1-commit.
|
|
59
|
+
"@aztec/blob-lib": "0.0.1-commit.71324e566",
|
|
60
|
+
"@aztec/ethereum": "0.0.1-commit.71324e566",
|
|
61
|
+
"@aztec/foundation": "0.0.1-commit.71324e566",
|
|
62
|
+
"@aztec/kv-store": "0.0.1-commit.71324e566",
|
|
63
|
+
"@aztec/stdlib": "0.0.1-commit.71324e566",
|
|
64
|
+
"@aztec/telemetry-client": "0.0.1-commit.71324e566",
|
|
65
65
|
"express": "^4.21.2",
|
|
66
66
|
"snappy": "^7.2.2",
|
|
67
67
|
"source-map-support": "^0.5.21",
|
package/src/client/config.ts
CHANGED
|
@@ -59,6 +59,12 @@ export interface BlobClientConfig extends BlobArchiveApiConfig {
|
|
|
59
59
|
|
|
60
60
|
/** Timeout for HTTP requests to the L1 RPC node in ms. */
|
|
61
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;
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
export const blobClientConfigMapping: ConfigMappingsType<BlobClientConfig> = {
|
|
@@ -87,7 +93,7 @@ export const blobClientConfigMapping: ConfigMappingsType<BlobClientConfig> = {
|
|
|
87
93
|
blobSinkMapSizeKb: {
|
|
88
94
|
env: 'BLOB_SINK_MAP_SIZE_KB',
|
|
89
95
|
description: 'The maximum possible size of the blob sink DB in KB. Overwrites the general dataStoreMapSizeKb.',
|
|
90
|
-
parseEnv: (val: string
|
|
96
|
+
parseEnv: (val: string) => +val,
|
|
91
97
|
},
|
|
92
98
|
blobAllowEmptySources: {
|
|
93
99
|
env: 'BLOB_ALLOW_EMPTY_SOURCES',
|
|
@@ -110,13 +116,23 @@ export const blobClientConfigMapping: ConfigMappingsType<BlobClientConfig> = {
|
|
|
110
116
|
blobHealthcheckUploadIntervalMinutes: {
|
|
111
117
|
env: 'BLOB_HEALTHCHECK_UPLOAD_INTERVAL_MINUTES',
|
|
112
118
|
description: 'Interval in minutes for uploading healthcheck file to file store (default: 60 = 1 hour)',
|
|
113
|
-
parseEnv: (val: string
|
|
119
|
+
parseEnv: (val: string) => +val,
|
|
114
120
|
},
|
|
115
121
|
l1HttpTimeoutMS: {
|
|
116
122
|
env: 'ETHEREUM_HTTP_TIMEOUT_MS',
|
|
117
123
|
description: 'Timeout for HTTP requests to the L1 RPC node in ms.',
|
|
118
124
|
...optionalNumberConfigHelper(),
|
|
119
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
|
+
},
|
|
120
136
|
...blobArchiveApiConfigMappings,
|
|
121
137
|
};
|
|
122
138
|
|
package/src/client/factory.ts
CHANGED
|
@@ -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
|
|
package/src/client/http.ts
CHANGED
|
@@ -30,6 +30,9 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
30
30
|
/** Cached beacon slot duration in seconds. Fetched once at startup. */
|
|
31
31
|
private beaconSecondsPerSlot?: number;
|
|
32
32
|
|
|
33
|
+
/** Indexes of consensus hosts that serve blob sidecars (supernodes). Populated by testSources(). */
|
|
34
|
+
private superNodeHostIndexes?: Set<number>;
|
|
35
|
+
|
|
33
36
|
constructor(
|
|
34
37
|
config?: BlobClientConfig,
|
|
35
38
|
private readonly opts: {
|
|
@@ -95,44 +98,75 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
95
98
|
const archiveUrl = this.archiveClient?.getBaseUrl();
|
|
96
99
|
this.log.info(`Testing configured blob sources`, { l1ConsensusHostUrls, archiveUrl });
|
|
97
100
|
|
|
98
|
-
let
|
|
101
|
+
let consensusSuperNodes = 0;
|
|
102
|
+
let consensusNonSuperNodes = 0;
|
|
103
|
+
let archiveSources = 0;
|
|
104
|
+
let blobSinks = 0;
|
|
105
|
+
|
|
106
|
+
const detectedSuperNodes = new Set<number>();
|
|
99
107
|
|
|
100
108
|
if (l1ConsensusHostUrls && l1ConsensusHostUrls.length > 0) {
|
|
101
109
|
for (let l1ConsensusHostIndex = 0; l1ConsensusHostIndex < l1ConsensusHostUrls.length; l1ConsensusHostIndex++) {
|
|
102
110
|
const l1ConsensusHostUrl = l1ConsensusHostUrls[l1ConsensusHostIndex];
|
|
103
111
|
try {
|
|
104
112
|
const { url, ...options } = getBeaconNodeFetchOptions(
|
|
105
|
-
`${l1ConsensusHostUrl}/eth/v1/beacon/headers`,
|
|
113
|
+
`${l1ConsensusHostUrl}/eth/v1/beacon/headers/head`,
|
|
106
114
|
this.config,
|
|
107
115
|
l1ConsensusHostIndex,
|
|
108
116
|
);
|
|
109
117
|
const res = await this.fetch(url, options);
|
|
110
|
-
if (res.ok) {
|
|
111
|
-
this.log.info(`L1 consensus host is reachable`, { l1ConsensusHostUrl });
|
|
112
|
-
successfulSourceCount++;
|
|
113
|
-
} else {
|
|
118
|
+
if (!res.ok) {
|
|
114
119
|
this.log.error(`Failure reaching L1 consensus host: ${res.statusText} (${res.status})`, {
|
|
115
120
|
l1ConsensusHostUrl,
|
|
116
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++;
|
|
117
153
|
}
|
|
118
154
|
} catch (err) {
|
|
119
155
|
this.log.error(`Error reaching L1 consensus host`, err, { l1ConsensusHostUrl });
|
|
120
156
|
}
|
|
121
157
|
}
|
|
122
|
-
} else {
|
|
123
|
-
this.log.warn('No L1 consensus host urls configured');
|
|
124
158
|
}
|
|
125
159
|
|
|
160
|
+
this.superNodeHostIndexes = detectedSuperNodes;
|
|
161
|
+
|
|
126
162
|
if (this.archiveClient) {
|
|
127
163
|
try {
|
|
128
164
|
const latest = await this.archiveClient.getLatestBlock();
|
|
129
165
|
this.log.info(`Archive client is reachable and synced to L1 block ${latest.number}`, { latest, archiveUrl });
|
|
130
|
-
|
|
166
|
+
archiveSources++;
|
|
131
167
|
} catch (err) {
|
|
132
168
|
this.log.error(`Error reaching archive client`, err, { archiveUrl });
|
|
133
169
|
}
|
|
134
|
-
} else {
|
|
135
|
-
this.log.warn('No archive client configured');
|
|
136
170
|
}
|
|
137
171
|
|
|
138
172
|
if (this.fileStoreClients.length > 0) {
|
|
@@ -141,7 +175,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
141
175
|
const accessible = await fileStoreClient.testConnection();
|
|
142
176
|
if (accessible) {
|
|
143
177
|
this.log.info(`FileStore is reachable`, { url: fileStoreClient.getBaseUrl() });
|
|
144
|
-
|
|
178
|
+
blobSinks++;
|
|
145
179
|
} else {
|
|
146
180
|
this.log.warn(`FileStore is not accessible`, { url: fileStoreClient.getBaseUrl() });
|
|
147
181
|
}
|
|
@@ -151,12 +185,24 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
151
185
|
}
|
|
152
186
|
}
|
|
153
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
|
+
|
|
154
196
|
if (successfulSourceCount === 0) {
|
|
155
197
|
if (this.config.blobAllowEmptySources) {
|
|
156
|
-
this.log.warn(
|
|
198
|
+
this.log.warn(summary);
|
|
157
199
|
} else {
|
|
158
|
-
throw new Error(
|
|
200
|
+
throw new Error(summary);
|
|
159
201
|
}
|
|
202
|
+
} else if (consensusSuperNodes === 0) {
|
|
203
|
+
this.log.warn(summary);
|
|
204
|
+
} else {
|
|
205
|
+
this.log.info(summary);
|
|
160
206
|
}
|
|
161
207
|
}
|
|
162
208
|
|
|
@@ -182,18 +228,15 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
182
228
|
}
|
|
183
229
|
|
|
184
230
|
/**
|
|
185
|
-
* Get the blob sidecar
|
|
186
|
-
*
|
|
187
|
-
* If requesting from the blob client, we send the blobkHash
|
|
188
|
-
* If requesting from the beacon node, we send the slot number
|
|
231
|
+
* Get the blob sidecar.
|
|
189
232
|
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
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`.
|
|
193
236
|
*
|
|
194
237
|
* @param blockHash - The block hash
|
|
195
238
|
* @param blobHashes - The blob hashes to fetch
|
|
196
|
-
* @param opts - Options
|
|
239
|
+
* @param opts - Options for slot resolution
|
|
197
240
|
* @returns The blobs
|
|
198
241
|
*/
|
|
199
242
|
public async getBlobSidecar(
|
|
@@ -206,12 +249,11 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
206
249
|
return [];
|
|
207
250
|
}
|
|
208
251
|
|
|
209
|
-
const isHistoricalSync = opts?.isHistoricalSync ?? false;
|
|
210
252
|
// Accumulate blobs across sources, preserving order and handling duplicates
|
|
211
253
|
// resultBlobs[i] will contain the blob for blobHashes[i], or undefined if not yet found
|
|
212
254
|
const resultBlobs: (Blob | undefined)[] = new Array(blobHashes.length).fill(undefined);
|
|
213
255
|
|
|
214
|
-
// Helper to get
|
|
256
|
+
// Helper to get missing blob hashes that we still need to fetch
|
|
215
257
|
const getMissingBlobHashes = (): Buffer[] =>
|
|
216
258
|
blobHashes
|
|
217
259
|
.map((bh, i) => (resultBlobs[i] === undefined ? bh : undefined))
|
|
@@ -240,84 +282,60 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
240
282
|
return blobs;
|
|
241
283
|
};
|
|
242
284
|
|
|
243
|
-
const { l1ConsensusHostUrls } = this.config;
|
|
244
|
-
|
|
245
285
|
const ctx = { blockHash, blobHashes: blobHashes.map(bufferToHex) };
|
|
246
286
|
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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;
|
|
252
294
|
}
|
|
253
|
-
|
|
295
|
+
return slotNumber;
|
|
296
|
+
};
|
|
254
297
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const consensusCtx = { l1ConsensusHostUrls, ...ctx };
|
|
259
|
-
this.log.trace(`Attempting to get slot number for block hash`, consensusCtx);
|
|
260
|
-
const slotNumber = await this.getSlotNumber(blockHash, opts?.parentBeaconBlockRoot, opts?.l1BlockTimestamp);
|
|
261
|
-
this.log.debug(`Got slot number ${slotNumber} from consensus host for querying blobs`, consensusCtx);
|
|
262
|
-
|
|
263
|
-
if (slotNumber) {
|
|
264
|
-
let l1ConsensusHostUrl: string;
|
|
265
|
-
for (let l1ConsensusHostIndex = 0; l1ConsensusHostIndex < l1ConsensusHostUrls.length; l1ConsensusHostIndex++) {
|
|
266
|
-
const missingHashes = getMissingBlobHashes();
|
|
267
|
-
if (missingHashes.length === 0) {
|
|
268
|
-
break;
|
|
269
|
-
}
|
|
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);
|
|
270
301
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
slotNumber,
|
|
274
|
-
l1ConsensusHostUrl,
|
|
275
|
-
...ctx,
|
|
276
|
-
});
|
|
277
|
-
const blobs = await this.getBlobsFromHost(
|
|
278
|
-
l1ConsensusHostUrl,
|
|
279
|
-
slotNumber,
|
|
280
|
-
l1ConsensusHostIndex,
|
|
281
|
-
getMissingBlobHashes(),
|
|
282
|
-
);
|
|
283
|
-
const result = await fillResults(blobs);
|
|
284
|
-
this.log.debug(
|
|
285
|
-
`Got ${blobs.length} blobs from consensus host (total: ${result.length}/${blobHashes.length})`,
|
|
286
|
-
{ slotNumber, l1ConsensusHostUrl, ...ctx },
|
|
287
|
-
);
|
|
288
|
-
if (result.length === blobHashes.length) {
|
|
289
|
-
return returnWithCallback(result);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
302
|
+
const preferFilestores = this.config.blobPreferFilestores ?? false;
|
|
303
|
+
const [trySourceA, trySourceB] = preferFilestores ? [tryFilestores, tryConsensus] : [tryConsensus, tryFilestores];
|
|
294
304
|
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
315
332
|
}
|
|
316
333
|
|
|
317
|
-
|
|
318
|
-
|
|
334
|
+
// Archive fallback
|
|
335
|
+
const missingAfterPrimary = getMissingBlobHashes();
|
|
336
|
+
if (missingAfterPrimary.length > 0 && this.archiveClient) {
|
|
319
337
|
const archiveCtx = { archiveUrl: this.archiveClient.getBaseUrl(), ...ctx };
|
|
320
|
-
this.log.trace(`Attempting to get ${
|
|
338
|
+
this.log.trace(`Attempting to get ${missingAfterPrimary.length} blobs from archive`, archiveCtx);
|
|
321
339
|
const allBlobs = await this.archiveClient.getBlobsFromBlock(blockHash);
|
|
322
340
|
if (!allBlobs) {
|
|
323
341
|
this.log.debug('No blobs found from archive client', archiveCtx);
|
|
@@ -339,7 +357,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
339
357
|
this.log.warn(
|
|
340
358
|
`Failed to fetch all blobs for ${blockHash} from all blob sources (got ${result.length}/${blobHashes.length})`,
|
|
341
359
|
{
|
|
342
|
-
l1ConsensusHostUrls,
|
|
360
|
+
l1ConsensusHostUrls: this.config.l1ConsensusHostUrls,
|
|
343
361
|
archiveUrl: this.archiveClient?.getBaseUrl(),
|
|
344
362
|
fileStoreUrls: this.fileStoreClients.map(c => c.getBaseUrl()),
|
|
345
363
|
},
|
|
@@ -348,6 +366,71 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
348
366
|
return returnWithCallback(result);
|
|
349
367
|
}
|
|
350
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
|
+
|
|
351
434
|
/**
|
|
352
435
|
* Try all filestores once (shuffled for load distribution).
|
|
353
436
|
* @param getMissingBlobHashes - Function to get remaining blob hashes to fetch
|
|
@@ -463,16 +546,17 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
463
546
|
baseUrl += `?${params.toString()}`;
|
|
464
547
|
}
|
|
465
548
|
|
|
466
|
-
const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
|
|
467
|
-
this.log.debug(`Fetching blob sidecar for ${blockHashOrSlot}`, { url, ...options });
|
|
468
|
-
|
|
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);
|
|
469
553
|
}
|
|
470
554
|
|
|
471
555
|
private async getLatestSlotNumber(hostUrl: string, l1ConsensusHostIndex?: number): Promise<number | undefined> {
|
|
472
556
|
try {
|
|
473
557
|
const baseUrl = `${hostUrl}/eth/v1/beacon/headers/head`;
|
|
474
|
-
const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
|
|
475
|
-
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 });
|
|
476
560
|
const res = await this.fetch(url, options);
|
|
477
561
|
if (res.ok) {
|
|
478
562
|
const body = await res.json();
|
|
@@ -735,12 +819,16 @@ function getBeaconNodeFetchOptions(url: string, config: BlobClientConfig, l1Cons
|
|
|
735
819
|
l1ConsensusHostApiKeyHeaders[l1ConsensusHostIndex];
|
|
736
820
|
|
|
737
821
|
let formattedUrl = url;
|
|
822
|
+
let logSafeUrl = url;
|
|
738
823
|
if (l1ConsensusHostApiKey && l1ConsensusHostApiKey.getValue() !== '' && !l1ConsensusHostApiKeyHeader) {
|
|
739
|
-
|
|
824
|
+
const separator = formattedUrl.includes('?') ? '&' : '?';
|
|
825
|
+
formattedUrl += `${separator}key=${l1ConsensusHostApiKey.getValue()}`;
|
|
826
|
+
logSafeUrl += `${separator}key=[REDACTED]`;
|
|
740
827
|
}
|
|
741
828
|
|
|
742
829
|
return {
|
|
743
830
|
url: formattedUrl,
|
|
831
|
+
logSafeUrl,
|
|
744
832
|
...(l1ConsensusHostApiKey &&
|
|
745
833
|
l1ConsensusHostApiKeyHeader && {
|
|
746
834
|
headers: {
|
package/src/client/interface.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
/**
|