p2w_autoattest.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  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 pyth_utils import *
  13. logging.basicConfig(
  14. level=logging.DEBUG, format="%(asctime)s | %(module)s | %(levelname)s | %(message)s"
  15. )
  16. P2W_SOL_ADDRESS = os.environ.get(
  17. "P2W_SOL_ADDRESS", "P2WH424242424242424242424242424242424242424"
  18. )
  19. P2W_ATTEST_INTERVAL = float(os.environ.get("P2W_ATTEST_INTERVAL", 5))
  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_EXIT_ON_ERROR = os.environ.get("P2W_EXIT_ON_ERROR", "False").lower() == "true"
  32. ATTESTATIONS = {
  33. "pendingSeqnos": [],
  34. }
  35. class P2WAutoattestStatusEndpoint(BaseHTTPRequestHandler):
  36. """
  37. A dumb endpoint for last attested price metadata.
  38. """
  39. def do_GET(self):
  40. logging.info(f"Got path {self.path}")
  41. sys.stdout.flush()
  42. data = json.dumps(ATTESTATIONS).encode("utf-8")
  43. logging.debug(f"Sending: {data}")
  44. ATTESTATIONS["pendingSeqnos"] = []
  45. self.send_response(200)
  46. self.send_header("Content-Type", "application/json")
  47. self.send_header("Content-Length", str(len(data)))
  48. self.end_headers()
  49. self.wfile.write(data)
  50. self.wfile.flush()
  51. def serve_attestations():
  52. """
  53. Run a barebones HTTP server to share Pyth2wormhole attestation history
  54. """
  55. server_address = ("", P2W_ATTESTATIONS_PORT)
  56. httpd = HTTPServer(server_address, P2WAutoattestStatusEndpoint)
  57. httpd.serve_forever()
  58. if SOL_AIRDROP_AMT > 0:
  59. # Fund the p2w owner
  60. sol_run_or_die(
  61. "airdrop",
  62. [
  63. str(SOL_AIRDROP_AMT),
  64. "--keypair",
  65. P2W_OWNER_KEYPAIR,
  66. "--commitment",
  67. "finalized",
  68. ],
  69. )
  70. if P2W_INITIALIZE_SOL_CONTRACT is not None:
  71. # Get actor pubkeys
  72. P2W_OWNER_ADDRESS = sol_run_or_die(
  73. "address", ["--keypair", P2W_OWNER_KEYPAIR], capture_output=True
  74. ).stdout.strip()
  75. PYTH_OWNER_ADDRESS = sol_run_or_die(
  76. "address", ["--keypair", PYTH_PROGRAM_KEYPAIR], capture_output=True
  77. ).stdout.strip()
  78. init_result = run_or_die(
  79. [
  80. "pyth2wormhole-client",
  81. "--log-level",
  82. "4",
  83. "--p2w-addr",
  84. P2W_SOL_ADDRESS,
  85. "--rpc-url",
  86. SOL_RPC_URL,
  87. "--payer",
  88. P2W_OWNER_KEYPAIR,
  89. "init",
  90. "--wh-prog",
  91. WORMHOLE_ADDRESS,
  92. "--owner",
  93. P2W_OWNER_ADDRESS,
  94. "--pyth-owner",
  95. PYTH_OWNER_ADDRESS,
  96. ],
  97. capture_output=True,
  98. die=False,
  99. )
  100. if init_result.returncode != 0:
  101. logging.error(
  102. "NOTE: pyth2wormhole-client init failed, retrying with set_config"
  103. )
  104. run_or_die(
  105. [
  106. "pyth2wormhole-client",
  107. "--log-level",
  108. "4",
  109. "--p2w-addr",
  110. P2W_SOL_ADDRESS,
  111. "--rpc-url",
  112. SOL_RPC_URL,
  113. "--payer",
  114. P2W_OWNER_KEYPAIR,
  115. "set-config",
  116. "--owner",
  117. P2W_OWNER_KEYPAIR,
  118. "--new-owner",
  119. P2W_OWNER_ADDRESS,
  120. "--new-wh-prog",
  121. WORMHOLE_ADDRESS,
  122. "--new-pyth-owner",
  123. PYTH_OWNER_ADDRESS,
  124. ],
  125. capture_output=True,
  126. )
  127. # Retrieve available symbols from the test pyth publisher if not provided in envs
  128. if P2W_ATTESTATION_CFG is None:
  129. P2W_ATTESTATION_CFG = "./attestation_cfg_test.yaml"
  130. conn = HTTPConnection(PYTH_TEST_ACCOUNTS_HOST, PYTH_TEST_ACCOUNTS_PORT)
  131. conn.request("GET", "/")
  132. res = conn.getresponse()
  133. pyth_accounts = None
  134. if res.getheader("Content-Type") == "application/json":
  135. pyth_accounts = json.load(res)
  136. else:
  137. logging.error("Bad Content type")
  138. sys.exit(1)
  139. cfg_yaml = f"""
  140. ---
  141. symbols:"""
  142. logging.info(
  143. f"Retrieved {len(pyth_accounts)} Pyth accounts from endpoint: {pyth_accounts}"
  144. )
  145. for acc in pyth_accounts:
  146. name = acc["name"]
  147. price = acc["price"]
  148. product = acc["product"]
  149. cfg_yaml += f"""
  150. - name: {name}
  151. price_addr: {price}
  152. product_addr: {product}"""
  153. with open(P2W_ATTESTATION_CFG, "w") as f:
  154. f.write(cfg_yaml)
  155. f.flush()
  156. attest_result = run_or_die(
  157. [
  158. "pyth2wormhole-client",
  159. "--log-level",
  160. "4",
  161. "--p2w-addr",
  162. P2W_SOL_ADDRESS,
  163. "--rpc-url",
  164. SOL_RPC_URL,
  165. "--payer",
  166. P2W_OWNER_KEYPAIR,
  167. "attest",
  168. "-f",
  169. P2W_ATTESTATION_CFG,
  170. ],
  171. capture_output=True,
  172. )
  173. logging.info("p2w_autoattest ready to roll!")
  174. logging.info(f"Attest Interval: {P2W_ATTEST_INTERVAL}")
  175. # Serve p2w endpoint
  176. endpoint_thread = threading.Thread(target=serve_attestations, daemon=True)
  177. endpoint_thread.start()
  178. # Let k8s know the service is up
  179. readiness_thread = threading.Thread(target=readiness, daemon=True)
  180. readiness_thread.start()
  181. seqno_regex = re.compile(r"Sequence number: (\d+)")
  182. while True:
  183. matches = seqno_regex.findall(attest_result.stdout)
  184. seqnos = list(map(lambda m: int(m), matches))
  185. ATTESTATIONS["pendingSeqnos"] += seqnos
  186. logging.info(f"{len(seqnos)} batch seqno(s) received: {seqnos})")
  187. attest_result = run_or_die(
  188. [
  189. "pyth2wormhole-client",
  190. "--log-level",
  191. "4",
  192. "--p2w-addr",
  193. P2W_SOL_ADDRESS,
  194. "--rpc-url",
  195. SOL_RPC_URL,
  196. "--payer",
  197. P2W_OWNER_KEYPAIR,
  198. "attest",
  199. "-f",
  200. P2W_ATTESTATION_CFG,
  201. ],
  202. capture_output=True,
  203. die=P2W_EXIT_ON_ERROR,
  204. )
  205. time.sleep(P2W_ATTEST_INTERVAL)