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 ofTRC20
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 ofTRC20
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.
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 fromsk
and0
. It is used to generate the key for signing the shielded input usingSpend Authority Signature
algorithm;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 fromsk
and1
. 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 byak
andnk
performing a BLAKE2s hash. It's mostly used by the recipient to view the shielded transactions he/she receives;ovk
: generated bysk
and2
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 aDiversifyHash
(namely, hashd
to the coordinate of the elliptic curve) first to generateg_d
. The scalar multiplication ofg_d
andivk
producespk_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
: everynote
matches a uniquenf
, the positions ofnf
andnote
on theMerkle
tree is related tonote_commitment
, it is used to prevent the double spending ofnote
.anchor
: the root of theMerkle
tree.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 tonote
value_commitment
: the commitment tonote
amountepk
: 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
TRC20
tokens 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
Merkle
tree by addingnote_commitment
to the leaf node. This step is implemented inverifyMintProof
pre-compiled contract and is added specially for zk-SNARKs.verifyMintProof
returns the latest root and node that needs to be updated of theMerkle
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. -
The root and node of the
Merkle
tree, which returned byverifyMintProof
, needs to be updated in the contract.mapping(bytes32 => bytes32) public roots; roots[latestRoot] = latestRoot;
roots
stores all historical roots of theMerkle
tree.Also, as
tree
stores the completeMerkle
tree, all updated nodes of theMerkle
tree will be updated totree
.mapping(uint256 => bytes32) public tree;
-
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
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
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 innullifiers
for each shielded input. If the result is no, then it is verified that thenote
has not been spent. It also needs to be verified whetheranchor
exists in the historical roots ofMerkle
tree. -
Verify zk-SNARKs, the authentication signature and Binding signature of shielded input. If the verification is successful, update the
Merkle
tree by addingnote_commitment
to the leaf node. This step is implemented inverifyTransferProof
pre-compiled contract and is added specially for zk-SNARKs.verifyTransferProof
returns the latest root and nodess that needs to be updated of theMerkle
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!");
-
The root and nodes that need to be updated of the
Merkle
tree, which returned byverifyTransferProof
, need to be updated to the contract. -
Add the
nf
of each shielded input tonullifier
to signify thenote
has been spent.for (uint256 i = 0; i < input.length; i++) { bytes32 nf = input[i][0]; nullifiers[nf] = nf; }
-
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
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
nf
andanchor
to determine whether the shielded input has been double-spent and whether theanchor
is the historical root of theMerkle
tree.require(nullifiers[nf] == 0, "The note has already been spent!"); require(roots[anchor] != 0, "The anchor must exist!");
-
Decide the
burn
scenario 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
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. -
Add the
nf
of the shielded input tonullifier
to signify thenote
has been spent.nullifiers[nf] = nf;
-
Call the
transfer
function ofTRC20
contract to transfer a certain amount ofTRC20
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.
Updated over 4 years ago