mirror of
https://github.com/vacp2p/de-mls.git
synced 2026-01-08 21:17:56 -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.
600 lines
21 KiB
Rust
600 lines
21 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_realtime_consensus_waiting() {
|
|
// Create consensus service
|
|
let consensus_service = ConsensusService::new();
|
|
|
|
let group_name = "test_group_realtime";
|
|
let expected_voters_count = 3;
|
|
|
|
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");
|
|
|
|
println!("Created proposal with ID: {}", proposal.proposal_id);
|
|
|
|
// Subscribe to consensus events
|
|
let mut consensus_events = consensus_service.subscribe_to_events();
|
|
let proposal_id = proposal.proposal_id;
|
|
|
|
// Start a background task that waits for consensus events
|
|
let group_name_clone = group_name;
|
|
let consensus_waiter = tokio::spawn(async move {
|
|
println!("Starting consensus event waiter for proposal {proposal_id:?}");
|
|
|
|
// Wait for consensus event with timeout
|
|
let timeout_duration = Duration::from_secs(10);
|
|
match 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
|
|
{
|
|
Ok(result) => {
|
|
println!("Consensus event waiter result: {result:?}");
|
|
result
|
|
}
|
|
Err(_) => {
|
|
println!("Consensus event waiter timed out");
|
|
Err("Timeout waiting for consensus".to_string())
|
|
}
|
|
}
|
|
});
|
|
|
|
// Wait a bit to ensure the waiter is running
|
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
|
|
// Add votes to reach consensus
|
|
let mut previous_vote_hash = proposal.votes[0].vote_hash.clone(); // Start with steward's vote hash
|
|
|
|
for i in 1..expected_voters_count {
|
|
let signer = PrivateKeySigner::random();
|
|
let proposal_owner = signer.address_bytes();
|
|
let mut vote = Vote {
|
|
vote_id: Uuid::new_v4().as_u128() as u32,
|
|
vote_owner: proposal_owner,
|
|
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: previous_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");
|
|
|
|
println!("Adding vote {} for proposal {}", i, proposal.proposal_id);
|
|
consensus_service
|
|
.process_incoming_vote(group_name, vote.clone())
|
|
.await
|
|
.expect("Failed to process vote");
|
|
|
|
// Update previous vote hash for next iteration
|
|
previous_vote_hash = vote.vote_hash.clone();
|
|
|
|
// Small delay between votes
|
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
}
|
|
|
|
// Wait for consensus result
|
|
let consensus_result = consensus_waiter
|
|
.await
|
|
.expect("Consensus waiter task failed");
|
|
|
|
// Verify consensus was reached
|
|
assert!(consensus_result.is_ok());
|
|
let result = consensus_result.unwrap();
|
|
assert!(result); // Should be true (yes votes)
|
|
|
|
println!("Test completed successfully - consensus reached!");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_consensus_timeout() {
|
|
// Create consensus service
|
|
let consensus_service = ConsensusService::new();
|
|
|
|
let group_name = "test_group_timeout";
|
|
let expected_voters_count = 5;
|
|
let signer = PrivateKeySigner::random();
|
|
let proposal_owner = signer.address_bytes();
|
|
|
|
// Need 4 votes for consensus
|
|
// 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");
|
|
|
|
println!("Created proposal with ID: {}", proposal.proposal_id);
|
|
|
|
// Subscribe to consensus events for timeout test
|
|
let mut consensus_events = consensus_service.subscribe_to_events();
|
|
let proposal_id = proposal.proposal_id;
|
|
|
|
// Start consensus event waiter with timeout
|
|
let group_name_clone = group_name;
|
|
let consensus_waiter = tokio::spawn(async move {
|
|
println!("Starting consensus event waiter with timeout for proposal {proposal_id:?}");
|
|
|
|
// Wait for consensus event - should timeout and trigger liveness criteria
|
|
let timeout_duration = Duration::from_secs(12); // Wait longer than consensus timeout (10s)
|
|
match 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} (via timeout/liveness criteria)");
|
|
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 {
|
|
Ok(result) => result,
|
|
Err(_) => Err("Test timeout waiting for consensus event".to_string())
|
|
}
|
|
});
|
|
|
|
// Don't add any additional votes - should timeout and apply liveness criteria
|
|
|
|
// Wait for consensus result
|
|
let consensus_result = consensus_waiter
|
|
.await
|
|
.expect("Consensus waiter task failed");
|
|
|
|
// Verify timeout occurred and liveness criteria was applied
|
|
// With liveness_criteria_yes = true, should return Ok(true)
|
|
assert!(consensus_result.is_ok());
|
|
let result = consensus_result.unwrap();
|
|
assert!(result); // Should be true due to liveness criteria
|
|
|
|
println!("Test completed successfully - timeout occurred and liveness criteria applied!");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_consensus_with_mixed_votes() {
|
|
// Create consensus service
|
|
let consensus_service = ConsensusService::new();
|
|
let signer = PrivateKeySigner::random();
|
|
let proposal_owner = signer.address_bytes();
|
|
|
|
let group_name = "test_group_mixed";
|
|
let expected_voters_count = 3;
|
|
|
|
// 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");
|
|
|
|
println!("Created proposal with ID: {}", proposal.proposal_id);
|
|
|
|
// Subscribe to consensus events
|
|
let mut consensus_events = consensus_service.subscribe_to_events();
|
|
let proposal_id = proposal.proposal_id;
|
|
|
|
// Start a background task that waits for consensus events
|
|
let group_name_clone = group_name;
|
|
let consensus_waiter = tokio::spawn(async move {
|
|
println!("Starting consensus event waiter for proposal {proposal_id:?}");
|
|
|
|
// Wait for consensus event with timeout
|
|
let timeout_duration = Duration::from_secs(15); // Allow time for votes to be processed
|
|
match 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
|
|
{
|
|
Ok(result) => {
|
|
println!("Consensus event waiter result: {result:?}");
|
|
result
|
|
}
|
|
Err(_) => {
|
|
println!("Consensus event waiter timed out");
|
|
Err("Timeout waiting for consensus".to_string())
|
|
}
|
|
}
|
|
});
|
|
|
|
// Wait a bit to ensure the waiter is running
|
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
|
|
// Add mixed votes: one yes, one no
|
|
let votes = vec![(2, false), (3, false)];
|
|
let mut previous_vote_hash = proposal.votes[0].vote_hash.clone(); // Start with steward's vote hash
|
|
|
|
for (i, vote_value) in votes {
|
|
let signer = PrivateKeySigner::random();
|
|
let proposal_owner = signer.address_bytes();
|
|
let mut vote = Vote {
|
|
vote_id: Uuid::new_v4().as_u128() as u32,
|
|
vote_owner: proposal_owner,
|
|
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: vote_value,
|
|
parent_hash: Vec::new(),
|
|
received_hash: previous_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");
|
|
|
|
println!(
|
|
"Adding vote {} (value: {}) for proposal {}",
|
|
i, vote_value, proposal.proposal_id
|
|
);
|
|
consensus_service
|
|
.process_incoming_vote(group_name, vote.clone())
|
|
.await
|
|
.expect("Failed to process vote");
|
|
|
|
// Update previous vote hash for next iteration
|
|
previous_vote_hash = vote.vote_hash.clone();
|
|
|
|
// Small delay between votes
|
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
}
|
|
|
|
// Wait for consensus result
|
|
let consensus_result = consensus_waiter
|
|
.await
|
|
.expect("Consensus waiter task failed");
|
|
|
|
// Verify consensus was reached
|
|
assert!(consensus_result.is_ok());
|
|
let result = consensus_result.unwrap();
|
|
// With 2 no votes and 1 yes vote, consensus should be no (false)
|
|
// However, if it times out, liveness criteria (true) will be applied
|
|
println!("Mixed votes test result: {result}");
|
|
// Don't assert specific result since it depends on timing vs. liveness criteria
|
|
|
|
println!("Test completed successfully - consensus reached with mixed votes!");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_rfc_vote_chain_validation() {
|
|
use de_mls::consensus::compute_vote_hash;
|
|
use de_mls::LocalSigner;
|
|
|
|
// Create consensus service
|
|
let consensus_service = ConsensusService::new();
|
|
|
|
let group_name = "test_rfc_validation";
|
|
let expected_voters_count = 3;
|
|
|
|
let signer1 = PrivateKeySigner::random();
|
|
let signer2 = PrivateKeySigner::random();
|
|
let _signer3 = PrivateKeySigner::random();
|
|
|
|
// Create first proposal with steward vote
|
|
let proposal = consensus_service
|
|
.create_proposal(
|
|
group_name,
|
|
"Test Proposal".to_string(),
|
|
vec![],
|
|
signer1.address_bytes(),
|
|
expected_voters_count,
|
|
300,
|
|
true,
|
|
)
|
|
.await
|
|
.expect("Failed to create proposal");
|
|
|
|
let proposal = consensus_service
|
|
.vote_on_proposal(group_name, proposal.proposal_id, true, signer1)
|
|
.await
|
|
.expect("Failed to vote on proposal");
|
|
|
|
println!("Created proposal with ID: {}", proposal.proposal_id);
|
|
|
|
// Create second vote from different voter
|
|
let mut vote2 = Vote {
|
|
vote_id: Uuid::new_v4().as_u128() as u32,
|
|
vote_owner: signer2.address_bytes(),
|
|
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(), // Different voter, no parent
|
|
received_hash: proposal.votes[0].vote_hash.clone(), // Should be hash of first vote
|
|
vote_hash: Vec::new(),
|
|
signature: Vec::new(),
|
|
};
|
|
|
|
// Compute vote hash and signature
|
|
vote2.vote_hash = compute_vote_hash(&vote2);
|
|
let vote2_bytes = vote2.encode_to_vec();
|
|
vote2.signature = signer2
|
|
.local_sign_message(&vote2_bytes)
|
|
.await
|
|
.expect("Failed to sign vote");
|
|
|
|
// Create proposal with two votes from different voters
|
|
let mut test_proposal = proposal.clone();
|
|
test_proposal.votes.push(vote2.clone());
|
|
|
|
// Validate the proposal - should pass RFC validation
|
|
let validation_result = consensus_service.validate_proposal(&test_proposal);
|
|
assert!(
|
|
validation_result.is_ok(),
|
|
"RFC validation should pass: {validation_result:?}"
|
|
);
|
|
|
|
// Test invalid vote chain (wrong received_hash)
|
|
let mut invalid_proposal = test_proposal.clone();
|
|
invalid_proposal.votes[1].received_hash = vec![0; 32]; // Wrong hash
|
|
|
|
let invalid_result = consensus_service.validate_proposal(&invalid_proposal);
|
|
assert!(
|
|
invalid_result.is_err(),
|
|
"Invalid vote chain should be rejected"
|
|
);
|
|
|
|
println!("RFC vote chain validation test completed successfully!");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_event_driven_timeout() {
|
|
// Create consensus service
|
|
let consensus_service = ConsensusService::new();
|
|
|
|
let group_name = "test_group_event_timeout";
|
|
let expected_voters_count = 3;
|
|
let signer = PrivateKeySigner::random();
|
|
let proposal_owner = signer.address_bytes();
|
|
|
|
// Create a proposal with only one vote (steward vote) - should timeout and apply liveness criteria
|
|
let proposal = consensus_service
|
|
.create_proposal(
|
|
group_name,
|
|
"Test Proposal".to_string(),
|
|
vec![],
|
|
proposal_owner,
|
|
expected_voters_count,
|
|
300,
|
|
true, // liveness criteria = 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");
|
|
|
|
println!(
|
|
"Created proposal with ID: {} - waiting for timeout",
|
|
proposal.proposal_id
|
|
);
|
|
|
|
// Subscribe to consensus events
|
|
let mut consensus_events = consensus_service.subscribe_to_events();
|
|
let proposal_id = proposal.proposal_id;
|
|
let group_name_clone = group_name;
|
|
|
|
// Wait for consensus event (should timeout after 10 seconds and apply liveness criteria)
|
|
let timeout_duration = Duration::from_secs(12); // Wait longer than consensus timeout (10s)
|
|
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} (via timeout/liveness criteria)");
|
|
return result;
|
|
}
|
|
}
|
|
ConsensusEvent::ConsensusFailed {
|
|
proposal_id: event_proposal_id,
|
|
reason,
|
|
} => {
|
|
if event_proposal_id == proposal_id {
|
|
panic!("Consensus failed for proposal {proposal_id}: {reason}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
panic!("Event channel closed unexpectedly");
|
|
})
|
|
.await
|
|
.expect("Timeout waiting for consensus event");
|
|
|
|
// Should be true due to liveness criteria
|
|
assert!(consensus_result);
|
|
|
|
println!("Test completed successfully - event-driven timeout worked!");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_liveness_criteria_functionality() {
|
|
// Create consensus service
|
|
let consensus_service = ConsensusService::new();
|
|
|
|
let group_name = "test_group_liveness";
|
|
let expected_voters_count = 3;
|
|
let signer = PrivateKeySigner::random();
|
|
let proposal_owner = signer.address_bytes();
|
|
|
|
// Test liveness criteria = false
|
|
let proposal_false = consensus_service
|
|
.create_proposal(
|
|
group_name,
|
|
"Test Proposal False".to_string(),
|
|
vec![],
|
|
proposal_owner.clone(),
|
|
expected_voters_count,
|
|
300,
|
|
false, // liveness criteria = false
|
|
)
|
|
.await
|
|
.expect("Failed to create proposal with liveness_criteria_yes = false");
|
|
|
|
// Test liveness criteria getter
|
|
let liveness_false = consensus_service
|
|
.get_proposal_liveness_criteria(group_name, proposal_false.proposal_id)
|
|
.await;
|
|
assert_eq!(liveness_false, Some(false));
|
|
|
|
// Test liveness criteria = true
|
|
let proposal_true = consensus_service
|
|
.create_proposal(
|
|
group_name,
|
|
"Test Proposal True".to_owned(),
|
|
vec![],
|
|
proposal_owner,
|
|
expected_voters_count,
|
|
300,
|
|
true, // liveness criteria = true
|
|
)
|
|
.await
|
|
.expect("Failed to create proposal with liveness_criteria_yes = true");
|
|
|
|
// Test liveness criteria getter
|
|
let liveness_true = consensus_service
|
|
.get_proposal_liveness_criteria(group_name, proposal_true.proposal_id)
|
|
.await;
|
|
assert_eq!(liveness_true, Some(true));
|
|
|
|
// Test non-existent proposal
|
|
let liveness_none = consensus_service
|
|
.get_proposal_liveness_criteria("nonexistent", 99999)
|
|
.await;
|
|
assert_eq!(liveness_none, None);
|
|
|
|
println!("Test completed successfully - liveness criteria functionality verified!");
|
|
}
|