Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Double Voting Prevention #3971

Closed
wants to merge 6 commits into from
Closed

Double Voting Prevention #3971

wants to merge 6 commits into from

Conversation

qvkare
Copy link

@qvkare qvkare commented Dec 16, 2024

This implements a robust double voting prevention mechanism for the HotShot. The implementation addresses a critical security vulnerability where nodes could potentially cast multiple votes for the same view under certain conditions.

Problem

In the current implementation, there's a significant security vulnerability in the voting mechanism:

// Current problematic implementation
if view > self.last_actions.da_vote {
    self.last_actions.da_vote = view;
}
// TODO Add logic to prevent double voting.  For now the simple check if
// the last voted view is less than the view we are trying to vote doesn't work
// because the leader of view n + 1 may propose to the DA (and we would vote)
// before the leader of view n.

This implementation fails to prevent double voting because:

  1. The leader of view n+1 might propose to DA before the leader of view n
  2. Simple view number comparison is insufficient for preventing double votes
  3. No persistent tracking of votes per view exists

Solution

1. New VoteTracker Component

Introduced a new VoteTracker struct that provides atomic vote tracking:

pub struct VoteTracker<TYPES: NodeType> {
    view_votes: HashMap<TYPES::View, HashSet<Arc<TYPES::SignatureKey>>>,
}

Key features:

  • Thread-safe vote tracking using Arc
  • O(1) vote verification
  • Automatic cleanup of old view data
  • Memory-efficient implementation

2. Consensus Integration

Modified the Consensus struct to incorporate vote tracking:

pub struct Consensus<TYPES: NodeType> {
    // ... existing fields ...
    vote_tracker: VoteTracker<TYPES>,
}

Updated vote handling logic:

pub fn update_action(&mut self, action: HotShotAction, view: TYPES::View) -> bool {
    match action {
        HotShotAction::DaVote => {
            let voter_key = Arc::new(self.public_key().clone());
            if !self.vote_tracker.record_vote(view, voter_key) {
                tracing::warn!("Prevented double voting attempt for view {}", view);
                return false;
            }
            // ... rest of the logic
        }
        // ... other actions
    }
}

Security Implications

  1. Vote Uniqueness

    • Each node can only vote once per view
    • Secured using node's public key
  2. Byzantine Fault Tolerance

    • Maintains BFT properties of the protocol
    • Prevents vote splitting attacks
    • Ensures vote finality within views
  3. View Synchronization

    • Handles out-of-order view proposals correctly
    • Maintains vote consistency across view changes
    • Prevents view-related voting attacks
graph TD
    A[Vote Request] --> B{Check Vote Tracker}
    B -->|Already Voted| C[Reject Vote]
    B -->|First Vote| D[Record Vote]
    D --> E[Update View State]
    E --> F[Cleanup Old Views]
    F --> G[Return Success]
Loading

Technical Implementation

1. Vote Recording Process

pub fn record_vote(
    &mut self,
    view: TYPES::View,
    voter_key: Arc<TYPES::SignatureKey>,
) -> bool {
    let votes = self.view_votes.entry(view).or_default();
    if votes.contains(&voter_key) {
        return false;
    }
    votes.insert(voter_key);
    true
}

Key aspects:

  • Uses efficient HashSet for O(1) lookups
  • Maintains vote history per view

2. Memory Management

pub fn cleanup_old_views(&mut self, current_view: TYPES::View) {
    self.view_votes.retain(|view, _| *view >= current_view);
}

Features:

  • Automatic garbage collection of old view data
  • Prevents memory leaks
  • Configurable retention policy

Time Complexity

Operation Complexity Notes
Vote Check O(1) HashSet lookup
Vote Record O(1) HashSet insert
Cleanup O(n) n = number of views
Memory Usage O(v * n) v = views, n = nodes

Unit Tests

#[test]
fn test_vote_tracking() {
    let mut tracker = VoteTracker::<SimpleVote>::new();
    let view = SimpleVote::View::default();
    let key1 = Arc::new(SimpleVote::SignatureKey::default());
    
    assert!(tracker.record_vote(view, key1.clone()));
    assert!(!tracker.record_vote(view, key1.clone())); // Double vote prevented
}

@qvkare qvkare requested a review from bfish713 as a code owner December 16, 2024 11:57
Comment on lines 570 to 571
// Clean up old vote records periodically
self.vote_tracker.cleanup_old_views(view);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this open us up tot he opposite problem would have? Vote in view n, then view n+1 then we can vote for view n again? The old code of course just allows any duplicate vote so there is an improvement with this code.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bfish713 I have implemented a robust system for tracking view sequences. Added a queue mechanism that handles out-of-order votes. This prevents double voting while allowing the network to be resilient. Sorry, I missed the importance of the TODO comment. Comprehensive test coverage of both epoch transitions and network partitioning scenarios is included in this solution.

Copy link
Collaborator

@bfish713 bfish713 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should discuss the solution to this before adding more details. I don't think we need this complex of vote tracking inserted at this point. DA voting is not the same as regular voting so double voting for a view is not really security issue, though not ideal. Also there is no requirement that votes be sequential. We can vote for view n+1 before view n for DA, we cannot when voting on a proposal.

The properties we would ideally enforce for voting on DA are:

  • Don't vote twice in a view
  • Don't vote for old views. And old view an be considered a view less or equal to the high QC view
  • Don't vote for views too far in the future, this can just be a heuristic value to prevent a leader from calculating all it's future views and spamming us with proposals.

Ideally most of these properties are enforced before we ever get update_action

Also can you elaborate on why there are changes to the epoch functions in consensus.rs

@qvkare qvkare closed this Dec 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants