Nightfall objects

Basic data objects used by Nightfall

Nightfall has a number of data abstractions that it uses internally. It's possible to use Nightfall without understanding these internal objects but it is always better to have some understanding of what an application that you use is doing.

The object hierarchy

Below, we will talk about commitments, nullifiers, transactions and blocks. A block contains transactions and a transaction contains commitments and/or nullifiers, at least in some sense.

Commitments

This is one of the most basic object types. It is somewhat analogous to a token in a conventional blockchain, although one should not stretch the analogy too far. Mathematically, it is a hash with the preimage comprising a number of items relevant to the layer 1 token it was created from. Specifically

c=H(ercAddress,tokenId,value,zkpPublicKey,salt)c = H(ercAddress, tokenId,value,zkpPublicKey,salt)

Where:

  • ercAddress is the address of the contract of the Layer 1 token.

  • tokenId is the token ID in the case of an ERC1155 or ERC721 token (0x00 for an ERC20).

  • value is the value in the case of an ERC20 or ERC1155 token (0 in the case of an ERC721)

  • zkpPublicKey is the described below.

  • salt is a random field value to ensure that each commitment is unique and that the preimage cannot be brute forced.

  • H is a hash function (Nightfall_3 uses Poseidon).

The commitment is used to prove that you own a certain value of tokens. For example, I can create a ZKP that the preimage of c contains my public key, and that it is my public key because I know the corresponding private key. I could at the same time prove that it has the value value. This is done in zero knowledge so that the only data on chain is the hash value c. An observer learns nothing about who owns the commitment nor its value. See the page about how Nightfall works for more details.

Note that equation 1 is a slight simplification. The tokenId is a 256 bit number and therefore won't fit into a bn254 field. This is a problem if we're using a field-based hash. To get around this, we move the top 4 bytes into the ercAddress variable before hashing, which has room because it's only 20 bytes long.

Nullifier

The nullifier object is used to spend a commitment or, rather, the absence of a nullifier in the blockchain record indicates that a commitment is available to spend. It's purpose, therefore, is to prevent double spending. It needs to be provably related to one and only one commitment and it must only be possible for the commitment owner to create it, but the relationships must not be obvious from the value. We define it thusly:

n=H(c,nullifierKey)n = H(c, nullifierKey)

The nullifierKey is a secret key derived from the zkpPublicKey used in the commitment. Only the commitment owner will know that. This prevents someone else spending your money. See the section on in-band secret distribution for details on key derivation.

Transaction

The transaction object is both a Node class/object and also a Solidity struct. It contains all the information associated with a transaction:

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

where:

  • packedInfo is a word that contains small values compressed into a single EVM word (value, fee, circuitHash, tokenType), where value is the value of the token fee is the amount to be paid to a Proposer for processing the transaction, circuitHash is a hash of the ZKP circuit that the transaction will interact with (it's used to select the correct verifier key), and tokenType is one of ERC20, ERC721 or ERC1155.

  • historicRootBlockNumberL2 is an array containing the blocks whose roots are used as the root of the Merkle proof that the commitments being nullified actually exist on chain and haven't just been made up.

  • tokenId is the ERC721 or ERC1155 tokenId. It is set to 0x00 for an ERC20 token.

  • recipientAddress is the Ethereum address that should receive the token resulting from the finalisation of a Withdraw transaction. It is only populated in the case of a withdraw.

  • commitments the values of the output commitments that this transaction creates

  • nullifiers the values of the nullifiers that nullify the input commitments to this transaction.

  • compressedSecrets these bytes contain the encryption of the compressed ercAddress, salt, value and tokenId . In the case of a transfer transaction, the recipient will be able to decrypt these values and take ownership of the commitment transferred to them. Details of how the (de)encryption works are in the section about in-band secret distribution.

  • proof compressed curve points representing the Groth16 zero-knowledge proof that nightfall 3 users. The compressed curve points are represented as the affine y coordinate, with the first bit of the word representing the parity of the x coordinate, so that the point can be unambiguously reconstructed using the curve equation.

The transaction object is the smallest 'unit of exchange' in Nightfall. It is created by a user and sent to a Proposer for inclusion in a block, either directly or by posting on chain (posting on-chain is required for a deposit but otherwise tends to be a large waste of gas and risks compromising privacy by Gas-tracking).

Note that transaction structs only ever exist as call-data.

Blocks

Layer 2 blocks represent the roll-up of Nightfall transactions and are submitted on-chain by a Proposer (see Nightfall's actors). Like transactions, they are also represented by a Node class and a Solidity struct:

struct Block {
        uint256 packedInfo;
        bytes32 root;
        bytes32 previousBlockHash;
        bytes32 frontierHash;
        bytes32 transactionHashesRoot; // This variable needs to be the last one in order proposeBlock to work
}

where:

  • packedInfo contains: the leafcount, which is the number of leaves present in the commitment Merkle tree before the commitments in this block are added to the Merkle tree; the address of the Proposer that submitted the block; and the block number (note this is the Layer 2 block number and nothing to do with the Ethereum block numbers).

  • root the value of the commitment tree root, after the new commitments included in this block have been added.

  • previousBlockHash the hash of the block prior to this one (ensures that blocks are submitted in the correct order).

  • frontierHash the hash of the current commitment Merkle tree frontier. Knowing this, greatly reduces the cost of challenging an incorrect commitment tree root.

  • transactionHashesRoot The transactions associated with this block have their hashes incorporated in a small Merkle tree and the root of the tree is included in the block. This means that a Challenger can prove (if needed) that a particular block contains a particular transaction by providing a Merkle proof to that effect.

Last updated