实现原理

利用零知识证明技术,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代币从合约地址转到用户的公开地址,如果是第二种交易方式,还会添加匿名输出的承诺。

实现原理

匿名账户体系

匿名账户采用与公开账户不同的密钥体系,如下图所示。

924

Key

每个密钥的用途如下:

  • sk(Spending Key): 用户随机生成的32字节比特串,是最核心的密钥,其他所有的可以都是由该密钥推导而来;
  • ask: 由sk0做BLAKE2b哈希得到,主要用于生成匿名输入的鉴权签名(Spend Authority Signature)算法的签名私钥;
  • ak: 由ask与椭圆曲线某基点做标量乘得到,主要用于生成匿名输入的鉴权签名的验签公钥;
  • nsk: 由sk1做BLAKE2b哈希得到,主要用于生成nk
  • nk: 由nsk与椭圆曲线某基点做标量乘得到,主要用于生成nullifier(防止双花);
  • ivk: 由aknk做BLAKE2s哈希得到,主要用于交易接收方查看接收到的匿名交易;
  • ovk: 由sk2做BLAKE2b哈希得到,主要用于交易发送方查看发送的匿名交易;
  • d(Diversifier): 用户选择的11字节的随机数, 是地址的一部分,d的存在主要是用于生成不同的地址,一定程度上打破交易的关联性;
  • pk_d: 是地址的一部分,d先做DiversifyHash(即将d哈希到椭圆曲线的点上)生成g_d, g_divk做标量乘得到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, nfnote的在Merkle树中的位置和note_commitment有关,目的是为了防止note双花。
  • anchor Merkle树的根
  • value_commitmentnote金额的承诺
  • rk 验证note的鉴权签名(Spend Authority Signature)的公钥

通过验证proof,用户可以花费某个note, 但是其他人无法知道用户花费的是Merkle树上的哪个note,也就无法知道用户的转账金额和地址,保证了交易发送方的隐私性和匿名性。

链上除了验证proof之外,针对每一个匿名输入,还需要提供鉴权签名,在链上做验证。

用户在进行转账时,对于每一个匿名输出,同样也都需要提供零知识证明的proof,证明用户知道交易的金额和接收者的地址。在链上验证proof时,需要提供的公开输入有:

  • note_commitmentnote的承诺
  • value_commitmentnote金额的承诺
  • epk 临时公钥,用于解密note

通过验证proof,证明交易接收方的地址和金额,除了交易双方之外,别人无法知道交易接收方以及转账金额,保证了交易接收方的隐私性和匿名性。

对于每一个匿名输出,还要提供额外的密文字段C_encC_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交易是将一定金额的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},密文字段。

在匿名合约里执行以下几步:

  1. 将指定金额的TRC20代币从用户地址转到匿名合约地址。

    bool transferResult = trc20Token.transferFrom(sender, address(this), rawValue);
    require(transferResult, "TransferFrom failed!");
    
  2. 验证零知识证明和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签名的消息哈希。

  3. verifyMintProof返回的Merkle树的根以及Merkle树需要更新的节点同步更新到合约中。

    mapping(bytes32 => bytes32) public roots;
    roots[latestRoot] = latestRoot;
    

    roots存储Merkle树所有的历史根。

    除此之外,将Merkle树需要更新的节点更新到tree中, tree存储完整的Merkle树。

    mapping(uint256 => bytes32) public tree;
    
  4. note_commitment, value_commitment, epk, c以及新添加的叶子节点的位置添加到交易log里。

    emit NewLeaf(leafCount - 1, output[0], output[1], output[2], c);
    

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.

在匿名合约里执行以下几步:

  1. 约束匿名输入和匿名输出的个数。为了验证零知识证明的效率,这里约束匿名输入和匿名输出的个多最多不超过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!");
    
  2. 双花及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树的历史根里。
  3. 验证零知识证明、匿名输入的鉴权签名以及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!");
    
  4. verifyTransferProof返回的Merkle树的根以及Merkle树需要更新的节点同步更新到合约中。
  5. 将每个匿名输入的nf添加到nullifier中,表明该note已被花费。
    for (uint256 i = 0; i < input.length; i++) {
        bytes32 nf = input[i][0];
        nullifiers[nf] = nf;
    }
    
  6. 将每个匿名输出的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交易实现一个匿名输入转账给一个公开地址,或者一个匿名输入转到一个公开地址和匿名地址。交易验证成功,利用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.

函数的执行过程如下:

  1. 验证nfanchor,判断匿名输入是否双花,以及anchor是否是Merkle树历史根。

    require(nullifiers[nf] == 0, "The note has already been spent!");
    require(roots[anchor] != 0, "The anchor must exist!");
    
  2. 根据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树需要更新的节点同步更新到合约中。

  1. 用匿名输入的nf添加到nullifier中,标记该note已被花费。
    nullifiers[nf] = nf;
    
  2. 调用TRC20合约的transfer函数执行转账,将指定金额的TRC20代币从匿名合约地址转到用户指定的公开地址。
    bool transferResult = trc20Token.transfer(payTo, rawValue);
    require(transferResult, "Transfer failed!");
    

Merkle路径

在构造匿名输入时,note的隐私信息包括note_commitmentMerkle路径以及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