1
0
Fork 0
walkingonions-boosted/sim.py

371 lines
14 KiB
Python

#!/usr/bin/env python3
import random # For simulation, not cryptography!
import math
import re
import statistics
import sys
import os
import logging
import resource
import network
import dirauth
import relay
import client
from bwparser import JansenBandwidthParser, KomloBandwidthParser
class Simulator:
def __init__(self, bw_parser, relaytarget, clienttarget, statslogger):
self.relaytarget = relaytarget
self.clienttarget = clienttarget
self.statslogger = statslogger
self.parser = bw_parser
self.network_size = self.parser.get_relay_num()
# Some (for now) hard-coded parameters
# The number of directory authorities
numdirauths = 9
# The fraction of relays that are fallback relays
# Taken from the live network in Jan 2020
fracfallbackrelays = 0.023
# Mean number of circuits created per client per epoch
self.gamma = 8.9
# Churn is controlled by three parameters:
# newmean: the mean number of new arrivals per epoch
# newstddev: the stddev number of new arrivals per epoch
# oldprob: the probability any given existing one leaves per epoch
# If target is the desired steady state number, then it should
# be the case that target * oldprob = newmean. That way, if the
# current number is below target, on average you add more than
# you remove, and if the current number is above target, on
# average you add fewer than you remove.
# For relays, looking at all the consensuses for Nov and Dec
# 2019, newmean is about 1.0% of the network size, and newstddev
# is about 0.3% of the network size.
self.relay_newmean = 0.010 * self.relaytarget
self.relay_newstddev = 0.003 * self.relaytarget
self.relay_oldprob = 0.010
# For clients, looking at how many clients request a consensus
# with an if-modified-since date more than 3 hours old (and so
# we treat them as "new") over several days in late Dec 2019,
# newmean is about 16% of all clients, and newstddev is about 4%
# of all clients.
# if the environment variable WOSIM_CLIENT_CHURN is set to 0,
# don't churn clients at all. This allows us to see the effect
# of client churn on relay bandwidth.
if os.getenv('WOSIM_CLIENT_CHURN', '1') == '0':
self.client_newmean = 0
self.client_newstddev = 0
self.client_oldprob = 0
else:
self.client_newmean = 0.16 * self.clienttarget
self.client_newstddev = 0.04 * self.clienttarget
self.client_oldprob = 0.16
# Start some dirauths
self.dirauthaddrs = []
self.dirauths = []
for i in range(numdirauths):
dira = dirauth.DirAuth(i, numdirauths)
self.dirauths.append(dira)
self.dirauthaddrs.append(dira.netaddr)
# Start some relays
self.relays = []
bw = self.calculate_relay_bandwidth()
for i in range(self.relaytarget):
new_relay = relay.Relay(self.dirauthaddrs, bw[i], 0)
self.relays.append(new_relay)
# The fallback relays are a hardcoded list of a small fraction
# of the relays, used by clients for bootstrapping
numfallbackrelays = int(self.relaytarget * fracfallbackrelays) + 1
fallbackrelays = random.sample(self.relays, numfallbackrelays)
for r in fallbackrelays:
r.set_is_fallbackrelay()
network.thenetwork.setfallbackrelays(fallbackrelays)
# Tick the epoch to build the first consensus
network.thenetwork.nextepoch()
# Start some clients
self.clients = []
for i in range(clienttarget):
self.clients.append(client.Client(self.dirauthaddrs))
# Throw away all the performance statistics to this point
for d in self.dirauths: d.perfstats.reset()
for r in self.relays: r.perfstats.reset()
# The clients' stats are already at 0, but they have the
# "bootstrapping" flag set, which we want to keep, so we
# won't reset them.
# Tick the epoch to bootstrap the clients
network.thenetwork.nextepoch()
def one_epoch(self):
"""Simulate one epoch."""
epoch = network.thenetwork.getepoch()
# Each client will start a random number of circuits in a
# Poisson distribution with mean gamma. To randomize the order
# of the clients creating each circuit, we actually use a
# Poisson distribution with mean (gamma*num_clients), and assign
# each event to a uniformly random client. (This does in fact
# give the required distribution.)
numclients = len(self.clients)
# simtime is the simulated time, measured in epochs (i.e.,
# 0=start of this epoch; 1=end of this epoch)
simtime = 0
numcircs = 0
allcircs = []
lastpercent = -1
while simtime < 1.0:
try:
allcircs.append(
random.choice(self.clients).channelmgr.new_circuit())
except ValueError as e:
self.statslogger.error(str(e))
raise e
simtime += random.expovariate(self.gamma * numclients)
numcircs += 1
percent = int(100*simtime)
#if percent != lastpercent:
if numcircs % 100 == 0:
logging.info("Creating circuits in epoch %s: %d%% (%d circuits)",
epoch, percent, numcircs)
lastpercent = percent
# gather stats
totsent = 0
totrecv = 0
dirasent = 0
dirarecv = 0
relaysent = 0
relayrecv = 0
clisent = 0
clirecv = 0
dirastats = network.PerfStatsStats()
for d in self.dirauths:
logging.debug("%s", d.perfstats)
dirasent += d.perfstats.bytes_sent
dirarecv += d.perfstats.bytes_received
dirastats.accum(d.perfstats)
totsent += dirasent
totrecv += dirarecv
relaystats = network.PerfStatsStats(True)
relaybstats = network.PerfStatsStats(True)
relaynbstats = network.PerfStatsStats(True)
relayfbstats = network.PerfStatsStats(True)
for r in self.relays:
logging.debug("%s", r.perfstats)
relaysent += r.perfstats.bytes_sent
relayrecv += r.perfstats.bytes_received
relaystats.accum(r.perfstats)
if r.is_fallbackrelay:
relayfbstats.accum(r.perfstats)
else:
if r.perfstats.is_bootstrapping:
relaybstats.accum(r.perfstats)
else:
relaynbstats.accum(r.perfstats)
totsent += relaysent
totrecv += relayrecv
clistats = network.PerfStatsStats()
clibstats = network.PerfStatsStats()
clinbstats = network.PerfStatsStats()
for c in self.clients:
logging.debug("%s", c.perfstats)
clisent += c.perfstats.bytes_sent
clirecv += c.perfstats.bytes_received
clistats.accum(c.perfstats)
if c.perfstats.is_bootstrapping:
clibstats.accum(c.perfstats)
else:
clinbstats.accum(c.perfstats)
totsent += clisent
totrecv += clirecv
self.statslogger.info("DirAuths sent=%s recv=%s bytes=%s" % \
(dirasent, dirarecv, dirasent+dirarecv))
self.statslogger.info("Relays sent=%s recv=%s bytes=%s" % \
(relaysent, relayrecv, relaysent+relayrecv))
self.statslogger.info("Client sent=%s recv=%s bytes=%s" % \
(clisent, clirecv, clisent+clirecv))
self.statslogger.info("Total sent=%s recv=%s bytes=%s" % \
(totsent, totrecv, totsent+totrecv))
numdirauths = len(self.dirauths)
numrelays = len(self.relays)
numclients = len(self.clients)
self.statslogger.info("Dirauths %s", dirastats)
self.statslogger.info("Relays %s", relaystats)
self.statslogger.info("Relays(FB) %s", relayfbstats)
self.statslogger.info("Relays(B) %s", relaybstats)
self.statslogger.info("Relays(NB) %s", relaynbstats)
self.statslogger.info("Clients %s", clistats)
self.statslogger.info("Clients(B) %s", clibstats)
self.statslogger.info("Clients(NB) %s", clinbstats)
# Close circuits
for c in allcircs:
c.close()
# Clear bootstrapping flag
for d in self.dirauths: d.perfstats.is_bootstrapping = False
for r in self.relays: r.perfstats.is_bootstrapping = False
for c in self.clients: c.perfstats.is_bootstrapping = False
# Churn relays
# Stop some of the (non-fallback) relays
relays_remaining = []
numrelays = len(self.relays)
numrelaysterminated = 0
lastpercent = 0
logging.info("Terminating some relays")
for i, r in enumerate(self.relays):
percent = int(100*(i+1)/numrelays)
if not r.is_fallbackrelay and \
random.random() < self.relay_oldprob:
r.terminate()
numrelaysterminated += 1
else:
# Keep this relay
relays_remaining.append(r)
if percent != lastpercent:
lastpercent = percent
logging.info("%d%% relays considered, %d terminated",
percent, numrelaysterminated)
self.relays = relays_remaining
# Start some new relays
relays_new = int(random.normalvariate(self.relay_newmean,
self.relay_newstddev))
logging.info("Starting %d new relays", relays_new)
bw = self.calculate_relay_bandwidth()
if relays_new > 0:
for i in range(relays_new):
new_relay = relay.Relay(self.dirauthaddrs, bw[i], 0)
self.relays.append(new_relay)
# churn clients
if self.client_oldprob > 0:
# Stop some of the clients
clients_remaining = []
numclients = len(self.clients)
numclientsterminated = 0
lastpercent = 0
logging.info("Terminating some clients")
for i, c in enumerate(self.clients):
percent = int(100*(i+1)/numclients)
if random.random() < self.client_oldprob:
c.terminate()
numclientsterminated += 1
else:
# Keep this client
clients_remaining.append(c)
if percent != lastpercent:
lastpercent = percent
logging.info("%d%% clients considered, %d terminated",
percent, numclientsterminated)
self.clients = clients_remaining
# Start some new clients
clients_new = int(random.normalvariate(self.client_newmean,
self.client_newstddev))
logging.info("Starting %d new clients", clients_new)
if clients_new > 0:
for i in range(clients_new):
self.clients.append(client.Client(self.dirauthaddrs))
# Reset stats
for d in self.dirauths: d.perfstats.reset()
for r in self.relays: r.perfstats.reset()
for c in self.clients: c.perfstats.reset()
# Tick the epoch
network.thenetwork.nextepoch()
def calculate_relay_bandwidth(self):
return self.parser.get_distribution()
if __name__ == '__main__':
# Args: womode snipauthmode networkscale numepochs randseed logdir
if len(sys.argv) != 7:
sys.stderr.write("Usage: womode snipauthmode networkscale numepochs randseed logdir\n")
sys.exit(1)
# sensible defaults
bandwidth_file = os.getenv('BW_FILE')
womode = network.WOMode[sys.argv[1].upper()]
snipauthmode = network.SNIPAuthMode[sys.argv[2].upper()]
networkscale = float(sys.argv[3])
numepochs = int(sys.argv[4])
randseed = int(sys.argv[5])
logfile = "%s/%s_%s_%f_%s_%s.log" % (sys.argv[6], womode.name,
snipauthmode.name, networkscale, numepochs, randseed)
bw_parser = None
if os.getenv('BW_ALGO') != "komlo":
bw_parser = JansenBandwidthParser(bw_file=bandwidth_file, netscale=networkscale)
else:
# keep the original assumption
bw_parser = KomloBandwidthParser(sample_size=(math.ceil(6500 * networkscale)))
# Seed the PRNG. On Ubuntu 18.04, this in fact makes future calls
# to (non-cryptographic) random numbers deterministic. On Ubuntu
# 16.04, it does not.
random.seed(randseed)
loglevel = logging.INFO
# Uncomment to see all the debug messages
# loglevel = logging.DEBUG
logging.basicConfig(level=loglevel,
format="%(asctime)s:%(levelname)s:%(message)s")
# The gathered statistics get logged separately
statslogger = logging.getLogger("simulator")
handler = logging.FileHandler(logfile)
handler.setFormatter(logging.Formatter("%(asctime)s:%(message)s"))
statslogger.addHandler(handler)
statslogger.setLevel(logging.INFO)
statslogger.info("Starting simulation %s", logfile)
# Set the Walking Onions style to use
network.thenetwork.set_wo_style(womode, snipauthmode)
# The steady-state numbers of relays and clients
network_size = bw_parser.get_relay_num()
relaytarget = math.ceil(network_size * networkscale)
clienttarget = math.ceil(2500000 * networkscale)
# Create the simulation
simulator = Simulator(bw_parser, relaytarget, clienttarget, statslogger)
for e in range(numepochs):
statslogger.info("Starting epoch %s simulation", e+3)
simulator.one_epoch()
maxmemmib = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
statslogger.info("%d MiB used", maxmemmib)