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
57#[cfg(test)]
58mod tests {
59    use super::*;
60    use crate::linked::parsed;
61    use bytes::Bytes;
62    use commonware_cryptography::{
63        ed25519::{self, Ed25519, PublicKey, Signature},
64        sha256::{self, Digest},
65    };
66    use commonware_utils::Array;
67    use commonware_utils::SizedSerialize;
68    use rand::SeedableRng;
69
70    /// Helper functions for TipManager tests.
71    mod helpers {
72        use super::*;
73
74        /// Creates a dummy link for testing.
75        pub fn create_dummy_node(
76            sequencer: PublicKey,
77            height: u64,
78            payload: &str,
79        ) -> parsed::Node<Ed25519, Digest> {
80            let signature = {
81                let mut data = Bytes::from(vec![3u8; Signature::SERIALIZED_LEN]);
82                Signature::read_from(&mut data).unwrap()
83            };
84            parsed::Node::<Ed25519, Digest> {
85                chunk: parsed::Chunk {
86                    sequencer,
87                    height,
88                    payload: sha256::hash(payload.as_bytes()),
89                },
90                signature,
91                parent: None,
92            }
93        }
94
95        /// Generates a deterministic public key for testing using the provided seed.
96        pub fn deterministic_public_key(seed: u64) -> ed25519::PublicKey {
97            let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
98            ed25519::Ed25519::new(&mut rng).public_key()
99        }
100
101        /// Inserts a tip into the given TipManager and returns the inserted node.
102        pub fn insert_tip(
103            manager: &mut TipManager<Ed25519, Digest>,
104            key: ed25519::PublicKey,
105            height: u64,
106            payload: &str,
107        ) -> parsed::Node<Ed25519, Digest> {
108            let node = create_dummy_node(key.clone(), height, payload);
109            manager.put(&node);
110            node
111        }
112    }
113
114    /// Different payloads for the same sequencer and height produce distinct thresholds.
115    #[test]
116    fn test_put_new_tip() {
117        let mut manager = TipManager::<Ed25519, Digest>::new();
118        let key = helpers::deterministic_public_key(1);
119        let node = helpers::create_dummy_node(key.clone(), 1, "payload");
120        assert!(manager.put(&node));
121        let got = manager.get(&key).unwrap();
122        assert_eq!(got.chunk, node.chunk);
123        assert_eq!(got.signature, node.signature);
124        assert_eq!(got.parent, node.parent);
125    }
126
127    /// Inserting a tip with the same height and payload returns false.
128    #[test]
129    fn test_put_same_height_same_payload() {
130        let mut manager = TipManager::<Ed25519, Digest>::new();
131        let key = helpers::deterministic_public_key(2);
132        let node = helpers::create_dummy_node(key.clone(), 1, "payload");
133        assert!(manager.put(&node));
134        assert!(!manager.put(&node));
135        let got = manager.get(&key).unwrap();
136        assert_eq!(got.chunk, node.chunk);
137        assert_eq!(got.signature, node.signature);
138        assert_eq!(got.parent, node.parent);
139    }
140
141    /// Inserting a tip with a higher height updates the stored tip.
142    #[test]
143    fn test_put_higher_tip() {
144        let mut manager = TipManager::<Ed25519, Digest>::new();
145        let key = helpers::deterministic_public_key(3);
146        let node1 = helpers::create_dummy_node(key.clone(), 1, "payload1");
147        assert!(manager.put(&node1));
148        let node2 = helpers::create_dummy_node(key.clone(), 2, "payload2");
149        assert!(manager.put(&node2));
150        let got = manager.get(&key).unwrap();
151        assert_eq!(got.chunk, node2.chunk);
152        assert_eq!(got.signature, node2.signature);
153        assert_eq!(got.parent, node2.parent);
154    }
155
156    /// Inserting a tip with a lower height panics.
157    #[test]
158    #[should_panic(expected = "Attempted to insert a lower-height tip")]
159    fn test_put_lower_tip_panics() {
160        let mut manager = TipManager::<Ed25519, Digest>::new();
161        let key = helpers::deterministic_public_key(4);
162        let node1 = helpers::create_dummy_node(key.clone(), 2, "payload");
163        assert!(manager.put(&node1));
164        let node2 = helpers::create_dummy_node(key.clone(), 1, "payload");
165        manager.put(&node2);
166    }
167
168    /// Inserting a tip with the same height but different payload panics.
169    #[test]
170    #[should_panic]
171    fn test_put_same_height_different_payload_panics() {
172        let mut manager = TipManager::<Ed25519, Digest>::new();
173        let key = helpers::deterministic_public_key(5);
174        let node1 = helpers::create_dummy_node(key.clone(), 1, "payload1");
175        assert!(manager.put(&node1));
176        let node2 = helpers::create_dummy_node(key.clone(), 1, "payload2");
177        manager.put(&node2);
178    }
179
180    /// Getting a tip for a nonexistent sequencer returns None.
181    #[test]
182    fn test_get_nonexistent() {
183        let manager = TipManager::<Ed25519, Digest>::new();
184        let key = helpers::deterministic_public_key(6);
185        assert!(manager.get(&key).is_none());
186    }
187
188    /// Multiple sequencers are handled independently.
189    #[test]
190    fn test_multiple_sequencers() {
191        let mut manager = TipManager::<Ed25519, Digest>::new();
192        let key1 = helpers::deterministic_public_key(10);
193        let key2 = helpers::deterministic_public_key(20);
194        let node1 = helpers::insert_tip(&mut manager, key1.clone(), 1, "payload1");
195        let node2 = helpers::insert_tip(&mut manager, key2.clone(), 2, "payload2");
196
197        let got1 = manager.get(&key1).unwrap();
198        let got2 = manager.get(&key2).unwrap();
199        assert_eq!(got1.chunk, node1.chunk);
200        assert_eq!(got2.chunk, node2.chunk);
201    }
202
203    /// Multiple updates for the same sequencer yield the tip with the highest height.
204    #[test]
205    fn test_put_multiple_updates() {
206        let mut manager = TipManager::<Ed25519, Digest>::new();
207        let key = helpers::deterministic_public_key(7);
208
209        // Insert tip with height 1.
210        let node1 = helpers::insert_tip(&mut manager, key.clone(), 1, "payload1");
211        let got1 = manager.get(&key).unwrap();
212        assert_eq!(got1.chunk.height, 1);
213        assert_eq!(got1.chunk.payload, node1.chunk.payload);
214
215        // Insert tip with height 2.
216        let node2 = helpers::insert_tip(&mut manager, key.clone(), 2, "payload2");
217        let got2 = manager.get(&key).unwrap();
218        assert_eq!(got2.chunk.height, 2);
219        assert_eq!(got2.chunk.payload, node2.chunk.payload);
220
221        // Insert tip with height 3.
222        let node3 = helpers::insert_tip(&mut manager, key.clone(), 3, "payload3");
223        let got3 = manager.get(&key).unwrap();
224        assert_eq!(got3.chunk.height, 3);
225        assert_eq!(got3.chunk.payload, node3.chunk.payload);
226
227        // Re-inserting the same tip should return false.
228        assert!(!manager.put(&node3));
229
230        // Insert tip with height 4.
231        let node4 = helpers::insert_tip(&mut manager, key.clone(), 4, "payload4");
232        let got4 = manager.get(&key).unwrap();
233        assert_eq!(got4.chunk.height, 4);
234        assert_eq!(got4.chunk.payload, node4.chunk.payload);
235    }
236}