agglayer_config/settlement_service.rs
1use std::time::Duration;
2
3use agglayer_primitives::U256;
4use serde::{Deserialize, Serialize};
5use serde_with::{serde_as, serde_conv};
6
7use crate::{with::HumanDuration, Multiplier};
8
9/// Policy for considering the transactions as settled on Ethereum.
10///
11/// Defines when a transaction should be considered settled on the Ethereum
12/// network, determining the security guarantees for the settlement operation.
13///
14/// This matches the `latest`, `safe`, and `finalized` block concepts from
15/// Ethereum clients, exposed over the JSON-RPC API.
16#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
17pub enum SettlementPolicy {
18 /// Transaction is considered settled immediately after the specified
19 /// number of confirmation blocks.
20 ///
21 /// **Security**: Vulnerable to chain reorganizations beyond the
22 /// confirmation count.
23 LatestBlock,
24
25 /// Transaction is considered settled when the containing block has been
26 /// considered "safe."
27 ///
28 /// **Security**: Very strong. Reversing a safe block would require
29 /// a significant portion of validators to be slashed.
30 ///
31 /// **Time**: Typically up to ~7 minutes on mainnet. Worst case scenario
32 /// 12-13 minutes.
33 #[default]
34 SafeBlock,
35
36 /// Transaction is considered settled only when the containing block has
37 /// been fully finalized.
38 ///
39 /// **Time**: Typically between 7-13 minutes on mainnet. Worst case scenario
40 /// ~19 minutes.
41 FinalizedBlock,
42}
43
44/// Transaction retry policy.
45#[serde_as]
46#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Default)]
47#[serde(rename_all = "kebab-case")]
48struct ConfigTxRetryPolicy {
49 /// Initial retry interval.
50 #[serde(default, skip_serializing_if = "crate::is_default")]
51 #[serde_as(as = "Option<HumanDuration>")]
52 initial_interval: Option<Duration>,
53
54 /// Interval multiplier for each subsequent retry.
55 #[serde(default, skip_serializing_if = "crate::is_default")]
56 interval_multiplier_factor: Option<Multiplier>,
57
58 /// Maximum interval between retries.
59 #[serde(default, skip_serializing_if = "crate::is_default")]
60 #[serde_as(as = "Option<HumanDuration>")]
61 max_interval: Option<Duration>,
62
63 /// Jitter factor to add randomness to retry intervals.
64 #[serde(default, skip_serializing_if = "crate::is_default")]
65 #[serde_as(as = "Option<HumanDuration>")]
66 jitter: Option<Duration>,
67}
68
69impl From<&TxRetryPolicy> for ConfigTxRetryPolicy {
70 fn from(value: &TxRetryPolicy) -> Self {
71 ConfigTxRetryPolicy {
72 initial_interval: Some(value.initial_interval),
73 interval_multiplier_factor: Some(value.interval_multiplier_factor),
74 max_interval: Some(value.max_interval),
75 jitter: Some(value.jitter),
76 }
77 }
78}
79
80serde_conv!(pub TransientRetryPolicy, TxRetryPolicy, TransientRetryPolicyImpl::new, TransientRetryPolicyImpl::get);
81
82#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
83struct TransientRetryPolicyImpl(ConfigTxRetryPolicy);
84
85impl TransientRetryPolicyImpl {
86 fn new(value: &TxRetryPolicy) -> Self {
87 TransientRetryPolicyImpl(ConfigTxRetryPolicy::from(value))
88 }
89
90 pub fn get(self) -> Result<TxRetryPolicy, std::convert::Infallible> {
91 Ok(TxRetryPolicy {
92 initial_interval: self.0.initial_interval.unwrap_or(Duration::from_secs(10)),
93 interval_multiplier_factor: self
94 .0
95 .interval_multiplier_factor
96 .unwrap_or(Multiplier::from_u64_per_1000(1500)),
97 max_interval: self.0.max_interval.unwrap_or(Duration::from_secs(120)),
98 jitter: self.0.jitter.unwrap_or(Duration::from_secs(1)),
99 })
100 }
101}
102
103serde_conv!(pub NonInclusionRetryPolicy, TxRetryPolicy, NonInclusionRetryPolicyImpl::new, NonInclusionRetryPolicyImpl::get);
104
105#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
106struct NonInclusionRetryPolicyImpl(ConfigTxRetryPolicy);
107
108impl NonInclusionRetryPolicyImpl {
109 fn new(value: &TxRetryPolicy) -> Self {
110 NonInclusionRetryPolicyImpl(ConfigTxRetryPolicy::from(value))
111 }
112
113 pub fn get(self) -> Result<TxRetryPolicy, std::convert::Infallible> {
114 Ok(TxRetryPolicy {
115 initial_interval: self.0.initial_interval.unwrap_or(Duration::from_secs(60)),
116 interval_multiplier_factor: self
117 .0
118 .interval_multiplier_factor
119 .unwrap_or(Multiplier::from_u64_per_1000(2000)),
120 max_interval: self.0.max_interval.unwrap_or(Duration::from_secs(600)),
121 jitter: self.0.jitter.unwrap_or(Duration::from_secs(10)),
122 })
123 }
124}
125
126/// Transaction retry policy for failed or pending settlement transactions.
127///
128/// Defines the strategy used when retrying failed or pending transactions.
129///
130/// Future versions may include additional policies such as:
131/// - **Exponential**: Exponential backoff with increasing intervals
132/// - **Jittered**: Random jitter to avoid thundering herd issues
133#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
134pub struct TxRetryPolicy {
135 /// Initial retry interval.
136 pub initial_interval: Duration,
137
138 /// Interval multiplier for each subsequent retry.
139 pub interval_multiplier_factor: Multiplier,
140
141 /// Maximum interval between retries.
142 pub max_interval: Duration,
143
144 /// Jitter factor to add randomness to retry intervals.
145 pub jitter: Duration,
146}
147
148impl TxRetryPolicy {
149 /// Default retry policy for transient failures.
150 pub fn default_on_transient_failure() -> Self {
151 TransientRetryPolicyImpl(ConfigTxRetryPolicy::default())
152 .get()
153 .unwrap()
154 }
155
156 /// Default retry policy for non-inclusion on L1.
157 pub fn default_non_inclusion_on_l1() -> Self {
158 NonInclusionRetryPolicyImpl(ConfigTxRetryPolicy::default())
159 .get()
160 .unwrap()
161 }
162}
163
164/// The settlement transaction configuration.
165#[serde_as]
166#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
167#[serde(rename_all = "kebab-case")]
168pub struct SettlementTransactionConfig {
169 /// Retry policy for the transaction when there is a transient failure.
170 #[serde(default = "TxRetryPolicy::default_on_transient_failure")]
171 #[serde_as(as = "TransientRetryPolicy")]
172 pub retry_on_transient_failure: TxRetryPolicy,
173
174 /// Retry policy for the transaction when it is not included on L1.
175 #[serde(default = "TxRetryPolicy::default_non_inclusion_on_l1")]
176 #[serde_as(as = "NonInclusionRetryPolicy")]
177 pub retry_on_not_included_on_l1: TxRetryPolicy,
178
179 /// Number of block confirmations required for
180 /// the transaction to resolve a receipt.
181 #[serde(default = "default_confirmations")]
182 pub confirmations: usize,
183
184 /// Finality level required for the transaction to be considered settled.
185 #[serde(default)]
186 pub settlement_policy: SettlementPolicy,
187
188 /// Gas limit multiplier factor for the transaction.
189 /// The gas is calculated as follows:
190 /// `gas = estimated_gas * gas_multiplier_factor`
191 #[serde(default, skip_serializing_if = "crate::is_default")]
192 pub gas_limit_multiplier_factor: Multiplier,
193
194 /// Ceiling for the gas limit for the transaction.
195 #[serde(default = "default_gas_limit_ceiling")]
196 pub gas_limit_ceiling: U256,
197
198 /// Minimum `max_fee_per_gas` (in wei) for the transaction.
199 /// Can be specified with units: "1gwei", "0.1eth", "1000000000wei".
200 /// Used for EIP1559 fee `max_fee_per_gas`.
201 #[serde(default, skip_serializing_if = "crate::is_default")]
202 #[serde_as(as = "crate::with::EthAmount")]
203 pub max_fee_per_gas_floor: u128,
204
205 /// Maximum `max_fee_per_gas` (in wei) for the transaction.
206 /// Use for EIP1559 `max_fee_per_gas`.
207 /// Can be specified with units: "100gwei", "0.01eth", "10000000000wei"
208 #[serde(default = "default_max_fee_per_gas_ceiling")]
209 #[serde_as(as = "crate::with::EthAmount")]
210 pub max_fee_per_gas_ceiling: u128,
211
212 /// Increase rate for the `max_fee_per_gas` for each retry attempt.
213 #[serde(default, skip_serializing_if = "crate::is_default")]
214 pub max_fee_per_gas_multiplier_factor: Multiplier,
215
216 /// Minimum `max_priority_fee_per_gas` (in wei) for the transaction.
217 /// Can be specified with units: "1gwei", "0.1eth", "1000000000wei".
218 /// Used for EIP1559 fee `max_priority_fee_per_gas`.
219 #[serde(default, skip_serializing_if = "crate::is_default")]
220 #[serde_as(as = "crate::with::EthAmount")]
221 pub max_priority_fee_per_gas_floor: u128,
222
223 /// Maximum `max_priority_fee_per_gas` (in wei) for the transaction.
224 /// Use for EIP1559 `max_priority_fee_per_gas`.
225 /// Can be specified with units: "100gwei", "0.01eth", "10000000000wei"
226 #[serde(default = "default_max_priority_fee_per_gas_ceiling")]
227 #[serde_as(as = "crate::with::EthAmount")]
228 pub max_priority_fee_per_gas_ceiling: u128,
229
230 /// Increase rate for the `max_priority_fee_per_gas` for each retry attempt.
231 #[serde(default, skip_serializing_if = "crate::is_default")]
232 pub max_priority_fee_per_gas_multiplier_factor: Multiplier,
233}
234
235impl Default for SettlementTransactionConfig {
236 fn default() -> Self {
237 Self {
238 retry_on_transient_failure: TransientRetryPolicyImpl(ConfigTxRetryPolicy::default())
239 .get()
240 .unwrap(),
241 retry_on_not_included_on_l1:
242 NonInclusionRetryPolicyImpl(ConfigTxRetryPolicy::default())
243 .get()
244 .unwrap(),
245 confirmations: default_confirmations(),
246 settlement_policy: SettlementPolicy::default(),
247 gas_limit_multiplier_factor: Multiplier::default(),
248 gas_limit_ceiling: default_gas_limit_ceiling(),
249 max_fee_per_gas_multiplier_factor: Multiplier::default(),
250 max_fee_per_gas_floor: 0,
251 max_fee_per_gas_ceiling: default_max_fee_per_gas_ceiling(),
252 max_priority_fee_per_gas_multiplier_factor: Multiplier::default(),
253 max_priority_fee_per_gas_floor: 0,
254 max_priority_fee_per_gas_ceiling: default_max_priority_fee_per_gas_ceiling(),
255 }
256 }
257}
258
259/// The settlement service configuration.
260///
261/// Contains service-wide configuration options for the Agglayer settlement
262/// service. This configuration is separate from transaction-specific settings
263/// and focuses on overall service behavior and integration points.
264///
265/// This structure is designed to hold settlement service-specific values that
266/// are not related to individual transactions.
267#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
268#[serde(rename_all = "kebab-case")]
269pub struct SettlementServiceConfig {
270 // TODO: introduce regexes / error numbers to identify "nonce already used" errors
271 // The exact format of this config will depend on the implementation and tests of RPC error
272 // handling in the settlement service.
273}
274
275/// The Agglayer settlement configuration.
276///
277/// This configuration controls how the Agglayer settlement service interacts
278/// with the L1 blockchain for settling certificates and validium transactions.
279/// It provides separate transaction configurations for certificate settlements
280/// and validium settlements, allowing fine-grained control over gas prices,
281/// retries, and confirmation requirements.
282///
283/// # Configuration Structure
284///
285/// The settlement configuration is organized into three main components:
286///
287/// - **Certificate Transaction Config**: Controls settlement transactions for
288/// certificates with pessimistic proofs (proofs of state transitions).
289/// - **Validium Transaction Config**: Controls settlement transactions for
290/// validium data (off-chain data availability).
291/// - **Settlement Service Config**: General settlement service configuration.
292///
293/// # Gas Price Configuration
294///
295/// Gas prices can be specified with units for readability:
296/// - `"1gwei"` = 1,000,000,000 wei
297/// - `"0.1eth"` = 100,000,000,000,000,000 wei
298/// - `"1000000000wei"` = 1,000,000,000 wei
299///
300/// The final gas price is calculated as:
301/// ```text
302/// gas_price = max(
303/// floor,
304/// min(ceiling, estimate_gas_price * gas_price_multiplier_factor)
305/// )
306/// ```
307///
308/// Each retry can increase the price by the multiplier factor.
309///
310/// # Security Considerations
311///
312/// - **Settlement policy**: Choose the appropriate finality level based on
313/// security requirements.
314/// - **Gas Price Ceiling**: Acts as a safety mechanism to prevent excessive
315/// transaction costs during network congestion.
316/// - **Confirmations**: Higher confirmation counts increase security for
317/// receipt retrieval but do not replace proper finality guarantees.
318/// - **Max Retries**: Should be set high enough to handle temporary network
319/// issues.
320#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
321#[serde(rename_all = "kebab-case")]
322pub struct SettlementConfig {
323 /// Configuration for certificate settlement transactions.
324 ///
325 /// This controls how pessimistic proofs (proofs of state transitions for
326 /// certificates) are submitted to the L1 settlement layer.
327 #[serde(default)]
328 pub pessimistic_proof_tx_config: SettlementTransactionConfig,
329
330 /// Configuration for validium settlement transactions.
331 ///
332 /// This controls how validium data (off-chain data availability proofs)
333 /// are submitted to the L1 settlement layer. Validium transactions may
334 /// have different gas and retry requirements than certificate transactions.
335 #[serde(default)]
336 pub validium_tx_config: SettlementTransactionConfig,
337
338 /// General settlement service configuration.
339 ///
340 /// Contains service-wide settings that apply to the overall settlement
341 /// service operation (beyond individual transaction parameters).
342 #[serde(default)]
343 pub settlement_service_config: SettlementServiceConfig,
344}
345
346/// Default number of confirmations required
347/// for the transaction to resolve a receipt.
348const fn default_confirmations() -> usize {
349 12
350}
351
352fn default_gas_limit_ceiling() -> U256 {
353 U256::from(60_000_000_u64)
354}
355
356/// Default gas price ceiling for the transaction.
357const fn default_max_fee_per_gas_ceiling() -> u128 {
358 // 100 gwei
359 100_000_000_000_u128
360}
361
362/// Default priority fee ceiling for the transaction.
363const fn default_max_priority_fee_per_gas_ceiling() -> u128 {
364 // 100 gwei
365 100_000_000_000_u128
366}