Light Protocol is privacy infrastructure on Solana. The core of the protocol is a shielded pool implemented in a Solana program. The shielded pool is basically Zcash in a smart contract (Solana program). The shielded pool`s ZKPs are generated with the Groth16 proving system, implemented over the bn254 curve. The circuit of the shielded pool is implemented in light-pool-client. The Merkle tree used in the current implementation is of height 18 and uses poseidon hashes, implemented over the bn254 curve. Nullifiers and Merkle tree leaves are stored in individual accounts as described below.
Solana programs are stateless. State is stored in accounts. Accounts which are owned (and derived) by programs are called programs derived accounts (pdas). Light Protocol stores state in five accounts: a tmp_storage_pda, a merkle_tree_storage_pda, nullifier_pdas, leaves_pdas, and user_accounts.
State Accounts:
tmp_storage_account:
- stores the state of a shielded pool transaction
- only has to exist temporary for the computation
- account_id: 1
- rent exempt: false
- size: 3900
merkle_tree_storage_account:
- stores the state of a sparse Merkle tree
- tree height 18
- account_id: 2
- rent exempt: true
- size: 16658
nullifier_pda:
- is derived from a nullifier plus nullifier domain separation
- stores whether it is initalized and the account type
- account_id: 3
- rent exempt: true
- size: 2
leaves_pda:
- stores two Merkle tree leaves plus the public key of the respective Merkle tree
- account_id: 4
- rent exempt: true
- size: 106
user_account:
-
stores encrypted user utxos
-
can only be modified by the signer account which initialized the user account
-
account_id: 5
-
rent exempt: true
-
size: 34 + UTXO_SIZE * UTXO_CAPACITY
(currently UTXO_SIZE = 216 and UTXO_CAPACITY = 100)
Token Accounts:
user_pda_token:
- spl token account of the user
- deposit: funds are transferred from this account
- withdrawal: funds are transferred to this account
relayer_pda_token:
- spl token account of the relayer
- receives fees
merkle_tree_pda_token:
- spl token account of the Merkle tree
- this is the pool account to which tokens are transferred to at deposit and transferred from at withdrawal
authority:
- hardcoded authority account which is required to withdraw from merkle_tree_pda_token account
The Light Protocol program accepts 4 types of instructions initialized new merkle tree account, initialize new onchain user account, modifiy onchain user account, close onchain user account and transact with shielded pool.
Initializes a new Merkle tree account by copying hardcoded bytes into the new Merkle tree account. The hardcoded bytes are defined in config.rs.
initialize_merkle_tree_account_selector = 240u8 instruction_data: [ 8_bytes_buffer + initialize_merkle_tree_account_selector ]
Accounts:
- signer
- merkle_tree_storage_pda
- rent_sysvar_info
New initialization bytes can be generated for Merkle trees of arbitrary heights with cargo test merkle_tree_print_init_data_and_instruction_order.
Initializes a new user account which was created in a different instruction. The signer public key is stored as the account`s authority.
initialize_user_account_selector = 100u8
instruction_data: [ 8_bytes_buffer + initialize_user_account_selector ]
Accounts:
- signer
- user account
- rent_sysvar_info
Stores an arbitrary number of encrypted utxos at speficied indices. Existing utxo data at the index is overwritten.
modify_user_account_selector = 101u8 instruction_data: [ 8_bytes_buffer + modify_user_account_selector + (index , encrypted_utxo_bytes) + ... + (index , encrypted_utxo_bytes) ]
Accounts:
- signer
- user account
- rent_sysvar_info
Closes a user account by transferring all of the user_account lamports to the invoking signer account.
close_user_account_selector = 102u8 instruction_data: 8_bytes_buffer + close_user_account_selector
Accounts:
- signer
- user account
- rent_sysvar_info
A complete a shielded pool transaction consists out of 1502 instructions. These instructions are the default path and do not require a selector. The index of the current computational step is stored in the tmp_storage_account and incremented at the end of every instruction. The passed-in instruction data and accounts vary depending on the instruction index. There are five different phases in the following order the send_data_instruction, check_root_instruction, ZKP_verification_instructions, merkle_tree_insert_instructions, and the last instruction.
send_data_instruction:
The first instruction sends in all data required for the computation, creates and initializes the tmp_storage_account, saves the data in the tmp_storage_account, and checks the external data hash.
instruction_data: [ 9_bytes_buffer +
root, public amount, external data hash, nullifier0, nullifier1, leaf_right, leaf_left, proof, recipient, ext_amount, relayer, fee ]
Accounts:
- signer
- tmp_storage_pda
- system_program_id
- rent_sysvar_info
check_root_instruction:
Searches the Merkle tree root history array for the Merkle tree root of the ZKP. instruction_data: none
Accounts:
- signer
- tmp_storage_pda
- merkle_tree_storage_pda
ZKP_verification_instructions:
Perform ZKP verification. instruction_data: none
Accounts:
- signer
- tmp_storage_pda
merkle_tree_insert_instructions:
Calculates a new Merkle tree root by inserting two new leaves. The leaf hashes are the commitment hashes of the output utxos. The first of the merkle_tree_insert_instructions locks the Merkle tree to prevent a race condition of several transactions trying to update the Merkle tree at the same time. The root and new leaves are not inserted in this instruction phase but in the last_instruction. Thus, the Merkle tree is only updated once all checks of the shielded transaction are passed successfully. This approach prevents corruption of the Merkle tree after instruction_data: none
Accounts:
- signer
- tmp_storage_pda
- merkle_tree_storage_pda
last_instruction:
Checks and inserts nullifiers, checks external amount, transfers tokens, transfers fees, inserts new merkle root.
Nullifiers are checked by trying to create pda accounts derived from the respective nullifier. If the account creation fails the nullifier already exists.
A positive external amount means the shielded transaction is a deposit. In this case tokens are transferred from a user_token_pda to the merkle_tree_token_pda.
A negative external amount which is equal to relayer fees means the transaction is an internal shielded pool transfer. In this case only the fees are transferred to the relayer.
A negative external amount greater than the relayer fees result mean the transaction is a withdrawal. The withdrawal token amount is transferred from the merkle_tree_token_pda to the user_token_account. After that, fees are transferred to the relayer.
At the end of the instruction the new Merkle tree root is inserted into the Merkle tree and the lock is released.
instruction_data: none
Accounts:
- signer
- tmp_storage_pda
- two_leaves_pda
- nullifier0_pda
- nullifier1_pda
- merkle_tree_pda
- merkle_tree_pda_token
- spl_program
- token_program_account
- rent_sysvar_info
- authority
- user_pda_token
- relayer_pda_token