RAILGUN’s transaction system is similar to the Bitcoin network’s, with the notable exception that everything is anonymized by ZK proofs. The Merkle Tree keeps track of all balances held by 0zk addresses in the system which can only be updated if proof is submitted to the RAILGUN smart contracts that pass the cryptographic validation rules.
UTXOs & Nullifiers
RAILGUN operates on a (U)TXO (unspent transaction output) model, (U) is in brackets as transaction outputs are completely hidden from outside observers. Each UTXO is an encrypted note of a public key that establishes who can spend the underlying asset, amount, token ID, and a randomness field to maintain encryption.
The output of RAILGUN circuits are UTXOs and Nullifiers, which is a hash generated from private keys that cannot be linked to a UTXO by anyone who is not a party to the transaction. Nullifiers are deterministically generated to further guard against double spends, put simply they nullify a UTXO and disallow it from being spent again in the system. Nullifiers (and therefore circuit outputs) are calculated by a hash of the Spending Key combined with path indices of the Merkle Root/Leaf note, meaning that each note will always generate a unique Nullifier. The party holding the Spending Key is the only actor who can link which Nullifier belongs to which UTXO. This also means that Relayers cannot change the transaction values, such as amount or destination address, without rendering the hash and note invalid and therefore failing the ZK proof checks.
With RAILGUN’s zk-SNARK circuits, users can arithmetically prove that they have valid UTXOs and therefore prevent double spend or false transactions whilst not revealing any underlying information. Once all the cryptographic validation tests are passed, the Relayer will then submit the note to the blockchain for consensus and confirmation and the spend transaction is complete.
RAILGUN Smart Contract: transact()
Spend transactions call the transact() function. This function verifies that the Nullifiers in the internal Merkle Tree path correspond to the circuit-inputs and number of circuit-outputs being spent. Essentially, this function feeds in a zk-SNARK proof that the number of inputs matches the number of outputs on the circuit and enforces the rule that Nullifiers must not have been seen anywhere on the Merkle Tree before. If it has, then this is not a valid spend transaction and the user does not have the requisite UTXOs (insufficient funds) to send the transaction. The transact() function also allows Relayers to check if any transactions are addressed to them.
The transact() function emits the following events which are triggered when the RAILGUN contract detects any balance changes in the system:
CommitmentBatch – New zk-SNARK circuit outputs (i.e. new notes) occurring from the send transaction and transact() call.
Nullifiers – Private double spend markers that nullify the zk-SNARK circuit inputs used in the send transaction to ensure the UTXOs cannot be used again.
For the purposes of integration, only a surface level understanding of the transact() function is required to integrate the RAILGUN SDK as the SDK again, handles the computation on the integrating dApp’s behalf.
For RAILGUN transactions to execute, some publicly observable input data will be broadcast to the wider blockchain network. These hashed public inputs are used to represent and validate outputs from the zk-SNARK circuits. This input data is always hashed and cannot be unencrypted by someone who is not a party to the transaction. Hashing means that whilst the hashed values can be seen, no outside observer can reverse the hashing algorithm to arrive at the unencrypted input data, maintaining complete privacy throughout.
The RAILGUN transact() function has the following public inputs:
Any previous Merkle Root from the entire Merkle Tree. The RAILGUN smart contract computes a new Merkle Root each time funds are transferred. This does not need to be the newest Merkle Root, rather it is a race condition for the first Merkle Root picked up by the function.
Each dApp integrated with RAILGUN has its own unique parameters. Due to the constraints of circuit design, i.e., all circuits must be fixed before computation, this hash is computed at the user side for each required parameter instead of designing a custom circuit for each parameter.
Hash of the following values:
formattedRandom – Randomization factor to preserve security in hashing
requireSuccess – Boolean value that requires the contract call to be successful before progressing
minGas – Minimum amount of gas to be supplied to the transaction
to/value/datanullifiers[nInputs] – Nullifiers for the input notes
Nullifiers themselves are also hashed values. This public input is used to nullify the UTXO being sent in the transaction. The nullification computation occurs in such a way that only the user can determine which UTXO has been spent, and an outside observer cannot link a Nullifier and note. The [nInputs] value is the number of inputs.
Any new inputs being created requires new notes and this parameter is the hashed note of the balance changes from the spend transaction.