From f2369a7867907f0805990ab62ed1effe2df26224 Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Wed, 11 Dec 2024 20:21:02 -0800 Subject: [PATCH 1/4] feat: min slot support for submitting svm bids --- auction-server/src/auction/api.rs | 6 ++ auction-server/src/auction/entities/bid.rs | 2 + .../src/auction/service/simulator.rs | 11 +++- .../src/auction/service/verification.rs | 55 ++++++++++++------- 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 +- 11 files changed, 74 insertions(+), 23 deletions(-) diff --git a/auction-server/src/auction/api.rs b/auction-server/src/auction/api.rs index f4c6f9fd..78b59c77 100644 --- a/auction-server/src/auction/api.rs +++ b/auction-server/src/auction/api.rs @@ -63,6 +63,7 @@ use { DisplayFromStr, }, solana_sdk::{ + clock::Slot, hash::Hash, signature::Signature, transaction::VersionedTransaction, @@ -315,6 +316,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)] @@ -725,6 +730,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 04a09a54..6d10e6a0 100644 --- a/auction-server/src/auction/entities/bid.rs +++ b/auction-server/src/auction/entities/bid.rs @@ -27,6 +27,7 @@ use { U256, }, solana_sdk::{ + clock::Slot, pubkey::Pubkey, signature::Signature, transaction::VersionedTransaction, @@ -241,6 +242,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 7e5f75ea..c631ee73 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, }, }, @@ -172,7 +173,7 @@ impl Simulator { 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() { @@ -190,11 +191,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..40f5b6fe 100644 --- a/auction-server/src/auction/service/verification.rs +++ b/auction-server/src/auction/service/verification.rs @@ -578,25 +578,42 @@ 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"), - }) - } - Ok(_) => Ok(()), + const RETRY_LIMIT: usize = 3; + let mut retry_count = 0; + 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 result.context.slot < bid.chain_data.slot.unwrap_or_default() + && retry_count < RETRY_LIMIT + { + retry_count += 1; + tracing::warn!( + "Simulation failed with stale slot. Simulation slot: {}, Bid Slot: {} Retry count: {}, Error: {:?}", + result.context.slot, + bid.chain_data.slot.unwrap_or_default(), + retry_count, + err + ); + continue; + } + let msgs = err.meta.logs; + Err(RestError::SimulationError { + result: Default::default(), + reason: msgs.join("\n"), + }) + } + 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 56ea723b..b5779d88 100644 --- a/sdk/js/src/index.ts +++ b/sdk/js/src/index.ts @@ -547,6 +547,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( From c5639f912eb8f03cad44b89dc63df3eb41272a16 Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Wed, 11 Dec 2024 20:23:35 -0800 Subject: [PATCH 2/4] Sleep between retries --- auction-server/src/auction/service/verification.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/auction-server/src/auction/service/verification.rs b/auction-server/src/auction/service/verification.rs index 40f5b6fe..76454d9a 100644 --- a/auction-server/src/auction/service/verification.rs +++ b/auction-server/src/auction/service/verification.rs @@ -578,7 +578,7 @@ impl Service { } pub async fn simulate_bid(&self, bid: &entities::BidCreate) -> Result<(), RestError> { - const RETRY_LIMIT: usize = 3; + const RETRY_LIMIT: usize = 5; let mut retry_count = 0; loop { let response = self @@ -604,6 +604,7 @@ impl Service { retry_count, err ); + tokio::time::sleep(Duration::from_millis(100)).await; continue; } let msgs = err.meta.logs; From 869e1f074b2984f2c6cd6d6378f5b8ede5d30d44 Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Fri, 13 Dec 2024 18:01:29 -0800 Subject: [PATCH 3/4] Address comments --- .../src/auction/service/simulator.rs | 2 + .../src/auction/service/verification.rs | 38 +++++++++++++------ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/auction-server/src/auction/service/simulator.rs b/auction-server/src/auction/service/simulator.rs index c631ee73..e4c9a4de 100644 --- a/auction-server/src/auction/service/simulator.rs +++ b/auction-server/src/auction/service/simulator.rs @@ -168,6 +168,8 @@ 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], diff --git a/auction-server/src/auction/service/verification.rs b/auction-server/src/auction/service/verification.rs index 76454d9a..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, @@ -579,7 +581,28 @@ impl Service { pub async fn simulate_bid(&self, bid: &entities::BidCreate) -> Result<(), RestError> { 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 + } + }; + loop { let response = self .config @@ -593,18 +616,9 @@ impl Service { })?; return match result.value { Err(err) => { - if result.context.slot < bid.chain_data.slot.unwrap_or_default() - && retry_count < RETRY_LIMIT - { + if should_retry(result.context.slot, retry_count, &err) { + tokio::time::sleep(RETRY_DELAY).await; retry_count += 1; - tracing::warn!( - "Simulation failed with stale slot. Simulation slot: {}, Bid Slot: {} Retry count: {}, Error: {:?}", - result.context.slot, - bid.chain_data.slot.unwrap_or_default(), - retry_count, - err - ); - tokio::time::sleep(Duration::from_millis(100)).await; continue; } let msgs = err.meta.logs; @@ -613,6 +627,8 @@ impl Service { 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(()), }; } From 5b4bc2ed59d8ffb8bbf5bfc977e7515a578ad386 Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Wed, 18 Dec 2024 11:39:33 +0100 Subject: [PATCH 4/4] fix import --- auction-server/api-types/src/bid.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auction-server/api-types/src/bid.rs b/auction-server/api-types/src/bid.rs index 59a1c67d..f0b85d9a 100644 --- a/auction-server/api-types/src/bid.rs +++ b/auction-server/api-types/src/bid.rs @@ -22,8 +22,8 @@ use { DisplayFromStr, }, solana_sdk::{ + clock::Slot, signature::Signature, - slot_history::Slot, transaction::VersionedTransaction, }, strum::AsRefStr,