@aztec/blob-client 0.0.1-commit.88e6f9396 → 0.0.1-commit.8c0b8ff
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/http.d.ts +3 -9
- package/dest/client/http.d.ts.map +1 -1
- package/dest/client/http.js +88 -106
- package/dest/client/interface.d.ts +1 -12
- package/dest/client/interface.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/client/http.ts +83 -131
- package/src/client/interface.ts +0 -11
package/dest/client/http.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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,
|
|
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;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;
|
|
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"}
|
package/dest/client/http.js
CHANGED
|
@@ -18,8 +18,6 @@ export class HttpBlobClient {
|
|
|
18
18
|
fileStoreUploadClient;
|
|
19
19
|
disabled;
|
|
20
20
|
healthcheckUploadIntervalId;
|
|
21
|
-
/** Cached beacon genesis time (seconds since Unix epoch). Fetched once at startup. */ beaconGenesisTime;
|
|
22
|
-
/** Cached beacon slot duration in seconds. Fetched once at startup. */ beaconSecondsPerSlot;
|
|
23
21
|
constructor(config, opts = {}){
|
|
24
22
|
this.opts = opts;
|
|
25
23
|
this.disabled = false;
|
|
@@ -69,22 +67,50 @@ export class HttpBlobClient {
|
|
|
69
67
|
l1ConsensusHostUrls,
|
|
70
68
|
archiveUrl
|
|
71
69
|
});
|
|
72
|
-
let
|
|
70
|
+
let consensusSuperNodes = 0;
|
|
71
|
+
let consensusNonSuperNodes = 0;
|
|
72
|
+
let archiveSources = 0;
|
|
73
|
+
let blobSinks = 0;
|
|
73
74
|
if (l1ConsensusHostUrls && l1ConsensusHostUrls.length > 0) {
|
|
74
75
|
for(let l1ConsensusHostIndex = 0; l1ConsensusHostIndex < l1ConsensusHostUrls.length; l1ConsensusHostIndex++){
|
|
75
76
|
const l1ConsensusHostUrl = l1ConsensusHostUrls[l1ConsensusHostIndex];
|
|
76
77
|
try {
|
|
77
|
-
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);
|
|
78
79
|
const res = await this.fetch(url, options);
|
|
79
|
-
if (res.ok) {
|
|
80
|
-
this.log.
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
this.log.error(`Failure reaching L1 consensus host: ${res.statusText} (${res.status})`, {
|
|
81
82
|
l1ConsensusHostUrl
|
|
82
83
|
});
|
|
83
|
-
|
|
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
|
+
}
|
|
84
109
|
} else {
|
|
85
|
-
this.log.
|
|
110
|
+
this.log.info(`L1 consensus host is reachable but could not determine head slot`, {
|
|
86
111
|
l1ConsensusHostUrl
|
|
87
112
|
});
|
|
113
|
+
consensusNonSuperNodes++;
|
|
88
114
|
}
|
|
89
115
|
} catch (err) {
|
|
90
116
|
this.log.error(`Error reaching L1 consensus host`, err, {
|
|
@@ -92,8 +118,6 @@ export class HttpBlobClient {
|
|
|
92
118
|
});
|
|
93
119
|
}
|
|
94
120
|
}
|
|
95
|
-
} else {
|
|
96
|
-
this.log.warn('No L1 consensus host urls configured');
|
|
97
121
|
}
|
|
98
122
|
if (this.archiveClient) {
|
|
99
123
|
try {
|
|
@@ -102,14 +126,12 @@ export class HttpBlobClient {
|
|
|
102
126
|
latest,
|
|
103
127
|
archiveUrl
|
|
104
128
|
});
|
|
105
|
-
|
|
129
|
+
archiveSources++;
|
|
106
130
|
} catch (err) {
|
|
107
131
|
this.log.error(`Error reaching archive client`, err, {
|
|
108
132
|
archiveUrl
|
|
109
133
|
});
|
|
110
134
|
}
|
|
111
|
-
} else {
|
|
112
|
-
this.log.warn('No archive client configured');
|
|
113
135
|
}
|
|
114
136
|
if (this.fileStoreClients.length > 0) {
|
|
115
137
|
for (const fileStoreClient of this.fileStoreClients){
|
|
@@ -119,7 +141,7 @@ export class HttpBlobClient {
|
|
|
119
141
|
this.log.info(`FileStore is reachable`, {
|
|
120
142
|
url: fileStoreClient.getBaseUrl()
|
|
121
143
|
});
|
|
122
|
-
|
|
144
|
+
blobSinks++;
|
|
123
145
|
} else {
|
|
124
146
|
this.log.warn(`FileStore is not accessible`, {
|
|
125
147
|
url: fileStoreClient.getBaseUrl()
|
|
@@ -132,12 +154,22 @@ export class HttpBlobClient {
|
|
|
132
154
|
}
|
|
133
155
|
}
|
|
134
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
|
+
}
|
|
135
163
|
if (successfulSourceCount === 0) {
|
|
136
164
|
if (this.config.blobAllowEmptySources) {
|
|
137
|
-
this.log.warn(
|
|
165
|
+
this.log.warn(summary);
|
|
138
166
|
} else {
|
|
139
|
-
throw new Error(
|
|
167
|
+
throw new Error(summary);
|
|
140
168
|
}
|
|
169
|
+
} else if (consensusSuperNodes === 0) {
|
|
170
|
+
this.log.warn(summary);
|
|
171
|
+
} else {
|
|
172
|
+
this.log.info(summary);
|
|
141
173
|
}
|
|
142
174
|
}
|
|
143
175
|
async sendBlobsToFilestore(blobs) {
|
|
@@ -223,7 +255,7 @@ export class HttpBlobClient {
|
|
|
223
255
|
...ctx
|
|
224
256
|
};
|
|
225
257
|
this.log.trace(`Attempting to get slot number for block hash`, consensusCtx);
|
|
226
|
-
const slotNumber = await this.getSlotNumber(blockHash
|
|
258
|
+
const slotNumber = await this.getSlotNumber(blockHash);
|
|
227
259
|
this.log.debug(`Got slot number ${slotNumber} from consensus host for querying blobs`, consensusCtx);
|
|
228
260
|
if (slotNumber) {
|
|
229
261
|
let l1ConsensusHostUrl;
|
|
@@ -238,7 +270,7 @@ export class HttpBlobClient {
|
|
|
238
270
|
l1ConsensusHostUrl,
|
|
239
271
|
...ctx
|
|
240
272
|
});
|
|
241
|
-
const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex
|
|
273
|
+
const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex);
|
|
242
274
|
const result = await fillResults(blobs);
|
|
243
275
|
this.log.debug(`Got ${blobs.length} blobs from consensus host (total: ${result.length}/${blobHashes.length})`, {
|
|
244
276
|
slotNumber,
|
|
@@ -337,14 +369,14 @@ export class HttpBlobClient {
|
|
|
337
369
|
}
|
|
338
370
|
}
|
|
339
371
|
async getBlobSidecarFrom(hostUrl, blockHashOrSlot, blobHashes = [], l1ConsensusHostIndex) {
|
|
340
|
-
const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex
|
|
372
|
+
const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
|
|
341
373
|
return (await processFetchedBlobs(blobs, blobHashes, this.log)).filter((b)=>b !== undefined);
|
|
342
374
|
}
|
|
343
|
-
async getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex
|
|
375
|
+
async getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex) {
|
|
344
376
|
try {
|
|
345
|
-
let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex
|
|
377
|
+
let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
|
|
346
378
|
if (res.ok) {
|
|
347
|
-
return
|
|
379
|
+
return parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
348
380
|
}
|
|
349
381
|
if (res.status === 404 && typeof blockHashOrSlot === 'number') {
|
|
350
382
|
const latestSlot = await this.getLatestSlotNumber(hostUrl, l1ConsensusHostIndex);
|
|
@@ -357,9 +389,9 @@ export class HttpBlobClient {
|
|
|
357
389
|
let currentSlot = blockHashOrSlot + 1;
|
|
358
390
|
while(res.status === 404 && maxRetries > 0 && latestSlot !== undefined && currentSlot <= latestSlot){
|
|
359
391
|
this.log.debug(`Trying slot ${currentSlot}`);
|
|
360
|
-
res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex
|
|
392
|
+
res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex);
|
|
361
393
|
if (res.ok) {
|
|
362
|
-
return
|
|
394
|
+
return parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
363
395
|
}
|
|
364
396
|
currentSlot++;
|
|
365
397
|
maxRetries--;
|
|
@@ -376,15 +408,8 @@ export class HttpBlobClient {
|
|
|
376
408
|
return [];
|
|
377
409
|
}
|
|
378
410
|
}
|
|
379
|
-
fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex
|
|
380
|
-
|
|
381
|
-
if (blobHashes && blobHashes.length > 0) {
|
|
382
|
-
const params = new URLSearchParams();
|
|
383
|
-
for (const hash of blobHashes){
|
|
384
|
-
params.append('versioned_hashes', `0x${hash.toString('hex')}`);
|
|
385
|
-
}
|
|
386
|
-
baseUrl += `?${params.toString()}`;
|
|
387
|
-
}
|
|
411
|
+
fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex) {
|
|
412
|
+
const baseUrl = `${hostUrl}/eth/v1/beacon/blob_sidecars/${blockHashOrSlot}`;
|
|
388
413
|
const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
|
|
389
414
|
this.log.debug(`Fetching blob sidecar for ${blockHashOrSlot}`, {
|
|
390
415
|
url,
|
|
@@ -430,45 +455,36 @@ export class HttpBlobClient {
|
|
|
430
455
|
*
|
|
431
456
|
* @param blockHash - The block hash
|
|
432
457
|
* @returns The slot number
|
|
433
|
-
*/ async getSlotNumber(blockHash
|
|
458
|
+
*/ async getSlotNumber(blockHash) {
|
|
434
459
|
const { l1ConsensusHostUrls, l1RpcUrls } = this.config;
|
|
435
460
|
if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
|
|
436
461
|
this.log.debug('No consensus host url configured');
|
|
437
462
|
return undefined;
|
|
438
463
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
this.log.debug(`Computed slot ${slot} from L1 block timestamp`, {
|
|
443
|
-
l1BlockTimestamp
|
|
444
|
-
});
|
|
445
|
-
return slot;
|
|
464
|
+
if (!l1RpcUrls || l1RpcUrls.length === 0) {
|
|
465
|
+
this.log.debug('No execution host url configured');
|
|
466
|
+
return undefined;
|
|
446
467
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
+
]
|
|
457
482
|
});
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
method: 'eth_getBlockByHash',
|
|
461
|
-
params: [
|
|
462
|
-
blockHash,
|
|
463
|
-
/*tx flag*/ false
|
|
464
|
-
]
|
|
465
|
-
});
|
|
466
|
-
if (res.parentBeaconBlockRoot) {
|
|
467
|
-
parentBeaconBlockRoot = res.parentBeaconBlockRoot;
|
|
468
|
-
}
|
|
469
|
-
} catch (err) {
|
|
470
|
-
this.log.error(`Error getting parent beacon block root`, err);
|
|
483
|
+
if (res.parentBeaconBlockRoot) {
|
|
484
|
+
parentBeaconBlockRoot = res.parentBeaconBlockRoot;
|
|
471
485
|
}
|
|
486
|
+
} catch (err) {
|
|
487
|
+
this.log.error(`Error getting parent beacon block root`, err);
|
|
472
488
|
}
|
|
473
489
|
if (!parentBeaconBlockRoot) {
|
|
474
490
|
this.log.error(`No parent beacon block root found for block ${blockHash}`);
|
|
@@ -500,10 +516,8 @@ export class HttpBlobClient {
|
|
|
500
516
|
}
|
|
501
517
|
/**
|
|
502
518
|
* Start the blob client.
|
|
503
|
-
*
|
|
504
|
-
* then uploads the initial healthcheck file (awaited) and starts periodic uploads.
|
|
519
|
+
* Uploads the initial healthcheck file (awaited) and starts periodic uploads.
|
|
505
520
|
*/ async start() {
|
|
506
|
-
await this.fetchBeaconConfig();
|
|
507
521
|
if (!this.fileStoreUploadClient) {
|
|
508
522
|
return;
|
|
509
523
|
}
|
|
@@ -522,40 +536,6 @@ export class HttpBlobClient {
|
|
|
522
536
|
}, intervalMs);
|
|
523
537
|
}
|
|
524
538
|
/**
|
|
525
|
-
* Fetches and caches beacon genesis time and slot duration from the first available consensus host.
|
|
526
|
-
* These static values enable timestamp-based slot resolution, eliminating the per-fetch headers call.
|
|
527
|
-
* Logs a warning and leaves fields undefined if all hosts fail, callers fall back gracefully.
|
|
528
|
-
*/ async fetchBeaconConfig() {
|
|
529
|
-
const { l1ConsensusHostUrls } = this.config;
|
|
530
|
-
if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
for(let i = 0; i < l1ConsensusHostUrls.length; i++){
|
|
534
|
-
try {
|
|
535
|
-
const { url: genesisUrl, ...genesisOptions } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrls[i]}/eth/v1/config/genesis`, this.config, i);
|
|
536
|
-
const { url: specUrl, ...specOptions } = getBeaconNodeFetchOptions(`${l1ConsensusHostUrls[i]}/eth/v1/config/spec`, this.config, i);
|
|
537
|
-
const [genesisRes, specRes] = await Promise.all([
|
|
538
|
-
this.fetch(genesisUrl, genesisOptions),
|
|
539
|
-
this.fetch(specUrl, specOptions)
|
|
540
|
-
]);
|
|
541
|
-
if (genesisRes.ok && specRes.ok) {
|
|
542
|
-
const genesis = await genesisRes.json();
|
|
543
|
-
const spec = await specRes.json();
|
|
544
|
-
this.beaconGenesisTime = BigInt(genesis.data.genesisTime);
|
|
545
|
-
this.beaconSecondsPerSlot = parseInt(spec.data.secondsPerSlot);
|
|
546
|
-
this.log.debug(`Fetched beacon genesis config`, {
|
|
547
|
-
genesisTime: this.beaconGenesisTime,
|
|
548
|
-
secondsPerSlot: this.beaconSecondsPerSlot
|
|
549
|
-
});
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
} catch (err) {
|
|
553
|
-
this.log.warn(`Failed to fetch beacon config from host ${l1ConsensusHostUrls[i]}`, err);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
this.log.warn('Could not fetch beacon genesis config from any consensus host — will use headers call fallback');
|
|
557
|
-
}
|
|
558
|
-
/**
|
|
559
539
|
* Stop the blob client, clearing any periodic tasks.
|
|
560
540
|
*/ stop() {
|
|
561
541
|
if (this.healthcheckUploadIntervalId) {
|
|
@@ -564,9 +544,10 @@ export class HttpBlobClient {
|
|
|
564
544
|
}
|
|
565
545
|
}
|
|
566
546
|
}
|
|
567
|
-
|
|
547
|
+
function parseBlobJsonsFromResponse(response, logger) {
|
|
568
548
|
try {
|
|
569
|
-
|
|
549
|
+
const blobs = response.data.map(parseBlobJson);
|
|
550
|
+
return blobs;
|
|
570
551
|
} catch (err) {
|
|
571
552
|
logger.error(`Error parsing blob json from response`, err);
|
|
572
553
|
return [];
|
|
@@ -576,9 +557,10 @@ async function parseBlobJsonsFromResponse(response, logger) {
|
|
|
576
557
|
// https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getBlobSidecars
|
|
577
558
|
// Here we attempt to parse the response data to Buffer, and check the lengths (via Blob's constructor), to avoid
|
|
578
559
|
// throwing an error down the line when calling Blob.fromJson().
|
|
579
|
-
|
|
580
|
-
const blobBuffer = Buffer.from(
|
|
581
|
-
const
|
|
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);
|
|
582
564
|
return blob.toJSON();
|
|
583
565
|
}
|
|
584
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,
|
|
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;
|
|
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.
|
|
3
|
+
"version": "0.0.1-commit.8c0b8ff",
|
|
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.8c0b8ff",
|
|
60
|
+
"@aztec/ethereum": "0.0.1-commit.8c0b8ff",
|
|
61
|
+
"@aztec/foundation": "0.0.1-commit.8c0b8ff",
|
|
62
|
+
"@aztec/kv-store": "0.0.1-commit.8c0b8ff",
|
|
63
|
+
"@aztec/stdlib": "0.0.1-commit.8c0b8ff",
|
|
64
|
+
"@aztec/telemetry-client": "0.0.1-commit.8c0b8ff",
|
|
65
65
|
"express": "^4.21.2",
|
|
66
66
|
"snappy": "^7.2.2",
|
|
67
67
|
"source-map-support": "^0.5.21",
|
package/src/client/http.ts
CHANGED
|
@@ -25,11 +25,6 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
25
25
|
private disabled = false;
|
|
26
26
|
private healthcheckUploadIntervalId?: NodeJS.Timeout;
|
|
27
27
|
|
|
28
|
-
/** Cached beacon genesis time (seconds since Unix epoch). Fetched once at startup. */
|
|
29
|
-
private beaconGenesisTime?: bigint;
|
|
30
|
-
/** Cached beacon slot duration in seconds. Fetched once at startup. */
|
|
31
|
-
private beaconSecondsPerSlot?: number;
|
|
32
|
-
|
|
33
28
|
constructor(
|
|
34
29
|
config?: BlobClientConfig,
|
|
35
30
|
private readonly opts: {
|
|
@@ -95,44 +90,68 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
95
90
|
const archiveUrl = this.archiveClient?.getBaseUrl();
|
|
96
91
|
this.log.info(`Testing configured blob sources`, { l1ConsensusHostUrls, archiveUrl });
|
|
97
92
|
|
|
98
|
-
let
|
|
93
|
+
let consensusSuperNodes = 0;
|
|
94
|
+
let consensusNonSuperNodes = 0;
|
|
95
|
+
let archiveSources = 0;
|
|
96
|
+
let blobSinks = 0;
|
|
99
97
|
|
|
100
98
|
if (l1ConsensusHostUrls && l1ConsensusHostUrls.length > 0) {
|
|
101
99
|
for (let l1ConsensusHostIndex = 0; l1ConsensusHostIndex < l1ConsensusHostUrls.length; l1ConsensusHostIndex++) {
|
|
102
100
|
const l1ConsensusHostUrl = l1ConsensusHostUrls[l1ConsensusHostIndex];
|
|
103
101
|
try {
|
|
104
102
|
const { url, ...options } = getBeaconNodeFetchOptions(
|
|
105
|
-
`${l1ConsensusHostUrl}/eth/v1/beacon/headers`,
|
|
103
|
+
`${l1ConsensusHostUrl}/eth/v1/beacon/headers/head`,
|
|
106
104
|
this.config,
|
|
107
105
|
l1ConsensusHostIndex,
|
|
108
106
|
);
|
|
109
107
|
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 {
|
|
108
|
+
if (!res.ok) {
|
|
114
109
|
this.log.error(`Failure reaching L1 consensus host: ${res.statusText} (${res.status})`, {
|
|
115
110
|
l1ConsensusHostUrl,
|
|
116
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++;
|
|
117
140
|
}
|
|
118
141
|
} catch (err) {
|
|
119
142
|
this.log.error(`Error reaching L1 consensus host`, err, { l1ConsensusHostUrl });
|
|
120
143
|
}
|
|
121
144
|
}
|
|
122
|
-
} else {
|
|
123
|
-
this.log.warn('No L1 consensus host urls configured');
|
|
124
145
|
}
|
|
125
146
|
|
|
126
147
|
if (this.archiveClient) {
|
|
127
148
|
try {
|
|
128
149
|
const latest = await this.archiveClient.getLatestBlock();
|
|
129
150
|
this.log.info(`Archive client is reachable and synced to L1 block ${latest.number}`, { latest, archiveUrl });
|
|
130
|
-
|
|
151
|
+
archiveSources++;
|
|
131
152
|
} catch (err) {
|
|
132
153
|
this.log.error(`Error reaching archive client`, err, { archiveUrl });
|
|
133
154
|
}
|
|
134
|
-
} else {
|
|
135
|
-
this.log.warn('No archive client configured');
|
|
136
155
|
}
|
|
137
156
|
|
|
138
157
|
if (this.fileStoreClients.length > 0) {
|
|
@@ -141,7 +160,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
141
160
|
const accessible = await fileStoreClient.testConnection();
|
|
142
161
|
if (accessible) {
|
|
143
162
|
this.log.info(`FileStore is reachable`, { url: fileStoreClient.getBaseUrl() });
|
|
144
|
-
|
|
163
|
+
blobSinks++;
|
|
145
164
|
} else {
|
|
146
165
|
this.log.warn(`FileStore is not accessible`, { url: fileStoreClient.getBaseUrl() });
|
|
147
166
|
}
|
|
@@ -151,12 +170,24 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
151
170
|
}
|
|
152
171
|
}
|
|
153
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
|
+
|
|
154
181
|
if (successfulSourceCount === 0) {
|
|
155
182
|
if (this.config.blobAllowEmptySources) {
|
|
156
|
-
this.log.warn(
|
|
183
|
+
this.log.warn(summary);
|
|
157
184
|
} else {
|
|
158
|
-
throw new Error(
|
|
185
|
+
throw new Error(summary);
|
|
159
186
|
}
|
|
187
|
+
} else if (consensusSuperNodes === 0) {
|
|
188
|
+
this.log.warn(summary);
|
|
189
|
+
} else {
|
|
190
|
+
this.log.info(summary);
|
|
160
191
|
}
|
|
161
192
|
}
|
|
162
193
|
|
|
@@ -257,7 +288,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
257
288
|
// The beacon api can query by slot number, so we get that first
|
|
258
289
|
const consensusCtx = { l1ConsensusHostUrls, ...ctx };
|
|
259
290
|
this.log.trace(`Attempting to get slot number for block hash`, consensusCtx);
|
|
260
|
-
const slotNumber = await this.getSlotNumber(blockHash
|
|
291
|
+
const slotNumber = await this.getSlotNumber(blockHash);
|
|
261
292
|
this.log.debug(`Got slot number ${slotNumber} from consensus host for querying blobs`, consensusCtx);
|
|
262
293
|
|
|
263
294
|
if (slotNumber) {
|
|
@@ -274,12 +305,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
274
305
|
l1ConsensusHostUrl,
|
|
275
306
|
...ctx,
|
|
276
307
|
});
|
|
277
|
-
const blobs = await this.getBlobsFromHost(
|
|
278
|
-
l1ConsensusHostUrl,
|
|
279
|
-
slotNumber,
|
|
280
|
-
l1ConsensusHostIndex,
|
|
281
|
-
getMissingBlobHashes(),
|
|
282
|
-
);
|
|
308
|
+
const blobs = await this.getBlobsFromHost(l1ConsensusHostUrl, slotNumber, l1ConsensusHostIndex);
|
|
283
309
|
const result = await fillResults(blobs);
|
|
284
310
|
this.log.debug(
|
|
285
311
|
`Got ${blobs.length} blobs from consensus host (total: ${result.length}/${blobHashes.length})`,
|
|
@@ -398,7 +424,7 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
398
424
|
blobHashes: Buffer[] = [],
|
|
399
425
|
l1ConsensusHostIndex?: number,
|
|
400
426
|
): Promise<Blob[]> {
|
|
401
|
-
const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex
|
|
427
|
+
const blobs = await this.getBlobsFromHost(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
|
|
402
428
|
return (await processFetchedBlobs(blobs, blobHashes, this.log)).filter((b): b is Blob => b !== undefined);
|
|
403
429
|
}
|
|
404
430
|
|
|
@@ -406,12 +432,11 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
406
432
|
hostUrl: string,
|
|
407
433
|
blockHashOrSlot: string | number,
|
|
408
434
|
l1ConsensusHostIndex?: number,
|
|
409
|
-
blobHashes?: Buffer[],
|
|
410
435
|
): Promise<BlobJson[]> {
|
|
411
436
|
try {
|
|
412
|
-
let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex
|
|
437
|
+
let res = await this.fetchBlobSidecars(hostUrl, blockHashOrSlot, l1ConsensusHostIndex);
|
|
413
438
|
if (res.ok) {
|
|
414
|
-
return
|
|
439
|
+
return parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
415
440
|
}
|
|
416
441
|
|
|
417
442
|
if (res.status === 404 && typeof blockHashOrSlot === 'number') {
|
|
@@ -426,9 +451,9 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
426
451
|
let currentSlot = blockHashOrSlot + 1;
|
|
427
452
|
while (res.status === 404 && maxRetries > 0 && latestSlot !== undefined && currentSlot <= latestSlot) {
|
|
428
453
|
this.log.debug(`Trying slot ${currentSlot}`);
|
|
429
|
-
res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex
|
|
454
|
+
res = await this.fetchBlobSidecars(hostUrl, currentSlot, l1ConsensusHostIndex);
|
|
430
455
|
if (res.ok) {
|
|
431
|
-
return
|
|
456
|
+
return parseBlobJsonsFromResponse(await res.json(), this.log);
|
|
432
457
|
}
|
|
433
458
|
currentSlot++;
|
|
434
459
|
maxRetries--;
|
|
@@ -451,17 +476,8 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
451
476
|
hostUrl: string,
|
|
452
477
|
blockHashOrSlot: string | number,
|
|
453
478
|
l1ConsensusHostIndex?: number,
|
|
454
|
-
blobHashes?: Buffer[],
|
|
455
479
|
): Promise<Response> {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
if (blobHashes && blobHashes.length > 0) {
|
|
459
|
-
const params = new URLSearchParams();
|
|
460
|
-
for (const hash of blobHashes) {
|
|
461
|
-
params.append('versioned_hashes', `0x${hash.toString('hex')}`);
|
|
462
|
-
}
|
|
463
|
-
baseUrl += `?${params.toString()}`;
|
|
464
|
-
}
|
|
480
|
+
const baseUrl = `${hostUrl}/eth/v1/beacon/blob_sidecars/${blockHashOrSlot}`;
|
|
465
481
|
|
|
466
482
|
const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
|
|
467
483
|
this.log.debug(`Fetching blob sidecar for ${blockHashOrSlot}`, { url, ...options });
|
|
@@ -503,50 +519,34 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
503
519
|
* @param blockHash - The block hash
|
|
504
520
|
* @returns The slot number
|
|
505
521
|
*/
|
|
506
|
-
private async getSlotNumber(
|
|
507
|
-
blockHash: `0x${string}`,
|
|
508
|
-
parentBeaconBlockRoot?: string,
|
|
509
|
-
l1BlockTimestamp?: bigint,
|
|
510
|
-
): Promise<number | undefined> {
|
|
522
|
+
private async getSlotNumber(blockHash: `0x${string}`): Promise<number | undefined> {
|
|
511
523
|
const { l1ConsensusHostUrls, l1RpcUrls } = this.config;
|
|
512
524
|
if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
|
|
513
525
|
this.log.debug('No consensus host url configured');
|
|
514
526
|
return undefined;
|
|
515
527
|
}
|
|
516
528
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
this.beaconGenesisTime !== undefined &&
|
|
521
|
-
this.beaconSecondsPerSlot !== undefined
|
|
522
|
-
) {
|
|
523
|
-
const slot = Number((l1BlockTimestamp - this.beaconGenesisTime) / BigInt(this.beaconSecondsPerSlot));
|
|
524
|
-
this.log.debug(`Computed slot ${slot} from L1 block timestamp`, { l1BlockTimestamp });
|
|
525
|
-
return slot;
|
|
529
|
+
if (!l1RpcUrls || l1RpcUrls.length === 0) {
|
|
530
|
+
this.log.debug('No execution host url configured');
|
|
531
|
+
return undefined;
|
|
526
532
|
}
|
|
527
533
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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],
|
|
537
543
|
});
|
|
538
|
-
try {
|
|
539
|
-
const res: RpcBlock = await client.request({
|
|
540
|
-
method: 'eth_getBlockByHash',
|
|
541
|
-
params: [blockHash, /*tx flag*/ false],
|
|
542
|
-
});
|
|
543
544
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
} catch (err) {
|
|
548
|
-
this.log.error(`Error getting parent beacon block root`, err);
|
|
545
|
+
if (res.parentBeaconBlockRoot) {
|
|
546
|
+
parentBeaconBlockRoot = res.parentBeaconBlockRoot;
|
|
549
547
|
}
|
|
548
|
+
} catch (err) {
|
|
549
|
+
this.log.error(`Error getting parent beacon block root`, err);
|
|
550
550
|
}
|
|
551
551
|
|
|
552
552
|
if (!parentBeaconBlockRoot) {
|
|
@@ -592,12 +592,9 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
592
592
|
|
|
593
593
|
/**
|
|
594
594
|
* Start the blob client.
|
|
595
|
-
*
|
|
596
|
-
* then uploads the initial healthcheck file (awaited) and starts periodic uploads.
|
|
595
|
+
* Uploads the initial healthcheck file (awaited) and starts periodic uploads.
|
|
597
596
|
*/
|
|
598
597
|
public async start(): Promise<void> {
|
|
599
|
-
await this.fetchBeaconConfig();
|
|
600
|
-
|
|
601
598
|
if (!this.fileStoreUploadClient) {
|
|
602
599
|
return;
|
|
603
600
|
}
|
|
@@ -622,53 +619,6 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
622
619
|
}, intervalMs);
|
|
623
620
|
}
|
|
624
621
|
|
|
625
|
-
/**
|
|
626
|
-
* Fetches and caches beacon genesis time and slot duration from the first available consensus host.
|
|
627
|
-
* These static values enable timestamp-based slot resolution, eliminating the per-fetch headers call.
|
|
628
|
-
* Logs a warning and leaves fields undefined if all hosts fail, callers fall back gracefully.
|
|
629
|
-
*/
|
|
630
|
-
private async fetchBeaconConfig(): Promise<void> {
|
|
631
|
-
const { l1ConsensusHostUrls } = this.config;
|
|
632
|
-
if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) {
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
for (let i = 0; i < l1ConsensusHostUrls.length; i++) {
|
|
637
|
-
try {
|
|
638
|
-
const { url: genesisUrl, ...genesisOptions } = getBeaconNodeFetchOptions(
|
|
639
|
-
`${l1ConsensusHostUrls[i]}/eth/v1/config/genesis`,
|
|
640
|
-
this.config,
|
|
641
|
-
i,
|
|
642
|
-
);
|
|
643
|
-
const { url: specUrl, ...specOptions } = getBeaconNodeFetchOptions(
|
|
644
|
-
`${l1ConsensusHostUrls[i]}/eth/v1/config/spec`,
|
|
645
|
-
this.config,
|
|
646
|
-
i,
|
|
647
|
-
);
|
|
648
|
-
|
|
649
|
-
const [genesisRes, specRes] = await Promise.all([
|
|
650
|
-
this.fetch(genesisUrl, genesisOptions),
|
|
651
|
-
this.fetch(specUrl, specOptions),
|
|
652
|
-
]);
|
|
653
|
-
|
|
654
|
-
if (genesisRes.ok && specRes.ok) {
|
|
655
|
-
const genesis = await genesisRes.json();
|
|
656
|
-
const spec = await specRes.json();
|
|
657
|
-
this.beaconGenesisTime = BigInt(genesis.data.genesisTime);
|
|
658
|
-
this.beaconSecondsPerSlot = parseInt(spec.data.secondsPerSlot);
|
|
659
|
-
this.log.debug(`Fetched beacon genesis config`, {
|
|
660
|
-
genesisTime: this.beaconGenesisTime,
|
|
661
|
-
secondsPerSlot: this.beaconSecondsPerSlot,
|
|
662
|
-
});
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
} catch (err) {
|
|
666
|
-
this.log.warn(`Failed to fetch beacon config from host ${l1ConsensusHostUrls[i]}`, err);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
this.log.warn('Could not fetch beacon genesis config from any consensus host — will use headers call fallback');
|
|
670
|
-
}
|
|
671
|
-
|
|
672
622
|
/**
|
|
673
623
|
* Stop the blob client, clearing any periodic tasks.
|
|
674
624
|
*/
|
|
@@ -680,9 +630,10 @@ export class HttpBlobClient implements BlobClientInterface {
|
|
|
680
630
|
}
|
|
681
631
|
}
|
|
682
632
|
|
|
683
|
-
|
|
633
|
+
function parseBlobJsonsFromResponse(response: any, logger: Logger): BlobJson[] {
|
|
684
634
|
try {
|
|
685
|
-
|
|
635
|
+
const blobs = response.data.map(parseBlobJson);
|
|
636
|
+
return blobs;
|
|
686
637
|
} catch (err) {
|
|
687
638
|
logger.error(`Error parsing blob json from response`, err);
|
|
688
639
|
return [];
|
|
@@ -693,9 +644,10 @@ async function parseBlobJsonsFromResponse(response: any, logger: Logger): Promis
|
|
|
693
644
|
// https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getBlobSidecars
|
|
694
645
|
// Here we attempt to parse the response data to Buffer, and check the lengths (via Blob's constructor), to avoid
|
|
695
646
|
// throwing an error down the line when calling Blob.fromJson().
|
|
696
|
-
|
|
697
|
-
const blobBuffer = Buffer.from(
|
|
698
|
-
const
|
|
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);
|
|
699
651
|
return blob.toJSON();
|
|
700
652
|
}
|
|
701
653
|
|
package/src/client/interface.ts
CHANGED
|
@@ -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 {
|