Skip to content

Commit e283b15

Browse files
authored
[price-service] Improve vaa validation (#814)
* [price-service/server] Improve Vaa Validation * [price-service/server] Improve performance * Update wormhole guardian version
1 parent f70f163 commit e283b15

13 files changed

+109
-12
lines changed

package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

price_pusher/docker-compose.mainnet.sample.yaml

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
services:
22
spy:
33
# Find latest Guardian images in https://github.com/wormhole-foundation/wormhole/pkgs/container/guardiand
4-
image: ghcr.io/wormhole-foundation/guardiand:v2.14.8.1
4+
image: ghcr.io/wormhole-foundation/guardiand:v2.17.0
55
command:
66
- "spy"
77
- "--nodeKey"
@@ -16,7 +16,7 @@ services:
1616
- "warn"
1717
price-service:
1818
# Find latest price service images https://gallery.ecr.aws/pyth-network/xc-server
19-
image: public.ecr.aws/pyth-network/xc-server:v3.0.0
19+
image: public.ecr.aws/pyth-network/xc-server:v3.0.3
2020
environment:
2121
SPY_SERVICE_HOST: "spy:7072"
2222
SPY_SERVICE_FILTERS: |
@@ -35,6 +35,7 @@ services:
3535
READINESS_SPY_SYNC_TIME_SECONDS: "20"
3636
READINESS_NUM_LOADED_SYMBOLS: "50"
3737
LOG_LEVEL: warning
38+
WORMHOLE_CLUSTER: mainnet
3839
healthcheck:
3940
test:
4041
[

price_pusher/docker-compose.testnet.sample.yaml

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
services:
22
spy:
33
# Find latest Guardian images in https://github.com/wormhole-foundation/wormhole/pkgs/container/guardiand
4-
image: ghcr.io/wormhole-foundation/guardiand:v2.14.8.1
4+
image: ghcr.io/wormhole-foundation/guardiand:v2.17.0
55
command:
66
- "spy"
77
- "--nodeKey"
@@ -16,7 +16,7 @@ services:
1616
- "warn"
1717
price-service:
1818
# Find latest price service images https://gallery.ecr.aws/pyth-network/xc-server
19-
image: public.ecr.aws/pyth-network/xc-server:v3.0.0
19+
image: public.ecr.aws/pyth-network/xc-server:v3.0.3
2020
environment:
2121
SPY_SERVICE_HOST: "spy:7072"
2222
SPY_SERVICE_FILTERS: |
@@ -35,6 +35,7 @@ services:
3535
READINESS_SPY_SYNC_TIME_SECONDS: "20"
3636
READINESS_NUM_LOADED_SYMBOLS: "50"
3737
LOG_LEVEL: warning
38+
WORMHOLE_CLUSTER: testnet
3839
healthcheck:
3940
test:
4041
[

price_service/server/.env.sample

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ SPY_SERVICE_HOST=0.0.0.0:7072
77
# testnet/mainnet pyth price_service deployment.
88
SPY_SERVICE_FILTERS=[{"chain_id":1,"emitter_address":"71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b"}]
99

10+
WORMHOLE_CLUSTER=localnet
11+
1012
# Number of seconds to sync with spy to be sure to have latest messages
1113
READINESS_SPY_SYNC_TIME_SECONDS=60
1214
READINESS_NUM_LOADED_SYMBOLS=5

price_service/server/docker-compose.mainnet.yaml

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
services:
22
spy:
33
# Find latest Guardian images in https://github.com/wormhole-foundation/wormhole/pkgs/container/guardiand
4-
image: ghcr.io/wormhole-foundation/guardiand:v2.14.8.1
4+
image: ghcr.io/wormhole-foundation/guardiand:v2.17.0
55
restart: on-failure
66
command:
77
- "spy"
@@ -17,7 +17,7 @@ services:
1717
- "warn"
1818
price-service:
1919
# Find latest price service images https://gallery.ecr.aws/pyth-network/xc-server
20-
image: public.ecr.aws/pyth-network/xc-server:v3.0.0
20+
image: public.ecr.aws/pyth-network/xc-server:v3.0.3
2121
restart: on-failure
2222
# Or alternatively use a locally built image
2323
# image: pyth_price_server
@@ -39,6 +39,7 @@ services:
3939
READINESS_SPY_SYNC_TIME_SECONDS: "20"
4040
READINESS_NUM_LOADED_SYMBOLS: "50"
4141
LOG_LEVEL: warning
42+
WORMHOLE_CLUSTER: mainnet
4243
DB_API_CLUSTER: pythnet
4344
REMOVE_EXPIRED_VALUES_INTERVAL_SECONDS: "60"
4445
CACHE_TTL_SECONDS: "300"

price_service/server/docker-compose.testnet.yaml

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
services:
22
spy:
33
# Find latest Guardian images in https://github.com/wormhole-foundation/wormhole/pkgs/container/guardiand
4-
image: ghcr.io/wormhole-foundation/guardiand:v2.14.8.1
4+
image: ghcr.io/wormhole-foundation/guardiand:v2.17.0
55
restart: on-failure
66
command:
77
- "spy"
@@ -17,7 +17,7 @@ services:
1717
- "warn"
1818
price-service:
1919
# Find latest price service images https://gallery.ecr.aws/pyth-network/xc-server
20-
image: public.ecr.aws/pyth-network/xc-server:v3.0.0
20+
image: public.ecr.aws/pyth-network/xc-server:v3.0.3
2121
restart: on-failure
2222
# Or alternatively use a locally built image
2323
# image: pyth_price_server
@@ -39,6 +39,7 @@ services:
3939
READINESS_SPY_SYNC_TIME_SECONDS: "20"
4040
READINESS_NUM_LOADED_SYMBOLS: "50"
4141
LOG_LEVEL: warning
42+
WORMHOLE_CLUSTER: testnet
4243
DB_API_CLUSTER: devnet
4344
REMOVE_EXPIRED_VALUES_INTERVAL_SECONDS: "60"
4445
CACHE_TTL_SECONDS: "300"

price_service/server/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pythnetwork/price-service-server",
3-
"version": "3.0.1",
3+
"version": "3.0.3",
44
"description": "Webservice for retrieving prices from the Pyth oracle.",
55
"private": "true",
66
"main": "index.js",

price_service/server/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Listener } from "./listen";
33
import { initLogger } from "./logging";
44
import { PromClient } from "./promClient";
55
import { RestAPI } from "./rest";
6+
import { wormholeClusterFromString } from "./vaa";
67
import { WebSocketAPI } from "./ws";
78

89
let configFile: string = ".env";
@@ -28,6 +29,7 @@ async function run() {
2829
{
2930
spyServiceHost: envOrErr("SPY_SERVICE_HOST"),
3031
filtersRaw: process.env.SPY_SERVICE_FILTERS,
32+
wormholeCluster: process.env.WORMHOLE_CLUSTER,
3133
readiness: {
3234
spySyncTimeSeconds: parseInt(
3335
envOrErr("READINESS_SPY_SYNC_TIME_SECONDS"),

price_service/server/src/listen.ts

+14
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ import {
1717
PriceAttestation,
1818
} from "@pythnetwork/wormhole-attester-sdk";
1919
import { HexString, PriceFeed } from "@pythnetwork/price-service-sdk";
20+
import { ethers } from "ethers";
2021
import LRUCache from "lru-cache";
2122
import { DurationInSec, sleep, TimestampInSec } from "./helpers";
2223
import { logger } from "./logging";
2324
import { PromClient } from "./promClient";
25+
import { isValidVaa, WormholeCluster, wormholeClusterFromString } from "./vaa";
2426

2527
export type PriceInfo = {
2628
vaa: Buffer;
@@ -64,6 +66,7 @@ type ListenerReadinessConfig = {
6466

6567
type ListenerConfig = {
6668
spyServiceHost: string;
69+
wormholeCluster?: string;
6770
filtersRaw?: string;
6871
readiness: ListenerReadinessConfig;
6972
webApiEndpoint?: string;
@@ -171,6 +174,7 @@ export class Listener implements PriceStore {
171174
private updateCallbacks: ((priceInfo: PriceInfo) => any)[];
172175
private observedVaas: LRUCache<VaaKey, boolean>;
173176
private vaasCache: VaaCache;
177+
private wormholeCluster: WormholeCluster;
174178

175179
constructor(config: ListenerConfig, promClient?: PromClient) {
176180
this.promClient = promClient;
@@ -188,6 +192,11 @@ export class Listener implements PriceStore {
188192
config.cacheTtl,
189193
config.cacheCleanupLoopInterval
190194
);
195+
if (config.wormholeCluster !== undefined) {
196+
this.wormholeCluster = wormholeClusterFromString(config.wormholeCluster);
197+
} else {
198+
this.wormholeCluster = "mainnet";
199+
}
191200
}
192201

193202
private loadFilters(filtersRaw?: string) {
@@ -305,6 +314,11 @@ export class Listener implements PriceStore {
305314
return;
306315
}
307316

317+
if (!isValidVaa(parsedVaa, this.wormholeCluster)) {
318+
logger.info("Ignoring an invalid VAA");
319+
return;
320+
}
321+
308322
let batchAttestation;
309323

310324
try {

price_service/server/src/vaa.ts

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { ParsedVaa } from "@certusone/wormhole-sdk";
2+
import { GuardianSet } from "@certusone/wormhole-spydk/lib/cjs/proto/publicrpc/v1/publicrpc";
3+
import { ethers } from "ethers";
4+
5+
const WormholeClusters = ["localnet", "testnet", "mainnet"] as const;
6+
export type WormholeCluster = typeof WormholeClusters[number];
7+
8+
export function wormholeClusterFromString(s: string): WormholeCluster {
9+
if (WormholeClusters.includes(s as WormholeCluster)) {
10+
return s as WormholeCluster;
11+
}
12+
throw new Error(`Invalid wormhole cluster: ${s}`);
13+
}
14+
15+
const guardianSets: Record<WormholeCluster, GuardianSet> = {
16+
localnet: {
17+
index: 0,
18+
addresses: ["0xbeFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe"],
19+
},
20+
testnet: {
21+
index: 0,
22+
addresses: ["0x13947Bd48b18E53fdAeEe77F3473391aC727C638"],
23+
},
24+
mainnet: {
25+
index: 3,
26+
addresses: [
27+
"0x58CC3AE5C097b213cE3c81979e1B9f9570746AA5",
28+
"0xfF6CB952589BDE862c25Ef4392132fb9D4A42157",
29+
"0x114De8460193bdf3A2fCf81f86a09765F4762fD1",
30+
"0x107A0086b32d7A0977926A205131d8731D39cbEB",
31+
"0x8C82B2fd82FaeD2711d59AF0F2499D16e726f6b2",
32+
"0x11b39756C042441BE6D8650b69b54EbE715E2343",
33+
"0x54Ce5B4D348fb74B958e8966e2ec3dBd4958a7cd",
34+
"0x15e7cAF07C4e3DC8e7C469f92C8Cd88FB8005a20",
35+
"0x74a3bf913953D695260D88BC1aA25A4eeE363ef0",
36+
"0x000aC0076727b35FBea2dAc28fEE5cCB0fEA768e",
37+
"0xAF45Ced136b9D9e24903464AE889F5C8a723FC14",
38+
"0xf93124b7c738843CBB89E864c862c38cddCccF95",
39+
"0xD2CC37A4dc036a8D232b48f62cDD4731412f4890",
40+
"0xDA798F6896A3331F64b48c12D1D57Fd9cbe70811",
41+
"0x71AA1BE1D36CaFE3867910F99C09e347899C19C3",
42+
"0x8192b6E7387CCd768277c17DAb1b7a5027c0b3Cf",
43+
"0x178e21ad2E77AE06711549CFBB1f9c7a9d8096e8",
44+
"0x5E1487F35515d02A92753504a8D75471b9f49EdB",
45+
"0x6FbEBc898F403E4773E95feB15E80C9A99c8348d",
46+
],
47+
},
48+
};
49+
50+
export function isValidVaa(vaa: ParsedVaa, cluster: WormholeCluster): boolean {
51+
const currentGuardianSet = guardianSets[cluster];
52+
if (vaa.guardianSetIndex !== currentGuardianSet.index) {
53+
return false;
54+
}
55+
56+
const threshold = Math.ceil((currentGuardianSet.addresses.length * 2) / 3);
57+
if (vaa.guardianSignatures.length < threshold) {
58+
return false;
59+
}
60+
61+
const digest = ethers.utils.keccak256(vaa.hash);
62+
63+
let validVaa = true;
64+
vaa.guardianSignatures.forEach((sig) => {
65+
if (
66+
ethers.utils.recoverAddress(digest, sig.signature) !==
67+
currentGuardianSet.addresses[sig.index]
68+
)
69+
validVaa = false;
70+
});
71+
72+
return validVaa;
73+
}

tilt_devnet/k8s/node.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ spec:
5353
path: bigtable-key.json
5454
containers:
5555
- name: guardiand
56-
image: ghcr.io/certusone/guardiand:v2.8.9
56+
image: ghcr.io/wormhole-foundation/guardiand:v2.17.0
5757
volumeMounts:
5858
- mountPath: /run/node
5959
name: node-rundir

tilt_devnet/k8s/pyth-price-server.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ spec:
6262
value: spy:7072
6363
- name: SPY_SERVICE_FILTERS
6464
value: '[{"chain_id":1,"emitter_address":"71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b"}]'
65+
- name: WORMHOLE_CLUSTER
66+
value: localnet
6567
- name: REST_PORT
6668
value: "4200"
6769
- name: PROM_PORT

tilt_devnet/k8s/spy.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ spec:
3535
terminationGracePeriodSeconds: 0
3636
containers:
3737
- name: spy
38-
image: ghcr.io/certusone/guardiand:v2.8.9
38+
image: ghcr.io/wormhole-foundation/guardiand:v2.17.0
3939
command:
4040
- /guardiand
4141
- spy

0 commit comments

Comments
 (0)