use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone, serde::Serialize)]
pub struct FinalityConfig {
pub chain: String,
pub safe_confirmations: u64,
pub finalized_confirmations: u64,
pub block_time: Duration,
pub reorg_window: usize,
pub allows_slot_skipping: bool,
pub has_sequencer: bool,
pub settlement_chain: Option<String>,
}
impl FinalityConfig {
pub fn safe_time(&self) -> Duration {
self.block_time * self.safe_confirmations as u32
}
pub fn finalized_time(&self) -> Duration {
self.block_time * self.finalized_confirmations as u32
}
}
pub struct FinalityRegistry {
configs: HashMap<String, FinalityConfig>,
}
impl FinalityRegistry {
pub fn new() -> Self {
let mut configs = HashMap::new();
configs.insert(
"ethereum".into(),
FinalityConfig {
chain: "ethereum".into(),
safe_confirmations: 32, finalized_confirmations: 64, block_time: Duration::from_secs(12),
reorg_window: 128,
allows_slot_skipping: false,
has_sequencer: false,
settlement_chain: None,
},
);
configs.insert(
"polygon".into(),
FinalityConfig {
chain: "polygon".into(),
safe_confirmations: 128,
finalized_confirmations: 256,
block_time: Duration::from_secs(2),
reorg_window: 512,
allows_slot_skipping: false,
has_sequencer: false,
settlement_chain: Some("ethereum".into()),
},
);
configs.insert(
"arbitrum".into(),
FinalityConfig {
chain: "arbitrum".into(),
safe_confirmations: 0, finalized_confirmations: 1, block_time: Duration::from_millis(250),
reorg_window: 64,
allows_slot_skipping: false,
has_sequencer: true,
settlement_chain: Some("ethereum".into()),
},
);
configs.insert(
"optimism".into(),
FinalityConfig {
chain: "optimism".into(),
safe_confirmations: 0,
finalized_confirmations: 1,
block_time: Duration::from_secs(2),
reorg_window: 64,
allows_slot_skipping: false,
has_sequencer: true,
settlement_chain: Some("ethereum".into()),
},
);
configs.insert(
"base".into(),
FinalityConfig {
chain: "base".into(),
safe_confirmations: 0,
finalized_confirmations: 1,
block_time: Duration::from_secs(2),
reorg_window: 64,
allows_slot_skipping: false,
has_sequencer: true,
settlement_chain: Some("ethereum".into()),
},
);
configs.insert(
"bsc".into(),
FinalityConfig {
chain: "bsc".into(),
safe_confirmations: 15,
finalized_confirmations: 15,
block_time: Duration::from_secs(3),
reorg_window: 64,
allows_slot_skipping: false,
has_sequencer: false,
settlement_chain: None,
},
);
configs.insert(
"avalanche".into(),
FinalityConfig {
chain: "avalanche".into(),
safe_confirmations: 1, finalized_confirmations: 1,
block_time: Duration::from_secs(2),
reorg_window: 32,
allows_slot_skipping: false,
has_sequencer: false,
settlement_chain: None,
},
);
configs.insert(
"solana".into(),
FinalityConfig {
chain: "solana".into(),
safe_confirmations: 1, finalized_confirmations: 32, block_time: Duration::from_millis(400),
reorg_window: 256,
allows_slot_skipping: true,
has_sequencer: false,
settlement_chain: None,
},
);
configs.insert(
"fantom".into(),
FinalityConfig {
chain: "fantom".into(),
safe_confirmations: 1,
finalized_confirmations: 1,
block_time: Duration::from_secs(1),
reorg_window: 32,
allows_slot_skipping: false,
has_sequencer: false,
settlement_chain: None,
},
);
configs.insert(
"scroll".into(),
FinalityConfig {
chain: "scroll".into(),
safe_confirmations: 0,
finalized_confirmations: 1,
block_time: Duration::from_secs(3),
reorg_window: 64,
allows_slot_skipping: false,
has_sequencer: true,
settlement_chain: Some("ethereum".into()),
},
);
configs.insert(
"zksync".into(),
FinalityConfig {
chain: "zksync".into(),
safe_confirmations: 0,
finalized_confirmations: 1,
block_time: Duration::from_secs(1),
reorg_window: 64,
allows_slot_skipping: false,
has_sequencer: true,
settlement_chain: Some("ethereum".into()),
},
);
configs.insert(
"linea".into(),
FinalityConfig {
chain: "linea".into(),
safe_confirmations: 0,
finalized_confirmations: 1,
block_time: Duration::from_secs(12),
reorg_window: 64,
allows_slot_skipping: false,
has_sequencer: true,
settlement_chain: Some("ethereum".into()),
},
);
configs.insert(
"cosmoshub".into(),
FinalityConfig {
chain: "cosmoshub".into(),
safe_confirmations: 1, finalized_confirmations: 1,
block_time: Duration::from_secs(6),
reorg_window: 32,
allows_slot_skipping: false,
has_sequencer: false,
settlement_chain: None,
},
);
configs.insert(
"osmosis".into(),
FinalityConfig {
chain: "osmosis".into(),
safe_confirmations: 1,
finalized_confirmations: 1,
block_time: Duration::from_secs(6),
reorg_window: 32,
allows_slot_skipping: false,
has_sequencer: false,
settlement_chain: None,
},
);
configs.insert(
"polkadot".into(),
FinalityConfig {
chain: "polkadot".into(),
safe_confirmations: 1, finalized_confirmations: 1,
block_time: Duration::from_secs(6),
reorg_window: 32,
allows_slot_skipping: true, has_sequencer: false,
settlement_chain: None,
},
);
configs.insert(
"kusama".into(),
FinalityConfig {
chain: "kusama".into(),
safe_confirmations: 1,
finalized_confirmations: 1,
block_time: Duration::from_secs(6),
reorg_window: 32,
allows_slot_skipping: true,
has_sequencer: false,
settlement_chain: None,
},
);
configs.insert(
"bitcoin".into(),
FinalityConfig {
chain: "bitcoin".into(),
safe_confirmations: 3,
finalized_confirmations: 6, block_time: Duration::from_secs(600), reorg_window: 12,
allows_slot_skipping: false,
has_sequencer: false,
settlement_chain: None,
},
);
configs.insert(
"aptos".into(),
FinalityConfig {
chain: "aptos".into(),
safe_confirmations: 1, finalized_confirmations: 1,
block_time: Duration::from_secs(4),
reorg_window: 32,
allows_slot_skipping: false,
has_sequencer: false,
settlement_chain: None,
},
);
configs.insert(
"sui".into(),
FinalityConfig {
chain: "sui".into(),
safe_confirmations: 1, finalized_confirmations: 1,
block_time: Duration::from_millis(500), reorg_window: 64,
allows_slot_skipping: false,
has_sequencer: false,
settlement_chain: None,
},
);
Self { configs }
}
pub fn get(&self, chain: &str) -> Option<&FinalityConfig> {
self.configs.get(chain)
}
pub fn register(&mut self, config: FinalityConfig) {
self.configs.insert(config.chain.clone(), config);
}
pub fn chains(&self) -> Vec<&str> {
self.configs.keys().map(|s| s.as_str()).collect()
}
pub fn confirmation_depth(&self, chain: &str) -> u64 {
self.configs
.get(chain)
.map(|c| c.finalized_confirmations)
.unwrap_or(12)
}
pub fn reorg_window(&self, chain: &str) -> usize {
self.configs
.get(chain)
.map(|c| c.reorg_window)
.unwrap_or(128)
}
pub fn is_l2(&self, chain: &str) -> bool {
self.configs
.get(chain)
.map(|c| c.settlement_chain.is_some())
.unwrap_or(false)
}
}
impl Default for FinalityRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_chains() {
let reg = FinalityRegistry::new();
let eth = reg.get("ethereum").expect("ethereum must exist");
assert_eq!(eth.safe_confirmations, 32);
assert_eq!(eth.finalized_confirmations, 64);
assert_eq!(eth.block_time, Duration::from_secs(12));
assert_eq!(eth.reorg_window, 128);
assert!(!eth.allows_slot_skipping);
assert!(!eth.has_sequencer);
assert!(eth.settlement_chain.is_none());
let poly = reg.get("polygon").expect("polygon must exist");
assert_eq!(poly.safe_confirmations, 128);
assert_eq!(poly.finalized_confirmations, 256);
assert_eq!(poly.block_time, Duration::from_secs(2));
assert_eq!(poly.settlement_chain.as_deref(), Some("ethereum"));
let arb = reg.get("arbitrum").expect("arbitrum must exist");
assert_eq!(arb.safe_confirmations, 0);
assert_eq!(arb.finalized_confirmations, 1);
assert!(arb.has_sequencer);
assert_eq!(arb.settlement_chain.as_deref(), Some("ethereum"));
let sol = reg.get("solana").expect("solana must exist");
assert_eq!(sol.safe_confirmations, 1);
assert_eq!(sol.finalized_confirmations, 32);
assert!(sol.allows_slot_skipping);
assert_eq!(sol.block_time, Duration::from_millis(400));
}
#[test]
fn confirmation_depth_known() {
let reg = FinalityRegistry::new();
assert_eq!(reg.confirmation_depth("ethereum"), 64);
}
#[test]
fn confirmation_depth_unknown() {
let reg = FinalityRegistry::new();
assert_eq!(reg.confirmation_depth("unknown_chain"), 12);
}
#[test]
fn custom_chain() {
let mut reg = FinalityRegistry::new();
reg.register(FinalityConfig {
chain: "mychain".into(),
safe_confirmations: 5,
finalized_confirmations: 10,
block_time: Duration::from_secs(6),
reorg_window: 50,
allows_slot_skipping: false,
has_sequencer: false,
settlement_chain: None,
});
let cfg = reg.get("mychain").expect("custom chain must exist");
assert_eq!(cfg.safe_confirmations, 5);
assert_eq!(cfg.finalized_confirmations, 10);
assert_eq!(cfg.block_time, Duration::from_secs(6));
assert_eq!(cfg.reorg_window, 50);
}
#[test]
fn is_l2() {
let reg = FinalityRegistry::new();
assert!(reg.is_l2("arbitrum"));
assert!(reg.is_l2("optimism"));
assert!(reg.is_l2("base"));
assert!(!reg.is_l2("ethereum"));
assert!(!reg.is_l2("solana"));
assert!(!reg.is_l2("unknown"));
}
#[test]
fn safe_time_calculation() {
let reg = FinalityRegistry::new();
let eth = reg.get("ethereum").unwrap();
assert_eq!(eth.safe_time(), Duration::from_secs(32 * 12));
assert_eq!(eth.finalized_time(), Duration::from_secs(64 * 12));
let arb = reg.get("arbitrum").unwrap();
assert_eq!(arb.safe_time(), Duration::ZERO);
let sol = reg.get("solana").unwrap();
assert_eq!(sol.safe_time(), Duration::from_millis(400));
}
}