Merge pull request #6351 from mempool/natsoni/liquid-audit-monitoring

Add Federation reserves ratio to monitoring page
This commit is contained in:
wiz 2026-02-28 19:50:11 +09:00 committed by GitHub
commit 38987bd403
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 83 additions and 0 deletions

View file

@ -57,4 +57,19 @@ export interface HealthCheckHost {
unreachable: boolean;
checked: boolean;
lastChecked: number;
hashes?: {
frontend?: string;
hybrid?: string;
backend?: string;
electrs?: string;
ssr?: string;
core?: string;
os?: string;
lastUpdated?: number;
};
liquidAudit?: {
pegRatio: number;
bitcoinLastBlockUpdate: number;
liquidLastBlockUpdate: number;
};
}

View file

@ -32,6 +32,11 @@ interface FailoverHost {
core?: string,
os?: string,
lastUpdated: number,
},
liquidAudit?: {
pegRatio: number,
bitcoinLastBlockUpdate: number,
liquidLastBlockUpdate: number,
}
}
@ -145,6 +150,8 @@ class FailoverRouter {
}
}
await this.$updateLiquidAudit(host);
// Check front and backend git hashes less often
if (Date.now() - host.hashes.lastUpdated > this.gitHashInterval) {
await Promise.all([
@ -351,6 +358,43 @@ class FailoverRouter {
}
}
private async $updateLiquidAudit(host: FailoverHost): Promise<void> {
if (config.MEMPOOL.NETWORK !== 'liquid') {
return;
}
try {
const [reservesResponse, pegsResponse] = await Promise.all([
this.pollConnection.get<any>(
`${host.publicDomain}/api/v1/liquid/reserves`, {
timeout: config.ESPLORA.FALLBACK_TIMEOUT,
headers: { 'Host': 'liquid.network' }
}
),
this.pollConnection.get<any>(
`${host.publicDomain}/api/v1/liquid/pegs`, {
timeout: config.ESPLORA.FALLBACK_TIMEOUT,
headers: { 'Host': 'liquid.network' }
}
),
]);
const reservesAmount = Number(reservesResponse.data?.amount);
const pegsAmount = Number(pegsResponse.data?.amount);
const bitcoinLastBlockUpdate = Number(reservesResponse.data?.lastBlockUpdate);
const liquidLastBlockUpdate = Number(pegsResponse.data?.lastBlockUpdate);
if (Number.isFinite(reservesAmount) && Number.isFinite(pegsAmount) && Number.isFinite(bitcoinLastBlockUpdate) && Number.isFinite(liquidLastBlockUpdate) && pegsAmount > 0) {
host.liquidAudit = {
pegRatio: (reservesAmount / pegsAmount) * 100,
bitcoinLastBlockUpdate,
liquidLastBlockUpdate,
};
}
} catch (e) {
// failed to get liquid audit values - do nothing
}
}
// returns the public mempool domain corresponding to an esplora server url
// (a bit of a hack to avoid manually specifying frontend & backend URLs for each esplora server)
private extractPublicDomain(url: string): string {
@ -585,6 +629,7 @@ class ElectrsApi implements AbstractBitcoinApi {
checked: !!host.checked,
lastChecked: host.lastChecked || 0,
hashes: host.hashes,
...(config.MEMPOOL.NETWORK === 'liquid' ? { liquidAudit: host.liquidAudit } : {}),
}));
} else {
return [];

View file

@ -19,6 +19,7 @@
<th class="rtt only-small">RTT</th>
<th class="rtt only-large">RTT</th>
<th class="height">Height</th>
<th *ngIf="isLiquid" class="peg">Peg</th>
<th class="hybrid hash">Hybrid</th>
<th class="frontend hash">Front</th>
<th class="backend hash">Back</th>
@ -35,6 +36,7 @@
<td class="rtt only-small">{{ (host.rtt / 1000) | number : '1.1-1' }} {{ host.rtt == null ? '' : 's'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
<td class="rtt only-large">{{ host.rtt | number : '1.0-0' }} {{ host.rtt == null ? '' : 'ms'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
<td class="height">{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '')) }}</td>
<td class="peg" *ngIf="isLiquid">{{ formatPegRatio(host.liquidAudit?.pegRatio) }}</td>
<ng-container *ngFor="let type of ['hybrid', 'frontend', 'backend', 'electrs', 'ssr', 'core', 'os']">
<td class="{{type}}" [class.hash]="type !== 'core' && type !== 'os'" [class.version]="type === 'core' || type === 'os'" [style.background-color]="colors[host.host][type]">
@if (type !== 'core' && type !== 'os' && host.hashes?.[type]) {

View file

@ -35,6 +35,10 @@
&.rtt, &.height {
text-align: right;
}
&.peg {
text-align: right;
white-space: nowrap;
}
&.only-small {
display: table-cell;
}

View file

@ -18,6 +18,7 @@ export class ServerHealthComponent implements OnInit {
interval: number;
now: number = Date.now();
colors: Record<string, Record<string, string>> = {};
isLiquid = false;
repoMap = {
frontend: 'mempool',
@ -93,6 +94,8 @@ export class ServerHealthComponent implements OnInit {
this.now = Date.now();
this.cd.markForCheck();
}, 1000);
this.isLiquid = this.stateService.network === 'liquid';
}
trackByFn(index: number, host: HealthCheckHost): string {
@ -171,4 +174,13 @@ export class ServerHealthComponent implements OnInit {
const hue = (index / (sortedVersions.length - 1)) * 120;
return `hsl(${hue}, 70%, 35%)`;
}
public formatPegRatio(pegRatio: number | null | undefined): string {
if (pegRatio == null) {
return '?';
}
const ratio = Math.floor(pegRatio);
return `${ratio}%${ratio >= 100 ? ' ✅' : ' 🔥'}`;
}
}

View file

@ -156,6 +156,11 @@ export interface HealthCheckHost {
core?: string;
os?: string;
};
liquidAudit?: {
pegRatio: number;
bitcoinLastBlockUpdate: number;
liquidLastBlockUpdate: number;
};
}
export interface StratumJob {