Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Locking by spending and creating UTXOs #121

Merged
merged 29 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
97f3894
spend inputs for locking
jimthematrix Jan 9, 2025
0e3de1a
Squashed commit of the following:
jimthematrix Jan 9, 2025
5a8e9f2
zeto lock with nullifiers
jimthematrix Jan 9, 2025
ac8c0f9
remove lock verifiers
jimthematrix Jan 14, 2025
4725fba
transfer() and transferLocked() with new circuits
jimthematrix Jan 14, 2025
61a0902
fix anon_nullifier and add locking support to nf_anon
jimthematrix Jan 15, 2025
e710d01
add locking support for NF token implementations
jimthematrix Jan 16, 2025
8a2504e
Add escrow contract based on Zeto_Anon for testing purposes
jimthematrix Jan 17, 2025
444a062
add escrow contracts to test locking flows
jimthematrix Jan 20, 2025
1a5a3a9
fix tests
jimthematrix Jan 22, 2025
eb8723f
fix circuit tests
jimthematrix Jan 22, 2025
7660df1
fix go-sdk integration test
jimthematrix Jan 22, 2025
7b3861b
update checks of inputs and output
jimthematrix Jan 22, 2025
a6a911e
update circuit integration tests
jimthematrix Jan 22, 2025
121185b
fix nf nullifier integration test
jimthematrix Jan 22, 2025
518cd5b
fix nf anon nullifier integration test
jimthematrix Jan 22, 2025
ff3fe10
fix test for the escrow flow
jimthematrix Jan 22, 2025
63a902a
fix anon_nullifier build and tests for batching
jimthematrix Jan 23, 2025
5f1a2df
fix deployment parameters for the factory
jimthematrix Jan 23, 2025
f4dff77
change the verifier initialization params to a struct
jimthematrix Jan 23, 2025
64966b5
move verifiers to the contracts/verifiers folder
jimthematrix Jan 23, 2025
c6c2d7e
fix factory unit tests
jimthematrix Jan 23, 2025
e7e96b7
fix tests for using the cloneable factory
jimthematrix Jan 24, 2025
073918c
consolidate solidity interfaces
jimthematrix Jan 24, 2025
7323357
use proof to check for existence in the locked commitments tree
jimthematrix Jan 27, 2025
a9c95c7
renaming of internal variables
jimthematrix Jan 27, 2025
1c54398
Update doc-site/docs/advanced/erc20-tokens-integration.md
jimthematrix Jan 28, 2025
8ced077
test for duplicate output utxos
Chengxuan Jan 29, 2025
925ee0c
Update inline comments in the transferLocked circuit
jimthematrix Jan 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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. If the mint function is used in addition to `deposit`, there may not be sufficient ERC20 balance in the Zeto contract to support all the `withdraw` calls.
jimthematrix marked this conversation as resolved.
Show resolved Hide resolved

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couldn't spot the difference here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's the spelling of "woner" vs. "owner" in the label on the bottom 😄

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
Loading