diff --git a/Cargo.lock b/Cargo.lock index 3690382b..7cb47d6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -358,18 +358,41 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" dependencies = [ - "borsh-derive", + "borsh-derive 0.9.3", "hashbrown 0.11.2", ] +[[package]] +name = "borsh" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" +dependencies = [ + "borsh-derive 0.10.3", + "hashbrown 0.12.3", +] + [[package]] name = "borsh-derive" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" dependencies = [ - "borsh-derive-internal", - "borsh-schema-derive-internal", + "borsh-derive-internal 0.9.3", + "borsh-schema-derive-internal 0.9.3", + "proc-macro-crate 0.1.5", + "proc-macro2 1.0.56", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" +dependencies = [ + "borsh-derive-internal 0.10.3", + "borsh-schema-derive-internal 0.10.3", "proc-macro-crate 0.1.5", "proc-macro2 1.0.56", "syn 1.0.109", @@ -386,6 +409,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "borsh-derive-internal" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" +dependencies = [ + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", +] + [[package]] name = "borsh-schema-derive-internal" version = "0.9.3" @@ -397,6 +431,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "borsh-schema-derive-internal" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" +dependencies = [ + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", +] + [[package]] name = "brotli" version = "3.3.4" @@ -2564,11 +2609,12 @@ dependencies = [ [[package]] name = "pythnet-sdk" -version = "1.13.6" -source = "git+https://github.com/pyth-network/pyth-crosschain?rev=60144002053a93f424be70decd8a8ccb8d618d81#60144002053a93f424be70decd8a8ccb8d618d81" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4f81d23a693463366640d1ba14c612841b5b07adc56ee20bbf9acaca2ad86b5" dependencies = [ "bincode", - "borsh", + "borsh 0.10.3", "bytemuck", "byteorder", "fast-math", @@ -3351,7 +3397,7 @@ version = "1.14.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c4d210c247714a742fa27629c30c63eefccb3fa7565168649dd58c275cb0cc" dependencies = [ - "borsh", + "borsh 0.9.3", "futures", "solana-banks-interface", "solana-program", @@ -3703,8 +3749,8 @@ dependencies = [ "bincode", "bitflags 1.3.2", "blake3", - "borsh", - "borsh-derive", + "borsh 0.9.3", + "borsh-derive 0.9.3", "bs58", "bv", "bytemuck", @@ -3894,7 +3940,7 @@ dependencies = [ "base64 0.13.1", "bincode", "bitflags 1.3.2", - "borsh", + "borsh 0.9.3", "bs58", "bytemuck", "byteorder", @@ -4024,7 +4070,7 @@ dependencies = [ "Inflector", "base64 0.13.1", "bincode", - "borsh", + "borsh 0.9.3", "bs58", "lazy_static", "log", @@ -4168,7 +4214,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbc000f0fdf1f12f99d77d398137c1751345b18c88258ce0f99b7872cf6c9bd6" dependencies = [ "assert_matches", - "borsh", + "borsh 0.9.3", "num-derive", "num-traits", "solana-program", diff --git a/program/rust/Cargo.toml b/program/rust/Cargo.toml index bbb1eb35..b755605e 100644 --- a/program/rust/Cargo.toml +++ b/program/rust/Cargo.toml @@ -17,7 +17,7 @@ num-traits = "0.2" byteorder = "1.4.3" serde = { version = "1.0", features = ["derive"], optional = true } strum = { version = "0.24.1", features = ["derive"], optional = true } -pythnet-sdk = { git = "https://github.com/pyth-network/pyth-crosschain", rev="60144002053a93f424be70decd8a8ccb8d618d81"} +pythnet-sdk = "2.2.0" solana-sdk = { version = "=1.14.17", optional = true } bitflags = { version = "2.6.0", features = ["bytemuck"] } @@ -31,7 +31,7 @@ rand = "0.8.5" quickcheck_macros = "1" bincode = "1.3.3" serde = { version = "1.0", features = ["derive"] } -pythnet-sdk = { git = "https://github.com/pyth-network/pyth-crosschain", rev="60144002053a93f424be70decd8a8ccb8d618d81", features = ["quickcheck"]} +pythnet-sdk = { version = "2.2.0" , features = ["quickcheck"]} serde_json = "1.0" test-generator = "0.3.1" csv = "1.1" diff --git a/program/rust/src/accounts/price.rs b/program/rust/src/accounts/price.rs index 6958970f..a569d054 100644 --- a/program/rust/src/accounts/price.rs +++ b/program/rust/src/accounts/price.rs @@ -13,10 +13,14 @@ use { }, pythnet_sdk::messages::{ PriceFeedMessage, + PublisherStakeCapsMessage, TwapMessage, }, solana_program::pubkey::Pubkey, - std::mem::size_of, + std::{ + mem::size_of, + u16, + }, }; /// Pythnet-specific PriceAccount implementation @@ -111,7 +115,7 @@ mod price_pythnet { }; PriceFeedMessage { - id: key.to_bytes(), + feed_id: key.to_bytes(), price, conf, exponent: self.exponent, @@ -141,7 +145,7 @@ mod price_pythnet { }; TwapMessage { - id: key.to_bytes(), + feed_id: key.to_bytes(), cumulative_price: self.price_cumulative.price, cumulative_conf: self.price_cumulative.conf, num_down_slots: self.price_cumulative.num_down_slots, @@ -270,7 +274,7 @@ impl PythOracleSerialize for PriceFeedMessage { bytes[i..i + 1].clone_from_slice(&[DISCRIMINATOR]); i += 1; - bytes[i..i + 32].clone_from_slice(&self.id[..]); + bytes[i..i + 32].clone_from_slice(&self.feed_id[..]); i += 32; bytes[i..i + 8].clone_from_slice(&self.price.to_be_bytes()); @@ -313,7 +317,7 @@ impl PythOracleSerialize for TwapMessage { bytes[i..i + 1].clone_from_slice(&[DISCRIMINATOR]); i += 1; - bytes[i..i + 32].clone_from_slice(&self.id[..]); + bytes[i..i + 32].clone_from_slice(&self.feed_id[..]); i += 32; bytes[i..i + 16].clone_from_slice(&self.cumulative_price.to_be_bytes()); @@ -340,3 +344,23 @@ impl PythOracleSerialize for TwapMessage { bytes.to_vec() } } + +impl PythOracleSerialize for PublisherStakeCapsMessage { + fn to_bytes(self) -> Vec { + const DISCRIMINATOR: u8 = 2; + let mut result = vec![DISCRIMINATOR]; + result.extend_from_slice(&self.publish_time.to_be_bytes()); + result.extend_from_slice( + &u16::try_from(self.caps.as_ref().len()) + .unwrap_or(u16::MAX) + .to_be_bytes(), + ); + + for cap in self.caps { + result.extend_from_slice(&cap.publisher); + result.extend_from_slice(&cap.cap.to_be_bytes()); + } + + result + } +} diff --git a/program/rust/src/tests/test_message.rs b/program/rust/src/tests/test_message.rs index 9e09d98d..427b106a 100644 --- a/program/rust/src/tests/test_message.rs +++ b/program/rust/src/tests/test_message.rs @@ -5,10 +5,15 @@ use { messages::{ Message, PriceFeedMessage, + PublisherStakeCapsMessage, TwapMessage, }, wire::from_slice, }, + quickcheck::{ + Gen, + QuickCheck, + }, quickcheck_macros::quickcheck, }; @@ -38,3 +43,25 @@ fn test_twap_message_roundtrip(input: TwapMessage) -> bool { _ => false, } } + + +fn prop_publisher_caps_message_roundtrip(input: PublisherStakeCapsMessage) -> bool { + let reconstructed = from_slice::(&input.clone().to_bytes()).unwrap(); + + println!("Failed test case:"); + println!("{:?}", input); + println!("{:?}", reconstructed); + + match reconstructed { + Message::PublisherStakeCapsMessage(reconstructed) => reconstructed == input, + _ => false, + } +} + +#[test] +fn test_publisher_caps_message_roundtrip() { + // Configure the size parameter for the generator + QuickCheck::new() + .gen(Gen::new(1024)) + .quickcheck(prop_publisher_caps_message_roundtrip as fn(PublisherStakeCapsMessage) -> bool); +} diff --git a/program/rust/src/tests/test_upd_price_with_validator.rs b/program/rust/src/tests/test_upd_price_with_validator.rs index 65dc2465..d4b313c3 100644 --- a/program/rust/src/tests/test_upd_price_with_validator.rs +++ b/program/rust/src/tests/test_upd_price_with_validator.rs @@ -130,7 +130,7 @@ fn test_upd_price_with_validator() { .unwrap(); let expected_messages1 = [ PriceFeedMessage { - id: price_account.key.to_bytes(), + feed_id: price_account.key.to_bytes(), price: 42, conf: 2, exponent: 0, @@ -141,7 +141,7 @@ fn test_upd_price_with_validator() { } .to_bytes(), TwapMessage { - id: price_account.key.to_bytes(), + feed_id: price_account.key.to_bytes(), cumulative_price: 42, cumulative_conf: 2, num_down_slots: 0, @@ -161,7 +161,7 @@ fn test_upd_price_with_validator() { let expected_messages2 = [ PriceFeedMessage { - id: price_account.key.to_bytes(), + feed_id: price_account.key.to_bytes(), price: 42, conf: 2, exponent: 0, @@ -172,7 +172,7 @@ fn test_upd_price_with_validator() { } .to_bytes(), TwapMessage { - id: price_account.key.to_bytes(), + feed_id: price_account.key.to_bytes(), cumulative_price: 84, cumulative_conf: 4, num_down_slots: 0, diff --git a/program/rust/src/validator.rs b/program/rust/src/validator.rs index 48761b7b..17a3aa10 100644 --- a/program/rust/src/validator.rs +++ b/program/rust/src/validator.rs @@ -15,33 +15,46 @@ use { }, utils::pyth_assert, }, - solana_sdk::pubkey::Pubkey, - std::mem::size_of, + pythnet_sdk::messages::{ + PublisherStakeCap, + PublisherStakeCapsMessage, + }, + solana_sdk::{ + program_error::ProgramError, + pubkey::Pubkey, + }, + std::{ + cmp::max, + collections::BTreeMap, + mem::size_of, + }, }; -// Attempts to validate and access the contents of an account as a PriceAccount. -fn validate_price_account( - price_account_info: &mut [u8], -) -> Result<&mut PriceAccount, AggregationError> { - // TODO: don't return error on non-price account? +// Checks that the account is a PriceAccount from the length and header. +fn check_price_account_header(price_account_info: &[u8]) -> Result<(), ProgramError> { pyth_assert( price_account_info.len() >= PriceAccount::MINIMUM_SIZE, OracleError::AccountTooSmall.into(), - ) - .map_err(|_| AggregationError::NotPriceFeedAccount)?; + )?; - { - let account_header = bytemuck::from_bytes::( - &price_account_info[0..size_of::()], - ); - - pyth_assert( - account_header.magic_number == PC_MAGIC - && account_header.account_type == PriceAccount::ACCOUNT_TYPE, - OracleError::InvalidAccountHeader.into(), - ) + let account_header = + bytemuck::from_bytes::(&price_account_info[0..size_of::()]); + + pyth_assert( + account_header.magic_number == PC_MAGIC + && account_header.account_type == PriceAccount::ACCOUNT_TYPE, + OracleError::InvalidAccountHeader.into(), + )?; + + Ok(()) +} + +// Attempts to validate and access the contents of an account as a PriceAccount. +fn validate_price_account( + price_account_info: &mut [u8], +) -> Result<&mut PriceAccount, AggregationError> { + check_price_account_header(price_account_info) .map_err(|_| AggregationError::NotPriceFeedAccount)?; - } let data = bytemuck::from_bytes_mut::( &mut price_account_info[0..size_of::()], @@ -128,3 +141,55 @@ pub fn aggregate_price( .to_bytes(), ]) } + +/// Load a price account as read-only, returning `None` if it isn't a valid price account. +fn checked_load_price_account(price_account_info: &[u8]) -> Option<&PriceAccount> { + check_price_account_header(price_account_info).ok()?; + Some(bytemuck::from_bytes::( + &price_account_info[0..size_of::()], + )) +} + +/// Computes the stake caps for each publisher based on the oracle program accounts provided +/// - `account_datas` - the account datas of the oracle program accounts +/// - `timestamp` - the timestamp to include in the message +/// - `m` - m is the cap per symbol, it gets split among all publishers of the symbol +/// - `z` - when a symbol has less than `z` publishers, each publisher gets a cap of `m/z` (instead of `m/number_of_publishers`). This is to prevent a single publisher from getting a large cap when there are few publishers. +/// +/// The stake cap for a publisher is computed as the sum of `m/min(z, number_of_publishers)` for all the symbols the publisher is publishing. +pub fn compute_publisher_stake_caps<'a>( + account_datas: impl IntoIterator, + timestamp: i64, + m: u64, + z: u64, +) -> Vec { + let mut publisher_caps: BTreeMap = BTreeMap::new(); // BTreeMap to ensure it will be sorted by publisher + for account in account_datas { + if let Some(price_account) = checked_load_price_account(account) { + let cap: u64 = m + .checked_div(max(u64::from(price_account.num_), z)) + .unwrap_or(0); + for i in 0..(price_account.num_ as usize) { + if let Some(pub_) = price_account.comp_.get(i).map(|comp| &comp.pub_) { + publisher_caps + .entry(*pub_) + .and_modify(|e: &mut u64| *e = e.saturating_add(cap)) + .or_insert(cap); + } + } + } + } + + PublisherStakeCapsMessage { + publish_time: timestamp, + caps: publisher_caps + .into_iter() + .map(|(publisher, cap)| PublisherStakeCap { + publisher: publisher.to_bytes(), + cap, + }) + .collect::>() + .into(), + } + .to_bytes() +}