Skip to main content

chainindex_core/
finality.rs

1//! Chain-specific finality models.
2//!
3//! Different blockchains have different finality guarantees. This module
4//! provides a registry of finality parameters keyed by chain name.
5
6use std::collections::HashMap;
7use std::time::Duration;
8
9/// Finality model for a blockchain.
10#[derive(Debug, Clone, serde::Serialize)]
11pub struct FinalityConfig {
12    /// Human-readable chain name (e.g. "ethereum", "polygon", "arbitrum").
13    pub chain: String,
14    /// Number of confirmations for "safe" status.
15    pub safe_confirmations: u64,
16    /// Number of confirmations for "finalized" status.
17    pub finalized_confirmations: u64,
18    /// Expected block/slot time.
19    pub block_time: Duration,
20    /// Recommended reorg window size (blocks to keep in tracker).
21    pub reorg_window: usize,
22    /// Whether this chain can have empty/skipped slots (e.g. Solana).
23    pub allows_slot_skipping: bool,
24    /// Whether the chain has a separate sequencer (L2s).
25    pub has_sequencer: bool,
26    /// Settlement layer chain name, if this is an L2 (e.g. "ethereum" for Arbitrum).
27    pub settlement_chain: Option<String>,
28}
29
30impl FinalityConfig {
31    /// Time to finality at the "safe" level.
32    pub fn safe_time(&self) -> Duration {
33        self.block_time * self.safe_confirmations as u32
34    }
35
36    /// Time to finality at the "finalized" level.
37    pub fn finalized_time(&self) -> Duration {
38        self.block_time * self.finalized_confirmations as u32
39    }
40}
41
42/// Registry of known chain finality configurations.
43pub struct FinalityRegistry {
44    configs: HashMap<String, FinalityConfig>,
45}
46
47impl FinalityRegistry {
48    /// Create a registry pre-populated with known chains.
49    pub fn new() -> Self {
50        let mut configs = HashMap::new();
51
52        // Ethereum (PoS)
53        configs.insert(
54            "ethereum".into(),
55            FinalityConfig {
56                chain: "ethereum".into(),
57                safe_confirmations: 32,      // ~6.4 min
58                finalized_confirmations: 64, // ~12.8 min (2 epochs)
59                block_time: Duration::from_secs(12),
60                reorg_window: 128,
61                allows_slot_skipping: false,
62                has_sequencer: false,
63                settlement_chain: None,
64            },
65        );
66
67        // Polygon PoS
68        configs.insert(
69            "polygon".into(),
70            FinalityConfig {
71                chain: "polygon".into(),
72                safe_confirmations: 128,
73                finalized_confirmations: 256,
74                block_time: Duration::from_secs(2),
75                reorg_window: 512,
76                allows_slot_skipping: false,
77                has_sequencer: false,
78                settlement_chain: Some("ethereum".into()),
79            },
80        );
81
82        // Arbitrum One
83        configs.insert(
84            "arbitrum".into(),
85            FinalityConfig {
86                chain: "arbitrum".into(),
87                safe_confirmations: 0, // Sequencer provides instant soft finality
88                finalized_confirmations: 1, // After batch posted to L1
89                block_time: Duration::from_millis(250),
90                reorg_window: 64,
91                allows_slot_skipping: false,
92                has_sequencer: true,
93                settlement_chain: Some("ethereum".into()),
94            },
95        );
96
97        // Optimism / Base
98        configs.insert(
99            "optimism".into(),
100            FinalityConfig {
101                chain: "optimism".into(),
102                safe_confirmations: 0,
103                finalized_confirmations: 1,
104                block_time: Duration::from_secs(2),
105                reorg_window: 64,
106                allows_slot_skipping: false,
107                has_sequencer: true,
108                settlement_chain: Some("ethereum".into()),
109            },
110        );
111
112        configs.insert(
113            "base".into(),
114            FinalityConfig {
115                chain: "base".into(),
116                safe_confirmations: 0,
117                finalized_confirmations: 1,
118                block_time: Duration::from_secs(2),
119                reorg_window: 64,
120                allows_slot_skipping: false,
121                has_sequencer: true,
122                settlement_chain: Some("ethereum".into()),
123            },
124        );
125
126        // BSC (BNB Smart Chain)
127        configs.insert(
128            "bsc".into(),
129            FinalityConfig {
130                chain: "bsc".into(),
131                safe_confirmations: 15,
132                finalized_confirmations: 15,
133                block_time: Duration::from_secs(3),
134                reorg_window: 64,
135                allows_slot_skipping: false,
136                has_sequencer: false,
137                settlement_chain: None,
138            },
139        );
140
141        // Avalanche C-Chain
142        configs.insert(
143            "avalanche".into(),
144            FinalityConfig {
145                chain: "avalanche".into(),
146                safe_confirmations: 1, // Avalanche has instant finality
147                finalized_confirmations: 1,
148                block_time: Duration::from_secs(2),
149                reorg_window: 32,
150                allows_slot_skipping: false,
151                has_sequencer: false,
152                settlement_chain: None,
153            },
154        );
155
156        // Solana
157        configs.insert(
158            "solana".into(),
159            FinalityConfig {
160                chain: "solana".into(),
161                safe_confirmations: 1,       // "confirmed" = 66% voted
162                finalized_confirmations: 32, // "finalized" = 31+ confirmations
163                block_time: Duration::from_millis(400),
164                reorg_window: 256,
165                allows_slot_skipping: true,
166                has_sequencer: false,
167                settlement_chain: None,
168            },
169        );
170
171        // Fantom
172        configs.insert(
173            "fantom".into(),
174            FinalityConfig {
175                chain: "fantom".into(),
176                safe_confirmations: 1,
177                finalized_confirmations: 1,
178                block_time: Duration::from_secs(1),
179                reorg_window: 32,
180                allows_slot_skipping: false,
181                has_sequencer: false,
182                settlement_chain: None,
183            },
184        );
185
186        // Scroll
187        configs.insert(
188            "scroll".into(),
189            FinalityConfig {
190                chain: "scroll".into(),
191                safe_confirmations: 0,
192                finalized_confirmations: 1,
193                block_time: Duration::from_secs(3),
194                reorg_window: 64,
195                allows_slot_skipping: false,
196                has_sequencer: true,
197                settlement_chain: Some("ethereum".into()),
198            },
199        );
200
201        // zkSync Era
202        configs.insert(
203            "zksync".into(),
204            FinalityConfig {
205                chain: "zksync".into(),
206                safe_confirmations: 0,
207                finalized_confirmations: 1,
208                block_time: Duration::from_secs(1),
209                reorg_window: 64,
210                allows_slot_skipping: false,
211                has_sequencer: true,
212                settlement_chain: Some("ethereum".into()),
213            },
214        );
215
216        // Linea
217        configs.insert(
218            "linea".into(),
219            FinalityConfig {
220                chain: "linea".into(),
221                safe_confirmations: 0,
222                finalized_confirmations: 1,
223                block_time: Duration::from_secs(12),
224                reorg_window: 64,
225                allows_slot_skipping: false,
226                has_sequencer: true,
227                settlement_chain: Some("ethereum".into()),
228            },
229        );
230
231        // Cosmos Hub
232        configs.insert(
233            "cosmoshub".into(),
234            FinalityConfig {
235                chain: "cosmoshub".into(),
236                safe_confirmations: 1, // BFT instant finality
237                finalized_confirmations: 1,
238                block_time: Duration::from_secs(6),
239                reorg_window: 32,
240                allows_slot_skipping: false,
241                has_sequencer: false,
242                settlement_chain: None,
243            },
244        );
245
246        // Osmosis
247        configs.insert(
248            "osmosis".into(),
249            FinalityConfig {
250                chain: "osmosis".into(),
251                safe_confirmations: 1,
252                finalized_confirmations: 1,
253                block_time: Duration::from_secs(6),
254                reorg_window: 32,
255                allows_slot_skipping: false,
256                has_sequencer: false,
257                settlement_chain: None,
258            },
259        );
260
261        // Polkadot
262        configs.insert(
263            "polkadot".into(),
264            FinalityConfig {
265                chain: "polkadot".into(),
266                safe_confirmations: 1, // GRANDPA finality
267                finalized_confirmations: 1,
268                block_time: Duration::from_secs(6),
269                reorg_window: 32,
270                allows_slot_skipping: true, // BABE can skip slots
271                has_sequencer: false,
272                settlement_chain: None,
273            },
274        );
275
276        // Kusama
277        configs.insert(
278            "kusama".into(),
279            FinalityConfig {
280                chain: "kusama".into(),
281                safe_confirmations: 1,
282                finalized_confirmations: 1,
283                block_time: Duration::from_secs(6),
284                reorg_window: 32,
285                allows_slot_skipping: true,
286                has_sequencer: false,
287                settlement_chain: None,
288            },
289        );
290
291        // Bitcoin
292        configs.insert(
293            "bitcoin".into(),
294            FinalityConfig {
295                chain: "bitcoin".into(),
296                safe_confirmations: 3,
297                finalized_confirmations: 6, // 6 confirmations standard
298                block_time: Duration::from_secs(600), // ~10 minutes
299                reorg_window: 12,
300                allows_slot_skipping: false,
301                has_sequencer: false,
302                settlement_chain: None,
303            },
304        );
305
306        // Aptos
307        configs.insert(
308            "aptos".into(),
309            FinalityConfig {
310                chain: "aptos".into(),
311                safe_confirmations: 1, // BFT instant finality
312                finalized_confirmations: 1,
313                block_time: Duration::from_secs(4),
314                reorg_window: 32,
315                allows_slot_skipping: false,
316                has_sequencer: false,
317                settlement_chain: None,
318            },
319        );
320
321        // Sui
322        configs.insert(
323            "sui".into(),
324            FinalityConfig {
325                chain: "sui".into(),
326                safe_confirmations: 1, // BFT instant finality
327                finalized_confirmations: 1,
328                block_time: Duration::from_millis(500), // ~500ms checkpoint time
329                reorg_window: 64,
330                allows_slot_skipping: false,
331                has_sequencer: false,
332                settlement_chain: None,
333            },
334        );
335
336        Self { configs }
337    }
338
339    /// Look up finality config by chain name.
340    pub fn get(&self, chain: &str) -> Option<&FinalityConfig> {
341        self.configs.get(chain)
342    }
343
344    /// Register a custom chain finality config.
345    pub fn register(&mut self, config: FinalityConfig) {
346        self.configs.insert(config.chain.clone(), config);
347    }
348
349    /// Get all known chain names.
350    pub fn chains(&self) -> Vec<&str> {
351        self.configs.keys().map(|s| s.as_str()).collect()
352    }
353
354    /// Get recommended confirmation depth for a chain.
355    /// Falls back to 12 if chain is unknown.
356    pub fn confirmation_depth(&self, chain: &str) -> u64 {
357        self.configs
358            .get(chain)
359            .map(|c| c.finalized_confirmations)
360            .unwrap_or(12)
361    }
362
363    /// Get recommended reorg window size for a chain.
364    /// Falls back to 128 if chain is unknown.
365    pub fn reorg_window(&self, chain: &str) -> usize {
366        self.configs
367            .get(chain)
368            .map(|c| c.reorg_window)
369            .unwrap_or(128)
370    }
371
372    /// Check if a chain is an L2 (has a settlement layer).
373    pub fn is_l2(&self, chain: &str) -> bool {
374        self.configs
375            .get(chain)
376            .map(|c| c.settlement_chain.is_some())
377            .unwrap_or(false)
378    }
379}
380
381impl Default for FinalityRegistry {
382    fn default() -> Self {
383        Self::new()
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390
391    #[test]
392    fn known_chains() {
393        let reg = FinalityRegistry::new();
394
395        // Ethereum
396        let eth = reg.get("ethereum").expect("ethereum must exist");
397        assert_eq!(eth.safe_confirmations, 32);
398        assert_eq!(eth.finalized_confirmations, 64);
399        assert_eq!(eth.block_time, Duration::from_secs(12));
400        assert_eq!(eth.reorg_window, 128);
401        assert!(!eth.allows_slot_skipping);
402        assert!(!eth.has_sequencer);
403        assert!(eth.settlement_chain.is_none());
404
405        // Polygon
406        let poly = reg.get("polygon").expect("polygon must exist");
407        assert_eq!(poly.safe_confirmations, 128);
408        assert_eq!(poly.finalized_confirmations, 256);
409        assert_eq!(poly.block_time, Duration::from_secs(2));
410        assert_eq!(poly.settlement_chain.as_deref(), Some("ethereum"));
411
412        // Arbitrum
413        let arb = reg.get("arbitrum").expect("arbitrum must exist");
414        assert_eq!(arb.safe_confirmations, 0);
415        assert_eq!(arb.finalized_confirmations, 1);
416        assert!(arb.has_sequencer);
417        assert_eq!(arb.settlement_chain.as_deref(), Some("ethereum"));
418
419        // Solana
420        let sol = reg.get("solana").expect("solana must exist");
421        assert_eq!(sol.safe_confirmations, 1);
422        assert_eq!(sol.finalized_confirmations, 32);
423        assert!(sol.allows_slot_skipping);
424        assert_eq!(sol.block_time, Duration::from_millis(400));
425    }
426
427    #[test]
428    fn confirmation_depth_known() {
429        let reg = FinalityRegistry::new();
430        assert_eq!(reg.confirmation_depth("ethereum"), 64);
431    }
432
433    #[test]
434    fn confirmation_depth_unknown() {
435        let reg = FinalityRegistry::new();
436        assert_eq!(reg.confirmation_depth("unknown_chain"), 12);
437    }
438
439    #[test]
440    fn custom_chain() {
441        let mut reg = FinalityRegistry::new();
442        reg.register(FinalityConfig {
443            chain: "mychain".into(),
444            safe_confirmations: 5,
445            finalized_confirmations: 10,
446            block_time: Duration::from_secs(6),
447            reorg_window: 50,
448            allows_slot_skipping: false,
449            has_sequencer: false,
450            settlement_chain: None,
451        });
452        let cfg = reg.get("mychain").expect("custom chain must exist");
453        assert_eq!(cfg.safe_confirmations, 5);
454        assert_eq!(cfg.finalized_confirmations, 10);
455        assert_eq!(cfg.block_time, Duration::from_secs(6));
456        assert_eq!(cfg.reorg_window, 50);
457    }
458
459    #[test]
460    fn is_l2() {
461        let reg = FinalityRegistry::new();
462        assert!(reg.is_l2("arbitrum"));
463        assert!(reg.is_l2("optimism"));
464        assert!(reg.is_l2("base"));
465        assert!(!reg.is_l2("ethereum"));
466        assert!(!reg.is_l2("solana"));
467        assert!(!reg.is_l2("unknown"));
468    }
469
470    #[test]
471    fn safe_time_calculation() {
472        let reg = FinalityRegistry::new();
473
474        // Ethereum: 32 confirmations * 12s = 384s
475        let eth = reg.get("ethereum").unwrap();
476        assert_eq!(eth.safe_time(), Duration::from_secs(32 * 12));
477
478        // Ethereum finalized: 64 * 12s = 768s
479        assert_eq!(eth.finalized_time(), Duration::from_secs(64 * 12));
480
481        // Arbitrum safe: 0 * 250ms = 0
482        let arb = reg.get("arbitrum").unwrap();
483        assert_eq!(arb.safe_time(), Duration::ZERO);
484
485        // Solana safe: 1 * 400ms = 400ms
486        let sol = reg.get("solana").unwrap();
487        assert_eq!(sol.safe_time(), Duration::from_millis(400));
488    }
489}