Browse Source

[solana receiver] Try solana signature verification builtin (#736)

* gr

* wtf

* wtf

* jfc

* jfc

* oh thank god

* make the test work

* cleanup

* cleanup
Jayant Krishnamurthy 2 years ago
parent
commit
c3b2322ac0

+ 1 - 0
target_chains/solana/Anchor.toml

@@ -16,3 +16,4 @@ wallet = "~/.config/solana/id.json"
 deploy = "anchor deploy --program-keypair program_address.json --program-name solana-receiver"
 cli_build = "cargo build --package pyth-solana-receiver-cli"
 cli_test = "cargo run --package pyth-solana-receiver-cli post-and-receive-vaa -v AQAAAAABABPz9W9ARa/V06ZBp68lkoGJ3tlnJRZ2/jYU4Wi1jdQAdQj3ZkPLKIMtzN3EqaCAjKrLG/sKqADTbPoaBtn9zKUBZAYEhwAAAAAAAfNGGVrALzfWDU24/6bvdMsb41UAR1Q6Sp7prPTXhpewAAAAAApC0/gBUDJXSAADAAEAAQIAAwCdfVoraihHxJUfK+NKfkJ4r4DsPSm8uMcxEWv1D/av5l7/DsJkQsV9dFZpW4Q2lOc3mxXPGyULJ+DkfmV/GVWq/wAAAAAAG8iMAAAAAAAAARb////7AAAAAAAbz04AAAAAAAABfQEAAAACAAAACwAAAABkBgSHAAAAAGQGBIcAAAAAZAYEhwAAAAAAG8hzAAAAAAAAAS8AAAAAZAYEhD3tPwvP5dgslVQroHu37+dlyxeoTqcDWIDDMUz5Y0KBMhuk1gj6dbp21tc9qnFavL3rnboCJX8FobWReLSfWZsAAAAAACBQdgAAAAAAAANS////+wAAAAAAID+JAAAAAAAAA00BAAAAAgAAAAsAAAAAZAYEhwAAAABkBgSHAAAAAGQGBIYAAAAAACBQKwAAAAAAAAOdAAAAAGQGBIRPu3srgFy1FVEoeBBWS6f74PhYsJvdbggVRmGyUzFy1DChkVj1pUwK34+3VgYnND8iobyFK4nVa+GszcXb+W0OAAAAAAAcQ44AAAAAAAAAhP////0AAAAAABxBSQAAAAAAAACXAQAAAAIAAAATAAAAAGQGBIcAAAAAZAYEhwAAAABkBgSGAAAAAAAcQ28AAAAAAAAAZgAAAABkBgSE"
+test = "anchor build && cargo test"

File diff suppressed because it is too large
+ 569 - 75
target_chains/solana/Cargo.lock


+ 5 - 0
target_chains/solana/README.md

@@ -29,3 +29,8 @@ Transaction successful : 3L1vxzSHQv6B6TwtoMv2Y6m7vFGz3hzqApGHEhHSLA9Jn5dNKeWRWKv
 Receiver program ID is 5dXnHcDXdXaiEp9QgknCDPsEhJStSqZqJ4ATirWfEqeY
 Transaction successful : u5y9Hqc18so3BnjSUvZkLZR4mvA8zkiBgzGKHSEYyWkHQhH3uQatM7xWf4kdrhjZFVGbfBLdR8RJJUmuf28ePtG
 ```
+
+## Unit tests
+
+Run `anchor run test` to run the unit tests in the `src/tests/` directory.
+**Warning**: do not confuse this command with `anchor test`, which doesn't do anything useful.

+ 8 - 0
target_chains/solana/programs/solana-receiver/Cargo.toml

@@ -20,7 +20,15 @@ anchor-lang = "0.26.0"
 wormhole-core = { git = "https://github.com/guibescos/wormhole", branch = "reisen/sdk-solana"}
 wormhole-solana = { git = "https://github.com/guibescos/wormhole", branch = "reisen/sdk-solana"}
 pyth-wormhole-attester-sdk = { path = "../../../../wormhole_attester/sdk/rust" }
+solana-program = "1.15.2"
+hex = "0.4.3"
 
 [dev-dependencies]
 pyth-sdk = "0.5.0"
 pyth-sdk-solana = "0.1.0"
+solana-program-test = "=1.15.2"
+solana-sdk = "=1.15.2"
+tokio = "1.14.1"
+bincode = "1.3.3"
+libsecp256k1 = "0.7.1"
+rand = "0.8.5"

+ 36 - 31
target_chains/solana/programs/solana-receiver/src/lib.rs

@@ -1,6 +1,9 @@
 pub mod error;
 pub mod state;
 
+#[cfg(test)]
+mod tests;
+
 use {
     wormhole::Chain::{
         self,
@@ -10,8 +13,9 @@ use {
     state::AnchorVaa,
     anchor_lang::prelude::*,
     pyth_wormhole_attester_sdk::BatchPriceAttestation,
+    solana_program::{ keccak, secp256k1_recover::secp256k1_recover },
 };
-
+use hex::ToHex;
 use crate::error::ReceiverError::*;
 
 declare_id!("pythKkWXoywbvTQVcWrNDz5ENvWteF7tem7xzW52NBK");
@@ -42,6 +46,33 @@ pub mod pyth_solana_receiver {
 
         Ok(())
     }
+
+    pub fn update(ctx: Context<Update>, data: Vec<u8>, recovery_id: u8, signature: [u8; 64]) -> Result<()> {
+        // This costs about 10k compute units
+        let message_hash = {
+            let mut hasher = keccak::Hasher::default();
+            hasher.hash(&data);
+            hasher.result()
+        };
+
+        // This costs about 25k compute units
+        let recovered_pubkey = secp256k1_recover(
+            &message_hash.0,
+            recovery_id,
+            &signature,
+        ).map_err(|_| ProgramError::InvalidArgument)?;
+
+        msg!("Recovered key: {}", recovered_pubkey.0.encode_hex::<String>());
+
+        // TODO: Check the pubkey is an expected value.
+        // Here we are checking the secp256k1 pubkey against a known authorized pubkey.
+        //
+        // if recovered_pubkey.0 != AUTHORIZED_PUBLIC_KEY {
+        //  return Err(ProgramError::InvalidArgument);
+        // }
+
+        Ok(())
+    }
 }
 
 #[derive(Accounts)]
@@ -64,34 +95,8 @@ impl crate::accounts::DecodePostedVaa {
     }
 }
 
-#[cfg(test)]
-mod tests {
-    use pyth_sdk::Identifier;
-    use pyth_wormhole_attester_sdk::PriceStatus;
-    use pyth_wormhole_attester_sdk::PriceAttestation;
-
-    #[test]
-    fn mock_attestation() {
-        // TODO: create a VAA with this attestation as payload
-        // and then invoke DecodePostedVaa
-
-        let _attestation = PriceAttestation {
-            product_id:                 Identifier::new([18u8; 32]),
-            price_id:                   Identifier::new([150u8; 32]),
-            price:                      0x2bad2feed7,
-            conf:                       101,
-            ema_price:                  -42,
-            ema_conf:                   42,
-            expo:                       -3,
-            status:                     PriceStatus::Trading,
-            num_publishers:             123212u32,
-            max_num_publishers:         321232u32,
-            attestation_time:           (0xdeadbeeffadeu64) as i64,
-            publish_time:               0xdadebeefi64,
-            prev_publish_time:          0xdeadbabei64,
-            prev_price:                 0xdeadfacebeefi64,
-            prev_conf:                  0xbadbadbeefu64,
-            last_attested_publish_time: (0xdeadbeeffadedeafu64) as i64,
-        };
-    }
+#[derive(Accounts)]
+pub struct Update<'info> {
+    #[account(mut)]
+    pub payer:          Signer<'info>,
 }

+ 2 - 0
target_chains/solana/programs/solana-receiver/src/tests/mod.rs

@@ -0,0 +1,2 @@
+mod simulator;
+mod test_update_price;

+ 163 - 0
target_chains/solana/programs/solana-receiver/src/tests/simulator.rs

@@ -0,0 +1,163 @@
+use {
+    crate::ID,
+    solana_program::{
+        bpf_loader_upgradeable::{
+            self,
+            UpgradeableLoaderState,
+        },
+        hash::hash,
+        hash::Hash,
+        instruction::{
+            AccountMeta,
+            Instruction,
+        },
+        native_token::LAMPORTS_PER_SOL,
+        pubkey::Pubkey,
+        rent::Rent,
+        stake_history::Epoch,
+        system_instruction,
+        system_program,
+    },
+    solana_program_test::{
+        BanksClient,
+        BanksClientError,
+        ProgramTest,
+        ProgramTestBanksClientExt,
+        read_file,
+    },
+    solana_sdk::{
+        account::Account,
+        signature::{
+            Keypair,
+            Signer,
+        },
+        transaction::Transaction,
+    },
+    std::{
+        mem::size_of,
+        path::Path,
+    },
+};
+
+/// Simulator for the state of the target chain program on Solana. You can run solana transactions against
+/// this struct to test how pyth instructions execute in the Solana runtime.
+pub struct ProgramSimulator {
+    pub program_id:            Pubkey,
+    banks_client:          BanksClient,
+    /// Hash used to submit the last transaction. The hash must be advanced for each new
+    /// transaction; otherwise, replayed transactions in different states can return stale
+    /// results.
+    last_blockhash:        Hash,
+    programdata_id:        Pubkey,
+    pub upgrade_authority: Keypair,
+    pub genesis_keypair:   Keypair,
+}
+
+impl ProgramSimulator {
+
+    /// Deploys the target chain contract as upgradable
+    pub async fn new() -> ProgramSimulator {
+        let mut bpf_data = read_file(
+            std::env::current_dir()
+                .unwrap()
+                .join(Path::new("../../target/deploy/pyth_solana_receiver.so")),
+        );
+
+
+        let mut program_test = ProgramTest::default();
+        let program_key = Pubkey::try_from(ID).unwrap();
+        // This PDA is the actual address in the real world
+        // https://docs.rs/solana-program/1.6.4/solana_program/bpf_loader_upgradeable/index.html
+        let (programdata_key, _) =
+            Pubkey::find_program_address(&[&program_key.to_bytes()], &bpf_loader_upgradeable::id());
+
+        let upgrade_authority_keypair = Keypair::new();
+
+        let program_deserialized = UpgradeableLoaderState::Program {
+            programdata_address: programdata_key,
+        };
+        let programdata_deserialized = UpgradeableLoaderState::ProgramData {
+            slot:                      1,
+            upgrade_authority_address: Some(upgrade_authority_keypair.pubkey()),
+        };
+
+        // Program contains a pointer to progradata
+        let program_vec = bincode::serialize(&program_deserialized).unwrap();
+        // Programdata contains a header and the binary of the program
+        let mut programdata_vec = bincode::serialize(&programdata_deserialized).unwrap();
+        programdata_vec.append(&mut bpf_data);
+
+        let program_account = Account {
+            lamports:   Rent::default().minimum_balance(program_vec.len()),
+            data:       program_vec,
+            owner:      bpf_loader_upgradeable::ID,
+            executable: true,
+            rent_epoch: Epoch::default(),
+        };
+        let programdata_account = Account {
+            lamports:   Rent::default().minimum_balance(programdata_vec.len()),
+            data:       programdata_vec,
+            owner:      bpf_loader_upgradeable::ID,
+            executable: false,
+            rent_epoch: Epoch::default(),
+        };
+
+        // Add to both accounts to program test, now the program is deploy as upgradable
+        program_test.add_account(program_key, program_account);
+        program_test.add_account(programdata_key, programdata_account);
+
+
+        // Start validator
+        let (banks_client, genesis_keypair, recent_blockhash) = program_test.start().await;
+
+        let mut result = ProgramSimulator {
+            program_id: program_key,
+            banks_client,
+            last_blockhash: recent_blockhash,
+            programdata_id: programdata_key,
+            upgrade_authority: upgrade_authority_keypair,
+            genesis_keypair,
+        };
+
+        // Transfer money to upgrade_authority so it can call the instructions
+        result
+            .airdrop(&result.upgrade_authority.pubkey(), 1000 * LAMPORTS_PER_SOL)
+            .await
+            .unwrap();
+
+        result
+    }
+
+
+    /// Process a transaction containing `instruction` signed by `signers`.
+    /// `payer` is used to pay for and sign the transaction.
+    pub async fn process_ix(
+        &mut self,
+        instruction: Instruction,
+        signers: &Vec<&Keypair>,
+        payer: &Keypair,
+    ) -> Result<(), BanksClientError> {
+        let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
+
+        let blockhash = self
+            .banks_client
+            .get_new_latest_blockhash(&self.last_blockhash)
+            .await
+            .unwrap();
+        self.last_blockhash = blockhash;
+
+        transaction.partial_sign(&[payer], self.last_blockhash);
+        transaction.partial_sign(signers, self.last_blockhash);
+
+        self.banks_client.process_transaction(transaction).await
+    }
+
+    /// Send `lamports` worth of SOL to the pubkey `to`.
+    pub async fn airdrop(&mut self, to: &Pubkey, lamports: u64) -> Result<(), BanksClientError> {
+        let instruction =
+            system_instruction::transfer(&self.genesis_keypair.pubkey(), to, lamports);
+
+        self.process_ix(instruction, &vec![], &self.genesis_keypair.insecure_clone())
+            .await
+    }
+}

+ 47 - 0
target_chains/solana/programs/solana-receiver/src/tests/test_update_price.rs

@@ -0,0 +1,47 @@
+use {
+    solana_program::{
+        program_error::ProgramError,
+        pubkey::Pubkey,
+        instruction::Instruction,
+    },
+    solana_sdk::{
+        signature::Signer,
+        keccak,
+    },
+    crate::instruction as receiver_instruction,
+    crate::accounts as receiver_accounts,
+};
+use anchor_lang::prelude::*;
+use anchor_lang::Discriminator;
+use anchor_lang::{
+Owner,
+ToAccountMetas,
+InstructionData,
+AnchorDeserialize,
+};
+use rand::rngs::OsRng;
+
+use crate::tests::simulator::ProgramSimulator;
+
+#[tokio::test]
+async fn test_update_price() {
+    let mut sim = ProgramSimulator::new().await;
+
+    let message = b"hello world";
+    let message_hash = {
+        let mut hasher = keccak::Hasher::default();
+        hasher.hash(message);
+        hasher.result()
+    };
+
+    let secp256k1_secret_key = libsecp256k1::SecretKey::random(&mut OsRng);
+    let secp_message = libsecp256k1::Message::parse(&message_hash.0);
+    let (signature, recovery_id) = libsecp256k1::sign(&secp_message, &secp256k1_secret_key);
+
+    let accounts = receiver_accounts::Update { payer: sim.genesis_keypair.pubkey() }.to_account_metas(None);
+    let instruction_data = receiver_instruction::Update { data: message.to_vec(), recovery_id: recovery_id.serialize(), signature: signature.serialize() }.data();
+
+    let inst = Instruction::new_with_bytes(sim.program_id, &instruction_data, accounts);
+
+    let result = sim.process_ix(inst, &vec![], &sim.genesis_keypair.insecure_clone()).await.unwrap();
+}

Some files were not shown because too many files changed in this diff