Solidity
Overview
Pip.solcontract has 2 main functions:deposit()andwithdraw(). Anyone can check a proof withcheckProof()to see if the proof is valid and if it has been used to withdraw yet. Owner can withdraw fees accrued viawithdrawFees().PoseidonT3.solon-chain hasher library is used to poseidon hash stuff.PlonkVerifierPPOT.solis the on-chain PLONK verifier created using PPoT phase 1 file. See more here: PLONK & PPoT.
Design Choices & Notes
Combined PRC20 & PLS functionality into one contract seamlessly by denoting PLS interactions with
address(0)token address.ZERO_VALUEexists to fill up sibling nodes with default zero values for tree nodes that are untouched, to reduce computation.Merkle tree data structure is used to store commitments because it's the most efficient, and the only plausible option when dealing with circuits. Using a mapping requires iteration through keys, which would be impossible in a circuit. The same for arrays, always exhaustively having to iterate through the array. With a merkle tree, we can always do the proof in a
log(n)succinct manner to prove the commitment exists for a particular root.System allows for multiple merkle trees to be generated. This reduces gas burden on depositors.
All the PIP pools only require the single verifier. The verification process and circuit logic is
denominationandtokenagnostic because the circuit is only concerned with constraining witness inputs to create a valid merkle tree.Proofs generated off-chain always use the latest root in the system to keep depositor-recipient anonymous. If the same deposit root was used during a withdraw, observers could easily speculate that the recipient is the person who deposited at that specific old root. By always using the latest root, this problem is solved.
Given the public values
nullifierHashandcommitment, it is impossible for any observer to compute the commonnullifierwhich generated them. Mathematically, given Hash(X, 1) and Hash(X, 2), finding X cannot be done faster than brute force.The
nullifierHashis the core element that prevents double spends. The beauty is that regardless of the root, regardless of the recipient, thenullifierHashstays the SAME, and there is only ONE per unique deposit, since it is derived from the user's singlenullifierand the specificleafIndexwhich is set in stone.Given a
nullifierHash, nobody can determine which leaf it corresponds to, unless they also know the nullifier.Proofs cannot be tampered during a withdraw because the witness inputs used to generate that proof take ALL the input signals as seen in the circuit. Any tampering of the witness inputs leads to an invalid proof, assuming there are no unconstrained intermediary signals in the circuit, which there are none.
For example, nobody can change the
recipient,gas, orfeevalues when callingwithdraw()because theverifyProof()function uses bilinear pairing checks against all witness inputs to regenerate the proof.
The
siblingNodesmapping can remain empty and doesn't need to be filled during construction. The first deposit properly sets the sibling nodes. Because the first deposit's path indices are all 0, at every level up the tree they will get set to a new value, per_insertCommitment().There was no reason to generate a genesis root value during construction, this happens naturally during the first deposit.
If someone deposits right when a withdraw is about to occur, everything functions normally. The withdraw is using "snapshotted" data at a point in time from the merkle tree, which will always be correct. At every point in the life of a merkle tree, it's data is permanent and true.
Generating a proof off chain is done in the user's browser. The finalized proof can be safely viewed publicly because it only displays the public signals and EC points. The EC points have taken into account all the public and private signals, but nobody can decipher the private signals from the EC points.
Frontend uses
window.crypto.getRandomValues()which uses OS secure RNG based on thermal noise and hardware events to generate the nullifier.
Last updated