From f24f86adf53acee982f2ee25aeb6eea289ffcc93 Mon Sep 17 00:00:00 2001 From: Stan Drozd Date: Wed, 4 Aug 2021 13:46:07 +0200 Subject: [PATCH] pyth2wormhole-client: Run an automated attestation script in Tilt Change-Id: Id2e6def6c246862601a206084867c5f1b26a6673 --- Dockerfile.client | 14 +- Tiltfile | 28 +++- devnet/p2w-client.yaml | 45 +++++++ devnet/pyth.yaml | 15 +-- devnet/solana-devnet.yaml | 2 +- shell.nix | 1 + solana/Dockerfile | 2 +- solana/keys/p2w_owner.json | 1 + solana/pyth2wormhole/client/src/cli.rs | 23 +++- solana/pyth2wormhole/client/src/main.rs | 84 ++++++++++-- .../pyth2wormhole/program/src/set_config.rs | 30 ++++- third_party/pyth/Dockerfile.p2w-client | 17 +++ .../pyth/{Dockerfile => Dockerfile.pyth} | 6 +- third_party/pyth/p2w_autoattest.py | 122 ++++++++++++++++++ third_party/pyth/pyth_publisher.py | 85 +++++++++--- third_party/pyth/pyth_utils.py | 65 +++++++--- 16 files changed, 457 insertions(+), 83 deletions(-) create mode 100644 devnet/p2w-client.yaml create mode 100644 solana/keys/p2w_owner.json create mode 100644 third_party/pyth/Dockerfile.p2w-client rename third_party/pyth/{Dockerfile => Dockerfile.pyth} (84%) create mode 100755 third_party/pyth/p2w_autoattest.py diff --git a/Dockerfile.client b/Dockerfile.client index 3c7904ad..31f5e130 100644 --- a/Dockerfile.client +++ b/Dockerfile.client @@ -3,14 +3,17 @@ FROM docker.io/library/rust:1.49@sha256:a50165ea96983c21832578afb1c8c028674c965b RUN apt-get update && apt-get install -yq libssl-dev libudev-dev pkg-config zlib1g-dev llvm clang ncat RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && apt-get install -y nodejs +RUN curl -sSfL https://release.solana.com/v1.7.8/install | sh RUN rustup default nightly-2021-08-01 RUN rustup component add rustfmt RUN --mount=type=cache,target=/root/.cache \ - cargo install --version =1.7.0 solana-cli && \ cargo install --version =2.0.12 spl-token-cli +ENV SOLANA_BIN_PATH="/root/.local/share/solana/install/active_release/bin" +ENV PATH="$SOLANA_BIN_PATH:$PATH" + ADD ethereum /usr/src/ethereum WORKDIR /usr/src/ethereum RUN --mount=type=cache,target=/root/.cache \ @@ -26,10 +29,15 @@ RUN --mount=type=cache,target=/root/.cache \ npm run build-contracts && \ npm run build + ADD solana /usr/src/solana -ADD proto /usr/src/proto +ADD proto /usr/src/proto WORKDIR /usr/src/solana + +RUN solana config set --keypair "/usr/src/solana/keys/solana-devnet.json" +RUN solana config set --url "http://solana-devnet:8899" + ENV EMITTER_ADDRESS="11111111111111111111111111111115" ENV BRIDGE_ADDRESS="Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o" @@ -39,7 +47,5 @@ RUN --mount=type=cache,target=/root/.cache \ set -xe && \ cargo build --manifest-path ./bridge/Cargo.toml --package client --release && \ cargo build --manifest-path ./modules/token_bridge/Cargo.toml --package client --release && \ - cp /usr/local/cargo/bin/solana /usr/local/bin && \ - cp /usr/local/cargo/bin/spl-token /usr/local/bin && \ cp bridge/target/release/client /usr/local/bin && \ cp modules/token_bridge/target/release/client /usr/local/bin/token-bridge-client diff --git a/Tiltfile b/Tiltfile index d86a2d6c..2a0cc734 100644 --- a/Tiltfile +++ b/Tiltfile @@ -113,7 +113,7 @@ k8s_resource("guardian", resource_deps = ["proto-gen", "solana-devnet"], port_fo docker_build( ref = "pyth", context = ".", - dockerfile = "third_party/pyth/Dockerfile", + dockerfile = "third_party/pyth/Dockerfile.pyth", ) k8s_yaml_with_ns("./devnet/pyth.yaml") @@ -136,13 +136,12 @@ k8s_resource( # solana client cli (used for devnet setup) docker_build( - ref = "solana-client", + ref = "bridge-client", context = ".", - only = ["./proto", "./solana", "./ethereum", "./clients/token_bridge"], + only = ["./proto", "./solana", "./ethereum", "./clients"], dockerfile = "Dockerfile.client", - # Ignore target folders from local (non-container) development. - ignore = ["./solana/target", "./solana/agent/target", "./solana/cli/target"], + ignore = ["./solana/*/target"], ) # solana smart contract @@ -167,6 +166,25 @@ k8s_resource( ], ) +# pyth2wormhole client + +docker_build( + ref = "p2w-client", + context = ".", + only = ["./solana", "./third_party"], + dockerfile = "./third_party/pyth/Dockerfile.p2w-client", + + # Ignore target folders from local (non-container) development. + ignore = ["./solana/*/target"], +) + +k8s_yaml_with_ns("devnet/p2w-client.yaml") + +k8s_resource("p2w-client", + resource_deps=["solana-devnet", "pyth"], + port_forwards=[] +) + # eth devnet docker_build( diff --git a/devnet/p2w-client.yaml b/devnet/p2w-client.yaml new file mode 100644 index 00000000..dce78147 --- /dev/null +++ b/devnet/p2w-client.yaml @@ -0,0 +1,45 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: p2w-client + labels: + app: p2w-client +spec: + ports: + - port: 8001 + name: http + protocol: TCP + clusterIP: None + selector: + app: p2w-client +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: p2w-client +spec: + selector: + matchLabels: + app: p2w-client + serviceName: p2w-client + replicas: 1 + template: + metadata: + labels: + app: p2w-client + spec: + restartPolicy: Always + terminationGracePeriodSeconds: 0 + containers: + - name: p2w-client + image: p2w-client + command: + - python3 + - /usr/src/pyth/p2w_autoattest.py + tty: true + readinessProbe: + tcpSocket: + port: 2000 + periodSeconds: 1 + failureThreshold: 300 diff --git a/devnet/pyth.yaml b/devnet/pyth.yaml index 267cbe4c..d05c42a1 100644 --- a/devnet/pyth.yaml +++ b/devnet/pyth.yaml @@ -13,6 +13,9 @@ spec: - port: 8898 name: pyth-tx protocol: TCP + - port: 4242 + name: pyth-accounts + protocol: TCP --- apiVersion: apps/v1 kind: StatefulSet @@ -23,8 +26,6 @@ spec: matchLabels: app: pyth serviceName: pyth - updateStrategy: - type: RollingUpdate template: metadata: labels: @@ -43,13 +44,7 @@ spec: port: 2000 periodSeconds: 1 failureThreshold: 300 - - name: pyth-tx - image: pyth - command: - - ./pyth_tx - - -r - - solana-devnet ports: - - containerPort: 8898 - name: pyth-tx + - containerPort: 4242 + name: pyth-accounts protocol: TCP diff --git a/devnet/solana-devnet.yaml b/devnet/solana-devnet.yaml index 46cafe46..d6a8dd66 100644 --- a/devnet/solana-devnet.yaml +++ b/devnet/solana-devnet.yaml @@ -94,7 +94,7 @@ spec: path: /health periodSeconds: 1 - name: setup - image: solana-client + image: bridge-client command: - /usr/src/solana/devnet_setup.sh readinessProbe: diff --git a/shell.nix b/shell.nix index 7fd465b8..e8296759 100644 --- a/shell.nix +++ b/shell.nix @@ -34,6 +34,7 @@ pkgs.mkShell { pkgconfig protobuf python3 + python3Packages.autopep8 whcluster whinotify whkube diff --git a/solana/Dockerfile b/solana/Dockerfile index 14bbade3..f3813ad3 100644 --- a/solana/Dockerfile +++ b/solana/Dockerfile @@ -43,7 +43,7 @@ RUN mkdir -p /opt/solana/deps ENV EMITTER_ADDRESS="11111111111111111111111111111115" ENV BRIDGE_ADDRESS="Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o" -# Build Wormhole Solana progrms +# Build Wormhole Solana programs RUN --mount=type=cache,target=bridge/target \ --mount=type=cache,target=modules/token_bridge/target \ --mount=type=cache,target=pyth2wormhole/target \ diff --git a/solana/keys/p2w_owner.json b/solana/keys/p2w_owner.json new file mode 100644 index 00000000..2e0e3842 --- /dev/null +++ b/solana/keys/p2w_owner.json @@ -0,0 +1 @@ +[39,20,181,104,82,27,70,145,227,136,168,14,170,24,33,88,145,152,180,229,219,142,247,114,237,79,52,97,84,65,213,172,49,165,99,116,254,135,110,132,214,114,59,200,109,253,45,43,74,172,107,84,162,223,23,15,78,167,240,137,234,123,4,231] \ No newline at end of file diff --git a/solana/pyth2wormhole/client/src/cli.rs b/solana/pyth2wormhole/client/src/cli.rs index efcce7f3..f0a93b07 100644 --- a/solana/pyth2wormhole/client/src/cli.rs +++ b/solana/pyth2wormhole/client/src/cli.rs @@ -26,9 +26,6 @@ pub struct Cli { pub rpc_url: String, #[clap(long)] pub p2w_addr: Pubkey, - /// The bridge program account - #[clap(long = "wh-prog")] - pub wh_prog: Pubkey, #[clap(subcommand)] pub action: Action, } @@ -37,8 +34,11 @@ pub struct Cli { pub enum Action { #[clap(about = "Initialize a pyth2wormhole program freshly deployed under ")] Init { - #[clap(long = "owner")] - new_owner_addr: Pubkey, + /// The bridge program account + #[clap(long = "wh-prog")] + wh_prog: Pubkey, + #[clap(long = "owner")] + owner_addr: Pubkey, #[clap(long = "pyth-owner")] pyth_owner_addr: Pubkey, }, @@ -53,4 +53,17 @@ pub enum Action { #[clap(long)] nonce: u32, }, + #[clap(about = "Update an existing pyth2wormhole program's settings (currently set owner only)")] + SetConfig { + /// Current owner keypair path + #[clap(long = "owner", default_value = "~/.config/solana/id.json")] + owner: String, + /// New owner to set + #[clap(long = "new-owner")] + new_owner_addr: Pubkey, + #[clap(long = "new-wh-prog")] + new_wh_prog: Pubkey, + #[clap(long = "new-pyth-owner")] + new_pyth_owner_addr: Pubkey, + }, } diff --git a/solana/pyth2wormhole/client/src/main.rs b/solana/pyth2wormhole/client/src/main.rs index b0455295..d78228d9 100644 --- a/solana/pyth2wormhole/client/src/main.rs +++ b/solana/pyth2wormhole/client/src/main.rs @@ -1,8 +1,8 @@ pub mod cli; -use borsh::BorshSerialize; +use borsh::{BorshDeserialize, BorshSerialize}; use clap::Clap; -use log::LevelFilter; +use log::{LevelFilter, error}; use solana_client::rpc_client::RpcClient; use solana_program::{ hash::Hash, @@ -54,6 +54,7 @@ use bridge::{ use pyth2wormhole::{ config::P2WConfigAccount, initialize::InitializeAccounts, + set_config::SetConfigAccounts, types::PriceAttestation, AttestData, Pyth2WormholeConfig, @@ -74,16 +75,31 @@ fn main() -> Result<(), ErrBox> { let tx = match cli.action { Action::Init { - new_owner_addr, + owner_addr, pyth_owner_addr, + wh_prog, } => handle_init( payer, p2w_addr, - new_owner_addr, - cli.wh_prog, + owner_addr, + wh_prog, pyth_owner_addr, recent_blockhash, )?, + Action::SetConfig { + owner, + new_owner_addr, + new_wh_prog, + new_pyth_owner_addr, + } => handle_set_config( + payer, + p2w_addr, + read_keypair_file(&*shellexpand::tilde(&owner))?, + new_owner_addr, + new_wh_prog, + new_pyth_owner_addr, + recent_blockhash, + )?, Action::Attest { product_addr, price_addr, @@ -139,6 +155,45 @@ fn handle_init( Ok(tx_signed) } +fn handle_set_config( + payer: Keypair, + p2w_addr: Pubkey, + owner: Keypair, + new_owner_addr: Pubkey, + new_wh_prog: Pubkey, + new_pyth_owner_addr: Pubkey, + recent_blockhash: Hash, +) -> Result { + use AccEntry::*; + + let payer_pubkey = payer.pubkey(); + + println!("Canary!"); + + let accs = SetConfigAccounts { + payer: Signer(payer), + current_owner: Signer(owner), + config: Derived(p2w_addr), + }; + + let config = Pyth2WormholeConfig { + owner: new_owner_addr, + wh_prog: new_wh_prog, + pyth_owner: new_pyth_owner_addr, + }; + let ix_data = (pyth2wormhole::instruction::Instruction::SetConfig, config); + + let (ix, signers) = accs.to_ix(p2w_addr, ix_data.try_to_vec()?.as_slice())?; + + let tx_signed = Transaction::new_signed_with_payer::>( + &[ix], + Some(&payer_pubkey), + signers.iter().collect::>().as_ref(), + recent_blockhash, + ); + Ok(tx_signed) +} + fn handle_attest( rpc: &RpcClient, // Needed for reading Pyth account data payer: Keypair, @@ -148,16 +203,19 @@ fn handle_attest( nonce: u32, recent_blockhash: Hash, ) -> Result { - let emitter_keypair = Keypair::new(); let message_keypair = Keypair::new(); + let p2w_config_addr = P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_addr); + + let config = Pyth2WormholeConfig::try_from_slice(rpc.get_account_data(&p2w_config_addr)?.as_slice())?; + // Derive dynamic seeded accounts let seq_addr = Sequence::key( &SequenceDerivationData { emitter_key: &emitter_keypair.pubkey(), }, - &wh_prog, + &config.wh_prog, ); // Arrange Attest accounts @@ -168,7 +226,7 @@ fn handle_attest( AccountMeta::new_readonly(system_program::id(), false), // config AccountMeta::new_readonly( - P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_addr), + p2w_config_addr, false, ), // pyth_product @@ -177,13 +235,11 @@ fn handle_attest( AccountMeta::new_readonly(price_addr, false), // clock AccountMeta::new_readonly(clock::id(), false), - - // wh_prog - AccountMeta::new_readonly(wh_prog, false), - + // wh_prog + AccountMeta::new_readonly(config.wh_prog, false), // wh_bridge AccountMeta::new( - Bridge::<{ AccountState::Initialized }>::key(None, &wh_prog), + Bridge::<{ AccountState::Initialized }>::key(None, &config.wh_prog), false, ), // wh_message @@ -193,7 +249,7 @@ fn handle_attest( // wh_sequence AccountMeta::new(seq_addr, false), // wh_fee_collector - AccountMeta::new(FeeCollector::<'_>::key(None, &wh_prog), false), + AccountMeta::new(FeeCollector::<'_>::key(None, &config.wh_prog), false), AccountMeta::new_readonly(rent::id(), false), ]; diff --git a/solana/pyth2wormhole/program/src/set_config.rs b/solana/pyth2wormhole/program/src/set_config.rs index cf903fa4..d0da0b17 100644 --- a/solana/pyth2wormhole/program/src/set_config.rs +++ b/solana/pyth2wormhole/program/src/set_config.rs @@ -1,19 +1,35 @@ -use solana_program::{msg, pubkey::Pubkey}; +use solana_program::{ + msg, + pubkey::Pubkey, +}; use solitaire::{ - AccountState, ExecutionContext, FromAccounts, Info, InstructionContext, Keyed, Peel, - Result as SoliResult, Signer, SolitaireError, ToInstruction, + AccountState, + ExecutionContext, + FromAccounts, + Info, + InstructionContext, + Keyed, + Mut, + Peel, + Result as SoliResult, + Signer, + SolitaireError, + ToInstruction, }; -use crate::config::{P2WConfigAccount, Pyth2WormholeConfig}; +use crate::config::{ + P2WConfigAccount, + Pyth2WormholeConfig, +}; #[derive(FromAccounts, ToInstruction)] pub struct SetConfig<'b> { /// Current config used by the program - pub config: P2WConfigAccount<'b, { AccountState::Initialized }>, + pub config: Mut>, /// Current owner authority of the program - pub current_owner: Signer>, + pub current_owner: Mut>>, /// Payer account for updating the account data - pub payer: Signer>, + pub payer: Mut>>, } impl<'b> InstructionContext<'b> for SetConfig<'b> { diff --git a/third_party/pyth/Dockerfile.p2w-client b/third_party/pyth/Dockerfile.p2w-client new file mode 100644 index 00000000..a0d57034 --- /dev/null +++ b/third_party/pyth/Dockerfile.p2w-client @@ -0,0 +1,17 @@ +FROM bridge-client + +RUN apt-get install -y python3 + +ADD third_party/pyth/pyth_utils.py /usr/src/pyth/pyth_utils.py +ADD third_party/pyth/p2w_autoattest.py /usr/src/pyth/p2w_autoattest.py + +RUN --mount=type=cache,target=/root/.cache \ + --mount=type=cache,target=target \ + --mount=type=cache,target=pyth2wormhole/target \ + cargo build --manifest-path ./pyth2wormhole/Cargo.toml --package pyth2wormhole-client && \ + mv pyth2wormhole/target/debug/pyth2wormhole-client /usr/local/bin/pyth2wormhole-client && \ + chmod a+rx /usr/src/pyth/*.py + +ENV P2W_OWNER_KEYPAIR="/usr/src/solana/keys/p2w_owner.json" +ENV PYTH_PUBLISHER_KEYPAIR="/usr/src/solana/keys/pyth_publisher.json" +ENV PYTH_PROGRAM_KEYPAIR="/usr/src/solana/keys/pyth_program.json" diff --git a/third_party/pyth/Dockerfile b/third_party/pyth/Dockerfile.pyth similarity index 84% rename from third_party/pyth/Dockerfile rename to third_party/pyth/Dockerfile.pyth index 5daf6afa..50242bfb 100644 --- a/third_party/pyth/Dockerfile +++ b/third_party/pyth/Dockerfile.pyth @@ -26,7 +26,7 @@ RUN cp /opt/solana/keys/pyth_publisher.json publish_key_pair.json && \ ENV PYTH_SRC_REV=31e3188bbf52ec1a25f71e4ab969378b27415b0a ENV PYTH_SRC_ROOT=/home/pyth/pyth-client -ADD https://github.com/drozdziak1/pyth-client/archive/$PYTH_SRC_REV.tar.gz . +ADD https://github.com/pyth-network/pyth-client/archive/$PYTH_SRC_REV.tar.gz . RUN tar -xvf *.tar.gz && \ rm -rf $PYTH_SRC_ROOT *.tar.gz && \ mv pyth-client-$PYTH_SRC_REV $PYTH_SRC_ROOT/ @@ -36,7 +36,9 @@ WORKDIR $PYTH_SRC_ROOT/build RUN cmake .. && make # Prepare setup script -ADD third_party/pyth/ /opt/pyth/ +ADD third_party/pyth/pyth_utils.py /opt/pyth/pyth_utils.py +ADD third_party/pyth/pyth_publisher.py /opt/pyth/pyth_publisher.py + RUN chmod a+rx /opt/pyth/*.py USER pyth diff --git a/third_party/pyth/p2w_autoattest.py b/third_party/pyth/p2w_autoattest.py new file mode 100755 index 00000000..f8bfa247 --- /dev/null +++ b/third_party/pyth/p2w_autoattest.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 + +# This script sets up a simple loop for periodical attestation of Pyth data +from pyth_utils import * + +from http.client import HTTPConnection + +import json +import os +import subprocess +import time +import threading + + +P2W_ADDRESS = "P2WH424242424242424242424242424242424242424" +P2W_ATTEST_INTERVAL = float(os.environ.get("P2W_ATTEST_INTERVAL", 5)) +P2W_OWNER_KEYPAIR = os.environ.get( + "P2W_OWNER_KEYPAIR", f"/usr/src/solana/keys/p2w_owner.json") + +PYTH_ACCOUNTS_HOST = "pyth" +PYTH_ACCOUNTS_PORT = 4242 + +WORMHOLE_ADDRESS = "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o" + +# Get actor pubkeys +P2W_OWNER_ADDRESS = sol_run_or_die( + "address", ["--keypair", P2W_OWNER_KEYPAIR], capture_output=True).stdout.strip() +PYTH_OWNER_ADDRESS = sol_run_or_die( + "address", ["--keypair", PYTH_PROGRAM_KEYPAIR], capture_output=True).stdout.strip() + + +# Top up pyth2wormhole owner +sol_run_or_die("airdrop", [ + str(SOL_AIRDROP_AMT), + "--keypair", P2W_OWNER_KEYPAIR, + "--commitment", "finalized", +], capture_output=True) + +# Initialize pyth2wormhole +init_result = run_or_die([ + "pyth2wormhole-client", + "--log-level", "4", + "--p2w-addr", P2W_ADDRESS, + "--rpc-url", SOL_RPC_URL, + "--payer", P2W_OWNER_KEYPAIR, + "init", + "--wh-prog", WORMHOLE_ADDRESS, + "--owner", P2W_OWNER_ADDRESS, + "--pyth-owner", PYTH_OWNER_ADDRESS, +], capture_output=True, die=False) + +if init_result.returncode != 0: + print("NOTE: pyth2wormhole-client init failed, retrying with set_config") + run_or_die([ + "pyth2wormhole-client", + "--log-level", "4", + "--p2w-addr", P2W_ADDRESS, + "--rpc-url", SOL_RPC_URL, + "--payer", P2W_OWNER_KEYPAIR, + "set-config", + "--owner", P2W_OWNER_KEYPAIR, + "--new-owner", P2W_OWNER_ADDRESS, + "--new-wh-prog", WORMHOLE_ADDRESS, + "--new-pyth-owner", PYTH_OWNER_ADDRESS, + ], capture_output=True) + +# Retrieve current price/product pubkeys from the pyth publisher +conn = HTTPConnection(PYTH_ACCOUNTS_HOST, PYTH_ACCOUNTS_PORT) + +conn.request("GET", "/") + +res = conn.getresponse() + +pyth_accounts = None + +if res.getheader("Content-Type") == "application/json": + pyth_accounts = json.load(res) +else: + print(f"Bad Content type {res.getheader('Content-Type')}", file=sys.stderr) + sys.exit(1) + +price_addr = pyth_accounts["price"] +product_addr = pyth_accounts["product"] + +nonce = 0 +attest_result = run_or_die([ + "pyth2wormhole-client", + "--log-level", "4", + "--p2w-addr", P2W_ADDRESS, + "--rpc-url", SOL_RPC_URL, + "--payer", P2W_OWNER_KEYPAIR, + "attest", + "--price", price_addr, + "--product", product_addr, + "--nonce", str(nonce), +], capture_output=True) + +print("p2w_autoattest ready to roll.") +print(f"ACCOUNTS: {pyth_accounts}") +print(f"Attest Interval: {P2W_ATTEST_INTERVAL}") + +# Let k8s know the service is up +readiness_thread = threading.Thread(target=readiness, daemon=True) +readiness_thread.start() + +nonce = 1 +while True: + attest_result = run_or_die([ + "pyth2wormhole-client", + "--log-level", "4", + "--p2w-addr", P2W_ADDRESS, + "--rpc-url", SOL_RPC_URL, + "--payer", P2W_OWNER_KEYPAIR, + "attest", + "--price", price_addr, + "--product", product_addr, + "--nonce", str(nonce), + ], capture_output=True) + time.sleep(P2W_ATTEST_INTERVAL) + nonce += 1 + +readiness_thread.join() diff --git a/third_party/pyth/pyth_publisher.py b/third_party/pyth/pyth_publisher.py index 1a761dc9..4ffd54f0 100644 --- a/third_party/pyth/pyth_publisher.py +++ b/third_party/pyth/pyth_publisher.py @@ -2,33 +2,72 @@ from pyth_utils import * +from http.server import HTTPServer, BaseHTTPRequestHandler + +import json import random import sys import threading import time -# Accept connections from readiness probe -def publisher_readiness(): - run_or_die(["nc", "-k", "-l", "-p", READINESS_PORT]) -# Update the specified price with random values +class P2WAccEndpoint(BaseHTTPRequestHandler): + """ + A dumb endpoint to respond with a JSON containing Pyth account addresses + """ + + def do_GET(self): + print(f"Got path {self.path}") + sys.stdout.flush() + data = json.dumps(ACCOUNTS).encode("utf-8") + print(f"Sending:\n{data}") + + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + self.wfile.flush() + + +ACCOUNTS = dict() + + def publisher_random_update(price_pubkey): + """ + Update the specified price with random values + """ value = random.randrange(1024) confidence = 1 - pyth_run_or_die("upd_price_val", args=[price_pubkey, str(value), str(confidence), "trading"]) - print("Price updated!") + pyth_run_or_die("upd_price_val", args=[ + price_pubkey, str(value), str(confidence), "trading" + ]) + print(f"Price {price_pubkey} value updated to {str(value)}!") + + +def accounts_endpoint(): + """ + Run a barebones HTTP server to share the dynamic Pyth + mapping/product/price account addresses + """ + server_address = ('', 4242) + httpd = HTTPServer(server_address, P2WAccEndpoint) + httpd.serve_forever() + # Fund the publisher -sol_run_or_die("airdrop", [str(SOL_AIRDROP_AMT), - "--keypair", PYTH_PUBLISHER_KEYPAIR, - "--commitment", "finalized", - ]) +sol_run_or_die("airdrop", [ + str(SOL_AIRDROP_AMT), + "--keypair", PYTH_PUBLISHER_KEYPAIR, + "--commitment", "finalized", +]) # Create a mapping pyth_run_or_die("init_mapping") # Add a product -prod_pubkey = pyth_run_or_die("add_product", capture_output=True).stdout.strip() +prod_pubkey = pyth_run_or_die( + "add_product", capture_output=True).stdout.strip() print(f"Added product {prod_pubkey}") # Add a price @@ -41,26 +80,40 @@ price_pubkey = pyth_run_or_die( print(f"Added price {price_pubkey}") -publisher_pubkey = sol_run_or_die("address", args=["--keypair", PYTH_PUBLISHER_KEYPAIR], capture_output=True).stdout.strip() +publisher_pubkey = sol_run_or_die("address", args=[ + "--keypair", PYTH_PUBLISHER_KEYPAIR +], capture_output=True).stdout.strip() # Become a publisher -pyth_run_or_die("add_publisher", args=[publisher_pubkey, price_pubkey], confirm=False, debug=True, capture_output=True) +pyth_run_or_die( + "add_publisher", args=[publisher_pubkey, price_pubkey], + confirm=False, + debug=True, + capture_output=True) print(f"Added publisher {publisher_pubkey}") # Update the price as the newly added publisher publisher_random_update(price_pubkey) -print(f"Updated price {price_pubkey}. Mock updates ready to roll. Updating every {str(PYTH_PUBLISHER_INTERVAL)} seconds") +print( + f"Mock updates ready to roll. Updating every {str(PYTH_PUBLISHER_INTERVAL)} seconds") # Spin off the readiness probe endpoint into a separate thread -readiness_thread = threading.Thread(target=publisher_readiness) +readiness_thread = threading.Thread(target=readiness, daemon=True) + +# Start an HTTP endpoint for looking up product/price address +http_service = threading.Thread(target=accounts_endpoint, daemon=True) + +ACCOUNTS["product"] = prod_pubkey +ACCOUNTS["price"] = price_pubkey readiness_thread.start() +http_service.start() while True: - print(f"Updating price {price_pubkey}") publisher_random_update(price_pubkey) time.sleep(PYTH_PUBLISHER_INTERVAL) sys.stdout.flush() readiness_thread.join() +http_service.join() diff --git a/third_party/pyth/pyth_utils.py b/third_party/pyth/pyth_utils.py index bc82d6f4..60205aa6 100644 --- a/third_party/pyth/pyth_utils.py +++ b/third_party/pyth/pyth_utils.py @@ -1,12 +1,15 @@ import os +import socketserver import sys import subprocess -PYTH=os.environ.get("PYTH", "./pyth") +PYTH = os.environ.get("PYTH", "./pyth") PYTH_KEY_STORE = os.environ.get("PYTH_KEY_STORE", "/home/pyth/.pythd") -PYTH_PROGRAM_KEYPAIR = f"{PYTH_KEY_STORE}/program_key_pair.json" -PYTH_PROGRAM_SO_PATH=os.environ.get("PYTH_PROGRAM_SO", "../target/oracle.so") -PYTH_PUBLISHER_KEYPAIR = f"{PYTH_KEY_STORE}/publish_key_pair.json" +PYTH_PROGRAM_KEYPAIR = os.environ.get( + "PYTH_PROGRAM_KEYPAIR", f"{PYTH_KEY_STORE}/publish_key_pair.json") +PYTH_PROGRAM_SO_PATH = os.environ.get("PYTH_PROGRAM_SO", "../target/oracle.so") +PYTH_PUBLISHER_KEYPAIR = os.environ.get( + "PYTH_PUBLISHER_KEYPAIR", f"{PYTH_KEY_STORE}/publish_key_pair.json") PYTH_PUBLISHER_INTERVAL = float(os.environ.get("PYTH_PUBLISHER_INTERVAL", "5")) SOL_AIRDROP_AMT = 100 @@ -14,10 +17,13 @@ SOL_RPC_HOST = "solana-devnet" SOL_RPC_PORT = 8899 SOL_RPC_URL = f"http://{SOL_RPC_HOST}:{str(SOL_RPC_PORT)}" -READINESS_PORT=os.environ.get("READINESS_PORT", "2000") +READINESS_PORT = int(os.environ.get("READINESS_PORT", "2000")) + -# pretend we're set -e def run_or_die(args, die=True, **kwargs): + """ + Opinionated subprocess.run() call with fancy logging + """ args_readable = ' '.join(args) print(f"CMD RUN\t{args_readable}", file=sys.stderr) sys.stderr.flush() @@ -42,18 +48,41 @@ def run_or_die(args, die=True, **kwargs): sys.stderr.flush() return ret -# Pyth boilerplate in front of run_or_die -def pyth_run_or_die(subcommand, args=[], debug=False, confirm=True, **kwargs): - return run_or_die([PYTH, subcommand] - + args - + (["-d"] if debug else []) - + ([] if confirm else ["-n"]) # Note: not all pyth subcommands accept -n - + ["-k", PYTH_KEY_STORE] - + ["-r", SOL_RPC_HOST] - + ["-c", "finalized"], **kwargs) -# Solana boilerplate in front of run_or_die +def pyth_run_or_die(subcommand, args=[], debug=False, confirm=True, **kwargs): + """ + Pyth boilerplate in front of run_or_die + """ + return run_or_die( + [PYTH, subcommand] + + args + + (["-d"] if debug else []) + # Note: not all pyth subcommands accept -n + + ([] if confirm else ["-n"]) + + ["-k", PYTH_KEY_STORE] + + ["-r", SOL_RPC_HOST] + + ["-c", "finalized"], **kwargs) + + def sol_run_or_die(subcommand, args=[], **kwargs): + """ + Solana boilerplate in front of run_or_die + """ return run_or_die(["solana", subcommand] - + args - + ["--url", SOL_RPC_URL], **kwargs) + + args + + ["--url", SOL_RPC_URL], **kwargs) + + +class ReadinessTCPHandler(socketserver.StreamRequestHandler): + def handle(self): + """TCP black hole""" + self.rfile.read(64) + + +def readiness(): + """ + Accept connections from readiness probe + """ + with socketserver.TCPServer(("0.0.0.0", READINESS_PORT), ReadinessTCPHandler) as srv: + srv.serve_forever() + # run_or_die(["nc", "-k", "-l", "-p", READINESS_PORT])