Breaking Down TornadoCash: A Beginner’s Guide to Explaining its Functionality to Friends

林瑋宸 Albert Lin
Taipei Ethereum Meetup
13 min readMay 11, 2023

--

TornadoCash 是一個廣為人知的匿名轉帳項目。在發生黑客攻擊事件時,人們往往會將錢轉移到 Tornado Cash,以便將錢款轉移而不被追蹤。然而,近年來,黑客攻擊事件的頻率和規模不斷增加,導致美國監管機構開始關注 TornadoCash,並實施一系列監管措施,例如關閉網站、逮捕開發者以及下架 GitHub 上的 Source Code 等。

您可能會好奇 TornadoCash 如何實現匿名轉帳。最近,我做了一些研究,試圖了解它的運作機制。在這裡,我將盡力用淺顯易懂的方式解釋 TornadoCash 如何保持匿名性,以及為什麼他們需要這樣做。希望這些知識可以幫助您更好地理解 TornadoCash。

TornadoCash 想要解決什麼?

在每筆交易中,都有一個發送者和一個接收者的角色。TornadoCash 希望能夠隱藏交易中的發送者,讓第三方無法透過交易記錄得知交易是由哪個發送者發起的。這樣一來,即使交易被記錄在區塊鏈上,也無法追蹤到發送者的身份。

目的:隱藏 sender

TornadoCash 採用怎樣的方法來做到這件事呢?

TornadoCash 使用了一個直觀且被許多專案所使用的方法來實現匿名交易,那就是混合器(Mixer)。混合器是一個將來自不同發送者的資金混合在一起的工具,以達到匿名交易的目的。

簡單的說,混合器就是一個箱子,發送者將錢放入箱子,然後接收者從箱子中取出錢。由於錢被混合在一起,因此無法確定接收者拿到的具體是哪個發送者的錢。這種方法讓交易變得匿名,並且無法從交易記錄中追蹤到發送者的身份。

採用 Mixer 的方式來隱藏交易來源

擁有收據的人才能當領錢

混合器的模式需要解決一個重要的問題:誰可以領錢?如果沒有適當的機制來驗證收款人的身份,那麼任何人都可以領取箱子中的錢,這樣會導致安全風險。

為了解決這個問題,TornadoCash 採用了一個很直觀的解決方案,那就是收據。收據是一種可以證明領款人身份的文件,就像旅館給客人的行李存放收據一樣。與現實生活中的行李寄放模式不同的是,TornadoCash 的收據並不是由 Smart Contract 生成,而是由使用者在存入資金時提供。這是因為 Smart Contract 的邏輯是存儲在區塊鏈上,任何人都可以查看。有心人士可以仿造出相同的收據,並試圖領取不屬於他們的資金。

採用收據的方式來證明有權限可以領取資金

TornadoCash 的收據是由使用者提供的,其中產生收據的方式是將 secretnullifier 一起進行哈希運算(hash)。 secretnullifier 都是由使用者自己選擇生成的。在 TornadoCash 的 Smart Contract 中,這個收據被稱為 commitment。其中,nullifier有著特殊的用途,我們稍後再來解釋它。雖然在 Smart Contract 中稱呼這個收據為 commitment,但在這裡我們還是繼續使用「收據」這個詞彙,以方便理解。

將 secret and nullifier 做 hash 得到的資料就是存放在 TornadoCash 的 commitment

收據的功用

收據的作用在鏈上實現起來比想像中要困難一些。在區塊鏈上,所有的資訊都是公開的,包括由 Alice 產生的收據。如果 Bob 拿著 Alice 的收據去向 Smart Contract 領錢,其他人很容易就會知道 Bob 拿到的錢是由 Alice 放進去的,這樣就無法實現隱藏發送者的目的。

因此,現有的收據方式需要改進。在改進之前,讓我們先來了解一下收據在整個系統中的目的。實際上,收據有兩個目的:

  1. 證明之前發送者有存入資金。
  2. 確保每個接收者只能領取一次資金。

以實際生活中寄放行李的例子來說,當你擁有收據時,這代表你之前已經寄放了行李,並且你只能領回行李一次。換句話說,如果我們能夠找到一種新的方式,在整個系統中證明有人存入了資金,並確保每個接收者只能領取一次資金,那麼就可以實現與收據相同的效果。

複習一下我們想要證明什麼

在開始解釋 TornadoCash 如何實現這兩個目的之前,我們先來複習一下為什麼我們要這麼做。如果 Bob 直接使用 Alice 的收據去領錢,其他人就可以從鏈上得知這筆交易是由 Alice 發起的,因此無法達到隱藏發送者的目的。

因此,我們需要設計一套方法,讓 Bob 不必直接使用 Alice 的收據去領錢,而是使用某種證明來領錢,這樣可以保證在領錢之前有人存入了資金,同時 Bob 的證明也只能領取一次錢。此外,這個證明也不會透露與 Alice 的收據有關的任何信息。這樣,即使 Bob 去領錢,我們也無法知道他收到的錢是由誰發送的,從而達到隱藏發送者的目的。這就是 TornadoCash 的設計思路。

證明之前發送者有存入資金

TornadoCash 使用了兩個技術來確認這件事情

  1. Merkle Tree
  2. Zero-Knowledge Proof

使用 Merkle Tree 紀錄存款

當使用者想要把錢放進 Smart Contract 時,他們需要提供一個收據,也就是前面提到 (secret, nullifier)的 hash 值,稱為 commitment。這時 TornadoCash Smart Contract 會將該 commitment 視為 Merkle Tree 的一個 leaf(葉子),並且加入 Merkle Tree 中(如下圖所示,假設該 commitment 放在 Merkle Tree 的 r6

Deposit Merkle Tree in TornadoCash

Merkle Tree 是一種資料結構,可以將最底層的 leaf 資料,兩兩做 Hash 值計算得到上一層的 parent hash值,如圖中的 r5r6 做 Hash 值運算後得到上一層的 H(r5,r6)。然後再將這些 parent hash 值兩兩做 Hash 值計算得到更上一層的 Hash 值,如圖中的 H(r5,r6)H(r7,r8) 做 Hash 值運算後得到 H(H(r5,r6),H(r7,r8))。直到得到這個 Merkle Tree 的 root,稱之為 Merkle Root

Merkle Tree 的資料結構具有一個特性,就是若要證明某筆資料是這個 Merkle Tree 的 leaf 之一,只需要提供該資料的 leaf 到 root 的中間經過的 parent hash,就可以證明該資料的確是構成 Merkle Tree 的 leaf 之一。以下圖為例,若要證明 r6 是這個 Merkle Tree 的葉子之一,只需要提供 r5r6H(r7,r8)H(H(r1,r2),H(r3,r4))這些資料即可。

若要得知 r6 是否回 merkle tree 的 leaf,只要提供藍色框框的資料,兩兩計算就可得 merkle root。不需要知道全部的 leaves

Step:

  1. r5r6 做 Hash 值計算得到 H(r5,r6)
  2. H(r5,r6)H(r7,r8) 做 Hash 值計算得到 H(H(r5,r6),H(r7,r8))
  3. H(H(r1,r2),H(r3,r4))H(H(r5,r6),H(r7,r8)) 做 Hash 值計算得到 Root
  4. 若計算出來的 RootMerkle Root相同就證明 r6Merkle Tree 的 leaf 之一

利用 ZKP 來隱藏存款來源

ZKP 技術的基本概念是提供了一種方法,讓使用者可以證明他們知道某些信息,而不需要揭示這些信息的實際內容。舉個例子來說(定義上可能會沒那麼精準,不過表達的意思是相同的),假設我聲稱我擁有一個指定地址的私鑰,那麼我可以通過發送任意量的加密貨幣到其他地址來證明我擁有這個私鑰,而不需要實際上公開這個私鑰。

在 TornadoCash 中,ZKP 技術被應用於隱藏存款來源。假設我們想要證明自己在 TornadoCash 上進行了存款(假設位置為 r6),我們可以提供以下的證明:

  • Knowledge: r6, r5, H(r7,r8)H(H(r1,r2),H(r3,r4))
  • Proof: 透過 ZKP 技術產生證明。
  • Verify: 在 TornadoCash 的驗證合約中計算 Proof 資料而得的 Root,並檢查是否與鏈上的 Merkle Root相符。若相符表示 r6 為 TornadoCash Merkle Tree 其中一個 leaf,也表示之前有人 deposit 資金在 TornadoCash。

透過 ZKP 技術,我們可以證明自己有某種特定知識,而不需要揭示實際的資訊。在這個例子中,我們證明了自己知道 r6, r5, H(r7,r8)H(H(r1,r2),H(r3,r4)) 這些資訊,而不需要揭示這些資訊的實際內容。通過這樣的證明,我們可以隱藏存款的來源,從而實現匿名交易。

確保每個接收者只能領取一次資金

是指在一筆交易中,該筆資產被領取兩次或更多次的情況。這是一種常見的攻擊手法,可能會導致 TornadoCash 遭受損失。為了避免這種情況發生,TornadoCash 引入了 nullifier 這個概念。存款時一起跟 secret 作 Hash 得到 commitment,並在取款時被使用。

當使用者進行取款時,必須提供 nullifier 的 hash 值 (hash(nullifier))。如果該值已經存在於 TornadoCash 的 contract 中,那麼就表示這筆存款已經被領取過了,因此這次的取款將會失敗。否則,如果 hash(nullifier) 尚未被記錄,則該取款將被認為是有效的,並且 TornadoCash 會將 hash(nullifier) 記錄在 contract 中,以確保這筆存款不會被再次領取。

總之,TornadoCash 通過使用 nullifier來防止 Double Withdrawal 的攻擊手法,確保了交易的安全性和可靠性。

在 TornadoCash 採用 map 紀錄 nullifier hash 值

TornadoCash Withdraw Proof

讓我們總結一下最後 TornadoCash proof 的資料包含:

  • nullifier: 產生存款時用的 nullifier(防止 double-withdrawal)。
  • secret: 產生存款時用的 secret
  • pathElements:Merkle Tree 上存款 commitment leaf 到 root 的中間經過的 hash 值。
  • pathIndices: 指示在 pathElements 中的每個元素是左邊還是右邊的 child。

這些資料將會被用來構建一個零知識證明(ZKP),讓 Bob 證明他有權取回這個存款,而不需要揭示他的 nullifiersecret。當驗證這個 ZKP 時,需要使用以下 public 資料:

  • root: TornadoCash 的 Merkle Tree root。
  • nullifierHash: Bob 的 nullifier 的 hash 值,用來檢查是否有重複領取的風險。

以上圖為例,當 TornadoCash 收到 proof 和 hash(nullifier) 會執行以下動作(假設 Alice deposit 的位置是在 r6):

  1. 透過 proof 中的 secretnullifier 先計算出 r6
  2. 利用 r6, r5, H(r7,r8), H(H(r1,r2),H(r3,r4))計算出 root
  3. 比對 root 跟 TornadoCash 中的 Merkle Root是否相同。若相同代表之前有存款。因為每次 deposit 都會導致 Merkle Root 改變,所以 TornadoCash 會紀錄最新的前 30 次 Merkle Root 來減少交易失敗的情形。實務上比對的是 root 有無在前 30 次的 Merkle Root 紀錄中。
  4. 將 map 中 key 為 hash(nullifier) 的位置設定為 true,以防止 double withdrawal。
  5. 將 1 ether 轉帳給 Bob。

上述步驟 Step1 ~ Step3 都是在 ZK 驗證 proof 過程中執行,所以其他人無法得知 secretnullifier 等相關資訊。

TL;DR

  1. TornadoCash 是匿名轉帳項目,使用混合器來隱藏交易發起者身份。
  2. TornadoCash 使用收據( comitment)來控制訪問權限。收據是由 secretnullifierHash 產生,,每個收據只能使用一次。
  3. 使用 Merkle Tree 記錄存款信息,將收據作為 leaf 節點,並計算出Merkle Root,使用者只需提供 leaf 到 root 的中間經過的數據即可證明該數據是 Merkle Tree 中的 leaf 之一。
  4. 利用 Zero-Knowledge Proof 來隱藏存款來源,使用 nullifier 防止Double Withdrawal 攻擊,TornadoCash Proof 需要與公共數據(如 Merkle Roothash(nullifier))進行比對驗證。

總體而言,TornadoCash 的設計思路非常出色,使用了多種技術保障交易的隱私性和安全性。這包括Merkle Tree、Zero-Knowledge Proof 和 nullifier 等技術,它們的結合能夠確保交易參與者的身份和交易細節不會被洩露,從而實現了隱藏交易發起者。文章中若有誤的部分還麻煩大家能不吝指教給予糾正,也歡迎一起討論交流意見。

最後感謝 Martinet 以及 Anton 幫忙 Review 文章和給出寶貴的意見!

--

--