1use crate::types::BlockSummary;
10
11#[derive(Debug, Clone)]
13pub struct ReorgEvent {
14 pub detected_at: u64,
16 pub dropped_blocks: Vec<BlockSummary>,
18 pub depth: u64,
20 pub reorg_type: ReorgType,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ReorgType {
27 ShortReorg,
29 DeepReorg,
31 RpcInconsistency,
33}
34
35impl std::fmt::Display for ReorgType {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 Self::ShortReorg => write!(f, "short reorg"),
39 Self::DeepReorg => write!(f, "deep reorg"),
40 Self::RpcInconsistency => write!(f, "RPC inconsistency"),
41 }
42 }
43}
44
45pub struct ReorgDetector {
47 last_finalized: Option<u64>,
49 _confirmation_depth: u64,
51}
52
53impl ReorgDetector {
54 pub fn new(confirmation_depth: u64) -> Self {
55 Self {
56 last_finalized: None,
57 _confirmation_depth: confirmation_depth,
58 }
59 }
60
61 pub fn check(
65 &mut self,
66 new_block: &BlockSummary,
67 previous_head: &BlockSummary,
68 window: &[BlockSummary],
69 ) -> Option<ReorgEvent> {
70 if !new_block.extends(previous_head) {
72 let (dropped, depth) = find_dropped_blocks(new_block, window);
73 let reorg_type = if depth <= 3 {
74 ReorgType::ShortReorg
75 } else {
76 ReorgType::DeepReorg
77 };
78 tracing::warn!(
79 depth,
80 at = new_block.number,
81 reorg_type = %reorg_type,
82 "Reorg detected"
83 );
84 return Some(ReorgEvent {
85 detected_at: new_block.number,
86 dropped_blocks: dropped,
87 depth,
88 reorg_type,
89 });
90 }
91
92 None
93 }
94
95 pub fn check_finalized(
99 &mut self,
100 new_finalized: u64,
101 window: &[BlockSummary],
102 ) -> Option<ReorgEvent> {
103 if let Some(last) = self.last_finalized {
104 if new_finalized < last {
105 tracing::warn!(
106 last_finalized = last,
107 new_finalized,
108 "Finalized block decreased — possible RPC inconsistency"
109 );
110 let dropped: Vec<_> = window
111 .iter()
112 .filter(|b| b.number > new_finalized)
113 .cloned()
114 .collect();
115 self.last_finalized = Some(new_finalized);
116 return Some(ReorgEvent {
117 detected_at: new_finalized,
118 dropped_blocks: dropped,
119 depth: last - new_finalized,
120 reorg_type: ReorgType::RpcInconsistency,
121 });
122 }
123 }
124 self.last_finalized = Some(new_finalized);
125 None
126 }
127}
128
129fn find_dropped_blocks(
131 new_block: &BlockSummary,
132 window: &[BlockSummary],
133) -> (Vec<BlockSummary>, u64) {
134 let mut dropped = Vec::new();
135 for block in window.iter().rev() {
136 if block.hash == new_block.parent_hash {
137 break;
139 }
140 dropped.push(block.clone());
141 }
142 let depth = dropped.len() as u64;
143 (dropped, depth)
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 fn b(num: u64, hash: &str, parent: &str) -> BlockSummary {
151 BlockSummary {
152 number: num,
153 hash: hash.into(),
154 parent_hash: parent.into(),
155 timestamp: (num * 12) as i64,
156 tx_count: 0,
157 }
158 }
159
160 #[test]
161 fn no_reorg_on_normal_chain() {
162 let mut det = ReorgDetector::new(12);
163 let head = b(100, "0xa", "0x0");
164 let new = b(101, "0xb", "0xa");
165 assert!(det.check(&new, &head, &[head.clone()]).is_none());
166 }
167
168 #[test]
169 fn detects_short_reorg() {
170 let mut det = ReorgDetector::new(12);
171 let block_99 = b(99, "0x99", "0x98");
172 let block_100 = b(100, "0xa", "0x99");
173 let _block_100b = b(100, "0xb", "0x99"); let new_101 = b(101, "0xc", "0xb");
177 let window = vec![block_99.clone(), block_100.clone()];
178
179 let result = det.check(&new_101, &block_100, &window);
180 assert!(result.is_some());
181 let event = result.unwrap();
182 assert_eq!(event.reorg_type, ReorgType::ShortReorg);
183 }
184
185 #[test]
186 fn rpc_inconsistency_detected() {
187 let mut det = ReorgDetector::new(12);
188 let window = vec![b(100, "0xa", "0x0"), b(101, "0xb", "0xa")];
189 det.check_finalized(100, &window); let result = det.check_finalized(98, &window); assert!(result.is_some());
192 assert_eq!(result.unwrap().reorg_type, ReorgType::RpcInconsistency);
193 }
194}