Przeglądaj źródła

Move simulation code from Troll Patrol to its own repo

Vecna 8 miesięcy temu
commit
19fc667b36
10 zmienionych plików z 1655 dodań i 0 usunięć
  1. 22 0
      Cargo.toml
  2. 21 0
      LICENSE
  3. 3 0
      README.md
  4. 84 0
      src/bridge.rs
  5. 256 0
      src/censor.rs
  6. 33 0
      src/config.rs
  7. 150 0
      src/extra_infos_server.rs
  8. 5 0
      src/lib.rs
  9. 399 0
      src/main.rs
  10. 682 0
      src/user.rs

+ 22 - 0
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"] }

+ 21 - 0
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.

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+# Lox Simulation
+
+This is a simulation for evaluating Lox and Troll Patrol.

+ 84 - 0
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::<String, u32>::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;
+    }
+}

+ 256 - 0
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,
+}

+ 33 - 0
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<IssuerPubKey>,
+    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,
+}

+ 150 - 0
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<String>,
+    req: Request<Body>,
+) -> Result<Response<Body>, 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<Command>) {
+    tokio::select! {
+        create_context = context_manager(context_rx) => create_context,
+    }
+}
+
+async fn context_manager(mut context_rx: mpsc::Receiver<Command>) {
+    let mut extra_infos_pages = Vec::<String>::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<Body>,
+        sender: oneshot::Sender<Result<Response<Body>, Infallible>>,
+    },
+}
+
+fn add_extra_infos(extra_infos_pages: &mut Vec<String>, request: Bytes) -> Response<Body> {
+    let extra_infos: HashSet<ExtraInfo> = 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<String>) -> Response<Body> {
+    let mut body_str = String::new();
+    for i in 0..extra_infos_pages.len() {
+        body_str
+            .push_str(format!("<a href=\"{}-extra-infos\">{}-extra-infos</a>\n", i, i).as_str());
+    }
+    prepare_header(body_str)
+}
+
+fn serve_extra_infos_file(extra_infos_pages: &Vec<String>, path: &str) -> Response<Body> {
+    if path.ends_with("-extra-infos") {
+        if let Ok(index) = &path[1..(path.len() - "-extra-infos".len())].parse::<usize>() {
+            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<Body> {
+    let mut resp = Response::new(Body::from(response));
+    resp.headers_mut()
+        .insert("Access-Control-Allow-Origin", HeaderValue::from_static("*"));
+    resp
+}

+ 5 - 0
src/lib.rs

@@ -0,0 +1,5 @@
+pub mod bridge;
+pub mod censor;
+pub mod config;
+pub mod extra_infos_server;
+pub mod user;

+ 399 - 0
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::<User>::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::<User>::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::<ExtraInfo>::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::<String, HashSet<String>>::new()
+                }
+            },
+            Err(e) => {
+                eprintln!(
+                    "Failed to get new blockages from Troll Patrol, error: {}",
+                    e
+                );
+                HashMap::<String, HashSet<String>>::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
+        );
+    }
+}

+ 682 - 0
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<Lox>,
+
+    // 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<Self> {
+        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<Self> {
+        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<Self> {
+        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<NegativeReport>,
+    ) -> Result<()> {
+        let date = get_date();
+        let pubkey = match serde_json::from_slice::<Option<PublicKey>>(
+            &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<PositiveReport>,
+    ) -> 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<Vec<User>> {
+        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<Mutex<>>
+    // 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<Vec<User>> {
+        // 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::<BridgeLine>::new();
+            let mut succeeded = Vec::<BridgeLine>::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::<NegativeReport>::new();
+            let mut positive_reports = Vec::<PositiveReport>::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::<User>::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::<User>::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<Vec<User>> {
+        // 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::<User>::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)
+    }
+}