In October 2018, Ernst & Young introduced Nightfall at Ethereum Devcon in Prague. On May 31st, 2019, EY released Nightfall into the public domain without the usual copyrights / licensing that frequently accompany open-source software. Nightfall enables the private transfer of ERC-20 and ERC-721 tokens on the Ethereum mainnet.
Setting up
The software achieves transactional privacy by leveraging zk-SNARKs using ZoKrates to hide the _to
and _value
arguments of a transfer function. The current method for zero-knowledge proofs to be stored and verified efficiently over a blockchain relies on having a set of public parameters for all parties to use for producing proofs. Read about ZCash’s generation of their public parameters here. Nightfall utilizes a “trusted benefactor” (referred to as “Tom” in the whitepaper) to perform its parameter generation process, known as the “Trusted Setup.” I assume that the trusted benefactor can be an individual or a collective, and is the entity responsible for producing the public parameters and deploying the necessary contracts. Nightfall features six main computations:
NFTokenShield.mint(_proof,_inputs,_vkId)
NFTokenShield.transfer(_proof,_inputs,_vkId)
NFTokenShield.burn(payTo,_proof,_inputs,_vkId)
FTokenShield.mint(_proof,_inputs,_vkId)
FTokenShield.transfer(_proof,_inputs,_vkId)
FTokenShield.burn(payTo,_proof,_inputs,_vkId)
All six functions each require the “Trusted Setup” to compute a pair of keys: the verification key, and the proving key. The six verification keys are checked into a zk-SNARK Verifier Registry in exchange for vkIds
, which are then shared along with the proving keys publicly for users to use in their proofs (created when users call one of the functions).
Tom proceeds to deploy the Shield contracts (I believe it’s called a Shield contract because it’s “shielding” the transaction from watchful eyes) NFTokenShield.sol
and FTokenShield.sol
onto Ethereum mainnet. In doing so, he chooses the type of ERC-20 token (ZRX, MKR, BAT, etc…) and the type of ERC-721 token (ENS, CryptoKitties, etc…) that the respective shield contracts will support. I assume that each instance of a shield contract can only support a single type of token and different instances of shield contracts will need to be deployed to accommodate different types. Tom then shares the Ethereum addresses of the two shield contracts.
Nightfall
The main workflow is simple: Mint => Transfer => Burn. The structure for the following three sections will be a breakdown of each function in detail as it pertains to ERC-721s. Each section will include a sub-section explaining how the function differs for ERC-20s. Once again, we will follow the age-old tale of Alice and Bob. Alice seeks to use Nightfall in order for the following three things to become private:
- All details of the ERC-721 token (the asset being transferred)
- The identity of the sender (Alice)
- The identity of the recipient (Bob)
Mint
Alice yearns to transfer ownership of her ERC-721 token to Bob in secret. She begins by converting her ERC-721 token into a private ERC-721 commitment, an act otherwise known as “minting.” Alice can achieve this with the Nightfall UI; I will attempt to provide a slightly simplified version of the steps:
Some definitions:
σ
= a randomly generated saltZ_A
= Private commitment token representing Alice’s ERC-721 tokenα
= The ERC-721 token id representing the asset being mintedpk_ZA
= Alice’s public key for the private commitment Z_A
x
= public inputs = (α, Z_A )
w
= private inputs = (pk_ZA, σ)
π
= proof of knowledge that Z_A
does, in fact, hide the asset α
h()
= sha256
- The client computes a commitment token
Z_A: = h( α | pk_ZA | σ)
(This is just a string concatenation ofα
,pk_ZA
andσ
fed into sha256) - The client computes the zk-SNARK pair
(π, x)
and sends it off to the Shield contract. It does so by selecting a set of arithmetic constraintsC
to generate a proofπ
such thatC
is satisfied given the inputsx
andw
.C
itself is satisfied if and only if the following condition is true:Z_A = h( α | pk_ZA | σ)
(proof thatZ_A
does in fact hideα
) - Shield contract verifies the validity of
(π, x)
if the proof checks out, it will: - execute the transfer of
α
, on behalf of Alice, to hold in escrow in the Shield Contract - Add
Z_A
to the next empty leaf in the contract’s Merkle Tree - Recalculate the path to the root of the Merkle Tree from Z_A and gives the
leafindex
ofZ_A
back to the caller. (The on-chain Merkle tree is represented by a flattened array, so theleafindex
is just the index in this array) - Alice stores this
leafindex
ofZ_A
along with other pertinent information in a local instance of MongoDB.
I will attempt to address security/privacy concerns in a later section. The software does come with many security warnings, and the Nightfall whitepaper makes clear that privacy is NOT achieved during mint.
* How minting differs for ERC-20 tokens
Essentially the only difference here is that the asset α
is replaced with e
, which represents the value of the ERC-20 token being minted. The caller sends the zk-SNARK pair (π, x)
to FTokenShield.mint
instead of NFTokenShield.mint
.
Transfer
Alice successfully mints her ERC-721 token in the section above and is now ready to transact privately. Recall that Alice wanted the following three things to become private with the transfer:
- All details of the ERC-721 token
- The identity of the sender (Alice)
- The identity of the recipient (Bob)
It will be during and only during transfer, that the above three things are ‘shielded’ from prying eyes. However, before the transfer can happen, Bob must first register his ‘zkp’ public key as well as his Whisper public key against his Ethereum address with a PKD (Public Key Directory). Alice will look up Bob’s Ethereum address in the PKD to retrieve Bob’s public keys. The transfer process (again slightly simplified) is as follows:
Some more definitions for reference:
σ
= the salt that Alice used in mintσ_AB
= new salt that Alice will pass to Bob Z_B
= The private token commitment calculated by Alice for Bob, with Bob’s public key and the new saltα
= The ERC-721 token id representing the asset being transferredpk_ZB
= Bob’s zkp public key that he registered against his Ethereum address in a PKD. Alice will use this public key to create the Z_B
private commitment for Bob. pk_ZA
= Alice’s public key for the private commitment Z_A
sk_ZA
= Alice’s secret key for the private commitment Z_A
N_A
= The nullifier of Alice’s commitment Z_A
N
= The list of nullifiers that have been usedroot
= The new Merkle root in the contractroots
= The list of roots stored on the Contract logging the new root every time it’s recalculatedsp_ZA
= The sister path to the ZA to the Merkle rootx
= public inputs to a zk-SNARK = (N_A, root, Z_B)
w
= private inputs to a zk-SNARK = (α, sp_ZA, sk_ZA, σ, pk_ZB, σ_AB)
C
= the arithmetic circuit C: (w,x) -> {0,1}
pC
= proving key for the arithmetic circuit C
π
= proof for the circuit C, public inputs x
and private inputs w
h()
= sha256
- Alice must first look up Bob’s zkp public key
pk_ZB
in a Public Key Directory. - Compute the commitment token
Z_B: = h( α | pk_ZB | σ_AB)
(Notice that this new commitment token contains the same asset, but Alice uses Bob’s zkp public key instead of her own, she also uses a new saltσ_AB
which she will pass to Bob through Whisper) - Compute
N_A: = h( σ | sk_ZA)
, the nullifier of Alice’sZ_A
commitment - Grab the new
root
to the on-chain Merkle tree as well as the ‘sister path’ to the commitment tokenZ_A
from the Shield contract and use them to set public inputx = (N_A, root, Z_B)
and private inputw = (α, sp_ZA, sk_ZA, σ, pk_ZB, σ_AB)
- The client computes the zk-SNARK pair
(π, x)
and sends it off to the Shield contract. It does so by selecting a set of arithmetic constraintsC
to generate a proofπ
such thatC
is satisfied given the inputsx
andw
.C
itself is satisfied if and only if the following five conditions are true:
pk_ZA = h(sk_ZA);
(proof of Alice’s knowledge of the secret key topk_ZA
)Z_A = h( α | pk_ZA | σ)
(This is the same as the satisfying condition of the constraintC
in ‘mint’)- Proof that
Z_A
belongs on the on-chain Merkle Tree N_A = h( σ | sk_ZA);
(Proof thatN_A
is indeed the nullifier toZ_A
)Z_B = h( α | pk_ZB | σ_AB);
(Proof thatZ_B
contains the same asset asZ_A
)
6. Shield contract verifies the validity of (π, x)
through the use of a Verifier contract and if the proof checks out, it will:
- Check that the
root
provided by the caller is inroots
(fail if not) - Check that
N_A
is not in the list of already used nullifiersN
(fail if not) - Add
Z_B
to the next empty leaf of the on-chain Merkle Tree - Recalculate the path to the root from
Z_B
- Append new
root
toroots
- Append
N_B
to the list of used nullifiersN
- Pass the
leafindex
ofZ_B
back to the caller
7. Alice stores this leafindex
of Z_B
along with other pertinent information in a local instance of MongoDB.
8. Alice then sends Bob several things: the salt σ_AB
, the public key of Bob pk_ZB
that Alice used to produce Z_B
, the ERC-721 token id α
, Z_B
itself, and the leafindex
of Z_B
store on the Merkle tree of the Shield contract all via Whisper.
9. Bob can now verify the correctness of the information provided by Alice for himself. He can also store relevant data in his local instance of MongoDB, including information on whether or not Alice’s information was valid.
Alice and Bob have been through quite the ordeal to get here. Alice just successfully transferred ownership of her ERC-721 token to Bob under the veil of the Shield contract. It is important to note that the asset can now be transferred indefinitely to any number of recipients under zero knowledge within the protection of the shield contract. In this very moment, no one except for Alice and Bob knows who the new owner of Alice’s ERC-721 token is. That is of course until Bob decides to ‘burn’ the private commitment Z_B
in exchange for the asset.
* How transfer differs for ERC-20 tokens
For ERC-20 tokens, the FTokenShield.transfer
function requires TWO inputs and TWO outputs (Unlike NFTokenShield.transfer
which takes in one private commitment Z_A
and outputs the private commitment of Bob Z_B
). FTokenShield.transfer
instead require two private commitments from the sender totaling to more than the amount being transferred, and the two outputs are the amount transferred to Bob, and Alice’s change. The whitepaper specifically states several reasons for this:
- Due to the nature of zk-SNARKs, having multiple permutations of the number of inputs of outputs means having to perform the “Trusted Setup” on each permutation to generate the appropriate verification keys. To avoid this complexity at this stage, the developers chose to support only the 2 outputs, 2 inputs solution for now.
- Having only one output requires the sender to own commitments which sum up to exactly the amount required by the recipient (so that there would be no change).
- Having a single input allows observers to infer more information from watching the shield contract.
For Alice, this means she needs to generate 2 private commitments (including 2 new salts), 1 for Bob, and 1 for herself to give herself change. She also needs to generate 2 nullifiers, one for each of her initial commitments. The rest of the protocol follows suit and handles the extra input and output as one would expect.
Burn
With Alice out of the picture, all that’s left to do is for Bob to burn the private commitment Z_B
in exchange for the actual ERC-721 token from the Shield contract. In order to release the asset from escrow, Bob will have to prove to the Shield contract he knows the secret key to Z_B
. He performs the following:
Some familiar definitions for reference:
σ
= a randomly generated saltZ_B
= The private commitment token representing Alice’s ERC-721 tokenα
= The ERC-721 token id representing the asset being mintedsk_ZB
= Bob’s secret key for the private commitment Z_B
pk_ZB
= Bob’s public key for the private commitment Z_B
sp_ZB
= the sister path of Z_B for the on-chain Merkle treeN
= list of nullifiers that have already been used. x
= public inputs = (α, N_B, root)
w
= private inputs = (sp_ZB, sk_ZB, σ_AB)
π
= proof of knowledge that Z_A
does, in fact, hide the asset α
h()
= sha256
- Compute
N_B: = h( σ | sk_ZB)
, the nullifier of Bob’sZ_B
commitment (Recall that Alice had to computeN_A
for her private commitmentZ_A
when she transferred the asset to Bob) - Grab the new
root
to the on-chain Merkle tree as well as the ‘sister path’ to the commitment tokenZ_B
from the Shield contract and use them to set public inputx = (α, N_B, root)
and private inputw = (sp_ZB, sk_ZB, σ_AB)
- The client computes the zk-SNARK pair
(π, x)
and sends it off to the Shield contract. It does so by selecting a set of arithmetic constraintsC
to generate a proofπ
such thatC
is satisfied given the inputsx
andw
.C
itself is satisfied if and only if the following conditions are true:
pk_ZB = h(sk_ZB);
(Proof that Bob has the correct secret key toZ_B
)Z_B = h( α | pk_ZB | σ_AB);
(Proof that the asset, the public key Bob provided, and the salt that Alice gave to Bob do in fact produceZ_B
)- Proof that
Z_B
belongs on the on-chain Merkle Tree N_B = h( σ | sk_ZB);
(Proof thatN_B
is the nullifier ofZ_B
)
4. Shield contract verifies the validity of (π, x)
through the use of a Verifier contract and if the proof checks out, it will:
- Check that the
root
provided by Bob is in the list ofroots
. (fail if not) - Check that
N_B
is not in the list of already used nullifiersN
(fail if already inN
) - Execute the transfer of the token
α
to thepayTo
Ethereum address that Bob specifies. - Append
N_B
to the list of used nullifiersN
5. Bob can now store any relevant data in his local instance of MongoDB
* How Burn differs for ERC-20 tokens
Essentially the only difference here is that the asset α
is replaced with e
, which represents the value of the ERC-20 token being burned. The caller sends the zk-SNARK pair (π, x)
to FTokenShield.burn
instead of NFTokenShield.burn
.
Privacy + Security Concerns
None of what I’m saying in this section is news. There are security and privacy warnings plastered all over the documentation letting readers know the limitations of the system. I chose to include this information as a separate section to not distract from the main purpose of explaining how Nightfall works.
Of the three main functions in the workflow (Mint, Transfer, and Burn), Transfer is the only function which shrouds the asset and the identities of the sender and recipient under the cover of privacy. Any eyes watching the Shield contracts can see activity from Ethereum addresses minting and burning assets/tokens. The only way to truly hide the identity of the sender is for the sender to mine ether into a never-before-used Ethereum address. The whitepaper mentions a solution to use delegated proxies/transactions to interact with the Shield contract which will need to be implemented in a future release.
I repeatedly dropped this line “Someone can now store any relevant data in his/her local instance of MongoDB” without any explanation. I didn’t want to have to explain that the secret keys for spending the token commitments are actually just stored in a local instance of MongoDB. You can find the schema here to see what exactly is stored. This obviously isn’t secure and will need to change for use in production.
Resources
I wrote this blog post with the intention to learn about EY Nightfall by writing about it. I mainly used the whitepaper and the GitHub code base as reference. Thanks for reading! Perhaps in the future I will do a deep dive into exactly how the zk-SNARK pair (π, x)
is generated. In the mean time, here are some resources if you would like to learn more about the technology:
- Z-Cash series explaining SNARKs
- Christian Reitwiessner — zk-SNARKs in a nutshell
- Vitalik Buterin on quadratic arithmetic programs (part 1)
- Vitalik Buterin on exploring elliptic curve pairings (part 2)
- Vitalik Buterin on zk-Snarks (part 3)
- PGHR13
- GM17
- Whisper