[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [torspec/master] Add proposal #327: "A First Take at PoW Over Introduction Circuits"
commit 1f52ffadc92adaa666d738b3a76ad4d2b603e968
Author: George Kadianakis <desnacked@xxxxxxxxxx>
Date: Tue Sep 22 14:22:15 2020 +0300
Add proposal #327: "A First Take at PoW Over Introduction Circuits"
---
proposals/327-pow-over-intro.txt | 1129 ++++++++++++++++++++++++++++++++++++++
1 file changed, 1129 insertions(+)
diff --git a/proposals/327-pow-over-intro.txt b/proposals/327-pow-over-intro.txt
new file mode 100644
index 0000000..fb58a7d
--- /dev/null
+++ b/proposals/327-pow-over-intro.txt
@@ -0,0 +1,1129 @@
+Filename: 327-pow-over-intro.txt
+Title: A First Take at PoW Over Introduction Circuits
+Author: George Kadianakis, Mike Perry, David Goulet, tevador
+Created: 2 April 2020
+Status: Draft
+
+0. Abstract
+
+ This proposal aims to thwart introduction flooding DoS attacks by introducing
+ a dynamic Proof-Of-Work protocol that occurs over introduction circuits.
+
+1. Motivation
+
+ So far our attempts at limiting the impact of introduction flooding DoS
+ attacks on onion services has been focused on horizontal scaling with
+ Onionbalance, optimizing the CPU usage of Tor and applying congestion control
+ using rate limiting. While these measures move the goalpost forward, a core
+ problem with onion service DoS is that building rendezvous circuits is a
+ costly procedure both for the service and for the network. For more
+ information on the limitations of rate-limiting when defending against DDoS,
+ see [REF_TLS_1].
+
+ If we ever hope to have truly reachable global onion services, we need to
+ make it harder for attackers to overload the service with introduction
+ requests. This proposal achieves this by allowing onion services to specify
+ an optional dynamic proof-of-work scheme that its clients need to participate
+ in if they want to get served.
+
+ With the right parameters, this proof-of-work scheme acts as a gatekeeper to
+ block amplification attacks by attackers while letting legitimate clients
+ through.
+
+1.1. Related work
+
+ For a similar concept, see the three internet drafts that have been proposed
+ for defending against TLS-based DDoS attacks using client puzzles [REF_TLS].
+
+1.2. Threat model [THREAT_MODEL]
+
+1.2.1. Attacker profiles [ATTACKER_MODEL]
+
+ This proposal is written to thwart specific attackers. A simple PoW proposal
+ cannot defend against all and every DoS attack on the Internet, but there are
+ adverary models we can defend against.
+
+ Let's start with some adversary profiles:
+
+ "The script-kiddie"
+
+ The script-kiddie has a single computer and pushes it to its
+ limits. Perhaps it also has a VPS and a pwned server. We are talking about
+ an attacker with total access to 10 Ghz of CPU and 10 GBs of RAM. We
+ consider the total cost for this attacker to be zero $.
+
+ "The small botnet"
+
+ The small botnet is a bunch of computers lined up to do an introduction
+ flooding attack. Assuming 500 medium-range computers, we are talking about
+ an attacker with total access to 10 Thz of CPU and 10 TB of RAM. We consider
+ the upfront cost for this attacker to be about $400.
+
+ "The large botnet"
+
+ The large botnet is a serious operation with many thousands of computers
+ organized to do this attack. Assuming 100k medium-range computers, we are
+ talking about an attacker with total access to 200 Thz of CPU and 200 TB of
+ RAM. The upfront cost for this attacker is about $36k.
+
+ We hope that this proposal can help us defend against the script-kiddie
+ attacker and small botnets. To defend against a large botnet we would need
+ more tools in our disposal (see [FUTURE_DESIGNS]).
+
+1.2.2. User profiles [USER_MODEL]
+
+ We have attackers and we have users. Here are a few user profiles:
+
+ "The standard web user"
+
+ This is a standard laptop/desktop user who is trying to browse the
+ web. They don't know how these defences work and they don't care to
+ configure or tweak them. They are gonna use the default values and if the
+ site doesn't load, they are gonna close their browser and be sad at Tor.
+ They run a 2Ghz computer with 4GB of RAM.
+
+ "The motivated user"
+
+ This is a user that really wants to reach their destination. They don't
+ care about the journey; they just want to get there. They know what's going
+ on; they are willing to tweak the default values and make their computer do
+ expensive multi-minute PoW computations to get where they want to be.
+
+ "The mobile user"
+
+ This is a motivated user on a mobile phone. Even tho they want to read the
+ news article, they don't have much leeway on stressing their machine to do
+ more computation.
+
+ We hope that this proposal will allow the motivated user to always connect
+ where they want to connect to, and also give more chances to the other user
+ groups to reach the destination.
+
+1.2.3. The DoS Catch-22 [CATCH22]
+
+ This proposal is not perfect and it does not cover all the use cases. Still,
+ we think that by covering some use cases and giving reachability to the
+ people who really need it, we will severely demotivate the attackers from
+ continuing the DoS attacks and hence stop the DoS threat all
+ together. Furthermore, by increasing the cost to launch a DoS attack, a big
+ class of DoS attackers will disappear from the map, since the expected ROI
+ will decrease.
+
+2. System Overview
+
+2.1. Tor protocol overview
+
+ +----------------------------------+
+ | Onion Service |
+ +-------+ INTRO1 +-----------+ INTRO2 +--------+ |
+ |Client |-------->|Intro Point|------->| PoW |-----------+ |
+ +-------+ +-----------+ |Verifier| | |
+ +--------+ | |
+ | | |
+ | | |
+ | +----------v---------+ |
+ | |Intro Priority Queue| |
+ +---------+--------------------+---+
+ | | |
+ Rendezvous | | |
+ circuits | | |
+ v v v
+
+
+
+ The proof-of-work scheme specified in this proposal takes place during the
+ introduction phase of the onion service protocol.
+
+ The system described in this proposal is not meant to be on all the time, and
+ should only be enabled by services when under duress. The percentage of
+ clients receiving puzzles can also be configured based on the load of the
+ service.
+
+ In summary, the following steps are taken for the protocol to complete:
+
+ 1) Service encodes PoW parameters in descriptor [DESC_POW]
+ 2) Client fetches descriptor and computes PoW [CLIENT_POW]
+ 3) Client completes PoW and sends results in INTRO1 cell [INTRO1_POW]
+ 4) Service verifies PoW and queues introduction based on PoW effort [SERVICE_VERIFY]
+
+2.2. Proof-of-work overview
+
+2.2.1. Primitives
+
+ For our proof-of-work function we will use the 'equix' scheme by tevador
+ [REF_EQUIX]. Equix is an assymetric PoW function based on Equihash<60,3>. It
+ features lightning fast verification speed, and also aims to minimize the
+ assymetry between CPU and GPU. Furthermore, it's designed for this particular
+ use-case and hence cryptocurrency miners are not incentivized to make
+ optimized ASICs for it.
+
+ The Equix scheme provides two functions that will be used in this proposal:
+ - equix_solve(seed, nonce, effort) which solves a puzzle instance.
+ - equix_verify(seed, nonce, effort, solution) which verifies a puzzle solution.
+
+ We tune equix in section [EQUIX_TUNING].
+
+2.2.2. Dynamic PoW
+
+ DoS is a dynamic problem where the attacker's capabilities constantly change,
+ and hence we want our proof-of-work system to be dynamic and not stuck with a
+ static difficulty setting. Hence, instead of forcing clients to go below a
+ static target like in Bitcoin to be successful, we ask clients to "bid" using
+ their PoW effort. Effectively, a client gets higher priority the higher
+ effort they put into their proof-of-work. This is similar to how
+ proof-of-stake works but instead of staking coins, you stake work.
+
+ The benefit here is that legitimate clients who really care about getting
+ access can spend a big amount of effort into their PoW computation, which
+ should guarantee access to the service given reasonable adversary models. See
+ [PARAM_TUNING] for more details about these guarantees and tradeoffs.
+
+ As a way to improve reachability and UX, the service tries to estimate the
+ effort needed for clients to get access at any given time and places it in
+ the descriptor. See [EFFORT_ESTIMATION] for more details.
+
+2.2.3. PoW effort
+
+ For our dynamic PoW system to work, we will need to be able to compare PoW
+ tokens with each other. To do so we define a function:
+ unsigned effort(uint8_t *token)
+ which takes as its argument a hash output token, interprets it as a
+ bitstring, and returns the quotient of dividing a bitstring of 1s by it.
+
+ So for example:
+ effort(00000001100010101101) = 11111111111111111111 / 00000001100010101101
+ or the same in decimal:
+ effort(6317) = 1048575 / 6317 = 165.
+
+ This definition of effort has the advantage of directly expressing the
+ expected number of hashes that the client had to calculate to reach the
+ effort. This is in contrast to the (cheaper) exponential effort definition of
+ taking the number of leading zero bits.
+
+3. Protocol specification
+
+3.1. Service encodes PoW parameters in descriptor [DESC_POW]
+
+ This whole protocol starts with the service encoding the PoW parameters in
+ the 'encrypted' (inner) part of the v3 descriptor. As follows:
+
+ "pow-params" SP type SP seed-b64 SP expiration-time NL
+
+ [At most once]
+
+ type: The type of PoW system used. We call the one specified here "v1"
+
+ seed-b64: A random seed that should be used as the input to the PoW
+ hash function. Should be 32 random bytes encoded in base64
+ without trailing padding.
+
+ suggested-effort: An unsigned integer specifying an effort value that
+ clients should aim for when contacting the service. See
+ [EFFORT_ESTIMATION] for more details here.
+
+ expiration-time: A timestamp in "YYYY-MM-DD SP HH:MM:SS" format after
+ which the above seed expires and is no longer valid as
+ the input for PoW. It's needed so that the size of our
+ replay cache does not grow infinitely. It should be
+ set to RAND_TIME(now+7200, 900) seconds.
+
+ The service should refresh its seed when expiration-time passes. The service
+ SHOULD keep its previous seed in memory and accept PoWs using it to avoid
+ race-conditions with clients that have an old seed. The service SHOULD avoid
+ generating two consequent seeds that have a common 4 bytes prefix. See
+ [INTRO1_POW] for more info.
+
+ By RAND_TIME(ts, interval) we mean a time between ts-interval and ts, chosen
+ uniformly at random.
+
+3.2. Client fetches descriptor and computes PoW [CLIENT_POW]
+
+ If a client receives a descriptor with "pow-params", it should assume that
+ the service is expecting a PoW input as part of the introduction protocol.
+
+ The client parses the descriptor and extracts the PoW parameters. It makes
+ sure that the <expiration-time> has not expired and if it has, it needs to
+ fetch a new descriptor.
+
+ The client should then extract the <suggested-effort> field to configure its
+ PoW 'target' (see [REF_TARGET]). The client SHOULD NOT accept 'target' values
+ that will cause an infinite PoW computation. {XXX: How to enforce this?}
+
+ To complete the PoW the client follows the following logic:
+
+ a) Client selects a target effort E.
+ b) Client generates a random 16-byte nonce N.
+ c) Client derives seed C by decoding 'seed-b64'.
+ d) Client calculates S = equix_solve(C || N || E)
+ e) Client calculates R = blake2b(C || N || E || S)
+ f) Client checks if R * E <= UINT32_MAX.
+ f1) If yes, success! The client can submit N, E, the first 4 bytes of C
+ and S.
+ f2) If no, fail! The client interprets N as a 16-byte little-endian
+ integer, increments it by 1 and goes back to step d).
+
+ At the end of the above procedure, the client should have S as the solution
+ of the Equix puzzle with N as the nonce, C as the seed. How quickly this
+ happens depends solely on the target effort E parameter.
+
+3.3. Client sends PoW in INTRO1 cell [INTRO1_POW]
+
+ Now that the client has an answer to the puzzle it's time to encode it into
+ an INTRODUCE1 cell. To do so the client adds an extension to the encrypted
+ portion of the INTRODUCE1 cell by using the EXTENSIONS field (see
+ [PROCESS_INTRO2] section in rend-spec-v3.txt). The encrypted portion of the
+ INTRODUCE1 cell only gets read by the onion service and is ignored by the
+ introduction point.
+
+ We propose a new EXT_FIELD_TYPE value:
+
+ [01] -- PROOF_OF_WORK
+
+ The EXT_FIELD content format is:
+
+ POW_VERSION [1 byte]
+ POW_NONCE [16 bytes]
+ POW_EFFORT [4 bytes]
+ POW_SEED [4 bytes]
+ POW_SOLUTION [16 bytes]
+
+ where:
+
+ POW_VERSION is 1 for the protocol specified in this proposal
+ POW_NONCE is the nonce 'N' from the section above
+ POW_SEED is the first 4 bytes of the seed used
+
+ This will increase the INTRODUCE1 payload size by 43 bytes since the
+ extension type and length is 2 extra bytes, the N_EXTENSIONS field is always
+ present and currently set to 0 and the EXT_FIELD is 41 bytes. According to
+ ticket #33650, INTRODUCE1 cells currently have more than 200 bytes
+ available.
+
+3.4. Service verifies PoW and handles the introduction [SERVICE_VERIFY]
+
+ When a service receives an INTRODUCE1 with the PROOF_OF_WORK extension, it
+ should check its configuration on whether proof-of-work is required to
+ complete the introduction. If it's not required, the extension SHOULD BE
+ ignored. If it is required, the service follows the procedure detailed in
+ this section.
+
+ If the service requires the PROOF_OF_WORK extension but received an
+ INTRODUCE1 cell without any embedded proof-of-work, the service SHOULD
+ consider this cell as a zero-effort introduction for the purposes of the
+ priority queue (see section [INTRO_QUEUE]).
+
+3.4.1. PoW verification [POW_VERIFY]
+
+ To verify the client's proof-of-work the service MUST do the following steps:
+
+ a) Find a valid seed C that starts with POW_SEED. Fail if no such seed
+ exists.
+ b) Fail if E = POW_EFFORT is lower than the minimum effort.
+ c) Fail if N = POW_NONCE is present in the replay cache (see [REPLAY_PROTECTION[)
+ d) Calculate R = blake2b(C || N || E || S)
+ e) Fail if R * E > UINT32_MAX
+ f) Fail if equix_verify(C || N || E, S) != EQUIX_OK
+ g) Put the request in the queue with a priority of E
+
+ If any of these steps fail the service MUST ignore this introduction request
+ and abort the protocol.
+
+ In this proposal we call the above steps the "top half" of introduction
+ handling. If all the steps of the "top half" have passed, then the circuit
+ is added to the introduction queue as detailed in section [INTRO_QUEUE].
+
+3.4.1.1. Replay protection [REPLAY_PROTECTION]
+
+ The service MUST NOT accept introduction requests with the same (seed, nonce)
+ tuple. For this reason a replay protection mechanism must be employed.
+
+ The simplest way is to use a simple hash table to check whether a (seed,
+ nonce) tuple has been used before for the actiev duration of a
+ seed. Depending on how long a seed stays active this might be a viable
+ solution with reasonable memory/time overhead.
+
+ If there is a worry that we might get too many introductions during the
+ lifetime of a seed, we can use a Bloom filter as our replay cache
+ mechanism. The probabilistic nature of Bloom filters means that sometimes we
+ will flag some connections as replays even if they are not; with this false
+ positive probability increasing as the number of entries increase. However,
+ with the right parameter tuning this probability should be negligible and
+ well handled by clients. {TODO: Figure bloom filter}
+
+3.4.2. The Introduction Queue [INTRO_QUEUE]
+
+3.4.2.1. Adding introductions to the introduction queue [ADD_QUEUE]
+
+ When PoW is enabled and a verified introduction comes through, the service
+ instead of jumping straight into rendezvous, queues it and prioritizes it
+ based on how much effort was devoted by the client to PoW. This means that
+ introduction requests with high effort should be prioritized over those with
+ low effort.
+
+ To do so, the service maintains an "introduction priority queue" data
+ structure. Each element in that priority queue is an introduction request,
+ and its priority is the effort put into its PoW:
+
+ When a verified introduction comes through, the service uses the effort()
+ function with the solution S as its input, and uses the output to place requests
+ into the right position of the priority_queue: The bigger the effort, the
+ more priority it gets in the queue. If two elements have the same effort, the
+ older one has priority over the newer one.
+
+3.4.2.2. Handling introductions from the introduction queue [HANDLE_QUEUE]
+
+ The service should handle introductions by pulling from the introduction
+ queue. We call this part of introduction handling the "bottom half" because
+ most of the computation happens in this stage. For a description of how we
+ expect such a system to work in Tor, see [TOR_SCHEDULER] section.
+
+3.4.3. PoW effort estimation [EFFORT_ESTIMATION]
+
+ The service starts with a default suggested-effort value of 5000 (see
+ [EQUIX_DIFFICULTY] section for more info).
+
+ Then during its operation the service continuously keeps track of the
+ received PoW cell efforts to inform its clients of the effort they should put
+ in their introduction to get service. The service informs the clients by
+ using the <suggested-effort> field in the descriptor.
+
+ Everytime the service handles or trims an introduction request from the
+ priority queue in [HANDLE_QUEUE], the service adds the request's effort to a
+ sorted list.
+
+ Then every HS_UPDATE_PERIOD seconds (which is controlled through a consensus
+ parameter and has a default value of 300 seconds) and while the DoS feature
+ is enabled, the service updates its <suggested-effort> value as follows:
+
+ 1. Set TOTAL_EFFORT to the sum of the effort of all valid requests that
+ have been received since the last HS descriptor update (this includes
+ all handled requests, trimmed requests and requests still in the queue)
+
+ 2. Set SUGGESTED_EFFORT = TOTAL_EFFORT / (SVC_BOTTOM_CAPACITY * HS_UPDATE_PERIOD).
+ The denominator above is the max number of requests that the service
+ could have handled during that time.
+
+ 3. Set <suggested-effort> to max(MIN_EFFORT, SUGGESTED_EFFORT).
+
+ During the above procedure we use the following default values:
+ - MIN_EFFORT = 1000, as the result of a simulation experiment [REF_TEVADOR_SIM]
+ - SVC_BOTTOM_CAPACITY = 100, which is the number of introduction requests
+ that can be handled by the service per second. This was computed in
+ [POW_DIFFICULTY_TOR] as 180, but we reduced it to 100 to account for
+ slower computers and networks.
+
+ The above algorithm is meant to balance the suggested effort based on the
+ effort of all received requests. It attempts to dynamically adjust the
+ suggested effort so that it increases when an attack is received, and tones
+ down when the attack has stopped.
+
+ It's worth noting that the suggested-effort is not a hard limit to the
+ efforts that are accepted by the service, and it's only meant to serve as a
+ guideline for clients to reduce the number of unsuccessful requests that get
+ to the service. The service still adds requests with lower effort than
+ suggested-effort to the priority queue in [ADD_QUEUE].
+
+ Finally, the above algorithm will never reset back to zero suggested-effort,
+ even if the attack is completely over. That's because in that case it would
+ be impossible to determine the total computing power of connecting
+ clients. Instead it will reset back to MIN_EFFORT, and the operator will have
+ to manually shut down the anti-DoS mechanism.
+
+ {XXX: SVC_BOTTOM_CAPACITY is static above and will not be accurate for all
+ boxes. Ideally we should calculate SVC_BOTTOM_CAPACITY dynamically based on
+ the resources of every onion service while the algorithm is running.}
+
+3.4.3.1. Updating descriptor with new suggested effort
+
+ Every HS_UPDATE_PERIOD seconds the service should upload a new descriptor
+ with a new suggested-effort value.
+
+ The service should avoid uploading descriptors too often to avoid overwheming
+ the HSDirs. The service SHOULD NOT upload descriptors more often than
+ HS_UPDATE_PERIOD. The service SHOULD NOT upload a new descriptor if the
+ suggested-effort value changes by less than 15%.
+
+ {XXX: Is this too often? Perhaps we can set different limits for when the
+ difficulty goes up and different for when it goes down. It's more important
+ to update the descriptor when the difficulty goes up.}
+
+ {XXX: What attacks are possible here? Can the attacker intentionally hit this
+ rate-limit and then influence the suggested effort so that clients do not
+ learn about the new effort?}
+
+4. Client behavior [CLIENT_BEHAVIOR]
+
+ This proposal introduces a bunch of new ways where a legitimate client can
+ fail to reach the onion service.
+
+ Furthermore, there is currently no end-to-end way for the onion service to
+ inform the client that the introduction failed. The INTRO_ACK cell is not
+ end-to-end (it's from the introduction point to the client) and hence it does
+ not allow the service to inform the client that the rendezvous is never gonna
+ occur.
+
+ For this reason we need to define some client behaviors to work around these
+ issues.
+
+4.1. Clients handling timeouts [CLIENT_TIMEOUT]
+
+ Alice can fail to reach the onion service if her introduction request gets
+ trimmed off the priority queue in [HANDLE_QUEUE], or if the service does not
+ get through its priority queue in time and the connection times out.
+
+ This section presents a heuristic method for the client getting service even
+ in such scenarios.
+
+ If the rendezvous request times out, the client SHOULD fetch a new descriptor
+ for the service to make sure that it's using the right suggested-effort for
+ the PoW and the right PoW seed. The client SHOULD NOT fetch service
+ descriptors more often than every 'hs-pow-desc-fetch-rate-limit' seconds
+ (which is controlled through a consensus parameter and has a default value of
+ 600 seconds).
+
+ {XXX: Is this too rare? Too often?}
+
+ When the client fetches a new descriptor, it should try connecting to the
+ service with the new suggested-effort and PoW seed. If that doesn't work, it
+ should double the effort and retry. The client should keep on
+ doubling-and-retrying until it manages to get service, or its able to fetch a
+ new descriptor again.
+
+ {XXX: This means that the client will keep on spinning and
+ doubling-and-retrying for a service under this situation. There will never be
+ a "Client connection timed out" page for the user. Is this good? Is this bad?
+ Should we stop doubling-and-retrying after some iterations? Or should we
+ throw a custom error page to the user, and ask the user to stop spinning
+ whenever they want?}
+
+4.3. Other descriptor issues
+
+ Another race condition here is if the service enables PoW, while a client has
+ a cached descriptor. How will the client notice that PoW is needed? Does it
+ need to fetch a new descriptor? Should there be another feedback mechanism?
+
+5. Attacker strategies [ATTACK_META]
+
+ Now that we defined our protocol we need to start tweaking the various
+ knobs. But before we can do that, we first need to understand a few
+ high-level attacker strategies to see what we are fighting against.
+
+5.1.1. Overwhelm PoW verification (aka "Overwhelm top half") [ATTACK_TOP_HALF]
+
+ A basic attack here is the adversary spamming with bogus INTRO cells so that
+ the service does not have computing capacity to even verify the
+ proof-of-work. This adversary tries to overwhelm the procedure in the
+ [POW_VERIFY] section.
+
+ That's why we need the PoW algorithm to have a cheap verification time so
+ that this attack is not possible: we tune this PoW parameter in section
+ [POW_TUNING_VERIFICATION].
+
+5.1.2. Overwhelm rendezvous capacity (aka "Overwhelm bottom half") [ATTACK_BOTTOM_HALF]
+
+ Given the way the introduction queue works (see [HANDLE_QUEUE]), a very
+ effective strategy for the attacker is to totally overwhelm the queue
+ processing by sending more high-effort introductions than the onion service
+ can handle at any given tick. This adversary tries to overwhelm the procedure
+ in the [HANDLE_QUEUE] section.
+
+ To do so, the attacker would have to send at least 20 high-effort
+ introduction cells every 100ms, where high-effort is a PoW which is above the
+ estimated level of "the motivated user" (see [USER_MODEL]).
+
+ An easier attack for the adversary, is the same strategy but with
+ introduction cells that are all above the comfortable level of "the standard
+ user" (see [USER_MODEL]). This would block out all standard users and only
+ allow motivated users to pass.
+
+5.1.3. Hybrid overwhelm strategy [ATTACK_HYBRID]
+
+ If both the top- and bottom- halves are processed by the same thread, this
+ opens up the possibility for a "hybrid" attack. Given the performance figures
+ for the bottom half (0.31 ms/req.) and the top half (5.5 ms/req.), the
+ attacker can optimally deny service by submitting 91 high-effort requests and
+ 1520 invalid requests per second. This will completely saturate the main loop
+ because:
+
+ 0.31*(1520+91) ~ 0.5 sec.
+ 5.5*91 ~ 0.5 sec.
+
+ This attack only has half the bandwidth requirement of [ATTACK_TOP_HALF] and
+ half the compute requirement of [ATTACK_BOTTOM_HALF].
+
+ Alternatively, the attacker can adjust the ratio between invalid and
+ high-effort requests depending on their bandwidth and compute capabilities.
+
+5.1.4. Gaming the effort estimation logic [ATTACK_EFFORT]
+
+ Another way to beat this system is for the attacker to game the effort
+ estimation logic (see [EFFORT_ESTIMATION]). Essentialy, there are two attacks
+ that we are trying to avoid:
+
+ - Attacker sets descriptor suggested-effort to a very high value effectively
+ making it impossible for most clients to produce a PoW token in a
+ reasonable timeframe.
+ - Attacker sets descriptor suggested-effort to a very small value so that
+ most clients aim for a small value while the attacker comfortably launches
+ an [ATTACK_BOTTOM_HALF] using medium effort PoW (see [REF_TEVADOR_1])
+
+5.1.4. Precomputed PoW attack
+
+ The attacker may precompute many valid PoW nonces and submit them all at once
+ before the current seed expires, overwhelming the service temporarily even
+ using a single computer. The current scheme gives the attackers 4 hours to
+ launch this attack since each seed lasts 2 hours and the service caches two
+ seeds.
+
+ An attacker with this attack might be aiming to DoS the service for a limited
+ amount of time, or to cause an [ATTACK_EFFORT] attack.
+
+6. Parameter tuning [POW_TUNING]
+
+ There are various parameters in this PoW system that need to be tuned:
+
+ We first start by tuning the time it takes to verify a PoW token. We do this
+ first because it's fundamental to the performance of onion services and can
+ turn into a DoS vector of its own. We will do this tuning in a way that's
+ agnostic to the chosen PoW function.
+
+ We will then move towards analyzing the default difficulty setting for our
+ PoW system. That defines the expected time for clients to succeed in our
+ system, and the expected time for attackers to overwhelm our system. Same as
+ above we will do this in a way that's agnostic to the chosen PoW function.
+
+ Finally, using those two pieces we will tune our PoW function and pick the
+ right default difficulty setting. At the end of this section we will know the
+ resources that an attacker needs to overwhelm the onion service, the
+ resources that the service needs to verify introduction requests, and the
+ resources that legitimate clients need to get to the onion service.
+
+6.1. PoW verification [POW_TUNING_VERIFICATION]
+
+ Verifying a PoW token is the first thing that a service does when it receives
+ an INTRODUCE2 cell and it's detailed in section [POW_VERIFY]. This
+ verification happens during the "top half" part of the process. Every
+ milisecond spent verifying PoW adds overhead to the already existing "top
+ half" part of handling an introduction cell. Hence we should be careful to
+ add minimal overhead here so that we don't enable attacks like [ATTACK_TOP_HALF].
+
+ During our performance measurements in [TOR_MEASUREMENTS] we learned that the
+ "top half" takes about 0.26 msecs in average, without doing any sort of PoW
+ verification. Using that value we compute the following table, that describes
+ the number of cells we can queue per second (aka times we can perform the
+ "top half" process) for different values of PoW verification time:
+
+ +---------------------+-----------------------+--------------+
+ |PoW Verification Time| Total "top half" time | Cells Queued |
+ | | | per second |
+ |---------------------|-----------------------|--------------|
+ | 0 msec | 0.26 msec | 3846 |
+ | 1 msec | 1.26 msec | 793 |
+ | 2 msec | 2.26 msec | 442 |
+ | 3 msec | 3.26 msec | 306 |
+ | 4 msec | 4.26 msec | 234 |
+ | 5 msec | 5.26 msec | 190 |
+ | 6 msec | 6.26 msec | 159 |
+ | 7 msec | 7.26 msec | 137 |
+ | 8 msec | 8.26 msec | 121 |
+ | 9 msec | 9.26 msec | 107 |
+ | 10 msec | 10.26 msec | 97 |
+ +---------------------+-----------------------+--------------+
+
+ Here is how you can read the table above:
+
+ - For a PoW function with a 1ms verification time, an attacker needs to send
+ 793 dummy introduction cells per second to succeed in a [ATTACK_TOP_HALF] attack.
+
+ - For a PoW function with a 2ms verification time, an attacker needs to send
+ 442 dummy introduction cells per second to succeed in a [ATTACK_TOP_HALF] attack.
+
+ - For a PoW function with a 10ms verification time, an attacker needs to send
+ 97 dummy introduction cells per second to succeed in a [ATTACK_TOP_HALF] attack.
+
+ Whether an attacker can succeed at that depends on the attacker's resources,
+ but also on the network's capacity.
+
+ Our purpose here is to have the smallest PoW verification overhead possible
+ that also allows us to achieve all our other goals.
+
+ [Note that the table above is simply the result of a naive multiplication and
+ does not take into account all the auxiliary overheads that happen every
+ second like the time to invoke the mainloop, the bottom-half processes, or
+ pretty much anything other than the "top-half" processing.
+
+ During our measurements the time to handle INTRODUCE2 cells dominates any
+ other action time: There might be events that require a long processing time,
+ but these are pretty infrequent (like uploading a new HS descriptor) and
+ hence over a long time they smooth out. Hence extrapolating the total cells
+ queued per second based on a single "top half" time seems like good enough to
+ get some initial intuition. That said, the values of "Cells queued per
+ second" from the table above, are likely much smaller than displayed above
+ because of all the auxiliary overheads.]
+
+6.2. PoW difficulty analysis [POW_DIFFICULTY_ANALYSIS]
+
+ The difficulty setting of our PoW basically dictates how difficult it should
+ be to get a success in our PoW system. An attacker who can get many successes
+ per second can pull a successfull [ATTACK_BOTTOM_HALF] attack against our
+ system.
+
+ In classic PoW systems, "success" is defined as getting a hash output below
+ the "target". However, since our system is dynamic, we define "success" as an
+ abstract high-effort computation.
+
+ Our system is dynamic but we still need a default difficulty settings that
+ will define the metagame and be used for bootstrapping the system. The client
+ and attacker can still aim higher or lower but for UX purposes and for
+ analysis purposes we do need to define a default difficulty.
+
+6.2.1. Analysis based on adversary power
+
+ In this section we will try to do an analysis of PoW difficulty without using
+ any sort of Tor-related or PoW-related benchmark numbers.
+
+ We created the table (see [REF_TABLE]) below which shows how much time a
+ legitimate client with a single machine should expect to burn before they get
+ a single success. The x-axis is how many successes we want the attacker to be
+ able to do per second: the more successes we allow the adversary, the more
+ they can overwhelm our introduction queue. The y-axis is how many machines
+ the adversary has in her disposal, ranging from just 5 to 1000.
+
+ ===============================================================
+ | Expected Time (in seconds) Per Success For One Machine |
+ ===========================================================================
+ | |
+ | Attacker Succeses 1 5 10 20 30 50 |
+ | per second |
+ | |
+ | 5 5 1 0 0 0 0 |
+ | 50 50 10 5 2 1 1 |
+ | 100 100 20 10 5 3 2 |
+ | Attacker 200 200 40 20 10 6 4 |
+ | Boxes 300 300 60 30 15 10 6 |
+ | 400 400 80 40 20 13 8 |
+ | 500 500 100 50 25 16 10 |
+ | 1000 1000 200 100 50 33 20 |
+ | |
+ ============================================================================
+
+ Here is how you can read the table above:
+
+ - If an adversary has a botnet with 1000 boxes, and we want to limit her to 1
+ success per second, then a legitimate client with a single box should be
+ expected to spend 1000 seconds getting a single success.
+
+ - If an adversary has a botnet with 1000 boxes, and we want to limit her to 5
+ successes per second, then a legitimate client with a single box should be
+ expected to spend 200 seconds getting a single success.
+
+ - If an adversary has a botnet with 500 boxes, and we want to limit her to 5
+ successes per second, then a legitimate client with a single box should be
+ expected to spend 100 seconds getting a single success.
+
+ - If an adversary has access to 50 boxes, and we want to limit her to 5
+ successes per second, then a legitimate client with a single box should be
+ expected to spend 10 seconds getting a single success.
+
+ - If an adversary has access to 5 boxes, and we want to limit her to 5
+ successes per second, then a legitimate client with a single box should be
+ expected to spend 1 seconds getting a single success.
+
+ With the above table we can create some profiles for default values of our
+ PoW difficulty. So for example, we can use the last case as the default
+ parameter for Tor Browser, and then create three more profiles for more
+ expensive cases, scaling up to the first case which could be hardest since
+ the client is expected to spend 15 minutes for a single introduction.
+
+6.2.2. Analysis based on Tor's performance [POW_DIFFICULTY_TOR]
+
+ To go deeper here, we can use the performance measurements from
+ [TOR_MEASUREMENTS] to get a more specific intuition on the default
+ difficulty. In particular, we learned that completely handling an
+ introduction cell takes 5.55 msecs in average. Using that value, we can
+ compute the following table, that describes the number of introduction cells
+ we can handle per second for different values of PoW verification:
+
+ +---------------------+-----------------------+--------------+
+ |PoW Verification Time| Total time to handle | Cells handled|
+ | | introduction cell | per second |
+ |---------------------|-----------------------|--------------|
+ | 0 msec | 5.55 msec | 180.18 |
+ | 1 msec | 6.55 msec | 152.67 |
+ | 2 msec | 7.55 msec | 132.45 |
+ | 3 msec | 8.55 msec | 116.96 |
+ | 4 msec | 9.55 mesc | 104.71 |
+ | 5 msec | 10.55 msec | 94.79 |
+ | 6 msec | 11.55 msec | 86.58 |
+ | 7 msec | 12.55 msec | 79.68 |
+ | 8 msec | 13.55 msec | 73.80 |
+ | 9 msec | 14.55 msec | 68.73 |
+ | 10 msec | 15.55 msec | 64.31 |
+ +---------------------+-----------------------+--------------+
+
+ Here is how you can read the table above:
+
+ - For a PoW function with a 1ms verification time, an attacker needs to send
+ 152 high-effort introduction cells per second to succeed in a
+ [ATTACK_BOTTOM_HALF] attack.
+
+ - For a PoW function with a 10ms verification time, an attacker needs to send
+ 64 high-effort introduction cells per second to succeed in a
+ [ATTACK_BOTTOM_HALF] attack.
+
+ We can use this table to specify a default difficulty that won't allow our
+ target adversary to succeed in an [ATTACK_BOTTOM_HALF] attack.
+
+ Of course, when it comes to this table, the same disclaimer as in section
+ [POW_TUNING_VERIFICATION] is valid. That is, the above table is just a
+ theoretical extrapolation and we expect the real values to be much lower
+ since they depend on auxiliary processing overheads, and on the network's
+ capacity.
+
+6.3. Tuning equix difficulty [EQUIX_DIFFICULTY]
+
+ The above two sections were not depending on a particular PoW scheme. They
+ gave us an intuition on the values we are aiming for in terms of verification
+ speed and PoW difficulty. Now we need to make things concrete:
+
+ As described in section [EFFORT_ESTIMATION] we start the service with a
+ default suggested-effort value of 5000. Given the benchmarks of EquiX
+ [REF_EQUIX] this should take about 2 to 3 seconds on a modern CPU.
+
+ With this default difficulty setting and given the table in
+ [POW_DIFFICULTY_ANALYSIS] this means that an attacker with 50 boxes will be
+ able to get about 20 successful PoWs per second, and an attacker with 100
+ boxes about 40 successful PoWs per second.
+
+ Then using the table in [POW_DIFFICULTY_TOR] we can see that the number of
+ attacker's successes is not enough to overwhelm the service through an
+ [ATTACK_BOTTOM_HALF] attack. That is, an attacker would need to do about 152
+ introductions per second to overwhelm the service, whereas they can only do
+ 40 with 100 boxes.
+
+7. Discussion
+
+7.1. UX
+
+ This proposal has user facing UX consequences.
+
+ Here is some UX improvements that don't need user-input:
+
+ - Primarily, there should be a way for Tor Browser to display to users that
+ additional time (and resources) will be needed to access a service that is
+ under attack. Depending on the design of the system, it might even be
+ possible to estimate how much time it will take.
+
+ And here are a few UX approaches that will need user-input and have an
+ increasing engineering difficulty. Ideally this proposal will not need
+ user-input and the default behavior should work for almost all cases.
+
+ a) Tor Browser needs a "range field" which the user can use to specify how
+ much effort they want to spend in PoW if this ever occurs while they are
+ browsing. The ranges could be from "Easy" to "Difficult", or we could try
+ to estimate time using an average computer. This setting is in the Tor
+ Browser settings and users need to find it.
+
+ b) We start with a default effort setting, and then we use the new onion
+ errors (see #19251) to estimate when an onion service connection has
+ failed because of DoS, and only then we present the user a "range field"
+ which they can set dynamically. Detecting when an onion service connection
+ has failed because of DoS can be hard because of the lack of feedback (see
+ [CLIENT_BEHAVIOR])
+
+ c) We start with a default effort setting, and if things fail we
+ automatically try to figure out an effort setting that will work for the
+ user by doing some trial-and-error connections with different effort
+ values. Until the connection succeeds we present a "Service is
+ overwhelmed, please wait" message to the user.
+
+7.2. Future work [FUTURE_WORK]
+
+7.2.1. Incremental improvements to this proposal
+
+ There are various improvements that can be done in this proposal, and while
+ we are trying to keep this v1 version simple, we need to keep the design
+ extensible so that we build more features into it. In particular:
+
+ - End-to-end introduction ACKs
+
+ This proposal suffers from various UX issues because there is no end-to-end
+ mechanism for an onion service to inform the client about its introduction
+ request. If we had end-to-end introduction ACKs many of the problems from
+ [CLIENT_BEHAVIOR] would be aleviated. The problem here is that end-to-end
+ ACKs require modifications on the introduction point code and a network
+ update which is a lengthy process.
+
+ - Multithreading scheduler
+
+ Our scheduler is pretty limited by the fact that Tor has a single-threaded
+ design. If we improve our multithreading support we could handle a much
+ greater amount of introduction requests per second.
+
+7.2.2. Future designs [FUTURE_DESIGNS]
+
+ This is just the beginning in DoS defences for Tor and there are various
+ futured designs and schemes that we can investigate. Here is a brief summary
+ of these:
+
+ "More advanced PoW schemes" -- We could use more advanced memory-hard PoW
+ schemes like MTP-argon2 or Itsuku to make it even harder for
+ adversaries to create successful PoWs. Unfortunately these schemes
+ have much bigger proof sizes, and they won't fit in INTRODUCE1 cells.
+ See #31223 for more details.
+
+ "Third-party anonymous credentials" -- We can use anonymous credentials and a
+ third-party token issuance server on the clearnet to issue tokens
+ based on PoW or CAPTCHA and then use those tokens to get access to the
+ service. See [REF_CREDS] for more details.
+
+ "PoW + Anonymous Credentials" -- We can make a hybrid of the above ideas
+ where we present a hard puzzle to the user when connecting to the
+ onion service, and if they solve it we then give the user a bunch of
+ anonymous tokens that can be used in the future. This can all happen
+ between the client and the service without a need for a third party.
+
+ All of the above approaches are much more complicated than this proposal, and
+ hence we want to start easy before we get into more serious projects.
+
+7.3. Environment
+
+ We love the environment! We are concerned of how PoW schemes can waste energy
+ by doing useless hash iterations. Here is a few reasons we still decided to
+ pursue a PoW approach here:
+
+ "We are not making things worse" -- DoS attacks are already happening and
+ attackers are already burning energy to carry them out both on the
+ attacker side, on the service side and on the network side. We think that
+ asking legitimate clients to carry out PoW computations is not gonna
+ affect the equation too much, since an attacker right now can very
+ quickly cause the same damage that hundreds of legitimate clients do a
+ whole day.
+
+ "We hope to make things better" -- The hope is that proposals like this will
+ make the DoS actors go away and hence the PoW system will not be used. As
+ long as DoS is happening there will be a waste of energy, but if we
+ manage to demotivate them with technical means, the network as a whole
+ will less wasteful. Also see [CATCH22] for a similar argument.
+
+8. Acknowledgements
+
+ Thanks a lot to tevador for the various improvements to the proposal and for
+ helping us understand and tweak the RandomX scheme.
+
+ Thanks to Solar Designer for the help in understanding the current PoW
+ landscape, the various approaches we could take, and teaching us a few neat
+ tricks.
+
+Appendix A. Little-t tor introduction scheduler
+
+ This section describes how we will implement this proposal in the "tor"
+ software (little-t tor).
+
+ The following should be read as if tor is an onion service and thus the end
+ point of all inbound data.
+
+A.1. The Main Loop [MAIN_LOOP]
+
+ Tor uses libevent for its mainloop. For network I/O operations, a mainloop
+ event is used to inform tor if it can read on a certain socket, or a
+ connection object in tor.
+
+ From there, this event will empty the connection input buffer (inbuf) by
+ extracting and processing a cell at a time. The mainloop is single threaded
+ and thus each cell is handled sequentially.
+
+ Processing an INTRODUCE2 cell at the onion service means a series of
+ operations (in order):
+
+ 1) Unpack cell from inbuf to local buffer.
+
+ 2) Decrypt cell (AES operations).
+
+ 3) Parse cell header and process it depending on its RELAY_COMMAND.
+
+ 4) INTRODUCE2 cell handling which means building a rendezvous circuit:
+ i) Path selection
+ ii) Launch circuit to first hop.
+
+ 5) Return to mainloop event which essentially means back to step (1).
+
+ Tor will read at most 32 cells out of the inbuf per mainloop round.
+
+A.2. Requirements for PoW
+
+ With this proposal, in order to prioritize cells by the amount of PoW work
+ it has done, cells can _not_ be processed sequentially as described above.
+
+ Thus, we need a way to queue a certain number of cells, prioritize them and
+ then process some cell(s) from the top of the queue (that is, the cells that
+ have done the most PoW effort).
+
+ We thus require a new cell processing flow that is _not_ compatible with
+ current tor design. The elements are:
+
+ - Validate PoW and place cells in a priority queue of INTRODUCE2 cells (as
+ described in section [INTRO_QUEUE]).
+
+ - Defer "bottom half" INTRO2 cell processing for after cells have been
+ queued into the priority queue.
+
+A.3. Proposed scheduler [TOR_SCHEDULER]
+
+ The intuitive way to address the A.2 requirements would be to do this
+ simple and naive approach:
+
+ 1) Mainloop: Empty inbuf INTRODUCE2 cells into priority queue
+
+ 2) Process all cells in pqueue
+
+ 3) Goto (1)
+
+ However, we are worried that handling all those cells before returning to the
+ mainloop opens possibilities of attack by an adversary since the priority
+ queue is not gonna be kept up to date while we process all those cells. This
+ means that we might spend lots of time dealing with introductions that don't
+ deserve it. See [BOTTOM_HALF_SCHEDULER] for more details.
+
+ We thus propose to split the INTRODUCE2 handling into two different steps:
+ "top half" and "bottom half" process, as also mentioned in [POW_VERIFY]
+ section above.
+
+A.3.1. Top half and bottom half scheduler
+
+ The top half process is responsible for queuing introductions into the
+ priority queue as follows:
+
+ a) Unpack cell from inbuf to local buffer.
+
+ b) Decrypt cell (AES operations).
+
+ c) Parse INTRODUCE2 cell header and validate PoW.
+
+ d) Return to mainloop event which essentially means step (1).
+
+ The top-half basically does all operations of section [MAIN_LOOP] except from (4).
+
+ An then, the bottom-half process is responsible for handling introductions
+ and doing rendezvous. To achieve this we introduce a new mainloop event to
+ process the priority queue _after_ the top-half event has completed. This new
+ event would do these operations sequentially:
+
+ a) Pop INTRODUCE2 cell from priority queue.
+
+ b) Parse and process INTRODUCE2 cell.
+
+ c) End event and yield back to mainloop.
+
+A.3.2. Scheduling the bottom half process [BOTTOM_HALF_SCHEDULER]
+
+ The question now becomes: when should the "bottom half" event get triggered
+ from the mainloop?
+
+ We propose that this event is scheduled in when the network I/O event
+ queues at least 1 cell into the priority queue. Then, as long as it has a
+ cell in the queue, it would re-schedule itself for immediate execution
+ meaning at the next mainloop round, it would execute again.
+
+ The idea is to try to empty the queue as fast as it can in order to provide a
+ fast response time to an introduction request but always leave a chance for
+ more cells to appear between cell processing by yielding back to the
+ mainloop. With this we are aiming to always have the most up-to-date version
+ of the priority queue when we are completing introductions: this way we are
+ prioritizing clients that spent a lot of time and effort completing their PoW.
+
+ If the size of the queue drops to 0, it stops scheduling itself in order to
+ not create a busy loop. The network I/O event will re-schedule it in time.
+
+ Notice that the proposed solution will make the service handle 1 single
+ introduction request at every main loop event. However, when we do
+ performance measurements we might learn that it's preferable to bump the
+ number of cells in the future from 1 to N where N <= 32.
+
+A.4 Performance measurements
+
+ This section will detail the performance measurements we've done on tor.git
+ for handling an INTRODUCE2 cell and then a discussion on how much more CPU
+ time we can add (for PoW validation) before it badly degrades our
+ performance.
+
+A.4.1 Tor measurements [TOR_MEASUREMENTS]
+
+ In this section we will derive measurement numbers for the "top half" and
+ "bottom half" parts of handling an introduction cell.
+
+ These measurements have been done on tor.git at commit
+ 80031db32abebaf4d0a91c01db258fcdbd54a471.
+
+ We've measured several set of actions of the INTRODUCE2 cell handling process
+ on Intel(R) Xeon(R) CPU E5-2650 v4. Our service was accessed by an array of
+ clients that sent introduction requests for a period of 60 seconds.
+
+ 1. Full Mainloop Event
+
+ We start by measuring the full time it takes for a mainloop event to
+ process an inbuf containing INTRODUCE2 cells. The mainloop event processed
+ 2.42 cells per invocation on average during our measurements.
+
+ Total measurements: 3279
+
+ Min: 0.30 msec - 1st Q.: 5.47 msec - Median: 5.91 msec
+ Mean: 13.43 msec - 3rd Q.: 16.20 msec - Max: 257.95 msec
+
+ 2. INTRODUCE2 cell processing (bottom-half)
+
+ We also measured how much time the "bottom half" part of the process
+ takes. That's the heavy part of processing an introduction request as seen
+ in step (4) of the [MAIN_LOOP] section:
+
+ Total measurements: 7931
+
+ Min: 0.28 msec - 1st Q.: 5.06 msec - Median: 5.33 msec
+ Mean: 5.29 msec - 3rd Q.: 5.57 msec - Max: 14.64 msec
+
+ 3. Connection data read (top half)
+
+ Now that we have the above pieces, we can use them to measure just the
+ "top half" part of the procedure. That's when bytes are taken from the
+ connection inbound buffer and parsed into an INTRODUCE2 cell where basic
+ validation is done.
+
+ There is an average of 2.42 INTRODUCE2 cells per mainloop event and so we
+ divide that by the full mainloop event mean time to get the time for one
+ cell. From that we substract the "bottom half" mean time to get how much
+ the "top half" takes:
+
+ => 13.43 / (7931 / 3279) = 5.55
+ => 5.55 - 5.29 = 0.26
+
+ Mean: 0.26 msec
+
+ To summarize, during our measurements the average number of INTRODUCE2 cells
+ a mainloop event processed is ~2.42 cells (7931 cells for 3279 mainloop
+ invocations).
+
+ This means that, taking the mean of mainloop event times, it takes ~5.55msec
+ (13.43/2.42) to completely process an INTRODUCE2 cell. Then if we look deeper
+ we see that the "top half" of INTRODUCE2 cell processing takes 0.26 msec in
+ average, whereas the "bottom half" takes around 5.33 msec.
+
+ The heavyness of the "bottom half" is to be expected since that's where 95%
+ of the total work takes place: in particular the rendezvous path selection
+ and circuit launch.
+
+A.2. References
+
+ [REF_EQUIX]: https://github.com/tevador/equix
+ https://github.com/tevador/equix/blob/master/devlog.md
+ [REF_TABLE]: The table is based on the script below plus some manual editing for readability:
+ https://gist.github.com/asn-d6/99a936b0467b0cef88a677baaf0bbd04
+ [REF_BOTNET]: https://media.kasperskycontenthub.com/wp-content/uploads/sites/43/2009/07/01121538/ynam_botnets_0907_en.pdf
+ [REF_CREDS]: https://lists.torproject.org/pipermail/tor-dev/2020-March/014198.html
+ [REF_TARGET]: https://en.bitcoin.it/wiki/Target
+ [REF_TLS]: https://www.ietf.org/archive/id/draft-nygren-tls-client-puzzles-02.txt
+ https://tools.ietf.org/id/draft-nir-tls-puzzles-00.html
+ https://tools.ietf.org/html/draft-ietf-ipsecme-ddos-protection-10
+ [REF_TLS_1]: https://www.ietf.org/archive/id/draft-nygren-tls-client-puzzles-02.txt
+ [REF_TEVADOR_1]: https://lists.torproject.org/pipermail/tor-dev/2020-May/014268.html
+ [REF_TEVADOR_2]: https://lists.torproject.org/pipermail/tor-dev/2020-June/014358.html
+ [REF_TEVADOR_SIM]: https://github.com/tevador/scratchpad/blob/master/tor-pow/effort_sim.md
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits