p2w_autoattest.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. #!/usr/bin/env python3
  2. # This script sets up a simple loop for periodical attestation of Pyth data
  3. from pyth_utils import *
  4. from http.client import HTTPConnection
  5. from http.server import HTTPServer, BaseHTTPRequestHandler
  6. import json
  7. import os
  8. import re
  9. import subprocess
  10. import time
  11. import threading
  12. P2W_ADDRESS = "P2WH424242424242424242424242424242424242424"
  13. P2W_ATTEST_INTERVAL = float(os.environ.get("P2W_ATTEST_INTERVAL", 5))
  14. P2W_OWNER_KEYPAIR = os.environ.get(
  15. "P2W_OWNER_KEYPAIR", f"/usr/src/solana/keys/p2w_owner.json")
  16. P2W_ATTESTATIONS_PORT = int(os.environ.get("P2W_ATTESTATIONS_PORT", 4343))
  17. PYTH_ACCOUNTS_HOST = "pyth"
  18. PYTH_ACCOUNTS_PORT = 4242
  19. WORMHOLE_ADDRESS = "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
  20. ATTESTATIONS = {
  21. "pendingSeqnos": [],
  22. }
  23. class P2WAutoattestStatusEndpoint(BaseHTTPRequestHandler):
  24. """
  25. A dumb endpoint for last attested price metadata.
  26. """
  27. def do_GET(self):
  28. print(f"Got path {self.path}")
  29. sys.stdout.flush()
  30. data = json.dumps(ATTESTATIONS).encode("utf-8")
  31. print(f"Sending:\n{data}")
  32. ATTESTATIONS["pendingSeqnos"] = []
  33. self.send_response(200)
  34. self.send_header("Content-Type", "application/json")
  35. self.send_header("Content-Length", str(len(data)))
  36. self.end_headers()
  37. self.wfile.write(data)
  38. self.wfile.flush()
  39. def serve_attestations():
  40. """
  41. Run a barebones HTTP server to share Pyth2wormhole attestation history
  42. """
  43. server_address = ('', P2W_ATTESTATIONS_PORT)
  44. httpd = HTTPServer(server_address, P2WAutoattestStatusEndpoint)
  45. httpd.serve_forever()
  46. # Get actor pubkeys
  47. P2W_OWNER_ADDRESS = sol_run_or_die(
  48. "address", ["--keypair", P2W_OWNER_KEYPAIR], capture_output=True).stdout.strip()
  49. PYTH_OWNER_ADDRESS = sol_run_or_die(
  50. "address", ["--keypair", PYTH_PROGRAM_KEYPAIR], capture_output=True).stdout.strip()
  51. # Top up pyth2wormhole owner
  52. sol_run_or_die("airdrop", [
  53. str(SOL_AIRDROP_AMT),
  54. "--keypair", P2W_OWNER_KEYPAIR,
  55. "--commitment", "finalized",
  56. ], capture_output=True)
  57. # Initialize pyth2wormhole
  58. init_result = run_or_die([
  59. "pyth2wormhole-client",
  60. "--log-level", "4",
  61. "--p2w-addr", P2W_ADDRESS,
  62. "--rpc-url", SOL_RPC_URL,
  63. "--payer", P2W_OWNER_KEYPAIR,
  64. "init",
  65. "--wh-prog", WORMHOLE_ADDRESS,
  66. "--owner", P2W_OWNER_ADDRESS,
  67. "--pyth-owner", PYTH_OWNER_ADDRESS,
  68. ], capture_output=True, die=False)
  69. if init_result.returncode != 0:
  70. print("NOTE: pyth2wormhole-client init failed, retrying with set_config")
  71. run_or_die([
  72. "pyth2wormhole-client",
  73. "--log-level", "4",
  74. "--p2w-addr", P2W_ADDRESS,
  75. "--rpc-url", SOL_RPC_URL,
  76. "--payer", P2W_OWNER_KEYPAIR,
  77. "set-config",
  78. "--owner", P2W_OWNER_KEYPAIR,
  79. "--new-owner", P2W_OWNER_ADDRESS,
  80. "--new-wh-prog", WORMHOLE_ADDRESS,
  81. "--new-pyth-owner", PYTH_OWNER_ADDRESS,
  82. ], capture_output=True)
  83. # Retrieve current price/product pubkeys from the pyth publisher
  84. conn = HTTPConnection(PYTH_ACCOUNTS_HOST, PYTH_ACCOUNTS_PORT)
  85. conn.request("GET", "/")
  86. res = conn.getresponse()
  87. pyth_accounts = None
  88. if res.getheader("Content-Type") == "application/json":
  89. pyth_accounts = json.load(res)
  90. else:
  91. print(f"Bad Content type {res.getheader('Content-Type')}", file=sys.stderr)
  92. sys.exit(1)
  93. price_addr = pyth_accounts["price"]
  94. product_addr = pyth_accounts["product"]
  95. nonce = 0
  96. attest_result = run_or_die([
  97. "pyth2wormhole-client",
  98. "--log-level", "4",
  99. "--p2w-addr", P2W_ADDRESS,
  100. "--rpc-url", SOL_RPC_URL,
  101. "--payer", P2W_OWNER_KEYPAIR,
  102. "attest",
  103. "--price", price_addr,
  104. "--product", product_addr,
  105. "--nonce", str(nonce),
  106. ], capture_output=True)
  107. print("p2w_autoattest ready to roll.")
  108. print(f"ACCOUNTS: {pyth_accounts}")
  109. print(f"Attest Interval: {P2W_ATTEST_INTERVAL}")
  110. # Serve p2w endpoint
  111. endpoint_thread = threading.Thread(target=serve_attestations, daemon=True)
  112. endpoint_thread.start()
  113. # Let k8s know the service is up
  114. readiness_thread = threading.Thread(target=readiness, daemon=True)
  115. readiness_thread.start()
  116. seqno_regex = re.compile(r"^Sequence number: (\d+)")
  117. nonce = 1
  118. while True:
  119. attest_result = run_or_die([
  120. "pyth2wormhole-client",
  121. "--log-level", "4",
  122. "--p2w-addr", P2W_ADDRESS,
  123. "--rpc-url", SOL_RPC_URL,
  124. "--payer", P2W_OWNER_KEYPAIR,
  125. "attest",
  126. "--price", price_addr,
  127. "--product", product_addr,
  128. "--nonce", str(nonce),
  129. ], capture_output=True)
  130. time.sleep(P2W_ATTEST_INTERVAL)
  131. matches = seqno_regex.match(attest_result.stdout)
  132. if matches is not None:
  133. seqno = int(matches.group(1))
  134. print(f"Got seqno {seqno}")
  135. ATTESTATIONS["pendingSeqnos"].append(seqno)
  136. else:
  137. print(f"Warning: Could not get sequence number")
  138. nonce += 1
  139. readiness_thread.join()