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
135 changes: 125 additions & 10 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,25 +65,140 @@ By completing these steps, the backend will be primed with the essential verifie

## Examples
enricobottazzi marked this conversation as resolved.
Show resolved Hide resolved

### Running the Inclusion Verification
The sequence in which the examples are introduced closely relates to the steps of the Summa protocol.
These examples will help to understand how the Summa works 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`.

The user will generate the `leaf_hash` themselves and then verify its inclusion in the tree.
### 1. Generating Message Signatures

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`
This example illustrates how to generate a CSV file containing signatures derived from a specific message, crucial for establishing `AddressOwnership`.
Creating the `signatures.csv` file is a preliminary step for initializing a `Round` in Summa.

This demonstration is introduced to be adaptable across various scenarios. For instance, you can compile this example that modified to support harware wallet into an executable binary, enabling an operator to run it with a hardware wallet. This operation should ideally be conducted within a secure environment inside a CEX, ensuring the system is isolated from any online networks to maximize protection against potential external threats.

The generated signatures are stored in a CSV file, utilizing a custom delimiter for easy parsing and verification. The output file is located at `src/apis/csv/signatures.csv`.

To run the example:
```
cargo run --example verify_inclusion
cargo run --example generate_signatures
```

Note: While this example employs hardcoded private keys for simplicity, it's essential to remember that exposing private keys directly in real-world applications can pose serious security risks. Therefore, it's recommended to create your own `signer` that taps into secure mechanisms, such as hardware wallets or protected key vaults.

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

This example demonstrates the process of submitting proof of address ownership to the Summa contract. After generating signatures for asset ownership (as shown in the `generate_signature` example), this step is essential to register those proofs on-chain, facilitating the validation of asset ownership within Summa.

In this example, a test environment is set up with the anvil instance by invoking the `initialize_test_env` method. This environment is also utilized in other examples such as `submit_solvency` and `verify_inclusion_on_contracts`.

Key points to note:

The instance of `AddressOwnership` is initialized with `signatures.csv`, and is named `address_ownership_client`. This instance has already loaded the signature data.

The `dispatch_proof_of_address_ownership` function sends a transaction to the Summa contract, registering the addresses owned by the CEX on the contract.

After dispatching the transaction via `dispatch_proof_of_address_ownerhip`, the example computes the hashed addresses (address_hashes) to verify they have been correctly registered on the Summa contract.

To execute this example:
```
cargo run --example submit_ownership
```

Upon successful execution, you should see the message:
```
Ownership proofs are submitted successfully!
```

Reminder: 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.

### 3. Submit Proof of Solvency

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

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, several parameters are required, including paths to specific CSV files (`assets.csv` and `entry_16.csv`), as well as a path to the ptau file (`ptau/hermez-raw-11`).

The roles of these files are as follows:
- `assets.csv`: This file is essential for calculating the total balance of assets for the solvency proof. Currently, only the CEX can generate this asset CSV file in its specific manner.

- `entry_16.csv`: This file is used to build the Merkle sum tree, where each leaf element originates from sixteen entries in the CSV.
X
- `ptau/hermez-raw-11`: Contains the Powers of Tau trusted setup parameters, essential for constructing the zk circuits.

An instance of Round dispatches the solvency proof using the `dispatch_solvency_proof` method.

To execute this example:
```
cargo run --example submit_solvency
enricobottazzi marked this conversation as resolved.
Show resolved Hide resolved
```

Upon successful execution, you will see the message:

```
"Solvency proof is submitted successfully!"
```

On successful execution, you'll observe a message indicating the verification outcome:
### 4. Generating and Exporting Inclusion Proofs

Assuming you are a CEX, let's say you've already committed the `solvency` and `ownership` proofs to the Summa contract. Now, you need to generate inclusion proofs for every user.

In this example, we demonstrate how to generate and export user-specific inclusion proofs using the Round. This proof is crucial for users as it helps them validate the presence of specific elements within the Merkle sum tree, which forms a part of the solvency proof submitted.

After generating the inclusion proof, end of example parts the inclusion proof is transformed into a JSON format, making it easily shareable.

To execute this example:
```
cargo run --example generate_inclusion
```

Upon successful execution, you can see this message and exported file `user_0_proof.json`.

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

### 5. Verify Proof of Inclusion

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

The user will receive the proof for a specific Round. There are two ways to verify the proof, one is on binary verifier in local environment, another is that the verifier function on the Summa contract.
enricobottazzi marked this conversation as resolved.
Show resolved Hide resolved

In the `verify_inclusion_on_local` example, the key part is that use `full_evm_verifier` method for verifying the proof with publicly downloaded `ptau` file.
We can think the demonstration of verifying in the example is that only shown excutable local verifier that is served from CEX in publicly way, such as github, or IPFS.

To run the verify inclusion on local example:
```
cargo run --example verify_inclusion_on_local
```

Like the user #0, you will see the result like:
```
Verifying the proof result for User #0: true
```

Another way to verify the inclusion proof, the user can use method on the Summa contract that already deployed on blockchain.

In the `verify_inclusion_on_contract` example, the procedure for verifying the inclusion proof using an on-chain method is illustrated. By leveraging the data from the Summa contract, users can effortlessly ascertain that the provided proof aligns with the data submitted by the CEX.

To elaborate:

Retrieving the MST Root: The user fetches the `mst_root` from the Summa contract. This root should match the `root_hash` provided in the proof. This verification process is akin to ensuring that the `leaf_hash` corresponds with the anticipated hash based on the `username` and `balances` provided by the CEX.

On-chain Function Verification: The user then invokes the `verify_inclusion_proof` method on the Summa contract. Since this is a view function, it returns a boolean value without incurring any gas fees, indicating the success or failure of the verification.

To run the verify inclusion on contract example:
```
cargo run --example verify_inclusion_on_contract
```

You will see the result like:
```
Verifying the proof on contract veirifer for User #0: true
enricobottazzi marked this conversation as resolved.
Show resolved Hide resolved
```


With the `verify_inclusion_on_local` and `verify_inclusion_on_contract` examples at their disposal, users are equipped with options, allowing them to choose their preferred verification method, be it local or on-chain. Moreover, by employing both verification strategies, users can achieve a heightened level of trust and transparency.
55 changes: 55 additions & 0 deletions backend/examples/generate_inclusion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#![feature(generic_const_exprs)]
use serde_json::{json, to_writer};
use std::fs;
use summa_backend::{apis::round::Round, tests::initialize_test_env};

const USER_INDEX: usize = 0;

#[tokio::main]
async fn main() {
// Initialize test environment
let (anvil, _, _, _, summa_contract, mut address_ownership_client) =
initialize_test_env().await;

address_ownership_client
.dispatch_proof_of_address_ownership()
.await
.unwrap();

// Initialize `Round` for submitting 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";

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();

// In a production environment, the CEX should dispatch the solvency proof to update the root of the Merkle sum tree prior to generating 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 public_input_vec = inclusion_proof.get_public_inputs();

// The structure of this output file may vary in production.
// For instance, the CEX might substitute `leaf_hash` with attributes like `username` and `balances`.
// Consequently, users would generate the `leaf_hash` on client-side before validating the proof.
let output = json!({
"proof": serde_json::to_string(&inclusion_proof.get_proof()).unwrap(),
"leaf_hash": serde_json::to_string(&public_input_vec[0][0]).unwrap(),
"root_hash": serde_json::to_string(&public_input_vec[0][1]).unwrap()
});

let filename = format!("user_{}_proof.json", USER_INDEX);
let file = fs::File::create(filename.clone()).expect("Unable to create file");
to_writer(file, &output).expect("Failed to write JSON to file");

println!("Exported proof to user #{}, as `{}`", USER_INDEX, filename);
}
34 changes: 34 additions & 0 deletions backend/examples/generate_signatures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#![feature(generic_const_exprs)]
use std::{error::Error, fs::File};

use csv::WriterBuilder;

mod mock_signer;
use mock_signer::sign_message;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
enricobottazzi marked this conversation as resolved.
Show resolved Hide resolved
// You can modify the message to gain better trust from users, or simply follow CEX requirements.
// The message will be used to verify addresses and register them in the `ownershipProofByAddress` mapping on the Summa contract.
let message = "Summa proof of solvency for CryptoExchange";
let path = "src/apis/csv/signatures.csv";

// Generate signatures for the given 'message' using the mock signer.
// For this example, the 'mock_signer' file contains only the 'sign_message' function.
// CEX should implement their own signer and use it here instead of 'sign_message'.
let signatures = sign_message(message).await?;

// Write the signatures to a CSV file to be used in the `verify_signatures` example.
// It's envisioned that this CSV file will remain internal to CEX, only the Summa contract will publish its contents.
let file = File::create(path)?;
let mut wtr = WriterBuilder::new().delimiter(b';').from_writer(file);
enricobottazzi marked this conversation as resolved.
Show resolved Hide resolved

for signature in signatures {
wtr.serialize(signature)?;
}

wtr.flush()?; // This will ensure all bytes are written
println!("Successfully exported signatures to {}", path);

Ok(())
}
25 changes: 25 additions & 0 deletions backend/examples/helpers/inclusion_proof.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use halo2_proofs::halo2curves::bn256::Fr as Fp;
use num_bigint::BigUint;
use serde::Deserialize;

use summa_solvency::merkle_sum_tree::Entry;

#[derive(Debug, Deserialize)]
pub struct InclusionProof {
enricobottazzi marked this conversation as resolved.
Show resolved Hide resolved
pub leaf_hash: String,
pub root_hash: String,
pub proof: String,
}

pub fn generate_leaf_hash<const N_ASSETS: usize>(user_name: String, balances: Vec<usize>) -> Fp
enricobottazzi marked this conversation as resolved.
Show resolved Hide resolved
where
[usize; N_ASSETS + 1]: Sized,
{
// Convert usize to BigInt for the `Entry` struct
let balances_big_uint: Vec<BigUint> = balances.into_iter().map(BigUint::from).collect();

let entry: Entry<N_ASSETS> =
Entry::new(user_name, balances_big_uint.try_into().unwrap()).unwrap();

entry.compute_leaf().hash
}
1 change: 1 addition & 0 deletions backend/examples/helpers/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod inclusion_proof;
52 changes: 52 additions & 0 deletions backend/examples/mock_signer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use std::{error::Error, str::FromStr};

use ethers::{
abi::{encode, Token},
signers::{LocalWallet, Signer},
utils::{keccak256, to_checksum},
};

use summa_backend::apis::csv_parser::SignatureRecord;

// Separated this function from the `generate_signatures.rs` for clarity on the example.
pub async fn sign_message(message: &str) -> Result<Vec<SignatureRecord>, Box<dyn Error>> {
enricobottazzi marked this conversation as resolved.
Show resolved Hide resolved
// Using private keys directly is insecure.
// Instead, consider leveraging hardware wallet support.
// `ethers-rs` provides support for both Ledger and Trezor hardware wallets.
//
// For example, you could use the Ledger wallet as shown below:
// let signing_wallets = (0..2).map(|index| Ledger::new(HDPath::LedgerLive(index), 1).await.unwrap()).collect();
//
// Refers to: https://docs.rs/ethers/latest/ethers/signers/index.html
let private_keys = &[
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
"0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a",
];

let signing_wallets: Vec<LocalWallet> = private_keys
.iter()
.map(|private_key| LocalWallet::from_str(private_key).unwrap())
.collect();

let encoded_message = encode(&[Token::String(message.to_owned())]);
let hashed_message = keccak256(encoded_message);

let mut signatures: Vec<SignatureRecord> = Vec::new();

// Iterating signing wallets and generate signature to put `signatures` vector
for wallet in signing_wallets {
let signature = wallet.sign_message(hashed_message).await.unwrap();
let record = SignatureRecord::new(
"ETH".to_string(),
to_checksum(&wallet.address(), None), //
format!("0x{}", signature.to_string()),
message.to_string(),
);
signatures.push(record);
}

Ok(signatures)
}

// To avoid build error
fn main() {}
67 changes: 67 additions & 0 deletions backend/examples/submit_ownership.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use ethers::{
abi::{encode, Token},
types::U256,
utils::keccak256,
};

use summa_backend::{apis::address_ownership::AddressOwnership, tests::initialize_test_env};

// In this example, we will demonstrate how to submit ownership of address to the Summa contract.
#[tokio::main]
async fn main() {
// We have already demonstrated how to generate a CSV file containing the asset ownership proofs, `AddressOwnershipProof`.
// For more details on this, kindly refer to the "generate_signature" example.

// Initialize test environment without `address_ownership` instance from `initialize_test_env` function.
let (anvil, _, _, _, summa_contract, _) = initialize_test_env().await;

// For the current demonstration, we'll use the same CSV file produced in `generate_signature` example.
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();

// Get 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]>>();

// Dispatches the proof of address ownership.
// In the client, 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);

// Check if the addresses are registered on 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!("Ownership proofs are submitted successfully!")
}
Loading