commonware_broadcast/linked/signer/
tip_manager.rs

1use crate::linked::parsed;
2use commonware_cryptography::Scheme;
3use commonware_utils::Array;
4use std::collections::{hash_map::Entry, HashMap};
5
6/// Manages the highest-height chunk for each sequencer.
7#[derive(Default, Debug)]
8pub struct TipManager<C: Scheme, D: Array> {
9    // The highest-height chunk for each sequencer.
10    // The chunk must have the threshold signature of its parent.
11    // Existence of the chunk implies:
12    // - The existence of the sequencer's entire chunk chain (from height zero)
13    // - That the chunk has been acked by this signer.
14    tips: HashMap<C::PublicKey, parsed::Node<C, D>>,
15}
16
17impl<C: Scheme, D: Array> TipManager<C, D> {
18    /// Creates a new `TipManager`.
19    pub fn new() -> Self {
20        Self {
21            tips: HashMap::new(),
22        }
23    }
24
25    /// Inserts a new tip. Returns true if the tip is new.
26    /// Panics if the new tip is lower-height than the existing tip.
27    pub fn put(&mut self, node: &parsed::Node<C, D>) -> bool {
28        match self.tips.entry(node.chunk.sequencer.clone()) {
29            Entry::Vacant(e) => {
30                e.insert(node.clone());
31                true
32            }
33            Entry::Occupied(mut e) => {
34                let old = e.get();
35                if old.chunk.height > node.chunk.height {
36                    panic!("Attempted to insert a lower-height tip");
37                }
38                if old.chunk.height == node.chunk.height {
39                    assert!(
40                        old.chunk.payload == node.chunk.payload,
41                        "New tip has the same height but a different payload"
42                    );
43                    return false;
44                }
45                e.insert(node.clone());
46                true
47            }
48        }
49    }
50
51    /// Returns the tip for the given sequencer.
52    pub fn get(&self, sequencer: &C::PublicKey) -> Option<parsed::Node<C, D>> {
53        self.tips.get(sequencer).cloned()
54    }
55
56    /// Returns the number of tips in the manager.
57    pub fn len(&self) -> usize {
58        self.tips.len()
59    }
60
61    /// Returns true if the manager is empty.
62    pub fn is_empty(&self) -> bool {
63        self.tips.is_empty()
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::linked::parsed;
71    use bytes::Bytes;
72    use commonware_cryptography::{
73        ed25519::{self, Ed25519, PublicKey, Signature},
74        sha256::{self, Digest},
75    };
76    use commonware_utils::Array;
77    use commonware_utils::SizedSerialize;
78    use rand::SeedableRng;
79
80    /// Helper functions for TipManager tests.
81    mod helpers {
82        use super::*;
83
84        /// Creates a dummy link for testing.
85        pub fn create_dummy_node(
86            sequencer: PublicKey,
87            height: u64,
88            payload: &str,
89        ) -> parsed::Node<Ed25519, Digest> {
90            let signature = {
91                let mut data = Bytes::from(vec![3u8; Signature::SERIALIZED_LEN]);
92                Signature::read_from(&mut data).unwrap()
93            };
94            parsed::Node::<Ed25519, Digest> {
95                chunk: parsed::Chunk {
96                    sequencer,
97                    height,
98                    payload: sha256::hash(payload.as_bytes()),
99                },
100                signature,
101                parent: None,
102            }
103        }
104
105        /// Generates a deterministic public key for testing using the provided seed.
106        pub fn deterministic_public_key(seed: u64) -> ed25519::PublicKey {
107            let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
108            ed25519::Ed25519::new(&mut rng).public_key()
109        }
110
111        /// Inserts a tip into the given TipManager and returns the inserted node.
112        pub fn insert_tip(
113            manager: &mut TipManager<Ed25519, Digest>,
114            key: ed25519::PublicKey,
115            height: u64,
116            payload: &str,
117        ) -> parsed::Node<Ed25519, Digest> {
118            let node = create_dummy_node(key.clone(), height, payload);
119            manager.put(&node);
120            node
121        }
122    }
123
124    /// Different payloads for the same sequencer and height produce distinct thresholds.
125    #[test]
126    fn test_put_new_tip() {
127        let mut manager = TipManager::<Ed25519, Digest>::new();
128        let key = helpers::deterministic_public_key(1);
129        let node = helpers::create_dummy_node(key.clone(), 1, "payload");
130        assert!(manager.put(&node));
131        let got = manager.get(&key).unwrap();
132        assert_eq!(got.chunk, node.chunk);
133        assert_eq!(got.signature, node.signature);
134        assert_eq!(got.parent, node.parent);
135    }
136
137    /// Inserting a tip with the same height and payload returns false.
138    #[test]
139    fn test_put_same_height_same_payload() {
140        let mut manager = TipManager::<Ed25519, Digest>::new();
141        let key = helpers::deterministic_public_key(2);
142        let node = helpers::create_dummy_node(key.clone(), 1, "payload");
143        assert!(manager.put(&node));
144        assert!(!manager.put(&node));
145        let got = manager.get(&key).unwrap();
146        assert_eq!(got.chunk, node.chunk);
147        assert_eq!(got.signature, node.signature);
148        assert_eq!(got.parent, node.parent);
149    }
150
151    /// Inserting a tip with a higher height updates the stored tip.
152    #[test]
153    fn test_put_higher_tip() {
154        let mut manager = TipManager::<Ed25519, Digest>::new();
155        let key = helpers::deterministic_public_key(3);
156        let node1 = helpers::create_dummy_node(key.clone(), 1, "payload1");
157        assert!(manager.put(&node1));
158        let node2 = helpers::create_dummy_node(key.clone(), 2, "payload2");
159        assert!(manager.put(&node2));
160        let got = manager.get(&key).unwrap();
161        assert_eq!(got.chunk, node2.chunk);
162        assert_eq!(got.signature, node2.signature);
163        assert_eq!(got.parent, node2.parent);
164    }
165
166    /// Inserting a tip with a lower height panics.
167    #[test]
168    #[should_panic(expected = "Attempted to insert a lower-height tip")]
169    fn test_put_lower_tip_panics() {
170        let mut manager = TipManager::<Ed25519, Digest>::new();
171        let key = helpers::deterministic_public_key(4);
172        let node1 = helpers::create_dummy_node(key.clone(), 2, "payload");
173        assert!(manager.put(&node1));
174        let node2 = helpers::create_dummy_node(key.clone(), 1, "payload");
175        manager.put(&node2);
176    }
177
178    /// Inserting a tip with the same height but different payload panics.
179    #[test]
180    #[should_panic]
181    fn test_put_same_height_different_payload_panics() {
182        let mut manager = TipManager::<Ed25519, Digest>::new();
183        let key = helpers::deterministic_public_key(5);
184        let node1 = helpers::create_dummy_node(key.clone(), 1, "payload1");
185        assert!(manager.put(&node1));
186        let node2 = helpers::create_dummy_node(key.clone(), 1, "payload2");
187        manager.put(&node2);
188    }
189
190    /// Getting a tip for a nonexistent sequencer returns None.
191    #[test]
192    fn test_get_nonexistent() {
193        let manager = TipManager::<Ed25519, Digest>::new();
194        let key = helpers::deterministic_public_key(6);
195        assert!(manager.get(&key).is_none());
196    }
197
198    /// Multiple sequencers are handled independently.
199    #[test]
200    fn test_multiple_sequencers() {
201        let mut manager = TipManager::<Ed25519, Digest>::new();
202        let key1 = helpers::deterministic_public_key(10);
203        let key2 = helpers::deterministic_public_key(20);
204        let node1 = helpers::insert_tip(&mut manager, key1.clone(), 1, "payload1");
205        let node2 = helpers::insert_tip(&mut manager, key2.clone(), 2, "payload2");
206
207        let got1 = manager.get(&key1).unwrap();
208        let got2 = manager.get(&key2).unwrap();
209        assert_eq!(got1.chunk, node1.chunk);
210        assert_eq!(got2.chunk, node2.chunk);
211    }
212
213    /// Multiple updates for the same sequencer yield the tip with the highest height.
214    #[test]
215    fn test_put_multiple_updates() {
216        let mut manager = TipManager::<Ed25519, Digest>::new();
217        let key = helpers::deterministic_public_key(7);
218
219        // Insert tip with height 1.
220        let node1 = helpers::insert_tip(&mut manager, key.clone(), 1, "payload1");
221        let got1 = manager.get(&key).unwrap();
222        assert_eq!(got1.chunk.height, 1);
223        assert_eq!(got1.chunk.payload, node1.chunk.payload);
224
225        // Insert tip with height 2.
226        let node2 = helpers::insert_tip(&mut manager, key.clone(), 2, "payload2");
227        let got2 = manager.get(&key).unwrap();
228        assert_eq!(got2.chunk.height, 2);
229        assert_eq!(got2.chunk.payload, node2.chunk.payload);
230
231        // Insert tip with height 3.
232        let node3 = helpers::insert_tip(&mut manager, key.clone(), 3, "payload3");
233        let got3 = manager.get(&key).unwrap();
234        assert_eq!(got3.chunk.height, 3);
235        assert_eq!(got3.chunk.payload, node3.chunk.payload);
236
237        // Re-inserting the same tip should return false.
238        assert!(!manager.put(&node3));
239
240        // Insert tip with height 4.
241        let node4 = helpers::insert_tip(&mut manager, key.clone(), 4, "payload4");
242        let got4 = manager.get(&key).unwrap();
243        assert_eq!(got4.chunk.height, 4);
244        assert_eq!(got4.chunk.payload, node4.chunk.payload);
245    }
246}