JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

私密智慧合約:當零知識遇上保密性

Zcash 能藏住一筆付款,但它唯一會的動詞是「轉帳」。下一次飛躍,是把一整個程式的狀態都藏起來——一種餘額保密、卻又能被證明正確的代幣。來認識 note、nullifier 與裝置端證明,以及它們一頭撞上的可組合性高牆。

從一枚私密硬幣到一個私密程式

在上一篇裡,Zcash屏蔽池藏住了一筆付款的發送者、接收者與金額。這很了不起——但請注意它有多狹窄。一筆屏蔽轉帳只會一個動詞:把價值從一個祕密持有者搬到另一個祕密持有者。規則被寫死在協定裡。你無法寫出一場出價在揭曉前保密的密封投標拍賣、一種餘額被藏起來卻每筆轉帳都可證明合法的代幣,或一場能證明你具投票資格、卻不顯示你是誰的私密投票。這每一樣都是一個程式,而非一筆付款。

區塊鏈隱私的前沿,是私密智慧合約:一份狀態與運算皆保密、卻仍能產出一個零知識證明、證明其規則被分毫不差地遵循的合約。最具代表性的通用嘗試是 Aztec——一條建在以太坊上、隱私優先的第二層;但相同的構想也出現在 Aleo、Miden 與 Penumbra。本篇要打開這台機器。三個零件撐起全部重量:note(你如何儲存隱藏狀態)、nullifier(你如何花掉它卻不洩漏你花了什麼),以及裝置端證明(你如何在自己的筆電上運行一份智慧合約,只把一個證明交給鏈)。

Note:長得像 UTXO 的隱藏狀態

藏起狀態的第一個後果是結構性的,而且常讓人意外。一份公開的 ERC-20 用 `balances[from] -= v; balances[to] += v` 來轉移價值。但要相減,你得先讀取 `balances[from]`——而讀取一個隱藏餘額,恰恰是我們想禁止的事。所以私密狀態不能是帳戶形狀的,它必須是 UTXO 形狀,就像比特幣:你絕不在原地修改餘額,而是銷毀舊的單位、創造新的單位。這正是為何帳戶模型對公開合約極好、對私密合約卻全無用處,也是為何 UTXO 式隱私一再出現。

於是私密狀態以 note 的形式存在。一個 note 是一筆小小的私密紀錄——對代幣而言可能是 `(value, owner, randomness)`,對更複雜的合約則可裝下開發者想要的任何欄位。但 note 不能以明文躺在公開鏈上,否則毫無隱私可言。所以鏈只儲存每個 note 的承諾(commitment):一個既具約束性(你日後無法宣稱該 note 裝的是別的值)、又能隱藏(它對那些值一無所洩)的雜湊——這份隱藏性來自那筆新鮮的隨機數。這就是 note commitment,由像 Pedersen 或 Poseidon 雜湊這類承諾方案建成。每一個曾被造出的承諾,都被當作葉子,附加進一棵巨大的「只增不刪」Merkle 樹——也就是 note 樹。

有一個細節決定了現實中的可用性:如果只有承諾是公開的,接收者要如何得知 note 的祕密內容,才能在日後花掉它?發送者會用接收者的公鑰把 note 的原像加密,再把那段密文當作鏈上日誌貼出。接收者於是對每一筆新日誌嘗試解密,藉此找出哪些 note 是自己的。這就是 note 發現(note discovery)問題,而它是一筆誠實的成本:天真地做,你的錢包必須掃描並嘗試解密鏈上的每一筆日誌。標籤化與「盲訊息檢索」等方案能縮小這筆成本,但它永遠無法完全消失。

Nullifier:花掉它,卻不洩漏花的是哪一個

現在來到最難的問題。note 樹只會增長——承諾被加入,卻從不被明顯地移除。那麼,當鏈連你花的是哪一個 note 都看不到時,它要怎麼阻止你把同一個 note 花兩次?如果「移除」是可見的,它會直指你的承諾,毀掉你才剛建立的隱私。答案是整個領域中最優雅的一招:nullifier

當你花掉一個 note,你的證明會公布恰好一個新值——也就是 nullifier——它由那個 note 與你的祕密金鑰決定性地推導而來,例如 `nf = hash(commitment, spend_sk)`。它有四個性質,合起來解開了這道謎題:它具決定性且唯一(一個 note 對應到恰好一個 nullifier);它不可連結(給定 `nf`,沒有觀察者能看出它毀掉的是哪個承諾,因為祕密金鑰被攪了進去);它綁定持有者(只有握有 `spend_sk` 的人能算出它);而且它是公開的。鏈維護一個不斷增長的 nullifier 集合。一筆花費唯有在它的 nullifier 從未出現過時才有效,接著該 nullifier 被加入集合。重複出現就是雙重支付,會被拒絕——而這一切都不必得知死去的是哪個 note。

這並非為了合約而臨時加裝的新點子——它正是你在前幾篇遇到的 Zcash 屏蔽池與 Tornado Cash 混幣器私密地防止雙重支付的方法。所有真正的工作都由一個零知識證明完成:在一個電路裡,你的私密輸入(也就是witness)證明底下每一條規則都成立,而只有公開的值會被釋出。以下是經典的花費電路:

// Spending one private note (Zcash / Aztec style), proved in zero knowledge.
//
// PUBLIC inputs  (posted on-chain; reveal nothing on their own):
//   root        a recent note-tree Merkle root
//   nf          the nullifier of the note being spent
//   cm_out[]    commitments of the new output notes
//
// PRIVATE witness (stays on YOUR device; never published):
//   value, owner_pk, r     the note being spent
//   spend_sk               your secret key  (owner_pk = derive(spend_sk))
//   path                   Merkle path proving the note is in the tree
//   out_values[], r_out[]  amounts + randomness of the new notes

circuit spend:
    // 1. the input note is well-formed
    cm = hash(value, owner_pk, r)

    // 2. you actually own it
    assert owner_pk == derive(spend_sk)

    // 3. the note exists in the tree -- WITHOUT revealing which leaf
    assert merkle_verify(root, leaf = cm, path)

    // 4. the nullifier is the unique, unlinkable tag of THIS note
    assert nf == hash(cm, spend_sk)

    // 5. value is conserved: input == sum(outputs) + fee
    assert value == sum(out_values) + fee
    for i in outputs:
        assert cm_out[i] == hash(out_values[i], recipient_pk[i], r_out[i])
公開輸入只洩漏一個樹根、一個 nullifier 與一批新承諾——其中沒有任何一項會暴露金額、當事人,或被花掉的是哪個 note。
  1. 實際的金額核對:你花掉一個價值 10 的 note,送 3 給 Bob、留 7 當找零、手續費 0。約束 5 成立:10 == 3 + 7 + 0。兩個輸出承諾 cm_out 被附加進樹;觀察者只看到兩片新葉子,什麼也學不到。
  2. 鏈針對 (root, nf, cm_out) 驗證該證明,在看不到 witness 的情況下確認全部五條約束。
  3. 鏈檢查 nf 是否已在 nullifier 集合中。若已存在,便以雙重支付為由拒絕。
  4. 否則鏈把 nf 加入 nullifier 集合,並將每個 cm_out 當作新葉子附加進 note 樹。那個輸入 note 自此永久死亡——而沒人得知過它究竟是哪一個。

私密執行:證明你照規則來

那個證明從哪來?不是鏈——鏈從不見你的祕密。在 Aztec 裡,一個私密函式在你自己裝置上的私密執行環境(PXE,Private eXecution Environment)中運行。它讀取你本機的 note、執行合約的私密邏輯,並產出公開輸出(新承諾、nullifier、加密日誌),連同一個零知識證明、證明函式主體被正確執行。只有證明與那些公開輸出會被廣播;輸入、數值、甚至你走了程式碼的哪一個分支,都留在你的機器上。由於這個證明是非互動式的,鏈不必來回問答就能驗證它。

一份真實合約有兩半。私密函式在裝置端運行、碰觸 note,如前所述。公開函式則在定序器(sequencer)上、針對公開狀態透明地運行,就像一般的 EVM 合約——而一個私密呼叫可以跨越一道明確的邊界,排入一個公開呼叫。這是刻意的切分:zkEVM 證明的是公開 EVM 位元組碼的正確執行,但這裡的目標更進一步——還要藏住輸入。你把必須保密的部分(你的餘額、你的出價)留在裝置上,把必須共享的部分(某個池的儲備、一份公開計票)推到透明層。

單一筆交易可能呼叫好幾個私密函式,每一個都產出自己的證明。為了讓鏈上的驗證者保持便宜,Aztec 用遞迴把它們摺疊起來:一個kernel 電路驗證前一個 kernel 的證明,加上新函式的證明,再輸出一個合併後的證明。這就是遞迴式證明組合,它意味著無論呼叫堆疊有多深,鏈都只需驗證整個私密呼叫堆疊的一個簡潔 SNARK。正是遞迴,讓一份可程式化的私密合約——而不只是一筆固定的屏蔽付款——在驗證成本上真正負擔得起。

可組合性的高牆

現在來到誠實而艱難的部分——這正是為何私密 DeFi 不只是打開一個功能開關。公開 DeFi 像樂高,因為任何合約都能同步地呼叫另一個合約並讀取它的回傳值:一個路由器向某個池詢價,池子回答,全在一筆原子交易裡完成。隱藏狀態打破了這一點。一個私密函式在你的裝置上運行,只能看到你的 note;它無法讀取另一位使用者的祕密餘額,或某個合約的祕密狀態,因為沒有別人會把他們的 witness 交給你。兩份私密合約,就是無法像兩份公開合約那樣彼此組合。

最深刻的例子是共享的可變狀態,而一個自動造市商把它講得很生動。要為一筆兌換定價,池子必須讀取自己的儲備;要撮合訂單,訂單簿必須看見那些訂單。你無法擁有一個全私密的共享訂單簿,因為撮合本質上就是一種「看」。所以真實的設計把世界切開:私密的使用者狀態(你的餘額,以 note 持有)加上公開的共享狀態(池子本身、它的儲備、它流動性池裡的價格)。你能藏住你是誰、握有多少,但你與公開池子互動這件事的存在——以及池子的狀態——仍是可見的。保密性在邊緣存活;共享的核心則攤在陽光下。

還跟著兩道摩擦。並行性:note 的平行化極為漂亮——每位使用者持有彼此不相交的 note,所以私密花費不會相爭。但任何透過共享公開槽位序列化的東西都可能衝突:若兩位使用者針對同一份公開狀態各自建構證明、又同時落入一個區塊,其中一個證明便是對著一份如今已過時的狀態算出來的,於是失效,逼著你付出昂貴的代價重新證明。搶先交易與發現成本:私密呼叫碰觸公開狀態的那道透明邊界是可見的,可能搶先交易;而接收者仍得付出先前提過的 note 發現掃描成本。這些都不致命——Aztec、Aleo 等系統確實交付了真實的私密合約——但這正是為何「把 DeFi 變私密就好」是這領域最難的問題之一。

通往私密狀態的其他路:FHE、MPC 與 TEE

ZK-note 這條路不是唯一的設計,而把替代方案點名出來,能更清楚 ZK 究竟替你買到了什麼。零知識/UTXO 路線給你強大的完整性(一個證明規則被遵守的證明)與自我保管的隱私(只有你握有自己的 witness)——但代價是沒有共享的私密狀態,外加一筆 note 發現成本。三個對手家族把這些限制換掉,各自靠加入一個不同的信任假設。

  1. FHE(全同態加密)——Zama 的 fhEVM、Fhenix:合約直接在加密資料上運算,所以你「能」擁有共享的加密狀態,例如一張每份合約都會碰的加密餘額映射。代價是:它慢上好幾個數量級,而且要解密一個結果需要一個門檻解密委員會——若夠多成員串謀,便能解開你的狀態。
  2. MPC(安全多方計算):狀態被祕密分享給一個委員會,由他們共同對其運算,而沒有任何單一方看得到明文。你得到共享的私密狀態,但你必須信任委員會中達門檻數的成員保持誠實,而且這種協定的通訊量很重。
  3. TEE(可信執行環境/硬體飛地,例如 Intel SGX):程式碼在一個連機器擁有者都無法窺視的密封晶片區域內運行——又快又通用,但你得信任晶片廠商、並信任那個飛地沒被攻破,而 SGX 的旁路攻擊已有數起在案。

把它們讀成一個單一的取捨,而非一份排名。ZK 免費給你完整性、並把信任收斂到你自己身上,卻無法原生地共享私密狀態。FHEMPC 給你共享的私密狀態,卻重新引入一個你必須信任其不串謀的委員會。TEE 給你速度與通用性,卻把信任搬進矽晶片裡。沒有任何一種設計能同時兼具私密、共享、無信任與快速——隱私有它自己的三難困境,而每一個真實系統都選了一個角。