reth_dns_discovery/
sync.rs

1use crate::tree::{LinkEntry, TreeRootEntry};
2use enr::EnrKeyUnambiguous;
3use linked_hash_set::LinkedHashSet;
4use secp256k1::SecretKey;
5use std::{
6    collections::HashMap,
7    time::{Duration, Instant},
8};
9
10/// A sync-able tree
11pub(crate) struct SyncTree<K: EnrKeyUnambiguous = SecretKey> {
12    /// Root of the tree
13    root: TreeRootEntry,
14    /// Link to this tree
15    link: LinkEntry<K>,
16    /// Timestamp when the root was updated
17    root_updated: Instant,
18    /// The state of the tree sync progress.
19    sync_state: SyncState,
20    /// Links contained in this tree
21    resolved_links: HashMap<String, LinkEntry<K>>,
22    /// Unresolved links of the tree
23    unresolved_links: LinkedHashSet<String>,
24    /// Unresolved nodes of the tree
25    unresolved_nodes: LinkedHashSet<String>,
26}
27
28// === impl SyncTree ===
29
30impl<K: EnrKeyUnambiguous> SyncTree<K> {
31    pub(crate) fn new(root: TreeRootEntry, link: LinkEntry<K>) -> Self {
32        Self {
33            root,
34            link,
35            root_updated: Instant::now(),
36            sync_state: SyncState::Pending,
37            resolved_links: Default::default(),
38            unresolved_links: Default::default(),
39            unresolved_nodes: Default::default(),
40        }
41    }
42
43    #[cfg(test)]
44    pub(crate) const fn root(&self) -> &TreeRootEntry {
45        &self.root
46    }
47
48    pub(crate) const fn link(&self) -> &LinkEntry<K> {
49        &self.link
50    }
51
52    pub(crate) const fn resolved_links_mut(&mut self) -> &mut HashMap<String, LinkEntry<K>> {
53        &mut self.resolved_links
54    }
55
56    pub(crate) fn extend_children(
57        &mut self,
58        kind: ResolveKind,
59        children: impl IntoIterator<Item = String>,
60    ) {
61        match kind {
62            ResolveKind::Enr => {
63                self.unresolved_nodes.extend(children);
64            }
65            ResolveKind::Link => {
66                self.unresolved_links.extend(children);
67            }
68        }
69    }
70
71    /// Advances the state of the tree by returning actions to perform
72    pub(crate) fn poll(&mut self, now: Instant, update_timeout: Duration) -> Option<SyncAction> {
73        match self.sync_state {
74            SyncState::Pending => {
75                self.sync_state = SyncState::Enr;
76                return Some(SyncAction::Link(self.root.link_root.clone()))
77            }
78            SyncState::Enr => {
79                self.sync_state = SyncState::Active;
80                return Some(SyncAction::Enr(self.root.enr_root.clone()))
81            }
82            SyncState::Link => {
83                self.sync_state = SyncState::Active;
84                return Some(SyncAction::Link(self.root.link_root.clone()))
85            }
86            SyncState::Active => {
87                if now > self.root_updated + update_timeout {
88                    self.sync_state = SyncState::RootUpdate;
89                    return Some(SyncAction::UpdateRoot)
90                }
91            }
92            SyncState::RootUpdate => return None,
93        }
94
95        if let Some(link) = self.unresolved_links.pop_front() {
96            return Some(SyncAction::Link(link))
97        }
98
99        let enr = self.unresolved_nodes.pop_front()?;
100        Some(SyncAction::Enr(enr))
101    }
102
103    /// Updates the root and returns what changed
104    pub(crate) fn update_root(&mut self, root: TreeRootEntry) {
105        let enr_unchanged = root.enr_root == self.root.enr_root;
106        let link_unchanged = root.link_root == self.root.link_root;
107
108        self.root = root;
109        self.root_updated = Instant::now();
110
111        let state = match (enr_unchanged, link_unchanged) {
112            // both unchanged — no resync needed
113            (true, true) => return,
114            // only ENR changed
115            (false, true) => {
116                self.unresolved_nodes.clear();
117                SyncState::Enr
118            }
119            // only LINK changed
120            (true, false) => {
121                self.unresolved_links.clear();
122                SyncState::Link
123            }
124            // both changed
125            (false, false) => {
126                self.unresolved_nodes.clear();
127                self.unresolved_links.clear();
128                SyncState::Pending
129            }
130        };
131        self.sync_state = state;
132    }
133}
134
135/// The action to perform by the service
136#[derive(Debug)]
137pub(crate) enum SyncAction {
138    UpdateRoot,
139    Enr(String),
140    Link(String),
141}
142
143/// How the [`SyncTree::update_root`] changed the root
144enum SyncState {
145    RootUpdate,
146    Pending,
147    Enr,
148    Link,
149    Active,
150}
151
152/// What kind of hash to resolve
153pub(crate) enum ResolveKind {
154    Enr,
155    Link,
156}
157
158// === impl ResolveKind ===
159
160impl ResolveKind {
161    pub(crate) const fn is_link(&self) -> bool {
162        matches!(self, Self::Link)
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use enr::EnrKey;
170    use secp256k1::rand::thread_rng;
171
172    fn base_root() -> TreeRootEntry {
173        // taken from existing tests to ensure valid formatting
174        let s = "enrtree-root:v1 e=QFT4PBCRX4XQCV3VUYJ6BTCEPU l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=3 sig=3FmXuVwpa8Y7OstZTx9PIb1mt8FrW7VpDOFv4AaGCsZ2EIHmhraWhe4NxYhQDlw5MjeFXYMbJjsPeKlHzmJREQE";
175        s.parse::<TreeRootEntry>().unwrap()
176    }
177
178    fn make_tree() -> SyncTree {
179        let secret_key = SecretKey::new(&mut thread_rng());
180        let link =
181            LinkEntry { domain: "nodes.example.org".to_string(), pubkey: secret_key.public() };
182        SyncTree::new(base_root(), link)
183    }
184
185    fn advance_to_active(tree: &mut SyncTree) {
186        // Move Pending -> (emit Link) -> Enr, then Enr -> (emit Enr) -> Active
187        let now = Instant::now();
188        let timeout = Duration::from_secs(60 * 60 * 24);
189        let _ = tree.poll(now, timeout);
190        let _ = tree.poll(now, timeout);
191    }
192
193    #[test]
194    fn update_root_unchanged_no_action_from_active() {
195        let mut tree = make_tree();
196        let now = Instant::now();
197        let timeout = Duration::from_secs(60 * 60 * 24);
198        advance_to_active(&mut tree);
199
200        // same root -> no resync
201        let same = base_root();
202        tree.update_root(same);
203        assert!(tree.poll(now, timeout).is_none());
204    }
205
206    #[test]
207    fn update_root_only_enr_changed_triggers_enr() {
208        let mut tree = make_tree();
209        advance_to_active(&mut tree);
210        let mut new_root = base_root();
211        new_root.enr_root = "NEW_ENR_ROOT".to_string();
212        let now = Instant::now();
213        let timeout = Duration::from_secs(60 * 60 * 24);
214
215        tree.update_root(new_root.clone());
216        match tree.poll(now, timeout) {
217            Some(SyncAction::Enr(hash)) => assert_eq!(hash, new_root.enr_root),
218            other => panic!("expected Enr action, got {:?}", other),
219        }
220    }
221
222    #[test]
223    fn update_root_only_link_changed_triggers_link() {
224        let mut tree = make_tree();
225        advance_to_active(&mut tree);
226        let mut new_root = base_root();
227        new_root.link_root = "NEW_LINK_ROOT".to_string();
228        let now = Instant::now();
229        let timeout = Duration::from_secs(60 * 60 * 24);
230
231        tree.update_root(new_root.clone());
232        match tree.poll(now, timeout) {
233            Some(SyncAction::Link(hash)) => assert_eq!(hash, new_root.link_root),
234            other => panic!("expected Link action, got {:?}", other),
235        }
236    }
237
238    #[test]
239    fn update_root_both_changed_triggers_link_then_enr() {
240        let mut tree = make_tree();
241        advance_to_active(&mut tree);
242        let mut new_root = base_root();
243        new_root.enr_root = "NEW_ENR_ROOT".to_string();
244        new_root.link_root = "NEW_LINK_ROOT".to_string();
245        let now = Instant::now();
246        let timeout = Duration::from_secs(60 * 60 * 24);
247
248        tree.update_root(new_root.clone());
249        match tree.poll(now, timeout) {
250            Some(SyncAction::Link(hash)) => assert_eq!(hash, new_root.link_root),
251            other => panic!("expected first Link action, got {:?}", other),
252        }
253        match tree.poll(now, timeout) {
254            Some(SyncAction::Enr(hash)) => assert_eq!(hash, new_root.enr_root),
255            other => panic!("expected second Enr action, got {:?}", other),
256        }
257    }
258}