diff --git a/.github/workflows/ci-solana-contract.yml b/.github/workflows/ci-solana-contract.yml new file mode 100644 index 000000000..178dd6533 --- /dev/null +++ b/.github/workflows/ci-solana-contract.yml @@ -0,0 +1,33 @@ +name: Test Solana Contract + +on: + pull_request: + paths: + - target_chains/solana/** + - pythnet/pythnet_sdk/** + push: + branches: + - main + paths: + - target_chains/solana/** + - pythnet/pythnet_sdk/** + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: target_chains/solana + steps: + - uses: actions/checkout@v2 + - name: Install Solana + run: | + sh -c "$(curl -sSfL https://release.solana.com/v1.16.20/install)" + echo "/home/runner/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH + - name: Build + run: cargo-build-sbf + - name: Run tests + run: cargo-test-sbf diff --git a/pythnet/pythnet_sdk/Cargo.toml b/pythnet/pythnet_sdk/Cargo.toml index edde49215..a52ac5921 100644 --- a/pythnet/pythnet_sdk/Cargo.toml +++ b/pythnet/pythnet_sdk/Cargo.toml @@ -11,7 +11,7 @@ crate-type = ["lib"] name = "pythnet_sdk" [features] -test-utils = ["dep:wormhole-sdk", "dep:serde_wormhole"] +test-utils = ["dep:wormhole-sdk", "dep:serde_wormhole", "dep:libsecp256k1", "dep:rand"] [dependencies] bincode = "1.3.1" @@ -28,6 +28,8 @@ slow_primes = "0.1.14" thiserror = "1.0.40" serde_wormhole = { git = "https://github.com/wormhole-foundation/wormhole", optional = true, tag="rust-sdk-2024-01-25"} wormhole-sdk = { git = "https://github.com/wormhole-foundation/wormhole", optional = true, tag="rust-sdk-2024-01-25"} +libsecp256k1 = {version ="0.7.1", optional = true} +rand = {version = "0.8.5", optional = true} [dev-dependencies] base64 = "0.21.0" diff --git a/pythnet/pythnet_sdk/src/test_utils/mod.rs b/pythnet/pythnet_sdk/src/test_utils/mod.rs index be382037e..bdd338389 100644 --- a/pythnet/pythnet_sdk/src/test_utils/mod.rs +++ b/pythnet/pythnet_sdk/src/test_utils/mod.rs @@ -23,8 +23,22 @@ use { }, }, byteorder::BigEndian, + libsecp256k1::{ + Message as libsecp256k1Message, + RecoveryId, + SecretKey, + Signature, + }, + rand::{ + seq::SliceRandom, + thread_rng, + }, serde_wormhole::RawMessage, wormhole_sdk::{ + vaa::{ + Body, + Header, + }, Address, Chain, Vaa, @@ -67,6 +81,18 @@ pub const DEFAULT_VALID_TIME_PERIOD: u64 = 180; const DEFAULT_SEQUENCE: u64 = 2; +const NUM_GUARDIANS: u8 = 19; // Matches wormhole mainnet +const DEFAULT_NUM_SIGNATURES: usize = 13; // Matches wormhole mainnet + +pub fn dummy_guardians() -> Vec { + let mut result: Vec = vec![]; + for i in 0..NUM_GUARDIANS { + let mut secret_key_bytes = [0u8; 32]; + secret_key_bytes[0] = i + 1; + result.push(SecretKey::parse(&secret_key_bytes).unwrap()); + } + result +} pub fn create_dummy_price_feed_message(value: i64) -> Message { let mut dummy_id = [0; 32]; @@ -155,12 +181,60 @@ pub fn create_vaa_from_payload( emitter_chain: Chain, sequence: u64, ) -> Vaa> { - let vaa: Vaa> = Vaa { - emitter_chain: emitter_chain, - emitter_address: emitter_address, + let guardians = dummy_guardians(); + + let body: Body> = Body { + emitter_chain, + emitter_address, sequence, payload: >::from(payload.to_vec()), ..Default::default() }; - vaa + + let digest = libsecp256k1Message::parse_slice(&body.digest().unwrap().secp256k_hash).unwrap(); + + let signatures: Vec<(Signature, RecoveryId)> = guardians + .iter() + .map(|x| libsecp256k1::sign(&digest, &x)) + .collect(); + let wormhole_signatures: Vec = signatures + .iter() + .enumerate() + .map(|(i, (x, y))| { + let mut signature = [0u8; 65]; + signature[..64].copy_from_slice(&x.serialize()); + signature[64] = y.serialize(); + wormhole_sdk::vaa::Signature { + index: i as u8, + signature, + } + }) + .collect(); + + let mut wormhole_signatures_subset: Vec = wormhole_signatures + .choose_multiple(&mut thread_rng(), DEFAULT_NUM_SIGNATURES) + .cloned() + .collect(); + + wormhole_signatures_subset.sort_by(|a, b| a.index.cmp(&b.index)); + + let header = Header { + version: 1, + signatures: wormhole_signatures_subset, + ..Default::default() + }; + + + (header, body).into() +} + +pub fn trim_vaa_signatures(vaa: Vaa<&RawMessage>, n: u8) -> Vaa<&RawMessage> { + let mut vaa_copy = vaa.clone(); + vaa_copy.signatures = vaa + .signatures + .choose_multiple(&mut thread_rng(), n.into()) + .cloned() + .collect(); + vaa_copy.signatures.sort_by(|a, b| a.index.cmp(&b.index)); + vaa_copy } diff --git a/target_chains/cosmwasm/Cargo.lock b/target_chains/cosmwasm/Cargo.lock index 0bdf91925..0983a1a9b 100644 --- a/target_chains/cosmwasm/Cargo.lock +++ b/target_chains/cosmwasm/Cargo.lock @@ -34,6 +34,12 @@ version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + [[package]] name = "autocfg" version = "1.1.0" @@ -585,6 +591,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -981,6 +997,16 @@ dependencies = [ "serde", ] +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + [[package]] name = "hmac" version = "0.12.1" @@ -990,6 +1016,17 @@ dependencies = [ "digest 0.10.6", ] +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array", + "hmac 0.8.1", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1106,6 +1143,54 @@ dependencies = [ "winapi", ] +[[package]] +name = "libsecp256k1" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b09eff1b35ed3b33b877ced3a691fc7a481919c7e29c53c906226fcf55e2a1" +dependencies = [ + "arrayref", + "base64", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy 0.2.2", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core", +] + [[package]] name = "linux-raw-sys" version = "0.3.1" @@ -1368,6 +1453,12 @@ dependencies = [ "spki", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro-crate" version = "0.1.5" @@ -1557,6 +1648,8 @@ dependencies = [ "byteorder", "fast-math", "hex", + "libsecp256k1", + "rand", "rustc_version", "serde", "serde_wormhole", @@ -1575,6 +1668,27 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -1666,7 +1780,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" dependencies = [ "crypto-bigint", - "hmac", + "hmac 0.12.1", "zeroize", ] diff --git a/target_chains/near/receiver/Cargo.lock b/target_chains/near/receiver/Cargo.lock index b47ff31e0..e002051cd 100644 --- a/target_chains/near/receiver/Cargo.lock +++ b/target_chains/near/receiver/Cargo.lock @@ -1097,6 +1097,27 @@ dependencies = [ "serde", ] +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array", + "hmac", +] + [[package]] name = "home" version = "0.5.4" @@ -1310,6 +1331,54 @@ version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +[[package]] +name = "libsecp256k1" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b09eff1b35ed3b33b877ced3a691fc7a481919c7e29c53c906226fcf55e2a1" +dependencies = [ + "arrayref", + "base64 0.13.1", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.8.5", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core", +] + [[package]] name = "libz-sys" version = "1.1.8" @@ -2237,6 +2306,7 @@ dependencies = [ "byteorder", "fast-math", "hex 0.4.3", + "libsecp256k1", "rustc_version", "serde", "serde_wormhole", diff --git a/target_chains/solana/Cargo.lock b/target_chains/solana/Cargo.lock index 022d77aa2..402f6f20c 100644 --- a/target_chains/solana/Cargo.lock +++ b/target_chains/solana/Cargo.lock @@ -3075,11 +3075,15 @@ dependencies = [ "byteorder", "fast-math", "hex", + "libsecp256k1 0.7.1", + "rand 0.8.5", "rustc_version", "serde", + "serde_wormhole", "sha3 0.10.6", "slow_primes", "thiserror", + "wormhole-sdk", ] [[package]] diff --git a/target_chains/solana/cli/src/main.rs b/target_chains/solana/cli/src/main.rs index dde94d1b3..bed6f2cc6 100644 --- a/target_chains/solana/cli/src/main.rs +++ b/target_chains/solana/cli/src/main.rs @@ -16,14 +16,11 @@ use { Cli, }, pyth_solana_receiver::{ + sdk::deserialize_accumulator_update_data, state::config::DataSource, PostUpdatesAtomicParams, }, - pythnet_sdk::wire::v1::{ - AccumulatorUpdateData, - MerklePriceUpdate, - Proof, - }, + pythnet_sdk::wire::v1::MerklePriceUpdate, serde_wormhole::RawMessage, solana_client::{ rpc_client::RpcClient, @@ -96,7 +93,8 @@ fn main() -> Result<()> { let payer = read_keypair_file(&*shellexpand::tilde(&keypair)).expect("Keypair not found"); - let (vaa, merkle_price_updates) = deserialize_accumulator_update_data(&payload)?; + let payload_bytes: Vec = base64::decode(payload)?; + let (vaa, merkle_price_updates) = deserialize_accumulator_update_data(payload_bytes)?; process_write_encoded_vaa_and_post_price_update( &rpc_client, @@ -114,7 +112,8 @@ fn main() -> Result<()> { let payer = read_keypair_file(&*shellexpand::tilde(&keypair)).expect("Keypair not found"); - let (vaa, merkle_price_updates) = deserialize_accumulator_update_data(&payload)?; + let payload_bytes: Vec = base64::decode(payload)?; + let (vaa, merkle_price_updates) = deserialize_accumulator_update_data(payload_bytes)?; process_post_price_update_atomic( &rpc_client, @@ -202,17 +201,6 @@ fn main() -> Result<()> { Ok(()) } -pub fn deserialize_accumulator_update_data( - payload: &str, -) -> Result<(Vec, Vec)> { - let payload_bytes: Vec = base64::decode(payload)?; - let accumulator_update_data = AccumulatorUpdateData::try_from_slice(payload_bytes.as_slice())?; - - match accumulator_update_data.proof { - Proof::WormholeMerkle { vaa, updates } => return Ok((vaa.as_ref().to_vec(), updates)), - } -} - pub fn process_upgrade_guardian_set( rpc_client: &RpcClient, vaa: &[u8], diff --git a/target_chains/solana/programs/pyth-solana-receiver/Cargo.toml b/target_chains/solana/programs/pyth-solana-receiver/Cargo.toml index 09278e811..531233a61 100644 --- a/target_chains/solana/programs/pyth-solana-receiver/Cargo.toml +++ b/target_chains/solana/programs/pyth-solana-receiver/Cargo.toml @@ -17,7 +17,7 @@ test-bpf = [] [dependencies] anchor-lang = "0.28.0" -pythnet-sdk = { path = "../../../../pythnet/pythnet_sdk", version = "2.0.0" } +pythnet-sdk = { path = "../../../../pythnet/pythnet_sdk" } solana-program = "1.16.20" byteorder = "1.4.3" wormhole-core-bridge-solana = {workspace = true} @@ -36,3 +36,4 @@ lazy_static = "1.4.0" program-simulator = { path = "../../program_simulator" } wormhole-sdk = { workspace = true } serde_wormhole = { workspace = true } +pythnet-sdk = { path = "../../../../pythnet/pythnet_sdk", features = ["test-utils"] } diff --git a/target_chains/solana/programs/pyth-solana-receiver/src/sdk.rs b/target_chains/solana/programs/pyth-solana-receiver/src/sdk.rs index ed8bc3602..c536d8c16 100644 --- a/target_chains/solana/programs/pyth-solana-receiver/src/sdk.rs +++ b/target_chains/solana/programs/pyth-solana-receiver/src/sdk.rs @@ -3,6 +3,7 @@ use { accounts, instruction, state::config::Config, + PostUpdatesAtomicParams, CONFIG_SEED, ID, TREASURY_SEED, @@ -12,7 +13,11 @@ use { system_program, InstructionData, }, - pythnet_sdk::wire::v1::MerklePriceUpdate, + pythnet_sdk::wire::v1::{ + AccumulatorUpdateData, + MerklePriceUpdate, + Proof, + }, solana_program::instruction::Instruction, wormhole_core_bridge_solana::state::GuardianSet, }; @@ -38,14 +43,7 @@ impl accounts::PostUpdatesAtomic { let config = get_config_address(); let treasury = get_treasury_address(); - let guardian_set = Pubkey::find_program_address( - &[ - GuardianSet::SEED_PREFIX, - guardian_set_index.to_be_bytes().as_ref(), - ], - &wormhole_address, - ) - .0; + let guardian_set = get_guardian_set_address(wormhole_address, guardian_set_index); accounts::PostUpdatesAtomic { payer, @@ -111,6 +109,38 @@ impl instruction::PostUpdates { } } + +impl instruction::PostUpdatesAtomic { + pub fn populate( + payer: Pubkey, + price_update_account: Pubkey, + wormhole_address: Pubkey, + guardian_set_index: u32, + vaa: Vec, + merkle_price_update: MerklePriceUpdate, + ) -> Instruction { + let post_update_accounts = accounts::PostUpdatesAtomic::populate( + payer, + price_update_account, + wormhole_address, + guardian_set_index, + ) + .to_account_metas(None); + Instruction { + program_id: ID, + accounts: post_update_accounts, + data: instruction::PostUpdatesAtomic { + params: PostUpdatesAtomicParams { + vaa, + merkle_price_update, + }, + } + .data(), + } + } +} + + pub fn get_treasury_address() -> Pubkey { Pubkey::find_program_address(&[TREASURY_SEED.as_ref()], &ID).0 } @@ -118,3 +148,25 @@ pub fn get_treasury_address() -> Pubkey { pub fn get_config_address() -> Pubkey { Pubkey::find_program_address(&[CONFIG_SEED.as_ref()], &ID).0 } + +pub fn get_guardian_set_address(wormhole_address: Pubkey, guardian_set_index: u32) -> Pubkey { + Pubkey::find_program_address( + &[ + GuardianSet::SEED_PREFIX, + guardian_set_index.to_be_bytes().as_ref(), + ], + &wormhole_address, + ) + .0 +} + +pub fn deserialize_accumulator_update_data( + accumulator_message: Vec, +) -> Result<(Vec, Vec)> { + let accumulator_update_data = + AccumulatorUpdateData::try_from_slice(accumulator_message.as_slice()).unwrap(); + + match accumulator_update_data.proof { + Proof::WormholeMerkle { vaa, updates } => return Ok((vaa.as_ref().to_vec(), updates)), + } +} diff --git a/target_chains/solana/programs/pyth-solana-receiver/tests/common/mod.rs b/target_chains/solana/programs/pyth-solana-receiver/tests/common/mod.rs index 8c0f28d26..631158c81 100644 --- a/target_chains/solana/programs/pyth-solana-receiver/tests/common/mod.rs +++ b/target_chains/solana/programs/pyth-solana-receiver/tests/common/mod.rs @@ -1,10 +1,12 @@ use { anchor_lang::AnchorSerialize, + libsecp256k1::PublicKey, program_simulator::ProgramSimulator, pyth_solana_receiver::{ instruction::Initialize, sdk::{ get_config_address, + get_guardian_set_address, get_treasury_address, }, state::config::{ @@ -13,23 +15,13 @@ use { }, ID, }, - pythnet_sdk::{ - accumulators::{ - merkle::MerkleTree, - Accumulator, - }, - hashers::keccak256_160::Keccak160, - messages::{ - Message, - PriceFeedMessage, - }, - wire::{ - v1::MerklePriceUpdate, - PrefixedVec, - }, + pythnet_sdk::test_utils::{ + dummy_guardians, + DEFAULT_DATA_SOURCE, }, serde_wormhole::RawMessage, solana_program::{ + keccak, pubkey::Pubkey, rent::Rent, }, @@ -42,111 +34,38 @@ use { wormhole_core_bridge_solana::{ state::{ EncodedVaa, + GuardianSet, Header, ProcessingStatus, }, ID as BRIDGE_ID, }, - wormhole_sdk::{ - Chain, - Vaa, - }, + wormhole_sdk::Vaa, }; -pub fn dummy_receiver_config(data_source: DataSource) -> Config { +pub const DEFAULT_GUARDIAN_SET_INDEX: u32 = 0; + +pub fn default_receiver_config() -> Config { Config { governance_authority: Pubkey::new_unique(), target_governance_authority: None, wormhole: BRIDGE_ID, valid_data_sources: vec![DataSource { - chain: data_source.chain, - emitter: data_source.emitter, + chain: DEFAULT_DATA_SOURCE.chain.into(), + emitter: Pubkey::from(DEFAULT_DATA_SOURCE.address.0), }], single_update_fee_in_lamports: 1, minimum_signatures: 5, } } -pub fn dummy_data_source() -> DataSource { - let emitter_address = Pubkey::new_unique().to_bytes(); - let emitter_chain = Chain::Pythnet; - DataSource { - chain: emitter_chain.into(), - emitter: Pubkey::from(emitter_address), - } -} - -pub fn dummy_price_messages() -> Vec { - vec![ - PriceFeedMessage { - feed_id: [0u8; 32], - price: 1, - conf: 2, - exponent: 3, - publish_time: 4, - prev_publish_time: 5, - ema_price: 6, - ema_conf: 7, - }, - PriceFeedMessage { - feed_id: [8u8; 32], - price: 9, - conf: 10, - exponent: 11, - publish_time: 12, - prev_publish_time: 13, - ema_price: 14, - ema_conf: 15, - }, - ] -} -pub fn dummy_price_updates( - price_feed_messages: Vec, -) -> (MerkleTree, Vec) { - let messages = price_feed_messages - .iter() - .map(|x| Message::PriceFeedMessage(*x)) - .collect::>(); - let price_feed_message_as_vec: Vec> = messages - .iter() - .map(|x| pythnet_sdk::wire::to_vec::<_, byteorder::BigEndian>(&x).unwrap()) - .collect(); - - let merkle_tree_accumulator = - MerkleTree::::from_set(price_feed_message_as_vec.iter().map(|x| x.as_ref())) - .unwrap(); - let merkle_price_updates: Vec = price_feed_message_as_vec - .iter() - .map(|x| MerklePriceUpdate { - message: PrefixedVec::::from(x.clone()), - proof: merkle_tree_accumulator.prove(x.as_ref()).unwrap(), - }) - .collect(); - - (merkle_tree_accumulator, merkle_price_updates) +pub struct ProgramTestFixtures { + pub program_simulator: ProgramSimulator, + pub encoded_vaa_addresses: Vec, } - -pub fn build_merkle_root_encoded_vaa( - merkle_tree_accumulator: MerkleTree, - data_source: &DataSource, -) -> Account { - let merkle_tree_payload: Vec = merkle_tree_accumulator.serialize(1, 1); - - let vaa_header: Vaa> = Vaa { - version: 1, - guardian_set_index: 0, - signatures: vec![], - timestamp: 0, - nonce: 0, - emitter_chain: Chain::from(data_source.chain), - emitter_address: wormhole_sdk::Address(data_source.emitter.to_bytes()), - sequence: 0, - consistency_level: 0, - payload: >::from(merkle_tree_payload), - }; - +pub fn build_encoded_vaa_account_from_vaa(vaa: Vaa<&RawMessage>) -> Account { let encoded_vaa_data = ( ::DISCRIMINATOR, Header { @@ -154,12 +73,11 @@ pub fn build_merkle_root_encoded_vaa( write_authority: Pubkey::new_unique(), version: 1, }, - serde_wormhole::to_vec(&vaa_header).unwrap(), + serde_wormhole::to_vec(&vaa).unwrap(), ) .try_to_vec() .unwrap(); - Account { lamports: Rent::default().minimum_balance(encoded_vaa_data.len()), data: encoded_vaa_data, @@ -169,37 +87,61 @@ pub fn build_merkle_root_encoded_vaa( } } +pub fn build_guardian_set_account() -> Account { + let guardian_set = GuardianSet { + index: DEFAULT_GUARDIAN_SET_INDEX, + keys: dummy_guardians() + .iter() + .map(|x| { + let mut result: [u8; 20] = [0u8; 20]; + result.copy_from_slice( + &keccak::hashv(&[&PublicKey::from_secret_key(x).serialize()[1..]]).0[12..], + ); + result + }) + .collect::>(), + creation_time: 0.into(), + expiration_time: 0.into(), + }; -pub struct ProgramTestFixtures { - pub program_simulator: ProgramSimulator, - pub encoded_vaa_address: Pubkey, - pub merkle_price_updates: Vec, + let guardian_set_data = ( + ::DISCRIMINATOR, + guardian_set, + ) + .try_to_vec() + .unwrap(); + + Account { + lamports: Rent::default().minimum_balance(guardian_set_data.len()), + data: guardian_set_data, + owner: BRIDGE_ID, + executable: false, + rent_epoch: 0, + } } /** * Setup to test the Pyth Receiver. The return values are a tuple composed of : * - The program simulator, which is used to send transactions - * - The pubkey of an encoded VAA account, which is pre-populated and can be used to test post_updates - * - A vector of MerklePriceUpdate, corresponding to that VAA + * - The pubkeys of the encoded VAA accounts corresponding to the VAAs passed as argument, these accounts are prepopulated and can be used to test post_updates */ -pub async fn setup_pyth_receiver( - price_feed_messages: Vec, -) -> ProgramTestFixtures { +pub async fn setup_pyth_receiver(vaas: Vec>) -> ProgramTestFixtures { let mut program_test = ProgramTest::default(); program_test.add_program("pyth_solana_receiver", ID, None); - let (merkle_tree_accumulator, merkle_price_updates) = dummy_price_updates(price_feed_messages); - let data_source = dummy_data_source(); - let merkle_root_encoded_vaa = - build_merkle_root_encoded_vaa(merkle_tree_accumulator, &data_source); - - let encoded_vaa_address = Pubkey::new_unique(); - program_test.add_account(encoded_vaa_address, merkle_root_encoded_vaa); - + let mut encoded_vaa_addresses: Vec = vec![]; + for vaa in vaas { + let encoded_vaa_address = Pubkey::new_unique(); + encoded_vaa_addresses.push(encoded_vaa_address); + program_test.add_account(encoded_vaa_address, build_encoded_vaa_account_from_vaa(vaa)); + } + program_test.add_account( + get_guardian_set_address(BRIDGE_ID, DEFAULT_GUARDIAN_SET_INDEX), + build_guardian_set_account(), + ); let mut program_simulator = ProgramSimulator::start_from_program_test(program_test).await; - let initial_config = dummy_receiver_config(data_source); - + let initial_config = default_receiver_config(); let setup_keypair: Keypair = program_simulator.get_funded_keypair().await.unwrap(); program_simulator @@ -225,7 +167,6 @@ pub async fn setup_pyth_receiver( ProgramTestFixtures { program_simulator, - encoded_vaa_address, - merkle_price_updates, + encoded_vaa_addresses, } } diff --git a/target_chains/solana/programs/pyth-solana-receiver/tests/test_initialize.rs b/target_chains/solana/programs/pyth-solana-receiver/tests/test_post_updates.rs similarity index 68% rename from target_chains/solana/programs/pyth-solana-receiver/tests/test_initialize.rs rename to target_chains/solana/programs/pyth-solana-receiver/tests/test_post_updates.rs index a5a7456cb..fd22e4773 100644 --- a/target_chains/solana/programs/pyth-solana-receiver/tests/test_initialize.rs +++ b/target_chains/solana/programs/pyth-solana-receiver/tests/test_post_updates.rs @@ -1,16 +1,23 @@ use { - crate::common::dummy_price_messages, common::{ setup_pyth_receiver, ProgramTestFixtures, }, pyth_solana_receiver::{ instruction::PostUpdates, + sdk::deserialize_accumulator_update_data, state::price_update::{ PriceUpdateV1, VerificationLevel, }, }, + pythnet_sdk::{ + messages::Message, + test_utils::{ + create_accumulator_message, + create_dummy_price_feed_message, + }, + }, solana_sdk::{ signature::Keypair, signer::Signer, @@ -22,13 +29,16 @@ mod common; #[tokio::test] async fn test_post_updates() { - let dummy_price_messages = dummy_price_messages(); + let feed_1 = create_dummy_price_feed_message(100); + let feed_2 = create_dummy_price_feed_message(200); + let message = create_accumulator_message(&[feed_1, feed_2], &[feed_1, feed_2], false); + let (vaa, merkle_price_updates) = deserialize_accumulator_update_data(message).unwrap(); + let ProgramTestFixtures { mut program_simulator, - encoded_vaa_address, - merkle_price_updates, - } = setup_pyth_receiver(dummy_price_messages.clone()).await; + encoded_vaa_addresses, + } = setup_pyth_receiver(vec![serde_wormhole::from_slice(&vaa).unwrap()]).await; let poster = program_simulator.get_funded_keypair().await.unwrap(); let price_update_keypair = Keypair::new(); @@ -38,7 +48,7 @@ async fn test_post_updates() { .process_ix( PostUpdates::populate( poster.pubkey(), - encoded_vaa_address, + encoded_vaa_addresses[0], price_update_keypair.pubkey(), merkle_price_updates[0].clone(), ), @@ -59,14 +69,17 @@ async fn test_post_updates() { price_update_account.verification_level, VerificationLevel::Full ); - assert_eq!(price_update_account.price_message, dummy_price_messages[0]); + assert_eq!( + Message::PriceFeedMessage(price_update_account.price_message), + feed_1 + ); // post another update to the same account program_simulator .process_ix( PostUpdates::populate( poster.pubkey(), - encoded_vaa_address, + encoded_vaa_addresses[0], price_update_keypair.pubkey(), merkle_price_updates[1].clone(), ), @@ -87,5 +100,8 @@ async fn test_post_updates() { price_update_account.verification_level, VerificationLevel::Full ); - assert_eq!(price_update_account.price_message, dummy_price_messages[1]); + assert_eq!( + Message::PriceFeedMessage(price_update_account.price_message), + feed_2 + ); } diff --git a/target_chains/solana/programs/pyth-solana-receiver/tests/test_post_updates_atomic.rs b/target_chains/solana/programs/pyth-solana-receiver/tests/test_post_updates_atomic.rs new file mode 100644 index 000000000..972692ffe --- /dev/null +++ b/target_chains/solana/programs/pyth-solana-receiver/tests/test_post_updates_atomic.rs @@ -0,0 +1,118 @@ +use { + crate::common::DEFAULT_GUARDIAN_SET_INDEX, + common::{ + setup_pyth_receiver, + ProgramTestFixtures, + }, + pyth_solana_receiver::{ + instruction::PostUpdatesAtomic, + sdk::deserialize_accumulator_update_data, + state::price_update::{ + PriceUpdateV1, + VerificationLevel, + }, + }, + pythnet_sdk::{ + messages::Message, + test_utils::{ + create_accumulator_message, + create_dummy_price_feed_message, + trim_vaa_signatures, + }, + }, + solana_sdk::{ + signature::Keypair, + signer::Signer, + }, + wormhole_core_bridge_solana::ID as BRIDGE_ID, +}; + +mod common; + + +#[tokio::test] +async fn test_post_updates_atomic() { + let feed_1 = create_dummy_price_feed_message(100); + let feed_2 = create_dummy_price_feed_message(200); + let message = create_accumulator_message(&[feed_1, feed_2], &[feed_1, feed_2], false); + let (vaa, merkle_price_updates) = deserialize_accumulator_update_data(message).unwrap(); + let vaa = serde_wormhole::to_vec(&trim_vaa_signatures( + serde_wormhole::from_slice(&vaa).unwrap(), + 5, + )) + .unwrap(); + + let ProgramTestFixtures { + mut program_simulator, + encoded_vaa_addresses: _, + } = setup_pyth_receiver(vec![]).await; + + let poster = program_simulator.get_funded_keypair().await.unwrap(); + let price_update_keypair = Keypair::new(); + + // post one update atomically + program_simulator + .process_ix( + PostUpdatesAtomic::populate( + poster.pubkey(), + price_update_keypair.pubkey(), + BRIDGE_ID, + DEFAULT_GUARDIAN_SET_INDEX, + vaa.clone(), + merkle_price_updates[0].clone(), + ), + &vec![&poster, &price_update_keypair], + None, + ) + .await + .unwrap(); + + + let mut price_update_account = program_simulator + .get_anchor_account_data::(price_update_keypair.pubkey()) + .await + .unwrap(); + + assert_eq!(price_update_account.write_authority, poster.pubkey()); + assert_eq!( + price_update_account.verification_level, + VerificationLevel::Partial(5) + ); + assert_eq!( + Message::PriceFeedMessage(price_update_account.price_message), + feed_1 + ); + + // post another update to the same account + program_simulator + .process_ix( + PostUpdatesAtomic::populate( + poster.pubkey(), + price_update_keypair.pubkey(), + BRIDGE_ID, + DEFAULT_GUARDIAN_SET_INDEX, + vaa.clone(), + merkle_price_updates[1].clone(), + ), + &vec![&poster, &price_update_keypair], + None, + ) + .await + .unwrap(); + + + price_update_account = program_simulator + .get_anchor_account_data::(price_update_keypair.pubkey()) + .await + .unwrap(); + + assert_eq!(price_update_account.write_authority, poster.pubkey()); + assert_eq!( + price_update_account.verification_level, + VerificationLevel::Partial(5) + ); + assert_eq!( + Message::PriceFeedMessage(price_update_account.price_message), + feed_2 + ); +}