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.
MINTrefers to the transfer ofTRC20tokens from public addresses to a shielded address. To be more specific,TRC20tokens are transferred from the user's address to the contract address and a commitment to this shielded output is added to the smart contract.TRANSFERsupports 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.BURNsupports 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 ofTRC20token 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.
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 fromskand0. It is used to generate the key for signing the shielded input usingSpend Authority Signaturealgorithm;ak: the value returned by multiplying a coordinate on the elliptic curve byask(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 fromskand1. It is used to generatenk;nk: generated by the scalar multiplication of nsk and a coordinate on the elliptic curve. It is used to generatenullifier(prevent double-spending);ivk: generated byakandnkperforming a BLAKE2s hash. It's mostly used by the recipient to view the shielded transactions he/she receives;ovk: generated byskand2performing 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.dwill perform aDiversifyHash(namely, hashdto the coordinate of the elliptic curve) first to generateg_d. The scalar multiplication ofg_dandivkproducespk_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: everynotematches a uniquenf, the positions ofnfandnoteon theMerkletree is related tonote_commitment, it is used to prevent the double spending ofnote.anchor: the root of theMerkletree.value_commitment: the commitment to the amount of thenote.rk: the public key that verifies the Spend Authority Signature of thenote
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 tonotevalue_commitment: the commitment tonoteamountepk: the temporary public key for decipheringnote
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 TransactionMINT 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 transferoutput:{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 transactionc:{C_enc||C_out}, ciphertext field.
Perform the following steps in the shielded contract:
-
Transfer the designated amount of
TRC20tokens from a user address to a shielded contract address.bool transferResult = trc20Token.transferFrom(sender, address(this), rawValue); require(transferResult, "TransferFrom failed!"); -
Verify zk-SNARKs and Binding signature. If the verification is successful, update the
Merkletree by addingnote_commitmentto the leaf node. This step is implemented inverifyMintProofpre-compiled contract and is added specially for zk-SNARKs.verifyMintProofreturns the latest root and node that needs to be updated of theMerkletree.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!");signHashis the message Hash of Binding signature. -
The root and node of the
Merkletree, which returned byverifyMintProof, needs to be updated in the contract.mapping(bytes32 => bytes32) public roots; roots[latestRoot] = latestRoot;rootsstores all historical roots of theMerkletree.Also, as
treestores the completeMerkletree, all updated nodes of theMerkletree will be updated totree.mapping(uint256 => bytes32) public tree; -
Add
note_commitment,value_commitment,epk,cand 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 TransactionTRANSFER 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 correspondingoutput.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 correspondingc.
Perform the following steps in the shielded contract:
-
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!"); -
Verify the validity of double spending and
Merkleroot.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
nfis innullifiersfor each shielded input. If the result is no, then it is verified that thenotehas not been spent. It also needs to be verified whetheranchorexists in the historical roots ofMerkletree. -
Verify zk-SNARKs, the authentication signature and Binding signature of shielded input. If the verification is successful, update the
Merkletree by addingnote_commitmentto the leaf node. This step is implemented inverifyTransferProofpre-compiled contract and is added specially for zk-SNARKs.verifyTransferProofreturns the latest root and nodess that needs to be updated of theMerkletree.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!"); -
The root and nodes that need to be updated of the
Merkletree, which returned byverifyTransferProof, need to be updated to the contract. -
Add the
nfof each shielded input tonullifierto signify thenotehas been spent.for (uint256 i = 0; i < input.length; i++) { bytes32 nf = input[i][0]; nullifiers[nf] = nf; } -
Add
note_commitment,value_commitment,epk,cand 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 TransactionBURN 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'sovk. 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 correspondingc.
Perform the function following the steps below:
-
Verify
nfandanchorto determine whether the shielded input has been double-spent and whether theanchoris the historical root of theMerkletree.require(nullifiers[nf] == 0, "The note has already been spent!"); require(roots[anchor] != 0, "The anchor must exist!"); -
Decide the
burnscenario based on the length ofoutput, 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.22.1 For scenario one, Verify zk-SNARKs, the authentication signature and Binding signature of shielded input. This step is implemented in
verifyBurnProofpre-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
verifyTransferProofpre-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. -
Add the
nfof the shielded input tonullifierto signify thenotehas been spent.nullifiers[nf] = nf; -
Call the
transferfunction ofTRC20contract to transfer a certain amount ofTRC20token 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.
References
[1] Zether protocol https://crypto.stanford.edu/~buenz/papers/zether.pdf
[2] TIP135 https://github.com/tronprotocol/tips/blob/master/tip-135.md
[3] TRONZ shielded protocol https://www.tronz.io/Shielded%20Transaction%20Protocol.pdf
Updated about 5 years ago