Implementation Details

Powered by zk-SNARKs, TRONZ achieved TRON-based TRC20 token shielded transaction, which is one of the few account model-based shield transaction solutions.

This article is mainly intended to help smart contract developers understand the design and implementation of TRC20 token shielded transactions.

Background

Currently, most shielded transaction solutions in the blockchain industry are built with UTXO model using technologies like zk-SNARKs and ring signature. For instance, Zcash adopts zk-SNARKs technology while Monero uses ring signature and Bulletproof. Few, however, adopts account model-based shielded transaction scheme because within the account model user's funds are fluid, and zk-SNARKs generated based on the account balance is only valid for a limited period, thus making it extremely difficult to implement a shielded transaction scheme.

In 2019, Benedikt Bünz and some others proposed a shielded transaction scheme for the account-based system - Zether Protocol[1]. Adopting a new zk-SNARKs mechanism Σ-Bullets, Zether Protocol is able to hide the transfer amount and addresses. This technology was deployed and tested on Ethereum and was proved to be flawed as it consumed too much gas. Even worse, every transaction has to be completed within one epoch or otherwise would fail. So in cases when the network is busy, transactions may often fail as they can not be packed or recorded on-chain.

In a bid to safeguard users' privacy in TRC20 token transactions, the TRONZ team adopts zk-SNARKs to implement the TRC20 token shielded transaction, protecting the confidentiality of both the amount and addresses of each transaction. Here we provide the standard implementation scheme of the shielded transaction for TRC20 tokens[2], which is fully compatible with standard TRC20 tokens and is able to hide both the amount and addresses of each transaction.

Design

To achieve TRC20 token shielded transaction, a smart contract is deployed to receive users' TRC20 tokens and execute the shielded transaction. In this way, the current UTXO-based shielded transaction scheme can be used to implement the shielded transactions based on the account model.

Our shielded transaction scheme employs two types of accounts: the public account and the shielded account. Public accounts are simply TRON accounts. Shielded accounts are similar to the account system of Zcash Salping.

We designed three shielded transaction modes: MINT, TRANSFER and BURN.

  • MINT refers to the transfer of TRC20 tokens from public addresses to a shielded address. To be more specific, TRC20 tokens are transferred from the user's address to the contract address and a commitment to this shielded output is added to the smart contract.
  • TRANSFER supports transfers from up to 2 shielded inputs to no more than 2 shielded outputs (By nature it is many-to-many transfer. Here we add a limit at the implementation level). After the validity of the shielded inputs and outputs is confirmed in the smart contract, a commitment to such shielded output will be added.
  • BURN supports two scenarios, the first scenario is to transfer from a shielded input to a public address. The other scenario is to transfer from a shielded input to a public address and a shielded output. After the validity of shielded input and shielded output is confirmed in the smart contract, a certain amount of TRC20 token will be transferred from the contract address to the user's public address. For the second scenario, it will also add the commitment of shielded output.

Implementation

Shielded Account System

Shielded accounts employ a different key system from the public accounts, as is shown below.

924

Here is the usage for each key:

  • sk(Spending Key): the 32-byte bit string randomly generated by the user. It is the core key from which all other keys derive;
  • ask: the BLAKE2b hash calculated from sk and 0. It is used to generate the key for signing the shielded input using Spend Authority Signature algorithm;
  • ak: the value returned by multiplying a coordinate on the elliptic curve by ask (scalar). It is used to generate the public key for verifying the shielded input using the Spend Authority Signature algorithm;
  • nsk: the BLAKE2b hash calculated from sk and 1. It is used to generate nk;
  • nk: generated by the scalar multiplication of nsk and a coordinate on the elliptic curve. It is used to generate nullifier(prevent double-spending);
  • ivk: generated by ak and nk performing a BLAKE2s hash. It's mostly used by the recipient to view the shielded transactions he/she receives;
  • ovk: generated by sk and 2 performing a BLAKE2b hash. It's mostly used by the sender to view the shielded transactions.
  • d (Diversifier): the 11-byte random number selected by the user. It is a part of the address and is mainly used for generating different addresses to break the relation between addresses and transactions;
  • pk_d: it is a part of the address. d will perform a DiversifyHash (namely, hash d to the coordinate of the elliptic curve) first to generate g_d. The scalar multiplication of g_d and ivk produces pk_d, and (d, pk_d) constitutes the shielded address.

Theory Behind Shielded Transaction

Every anonymous output is a note. Note = (d, pk_d, value, rcm). (d, pk_d)is the transaction address, value is the transaction amount, and rcm is the random number that falls in the scalar range of the Jubjub elliptic curve, namely rcm < 0xe7db4ea6533afa906673b0101343b00a6682093ccc81082d0970e5ed6f72cb7. The getrcm interface provided by the chain can randomly generate rcm. To ensure the anonymity and privacy of the transaction, the note is not on chain. It is the commitment of the note that is on the chain, which is called note_commitment. After each shielded transaction is successfully verified, the note_commitment will be stored at the leaf node of the Merkle tree. Similarly, every anonymous input is also a note.

When spending a note, the user needs to provide zero-knowledge proof to prove that the user knows the private information of the note being spent. When verifying the proof on the chain, public input is required.

  • nf: every note matches a unique nf, the positions of nf and note on the Merkle tree is related to note_commitment , it is used to prevent the double spending of note.
  • anchor: the root of the Merkle tree.
  • value_commitment: the commitment to the amount of the note.
  • rk: the public key that verifies the Spend Authority Signature of the note

Users can spend a certain note by verifying the proof, but others have no way to know which note on the Merkle tree is being spent, which means they won't be able to know the exact amount and the address of the transaction. The privacy and anonymity of the sender can thus be protected.

In addition to verifying the proof, it is also required to provide Spend Authority Signature to complete an on-chain verification for each anonymous input.

When performing a transaction, every shielded output also needs zero-knowledge proof to ensure that the user knows the amount of the transaction and the address o the recipient. When verifying a proof, following public inputs are required:

  • note_commitment: the commitment to note
  • value_commitment: the commitment to note amount
  • epk: the temporary public key for deciphering note

Verifying proof confirms the recipient‘s address and amount of the transaction, which are known to no one except the sender and the receiver. Thus, the privacy and anonymity of the receiver are guaranteed.

Every shielded output requires extra ciphertext fields C_enc and C_out, so that the sender and the receiver can decipher information from the note.

Besides, verification of a transaction requires verifying Binding signature, which ensures the balance of the transaction amount for the sender and the recipient.

For details of the protocol, please refer to TRONZ Privacy Protocol [3]

Implementation of Shielded Transactions

TRC20 token shielded transaction is implemented through smart contracts (hereafter referred to as shielded contract).

In deploying the shielded contract, the TRC20 contract address is binded so that the privacy protocol is applied only to the shielded transaction of the TRC20 tokens.

constructor (address trc20ContractAddress, uint256 scalingFactorExponent) public {
    require(scalingFactorExponent < 77, "The scalingFactorLogarithm is out of range!");
    scalingFactor = 10 ** scalingFactorExponent;
    owner = msg.sender;
    trc20Token = TokenTRC20(trc20ContractAddress);
}

Apart from TRC20 contract address, scalingFactorExponent needs to be set up in the contract, mainly for supporting TRC20 tokens with higher precision (Decimals). The shielded contract requires that the transfer amount must be a multiple of scalingFactor.

The variable frontier stores the Merkle tree, and leafCount represents the number of nodes on the current Merkle tree.

bytes32[33] frontier;
uint256 public leafCount;

MINT Transaction

MINT transaction transfers a certain amount of TRC20 tokens to a shielded contract address, and adds the shielded output note_commitment to a leaf node on the shielded contract Merkle tree.

MINT transaction transfers TRC20 tokens from a user account to a shielded contract account, therefore, before implementing MINT, the approve(address _spender, uint256 _value) function of the TRC20 contract needs to be called to allow a certain amount of TRC20 tokens to be transferred to the shielded contract account. _spender represents the shielded contract address and _value represents the transfer amount.

function mint(uint256 rawValue, bytes32[9] calldata output, bytes32[2] calldata bindingSignature, bytes32[21] calldata c) external {}

MINT transaction is executed through triggering the shielded contract's mint function. Parameters of the function include:

  • rawValue: Amount of transfer
  • output: {note_commitment||value_commitment||epk||proof}
  • bindingSignature: Binding signature of a transaction that is used to verify the balance of input and output amount within a transaction
  • c: {C_enc||C_out}, ciphertext field.

Perform the following steps in the shielded contract:

  1. Transfer the designated amount of TRC20 tokens from a user address to a shielded contract address.

    bool transferResult = trc20Token.transferFrom(sender, address(this), rawValue);
    require(transferResult, "TransferFrom failed!");
    
  2. Verify zk-SNARKs and Binding signature. If the verification is successful, update the Merkle tree by adding note_commitment to the leaf node. This step is implemented in verifyMintProof pre-compiled contract and is added specially for zk-SNARKs. verifyMintProof returns the latest root and node that needs to be updated of the Merkle tree.

    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 is the message Hash of Binding signature.

  3. The root and node of the Merkle tree, which returned by verifyMintProof, needs to be updated in the contract.

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

    roots stores all historical roots of the Merkle tree.

    Also, as tree stores the complete Merkle tree, all updated nodes of the Merkle tree will be updated to tree.

    mapping(uint256 => bytes32) public tree;
    
  4. Add note_commitment, value_commitment, epk, c and the location of the newly-added leaf node to the transaction log.

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

TRANSFER Transaction

TRANSFER transaction supports transfers from multiple shielded inputs to multiple shielded outputs. Once a transaction is confirmed, note_commitment of the shielded outputs will be added to the leaf node of the shielded contract’s Merkle tree.

TRANSFER transaction is executed through triggering the shielded contract’s transfer function.

function transfer(bytes32[10][] calldata input, bytes32[2][] calldata spendAuthoritySignature, bytes32[9][] calldata output, bytes32[2] calldata bindingSignature, bytes32[21][] calldata c) external {}

Parameters of the function include:

  • input: {nf||anchor||value_commitment||rk||proof}, variable-length array. Multiple shielded inputs are supported.
  • spendAuthoritySignature: Authentication signature of the shielded input. Each shielded input has a corresponding authentication signature.
  • output: {note_commitment||value_commitment||epk||proof}, each shielded output has a corresponding output.
  • bindingSignature: Binding signature of a transaction that is used to verify the balance of input and output amount within a transaction.
  • c: {C_enc||C_out}, ciphertext field. Each shielded output has a corresponding c.

Perform the following steps in the shielded contract:

  1. Limit the number of shielded inputs and outputs. In order to verify the efficiency of zk-SNARKS, set the upper limit of the number of shielded inputs and outputs to two.

    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. Verify the validity of double spending and Merkle root.

    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!");
    }
    

    Verify whether nf is in nullifiers for each shielded input. If the result is no, then it is verified that the note has not been spent. It also needs to be verified whether anchor exists in the historical roots of Merkle tree.

  3. Verify zk-SNARKs, the authentication signature and Binding signature of shielded input. If the verification is successful, update the Merkle tree by adding note_commitment to the leaf node. This step is implemented in verifyTransferProof pre-compiled contract and is added specially for zk-SNARKs. verifyTransferProof returns the latest root and nodess that needs to be updated of the Merkle tree.

    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. The root and nodes that need to be updated of the Merkle tree, which returned by verifyTransferProof, need to be updated to the contract.

  5. Add the nf of each shielded input to nullifier to signify the note has been spent.

    for (uint256 i = 0; i < input.length; i++) {
        bytes32 nf = input[i][0];
        nullifiers[nf] = nf;
    }
    
  6. Add note_commitment, value_commitment, epk, c and the location of the newly-added leaf node of each shielded output to transaction 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 Transaction

BURN transaction enables transfers from a shielded input to either a public address or a public address and a shielded output. Once a transaction is confirmed, a certain amount of TRC20 token will be transferred from the shielded contract address to the user's public address through TRC20 contract's transfer function. For the second scenario, the note_commitment of the shielded output will also be added to the Merkle tree.

BURN transaction is executed through triggering the burn function of the shielded contract.

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 {}

Parameters of the function include:

  • input: {nf||anchor||value_commitment||rk||proof}
  • spendAuthoritySignature: Authentication signature of shielded input.
  • rawValue: Amount of transfer.
  • bindingSignature: Binding signature of a transaction that is used to verify the balance of input and output amount within a transaction.
  • payTo: Public address of the transaction's receiver.
  • burnCipher: Encryption of the receiving address and the transfer amount. Encryption key is the sender's ovk. This parameter is mainly used by the transaction sender to track his transaction history.
  • output: {note_commitment||value_commitment||epk||proof}
  • c: {C_enc||C_out},ciphertext field. Each shielded output has a corresponding c.

Perform the function following the steps below:

  1. Verify nf and anchor to determine whether the shielded input has been double-spent and whether the anchor is the historical root of the Merkle tree.

    require(nullifiers[nf] == 0, "The note has already been spent!");
    require(roots[anchor] != 0, "The anchor must exist!");
    
  2. Decide the burn scenario based on the length of output, if it is scenario one(transfer from a shielded input to a public address), execute the step 2.1, it it is the scenario two(transfer from a shielded input to a public address and a shielded output), execute the step 2.2

    2.1 For scenario one, Verify zk-SNARKs, the authentication signature and Binding signature of shielded input. This step is implemented in verifyBurnProof pre-compiled contract and is specially added for zk-SNARKs.

    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 For scenario two, Verify zk-SNARKs of shielded input and shielded output, verify the authentication signature and Binding signature of shielded input. This step is implemented in verifyTransferProof pre-compiled contract and is specially added for zk-SNARKs.

       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!");
    

    The root and nodes that need to be updated, which returned by verifyTransferProof, needs to be updated to the contract.

  3. Add the nf of the shielded input to nullifier to signify the note has been spent.

    nullifiers[nf] = nf;
    
  4. Call the transfer function of TRC20 contract to transfer a certain amount of TRC20 token from the shielded contract address to the public address specified by the user.

    bool transferResult = trc20Token.transfer(payTo, rawValue);
    require(transferResult, "Transfer failed!");
    

Merkle Path

In the construction of a shielded input, private information of note includes the Merkle path and Merkle tree root of note_commitment. To help users construct zk-SNARKs with ease, the shielded contract provides getPath as a method to calculate the Merkle path of a leaf node at a specified location.

function getPath(uint256 position) public view returns (bytes32, bytes32[32] memory) {}

Enter the location of the leaf node using getPath. Merkle root and Merkle path will be returned.