实现原理
利用零知识证明技术,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
双花。anchor
Merkle
树的根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
树需要更新的节点同步更新到合约中。
- 用匿名输入的
nf
添加到nullifier
中,标记该note
已被花费。nullifiers[nf] = nf;
- 调用
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 over 2 years ago