diff --git a/bin/citrea/tests/bitcoin/utils.rs b/bin/citrea/tests/bitcoin/utils.rs index ee2afbf8a9..4ef2085a49 100644 --- a/bin/citrea/tests/bitcoin/utils.rs +++ b/bin/citrea/tests/bitcoin/utils.rs @@ -188,6 +188,8 @@ pub async fn spawn_bitcoin_da_service( utxo_selection_mode, rpc_timeout_secs: None, rpc_connect_timeout_secs: None, + max_fee_rate_sat_to_pay: None, + fee_rate_cap_duration_secs: None, }; let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); diff --git a/crates/bitcoin-da/src/fee.rs b/crates/bitcoin-da/src/fee.rs index 42c8f1c3fd..db57c98172 100644 --- a/crates/bitcoin-da/src/fee.rs +++ b/crates/bitcoin-da/src/fee.rs @@ -99,6 +99,7 @@ impl FeeService { ) -> Self { let mempool_space_url = mempool_space_url.unwrap_or_else(|| DEFAULT_MEMPOOL_SPACE_URL.to_string()); + Self { client, network, @@ -139,10 +140,13 @@ impl FeeService { .fee_rate } }; + let sat_vkb = smart_fee.map_or(1000, |rate| rate.to_sat()); + let sat_vb = sat_vkb / 1000; + + tracing::debug!("Fee rate: {sat_vb} sat/vb"); - tracing::debug!("Fee rate: {} sat/vb", sat_vkb / 1000); - Ok(sat_vkb / 1000) + Ok(sat_vb) } /// Bump TX fee via cpfp. diff --git a/crates/bitcoin-da/src/service.rs b/crates/bitcoin-da/src/service.rs index ddc718f62d..42d8db5613 100644 --- a/crates/bitcoin-da/src/service.rs +++ b/crates/bitcoin-da/src/service.rs @@ -66,7 +66,10 @@ use crate::REVEAL_OUTPUT_AMOUNT; pub(crate) type Result = std::result::Result; -const POLLING_INTERVAL: u64 = 10; // seconds +const POLLING_INTERVAL: u64 = 10; // 10 seconds + +const DEFAULT_FEE_RATE_CAP_DURATION_SECS: u64 = 3600; // 1 hour default cap duration +const DEFAULT_MAX_FEE_RATE_SAT_VB: u64 = 15; // 15sat/vb default max fee rate /// Map sov Network to Bitcoin Network. pub fn network_to_bitcoin_network(network: &Network) -> bitcoin::Network { @@ -124,6 +127,12 @@ pub struct BitcoinServiceConfig { /// Connection timeout for RPC in seconds pub rpc_connect_timeout_secs: Option, + + /// Max fee rate in sat/vb + pub max_fee_rate_sat_to_pay: Option, + + /// Fee rate cap duration in seconds + pub fee_rate_cap_duration_secs: Option, } impl citrea_common::FromEnv for BitcoinServiceConfig { @@ -149,6 +158,12 @@ impl citrea_common::FromEnv for BitcoinServiceConfig { rpc_connect_timeout_secs: read_env("BITCOIN_RPC_CONNECT_TIMEOUT_SECS") .ok() .and_then(|v| v.parse::().ok()), + max_fee_rate_sat_to_pay: read_env("BITCOIN_MAX_FEE_RATE_SAT_TO_PAY") + .ok() + .and_then(|v| v.parse::().ok()), + fee_rate_cap_duration_secs: read_env("BITCOIN_FEE_RATE_CAP_DURATION_SECS") + .ok() + .and_then(|v| v.parse::().ok()), }) } } @@ -170,6 +185,8 @@ pub struct BitcoinService { tx_queue: Arc>>, pub(crate) tx_signer: TxSigner, utxo_selection_mode: UtxoSelectionMode, + max_fee_rate_sat_to_pay: u64, + fee_rate_cap_duration_secs: u64, } impl BitcoinService { @@ -185,6 +202,8 @@ impl BitcoinService { reveal_tx_prefix: Vec, tx_backup_dir: PathBuf, utxo_selection_mode: UtxoSelectionMode, + max_fee_rate_sat_to_pay: u64, + fee_rate_cap_duration_secs: u64, ) -> Self { Self { tx_signer: TxSigner::new(client.clone()), @@ -202,6 +221,8 @@ impl BitcoinService { ))), tx_queue: Arc::new(Mutex::new(VecDeque::new())), utxo_selection_mode, + max_fee_rate_sat_to_pay, + fee_rate_cap_duration_secs, } } @@ -242,6 +263,12 @@ impl BitcoinService { .map_err(|_| BitcoinServiceError::InvalidPrivateKey)?; let utxo_selection_mode = config.utxo_selection_mode.clone().unwrap_or_default(); + let max_fee_rate_sat_to_pay = config + .max_fee_rate_sat_to_pay + .unwrap_or(DEFAULT_MAX_FEE_RATE_SAT_VB); + let fee_rate_cap_duration_secs = config + .fee_rate_cap_duration_secs + .unwrap_or(DEFAULT_FEE_RATE_CAP_DURATION_SECS); Ok(Self::new( client, network, @@ -253,6 +280,8 @@ impl BitcoinService { chain_params.reveal_tx_prefix, tx_backup_dir.to_path_buf(), utxo_selection_mode, + max_fee_rate_sat_to_pay, + fee_rate_cap_duration_secs, )) } @@ -286,6 +315,7 @@ impl BitcoinService { if let Some(request) = request_opt { trace!("A new request is received"); + let start = std::time::Instant::now(); loop { // Build and queue tx with retries: let fee_sat_per_vbyte = match self.fee.get_fee_rate().await { @@ -296,6 +326,26 @@ impl BitcoinService { continue; } }; + + // Cap fee at self.max_fee_rate_sat_to_pay for a maximum of `self.fee_rate_cap_duration_secs`. + // If `self.fee_rate_cap_duration_secs` is exceeded, send transaction with fee rate above `self.max_fee_rate_sat_to_pay` anyway + let elapsed = start.elapsed().as_secs(); + + if fee_sat_per_vbyte > self.max_fee_rate_sat_to_pay + && elapsed < self.fee_rate_cap_duration_secs { + warn!("Fee rate {} sat/vb above cap of {}. Waiting (elapsed: {}s / max: {}s)", fee_sat_per_vbyte, self.max_fee_rate_sat_to_pay, elapsed, self.fee_rate_cap_duration_secs); + tokio::time::sleep(Duration::from_secs(10)).await; + continue; + } + + if fee_sat_per_vbyte > self.max_fee_rate_sat_to_pay + && elapsed >= self.fee_rate_cap_duration_secs { + warn!( + "Fee rate {} sat/vb above cap of {} sat/vb, but cap duration of {}s exceeded. Sending transaction anyway", + fee_sat_per_vbyte, self.max_fee_rate_sat_to_pay, self.fee_rate_cap_duration_secs + ); + } + match self .send_transaction_with_fee_rate( request.tx_request.clone(),