欄位
值
狀態
草案 (Draft)
類型
標準軌跡 (Standards Track)
類別
ERC
依賴項目
EIP-712, ERC-5564
摘要 (Abstract)
本 ERC 定義了一種具備隱私保護功能的 ERC-1155 多代幣標準。每個地址、每個 tokenId 的餘額透過 Poseidon 哈希承諾(hash commitments)進行隱藏,而其有效性則透過鏈上的 Groth16 零知識證明(ZK proofs)進行驗證。代幣類型(tokenId)和總供應量保持公開。
本標準將 VOSA (虛擬一次性子帳戶) 模型擴展至多代幣場景——每個(地址, tokenId)對存儲一個隱藏餘額數量的承諾。所有權透過標準 ECDSA 簽名證明(兼容 MetaMask,無需自定義錢包)。
隱私模型 :數量和各地址持有量被隱藏。代幣類型、轉帳圖譜(哪些 VOSA 進行了交易)以及每個 tokenId 的總供應量保持可見。這在保護餘額隱私的同時,也能滿足合規性要求。
核心特性 :
每個 tokenId 的餘額隱私(數量隱藏在 Poseidon 承諾中)
每個 tokenId 可進行任意數量的轉帳(非固定面額)
鏈上 Groth16 ZK 證明驗證(完全去中心化)
標準 ECDSA 簽名 + EVM 原生 20 字節地址
O(1) 支出追蹤,具備可配置的週期性清理功能
跨多個 tokenId 的批量鑄造
針對每個 tokenId 的策略(Policy,循環授權)和許可(Permit,一次性授權)
動機 (Motivation)
ERC-1155 的隱私缺口
ERC-1155 公開了所有持有資訊:
balanceOf(0x1234, tokenId=42) → 500 // 任何人都能看到各項目的持有量
TransferSingle(from, to, id, 100) // 數量是公開的
這在多代幣場景中會產生問題:
為何選擇 ERC-1155(多代幣)而非 ERC-721(單一 NFT)?
ERC-1155 涵蓋了更廣泛的使用場景。許多「NFT」應用實際上需要每種代幣類型具備同質化數量 :
使用場景
代幣模型
為何使用 1155?
遊戲道具
500 把劍,200 瓶藥水
每種類型內同質化,存在多種類型
活動門票
1000 張普通票,50 張 VIP 票
每個等級有對應數量
忠誠度積分
多個積分類別
每個類別內餘額同質化
代幣化商品
100 盎司黃金,50 盎司白銀
每個資產類型有對應數量
半同質化代幣
藝術品的第 1-100 號版本
相同類型,多個副本
對於真正獨一無二的 1-of-1 項目,只需將數量設為 1 —— 協議會將其視為特殊情況處理。
隱私對比
解決方案
數量隱私
持有者身份
轉帳圖譜
代幣類型可見
合規性
ERC-1155
❌
❌
❌
✅
透明
本標準
✅
✅
❌ (設計使然)
✅
可審計
全隱私 NFT
✅
✅
✅
❌
困難
設計理念
本標準旨在實現餘額隱私 ,而非匿名混幣 :
規範 (Specification)
本文件中的關鍵詞「必須」(MUST)、「不得」(MUST NOT)、「要求」(REQUIRED)、「應當」(SHALL)、「不應當」(SHALL NOT)、「應該」(SHOULD)、「不應該」(SHOULD NOT)、「推薦」(RECOMMENDED)、「不推薦」(NOT RECOMMENDED)、「可以」(MAY) 和「可選」(OPTIONAL) 應按照 RFC 2119 和 RFC 8174 的描述進行解釋。
概述
┌──────────────────────────────────────────────────────────────────────────┐
│ 隱私 ERC-1155 架構 │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ 存儲: │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ mapping(address => mapping(uint256 => bytes32)) balanceCommitment │ │
│ │ mapping(uint256 => uint256) totalSupply │ │
│ │ mapping(address => mapping(uint256 => PolicyMeta)) policies │ │
│ │ mapping(address => mapping(uint256 => PermitMeta)) permits │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ 操作: │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ mint(recipient, tokenId, amount, commitment, proof) │ │
│ │ mintBatch(recipient, tokenIds[], amounts[], commitments[], ...) │ │
│ │ burn(input, tokenId, amount, proof, signature) │ │
│ │ transfer(tokenId, inputs[], outputs[], proof, signatures) │ │
│ │ consolidate(tokenId, inputs[], output, proof, signatures) │ │
│ │ createPolicy(tokenId, spender, commitment, proof, signature) │ │
│ │ createPermit(tokenId, spender, commitment, proof, signature) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
每個 (address, tokenId) 的鏈上狀態:
mapping(address => mapping(uint256 => bytes32)) balanceCommitment;
// bytes32(0) → 從未使用
// Poseidon(tokenId, amount, blinder, ts) → 活躍餘額(數量已隱藏)
// SPENT_PREFIX | block.number → 已支出(在週期窗口後可清理)
與隱私 ERC-20 的關鍵差異
維度
隱私 ERC-20
隱私 ERC-1155
存儲
address → commitment
address → tokenId → commitment
承諾 (Commitment)
Poseidon(amount, blinder, ts)
Poseidon(tokenId, amount, blinder, ts)
供應量
單一 totalSupply
每個代幣類型對應 totalSupply[tokenId]
策略/許可 (Policy/Permit)
按地址
按 (address, tokenId)
EIP-712
類型哈希中無 tokenId
所有類型哈希均包含 tokenId
批量鑄造
不適用
針對多個 tokenId 的 mintBatch()
轉帳
任意數量,單一代幣
每次轉帳調用僅限一個 tokenId
常量
bytes16 public constant SPENT_PREFIX = 0xDEADDEADDEADDEADDEADDEADDEADDEAD;
uint256 public constant MAX_INPUTS = 10;
uint256 public constant MAX_OUTPUTS = 10;
uint256 public constant TIMESTAMP_WINDOW = 2 hours;
uint256 public constant DEFAULT_CLEANUP_WINDOW = 216_000; // 約 1 個月
數據結構
struct PolicyMeta {
address owner;
address spender;
uint256 expiry; // 0 = 永不過期
bool revoked;
}
struct PermitMeta {
address owner;
address spender;
uint256 expiry;
bool revoked;
bool used; // 單次使用
}
struct CreatePolicyParams {
address input;
bytes32 inputCommitment;
address policyAddress;
bytes32 policyCommitment;
uint256 policyTimestamp;
address changeAddress;
bytes32 changeCommitment;
uint256 changeTimestamp;
address spender;
uint256 expiry;
uint256 deadline;
}
struct CreatePermitParams {
address input;
bytes32 inputCommitment;
address permitAddress;
bytes32 permitCommitment;
uint256 permitTimestamp;
address changeAddress;
bytes32 changeCommitment;
uint256 changeTimestamp;
address spender;
uint256 expiry;
uint256 deadline;
}
核心接口
元數據 (Metadata)
interface IPrivateERC1155Metadata {
function uri(uint256 tokenId) external view returns (string memory);
function totalSupply(uint256 tokenId) external view returns (uint256);
}
VOSA 狀態管理
interface IPrivateERC1155VOSA {
function balanceCommitment(address vosa, uint256 tokenId) external view returns (bytes32);
function hasBalance(address vosa, uint256 tokenId) external view returns (bool);
function isEverUsed(address vosa, uint256 tokenId) external view returns (bool);
function isSpent(address vosa, uint256 tokenId) external view returns (bool);
function batchHasBalance(address[] calldata addrs, uint256[] calldata tokenIds)
external view returns (bool[] memory);
}
核心操作
interface IPrivateERC1155Core {
/// @notice 鑄造代幣 — 發行時 tokenId 和數量是公開的
function mint(
address recipient, uint256 tokenId, uint256 amount, bytes32 commitment,
uint256 outputTimestamp, bytes calldata ephemeralPubKey,
bytes calldata proof, bytes calldata memo
) external returns (bool);
/// @notice 跨多個 tokenId 批量鑄造 function mintBatch( address recipient, uint256[] calldata tokenIds, uint256[] calldata amounts, bytes32[] calldata commitments, uint256[] calldata outputTimestamps, bytes[] calldata ephemeralPubKeys, bytes[] calldata proofs, bytes calldata memo ) external returns (bool); /// @notice 銷毀代幣(全部或部分銷毀並找零) function burn( address input, uint256 tokenId, bytes32 inputCommitment, uint256 amount, address changeAddress, bytes32 changeCommitment, uint256 changeTimestamp, bytes calldata changeEphemeralPubKey, bytes calldata signature, bytes calldata proof, uint256 deadline ) external returns (bool); /// @notice 隱私轉帳(N 個輸入 → M 個輸出,單一 tokenId,數量隱藏) function transfer( uint256 tokenId, address[] calldata inputs, bytes32[] calldata inputCommitments, address[] calldata outputs, bytes32[] calldata outputCommitments, uint256[] calldata outputTimestamps, bytes[] calldata ephemeralPubKeys, bytes[] calldata signatures, bytes calldata proof, uint256 deadline, int256[] calldata policyChangeIndices, bytes calldata memo ) external returns (bool); /// @notice 將相同 tokenId 的多個 VOSA 合併為一個 function consolidate( uint256 tokenId, address[] calldata inputs, bytes32[] calldata inputCommitments, address output, bytes32 outputCommitment, uint256 outputTimestamp, bytes calldata ephemeralPubKey, bytes[] calldata signatures, bytes calldata proof, uint256 deadline ) external returns (bool);
}
授權接口
策略(循環授權,按 tokenId)
interface IPrivateERC1155Policy {
function createPolicy(
uint256 tokenId, CreatePolicyParams calldata params,
bytes[] calldata ephemeralPubKeys, bytes calldata ownerSignature,
bytes calldata proof, bytes calldata memo
) external returns (bool);
function revokePolicy(address policyAddress, uint256 tokenId) external; function getPolicy(address policyAddress, uint256 tokenId) external view returns ( address owner, address spender, uint256 expiry, bool revoked, bool isActive );
}
許可(一次性授權,按 tokenId)
interface IPrivateERC1155Permit {
function createPermit(
uint256 tokenId, CreatePermitParams calldata params,
bytes[] calldata ephemeralPubKeys, bytes calldata ownerSignature,
bytes calldata proof, bytes calldata memo
) external returns (bool);
function revokePermit(address permitAddress, uint256 tokenId) external; function getPermit(address permitAddress, uint256 tokenId) external view returns ( address owner, address spender, uint256 expiry, bool revoked, bool used, bool isActive );
}
事件 (Events)
interface IPrivateERC1155Events {
event TransferSingle(address indexed operator, address[] inputs, address[] outputs,
uint256 indexed tokenId, bytes32[] outputCommitments,
bytes[] ephemeralPubKeys, bytes memo);
event TransferBatch(address indexed operator, address from, address to,
uint256[] tokenIds, bytes32[] outputCommitments,
bytes[] ephemeralPubKeys, bytes memo);
event Burn(address indexed input, uint256 indexed tokenId, uint256 amount,
address changeAddress, bytes32 changeCommitment, bytes changeEphemeralPubKey);
event Consolidate(uint256 indexed tokenId, address[] inputs, address indexed output,
bytes32 outputCommitment, bytes ephemeralPubKey);
event PolicyCreated(uint256 indexed tokenId, address indexed policyAddress,
address indexed owner, address spender, uint256 expiry,
bytes32 policyCommitment, address changeAddress, bytes32 changeCommitment,
bytes[] ephemeralPubKeys, bytes memo);
event PolicyRevoked(address indexed policyAddress, uint256 indexed tokenId);
event PolicyMigrated(address indexed oldAddress, address indexed newAddress,
uint256 indexed tokenId);
event PermitCreated(uint256 indexed tokenId, address indexed permitAddress,
address indexed owner, address spender, uint256 expiry,
bytes32 permitCommitment, address changeAddress, bytes32 changeCommitment,
bytes[] ephemeralPubKeys, bytes memo);
event PermitUsed(address indexed permitAddress, uint256 indexed tokenId);
event PermitRevoked(address indexed permitAddress, uint256 indexed tokenId);
event AddressCleaned(address indexed addr, uint256 indexed tokenId, uint256 spentBlock);
event MinterUpdated(address indexed newMinter);
event VerifierUpdated(string indexed verifierType, address indexed newVerifier);
event CleanupWindowUpdated(uint256 oldWindow, uint256 newWindow);
}
EIP-712 類型定義
所有類型哈希均將 tokenId 作為第一個欄位,將簽名綁定到特定的代幣類型:
bytes32 constant TRANSFER_1155_TYPEHASH = keccak256(
"Transfer1155(uint256 tokenId,bytes32 inputsHash,bytes32 inputCommitmentsHash,"
"bytes32 outputsHash,bytes32 outputCommitmentsHash,uint256 deadline)"
);
bytes32 constant BURN_1155_TYPEHASH = keccak256(
"Burn1155(uint256 tokenId,address input,bytes32 inputCommitment,uint256 amount,"
"address changeAddress,bytes32 changeCommitment,uint256 changeTimestamp,uint256 deadline)"
);
bytes32 constant CONSOLIDATE_1155_TYPEHASH = keccak256(
"Consolidate1155(uint256 tokenId,bytes32 inputsHash,bytes32 inputCommitmentsHash,"
"address output,bytes32 outputCommitment,uint256 deadline)"
);
bytes32 constant CREATE_POLICY_1155_TYPEHASH = keccak256(
"CreatePolicy1155(uint256 tokenId,address input,bytes32 inputCommitment,"
"address policyAddress,bytes32 policyCommitment,uint256 policyTimestamp,"
"address changeAddress,bytes32 changeCommitment,uint256 changeTimestamp,"
"address spender,uint256 expiry,uint256 deadline)"
);
bytes32 constant CREATE_PERMIT_1155_TYPEHASH = keccak256(
"CreatePermit1155(uint256 tokenId,address input,bytes32 inputCommitment,"
"address permitAddress,bytes32 permitCommitment,uint256 permitTimestamp,"
"address changeAddress,bytes32 changeCommitment,uint256 changeTimestamp,"
"address spender,uint256 expiry,uint256 deadline)"
);
ZK 電路規範
承諾格式
commitment = Poseidon(tokenId, amount, blinder, timestamp)
其中:
tokenId: uint256,公開(識別代幣類型)
amount: uint256,必須 < 2^96
blinder: 域元素(Field element),不得為 0
timestamp: uint256,用於防重放攻擊
與 ERC-20 的關鍵差異:tokenId 被包含在承諾中。電路強制要求轉帳中的所有輸入和輸出必須使用相同的 tokenId ,防止跨代幣類型的操縱。
Amount1155 電路(鑄造/銷毀)
公開輸入:tokenId, inputCommitments[], outputCommitments[], absAmount,
isWithdraw, outputTimestamps[], txHash
私有輸入:amounts[], blinders[], timestamps[]
約束條件:
每個承諾 == Poseidon(tokenId, amount, blinder, timestamp)
所有承諾使用相同的 tokenId
鑄造:outputAmount == absAmount
銷毀:inputAmount == absAmount + changeAmount
所有數量在 [0, 2^96) 範圍內
所有 blinder ≠ 0
txHash 綁定(包含 tokenId)
變體
角色
公開信號
預估約束數
Amount_0_1_1155
鑄造
6 (tokenId, commitment, amount, isWithdraw, timestamp, txHash)
~900
Amount_1_0_1155
全額銷毀
5 (tokenId, commitment, amount, isWithdraw, txHash)
~900
Amount_1_1_1155
部分銷毀
7 (tokenId, 2 commitments, amount, isWithdraw, timestamp, txHash)
~1,400
Transfer1155 電路
公開輸入:tokenId, inputCommitments[], outputCommitments[],
outputTimestamps[], txHash
私有輸入:inputAmounts[], inputBlinders[], inputTimestamps[],
outputAmounts[], outputBlinders[]
約束條件:
每個 inputCommitment == Poseidon(tokenId, inputAmount, inputBlinder, inputTimestamp)
每個 outputCommitment == Poseidon(tokenId, outputAmount, outputBlinder, outputTimestamp)
所有承諾使用相同的 tokenId(由電路強制執行)
sum(inputAmounts) == sum(outputAmounts)(數量守恆)
所有數量在 [0, 2^96) 範圍內
所有 blinder ≠ 0
txHash == Poseidon(tokenId, inputCommitments..., outputCommitments...)
變體
角色
預估約束數
Transfer_1_2_1155
1→2 (轉帳 + 找零)
~1,800
Transfer_2_2_1155
2→2 (標準轉帳)
~2,300
Transfer_5_1_1155
5→1 (合併)
~3,200
註:合約允許最多 MAX_INPUTS=10,但目前的電路部署支持最多 5 個輸入。更大的變體可以從相同的模板編譯。
Poseidon 參數
哈希:Poseidon
寬度:承諾使用 t = 5 (4 輸入 + 1 容量);txHash 視情況而定
輪數:RF = 8 全輪,RP = 60 部分輪
域:BN254 Fr
授權邏輯
按 tokenId 授權 —— 每個 (address, tokenId) 最多有一個授權簽名者:
function _getAuthorizedSigner(address input, uint256 tokenId) internal view returns (address) {
PolicyMeta memory pMeta = _policyMeta[input][tokenId];
PermitMeta memory tMeta = _permitMeta[input][tokenId];
if (pMeta.owner != address(0)) { bool isActive = !pMeta.revoked && (pMeta.expiry == 0 || block.timestamp < pMeta.expiry); return isActive ? pMeta.spender : pMeta.owner; } else if (tMeta.owner != address(0)) { bool isActive = !tMeta.used && !tMeta.revoked && (tMeta.expiry == 0 || block.timestamp < tMeta.expiry); return isActive ? tMeta.spender : tMeta.owner; } return input;
}
週期清理
與隱私 ERC-20 相同:SPENT 標記編碼了 block.number,在可配置的清理窗口後可刪除。
function cleanup(address[] calldata addrs, uint256[] calldata tokenIds) external returns (uint256 cleaned);
原理闡述 (Rationale)
為何使用 4 個輸入的 Poseidon?
ERC-20 承諾使用 Poseidon(amount, blinder, timestamp) —— 3 個輸入。ERC-1155 增加了 tokenId 作為第 4 個輸入:Poseidon(tokenId, amount, blinder, timestamp)。這將承諾綁定到特定的代幣類型,防止將 100 把劍的承諾冒充為 100 瓶藥水。
電路強制要求轉帳中的所有輸入和輸出使用相同的 tokenId,因此從構造上杜絕了跨類型轉帳。
為何每次轉帳僅限單一 tokenId?
每次 transfer() 調用僅針對一個 tokenId。這保持了電路的簡單性(無需跨代幣的數量守恆)和驗證器的高效性。轉帳多種代幣類型需要多次調用 —— 這在實踐中是可以接受的,因為多類型的原子轉帳較為少見。
跨多個 tokenId 的批量鑄造透過 mintBatch() 支持,因為鑄造不需要 ZK 證明來保證數量守恆(鑄造數量是公開的)。
為何不隱藏 tokenId?
隱藏 tokenId 將需要:
對於大多數使用場景(遊戲道具、忠誠度積分、門票)來說,被轉帳的代幣類型並不敏感,隱藏它的收益微乎其微。保持 tokenId 公開極大地簡化了協議。
為何每個 (address, tokenId) 使用一個 SPENT_MARKER?
單個地址可以持有不同的 tokenId。支出一個 tokenId 不應影響其他 tokenId。SPENT 標記是按 (address, tokenId) 對設置的,允許獨立的生命週期管理。
性能 (Performance)
預估 Gas 消耗(使用模擬驗證器 —— 生產環境請為每個 Groth16 驗證增加約 200K):
操作
Gas
備註
mint (單個)
~320–360K
單一 tokenId
mintBatch (5 個 tokenId)
~1.4–1.6M
一筆交易 5 個 tokenId
burn (帶找零)
~350–390K
部分銷毀
transfer (1→2)
~370–410K
1 輸入, 2 輸出
transfer (2→2)
~420–460K
標準轉帳
consolidate (5→1)
~500–550K
合併 5 個 VOSA
createPolicy
~410–460K
按 tokenId
revokePolicy
~30–35K
由於承諾計算中增加了 tokenId 欄位(4 輸入 Poseidon 對比 3 輸入),電路約束數量比 ERC-20 等效項高出約 15%。
向後兼容性 (Backwards Compatibility)
本標準未 實現 ERC-1155 接口(如 balanceOf(address,uint256), safeTransferFrom 等)。它是一個獨立的隱私保護多代幣標準。保留了 uri() 函數以實現元數據兼容性。
安全考量 (Security Considerations)
跨代幣類型攻擊防禦
電路強制要求轉帳中的所有承諾使用相同的 tokenId 。這在 ZK 證明內部進行驗證 —— 無法在不使證明失效的情況下混合代幣類型。
可信設置 (Trusted Setup)
與隱私 ERC-20 相同:Groth16 每個電路變體都需要一個可信設置儀式。
搶跑保護 (Front-Running Protection)
EIP-712 簽名綁定了 tokenId 和所有可變參數。針對 tokenId=42 的簽名無法在 tokenId=7 上重放。
雙花 (Double-Spending)
使用與隱私 ERC-20 相同的 O(1) SPENT_MARKER 機制,但以 (address, tokenId) 為索引。
數量安全
範圍證明強制執行 0 ≤ amount < 2^96。數量守恆在 ZK 電路中按 tokenId 執行。
批量鑄造原子性
mintBatch() 是原子的 —— 要麼所有 tokenId 都鑄造成功,要麼都不成功。每個 tokenId 都有自己的承諾和 ZK 證明。
參考實現 (Reference Implementation)
nft-native/
├── contracts/
│ ├── src/
│ │ ├── PrivateERC1155.sol # 主合約 (約 1160 行)
│ │ ├── libraries/
│ │ │ └── PublicInputBuilder.sol # ZK 公開輸入構造輔助工具
│ │ ├── interfaces/
│ │ │ ├── IGroth16Verifier.sol
│ │ │ └── IPoseidon.sol
│ │ └── mocks/
│ │ ├── MockVerifier.sol
│ │ └── MockPoseidon.sol
│ └── test/
│ └── PrivateERC1155.test.ts
├── circuits/
│ ├── src/
│ │ ├── commitment1155.circom # Poseidon(tokenId, amount, blinder, ts)
│ │ ├── amount1155.circom # 鑄造/銷毀電路 (0→1, 1→0, 1→1)
│ │ ├── transfer1155.circom # 轉帳電路 (N→M)
│ │ └── amount_0_1_main.circom # 鑄造電路入口點
│ └── lib/
│ ├── poseidon.circom # circomlib Poseidon
│ └── comparators.circom # circomlib 比較器 + bitify
└── circuits/scripts/
├── compile.sh # 電路編譯
└── test.sh # Witness + 證明生成測試
討論問題
每次轉帳單一 tokenId :這是否可以接受?或者標準是否應該支持原子的多 tokenId 轉帳(代價是電路複雜度大幅增加)?
批量操作 :transferBatch()(一次調用轉帳多個 tokenId)是否應該成為標準的一部分,還是順序調用 transfer() 就足夠了?
tokenId 可見性 :是否存在某些場景,隱藏代幣類型的重要性足以抵消電路複雜度的增加?
元數據隱私 :目前的 uri() 是公開的。是否應該有一種機制為每個持有者提供加密的元數據?
與 ERC-1155 的互操作性 :是否應該提供一個 CompatibleERC1155 變體(類似 CompatibleERC20),具備公開餘額 + 隱私模式切換功能?
版權
透過 CC0 放棄版權及相關權利。
1 則貼文 - 1 位參與者 [閱讀完整主題](https://ethereum-magicians.org/t/draft-erc-private-erc-1155-privacy-preserving-multi-token-standard/27894)