1. 引言
2023年9月28日,a16z 的加密團隊發布了 Nakamoto Challenge,列出了區塊鏈中需要解決的最重要問題。尤其是其中的第四個問題格外引人注意:“合規的可編程隱私”,因為Zama團隊已經在這方面積極思考了一段時間。本文提出了使用同態加密和Zama fhEVM 機密智能合約協議的第一個解決方案。
fhEVM 是一個常規的 EVM,它通過一些預編譯功能使得使用Zama TFHE-rs 同態加密庫可以對加密狀態進行計算。從開發者的角度來看,這里沒有涉及加密學:
- 他們只需編寫使用所提供的加密數據類型(如 euint32、ebool 等)的 Solidity 代碼。
與其他隱私解決方案相比,fhEVM 的一個大優勢是所有數據和計算都發生在鏈上。這意味著可以像常規的明文合約一樣,擁有相同的組合性和數據可用性。【本文各種接口已被重構,理解思想即可。】
這一特性是構建可編程隱私的關鍵,因為所有的訪問控制邏輯都可以在合約本身中定義。協議中沒有任何需要硬編碼的部分,用戶也不需要進行鏈下操作來確保合規性。應用可以直接強化執行合規性,僅用幾行 Solidity 代碼!
本文將展示如何使用鏈上 DID 構建一個合規的 ERC20 代幣。
2. 通過鏈上機密 DID 實現身份抽象
去中心化標識符(Decentralized Identifier,DID)是一種由實體(如政府、登記機關、公司或用戶本人)頒發的獨特數字身份。DID 可以與一個加密密鑰相關聯,用于證明用戶擁有該 DID,如 EVM 錢包。但它也可以存儲一整套屬性,如用戶的年齡、國籍、社會安全號碼等。這些屬性可以用來證明你滿足某些條件(稱為“證明”),如超過18歲或不是納尼亞國公民。
大多數 DID 實現是在客戶端進行的,并使用零知識證明生成證明。雖然在許多情況下這已經足夠,但當涉及多個用戶參與交易、需要對 DID 應用復雜規則或需要為所有人維護一套共同的規則時,這個方案會變得復雜起來。這實際上是邊緣計算與云計算應用之間的取舍問題。
然而,擁有一個集中式的 DID 注冊中心可以解決這些問題,因為可以簡單地請求注冊中心來檢查每個人是否符合要求。它還可以簡化監管的追蹤,因為只需要在一個地方實現它。區塊鏈將是一個完美的基礎設施,因為它可以實現 DID 與需要合規性的應用之間的組合性,以及不同法規之間的組合性。
問題是:每個人都能看到每個人的身份!
幸運的是,有一個解決方案:
- 同態加密,具體來說是 fhEVM!
- 得益于對加密狀態的組合性能力,可以直接將用戶的 DID 以加密形式托管在鏈上,并讓合規應用通過簡單的合約調用來驗證屬性。
- 通過智能合約管理身份的能力,稱之為“身份抽象”,這類似于如何通過智能合約管理資金的賬戶抽象。
這個教程分為三部分:
- 1)身份抽象:是通過一個注冊合約來完成的,注冊合約負責管理身份和證明。在這里,假設 DID 是官方政府身份證明。注冊中心由一個中央機構(如 AFNIC)管理,后者可以創建注冊商(如 KYC 公司,如 Onfido、Jumio 等),然后這些注冊商可以創建用戶 DID。用戶通過他們的注冊商來管理和更新他們的 DID。
- 2)監管:在一個合約中定義,該合約編碼了一套基于用戶 DID 中信息的規則,用于管理個人之間的代幣轉移。它基本上在合約級別強制執行監管,而不是在用戶級別。
- 3)合規的機密轉賬:在一個合規的 ERC20 合約中實現,該合約使用監管合約來執行代幣轉移中的合規性,而不需要對 ERC20 API 本身進行任何更改。在這個示例中,使用了一個機密 ERC20 合約,其中余額和金額是隱藏的,但它同樣適用于常規的明文 ERC20 代幣。
Zama鏈上機密 DID 協議架構:
3. 身份注冊合約
IdentityRegistry
合約是一個用戶 DID 的注冊中心,這些 DID 是由注冊商頒發的,并包含一組加密的標識符,如用戶的國籍、年齡、社會安全號碼等。這些標識符作為加密的 32 位值(euint32)存儲。
該合約還處理權限管理,如下所示:
- 允許合約所有者(如 AFNIC)添加、移除或更新注冊商。
- 允許注冊商添加、移除或更新他們創建的用戶 DID。
- 允許用戶授權智能合約訪問他們 DID 的特定屬性。需要注意的是,用戶有責任不向惡意合約提供訪問權限,就像他們有責任不讓惡意合約花費他們的代幣一樣。
第一步,實現創建和管理 DID 的邏輯:
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity 0.8.19;import "fhevm/lib/TFHE.sol";contract IdentityRegistry is EIP712WithModifier, Ownable {// 從錢包到注冊商ID的映射mapping(address => uint) public registrars;// 從錢包到身份的映射mapping(address => Identity) internal identities;struct Identity {uint registrarId;mapping(string => euint32) identifiers;}mapping(address => mapping(address => mapping(string => bool))) permissions; // 用戶 => 合約 => 標識符[]event NewRegistrar(address wallet, uint registrarId);event NewDid(address wallet);event RemoveDid(address wallet);constructor() Ownable() EIP712WithModifier("Authorization token", "1") {_transferOwnership(msg.sender);}// 添加注冊商function addRegistrar(address wallet, uint registrarId) public onlyOwner {require(registrarId > 0, "registrarId needs to be > 0");registrars[wallet] = registrarId;emit NewRegistrar(wallet, registrarId);}function removeRegistrar(address wallet) public onlyOwner {delete registrars[wallet];}function getRegistrar(address wallet) public view returns (uint) {return identities[wallet].registrarId;}// 添加用戶 DIDfunction addDid(address wallet) public onlyRegistrar {require(identities[wallet].registrarId == 0, "This wallet is already registered");Identity storage newIdentity = identities[wallet];newIdentity.registrarId = registrars[msg.sender];emit NewDid(wallet);}function removeDid(address wallet) public onlyExistingWallet(wallet) onlyRegistrarOf(wallet) {require(identities[wallet].registrarId > 0, "This wallet isn't registered");delete identities[wallet];emit RemoveDid(wallet);}modifier onlyExistingWallet(address wallet) {require(identities[wallet].registrarId > 0, "This wallet isn't registered");_;}modifier onlyRegistrar() {require(registrars[msg.sender] > 0, "You're not a registrar");_;}modifier onlyRegistrarOf(address wallet) {uint registrarId = registrars[msg.sender];require(identities[wallet].registrarId == registrarId, "You're not managing this identity");_;}
}
其中:
onlyRegistrar
和onlyRegistrarOf
注冊商(Registrar):只有注冊商可以添加或刪除用戶的 DID。每個錢包都與一個注冊商 ID 相關聯,注冊商 ID 必須大于 0。- 身份管理:每個用戶都有一個關聯的
Identity
,其中包含該用戶的注冊商 ID 和一組加密的標識符(如年齡、國籍等)。 - 權限管理:用戶可以授權特定的智能合約訪問其 DID 的特定屬性。合約會驗證這些權限,確保只有經過授權的合約可以訪問用戶的身份信息。
- 合約功能:
addRegistrar
:允許合約所有者添加新的注冊商。removeRegistrar
:允許合約所有者移除注冊商。addDid
:允許注冊商為錢包添加 DID。removeDid
:允許注冊商移除錢包的 DID。getRegistrar
:查詢用戶 DID 所屬的注冊商 ID。
通過這種方式,可以在鏈上安全地管理和更新用戶的身份信息,同時保證隱私性和合規性。
現在,下一步是實現identifiers標識符和訪問控制的邏輯。
標識符只是一個字符串(如“出生日期”)和一個加密的 32 位值。它只能由注冊商創建或更新,用戶不能創建自己的標識符,因為希望這些標識符由注冊商進行認證。
然而,由于標識符是加密的,用戶需要授權某個合約訪問特定的值,將通過一個簡單的訪問控制機制來處理這一點,這類似于你如何允許合約花費你的 ERC20 代幣。
contract IdentityRegistry is EIP712WithModifier, Ownable {...mapping(address => mapping(address => mapping(string => bool))) permissions; // 用戶 => 合約 => 標識符[]// 設置用戶的標識符function setIdentifier(address wallet, string calldata identifier, bytes calldata encryptedValue) public {euint32 value = TFHE.asEuint32(encryptedValue);setIdentifier(wallet, identifier, value);}function setIdentifier(address wallet,string calldata identifier,euint32 value) internal onlyExistingWallet(wallet) onlyRegistrarOf(wallet) {identities[wallet].identifiers[identifier] = value;}// 用戶處理權限function grantAccess(address allowed, string[] calldata identifiers) public {for (uint i = 0; i < identifiers.length; i++) {permissions[msg.sender][allowed][identifiers[i]] = true;}}function revokeAccess(address allowed, string[] calldata identifiers) public {for (uint i = 0; i < identifiers.length; i++) {delete permissions[msg.sender][allowed][identifiers[i]];}}...
}
現在可以通過添加必要的 getter 來完成身份注冊合約,同時加入一些條件和錯誤處理。
contract IdentityRegistry is EIP712WithModifier, Ownable {...// 獲取加密的標識符function reencryptIdentifier(address wallet,string calldata identifier,bytes32 publicKey,bytes calldata signature) public view onlySignedPublicKey(publicKey, signature) returns (bytes memory) {euint32 ident = _getIdentifier(wallet, identifier);require(TFHE.isInitialized(ident), "This identifier is unknown");return TFHE.reencrypt(ident, publicKey, 0);}function getIdentifier(address wallet, string calldata identifier) public view returns (euint32) {return _getIdentifier(wallet, identifier);}function _getIdentifier(address wallet,string calldata identifier) internal view onlyExistingWallet(wallet) onlyAllowed(wallet, identifier) returns (euint32) {return identities[wallet].identifiers[identifier];}modifier onlyAllowed(address wallet, string memory identifier) {require(owner() == msg.sender || permissions[wallet][msg.sender][identifier],"User didn't give you permission to access this identifier.");_;}
}
其中:
setIdentifier
:允許注冊商為錢包設置新的標識符。標識符是通過加密的 32 位值進行存儲。grantAccess
和revokeAccess
:允許用戶授權或撤銷合約訪問其標識符的權限,權限是基于合約和標識符的。reencryptIdentifier
:允許用戶通過簽名證明他們的身份,并獲取加密的標識符。這是為了確保數據的安全傳輸和使用。getIdentifier
和_getIdentifier
:獲取標識符的加密值,只有授權的合約和用戶才能訪問。onlyAllowed
:訪問控制修飾符,確保只有授權的用戶和合約可以訪問特定的標識符。
通過這樣的設計,可以確保用戶的身份信息在鏈上安全存儲,同時保證合規性和隱私保護。
4. 規則合約
下一步是創建規則合約。
在為兩個個體之間的轉賬實現一組規則時,需要認識到這些規則可能會隨著時間的推移而變化。擁有一個單一的智能合約來定義給定上下文(如貨幣轉賬)的所有規則,意味著 ERC20 合約不必自己跟蹤規則。政府可以簡單地更新這個合約,并且它會自動傳播到所有實現了該合約的代幣。
從本質上講,規則合約只是一組與加密的身份屬性匹配的條件。為了避免濫用,用戶不會直接授予規則合約訪問權限,而是將權限授予 ERC20 代幣合約,然后它執行一個委托調用到規則合約。這樣做確保了只有用戶信任的 ERC20 合約才能訪問他們的信息。請記住,在進行轉賬之前,發送方和接收方都必須授予 ERC20 合約訪問權限。
在本例中將實現一些基本規則:
- 在同一國家內的轉賬沒有限制,但轉賬到其他國家的金額限制為 10,000 個代幣。
- 被列入黑名單的用戶不能進行轉賬或接收代幣。
- 用戶不能將代幣轉賬到被列入黑名單的國家。
與其讓交易失敗(這可能會暴露敏感信息),將簡單地將轉賬金額設置為 0,如果沒有滿足其中一個條件。這使用了一個同態三元操作符,稱為 cmux
:value = TFHE.cmux(encryptedCondition, valueIfTrue, valueIfFalse);
// SPDX-License-Identifier: BSD-3-Clause-Clearpragma solidity 0.8.19;import "fhevm/lib/TFHE.sol";
import "./IdentityRegistry.sol";interface ICompliantERC20 {function getIdentifier(address wallet, string calldata identifier) external view returns (euint32);
}contract ERC20Rules {string[] public identifiers;mapping(address => uint32) public whitelistedWallets;mapping(string => uint8) public countries;uint16[] public country2CountryRestrictions;constructor() {identifiers = ["country", "blacklist"];whitelistedWallets[address(0x133725C6461120439E85887C7639316CB27a2D9d)] = 1;whitelistedWallets[address(0x4CaCeF78615AFecEf7eF182CfbD243195Fc90a29)] = 2;countries["fr"] = 1;countries["us"] = 2;country2CountryRestrictions = [createRestriction(countries["us"], countries["fr"])];}function createRestriction(uint16 from, uint16 to) internal pure returns (uint16) {return (from << 8) + to;}function getIdentifiers() public view returns (string[] memory) {return identifiers;}function getC2CRestrictions() public view returns (uint16[] memory) {return country2CountryRestrictions;}function transfer(address from, address to, euint32 amount) public view returns (euint32) {ICompliantERC20 erc20 = ICompliantERC20(msg.sender);// 條件 1:不同國家之間的轉賬限額為 10,000 個代幣ebool transferLimitOK = checkLimitTransfer(erc20, from, to, amount);ebool condition = transferLimitOK;// 條件 2:檢查是否有黑名單用戶ebool blacklistOK = checkBlacklist(erc20, from, to);condition = TFHE.and(condition, blacklistOK);// 條件 3:檢查國家間轉賬規則ebool c2cOK = checkCountryToCountry(erc20, from, to);condition = TFHE.and(condition, c2cOK);return TFHE.cmux(condition, amount, TFHE.asEuint32(0));}function checkLimitTransfer(ICompliantERC20 erc20,address from,address to,euint32 amount) internal view returns (ebool) {euint8 fromCountry = TFHE.asEuint8(erc20.getIdentifier(from, "country"));euint8 toCountry = TFHE.asEuint8(erc20.getIdentifier(to, "country"));require(TFHE.isInitialized(fromCountry) && TFHE.isInitialized(toCountry), "You don't have access");ebool sameCountry = TFHE.eq(fromCountry, toCountry);ebool amountBelow10k = TFHE.le(amount, 10000);return TFHE.or(sameCountry, amountBelow10k);}function checkBlacklist(ICompliantERC20 erc20, address from, address to) internal view returns (ebool) {ebool fromBlacklisted = TFHE.asEbool(erc20.getIdentifier(from, "blacklist"));ebool toBlacklisted = TFHE.asEbool(erc20.getIdentifier(to, "blacklist"));return TFHE.not(TFHE.and(toBlacklisted, fromBlacklisted));}function checkCountryToCountry(ICompliantERC20 erc20, address from, address to) internal view returns (ebool) {// 禁止從國家 2 轉賬到國家 1uint16[] memory c2cRestrictions = getC2CRestrictions();euint32 fromCountry = erc20.getIdentifier(from, "country");euint32 toCountry = erc20.getIdentifier(to, "country");require(TFHE.isInitialized(fromCountry) && TFHE.isInitialized(toCountry), "You don't have access");euint16 countryToCountry = TFHE.shl(TFHE.asEuint16(fromCountry), 8) + TFHE.asEuint16(toCountry);ebool condition = TFHE.asEbool(true);// 檢查所有國家間的轉賬限制for (uint i = 0; i < c2cRestrictions.length; i++) {condition = TFHE.and(condition, TFHE.ne(countryToCountry, c2cRestrictions[i]));}return condition;}
}
其中:
createRestriction
:創建一個國家到國家的轉賬限制,兩個國家通過一個 16 位的編碼表示。getIdentifiers
:返回所有的標識符數組。getC2CRestrictions
:返回所有的國家間轉賬限制。transfer
:檢查轉賬是否滿足所有條件(如限額、黑名單、國家間轉賬規則),如果任何條件不滿足,則將轉賬金額設置為 0。checkLimitTransfer
:檢查轉賬是否超出了不同國家之間的限額。checkBlacklist
:檢查發送方和接收方是否在黑名單中。checkCountryToCountry
:檢查兩個國家之間是否允許轉賬。
這種設計可以確保在轉賬過程中遵循規則和合規性要求,同時保障用戶的隱私和安全。
5. 合規的機密 ERC20 合約
現在有了身份注冊合約和規則合約,接下來創建合規、隱私保護的代幣合約。這個合約將被稱為 CompliantERC20,并具有以下關鍵特性:
- 用戶余額和轉賬金額是加密的。
- 轉賬時通過調用規則合約來強制執行合規性。
- 某些余額的可見性可以授予白名單地址(如監管機構)。
// SPDX-License-Identifier: BSD-3-Clause-Clearpragma solidity 0.8.19;import "fhevm/abstracts/EIP712WithModifier.sol";
import "./ERC20Rules.sol";
import "./IdentityRegistry.sol";abstract contract AbstractCompliantERC20 is EIP712WithModifier {IdentityRegistry identityContract;ERC20Rules rulesContract;mapping(address => euint32) internal balances;constructor(address _identityAddr, address _rulesAddr) EIP712WithModifier("Authorization token", "1") {identityContract = IdentityRegistry(_identityAddr);rulesContract = ERC20Rules(_rulesAddr);}function identifiers() public view returns (string[] memory) {return rulesContract.getIdentifiers();}function getIdentifier(address wallet, string calldata identifier) external view returns (euint32) {require(msg.sender == address(rulesContract), "Access restricted to the current ERC20Rules");return identityContract.getIdentifier(wallet, identifier);}function balanceOf(address wallet,bytes32 publicKey,bytes calldata signature) public view onlySignedPublicKey(publicKey, signature) returns (bytes memory) {// 用戶可以查看自己的余額if (wallet == msg.sender) {return TFHE.reencrypt(balances[msg.sender], publicKey, 0);}// 國家可以查看所有公民的余額uint32 userCountry = rulesContract.whitelistedWallets(msg.sender);require(userCountry > 0, "You're not registered as a whitelisted wallet");euint32 walletCountry = identityContract.getIdentifier(wallet, "country");ebool sameCountry = TFHE.eq(walletCountry, userCountry);euint32 balance = TFHE.isInitialized(balances[wallet]) ? balances[wallet] : TFHE.asEuint32(0);balance = TFHE.cmux(sameCountry, balance, TFHE.asEuint32(0));return TFHE.reencrypt(balance, publicKey, 0);}// 轉賬加密金額function _transfer(address from, address to, euint32 _amount) internal {// 條件 1:資金充足ebool enoughFund = TFHE.le(_amount, balances[from]);euint32 amount = TFHE.cmux(enoughFund, _amount, TFHE.asEuint32(0));amount = rulesContract.transfer(from, to, amount);balances[to] = balances[to] + amount;balances[from] = balances[from] - amount;}
}
其中:
balanceOf
:此方法根據用戶的公鑰和簽名返回加密后的余額。用戶只能查看自己的余額,如果是同一國家的用戶,國家可以查看所有公民的余額。_transfer
:在轉賬過程中,通過規則合約來強制執行合規性,確保資金充足后,轉賬金額經過合規性檢查,然后將金額從發送方轉賬到接收方。
在這個合約中,規則合約是通過簡單的調用來觸發的。這意味著用戶必須在發起任何轉賬之前提供對 ERC20 合約的訪問權限;否則,轉賬將被回滾。
最終,創建的 ERC20 合約:
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity 0.8.19;import "fhevm/lib/TFHE.sol";
import "./AbstractCompliantERC20.sol";contract CompliantERC20 is AbstractCompliantERC20 {...// 從消息發送者地址轉賬加密金額到 `to` 地址function transfer(address to, bytes calldata encryptedAmount) public {transfer(to, TFHE.asEuint32(encryptedAmount));}// 從消息發送者地址轉賬金額到 `to` 地址function transfer(address to, euint32 amount) public {_transfer(msg.sender, to, amount);}
}
與用戶授權 DeFi 協議花費他們的代幣類似,用戶需要授權合約訪問規則合約所需的標識符。這是通過調用 Identity.grantAccess(contractAddress, identifiers)
來實現的,標識符可以通過調用 ERC20.identifiers()
view方法來獲取。這個列表直接來自 ERC20Rules
合約,用于允許屬性的更新。
6. 合規性與隱私可以共存!
如果擁有正確的工具,構建合規性并不是一件困難的事情。雖然Zama最初構建了 fhEVM 以在區塊鏈中實現隱私保護,但很快意識到,這項技術可以用于身份管理,從而實現可編程的合規性。
參考資料
[1] Zama團隊2023年11月23日博客 Programmable Privacy and Onchain Compliance using Homomorphic Encryption