commonware_broadcast/linked/signer/
tip_manager.rs

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