p2w_autoattest.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. #!/usr/bin/env python3
  2. # This script sets up a simple loop for periodical attestation of Pyth data
  3. import json
  4. import logging
  5. import os
  6. import re
  7. import sys
  8. import threading
  9. import time
  10. from http.client import HTTPConnection
  11. from http.server import BaseHTTPRequestHandler, HTTPServer
  12. from subprocess import PIPE, STDOUT, Popen
  13. from pyth_utils import *
  14. logging.basicConfig(
  15. level=logging.DEBUG, format="%(asctime)s | %(module)s | %(levelname)s | %(message)s"
  16. )
  17. P2W_SOL_ADDRESS = os.environ.get(
  18. "P2W_SOL_ADDRESS", "P2WH424242424242424242424242424242424242424"
  19. )
  20. P2W_OWNER_KEYPAIR = os.environ.get(
  21. "P2W_OWNER_KEYPAIR", "/usr/src/solana/keys/p2w_owner.json"
  22. )
  23. P2W_ATTESTATIONS_PORT = int(os.environ.get("P2W_ATTESTATIONS_PORT", 4343))
  24. P2W_INITIALIZE_SOL_CONTRACT = os.environ.get("P2W_INITIALIZE_SOL_CONTRACT", None)
  25. PYTH_TEST_ACCOUNTS_HOST = "pyth"
  26. PYTH_TEST_ACCOUNTS_PORT = 4242
  27. P2W_ATTESTATION_CFG = os.environ.get("P2W_ATTESTATION_CFG", None)
  28. WORMHOLE_ADDRESS = os.environ.get(
  29. "WORMHOLE_ADDRESS", "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
  30. )
  31. P2W_MAX_LOG_LINES = int(os.environ.get("P2W_MAX_LOG_LINES", 1000))
  32. ATTESTATIONS = {
  33. "pendingSeqnos": [],
  34. }
  35. SEQNO_REGEX = re.compile(r"Sequence number: (\d+)")
  36. class P2WAutoattestStatusEndpoint(BaseHTTPRequestHandler):
  37. """
  38. A dumb endpoint for last attested price metadata.
  39. """
  40. def do_GET(self):
  41. logging.info(f"Got path {self.path}")
  42. sys.stdout.flush()
  43. data = json.dumps(ATTESTATIONS).encode("utf-8")
  44. logging.debug(f"Sending: {data}")
  45. ATTESTATIONS["pendingSeqnos"] = []
  46. self.send_response(200)
  47. self.send_header("Content-Type", "application/json")
  48. self.send_header("Content-Length", str(len(data)))
  49. self.end_headers()
  50. self.wfile.write(data)
  51. self.wfile.flush()
  52. def serve_attestations():
  53. """
  54. Run a barebones HTTP server to share Pyth2wormhole attestation history
  55. """
  56. server_address = ("", P2W_ATTESTATIONS_PORT)
  57. httpd = HTTPServer(server_address, P2WAutoattestStatusEndpoint)
  58. httpd.serve_forever()
  59. if SOL_AIRDROP_AMT > 0:
  60. # Fund the p2w owner
  61. sol_run_or_die(
  62. "airdrop",
  63. [
  64. str(SOL_AIRDROP_AMT),
  65. "--keypair",
  66. P2W_OWNER_KEYPAIR,
  67. "--commitment",
  68. "finalized",
  69. ],
  70. )
  71. def find_and_log_seqnos(s):
  72. # parse seqnos
  73. matches = SEQNO_REGEX.findall(s)
  74. seqnos = list(map(lambda m: int(m), matches))
  75. ATTESTATIONS["pendingSeqnos"] += seqnos
  76. if len(seqnos) > 0:
  77. logging.info(f"{len(seqnos)} batch seqno(s) received: {seqnos})")
  78. if P2W_INITIALIZE_SOL_CONTRACT is not None:
  79. # Get actor pubkeys
  80. P2W_OWNER_ADDRESS = sol_run_or_die(
  81. "address", ["--keypair", P2W_OWNER_KEYPAIR], capture_output=True
  82. ).stdout.strip()
  83. PYTH_OWNER_ADDRESS = sol_run_or_die(
  84. "address", ["--keypair", PYTH_PROGRAM_KEYPAIR], capture_output=True
  85. ).stdout.strip()
  86. init_result = run_or_die(
  87. [
  88. "pyth2wormhole-client",
  89. "--log-level",
  90. "4",
  91. "--p2w-addr",
  92. P2W_SOL_ADDRESS,
  93. "--rpc-url",
  94. SOL_RPC_URL,
  95. "--payer",
  96. P2W_OWNER_KEYPAIR,
  97. "init",
  98. "--wh-prog",
  99. WORMHOLE_ADDRESS,
  100. "--owner",
  101. P2W_OWNER_ADDRESS,
  102. "--pyth-owner",
  103. PYTH_OWNER_ADDRESS,
  104. ],
  105. capture_output=True,
  106. die=False,
  107. )
  108. if init_result.returncode != 0:
  109. logging.error(
  110. "NOTE: pyth2wormhole-client init failed, retrying with set_config"
  111. )
  112. run_or_die(
  113. [
  114. "pyth2wormhole-client",
  115. "--log-level",
  116. "4",
  117. "--p2w-addr",
  118. P2W_SOL_ADDRESS,
  119. "--rpc-url",
  120. SOL_RPC_URL,
  121. "--payer",
  122. P2W_OWNER_KEYPAIR,
  123. "set-config",
  124. "--owner",
  125. P2W_OWNER_KEYPAIR,
  126. "--new-owner",
  127. P2W_OWNER_ADDRESS,
  128. "--new-wh-prog",
  129. WORMHOLE_ADDRESS,
  130. "--new-pyth-owner",
  131. PYTH_OWNER_ADDRESS,
  132. ],
  133. capture_output=True,
  134. )
  135. # Retrieve available symbols from the test pyth publisher if not provided in envs
  136. if P2W_ATTESTATION_CFG is None:
  137. P2W_ATTESTATION_CFG = "./attestation_cfg_test.yaml"
  138. conn = HTTPConnection(PYTH_TEST_ACCOUNTS_HOST, PYTH_TEST_ACCOUNTS_PORT)
  139. conn.request("GET", "/")
  140. res = conn.getresponse()
  141. pyth_accounts = None
  142. if res.getheader("Content-Type") == "application/json":
  143. pyth_accounts = json.load(res)
  144. else:
  145. logging.error("Bad Content type")
  146. sys.exit(1)
  147. logging.info(
  148. f"Retrieved {len(pyth_accounts)} Pyth accounts from endpoint: {pyth_accounts}"
  149. )
  150. cfg_yaml = """
  151. ---
  152. symbol_groups:
  153. - group_name: things
  154. conditions:
  155. min_freq_secs: 17
  156. symbols:
  157. """
  158. # integer-divide the symbols in ~half for two test
  159. # groups. Assumes arr[:idx] is exclusive, and arr[idx:] is
  160. # inclusive
  161. half_len = len(pyth_accounts) // 2;
  162. for thing in pyth_accounts[:half_len]:
  163. name = thing["name"]
  164. price = thing["price"]
  165. product = thing["product"]
  166. cfg_yaml += f"""
  167. - name: {name}
  168. price_addr: {price}
  169. product_addr: {product}"""
  170. cfg_yaml += f"""
  171. - group_name: stuff
  172. conditions:
  173. min_freq_secs: 19
  174. symbols:
  175. """
  176. for stuff in pyth_accounts[half_len:]:
  177. name = stuff["name"]
  178. price = stuff["price"]
  179. product = stuff["product"]
  180. cfg_yaml += f"""
  181. - name: {name}
  182. price_addr: {price}
  183. product_addr: {product}"""
  184. with open(P2W_ATTESTATION_CFG, "w") as f:
  185. f.write(cfg_yaml)
  186. f.flush()
  187. # Send the first attestation in one-shot mode for testing
  188. first_attest_result = run_or_die(
  189. [
  190. "pyth2wormhole-client",
  191. "--log-level",
  192. "4",
  193. "--p2w-addr",
  194. P2W_SOL_ADDRESS,
  195. "--rpc-url",
  196. SOL_RPC_URL,
  197. "--payer",
  198. P2W_OWNER_KEYPAIR,
  199. "attest",
  200. "-f",
  201. P2W_ATTESTATION_CFG,
  202. ],
  203. capture_output=True,
  204. )
  205. logging.info("p2w_autoattest ready to roll!")
  206. find_and_log_seqnos(first_attest_result.stdout)
  207. # Serve p2w endpoint
  208. endpoint_thread = threading.Thread(target=serve_attestations, daemon=True)
  209. endpoint_thread.start()
  210. # Let k8s know the service is up
  211. readiness_thread = threading.Thread(target=readiness, daemon=True)
  212. readiness_thread.start()
  213. # Do not exit this script if a continuous attestation stops for
  214. # whatever reason (this avoids k8s restart penalty)
  215. while True:
  216. # Start the child process in daemon mode
  217. p2w_client_process = Popen(
  218. [
  219. "pyth2wormhole-client",
  220. "--log-level",
  221. "4",
  222. "--p2w-addr",
  223. P2W_SOL_ADDRESS,
  224. "--rpc-url",
  225. SOL_RPC_URL,
  226. "--payer",
  227. P2W_OWNER_KEYPAIR,
  228. "attest",
  229. "-f",
  230. P2W_ATTESTATION_CFG,
  231. "-d",
  232. ],
  233. stdout=PIPE,
  234. stderr=STDOUT,
  235. text=True,
  236. )
  237. saved_log_lines = []
  238. # Keep listening for seqnos until the program exits
  239. while p2w_client_process.poll() is None:
  240. line = p2w_client_process.stdout.readline()
  241. # Always pass output to the debug level
  242. logging.debug(f"pyth2wormhole-client: {line}")
  243. find_and_log_seqnos(line)
  244. # Extend with new line
  245. saved_log_lines.append(line)
  246. # trim back to specified maximum
  247. if len(saved_log_lines) > P2W_MAX_LOG_LINES:
  248. saved_log_lines.pop(0)
  249. # Yell if the supposedly non-stop attestation process exits
  250. logging.warn(f"pyth2wormhole-client stopped unexpectedly with code {p2w_client_process.retcode}")
  251. logging.warn(f"Last {len(saved_log_lines)} log lines:\n{(saved_log_lines)}")