@@ -2,33 +2,50 @@ import express, { Request, Response } from "express";
22import { createPublicClient , http } from "viem" ;
33import client from "prom-client" ;
44
5- const { ROLLUP_CONTRACT_ADDRESS , ETHEREUM_HOST , NETWORK } = process . env ;
5+ const { ROLLUP_CONTRACT_ADDRESS , ETHEREUM_HOSTS , NETWORK } = process . env ;
66
77//////////////////////////////
88// IMPORTANT: Bump VERSION file when making changes
99//////////////////////////////
1010
11- if ( ! ROLLUP_CONTRACT_ADDRESS || ! ETHEREUM_HOST || ! NETWORK ) {
11+ const ethereumRpcUrls = ( ETHEREUM_HOSTS ?? "" )
12+ . split ( "," )
13+ . map ( ( u : string ) => u . trim ( ) )
14+ . filter ( Boolean ) ;
15+
16+ if ( ! ROLLUP_CONTRACT_ADDRESS || ethereumRpcUrls . length === 0 || ! NETWORK ) {
1217 console . error (
13- "ROLLUP_CONTRACT_ADDRESS, ETHEREUM_HOST and NETWORK are required. Provided: " ,
18+ "ROLLUP_CONTRACT_ADDRESS, ETHEREUM_HOSTS and NETWORK are required. Provided: " ,
1419 ROLLUP_CONTRACT_ADDRESS ,
15- ETHEREUM_HOST ,
16- NETWORK
20+ ETHEREUM_HOSTS ,
21+ NETWORK ,
1722 ) ;
1823 throw new Error (
19- "ROLLUP_CONTRACT_ADDRESS, ETHEREUM_HOST and NETWORK are required"
24+ "ROLLUP_CONTRACT_ADDRESS, ETHEREUM_HOSTS and NETWORK are required" ,
2025 ) ;
2126}
2227
2328if ( ! ROLLUP_CONTRACT_ADDRESS . startsWith ( "0x" ) ) {
2429 throw new Error ( "ROLLUP_CONTRACT_ADDRESS must start with 0x" ) ;
2530}
2631
27- const transport = http ( ETHEREUM_HOST ) ;
32+ const RPC_TIMEOUT_MS = 12_000 ;
2833
29- const publicClient = createPublicClient ( {
30- transport,
31- } ) ;
34+ const publicClientsByRpcUrl = new Map <
35+ string ,
36+ ReturnType < typeof createPublicClient >
37+ > ( ) ;
38+
39+ function getPublicClient ( rpcUrl : string ) {
40+ let c = publicClientsByRpcUrl . get ( rpcUrl ) ;
41+ if ( ! c ) {
42+ c = createPublicClient ( {
43+ transport : http ( rpcUrl , { timeout : RPC_TIMEOUT_MS } ) ,
44+ } ) ;
45+ publicClientsByRpcUrl . set ( rpcUrl , c ) ;
46+ }
47+ return c ;
48+ }
3249
3350const ROLLUP_ABI = [
3451 {
@@ -74,23 +91,95 @@ const pendingCheckpointNumberGauge = new client.Gauge({
7491 labelNames : [ "network" ] ,
7592} ) ;
7693
77- async function updateCheckpointNumbers ( ) : Promise < void > {
78- try {
79- const provenCheckpointNumber = await publicClient . readContract ( {
94+ const POLL_INTERVAL_MS = 36_000 ;
95+
96+ let lastStartedUpdateId = 0 ;
97+
98+ async function readCheckpointsFromRpc (
99+ rpcUrl : string ,
100+ blockNumber : bigint ,
101+ ) : Promise < { proven : number ; pending : number } > {
102+ const publicClient = getPublicClient ( rpcUrl ) ;
103+ const [ provenCheckpointNumber , pendingCheckpointNumber ] = await Promise . all ( [
104+ publicClient . readContract ( {
80105 address : ROLLUP_CONTRACT_ADDRESS as `0x${string } `,
81106 abi : ROLLUP_ABI ,
82107 functionName : "getProvenCheckpointNumber" ,
83- } ) ;
84- provenCheckpointNumberGauge . set ( Number ( provenCheckpointNumber ) ) ;
85-
86- const pendingCheckpointNumber = await publicClient . readContract ( {
108+ blockNumber,
109+ } ) ,
110+ publicClient . readContract ( {
87111 address : ROLLUP_CONTRACT_ADDRESS as `0x${string } `,
88112 abi : ROLLUP_ABI ,
89113 functionName : "getPendingCheckpointNumber" ,
114+ blockNumber,
115+ } ) ,
116+ ] ) ;
117+ return {
118+ proven : Number ( provenCheckpointNumber ) ,
119+ pending : Number ( pendingCheckpointNumber ) ,
120+ } ;
121+ }
122+
123+ async function updateCheckpointNumbers ( ) : Promise < void > {
124+ const thisUpdateId = ++ lastStartedUpdateId ;
125+ const startedAt = Date . now ( ) ;
126+ try {
127+ const blockNumber = await getPublicClient (
128+ ethereumRpcUrls [ 0 ] ! ,
129+ ) . getBlockNumber ( ) ;
130+ const settled = await Promise . allSettled (
131+ ethereumRpcUrls . map ( ( url ) => readCheckpointsFromRpc ( url , blockNumber ) ) ,
132+ ) ;
133+
134+ if ( thisUpdateId !== lastStartedUpdateId ) {
135+ console . log ( "skipped stale checkpoint read" , {
136+ updateId : thisUpdateId ,
137+ latestUpdateId : lastStartedUpdateId ,
138+ elapsedMs : Date . now ( ) - startedAt ,
139+ } ) ;
140+ return ;
141+ }
142+
143+ const successes : { proven : number ; pending : number } [ ] = [ ] ;
144+ const failures : { rpcUrl : string ; reason : unknown } [ ] = [ ] ;
145+ for ( let i = 0 ; i < settled . length ; i ++ ) {
146+ const r = settled [ i ] ! ;
147+ const rpcUrl = ethereumRpcUrls [ i ] ! ;
148+ if ( r . status === "fulfilled" ) {
149+ successes . push ( r . value ) ;
150+ } else {
151+ failures . push ( { rpcUrl, reason : r . reason } ) ;
152+ }
153+ }
154+
155+ if ( successes . length === 0 ) {
156+ console . error (
157+ `checkpoint update failed: all ${ ethereumRpcUrls . length } RPC host(s) failed (updateId=${ thisUpdateId } )` ,
158+ failures ,
159+ ) ;
160+ return ;
161+ }
162+
163+ const proven = Math . max ( ...successes . map ( ( s ) => s . proven ) ) ;
164+ const pending = Math . max ( ...successes . map ( ( s ) => s . pending ) ) ;
165+ provenCheckpointNumberGauge . set ( proven ) ;
166+ pendingCheckpointNumberGauge . set ( pending ) ;
167+ console . log ( "checkpoints updated" , {
168+ updateId : thisUpdateId ,
169+ proven,
170+ pending,
171+ rpcHostsOk : successes . length ,
172+ rpcHostsFailed : failures . length ,
173+ elapsedMs : Date . now ( ) - startedAt ,
90174 } ) ;
91- pendingCheckpointNumberGauge . set ( Number ( pendingCheckpointNumber ) ) ;
175+ if ( failures . length > 0 ) {
176+ console . warn (
177+ `checkpoint read: ${ failures . length } RPC host(s) failed; using max across ${ successes . length } successful response(s)` ,
178+ failures . map ( ( f ) => ( { rpcUrl : f . rpcUrl , reason : f . reason } ) ) ,
179+ ) ;
180+ }
92181 } catch ( error ) {
93- console . error ( "Error updating checkpoint numbers:" , error ) ;
182+ console . error ( `checkpoint update failed (updateId= ${ thisUpdateId } )` , error ) ;
94183 }
95184}
96185
@@ -102,10 +191,16 @@ app.get("/metrics", async (_req: Request, res: Response) => {
102191
103192const port = process . env . PORT ? Number ( process . env . PORT ) : 8080 ;
104193app . listen ( port , ( ) => {
105- console . log ( `Metrics server listening on port ${ port } ` ) ;
194+ console . log ( "metrics server listening" , {
195+ port,
196+ network : NETWORK ,
197+ rollup : ROLLUP_CONTRACT_ADDRESS ,
198+ ethereumRpcUrls,
199+ pollIntervalMs : POLL_INTERVAL_MS ,
200+ } ) ;
106201} ) ;
107202
108- setInterval ( updateCheckpointNumbers , 36000 ) ;
203+ setInterval ( updateCheckpointNumbers , POLL_INTERVAL_MS ) ;
109204updateCheckpointNumbers ( ) ;
110205
111206// Expose default process metrics, including process_start_time_seconds
0 commit comments