Skip to content

Commit

Permalink
feat: Enforce prioritization fees (#242)
Browse files Browse the repository at this point in the history
* feat: Enforce prioritization fees

* feat: update sdks to use the latest prioritization fees
  • Loading branch information
m30m authored Nov 18, 2024
1 parent 62cc829 commit 435f282
Show file tree
Hide file tree
Showing 12 changed files with 142 additions and 36 deletions.
1 change: 1 addition & 0 deletions auction-server/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions auction-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ anchor-lang = "0.30.1"
express-relay = { path = "../contracts/svm/programs/express_relay" }
solana-rpc-client = "2.0.13"
solana-transaction-status = "2.0.13"
borsh = "1.5.1"

# The curve25519-dalek crate is a dependency of solana-sdk.
# This crate relies on a specific version of zeroize that is incompatible with many other packages.
Expand Down
52 changes: 52 additions & 0 deletions auction-server/src/auction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use {
ChainStoreEvm,
ChainStoreSvm,
LookupTableCache,
MicroLamports,
SimulatedBidCoreFields,
SimulatedBidEvm,
SimulatedBidSvm,
Expand All @@ -60,6 +61,7 @@ use {
axum::async_trait,
axum_prometheus::metrics,
bincode::serialized_size,
borsh::de::BorshDeserialize,
ethers::{
abi,
contract::{
Expand Down Expand Up @@ -124,6 +126,7 @@ use {
solana_sdk::{
address_lookup_table::state::AddressLookupTable,
commitment_config::CommitmentConfig,
compute_budget,
instruction::CompiledInstruction,
packet::PACKET_DATA_SIZE,
pubkey::Pubkey,
Expand Down Expand Up @@ -1152,6 +1155,11 @@ pub async fn handle_bid_svm(
.ok_or(RestError::InvalidChainId)?
.as_ref();

let compute_budget = chain_store
.get_minimum_acceptable_prioritization_fee()
.await
.unwrap_or(0);
check_compute_budget(compute_budget, &bid.transaction)?;

let bid_data_svm =
extract_bid_data_svm(chain_store, bid.transaction.clone(), &chain_store.client).await?;
Expand Down Expand Up @@ -1235,6 +1243,50 @@ fn all_signature_exists_svm(
Ok(())
}

fn check_compute_budget(
compute_budget: MicroLamports,
transaction: &VersionedTransaction,
) -> Result<(), RestError> {
let budgets: Vec<u64> = transaction
.message
.instructions()
.iter()
.filter_map(|instruction| {
let program_id = instruction.program_id(transaction.message.static_account_keys());
if program_id != &compute_budget::id() {
return None;
}

match compute_budget::ComputeBudgetInstruction::try_from_slice(&instruction.data) {
Ok(compute_budget::ComputeBudgetInstruction::SetComputeUnitPrice(price)) => {
Some(price)
}
_ => None,
}
})
.collect();
if budgets.len() > 1 {
return Err(RestError::BadParameters(
"Multiple SetComputeUnitPrice instructions".to_string(),
));
}
if budgets.is_empty() && compute_budget > 0 {
return Err(RestError::BadParameters(format!(
"No SetComputeUnitPrice instruction. Minimum compute budget is {}",
compute_budget
)));
}
if let Some(budget) = budgets.first() {
if *budget < compute_budget {
return Err(RestError::BadParameters(format!(
"Compute budget is too low. Minimum compute budget is {}",
compute_budget
)));
}
}
Ok(())
}

async fn verify_signatures_svm(
store_new: Arc<StoreNew>,
chain_store: &ChainStoreSvm,
Expand Down
66 changes: 52 additions & 14 deletions auction-server/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,17 @@ use {
time::Duration,
},
time::UtcOffset,
tokio::sync::{
broadcast::{
self,
Receiver,
Sender,
tokio::{
sync::{
broadcast::{
self,
Receiver,
Sender,
},
Mutex,
RwLock,
},
Mutex,
RwLock,
time::Instant,
},
tokio_util::task::TaskTracker,
utoipa::{
Expand Down Expand Up @@ -559,21 +562,28 @@ impl ChainStoreEvm {
}
}

pub type MicroLamports = u64;
#[derive(Clone, Debug)]
struct PrioritizationFeeSample {
///micro-lamports per compute unit.
fee: MicroLamports,
sample_time: Instant,
}

pub struct ChainStoreSvm {
pub core_fields: ChainStoreCoreFields<SimulatedBidSvm>,

tx_broadcaster_client: Arc<RpcClient>,
log_sender: Sender<Response<RpcLogsResponse>>,
// only to avoid closing the channel
_dummy_log_receiver: Receiver<Response<RpcLogsResponse>>,
recent_prioritization_fees: RwLock<VecDeque<PrioritizationFeeSample>>,
pub client: RpcClient,
pub config: ConfigSvm,
pub express_relay_svm: ExpressRelaySvm,
pub wallet_program_router_account: Pubkey,
pub name: String,
pub lookup_table_cache: LookupTableCache,
/// Recent network prioritization fees in micro-lamports per compute unit.
pub recent_prioritization_fees: RwLock<VecDeque<u64>>,
}

const SVM_SEND_TRANSACTION_RETRY_COUNT: i32 = 5;
Expand Down Expand Up @@ -736,16 +746,42 @@ impl ChainStoreSvm {

/// Polls an estimate of recent priotization fees and stores it in `recent_prioritization_fees`.
/// `recent_prioritization_fees` stores the last 12 estimates received.
pub async fn get_and_store_recent_prioritization_fee(&self) -> Result<u64, ClientError> {
pub async fn get_and_store_recent_prioritization_fee(
&self,
) -> Result<MicroLamports, ClientError> {
let fee = self.get_median_prioritization_fee().await?;
let mut write_guard = self.recent_prioritization_fees.write().await;
write_guard.push_back(fee);
let sample = PrioritizationFeeSample {
fee,
sample_time: Instant::now(),
};
tracing::info!("Last prioritization fee: {:?}", sample);
write_guard.push_back(sample);
if write_guard.len() > 12 {
write_guard.pop_front();
}
tracing::info!("Recent prioritization fees: {:?}", write_guard);
Ok(fee)
}

/// Get the minimum prioritization fee that is acceptable for submission on chain.
/// In order to avoid rejection of transactions because of recent changes in the priority fees,
/// we consider the minimum priority fee that was acceptable in the last 15 seconds.
/// This timeframe should include at least 2 samples, otherwise we will reject bids if the
/// latest sample is higher than the previous one and the searchers have not updated their
/// priority fees fast enough.
pub async fn get_minimum_acceptable_prioritization_fee(&self) -> Option<MicroLamports> {
let budgets = self.recent_prioritization_fees.read().await;
budgets
.iter()
.filter_map(|b| {
if b.sample_time.elapsed() < Duration::from_secs(15) {
Some(b.fee)
} else {
None
}
})
.min()
}
}

pub type BidId = Uuid;
Expand Down Expand Up @@ -1644,7 +1680,9 @@ impl Store {
#[serde_as]
#[derive(Serialize, Clone, ToSchema, ToResponse)]
pub struct SvmChainUpdate {
pub chain_id: ChainId,
pub chain_id: ChainId,
#[serde_as(as = "DisplayFromStr")]
pub blockhash: Hash,
pub blockhash: Hash,
/// The prioritization fee that the server suggests to use for the next transaction
pub latest_prioritization_fee: MicroLamports,
}
7 changes: 4 additions & 3 deletions auction-server/src/watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,11 @@ pub async fn run_watcher_loop_svm(store: Arc<Store>, chain_id: String) -> Result
.await,
chain_store.get_and_store_recent_prioritization_fee().await,
) {
(Ok(result), Ok(_fee)) => {
(Ok(result), Ok(fee)) => {
store.broadcast_svm_chain_update(SvmChainUpdate {
chain_id: chain_id.clone(),
blockhash: result.0,
chain_id: chain_id.clone(),
blockhash: result.0,
latest_prioritization_fee: fee,
});
}
(Err(e), _) => {
Expand Down
4 changes: 2 additions & 2 deletions sdk/js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion sdk/js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pythnetwork/express-relay-js",
"version": "0.14.0",
"version": "0.14.1",
"description": "Utilities for interacting with the express relay protocol",
"homepage": "https://github.com/pyth-network/per/tree/main/sdk/js",
"author": "Douro Labs",
Expand Down
20 changes: 14 additions & 6 deletions sdk/js/src/examples/simpleSearcherLimo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import {
Keypair,
PublicKey,
Connection,
Blockhash,
TransactionInstruction,
ComputeBudgetProgram,
} from "@solana/web3.js";

import * as limo from "@kamino-finance/limo-sdk";
Expand All @@ -39,7 +39,7 @@ export class SimpleSearcherLimo {
protected readonly connectionSvm: Connection;
protected mintDecimals: Record<string, number> = {};
protected expressRelayConfig: ExpressRelaySvmConfig | undefined;
protected recentBlockhash: Record<ChainId, Blockhash> = {};
protected latestChainUpdate: Record<ChainId, SvmChainUpdate> = {};
protected readonly bid: anchor.BN;
constructor(
public endpointExpressRelay: string,
Expand Down Expand Up @@ -101,7 +101,14 @@ export class SimpleSearcherLimo {
);

const ixsTakeOrder = await this.generateTakeOrderIxs(limoClient, order);
const txRaw = new anchor.web3.Transaction().add(...ixsTakeOrder);
const feeInstruction = ComputeBudgetProgram.setComputeUnitPrice({
microLamports:
this.latestChainUpdate[this.chainId].latest_prioritization_fee,
});
const txRaw = new anchor.web3.Transaction().add(
feeInstruction,
...ixsTakeOrder
);

const bidAmount = await this.getBidAmount(order);

Expand All @@ -118,7 +125,8 @@ export class SimpleSearcherLimo {
config.feeReceiverRelayer
);

bid.transaction.recentBlockhash = this.recentBlockhash[this.chainId];
bid.transaction.recentBlockhash =
this.latestChainUpdate[this.chainId].blockhash;
bid.transaction.sign(this.searcher);
return bid;
}
Expand Down Expand Up @@ -208,7 +216,7 @@ export class SimpleSearcherLimo {
}

async opportunityHandler(opportunity: Opportunity) {
if (!this.recentBlockhash[this.chainId]) {
if (!this.latestChainUpdate[this.chainId]) {
console.log(
`No recent blockhash for chain ${this.chainId}, skipping bid`
);
Expand All @@ -228,7 +236,7 @@ export class SimpleSearcherLimo {
}

async svmChainUpdateHandler(update: SvmChainUpdate) {
this.recentBlockhash[update.chain_id] = update.blockhash;
this.latestChainUpdate[update.chain_id] = update;
}

// NOTE: Developers are responsible for implementing custom removal logic specific to their use case.
Expand Down
7 changes: 4 additions & 3 deletions sdk/js/src/serverTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface paths {
* Fetch opportunities ready for execution or historical opportunities
* @description depending on the mode. You need to provide `chain_id` for historical mode.
* Opportunities are sorted by creation time in ascending order.
* Total number of opportunities returned is limited by 20.
* Total number of opportunities returned is capped by the server to preserve bandwidth.
*/
get: operations["get_opportunities"];
/**
Expand Down Expand Up @@ -773,6 +773,7 @@ export interface components {
SvmChainUpdate: {
blockhash: components["schemas"]["Hash"];
chain_id: components["schemas"]["ChainId"];
latest_prioritization_fee: components["schemas"]["MicroLamports"];
};
TokenAmountEvm: {
/**
Expand Down Expand Up @@ -926,7 +927,7 @@ export interface operations {
* Fetch opportunities ready for execution or historical opportunities
* @description depending on the mode. You need to provide `chain_id` for historical mode.
* Opportunities are sorted by creation time in ascending order.
* Total number of opportunities returned is limited by 20.
* Total number of opportunities returned is capped by the server to preserve bandwidth.
*/
get_opportunities: {
parameters: {
Expand All @@ -946,7 +947,7 @@ export interface operations {
*/
from_time?: string | null;
/**
* @description The maximum number of opportunities to return. Capped at 100.
* @description The maximum number of opportunities to return. Capped at 100; if more than 100 requested, at most 100 will be returned.
* @example 20
*/
limit?: number;
Expand Down
1 change: 1 addition & 0 deletions sdk/python/express_relay/models/svm.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ class SvmChainUpdate(BaseModel):
"""
chain_id: str
blockhash: SvmHash
latest_prioritization_fee: int


class ProgramSvm(Enum):
Expand Down
Loading

0 comments on commit 435f282

Please sign in to comment.