Skip to content

Commit

Permalink
Refactor the contract according to V1 consolidation spec (#178)
Browse files Browse the repository at this point in the history
* chore: add audit comments (#168)

* Refactor backend by following internal audit comments (#173)

* feat: create bash script for updating verifier interface files in backend

* fix: error propagation with try operator; remove unnecessaries

* refactor: changed data type in 'MstInclusionProof'

* fix: generate solvency verifier contract

* chore: remove left over

* chore: update README

* fix: remove left over; assert term

* fix: update README; small fixes

* feat: Signer accepts address or file path for init

* Refactor contract according to V1 consolidation spec (#169)

* Refactor smart contract

* Update smart contract module readme

---------

Co-authored-by: JinHwan <[email protected]>
  • Loading branch information
alxkzmn and sifnoc authored Nov 8, 2023
1 parent ffd8259 commit 19dcb54
Show file tree
Hide file tree
Showing 20 changed files with 406 additions and 1,797 deletions.
1 change: 1 addition & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
# will have compiled files and executables
/target
.env
*_proof.json
53 changes: 34 additions & 19 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,22 @@ SIGNATURE_VERIFICATION_MESSAGE="Summa proof of solvency for CryptoExchange" carg

## Important Notices

### Generating Verifiers for Backend
### Generating and updating verifier contracts for Backend

The following steps are optional and are only required if you need to update the verifier contracts for the backend:
The verifier contracts in the backend were generated using a predefined set of parameters: `N_ASSETS = 2` and `N_BYTES=14`, as indicated [here](https://github.com/summa-dev/summa-solvency/blob/master/zk_prover/examples/gen_solvency_verifier.rs#L21-L22).
If you intend to work with different parameters, you'll need to adjust these hard-coded values and then generate new verifier contracts.

The process described below assists in both generating the verifiers and updating the Summa contract, which integrates the new verifiers as constructors.

1. **Build the Verifier Contracts**:
- Move to the `zk_prover` directory.
- Run the [`gen_solvency_verifier`](https://github.com/summa-dev/summa-solvency/blob/master/zk_prover/examples/gen_solvency_verifier.rs) and [`gen_inclusion_verifier`](https://github.com/summa-dev/summa-solvency/blob/master/zk_prover/examples/gen_inclusion_verifier.rs) located within the `zk_prover/examples`.
- For detailed instructions [building a solvency verifier contract](https://github.com/summa-dev/summa-solvency/tree/master/zk_prover#build-a-solvency-verifier-contract) and [building an inclusion verifier contract.](https://github.com/summa-dev/summa-solvency/tree/master/zk_prover#build-an-inclusion-verifier-contract)
2. **Deploy Contracts to Local Environment**:
- Navigate to the `contracts` directory
- Deploy the contracts to a Hardhat environment. This step will refresh the ABI files(`src/contracts/abi/*.json`) in the backend.
3. **Generate Rust Interface Files**:
- Move to the `backend` directory.
- Execute the build script in the backend. This will produce the Rust interface files: `inclusion_verifier.rs`, `solvency_verifier.rs`, and `summa_contract.rs`.
#### Using the Bash Script

By completing these steps, the backend will be primed with the essential verifiers for its tasks.
We have provided a bash script to automate the process of updating the verifier contracts and the Summa contract. To use the script:

Ensure you have the necessary permissions to execute the script.

```
backend $ chmod +x scripts/update_verifier_contracts.sh
```

## Summa solvency flow example

Expand All @@ -83,9 +83,6 @@ Key points:

- 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


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:
Expand All @@ -97,12 +94,16 @@ If executed successfully, you'll see:

### 2. Submit Proof of Solvency

This step is crucial for two primary reasons: first, to validate the root hash of the Merkle Sum Tree (`mst_root`); and second, to ensure that the assets held by the CEX exceed their liabilities, as confirmed through the proof verification on the Summa contract.
The CEX must submit this proof of solvency to the Summa contract. Currently, it's a mandatory requirement to provide this proof before generating the inclusion proof for each user in the current round.
This step is also crucial for two primary reasons:

first, to validate the root hash of the Merkle Sum Tree (`mst_root`); and second, to ensure that the assets held by the CEX exceed their liabilities, as confirmed through the proof verification on the Summa contract.
The CEX must submit this proof of solvency to the Summa contract.

Currently, it's a mandatory requirement to provide this proof before generating the inclusion proof for each user in the current round.

Without this verification, 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 step, we'll guide you through the process of submitting a solvency proof using the Round to the Summa contract.
In here, we'll introduce 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:
Expand Down Expand Up @@ -147,3 +148,17 @@ The result will display as:
```
4. Verifying the proof on contract verifier for User #0: true
```

### 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:

**Note:** In a production environment, users can independently verify their proof using public interfaces, such as Etherscan, as shown below:
![Summa contract interface on Etherscan](summa_verifier_interface.png)
This offers an added layer of transparency and trust.
91 changes: 19 additions & 72 deletions backend/examples/summa_solvency_flow.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
#![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 ethers::types::U256;
use serde_json::{from_reader, to_string_pretty};

use summa_backend::{
apis::{
address_ownership::AddressOwnership,
round::{MstInclusionProof, Round},
},
contracts::signer::AddressInput,
tests::initialize_test_env,
};
use summa_solvency::merkle_sum_tree::utils::generate_leaf_hash;
Expand All @@ -29,52 +26,25 @@ async fn main() -> Result<(), Box<dyn Error>> {
//
// Each CEX prepares its own `signature` CSV file.
let signature_csv_path = "src/apis/csv/signatures.csv";

// Using AddressInput::Address to directly provide the summa_contract's address.
// For deployed contracts, if the address is stored in a config file,
// you can alternatively use AddressInput::Path to specify the file's path.
let mut address_ownership_client = AddressOwnership::new(
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
anvil.chain_id(),
anvil.endpoint().as_str(),
summa_contract.address(),
AddressInput::Address(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();
.await?;

// 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
Expand All @@ -89,7 +59,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", // anvil account [0]
anvil.chain_id(),
anvil.endpoint().as_str(),
summa_contract.address(),
AddressInput::Address(summa_contract.address()),
entry_csv,
asset_csv,
params_path,
Expand All @@ -98,21 +68,13 @@ async fn main() -> Result<(), Box<dyn Error>> {
.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(););
round.dispatch_solvency_proof().await?;

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.
// Generate and export the inclusion proof for the specified user to a JSON file.
let inclusion_proof = round.get_proof_of_inclusion(USER_INDEX).unwrap();

let filename = format!("user_{}_proof.json", USER_INDEX);
Expand Down Expand Up @@ -142,49 +104,34 @@ async fn main() -> Result<(), Box<dyn Error>> {

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

let leaf_hash = public_inputs[0][0];
let leaf_hash = public_inputs[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();
let mst_root = summa_contract.mst_roots(snapshot_time).call().await?;

// 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 proof = inclusion_proof.get_proof();
let verification_result = summa_contract
.verify_inclusion_proof(proof, public_inputs, snapshot_time)
.await
.unwrap();
.verify_inclusion_proof(proof.clone(), public_inputs.clone(), snapshot_time)
.await?;

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

// Wrapping up
drop(anvil);
Ok(())
}
26 changes: 26 additions & 0 deletions backend/scripts/update_verifier_contracts.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/bin/bash
set -e

# Build the verifier contracts
echo "1. Building verifier contracts"
cd ../zk_prover
cargo run --release --example gen_inclusion_verifier
cargo run --release --example gen_solvency_verifier

# Deploy contracts to local environment
echo "2. Deploying contracts to local environment"
cd ../contracts
npm install
npx hardhat node &
HARDHAT_PID=$!
sleep 5
npx hardhat run scripts/deploy.ts --network localhost

# Generate interface files for Backend
echo "3. Generating interface files for Backend"
cd ../backend
cargo build

# Wrap up
echo "Terminate hardhat node"
kill $HARDHAT_PID
10 changes: 6 additions & 4 deletions backend/src/apis/address_ownership.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::contracts::{generated::summa_contract::AddressOwnershipProof, signer::SummaSigner};
use ethers::types::Address;
use crate::contracts::{
generated::summa_contract::AddressOwnershipProof,
signer::{AddressInput, SummaSigner},
};
use std::{error::Error, result::Result};

use super::csv_parser::parse_signature_csv;
Expand All @@ -14,14 +16,14 @@ impl AddressOwnership {
signer_key: &str,
chain_id: u64,
rpc_url: &str,
summa_sc_address: Address,
summa_address_input: AddressInput,
signature_csv_path: &str,
) -> Result<AddressOwnership, Box<dyn Error>> {
let address_ownership_proofs = parse_signature_csv(signature_csv_path)?;

Ok(AddressOwnership {
address_ownership_proofs,
signer: SummaSigner::new(signer_key, chain_id, rpc_url, summa_sc_address),
signer: SummaSigner::new(signer_key, chain_id, rpc_url, summa_address_input),
})
}

Expand Down
32 changes: 15 additions & 17 deletions backend/src/apis/round.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
use ethers::{
abi::Address,
types::{Bytes, U256},
};
use ethers::types::{Bytes, U256};
use halo2_proofs::{
halo2curves::bn256::{Bn256, Fr as Fp, G1Affine},
plonk::{ProvingKey, VerifyingKey},
poly::kzg::commitment::ParamsKZG,
};
use serde::{Deserialize, Serialize};
use snark_verifier_sdk::{evm::gen_evm_proof_shplonk, CircuitExt};
use std::error::Error;

use super::csv_parser::parse_asset_csv;
use crate::contracts::{generated::summa_contract::summa::Asset, signer::SummaSigner};
use crate::contracts::{
generated::summa_contract::summa::Asset,
signer::{AddressInput, SummaSigner},
};
use summa_solvency::{
circuits::{
merkle_sum_tree::MstInclusionCircuit,
Expand Down Expand Up @@ -46,17 +45,17 @@ impl SolvencyProof {

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MstInclusionProof {
public_inputs: Vec<Vec<Fp>>,
proof: Vec<u8>,
public_inputs: Vec<U256>,
proof_calldata: Bytes,
}

impl MstInclusionProof {
pub fn get_public_inputs(&self) -> &Vec<Vec<Fp>> {
pub fn get_public_inputs(&self) -> &Vec<U256> {
&self.public_inputs
}

pub fn get_proof(&self) -> &Vec<u8> {
&self.proof
pub fn get_proof(&self) -> &Bytes {
&self.proof_calldata
}
}

Expand All @@ -82,7 +81,7 @@ where
signer_key: &str,
chain_id: u64,
rpc_url: &str,
summa_sc_address: Address,
summa_address_input: AddressInput,
entry_csv_path: &str,
asset_csv_path: &str,
params_path: &str,
Expand All @@ -96,7 +95,7 @@ where
params_path,
)
.unwrap(),
signer: SummaSigner::new(signer_key, chain_id, rpc_url, summa_sc_address),
signer: SummaSigner::new(signer_key, chain_id, rpc_url, summa_address_input),
})
}

Expand Down Expand Up @@ -203,16 +202,15 @@ where
MstInclusionCircuit::<LEVELS, N_ASSETS, N_BYTES>::init(self.mst.clone(), user_index);

// Currently, default manner of generating a inclusion proof for solidity-verifier.
let proof = gen_evm_proof_shplonk(
let calldata = gen_proof_solidity_calldata(
&self.trusted_setup[0].0,
&self.trusted_setup[0].1,
circuit.clone(),
circuit.instances(),
);

Ok(MstInclusionProof {
public_inputs: circuit.instances(),
proof,
proof_calldata: calldata.0,
public_inputs: calldata.1,
})
}
}
Loading

0 comments on commit 19dcb54

Please sign in to comment.