实现原理
利用零知识证明技术,TRONZ实现了针对波场TRC20代币的匿名交易,是为数不多的实现了基于账户模型的隐私交易方案。目前该方案仅支持TRC20代币,根据TRONZ团队目前的规划,有望在2020年下半年实现针对TRC10代币的匿名交易。
本文主要面向智能合约开发者,帮助开发者理解TRC20代币匿名交易的设计思路和实现原理。
背景
目前区块链的隐私交易方案大多数是基于UTXO模型实现的,利用的是零知识证明和环签名等技术,如Zcash采用zk-SNARKs技术, Monero采用环签名和Bulletproof技术等,而基于账户模型的隐私交易方案非常少,究其原因主要是因为账户模型下用户账户的金额是动态变化的,针对账户余额产生的零知识证明具有时效性,整个隐私交易方案实现起来很困难。
2019年Benedikt Bünz等提出了针对账户模型的隐私交易方案——Zether协议[1]。Zether协议利用一种新的零知识证明机制Σ-Bullets, 可以实现交易金额和地址隐藏。该技术还不完善,在以太坊上部署测试过,gas消耗太大,成本太高,而且一笔交易必须得一个epoch之内完成,否则会失败,这就导致了遇到网络繁忙时,交易一直无法打包上链,导致交易的失败。
为了保护用户进行TRC20代币交易的隐私,TRONZ技术团队利用零知识证明技术实现了TRC20代币的匿名交易,保护交易双方金额和地址的隐私性。我们提供了TRC20代币的匿名交易标准实现方案[2],该技术方案完全兼容标准的TRC20代币,可以隐藏交易双方的金额和地址。
设计思路
TRC20代币的匿名交易总体思路是通过部署一个智能合约,用户将TRC20代币转给智能合约,通过智能合约来执行匿名交易,这样可以可以利用现有的基于UTXO模型的隐私交易方案来实现基于账户模型的隐私交易。
我们的匿名交易方案需要两套账户体系,一个是公开账户,另一个是匿名账户。公开账户直接使用TRON的账户,匿名账户借鉴Zcash Sapling的账户体系。
我们设计了三种匿名转账的模式,分别是MINT, TRANSFER,BURN.
MINT交易是将TRC20代币从公开账户地址转到一个匿名账户地址,具体来说将TRC20代币从用户地址转到合约地址,对应地在智能合约里增加这笔匿名输出的承诺(commitment).TRANSFER交易支持最多2个匿名输入转到最多2个匿名输出(本质上可以支持多对多的交易,在实现上做了限制),在智能合约里验证匿名输入和输出的有效性之后,添加匿名输出的承诺。BURN交易提供两种交易方式,一种是将一个匿名输入转出到一个公开账户地址,另一种是将一个匿名输入转出到一个公开地址账户和一个匿名输出。在智能合约里验证匿名输入(和匿名输出)的有效性之后,将一定金额的TRC20代币从合约地址转到用户的公开地址,如果是第二种交易方式,还会添加匿名输出的承诺。
实现原理
匿名账户体系
匿名账户采用与公开账户不同的密钥体系,如下图所示。

Key
每个密钥的用途如下:
sk(Spending Key): 用户随机生成的32字节比特串,是最核心的密钥,其他所有的可以都是由该密钥推导而来;ask: 由sk与0做BLAKE2b哈希得到,主要用于生成匿名输入的鉴权签名(Spend Authority Signature)算法的签名私钥;ak: 由ask与椭圆曲线某基点做标量乘得到,主要用于生成匿名输入的鉴权签名的验签公钥;nsk: 由sk与1做BLAKE2b哈希得到,主要用于生成nk;nk: 由nsk与椭圆曲线某基点做标量乘得到,主要用于生成nullifier(防止双花);ivk: 由ak与nk做BLAKE2s哈希得到,主要用于交易接收方查看接收到的匿名交易;ovk: 由sk与2做BLAKE2b哈希得到,主要用于交易发送方查看发送的匿名交易;d(Diversifier): 用户选择的11字节的随机数, 是地址的一部分,d的存在主要是用于生成不同的地址,一定程度上打破交易的关联性;pk_d: 是地址的一部分,d先做DiversifyHash(即将d哈希到椭圆曲线的点上)生成g_d,g_d与ivk做标量乘得到pk_d.(d, pk_d)组成匿名地址。
匿名交易原理
每个匿名输出是一个note, note = (d, pk_d, value, rcm). (d, pk_d)是交易地址,value为交易金额,rcm为随机数,取值范围在Jubjub椭圆曲线标量域中,即rcm < 0xe7db4ea6533afa906673b0101343b00a6682093ccc81082d0970e5ed6f72cb7。链上提供getrcm接口可以生成随机的rcm. 为了保证交易的匿名性和隐私性,note并不上链,上链的是note的承诺,称为note_commitment. 每一笔匿名交易验证成功之后,将note_commitment存在Merkle树的叶子节点。同样地,每一个匿名输入也是一个note.
用户在花费一个note的时候需要提供零知识证明的proof,证明用户知道要花费note的隐私信息。在链上验证proof时,还需要提供公开输入。
nf每一个note对应唯一的nf,nf与note的在Merkle树中的位置和note_commitment有关,目的是为了防止note双花。anchorMerkle树的根value_commitment对note金额的承诺rk验证note的鉴权签名(Spend Authority Signature)的公钥
通过验证proof,用户可以花费某个note, 但是其他人无法知道用户花费的是Merkle树上的哪个note,也就无法知道用户的转账金额和地址,保证了交易发送方的隐私性和匿名性。
链上除了验证proof之外,针对每一个匿名输入,还需要提供鉴权签名,在链上做验证。
用户在进行转账时,对于每一个匿名输出,同样也都需要提供零知识证明的proof,证明用户知道交易的金额和接收者的地址。在链上验证proof时,需要提供的公开输入有:
note_commitment对note的承诺value_commitment对note金额的承诺epk临时公钥,用于解密note
通过验证proof,证明交易接收方的地址和金额,除了交易双方之外,别人无法知道交易接收方以及转账金额,保证了交易接收方的隐私性和匿名性。
对于每一个匿名输出,还要提供额外的密文字段C_enc和C_out,使发送方和接收方都能解密得到note的信息。
除此之外,在验证每一笔交易时,还需要验证Binding签名。通过验证Binding签名,确保交易双方金额的平衡。
关于协议的具体内容,详见TRONZ匿名协议[3]
匿名交易实现
TRC20代币的匿名交易是通过智能合约(以下称其为匿名合约)实现的。
在部署匿名合约时,绑定TRC20合约的地址,使匿名合约只针对该TRC20代币实现匿名交易。
constructor (address trc20ContractAddress, uint256 scalingFactorExponent) public {
require(scalingFactorExponent < 77, "The scalingFactorLogarithm is out of range!");
scalingFactor = 10 ** scalingFactorExponent;
owner = msg.sender;
trc20Token = TokenTRC20(trc20ContractAddress);
}
除了TRC20合约地址之外,还需要设置scalingFactorExponent字段,在合约里设置scalingFactor字段。scalingFactor字段的设置主要是为了支持高精度(Decimals)的TRC20代币。在匿名合约里约束,用户在进行转账时,转账金额必须是scalingFactor的倍数。
匿名合约中frontier变量存储Merkle树,leafCount为当前Merkle树的叶子节点个数。
bytes32[33] frontier;
uint256 public leafCount;
MINT交易
MINT交易MINT交易是将一定金额的TRC20代币转到匿名合约地址,同时将匿名输出的note_commitment添加到匿名合约Merkle树的叶子节点。
由于执行MINT交易是将TRC20代币从用户账户转到匿名合约账户,所以在执行MINT之前,需要首先调用TRC20合约的approve(address _spender, uint256 _value) 函数允许匿名合约从用户账户转移一定金额的TRC20代币到自己账户。_spender是匿名合约地址,_value是需要转账的金额。
function mint(uint256 rawValue, bytes32[9] calldata output, bytes32[2] calldata bindingSignature, bytes32[21] calldata c) external {}
通过触发匿名合约mint函数来执行MINT交易。函数的参数包括:
rawValue: 转账金额output:{note_commitment||value_commitment||epk||proof}bindingSignature: 交易的Binding签名,用来验证交易输入和输出的金额平衡c:{C_enc||C_out},密文字段。
在匿名合约里执行以下几步:
-
将指定金额的
TRC20代币从用户地址转到匿名合约地址。bool transferResult = trc20Token.transferFrom(sender, address(this), rawValue); require(transferResult, "TransferFrom failed!"); -
验证零知识证明和Binding签名,如果验证成功,则更新
Merkle树,将note_commitment添加到叶子节点。这一步是在verifyMintProof预编译合约实现的,这是专为零知识证明添加的。verifyMintProof返回Merkle树的最新根,以及Merkle树需要更新的节点。bytes32 signHash = sha256(abi.encodePacked(address(this), value, output, c)); (bytes32[] memory ret) = verifyMintProof(output, bindingSignature, value, signHash, frontier, leafCount); uint256 result = uint256(ret[0]); require(result == 1, "The proof and signature have not been verified by the contract!");signHash为Binding签名的消息哈希。 -
将
verifyMintProof返回的Merkle树的根以及Merkle树需要更新的节点同步更新到合约中。mapping(bytes32 => bytes32) public roots; roots[latestRoot] = latestRoot;roots存储Merkle树所有的历史根。除此之外,将
Merkle树需要更新的节点更新到tree中,tree存储完整的Merkle树。mapping(uint256 => bytes32) public tree; -
将
note_commitment,value_commitment,epk,c以及新添加的叶子节点的位置添加到交易log里。emit NewLeaf(leafCount - 1, output[0], output[1], output[2], c);
TRANSFER交易
TRANSFER交易TRANSFER交易实现多个匿名输入转账给多个匿名输出,交易验证成功,将匿名输出的note_commitment添加到匿名合约Merkle树的叶子节点。
通过触发匿名合约transfer函数来执行TRANSFER交易。
function transfer(bytes32[10][] calldata input, bytes32[2][] calldata spendAuthoritySignature, bytes32[9][] calldata output, bytes32[2] calldata bindingSignature, bytes32[21][] calldata c) external {}
函数的参数包括:
input:{nf||anchor||value_commitment||rk||proof}, 变长数组,支持多个匿名输入。spendAuthoritySignature: 匿名输入的鉴权签名,每一个匿名输入对应一个鉴权签名。output:{note_commitment||value_commitment||epk||proof},每一个匿名输出对应一个output.bindingSignature: 交易的Binding签名,用来验证交易输入和输出的金额平衡。c:{C_enc||C_out},密文字段,每一个匿名输出对应一个c.
在匿名合约里执行以下几步:
- 约束匿名输入和匿名输出的个数。为了验证零知识证明的效率,这里约束匿名输入和匿名输出的个多最多不超过2个。
require(input.length >= 1 && input.length <= 2, "Input number must be 1 or 2!"); require(input.length == spendAuthoritySignature.length, "Input number must be equal to spendAuthoritySignature number!"); require(output.length >= 1 && output.length <= 2, "Output number must be 1 or 2!"); require(output.length == c.length, "Output number must be equal to c number!"); - 双花及
Merkle树根的有效性验证。
对于每一个匿名输入,验证for (uint256 i = 0; i < input.length; i++) { require(nullifiers[input[i][0]] == 0, "The note has already been spent!"); require(roots[input[i][1]] != 0, "The anchor must exist!"); }nf是否在nullifiers中,如果不在,则验证通过,说明该note没有被花费。除此之外,还需要验证anchor是否在Merkle树的历史根里。 - 验证零知识证明、匿名输入的鉴权签名以及Binding签名,如果验证成功,则更新
Merkle树,将note_commitment添加到叶子节点。这一步是在verifyTransferProof预编译合约实现的,这也是专为零知识证明添加的。verifyTransferProof返回Merkle树的最新根,以及Merkle树需要更新的节点。bytes32 signHash = sha256(abi.encodePacked(address(this), input, output, c)); (bytes32[] memory ret) = verifyTransferProof(input, spendAuthoritySignature, output, bindingSignature, signHash, frontier, leafCount); uint256 result = uint256(ret[0]); require(result == 1, "The proof and signature have not been verified by the contract!"); - 将
verifyTransferProof返回的Merkle树的根以及Merkle树需要更新的节点同步更新到合约中。 - 将每个匿名输入的
nf添加到nullifier中,表明该note已被花费。for (uint256 i = 0; i < input.length; i++) { bytes32 nf = input[i][0]; nullifiers[nf] = nf; } - 将每个匿名输出的
note_commitment,value_commitment,epk,c以及新添加的叶子节点的位置添加到交易log里。for (uint256 i = 0; i < output.length; i++) { emit NewLeaf(leafCount - (output.length - i), output[i][0], output[i][1], output[i][2], c[i]); }
BURN交易
BURN交易BURN交易实现一个匿名输入转账给一个公开地址,或者一个匿名输入转到一个公开地址和匿名地址。交易验证成功,利用TRC20合约的transfer函数,将TRC20代币从匿名合约账户转到用户提供的公开地址,如果是第二种交易,则除了将TRC20代币从匿名合约账户转到用户提供的公开地址之外,还会将匿名输出的note_commitment添加到匿名合约Merkle树的叶子节点。
通过触发匿名合约burn函数来执行BURN交易。
function burn(bytes32[10] calldata input, bytes32[2] calldata spendAuthoritySignature, uint256 rawValue, bytes32[2] calldata bindingSignature, address payTo, bytes32[3] calldata burnCipher, bytes32[9][] calldata output, bytes32[21][] calldata c) external {}
函数的参数包括:
input:{nf||anchor||value_commitment||rk||proof}spendAuthoritySignature: 匿名输入的鉴权签名。rawValue: 转账金额。bindingSignature: 交易的Binding签名,用来验证交易输入和输出的金额平衡。payTo: 交易接收方的公开地址。burnCipher: 对接收方地址和转账金额的加密,加密密钥为发送方的ovk,该参数主要用来交易发送方追踪自己的交易记录。output:{note_commitment||value_commitment||epk||proof}c:{C_enc||C_out},密文字段,每一个匿名输出对应一个c.
函数的执行过程如下:
-
验证
nf及anchor,判断匿名输入是否双花,以及anchor是否是Merkle树历史根。require(nullifiers[nf] == 0, "The note has already been spent!"); require(roots[anchor] != 0, "The anchor must exist!"); -
根据
output的长度判断是哪一种BURN交易。如果是第一种交易,即一个匿名转入转到一个公开地址,则执行第2.1步;如果是第二种交易,即一个匿名转入转到一个公开地址和一个匿名地址,则执行第2.2步。
2.1 针对第一种BURN交易验证匿名输入的零知识证明、匿名输入的鉴权签名以及Binding签名。这一步是在verifyBurnProof预编译合约实现的,这也是专为零知识证明添加的。``` bytes32 signHash = sha256(abi.encodePacked(address(this), input, output, c, payTo, value)); (bool result) = verifyBurnProof(input, spendAuthoritySignature, value, bindingSignature, signHash); require(result, "The proof and signature have not been verified by the contract!"); ```
2.2 针对第二种BURN交易验证匿名输入和输出的零知识证明、匿名输入的鉴权签名以及Binding签名。这一步是在verifyTransferProof预编译合约实现的。
bytes32 signHash = sha256(abi.encodePacked(address(this), input, output, c, payTo, value));
(bytes32[] memory ret) = verifyTransferProof(inputs, spendAuthoritySignatures, output, bindingSignature, signHash, value, frontier, leafCount);
uint256 result = uint256(ret[0]);
require(result == 1, "The proof and signature have not been verified by the contract!");
将verifyTransferProof返回的Merkle树的根以及Merkle树需要更新的节点同步更新到合约中。
3. 用匿名输入的nf添加到nullifier中,标记该note已被花费。
nullifiers[nf] = nf;
4. 调用TRC20合约的transfer函数执行转账,将指定金额的TRC20代币从匿名合约地址转到用户指定的公开地址。
bool transferResult = trc20Token.transfer(payTo, rawValue); require(transferResult, "Transfer failed!");
Merkle路径
在构造匿名输入时,note的隐私信息包括note_commitment的Merkle路径以及Merkle树的根。为了帮助用户方便构造零知识证明,匿名合约提供getPath方法,计算指定位置的叶子节点的Merkle路径。
function getPath(uint256 position) public view returns (bytes32, bytes32[32] memory) {}
getPath方法输入叶子节点的位置,返回Merkle树的根以及Merkle路径。
参考文献
[1] Zether协议 https://crypto.stanford.edu/~buenz/papers/zether.pdf
[2] TIP135 https://github.com/tronprotocol/tips/blob/master/tip-135.md
[3] TRONZ匿名协议 https://www.tronz.io/Shielded%20Transaction%20Protocol.pdf
Updated 9 months ago