# Tornado Cash - with code 隱藏交易訊息的解方:混幣協議 - Tornado Cash 的背後運作原理。 (本篇文章不包含任何對美國制裁的評論,單就技術層面進行介紹。) Published on October 12, 2022 by Chen YanLong. ###### tags: `blockchain` `ethereum` --- 你需要具備的基礎知識: * merkle tree * smart contract --- ## Table of contents * Intro * Architecture * Code * Conclusion * Ref. --- # Intro 以太坊,或者說整個區塊鏈都擁有的一個完美特性:透明化,是 Tornado Cash 想解決的問題。想像你的錢包裡有一百萬,你想把這些錢打到其他錢包裡,但是如果有人知道你擁有這個錢包,那他就可以在 Etherscan 上輕鬆的看到「你」把錢打進其他錢包裡。如果你想要隱藏你的交易訊息,那你可以使用混幣器: Tornado Cash。 可以想像 Tornado Cash 是一個巨大的金庫,任何人都可以使用 A 錢包存規定的固定金額(比如說100USDT、1000USDT之類的)進去(當然你可存很多次來滿足你的需求),而協議會生成一張支票,你可以拿著這張支票用 B 錢包提領你存入的金額。當然以上只是簡化過的舉例說明,ZKP 會保證沒有人可以知道 B 錢包領出的錢是從哪一個錢包存入的。 ![](https://i.imgur.com/meGvQqJ.jpg) * 當使用者越多時,交易資訊就越難以辨識。相對的,如果使用者只有一個,那就喪失隱藏交易資訊的功能。 * 固定金額讓交易資訊更難辨識 --- # Architecture Tornado Cash 使用 **Solidity** 撰寫智能合約,透過 **circom** 編譯成的 zkSNARKs 零知識證明達成鏈上的匿名交易。 使用者存款時,需要提供 commitment 的 hash。commitment 包含兩樣東西,一個是nullifier,一個是 randomness ( 在 github 中稱做 secret )。這兩項 concat 之後成為 preimage,再 hash 之後成為 commitment。前者在提款時需要由提款人提供給合約,用於確保此個 commitment 只能使用一次;後者則增加亂數。 ![](https://i.imgur.com/lfmmTWW.jpg) Tornado Cash 收到 commitment 之後,會將其儲存在合約內的 Merkle tree 之中。合約會更新 Merkle root,並傳回所存入之 leaf 的 index。等待提款者提款。 使用者提款時,需要提供 proof,證明其 commitment 確實存於此 merkle tree 當中。以及須一併提供 nullifier 供合約驗證此存款是否被提領過。合約確認此存款未被提領過後,便會將存款轉給使用者,並將 nullifier 存在合約當中,確保不會再有人使用同一個 nullifier 進行提領。 :::info 因為有 Gas 的問題, 提款者可能會想使用 **Relayer**。 想要完全的隱藏交易訊息最好的方式 就是建立一個新的錢包去提領。由於在提款的時候合約會向提款者收取 gas fee。如果用舊錢包打幣給新錢包,那這筆打幣的交易就容易成為追蹤的對象。於是**為了使 gas fee 不要成為隱藏交易訊息的敗因**,使用者可以請 Relayer 代為執行提領的動作並預先支出 gas fee,扣除 gas fee 及服務費之後,Relayer 再將剩下的金額退還給使用者。 ::: ![](https://i.imgur.com/GC67Xio.jpg) --- # Code Tornado Cash 在 2022 八月遭到美國政府制裁,原因是其洗錢功能過於強大,甚至連 github 的[原始碼](https://github.com/tornadocash/tornado-core)都曾被封存。所幸近期又開放讀取。 Tornado Cash 的原始碼中包含 contracts 和 circuits 及其他資料夾,前者存放合約,後者則是存放產生零知識證明的 .circom 檔案。 整個 Tornado Cash 最主要做的事情就是 Deposit 和 Withdraw,以下會跟著這兩件事情看程式在背後做了哪些事。 ## Deposit deposit 的函數接收一個參數 \_commitment,由 Pedersen 雜湊函數將 nullifier 與 secret concat 加雜湊產生。這些東西要在線下產生。 ```solidity=1 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); } ``` 第四行 \_insert 這個函數存於 MerkleTreeWithHistory.sol 中,這個檔案裡面處理所有有關 Merkle Tree 的事件,其中 Merkle tree 的建立使用了 MiMC 雜湊函數。 :::warning 這裡需要注意一點,Tornado Cash 所使用的是 Sparse Merkle Tree,他跟普通的 Merkle Tree 不一樣的是他在初始設定時先將每個 leaf 設定好了預設值。換言之,在儲存資料之前這棵樹已經「長」好了。 ```solidity function zeros(uint256 i) public pure returns (bytes32) ``` zeros( )這個函數裡面寫好了每一層的預設值。 ::: \_insert( ) 做的事情是將傳入的 commitment 存入已存在的 Merkle Tree 當中的其中一個 leaf 並回傳所存入的 index。 ```solidity function _insert(bytes32 _leaf) internal returns (uint32 index) ``` ## Withdraw Tornado 最難達成的就是 withdraw,為了確保交易資訊的隱密性,Tornado Cash 不能接收到原始的 nullifier 與 secret,也就是 preimage。否則有心者就可以藉此推敲出交易的資訊。 因此這方面要由 withdraw.circom 建立一個 ZK proof , 再交由合約做驗證並提款。 ``` template Withdraw(levels) { signal input root; signal input nullifierHash; signal private input nullifier; signal private input secret; signal private input pathElements[levels]; signal private input pathIndices[levels]; ``` Withdraw( )首先接收數個參數(我僅列出有使用到的),他的功能是證明使用者提供的 secret 和 nullifier 所組合成的 commitment 真的存在於 Merkle Tree 之中。 可以注意到 nullifier 和 secret 是以 private 的方式傳入,所以再傳入一個 nullfierHash 因為合約真的不知道 nullifier 的值是多少。 pathElements[levels] 存的是目標 leaf 要計算到 root 中間需要 hash 過的元素。pathIndicies[levels] 則是由 {0, 1} 組成,標示所在位置在左邊或右邊的陣列。 ``` component tree = MerkleTreeChecker(levels); tree.leaf <== hasher.commitment; tree.root <== root; for (var i = 0; i < levels; i++) { tree.pathElements[i] <== pathElements[i]; tree.pathIndices[i] <== pathIndices[i]; } ``` 經由以上的運算後證明用 private 變數算出來的 root 和 public root 的結果是一致的。將 circom 產生的 proof 交由 Tornado.sol 的 withdraw( )進行驗證。 ```solidity=1 function withdraw( bytes calldata _proof, bytes32 _root, bytes32 _nullifierHash, address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund ) external payable nonReentrant { ``` 首先確認傳入的 nullifierHash 未被使用過,若沒有使用過,合約會將這個nullifierHash 標示為已使用;若是使用過,則合約會報錯。 ```solidity require(!nullifierHashes[_nullifierHash], "The note has been already spent"); ``` 接著確認傳來的 root 確實是在過去的某一個時點存在過的。 Tornado Cash 的合約中存有歷史上所有的 merkle root,意即每次執行完_insert( ) 時都會更新新的 merkle root 在 roots[ ]裡頭。 ```solidity require(isKnownRoot(_root), "Cannot find your merkle root"); ``` 最後交由 verifiyProof( ) 驗證 \_proof 的真實性。此函數使用到密碼學中 Pairing 的概念,詳細的邏輯我也沒有到非常清楚。總之這個函數若是傳回 true 則代表 \_proof 是真的,false 則否。 ```solidity require( verifier.verifyProof( _proof, [uint256(_root), uint256(_nullifierHash), uint256(_recipient), uint256(_relayer), _fee, _refund] ), "Invalid withdraw proof" ); ``` 若一切都沒有問題,合約就會進行轉帳的部分,結束這個交易。 ```solidity _processWithdraw(_recipient, _relayer, _fee, _refund); ``` --- # Conclusion Tornado Cash 在2022年8月被列入 OFAC 的 SDN 制裁名單當中,禁止所有美國人與該協議或相關錢包地址進行互動 ([ link ](https://www.blocktempo.com/tether-wont-freeze-tornado-cash-addresses/))。目前除了原始碼重新開源沒有其他更進一步的消息。不過 Tornado Cash 還是一個學習 ZKP 很好的教材。 Circom 所產生的零知識證明可以保證在沒有擁有真實的 preimage 的情況之下你很難做出正確的 proof。而如果你擁有真實的 preimage,則在不透漏真實的preimage 之下你可以證明自己知道這個 preimage ,這方面可以相信密碼學家的奧妙,也就是零知識證明的厲害的地方。 --- # Ref. * [TornadoCash whitepaper v1.4](chrome-extension://efaidnbmnnnibpcajpcglclefindmkaj/https://berkeley-defi.github.io/assets/material/Tornado%20Cash%20Whitepaper.pdf) * [Understanding zero knowledge proofs through the source code of tornado cash](https://betterprogramming.pub/understanding-zero-knowledge-proofs-through-the-source-code-of-tornado-cash-41d335c5475f) * [[ZKP 讀書會] Tornado Cash](https://medium.com/taipei-ethereum-meetup/zkp-study-group-tornado-cash-fdbb84d44b93) * [Tornado Cash github](https://github.com/tornadocash/tornado-core)