From b6a80c9b7d067fb4d078e7f4d3b67f2aca81ab3d Mon Sep 17 00:00:00 2001 From: Vecna Date: Tue, 30 Apr 2024 01:30:37 -0400 Subject: [PATCH] First iteration of users for simulation I think this is designed in a way that will make it very hard to parallelize later. I should fix that when I can. --- Cargo.toml | 3 +- src/lib.rs | 2 + src/simulation/state.rs | 20 +++ src/simulation/user.rs | 349 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 373 insertions(+), 1 deletion(-) create mode 100644 src/simulation/state.rs create mode 100644 src/simulation/user.rs diff --git a/Cargo.toml b/Cargo.toml index 98edbf3..8039a7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ hyper-rustls = "0.26.0" hyper-util = { version = "0.1", features = ["full"] } julianday = "1.2.0" lazy_static = "1" +lox_cli = { path = "../lox_cli", version = "0.1", optional = true } lox-library = { git = "https://gitlab.torproject.org/vecna/lox.git", version = "0.1.0" } nalgebra = "0.29" rand = { version = "0.8" } @@ -42,4 +43,4 @@ x25519-dalek = { version = "2", features = ["serde", "static_secrets"] } base64 = "0.21.7" [features] -simulation = [] +simulation = ["lox_cli"] diff --git a/src/lib.rs b/src/lib.rs index c8eab58..50a9c22 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,8 @@ pub mod request_handler; #[cfg(any(test, feature = "simulation"))] pub mod simulation { pub mod extra_infos_server; + pub mod state; + pub mod user; } use analysis::Analyzer; diff --git a/src/simulation/state.rs b/src/simulation/state.rs new file mode 100644 index 0000000..e05f88e --- /dev/null +++ b/src/simulation/state.rs @@ -0,0 +1,20 @@ +use lox_cli::{networking::*, *}; +use lox_library::IssuerPubKey; +use std::collections::HashMap; +use x25519_dalek::PublicKey; + +pub struct State { + pub la_pubkeys: Vec, + pub net: HyperNet, + pub net_test: HyperNet, + pub net_tp: HyperNet, + // Probability that if Alice invites Bob, Alice and Bob are in the same + // country. This is in *addition* to the regular probability that Bob is in + // that country by random selection. + pub prob_friend_in_same_country: f64, + pub prob_user_invites_friend: f64, + pub prob_user_is_censor: f64, + pub prob_user_submits_reports: f64, + pub probs_user_in_country: Vec<(String, f64)>, + pub tp_pubkeys: HashMap, +} diff --git a/src/simulation/user.rs b/src/simulation/user.rs new file mode 100644 index 0000000..d516ea5 --- /dev/null +++ b/src/simulation/user.rs @@ -0,0 +1,349 @@ +// User behavior in simulation + +use crate::{ + get_date, negative_report::NegativeReport, positive_report::PositiveReport, + simulation::state::State, BridgeDistributor, +}; +use lox_cli::{networking::*, *}; +use lox_library::{ + bridge_table::{BridgeLine, MAX_BRIDGES_PER_BUCKET}, + cred::{Invitation, Lox}, + proto::check_blockage::MIN_TRUST_LEVEL, + scalar_u32, IssuerPubKey, +}; +use rand::Rng; + +pub struct User { + // Does this user cooperate with a censor? + censor: bool, + + // 2-character country code + country: String, + + // The user always has a primary credential. If this credential's bucket is + // blocked, the user may replace it or temporarily hold two credentials + // while waiting to migrate from the primary credential. + primary_cred: Lox, + secondary_cred: Option, + + // Does the user submit reports to Troll Patrol? + submits_reports: bool, +} + +impl User { + pub async fn new(state: &State) -> Self { + let (cred, bl) = get_lox_credential( + &state.net, + &get_open_invitation(&state.net).await, + get_lox_pub(&state.la_pubkeys), + ) + .await; + + // Probabilistically decide whether this user cooperates with a censor + let mut rng = rand::thread_rng(); + let num: f64 = rng.gen_range(0.0..1.0); + let censor = num < state.prob_user_is_censor; + + // Probabilistically decide whether this user submits reports + let num: f64 = rng.gen_range(0.0..1.0); + let submits_reports = num < state.prob_user_submits_reports; + + // Probabilistically decide user's country + let num: f64 = rng.gen_range(0.0..1.0); + let cc = { + let mut cc = String::default(); + for (country, prob) in &state.probs_user_in_country { + let mut prob = *prob; + if prob < num { + cc = country.to_string(); + break; + } else { + prob -= num; + } + } + cc + }; + + Self { + censor: censor, + country: cc, + primary_cred: cred, + secondary_cred: None, + submits_reports: submits_reports, + } + } + + // TODO: This should probably return an actual error type + pub async fn invite(&mut self, state: &State) -> Result { + let etable = get_reachability_credential(&state.net).await; + let (new_cred, invite) = issue_invite( + &state.net, + &self.primary_cred, + &etable, + get_lox_pub(&state.la_pubkeys), + get_reachability_pub(&state.la_pubkeys), + get_invitation_pub(&state.la_pubkeys), + ) + .await; + self.primary_cred = new_cred; + let (friend_cred, bucket) = redeem_invite( + &state.net, + &invite, + get_lox_pub(&state.la_pubkeys), + get_invitation_pub(&state.la_pubkeys), + ) + .await; + + // Probabilistically decide whether this user cooperates with a censor + // We do not influence this by the inviting friend's status. Anyone + // might have friends who are untrustworthy, and censors may invite + // non-censors to maintain an illusion of trustworthiness. Also, a + // "censor" user may not be knowingly helping a censor. + let mut rng = rand::thread_rng(); + let num: f64 = rng.gen_range(0.0..1.0); + let censor = num < state.prob_user_is_censor; + + // Probabilistically decide whether this user submits reports + let num: f64 = rng.gen_range(0.0..1.0); + let submits_reports = num < state.prob_user_submits_reports; + + // Determine user's country + let num: f64 = rng.gen_range(0.0..1.0); + let cc = if num < state.prob_friend_in_same_country { + self.country.to_string() + } else { + // Probabilistically decide user's country + let mut num: f64 = rng.gen_range(0.0..1.0); + let mut cc = String::default(); + for (country, prob) in &state.probs_user_in_country { + let prob = *prob; + if prob < num { + cc = country.to_string(); + break; + } else { + num -= prob; + } + } + cc + }; + + Ok(Self { + censor: censor, + country: cc, + primary_cred: friend_cred, + secondary_cred: None, + submits_reports: submits_reports, + }) + } + + // Attempt to "connect" to the bridge, returns true if successful + pub fn connect(&self, bridge: &BridgeLine) -> bool { + true + } + + pub async fn send_negative_reports(state: &State, reports: Vec) { + let date = get_date(); + let pubkey = state.tp_pubkeys.get(&date).unwrap(); + for report in reports { + state + .net_tp + .request( + "/negativereport".to_string(), + bincode::serialize(&report.encrypt(&pubkey)).unwrap(), + ) + .await; + } + } + + pub async fn send_positive_reports(state: &State, reports: Vec) { + for report in reports { + state + .net_tp + .request("/positivereport".to_string(), report.to_json().into_bytes()) + .await; + } + } + + // User performs daily connection attempts, etc. and returns a vector of + // newly invited friends and a vector of fingerprints of successfully + // contacted bridges. + pub async fn daily_tasks(&mut self, state: &State) -> (Vec, Vec<[u8; 20]>) { + // Download bucket to see if bridge is still reachable + // (We assume that this step can be done even if the user can't actually + // talk to the LA.) + let (bucket, reachcred) = get_bucket(&state.net, &self.primary_cred).await; + let level = scalar_u32(&self.primary_cred.trust_level).unwrap(); + + // Can we level up the main credential? + let can_level_up = reachcred.is_some() + && (level == 0 && eligible_for_trust_promotion(&state.net, &self.primary_cred).await + || level > 0 && eligible_for_level_up(&state.net, &self.primary_cred).await); + + // Can we migrate the main credential? + let can_migrate = reachcred.is_none() && level >= MIN_TRUST_LEVEL; + + // Can we level up the secondary credential? + let mut second_level_up = false; + + let mut failed = Vec::::new(); + let mut succeeded = Vec::::new(); + for i in 0..bucket.len() { + // At level 0, we only have 1 bridge + if (level > 0 || i == 0) && self.connect(&bucket[i]) { + if self.submits_reports && level >= 3 { + succeeded.push(bucket[i]); + } + break; + } else { + if self.submits_reports { + failed.push(bucket[i]); + } + } + } + let second_cred = if succeeded.len() < 1 { + if self.secondary_cred.is_some() { + std::mem::replace(&mut self.secondary_cred, None) + } else { + // Get new credential + let (cred, bl) = get_lox_credential( + &state.net, + &get_open_invitation(&state.net).await, + get_lox_pub(&state.la_pubkeys), + ) + .await; + Some(cred) + } + } else { + // If we're able to connect with the primary credential, don't + // keep a secondary one. + None + }; + if second_cred.is_some() { + let second_cred = second_cred.as_ref().unwrap(); + let (second_bucket, second_reachcred) = get_bucket(&state.net, &second_cred).await; + if self.connect(&second_bucket[0]) { + succeeded.push(second_bucket[0]); + if second_reachcred.is_some() + && eligible_for_trust_promotion(&state.net, &second_cred).await + { + second_level_up = true; + } + } else { + failed.push(second_bucket[0]); + } + } + + let mut negative_reports = Vec::::new(); + let mut positive_reports = Vec::::new(); + if self.submits_reports { + for bridge in &failed { + negative_reports.push(NegativeReport::from_bridgeline( + *bridge, + self.country.to_string(), + BridgeDistributor::Lox, + )); + } + if level >= 3 { + for bridge in &succeeded { + positive_reports.push( + PositiveReport::from_lox_credential( + bridge.fingerprint, + None, + &self.primary_cred, + get_lox_pub(&state.la_pubkeys), + self.country.to_string(), + ) + .unwrap(), + ); + } + } + } + + // We might restrict these steps to succeeded.len() > 0, but we do + // assume the user can contact the LA somehow, so let's just allow it. + if can_level_up { + let cred = level_up( + &state.net, + &self.primary_cred, + &reachcred.unwrap(), + get_lox_pub(&state.la_pubkeys), + get_reachability_pub(&state.la_pubkeys), + ) + .await; + self.primary_cred = cred; + self.secondary_cred = None; + } + // We favor starting over at level 1 to migrating + else if second_level_up { + let second_cred = second_cred.as_ref().unwrap(); + let cred = trust_migration( + &state.net, + &second_cred, + &trust_promotion(&state.net, &second_cred, get_lox_pub(&state.la_pubkeys)).await, + get_lox_pub(&state.la_pubkeys), + get_migration_pub(&state.la_pubkeys), + ) + .await; + self.primary_cred = cred; + self.secondary_cred = None; + } else if can_migrate { + let cred = blockage_migration( + &state.net, + &self.primary_cred, + &check_blockage( + &state.net, + &self.primary_cred, + get_lox_pub(&state.la_pubkeys), + ) + .await, + get_lox_pub(&state.la_pubkeys), + get_migration_pub(&state.la_pubkeys), + ) + .await; + self.primary_cred = cred; + self.secondary_cred = None; + } else if second_cred.is_some() { + // Couldn't connect with primary credential + if succeeded.len() > 0 { + // Keep the second credential only if it's useful + self.secondary_cred = second_cred; + } + } + + if negative_reports.len() > 0 { + Self::send_negative_reports(&state, negative_reports).await; + } + if positive_reports.len() > 0 { + Self::send_positive_reports(&state, positive_reports).await; + } + + // Invite friends if applicable + let invitations = scalar_u32(&self.primary_cred.invites_remaining).unwrap(); + let mut new_friends = Vec::::new(); + for i in 0..invitations { + let mut rng = rand::thread_rng(); + let num: f64 = rng.gen_range(0.0..1.0); + if num < state.prob_user_invites_friend { + match self.invite(&state).await { + Ok(friend) => { + // You really shouldn't push your friends, especially + // new ones whose boundaries you might not know well. + new_friends.push(friend); + } + Err(e) => { + println!("{}", e); + } + } + } + } + + // List of fingerprints we contacted. This should not actually be more + // than one. + let mut connections = Vec::<[u8; 20]>::new(); + for bridge in succeeded { + connections.push(bridge.get_hashed_fingerprint()); + } + + (new_friends, connections) + } +}