Intro
Tl;dr for experts — see here!
600 years after the Florentine magnates brought modern banking to medieval Europe, humans invented Bitcoin and the more general notion of blockchains. Blockchains and the decentralized finance systems which sit on top of them have several advantages over traditional banking and payment systems, including censorship resistance and cryptographically backed ownership of assets. One reality of of moving transactions onto blockchains, however, is that all data (sender, recipient, and amount, as well as account balances) becomes public by necessity — nodes, sequencers, challengers, and ZK provers alike all must be able to access the entirety of the blockchain state and every transaction which happens. This, in turn, makes blockchain-based financial systems awkward for individuals and businesses alike: the former may not want it to be publicly known that they purchase a $W coffee from coffee shop X every morning, and the latter may not want to publicly disclose that they pay $Y to vendor Z every month for services.
Such concerns about financial privacy mitigating the usability of blockchain-based financial systems led to the development of private transactions. Existing projects have been deployed on public blockchains but use zero-knowledge (ZK) proofs and related cryptographic techniques to establish transaction systems that conceal the identities of participants and the amount transferred. Privacy, however, can be a double-edged sword. The same mechanisms that protect ordinary users from surveillance also make it harder to identify stolen funds, enforce sanctions, or investigate financial crime. Various attacks and illicit behavior on existing systems, for example, resulted in allegedly hundreds of millions of dollars in stolen assets, money laundering prosecutions, and more.
To allow for use cases such as private peer-to-peer transactions, business payments, protocol fees, and anonymous lending and swapping, hugely increasing the usefulness of blockchain-based financial primitives, while severely limiting the ability of bad actors to move large amounts privately and even allowing a trusted third-party auditor to keep an eye on all transactions, the TFH Applied Research Team is sharing technical ideas toward a theoretical construction: WorldPx. To attain such accountability, WorldPx’s construction combines Proof of Human Sybil resistance properties with the notion of an anonymous budget (as proposed in this paper), and enforces verifiable encryption of all private transaction contents under an auditor key. The overall construction remains extremely flexible as well — this blog details how to extend the private peer-to-peer payment scheme to anonymous lending and swapping, but can be extended to even more DeFi primitives.
This blog also covers the decisions that went into designing WorldPx, and discusses its future directions. Note that WorldPx is a technical idea in exploration, and is not in production.
Preface: a correct, private, and accountable transaction system
In WorldPx, we explore a transaction system with three properties: correctness, privacy, and accountability. Informally, a correct, privacy-preserving peer-to-peer transaction system should have the following properties, which we list below. Note that our exploration assumes a working public blockchain with programmable smart contracts and an existing system of public accounts and balances.
WorldPx “API” (basic, informal)
The most basic peer-to-peer private transaction system includes the following three primitives:
- shield(addr, amount, type) — converts amount of token of type (like USDC or ETH) from addr owner's public balance to their private balance. In other words, the owner of addr's public balance should be reduced by amount and their private balance increased by amount. shield operation does not hide any of its inputs, but the final private balance of the user remains hidden.
- unshield(addr, amount, type) — converts amount of token of type from the addr owner's private balance to their public balance. Similar to shield, unshield also does not hide any of its inputs.
- private_transfer([sender_addr], [recipient_addr], [amount], [type]) — deducts amount from sender_addr's private balance of tokens of type and adds the corresponding amount to recipient_addr's private balance. In private_transfer, all input fields are private, even to the underlying blockchain. We use the bracket [] notation later as well to indicate private inputs.
Privacy properties (informal)
WorldPx as proposed would offer the following privacy guarantees:
- During a private_transfer() call, the [sender], [recipient], and [amount] involved are private (indistinguishable from random) to every party except the auditor and the sender/recipient.
- Every pair of private send operations are unlinkable against one another. In other words, if we have two transactions (sender_1, recipient_1, amount_1) and (sender_2, recipient_2, amount_2) , cases where sender_1 = sender_2 and sender_1 != sender_2 are indistinguishable from the public contents of the transactions (and similarly with the amount and recipient fields).
- A corollary of the above two properties is private balances, where users’ balances are not known to any party but the auditor; in fact, no other party learns about the changes to a user's balance, or even if there is a change at all (with the exception of shield/unshield operations).
Correctness / liveness guarantees (informal)
A functioning and secure private transaction requires the following guarantees:
- No negative private balances — senders cannot ever send more private token than they own.
- No double-spend — once a private token has been spent, it cannot be spent again.
- Ownership soundness — only the owner of a private token can spend it.
- System liveness/censorship resistance — the liveness + censorship resistance properties of the system can be reduced to that of the underlying blockchain which it resides within.
WorldPx would further guarantee conservation of value — money cannot be created nor destroyed. Every private token which is created must have a corresponding amount of public token which is locked up. As a corollary, WorldPx would also allow users to exchange their private funds for public ones at any time — since there is always a corresponding amount of locked-up public token, users can always exchange their private tokens for public ones if they wish to exit the system altogether.
Accountability guarantees (informal)
As mentioned in the introduction, existing private transactions projects were challenged by the reality of wishing to extend privacy to everyday transactions, but in the process enabled malicious uses such as private money laundering or transactions with sanctioned wallets. To allow legitimate users to still transact privately while making it prohibitively difficult for bad actors to use the system in a malicious way without being identified, our technical exploration targets the following two specific accountability properties —
- Limited private_transfer() and shield() amounts for each user within limited time periods, e.g. each user can only transfer $1000/month, in a manner which is cryptographically enforceable by the smart contract. This, in tandem with Proof of Human’s Sybil-resistance properties, severely raises the difficulty of private transfer at a large scale, and thus by default makes it difficult for bad actors who wish to money launder large amounts, e.g., to do so.
- Verifiable auditability via a third-party trusted auditor — to continue allowing any blockchain analysis company to work with us, flagging suspicious accounts and transactions, updating their blacklist, etc., our system should ensure that they still have access to all transaction information, in a way which is also cryptographically enforceable by the smart contract. This ensures that all the existing analysis and processes which auditors already run against World Chain can be applied with no change at all to the proposed private transactions system, and makes it as challenging for bad actors to use the proposed private system without being detected by the auditors as it would be for them to use any public transactions system being monitored by the same auditing party.
Setting up the stage
WorldPx is a proposed L2 private transaction system inspired by previous ZK-based UTXO commitment-style constructions, made accountable by combining Proof of Human with monthly private transfer budget and verifiable trusted third-party auditing, with potential expansions to include additional DeFi features like Uniswap and lending pools (e.g. Aave). Its core protocol relies on a smart contract on World Chain (Ethereum L2) to process anonymous user requests and maintain a public state.
WorldPx would use the Unspent Transaction Output (UTXO) model: user balances are expressed using individual unspent tokens, each a record of ownership of a certain amount of token value. During each private transaction, the sender nullifies a token they own and generates a token of the same value for the recipient. To ensure correctness, the sender proves to the blockchain that they own a valid token and that the total value of the spent tokens matches that of the newly created tokens. To preserve privacy, these statements are proven in zero knowledge. To provide accountability, the sender additionally proves that the transfer stays within the allowed budget and includes an encrypted record of the transaction for a third-party auditor.
The actors: users, the auditor, the blockchain, and its state
Our private transaction exploration consists of three essential components. The first are private users, Alice and Bob, who participate in the transaction system and can behave arbitrarily and maliciously. The second is a trusted auditing authority, which holds a special auditing key, allowing it to monitor the system. The auditor has access to all information regarding each transaction and may choose to audit the transactions at its own pace. The third component is a public L2 blockchain, which we assume to be censorship-resistant and secure against malicious actors. Its correctness can be independently checked by anyone, and all data stored in its state is publicly visible. Alice and Bob must therefore avoid revealing their secrets when interacting with it.
The L2 blockchain contains a smart contract, , for each type of private token. includes WorldPx’s publicly visible state, which consists of four structures: a commitment tree, whose leaves are the commitments to all tokens ever created, stored in a public Merkle tree; a nullifier set, which consists of the nullifier hashes (nullifiers) of all spent tokens, as a public set; an address tree, the leaves of which are the public identities of all registered users, and an encrypted transaction list, which encrypts every transaction in history in ciphertext in two copies, one for the recipient and the other for the auditor. As the system processes more and more transactions, all four structures grow. They never shrink. We describe these terms and definitions in more detail in later sections.
The cryptographic props: hashes, Merkle trees, public-key encryption, and zero-knowledge proofs
WorldPx’s proposed design relies on three core concepts: hashes, public-key encryption, and zero-knowledge proofs.
Hashes + hash commitments
A hash is a one-way, collision-resistant fingerprint of a value. We denote a hash over a value as . A hash is a one-way function: given only , it is computationally infeasible to recover . We use to denote a hash applied on the concatenation of and . The one-way property also guarantees is irrecoverable from , even when is known. It is also collision-resistant: even knowing , it is computationally infeasible to find a different value such that .
To ensure unambiguity of hash inputs, we use domain separators within our hash constructions — for example, our token’s inner hash commitment looks like the following, where "token" is the domain separator:
The collision-resistance + one-way properties of a hash allow it to be used as a commitment scheme, i.e. a set of functions and such that
- For message and blinding factor , we can compute commitment , which can simply be .
- For commitment and message and blinding factor , we an verify that commitment was generated exactly using message and blinding factor , using the function
- The binding property of the commitment ensures that it is computationally infeasible to find another tuple which will cause to return , and the hiding property of the commitment ensures that given just , a (polynomial time) adversary learns nothing about the which generated that commitment.
Merkle trees
Another application of hashing is that of Merkle trees, a cryptographic data structure which enables short membership proofs. Roughly speaking, a Merkle tree consists of a list of leaf nodes , and recursively builds up the next tree layer by taking the current layer and computing pairwise hashes, e.g. . In the final layer of the Merkle tree, there is a single node, denoted the tree root.
WorldPx’s construction stores token commitments on-chain as the leaves of a Merkle tree , where each leaf is a token commitment and each internal node is the hash of its two children. For Alice to show that her claimed token commitment exists, she proves that there is a path from her token hash to the root of the commitment tree, in zero-knowledge, to avoid revealing the exact leaf her token is in. The collision-resistance property of the hash guarantees that the correct root of , which is stored within a smart contract, can only be derived from a token hash which is a leaf . The same concept also applies to the address tree , where Alice can prove that she belongs to a list of users with Proof of Human without revealing her identity.
Public key encryption
A public-key (PK) cryptosystem enables any two parties to communicate with each other while hiding the content of the message from anyone else. To do so, all parties first derive a keypair, which consists of a private and public key. Everyone publishes their public keys. For a sender Alice to send a message to Bob, she encrypts it into a ciphertext using Bob’s public key. Since only Bob knows his private key, only he can decrypt the ciphertext and recover the transaction; to everyone else, the ciphertext reveals no information about its contents.
In our case, whenever Alice wishes to private_transfer() some tokens to Bob, she will include an encryption of the contents of the private transaction under Bob’s public key on chain, which allows Bob to scan through the contents of the blockchain and discover which transactions have him as a recipient, and the contents of the token commitments therein.
For efficiency, Alice can directly send the transaction contents to Bob if they have a previously established private side channel.
Zero-knowledge succinct argument of knowledge
Finally, a zero-knowledge (succinct) argument of knowledge (zk-SNARK) is a proof over the correctness of a statement, without revealing why it is true or disclosing the secret information used to prove it. In practical terms, a computationally-weak verifier (such as a smart contract) can force a powerful prover (such as Alice) to run a specific function correctly (such as private_transfer()!) and prove that outputs are correctly computed from inputs, while keeping private inputs and intermediate function values hidden from the verifier.
For example, as discussed earlier, Alice can use a zk-SNARK to show that she correctly computes a token commitment from a secret user address, serial number, token type, blinding factor, and value, without revealing any of those fields to the verifier smart contract! Within the same proof, and without exposing the commitment itself, Alice shows that a chain of secret values forms a valid Merkle path from her token commitment to the root of . She then reveals only the root, and the smart contract checks that Alice’s claimed root is equivalent to the one stored within itself.
The proof therefore shows that Alice knows a token commitment that belongs to the commitment tree and that both the commitment and Merkle path were computed correctly. Combined with the security properties of the hash function and Merkle tree, this convinces everyone that Alice owns a token in the tree without revealing the token’s private information, or even its commitment.
The acts: shield, unshield, assign_budget, and private_transfer
As briefly described earlier, there are three user actions: shield converts some public tokens into an owned private token; unshield spends an owned private token and adds its value to a public account; and private_transfer transfers some of their private tokens to another user. Conceptually, these actions behave similarly to their counterparts in prior private transaction constructions, but with additional accountability guarantees from auditing and budgets.
For Alice to be eligible in WorldPx, she needs to first register with a registration authority, where she presents her Proof of Human to verify that she is a unique human. She samples a random value as her private identity and publishes the corresponding public id , linked to a nullifier generated by her World ID (see here for details!). This address is added to the Merkle tree.
WorldPx’s UTXO model states that every transaction is performed by either generating new private tokens and/or nullifying existing ones: in particular, shield generates private tokens, unshield nullifies private tokens, and private_transfer does both.
Each private token has five components: **the blinding factor is a random value known only by the owner, the owner id is the token’s owner, as defined above, the token value is self-explanatory, and the serial number is a unique, public value to differentiate between tokens.
The token commitment uses the above fields, and is defined as a nested hash:
The token commitment’s corresponding nullifier is defined as , where the is the one corresponding with the field above. This ensures that a token cannot be spent twice, without revealing anything about which tokens have already been spent.
Why do we structure our token commitment in this way, rather than e.g. just a flat hash? When generating a new token in private_transfer(), the user sends the outer hash to the smart contract. This allows the smart contract to choose the serial number without learning any of the internal fields, and protects against a “fool's gold” attack, where a malicious sender chooses an already-used serial number, causing the recipient to not be able to spend the resulting token. When generating a new token in shield , the user sends the inner hash so that the smart contract can directly apply the public values of , , and (and the serial number!), without learning the blinding factor .
Shielding a token
Let refer to the smart contract corresponding to the token type (e.g. USDC) which Alice wishes to interact with. Note that contains the following —
- A public pool of token type which gets deposited into whenever users call shield, and which gets withdrawn from whenever users call unshield.
- The root and frontier of , the Merkle tree of Proof of Human verified participants.
- The root and frontier of , the Merkle tree of all valid private token commitments.
When Alice calls the shield functionality within , she sends amount of the token publicly to . She then samples the token randomness and computes , the inner component of the token commitment, then sends it to . The contract then looks up the corresponding to her address, generates a unique serial number (by looking at the index of the next empty leaf slot in ), and generates both the second inner commitment and the outer token commitment and inserts as a leaf in .
At this point, Alice owns a private token. During shield, all of these values except are public. This does not introduce additional privacy loss, since the shield operation originates from Alice’s public account and the corresponding transaction details are already visible on-chain. Importantly, no future transaction will ever reveal the same in the clear.
Unshielding a token
An unshield is the opposite. Alice nullifies her private token to withdraw the corresponding amount of public token from the smart contract. To do so, Alice generates a ZK proof stating that:
- She knows a secret user id which hashes to some public value .
- She knows a secret token with public value and type , whose commitment is a leaf in (i.e. has a Merkle path which leads to the on-chain root of ) without revealing , , or .
- Using the same and , she correctly computes the public nullifier .
- She correctly includes all relevant information about the transaction (in particular, the contents of the secret token) in an encrypted message for the auditor (i.e. encrypted with the auditor’s public key).
Alice then sends to and invokes the unshield smart contract function call. verifies , checks that has not previously appeared in the nullifier set, and looks up Alice’s wallet address corresponding to . If the checks pass, it publicly transfers tokens of to .
Transferring a token
Finally, a private_transfer combines token nullification and generation. WorldPx handles a general case for private_transfer in which Alice spends two input tokens and creates two output tokens. We assume Alice owns two tokens, and (note that is Alice’s public identity); if she doesn’t, she can always use shield to create a zero-value token. We denote the commitments to these tokens as .
Suppose Alice wants to transfer some amount to Bob, but might not be the exact amount she wishes to send, so she nullifies both above tokens and generates two new tokens with the same combined value: , owned by Bob (note that is Bob’s public identity) and carrying the transfer amount, and , returned to Alice as change. We denote the commitments to these tokens as .
Alice proves the correctness of this entire operation with a ZK proof , which establishes that:
- She knows a secret user id which hashes to a public ID , where is a leaf of . She does not reveal .
- She knows two valid input tokens , , both owned by , whose commitments are leaves of , without revealing anything about the tokens or their commitments.
- Using the same and from the two input tokens, she correctly computes their public nullifiers, and .
- She then creates two output token templates as described above, and . Alice proves that , i.e. no value was created nor destroyed (range checks required here to prevent field overflow).
- She then computes the inner commitments for and (note that she does not compute the final for either since picks ):
- She shows that the s are all the same, and that matches the ones in .
- She correctly encrypts the transaction details into a ciphertext for the auditor.
She sends , and an encrypted message for Bob to and invokes the private_transfer function. The function checks that both nullifiers are unused and the verifies. If the checks pass, it generates unique serial numbers for the new output tokens and hashes them into the corresponding partial commitments, then inserts these into .
Finally, Alice can then either notify Bob directly or rely on Bob to scan the public state and discover the transaction himself.
Tl;dr for experts
Note: If you’ve read the remainder of the blog up to this section, feel free to skip to the “Proof of Human for Sybil Resistance” section below!
WorldPx is a proposed augmented version of a standard UTXO commitment-based system which is (almost entirely) implementable as a pure smart contract on World Chain, an Ethereum-based L2. Note that WorldPx is in the exploratory stage, and is not implemented. This blog is an outline of what we feel is a clean, extensible design with accountability built-in. Key design choices in the basic peer-to-peer transaction construction include the following —
- WorldPx uses Poseidon over BN-254’s scalar field to build its token commitment function, Merkle trees, and nullifier function.
- WorldPx uses Groth16 over the BN-254 curve as its ZK proof system, and verifies Groth16 proofs directly via a verifier smart contract on World Chain.
- WorldPx allows the smart contract to choose the newly minted token’s serial number during a private_transfer() call by using the Merkle index where the newly minted token’s commitment will live as a Merkle leaf. This constrains us to use a doubly-nested commitment where the sender computes a partial Poseidon commitment by absorbing the commitment’s blinding factor and the smart contract itself “completes” the commitment by absorbing the serial number, recipient public address, token type, and token value, but allows us to avoid serial number collisions and prevents the so-called “fool’s gold” attack where a sender simply burns a token rather than actually sending it to the recipient, by maliciously issuing a duplicate serial number token for the recipient.
- Nullifiers are stored directly within the smart contract, as is the entire Merkle tree of token commitments. We can optimize both of these, but such optimization is outside the scope of this blog post.
- Notation-wise, let Alice be the canonical sender, and let Bob be the canonical recipient. Let be the private token smart contract for token type .
The Acts II: Proof of Human, Anonymity Budget, Verifiable Encryption
Proof of Human for Sybil Resistance
In theory, to use Proof of Human for Sybil resistance, recall that WorldPx’s token commitment structure is as follows:
In particular, recall that is the token owner’s public ID associated with a World Chain address . In WorldPx’s case, there is a separate Merkle tree (denote this ) stored within the smart contract whose leaves are exactly the set of all s who have registered with the smart contract via Proof of Human.
The only way a user can insert() themselves into is to provide a Proof of Human ZK proof which absorbs into its transcript (but not its nullifier!) and computes the (Proof of Human, not WorldPx) nullifier
Thus, in order to perform a shield() or private_transfer() call, the corresponding ZK proofs require the sender to additionally prove that the field within is a member of . This ensures that every account which is attempting to shield or privately transfer is tied to Proof of Human, and allows WorldPx to support roughly one WorldPx-enabled account per unique human.
Anonymity budget a la UTT
With Sybil resistance properties from Proof of Human, WorldPx can now implement the “budget token” idea inspired by UTT.
WorldPx expands to contain yet another Merkle tree (denote this ) whose leaves contain “budget token” commitments of the following form:
The idea is as follows — every month, each user is allotted “anonymity budget” to spend, i.e. the total amount of tokens they send via private_transfer() cannot exceed . Our exploration supports two ways for budget assignment. The simple way is to have itself “batch insert” a bunch of leaves into fresh , where for each leaf (containing ) in , it creates a with a blinding factor and expiration equal to the end of the time period (e.g. one month). A second approach is to have each user manually request their budget at the beginning of each month, e.g. via an assign_budget() call to . This function would take a from above, and check that the who is requesting is a member of . Finally, would create from standard fields (, , ) after picking a unique , and add the commitment to .
During a private_transfer() call, WorldPx adds the following constraints to the proof:
- The field within is the same as the one within (the token which is being spent)
- The sender (Alice) creates a new which contains the same , and (expiration date) fields as the current one, and additionally contains as its “value” field, where is the amount sent in the private_transfer() call. Of course, the user needs to prove .
- Alice computes to nullify the existing budget token.
- inserts into and adds into its nullifier pool, thus nullifying .
Note that this anonymity budget idea depends on Proof of Human: during private account creation, a nullifier limits each unique human to one WorldPx-enabled account. Without this Sybil-resistance property, a single person could simply create unlimited accounts and privately transfer as much as they please.
Verifiable encryption for auditor
Just the above, however, is not sufficient to support an auditable design — future laws or regulation bodies may require a stronger notion of compliance, and thus ideally WorldPx would have the ability to give an external auditor/monitor the ability to “peer into” the contents of all transactions on-chain, without giving even ourselves (i.e. the operators of the World Chain sequencer) the ability to do so!
To satisfy these compliance requirements, we assume an auditor with keypair and encryption scheme . Every time Alice creates or nullifies a private token, she includes the following ciphertext alongside each transaction:
This is the exact contents of and it allows the holder of to read the entire private_transfer() call in plaintext!
A malicious sender can, of course, simply encrypt random garbage. We thus force Alice to prove (as part of ) that the values which reside within are the same as those which open the corresponding token commitments.
Finally, to express the stream cipher relatively efficiently in circuit, we instantiate the above cipher via an ECDH + Poseidon instantiation of HPKE, using Poseidon as an AEAD stream cipher (we simply add an extra squeeze to receive a tag at the very end).
Encore: expanding WorldPx for DeFi
In this blog, we introduced an exploratory technical approach, WorldPx, which includes a basic private transaction model that achieves user privacy and transaction accountability across three operations: shield, unshield, and private_transfer. Our longer-term goal is to support decentralized finance (DeFi), where users can access financial services such as trading, lending, asset swapping, and borrowing directly on-chain. Building on the same core concepts and cryptographic tools, we sketch high-level designs for extending WorldPx to support several specific DeFi functionalities.
First, we introduce two new functions to : shield_for_recipient and unshield_for_recipient. These functions extend shield and unshield, respectively, by allowing funds to move between one user’s public account and another user’s private address.
- shield_for_recipient(sender, [recipient], amount, type) sends amount of token type from sender's public balance to , then mints a new private token of type type and worth amount in the name of recipient.
- unshield_for_recipient([sender], recipient, amount, type) nullifies a private token owned by sender with type type and worth amount, then sends the same amount of token type to recipient's public balance.
While technically straightforward, a general version of these creates a potential loophole in the monthly budget: unlike the original operations, which only move funds within the same user’s public/private account pair, these operations can act as transfers outside private_transfer. Since these functionalities are extraneous in the context of regular peer-to-peer transactions, we can start by simply having a whitelist of private addresses which shield_for_recipient() and unshield_for_recipient() are allowed to interact with.
Note that both applications will be identity-hiding, but not private, in the sense that the amounts and token types being exchanged will be known. The reason for this is as follows — in both cases, the action (adding capital to a lending pool, swapping one token type for another at some exchange rate) changes the global public state of the DeFi smart contract in some deterministic way! In the former case, anyone observing the state of the lending pool will immediately see that tokens of were just deposited, while anyone observing the state of the swap pool will also see that “ tokens of were just exchanged for tokens of ”.
Additionally, such global state cannot be made private without fundamentally changing the financial primitive which these services provide. In the case of a lending pool, a borrower needs to learn the exact amount of tokens in the pool so they can decide how much to borrow, and for asset swap, they need to know the exchange rate. We thus do not attempt to hide the amounts or token types which are involved, and instead focus on hiding the identities of the users who are interacting with the services.
DeFi sketch 1: identity-hiding lending pool
We model the vanilla lending pool off a simplified version of Aave, and assume the point of view of an asset lender (not a borrower). Let denote the lending pool smart contract. Roughly speaking, there is a “compound factor” which represents the cumulative compound interest factor from day 1 to day . stores as a variable that periodically increases. To use such a service, a lender calls two smart contract functions —
- deposit_to_pool(sender, amount, type) , which sends amount of token type type to and receives of a “pool token” type (which we’ll call pool_type) in return.
- withdraw_from_pool(recipient, pool_token_amount, pool_type), which sends pool_token_amount of the pool token pool_type to and receives amount of token type in return.
In the identity-hiding version, we modify the functionality of deposit_to_pool() and withdraw_from_pool() slightly (indeed, by simply calling our helper functions shield_for_recipient() and unshield_for_recipient() , as described earlier):
- We augment with an additional Merkle tree, , whose leaves will contain blinded commitments to all “pool tokens” which have been issued to users. will also contain a corresponding nullifier set for .
- Commitments to pool tokens will be structured slightly differently —
- The reason for the above is similar to that of private transfer -- the smart contract shouldn’t learn any of the lender’s secrets, but needs to compute and “in real time”, and ensure that is the correct pool_type corresponding to the type of token which was just deposited.
- identity_hiding_deposit_to_pool([lender], amount, type) now first has the lender call ’s unshield_for_recipient([lender], $S_\text{pool}$, amount, type) function, which nullifies their private token of type worth amount and sends the corresponding amount publicly to . Next, the lender sends a partial commitment ( from above) to , who uses this, alongside computing a pool token amount using its and the amount which was deposited, as well as a new serial number , to mint a new private pool token commitment . Finally, appends to , which means that the lender now owns a private pool token worth .
- identity_hiding_withdraw_from_pool([lender], pool_token_amount, pool_type) now takes lender private pool token with amount pool_token_amount and nullifies it. Next, computes the corresponding and calls the shield_for_recipient($S_\text{pool}$, [lender], amount, type) function within , depositing to and minting a new private token of type type for lender (note that the lender themselves needs to compute this partial/inner commitment and pass it through , since doesn’t have the information by itself). Finally, appends the new private token commitment to , which means that the lender now owns a private token worth .
DeFi sketch 2: identity-hiding swap
Similarly, we model the simplified version of a swap protocol off of Uniswap. In this case, we assume that point of view of a trader (not a pool LP) who hopes to swap tokens of type for those of . We denote the swap smart contract as .
- Assume that has a paired pool specifically between tokens and . In particular, this paired pool has some amount of each type, , and an exchange rate which is determined by the amounts, .
- Our trader calls a single function, swap(trader, type_1, type_2, amount) , i.e. “swap amount of token type type_1 for as much type_2 as possible”. Let denote the amount of token remaining if amount of is deposited. We have that can be written as the following —
- computes , i.e. the total amount of token which should be given back to the trader, and sends this to the trader, updating the pool balance in the process.
In the identity-hiding swap case, we instead have identity_hiding_swap([trader], type_1, type_2, amount):
- The trader calls ’s unshield_for_recipient([trader], $S_\text{pool}$, amount, type_1) function, which nullifies a private token owned by trader of type_1 worth amount. sends the resulting public amount of type_1 token to .
- computes , as described above, and calls ’s shield_for_recipient($S_\text{swap}$, [trader], amount_2, type_2) function. In other words, sends amount_2 of type_2 publicly to (alongside a pre-populated partial commitment from trader), who mints a new private token worth amount_2 of type_2 and adds the commitment to .
- The above means that trader now owns a private token of type_2 worth amount_2, thus completing the swap without ever revealing the identity of trader.
Outro: applause… for you!
If you’ve made it this far, thank you so much for reading our blog post! We’d like to note once again that none of the above is implemented or in production, and if this is the kind of stuff you like thinking about/working on (especially if you have any questions or extensions to the above), please do reach out to us!