Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

L1 fee throttle #1528

Open
wants to merge 14 commits into
base: nightly
Choose a base branch
from
6 changes: 3 additions & 3 deletions Cargo.lock

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

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,10 @@ alloy-eips = { version = "0.4.2", default-features = false }
alloy-consensus = { version = "0.4.2", default-features = false, features = ["serde", "serde-bincode-compat"] }
alloy-network = { version = "0.4.2", default-features = false }

citrea-e2e = { git = "https://github.com/chainwayxyz/citrea-e2e", rev = "af85eae3010331df0e0cda5288f741b6d298813f" }
citrea-e2e = { git = "https://github.com/chainwayxyz/citrea-e2e", rev = "6cee848" }

[patch.crates-io]
bitcoincore-rpc = { version = "0.18.0", git = "https://github.com/chainwayxyz/rust-bitcoincore-rpc.git", rev = "ca3cfa2" }
bitcoincore-rpc = { version = "0.18.0", git = "https://github.com/chainwayxyz/rust-bitcoincore-rpc.git", rev = "c863fd0" }

[profile.release]
opt-level = 3
Expand Down
5 changes: 4 additions & 1 deletion bin/citrea/src/rollup/bitcoin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use bitcoin_da::spec::{BitcoinSpec, RollupParams};
use bitcoin_da::verifier::BitcoinVerifier;
use citrea_common::rpc::register_healthcheck_rpc;
use citrea_common::tasks::manager::TaskManager;
use citrea_common::FullNodeConfig;
use citrea_common::{FeeThrottleConfig, FullNodeConfig};
use citrea_primitives::forks::use_network_forks;
use citrea_primitives::{TO_BATCH_PROOF_PREFIX, TO_LIGHT_CLIENT_PREFIX};
use citrea_risc0_adapter::host::Risc0BonsaiHost;
Expand Down Expand Up @@ -117,6 +117,7 @@ impl RollupBlueprint for BitcoinRollup {
rollup_config: &FullNodeConfig<Self::DaConfig>,
require_wallet_check: bool,
task_manager: &mut TaskManager<()>,
throttle_config: Option<FeeThrottleConfig>,
) -> Result<Arc<Self::DaService>, anyhow::Error> {
let (tx, rx) = unbounded_channel::<TxRequestWithNotifier<TxidWrapper>>();

Expand All @@ -128,6 +129,7 @@ impl RollupBlueprint for BitcoinRollup {
to_batch_proof_prefix: TO_BATCH_PROOF_PREFIX.to_vec(),
},
tx,
throttle_config,
)
.await?
} else {
Expand All @@ -142,6 +144,7 @@ impl RollupBlueprint for BitcoinRollup {
.await?
};
let service = Arc::new(bitcoin_service);

// until forced transactions are implemented,
// require_wallet_check is set false for full nodes.
if require_wallet_check {
Expand Down
3 changes: 2 additions & 1 deletion bin/citrea/src/rollup/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::sync::Arc;
use async_trait::async_trait;
use citrea_common::rpc::register_healthcheck_rpc;
use citrea_common::tasks::manager::TaskManager;
use citrea_common::FullNodeConfig;
use citrea_common::{FeeThrottleConfig, FullNodeConfig};
use citrea_primitives::forks::use_network_forks;
// use citrea_sp1::host::SP1Host;
use citrea_risc0_adapter::host::Risc0BonsaiHost;
Expand Down Expand Up @@ -84,6 +84,7 @@ impl RollupBlueprint for MockDemoRollup {
rollup_config: &FullNodeConfig<Self::DaConfig>,
_require_wallet_check: bool,
_task_manager: &mut TaskManager<()>,
_throttle_config: Option<FeeThrottleConfig>,
) -> Result<Arc<Self::DaService>, anyhow::Error> {
Ok(Arc::new(MockDaService::new(
rollup_config.da.sender_address.clone(),
Expand Down
13 changes: 9 additions & 4 deletions bin/citrea/src/rollup/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@ pub trait CitreaRollupBlueprint: RollupBlueprint {
{
let mut task_manager = TaskManager::default();
let da_service = self
.create_da_service(&rollup_config, true, &mut task_manager)
.create_da_service(
&rollup_config,
true,
&mut task_manager,
Some(sequencer_config.fee_throttle.clone()),
)
.await?;

// TODO: Double check what kind of storage needed here.
Expand Down Expand Up @@ -175,7 +180,7 @@ pub trait CitreaRollupBlueprint: RollupBlueprint {
{
let mut task_manager = TaskManager::default();
let da_service = self
.create_da_service(&rollup_config, false, &mut task_manager)
.create_da_service(&rollup_config, false, &mut task_manager, None)
.await?;

// TODO: Double check what kind of storage needed here.
Expand Down Expand Up @@ -307,7 +312,7 @@ pub trait CitreaRollupBlueprint: RollupBlueprint {
{
let mut task_manager = TaskManager::default();
let da_service = self
.create_da_service(&rollup_config, true, &mut task_manager)
.create_da_service(&rollup_config, true, &mut task_manager, None)
.await?;

// Migrate before constructing ledger_db instance so that no lock is present.
Expand Down Expand Up @@ -441,7 +446,7 @@ pub trait CitreaRollupBlueprint: RollupBlueprint {

let mut task_manager = TaskManager::default();
let da_service = self
.create_da_service(&rollup_config, true, &mut task_manager)
.create_da_service(&rollup_config, true, &mut task_manager, None)
.await?;

let rocksdb_config = RocksdbConfig::new(
Expand Down
1 change: 1 addition & 0 deletions bin/citrea/tests/bitcoin_e2e/batch_prover_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ impl TestCase for SkipPreprovenCommitmentsTest {
to_batch_proof_prefix: TO_BATCH_PROOF_PREFIX.to_vec(),
},
tx,
None,
)
.await
.unwrap(),
Expand Down
2 changes: 2 additions & 0 deletions bin/citrea/tests/bitcoin_e2e/light_client_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,7 @@ impl TestCase for LightClientBatchProofMethodIdUpdateTest {
to_batch_proof_prefix: TO_BATCH_PROOF_PREFIX.to_vec(),
},
tx,
None,
)
.await
.unwrap(),
Expand Down Expand Up @@ -810,6 +811,7 @@ impl TestCase for LightClientUnverifiableBatchProofTest {
to_batch_proof_prefix: TO_BATCH_PROOF_PREFIX.to_vec(),
},
tx,
None,
)
.await
.unwrap(),
Expand Down
120 changes: 119 additions & 1 deletion bin/citrea/tests/bitcoin_e2e/sequencer_test.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
use std::net::SocketAddr;
use std::time::{Duration, SystemTime};

use alloy_primitives::U64;
use anyhow::bail;
use async_trait::async_trait;
use bitcoin_da::rpc::DaRpcClient;
use citrea_e2e::config::SequencerConfig;
use citrea_e2e::framework::TestFramework;
use citrea_e2e::node::Config;
use citrea_e2e::test_case::{TestCase, TestCaseRunner};
use citrea_e2e::traits::Restart;
use citrea_e2e::traits::{NodeT, Restart};
use citrea_e2e::Result;
use reth_primitives::BlockNumberOrTag;
use sov_ledger_rpc::LedgerRpcClient;

use super::get_citrea_path;
use crate::evm::make_test_client;

struct BasicSequencerTest;

Expand Down Expand Up @@ -145,3 +152,114 @@ async fn test_sequencer_missed_da_blocks() -> Result<()> {
.run()
.await
}

struct DaThrottleTest;

#[async_trait]
impl TestCase for DaThrottleTest {
async fn run_test(&mut self, f: &mut TestFramework) -> Result<()> {
let sequencer = f.sequencer.as_ref().unwrap();
let da = f.bitcoin_nodes.get(0).expect("DA not running.");

let seq_config = sequencer.config();
let seq_test_client = make_test_client(SocketAddr::new(
seq_config.rpc_bind_host().parse()?,
seq_config.rpc_bind_port(),
))
.await?;

let base_l1_fee_rate = 2_500_000_000f64;

// Get initial usage stats
let initial_usage = sequencer.client.http_client().da_usage_window().await?;
assert_eq!(initial_usage.total_bytes, 0);
assert_eq!(initial_usage.usage_ratio, 0.0);

sequencer.client.send_publish_batch_request().await?;
sequencer.wait_for_l2_height(1, None).await?;

let seq_block = seq_test_client
.eth_get_block_by_number_with_detail(Some(BlockNumberOrTag::Latest))
.await;

let l1_fee_rate = seq_block.other.get("l1FeeRate").unwrap().as_f64().unwrap();
assert_eq!(l1_fee_rate, base_l1_fee_rate);

// Generate seqcommitments to increase DA usage
for _ in 0..sequencer.min_soft_confirmations_per_commitment() - 1 {
sequencer.client.send_publish_batch_request().await?;
}

// Wait for tx to hit mempool and check DA usage increased
da.wait_mempool_len(2, None).await?;
let da_usage = sequencer.client.http_client().da_usage_window().await?;
assert!(da_usage.total_bytes > 0);
assert!(da_usage.usage_ratio > 0.0);

// Generate more seqcoms to exceed threshold
let n_txs = 3;
for _ in 0..n_txs {
for _ in 0..sequencer.min_soft_confirmations_per_commitment() {
sequencer.client.send_publish_batch_request().await?;
}
}
da.wait_mempool_len(2 + 2 * n_txs, None).await?;

// Check that usage is above threshold and multiplier > 1
let usage_after_seqcom = sequencer.client.http_client().da_usage_window().await?;
assert!(usage_after_seqcom.total_bytes > da_usage.total_bytes);
assert!(usage_after_seqcom.usage_ratio > da_usage.usage_ratio);
assert!(usage_after_seqcom.fee_multiplier > Some(1.0));

sequencer.client.send_publish_batch_request().await?;

let seq_block = seq_test_client
.eth_get_block_by_number_with_detail(Some(BlockNumberOrTag::Latest))
.await;
let throttled_l1_fee_rate = seq_block.other.get("l1FeeRate").unwrap().as_f64().unwrap();
assert_eq!(
throttled_l1_fee_rate,
(base_l1_fee_rate * usage_after_seqcom.fee_multiplier.unwrap()).floor()
);

// Check that usage window is correclty resetted on interval
let interval = seq_config
.rollup_config()
.da
.monitoring
.as_ref()
.unwrap()
.window_duration_secs;
let next_reset = interval
- (SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
- da_usage.start_time);

// Sleep until next_reset + a 1s buffer
tokio::time::sleep(Duration::from_secs(next_reset + 1)).await;
let resetted_usage = sequencer.client.http_client().da_usage_window().await?;
assert_eq!(resetted_usage.total_bytes, 0);
assert_eq!(resetted_usage.usage_ratio, 0.0);
assert_eq!(resetted_usage.fee_multiplier, Some(1.0));
assert_eq!(resetted_usage.start_time, da_usage.start_time + interval);

sequencer.client.send_publish_batch_request().await?;

let seq_block = seq_test_client
.eth_get_block_by_number_with_detail(Some(BlockNumberOrTag::Latest))
.await;
let l1_fee_rate = seq_block.other.get("l1FeeRate").unwrap().as_f64().unwrap();
assert_eq!(l1_fee_rate, base_l1_fee_rate);
Ok(())
}
}

#[tokio::test]
async fn test_da_throttle() -> Result<()> {
TestCaseRunner::new(DaThrottleTest)
.set_citrea_path(get_citrea_path())
.run()
.await
}
68 changes: 66 additions & 2 deletions crates/bitcoin-da/src/fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use bitcoincore_rpc::json::{
BumpFeeResult, CreateRawTransactionInput, WalletCreateFundedPsbtOptions,
};
use bitcoincore_rpc::{Client, RpcApi};
use citrea_common::FeeThrottleConfig;
use tracing::{debug, instrument, trace, warn};

use crate::monitoring::{MonitoredTx, MonitoredTxKind};
Expand Down Expand Up @@ -186,10 +187,45 @@ pub(crate) async fn get_fee_rate_from_mempool_space(
Ok(Some(fee_rate))
}

#[derive(Debug, Clone)]
pub struct FeeThrottleService {
config: FeeThrottleConfig,
}

impl FeeThrottleService {
pub fn new(config: FeeThrottleConfig) -> Result<Self> {
config.validate()?;

Ok(Self { config })
}

/// Get adjusted fee rate according to current da usage
/// Returns base_fee_multiplier (1.0) when usage is below capacity threshold
/// When usage exceeds threshold, increases as: base_fee_multiplier * (1 + scalar * x^factor)
/// where x is the normalized excess usage, capped at max_fee_multiplier
/// Resulting multiplier is capped at max_fee_multiplier
#[instrument(level = "trace", skip_all, ret)]
pub fn get_fee_rate_multiplier(&self, usage_ratio: f64) -> f64 {
if usage_ratio <= self.config.capacity_threshold {
return self.config.base_fee_multiplier;
}

let excess = usage_ratio - self.config.capacity_threshold;
let normalized_excess = excess / (1.0 - self.config.capacity_threshold);
let multiplier = (self.config.base_fee_multiplier
* (1.0
+ self.config.fee_multiplier_scalar
* normalized_excess.powf(self.config.fee_exponential_factor)))
.min(self.config.max_fee_multiplier);

debug!("DA usage ratio: {usage_ratio:.2}, multiplier: {multiplier:.2}");
multiplier
}
}

#[cfg(test)]
mod tests {

use super::{get_fee_rate_from_mempool_space, DEFAULT_MEMPOOL_SPACE_URL};
use super::*;

#[tokio::test]
async fn test_mempool_space_fee_rate() {
Expand All @@ -216,4 +252,32 @@ mod tests {
.unwrap()
);
}

#[test]
fn test_fee_multiplier() {
let test_cases = vec![
(0.0, 1.0), // No usage
(0.25, 1.0), // Below threshold
(0.5, 1.0), // At threshold
(0.6, 1.016), // Above threshold, start increasing fee
(0.7, 1.256),
(0.8, 2.296),
(0.85, 3.40),
(0.9, 4.0), // Max multiplier hit
(0.95, 4.0),
(1.0, 4.0),
];

let fee_service = FeeThrottleService::new(FeeThrottleConfig::default()).unwrap();
for (usage, expected) in test_cases {
let multiplier = fee_service.get_fee_rate_multiplier(usage);
assert!(
(multiplier - expected).abs() < 0.1,
"Usage {}: expected multiplier {}, got {}",
usage,
expected,
multiplier
);
}
}
}
Loading
Loading