Skip to content

Commit b10d02e

Browse files
committed
cloud_functions: getQuorumHeight
1 parent ebfb796 commit b10d02e

File tree

2 files changed

+126
-0
lines changed

2 files changed

+126
-0
lines changed

cloud_functions/scripts/deploy.sh

+1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ gcloud functions --project "$GCP_PROJECT" deploy compute-missing-vaas --entry-po
133133
gcloud functions --project "$GCP_PROJECT" deploy compute-ntt-rate-limits --entry-point computeNTTRateLimits --gen2 --runtime nodejs22 --trigger-http --allow-unauthenticated --timeout 300 --memory 512MB --region europe-west3 --set-env-vars NETWORK=$NETWORK,FUNCTION=computeNTTRateLimits
134134
gcloud functions --project "$GCP_PROJECT" deploy compute-total-supply-and-locked --entry-point computeTotalSupplyAndLocked --gen2 --runtime nodejs22 --trigger-http --allow-unauthenticated --timeout 300 --memory 512MB --region europe-west3 --set-env-vars NETWORK=$NETWORK,FUNCTION=computeTotalSupplyAndLocked
135135
gcloud functions --project "$GCP_PROJECT" deploy get-ntt-rate-limits --entry-point getNTTRateLimits --gen2 --runtime nodejs22 --trigger-http --allow-unauthenticated --timeout 300 --memory 512MB --region europe-west3 --set-env-vars NETWORK=$NETWORK,FUNCTION=getNTTRateLimits
136+
gcloud functions --project "$GCP_PROJECT" deploy get-quorum-height --entry-point getQuorumHeight --gen2 --runtime nodejs22 --trigger-http --allow-unauthenticated --timeout 300 --memory 512MB --region europe-west3 --set-env-vars CLOUD_FUNCTIONS_REFRESH_TIME_INTERVAL=$CLOUD_FUNCTIONS_REFRESH_TIME_INTERVAL,NETWORK=$NETWORK,FUNCTION=getQuorumHeight
136137
gcloud functions --project "$GCP_PROJECT" deploy get-total-supply-and-locked --entry-point getTotalSupplyAndLocked --gen2 --runtime nodejs22 --trigger-http --allow-unauthenticated --timeout 300 --memory 512MB --region europe-west3 --set-env-vars NETWORK=$NETWORK,FUNCTION=getTotalSupplyAndLocked
137138
gcloud functions --project "$GCP_PROJECT" deploy latest-blocks --entry-point getLatestBlocks --gen2 --runtime nodejs22 --trigger-http --allow-unauthenticated --timeout 300 --memory 512MB --region europe-west3 --set-env-vars CLOUD_FUNCTIONS_REFRESH_TIME_INTERVAL=$CLOUD_FUNCTIONS_REFRESH_TIME_INTERVAL,FIRESTORE_LATEST_COLLECTION=$FIRESTORE_LATEST_COLLECTION,NETWORK=$NETWORK,FUNCTION=getLatestBlocks
138139
gcloud functions --project "$GCP_PROJECT" deploy latest-tvltvm --entry-point getLatestTvlTvm --gen2 --runtime nodejs22 --trigger-http --allow-unauthenticated --timeout 300 --memory 512MB --region europe-west3 --set-env-vars CLOUD_FUNCTIONS_REFRESH_TIME_INTERVAL=$CLOUD_FUNCTIONS_REFRESH_TIME_INTERVAL,FIRESTORE_LATEST_TVLTVM_COLLECTION=$FIRESTORE_LATEST_TVLTVM_COLLECTION,NETWORK=$NETWORK,FUNCTION=getLatestTvlTvm
+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import {
2+
assertEnvironmentVariable,
3+
STANDBY_GUARDIANS,
4+
} from '@wormhole-foundation/wormhole-monitor-common';
5+
import { Firestore } from 'firebase-admin/firestore';
6+
7+
// TODO: pulled from getLastHeartbeats.ts, could be shared.
8+
interface Heartbeat {
9+
nodeName: string;
10+
counter: string;
11+
timestamp: string;
12+
networks: HeartbeatNetwork[];
13+
version: string;
14+
guardianAddr: string;
15+
bootTimestamp: string;
16+
features: string[];
17+
p2pNodeAddr?: string;
18+
}
19+
20+
interface HeartbeatNetwork {
21+
id: number;
22+
height: string;
23+
contractAddress: string;
24+
errorCount: string;
25+
safeHeight: string;
26+
finalizedHeight: string;
27+
}
28+
29+
const isTestnet = assertEnvironmentVariable('NETWORK') === 'TESTNET';
30+
31+
export function getQuorumCount(numGuardians: number): number {
32+
return isTestnet ? 1 : Math.floor((numGuardians * 2) / 3 + 1);
33+
}
34+
35+
async function getHeartbeats_() {
36+
const heartbeats: Heartbeat[] = [];
37+
const firestoreDb = new Firestore();
38+
try {
39+
const collectionRef = firestoreDb.collection('heartbeats');
40+
const snapshot = await collectionRef.get();
41+
snapshot.docs.forEach((doc) => {
42+
heartbeats.push(doc.data() as Heartbeat);
43+
});
44+
} catch (e) {
45+
console.error(e);
46+
}
47+
return heartbeats;
48+
}
49+
50+
let cache = { heartbeats: [] as Heartbeat[], lastUpdated: Date.now() };
51+
// default refresh interval = 15 sec
52+
const REFRESH_TIME_INTERVAL =
53+
Number(process.env.CLOUD_FUNCTIONS_REFRESH_TIME_INTERVAL) || 1000 * 15;
54+
55+
export async function getQuorumHeight(req: any, res: any) {
56+
res.set('Access-Control-Allow-Origin', '*');
57+
if (req.method === 'OPTIONS') {
58+
// Send response to OPTIONS requests
59+
res.set('Access-Control-Allow-Methods', 'GET');
60+
res.set('Access-Control-Allow-Headers', 'Content-Type');
61+
res.set('Access-Control-Max-Age', '3600');
62+
res.status(204).send('');
63+
return;
64+
}
65+
const { chainId: chainIdStr } = req.query;
66+
const chainId = Number(chainIdStr);
67+
if (Number.isNaN(chainId)) {
68+
res.status(400).send(`Invalid chainId`);
69+
return;
70+
}
71+
let heartbeats: Heartbeat[] = [];
72+
try {
73+
if (cache.heartbeats.length === 0 || Date.now() - cache.lastUpdated > REFRESH_TIME_INTERVAL) {
74+
if (cache.heartbeats.length === 0) {
75+
console.log(`cache is empty, setting cache.heartbeats ${new Date()}`);
76+
} else {
77+
console.log(`cache is older than ${REFRESH_TIME_INTERVAL} ms, refreshing ${new Date()}`);
78+
}
79+
let prevDate = Date.now();
80+
heartbeats = await getHeartbeats_();
81+
let timeDiff = Date.now() - prevDate;
82+
console.log('After getHeartbeats_=', timeDiff);
83+
cache.heartbeats = heartbeats;
84+
cache.lastUpdated = Date.now();
85+
} else {
86+
console.log(`cache is still valid, not refreshing ${new Date()}`);
87+
heartbeats = cache.heartbeats;
88+
}
89+
if (heartbeats.length) {
90+
const latestHeights: bigint[] = [];
91+
const safeHeights: bigint[] = [];
92+
const finalizedHeights: bigint[] = [];
93+
for (const heartbeat of heartbeats) {
94+
// filter out standby guardians
95+
if (
96+
!STANDBY_GUARDIANS.find(
97+
(g) => g.pubkey.toLowerCase() === heartbeat.guardianAddr.toLowerCase()
98+
)
99+
) {
100+
const network = heartbeat.networks.find((n) => n.id === chainId);
101+
latestHeights.push(BigInt(network?.height || '0'));
102+
safeHeights.push(BigInt(network?.safeHeight || '0'));
103+
finalizedHeights.push(BigInt(network?.finalizedHeight || '0'));
104+
}
105+
}
106+
latestHeights.sort((a, b) => (a > b ? -1 : a < b ? 1 : 0));
107+
safeHeights.sort((a, b) => (a > b ? -1 : a < b ? 1 : 0));
108+
finalizedHeights.sort((a, b) => (a > b ? -1 : a < b ? 1 : 0));
109+
const quorumIdx = getQuorumCount(latestHeights.length) - 1;
110+
res.status(200).send(
111+
JSON.stringify({
112+
latest: latestHeights[quorumIdx].toString(),
113+
safe: safeHeights[quorumIdx].toString(),
114+
finalized: finalizedHeights[quorumIdx].toString(),
115+
})
116+
);
117+
} else {
118+
console.log('no heartbeats');
119+
res.sendStatus(500);
120+
}
121+
} catch (e) {
122+
console.log(e);
123+
res.sendStatus(500);
124+
}
125+
}

0 commit comments

Comments
 (0)