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

More examples in the backend #159

Merged
merged 12 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
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
6 changes: 6 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ jobs:
run: |
cd backend
cargo test --release -- --nocapture

- name: Test example
run: |
cd backend
cargo run --release --example summa_solvency_flow

85 changes: 72 additions & 13 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,27 +63,86 @@ The following steps are optional and are only required if you need to update the

By completing these steps, the backend will be primed with the essential verifiers for its tasks.

## Examples
## Summa solvency flow example

### Running the Inclusion Verification
This example illustrates how Summa interacts with the Summa contract and the user side.

This example demonstrates how a user can verify the inclusion of their account in the Merkle Sum Tree.
In this example, the CEX provides the user with their `balances` and `username`, but not the `leaf_hash`.
To execute this example, use the command:

```
cargo run --release --example summa_solvency_flow
```

### 1. Submitting Address Ownership to the Summa Contract

First, we submit proof of address ownership to the Summa contract. This is a critical step to register these proofs on-chain, facilitating the validation of asset ownership within Summa.

Key points:

- An instance of `AddressOwnership`, named `address_ownership_client`, is initialized with the `signatures.csv` file, which contains the signature data.

- The `dispatch_proof_of_address_ownership` function sends a transaction to the Summa contract to register CEX-owned addresses.

- After dispatching the transaction, the example computes the hashed addresses (address_hashes) to verify they've been correctly registered in the Summa contract

The user will generate the `leaf_hash` themselves and then verify its inclusion in the tree.

Make sure you have the required files:
- `backend/ptau/hermez-raw-11`
- `backend/src/apis/csv/assets.csv`
- `zk_prover/src/merkle_sum_tree/csv/entry_16.csv`
Note: This demonstration takes place in a test environment. In real-world production, always ensure that the Summa contract is correctly deployed on the target chain.

If executed successfully, you'll see:

To run the example:
```
cargo run --example verify_inclusion
1. Ownership proofs are submitted successfully!
```

On successful execution, you'll observe a message indicating the verification outcome:

### 2. Submit Proof of Solvency
Copy link
Member

Choose a reason for hiding this comment

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

Can you specify that the goal of the proof of solvency is also to check that the assets controlled by the exchange are greater than the liabilities (as committed in the Merkle Sum Tree Root Hash)


Before generate inclusion proof for every user of the current round, You should submit proof of solvency to Summa contract. Currently, we made this as mandatory way to commit the root hash of the Merkle Sum Tree.

Without this process, It seems the user may not trust to the inclusion proof for the round. becuase the `mst_root` is not published on contract. More specifically, it means that the `mst_root` is not correctly verified on the Summa contract.

In this example, we'll guide you through the process of submitting a solvency proof using the Round to the Summa contract.
The Round serves as the core of the backend in Summa, and we have briefly described it in the Components section.

To initialize the `Round` instance, you'll need paths to specific CSV files (`assets.csv` and `entry_16.csv`) and the `ptau/hermez-raw-11` file. Here's what each file does:

- `assets.csv`: Calculates the total balance of assets for the solvency proof. Only the CEX can generate this file.
- `entry_16.csv`: Used to build the Merkle sum tree, with each leaf element derived from sixteen entries in the CSV.
- `ptau/hermez-raw-11`: Contains parameters for constructing the zk circuits.

Using the `Round` instance, the solvency proof is dispatched to the Summa contract with the `dispatch_solvency_proof` method.

If this step successfully ran, you can see this message:

```
2. Solvency proof is submitted successfully!
```

### 3. Generating and Exporting Inclusion Proofs

Assuming you're a CEX, after committing the `solvency` and `ownership` proofs to the Summa contract, you should generate inclusion proofs for every user. This proof verifies the presence of specific elements in the Merkle sum tree, which is part of the solvency proof.

After generating the inclusion proof, it's transformed into a JSON format for easy sharing.

Upon successful execution, you'll find a file named `user_0_proof.json` and see the following message:

```
3. Exported proof to user #0, as `user_0_proof.json`
```

### 4. Verify Proof of Inclusion

This is the final step in the Summa process and the only part that occurs on the user side.

Users receive the proof for a specific round and use methods available on the deployed Summa contract. Importantly, the Summa contract verifier function is a view function, meaning it doesn't consume gas or change the blockchain's state.

In this step, you'll see:

- Retrieve the `mst_root` from the Summa contract and match it with the `root_hash` in the proof.
- Ensure the `leaf_hash` aligns with the hash based on the `username` and `balances` provided by the CEX.
- Use the `verify_inclusion_proof` method on the Summa contract to validate the proof.

The result will display as:
```
Verifying the proof result for User #0: true
4. Verifying the proof on contract verifier for User #0: true
```
190 changes: 190 additions & 0 deletions backend/examples/summa_solvency_flow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
#![feature(generic_const_exprs)]
use std::{error::Error, fs::File, io::BufReader, io::Write};

use ethers::{
abi::{encode, Token},
types::{Bytes, U256},
utils::keccak256,
};
use serde_json::{from_reader, to_string_pretty};

use summa_backend::{
apis::{
address_ownership::AddressOwnership,
round::{MstInclusionProof, Round},
},
tests::initialize_test_env,
};
use summa_solvency::merkle_sum_tree::utils::generate_leaf_hash;

const N_ASSETS: usize = 2;
const USER_INDEX: usize = 0;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// Initialize test environment without `address_ownership` instance from `initialize_test_env` function.
let (anvil, _, _, _, summa_contract) = initialize_test_env().await;

// 1. Submit ownership proof
//
// Each CEX prepares its own `signature` CSV file.
let signature_csv_path = "src/apis/csv/signatures.csv";
let mut address_ownership_client = AddressOwnership::new(
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
anvil.chain_id(),
anvil.endpoint().as_str(),
summa_contract.address(),
signature_csv_path,
)
.unwrap();

// Retrieve hashed addresses using the `keccak256` method.
let address_hashes = address_ownership_client
.get_ownership_proofs()
.iter()
.map(|x| keccak256(encode(&[Token::String(x.cex_address.clone())])))
.collect::<Vec<[u8; 32]>>();

// Dispatch the proof of address ownership.
// the `dispatch_proof_of_address_ownership` function sends a transaction to the Summa contract.
address_ownership_client
.dispatch_proof_of_address_ownership()
.await
.unwrap();

// If the `addressHash` isn't found in the `addressOwnershipProofs` mapping of the Summa contract,
// it will return 0; otherwise, it will return a non-zero value.
//
// You can find unregistered address with null bytes as follows:
//
// let unregistered = summa_contract
// .ownership_proof_by_address([0u8; 32])
// .call()
// .await
// .unwrap();
//
// assert_eq!(unregistered, 0);

// Verify whether the addresses have been registered within the Summa contract.
for address_hash in address_hashes.iter() {
let registered = summa_contract
.ownership_proof_by_address(*address_hash)
.call()
.await
.unwrap();

assert_ne!(registered, U256::from(0));
}
println!("1. Ownership proofs are submitted successfully!");

// 2. Submit solvency proof
//
// Initialize the `Round` instance to submit the proof of solvency.
let asset_csv = "src/apis/csv/assets.csv";
let entry_csv = "../zk_prover/src/merkle_sum_tree/csv/entry_16.csv";
let params_path = "ptau/hermez-raw-11";

// Using the `round` instance, the solvency proof is dispatched to the Summa contract with the `dispatch_solvency_proof` method.
let mut round = Round::<4, 2, 14>::new(
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", // anvil account [0]
anvil.chain_id(),
anvil.endpoint().as_str(),
summa_contract.address(),
entry_csv,
asset_csv,
params_path,
1,
)
.unwrap();

// Sends the solvency proof, which should ideally complete without errors.
assert_eq!(round.dispatch_solvency_proof().await.unwrap(), ());

// You can also use the `solvency_proof_submitted_filter` method to check if the solvency proof is submitted.
// println!("{:?}", summa_contract
// .solvency_proof_submitted_filter()
// .query()
// .await
// .unwrap(););

println!("2. Solvency proof is submitted successfully!");

// 3. Generate Inclusion Proof
//
// In a production setup, the CEX should first dispatch the solvency proof to update the Merkle sum tree's root before generating any inclusion proofs.
// Otherwise, users might distrust the provided `root_hash` in the inclusion proof, as it hasn't been published on-chain.
let inclusion_proof = round.get_proof_of_inclusion(USER_INDEX).unwrap();

let filename = format!("user_{}_proof.json", USER_INDEX);
let mut file = File::create(filename.clone()).expect("Unable to create file");
let output = to_string_pretty(&inclusion_proof).unwrap();
file.write_all(output.as_bytes())
.expect("Failed to write JSON to file");

println!(
"3. Exported proof to user #{}, as `{}`",
USER_INDEX, filename
);

// 4. Verify Inclusion Proof
//
// The `snapshot_time` denotes the specific moment when entries were created for the Merkle sum tree.
// This timestamp is established during the initialization of the Round instance.
let snapshot_time = U256::from(1);

// When verifying the inclusion proof from the user's perspective, the user have to fetch `proof`.
// Assume that the `proof` file has been downloaded from the CEX.
let proof_file = File::open(format!("user_{}_proof.json", USER_INDEX))?;
let reader = BufReader::new(proof_file);
let downloaded_inclusion_proof: MstInclusionProof = from_reader(reader)?;

let public_inputs = downloaded_inclusion_proof.get_public_inputs();

// Verify the `leaf_hash` from the proof file.
// It's assumed that both `user_name` and `balances` are provided by the CEX.
let user_name = "dxGaEAii".to_string();
let balances = vec![11888, 41163];

let leaf_hash = public_inputs[0][0];
assert_eq!(
leaf_hash,
generate_leaf_hash::<N_ASSETS>(user_name.clone(), balances.clone())
);

// Before verifying `root_hath`, convert type of `proof` and `public_inputs` to the type of `Bytes` and `Vec<U256>`.
let proof: Bytes = Bytes::from(inclusion_proof.get_proof().clone());
let public_inputs: Vec<U256> = inclusion_proof
.get_public_inputs()
.iter()
.flat_map(|input_set| {
input_set.iter().map(|input| {
let mut bytes = input.to_bytes();
bytes.reverse();
U256::from_big_endian(&bytes)
})
})
.collect();

// Get `mst_root` from contract. the `mst_root` is disptached by CEX with specific time `snapshot_time`.
let mst_root = summa_contract
.mst_roots(snapshot_time)
.call()
.await
.unwrap();

// Match the `mst_root` with the `root_hash` derived from the proof.
assert_eq!(mst_root, public_inputs[1]);

// Validate the inclusion proof using the contract verifier.
let verification_result = summa_contract
.verify_inclusion_proof(proof, public_inputs, snapshot_time)
.await
.unwrap();

println!(
"4. Verifying the proof on contract veirifer for User #{}: {}",
USER_INDEX, verification_result
);

Ok(())
}
Loading