mirror of
https://github.com/vacp2p/de-mls.git
synced 2026-01-09 13:38:06 -05:00
* start to update ui * remove websocket ui * refactor code after ws removing * update frontend * Unable real consensus result * update proposal ui * add current pending proposal to ui section * Refactor UI and backend for ban request feature - Updated `build.rs` to ensure proper protobuf compilation. - Removed `tracing-subscriber` dependency from `Cargo.toml` and `Cargo.lock`. - Refactored `main.rs` in the desktop UI to improve state management and UI interactions for group chat. - Enhanced `Gateway` and `User` structs to support sending ban requests and processing related events. - Updated UI components to reflect changes in proposal handling and improve user experience during voting and group management. - Added tests for new functionality and improved error handling across modules. * Add mls_crypto integration and group member management features - Introduced `mls_crypto` as a dependency for wallet address normalization. - Enhanced the desktop UI to support group member management, including requesting user bans. - Implemented new commands and events in the `Gateway` and `User` structs for retrieving group members and processing ban requests. - Updated the `User` struct to include methods for fetching group members and validating wallet addresses. - Refactored various components to improve state management and UI interactions related to group chat and member actions. - Added error handling for invalid wallet addresses and improved overall user experience in group management features. * Replace Apache License with a new version and add MIT License; update README to reflect new licensing information and enhance user module documentation. Refactor user module to improve group management, consensus handling, and messaging features, including ban request processing and proposal management. * Update dependencies and refactor package names for consistency * update ci * update ci * - Added `mls_crypto` as a dependency for wallet address normalization. - Introduced a new CSS file for styling the desktop UI, improving overall aesthetics and user experience. - Enhanced the `User` and `Gateway` structs to support group member management, including ban requests and proposal handling. - Implemented new commands and events for retrieving group members and processing ban requests. - Refactored various components to improve state management and UI interactions related to group chat and member actions. - Updated the `Cargo.toml` and `Cargo.lock` files to reflect new dependencies and configurations. - Added new profiles for development builds targeting WebAssembly, server, and Android environments.
360 lines
12 KiB
Rust
360 lines
12 KiB
Rust
use alloy::signers::local::PrivateKeySigner;
|
|
use de_mls::consensus::{compute_vote_hash, ConsensusEvent, ConsensusService};
|
|
use de_mls::protos::consensus::v1::Vote;
|
|
use de_mls::LocalSigner;
|
|
use prost::Message;
|
|
use std::time::Duration;
|
|
use uuid::Uuid;
|
|
|
|
#[tokio::test]
|
|
async fn test_basic_consensus_service() {
|
|
// Create consensus service
|
|
let consensus_service = ConsensusService::new();
|
|
|
|
let group_name = "test_group";
|
|
let expected_voters_count = 3;
|
|
|
|
let signer = PrivateKeySigner::random();
|
|
let proposal_owner_address = signer.address();
|
|
let proposal_owner = proposal_owner_address.to_string().as_bytes().to_vec();
|
|
|
|
// Create a proposal
|
|
let proposal = consensus_service
|
|
.create_proposal(
|
|
group_name,
|
|
"Test Proposal".to_string(),
|
|
vec![],
|
|
proposal_owner,
|
|
expected_voters_count,
|
|
300,
|
|
true,
|
|
)
|
|
.await
|
|
.expect("Failed to create proposal");
|
|
|
|
let proposal = consensus_service
|
|
.vote_on_proposal(group_name, proposal.proposal_id, true, signer)
|
|
.await
|
|
.expect("Failed to vote on proposal");
|
|
|
|
// Verify proposal was created
|
|
let active_proposals = consensus_service.get_active_proposals(group_name).await;
|
|
assert_eq!(active_proposals.len(), 1);
|
|
assert_eq!(active_proposals[0].proposal_id, proposal.proposal_id);
|
|
|
|
// Verify group statistics
|
|
let group_stats = consensus_service.get_group_stats(group_name).await;
|
|
assert_eq!(group_stats.total_sessions, 1);
|
|
assert_eq!(group_stats.active_sessions, 1);
|
|
|
|
// Verify consensus threshold calculation
|
|
// With 3 expected voters, we need 2n/3 = 2 votes for consensus
|
|
// Initially we have 1 vote (steward), so we don't have sufficient votes
|
|
assert!(
|
|
!consensus_service
|
|
.has_sufficient_votes(group_name, proposal.proposal_id)
|
|
.await
|
|
);
|
|
|
|
let signer_2 = PrivateKeySigner::random();
|
|
let proposal_owner_2 = signer_2.address_bytes();
|
|
// Add 1 more vote (total 2 votes)
|
|
let mut vote = Vote {
|
|
vote_id: Uuid::new_v4().as_u128() as u32,
|
|
vote_owner: proposal_owner_2,
|
|
proposal_id: proposal.proposal_id,
|
|
timestamp: std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.expect("Failed to get current time")
|
|
.as_secs(),
|
|
vote: true,
|
|
parent_hash: Vec::new(),
|
|
received_hash: proposal.votes[0].vote_hash.clone(), // Reference steward's vote hash
|
|
vote_hash: Vec::new(),
|
|
signature: Vec::new(),
|
|
};
|
|
|
|
// Compute vote hash
|
|
vote.vote_hash = compute_vote_hash(&vote);
|
|
let vote_bytes = vote.encode_to_vec();
|
|
vote.signature = signer_2
|
|
.local_sign_message(&vote_bytes)
|
|
.await
|
|
.expect("Failed to sign vote");
|
|
|
|
consensus_service
|
|
.process_incoming_vote(group_name, vote)
|
|
.await
|
|
.expect("Failed to process vote");
|
|
|
|
// Now we should have sufficient votes (2 out of 3 expected voters)
|
|
assert!(
|
|
consensus_service
|
|
.has_sufficient_votes(group_name, proposal.proposal_id)
|
|
.await
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_multi_group_consensus_service() {
|
|
// Create consensus service with max 10 sessions per group
|
|
let consensus_service = ConsensusService::new_with_max_sessions(10);
|
|
|
|
// Test group 1
|
|
let group1_name = "test_group_1";
|
|
let group1_members_count = 3;
|
|
let signer_1 = PrivateKeySigner::random();
|
|
let proposal_owner_1 = signer_1.address_bytes();
|
|
|
|
// Test group 2
|
|
let group2_name = "test_group_2";
|
|
let group2_members_count = 3;
|
|
let signer_2 = PrivateKeySigner::random();
|
|
let proposal_owner_2 = signer_2.address_bytes();
|
|
|
|
// Create proposals for group 1
|
|
let proposal_1 = consensus_service
|
|
.create_proposal(
|
|
group1_name,
|
|
"Test Proposal".to_string(),
|
|
vec![],
|
|
proposal_owner_1,
|
|
group1_members_count,
|
|
300,
|
|
true,
|
|
)
|
|
.await
|
|
.expect("Failed to create proposal");
|
|
|
|
let _proposal_1 = consensus_service
|
|
.vote_on_proposal(group1_name, proposal_1.proposal_id, true, signer_1)
|
|
.await
|
|
.expect("Failed to vote on proposal");
|
|
|
|
let proposal_2 = consensus_service
|
|
.create_proposal(
|
|
group2_name,
|
|
"Test Proposal".to_string(),
|
|
vec![],
|
|
proposal_owner_2.clone(),
|
|
group2_members_count,
|
|
300,
|
|
true,
|
|
)
|
|
.await
|
|
.expect("Failed to create proposal");
|
|
|
|
let _proposal_2 = consensus_service
|
|
.vote_on_proposal(group2_name, proposal_2.proposal_id, true, signer_2.clone())
|
|
.await
|
|
.expect("Failed to vote on proposal");
|
|
|
|
// Create proposal for group 2
|
|
let proposal_3 = consensus_service
|
|
.create_proposal(
|
|
group2_name,
|
|
"Test Proposal".to_string(),
|
|
vec![],
|
|
proposal_owner_2,
|
|
group2_members_count,
|
|
300,
|
|
true,
|
|
)
|
|
.await
|
|
.expect("Failed to create proposal");
|
|
|
|
let _proposal_3 = consensus_service
|
|
.vote_on_proposal(group2_name, proposal_3.proposal_id, true, signer_2)
|
|
.await
|
|
.expect("Failed to vote on proposal");
|
|
|
|
// Verify proposals are created for both groups
|
|
let group1_proposals = consensus_service.get_active_proposals(group1_name).await;
|
|
let group2_proposals = consensus_service.get_active_proposals(group2_name).await;
|
|
|
|
assert_eq!(group1_proposals.len(), 1);
|
|
assert_eq!(group2_proposals.len(), 2);
|
|
|
|
// Verify group statistics
|
|
let group1_stats = consensus_service.get_group_stats(group1_name).await;
|
|
let group2_stats = consensus_service.get_group_stats(group2_name).await;
|
|
|
|
assert_eq!(group1_stats.total_sessions, 1);
|
|
assert_eq!(group1_stats.active_sessions, 1);
|
|
assert_eq!(group2_stats.total_sessions, 2);
|
|
assert_eq!(group2_stats.active_sessions, 2);
|
|
|
|
// Verify overall statistics
|
|
let overall_stats = consensus_service.get_overall_stats().await;
|
|
assert_eq!(overall_stats.total_sessions, 3);
|
|
assert_eq!(overall_stats.active_sessions, 3);
|
|
|
|
// Verify active groups
|
|
let active_groups = consensus_service.get_active_groups().await;
|
|
assert_eq!(active_groups.len(), 2);
|
|
assert!(active_groups.contains(&group1_name.to_string()));
|
|
assert!(active_groups.contains(&group2_name.to_string()));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_consensus_threshold_calculation() {
|
|
let consensus_service = ConsensusService::new();
|
|
let mut consensus_events = consensus_service.subscribe_to_events();
|
|
|
|
let group_name = "test_group_threshold";
|
|
let expected_voters_count = 5;
|
|
let signer = PrivateKeySigner::random();
|
|
let proposal_owner = signer.address_bytes();
|
|
|
|
// Create a proposal
|
|
let proposal = consensus_service
|
|
.create_proposal(
|
|
group_name,
|
|
"Test Proposal".to_string(),
|
|
vec![],
|
|
proposal_owner,
|
|
expected_voters_count,
|
|
300,
|
|
true,
|
|
)
|
|
.await
|
|
.expect("Failed to create proposal");
|
|
|
|
let proposal = consensus_service
|
|
.vote_on_proposal(group_name, proposal.proposal_id, true, signer)
|
|
.await
|
|
.expect("Failed to vote on proposal");
|
|
|
|
// With 5 expected voters, we need 2n/3 = 3.33... -> 4 votes for consensus
|
|
// Initially we have 1 vote (steward), so we don't have sufficient votes
|
|
assert!(
|
|
!consensus_service
|
|
.has_sufficient_votes(group_name, proposal.proposal_id)
|
|
.await
|
|
);
|
|
|
|
for _ in 0..4 {
|
|
let signer = PrivateKeySigner::random();
|
|
let vote_owner = signer.address_bytes();
|
|
let mut vote = Vote {
|
|
vote_id: Uuid::new_v4().as_u128() as u32,
|
|
vote_owner: vote_owner.clone(),
|
|
proposal_id: proposal.proposal_id,
|
|
timestamp: std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.expect("Failed to get current time")
|
|
.as_secs(),
|
|
vote: true,
|
|
parent_hash: Vec::new(),
|
|
received_hash: proposal.votes[0].vote_hash.clone(), // Reference previous vote's hash
|
|
vote_hash: Vec::new(),
|
|
signature: Vec::new(),
|
|
};
|
|
|
|
// Compute vote hash
|
|
vote.vote_hash = compute_vote_hash(&vote);
|
|
let vote_bytes = vote.encode_to_vec();
|
|
vote.signature = signer
|
|
.local_sign_message(&vote_bytes)
|
|
.await
|
|
.expect("Failed to sign vote");
|
|
|
|
let result = consensus_service
|
|
.process_incoming_vote(group_name, vote.clone())
|
|
.await;
|
|
|
|
result.expect("Failed to process vote");
|
|
}
|
|
|
|
// With 4 out of 5 votes, we should have sufficient votes for consensus
|
|
assert!(
|
|
consensus_service
|
|
.has_sufficient_votes(group_name, proposal.proposal_id)
|
|
.await
|
|
);
|
|
|
|
// Subscribe to consensus events and wait for natural consensus
|
|
let proposal_id = proposal.proposal_id;
|
|
let group_name_clone = group_name;
|
|
|
|
// Wait for consensus event with timeout
|
|
let timeout_duration = Duration::from_secs(15);
|
|
let consensus_result = tokio::time::timeout(timeout_duration, async {
|
|
while let Ok((event_group_name, event)) = consensus_events.recv().await {
|
|
if event_group_name == group_name_clone {
|
|
match event {
|
|
ConsensusEvent::ConsensusReached {
|
|
proposal_id: event_proposal_id,
|
|
result,
|
|
} => {
|
|
if event_proposal_id == proposal_id {
|
|
println!("Consensus reached for proposal {proposal_id}: {result}");
|
|
return Ok(result);
|
|
}
|
|
}
|
|
ConsensusEvent::ConsensusFailed {
|
|
proposal_id: event_proposal_id,
|
|
reason,
|
|
} => {
|
|
if event_proposal_id == proposal_id {
|
|
println!("Consensus failed for proposal {proposal_id}: {reason}");
|
|
return Err(format!("Consensus failed: {reason}"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err("Event channel closed".to_string())
|
|
})
|
|
.await
|
|
.expect("Timeout waiting for consensus event")
|
|
.expect("Consensus should succeed");
|
|
|
|
// Should have consensus result based on 2n/3 threshold
|
|
assert!(consensus_result); // All votes were true, so result should be true
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_remove_group_sessions() {
|
|
let consensus_service = ConsensusService::new();
|
|
|
|
let group_name = "test_group_remove";
|
|
let expected_voters_count = 2;
|
|
let signer = PrivateKeySigner::random();
|
|
let proposal_owner = signer.address_bytes();
|
|
|
|
// Create a proposal
|
|
let proposal = consensus_service
|
|
.create_proposal(
|
|
group_name,
|
|
"Test Proposal".to_string(),
|
|
vec![],
|
|
proposal_owner,
|
|
expected_voters_count,
|
|
300,
|
|
true,
|
|
)
|
|
.await
|
|
.expect("Failed to create proposal");
|
|
|
|
let _proposal = consensus_service
|
|
.vote_on_proposal(group_name, proposal.proposal_id, true, signer)
|
|
.await
|
|
.expect("Failed to vote on proposal");
|
|
|
|
// Verify proposal exists
|
|
let group_stats = consensus_service.get_group_stats(group_name).await;
|
|
assert_eq!(group_stats.total_sessions, 1);
|
|
|
|
// Remove group sessions
|
|
consensus_service.remove_group_sessions(group_name).await;
|
|
|
|
// Verify group sessions are removed
|
|
let group_stats_after = consensus_service.get_group_stats(group_name).await;
|
|
assert_eq!(group_stats_after.total_sessions, 0);
|
|
|
|
// Verify group is not in active groups
|
|
let active_groups = consensus_service.get_active_groups().await;
|
|
assert!(!active_groups.contains(&group_name.to_string()));
|
|
}
|