Skip to content

Commit a3199b7

Browse files
jayantkJayant Krishnamurthy
andauthored
Integration test for attestations (#413)
* integration test ??? * update * better logging * hm * convert ids * fix conversion * what * speed things up * handle dynamic symbols properly * pr comments Co-authored-by: Jayant Krishnamurthy <[email protected]>
1 parent 4821b87 commit a3199b7

File tree

7 files changed

+147
-20
lines changed

7 files changed

+147
-20
lines changed

Tiltfile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,22 @@ k8s_resource(
203203
trigger_mode = trigger_mode,
204204
)
205205

206+
# attestations checking script
207+
docker_build(
208+
ref = "check-attestations",
209+
context = ".",
210+
only = ["./third_party"],
211+
dockerfile = "./third_party/pyth/Dockerfile.check-attestations",
212+
)
213+
214+
k8s_yaml_with_ns("devnet/check-attestations.yaml")
215+
k8s_resource(
216+
"check-attestations",
217+
resource_deps = ["pyth-price-service", "pyth", "p2w-attest"],
218+
labels = ["pyth"],
219+
trigger_mode = trigger_mode,
220+
)
221+
206222
# Pyth2wormhole relay
207223
docker_build(
208224
ref = "p2w-relay",

devnet/check-attestations.yaml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
apiVersion: v1
3+
kind: Service
4+
metadata:
5+
name: check-attestations
6+
labels:
7+
app: check-attestations
8+
spec:
9+
clusterIP: None
10+
selector:
11+
app: check-attestations
12+
---
13+
apiVersion: apps/v1
14+
kind: StatefulSet
15+
metadata:
16+
name: check-attestations
17+
spec:
18+
selector:
19+
matchLabels:
20+
app: check-attestations
21+
serviceName: check-attestations
22+
replicas: 1
23+
template:
24+
metadata:
25+
labels:
26+
app: check-attestations
27+
spec:
28+
restartPolicy: Always
29+
terminationGracePeriodSeconds: 0
30+
containers:
31+
- name: check-attestations
32+
image: check-attestations
33+
command:
34+
- python3
35+
- /usr/src/pyth/check_attestations.py
36+
tty: true
37+
readinessProbe:
38+
tcpSocket:
39+
port: 2000
40+
periodSeconds: 1
41+
failureThreshold: 300
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#syntax=docker/dockerfile:1.2@sha256:e2a8561e419ab1ba6b2fe6cbdf49fd92b95912df1cf7d313c3e2230a333fdbcc
2+
FROM python:3.9-alpine
3+
4+
RUN pip install base58
5+
6+
ADD third_party/pyth/pyth_utils.py /usr/src/pyth/pyth_utils.py
7+
ADD third_party/pyth/check_attestations.py /usr/src/pyth/check_attestations.py
8+
9+
RUN chmod a+rx /usr/src/pyth/*.py
10+
11+
ENV READINESS_PORT=2000
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env python3
2+
3+
# This script is a CI test in tilt that verifies that prices are flowing through the entire system properly.
4+
# It checks that all prices being published by the pyth publisher are showing up at the price service.
5+
import base58
6+
import logging
7+
import time
8+
from pyth_utils import *
9+
10+
logging.basicConfig(
11+
level=logging.DEBUG, format="%(asctime)s | %(module)s | %(levelname)s | %(message)s"
12+
)
13+
14+
# Where to read the set of accounts from
15+
PYTH_TEST_ACCOUNTS_HOST = "pyth"
16+
PYTH_TEST_ACCOUNTS_PORT = 4242
17+
18+
PRICE_SERVICE_HOST = "pyth-price-service"
19+
PRICE_SERVICE_PORT = 4200
20+
21+
def base58_to_hex(base58_string):
22+
asc_string = base58.b58decode(base58_string)
23+
return asc_string.hex()
24+
25+
all_prices_attested = False
26+
while not all_prices_attested:
27+
publisher_state_map = get_pyth_accounts(PYTH_TEST_ACCOUNTS_HOST, PYTH_TEST_ACCOUNTS_PORT)
28+
pyth_price_account_ids = sorted([base58_to_hex(x["price"]) for x in publisher_state_map["symbols"]])
29+
price_ids = sorted(get_json(PRICE_SERVICE_HOST, PRICE_SERVICE_PORT, "/api/price_feed_ids"))
30+
31+
if price_ids == pyth_price_account_ids:
32+
if publisher_state_map["all_symbols_added"]:
33+
logging.info("Price ids match and all symbols added. Enabling readiness probe")
34+
all_prices_attested = True
35+
else:
36+
logging.info("Price ids match but still waiting for more symbols to come online.")
37+
else:
38+
logging.info("Price ids do not match")
39+
logging.info(f"published ids: {pyth_price_account_ids}")
40+
logging.info(f"attested ids: {price_ids}")
41+
42+
time.sleep(10)
43+
44+
# Let k8s know the service is up
45+
readiness()

third_party/pyth/p2w_autoattest.py

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -109,20 +109,7 @@
109109
# Retrieve available symbols from the test pyth publisher if not provided in envs
110110
if P2W_ATTESTATION_CFG is None:
111111
P2W_ATTESTATION_CFG = "./attestation_cfg_test.yaml"
112-
conn = HTTPConnection(PYTH_TEST_ACCOUNTS_HOST, PYTH_TEST_ACCOUNTS_PORT)
113-
114-
conn.request("GET", "/")
115-
116-
res = conn.getresponse()
117-
118-
publisher_state_map = {}
119-
120-
if res.getheader("Content-Type") == "application/json":
121-
publisher_state_map = json.load(res)
122-
else:
123-
logging.error("Bad Content type")
124-
sys.exit(1)
125-
112+
publisher_state_map = get_pyth_accounts(PYTH_TEST_ACCOUNTS_HOST, PYTH_TEST_ACCOUNTS_PORT)
126113
pyth_accounts = publisher_state_map["symbols"]
127114

128115
logging.info(
@@ -167,7 +154,7 @@
167154
cfg_yaml += f"""
168155
- group_name: longer_interval_sensitive_changes
169156
conditions:
170-
min_interval_secs: 10
157+
min_interval_secs: 3
171158
price_changed_bps: 300
172159
symbols:
173160
"""
@@ -186,7 +173,7 @@
186173
cfg_yaml += f"""
187174
- group_name: mapping
188175
conditions:
189-
min_interval_secs: 30
176+
min_interval_secs: 10
190177
price_changed_bps: 500
191178
symbols: []
192179
"""

third_party/pyth/pyth_publisher.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ def do_GET(self):
3131
self.wfile.flush()
3232

3333
# Test publisher state that gets served via the HTTP endpoint. Note: the schema of this dict is extended here and there
34-
HTTP_ENDPOINT_DATA = {"symbols": [], "mapping_address": None}
34+
# all_symbols_added is set to True once all dynamically-created symbols are added to the on-chain program. This
35+
# flag allows the integration test in check_attestations.py to determine that every on-chain symbol is being attested.
36+
HTTP_ENDPOINT_DATA = {"symbols": [], "mapping_address": None, "all_symbols_added": False}
3537

3638

3739
def publisher_random_update(price_pubkey):
@@ -154,8 +156,8 @@ def add_symbol(num: int):
154156

155157
# Add a symbol if new symbol interval configured. This will add a new symbol if PYTH_NEW_SYMBOL_INTERVAL_SECS
156158
# is passed since adding the previous symbol. The second constraint ensures that
157-
# at most PYTH_TEST_SYMBOL_COUNT new price symbols are created.
158-
if PYTH_NEW_SYMBOL_INTERVAL_SECS > 0 and dynamically_added_symbols < PYTH_TEST_SYMBOL_COUNT:
159+
# at most PYTH_DYNAMIC_SYMBOL_COUNT new price symbols are created.
160+
if PYTH_NEW_SYMBOL_INTERVAL_SECS > 0 and dynamically_added_symbols < PYTH_DYNAMIC_SYMBOL_COUNT:
159161
# Do it if enough time passed
160162
now = time.monotonic()
161163
if (now - last_new_sym_added_at) >= PYTH_NEW_SYMBOL_INTERVAL_SECS:
@@ -164,6 +166,9 @@ def add_symbol(num: int):
164166
next_new_symbol_id += 1
165167
dynamically_added_symbols += 1
166168

169+
if dynamically_added_symbols >= PYTH_DYNAMIC_SYMBOL_COUNT:
170+
HTTP_ENDPOINT_DATA["all_symbols_added"] = True
171+
167172
time.sleep(PYTH_PUBLISHER_INTERVAL_SECS)
168173
sys.stdout.flush()
169174

third_party/pyth/pyth_utils.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import logging
12
import os
3+
import json
24
import socketserver
35
import subprocess
46
import sys
7+
from http.client import HTTPConnection
58

69
# Settings specific to local devnet Pyth instance
710
PYTH = os.environ.get("PYTH", "./pyth")
@@ -17,14 +20,15 @@
1720
# How long to sleep between mock Pyth price updates
1821
PYTH_PUBLISHER_INTERVAL_SECS = float(os.environ.get("PYTH_PUBLISHER_INTERVAL_SECS", "5"))
1922
PYTH_TEST_SYMBOL_COUNT = int(os.environ.get("PYTH_TEST_SYMBOL_COUNT", "11"))
23+
PYTH_DYNAMIC_SYMBOL_COUNT = int(os.environ.get("PYTH_DYNAMIC_SYMBOL_COUNT", "3"))
2024

2125
# If above 0, adds a new test symbol periodically, waiting at least
2226
# the given number of seconds in between
2327
#
2428
# NOTE: the new symbols are added in the HTTP endpoint used by the
2529
# p2w-attest service in Tilt. You may need to wait to see p2w-attest
2630
# pick up brand new symbols
27-
PYTH_NEW_SYMBOL_INTERVAL_SECS = int(os.environ.get("PYTH_NEW_SYMBOL_INTERVAL_SECS", "120"))
31+
PYTH_NEW_SYMBOL_INTERVAL_SECS = int(os.environ.get("PYTH_NEW_SYMBOL_INTERVAL_SECS", "30"))
2832

2933
PYTH_MAPPING_KEYPAIR = os.environ.get(
3034
"PYTH_MAPPING_KEYPAIR", f"{PYTH_KEY_STORE}/mapping_key_pair.json"
@@ -108,6 +112,24 @@ def sol_run_or_die(subcommand, args=[], **kwargs):
108112
return run_or_die(["solana", subcommand] + args + ["--url", SOL_RPC_URL], **kwargs)
109113

110114

115+
def get_json(host, port, path):
116+
conn = HTTPConnection(host, port)
117+
conn.request("GET", path)
118+
res = conn.getresponse()
119+
120+
# starstwith because the header value may include optional fields after (like charset)
121+
if res.getheader("Content-Type").startswith("application/json"):
122+
return json.load(res)
123+
else:
124+
logging.error(f"Error getting {host}:{port}{path} : Content-Type was not application/json")
125+
logging.error(f"HTTP response code: {res.getcode()}")
126+
logging.error(f"HTTP headers: {res.getheaders()}")
127+
logging.error(f"Message: {res.msg}")
128+
sys.exit(1)
129+
130+
def get_pyth_accounts(host, port):
131+
return get_json(host, port, "/")
132+
111133
class ReadinessTCPHandler(socketserver.StreamRequestHandler):
112134
def handle(self):
113135
"""TCP black hole"""

0 commit comments

Comments
 (0)