Repository: Hyle-org/hyle
Pull request #914: 🐛 Recover from consensus stoppage if everyone stopped on the same slot/view
Author: login: wraitii
Created: 2025-03-18T12:55:52Z | Merged: 2025-03-21T10:03:14Z



Diff:
diff --git a/src/consensus.rs b/src/consensus.rs
index 654fb69de..4d97eb77a 100644
--- a/src/consensus.rs
+++ b/src/consensus.rs
@@ -736,6 +736,9 @@ impl Consensus {
                         break;
                     },
                     GenesisEvent::NoGenesis => {
+                        // First wait a couple seconds to hopefully have reconnected with all peers.
+                        info!("🧳 Waiting a few seconds before processing with rejoin to connect with peers.");
+                        tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
                         // If we deserialized, we might be a follower or a leader.
                         // There's a few possibilities: maybe we're restarting quick enough that we're still synched,
                         // maybe we actually would block consensus by having a large stake
@@ -743,6 +746,8 @@ impl Consensus {
                         // Regardless, we should probably assume that we need to catch up.
                         // TODO: this logic can be improved.
                         self.bft_round_state.state_tag = StateTag::Joining;
+                        // Set up an initial timeout to ensure we don't get stuck if we miss commits
+                        self.bft_round_state.timeout.state.schedule_next(get_current_timestamp());
                         break;
                     },
                 }
@@ -759,9 +764,12 @@ impl Consensus {
     }
 
     async fn start(&mut self) -> Result<()> {
-        info!("🚀 Starting consensus");
+        info!(
+            "🚀 Starting consensus as {:?}",
+            self.bft_round_state.state_tag
+        );
 
-        let mut timeout_ticker = interval(Duration::from_millis(100));
+        let mut timeout_ticker = interval(Duration::from_millis(200));
         timeout_ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
 
         module_handle_messages! {
diff --git a/src/consensus/role_follower.rs b/src/consensus/role_follower.rs
index 5bb669948..3db780e01 100644
--- a/src/consensus/role_follower.rs
+++ b/src/consensus/role_follower.rs
@@ -62,29 +62,39 @@ impl FollowerRole for Consensus {
         );
 
         if matches!(self.bft_round_state.state_tag, StateTag::Joining) {
-            // Ignore obviously outdated messages.
-            // We'll be optimistic for ones in the future and hope that
-            // maybe we'll have caught up by the time the commit rolls around.
-            if consensus_proposal.slot <= self.bft_round_state.joining.staking_updated_to {
+            // Shortcut - if this is the prepare we expected, exit joining mode.
+            let is_next_prepare = consensus_proposal.view == 0
+                && consensus_proposal.slot == self.bft_round_state.consensus_proposal.slot + 1
+                || consensus_proposal.view == self.bft_round_state.consensus_proposal.view + 1
+                    && consensus_proposal.slot == self.bft_round_state.consensus_proposal.slot;
+            if is_next_prepare {
                 info!(
-                        "🌑 Outdated Prepare message (Slot {} / view {} while at {}) received while joining. Ignoring.",
-                        consensus_proposal.slot, consensus_proposal.view, self.bft_round_state.joining.staking_updated_to
-                    );
+                    "Received Prepare message for next slot while joining. Exiting joining mode."
+                );
+                self.bft_round_state.state_tag = StateTag::Follower;
+            } else {
+                // Ignore obviously outdated messages.
+                // We'll be optimistic for ones in the future and hope that
+                // maybe we'll have caught up by the time the commit rolls around.
+                if consensus_proposal.slot <= self.bft_round_state.joining.staking_updated_to {
+                    info!(
+                            "🌑 Outdated Prepare message (Slot {} / view {} while at {}) received while joining. Ignoring.",
+                            consensus_proposal.slot, consensus_proposal.view, self.bft_round_state.joining.staking_updated_to
+                        );
+                    return Ok(());
+                }
+                info!(
+                    "🌕 Prepare message (Slot {} / view {}) received while joining. Storing.",
+                    consensus_proposal.slot, consensus_proposal.view
+                );
+                // Store the message until we receive a matching Commit.
+                // Because we may receive old or rogue proposals, we store all of them.
+                self.bft_round_state
+                    .joining
+                    .buffered_prepares
+                    .push(consensus_proposal);
                 return Ok(());
             }
-            info!(
-                "🌕 Prepare message (Slot {} / view {}) received while joining. Storing.",
-                consensus_proposal.slot, consensus_proposal.view
-            );
-            // Store the message until we receive a matching Commit.
-            // Because we may receive old or rogue proposals, we store all of them.
-            // TODO: it would be slightly DOS-safer to only save those from validators we know,
-            // but I'm not sure it's an actual problem in practice.
-            self.bft_round_state
-                .joining
-                .buffered_prepares
-                .push(consensus_proposal);
-            return Ok(());
         }
         // If received proposal for next slot, we continue processing and will try to fast forward
         // if received proposal is for an even further slot, we buffer it
@@ -458,25 +468,28 @@ impl Consensus {
             return Ok(());
         };
         // Use that as our proposal.
-        self.bft_round_state.consensus_proposal = self
+        let mut potential_proposal = self
             .bft_round_state
             .joining
             .buffered_prepares
             .swap_remove(proposal_index);
-        self.bft_round_state.joining.buffered_prepares.clear();
 
         // At this point check that we're caught up enough that it's realistic to verify the QC.
-        if self.bft_round_state.joining.staking_updated_to + 1
-            < self.bft_round_state.consensus_proposal.slot
-        {
+        if self.bft_round_state.joining.staking_updated_to + 1 < potential_proposal.slot {
             info!(
                 "🏃Ignoring commit message, we are only caught up to {} ({} needed).",
                 self.bft_round_state.joining.staking_updated_to,
-                self.bft_round_state.consensus_proposal.slot - 1
+                potential_proposal.slot - 1
             );
             return Ok(());
         }
 
+        std::mem::swap(
+            &mut self.bft_round_state.consensus_proposal,
+            &mut potential_proposal,
+        );
+        self.bft_round_state.joining.buffered_prepares.clear();
+
         info!(
             "📦 Commit message received for slot {}, trying to synchronize.",
             self.bft_round_state.consensus_proposal.slot
@@ -491,6 +504,11 @@ impl Consensus {
             )
             .is_err()
         {
+            // Swap back
+            std::mem::swap(
+                &mut self.bft_round_state.consensus_proposal,
+                &mut potential_proposal,
+            );
             self.bft_round_state.state_tag = StateTag::Joining;
             bail!("⛑️ Failed to synchronize, retrying soon.");
         }
diff --git a/src/consensus/role_timeout.rs b/src/consensus/role_timeout.rs
index e5f690677..8c85365b8 100644
--- a/src/consensus/role_timeout.rs
+++ b/src/consensus/role_timeout.rs
@@ -4,9 +4,12 @@ use borsh::{BorshDeserialize, BorshSerialize};
 use tracing::{debug, info, trace, warn};
 
 use super::Consensus;
-use crate::model::{
-    utils::get_current_timestamp, ConsensusNetMessage, QuorumCertificate, SignedByValidator, Slot,
-    Ticket, ValidatorPublicKey, View,
+use crate::{
+    consensus::StateTag,
+    model::{
+        utils::get_current_timestamp, ConsensusNetMessage, QuorumCertificate, SignedByValidator,
+        Slot, Ticket, ValidatorPublicKey, View,
+    },
 };
 use anyhow::{bail, Context, Result};
 
@@ -132,6 +135,11 @@ impl TimeoutRole for Consensus {
             self.bft_round_state.consensus_proposal.view
         ))?;
 
+        // This TC is for our current slot and view, so we can leave Joining mode
+        if matches!(self.bft_round_state.state_tag, StateTag::Joining) {
+            self.bft_round_state.state_tag = StateTag::Leader;
+        }
+
         self.carry_on_with_ticket(Ticket::TimeoutQC(received_timeout_certificate.clone()))
     }
 
@@ -254,6 +262,10 @@ impl TimeoutRole for Consensus {
                 .schedule_next(get_current_timestamp());
 
             if &self.next_leader()? == self.crypto.validator_pubkey() {
+                // This TC is for our current slot and view (by construction), so we can leave Joining mode
+                if matches!(self.bft_round_state.state_tag, StateTag::Joining) {
+                    self.bft_round_state.state_tag = StateTag::Leader;
+                }
                 self.carry_on_with_ticket(Ticket::TimeoutQC(timeout_certificate))?;
             } else {
                 // Broadcast the Timeout Certificate to all validators
diff --git a/tests/consensus_tests.rs b/tests/consensus_tests.rs
index e2b6601d2..8083c0797 100644
--- a/tests/consensus_tests.rs
+++ b/tests/consensus_tests.rs
@@ -316,4 +316,55 @@ mod e2e_consensus {
 
         Ok(())
     }
+
+    #[test_log::test(tokio::test)]
+    async fn can_restart_multi_node_after_txs() -> Result<()> {
+        let mut ctx = E2ECtx::new_multi_with_indexer(4, 500).await?;
+
+        _ = ctx.wait_height(1).await;
+
+        // Gen a few txs
+        let mut tx_ctx = init_states(&mut ctx).await;
+
+        warn!("Starting generating txs");
+
+        for i in 0..2 {
+            _ = gen_txs(&mut ctx, &mut tx_ctx, format!("alex{}", i), 100 + i).await;
+        }
+
+        _ = ctx.wait_height(2).await;
+
+        ctx.stop_all().await;
+
+        tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
+
+        warn!("Restarting nodes");
+
+        ctx.restart_node(0)?;
+        ctx.restart_node(1)?;
+        ctx.restart_node(2)?;
+        ctx.restart_node(3)?;
+        ctx.restart_node(4)?;
+
+        ctx.wait_height(2).await?;
+
+        for i in 2..4 {
+            _ = gen_txs(&mut ctx, &mut tx_ctx, format!("alex{}", i), 100 + i).await;
+        }
+
+        ctx.wait_height(3).await?;
+
+        let state: Hyllar = ctx
+            .indexer_client()
+            .fetch_current_state(&ContractName::new("hyllar"))
+            .await?;
+
+        for i in 0..4 {
+            let balance = state.balance_of(&format!("alex{}.hydentity", i));
+            info!("Checking alex{}.hydentity balance: {:?}", i, balance);
+            assert_eq!(balance.unwrap(), ((100 + i) as u128));
+        }
+
+        Ok(())
+    }
 }
diff --git a/tests/fixtures/ctx.rs b/tests/fixtures/ctx.rs
index 140e9bf73..cd639d1b7 100644
--- a/tests/fixtures/ctx.rs
+++ b/tests/fixtures/ctx.rs
@@ -297,6 +297,10 @@ impl E2ECtx {
         })
     }
 
+    pub async fn stop_all(&mut self) {
+        futures::future::join_all(self.nodes.iter_mut().map(|node| node.stop())).await;
+    }
+
     pub async fn stop_node(&mut self, index: usize) -> Result<()> {
         self.nodes[index].stop().await
     }


Reviews:
user: login: BertrandD
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T08:35:43Z
user: login: BertrandD
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-21T08:43:26Z
user: login: hhalex
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T08:58:10Z
user: login: hhalex
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T09:00:41Z
user: login: hhalex
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T09:01:45Z
user: login: hhalex
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T09:02:49Z
user: login: BertrandD
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T09:07:02Z
user: login: wraitii
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T09:29:06Z
user: login: wraitii
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T09:31:50Z
user: login: wraitii
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T09:32:22Z
user: login: BertrandD
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-21T09:56:49Z

Review comments:
diff_hunk: @@ -61,30 +61,48 @@ impl FollowerRole for Consensus {
             "Received Prepare message: {}", consensus_proposal
         );
 
+        info!(
path: src/consensus/role_follower.rs
user: login: BertrandD
body: debug ?
created_at: 2025-03-21T08:35:43Z
author_association: MEMBER
diff_hunk: @@ -491,6 +512,11 @@ impl Consensus {
             )
             .is_err()
         {
+            // Swap back
+            std::mem::swap(
path: src/consensus/role_follower.rs
user: login: BertrandD
body: when I see that, I ask myself if it wouldn't be cleaner to edit `try_commit_current_proposal` to `try_commit_proposal` and pass the proposal as parameter :thinking: 

could be a future refacto
created_at: 2025-03-21T08:39:06Z
author_association: MEMBER
diff_hunk: @@ -297,6 +297,11 @@ impl E2ECtx {
         })
     }
 
+    pub async fn stop_all(&mut self) -> Result<()> {
+        futures::future::join_all(self.nodes.iter_mut().map(|node| node.stop())).await;
+        Ok(())
+    }
path: tests/fixtures/ctx.rs
user: login: BertrandD
body: don't need a `Result` here I think

```suggestion
    pub async fn stop_all(&mut self) {
        futures::future::join_all(self.nodes.iter_mut().map(|node| node.stop())).await;
    }
```
created_at: 2025-03-21T08:41:44Z
author_association: MEMBER
diff_hunk: @@ -132,6 +135,11 @@ impl TimeoutRole for Consensus {
             self.bft_round_state.consensus_proposal.view
         ))?;
 
+        // This TC is for our current slot and view, so we can leave Joining mode
+        if matches!(self.bft_round_state.state_tag, StateTag::Joining) {
+            self.bft_round_state.state_tag = StateTag::Leader;
path: src/consensus/role_timeout.rs
user: login: BertrandD
body: why leader ?
created_at: 2025-03-21T08:42:38Z
author_association: MEMBER
diff_hunk: @@ -254,6 +262,10 @@ impl TimeoutRole for Consensus {
                 .schedule_next(get_current_timestamp());
 
             if &self.next_leader()? == self.crypto.validator_pubkey() {
+                // This TC is for our current slot and view (by construction), so we can leave Joining mode
+                if matches!(self.bft_round_state.state_tag, StateTag::Joining) {
+                    self.bft_round_state.state_tag = StateTag::Leader;
path: src/consensus/role_timeout.rs
user: login: BertrandD
body: same, why leader ?
created_at: 2025-03-21T08:43:05Z
author_association: MEMBER
diff_hunk: @@ -61,30 +61,48 @@ impl FollowerRole for Consensus {
             "Received Prepare message: {}", consensus_proposal
         );
 
+        info!(
+            "At round {} {}",
+            self.bft_round_state.consensus_proposal.slot,
+            self.bft_round_state.consensus_proposal.view
+        );
+
         if matches!(self.bft_round_state.state_tag, StateTag::Joining) {
-            // Ignore obviously outdated messages.
-            // We'll be optimistic for ones in the future and hope that
-            // maybe we'll have caught up by the time the commit rolls around.
-            if consensus_proposal.slot <= self.bft_round_state.joining.staking_updated_to {
+            // Shortcut - if this is the prepare we expected, exit joining mode.
+            let is_next_prepare = consensus_proposal.view == 0
+                && consensus_proposal.slot == self.bft_round_state.consensus_proposal.slot + 1
+                || consensus_proposal.view == self.bft_round_state.consensus_proposal.view + 1
+                    && consensus_proposal.slot == self.bft_round_state.consensus_proposal.slot;
+            if is_next_prepare {
+                info!(
+                    "Received Prepare message for next slot while joining. Exiting joining mode."
+                );
+                self.bft_round_state.state_tag = StateTag::Follower;
+            } else {
+                // Ignore obviously outdated messages.
+                // We'll be optimistic for ones in the future and hope that
+                // maybe we'll have caught up by the time the commit rolls around.
+                if consensus_proposal.slot <= self.bft_round_state.joining.staking_updated_to {
+                    info!(
+                            "🌑 Outdated Prepare message (Slot {} / view {} while at {}) received while joining. Ignoring.",
+                            consensus_proposal.slot, consensus_proposal.view, self.bft_round_state.joining.staking_updated_to
+                        );
+                    return Ok(());
+                }
                 info!(
-                        "🌑 Outdated Prepare message (Slot {} / view {} while at {}) received while joining. Ignoring.",
-                        consensus_proposal.slot, consensus_proposal.view, self.bft_round_state.joining.staking_updated_to
-                    );
+                    "🌕 Prepare message (Slot {} / view {}) received while joining. Storing.",
+                    consensus_proposal.slot, consensus_proposal.view
+                );
+                // Store the message until we receive a matching Commit.
+                // Because we may receive old or rogue proposals, we store all of them.
+                // TODO: it would be slightly DOS-safer to only save those from validators we know,
path: src/consensus/role_follower.rs
user: login: hhalex
body: Ça paraît dangereux de discard les messages des noeuds qu'on ne connaît pas ? Si ça se trouve on est en retard, et le noeud est staked + bond dans un slot futur ?
created_at: 2025-03-21T08:58:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -132,6 +135,11 @@ impl TimeoutRole for Consensus {
             self.bft_round_state.consensus_proposal.view
         ))?;
 
+        // This TC is for our current slot and view, so we can leave Joining mode
+        if matches!(self.bft_round_state.state_tag, StateTag::Joining) {
+            self.bft_round_state.state_tag = StateTag::Leader;
path: src/consensus/role_timeout.rs
user: login: hhalex
body: Parce qu'on a déjà check plus haut l119 qu'on est le next leader, et donc qu'on process le timeout certificate
created_at: 2025-03-21T09:00:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -254,6 +262,10 @@ impl TimeoutRole for Consensus {
                 .schedule_next(get_current_timestamp());
 
             if &self.next_leader()? == self.crypto.validator_pubkey() {
+                // This TC is for our current slot and view (by construction), so we can leave Joining mode
+                if matches!(self.bft_round_state.state_tag, StateTag::Joining) {
+                    self.bft_round_state.state_tag = StateTag::Leader;
path: src/consensus/role_timeout.rs
user: login: hhalex
body: l 264
created_at: 2025-03-21T09:01:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -316,4 +316,58 @@ mod e2e_consensus {
 
         Ok(())
     }
+
+    #[test_log::test(tokio::test)]
+    async fn can_restart_multi_node_after_txs() -> Result<()> {
+        let mut ctx = E2ECtx::new_multi_with_indexer(4, 500).await?;
+
+        _ = ctx.wait_height(1).await;
+
+        // Gen a few txs
+        let mut tx_ctx = init_states(&mut ctx).await;
+
+        warn!("Starting generating txs");
+
+        for i in 0..2 {
+            _ = gen_txs(&mut ctx, &mut tx_ctx, format!("alex{}", i), 100 + i).await;
+        }
+
+        _ = ctx.wait_height(2).await;
+
+        ctx.stop_all().await?;
+
+        tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
+
+        warn!("Restarting nodes");
+
+        ctx.restart_node(0)?;
+        ctx.restart_node(1)?;
+        ctx.restart_node(2)?;
+        ctx.restart_node(3)?;
+        ctx.restart_node(4)?;
+
+        ctx.wait_height(0).await?;
+
+        // Give it some time - they might take a few rounds to connect to each other.
+        tokio::time::sleep(tokio::time::Duration::from_secs(6)).await;
path: tests/consensus_tests.rs
user: login: hhalex
body: Pourquoi pas un wait height (xx) moins flaky ?
created_at: 2025-03-21T09:02:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -254,6 +262,10 @@ impl TimeoutRole for Consensus {
                 .schedule_next(get_current_timestamp());
 
             if &self.next_leader()? == self.crypto.validator_pubkey() {
+                // This TC is for our current slot and view (by construction), so we can leave Joining mode
+                if matches!(self.bft_round_state.state_tag, StateTag::Joining) {
+                    self.bft_round_state.state_tag = StateTag::Leader;
path: src/consensus/role_timeout.rs
user: login: BertrandD
body: oups, indeed
created_at: 2025-03-21T09:07:02Z
author_association: MEMBER
diff_hunk: @@ -491,6 +512,11 @@ impl Consensus {
             )
             .is_err()
         {
+            // Swap back
+            std::mem::swap(
path: src/consensus/role_follower.rs
user: login: wraitii
body: Je change dans une PR upcoming qui rend ça plus clean
created_at: 2025-03-21T09:29:06Z
author_association: MEMBER
diff_hunk: @@ -61,30 +61,48 @@ impl FollowerRole for Consensus {
             "Received Prepare message: {}", consensus_proposal
         );
 
+        info!(
path: src/consensus/role_follower.rs
user: login: wraitii
body: Ah mais je me demandais pourquoi c'était là ça dans une autre PR mais c'est de ma faute ok, je vais suppr
created_at: 2025-03-21T09:31:50Z
author_association: MEMBER
diff_hunk: @@ -61,30 +61,48 @@ impl FollowerRole for Consensus {
             "Received Prepare message: {}", consensus_proposal
         );
 
+        info!(
+            "At round {} {}",
+            self.bft_round_state.consensus_proposal.slot,
+            self.bft_round_state.consensus_proposal.view
+        );
+
         if matches!(self.bft_round_state.state_tag, StateTag::Joining) {
-            // Ignore obviously outdated messages.
-            // We'll be optimistic for ones in the future and hope that
-            // maybe we'll have caught up by the time the commit rolls around.
-            if consensus_proposal.slot <= self.bft_round_state.joining.staking_updated_to {
+            // Shortcut - if this is the prepare we expected, exit joining mode.
+            let is_next_prepare = consensus_proposal.view == 0
+                && consensus_proposal.slot == self.bft_round_state.consensus_proposal.slot + 1
+                || consensus_proposal.view == self.bft_round_state.consensus_proposal.view + 1
+                    && consensus_proposal.slot == self.bft_round_state.consensus_proposal.slot;
+            if is_next_prepare {
+                info!(
+                    "Received Prepare message for next slot while joining. Exiting joining mode."
+                );
+                self.bft_round_state.state_tag = StateTag::Follower;
+            } else {
+                // Ignore obviously outdated messages.
+                // We'll be optimistic for ones in the future and hope that
+                // maybe we'll have caught up by the time the commit rolls around.
+                if consensus_proposal.slot <= self.bft_round_state.joining.staking_updated_to {
+                    info!(
+                            "🌑 Outdated Prepare message (Slot {} / view {} while at {}) received while joining. Ignoring.",
+                            consensus_proposal.slot, consensus_proposal.view, self.bft_round_state.joining.staking_updated_to
+                        );
+                    return Ok(());
+                }
                 info!(
-                        "🌑 Outdated Prepare message (Slot {} / view {} while at {}) received while joining. Ignoring.",
-                        consensus_proposal.slot, consensus_proposal.view, self.bft_round_state.joining.staking_updated_to
-                    );
+                    "🌕 Prepare message (Slot {} / view {}) received while joining. Storing.",
+                    consensus_proposal.slot, consensus_proposal.view
+                );
+                // Store the message until we receive a matching Commit.
+                // Because we may receive old or rogue proposals, we store all of them.
+                // TODO: it would be slightly DOS-safer to only save those from validators we know,
path: src/consensus/role_follower.rs
user: login: wraitii
body: Pas faux
created_at: 2025-03-21T09:32:22Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-03-18T13:00:34Z
body: ## [Codecov](https://app.codecov.io/gh/Hyle-org/hyle/pull/914?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hyle-org) Report
Attention: Patch coverage is `54.34783%` with `21 lines` in your changes missing coverage. Please review.

:white_check_mark: All tests successful. No failed tests found.

| [Files with missing lines](https://app.codecov.io/gh/Hyle-org/hyle/pull/914?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hyle-org) | Patch % | Lines |
|---|---|---|
| [src/consensus/role\_follower.rs](https://app.codecov.io/gh/Hyle-org/hyle/pull/914?src=pr&el=tree&filepath=src%2Fconsensus%2Frole_follower.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hyle-org#diff-c3JjL2NvbnNlbnN1cy9yb2xlX2ZvbGxvd2VyLnJz) | 59.37% | [13 Missing :warning: ](https://app.codecov.io/gh/Hyle-org/hyle/pull/914?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hyle-org) |
| [src/consensus.rs](https://app.codecov.io/gh/Hyle-org/hyle/pull/914?src=pr&el=tree&filepath=src%2Fconsensus.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hyle-org#diff-c3JjL2NvbnNlbnN1cy5ycw==) | 25.00% | [6 Missing :warning: ](https://app.codecov.io/gh/Hyle-org/hyle/pull/914?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hyle-org) |
| [src/consensus/role\_timeout.rs](https://app.codecov.io/gh/Hyle-org/hyle/pull/914?src=pr&el=tree&filepath=src%2Fconsensus%2Frole_timeout.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hyle-org#diff-c3JjL2NvbnNlbnN1cy9yb2xlX3RpbWVvdXQucnM=) | 66.66% | [2 Missing :warning: ](https://app.codecov.io/gh/Hyle-org/hyle/pull/914?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hyle-org) |

| [Files with missing lines](https://app.codecov.io/gh/Hyle-org/hyle/pull/914?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hyle-org) | Coverage Δ | |
|---|---|---|
| [src/consensus/role\_timeout.rs](https://app.codecov.io/gh/Hyle-org/hyle/pull/914?src=pr&el=tree&filepath=src%2Fconsensus%2Frole_timeout.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hyle-org#diff-c3JjL2NvbnNlbnN1cy9yb2xlX3RpbWVvdXQucnM=) | `82.75% <66.66%> (-0.58%)` | :arrow_down: |
| [src/consensus.rs](https://app.codecov.io/gh/Hyle-org/hyle/pull/914?src=pr&el=tree&filepath=src%2Fconsensus.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hyle-org#diff-c3JjL2NvbnNlbnN1cy5ycw==) | `89.10% <25.00%> (-0.66%)` | :arrow_down: |
| [src/consensus/role\_follower.rs](https://app.codecov.io/gh/Hyle-org/hyle/pull/914?src=pr&el=tree&filepath=src%2Fconsensus%2Frole_follower.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hyle-org#diff-c3JjL2NvbnNlbnN1cy9yb2xlX2ZvbGxvd2VyLnJz) | `79.33% <59.37%> (-1.03%)` | :arrow_down: |

... and [1 file with indirect coverage changes](https://app.codecov.io/gh/Hyle-org/hyle/pull/914/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hyle-org)

author_association: NONE
user: login: BertrandD
created_at: 2025-03-21T08:44:03Z
body: wanted to comment not approve ><
author_association: MEMBER