p2w_autoattest.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  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. cfg_yaml = f"""
  148. ---
  149. symbols:"""
  150. logging.info(
  151. f"Retrieved {len(pyth_accounts)} Pyth accounts from endpoint: {pyth_accounts}"
  152. )
  153. for acc in pyth_accounts:
  154. name = acc["name"]
  155. price = acc["price"]
  156. product = acc["product"]
  157. cfg_yaml += f"""
  158. - name: {name}
  159. price_addr: {price}
  160. product_addr: {product}"""
  161. with open(P2W_ATTESTATION_CFG, "w") as f:
  162. f.write(cfg_yaml)
  163. f.flush()
  164. # Send the first attestation in one-shot mode for testing
  165. first_attest_result = run_or_die(
  166. [
  167. "pyth2wormhole-client",
  168. "--log-level",
  169. "4",
  170. "--p2w-addr",
  171. P2W_SOL_ADDRESS,
  172. "--rpc-url",
  173. SOL_RPC_URL,
  174. "--payer",
  175. P2W_OWNER_KEYPAIR,
  176. "attest",
  177. "-f",
  178. P2W_ATTESTATION_CFG,
  179. ],
  180. capture_output=True,
  181. )
  182. logging.info("p2w_autoattest ready to roll!")
  183. find_and_log_seqnos(first_attest_result.stdout)
  184. # Serve p2w endpoint
  185. endpoint_thread = threading.Thread(target=serve_attestations, daemon=True)
  186. endpoint_thread.start()
  187. # Let k8s know the service is up
  188. readiness_thread = threading.Thread(target=readiness, daemon=True)
  189. readiness_thread.start()
  190. # Do not exit this script if a continuous attestation stops for
  191. # whatever reason (this avoids k8s restart penalty)
  192. while True:
  193. # Start the child process in daemon mode
  194. p2w_client_process = Popen(
  195. [
  196. "pyth2wormhole-client",
  197. "--log-level",
  198. "4",
  199. "--p2w-addr",
  200. P2W_SOL_ADDRESS,
  201. "--rpc-url",
  202. SOL_RPC_URL,
  203. "--payer",
  204. P2W_OWNER_KEYPAIR,
  205. "attest",
  206. "-f",
  207. P2W_ATTESTATION_CFG,
  208. "-d",
  209. ],
  210. stdout=PIPE,
  211. stderr=STDOUT,
  212. text=True,
  213. )
  214. saved_log_lines = []
  215. # Keep listening for seqnos until the program exits
  216. while p2w_client_process.poll() is None:
  217. line = p2w_client_process.stdout.readline()
  218. # Always pass output to the debug level
  219. logging.debug(f"pyth2wormhole-client: {line}")
  220. find_and_log_seqnos(line)
  221. # Extend with new line
  222. saved_log_lines.append(line)
  223. # trim back to specified maximum
  224. if len(saved_log_lines) > P2W_MAX_LOG_LINES:
  225. saved_log_lines.pop(0)
  226. # Yell if the supposedly non-stop attestation process exits
  227. logging.warn(f"pyth2wormhole-client stopped unexpectedly with code {p2w_client_process.retcode}")
  228. logging.warn(f"Last {len(saved_log_lines)} log lines:\n{(saved_log_lines)}")