Behind the Tornado.cash front-end sits a number of Circom circuits, which enable the fundamental privacy guarantees that Tornado.cash users enjoy. These circuits implement the Zero Knowledge protocol that Tornado.cash's smart contracts interface with to prove claims about a user's deposit, such as that it is valid, that is hasn't already been withdrawn, and in the context of Anonymity Mining, the number of blocks that exist between a note's deposit transaction and its withdrawal.
How ZK Circuits Work
SNARKs and GROTH16
Before trying to understand how Tornado.cash works under the hood, you first need to understand Zero Knowledge circuits, how they're constructed, and how proofs are generated client-side, then verified on-chain. While there are a few different types of ZK systems, Tornado.cash relies upon a variant known as "succinct non-interactive arguments of knowledge" (SNARK), specifically a variant called GROTH16.
Circom and snarkjs
Because we're not all Vitalik, it's best if we have some simple tools that will abstract away the generation and execution of these complicated polynomial commitments. This is where Circom and snarkjs come in.
Circom is easiest to think of as a compiler for a circuit language which acts very much like the kind of hardware description language that electrical engineers would use to describe an electrical circuit. Except instead of an electrical circuit, we're describing an arithmetic circuit, which contains components, and the way that they connect together.
To understand R1CS (Rank-1 constraint system), there is of course more math. And where there's important cryptosystem math, there's a post by Vitalik.
An R1CS is a sequence of groups of three vectors $$(a, b, c)$$, and the solution to an R1CS is a vector $$s$$, where $$s$$ must satisfy the equation $$s . a * s . b - s . c = 0$$, where $$.$$ represents the dot product - in simpler terms, if we "zip together" $$a$$ and $$s$$, multiplying the two values in the same positions, and then take the sum of these products, then do the same to $$b$$ and $$s$$ and then $$c$$ and $$s$$, then the third result equals the product of the first two results.
The next step is taking this R1CS and converting it into QAP form, which implements the exact same logic except using polynomials instead of dot products ... instead of checking the constraints in the R1CS individually, we can now check all of the constraints at the same time by doing the dot product check on the polynomials.
If we try to falsify any of the variables in the R1CS solution that we are deriving this QAP solution from - say, set the last one to 31 instead of 30, then we get a $$t$$ polynomial that fails one of the checks.
In short, the R1CS is a set of polynomial constraints which any proof generated by the circuit must satisfy. These constraints are generated by Circom based on the relationship between various "signals" and operations in your circuit design.
Now, depending on what you're using Tornado.cash for, you might not want any witnesses. However, don't worry, if everything is working correctly, all of the witnesses to your interactions with Tornado.cash will be aggressively compacted, and their bodies disposed of as you please.
In the context of a SNARK circuit, a witness is the set of values that need to be generated from the inputs to the circuit, based on the circuit design, to satisfy all of the constraints imposed by the circuit. You can think of the witness generator produced by Circom as a circuit-specific decompression function which runs your inputs through the circuit, and snapshots all of the various intermediate values that are produced along the way.
With this expanded form generated from your inputs, you know which values must be assigned to the constraints specified by the R1CS in order to construct a valid proof.
When you think of a "proof", you probably imagine that it's an incontrovertible guarantee that something is true. However, in the context of a SNARK, a "proof" actually represents an argument that something is almost certainly true. If we were to try to transmit the solution to every single polynomial constraint imposed by a circuit, we would end up with proofs that were orders of magnitude larger than if we simply show that certain sorts of relationships hold true between the intermediate state values within the circuit.
It's possible that for any given circuit, someone with sufficient computing power could generate a proof that satisfies the circuit's constraints in a malformed way, but this would be roughly equivalent in difficulty to factoring large primes.
So, when generating a proof for a SNARK circuit, you're calculating the intermediate states of your circuit for a given input (witness generation), and then calculating the relationships between your inputs, the intermediate states, and the circuit's outputs.
Once you have the proof that you've satisfied the necessary set of constraints, you can then publish that proof and some subset of your inputs and outputs (a.k.a. public signals). Knowing the R1CS, your public signals, your proof, and the circuit's proving key, anyone can then verify that your proof satisfies the R1CS, and that your public signals are what would be expected to correspond to your proof.
With that understanding of ZK proving circuits well-in-hand, let's delve into how Tornado.cash uses some relatively simple circuits to enable you to privately and permissionlessly obscure the relationship between your deposit and withdrawal transactions on a public blockchain network, and then to later prove things about the relationship between your deposit and withdrawal (e.g. how long you waited before withdrawing).
Tornado.cash is best understood as having two separate major components.
Core Deposit Circuit
The core deposit circuit is what most users interact with, proving that a user has created a commitment representing the deposit of some corresponding asset denomination, that they haven't yet withdrawn that asset, and that they know the secret that they supplied when generating the initial commitment.
The anonymity mining circuits form the basis for the Anonymity Mining program, which incentivizes users to leave their deposits in the contract for longer periods of time, so as to ensure that the Tornado.cash deposit pools maintain a large number of active deposits (thus increasing k-anonymity for other users).