Basic Nightfall workflow

How Nightfall works

NB: This workflow describes the Deposit, Transfer and Withdraw transactions. In practice however one should never simply Deposit, Transfer and Withdraw. Doing so will make your private transactions easily discoverable. See the section on security and Privacy for more information.

This section describes a typical Nightfall workflow, with some detail of the internal processes that Nightfall is carrying out. Reading this section will help you understand how Nightfall works. It briefly describes some of Nightfall's APIs but the API section has more detail.

Allow listing

Before a User, Proposer or Challenger can interact with Nightfall, they need to get their Ethereum address allowlisted. Nightfall's X509.sol contract is used for this, and the Nf3 class provides a node API for interacting with it.

Firstly, the User, Proposer or Challenger must obtain an x509 certificate that X509.sol will accept. Suitable certificates for currently deployed Nightfall contracts are Entrust Code Signing Extended Validation (EV) or Document Signing EV certificates , Digicert Code Signing EV certificates or EY issued certificates. This is an out-of-band process; obtaining the certificate is nothing to do with Nightfall. The certificate must be in DER encoded format (if you have PEM, convert it with openssl or a similar utility)

Next, the User, Proposer or Challenger must sign their Ethereum address with the private key corresponding to their x509 certificate. This proves that they are the owner of the Ethereum address by tying it back to the x509 certificate. It's a standard RSA signature with PKCS#1 padding. Normally this would be done in a hardware security module but for test purposes there is a utility called sign-address.mjs that can achieve the same thing:

node test/unit/utils/sign-address.mjs path/to/private-key my-ethereum-address

Where the private-key should be stored unencrypted in a DER encoded file.

Once the certificate and signed Ethereum address is available, they should be given to X509.sol which will validate the certificate and verify the signature. This is best done by using a method of the Nf3 class:

await nf3.validateCertificate(


  • certificate is a Buffer containing the binary DER certificate.

  • ethereumAddressSignature is a Buffer containing the binary DER signature over the Ethereum Address (can be null if we're dealing with an intermediate certificate.

  • isEndUser is a boolean which is true when dealing with an end-user certificate

  • checkOnly if true, the certificate will be validated but, in the case of an intermediate certificate, it's public key won't be added to the trust store and, in the case of an end-user certificate, the signature over the Ethereum address will not be verified. This is useful for testing.

  • oidGroup is a selector for the OIDs that are checked for presence in the certificate. Its value depends on where the certificate originated from (0=Entrust code signer, 1=Entrust document signer, 2=EY certificate, 3=Digicert code signer).

  • address is a string containing the Ethereum address that has been signed (prefixed by 0x...).

If everything checks out, Nightfall will add the Ethereum address to the allowlist. It is possible that one would need to validate a certificate chain, rather than just the end user certificate, if X509.sol had not previously encountered the intermediate CA certs. This is done by sequentially passing in the chain's certificates, most authoritative first, until the end-user cert is reached. For now, we'll just assume the simple case.

It's quite expensive to add an address to the allow list in this way (~1MGas) but fortunately it only needs doing once for each certificate (it will need to be repeated once the certificate expires).

The Deposit transaction

Now that the User is allowlisted, Nightfall will allow them to interact with its other smart contracts. However, before Nightfall can be used for transfer, the User must commit some funds to Nightfall's Layer 2 (at least equal to the amount that they wish to transfer). This is made easy by using the Nf3 classes' Deposit method, which interacts with the Client container and the Shield contract to do the right things.

await deposit(
    fee = this.defaultFeeTokenValue,
    providedCommitmentsFee = [],
    salt = undefined,


  • ercAddress is a hex string containing the address of the ERC20, 721 or 1155 contract that Nightfall should take funds from to fund the deposit.

  • tokenType is the type of ERC token we are dealing with. Valid values are 'ERC20', 'ERC721' or 'ERC1155'.

  • value is the quantity of the ERC token (0 in the case of an ERC721 as it has no meaning for this token.

  • tokenId is the token ID for an ERC721 or ERC1155 token, for an ERC20 it has no meaning and should be set to '0x00'.

  • fee is the amount to be paid to the Proposer for including this deposit transaction in a Layer 2 block.

  • providedCommitmentsFee Advanced use; this will be covered later.

  • salt Advanced use; this will be covered later.

Under the hood, this Deposit call is doing quite a lot of work:

  1. It will approve the Nightfall Shield.sol contract to transfer funds from your ERC20,721 or 1155 balance (an amount equal to that you wish to deposit into the Nightfall Layer 2)

  2. It will contact the Client to assemble a Layer 2 transaction struct and embed that into an unsigned Ethereum transaction, which will post the transaction Struct to the Shield contract,

  3. It will sign the Ethereum transaction and send it to the blockchain.

The (Layer 2) Transaction struct is worth a look. Here it is in Solidity, although the Nightfall containers have an equivalent Nodejs object.

   struct Transaction {
        uint256 packedInfo;
        uint256[] historicRootBlockNumberL2;
        bytes32 tokenId;
        bytes32 ercAddress;
        bytes32 recipientAddress;
        bytes32[] commitments;
        bytes32[] nullifiers;
        bytes32[2] compressedSecrets;
        uint256[4] proof;

A detailed description is out of scope for this section, but a few things are noteworthy:

  • Irrespective of the transaction type (e.g. Deposit, Transfer, Withdraw) the transaction struct is the same, although not all of the fields are populated for all transaction types.

  • It contains a compressed Groth 16 proof of the correctness of the transaction

  • It contains secrets that can be decrypted by a recipient to take ownership of a commitment during a Layer 2 transfer

  • The transfer struct is not stored on-chain, except as calldata.

Note that a deposit transaction is not private. It can't be because it interacts with a Layer 1 ERC contract. This means that if you deposit tokens into Nightfall everyone will know you did that, what type they were and how much/many.

It's also quite expensive to do a deposit, about 130 kGas. That's mostly because of the ERCx transactions that Nightfall has to do. Fortunately it should not be a transaction that you do often.

Paying Proposers

Note that each time you create a transaction of any type, you have to pay a Proposer to incorporate it into a block. Proposers advertise their fees on chain as part of their registration.

The payment is made as a layer 2 transaction and is incorporated into the circuit that is used to enforce the transaction that you are making. This is to preserve privacy; payment to a Proposer on Layer 1 would leak information about what you are doing. The upshot of this is that the Proposer payment transaction is completely transparent to the user, the fees are just taken from your L2 balance.

The is one caveat however; you do need to have sufficient funds in your Layer 2 balance to pay the proposer and these funds must be in the correct currency that the Proposer accepts. For test deployments this is the ERC20Mock token and for Mumbai and Polygon it is WMATIC. If you are doing your own Nightfall deployment, it can be any ERC20 that you like (set by the environment variable


For example FEE_L2_TOKEN_ID=WMATIC. Note that the address corresponding to the token name be set in default/config.js under the environment object; Nightfall has no way to introspect the token name.

If you haven't deposited sufficient balance of this token into Layer 2, Nightfall will complain and will not process your transaction. The only exception to this rule is a Deposit that is made in the Proposer payment currency, in which case a Proposer payment will be taken from the Deposit and the rest credited to your balance (and available for future Proposer payments).

The transfer transaction

Having deposited some tokens into Nightfall's Layer 2, you are now in a position to transfer tokens to someone else within Nightfall's Layer 2. These transfers do not interact with any ERCx contract and are completely private; unless you are careless, no one will know what tokens you send to whom, or how many/much you sent.

In the case of an fungible token, you can transfer any amount, up to the maximum that you have in your Layer 2 balance (either through a deposit transaction of by someone else sending you the funds). For a non-fungible token, you can only transfer whole tokens, that sort of definitional.

Again, the Nf3 class is the easiest way to tell a Nightfall node to do a transfer transaction for you.

await nf3.transfer(
    offchain = false,
    fee = this.defaultFeeTokenValue,


  • offchain is a boolean. If set to false, the transaction will be signed and posted to the blockchain as per a deposit transaction. However, a transfer does not need to interact with any smart contract, and therefore the only purpose in posting it to the blockchain is for a Proposer to pick up the transaction. Notarising the transaction to the blockchain in this way is therefore an expensive way to send it to a Proposer. Instead, it can be sent directly to a Proposer (or many Proposers). Proposers have an http:// endpoint for this purpose. They actually then proxy the transaction to their Optimist instance, so it can also be sent directly there. Proposers publish this URL to the blockchain as a means of Proposer discovery. The direct sending is enabled by setting offchain to true.

  • compressedZkpPublicKey is the public key corresponding to the transfer recipient's zkp private key (also known as a viewing key). It's a Babyjubjub curve point in compressed form (y coordinate and a parity bit). It's role is equivalent to an account address in an conventional Ethereum blockchain; it identifies the recipient of the transfer. If someone asks for your 'Nightfall address', you should give them your compressedZkpPublicKey.

The other parameters are the same as the deposit transaction. providedCommitments and providedCommitmentsFee are for advanced use. They can be left undefined.

A transfer transaction is very cheap to do. The gas cost is only the cost of posting a Layer 2 block, amortised across all the transactions in that block, plus any fees that the Proposer charges (run your own Proposer if you don't want to pay fees). Assuming there are enough transactions (~60) in the block to amortise the overhead of the block header, then the gas cost is about 6500 Gas.

The Withdraw transaction

Completing the trio of basic Nightfall transactions is the withdraw transaction. This is the inverse of a deposit transaction. It takes tokens out of Layer 2 and returns them to their respective ERCx contract. One slight complication is that, although the tokens are returned to the ERCx contract, they remain as part of Nightfall's balance, and are not returned to the owner until the Layer 2 block which contains the withdraw transaction is finalised. Being an Optimistic Layer 2, this takes one week, to allow sufficient time for incorrect Layer 2 blocks to be challenged. See the pages on Liquidity Providers for a way around that delay.

The Nf3 classes withdraw method is called like this:

 await nf3.withdraw(
    offchain = false,
    fee = this.defaultFeeTokenValue,

All of these parameters have been covered except for the recipientAddress which, as you might guess, is the Ethereum address that will take ownership of the tokens once the withdrawal is finalised.

Once this transaction is completed Nf3 will have an attribute nf3.latestWithdrawHash, which contains the hash of the withdraw transaction and is needed to claim the tokens. After the Layer 2 block which contained the withdraw transaction is finalised, the owner of the tokens can claim them by calling:

await nf3.finaliseWithdrawal(withdrawTransactionHash)

Where withdrawTransactionHash is the value of nf3.latestWithdrawHash resulting from the withdraw transaction that you are concerned with.

Tokenise, Transform and Burn transactions


Last updated