Nightfall objects
Basic data objects used by Nightfall
Last updated
Basic data objects used by Nightfall
Last updated
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.
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.
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
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.
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:
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.
The transaction object is both a Node class/object and also a Solidity struct. It contains all the information associated with a transaction:
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.
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:
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.