From dbc69447fd4c044698c2c2655f8c1ce187613821 Mon Sep 17 00:00:00 2001 From: jordy25519 Date: Mon, 21 Oct 2024 17:47:39 +0800 Subject: [PATCH 1/2] shrink RFQ tx size - extract pubkey from ed25519 verify ix - don't check signature from drift (its already checked) extract common order params --- programs/drift/src/error.rs | 2 + programs/drift/src/instructions/user.rs | 37 +++++---- programs/drift/src/lib.rs | 6 +- programs/drift/src/state/order_params.rs | 61 +++++++++++++- .../drift/src/validation/sig_verification.rs | 80 +++++++++++++++++++ 5 files changed, 167 insertions(+), 19 deletions(-) diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index a20046839..3e8e665c5 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -579,6 +579,8 @@ pub enum ErrorCode { InvalidVerificationIxIndex, #[msg("Swift message verificaiton failed")] SigVerificationFailed, + #[msg("Signed message verificaiton failed")] + MsgVerificationFailed, #[msg("Market index mismatched b/w taker and maker swift order params")] MismatchedSwiftOrderParamsMarketIndex, #[msg("Swift only available for market/oracle perp orders")] diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 4b839c704..d63179394 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -29,7 +29,7 @@ use crate::instructions::SpotFulfillmentType; use crate::math::casting::Cast; use crate::math::liquidation::is_user_being_liquidated; use crate::math::margin::{ - calculate_max_withdrawable_amount, meets_initial_margin_requirement, + calculate_max_withdrawable_amount, meets_maintenance_margin_requirement, meets_place_order_margin_requirement, meets_withdraw_margin_requirement, validate_spot_margin_trading, MarginRequirementType, }; @@ -53,7 +53,9 @@ use crate::state::fulfillment_params::phoenix::PhoenixFulfillmentParams; use crate::state::fulfillment_params::serum::SerumFulfillmentParams; use crate::state::high_leverage_mode_config::HighLeverageModeConfig; use crate::state::oracle::StrictOraclePrice; +use crate::state::order_params::RFQMakerOrderParams; use crate::state::order_params::RFQMatch; +use crate::state::order_params::RFQTradeParams; use crate::state::order_params::{ ModifyOrderParams, OrderParams, PlaceAndTakeOrderSuccessCondition, PlaceOrderOptions, PostOnlyParam, @@ -71,12 +73,12 @@ use crate::state::spot_market_map::{ }; use crate::state::state::State; use crate::state::traits::Size; -use crate::state::user::derive_user_account; use crate::state::user::ReferrerStatus; use crate::state::user::{MarginMode, MarketType, OrderType, ReferrerName, User, UserStats}; use crate::state::user_map::{load_user_maps, UserMap, UserStatsMap}; use crate::validate; -use crate::validation::sig_verification::verify_ed25519_ix; +use crate::validation::sig_verification::extract_ed25519_ix_pubkey; +use crate::validation::sig_verification::verify_ed25519_digest; use crate::validation::user::validate_user_deletion; use crate::validation::whitelist::validate_whitelist_token; use crate::{controller, math}; @@ -882,27 +884,32 @@ pub fn handle_place_perp_order<'c: 'info, 'info>( pub fn handle_place_and_match_rfq_orders<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, PlaceAndMatchRFQOrders<'info>>, - rfq_matches: Vec, + rfq_trade_params: RFQTradeParams, ) -> Result<()> { // Verify everything before proceeding to match the rfq orders - let ix_sysvar = &ctx.accounts.ix_sysvar.to_account_info(); - let number_of_verify_ixs_needed = rfq_matches.len(); - let ix_idx = load_current_index_checked(ix_sysvar)?; + let number_of_verify_ixs_needed = rfq_trade_params.matches.len(); + let ix_idx = load_current_index_checked(ix_sysvar)? as usize; - for i in 0..rfq_matches.len() { + let mut verified_rfq_matches = Vec::with_capacity(number_of_verify_ixs_needed); + for (idx, match_params) in rfq_trade_params.matches.iter().enumerate() { // First verify that the message is legitimate - let maker_order_params = &rfq_matches[i].maker_order_params; let ix: Instruction = load_instruction_at_checked( - ix_idx as usize - number_of_verify_ixs_needed + i, + ix_idx - number_of_verify_ixs_needed + idx, ix_sysvar, )?; - verify_ed25519_ix( + let maker_pubkey = extract_ed25519_ix_pubkey(&ix.data)?; + let maker_order_params = RFQMakerOrderParams::new(maker_pubkey, rfq_trade_params.common, match_params.maker_order_stub); + verify_ed25519_digest( &ix, - &maker_order_params.authority.to_bytes(), - &maker_order_params.clone().try_to_vec()?, - &rfq_matches[i].maker_signature, + &maker_pubkey, + &maker_order_params.digest(), )?; + + verified_rfq_matches.push(RFQMatch { + base_asset_amount: match_params.base_asset_amount, + maker_order_params, + }); } // TODO: generalize to support multiple market types @@ -928,7 +935,7 @@ pub fn handle_place_and_match_rfq_orders<'c: 'info, 'info>( place_and_match_rfq_orders( &ctx.accounts.user, &ctx.accounts.user_stats, - rfq_matches, + verified_rfq_matches, maker_rfq_account_map, &makers_and_referrer, &makers_and_referrer_stats, diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index d0a3d2ead..32bc47afd 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -12,7 +12,7 @@ use state::oracle::OracleSource; use crate::controller::position::PositionDirection; use crate::state::oracle::PrelaunchOracleParams; -use crate::state::order_params::{ModifyOrderParams, OrderParams, RFQMatch}; +use crate::state::order_params::{ModifyOrderParams, OrderParams, RFQTradeParams}; use crate::state::perp_market::{ContractTier, MarketStatus}; use crate::state::settle_pnl_mode::SettlePnlMode; use crate::state::spot_market::AssetTier; @@ -183,9 +183,9 @@ pub mod drift { pub fn place_and_match_rfq_orders<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, PlaceAndMatchRFQOrders<'info>>, - rfq_matches: Vec, + rfq_trade_params: RFQTradeParams, ) -> Result<()> { - handle_place_and_match_rfq_orders(ctx, rfq_matches) + handle_place_and_match_rfq_orders(ctx, rfq_trade_params) } pub fn place_spot_order<'c: 'info, 'info>( diff --git a/programs/drift/src/state/order_params.rs b/programs/drift/src/state/order_params.rs index 6e7f7ebea..0a869fa29 100644 --- a/programs/drift/src/state/order_params.rs +++ b/programs/drift/src/state/order_params.rs @@ -651,6 +651,27 @@ pub struct SwiftTriggerOrderParams { pub base_asset_amount: u64, } +/// Common order parameters for an RFQ trade +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Eq, PartialEq, Debug, Copy)] +pub struct RFQOrderCommon { + pub uuid: [u8; 8], + pub market_index: u16, + pub market_type: MarketType, + pub taker_direction: PositionDirection, +} + +/// Compact RFQ maker order parameters for tx size optimization +/// +/// It is intended to be combined with `RFQOrderCommon` and tx context to produce a full `RFQMakerOrderParams` message +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Eq, PartialEq, Debug, Copy)] +pub struct RFQMakerOrderStub { + pub sub_account_id: u16, + pub base_asset_amount: u64, + pub price: u64, + pub max_ts: i64, +} + +/// Fully assembled maker order parameters for which a valid signature must exist #[derive(AnchorSerialize, AnchorDeserialize, Clone, Eq, PartialEq, Debug, Copy)] pub struct RFQMakerOrderParams { pub uuid: [u8; 8], @@ -664,6 +685,29 @@ pub struct RFQMakerOrderParams { pub max_ts: i64, } +impl RFQMakerOrderParams { + /// Build `RFQMakerOrderParams` from parts + pub fn new(pubkey: [u8; 32], common: RFQOrderCommon, params: RFQMakerOrderStub) -> Self { + Self { + authority: pubkey.into(), + sub_account_id: params.sub_account_id, + uuid: common.uuid, + market_index: common.market_index, + market_type: common.market_type, + direction: common.taker_direction.opposite(), + base_asset_amount: params.base_asset_amount, + price: params.price, + max_ts: params.max_ts + } + } + /// Calculate sha256 digest of these maker order parameters + pub fn digest(&self) -> [u8; 32] { + solana_program::hash::hash( + &self.try_to_vec().unwrap() + ).to_bytes() + } +} + #[derive(AnchorSerialize, AnchorDeserialize, Clone, Eq, PartialEq, Debug)] pub struct RFQMakerMessage { pub order_params: RFQMakerOrderParams, @@ -672,9 +716,24 @@ pub struct RFQMakerMessage { #[derive(AnchorSerialize, AnchorDeserialize, Clone, Eq, PartialEq, Debug)] pub struct RFQMatch { + /// How much taker wants to match pub base_asset_amount: u64, pub maker_order_params: RFQMakerOrderParams, - pub maker_signature: [u8; 64], +} + +/// Unverified and compact `RFQMatch` for use in RFQ Ix data +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Eq, PartialEq, Debug)] +pub struct RFQMatchStub { + /// How much taker wants to match + pub base_asset_amount: u64, + pub maker_order_stub: RFQMakerOrderStub, +} + +/// Ix parameters for an RFQ trade +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Eq, PartialEq, Debug)] +pub struct RFQTradeParams { + pub common: RFQOrderCommon, + pub matches: Vec, } fn get_auction_duration( diff --git a/programs/drift/src/validation/sig_verification.rs b/programs/drift/src/validation/sig_verification.rs index 7ad675d44..d182a58ee 100644 --- a/programs/drift/src/validation/sig_verification.rs +++ b/programs/drift/src/validation/sig_verification.rs @@ -78,3 +78,83 @@ fn check_ed25519_data(data: &[u8], pubkey: &[u8], msg: &[u8], sig: &[u8]) -> Res Ok(()) } + +/// Check Ed25519Program instruction data verifies the given digest +/// +/// `ix_data` the Ed25519Program compliant instruction data [see](https://github.com/solana-labs/solana/blob/master/sdk/src/ed25519_instruction.rs)) +/// `digest` expected digest signed by the offchain client i.e 'sha256(appMessage.serialize())' +/// `pubkey` expected pubkey of the signer +/// +pub fn verify_ed25519_digest(ix: &Instruction, pubkey: &[u8; 32], digest: &[u8; 32]) -> Result<()> { + if ix.program_id != ED25519_ID || ix.accounts.len() != 0 { + msg!("Invalid Ix: program ID: {:?}", ix.program_id); + msg!("Invalid Ix: accounts: {:?}", ix.accounts.len()); + return Err(ErrorCode::MsgVerificationFailed.into()); + } + + let ix_data = &ix.data; + // According to this layout used by the Ed25519Program] + if ix_data.len() <= 112 { + msg!( + "Invalid Ix: data: {:?}, len: {:?}", + ix.data.len(), + 16 + 64 + 32 + digest.len() + ); + return Err(ErrorCode::MsgVerificationFailed.into()); + } + + // Check the ed25519 verify ix header is sound + let num_signatures = ix_data[0]; + let padding = ix_data[1]; + let signature_offset = u16::from_le_bytes( + ix_data[2..=3].try_into().unwrap() + ); + let signature_instruction_index = u16::from_le_bytes(ix_data[4..=5].try_into().unwrap()); + let public_key_offset = u16::from_le_bytes(ix_data[6..=7].try_into().unwrap()); + let public_key_instruction_index = u16::from_le_bytes(ix_data[8..=9].try_into().unwrap()); + let message_data_offset = u16::from_le_bytes(ix_data[10..=11].try_into().unwrap()); + let message_data_size = u16::from_le_bytes(ix_data[12..=13].try_into().unwrap()); + let message_instruction_index = u16::from_le_bytes(ix_data[14..=15].try_into().unwrap()); + + // Expected values + let exp_public_key_offset: u16 = 16; + let exp_signature_offset: u16 = exp_public_key_offset + 32_u16; + let exp_message_data_offset: u16 = exp_signature_offset + 64_u16; + let exp_num_signatures: u8 = 1; + + // Header + if num_signatures != exp_num_signatures + || padding != 0 + || signature_offset != exp_signature_offset + || signature_instruction_index != u16::MAX + || public_key_offset != exp_public_key_offset + || public_key_instruction_index != u16::MAX + || message_data_offset != exp_message_data_offset + || message_instruction_index != u16::MAX + { + return Err(ErrorCode::MsgVerificationFailed.into()); + } + + // verify data is for digest and pubkey + let ix_msg_data = &ix_data[112..]; + if ix_msg_data != digest && message_data_size != digest.len() as u16 { + return Err(ErrorCode::MsgVerificationFailed.into()); + } + + let ix_pubkey = &ix_data[16..16 + 32]; + if ix_pubkey != pubkey { + return Err(ErrorCode::MsgVerificationFailed.into()); + } + + Ok(()) +} + +/// Extract pubkey from serialized Ed25519Program instruction data +pub fn extract_ed25519_ix_pubkey(ix_data: &[u8]) -> Result<[u8; 32]> { + match ix_data[16..16 + 32].try_into() { + Ok(raw) => Ok(raw), + Err(_) => { + Err(ErrorCode::SigVerificationFailed.into()) + } + } +} From 4b8885421611a20337a1e4b9d75803e42a03330b Mon Sep 17 00:00:00 2001 From: jordy25519 Date: Mon, 21 Oct 2024 19:16:38 +0800 Subject: [PATCH 2/2] fmt --- programs/drift/src/instructions/user.rs | 24 +++++++++---------- programs/drift/src/state/order_params.rs | 8 +++---- .../drift/src/validation/sig_verification.rs | 16 ++++++------- 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index d63179394..80a74b1d3 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -29,9 +29,9 @@ use crate::instructions::SpotFulfillmentType; use crate::math::casting::Cast; use crate::math::liquidation::is_user_being_liquidated; use crate::math::margin::{ - calculate_max_withdrawable_amount, - meets_maintenance_margin_requirement, meets_place_order_margin_requirement, - meets_withdraw_margin_requirement, validate_spot_margin_trading, MarginRequirementType, + calculate_max_withdrawable_amount, meets_maintenance_margin_requirement, + meets_place_order_margin_requirement, meets_withdraw_margin_requirement, + validate_spot_margin_trading, MarginRequirementType, }; use crate::math::safe_math::SafeMath; use crate::math::spot_balance::get_token_value; @@ -894,17 +894,15 @@ pub fn handle_place_and_match_rfq_orders<'c: 'info, 'info>( let mut verified_rfq_matches = Vec::with_capacity(number_of_verify_ixs_needed); for (idx, match_params) in rfq_trade_params.matches.iter().enumerate() { // First verify that the message is legitimate - let ix: Instruction = load_instruction_at_checked( - ix_idx - number_of_verify_ixs_needed + idx, - ix_sysvar, - )?; + let ix: Instruction = + load_instruction_at_checked(ix_idx - number_of_verify_ixs_needed + idx, ix_sysvar)?; let maker_pubkey = extract_ed25519_ix_pubkey(&ix.data)?; - let maker_order_params = RFQMakerOrderParams::new(maker_pubkey, rfq_trade_params.common, match_params.maker_order_stub); - verify_ed25519_digest( - &ix, - &maker_pubkey, - &maker_order_params.digest(), - )?; + let maker_order_params = RFQMakerOrderParams::new( + maker_pubkey, + rfq_trade_params.common, + match_params.maker_order_stub, + ); + verify_ed25519_digest(&ix, &maker_pubkey, &maker_order_params.digest())?; verified_rfq_matches.push(RFQMatch { base_asset_amount: match_params.base_asset_amount, diff --git a/programs/drift/src/state/order_params.rs b/programs/drift/src/state/order_params.rs index 0a869fa29..3a1a451bf 100644 --- a/programs/drift/src/state/order_params.rs +++ b/programs/drift/src/state/order_params.rs @@ -661,7 +661,7 @@ pub struct RFQOrderCommon { } /// Compact RFQ maker order parameters for tx size optimization -/// +/// /// It is intended to be combined with `RFQOrderCommon` and tx context to produce a full `RFQMakerOrderParams` message #[derive(AnchorSerialize, AnchorDeserialize, Clone, Eq, PartialEq, Debug, Copy)] pub struct RFQMakerOrderStub { @@ -697,14 +697,12 @@ impl RFQMakerOrderParams { direction: common.taker_direction.opposite(), base_asset_amount: params.base_asset_amount, price: params.price, - max_ts: params.max_ts + max_ts: params.max_ts, } } /// Calculate sha256 digest of these maker order parameters pub fn digest(&self) -> [u8; 32] { - solana_program::hash::hash( - &self.try_to_vec().unwrap() - ).to_bytes() + solana_program::hash::hash(&self.try_to_vec().unwrap()).to_bytes() } } diff --git a/programs/drift/src/validation/sig_verification.rs b/programs/drift/src/validation/sig_verification.rs index d182a58ee..e4eeb6d49 100644 --- a/programs/drift/src/validation/sig_verification.rs +++ b/programs/drift/src/validation/sig_verification.rs @@ -80,11 +80,13 @@ fn check_ed25519_data(data: &[u8], pubkey: &[u8], msg: &[u8], sig: &[u8]) -> Res } /// Check Ed25519Program instruction data verifies the given digest -/// -/// `ix_data` the Ed25519Program compliant instruction data [see](https://github.com/solana-labs/solana/blob/master/sdk/src/ed25519_instruction.rs)) +/// +/// `ix` an Ed25519Program instruction [see](https://github.com/solana-labs/solana/blob/master/sdk/src/ed25519_instruction.rs)) +/// /// `digest` expected digest signed by the offchain client i.e 'sha256(appMessage.serialize())' +/// /// `pubkey` expected pubkey of the signer -/// +/// pub fn verify_ed25519_digest(ix: &Instruction, pubkey: &[u8; 32], digest: &[u8; 32]) -> Result<()> { if ix.program_id != ED25519_ID || ix.accounts.len() != 0 { msg!("Invalid Ix: program ID: {:?}", ix.program_id); @@ -106,9 +108,7 @@ pub fn verify_ed25519_digest(ix: &Instruction, pubkey: &[u8; 32], digest: &[u8; // Check the ed25519 verify ix header is sound let num_signatures = ix_data[0]; let padding = ix_data[1]; - let signature_offset = u16::from_le_bytes( - ix_data[2..=3].try_into().unwrap() - ); + let signature_offset = u16::from_le_bytes(ix_data[2..=3].try_into().unwrap()); let signature_instruction_index = u16::from_le_bytes(ix_data[4..=5].try_into().unwrap()); let public_key_offset = u16::from_le_bytes(ix_data[6..=7].try_into().unwrap()); let public_key_instruction_index = u16::from_le_bytes(ix_data[8..=9].try_into().unwrap()); @@ -153,8 +153,6 @@ pub fn verify_ed25519_digest(ix: &Instruction, pubkey: &[u8; 32], digest: &[u8; pub fn extract_ed25519_ix_pubkey(ix_data: &[u8]) -> Result<[u8; 32]> { match ix_data[16..16 + 32].try_into() { Ok(raw) => Ok(raw), - Err(_) => { - Err(ErrorCode::SigVerificationFailed.into()) - } + Err(_) => Err(ErrorCode::SigVerificationFailed.into()), } }