p2w_autoattest.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  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. from http.client import HTTPConnection
  10. from subprocess import PIPE, STDOUT, Popen
  11. from pyth_utils import *
  12. logging.basicConfig(
  13. level=logging.DEBUG, format="%(asctime)s | %(module)s | %(levelname)s | %(message)s"
  14. )
  15. P2W_SOL_ADDRESS = os.environ.get(
  16. "P2W_SOL_ADDRESS", "P2WH424242424242424242424242424242424242424"
  17. )
  18. P2W_OWNER_KEYPAIR = os.environ.get(
  19. "P2W_OWNER_KEYPAIR", "/usr/src/solana/keys/p2w_owner.json"
  20. )
  21. P2W_ATTESTATIONS_PORT = int(os.environ.get("P2W_ATTESTATIONS_PORT", 4343))
  22. P2W_INITIALIZE_SOL_CONTRACT = os.environ.get("P2W_INITIALIZE_SOL_CONTRACT", None)
  23. PYTH_TEST_ACCOUNTS_HOST = "pyth"
  24. PYTH_TEST_ACCOUNTS_PORT = 4242
  25. P2W_ATTESTATION_CFG = os.environ.get("P2W_ATTESTATION_CFG", None)
  26. WORMHOLE_ADDRESS = os.environ.get(
  27. "WORMHOLE_ADDRESS", "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
  28. )
  29. # attester needs string, but we validate as int first
  30. P2W_RPC_TIMEOUT_SECS = str(int(os.environ.get("P2W_RPC_TIMEOUT_SECS", "20")))
  31. if SOL_AIRDROP_AMT > 0:
  32. # Fund the p2w owner
  33. sol_run_or_die(
  34. "airdrop",
  35. [
  36. str(SOL_AIRDROP_AMT),
  37. "--keypair",
  38. P2W_OWNER_KEYPAIR,
  39. "--commitment",
  40. "finalized",
  41. ],
  42. )
  43. if P2W_INITIALIZE_SOL_CONTRACT is not None:
  44. # Get actor pubkeys
  45. P2W_OWNER_ADDRESS = sol_run_or_die(
  46. "address", ["--keypair", P2W_OWNER_KEYPAIR], capture_output=True
  47. ).stdout.strip()
  48. PYTH_OWNER_ADDRESS = sol_run_or_die(
  49. "address", ["--keypair", PYTH_PROGRAM_KEYPAIR], capture_output=True
  50. ).stdout.strip()
  51. init_result = run_or_die(
  52. [
  53. "pyth2wormhole-client",
  54. "--p2w-addr",
  55. P2W_SOL_ADDRESS,
  56. "--rpc-url",
  57. SOL_RPC_URL,
  58. "--payer",
  59. P2W_OWNER_KEYPAIR,
  60. "init",
  61. "--wh-prog",
  62. WORMHOLE_ADDRESS,
  63. "--owner",
  64. P2W_OWNER_ADDRESS,
  65. "--pyth-owner",
  66. PYTH_OWNER_ADDRESS,
  67. ],
  68. capture_output=True,
  69. die=False,
  70. )
  71. if init_result.returncode != 0:
  72. logging.error(
  73. "NOTE: pyth2wormhole-client init failed, retrying with set_config"
  74. )
  75. run_or_die(
  76. [
  77. "pyth2wormhole-client",
  78. "--p2w-addr",
  79. P2W_SOL_ADDRESS,
  80. "--rpc-url",
  81. SOL_RPC_URL,
  82. "--payer",
  83. P2W_OWNER_KEYPAIR,
  84. "set-config",
  85. "--owner",
  86. P2W_OWNER_KEYPAIR,
  87. "--new-owner",
  88. P2W_OWNER_ADDRESS,
  89. "--new-wh-prog",
  90. WORMHOLE_ADDRESS,
  91. "--new-pyth-owner",
  92. PYTH_OWNER_ADDRESS,
  93. ],
  94. capture_output=True,
  95. )
  96. # Retrieve available symbols from the test pyth publisher if not provided in envs
  97. if P2W_ATTESTATION_CFG is None:
  98. P2W_ATTESTATION_CFG = "./attestation_cfg_test.yaml"
  99. conn = HTTPConnection(PYTH_TEST_ACCOUNTS_HOST, PYTH_TEST_ACCOUNTS_PORT)
  100. conn.request("GET", "/")
  101. res = conn.getresponse()
  102. publisher_state_map = {}
  103. if res.getheader("Content-Type") == "application/json":
  104. publisher_state_map = json.load(res)
  105. else:
  106. logging.error("Bad Content type")
  107. sys.exit(1)
  108. pyth_accounts = publisher_state_map["symbols"]
  109. logging.info(
  110. f"Retrieved {len(pyth_accounts)} Pyth accounts from endpoint: {pyth_accounts}"
  111. )
  112. mapping_addr = publisher_state_map["mapping_addr"]
  113. cfg_yaml = f"""
  114. ---
  115. mapping_addr: {mapping_addr}
  116. mapping_reload_interval_mins: 1 # Very fast for testing purposes
  117. min_rpc_interval_ms: 0 # RIP RPC
  118. max_batch_jobs: 1000 # Where we're going there's no oomkiller
  119. default_attestation_conditions:
  120. min_interval_secs: 60
  121. symbol_groups:
  122. - group_name: fast_interval_only
  123. conditions:
  124. min_interval_secs: 1
  125. symbols:
  126. """
  127. # integer-divide the symbols in ~half for two test
  128. # groups. Assumes arr[:idx] is exclusive, and arr[idx:] is
  129. # inclusive
  130. third_len = len(pyth_accounts) // 3;
  131. for thing in pyth_accounts[:third_len]:
  132. name = thing["name"]
  133. price = thing["price"]
  134. product = thing["product"]
  135. cfg_yaml += f"""
  136. - type: key
  137. name: {name}
  138. price: {price}
  139. product: {product}"""
  140. # End of fast_interval_only
  141. cfg_yaml += f"""
  142. - group_name: longer_interval_sensitive_changes
  143. conditions:
  144. min_interval_secs: 10
  145. price_changed_bps: 300
  146. symbols:
  147. """
  148. for stuff in pyth_accounts[third_len:-third_len]:
  149. name = stuff["name"]
  150. price = stuff["price"]
  151. product = stuff["product"]
  152. cfg_yaml += f"""
  153. - type: key
  154. name: {name}
  155. price: {price}
  156. product: {product}"""
  157. cfg_yaml += f"""
  158. - group_name: mapping
  159. conditions:
  160. min_interval_secs: 30
  161. price_changed_bps: 500
  162. symbols: []
  163. """
  164. with open(P2W_ATTESTATION_CFG, "w") as f:
  165. f.write(cfg_yaml)
  166. f.flush()
  167. # Set helpfully chatty logging default, filtering especially annoying
  168. # modules like async HTTP requests and tokio runtime logs
  169. os.environ["RUST_LOG"] = os.environ.get("RUST_LOG", "pyth2wormhole_client,solana_client,main,pyth_sdk_solana=trace")
  170. # Send the first attestation in one-shot mode for testing
  171. first_attest_result = run_or_die(
  172. [
  173. "pyth2wormhole-client",
  174. "--commitment",
  175. "confirmed",
  176. "--p2w-addr",
  177. P2W_SOL_ADDRESS,
  178. "--rpc-url",
  179. SOL_RPC_URL,
  180. "--payer",
  181. P2W_OWNER_KEYPAIR,
  182. "attest",
  183. "-f",
  184. P2W_ATTESTATION_CFG,
  185. "--timeout",
  186. P2W_RPC_TIMEOUT_SECS,
  187. ],
  188. capture_output=True,
  189. )
  190. logging.info("p2w_autoattest ready to roll!")
  191. # Let k8s know the service is up
  192. readiness_thread = threading.Thread(target=readiness, daemon=True)
  193. readiness_thread.start()
  194. # Do not exit this script if a continuous attestation stops for
  195. # whatever reason (this avoids k8s restart penalty)
  196. while True:
  197. # Start the child process in daemon mode
  198. p2w_client_process = Popen(
  199. [
  200. "pyth2wormhole-client",
  201. "--commitment",
  202. "confirmed",
  203. "--p2w-addr",
  204. P2W_SOL_ADDRESS,
  205. "--rpc-url",
  206. SOL_RPC_URL,
  207. "--payer",
  208. P2W_OWNER_KEYPAIR,
  209. "attest",
  210. "-f",
  211. P2W_ATTESTATION_CFG,
  212. "-d",
  213. "--timeout",
  214. P2W_RPC_TIMEOUT_SECS,
  215. ]
  216. )
  217. # Wait for an unexpected process exit
  218. retcode = p2w_client_process.wait()
  219. # Yell if the supposedly non-stop attestation process exits
  220. logging.warn(f"pyth2wormhole-client stopped unexpectedly with code {retcode}")