1use crate::discovery::lookup::IterativeLookup;
2use crate::discv5::messages::Message;
3use crate::{
4 discv5::messages::Packet,
5 types::{Node, NodeRecord},
6};
7use ethrex_common::H256;
8use lru::LruCache;
9use rand::RngCore;
10use rustc_hash::{FxHashMap, FxHashSet};
11use std::{
12 net::{IpAddr, SocketAddr},
13 num::NonZero,
14 time::{Duration, Instant},
15};
16use tracing::trace;
17
18pub const MAX_WHOAREYOU_RATE_LIMIT_ENTRIES: usize = 10_000;
20const IP_VOTE_WINDOW: Duration = Duration::from_secs(300);
22const IP_VOTE_THRESHOLD: usize = 3;
24const MESSAGE_CACHE_TIMEOUT: Duration = Duration::from_secs(2);
26const SESSION_TTL: Duration = Duration::from_secs(3600);
29
30#[derive(Debug, Clone)]
33pub struct SessionSource {
34 pub ip: IpAddr,
35 pub established_at: Instant,
36}
37
38#[derive(Debug)]
40pub struct Discv5State {
41 pub counter: u32,
43 pub pending_by_nonce: FxHashMap<[u8; 12], (Node, Message, Instant)>,
45 pub pending_challenges: FxHashMap<H256, (Vec<u8>, Instant, Vec<u8>)>,
48 pub whoareyou_rate_limit: LruCache<(IpAddr, H256), Instant>,
50 pub whoareyou_global_count: u32,
52 pub whoareyou_global_window_start: Instant,
54 pub session_ips: FxHashMap<H256, SessionSource>,
57 pub ip_votes: FxHashMap<IpAddr, FxHashSet<H256>>,
59 pub ip_vote_period_start: Option<Instant>,
61 pub first_ip_vote_round_completed: bool,
63 pub active_lookups: Vec<IterativeLookup>,
65}
66
67impl Default for Discv5State {
68 fn default() -> Self {
69 Self {
70 counter: 0,
71 pending_by_nonce: Default::default(),
72 pending_challenges: Default::default(),
73 whoareyou_rate_limit: LruCache::new(
74 NonZero::new(MAX_WHOAREYOU_RATE_LIMIT_ENTRIES)
75 .expect("MAX_WHOAREYOU_RATE_LIMIT_ENTRIES must be non-zero"),
76 ),
77 whoareyou_global_count: 0,
78 whoareyou_global_window_start: Instant::now(),
79 session_ips: Default::default(),
80 ip_votes: Default::default(),
81 ip_vote_period_start: None,
82 first_ip_vote_round_completed: false,
83 active_lookups: Vec::new(),
84 }
85 }
86}
87
88impl Discv5State {
89 pub fn next_nonce<R: RngCore>(&mut self, rng: &mut R) -> [u8; 12] {
93 let counter = self.counter;
94 self.counter = self.counter.wrapping_add(1);
95
96 let mut nonce = [0u8; 12];
97 nonce[..4].copy_from_slice(&counter.to_be_bytes());
98 rng.fill_bytes(&mut nonce[4..]);
99 nonce
100 }
101
102 pub fn cleanup_stale_entries(&mut self) -> Option<IpAddr> {
105 let now = Instant::now();
106
107 let before_messages = self.pending_by_nonce.len();
108 self.pending_by_nonce
109 .retain(|_nonce, (_node, _message, timestamp)| {
110 now.duration_since(*timestamp) < MESSAGE_CACHE_TIMEOUT
111 });
112 let removed_messages = before_messages - self.pending_by_nonce.len();
113
114 let before_challenges = self.pending_challenges.len();
115 self.pending_challenges
116 .retain(|_src_id, (_challenge_data, timestamp, _raw)| {
117 now.duration_since(*timestamp) < MESSAGE_CACHE_TIMEOUT
118 });
119 let removed_challenges = before_challenges - self.pending_challenges.len();
120
121 let before_sessions = self.session_ips.len();
122 self.session_ips
123 .retain(|_node_id, source| now.duration_since(source.established_at) < SESSION_TTL);
124 let removed_sessions = before_sessions - self.session_ips.len();
125
126 let total_removed = removed_messages + removed_challenges + removed_sessions;
127 if total_removed > 0 {
128 trace!(
129 protocol = "discv5",
130 "Cleaned up {} stale entries ({} messages, {} challenges)",
131 total_removed,
132 removed_messages,
133 removed_challenges,
134 );
135 }
136
137 if let Some(start) = self.ip_vote_period_start
138 && now.duration_since(start) >= IP_VOTE_WINDOW
139 {
140 return self.finalize_ip_vote_round();
141 }
142 None
143 }
144
145 pub fn record_ip_vote(&mut self, reported_ip: IpAddr, voter_id: H256) -> Option<IpAddr> {
148 if Self::is_private_ip(reported_ip) {
149 return None;
150 }
151
152 let now = Instant::now();
153
154 if self.ip_vote_period_start.is_none() {
155 self.ip_vote_period_start = Some(now);
156 }
157
158 self.ip_votes
159 .entry(reported_ip)
160 .or_default()
161 .insert(voter_id);
162
163 let total_votes: usize = self.ip_votes.values().map(|v| v.len()).sum();
164 let round_ended = if !self.first_ip_vote_round_completed {
165 total_votes >= IP_VOTE_THRESHOLD
166 } else {
167 self.ip_vote_period_start
168 .is_some_and(|start| now.duration_since(start) >= IP_VOTE_WINDOW)
169 };
170
171 if round_ended {
172 return self.finalize_ip_vote_round();
173 }
174 None
175 }
176
177 fn finalize_ip_vote_round(&mut self) -> Option<IpAddr> {
180 let winner = self
181 .ip_votes
182 .iter()
183 .map(|(ip, voters)| (*ip, voters.len()))
184 .max_by_key(|(_, count)| *count);
185
186 let result = winner.and_then(|(winning_ip, vote_count)| {
187 (vote_count >= IP_VOTE_THRESHOLD).then_some(winning_ip)
188 });
189
190 self.ip_votes.clear();
191 self.ip_vote_period_start = Some(Instant::now());
192 self.first_ip_vote_round_completed = true;
193
194 result
195 }
196
197 pub fn is_private_ip(ip: IpAddr) -> bool {
199 match ip {
200 IpAddr::V4(v4) => v4.is_private() || v4.is_loopback() || v4.is_link_local(),
201 IpAddr::V6(v6) => {
202 v6.is_loopback()
203 || v6.is_unspecified()
204 || (v6.segments()[0] & 0xfe00) == 0xfc00
206 || (v6.segments()[0] & 0xffc0) == 0xfe80
208 }
209 }
210 }
211}
212
213pub(crate) fn update_local_ip(
215 local_node: &mut Node,
216 local_node_record: &mut NodeRecord,
217 signer: &secp256k1::SecretKey,
218 new_ip: IpAddr,
219) {
220 let mut updated_node = local_node.clone();
221 updated_node.ip = new_ip;
222 let new_seq = local_node_record.seq + 1;
223 let Ok(mut new_record) = NodeRecord::from_node(&updated_node, new_seq, signer) else {
224 tracing::error!(%new_ip, "Failed to create new ENR for IP update");
225 return;
226 };
227 if let Some(fork_id) = local_node_record.get_fork_id().cloned()
228 && new_record.set_fork_id(fork_id, signer).is_err()
229 {
230 tracing::error!(%new_ip, "Failed to set fork_id in new ENR, aborting IP update");
231 return;
232 }
233 local_node.ip = new_ip;
234 *local_node_record = new_record;
235}
236
237#[derive(Debug, Clone)]
238pub struct Discv5Message {
239 pub(crate) from: SocketAddr,
240 pub(crate) packet: Packet,
241}
242
243impl Discv5Message {
244 pub fn from(packet: Packet, from: SocketAddr) -> Self {
245 Self { from, packet }
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use rand::{SeedableRng, rngs::StdRng};
253
254 fn make_test_state() -> Discv5State {
255 Discv5State::default()
256 }
257
258 #[tokio::test]
259 async fn test_next_nonce_counter() {
260 let mut rng = StdRng::seed_from_u64(7);
261 let mut state = make_test_state();
262
263 let n1 = state.next_nonce(&mut rng);
264 let n2 = state.next_nonce(&mut rng);
265
266 assert_eq!(&n1[..4], &[0, 0, 0, 0]);
267 assert_eq!(&n2[..4], &[0, 0, 0, 1]);
268 assert_ne!(&n1[4..], &n2[4..]);
269 }
270
271 #[tokio::test]
272 async fn test_ip_voting_returns_winning_ip() {
273 let mut state = make_test_state();
274
275 let new_ip: IpAddr = "203.0.113.50".parse().unwrap();
276 let voter1 = H256::from_low_u64_be(1);
277 let voter2 = H256::from_low_u64_be(2);
278 let voter3 = H256::from_low_u64_be(3);
279
280 assert_eq!(state.record_ip_vote(new_ip, voter1), None);
281 assert_eq!(state.record_ip_vote(new_ip, voter2), None);
282 assert_eq!(state.record_ip_vote(new_ip, voter3), Some(new_ip));
284 assert!(state.ip_votes.is_empty());
285 }
286
287 #[tokio::test]
288 async fn test_ip_voting_same_peer_votes_once() {
289 let mut state = make_test_state();
290
291 let new_ip: IpAddr = "203.0.113.50".parse().unwrap();
292 let same_voter = H256::from_low_u64_be(1);
293
294 state.record_ip_vote(new_ip, same_voter);
295 state.record_ip_vote(new_ip, same_voter);
296 state.record_ip_vote(new_ip, same_voter);
297
298 assert_eq!(state.ip_votes.get(&new_ip).map(|v| v.len()), Some(1));
299 }
300
301 #[tokio::test]
302 async fn test_ip_voting_ignores_private_ips() {
303 let mut state = make_test_state();
304
305 let voter1 = H256::from_low_u64_be(1);
306
307 let private_ip: IpAddr = "192.168.1.100".parse().unwrap();
308 state.record_ip_vote(private_ip, voter1);
309 assert!(state.ip_votes.is_empty());
310
311 let loopback: IpAddr = "127.0.0.1".parse().unwrap();
312 state.record_ip_vote(loopback, voter1);
313 assert!(state.ip_votes.is_empty());
314
315 let public_ip: IpAddr = "203.0.113.50".parse().unwrap();
316 state.record_ip_vote(public_ip, voter1);
317 assert_eq!(state.ip_votes.get(&public_ip).map(|v| v.len()), Some(1));
318 }
319
320 #[tokio::test]
321 async fn test_ip_voting_split_votes_no_winner() {
322 let mut state = make_test_state();
323
324 let ip1: IpAddr = "203.0.113.50".parse().unwrap();
325 let ip2: IpAddr = "203.0.113.51".parse().unwrap();
326 let voter1 = H256::from_low_u64_be(1);
327 let voter2 = H256::from_low_u64_be(2);
328 let voter3 = H256::from_low_u64_be(3);
329
330 state.record_ip_vote(ip1, voter1);
331 state.record_ip_vote(ip2, voter2);
332 assert_eq!(state.record_ip_vote(ip1, voter3), None);
334 assert!(state.ip_votes.is_empty());
335 assert!(state.first_ip_vote_round_completed);
336 }
337
338 #[tokio::test]
339 async fn test_ip_vote_cleanup() {
340 let mut state = make_test_state();
341
342 let ip: IpAddr = "203.0.113.50".parse().unwrap();
343 let voter1 = H256::from_low_u64_be(1);
344
345 let mut voters = FxHashSet::default();
346 voters.insert(voter1);
347 state.ip_votes.insert(ip, voters);
348 state.ip_vote_period_start = Some(Instant::now());
349 assert_eq!(state.ip_votes.len(), 1);
350
351 assert_eq!(state.cleanup_stale_entries(), None);
353 assert_eq!(state.ip_votes.len(), 1);
354 assert!(!state.first_ip_vote_round_completed);
355 }
356}