From 20375faa7f1c0dd360a1331a8a78e88760a0ab0b Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Thu, 19 Dec 2024 09:21:10 +0100 Subject: [PATCH] Feat: min slot bid svm (#291) add optional parameter for svm bids to sepcify the minimum slot where the bid is applicable. If simulation fails for any reason and the simulation context has a lower slot compared to the bid, we will retry hoping that the RPC will catch up soon. --- auction-server/api-types/src/bid.rs | 5 ++ auction-server/src/auction/api.rs | 1 + auction-server/src/auction/entities/bid.rs | 2 + .../src/auction/service/simulator.rs | 13 +++- .../src/auction/service/verification.rs | 70 ++++++++++++++----- sdk/js/src/examples/simpleSearcherLimo.ts | 1 + sdk/js/src/index.ts | 1 + sdk/js/src/serverTypes.d.ts | 7 ++ sdk/js/src/types.ts | 6 ++ sdk/python/express_relay/models/__init__.py | 3 + sdk/python/express_relay/models/svm.py | 3 + .../searcher/examples/simple_searcher_svm.py | 2 +- 12 files changed, 92 insertions(+), 22 deletions(-) diff --git a/auction-server/api-types/src/bid.rs b/auction-server/api-types/src/bid.rs index d6fc5521..f0b85d9a 100644 --- a/auction-server/api-types/src/bid.rs +++ b/auction-server/api-types/src/bid.rs @@ -22,6 +22,7 @@ use { DisplayFromStr, }, solana_sdk::{ + clock::Slot, signature::Signature, transaction::VersionedTransaction, }, @@ -258,6 +259,10 @@ pub struct BidCreateSvm { #[schema(example = "SGVsbG8sIFdvcmxkIQ==", value_type = String)] #[serde(with = "crate::serde::transaction_svm")] pub transaction: VersionedTransaction, + /// The minimum slot required for the bid to be executed successfully + /// None if the bid can be executed at any recent slot + #[schema(example = 293106477, value_type = Option)] + pub slot: Option, } #[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] diff --git a/auction-server/src/auction/api.rs b/auction-server/src/auction/api.rs index 41673306..40044d54 100644 --- a/auction-server/src/auction/api.rs +++ b/auction-server/src/auction/api.rs @@ -429,6 +429,7 @@ impl ApiTrait for Svm { initiation_time: OffsetDateTime::now_utc(), chain_data: entities::BidChainDataCreateSvm { transaction: bid_create_svm.transaction.clone(), + slot: bid_create_svm.slot, }, }) } diff --git a/auction-server/src/auction/entities/bid.rs b/auction-server/src/auction/entities/bid.rs index a6cd2c13..0f6d3f8b 100644 --- a/auction-server/src/auction/entities/bid.rs +++ b/auction-server/src/auction/entities/bid.rs @@ -25,6 +25,7 @@ use { }, express_relay_api_types::bid as api, solana_sdk::{ + clock::Slot, pubkey::Pubkey, signature::Signature, transaction::VersionedTransaction, @@ -239,6 +240,7 @@ pub struct BidCreate { #[derive(Clone, Debug)] pub struct BidChainDataCreateSvm { pub transaction: VersionedTransaction, + pub slot: Option, } #[derive(Clone, Debug)] diff --git a/auction-server/src/auction/service/simulator.rs b/auction-server/src/auction/service/simulator.rs index ded482ad..15c187ab 100644 --- a/auction-server/src/auction/service/simulator.rs +++ b/auction-server/src/auction/service/simulator.rs @@ -15,6 +15,7 @@ use { client_error, rpc_response::{ Response, + RpcResponseContext, RpcResult, }, }, @@ -166,12 +167,14 @@ impl Simulator { Ok(result.value) } + /// Fetches multiple accounts from the RPC in chunks + /// There is no guarantee that all the accounts will be fetched with the same slot async fn get_multiple_accounts_chunked( &self, keys: &[Pubkey], ) -> RpcResult>> { let mut result = vec![]; - let mut last_context = None; + let mut context_with_min_slot: Option = None; const MAX_RPC_ACCOUNT_LIMIT: usize = 100; // Ensure at least one call is made, even if keys is empty let key_chunks = if keys.is_empty() { @@ -189,11 +192,15 @@ impl Simulator { for chunk_result in chunk_results { let chunk_result = chunk_result?; result.extend(chunk_result.value); - last_context = Some(chunk_result.context); + if context_with_min_slot.is_none() + || context_with_min_slot.as_ref().unwrap().slot > chunk_result.context.slot + { + context_with_min_slot = Some(chunk_result.context); + } } Ok(Response { value: result, - context: last_context.unwrap(), // Safe because we ensured at least one call was made + context: context_with_min_slot.unwrap(), // Safe because we ensured at least one call was made }) } diff --git a/auction-server/src/auction/service/verification.rs b/auction-server/src/auction/service/verification.rs index f5e17cbd..55a782ce 100644 --- a/auction-server/src/auction/service/verification.rs +++ b/auction-server/src/auction/service/verification.rs @@ -59,8 +59,10 @@ use { U256, }, }, + litesvm::types::FailedTransactionMetadata, solana_sdk::{ address_lookup_table::state::AddressLookupTable, + clock::Slot, commitment_config::CommitmentConfig, compute_budget, instruction::CompiledInstruction, @@ -578,25 +580,57 @@ impl Service { } pub async fn simulate_bid(&self, bid: &entities::BidCreate) -> Result<(), RestError> { - let response = self - .config - .chain_config - .simulator - .simulate_transaction(&bid.chain_data.transaction) - .await; - let result = response.map_err(|e| { - tracing::error!("Error while simulating bid: {:?}", e); - RestError::TemporarilyUnavailable - })?; - match result.value { - Err(err) => { - let msgs = err.meta.logs; - Err(RestError::SimulationError { - result: Default::default(), - reason: msgs.join("\n"), - }) + const RETRY_LIMIT: usize = 5; + const RETRY_DELAY: Duration = Duration::from_millis(100); + let mut retry_count = 0; + let bid_slot = bid.chain_data.slot.unwrap_or_default(); + + let should_retry = |result_slot: Slot, + retry_count: usize, + err: &FailedTransactionMetadata| + -> bool { + if result_slot < bid_slot && retry_count < RETRY_LIMIT { + tracing::warn!( + "Simulation failed with stale slot. Simulation slot: {}, Bid Slot: {}, Retry count: {}, Error: {:?}", + result_slot, + bid_slot, + retry_count, + err + ); + true + } else { + false } - Ok(_) => Ok(()), + }; + + loop { + let response = self + .config + .chain_config + .simulator + .simulate_transaction(&bid.chain_data.transaction) + .await; + let result = response.map_err(|e| { + tracing::error!("Error while simulating bid: {:?}", e); + RestError::TemporarilyUnavailable + })?; + return match result.value { + Err(err) => { + if should_retry(result.context.slot, retry_count, &err) { + tokio::time::sleep(RETRY_DELAY).await; + retry_count += 1; + continue; + } + let msgs = err.meta.logs; + Err(RestError::SimulationError { + result: Default::default(), + reason: msgs.join("\n"), + }) + } + // Not important to check if bid slot is less than simulation slot if simulation is successful + // since we want to fix incorrect verifications due to stale slot + Ok(_) => Ok(()), + }; } } diff --git a/sdk/js/src/examples/simpleSearcherLimo.ts b/sdk/js/src/examples/simpleSearcherLimo.ts index 25a216dc..7657ff42 100644 --- a/sdk/js/src/examples/simpleSearcherLimo.ts +++ b/sdk/js/src/examples/simpleSearcherLimo.ts @@ -124,6 +124,7 @@ export class SimpleSearcherLimo { config.relayerSigner, config.feeReceiverRelayer ); + bid.slot = opportunity.slot; bid.transaction.recentBlockhash = this.latestChainUpdate[this.chainId].blockhash; diff --git a/sdk/js/src/index.ts b/sdk/js/src/index.ts index 1ab7f696..d494a213 100644 --- a/sdk/js/src/index.ts +++ b/sdk/js/src/index.ts @@ -549,6 +549,7 @@ export class Client { return { chain_id: bid.chainId, + slot: bid.slot, transaction: bid.transaction .serialize({ requireAllSignatures: false }) .toString("base64"), diff --git a/sdk/js/src/serverTypes.d.ts b/sdk/js/src/serverTypes.d.ts index 43c29487..4d137969 100644 --- a/sdk/js/src/serverTypes.d.ts +++ b/sdk/js/src/serverTypes.d.ts @@ -118,6 +118,13 @@ export interface components { * @example solana */ chain_id: string; + /** + * Format: int64 + * @description The minimum slot required for the bid to be executed successfully + * None if the bid can be executed at any recent slot + * @example 293106477 + */ + slot?: number | null; /** * @description The transaction for bid. * @example SGVsbG8sIFdvcmxkIQ== diff --git a/sdk/js/src/types.ts b/sdk/js/src/types.ts index b8f40eab..d3d2520b 100644 --- a/sdk/js/src/types.ts +++ b/sdk/js/src/types.ts @@ -213,6 +213,12 @@ export type BidSvm = { * @example solana */ chainId: ChainId; + /** + * @description The minimum slot required for the bid to be executed successfully + * None if the bid can be executed at any recent slot + * @example 293106477 + */ + slot?: number | null; /** * @description The execution environment for the bid. */ diff --git a/sdk/python/express_relay/models/__init__.py b/sdk/python/express_relay/models/__init__.py index 2cbd5090..443d5df0 100644 --- a/sdk/python/express_relay/models/__init__.py +++ b/sdk/python/express_relay/models/__init__.py @@ -167,11 +167,14 @@ class PostBidMessageParamsSvm(BaseModel): method: A string literal "post_bid". chain_id: The chain ID to bid on. transaction: The transaction including the bid. + slot: The minimum slot required for the bid to be executed successfully + None if the bid can be executed at any recent slot """ method: Literal["post_bid"] chain_id: str transaction: SvmTransaction + slot: int | None def get_discriminator_value(v: Any) -> str: diff --git a/sdk/python/express_relay/models/svm.py b/sdk/python/express_relay/models/svm.py index 3896d738..bbdb0afe 100644 --- a/sdk/python/express_relay/models/svm.py +++ b/sdk/python/express_relay/models/svm.py @@ -191,10 +191,13 @@ class BidSvm(BaseModel): Attributes: transaction: The transaction including the bid chain_id: The chain ID to bid on. + slot: The minimum slot required for the bid to be executed successfully + None if the bid can be executed at any recent slot """ transaction: SvmTransaction chain_id: str + slot: int | None class _OrderPydanticAnnotation: diff --git a/sdk/python/express_relay/searcher/examples/simple_searcher_svm.py b/sdk/python/express_relay/searcher/examples/simple_searcher_svm.py index 43e0398e..3b759b6c 100644 --- a/sdk/python/express_relay/searcher/examples/simple_searcher_svm.py +++ b/sdk/python/express_relay/searcher/examples/simple_searcher_svm.py @@ -163,7 +163,7 @@ async def generate_bid(self, opp: OpportunitySvm) -> BidSvm: transaction.partial_sign( [self.private_key], recent_blockhash=latest_chain_update.blockhash ) - bid = BidSvm(transaction=transaction, chain_id=self.chain_id) + bid = BidSvm(transaction=transaction, chain_id=self.chain_id, slot=opp.slot) return bid async def generate_take_order_ixs(