Skip to content

Commit

Permalink
Merge pull request #121 from hyperledger-labs/lock-nullifiers
Browse files Browse the repository at this point in the history
Locking by spending and creating UTXOs
  • Loading branch information
jimthematrix authored Jan 29, 2025
2 parents cb39891 + 925ee0c commit f962f1c
Show file tree
Hide file tree
Showing 143 changed files with 4,722 additions and 3,509 deletions.
54 changes: 54 additions & 0 deletions doc-site/docs/advanced/erc20-tokens-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# ERC20 Tokens integration

All fungible Zeto implementations support integration with an ERC20 contract via `deposit` and `withdraw` functions.

![erc20 integration](../images/erc20-deposit-withdraw.jpg)

## Deposit

When depositing, users take their balances in ERC20 and exchanges for the same amount in Zeto tokens. The deposited amount will be transfered to the Zeto contract to own, until the time when a withdraw is called.

The ZKP circuit for the deposit function contains the following statements:

- the commitments for the output UTXOs are based on positive numbers
- the commitments for the output UTXOs are well formed, obeying the `hash(value, owner public key, salt)` formula
- the sum of the output UTXO values are returned as the output signal, which can be compared with the `amount` value in the transaction call. aka `depositAmount == sum(outputs)`

One obvious observation with the deposit function is that it leaks the value of the output UTXO. For instance, consider the following transaction:

```javascript
deposit(amount, outputUTXO, proof);
```

The output UTXO's value will be equal to the `amount`. To mitigate this, the output is an array of UTXOs, of size `2`. This way the exact value of each of the UTXOs in the output is unknown except by the owner(s).

## Withdraw

When withdrawing, users spend their UTXOs in the Zeto contract and request for the corresponding amount to be transferred to their Ethereum account in the ERC20 contract.

The ZKP circuit for the withdraw function contains the following statements:

- the commitments for the output UTXOs are based on positive numbers
- the commitments for the input and output UTXOs are well formed, obeying the `hash(value, owner public key, salt)` formula
- the sum of the input UTXO values is subtracted by the sum of the output UTXO values, with the result returned as the output signal, which can be compared with the `amount` value in the transaction call. aka `sum(inputs) == sum(outputs) + withdarwAmount`

## How to use ERC20 integration

It's very easy to enable the ERC20 integration on any fungible Zeto implementation. Call `setERC20(erc20_contract_address)` to configure the ERC20 contract that the Zeto token should work with. That's it!

## deposit/withdraw vs. mint

A solution developer who considers using the ERC20 integration feature must take into account how this works alongside the `mint` function. While the `mint` function preserves the privacy of new token issuance inside the Zeto contract, it could lead to an insufficient balance in the ERC20 contract when the `withdraw` function is invoked.

Consider the following sequence of events:

- The Zeto contract is deployed and configured to work with an ERC20 contract
- Alice deposits 100 from her ERC20 balance
> Zeto contract's balance becomes 100
- The regulator mints 50 to Alice
- Bob deposits 100 from his ERC20 balance
> Zeto contract's balance becomes 200
- Alice withdraws all her 150 Zeto tokens
> Zeto contract's balance becomes 50
- Bob attempts to withdraw 100
> This will fail because the Zeto contract's balance is below the requested amount
30 changes: 30 additions & 0 deletions doc-site/docs/advanced/locks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Locking UTXOs

In a typical atomic swap flow, based on the popular ERC20 token standard, the tokens from the trading parties are transferred to an escrow contract, which then coordinates the settlements with all the trading parties to ensure safety for all the involved parties.

This type of swap design is not possible with Zeto tokens, unfortunately. An escrow contract can not own tokens because Solidity contract doesn't have the ability to generate ZK proofs required to spend Zeto tokens.

This is where the `locking` mechanism comes in.

![locking and spending](../images/locking-spending.jpg)

As illustrated above, regular (unlocked) UTXOs can be spent by any Ethereum account submitting a valid proof. This is an important privacy feature because it doesn't require the Ethereum transaction signing account to be tied to the ownership of the Zeto tokens. As a result, the Zeto tokens owner can use a different Ethereum signing key for each transaction, to avoid their transaction history to be analyzed based on the base ledger transactions.

On the other hand, a UTXO can be locked with a designated `spender`, which is an Ethereum account address. The owner of the token is still required to produce a valid proof, which then must be submitted by the designated `spender` key, signing the transaction to spend the locked UTXO(s).

![locking transaction](../images/locking-tx.jpg)

In the locking transaction above, a locked UTXO, \#3 was created. The owner is still Alice, but the spender has been set to the address of an escrow contract. This means Alice as the owner can no longer spend UTXO \#3, even though she can produce a valid spending proof. In order to spend a locked UTXO, a valid proof must be submitted by the designated spender.

## Lock, then delegate

The following diagram illustrates a typical flow to use the locking mechanism.

![locking flow](../images/locking-flow.jpg)

- Alice and Bob are in a bilateral trade where Alice sends Bob 100 Zeto tokens for payment, at the same time Bob sends Alice some asset tokens which are omitted from the diagram
- In transaction 1, `Tx1`, Alice calls `lock()` to lock 100 into a new UTXO \#3, by spending two existing UTOXs \#1 and \#2. The transaction also creates \#4 for the remainder value, which is unlocked. This transaction designates the escrow contract as the spender for the locked UTXO \#3
- Alice then sends another transaction, `Tx2`, to call `prepareUnlock()` on the escrow contract and sends a valid proof to the contract. This proof can be used to spend the locked \#3 UTXO and create \#5, which will be owned by Bob
> the contract will verify that the proof is valid for the intended UTXO spending
- Alice and Bob continues with the trade using the escrow contract logic. The details of the remainder of the trade flow are omitted
- When the trade setup is complete, and ready to settle atomically, a party can call the escrow contract to carry out the execution phase. The escrow contract calls Zeto to `transfer()` the locked UTXO \#3 and creates \#5, as was originally intended in the trade setup phase
64 changes: 64 additions & 0 deletions doc-site/docs/advanced/utxo-array-sizes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Supporting different UTXO array sizes

Using ZK proofs presents a special challenge for supporting UTXO inputs and outputs that are of different sizes. For instance, a transaction proposal that consumes 1 UTXO, owned by Alice, but generates 3 UTXOs to be owned by Bob, Charlie and Alice, will require a different circuit for the proof than a transaction that consumes 2 UTXOs and generates 2 UTXOs.

![different circuits](../images/array-sizes-different-circuits.jpg)
_Using different array sizes for the input signals require different verification circuits_

This is because a ZKP circuit must always perform the exact same computation. Therefore, if there are arrays in the input signals, the size of the arrays must be known at compile time.

![same circuit](../images/array-sizes-same-circuit.jpg)
_Using the same array sizes for the input signals require the same verification circuit_

For all Zeto token implementations, two sizes are chosen for the circuits: `2` and `10`.

## Size = 2

For example, the following top-level circuit is for the token implementation `Zeto_Anon`,

```
[file: zkp/circuits/anon.circom]
include "./basetokens/anon_base.circom";
component main { public [ inputCommitments, outputCommitments ] } = Zeto(2, 2);
```

The `Zeto(2, 2)` part provides fixed values for the parameterized circuit template from `basetokens/anon_base.circom`, which looks like this,

```
template Zeto(nInputs, nOutputs) {
signal input inputCommitments[nInputs];
signal input inputValues[nInputs];
signal input inputSalts[nInputs];
signal input outputCommitments[nOutputs];
signal input outputValues[nOutputs];
signal input outputSalts[nOutputs];
signal input outputOwnerPublicKeys[nOutputs][2];
...
}
```

The parameterized template support different array sizes for both the inputs and outputs, but for the final circuit to be compiled, we set the size to `2` for both the inputs and outputs. This corresponds to the Solidity function in the token implementation:

```javascript
function transfer(
uint256[] memory inputs,
uint256[] memory outputs,
Commonlib.Proof calldata proof,
bytes calldata data
) public returns (bool) { ... }
```

When a transaction calls this function with inputs and outputs sizes of 1 or 2, the Solidity code will pad the arrays to size 2, and use the verifier library generated from the above circuit (`Zeto(2, 2)`) to verify the proof.

## Size = 10

To support array size of 10 in the input signals, we simply set the size parameters to `10` in the top-level circuit:

```
[file: zkp/circuits/anon_batch.circom]
include "./basetokens/anon_base.circom";
component main { public [ inputCommitments, outputCommitments ] } = Zeto(10, 10);
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc-site/docs/images/erc20-deposit-withdraw.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc-site/docs/images/locking-flow.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc-site/docs/images/locking-spending.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc-site/docs/images/locking-tx.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified doc-site/docs/images/overview.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion doc-site/docs/images/zeto-arch.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 13 additions & 13 deletions doc-site/docs/implementations/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@ Zeto is not a single privacy-preserving token implementation. It's a collection

Below is a summary and comparison table among the current list of implementations.

| Fungible Token Implementation | Anonymity | History Masking | Encryption | KYC | Non-repudiation | Gas Cost (estimate) |
| ----------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------- |
| Zeto_Anon | :heavy_check_mark: | - | - | - | - | 326,583 |
| Zeto_AnonNullifier | :heavy_check_mark: | :heavy_check_mark: | - | - | - | 2,005,587 |
| Zeto_AnonEnc | :heavy_check_mark: | - | :heavy_check_mark: | - | - | 425,338 |
| Zeto_AnonEncNullifier | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | - | - | 2,472,994 |
| Zeto_AnonNullifierKyc | :heavy_check_mark: | :heavy_check_mark: | - | :heavy_check_mark: | - | 2,310,424 |
| Zeto_AnonEncNullifierKyc | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | - | 2,414,345 |
| Zeto_AnonEncNullifierNonRepudiation | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | - | :heavy_check_mark: | 2,763,071 |
| Fungible Token Implementation | Anonymity | History Masking | Locking | Encryption | KYC | Non-repudiation | Gas Cost (estimate) |
| ----------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------- |
| Zeto_Anon | :heavy_check_mark: | - | :heavy_check_mark: | - | - | - | 326,583 |
| Zeto_AnonNullifier | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | - | - | - | 2,005,587 |
| Zeto_AnonEnc | :heavy_check_mark: | - | :soon: | :heavy_check_mark: | - | - | 425,338 |
| Zeto_AnonEncNullifier | :heavy_check_mark: | :heavy_check_mark: | :soon: | :heavy_check_mark: | - | - | 2,472,994 |
| Zeto_AnonNullifierKyc | :heavy_check_mark: | :heavy_check_mark: | :soon: | - | :heavy_check_mark: | - | 2,310,424 |
| Zeto_AnonEncNullifierKyc | :heavy_check_mark: | :heavy_check_mark: | :soon: | :heavy_check_mark: | :heavy_check_mark: | - | 2,414,345 |
| Zeto_AnonEncNullifierNonRepudiation | :heavy_check_mark: | :heavy_check_mark: | :soon: | :heavy_check_mark: | - | :heavy_check_mark: | 2,763,071 |

| Non-Fungible Token Implementation | Anonymity | History Masking | Encryption | KYC | Non-repudiation | Gas Cost (estimate) |
| --------------------------------- | ------------------ | ------------------ | ---------- | --- | --------------- | ------------------- |
| Zeto_NfAnon | :heavy_check_mark: | - | - | - | - | 271,890 |
| Zeto_NfAnonNullifier | :heavy_check_mark: | :heavy_check_mark: | - | - | - | 1,450,258 |
| Non-Fungible Token Implementation | Anonymity | History Masking | Locking | Encryption | KYC | Non-repudiation | Gas Cost (estimate) |
| --------------------------------- | ------------------ | ------------------ | ------------------ | ---------- | --- | --------------- | ------------------- |
| Zeto_NfAnon | :heavy_check_mark: | - | :heavy_check_mark: | - | - | - | 271,890 |
| Zeto_NfAnonNullifier | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | - | - | - | 1,450,258 |

The various patterns in this project use Zero Knowledge Proofs (ZKP) to demonstrate the validity of the proposed transaction. There is no centralized party to trust as in the Notary pattern, which is not implemented in this project but [in the Paladin project](https://lf-decentralized-trust-labs.github.io/paladin/head/concepts/tokens/).

Expand Down
4 changes: 4 additions & 0 deletions doc-site/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ nav:
- Non-Fungible:
- Zeto_NfAnon: implementations/nf_anon.md
- Zeto_NfAnonNullifier: implementations/nf_anon_nullifier.md
- Advanced Topics:
- UTXO array sizes in ZKP circuits: advanced/utxo-array-sizes.md
- ERC20 tokens integration: advanced/erc20-tokens-integration.md
- Locks for multi-step trade flows: advanced/locks.md
- FAQs: faqs.md
- Glossary: glossary.md
- Contributing:
Expand Down
4 changes: 2 additions & 2 deletions go-sdk/integration-test/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ func (s *E2ETestSuite) TestZeto_anon_enc_SuccessfulProving() {
}

func (s *E2ETestSuite) TestZeto_anon_nullifier_SuccessfulProving() {
calc, provingKey, err := loadCircuit("anon_nullifier")
calc, provingKey, err := loadCircuit("anon_nullifier_transfer")
assert.NoError(s.T(), err)
assert.NotNil(s.T(), calc)

Expand Down Expand Up @@ -583,7 +583,7 @@ func (s *E2ETestSuite) TestZeto_nf_anon_SuccessfulProvingWithConcurrency() {
}

func (s *E2ETestSuite) TestZeto_nf_anon_nullifier_SuccessfulProving() {
calc, provingKey, err := loadCircuit("nf_anon_nullifier")
calc, provingKey, err := loadCircuit("nf_anon_nullifier_transfer")
assert.NoError(s.T(), err)
assert.NotNil(s.T(), calc)

Expand Down
50 changes: 9 additions & 41 deletions solidity/contracts/factory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ pragma solidity ^0.8.27;

import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IZetoFungibleInitializable} from "./lib/interfaces/izeto_fungible_initializable.sol";
import {IZetoNonFungibleInitializable} from "./lib/interfaces/izeto_nf_initializable.sol";
import {IZetoInitializable} from "./lib/interfaces/izeto_initializable.sol";

contract ZetoTokenFactory is Ownable {
// all the addresses needed by the factory to
Expand All @@ -27,13 +26,7 @@ contract ZetoTokenFactory is Ownable {
// the rest of the addresses are used to initialize
struct ImplementationInfo {
address implementation;
address depositVerifier;
address withdrawVerifier;
address lockVerifier;
address verifier;
address batchVerifier;
address batchWithdrawVerifier;
address batchLockVerifier;
IZetoInitializable.VerifiersInfo verifiers;
}

event ZetoTokenDeployed(address indexed zetoToken);
Expand All @@ -51,7 +44,7 @@ contract ZetoTokenFactory is Ownable {
"Factory: implementation address is required"
);
require(
implementation.verifier != address(0),
implementation.verifiers.verifier != address(0),
"Factory: verifier address is required"
);
// the depositVerifier and withdrawVerifier are optional
Expand All @@ -71,44 +64,27 @@ contract ZetoTokenFactory is Ownable {
// check that the registered implementation is for a fungible token
// and has the required verifier addresses
require(
args.depositVerifier != address(0),
args.verifiers.depositVerifier != address(0),
"Factory: depositVerifier address is required"
);
require(
args.withdrawVerifier != address(0),
args.verifiers.withdrawVerifier != address(0),
"Factory: withdrawVerifier address is required"
);
require(
args.batchVerifier != address(0),
args.verifiers.batchVerifier != address(0),
"Factory: batchVerifier address is required"
);
require(
args.batchWithdrawVerifier != address(0),
args.verifiers.batchWithdrawVerifier != address(0),
"Factory: batchWithdrawVerifier address is required"
);
require(
args.lockVerifier != address(0),
"Factory: lockVerifier address is required"
);
require(
args.batchLockVerifier != address(0),
"Factory: batchLockVerifier address is required"
);
address instance = Clones.clone(args.implementation);
require(
instance != address(0),
"Factory: failed to clone implementation"
);
(IZetoFungibleInitializable(instance)).initialize(
initialOwner,
args.verifier,
args.depositVerifier,
args.withdrawVerifier,
args.batchVerifier,
args.batchWithdrawVerifier,
args.lockVerifier,
args.batchLockVerifier
);
(IZetoInitializable(instance)).initialize(initialOwner, args.verifiers);
emit ZetoTokenDeployed(instance);
return instance;
}
Expand All @@ -122,20 +98,12 @@ contract ZetoTokenFactory is Ownable {
args.implementation != address(0),
"Factory: failed to find implementation"
);
require(
args.lockVerifier != address(0),
"Factory: lockVerifier address is required"
);
address instance = Clones.clone(args.implementation);
require(
instance != address(0),
"Factory: failed to clone implementation"
);
(IZetoNonFungibleInitializable(instance)).initialize(
initialOwner,
args.verifier,
args.lockVerifier
);
(IZetoInitializable(instance)).initialize(initialOwner, args.verifiers);
emit ZetoTokenDeployed(instance);
return instance;
}
Expand Down
Loading

0 comments on commit f962f1c

Please sign in to comment.