feat: pyth pull-based push oracle (#1370)

* feat: implement oracle instance

* Go

* Remove key

* Go

* Add instance id, fix conditional deser

* Go

* Rename

* Revert changes to cli

* Checkpoint

* Cleanup deps

* Refactor tests

* Cleanup deps

* Write test

* Fix comment

* Shard id

* ADd tests

* Extract common test utils

* Fix test

* Better name

* Cleanup

* Instance -> shard

* Update test

* Make shard id a u16
This commit is contained in:
guibescos 2024-04-01 14:43:47 +01:00 committed by GitHub
parent a888ba318c
commit 866b6a5b4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 644 additions and 53 deletions

View File

@ -6,6 +6,7 @@ use {
},
hashers::keccak256_160::Keccak160,
messages::{
FeedId,
Message,
PriceFeedMessage,
TwapMessage,
@ -95,22 +96,30 @@ pub fn dummy_guardians() -> Vec<SecretKey> {
result
}
pub fn create_dummy_price_feed_message(value: i64) -> Message {
pub fn create_dummy_feed_id(value: i64) -> FeedId {
let mut dummy_id = [0; 32];
dummy_id[0] = value as u8;
dummy_id
}
pub fn create_dummy_price_feed_message_with_feed_id(value: i64, feed_id: FeedId) -> Message {
let msg = PriceFeedMessage {
feed_id: dummy_id,
price: value,
conf: value as u64,
exponent: value as i32,
publish_time: value,
feed_id,
price: value,
conf: value as u64,
exponent: value as i32,
publish_time: value,
prev_publish_time: value,
ema_price: value,
ema_conf: value as u64,
ema_price: value,
ema_conf: value as u64,
};
Message::PriceFeedMessage(msg)
}
pub fn create_dummy_price_feed_message(value: i64) -> Message {
create_dummy_price_feed_message_with_feed_id(value, create_dummy_feed_id(value))
}
pub fn create_dummy_twap_message() -> Message {
let msg = TwapMessage {
feed_id: [0; 32],

View File

@ -1018,6 +1018,30 @@ dependencies = [
"unreachable",
]
[[package]]
name = "common-test-utils"
version = "0.1.0"
dependencies = [
"anchor-lang",
"bincode",
"lazy_static",
"libsecp256k1 0.7.1",
"program-simulator",
"pyth-sdk",
"pyth-sdk-solana",
"pyth-solana-receiver",
"pyth-solana-receiver-sdk",
"pythnet-sdk",
"rand 0.8.5",
"serde_wormhole",
"solana-program",
"solana-program-test",
"solana-sdk",
"tokio",
"wormhole-core-bridge-solana",
"wormhole-vaas-serde",
]
[[package]]
name = "concurrent-queue"
version = "2.3.0"
@ -2990,6 +3014,23 @@ dependencies = [
"solana-sdk",
]
[[package]]
name = "pyth-push-oracle"
version = "0.1.0"
dependencies = [
"anchor-lang",
"common-test-utils",
"program-simulator",
"pyth-solana-receiver",
"pyth-solana-receiver-sdk",
"pythnet-sdk",
"serde_wormhole",
"solana-program",
"solana-sdk",
"tokio",
"wormhole-vaas-serde",
]
[[package]]
name = "pyth-sdk"
version = "0.8.0"
@ -3026,19 +3067,13 @@ name = "pyth-solana-receiver"
version = "0.1.0"
dependencies = [
"anchor-lang",
"bincode",
"byteorder",
"lazy_static",
"libsecp256k1 0.7.1",
"common-test-utils",
"program-simulator",
"pyth-sdk",
"pyth-sdk-solana",
"pyth-solana-receiver-sdk",
"pythnet-sdk",
"rand 0.8.5",
"serde_wormhole",
"solana-program",
"solana-program-test",
"solana-sdk",
"tokio",
"wormhole-core-bridge-solana",

View File

@ -3,7 +3,8 @@ members = [
"programs/*",
"cli/",
"program_simulator/",
"pyth_solana_receiver_sdk/"
"pyth_solana_receiver_sdk/",
"common_test_utils"
]
[profile.release]

View File

@ -0,0 +1,28 @@
[package]
name = "common-test-utils"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["lib"]
name = "common_test_utils"
[dependencies]
pyth-sdk = "0.8.0"
pyth-sdk-solana = "0.8.0"
solana-program-test = { workspace = true }
solana-sdk = { workspace = true }
tokio = "1.14.1"
bincode = "1.3.3"
libsecp256k1 = "0.7.1"
rand = "0.8.5"
lazy_static = "1.4.0"
program-simulator = { path = "../program_simulator" }
wormhole-vaas-serde = { workspace = true }
serde_wormhole = { workspace = true }
pythnet-sdk = { path = "../../../pythnet/pythnet_sdk", features = ["test-utils"] }
anchor-lang = { workspace = true }
solana-program = { workspace = true }
pyth-solana-receiver = { path = "../programs/pyth-solana-receiver" }
wormhole-core-bridge-solana = {workspace = true}
pyth-solana-receiver-sdk = { path = "../pyth_solana_receiver_sdk"}

View File

@ -11,9 +11,12 @@ use {
},
ID,
},
pyth_solana_receiver_sdk::config::{
Config,
DataSource,
pyth_solana_receiver_sdk::{
config::{
Config,
DataSource,
},
PYTH_PUSH_ORACLE_ID,
},
pythnet_sdk::test_utils::{
dummy_guardians,
@ -163,6 +166,7 @@ pub async fn setup_pyth_receiver(
) -> ProgramTestFixtures {
let mut program_test = ProgramTest::default();
program_test.add_program("pyth_solana_receiver", ID, None);
program_test.add_program("pyth_push_oracle", PYTH_PUSH_ORACLE_ID, None);
let mut encoded_vaa_addresses: Vec<Pubkey> = vec![];
for vaa in vaas {

View File

@ -0,0 +1,31 @@
[package]
name = "pyth-push-oracle"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "pyth_push_oracle"
[features]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
test-bpf = []
[dependencies]
anchor-lang = { workspace = true }
pythnet-sdk = { path = "../../../../pythnet/pythnet_sdk" }
solana-program = { workspace = true }
pyth-solana-receiver-sdk = { path = "../../pyth_solana_receiver_sdk"}
pyth-solana-receiver = { path = "../pyth-solana-receiver", features = ["cpi"]}
[dev-dependencies]
solana-sdk = { workspace = true }
tokio = "1.14.1"
program-simulator = { path = "../../program_simulator" }
wormhole-vaas-serde = { workspace = true }
serde_wormhole = { workspace = true }
common-test-utils = { path = "../../common_test_utils" }

View File

@ -0,0 +1,99 @@
use {
anchor_lang::prelude::*,
pyth_solana_receiver::{
cpi::accounts::PostUpdate,
program::PythSolanaReceiver,
PostUpdateParams,
},
pyth_solana_receiver_sdk::{
price_update::PriceUpdateV2,
PYTH_PUSH_ORACLE_ID,
},
pythnet_sdk::messages::FeedId,
};
pub mod sdk;
pub const ID: Pubkey = PYTH_PUSH_ORACLE_ID;
#[error_code]
pub enum PushOracleError {
#[msg("Updates must be monotonically increasing")]
UpdatesNotMonotonic,
#[msg("Trying to update price feed with the wrong feed id")]
PriceFeedMessageMismatch,
}
#[program]
pub mod pyth_push_oracle {
use super::*;
pub fn update_price_feed(
ctx: Context<UpdatePriceFeed>,
params: PostUpdateParams,
shard_id: u16,
feed_id: FeedId,
) -> Result<()> {
let cpi_program = ctx.accounts.pyth_solana_receiver.to_account_info().clone();
let cpi_accounts = PostUpdate {
payer: ctx.accounts.payer.to_account_info().clone(),
encoded_vaa: ctx.accounts.encoded_vaa.to_account_info().clone(),
config: ctx.accounts.config.to_account_info().clone(),
treasury: ctx.accounts.treasury.to_account_info().clone(),
price_update_account: ctx.accounts.price_feed_account.to_account_info().clone(),
system_program: ctx.accounts.system_program.to_account_info().clone(),
write_authority: ctx.accounts.price_feed_account.to_account_info().clone(),
};
let seeds = &[
&shard_id.to_le_bytes(),
feed_id.as_ref(),
&[*ctx.bumps.get("price_feed_account").unwrap()],
];
let signer_seeds = &[&seeds[..]];
let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
let current_timestamp = {
if ctx.accounts.price_feed_account.data_is_empty() {
0
} else {
let price_feed_account_data = ctx.accounts.price_feed_account.try_borrow_data()?;
let price_feed_account =
PriceUpdateV2::try_deserialize(&mut &price_feed_account_data[..])?;
price_feed_account.price_message.publish_time
}
};
pyth_solana_receiver::cpi::post_update(cpi_context, params)?;
{
let price_feed_account_data = ctx.accounts.price_feed_account.try_borrow_data()?;
let price_feed_account =
PriceUpdateV2::try_deserialize(&mut &price_feed_account_data[..])?;
require!(
price_feed_account.price_message.publish_time > current_timestamp,
PushOracleError::UpdatesNotMonotonic
);
require!(
price_feed_account.price_message.feed_id == feed_id,
PushOracleError::PriceFeedMessageMismatch
);
}
Ok(())
}
}
#[derive(Accounts)]
#[instruction(params : PostUpdateParams, shard_id : u16, feed_id : FeedId)]
pub struct UpdatePriceFeed<'info> {
#[account(mut)]
pub payer: Signer<'info>,
pub pyth_solana_receiver: Program<'info, PythSolanaReceiver>,
pub encoded_vaa: AccountInfo<'info>,
pub config: AccountInfo<'info>,
#[account(mut)]
pub treasury: AccountInfo<'info>,
#[account(mut, seeds = [&shard_id.to_le_bytes(), &feed_id], bump)]
pub price_feed_account: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}

View File

@ -0,0 +1,74 @@
use {
crate::{
accounts,
instruction,
PostUpdateParams,
ID,
},
anchor_lang::{
prelude::*,
system_program,
InstructionData,
},
pyth_solana_receiver::sdk::{
get_config_address,
get_treasury_address,
},
pythnet_sdk::{
messages::FeedId,
wire::v1::MerklePriceUpdate,
},
solana_program::instruction::Instruction,
};
pub fn get_price_feed_address(shard_id: u16, feed_id: FeedId) -> Pubkey {
Pubkey::find_program_address(&[&shard_id.to_le_bytes(), feed_id.as_ref()], &ID).0
}
impl accounts::UpdatePriceFeed {
pub fn populate(
payer: Pubkey,
encoded_vaa: Pubkey,
shard_id: u16,
feed_id: FeedId,
treasury_id: u8,
) -> Self {
accounts::UpdatePriceFeed {
payer,
encoded_vaa,
config: get_config_address(),
treasury: get_treasury_address(treasury_id),
price_feed_account: get_price_feed_address(shard_id, feed_id),
pyth_solana_receiver: pyth_solana_receiver::ID,
system_program: system_program::ID,
}
}
}
impl instruction::UpdatePriceFeed {
pub fn populate(
payer: Pubkey,
encoded_vaa: Pubkey,
shard_id: u16,
feed_id: FeedId,
treasury_id: u8,
merkle_price_update: MerklePriceUpdate,
) -> Instruction {
let update_price_feed_accounts =
accounts::UpdatePriceFeed::populate(payer, encoded_vaa, shard_id, feed_id, treasury_id)
.to_account_metas(None);
Instruction {
program_id: ID,
accounts: update_price_feed_accounts,
data: instruction::UpdatePriceFeed {
params: PostUpdateParams {
merkle_price_update,
treasury_id,
},
shard_id,
feed_id,
}
.data(),
}
}
}

View File

@ -0,0 +1,324 @@
use {
common_test_utils::{
assert_treasury_balance,
setup_pyth_receiver,
ProgramTestFixtures,
WrongSetupOption,
},
program_simulator::into_transaction_error,
pyth_push_oracle::{
instruction::UpdatePriceFeed,
sdk::get_price_feed_address,
PushOracleError,
},
pyth_solana_receiver::sdk::{
deserialize_accumulator_update_data,
DEFAULT_TREASURY_ID,
},
pyth_solana_receiver_sdk::price_update::{
PriceUpdateV2,
VerificationLevel,
},
pythnet_sdk::{
messages::Message,
test_utils::{
create_accumulator_message,
create_dummy_feed_id,
create_dummy_price_feed_message_with_feed_id,
},
},
solana_sdk::{
rent::Rent,
signer::Signer,
},
};
const DEFAULT_SHARD: u16 = 0;
const SECOND_SHARD: u16 = 1;
#[tokio::test]
async fn test_update_price_feed() {
let feed_id: [u8; 32] = create_dummy_feed_id(100);
let feed_id_2: [u8; 32] = create_dummy_feed_id(200);
let feed_1_old = create_dummy_price_feed_message_with_feed_id(100, feed_id);
let feed_1_recent = create_dummy_price_feed_message_with_feed_id(200, feed_id);
let feed_2 = create_dummy_price_feed_message_with_feed_id(300, feed_id_2);
let message = create_accumulator_message(
&[feed_1_old, feed_1_recent, feed_2],
&[feed_1_old, feed_1_recent, feed_2],
false,
false,
);
let (vaa, merkle_price_updates) = deserialize_accumulator_update_data(message).unwrap();
let ProgramTestFixtures {
mut program_simulator,
encoded_vaa_addresses,
governance_authority: _,
} = setup_pyth_receiver(
vec![serde_wormhole::from_slice(&vaa).unwrap()],
WrongSetupOption::None,
)
.await;
assert_treasury_balance(&mut program_simulator, 0, DEFAULT_TREASURY_ID).await;
let poster = program_simulator.get_funded_keypair().await.unwrap();
// post one update
program_simulator
.process_ix_with_default_compute_limit(
UpdatePriceFeed::populate(
poster.pubkey(),
encoded_vaa_addresses[0],
DEFAULT_SHARD,
feed_id,
DEFAULT_TREASURY_ID,
merkle_price_updates[0].clone(),
),
&vec![&poster],
None,
)
.await
.unwrap();
assert_treasury_balance(
&mut program_simulator,
Rent::default().minimum_balance(0),
DEFAULT_TREASURY_ID,
)
.await;
let price_feed_account = program_simulator
.get_anchor_account_data::<PriceUpdateV2>(get_price_feed_address(DEFAULT_SHARD, feed_id))
.await
.unwrap();
assert_eq!(
price_feed_account.write_authority,
get_price_feed_address(DEFAULT_SHARD, feed_id)
);
assert_eq!(
price_feed_account.verification_level,
VerificationLevel::Full
);
assert_eq!(
Message::PriceFeedMessage(price_feed_account.price_message),
feed_1_old
);
assert_eq!(
price_feed_account.posted_slot,
program_simulator.get_clock().await.unwrap().slot
);
// post another update, same price feed
program_simulator
.process_ix_with_default_compute_limit(
UpdatePriceFeed::populate(
poster.pubkey(),
encoded_vaa_addresses[0],
DEFAULT_SHARD,
feed_id,
DEFAULT_TREASURY_ID,
merkle_price_updates[1].clone(),
),
&vec![&poster],
None,
)
.await
.unwrap();
assert_treasury_balance(
&mut program_simulator,
Rent::default().minimum_balance(0) + 1,
DEFAULT_TREASURY_ID,
)
.await;
let price_feed_account = program_simulator
.get_anchor_account_data::<PriceUpdateV2>(get_price_feed_address(DEFAULT_SHARD, feed_id))
.await
.unwrap();
assert_eq!(
price_feed_account.write_authority,
get_price_feed_address(DEFAULT_SHARD, feed_id)
);
assert_eq!(
price_feed_account.verification_level,
VerificationLevel::Full
);
assert_eq!(
Message::PriceFeedMessage(price_feed_account.price_message),
feed_1_recent
);
assert_eq!(
price_feed_account.posted_slot,
program_simulator.get_clock().await.unwrap().slot
);
// post another update, outdated
assert_eq!(
program_simulator
.process_ix_with_default_compute_limit(
UpdatePriceFeed::populate(
poster.pubkey(),
encoded_vaa_addresses[0],
DEFAULT_SHARD,
feed_id,
DEFAULT_TREASURY_ID,
merkle_price_updates[1].clone(),
),
&vec![&poster],
None,
)
.await
.unwrap_err()
.unwrap(),
into_transaction_error(PushOracleError::UpdatesNotMonotonic)
);
assert_treasury_balance(
&mut program_simulator,
Rent::default().minimum_balance(0) + 1,
DEFAULT_TREASURY_ID,
)
.await;
let price_feed_account = program_simulator
.get_anchor_account_data::<PriceUpdateV2>(get_price_feed_address(DEFAULT_SHARD, feed_id))
.await
.unwrap();
assert_eq!(
price_feed_account.write_authority,
get_price_feed_address(DEFAULT_SHARD, feed_id)
);
assert_eq!(
price_feed_account.verification_level,
VerificationLevel::Full
);
assert_eq!(
Message::PriceFeedMessage(price_feed_account.price_message),
feed_1_recent
);
assert_eq!(
price_feed_account.posted_slot,
program_simulator.get_clock().await.unwrap().slot
);
// works if you change the shard
program_simulator
.process_ix_with_default_compute_limit(
UpdatePriceFeed::populate(
poster.pubkey(),
encoded_vaa_addresses[0],
SECOND_SHARD,
feed_id,
DEFAULT_TREASURY_ID,
merkle_price_updates[0].clone(),
),
&vec![&poster],
None,
)
.await
.unwrap();
assert_treasury_balance(
&mut program_simulator,
Rent::default().minimum_balance(0) + 2,
DEFAULT_TREASURY_ID,
)
.await;
let price_feed_account = program_simulator
.get_anchor_account_data::<PriceUpdateV2>(get_price_feed_address(DEFAULT_SHARD, feed_id))
.await
.unwrap();
assert_eq!(
price_feed_account.write_authority,
get_price_feed_address(DEFAULT_SHARD, feed_id)
);
assert_eq!(
price_feed_account.verification_level,
VerificationLevel::Full
);
assert_eq!(
Message::PriceFeedMessage(price_feed_account.price_message),
feed_1_recent
);
assert_eq!(
price_feed_account.posted_slot,
program_simulator.get_clock().await.unwrap().slot
);
let price_feed_account = program_simulator
.get_anchor_account_data::<PriceUpdateV2>(get_price_feed_address(SECOND_SHARD, feed_id))
.await
.unwrap();
assert_eq!(
price_feed_account.write_authority,
get_price_feed_address(SECOND_SHARD, feed_id)
);
assert_eq!(
price_feed_account.verification_level,
VerificationLevel::Full
);
assert_eq!(
Message::PriceFeedMessage(price_feed_account.price_message),
feed_1_old
);
assert_eq!(
price_feed_account.posted_slot,
program_simulator.get_clock().await.unwrap().slot
);
// try to post the wrong price feed id
assert_eq!(
program_simulator
.process_ix_with_default_compute_limit(
UpdatePriceFeed::populate(
poster.pubkey(),
encoded_vaa_addresses[0],
DEFAULT_SHARD,
feed_id,
DEFAULT_TREASURY_ID,
merkle_price_updates[2].clone(),
),
&vec![&poster],
None,
)
.await
.unwrap_err()
.unwrap(),
into_transaction_error(PushOracleError::PriceFeedMessageMismatch)
);
let price_feed_account = program_simulator
.get_anchor_account_data::<PriceUpdateV2>(get_price_feed_address(DEFAULT_SHARD, feed_id))
.await
.unwrap();
assert_eq!(
price_feed_account.write_authority,
get_price_feed_address(DEFAULT_SHARD, feed_id)
);
assert_eq!(
price_feed_account.verification_level,
VerificationLevel::Full
);
assert_eq!(
Message::PriceFeedMessage(price_feed_account.price_message),
feed_1_recent
);
assert_eq!(
price_feed_account.posted_slot,
program_simulator.get_clock().await.unwrap().slot
);
}

View File

@ -25,16 +25,9 @@ wormhole-raw-vaas = {version = "0.1.3", features = ["ruint", "on-chain"], defaul
pyth-solana-receiver-sdk = { path = "../../pyth_solana_receiver_sdk"}
[dev-dependencies]
pyth-sdk = "0.8.0"
pyth-sdk-solana = "0.8.0"
solana-program-test = { workspace = true }
solana-sdk = { workspace = true }
tokio = "1.14.1"
bincode = "1.3.3"
libsecp256k1 = "0.7.1"
rand = "0.8.5"
lazy_static = "1.4.0"
program-simulator = { path = "../../program_simulator" }
wormhole-vaas-serde = { workspace = true }
serde_wormhole = { workspace = true }
pythnet-sdk = { path = "../../../../pythnet/pythnet_sdk", features = ["test-utils"] }
common-test-utils = { path = "../../common_test_utils" }

View File

@ -1,8 +1,8 @@
use {
crate::common::WrongSetupOption,
common::{
common_test_utils::{
setup_pyth_receiver,
ProgramTestFixtures,
WrongSetupOption,
},
program_simulator::into_transaction_error,
pyth_solana_receiver::{
@ -30,8 +30,6 @@ use {
solana_sdk::signer::Signer,
};
mod common;
#[tokio::test]
async fn test_governance() {

View File

@ -1,12 +1,10 @@
use {
crate::common::{
common_test_utils::{
assert_treasury_balance,
WrongSetupOption,
DEFAULT_GUARDIAN_SET_INDEX,
},
common::{
setup_pyth_receiver,
ProgramTestFixtures,
WrongSetupOption,
DEFAULT_GUARDIAN_SET_INDEX,
},
program_simulator::into_transaction_error,
pyth_solana_receiver::{
@ -51,9 +49,6 @@ use {
wormhole_core_bridge_solana::ID as BRIDGE_ID,
};
mod common;
// This file is meant to test the errors that can be thrown by post_price_update_from_vaa
#[tokio::test]
async fn test_invalid_wormhole_message() {

View File

@ -1,11 +1,9 @@
use {
crate::common::{
common_test_utils::{
assert_treasury_balance,
WrongSetupOption,
},
common::{
setup_pyth_receiver,
ProgramTestFixtures,
WrongSetupOption,
},
program_simulator::into_transaction_error,
pyth_solana_receiver::{
@ -38,8 +36,6 @@ use {
},
};
mod common;
#[tokio::test]
async fn test_post_update() {

View File

@ -1,12 +1,10 @@
use {
crate::common::{
common_test_utils::{
assert_treasury_balance,
WrongSetupOption,
DEFAULT_GUARDIAN_SET_INDEX,
},
common::{
setup_pyth_receiver,
ProgramTestFixtures,
WrongSetupOption,
DEFAULT_GUARDIAN_SET_INDEX,
},
program_simulator::into_transaction_error,
pyth_solana_receiver::{
@ -41,8 +39,6 @@ use {
wormhole_sdk::Vaa,
};
mod common;
#[tokio::test]
async fn test_post_update_atomic() {

View File

@ -1,7 +1,15 @@
use anchor_lang::declare_id;
use {
anchor_lang::declare_id,
solana_program::{
pubkey,
pubkey::Pubkey,
},
};
pub mod config;
pub mod error;
pub mod price_update;
declare_id!("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ");
pub const PYTH_PUSH_ORACLE_ID: Pubkey = pubkey!("F9SP6tBXw9Af7BYauo7Y2R5Es2mpv8FP5aNCXMihp6Za");