From 19fc667b36ecce4e7b8e2ae8efb740c05cb93fe0 Mon Sep 17 00:00:00 2001 From: Vecna Date: Tue, 18 Jun 2024 07:23:05 -0400 Subject: [PATCH] Move simulation code from Troll Patrol to its own repo --- Cargo.toml | 22 ++ LICENSE | 21 ++ README.md | 3 + src/bridge.rs | 84 +++++ src/censor.rs | 256 ++++++++++++++ src/config.rs | 33 ++ src/extra_infos_server.rs | 150 +++++++++ src/lib.rs | 5 + src/main.rs | 399 ++++++++++++++++++++++ src/user.rs | 682 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 1655 insertions(+) create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/bridge.rs create mode 100644 src/censor.rs create mode 100644 src/config.rs create mode 100644 src/extra_infos_server.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/user.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1e5894e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "lox-simulation" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0" +array-bytes = "6.2.0" +bincode = "1" +clap = { version = "4.4.14", features = ["derive"] } +hyper = { version = "0.14.28", features = ["full"] } +lox_cli = { git = "https://git-crysp.uwaterloo.ca/vvecna/lox_cli.git", version = "0.1" } +lox-library = { git = "https://gitlab.torproject.org/vecna/lox.git", version = "0.1.0" } +memory-stats = "1.0.0" +rand = "0.8" +serde = "1.0.197" +serde_json = "1.0" +tokio = { version = "1", features = ["full"] } +troll-patrol = { git = "https://git-crysp.uwaterloo.ca/vvecna/troll-patrol.git", version = "0.1.0", features = ["simulation"] } +x25519-dalek = { version = "2", features = ["serde"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bd8f280 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Ivy Vecna + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..95111a9 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Lox Simulation + +This is a simulation for evaluating Lox and Troll Patrol. diff --git a/src/bridge.rs b/src/bridge.rs new file mode 100644 index 0000000..93c4818 --- /dev/null +++ b/src/bridge.rs @@ -0,0 +1,84 @@ +use lox_library::bridge_table::BridgeLine; +use std::collections::BTreeMap; +use troll_patrol::{extra_info::ExtraInfo, get_date}; + +// The Bridge struct only tracks data for today +pub struct Bridge { + pub fingerprint: [u8; 20], + + // The following four values are Julian dates used to track + // accuracy of the Troll Patrol system. A value of 0 is used to + // indicate this event *has not happened yet*. Note that 0 is not a + // date we will ever encounter. + + // Date the bridge was first distributed to a user, i.e., the date + // we created this Bridge object + pub first_distributed: u32, + + // First date a censor blocked this bridge + pub first_blocked: u32, + + // First date Troll Patrol detected that this bridge was blocked + // (whether or not it was correct) + pub first_detected_blocked: u32, + + // First date Troll Patrol received a positive report for this + // bridge (for identifying stage three) + pub first_positive_report: u32, + + pub real_connections: u32, + pub total_connections: u32, +} + +impl Bridge { + pub fn new(fingerprint: &[u8; 20]) -> Self { + Self { + fingerprint: *fingerprint, + first_distributed: get_date(), + first_blocked: 0, + first_detected_blocked: 0, + first_positive_report: 0, + real_connections: 0, + total_connections: 0, + } + } + + pub fn from_bridge_line(bridgeline: &BridgeLine) -> Self { + Self::new(&bridgeline.get_hashed_fingerprint()) + } + + pub fn connect_real(&mut self) { + self.real_connections += 1; + self.total_connections += 1; + } + + pub fn connect_total(&mut self) { + self.total_connections += 1; + } + + // Let the censor simulate a bunch of connections at once + pub fn censor_flood(&mut self, num_connections: u32) { + self.total_connections += num_connections; + } + + // Generate an extra-info report for today + pub fn gen_extra_info(&self, country: &str) -> ExtraInfo { + let mut bridge_ips = BTreeMap::::new(); + // Round up to a multiple of 8 + let rounded_connection_count = + self.total_connections + 7 - (self.total_connections + 7) % 8; + //let rounded_connection_count = (self.total_connections + 7) / 8 * 8; + bridge_ips.insert(country.to_string(), rounded_connection_count); + ExtraInfo { + nickname: String::from("simulation-bridge"), + fingerprint: self.fingerprint, + date: get_date(), + bridge_ips, + } + } + + pub fn reset_for_tomorrow(&mut self) { + self.real_connections = 0; + self.total_connections = 0; + } +} diff --git a/src/censor.rs b/src/censor.rs new file mode 100644 index 0000000..8b0a463 --- /dev/null +++ b/src/censor.rs @@ -0,0 +1,256 @@ +use crate::{bridge::Bridge, config::Config}; + +use lox_cli::{get_lox_pub, networking::Networking}; +use lox_library::{cred::Lox, scalar_u32}; +use rand::Rng; +use serde::Deserialize; +use std::{ + cmp::min, + collections::{HashMap, HashSet}, +}; +use troll_patrol::{get_date, positive_report::PositiveReport}; + +pub struct Censor { + // If we have a bootstrapping period, the censor does not begin + // until this date. + pub start_date: u32, + + pub known_bridges: HashSet<[u8; 20]>, + + // We don't actually implement the technical restriction to prevent + // one Lox credential from being used to submit many reports, so we + // just implement this as a map of bridge fingerprint to (most + // recent Lox credential for this bridge, count of unique level 3+ + // credentials we have for this bridge). + pub lox_credentials: HashMap<[u8; 20], (Lox, u32)>, + + // If censor implements random blocking, this is the date when it + // will start blocking all the bridges it knows. + pub delay_date: u32, + + // If censor implements partial blocking, what percent of + // connections are blocked? + pub partial_blocking_percent: f64, +} + +impl Censor { + pub fn new(config: &Config) -> Self { + let start_date = get_date() + config.bootstrapping_period_duration; + let mut rng = rand::thread_rng(); + let delay_date = if config.censor_speed == Speed::Random { + let num: u32 = rng.gen_range(1..365); + start_date + num + } else { + 0 + }; + let partial_blocking_percent = if config.censor_totality == Totality::Partial { + config.censor_partial_blocking_percent + } else { + 1.0 + }; + Censor { + start_date, + known_bridges: HashSet::<[u8; 20]>::new(), + lox_credentials: HashMap::<[u8; 20], (Lox, u32)>::new(), + delay_date: delay_date, + partial_blocking_percent: partial_blocking_percent, + } + } + + pub fn knows_bridge(&self, fingerprint: &[u8; 20]) -> bool { + self.known_bridges.contains(fingerprint) + } + + pub fn blocks_bridge(&self, config: &Config, fingerprint: &[u8; 20]) -> bool { + self.knows_bridge(fingerprint) + && (config.censor_speed == Speed::Fast + || config.censor_speed == Speed::Lox && self.has_lox_cred(fingerprint) + || config.censor_speed == Speed::Random && self.delay_date <= get_date()) + } + + pub fn learn_bridge(&mut self, fingerprint: &[u8; 20]) { + self.known_bridges.insert(*fingerprint); + } + + pub fn has_lox_cred(&self, fingerprint: &[u8; 20]) -> bool { + self.lox_credentials.contains_key(fingerprint) + && self.lox_credentials.get(fingerprint).unwrap().1 > 0 + } + + pub fn give_lox_cred(&mut self, fingerprint: &[u8; 20], cred: &Lox) { + // We only need one level 3+ credential per bridge. (This will + // change if we restrict positive reports to one per bridge per + // credential.) + if scalar_u32(&cred.trust_level).unwrap() >= 3 { + // We want to clone the credential, but that's not allowed, + // so we're going to serialize it and then deserialize it. + let cloned_cred = bincode::deserialize(&bincode::serialize(&cred).unwrap()).unwrap(); + + // Insert the new credential and add to the count of unique + // credentials we have. We assume that a duplicate + // credential will never be given. If we don't want to make + // this assumption, we could change the count from a u32 to + // a set of credential IDs and get the count as its length. + let count = match self.lox_credentials.get(fingerprint) { + Some((_cred, count)) => *count, + None => 0, + }; + self.lox_credentials + .insert(*fingerprint, (cloned_cred, count + 1)); + } + } + + // Censor sends a positive report for the given bridge. Returns true + // if successful, false otherwise. + pub async fn send_positive_report(&self, config: &Config, fingerprint: &[u8; 20]) -> bool { + // If we don't have an appropriate Lox credential, we can't send + // a report. Return false. + if !self.has_lox_cred(fingerprint) { + return false; + } + + let (cred, _) = &self.lox_credentials.get(fingerprint).unwrap(); + let pr = PositiveReport::from_lox_credential( + *fingerprint, + None, + cred, + get_lox_pub(&config.la_pubkeys), + config.country.clone(), + ) + .unwrap(); + if config + .tp_net + .request("/positivereport".to_string(), pr.to_json().into_bytes()) + .await + .is_err() + { + // failed to send positive report + return false; + } + true + } + + // Make a bunch of connections and submit positive reports if possible + async fn flood(&self, config: &Config, bridges: &mut HashMap<[u8; 20], Bridge>) { + // Only do this if Flooding censor + if config.censor_secrecy == Secrecy::Flooding { + for fingerprint in &self.known_bridges { + // Only do this if we're blocking the bridge + if config.censor_speed == Speed::Fast + || config.censor_speed == Speed::Lox && self.has_lox_cred(fingerprint) + || config.censor_speed == Speed::Random && self.delay_date <= get_date() + { + let bridge = bridges.get_mut(fingerprint).unwrap(); + + // A large number + let num_connections = 30000; + + // Make a bunch of connections to the bridge + bridge.censor_flood(num_connections); + + // If we have a lv3+ credential, submit a bunch of + // positive reports + if self.has_lox_cred(fingerprint) { + let (_cred, cred_count) = + &self.lox_credentials.get(&bridge.fingerprint).unwrap(); + let num_prs = if config.one_positive_report_per_cred { + *cred_count + } else { + 30000 + }; + for _ in 0..num_prs { + self.send_positive_report(config, &bridge.fingerprint).await; + } + } + } + } + } + } + + // Send one positive report per connection we blocked + async fn send_positive_reports( + &self, + config: &Config, + bridges: &mut HashMap<[u8; 20], Bridge>, + ) { + // Only do this if Hiding censor. Flooding censors should use + // flood() instead. + if config.censor_secrecy == Secrecy::Hiding { + for fingerprint in &self.known_bridges { + // Only do this if we're blocking the bridge + if self.blocks_bridge(config, fingerprint) && self.has_lox_cred(fingerprint) { + let bridge = bridges.get_mut(fingerprint).unwrap(); + + // We may be restricted to one positive report per + // credential + let num_reports_to_send = if config.one_positive_report_per_cred { + min( + bridge.total_connections - bridge.real_connections, + self.lox_credentials.get(fingerprint).unwrap().1, + ) + } else { + bridge.total_connections - bridge.real_connections + }; + for _ in 0..num_reports_to_send { + self.send_positive_report(config, fingerprint).await; + } + } + } + } + } + + fn recompute_delay(&mut self, config: &Config) { + // Only do this if Random censor + if config.censor_speed == Speed::Random + && self.delay_date + config.censor_event_duration <= get_date() + { + // Compute new delay date + self.delay_date = { + let mut rng = rand::thread_rng(); + let num: u32 = rng.gen_range(1..365); + get_date() + num + } + } + } + + pub async fn end_of_day_tasks( + &mut self, + config: &Config, + bridges: &mut HashMap<[u8; 20], Bridge>, + ) { + if get_date() >= self.start_date { + if config.censor_secrecy == Secrecy::Flooding + && !(config.censor_speed == Speed::Random && self.delay_date <= get_date()) + { + self.flood(config, bridges).await; + } else if config.censor_secrecy == Secrecy::Hiding + && !(config.censor_speed == Speed::Random && self.delay_date <= get_date()) + { + self.send_positive_reports(config, bridges).await; + } + + self.recompute_delay(config); + } + } +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +pub enum Speed { + Fast, + Lox, + Random, +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +pub enum Secrecy { + Overt, + Hiding, + Flooding, +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +pub enum Totality { + Full, + Partial, + Throttling, +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..17a102e --- /dev/null +++ b/src/config.rs @@ -0,0 +1,33 @@ +use crate::censor; + +use lox_cli::networking::*; +use lox_library::IssuerPubKey; + +pub struct Config { + pub la_pubkeys: Vec, + pub la_net: HyperNet, + pub tp_net: HyperNet, + pub bootstrapping_period_duration: u32, + // Define censor behavior + pub censor_secrecy: censor::Secrecy, + pub censor_speed: censor::Speed, + pub censor_event_duration: u32, + pub censor_totality: censor::Totality, + pub censor_partial_blocking_percent: f64, + // We model only one country at a time because Lox assumes censors + // share information with each other. + pub country: String, + pub one_positive_report_per_cred: bool, + // Probability that a censor-cooperating user can convince an honest + // user to give them an invite. + pub prob_censor_gets_invite: f64, + // Probability that a connection randomly fails, even though censor + // does not block the bridge + pub prob_connection_fails: f64, + // If the connection fails, retry how many times? + pub num_connection_retries: u32, + pub prob_user_invites_friend: f64, + pub prob_user_is_censor: f64, + pub prob_user_submits_reports: f64, + pub prob_user_treats_throttling_as_blocking: f64, +} diff --git a/src/extra_infos_server.rs b/src/extra_infos_server.rs new file mode 100644 index 0000000..b52b7ab --- /dev/null +++ b/src/extra_infos_server.rs @@ -0,0 +1,150 @@ +use hyper::{ + body::{self, Bytes}, + header::HeaderValue, + server::conn::AddrStream, + service::{make_service_fn, service_fn}, + Body, Method, Request, Response, Server, +}; +use serde_json::json; +use std::{collections::HashSet, convert::Infallible, net::SocketAddr, time::Duration}; +use tokio::{ + spawn, + sync::{mpsc, oneshot}, + time::sleep, +}; +use troll_patrol::extra_info::ExtraInfo; + +async fn serve_extra_infos( + extra_infos_pages: &mut Vec, + req: Request, +) -> Result, Infallible> { + match req.method() { + &Method::OPTIONS => Ok(Response::builder() + .header("Access-Control-Allow-Origin", HeaderValue::from_static("*")) + .header("Access-Control-Allow-Headers", "accept, content-type") + .header("Access-Control-Allow-Methods", "POST") + .status(200) + .body(Body::from("Allow POST")) + .unwrap()), + _ => match req.uri().path() { + "/" => Ok::<_, Infallible>(serve_index(&extra_infos_pages)), + "/add" => Ok::<_, Infallible>({ + let bytes = body::to_bytes(req.into_body()).await.unwrap(); + add_extra_infos(extra_infos_pages, bytes) + }), + path => Ok::<_, Infallible>({ + // Serve the requested file + serve_extra_infos_file(&extra_infos_pages, path) + }), + }, + } +} + +pub async fn server() { + let (context_tx, context_rx) = mpsc::channel(32); + let request_tx = context_tx.clone(); + + spawn(async move { create_context_manager(context_rx).await }); + + let addr = SocketAddr::from(([127, 0, 0, 1], 8004)); + let make_svc = make_service_fn(move |_conn: &AddrStream| { + let request_tx = request_tx.clone(); + let service = service_fn(move |req| { + let request_tx = request_tx.clone(); + let (response_tx, response_rx) = oneshot::channel(); + let cmd = Command::Request { + req, + sender: response_tx, + }; + async move { + request_tx.send(cmd).await.unwrap(); + response_rx.await.unwrap() + } + }); + async move { Ok::<_, Infallible>(service) } + }); + let server = Server::bind(&addr).serve(make_svc); + println!("Listening on localhost:8004"); + if let Err(e) = server.await { + eprintln!("server error: {}", e); + } +} + +async fn create_context_manager(context_rx: mpsc::Receiver) { + tokio::select! { + create_context = context_manager(context_rx) => create_context, + } +} + +async fn context_manager(mut context_rx: mpsc::Receiver) { + let mut extra_infos_pages = Vec::::new(); + + while let Some(cmd) = context_rx.recv().await { + use Command::*; + match cmd { + Request { req, sender } => { + let response = serve_extra_infos(&mut extra_infos_pages, req).await; + if let Err(e) = sender.send(response) { + eprintln!("Server Response Error: {:?}", e); + } + sleep(Duration::from_millis(1)).await; + } + } + } +} + +#[derive(Debug)] +enum Command { + Request { + req: Request, + sender: oneshot::Sender, Infallible>>, + }, +} + +fn add_extra_infos(extra_infos_pages: &mut Vec, request: Bytes) -> Response { + let extra_infos: HashSet = match serde_json::from_slice(&request) { + Ok(req) => req, + Err(e) => { + let response = json!({"error": e.to_string()}); + let val = serde_json::to_string(&response).unwrap(); + return prepare_header(val); + } + }; + + let mut extra_infos_file = String::new(); + for extra_info in extra_infos { + extra_infos_file.push_str(extra_info.to_string().as_str()); + } + if extra_infos_file.len() > 0 { + extra_infos_pages.push(extra_infos_file); + } + prepare_header("OK".to_string()) +} + +fn serve_index(extra_infos_pages: &Vec) -> Response { + let mut body_str = String::new(); + for i in 0..extra_infos_pages.len() { + body_str + .push_str(format!("{}-extra-infos\n", i, i).as_str()); + } + prepare_header(body_str) +} + +fn serve_extra_infos_file(extra_infos_pages: &Vec, path: &str) -> Response { + if path.ends_with("-extra-infos") { + if let Ok(index) = &path[1..(path.len() - "-extra-infos".len())].parse::() { + if extra_infos_pages.len() > *index { + return prepare_header(extra_infos_pages[*index].clone()); + } + } + } + prepare_header("Not a valid file".to_string()) +} + +// Prepare HTTP Response for successful Server Request +fn prepare_header(response: String) -> Response { + let mut resp = Response::new(Body::from(response)); + resp.headers_mut() + .insert("Access-Control-Allow-Origin", HeaderValue::from_static("*")); + resp +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d4df88d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +pub mod bridge; +pub mod censor; +pub mod config; +pub mod extra_infos_server; +pub mod user; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..93fea60 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,399 @@ +// Before running this, run: +// 1. rdsys +// 2. lox-distributor +// 3. troll-patrol with the feature "simulation" + +use lox_simulation::{ + bridge::Bridge, + censor::{self, Censor}, + config::Config as SConfig, + extra_infos_server, + user::User, +}; + +use clap::Parser; +use lox_cli::{networking::*, *}; +use memory_stats::memory_stats; +use rand::{prelude::SliceRandom, Rng}; +use serde::Deserialize; +use std::{ + collections::{HashMap, HashSet}, + fs::File, + io::BufReader, + path::PathBuf, + time::Duration, +}; +use tokio::{spawn, time::sleep}; +use troll_patrol::{extra_info::ExtraInfo, get_date, increment_simulated_date}; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// Name/path of the configuration file + #[arg(short, long, default_value = "simulation_config.json")] + config: PathBuf, +} + +#[derive(Debug, Deserialize)] +pub struct Config { + pub la_port: u16, + pub la_test_port: u16, + pub tp_port: u16, + pub tp_test_port: u16, + pub bootstrapping_period_duration: u32, + pub censor_secrecy: censor::Secrecy, + pub censor_speed: censor::Speed, + pub censor_event_duration: u32, + pub censor_totality: censor::Totality, + pub censor_partial_blocking_percent: f64, + pub country: String, + pub min_new_users_per_day: u32, + pub max_new_users_per_day: u32, + pub num_connection_retries: u32, + // How many days to simulate + pub num_days: u32, + pub one_positive_report_per_cred: bool, + pub prob_censor_gets_invite: f64, + pub prob_connection_fails: f64, + pub prob_user_invites_friend: f64, + pub prob_user_is_censor: f64, + pub prob_user_submits_reports: f64, + pub prob_user_treats_throttling_as_blocking: f64, +} + +#[tokio::main] +pub async fn main() { + let args: Args = Args::parse(); + + let config: Config = serde_json::from_reader(BufReader::new( + File::open(&args.config).expect("Could not read config file"), + )) + .expect("Reading config file from JSON failed"); + + let la_net = HyperNet { + hostname: format!("http://localhost:{}", config.la_port), + }; + let la_net_test = HyperNet { + hostname: format!("http://localhost:{}", config.la_test_port), + }; + let tp_net = HyperNet { + hostname: format!("http://localhost:{}", config.tp_port), + }; + let tp_net_test = HyperNet { + hostname: format!("http://localhost:{}", config.tp_test_port), + }; + let extra_infos_net = HyperNet { + hostname: "http://localhost:8004".to_string(), + }; + + let la_pubkeys = get_lox_auth_keys(&la_net).await.unwrap(); + + let sconfig = SConfig { + la_pubkeys, + la_net, + tp_net, + bootstrapping_period_duration: config.bootstrapping_period_duration, + censor_secrecy: config.censor_secrecy, + censor_speed: config.censor_speed, + censor_event_duration: config.censor_event_duration, + censor_totality: config.censor_totality, + censor_partial_blocking_percent: config.censor_partial_blocking_percent, + country: config.country, + num_connection_retries: config.num_connection_retries, + one_positive_report_per_cred: config.one_positive_report_per_cred, + prob_censor_gets_invite: config.prob_censor_gets_invite, + prob_connection_fails: config.prob_connection_fails, + prob_user_invites_friend: config.prob_user_invites_friend, + prob_user_is_censor: config.prob_user_is_censor, + prob_user_submits_reports: config.prob_user_submits_reports, + prob_user_treats_throttling_as_blocking: config.prob_user_treats_throttling_as_blocking, + }; + + let mut rng = rand::thread_rng(); + + // Set up censor + let mut censor = Censor::new(&sconfig); + + // Set up bridges (no bridges yet) + let mut bridges = HashMap::<[u8; 20], Bridge>::new(); + + // Set up users + let mut users = Vec::::new(); + + // Set up extra-infos server + spawn(async move { + extra_infos_server::server().await; + }); + sleep(Duration::from_millis(1)).await; + + let mut false_neg = 0; + let mut false_pos = 0; + let mut true_neg = 0; + let mut true_pos = 0; + + // Track memory use during simulation + let mut max_physical_mem = 0; + let mut max_virtual_mem = 0; + + // Main loop + for day in 1..=config.num_days { + println!("Starting day {} of the simulation", day); + println!( + " We have {} users and {} bridges", + users.len(), + bridges.len() + ); + println!( + " The censor has learned {} bridges", + censor.known_bridges.len() + ); + println!(" Accuracy thus far:"); + println!(" True Positives: {}", true_pos); + println!(" True Negatives: {}", true_neg); + println!(" False Positives: {}", false_pos); + println!(" False Negatives: {}", false_neg); + + if let Some(usage) = memory_stats() { + if usage.physical_mem > max_physical_mem { + max_physical_mem = usage.physical_mem; + } + if usage.virtual_mem > max_virtual_mem { + max_virtual_mem = usage.virtual_mem; + } + } else { + println!("Failed to get the current memory usage"); + } + + // USER TASKS + + // Number of users who want to join today + let mut num_users_requesting_invites: u32 = + rng.gen_range(config.min_new_users_per_day..=config.max_new_users_per_day); + + // How many of the new users are censors? + let mut num_new_censor_users = 0; + for _ in 0..num_users_requesting_invites { + let num: f64 = rng.gen_range(0.0..1.0); + if num < config.prob_user_is_censor { + num_new_censor_users += 1; + num_users_requesting_invites -= 1; + } + } + + // Determine whether each new censor user can get an invite from + // an existing trusted user or needs to join via open-entry + // invite. Note: We still favor honest users by giving them + // invites *first*. This means if only a small number of invites + // are available, the censor may still not get invited. + let mut num_censor_invitations = 0; + for _ in 0..num_new_censor_users { + let num: f64 = rng.gen_range(0.0..1.0); + if num < config.prob_censor_gets_invite { + num_censor_invitations += 1; + num_new_censor_users -= 1; + } + } + + let mut new_users = Vec::::new(); + + // Shuffle users so they act in a random order + users.shuffle(&mut rng); + + // Users do daily actions + for user in &mut users { + let invited_friends = user + .daily_tasks( + &sconfig, + num_users_requesting_invites, + num_censor_invitations, + &mut bridges, + &mut censor, + ) + .await; + + if invited_friends.is_ok() { + let mut invited_friends = invited_friends.unwrap(); + if invited_friends.len() > 0 { + if !user.is_censor { + // Censors always invite as many censor friends + // as possible. Honest users may invite honest + // friends, or they may accidentally invite + // censor friends. + for inv_friend in &invited_friends { + if inv_friend.is_censor { + num_censor_invitations -= 1; + } else { + num_users_requesting_invites -= 1; + } + } + } + // If this user invited any friends, add them to the + // list of users + new_users.append(&mut invited_friends); + } + } + } + + // Add new users + users.append(&mut new_users); + + // If any users couldn't get invites, they join with open-entry + // invitations + for _ in 0..num_users_requesting_invites { + let user = User::new(&sconfig, false).await; + if user.is_ok() { + users.push(user.unwrap()); + } else { + eprintln!("Failed to create new user."); + } + } + + // If any censor users couldn't get invites, they also join with + // open-entry invitations + for _ in 0..(num_new_censor_users + num_censor_invitations) { + let user = User::new(&sconfig, true).await; + if user.is_ok() { + users.push(user.unwrap()); + } else { + eprintln!("Failed to create new censor user."); + } + } + + // CENSOR TASKS + censor.end_of_day_tasks(&sconfig, &mut bridges).await; + + // BRIDGE TASKS + let mut new_extra_infos = HashSet::::new(); + for (_, bridge) in bridges.iter_mut() { + // Bridge reports its connections for the day + new_extra_infos.insert(bridge.gen_extra_info(&sconfig.country)); + + // Bridge resets for tomorrow + bridge.reset_for_tomorrow(); + } + + // Publish all the bridges' extra-infos for today + let result = extra_infos_net + .request( + "/add".to_string(), + serde_json::to_string(&new_extra_infos).unwrap().into(), + ) + .await; + if result.is_ok() { + result.unwrap(); + } else { + eprintln!("Failed to publish new extra-infos"); + } + + // TROLL PATROL TASKS + let new_blockages_resp = tp_net_test.request("/update".to_string(), vec![]).await; + let new_blockages = match new_blockages_resp { + Ok(resp) => match serde_json::from_slice(&resp) { + Ok(new_blockages) => new_blockages, + Err(e) => { + eprintln!("Failed to deserialize new blockages, error: {}", e); + HashMap::>::new() + } + }, + Err(e) => { + eprintln!( + "Failed to get new blockages from Troll Patrol, error: {}", + e + ); + HashMap::>::new() + } + }; + + // Since we have only one censor, just convert to a set of bridges + let mut blocked_bridges = HashSet::<[u8; 20]>::new(); + for (bridge, ccs) in new_blockages { + let fingerprint = array_bytes::hex2array(bridge).unwrap(); + if ccs.contains(&sconfig.country) { + blocked_bridges.insert(fingerprint); + } + } + + for (fingerprint, bridge) in &mut bridges { + let detected_blocked = blocked_bridges.contains(fingerprint); + + // If this is the first day Troll Patrol has determined this + // bridge is blocked, note that for stats + if detected_blocked && bridge.first_detected_blocked == 0 { + bridge.first_detected_blocked = get_date(); + } + + // Check if censor actually blocks this bridge + let really_blocked = censor.blocks_bridge(&sconfig, fingerprint); + if really_blocked && bridge.first_blocked == 0 { + bridge.first_blocked = get_date(); + } + if detected_blocked && really_blocked { + true_pos += 1; + } else if detected_blocked { + false_pos += 1; + } else if really_blocked { + false_neg += 1; + } else { + true_neg += 1; + } + } + + // LOX AUTHORITY TASKS + + // Advance LA's time to tomorrow + let result = la_net_test + .request( + "/advancedays".to_string(), + serde_json::to_string(&(1 as u16)).unwrap().into(), + ) + .await; + if result.is_ok() { + result.unwrap(); + } else { + eprintln!("Failed to advance time for LA"); + } + + // SIMULATION TASKS + + // Advance simulated time to tomorrow + increment_simulated_date(); + } + + // Print various information about the simulation run + println!( + "\nSimulation ended with {} users and {} bridges", + users.len(), + bridges.len() + ); + println!("The censor learned {} bridges", censor.known_bridges.len()); + + println!( + "\nMaximum physical memory usage during simulation: {}", + max_physical_mem + ); + println!( + "Maximum virtual memory usage during simulation: {}\n", + max_virtual_mem + ); + + println!("True Positives: {}", true_pos); + println!("True Negatives: {}", true_neg); + println!("False Positives: {}", false_pos); + println!("False Negatives: {}", false_neg); + + println!("\nFull stats per bridge:"); + + println!( + "Fingerprint,first_distributed,first_blocked,first_detected_blocked,first_positive_report" + ); + for (fingerprint, bridge) in bridges { + println!( + "{},{},{},{},{}", + array_bytes::bytes2hex("", fingerprint), + bridge.first_distributed, + bridge.first_blocked, + bridge.first_detected_blocked, + bridge.first_positive_report + ); + } +} diff --git a/src/user.rs b/src/user.rs new file mode 100644 index 0000000..79e4f5d --- /dev/null +++ b/src/user.rs @@ -0,0 +1,682 @@ +// User behavior in simulation + +use crate::{ + bridge::Bridge, + censor::{Censor, Secrecy::*, Totality::*}, + config::Config, +}; + +use anyhow::{anyhow, Result}; +use lox_cli::{networking::*, *}; +use lox_library::{ + bridge_table::BridgeLine, cred::Lox, proto::check_blockage::MIN_TRUST_LEVEL, scalar_u32, +}; +use rand::Rng; +use std::{cmp::min, collections::HashMap}; +use troll_patrol::{ + get_date, negative_report::NegativeReport, positive_report::PositiveReport, BridgeDistributor, +}; +use x25519_dalek::PublicKey; + +// Helper function to probabilistically return true or false +pub fn event_happens(probability: f64) -> bool { + let mut rng = rand::thread_rng(); + let num: f64 = rng.gen_range(0.0..1.0); + num < probability +} + +pub struct User { + // Does this user cooperate with a censor? + pub is_censor: bool, + + // 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. + pub primary_cred: Lox, + secondary_cred: Option, + + // Does the user submit reports to Troll Patrol? + submits_reports: bool, + + // How likely is this user to use bridges on a given day? + prob_use_bridges: f64, +} + +impl User { + pub async fn new(config: &Config, is_censor: bool) -> Result { + let cred = get_lox_credential( + &config.la_net, + &get_open_invitation(&config.la_net).await?, + get_lox_pub(&config.la_pubkeys), + ) + .await? + .0; + + // Probabilistically decide whether this user submits reports + let submits_reports = if is_censor { + false + } else { + event_happens(config.prob_user_submits_reports) + }; + + // Randomly determine how likely this user is to use bridges on + // a given day + let mut rng = rand::thread_rng(); + let prob_use_bridges = rng.gen_range(0.0..=1.0); + + Ok(Self { + is_censor, + primary_cred: cred, + secondary_cred: None, + submits_reports: submits_reports, + prob_use_bridges: prob_use_bridges, + }) + } + + pub async fn trusted_user(config: &Config) -> Result { + let cred = get_lox_credential( + &config.la_net, + &get_open_invitation(&config.la_net).await?, + get_lox_pub(&config.la_pubkeys), + ) + .await? + .0; + Ok(Self { + is_censor: false, + primary_cred: cred, + secondary_cred: None, + submits_reports: true, + prob_use_bridges: 1.0, + }) + } + + // TODO: This should probably return an actual error type + pub async fn invite( + &mut self, + config: &Config, + censor: &mut Censor, + invited_user_is_censor: bool, + ) -> Result { + let etable = get_reachability_credential(&config.la_net).await?; + let (new_cred, invite) = issue_invite( + &config.la_net, + &self.primary_cred, + &etable, + get_lox_pub(&config.la_pubkeys), + get_reachability_pub(&config.la_pubkeys), + get_invitation_pub(&config.la_pubkeys), + ) + .await?; + self.primary_cred = new_cred; + if self.is_censor { + // Make sure censor has access to each bridge and each + // credential + let (bucket, _reachcred) = get_bucket(&config.la_net, &self.primary_cred).await?; + for bl in bucket { + let fingerprint = bl.get_hashed_fingerprint(); + censor.learn_bridge(&fingerprint); + censor.give_lox_cred(&fingerprint, &self.primary_cred); + } + } + let friend_cred = redeem_invite( + &config.la_net, + &invite, + get_lox_pub(&config.la_pubkeys), + get_invitation_pub(&config.la_pubkeys), + ) + .await? + .0; + + // Calling function decides if the invited user is a censor + let is_censor = invited_user_is_censor; + + // Probabilistically decide whether this user submits reports + let submits_reports = if is_censor { + false + } else { + event_happens(config.prob_user_submits_reports) + }; + + // Randomly determine how likely this user is to use bridges on + // a given day + let mut rng = rand::thread_rng(); + let prob_use_bridges = rng.gen_range(0.0..=1.0); + + Ok(Self { + is_censor, + primary_cred: friend_cred, + secondary_cred: None, + submits_reports: submits_reports, + prob_use_bridges: prob_use_bridges, + }) + } + + // Attempt to "connect" to the bridge, returns true if successful. + // Note that this does not involve making a real connection to a + // real bridge. The function is async because the *censor* might + // submit a positive report during this function. + pub fn connect(&self, config: &Config, bridge: &mut Bridge, censor: &Censor) -> bool { + if censor.blocks_bridge(config, &bridge.fingerprint) { + if config.censor_totality == Full + || config.censor_totality == Partial + && event_happens(censor.partial_blocking_percent) + { + // If censor tries to hide its censorship, record a + // false connection + if config.censor_secrecy == Hiding { + bridge.connect_total(); + } + + // Return false because the connection failed + return false; + } else if config.censor_totality == Throttling { + // With some probability, the user connects but gives up + // because there is too much interference. In this case, + // a real connection occurs, but we treat it like a + // false connection from the censor. + if event_happens(config.prob_user_treats_throttling_as_blocking) { + bridge.connect_total(); + + // Return false because there was interference + // detected in the connection + return false; + } + } + } + + // Connection may randomly fail, without censor intervention + let mut connection_fails = true; + // The user retries some number of times + for _ in 0..=config.num_connection_retries { + if !event_happens(config.prob_connection_fails) { + connection_fails = false; + break; + } + } + if connection_fails { + return false; + } + + // If we haven't returned yet, the connection succeeded + bridge.connect_real(); + true + } + + pub async fn get_new_credential(config: &Config) -> Result<(Lox, BridgeLine)> { + get_lox_credential( + &config.la_net, + &get_open_invitation(&config.la_net).await?, + get_lox_pub(&config.la_pubkeys), + ) + .await + } + + pub async fn send_negative_reports( + config: &Config, + reports: Vec, + ) -> Result<()> { + let date = get_date(); + let pubkey = match serde_json::from_slice::>( + &config + .tp_net + .request("/nrkey".to_string(), serde_json::to_string(&date)?.into()) + .await?, + )? { + Some(v) => v, + None => return Err(anyhow!("No available negative report encryption key")), + }; + for report in reports { + config + .tp_net + .request( + "/negativereport".to_string(), + bincode::serialize(&report.encrypt(&pubkey))?, + ) + .await?; + } + Ok(()) + } + + pub async fn send_positive_reports( + config: &Config, + reports: Vec, + ) -> Result<()> { + for report in reports { + config + .tp_net + .request("/positivereport".to_string(), report.to_json().into_bytes()) + .await?; + } + Ok(()) + } + + pub async fn daily_tasks( + &mut self, + config: &Config, + num_users_requesting_invites: u32, + num_censor_invites: u32, + bridges: &mut HashMap<[u8; 20], Bridge>, + censor: &mut Censor, + ) -> Result> { + if self.is_censor { + self.daily_tasks_censor(config, bridges, censor).await + } else { + self.daily_tasks_non_censor( + config, + num_users_requesting_invites, + num_censor_invites, + bridges, + censor, + ) + .await + } + } + + // User performs daily connection attempts, etc. and returns a + // vector of newly invited friends. + // TODO: The map of bridges and the censor should be Arc> + // or something so we can parallelize this. + pub async fn daily_tasks_non_censor( + &mut self, + config: &Config, + num_users_requesting_invites: u32, + num_censor_invites: u32, + bridges: &mut HashMap<[u8; 20], Bridge>, + censor: &mut Censor, + ) -> Result> { + // Probabilistically decide if the user should use bridges today + if event_happens(self.prob_use_bridges) { + // 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(&config.la_net, &self.primary_cred).await?; + let level = match scalar_u32(&self.primary_cred.trust_level) { + Some(v) => v, + None => return Err(anyhow!("Failed to get trust level from credential")), + }; + + // Make sure each bridge in bucket is in the global bridges set + for bridgeline in bucket { + if bridgeline != BridgeLine::default() { + if !bridges.contains_key(&bridgeline.get_hashed_fingerprint()) { + let bridge = Bridge::from_bridge_line(&bridgeline); + bridges.insert(bridgeline.get_hashed_fingerprint(), bridge); + } + } + } + + // Can we level up the main credential? + let can_level_up = reachcred.is_some() + && (level == 0 + && eligible_for_trust_promotion(&config.la_net, &self.primary_cred).await + || level > 0 + && eligible_for_level_up(&config.la_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(); + // Try to connect to each bridge + for i in 0..bucket.len() { + // At level 0, we only have 1 bridge + if bucket[i] != BridgeLine::default() { + if self.connect( + &config, + bridges + .get_mut(&bucket[i].get_hashed_fingerprint()) + .unwrap(), + &censor, + ) { + succeeded.push(bucket[i]); + } else { + failed.push(bucket[i]); + } + } + } + + // If we were not able to connect to any bridges, get a + // second credential + 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 + match Self::get_new_credential(&config).await { + Ok((cred, _bl)) => Some(cred), + Err(e) => { + eprintln!("Failed to get new Lox credential. Error: {}", e); + None + } + } + } + } 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(&config.la_net, &second_cred).await?; + for bridgeline in second_bucket { + if bridgeline != BridgeLine::default() { + if !bridges.contains_key(&bridgeline.get_hashed_fingerprint()) { + bridges.insert( + bridgeline.get_hashed_fingerprint(), + Bridge::from_bridge_line(&bridgeline), + ); + } + // Attempt to connect to second cred's bridge + if self.connect( + &config, + bridges + .get_mut(&bridgeline.get_hashed_fingerprint()) + .unwrap(), + censor, + ) { + succeeded.push(bridgeline); + if second_reachcred.is_some() + && eligible_for_trust_promotion(&config.la_net, &second_cred).await + { + second_level_up = true; + } + } else { + failed.push(bridgeline); + } + } + } + } + + let mut negative_reports = Vec::::new(); + let mut positive_reports = Vec::::new(); + + if self.submits_reports { + for bridgeline in &failed { + negative_reports.push(NegativeReport::from_bridgeline( + *bridgeline, + config.country.to_string(), + BridgeDistributor::Lox, + )); + } + if level >= 3 { + for bridgeline in &succeeded { + // If we haven't received a positive report yet, + // add a record about it with today's date + let bridge = bridges + .get_mut(&bridgeline.get_hashed_fingerprint()) + .unwrap(); + if bridge.first_positive_report == 0 { + bridge.first_positive_report = get_date(); + } + + positive_reports.push( + PositiveReport::from_lox_credential( + bridgeline.get_hashed_fingerprint(), + None, + &self.primary_cred, + get_lox_pub(&config.la_pubkeys), + config.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 = if level == 0 { + trust_migration( + &config.la_net, + &self.primary_cred, + &trust_promotion( + &config.la_net, + &self.primary_cred, + get_lox_pub(&config.la_pubkeys), + ) + .await?, + get_lox_pub(&config.la_pubkeys), + get_migration_pub(&config.la_pubkeys), + ) + .await? + } else { + level_up( + &config.la_net, + &self.primary_cred, + &reachcred.unwrap(), + get_lox_pub(&config.la_pubkeys), + get_reachability_pub(&config.la_pubkeys), + ) + .await? + }; + self.primary_cred = cred; + self.secondary_cred = None; + } + // We favor starting over at level 1 to migrating to level + // 1, but if we have a level 4 credential for a bridge that + // hasn't been marked blocked, save the credential so we can + // migrate to a level 2 cred. Note that second_level_up is + // only true if we were unable to connect with bridges from + // our primary credential. + else if second_level_up && (level <= MIN_TRUST_LEVEL || reachcred.is_none()) { + let second_cred = second_cred.as_ref().unwrap(); + let cred = trust_migration( + &config.la_net, + &second_cred, + &trust_promotion( + &config.la_net, + &second_cred, + get_lox_pub(&config.la_pubkeys), + ) + .await?, + get_lox_pub(&config.la_pubkeys), + get_migration_pub(&config.la_pubkeys), + ) + .await?; + self.primary_cred = cred; + self.secondary_cred = None; + } else if can_migrate { + let cred = blockage_migration( + &config.la_net, + &self.primary_cred, + &check_blockage( + &config.la_net, + &self.primary_cred, + get_lox_pub(&config.la_pubkeys), + ) + .await?, + get_lox_pub(&config.la_pubkeys), + get_migration_pub(&config.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(&config, negative_reports).await?; + } + if positive_reports.len() > 0 { + Self::send_positive_reports(&config, positive_reports).await?; + } + + // Invite friends if applicable + let invitations = match scalar_u32(&self.primary_cred.invites_remaining) { + Some(v) => v, + None => 0, // This is probably an error case that should not happen + }; + let mut new_friends = Vec::::new(); + for _i in 0..min(invitations, num_users_requesting_invites) { + if event_happens(config.prob_user_invites_friend) { + // Invite non-censor friend + match self.invite(&config, censor, false).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); + } + } + } + } + + // Invite censor users if applicable + let invitations = invitations - new_friends.len() as u32; + for _i in 0..min(invitations, num_censor_invites) { + if event_happens(config.prob_user_invites_friend) { + // Invite non-censor friend + match self.invite(&config, censor, true).await { + Ok(friend) => { + new_friends.push(friend); + } + Err(e) => { + println!("{}", e); + } + } + } + } + + Ok(new_friends) + } else { + Ok(Vec::::new()) + } + } + + // User cooperates with censor and performs daily tasks to try to + // learn more bridges. + pub async fn daily_tasks_censor( + &mut self, + config: &Config, + bridges: &mut HashMap<[u8; 20], Bridge>, + censor: &mut Censor, + ) -> Result> { + // Download bucket to see if bridge is still reachable and if we + // have any new bridges + let (bucket, reachcred) = get_bucket(&config.la_net, &self.primary_cred).await?; + let level = scalar_u32(&self.primary_cred.trust_level).unwrap(); + + // Make sure each bridge is in global bridges set and known by + // censor + for bridgeline in bucket { + if bridgeline != BridgeLine::default() { + if !bridges.contains_key(&bridgeline.get_hashed_fingerprint()) { + let bridge = Bridge::from_bridge_line(&bridgeline); + bridges.insert(bridgeline.get_hashed_fingerprint(), bridge); + } + censor.learn_bridge(&bridgeline.get_hashed_fingerprint()); + } + } + + // Censor user tries to level up their primary credential + if reachcred.is_some() { + if level == 0 && eligible_for_trust_promotion(&config.la_net, &self.primary_cred).await + || level > 0 && eligible_for_level_up(&config.la_net, &self.primary_cred).await + { + let new_cred = if level == 0 { + trust_migration( + &config.la_net, + &self.primary_cred, + &trust_promotion( + &config.la_net, + &self.primary_cred, + get_lox_pub(&config.la_pubkeys), + ) + .await?, + get_lox_pub(&config.la_pubkeys), + get_migration_pub(&config.la_pubkeys), + ) + .await? + } else { + level_up( + &config.la_net, + &self.primary_cred, + &reachcred.unwrap(), + get_lox_pub(&config.la_pubkeys), + get_reachability_pub(&config.la_pubkeys), + ) + .await? + }; + self.primary_cred = new_cred; + let (bucket, _reachcred) = get_bucket(&config.la_net, &self.primary_cred).await?; + // Make sure each bridge is in global bridges set and + // known by censor + for bl in bucket { + let fingerprint = bl.get_hashed_fingerprint(); + if !bridges.contains_key(&fingerprint) { + let bridge = Bridge::from_bridge_line(&bl); + bridges.insert(fingerprint, bridge); + } + censor.learn_bridge(&fingerprint); + censor.give_lox_cred(&fingerprint, &self.primary_cred); + } + } + } else { + // LA has identified this bucket as blocked. This change + // will not be reverted, so replace the primary credential + // with a new level 0 credential and work on gaining trust + // for that one. + let res = Self::get_new_credential(&config).await; + if res.is_ok() { + let (new_cred, bl) = res.unwrap(); + let fingerprint = bl.get_hashed_fingerprint(); + if !bridges.contains_key(&fingerprint) { + let bridge = Bridge::from_bridge_line(&bl); + bridges.insert(fingerprint, bridge); + } + censor.learn_bridge(&fingerprint); + // Censor doesn't want new_cred yet + self.primary_cred = new_cred; + } else { + eprintln!("Censor failed to get new credential"); + } + } + + // Separately from primary credential, censor user requests a + // new secondary credential each day just to block the + // open-entry bridges. This is stored but not reused. + let res = Self::get_new_credential(&config).await; + if res.is_ok() { + let (_new_cred, bl) = res.unwrap(); + let fingerprint = bl.get_hashed_fingerprint(); + if !bridges.contains_key(&fingerprint) { + let bridge = Bridge::from_bridge_line(&bl); + bridges.insert(fingerprint, bridge); + } + censor.learn_bridge(&fingerprint); + // Censor doesn't want new_cred. User doesn't actually use + // secondary_cred, so don't store it. + } else { + eprintln!("Censor failed to get new credential"); + } + + // Censor user invites as many censor friends as possible + let invitations = scalar_u32(&self.primary_cred.invites_remaining).unwrap(); + let mut new_friends = Vec::::new(); + for _ in 0..invitations { + match self.invite(&config, censor, true).await { + Ok(friend) => { + new_friends.push(friend); + } + Err(e) => { + println!("{}", e); + } + } + } + Ok(new_friends) + } +}