TR kizaki Tech Memo

Understanding how worked Tornado Cash prevent for Money Laundering

Tornado cash had the popular non-custodial crypto mixing service.

Lately, The U.S. sanctions on Tornado cash if continued use of the service could see you fined or sent to jail…

The Tornado Cash made people run anonymous crypto transactions.

When you transfer crypto funds into Tornado cash, they get mixed up with funds deposited by other users. The name of the crypto mixer service.

While many people use Tornado cash because they want to protect their privacy, others have taken advantage of the platform’s anonymity for money laundering or some similar activities…That’s why The U.S. banned Tornado Cash and we can’t see Github code to relevant Tornado Cash already.

So, I would like to understand what was going on code of the smart contract on Tornado cash. Let’s see the prototype some code and conceptions.


How to hide details from the sender to receiver on Tornado Cash?

For example, everyone sends the same amount of 1 ETH into Tornado for anonymity.

e.g. hash(secret, nullifier) = 0x234

TR deposit with hash 0x234 to Tornado Cash,

An anonymous user came along and how the identity of the user reveals on withdrawal with a hash (secret, nullifier) = 0x234.

The computer recognized TR from the hash. This is a one-way function.

Proof that I know hash(secret, nullifier) in this list without revealing.

Anonymous users stay anonymous. This is called “zero-knowledge proof”.

Proof does not reveal the identity of an anonymous user.

“zk-SNARK” is the method of zero-knowledge proof.


What is a Nullifier?

Tornado cash doesn’t know who is withdrawing.

proof

h(s, n) in list ,

h(n) == nullifier cash

Once the proof checkout, Tornado cash records that the hash of the nullifier has been spent.

The anonymous user gets the 1ETH back and if the same user tried to withdraw again using the same proof then it will fail.

Because the hash of the nullifier has already been spent and it’s already recorded on Tornado Cash smart contract.

That is the purpose of the Nullifier to prevent double-spending.


How recorded the hash?

The way the hash is recorded in tornado cash is by Markle Tree.

Actually, Markle tree looks like an upside-down tree. e.g. Root ⇒ Leaf

Merkle Tree is used in the specific implementation of tornado cash. Each time the user makes a deposit, the user will call insert to insert nodes into the Merkle Tree (deposit certificate).


e.g. Deposit, Withdraw, VerifyProof

function deposit(bytes32 _commitment) external payable nonReentrant {
 require(!commitments[_commitment], “The commitment has been submitted”);

uint32 insertedIndex = _insert(_commitment);
 commitments[_commitment] = true;
 _processDeposit();
emit Deposit(_commitment, insertedIndex, block.timestamp);
 }
function _insert(bytes32 _leaf) internal returns(uint32 index) {
 uint32 currentIndex = nextIndex;
 require(currentIndex != uint32(2)**levels, “Merkle tree is full. No more leafs can be added”);
 nextIndex += 1;
 bytes32 currentLevelHash = _leaf;
 bytes32 left;
 bytes32 right;
for (uint32 i = 0; i < levels; i++) {
 if (currentIndex % 2 == 0) {
 left = currentLevelHash;
 right = zeros[i];
filledSubtrees[i] = currentLevelHash;
 } else {
 left = filledSubtrees[i];
 right = currentLevelHash;
 }
currentLevelHash = hashLeftRight(left, right);
currentIndex /= 2;
 }
currentRootIndex = (currentRootIndex + 1) % ROOT_HISTORY_SIZE;
 roots[currentRootIndex] = currentLevelHash;
 return nextIndex — 1;
 }
function withdraw(bytes calldata _proof, bytes32 _root, bytes32 _nullifierHash, address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) external payable nonReentrant {
 require(_fee <= denomination, “Fee exceeds transfer value”);
 require(!nullifierHashes[_nullifierHash], “The note has been already spent”);
 require(isKnownRoot(_root), “Cannot find your merkle root”); 
 require(verifier.verifyProof(_proof, [uint256(_root), uint256(_nullifierHash), uint256(_recipient), uint256(_relayer), _fee, _refund]), “Invalid withdraw proof”);
nullifierHashes[_nullifierHash] = true;
 _processWithdraw(_recipient, _relayer, _fee, _refund);
 emit Withdrawal(_recipient, _nullifierHash, _relayer, _fee);
 }
function verifyProof(
 bytes memory proof,
 uint256[6] memory input
 ) public view returns (bool) {
 uint256[8] memory p = abi.decode(proof, (uint256[8]));
for (uint8 i = 0; i < p.length; i++) {
 require(p[i] < PRIME_Q, “verifier-proof-element-gte-prime-q”);
 }
Proof memory _proof;
 _proof.A = Pairing.G1Point(p[0], p[1]);
 _proof.B = Pairing.G2Point([p[2], p[3]], [p[4], p[5]]);
 _proof.C = Pairing.G1Point(p[6], p[7]);
VerifyingKey memory vk = verifyingKey();
Pairing.G1Point memory vk_x = Pairing.G1Point(0, 0);
 vk_x = Pairing.plus(vk_x, vk.IC[0]);
for (uint256 i = 0; i < input.length; i++) {
 require(input[i] < SNARK_SCALAR_FIELD, “verifier-gte-snark-scalar-field”);
 vk_x = Pairing.plus(vk_x, Pairing.scalar_mul(vk.IC[i + 1], input[i]));
 }
return Pairing.pairing(
 Pairing.negate(_proof.A),
 _proof.B,
 vk.alfa1,
 vk.beta2,
 vk_x,
 vk.gamma2,
 _proof.C,
 vk.delta2
 );
 }
function _processWithdraw(address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) internal {

 require(msg.value == 0, “Message value is supposed to be zero for ETH instance”);
 require(_refund == 0, “Refund value is supposed to be zero for ETH instance”);
(bool success, ) = _recipient.call.value(denomination — _fee)(“”);
 require(success, “payment to _recipient did not go thru”);
 if (_fee > 0) {
 (success, ) = _relayer.call.value(_fee)(“”);
 require(success, “payment to _relayer did not go thru”);
 }
 }

The issue with the deposit and withdrawal time interval, if the deposit and withdrawal time interval is short, it may lead to the time correlation of the deposit and withdrawal transactions, resulting in the reduction of anonymity. Therefore, the official recommendation is to perform the withdrawal operation for a period of time after the deposit.