Skip to content

Commit b0ab39a

Browse files
apollo_consensus_orchestrator: add min gas price to fee market calculation
1 parent d39c988 commit b0ab39a

File tree

12 files changed

+432
-25
lines changed

12 files changed

+432
-25
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/apollo_config_manager/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ apollo_node_config = { workspace = true, features = ["testing"] }
3535
starknet_api.workspace = true
3636
tempfile.workspace = true
3737
tracing-test.workspace = true
38+
validator.workspace = true

crates/apollo_config_manager/src/config_manager_tests.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
use apollo_config_manager_config::config::ConfigManagerConfig;
22
use apollo_consensus_config::config::ConsensusDynamicConfig;
33
use apollo_consensus_config::ValidatorId;
4+
use apollo_consensus_orchestrator_config::config::{
5+
parse_price_per_height,
6+
ContextDynamicConfig,
7+
PricePerHeight,
8+
};
49
use apollo_node_config::node_config::NodeDynamicConfig;
10+
use validator::Validate;
511

612
use crate::config_manager::ConfigManager;
713

@@ -54,3 +60,108 @@ async fn config_manager_update_config() {
5460
new_consensus_dynamic_config
5561
);
5662
}
63+
64+
#[test]
65+
fn test_context_dynamic_config_serialize_deserialize() {
66+
let config = ContextDynamicConfig {
67+
min_l2_gas_price_per_height: vec![
68+
PricePerHeight { height: 100, price: 10_000_000_000 },
69+
PricePerHeight { height: 500, price: 20_000_000_000 },
70+
PricePerHeight { height: 1000, price: 30_000_000_000 },
71+
],
72+
};
73+
74+
// Serialize to JSON
75+
let json = serde_json::to_string(&config).expect("Failed to serialize");
76+
77+
// Deserialize back
78+
let deserialized: ContextDynamicConfig =
79+
serde_json::from_str(&json).expect("Failed to deserialize");
80+
81+
// Should match original
82+
assert_eq!(deserialized, config);
83+
}
84+
85+
#[test]
86+
fn test_context_dynamic_config_serialize_deserialize_empty() {
87+
let config = ContextDynamicConfig { min_l2_gas_price_per_height: vec![] };
88+
89+
let json = serde_json::to_string(&config).expect("Failed to serialize");
90+
let deserialized: ContextDynamicConfig =
91+
serde_json::from_str(&json).expect("Failed to deserialize");
92+
93+
assert_eq!(deserialized, config);
94+
}
95+
96+
#[test]
97+
fn test_parse_price_per_height_with_whitespace() {
98+
// Test that whitespace is properly trimmed during parsing
99+
let data = " 100 : 10000000000 , 500 : 20000000000 ";
100+
// This func is used for deserialization of the min_l2_gas_price_per_height field.
101+
let result = parse_price_per_height(data).expect("Failed to parse");
102+
103+
assert_eq!(result.len(), 2);
104+
assert_eq!(result[0].height, 100);
105+
assert_eq!(result[0].price, 10_000_000_000);
106+
assert_eq!(result[1].height, 500);
107+
assert_eq!(result[1].price, 20_000_000_000);
108+
}
109+
110+
#[test]
111+
fn test_context_dynamic_config_validation_valid() {
112+
let config = ContextDynamicConfig {
113+
min_l2_gas_price_per_height: vec![
114+
PricePerHeight { height: 100, price: 10_000_000_000 },
115+
PricePerHeight { height: 500, price: 20_000_000_000 },
116+
PricePerHeight { height: 1000, price: 30_000_000_000 },
117+
],
118+
};
119+
120+
assert!(config.validate().is_ok());
121+
}
122+
123+
#[test]
124+
fn test_context_dynamic_config_validation_price_below_minimum() {
125+
let config = ContextDynamicConfig {
126+
min_l2_gas_price_per_height: vec![
127+
PricePerHeight { height: 100, price: 500_000_000 }, // Below 8 gwei
128+
],
129+
};
130+
131+
assert!(config.validate().is_err());
132+
}
133+
134+
#[test]
135+
fn test_context_dynamic_config_validation_heights_not_in_order() {
136+
let config = ContextDynamicConfig {
137+
min_l2_gas_price_per_height: vec![
138+
PricePerHeight { height: 500, price: 10_000_000_000 },
139+
PricePerHeight { height: 100, price: 20_000_000_000 }, // Out of order
140+
],
141+
};
142+
143+
assert!(config.validate().is_err());
144+
}
145+
146+
#[test]
147+
fn test_context_dynamic_config_validation_duplicate_heights() {
148+
let config = ContextDynamicConfig {
149+
min_l2_gas_price_per_height: vec![
150+
PricePerHeight { height: 100, price: 10_000_000_000 },
151+
PricePerHeight { height: 100, price: 20_000_000_000 }, // Duplicate
152+
],
153+
};
154+
155+
assert!(config.validate().is_err());
156+
}
157+
158+
#[test]
159+
fn test_context_dynamic_config_validation_price_at_minimum() {
160+
let config = ContextDynamicConfig {
161+
min_l2_gas_price_per_height: vec![
162+
PricePerHeight { height: 100, price: 8_000_000_000 }, // Exactly 8 gwei
163+
],
164+
};
165+
166+
assert!(config.validate().is_ok());
167+
}

crates/apollo_consensus_orchestrator/src/fee_market/mod.rs

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
use std::cmp::max;
22

3+
use apollo_consensus_orchestrator_config::config::PricePerHeight;
34
use ethnum::U256;
45
use serde::Serialize;
5-
use starknet_api::block::GasPrice;
6+
use starknet_api::block::{BlockNumber, GasPrice};
67
use starknet_api::execution_resources::GasAmount;
78
use starknet_api::versioned_constants_logic::VersionedConstantsTrait;
9+
use tracing::info;
810

911
use crate::orchestrator_versioned_constants;
1012

@@ -20,35 +22,67 @@ pub struct FeeMarketInfo {
2022
pub next_l2_gas_price: GasPrice,
2123
}
2224

25+
/// Get the minimum gas price for a given block height from the min_l2_gas_price_per_height
26+
/// configuration. If not exist for the given height, use versioned constants min_gas_price as
27+
/// fallback.
28+
///
29+
/// # Parameters
30+
/// - `height`: The block height to look up.
31+
/// - `min_l2_gas_price_per_height`: List of height-price pairs from configuration, assumed to be
32+
/// sorted by height in ascending order.
33+
pub fn get_min_gas_price_for_height(
34+
height: BlockNumber,
35+
min_l2_gas_price_per_height: &[PricePerHeight],
36+
) -> GasPrice {
37+
let fallback_min_gas_price =
38+
orchestrator_versioned_constants::VersionedConstants::latest_constants().min_gas_price;
39+
min_l2_gas_price_per_height
40+
.iter()
41+
.rev()
42+
.find(|e| e.height <= height.0)
43+
.map(|e| GasPrice(e.price))
44+
.unwrap_or(fallback_min_gas_price)
45+
}
46+
2347
/// Calculate the base gas price for the next block according to EIP-1559.
2448
///
2549
/// # Parameters
2650
/// - `price`: The base gas price per unit (in fri) of the current block.
2751
/// - `gas_used`: The total gas used in the current block.
2852
/// - `gas_target`: The target gas usage per block.
53+
/// - `min_gas_price`: The minimum gas price to enforce.
2954
pub fn calculate_next_base_gas_price(
3055
price: GasPrice,
3156
gas_used: GasAmount,
3257
gas_target: GasAmount,
58+
min_gas_price: GasPrice,
3359
) -> GasPrice {
3460
let versioned_constants =
3561
orchestrator_versioned_constants::VersionedConstants::latest_constants();
3662
assert!(
3763
gas_target < versioned_constants.max_block_size,
3864
"Gas target must be lower than max block size."
3965
);
40-
// A minimum gas price prevents precision loss. Additionally, a minimum gas price helps avoid
41-
// extended periods of low pricing.
42-
assert!(
43-
price >= versioned_constants.min_gas_price,
44-
"The gas price must be at least the minimum to prevent precision loss."
45-
);
4666
assert!(gas_target.0 > 0, "Gas target must be greater than zero.");
4767
assert!(
4868
versioned_constants.gas_price_max_change_denominator > 0,
4969
"Denominator constant must be greater than zero."
5070
);
5171

72+
// If the current price is below the minimum, apply a gradual adjustment and return early.
73+
// This allows the price to increase by at most 1/gas_price_max_change_denominator per block.
74+
if price < min_gas_price {
75+
let max_increase = price.0 / versioned_constants.gas_price_max_change_denominator;
76+
let adjusted = price.0 + max_increase;
77+
// Cap at min_gas_price to avoid overshooting
78+
let adjusted_price = adjusted.min(min_gas_price.0);
79+
info!(
80+
"Fee Market: Price {} below minimum gas price {}, adjusted price: {} )",
81+
price.0, min_gas_price.0, adjusted_price
82+
);
83+
return GasPrice(adjusted_price);
84+
}
85+
5286
// Use U256 to avoid overflow, as multiplying a u128 by a u64 remains within U256 bounds.
5387
let gas_delta = U256::from(gas_used.0.abs_diff(gas_target.0));
5488
let gas_target_u256 = U256::from(gas_target.0);
@@ -71,5 +105,5 @@ pub fn calculate_next_base_gas_price(
71105

72106
// Price should not realistically exceed u128::MAX, bound to avoid theoretical overflow.
73107
let adjusted_price = u128::try_from(adjusted_price_u256).unwrap_or(u128::MAX);
74-
GasPrice(max(adjusted_price, versioned_constants.min_gas_price.0))
108+
GasPrice(max(adjusted_price, min_gas_price.0))
75109
}

0 commit comments

Comments
 (0)