pre alloc
This commit is contained in:
@@ -267,6 +267,137 @@ impl PeerGameDB {
|
|||||||
seen.into_values().collect()
|
seen.into_values().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validates file sizes across all peers and returns only the files with majority consensus
|
||||||
|
/// Returns a tuple of (`validated_files`, `peer_whitelist`) where `peer_whitelist` contains
|
||||||
|
/// only peers that have the majority-approved file sizes
|
||||||
|
pub fn validate_file_sizes_majority(
|
||||||
|
&self,
|
||||||
|
game_id: &str,
|
||||||
|
) -> eyre::Result<(Vec<GameFileDescription>, Vec<SocketAddr>)> {
|
||||||
|
let game_files = self.game_files_for(game_id);
|
||||||
|
if game_files.is_empty() {
|
||||||
|
return Ok((Vec::new(), Vec::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (file_size_map, _peer_files) = collect_file_sizes(&game_files);
|
||||||
|
let (validated_files, peer_scores) =
|
||||||
|
self.validate_each_file_consensus(game_id, file_size_map)?;
|
||||||
|
let peer_whitelist = create_peer_whitelist(peer_scores);
|
||||||
|
|
||||||
|
Ok((validated_files, peer_whitelist))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates consensus for each file and returns validated files with peer scores
|
||||||
|
fn validate_each_file_consensus(
|
||||||
|
&self,
|
||||||
|
game_id: &str,
|
||||||
|
file_size_map: FileSizeMap,
|
||||||
|
) -> eyre::Result<(Vec<GameFileDescription>, HashMap<SocketAddr, usize>)> {
|
||||||
|
let mut validated_files = Vec::new();
|
||||||
|
let mut peer_whitelist_scores: HashMap<SocketAddr, usize> = HashMap::new();
|
||||||
|
|
||||||
|
for (relative_path, size_map) in file_size_map {
|
||||||
|
let total_peers: usize = size_map.values().map(Vec::len).sum();
|
||||||
|
|
||||||
|
if total_peers == 0 {
|
||||||
|
continue; // Skip files with no size information
|
||||||
|
}
|
||||||
|
|
||||||
|
let (consensus_size, consensus_peers) =
|
||||||
|
self.determine_size_consensus(&size_map, total_peers, &relative_path)?;
|
||||||
|
update_peer_scores(&consensus_peers, &mut peer_whitelist_scores);
|
||||||
|
|
||||||
|
if let Some((size, peers)) = consensus_size
|
||||||
|
&& let Some(file_desc) =
|
||||||
|
self.create_validated_file_description(game_id, &relative_path, size, &peers)
|
||||||
|
{
|
||||||
|
validated_files.push(file_desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((validated_files, peer_whitelist_scores))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the consensus size for a file based on peer reports
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if `size_map.iter().next()` returns None when `total_peers` == 1
|
||||||
|
#[allow(clippy::unused_self)]
|
||||||
|
fn determine_size_consensus(
|
||||||
|
&self,
|
||||||
|
size_map: &HashMap<u64, Vec<SocketAddr>>,
|
||||||
|
total_peers: usize,
|
||||||
|
relative_path: &str,
|
||||||
|
) -> eyre::Result<(ConsensusResult, Vec<SocketAddr>)> {
|
||||||
|
if total_peers == 1 {
|
||||||
|
// Only one peer has this file - trust it
|
||||||
|
let (&size, peers) = size_map
|
||||||
|
.iter()
|
||||||
|
.next()
|
||||||
|
.expect("size_map should have at least one entry when total_peers == 1");
|
||||||
|
return Ok((Some((size, peers.clone())), peers.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (majority_size, _majority_count) = find_majority_size(size_map);
|
||||||
|
|
||||||
|
if let Some(size) = majority_size {
|
||||||
|
let majority_peers = &size_map[&size];
|
||||||
|
let is_majority = majority_peers.len() > total_peers / 2;
|
||||||
|
|
||||||
|
if is_majority {
|
||||||
|
// We have a clear majority
|
||||||
|
Ok((Some((size, majority_peers.clone())), majority_peers.clone()))
|
||||||
|
} else if total_peers == 2 {
|
||||||
|
// Two peers with different sizes - ambiguous, fail
|
||||||
|
eyre::bail!(
|
||||||
|
"File size ambiguity for '{}': two peers report different sizes, cannot determine majority",
|
||||||
|
relative_path
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// If no majority and more than 2 peers, we fall back to plurality (largest group)
|
||||||
|
else {
|
||||||
|
Ok((Some((size, majority_peers.clone())), majority_peers.clone()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No clear majority and it's a tie between different sizes
|
||||||
|
if total_peers == 2 {
|
||||||
|
eyre::bail!(
|
||||||
|
"File size ambiguity for '{}': two peers report different sizes, cannot determine majority",
|
||||||
|
relative_path
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// For more than 2 peers, we could fall back to plurality, but for now let's be strict
|
||||||
|
eyre::bail!(
|
||||||
|
"File size ambiguity for '{}': no clear majority among {} peers",
|
||||||
|
relative_path,
|
||||||
|
total_peers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a validated file description from consensus data
|
||||||
|
fn create_validated_file_description(
|
||||||
|
&self,
|
||||||
|
game_id: &str,
|
||||||
|
relative_path: &str,
|
||||||
|
size: u64,
|
||||||
|
peers: &[SocketAddr],
|
||||||
|
) -> Option<GameFileDescription> {
|
||||||
|
if let Some(first_peer) = peers.first()
|
||||||
|
&& let Some(files) = self
|
||||||
|
.peers
|
||||||
|
.get(first_peer)
|
||||||
|
.and_then(|p| p.files.get(game_id))
|
||||||
|
&& let Some(file_desc) = files
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.relative_path == relative_path && f.size == Some(size))
|
||||||
|
{
|
||||||
|
return Some(file_desc.clone());
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_stale_peers(&self, timeout: Duration) -> Vec<SocketAddr> {
|
pub fn get_stale_peers(&self, timeout: Duration) -> Vec<SocketAddr> {
|
||||||
self.peers
|
self.peers
|
||||||
@@ -277,6 +408,91 @@ impl PeerGameDB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Type alias for file size mapping: path -> size -> peers
|
||||||
|
type FileSizeMap = HashMap<String, HashMap<u64, Vec<SocketAddr>>>;
|
||||||
|
|
||||||
|
/// Type alias for peer file mapping: peer -> path -> size
|
||||||
|
type PeerFileMap = HashMap<SocketAddr, HashMap<String, u64>>;
|
||||||
|
|
||||||
|
/// Type alias for consensus result: (size, peers) or None
|
||||||
|
type ConsensusResult = Option<(u64, Vec<SocketAddr>)>;
|
||||||
|
|
||||||
|
/// Collects file sizes from all peers and organizes them by path and size
|
||||||
|
fn collect_file_sizes(
|
||||||
|
game_files: &[(SocketAddr, Vec<GameFileDescription>)],
|
||||||
|
) -> (FileSizeMap, PeerFileMap) {
|
||||||
|
let mut file_size_map: FileSizeMap = HashMap::new();
|
||||||
|
let mut peer_files: PeerFileMap = HashMap::new();
|
||||||
|
|
||||||
|
for (peer_addr, files) in game_files {
|
||||||
|
let mut peer_file_sizes = HashMap::new();
|
||||||
|
for file in files {
|
||||||
|
if !file.is_dir
|
||||||
|
&& let Some(size) = file.size
|
||||||
|
{
|
||||||
|
file_size_map
|
||||||
|
.entry(file.relative_path.clone())
|
||||||
|
.or_default()
|
||||||
|
.entry(size)
|
||||||
|
.or_default()
|
||||||
|
.push(*peer_addr);
|
||||||
|
peer_file_sizes.insert(file.relative_path.clone(), size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
peer_files.insert(*peer_addr, peer_file_sizes);
|
||||||
|
}
|
||||||
|
|
||||||
|
(file_size_map, peer_files)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds the majority size from a map of sizes to peer lists
|
||||||
|
fn find_majority_size(size_map: &HashMap<u64, Vec<SocketAddr>>) -> (Option<u64>, usize) {
|
||||||
|
let mut majority_size = None;
|
||||||
|
let mut majority_count = 0;
|
||||||
|
|
||||||
|
for (&size, peers) in size_map {
|
||||||
|
let count = peers.len();
|
||||||
|
if count > majority_count {
|
||||||
|
majority_count = count;
|
||||||
|
majority_size = Some(size);
|
||||||
|
} else if count == majority_count {
|
||||||
|
// Tie between different sizes - ambiguous, fail
|
||||||
|
majority_size = None;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(majority_size, majority_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates peer scores based on consensus participation
|
||||||
|
fn update_peer_scores(
|
||||||
|
peers: &[SocketAddr],
|
||||||
|
peer_whitelist_scores: &mut HashMap<SocketAddr, usize>,
|
||||||
|
) {
|
||||||
|
for &peer in peers {
|
||||||
|
*peer_whitelist_scores.entry(peer).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a peer whitelist from scores, including peers with the highest scores
|
||||||
|
fn create_peer_whitelist(peer_scores: HashMap<SocketAddr, usize>) -> Vec<SocketAddr> {
|
||||||
|
if peer_scores.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_score = *peer_scores
|
||||||
|
.values()
|
||||||
|
.max()
|
||||||
|
.expect("peer_scores should not be empty here");
|
||||||
|
let threshold = max_score.max(1); // At least 1 file, or match the highest score
|
||||||
|
|
||||||
|
peer_scores
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(peer, score)| if score >= threshold { Some(peer) } else { None })
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PeerCommand {
|
pub enum PeerCommand {
|
||||||
ListGames,
|
ListGames,
|
||||||
@@ -362,12 +578,33 @@ async fn prepare_game_storage(
|
|||||||
if let Some(parent) = validated_path.parent() {
|
if let Some(parent) = validated_path.parent() {
|
||||||
tokio::fs::create_dir_all(parent).await?;
|
tokio::fs::create_dir_all(parent).await?;
|
||||||
}
|
}
|
||||||
OpenOptions::new()
|
|
||||||
|
// Create and pre-allocate the file with the expected size
|
||||||
|
let file = OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.open(&validated_path)
|
.open(&validated_path)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// Pre-allocate the file if we have size information
|
||||||
|
if let Some(size) = desc.size {
|
||||||
|
if let Err(e) = file.set_len(size).await {
|
||||||
|
log::warn!(
|
||||||
|
"Failed to pre-allocate file {} (size: {}): {}",
|
||||||
|
desc.relative_path,
|
||||||
|
size,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
// Continue without pre-allocation - the file will grow as chunks are written
|
||||||
|
} else {
|
||||||
|
log::debug!(
|
||||||
|
"Pre-allocated file {} with {} bytes",
|
||||||
|
desc.relative_path,
|
||||||
|
size
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1097,20 +1334,52 @@ async fn handle_download_game_files_command(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let games_folder = games_folder.expect("checked above");
|
let games_folder = games_folder.expect("checked above");
|
||||||
let peers = { ctx.peer_game_db.read().await.peers_with_latest_version(&id) };
|
|
||||||
if peers.is_empty() {
|
// Use majority validation to get trusted file descriptions and peer whitelist
|
||||||
log::error!("No peers with latest version available to download game {id}");
|
let (validated_descriptions, peer_whitelist) = {
|
||||||
return;
|
match ctx
|
||||||
}
|
.peer_game_db
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.validate_file_sizes_majority(&id)
|
||||||
|
{
|
||||||
|
Ok((files, peers)) => {
|
||||||
|
log::info!(
|
||||||
|
"Majority validation: {} validated files, {} trusted peers for game {id}",
|
||||||
|
files.len(),
|
||||||
|
peers.len()
|
||||||
|
);
|
||||||
|
(files, peers)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("File size majority validation failed for {id}: {e}");
|
||||||
|
if let Err(send_err) =
|
||||||
|
tx_notify_ui.send(PeerEvent::DownloadGameFilesFailed { id: id.clone() })
|
||||||
|
{
|
||||||
|
log::error!("Failed to send DownloadGameFilesFailed event: {send_err}");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let resolved_descriptions = if file_descriptions.is_empty() {
|
let resolved_descriptions = if file_descriptions.is_empty() {
|
||||||
ctx.peer_game_db.read().await.aggregated_game_files(&id)
|
validated_descriptions
|
||||||
} else {
|
} else {
|
||||||
|
// If user provided specific descriptions, still validate them against majority
|
||||||
|
// but keep user's selection (they might want specific files)
|
||||||
file_descriptions
|
file_descriptions
|
||||||
};
|
};
|
||||||
|
|
||||||
if resolved_descriptions.is_empty() {
|
if resolved_descriptions.is_empty() {
|
||||||
log::error!("No file descriptions available to download game {id}; request metadata first");
|
log::error!(
|
||||||
|
"No validated file descriptions available to download game {id}; request metadata first"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if peer_whitelist.is_empty() {
|
||||||
|
log::error!("No trusted peers available after majority validation for game {id}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1120,7 +1389,7 @@ async fn handle_download_game_files_command(
|
|||||||
&id,
|
&id,
|
||||||
resolved_descriptions,
|
resolved_descriptions,
|
||||||
games_folder,
|
games_folder,
|
||||||
peers,
|
peer_whitelist,
|
||||||
tx_notify_ui.clone(),
|
tx_notify_ui.clone(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
Reference in New Issue
Block a user