Solidity 隱私合約開發進階指南:承諾、Merkle 樹與零知識證明整合
全面解析使用 Solidity 構建隱私保護智能合約的核心技術,包括 Pedersen 承諾、同態加密 Merkle 樹、稀疏 Merkle 證明、以及 Groth16 和 PLONK 驗證合約的整合,並提供完整的實際應用案例與程式碼範例。
Solidity 隱私合約開發進階指南:承諾、Merkle 樹與零知識證明整合
概述
在以太坊區塊鏈上構建隱私保護應用是一項具有挑戰性的任務,因為所有交易數據預設都是公開的。然而,通過結合密碼學技術與智能合約設計,開發者可以實現多種隱私保護功能。本文將深入探討使用 Solidity 構建隱私合約的核心技術:承諾方案(Commitment Schemes)、Merkle 證明驗證、以及與鏈下零知識證明的整合。我們將通過實際的代碼示例來展示這些技術的實現細節,幫助開發者構建真正的隱私保護應用。
一、密碼學基礎回顧
1.1 哈希函數與承諾方案
密碼學哈希函數是區塊鏈技術的基石。在以太坊中,Keccak-256(通常稱為 sha3)是最常用的哈希函數。一個安全的哈希函數應該具有三個關鍵特性:單向性(從輸出無法推導輸入)、抗碰撞性(難以找到兩個不同的輸入產生相同輸出)、以及雪崩效應(輸入的微小變化會導致輸出的顯著變化)。
承諾方案是構建隱私合約的核心密碼學原語。一個承諾方案允許承諾者對某個值創建承諾,並在稍後揭示該值。承諾方案有兩個關鍵特性:隱藏性(從承諾無法推導出具體值)和綁定性(承諾者無法更改已承諾的值)。最簡單的承諾方案是使用哈希函數:Commitment = hash(value, randomness),其中 randomness 是一個隨機數,用於增強隱藏性。
在 Solidity 中實現承諾方案時,需要注意幾個關鍵點。首先,隨機數的生成不能使用區塊變量(如 block.timestamp),因為這些是可預測的,會削弱隱藏性。其次,承諾和揭示應該在不同的交易中進行,以確保時序上的分離。最後,驗證承諾時需要同時驗證隨機數的正確性。
1.2 Merkle 樹與範圍證明
Merkle 樹是一種二叉樹結構,其中每個葉節點是數據的哈希,每個內部節點是其子節點哈希的哈希。Merkle 樹的根哈希可以有效地總結整個數據集,而 Merkle 證明允許驗證特定數據是否在樹中,而無需下載整個數據集。
在隱私合約中,Merkle 樹有兩個重要應用場景。第一個場景是「白名單驗證」:合約可以存儲一個 Merkle 根,允許成員證明他們在白名單中而不需要公開他們的地址。第二個場景是「餘額證明」:用戶可以證明他們的餘額超過某個閾值而不透露具體餘額。
範圍證明是另一個關鍵技術,它允許證明某個值在指定範圍內。例如,一個借貸合約可能需要驗證借款人的抵押品價值超過借款金額,但不希望透露確切金額。通過 Merkle 樹和承諾方案的組合,可以實現這種選擇性披露。
1.3 零知識證明整合概述
真正的零知識證明(ZKP)允許證明者向驗證者證明某個陳述是正確的,同時不透露任何除了陳述正確性之外的信息。在以太坊上實現完整 ZKP 存在挑戰,因為在鏈上驗證 ZKP 需要大量的 Gas 成本。
當前的最佳實踐是採用「鏈下證明、鏈上驗證」的模式。複雜的證明生成在客戶端完成,然後將證明提交到區塊鏈上的驗證合約。驗證合約執行相對簡單的密碼學檢查,確保證明的有效性。
Solidity 本身不是為 ZKP 設計的,但可以通過預編譯合約和優化的驗證算法來實現高效驗證。常見的驗證算法包括 Groth16、PLONK 和 Halo2,每種都有不同的性能特性。
二、承諾方案實現
2.1 基本的 Pedersen 承諾合約
Pedersen 承諾是一種基於橢圓曲線的承諾方案,提供加法同態特性,這意味著可以在加密值之間進行算術運算而不需要解密。以下是一個完整的 Pedersen 承諾合約實現。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
/**
* @title PedersenCommitment
* @dev 實現基本的 Pedersen 承諾方案
*
* 承諾公式:C = g^value * h^randomness mod p
* 其中:
* - g 和 h 是生成元(固定在合約中)
* - value 是承諾的值
* - randomness 是隨機數
* - p 是橢圓曲線的階
*
* 注意:此實現使用 BN128 曲線
*/
contract PedersenCommitment is ReentrancyGuard {
// 橢圓曲線參數(BN128)
// G1 生成點
uint256 constant G1_X = 1;
uint256 constant G1_Y = 2;
// 第二生成點(用於隨機數)
uint256 constant G2_X_REAL = 11559732032986387107991004021392285783925814761872452480429701767204806682569929;
uint256 constant G2_X_IMAG = 10857046999023057135944570762232829481370756364378512086930520185823550095843281;
uint256 constant G2_Y_REAL = 408236787586343368133220340314543556831685132759340120810574107621412009353984849;
uint256 constant G2_Y_IMAG = 13838061751086430099700209215766093284412878428138373054851687027583696908822139;
// 曲線順序
uint256 constant CURVE_ORDER = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
// 承諾存儲
struct Commitment {
bytes32 commitmentHash; // 承諾的 keccak256 哈希
uint256 value; // 承諾的值(加密存儲)
uint256 randomness; // 隨機數(加密存儲)
address depositor; // 存款人
uint256 timestamp; // 創建時間
bool revealed; // 是否已揭示
}
// 承諾 ID 到承諾的映射
mapping(bytes32 => Commitment) public commitments;
// 事件
event CommitmentCreated(
bytes32 indexed commitmentId,
bytes32 commitmentHash,
address indexed depositor,
uint256 timestamp
);
event CommitmentRevealed(
bytes32 indexed commitmentId,
uint256 value,
uint256 randomness,
address indexed depositor
);
/**
* @dev 創建承諾
* @param value 承諾的值
* @param randomness 隨機數(應該由客戶端生成)
* @return commitmentId 承諾的唯一 ID
*/
function createCommitment(uint256 value, uint256 randomness)
public
nonReentrant
returns (bytes32)
{
require(value < CURVE_ORDER, "Value too large");
require(randomness < CURVE_ORDER, "Randomness too large");
require(randomness != 0, "Randomness cannot be zero");
// 計算承諾
bytes32 commitmentHash = _computeCommitment(value, randomness);
// 生成唯一 ID
bytes32 commitmentId = keccak256(
abi.encodePacked(commitmentHash, msg.sender, block.timestamp)
);
// 存儲承諾(值和隨機數加密存儲,實際應用中應使用更多保護)
commitments[commitmentId] = Commitment({
commitmentHash: commitmentHash,
value: value,
randomness: randomness,
depositor: msg.sender,
timestamp: block.timestamp,
revealed: false
});
emit CommitmentCreated(commitmentId, commitmentHash, msg.sender, block.timestamp);
return commitmentId;
}
/**
* @dev 揭示承諾
* @param commitmentId 承諾 ID
* @param value 原始值
* @param randomness 隨機數
*/
function revealCommitment(
bytes32 commitmentId,
uint256 value,
uint256 randomness
) public nonReentrant {
Commitment storage commitment = commitments[commitmentId];
require(commitment.depositor == msg.sender, "Not depositor");
require(!commitment.revealed, "Already revealed");
// 驗證承諾
bytes32 computedHash = _computeCommitment(value, randomness);
require(computedHash == commitment.commitmentHash, "Invalid reveal");
commitment.revealed = true;
emit CommitmentRevealed(commitmentId, value, randomness, msg.sender);
}
/**
* @dev 驗證承諾是否有效(不揭示)
* @param commitmentId 承諾 ID
* @return bool 承諾是否存在且未揭示
*/
function verifyCommitment(bytes32 commitmentId)
public
view
returns (bool)
{
Commitment storage commitment = commitments[commitmentId];
return commitment.depositor != address(0) && !commitment.revealed;
}
/**
* @dev 計算 Pedersen 承諾
* @param value 承諾的值
* @param randomness 隨機數
* @return bytes32 承諾的哈希
*/
function _computeCommitment(uint256 value, uint256 randomness)
internal
pure
returns (bytes32)
{
// 計算 C = g^value * h^randomness
// 使用簡化的實現:實際需要橢圓曲線運算
// 這裡使用 keccak256 模擬
return keccak256(abi.encodePacked(value, randomness));
}
/**
* @dev 批量創建承諾
* @param values 值的數組
* @param randomnesses 隨機數的數組
* @return bytes32[] 承諾 ID 的數組
*/
function batchCreateCommitment(
uint256[] calldata values,
uint256[] calldata randomnesses
) external nonReentrant returns (bytes32[] memory) {
require(values.length == randomnesses.length, "Length mismatch");
bytes32[] memory commitmentIds = new bytes32[](values.length);
for (uint256 i = 0; i < values.length; i++) {
commitmentIds[i] = createCommitment(values[i], randomnesses[i]);
}
return commitmentIds;
}
}
2.2 同態加密承諾
同態加密允許在加密數據上直接進行算術運算,而無需解密。這對於構建隱私借貸、隱私拍賣等應用特別有用。以下是一個支持簡單同態操作的承諾合約。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/**
* @title HomomorphicCommitment
* @dev 實現同態加密特性的承諾方案
*
* 支持的操作:
* - 承諾相加:C1 + C2 = C(value1 + value2)
* - 標量乘法:k * C = C(k * value)
*
* 這使用「加法同態」加密方案
*/
contract HomomorphicCommitment {
uint256 public constant MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
struct EncryptedValue {
bytes32 commitment;
uint256[2] encrypted; // [value + random*modulus, random]
}
mapping(bytes32 => EncryptedValue) public encryptedValues;
event ValueEncrypted(
bytes32 indexed id,
uint256[2] encrypted
);
event ValuesAdded(
bytes32 indexed resultId,
bytes32 indexed id1,
bytes32 indexed id2
);
/**
* @dev 創建加密值
* @param value 原始值
* @param random 隨機數
* @return bytes32 加密值的 ID
*/
function encrypt(uint256 value, uint256 random)
public
returns (bytes32)
{
require(value < MODULUS, "Value too large");
require(random < MODULUS, "Random too large");
// 加密:enc = value + random * MODULUS
uint256[2] memory encrypted;
encrypted[0] = value + random * MODULUS;
encrypted[1] = random;
bytes32 id = keccak256(abi.encodePacked(encrypted, msg.sender));
encryptedValues[id] = EncryptedValue({
commitment: keccak256(abi.encodePacked(value, random)),
encrypted: encrypted
});
emit ValueEncrypted(id, encrypted);
return id;
}
/**
* @dev 同態加法:計算 C1 + C2
* @param id1 第一個加密值 ID
* @param id2 第二個加密值 ID
* @return bytes32 結果的 ID
*/
function add(bytes32 id1, bytes32 id2)
public
returns (bytes32)
{
EncryptedValue storage v1 = encryptedValues[id1];
EncryptedValue storage v2 = encryptedValues[id2];
require(v1.encrypted[1] != 0, "First value not found");
require(v2.encrypted[1] != 0, "Second value not found");
// 同態加法:(v1 + r1*p) + (v2 + r2*p) = (v1+v2) + (r1+r2)*p
uint256[2] memory result;
result[0] = (v1.encrypted[0] + v2.encrypted[0]) % MODULUS;
result[1] = (v1.encrypted[1] + v2.encrypted[1]) % MODULUS;
bytes32 resultId = keccak256(abi.encodePacked(result, msg.sender));
// 計算實際值(這裡簡化處理)
bytes32 actualCommitment = keccak256(abi.encodePacked(
(v1.encrypted[0] + v2.encrypted[0] - v1.encrypted[1] - v2.encrypted[1]) % MODULUS,
(v1.encrypted[1] + v2.encrypted[1]) % MODULUS
));
encryptedValues[resultId] = EncryptedValue({
commitment: actualCommitment,
encrypted: result
});
emit ValuesAdded(resultId, id1, id2);
return resultId;
}
/**
* @dev 驗證兩個加密值之和
* @param id1 第一個加密值 ID
* @param id2 第二個加密值 ID
* @param expectedResultId 預期結果 ID
* @return bool 驗證是否成功
*/
function verifySum(
bytes32 id1,
bytes32 id2,
bytes32 expectedResultId
) public view returns (bool) {
EncryptedValue storage v1 = encryptedValues[id1];
EncryptedValue storage v2 = encryptedValues[id2];
EncryptedValue storage expected = encryptedValues[expectedResultId];
// 驗證:enc1 + enc2 == expected
uint256 computedSum0 = (v1.encrypted[0] + v2.encrypted[0]) % MODULUS;
uint256 computedSum1 = (v1.encrypted[1] + v2.encrypted[1]) % MODULUS;
return computedSum0 == expected.encrypted[0] &&
computedSum1 == expected.encrypted[1];
}
}
三、Merkle 樹實現
3.1 稀疏 Merkle 樹
稀疏 Merkle 樹(SMT)是一種優化的 Merkle 樹,特別適合處理包含大量「空插槽」的數據集。在以太坊中, SMT 被廣泛用於帳戶狀態存儲和空投驗證。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
/**
* @title SparseMerkleTree
* @dev 實現稀疏 Merkle 樹(SMT)
*
* SMT 與標準 Merkle 樹的區別:
* - 支持高效的「不存在證明」
* - 自動處理稀疏數據
* - 適合大規模集合驗證
*/
contract SparseMerkleTree {
// 樹參數
uint256 constant TREE_DEPTH = 256; // 支援 2^256 個葉節點
uint256 constant ZERO_VALUE = 0;
uint256 constant ZERO_HASH = 0x0; // 零哈希
// Merkle 樹根
bytes32 public root;
uint256 public leafCount;
// 節點哈希映射
mapping(bytes32 => uint256) public values;
// 每層的零哈希(用於空路徑)
bytes32[TREE_DEPTH + 1] public zeroHashes;
// 事件
event LeafInserted(uint256 indexed index, bytes32 value, bytes32 newRoot);
constructor() {
// 初始化零哈希
_initializeZeroHashes();
root = zeroHashes[TREE_DEPTH];
}
/**
* @dev 初始化零哈希數組
*/
function _initializeZeroHashes() internal {
zeroHashes[0] = bytes32(ZERO_VALUE);
for (uint256 i = 1; i <= TREE_DEPTH; i++) {
zeroHashes[i] = _hashPair(zeroHashes[i-1], zeroHashes[i-1]);
}
}
/**
* @dev 計算兩個子節點的父節點哈希
*/
function _hashPair(bytes32 left, bytes32 right) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(left, right));
}
/**
* @dev 計算節點的 Merkle 根
*/
function _computeNodeHash(
bytes32 left,
bytes32 right,
uint256 depth
) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(left, right, depth));
}
/**
* @dev 插入新葉節點
* @param value 要插入的值
* @return bytes32 新的 Merkle 根
*/
function insert(bytes32 value) public returns (bytes32) {
require(value != bytes32(0), "Value cannot be zero");
uint256 index = leafCount;
bytes32 currentHash = value;
// 從葉節點向上計算到根
for (uint256 i = 0; i < TREE_DEPTH; i++) {
uint256 bit = (index >> i) & 1;
bytes32 sibling;
if (bit == 0) {
// 左子節點
sibling = zeroHashes[i];
currentHash = _hashPair(currentHash, sibling);
} else {
// 右子節點
sibling = zeroHashes[i];
currentHash = _hashPair(sibling, currentHash);
}
}
root = currentHash;
leafCount++;
emit LeafInserted(index, value, root);
return root;
}
/**
* @dev 批量插入多個葉節點
* @param values 要插入的值數組
*/
function batchInsert(bytes32[] calldata values) external {
for (uint256 i = 0; i < values.length; i++) {
insert(values[i]);
}
}
/**
* @dev 驗證 Merkle 證明
* @param leaf 葉節點值
* @param proof Merkle 證明
* @param index 葉節點索引
* @return bool 驗證是否成功
*/
function verify(
bytes32 leaf,
bytes32[] calldata proof,
uint256 index
) public view returns (bool) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
uint256 bit = (index >> i) & 1;
if (bit == 0) {
computedHash = _hashPair(computedHash, proof[i]);
} else {
computedHash = _hashPair(proof[i], computedHash);
}
}
return computedHash == root;
}
/**
* @dev 生成完整的 Merkle 證明(包括路徑和深度)
* @param index 葉節點索引
* @return proof 完整的 Merkle 證明
*/
function getProof(uint256 index) public view returns (bytes32[] memory proof) {
require(index < leafCount, "Index out of bounds");
proof = new bytes32[](TREE_DEPTH);
bytes32 currentHash = values[bytes32(index)];
uint256 idx = index;
for (uint256 i = 0; i < TREE_DEPTH; i++) {
uint256 bit = idx & 1;
if (bit == 0) {
// 左節點
proof[i] = zeroHashes[i];
currentHash = _hashPair(currentHash, zeroHashes[i]);
} else {
// 右節點 - 需要獲取兄弟節點
// 這裡簡化處理
proof[i] = currentHash;
}
idx >>= 1;
}
}
}
3.2 可升級的 Merkle 白名單
以下是一個完整的白名單合約,允許用戶通過 Merkle 證明驗證成員身份而不暴露地址。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
/**
* @title MerkleWhitelist
* @dev 基於 Merkle 樹的白名單合約
*
* 特點:
* - 隱私保護:不直接存儲用戶地址
* - 高效驗證:O(log N) 證明大小
* - 可升級:可更新 Merkle 根以添加/移除成員
*/
contract MerkleWhitelist is Ownable, ReentrancyGuard {
// Merkle 根
bytes32 public merkleRoot;
// 已Claim的份額追蹤(防止重複Claim)
mapping(bytes32 => bool) public claimed;
// 合約狀態
bool public isActive;
// Claim 限額
uint256 public constant CLAIM_LIMIT = 1 ether;
// 事件
event MerkleRootUpdated(bytes32 oldRoot, bytes32 newRoot);
event Claimed(address indexed account, uint256 amount);
constructor() Ownable() {
isActive = true;
}
/**
* @dev 更新 Merkle 根
* @param newRoot 新的 Merkle 根
*/
function updateMerkleRoot(bytes32 newRoot) external onlyOwner {
bytes32 oldRoot = merkleRoot;
merkleRoot = newRoot;
emit MerkleRootUpdated(oldRoot, newRoot);
}
/**
* @dev 激活/停用合約
* @param _isActive 新的激活狀態
*/
function setActive(bool _isActive) external onlyOwner {
isActive = _isActive;
}
/**
* @dev Claim 白名單份額
* @param amount Claim 的金額
* @param merkleProof Merkle 證明
*/
function claim(uint256 amount, bytes32[] calldata merkleProof)
external
nonReentrant
{
require(isActive, "Contract not active");
require(amount <= CLAIM_LIMIT, "Amount exceeds limit");
// 防止重複Claim
bytes32 claimId = keccak256(abi.encodePacked(msg.sender, amount));
require(!claimed[claimId], "Already claimed");
// 驗證 Merkle 證明
bytes32 leaf = keccak256(abi.encodePacked(msg.sender, amount));
bool valid = _verifyMerkleProof(leaf, merkleProof);
require(valid, "Invalid proof");
// 標記為已Claim
claimed[claimId] = true;
// 發放獎勵(這裡簡化處理)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
emit Claimed(msg.sender, amount);
}
/**
* @dev 驗證 Merkle 證明
* @param leaf 葉節點
* @param proof Merkle 證明
* @return bool 驗證結果
*/
function _verifyMerkleProof(bytes32 leaf, bytes32[] calldata proof)
internal
view
returns (bool)
{
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
bytes32 proofElement = proof[i];
if (computedHash < proofElement) {
computedHash = keccak256(
abi.encodePacked(computedHash, proofElement)
);
} else {
computedHash = keccak256(
abi.encodePacked(proofElement, computedHash)
);
}
}
return computedHash == merkleRoot;
}
/**
* @dev 批量驗證預計算(用於前端驗證)
* @param leaf 葉節點
* @param proof Merkle 證明
* @return bool 驗證結果
*/
function verifyProof(bytes32 leaf, bytes32[] calldata proof)
external
view
returns (bool)
{
return _verifyMerkleProof(leaf, proof);
}
/**
* @dev 接收 ETH
*/
receive() external payable {}
}
四、零知識證明驗證合約
4.1 Groth16 驗證器
Groth16 是一種高效的 zk-SNARK 驗證算法,廣泛應用於各種隱私保護應用。以下是一個簡化的 Groth16 驗證合約框架。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/**
* @title Groth16Verifier
* @dev Groth16 零知識證明驗證合約
*
* 此合約實現了最基礎的 Groth16 驗證邏輯
* 實際部署時應使用 noir 或 snarkjs 生成的驗證合約
*/
contract Groth16Verifier {
// 驗證關鍵(由可信設置生成)
uint256[2] public IC0; // 初始曲線點
uint256[2][] public IC; // 輸入對應的曲線點
// 成對檢查合約地址(以太坊預編譯)
Pairing public pairing;
struct G1Point {
uint256 x;
uint256 y;
}
struct G2Point {
uint256[2] x;
uint256[2] y;
}
struct Proof {
G1Point a;
G2Point b;
G1Point c;
}
constructor(address pairingAddress) {
pairing = Pairing(pairingAddress);
// 初始化 IC(由 noir/snarkjs 生成)
// 這裡是示例值
IC0 = [
11559732032986387107991004021392285783925814761872452480429701767204806682569929,
10857046999023057135944570762232829481370756364378512086930520185823550095843281
];
}
/**
* @dev 驗證零知識證明
* @param input 公開輸入數組
* @param proof ZK 證明
* @return bool 驗證是否成功
*/
function verify(uint256[] memory input, Proof memory proof)
public
view
returns (bool)
{
// 驗證 inputs 數量與 IC 匹配
require(input.length + 1 == IC.length, "Invalid input length");
// 計算線性組合:vkX = IC0 + Σ(input[i] * IC[i+1])
uint256[3] memory vkX = [IC0[0], IC0[1], 0];
for (uint256 i = 0; i < input.length; i++) {
// vkX += input[i] * IC[i+1]
(uint256 px, uint256 py) = _scalarMul(IC[i+1], input[i]);
vkX[0] = addmod(vkX[0], px, 21888242871839275222246405745257275088548364400416034343698204186575808495617);
vkX[1] = addmod(vkX[1], py, 21888242871839275222246405745257275088548364400416034343698204186575808495617);
}
// 成對檢查
return pairing.verifyProof(
[proof.a.x, proof.a.y],
[proof.b.x[0], proof.b.x[1], proof.b.y[0], proof.b.y[1]],
[proof.c.x, proof.c.y],
[vkX[0], vkX[1]]
);
}
/**
* @dev 標量乘法
*/
function _scalarMul(uint256[2] memory p, uint256 s)
internal
pure
returns (uint256, uint256)
{
// 簡化實現
// 實際應使用elliptic curve library
return (p[0], p[1]);
}
}
/**
* @dev 成對檢查接口(以太坊 BN128 預編譯)
*/
interface Pairing {
function verifyProof(
uint256[2] memory a,
uint256[2][2] memory b,
uint256[2] memory c,
uint256[2] memory input
) external view returns (bool);
}
4.2 PLONK 驗證器
PLONK(Permutations over Lagrange-bases for Oecumenical Noninteractive arguments of Knowledge)是一種通用的 zk-SNARK,無需每次電路變更時都進行可信設置。以下是 PLONK 驗證的框架實現。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/**
* @title PLONKVerifier
* @dev PLONK 零知識證明驗證合約
*
* PLONK 特點:
* - 通用可信設置(只需一次)
* - 較大的證明大小
* - 更靈活的電路設計
*/
contract PLONKVerifier {
// Trusted setup 公共參數
bytes32 public constant SETUP_HASH = 0x00; // 實際應存儲實際的 setup hash
// 驗證關鍵(壓縮形式)
bytes32 public verificationKeyHash;
struct Proof {
// 線性組合點
uint256[2]W1;
uint256[2]W2;
uint256[2]W3;
// 批量打開
uint256[2]T;
// 原始輸入承諾
uint256[2]X;
}
struct PublicInput {
bytes32[] commitments;
}
/**
* @dev 驗證 PLONK 證明
* @param proof PLONK 證明
* @param publicInputs 公開輸入
* @return bool 驗證結果
*/
function verify(Proof memory proof, PublicInput memory publicInputs)
public
view
returns (bool)
{
// PLONK 驗證涉及多個步驟:
// 1. 驗證 commitments
// 2. 驗證 ZK 乘積協議
// 3. 驗證線性組合
// 4. 驗證批次打開
// 這裡是簡化的框架
// 完整實現需要處理多個橢圓曲線配對
// 預檢查
require(proof.W1[0] != 0 || proof.W1[1] != 0, "Invalid W1");
require(proof.W2[0] != 0 || proof.W2[1] != 0, "Invalid W2");
// 返回結果(實際應執行完整驗證)
return true;
}
/**
* @dev 批量驗證多個證明
* @param proofs 證明數組
* @param publicInputs 公開輸入數組
* @return bool[] 驗證結果數組
*/
function batchVerify(
Proof[] memory proofs,
PublicInput[] memory publicInputs
) external view returns (bool[] memory) {
bool[] memory results = new bool[](proofs.length);
for (uint256 i = 0; i < proofs.length; i++) {
results[i] = verify(proofs[i], publicInputs[i]);
}
return results;
}
}
五、實際應用案例
5.1 隱私拍賣合約
以下是一個完整的隱私拍賣合約,展示了如何結合多種隱私技術。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
/**
* @title PrivacyAuction
* @dev 隱私拍賣合約
*
* 功能:
* - 隱藏投標金額
* - 防止圍標(通過隱藏投標者身份)
* - 公開揭示最終結果
*/
contract PrivacyAuction is ReentrancyGuard {
// 拍賣參數
address public beneficiary;
uint256 public auctionEndTime;
bool public ended;
// 最高出價
address public highestBidder;
uint256 public highestBid;
// Merkle 根(投標承諾)
bytes32 public biddingMerkleRoot;
// 投標追蹤
mapping(bytes32 => bool) public bidRevealed;
mapping(address => uint256) public pendingReturns;
// 事件
event BidSubmitted(address indexed bidder, bytes32 commitment);
event BidRevealed(address indexed bidder, uint256 amount);
event AuctionEnded(address indexed winner, uint256 amount);
modifier onlyBefore() {
require(block.timestamp < auctionEndTime, "Auction ended");
_;
}
modifier onlyAfter() {
require(block.timestamp >= auctionEndTime, "Auction not ended");
require(!ended, "Auction already ended");
_;
}
constructor(
address _beneficiary,
uint256 _durationSeconds,
bytes32 _merkleRoot
) {
beneficiary = _beneficiary;
auctionEndTime = block.timestamp + _durationSeconds;
biddingMerkleRoot = _merkleRoot;
}
/**
* @dev 提交隱藏投標
* @param commitment 投標承諾(金額的哈希 + 隨機數)
* @param merkleProof Merkle 證明
*/
function submitBid(bytes32 commitment, bytes32[] calldata merkleProof)
external
onlyBefore
nonReentrant
{
// 驗證 Merkle 證明
bytes32 leaf = keccak256(abi.encodePacked(msg.sender, commitment));
bool valid = _verifyMerkleProof(leaf, merkleProof);
require(valid, "Invalid merkle proof");
// 存儲承諾
bytes32 bidId = keccak256(
abi.encodePacked(commitment, msg.sender, block.timestamp)
);
emit BidSubmitted(msg.sender, commitment);
}
/**
* @dev 揭示投標
* @param amount 投標金額
* @param randomness 隨機數
*/
function revealBid(uint256 amount, uint256 randomness)
external
onlyAfter
nonReentrant
{
bytes32 commitment = keccak256(abi.encodePacked(amount, randomness));
bytes32 bidId = keccak256(
abi.encodePacked(commitment, msg.sender, block.timestamp)
);
require(!bidRevealed[bidId], "Already revealed");
// 驗證承諾
require(
_computeCommitment(amount, randomness) == commitment,
"Commitment mismatch"
);
bidRevealed[bidId] = true;
// 更新最高出價
if (amount > highestBid) {
// 退還之前的高價者
if (highestBidder != address(0)) {
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = amount;
} else {
// 退款
pendingReturns[msg.sender] += amount;
}
emit BidRevealed(msg.sender, amount);
}
/**
* @dev 結束拍賣
*/
function endAuction() external onlyAfter nonReentrant {
ended = true;
if (highestBidder != address(0)) {
(bool success, ) = beneficiary.call{value: highestBid}("");
require(success, "Transfer failed");
}
emit AuctionEnded(highestBidder, highestBid);
}
/**
* @dev 領取退款
*/
function claimPendingReturns() external nonReentrant {
uint256 amount = pendingReturns[msg.sender];
require(amount > 0, "No pending returns");
pendingReturns[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
/**
* @dev 計算承諾
*/
function _computeCommitment(uint256 value, uint256 randomness)
internal
pure
returns (bytes32)
{
return keccak256(abi.encodePacked(value, randomness));
}
/**
* @dev 驗證 Merkle 證明
*/
function _verifyMerkleProof(bytes32 leaf, bytes32[] calldata proof)
internal
view
returns (bool)
{
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
bytes32 proofElement = proof[i];
if (computedHash < proofElement) {
computedHash = keccak256(
abi.encodePacked(computedHash, proofElement)
);
} else {
computedHash = keccak256(
abi.encodePacked(proofElement, computedHash)
);
}
}
return computedHash == biddingMerkleRoot;
}
receive() external payable {}
}
六、安全性考量與最佳實踐
6.1 常見漏洞與防護
在開發隱私合約時,需要特別注意以下安全問題。首先是「承諾時序攻擊」:如果承諾和揭示在同一筆交易中完成,攻擊者可以通過 MEV 機器人觀察內存池中的交易,提取承諾信息並搶先揭示。防護措施是在承諾和揭示之間添加延遲,或者使用 Commit-Reveal 方案的改進版本。
第二個問題是「隨機數可預測性」:使用 block.timestamp 或 block.difficulty 作為隨機數源會削弱承諾的隱藏性。應該要求用戶提供隨機數,或者使用區塊鏈之外的隨機數源(如 Chainlink VRF)。
第三個是「重放攻擊」:如果承諾驗證不嚴格,攻擊者可能重放舊的有效承諾。應該包含時間戳和 nonce,確保每個承諾只能使用一次。
6.2 Gas 優化
隱私合約的一個主要挑戰是 Gas 成本。以下是一些優化策略:
Merkle 樹優化:減少樹深度可以顯著降低驗證成本。如果只需要驗證有限數量的成員,可以將深度從 256 減少到較小的值。
批量驗證:將多個驗證合併到單一交易中可以分攤固定成本。 PLONK 等通用證明系統特別適合這種優化。
預編譯合約:使用以太坊預編譯的橢圓曲線運算可以大幅降低 Gas 消耗。
6.3 測試策略
隱私合約的測試需要特別的方法。單元測試應該覆蓋所有約束邏輯,確保驗證函數正確。集成測試需要模擬完整的 Commit-Reveal 流程。模糊測試可以幫助發現邊界情況和潛在漏洞。
結論
本文深入探討了使用 Solidity 構建隱私保護智能合約的核心技術。通過承諾方案,開發者可以實現數值的隱藏和驗證;通過 Merkle 樹,可以實現高效的集合成員驗證;通過零知識證明,可以實現複雜的隱私邏輯。
這些技術的組合為構建隱私借貸、隱私拍賣、隱私投票等應用提供了堅實的基礎。雖然每種技術都有其權衡和限制,但通過合理的架構設計,可以在實用性和隱私保護之間取得良好的平衡。
建議開發者在實際項目中使用經過審計的庫(如 OpenZeppelin 和 circom),並在部署前進行全面的安全審計。隨著零知識證明技術的持續發展,我們可以期待看到更多創新的隱私保護應用出現在以太坊生態系統中。
參考資料
- OpenZeppelin Contracts. https://docs.openzeppelin.com/contracts
- Merkle Tree 密碼學基礎。 https://github.com/ethereum/research/wiki/Merkle-Trees
- Groth16 論文。 https://eprint.iacr.org/2016/260
- PLONK 論文。 https://eprint.iacr.org/2019/953
- 零知識證明開發工具。 https://github.com/iden3/snarkjs
- 以太坊預編譯合約。 https://ethereum.github.io/yellowpaper/paper.pdf
相關文章
- Noir 隱私合約開發完整指南:從零知識證明到實際應用 — 深入介紹 Noir 語言的開發環境、語法特性與實際應用,包括零知識證明基礎、承諾方案實現、以及如何在以太坊上構建隱私保護應用,提供完整的程式碼範例與開發實踐指南。
- 智能合約部署實戰:從環境搭建到主網上線 — 涵蓋從開發環境準備、本地測試、測試網部署、正式環境部署的完整流程,深入探討 Gas 優化、安全審計、合約升級等進階主題,提供開發者從新手到專業的實戰指南。
- EIP-1559 深度解析:以太坊費用市場的範式轉移 — 深入解析 EIP-1559 的費用結構、ETH 燃燒機制、經濟學意涵,以及對用戶、驗證者和生態系統的影響。
- EIP-7702 帳戶抽象完整指南 — 深入介紹 EIP-7702 讓 EOA 臨時獲得合約功能的技术原理,涵蓋社交恢復錢包、自動化交易、權限委托等應用場景。
- Verkle Trie 深度技術解析:以太坊狀態革命的密碼學基礎 — 深入探討 Verkle Trie 的密碼學基礎、KZG 承諾機制、與 MPT 的詳細比較、實際實現考量,以及對 Stateless Client 的深遠影響,包含完整的程式碼範例與數學推導。
延伸閱讀與來源
- Ethereum.org Developers 官方開發者入口與技術文件
- EIPs 以太坊改進提案
這篇文章對您有幫助嗎?
請告訴我們如何改進:
評論
發表評論
注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。
目前尚無評論,成為第一個發表評論的人吧!