Zero Knowledge Token Explained
Origin of the Underlying Technology
The Zero Knowledge Token service is built on the general category of cryptographic technologies called zero knowledge proof. The details of how zero knowledge proofs work is beyond the scope of this documentation, but a high level summary is that these technologies enable a party (the prover) to perform some computation on their own computers and demonstrate to other parties (the verifiers) that the computation has been performed correctly according to certain specifications, without disclosing any of the data involved in the computation. As a result, the verifiers can trust that the resulted state from the private calcuation is valid, without gaining any insight as to what the state is.
There are different types of zero knowledge proof designs available today. The most well known is zkSNARKs, which is known for its succinct (aka short) proofs. zkSNARKs can be used in practice for different types of computations. For each type of computations, or circuits, a set of public parameters are needed which must be generated from a procedure called Trusted Setup.
The zero knowledge proof system used by Kaleido's Zero Knowledge Token service is Σ-Bullets, proposed by Benedikt Bünz etc. in the Zether paper. Σ-Bullets allows Bulletproofs to work with Sigma protocols. Zether enables confidential token transfer between parties and allows for on chain verification of these transfers. We actually use the anonymous extension of Zether as proposed by Benjamin Diamond from JPMorgan’s Quorum team, who adds anonymity to the confidential token transfers.
The service is currently available as a Kaleido Labs Service.
How Anonymous Zether Works
A basic Zether integration for confidential transfer of tokens works roughly like the following,
- Zether smart contract (ZSC) is deployed on the network. There is one to one mapping between an ERC20 contract and the corresponding ZSC. In ZSC, accounts are identified by public keys.
- ZSC maintains mappings between accounts and encrypted balances.
- On ZSC, users can
- Fund an account with ERC20 tokens and obtain equal amount of shielded tokens (Let’s call them ZTH) in exchange. The ZSC is designated as the escrow of the ERC20 tokens.
- Transfer ZTH tokens from one account to another.
- Withdraw to exchange ZTH token from an account back to ERC20 tokens, at which point the ZSC releases the ERC20 tokens back to the original funding Ethereum account.
- To do a confidential transfer, users must
- Hold a private key corresponding to the account in ZSC.
- Must have enough ZTH balance to transfer.
- Create a confidential transfer by encrypting the value to send using both their public key and receiver’s public key, and a zero knowledge range proof which proves that the encrypted value is positive, and the user’s balance after the transfer is positive.
- The transaction with the payload is sent to ZSC, which updates the encrypted state of both the sender and the receiver after verifying the proof.
- To do a withdraw, users must
- Create a zero knowledge proof for the knowledge of the balance the account holds without revealing the corresponding private key.
- Send the transaction to ZSC with the proof, at which point the ZSC credits the equal amount of ERC20 tokens back to user’s Ethereum address.
The Zether protocol as described in the paper has been extended by Ben Diamond from the Quorum team in his anonymous Zether work. The transfers are modified to additionally encrypt 0 by public keys of the additional participants (let’s call them the decoys), along with a random number. Note that the anonymity feature comes at a cost, with the proof generation and verification time being O(N*logN), and proof size O(N), where N is the size of the anonymity set.
Transaction Processing in Epochs
The Zether protocol applies transactions in epochs. Epochs allow for protection against front-running and replay types of attacks. A transfer or a burn proof can be successfully verified only in the same epoch that the proof is generated for. Since proofs are generated against the current state of sender, a proof becomes invalid if the sender’s state changes due to an incoming transfer. The result is there would be many failed transfer transactions due to failed proof verifications.
To prevent against this, epoch is introduced in Zether transaction processing. All transfers are put into pending state during an epoch and are only applied in a future epoch whenever the account is trying to spend the funds.
While epoch solves the state management problem between the proof generator and the proof verifier, it also requires careful arrangement of transfer transactions in the client application. Let’s use the diagram below to illustrate.
Epoch length in the above example is configured to the equivalent of three block periods. Transfer transaction 1 starts at time ts1 and ends at te1. Transfer transaction 2 starts at time ts2 and ends at te2. Same is true for transaction 3.
Notice that the proof for the first two transactions were generated during epoch 1, and both transactions were submitted before epoch 1 ended. Both transactions will be successfully verified as expected.
For transaction #3, however, the proof generation was started during epoch 1 and did not complete until epoch 2, but verification is performed during epoch 2. As a result, it was marked as invproof verifier inside the ZSC contract. The transaction is marked as failed by the EVM.
Epoch length is an important parameter which depends on block intervals, proof generation and verification times. Epoch length should definitely be greater than the sum of proof generation time, proof verification time and propagation time. This ensures that the epoch used during the proof generation is the same one that is used during verification by ZSC. Epoch length should also be greater than the block period to allow for multiple blocks to be published within an epoch duration. More on epochs later in the Further Considerations section.
Current Performance and Bottlenecks
A good performance indicator for the service would be shielded transfer transactions throughput. By design, only one transfer-out transaction can be made from a single account in an epoch. But an account can receive multiple transfer-in transactions in an epoch. Then the maximum possible achievable throughput would be number-of-accounts/epoch.
The epoch length is lower bounded by proof generation and verification time. The block gasLimit restricts the number of transactions inside the block. Either higher block gasLimit or having more blocks within an epoch will allow more transactions to go through. Kaleido already sets the targetGasLimit to maximum value, so block gasLimit will not be the performance bottleneck. For an anonymity set of size 8, which we recommend, it takes approximately 2200 ms to generate proof and 135 ms to verify. The gas consumption is around 27 million. [source: AnonZether]
There are several gotchas and suggested workarounds to help you understand the service better and maximize its functionality. This likely fails to encompass every potential scenario, so please reach out if you are stuck while using the service.
If your applications are hitting transaction failures during a transfer, it's most likely due to the proof generation vs. proof verification ending up in different epochs. To mitigate this, we recommend two techniques:
- Make sure when the current state is downloaded from ZSC, it’s as close to the beginning of the current epoch as possible. Epoch length is currently set to 15 seconds for both Raft and IBFT. This is subject to change in the future but it will be clearly documented. Every Zether epoch starts at a multiple of 15 seconds since the Unix time Epoch. Capture the failure and retry in the next epoch. Time until next epoch can be given by
ceil(currentTimeInSeconds/ ZSC_EPOCH_LENGTH) * ZSC_EPOCH_LENGTH – currentTimeInSeconds
- The service currently allows users to generate only one shielded account (elgamal pair) per Ethereum account. This is a design decision in Kaleido to make the user experience easy to follow. The underlying ZSC implementation allows multiple elgamal accounts to be mapped to a single Ethereum account. So it’s conceivable that in the future this will be also enabled in the Kaleido service. If you have a use case that requires a single Ethereum account to be mapped to multiple shielded accounts, we would like to hear from you. The easiest way to contact us about such requirements is clicking the Contact Us button in the product.
Finally, you need to register the Ethereum account to shielded account mapping on ZSC contract before you can fund it and do transfer and withdraw from sender account. The withdraw operation will transfer the ERC20 funds to the Ethereum account to which the shielded account (elgamal public key) was registered with.
Anonymity is as good as the anonymity set and there is definitely a tradeoff between performance and size of anonymity set. The best case scenario will be to use all registered accounts in every transfer transaction but it would downgrade the performance and transfer might even fail if proof generation and verification times cross epoch boundary. The current implementation only allows anonymity sets of size of 2^k for k>=1. This is for efficient proof generation. For anonymous transfers, it is good practice to include keys that are funded. As it is easy to rule out accounts which were never funded, as decoys. It is also good practice to only withdraw from an account once it has been part of good amount of transfer transactions.