simulator.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. #!/usr/bin/env python3
  2. import random # For simulation, not cryptography!
  3. import math
  4. import sys
  5. import logging
  6. import resource
  7. import network
  8. import dirauth
  9. import relay
  10. import client
  11. class Simulator:
  12. def __init__(self, relaytarget, clienttarget, statslogger):
  13. self.relaytarget = relaytarget
  14. self.clienttarget = clienttarget
  15. self.statslogger = statslogger
  16. # Some (for now) hard-coded parameters
  17. # The number of directory authorities
  18. numdirauths = 9
  19. # The fraction of relays that are fallback relays
  20. fracfallbackrelays = 0.05
  21. # Mean number of circuits created per client per epoch
  22. self.gamma = 8.9
  23. # Start some dirauths
  24. self.dirauthaddrs = []
  25. self.dirauths = []
  26. for i in range(numdirauths):
  27. dira = dirauth.DirAuth(i, numdirauths)
  28. self.dirauths.append(dira)
  29. self.dirauthaddrs.append(dira.netaddr)
  30. # Start some relays
  31. self.relays = []
  32. for i in range(self.relaytarget):
  33. # Relay bandwidths (at least the ones fast enough to get used)
  34. # in the live Tor network (as of Dec 2019) are well approximated
  35. # by (200000-(200000-25000)/3*log10(x)) where x is a
  36. # uniform integer in [1,2500]
  37. x = random.randint(1,2500)
  38. bw = int(200000-(200000-25000)/3*math.log10(x))
  39. self.relays.append(relay.Relay(self.dirauthaddrs, bw, 0))
  40. # The fallback relays are a hardcoded list of a small fraction
  41. # of the relays, used by clients for bootstrapping
  42. numfallbackrelays = int(self.relaytarget * fracfallbackrelays) + 1
  43. fallbackrelays = random.sample(self.relays, numfallbackrelays)
  44. for r in fallbackrelays:
  45. r.set_is_fallbackrelay()
  46. network.thenetwork.setfallbackrelays(fallbackrelays)
  47. # Tick the epoch to build the first consensus
  48. network.thenetwork.nextepoch()
  49. # Start some clients
  50. self.clients = []
  51. for i in range(clienttarget):
  52. self.clients.append(client.Client(self.dirauthaddrs))
  53. # Throw away all the performance statistics to this point
  54. for d in self.dirauths: d.perfstats.reset()
  55. for r in self.relays: r.perfstats.reset()
  56. for c in self.clients: c.perfstats.reset()
  57. # Tick the epoch to bootstrap the clients
  58. network.thenetwork.nextepoch()
  59. def one_epoch(self):
  60. """Simulate one epoch."""
  61. epoch = network.thenetwork.getepoch()
  62. # Each client will start a random number of circuits in a
  63. # Poisson distribution with mean gamma. To randomize the order
  64. # of the clients creating each circuit, we actually use a
  65. # Poisson distribution with mean (gamma*num_clients), and assign
  66. # each event to a uniformly random client. (This does in fact
  67. # give the required distribution.)
  68. numclients = len(self.clients)
  69. # simtime is the simulated time, measured in epochs (i.e.,
  70. # 0=start of this epoch; 1=end of this epoch)
  71. simtime = 0
  72. numcircs = 0
  73. allcircs = []
  74. lastpercent = -1
  75. while simtime < 1.0:
  76. allcircs.append(
  77. random.choice(self.clients).channelmgr.new_circuit())
  78. simtime += random.expovariate(self.gamma * numclients)
  79. numcircs += 1
  80. percent = int(100*simtime)
  81. #if percent != lastpercent:
  82. if numcircs % 100 == 0:
  83. logging.info("Creating circuits in epoch %s: %d%% (%d circuits)",
  84. epoch, percent, numcircs)
  85. lastpercent = percent
  86. # gather stats
  87. totsent = 0
  88. totrecv = 0
  89. dirasent = 0
  90. dirarecv = 0
  91. relaysent = 0
  92. relayrecv = 0
  93. clisent = 0
  94. clirecv = 0
  95. dirastats = network.PerfStatsStats()
  96. for d in self.dirauths:
  97. logging.debug("%s", d.perfstats)
  98. dirasent += d.perfstats.bytes_sent
  99. dirarecv += d.perfstats.bytes_received
  100. dirastats.accum(d.perfstats)
  101. totsent += dirasent
  102. totrecv += dirarecv
  103. relaystats = network.PerfStatsStats()
  104. relaybstats = network.PerfStatsStats()
  105. relaynbstats = network.PerfStatsStats()
  106. for r in self.relays:
  107. logging.debug("%s", r.perfstats)
  108. relaysent += r.perfstats.bytes_sent
  109. relayrecv += r.perfstats.bytes_received
  110. relaystats.accum(r.perfstats)
  111. if r.perfstats.is_bootstrapping:
  112. relaybstats.accum(r.perfstats)
  113. else:
  114. relaynbstats.accum(r.perfstats)
  115. totsent += relaysent
  116. totrecv += relayrecv
  117. clistats = network.PerfStatsStats()
  118. clibstats = network.PerfStatsStats()
  119. clinbstats = network.PerfStatsStats()
  120. for c in self.clients:
  121. logging.debug("%s", c.perfstats)
  122. clisent += c.perfstats.bytes_sent
  123. clirecv += c.perfstats.bytes_received
  124. clistats.accum(c.perfstats)
  125. if c.perfstats.is_bootstrapping:
  126. clibstats.accum(c.perfstats)
  127. else:
  128. clinbstats.accum(c.perfstats)
  129. totsent += clisent
  130. totrecv += clirecv
  131. self.statslogger.info("DirAuths sent=%s recv=%s" % (dirasent, dirarecv))
  132. self.statslogger.info("Relays sent=%s recv=%s" % (relaysent, relayrecv))
  133. self.statslogger.info("Client sent=%s recv=%s" % (clisent, clirecv))
  134. self.statslogger.info("Total sent=%s recv=%s" % (totsent, totrecv))
  135. numdirauths = len(self.dirauths)
  136. numrelays = len(self.relays)
  137. numclients = len(self.clients)
  138. self.statslogger.info("Dirauths %s", dirastats)
  139. self.statslogger.info("Relays %s", relaystats)
  140. self.statslogger.info("Relays(B) %s", relaybstats)
  141. self.statslogger.info("Relays(NB) %s", relaynbstats)
  142. self.statslogger.info("Clients %s", clistats)
  143. self.statslogger.info("Clients(B) %s", clibstats)
  144. self.statslogger.info("Clients(NB) %s", clinbstats)
  145. # Close circuits
  146. for c in allcircs:
  147. c.close()
  148. # Reset stats
  149. for d in self.dirauths: d.perfstats.reset()
  150. for r in self.relays: r.perfstats.reset()
  151. for c in self.clients: c.perfstats.reset()
  152. # Churn relays
  153. # Churn is controlled by three parameters:
  154. # newmean: the mean number of new arrivals per epoch
  155. # newstddev: the stddev number of new arrivals per epoch
  156. # oldprob: the probability any given existing one leaves per epoch
  157. # If target is the desired steady state number, then it should
  158. # be the case that target * oldprob = newmean. That way, if the
  159. # current number is below target, on average you add more than
  160. # you remove, and if the current number is above target, on
  161. # average you add fewer than you remove.
  162. # For relays, looking at all the consensuses for Nov and Dec
  163. # 2019, newmean is about 1.0% of the network size, and newstddev
  164. # is about 0.3% of the network size.
  165. relay_newmean = 0.010 * self.relaytarget
  166. relay_newstddev = 0.003 * self.relaytarget
  167. relay_oldprob = 0.010
  168. # Stop some of the (non-fallback) relays
  169. relays_terminating = []
  170. for r in self.relays:
  171. if not r.is_fallbackrelay and random.random() < relay_oldprob:
  172. relays_terminating.append(r)
  173. for r in relays_terminating:
  174. r.terminate()
  175. self.relays.remove(r)
  176. # Start some new relays
  177. relays_new = int(random.normalvariate(relay_newmean, relay_newstddev))
  178. if relays_new > 0:
  179. for i in range(relays_new):
  180. x = random.randint(1,2500)
  181. bw = int(200000-(200000-25000)/3*math.log10(x))
  182. self.relays.append(relay.Relay(self.dirauthaddrs, bw, 0))
  183. # TODO: churn clients
  184. # Tick the epoch
  185. network.thenetwork.nextepoch()
  186. if __name__ == '__main__':
  187. # Args: womode snipauthmode networkscale numepochs randseed
  188. if len(sys.argv) != 6:
  189. sys.stderr.write("Usage: womode snipauthmode networkscale numepochs randseed\n")
  190. sys.exit(1)
  191. womode = network.WOMode[sys.argv[1].upper()]
  192. snipauthmode = network.SNIPAuthMode[sys.argv[2].upper()]
  193. networkscale = float(sys.argv[3])
  194. numepochs = int(sys.argv[4])
  195. randseed = int(sys.argv[5])
  196. logfile = "%s_%s_%f_%s_%s.log" % (womode.name, snipauthmode.name,
  197. networkscale, numepochs, randseed)
  198. # Seed the PRNG. On Ubuntu 18.04, this in fact makes future calls
  199. # to (non-cryptographic) random numbers deterministic. On Ubuntu
  200. # 16.04, it does not.
  201. random.seed(randseed)
  202. loglevel = logging.INFO
  203. # Uncomment to see all the debug messages
  204. # loglevel = logging.DEBUG
  205. logging.basicConfig(level=loglevel,
  206. format="%(asctime)s:%(levelname)s:%(message)s")
  207. # The gathered statistics get logged separately
  208. statslogger = logging.getLogger("simulator")
  209. handler = logging.FileHandler(logfile)
  210. handler.setFormatter(logging.Formatter("%(asctime)s:%(message)s"))
  211. statslogger.addHandler(handler)
  212. statslogger.setLevel(logging.INFO)
  213. # Set the Walking Onions style to use
  214. network.thenetwork.set_wo_style(womode, snipauthmode)
  215. # The steady-state numbers of relays and clients
  216. relaytarget = math.ceil(6500 * networkscale)
  217. clienttarget = math.ceil(2500000 * networkscale)
  218. # Create the simulation
  219. simulator = Simulator(relaytarget, clienttarget, statslogger)
  220. for e in range(numepochs):
  221. statslogger.info("Starting epoch %s simulation", e+3)
  222. simulator.one_epoch()
  223. maxmemmib = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
  224. statslogger.info("%d MiB used", maxmemmib)