hotshot_testing/
consistency_task.rs

1// Copyright (c) 2021-2024 Espresso Systems (espressosys.com)
2// This file is part of the HotShot repository.
3
4// You should have received a copy of the MIT License
5// along with the HotShot repository. If not, see <https://mit-license.org/>.
6
7#![allow(clippy::unwrap_or_default)]
8use std::collections::BTreeMap;
9
10use async_broadcast::Sender;
11use async_trait::async_trait;
12use committable::Committable;
13use hotshot_example_types::block_types::TestBlockHeader;
14use hotshot_types::{
15    data::{Leaf2, ViewNumber},
16    event::{Event, EventType},
17    message::UpgradeLock,
18    traits::node_implementation::NodeType,
19    vote::HasViewNumber,
20};
21use hotshot_utils::anytrace::*;
22use tokio::task::JoinHandle;
23use versions::{Upgrade, VERSION_0_0};
24
25use crate::{
26    overall_safety_task::OverallSafetyPropertiesDescription,
27    test_builder::TransactionValidator,
28    test_task::{TestEvent, TestResult, TestTaskState, spawn_timeout_task},
29};
30
31/// Map from views to leaves for a single node, allowing multiple leaves for each view (because the node may a priori send us multiple leaves for a given view).
32pub type NodeMap<TYPES> = BTreeMap<ViewNumber, Vec<Leaf2<TYPES>>>;
33
34/// A sanitized map from views to leaves for a single node, with only a single leaf per view.
35pub type NodeMapSanitized<TYPES> = BTreeMap<ViewNumber, Leaf2<TYPES>>;
36
37/// Validate that the `NodeMap` only has a single leaf per view.
38fn sanitize_node_map<TYPES: NodeType>(
39    node_map: &NodeMap<TYPES>,
40) -> Result<NodeMapSanitized<TYPES>> {
41    let mut result = BTreeMap::new();
42
43    for (view, leaves) in node_map.iter() {
44        let mut reduced = leaves.clone();
45
46        reduced.dedup();
47
48        match reduced.len() {
49            0 => {},
50            1 => {
51                result.insert(*view, reduced[0].clone());
52            },
53            _ => {
54                bail!(
55                    "We have received inconsistent leaves for view {view}. Leaves:\n\n{leaves:?}"
56                );
57            },
58        }
59    }
60
61    Ok(result)
62}
63
64/// For a NodeMapSanitized, we validate that each leaf extends the preceding leaf.
65async fn validate_node_map<TYPES: NodeType>(node_map: &NodeMapSanitized<TYPES>) -> Result<()> {
66    // We first scan 3-chains to find an upgrade certificate that has reached a decide.
67    let leaf_triples = node_map
68        .values()
69        .zip(node_map.values().skip(1))
70        .zip(node_map.values().skip(2))
71        .map(|((a, b), c)| (a, b, c));
72
73    let mut decided_upgrade_certificate = None;
74    let mut view_decided = ViewNumber::new(0);
75
76    for (grandparent, _parent, child) in leaf_triples {
77        if let Some(cert) = grandparent.upgrade_certificate()
78            && cert.data.decide_by <= child.view_number()
79        {
80            decided_upgrade_certificate = Some(cert);
81            view_decided = child.view_number();
82
83            break;
84        }
85    }
86
87    // To mimic consensus to use e.g. the `extends_upgrade` method,
88    // we cannot immediately put the upgrade certificate in the lock.
89    //
90    // Instead, we initialize an empty lock and add the certificate in the appropriate view.
91    let upgrade_lock = UpgradeLock::<TYPES>::new(Upgrade::trivial(VERSION_0_0));
92
93    let leaf_pairs = node_map.values().zip(node_map.values().skip(1));
94
95    // Check that the child leaf follows the parent, possibly with a gap.
96    for (parent, child) in leaf_pairs {
97        ensure!(
98            child.justify_qc().view_number >= parent.view_number(),
99            "The node has provided leaf:\n\n{child:?}\n\nbut its quorum certificate points to a \
100             view before the most recent leaf:\n\n{parent:?}"
101        );
102
103        child
104            .extends_upgrade(parent, &upgrade_lock)
105            .context(|e| error!("Leaf {child:?} does not extend its parent {parent:?}: {e}"))?;
106
107        ensure!(
108            child.height() > parent.height(),
109            "The node has decided leaf\n\n{child:?}\n\nextending leaf\n\n{parent:?}but the block \
110             height did not increase."
111        );
112
113        // We want to make sure the commitment matches,
114        // but allow for the possibility that we may have skipped views in between.
115        if child.justify_qc().view_number == parent.view_number()
116            && child.justify_qc().data.leaf_commit != parent.commit()
117        {
118            bail!(
119                "The node has provided leaf:\n\n{child:?}\n\nwhich points \
120                 to:\n\n{parent:?}\n\nbut the commits do not match."
121            );
122        }
123
124        if child.view_number() == view_decided {
125            upgrade_lock.set_decided_upgrade_cert(decided_upgrade_certificate.clone());
126        }
127    }
128
129    Ok(())
130}
131
132/// A map from node ids to `NodeMap`s; note that the latter may have multiple leaves per view in principle.
133pub type NetworkMap<TYPES> = BTreeMap<usize, NodeMap<TYPES>>;
134
135/// A map from node ids to `NodeMapSanitized`s; the latter has been sanitized validated to have a single leaf per view.
136pub type NetworkMapSanitized<TYPES> = BTreeMap<usize, NodeMapSanitized<TYPES>>;
137
138/// Validate that each node has only produced one unique leaf per view, and produce a `NetworkMapSanitized`.
139fn sanitize_network_map<TYPES: NodeType>(
140    network_map: &NetworkMap<TYPES>,
141) -> Result<NetworkMapSanitized<TYPES>> {
142    let mut result = BTreeMap::new();
143
144    for (node, node_map) in network_map {
145        result.insert(
146            *node,
147            sanitize_node_map(node_map)
148                .context(|e| error!("Node {node} produced inconsistent leaves: {e}"))?,
149        );
150    }
151
152    Ok(result)
153}
154
155pub type ViewMap<TYPES> = BTreeMap<ViewNumber, BTreeMap<usize, Leaf2<TYPES>>>;
156
157// Invert the network map by interchanging the roles of the node_id and view number.
158//
159// # Errors
160//
161// Returns an error if any node map is invalid.
162async fn invert_network_map<TYPES: NodeType>(
163    network_map: &NetworkMapSanitized<TYPES>,
164) -> Result<ViewMap<TYPES>> {
165    let mut inverted_map = BTreeMap::new();
166    for (node_id, node_map) in network_map.iter() {
167        validate_node_map::<TYPES>(node_map)
168            .await
169            .context(|e| error!("Node {node_id} has an invalid leaf history: {e}"))?;
170
171        // validate each node's leaf map
172        for (view, leaf) in node_map.iter() {
173            let view_map = inverted_map.entry(*view).or_insert(BTreeMap::new());
174            view_map.insert(*node_id, leaf.clone());
175        }
176    }
177
178    Ok(inverted_map)
179}
180
181/// A view map, sanitized to have exactly one leaf per view.
182pub type ViewMapSanitized<TYPES> = BTreeMap<ViewNumber, Leaf2<TYPES>>;
183
184fn sanitize_view_map<TYPES: NodeType>(
185    view_map: &ViewMap<TYPES>,
186) -> Result<ViewMapSanitized<TYPES>> {
187    let mut result = BTreeMap::new();
188
189    for (view, leaf_map) in view_map.iter() {
190        let mut node_leaves: Vec<_> = leaf_map.iter().collect();
191
192        node_leaves.dedup_by(|(_node_a, leaf_a), (_node_b, leaf_b)| leaf_a == leaf_b);
193
194        ensure!(
195            node_leaves.len() <= 1,
196            error!(
197                "The network does not agree on the following views: {}",
198                leaf_map
199                    .iter()
200                    .fold(format!("\n\nView {view}:"), |acc, (node, leaf)| {
201                        format!("{acc}\n\nNode {node} sent us leaf:\n\n{leaf:?}")
202                    })
203            )
204        );
205
206        if let Some(leaf) = node_leaves.first() {
207            result.insert(*view, leaf.1.clone());
208        }
209    }
210
211    for (parent, child) in result.values().zip(result.values().skip(1)) {
212        // We want to make sure the aggregated leafmap has not missed a decide event
213        if child.justify_qc().data.leaf_commit != parent.commit() {
214            bail!(
215                "The network has decided:\n\n{child:?}\n\nwhich succeeds:\n\n{parent:?}\n\nbut \
216                 the commits do not match. Did we miss an intermediate leaf?"
217            );
218        }
219    }
220
221    Ok(result)
222}
223
224enum TestProgress {
225    Incomplete,
226    Finished,
227}
228
229/// Data availability task state
230pub struct ConsistencyTask<TYPES: NodeType> {
231    /// A map from node ids to (leaves keyed on view number)
232    pub consensus_leaves: NetworkMap<TYPES>,
233    /// safety task requirements
234    pub safety_properties: OverallSafetyPropertiesDescription,
235    /// whether we should have seen an upgrade certificate or not
236    pub ensure_upgrade: bool,
237    /// a list of errors accumulated by the task
238    pub errors: Vec<Error>,
239    /// channel used to shutdown the test
240    pub test_sender: Sender<TestEvent>,
241    /// function used to validate the number of transactions committed in each block
242    pub validate_transactions: TransactionValidator,
243    /// running timeout task
244    pub timeout_task: JoinHandle<()>,
245}
246
247impl<TYPES: NodeType<BlockHeader = TestBlockHeader>> ConsistencyTask<TYPES> {
248    pub async fn validate(&self) -> Result<()> {
249        let sanitized_network_map = sanitize_network_map(&self.consensus_leaves)?;
250
251        let inverted_map = invert_network_map::<TYPES>(&sanitized_network_map).await?;
252
253        let sanitized_view_map = sanitize_view_map(&inverted_map)?;
254        let num_successful_views = sanitized_view_map.iter().len();
255
256        // check that we've succeeded in enough views
257        ensure!(
258            num_successful_views >= self.safety_properties.num_successful_views,
259            "Not enough successful views: expected {:?} but got {:?}",
260            self.safety_properties.num_successful_views,
261            num_successful_views,
262        );
263
264        let expected_upgrade = self.ensure_upgrade;
265        let actual_upgrade = sanitized_view_map.iter().fold(false, |acc, (_view, leaf)| {
266            acc || leaf.upgrade_certificate().is_some()
267        });
268
269        let mut transactions = Vec::new();
270
271        transactions = sanitized_view_map
272            .iter()
273            .fold(transactions, |mut acc, (view, leaf)| {
274                acc.push((**view, leaf.block_header().metadata.num_transactions));
275
276                acc
277            });
278
279        (self.validate_transactions)(&transactions)?;
280
281        ensure!(
282            expected_upgrade == actual_upgrade,
283            "Mismatch between expected and actual upgrade. Expected upgrade: {expected_upgrade}. \
284             Actual upgrade: {actual_upgrade}"
285        );
286
287        Ok(())
288    }
289
290    async fn partial_validate(&self) -> Result<TestProgress> {
291        self.check_view_success().await?;
292        self.check_view_failure().await?;
293
294        self.check_total_successes().await
295    }
296
297    fn add_error(&mut self, error: Error) {
298        self.errors.push(error);
299    }
300
301    async fn handle_result(&mut self, result: Result<TestProgress>) {
302        match result {
303            Ok(TestProgress::Finished) => {
304                let _ = self.test_sender.broadcast(TestEvent::Shutdown).await;
305            },
306            Err(e) => {
307                self.add_error(e);
308                let _ = self.test_sender.broadcast(TestEvent::Shutdown).await;
309            },
310            Ok(TestProgress::Incomplete) => {},
311        }
312    }
313
314    async fn check_total_successes(&self) -> Result<TestProgress> {
315        let sanitized_network_map = sanitize_network_map(&self.consensus_leaves)?;
316
317        let inverted_map = invert_network_map::<TYPES>(&sanitized_network_map).await?;
318
319        if inverted_map.len() >= self.safety_properties.num_successful_views {
320            Ok(TestProgress::Finished)
321        } else {
322            Ok(TestProgress::Incomplete)
323        }
324    }
325    pub async fn check_view_success(&self) -> Result<()> {
326        for (node_id, node_map) in self.consensus_leaves.iter() {
327            for (view, leaf) in node_map {
328                ensure!(
329                    !self.safety_properties.expected_view_failures.contains(view),
330                    "Expected a view failure, but got a decided leaf for view {view} from node \
331                     {node_id}.\n\nLeaf:\n\n{leaf:?}"
332                );
333            }
334        }
335
336        Ok(())
337    }
338
339    pub async fn check_view_failure(&self) -> Result<()> {
340        let sanitized_network_map = sanitize_network_map(&self.consensus_leaves)?;
341
342        let mut inverted_map = invert_network_map::<TYPES>(&sanitized_network_map).await?;
343
344        let (current_view, _) = inverted_map
345            .pop_last()
346            .context(error!("Leaf map is empty, which should be impossible"))?;
347        let Some((last_view, _)) = inverted_map.pop_last() else {
348            // the view cannot fail if there wasn't a prior view in the map.
349            return Ok(());
350        };
351
352        // filter out views we expected to (possibly) fail
353        let unexpected_failed_views: Vec<_> = (*(last_view + 1)..*current_view)
354            .filter(|view| {
355                !self.safety_properties.expected_view_failures.contains(view)
356                    && !self.safety_properties.possible_view_failures.contains(view)
357            })
358            .collect();
359
360        ensure!(
361            unexpected_failed_views.is_empty(),
362            "Unexpected failed views: {:?}",
363            unexpected_failed_views
364        );
365
366        Ok(())
367    }
368}
369
370#[async_trait]
371impl<TYPES: NodeType<BlockHeader = TestBlockHeader>> TestTaskState for ConsistencyTask<TYPES> {
372    type Event = Event<TYPES>;
373    type Error = Error;
374
375    /// Handles an event from one of multiple receivers.
376    async fn handle_event(&mut self, (message, id): (Self::Event, usize)) -> Result<()> {
377        if let Event {
378            event:
379                EventType::Decide {
380                    leaf_chain,
381                    committing_qc,
382                    deciding_qc,
383                    ..
384                },
385            ..
386        } = message
387        {
388            {
389                let mut timeout_task = spawn_timeout_task(
390                    self.test_sender.clone(),
391                    self.safety_properties.decide_timeout,
392                );
393
394                std::mem::swap(&mut self.timeout_task, &mut timeout_task);
395
396                timeout_task.abort();
397            }
398
399            match deciding_qc {
400                Some(deciding_qc) => {
401                    let last_leaf = &leaf_chain[0].leaf;
402                    ensure!(committing_qc.view_number() == last_leaf.view_number());
403                    ensure!(committing_qc.leaf_commit() == last_leaf.commit());
404                    ensure!(deciding_qc.view_number() == committing_qc.view_number() + 1);
405                },
406                None => {
407                    // Once we hit the epoch upgrade, every subsequent decide should come with a QC
408                    // chain proving its own finality.
409                    ensure!(
410                        committing_qc.epoch().is_none(),
411                        "expected 2nd deciding QC for post-epochs decide"
412                    );
413                },
414            }
415
416            for leaf_info in leaf_chain.iter().rev() {
417                let map = &mut self.consensus_leaves.entry(id).or_insert(BTreeMap::new());
418
419                map.entry(leaf_info.leaf.view_number())
420                    .and_modify(|vec| vec.push(leaf_info.leaf.clone()))
421                    .or_insert(vec![leaf_info.leaf.clone()]);
422
423                let result = self.partial_validate().await;
424
425                self.handle_result(result).await;
426            }
427        }
428
429        Ok(())
430    }
431
432    async fn check(&self) -> TestResult {
433        self.timeout_task.abort();
434
435        let mut errors: Vec<_> = self.errors.iter().map(|e| e.to_string()).collect();
436
437        if let Err(e) = self.validate().await {
438            errors.push(e.to_string());
439        }
440
441        if !errors.is_empty() {
442            TestResult::Fail(Box::new(errors))
443        } else {
444            TestResult::Pass
445        }
446    }
447}