Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cloud_functions: getQuorumHeight #416

Merged
merged 1 commit into from
Mar 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cloud_functions/scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ gcloud functions --project "$GCP_PROJECT" deploy compute-missing-vaas --entry-po
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
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
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
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
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
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
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
Expand Down
125 changes: 125 additions & 0 deletions cloud_functions/src/getQuorumHeight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {
assertEnvironmentVariable,
STANDBY_GUARDIANS,
} from '@wormhole-foundation/wormhole-monitor-common';
import { Firestore } from 'firebase-admin/firestore';

// TODO: pulled from getLastHeartbeats.ts, could be shared.
interface Heartbeat {
nodeName: string;
counter: string;
timestamp: string;
networks: HeartbeatNetwork[];
version: string;
guardianAddr: string;
bootTimestamp: string;
features: string[];
p2pNodeAddr?: string;
}

interface HeartbeatNetwork {
id: number;
height: string;
contractAddress: string;
errorCount: string;
safeHeight: string;
finalizedHeight: string;
}

const isTestnet = assertEnvironmentVariable('NETWORK') === 'TESTNET';

export function getQuorumCount(numGuardians: number): number {
return isTestnet ? 1 : Math.floor((numGuardians * 2) / 3 + 1);
}

async function getHeartbeats_() {
const heartbeats: Heartbeat[] = [];
const firestoreDb = new Firestore();
try {
const collectionRef = firestoreDb.collection('heartbeats');
const snapshot = await collectionRef.get();
snapshot.docs.forEach((doc) => {
heartbeats.push(doc.data() as Heartbeat);
});
} catch (e) {
console.error(e);
}
return heartbeats;
}

let cache = { heartbeats: [] as Heartbeat[], lastUpdated: Date.now() };
// default refresh interval = 15 sec
const REFRESH_TIME_INTERVAL =
Number(process.env.CLOUD_FUNCTIONS_REFRESH_TIME_INTERVAL) || 1000 * 15;

export async function getQuorumHeight(req: any, res: any) {
res.set('Access-Control-Allow-Origin', '*');
if (req.method === 'OPTIONS') {
// Send response to OPTIONS requests
res.set('Access-Control-Allow-Methods', 'GET');
res.set('Access-Control-Allow-Headers', 'Content-Type');
res.set('Access-Control-Max-Age', '3600');
res.status(204).send('');
return;
}
const { chainId: chainIdStr } = req.query;
const chainId = Number(chainIdStr);
if (Number.isNaN(chainId)) {
res.status(400).send(`Invalid chainId`);
return;
}
let heartbeats: Heartbeat[] = [];
try {
if (cache.heartbeats.length === 0 || Date.now() - cache.lastUpdated > REFRESH_TIME_INTERVAL) {
if (cache.heartbeats.length === 0) {
console.log(`cache is empty, setting cache.heartbeats ${new Date()}`);
} else {
console.log(`cache is older than ${REFRESH_TIME_INTERVAL} ms, refreshing ${new Date()}`);
}
let prevDate = Date.now();
heartbeats = await getHeartbeats_();
let timeDiff = Date.now() - prevDate;
console.log('After getHeartbeats_=', timeDiff);
cache.heartbeats = heartbeats;
cache.lastUpdated = Date.now();
} else {
console.log(`cache is still valid, not refreshing ${new Date()}`);
heartbeats = cache.heartbeats;
}
if (heartbeats.length) {
const latestHeights: bigint[] = [];
const safeHeights: bigint[] = [];
const finalizedHeights: bigint[] = [];
for (const heartbeat of heartbeats) {
// filter out standby guardians
if (
!STANDBY_GUARDIANS.find(
(g) => g.pubkey.toLowerCase() === heartbeat.guardianAddr.toLowerCase()
)
) {
const network = heartbeat.networks.find((n) => n.id === chainId);
latestHeights.push(BigInt(network?.height || '0'));
safeHeights.push(BigInt(network?.safeHeight || '0'));
finalizedHeights.push(BigInt(network?.finalizedHeight || '0'));
}
}
latestHeights.sort((a, b) => (a > b ? -1 : a < b ? 1 : 0));
safeHeights.sort((a, b) => (a > b ? -1 : a < b ? 1 : 0));
finalizedHeights.sort((a, b) => (a > b ? -1 : a < b ? 1 : 0));
const quorumIdx = getQuorumCount(latestHeights.length) - 1;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this polling the 13th highest height? Should we remove the - 1 at the end

res.status(200).send(
JSON.stringify({
latest: latestHeights[quorumIdx].toString(),
safe: safeHeights[quorumIdx].toString(),
finalized: finalizedHeights[quorumIdx].toString(),
})
);
} else {
console.log('no heartbeats');
res.sendStatus(500);
}
} catch (e) {
console.log(e);
res.sendStatus(500);
}
}
Loading