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

* gr

* wtf

* wtf

* jfc

* jfc

* oh thank god

* make the test work

* cleanup

* cleanup
This commit is contained in:
Jayant Krishnamurthy 2023-04-05 10:44:45 -07:00 committed by GitHub
parent bbdd98fa06
commit c3b2322ac0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1665 additions and 269 deletions

View File

@ -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 Load Diff

View File

@ -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.

View File

@ -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"

View File

@ -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>,
}

View File

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

View File

@ -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
}
}

View File

@ -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();
}