diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index bbb1f6f5..24f54063 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -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 + \ No newline at end of file diff --git a/backend/README.md b/backend/README.md index 16b26423..5168e2b4 100644 --- a/backend/README.md +++ b/backend/README.md @@ -63,27 +63,87 @@ 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 + +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. + +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. +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 ``` diff --git a/backend/examples/summa_solvency_flow.rs b/backend/examples/summa_solvency_flow.rs new file mode 100644 index 00000000..76a0861d --- /dev/null +++ b/backend/examples/summa_solvency_flow.rs @@ -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> { + // 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::>(); + + // 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::(user_name.clone(), balances.clone()) + ); + + // Before verifying `root_hath`, convert type of `proof` and `public_inputs` to the type of `Bytes` and `Vec`. + let proof: Bytes = Bytes::from(inclusion_proof.get_proof().clone()); + let public_inputs: Vec = 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(()) +} diff --git a/backend/examples/verify_inclusion.rs b/backend/examples/verify_inclusion.rs deleted file mode 100644 index 4262fed0..00000000 --- a/backend/examples/verify_inclusion.rs +++ /dev/null @@ -1,105 +0,0 @@ -#![feature(generic_const_exprs)] -use halo2_proofs::halo2curves::{bn256::Fr as Fp, ff::PrimeField}; -use num_bigint::BigUint; - -use summa_backend::apis::round::Snapshot; -use summa_solvency::{ - circuits::{ - merkle_sum_tree::MstInclusionCircuit, - utils::{full_verifier, generate_setup_artifacts}, - }, - merkle_sum_tree::Entry, -}; - -// The CEX may only provide `balances` and `username` to the user without `leaf_hash`. -// In this case, the user will have to generate `leaf_hash` themselves with this method. -fn generate_leaf_hash(user_name: String, balances: Vec) -> Fp -where - [usize; N_ASSETS + 1]: Sized, -{ - // Convert usize to BigInt for the `Entry` struct - let balances_big_uint: Vec = balances.into_iter().map(BigUint::from).collect(); - - let entry: Entry = - Entry::new(user_name, balances_big_uint.try_into().unwrap()).unwrap(); - - entry.compute_leaf().hash -} - -fn main() { - const LEVELS: usize = 4; - const N_ASSETS: usize = 2; - const N_BYTES: usize = 8; - - let ptau_path = "./ptau/hermez-raw-11"; - let asset_csv_path = "src/apis/csv/assets.csv"; - let entry_csv_path = "../zk_prover/src/merkle_sum_tree/csv/entry_16.csv"; - - // CEX Generate the Merkle Sum Tree and then initialize the circuit. - // Note that `signature_csv` is empty because this is only needed to generate π of Solvency, which is not the case here. - let snapshot = - Snapshot::::new(&asset_csv_path, &entry_csv_path, &ptau_path) - .unwrap(); - - let inclusion_proof = snapshot.generate_proof_of_inclusion(0 as usize).unwrap(); - - let encoded: Vec = bincode::serialize(&inclusion_proof.get_proof()).unwrap(); - - // Most likely, the user will receive the proof file and load it for verification like the one below. - // - // let mut file = File::open("examples/entry_0_proof.bin").unwrap(); - // let mut encoded = Vec::new(); - // file.read_to_end(&mut encoded).unwrap(); - // - // There are two public inputs: root_hash and leaf_hash. - // The root_hash is publicly shared, but the leaf_hash is not. - // Only the user can verify the leaf_hash using their name (username) and balances. - // The verifier should have access to both the username and balances. - // - // root_hash = 0x02e021d9bf99c5bd7267488b6a7a5cf5f7d00222a41b6a9b971899c44089e0c5 - let root_hash = "1300633067792667740851197998552728163078912135282962223512949070409098715333"; - - // When verifying the inclusion proof on the user side, - // you have to load two files: `ptau` and `proof`. - let proof: Vec = bincode::deserialize(&encoded[..]).unwrap(); - - // Importantly, the user should verify the leaf hash using their username and balances. - let user_name = "dxGaEAii".to_string(); - let balances_usize = vec![11888, 41163]; - - // index 0 user's leaf_hash : 0x0e113acd03b98f0bab0ef6f577245d5d008cbcc19ef2dab3608aa4f37f72a407 - let leaf_hash = Fp::from_str_vartime( - "6362822108736413915574850018842190920390136280184018644072260166743334495239", - ) - .unwrap(); - - // This is the purpose of the example - // When the user receives their `leaf_hash`, the user must verify the `leaf_hash` from their `balances` and `username`. - assert_eq!( - leaf_hash, - generate_leaf_hash::(user_name.clone(), balances_usize.clone()) - ); - - let mst_inclusion_circuit = MstInclusionCircuit::::init_empty(); - - // The CEX can serve the params and vk by using the get_trusted_setup_for_mst_inclusion() method from the snapshot instance, as shown below: - // let (params, _, vk) = snapshot.get_trusted_setup_for_mst_inclusion(); - // - // However, on the user side, creating a snapshot is not possible due to the lack of the entry.csv and signature.csv files. - // Therefore, we can generate the artifacts from the ptau file with a specified k, which is the same number used while generating the proof. - let (params, _, vk) = - generate_setup_artifacts(11, Some(ptau_path), mst_inclusion_circuit).unwrap(); - - let verification_result: bool = full_verifier( - ¶ms, - &vk, - proof, - vec![vec![leaf_hash, Fp::from_str_vartime(root_hash).unwrap()]], - ); - - // Given the proper inputs (`root_hash` and `leaf_hash`), the proof is valid. - println!( - "Verifying the proof result for User #0: {}", - verification_result - ); -} diff --git a/backend/src/apis/address_ownership.rs b/backend/src/apis/address_ownership.rs index 0f1b96dc..7b56348b 100644 --- a/backend/src/apis/address_ownership.rs +++ b/backend/src/apis/address_ownership.rs @@ -21,10 +21,14 @@ impl AddressOwnership { 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_sc_address), }) } + pub fn get_ownership_proofs(&self) -> &Vec { + &self.address_ownership_proofs + } + // This function dispatches the proof of address ownership. Before calling this function, // ensure externally that the provided `addresses` in `address_ownership_proof` are not already registered // on the Summa contract. diff --git a/backend/src/apis/csv/signatures.csv b/backend/src/apis/csv/signatures.csv index 613bc0a6..89e4ba16 100644 --- a/backend/src/apis/csv/signatures.csv +++ b/backend/src/apis/csv/signatures.csv @@ -1,3 +1,3 @@ -chain;address;signature;message -ETH;0x70997970C51812dc3A010C7d01b50e0d17dc79C8;0x089b32327d332c295dc3b8873c205b72153211de6dc1c51235782b091cefb9d06d6df2661b86a7d441cd322f125b84901486b150e684221a7b7636eb8182af551b;Summa proof of solvency for CryptoExchange -ETH;0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC;0xb17a9e25265d3b88de7bfad81e7accad6e3d5612308ff83cc0fef76a34152b0444309e8fc3dea5139e49b6fc83a8553071a7af3d0cfd3fb8c1aea2a4c171729c1c;Summa proof of solvency for CryptoExchange +chain;address;signature;message +ETH;0x70997970C51812dc3A010C7d01b50e0d17dc79C8;0x089b32327d332c295dc3b8873c205b72153211de6dc1c51235782b091cefb9d06d6df2661b86a7d441cd322f125b84901486b150e684221a7b7636eb8182af551b;Summa proof of solvency for CryptoExchange +ETH;0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC;0xb17a9e25265d3b88de7bfad81e7accad6e3d5612308ff83cc0fef76a34152b0444309e8fc3dea5139e49b6fc83a8553071a7af3d0cfd3fb8c1aea2a4c171729c1c;Summa proof of solvency for CryptoExchange diff --git a/backend/src/apis/csv_parser.rs b/backend/src/apis/csv_parser.rs index e28e1843..3bde75e1 100644 --- a/backend/src/apis/csv_parser.rs +++ b/backend/src/apis/csv_parser.rs @@ -4,18 +4,29 @@ use ethers::{ abi::AbiEncode, types::{Bytes, U256}, }; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use crate::contracts::generated::summa_contract::{AddressOwnershipProof, Asset}; -#[derive(Debug, Deserialize)] -struct SignatureRecord { +#[derive(Debug, Deserialize, Serialize)] +pub struct SignatureRecord { chain: String, address: String, signature: String, message: String, } +impl SignatureRecord { + pub fn new(chain: String, address: String, signature: String, message: String) -> Self { + Self { + chain, + address, + signature, + message, + } + } +} + pub fn parse_signature_csv>( path: P, ) -> Result, Box> { diff --git a/backend/src/apis/mod.rs b/backend/src/apis/mod.rs index 712b5c53..82fca001 100644 --- a/backend/src/apis/mod.rs +++ b/backend/src/apis/mod.rs @@ -1,3 +1,3 @@ pub mod address_ownership; -mod csv_parser; +pub mod csv_parser; pub mod round; diff --git a/backend/src/apis/round.rs b/backend/src/apis/round.rs index 08c2a5e4..c1f47b45 100644 --- a/backend/src/apis/round.rs +++ b/backend/src/apis/round.rs @@ -7,7 +7,8 @@ use halo2_proofs::{ plonk::{ProvingKey, VerifyingKey}, poly::kzg::commitment::ParamsKZG, }; -use snark_verifier_sdk::CircuitExt; +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; @@ -16,7 +17,7 @@ use summa_solvency::{ circuits::{ merkle_sum_tree::MstInclusionCircuit, solvency::SolvencyCircuit, - utils::{full_prover, gen_proof_solidity_calldata, generate_setup_artifacts}, + utils::{gen_proof_solidity_calldata, generate_setup_artifacts}, }, merkle_sum_tree::MerkleSumTree, }; @@ -43,7 +44,7 @@ impl SolvencyProof { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct MstInclusionProof { public_inputs: Vec>, proof: Vec, @@ -95,7 +96,7 @@ where params_path, ) .unwrap(), - signer: SummaSigner::new(&vec![], signer_key, chain_id, rpc_url, summa_sc_address), + signer: SummaSigner::new(signer_key, chain_id, rpc_url, summa_sc_address), }) } @@ -103,23 +104,22 @@ where self.timestamp } - pub async fn dispatch_solvency_proof(&mut self) -> Result<(), &'static str> { + pub async fn dispatch_solvency_proof(&mut self) -> Result<(), Box> { let proof: SolvencyProof = match self.snapshot.generate_proof_of_solvency() { Ok(p) => p, - Err(_) => return Err("Failed to generate proof of solvency"), + Err(e) => return Err(format!("Failed to generate proof of solvency: {}", e).into()), }; - let result = self - .signer + self.signer .submit_proof_of_solvency( proof.public_inputs[0], self.snapshot.assets_state.to_vec(), proof.proof_calldata, U256::from(self.get_timestamp()), ) - .await; + .await?; - Ok(result.unwrap()) + Ok(()) } pub fn get_proof_of_inclusion( @@ -202,7 +202,8 @@ where let circuit = MstInclusionCircuit::::init(self.mst.clone(), user_index); - let proof = full_prover( + // Currently, default manner of generating a inclusion proof for solidity-verifier. + let proof = gen_evm_proof_shplonk( &self.trusted_setup[0].0, &self.trusted_setup[0].1, circuit.clone(), diff --git a/backend/src/contracts/generated/mod.rs b/backend/src/contracts/generated/mod.rs index a4524590..a944f3aa 100644 --- a/backend/src/contracts/generated/mod.rs +++ b/backend/src/contracts/generated/mod.rs @@ -1,3 +1,3 @@ -pub mod inclusion_verifier; -pub mod solvency_verifier; pub mod summa_contract; +pub mod solvency_verifier; +pub mod inclusion_verifier; \ No newline at end of file diff --git a/backend/src/contracts/signer.rs b/backend/src/contracts/signer.rs index 67ee632c..5b4f3ccc 100644 --- a/backend/src/contracts/signer.rs +++ b/backend/src/contracts/signer.rs @@ -1,13 +1,10 @@ use crate::contracts::generated::summa_contract::Summa; use ethers::{ - abi::{encode, Token}, prelude::SignerMiddleware, providers::{Http, Provider}, - signers::{LocalWallet, Signer, WalletError}, - types::{Address, Signature}, - utils::keccak256, + signers::{LocalWallet, Signer}, + types::Address, }; -use futures::future::join_all; use serde_json::Value; use std::{ error::Error, fs::File, io::BufReader, path::Path, str::FromStr, sync::Arc, time::Duration, @@ -17,26 +14,18 @@ use super::generated::summa_contract::{AddressOwnershipProof, Asset}; #[derive(Debug)] pub struct SummaSigner { - signing_wallets: Vec, summa_contract: Summa, LocalWallet>>, } impl SummaSigner { /// Creates a new SummaSigner instance /// # Arguments - /// * `private_keys` - A list of the private keys of the addresses holding the exchange assets - /// * `main_signer_key` - The private key of wallet that will interact with the chain on behalf of the exchange + /// * `signer_key` - The private key of wallet that will interact with the chain on behalf of the exchange /// * `chain_id` - The chain id of the network /// * `rpc_url` - The RPC URL of the network /// * `address` - The address of the Summa contract - pub fn new( - private_keys: &[&str], - main_signer_key: &str, - chain_id: u64, - rpc_url: &str, - address: Address, - ) -> Self { - let wallet: LocalWallet = LocalWallet::from_str(main_signer_key).unwrap(); + pub fn new(signer_key: &str, chain_id: u64, rpc_url: &str, address: Address) -> Self { + let wallet: LocalWallet = LocalWallet::from_str(signer_key).unwrap(); let provider = Provider::::try_from(rpc_url) .unwrap() @@ -48,10 +37,6 @@ impl SummaSigner { let contract = Summa::new(address, client); Self { - signing_wallets: private_keys - .iter() - .map(|private_key| LocalWallet::from_str(private_key).unwrap()) - .collect(), summa_contract: contract, } } @@ -80,23 +65,6 @@ impl SummaSigner { Ok(address) } - async fn sign_message(wallet: &LocalWallet, message: &str) -> Signature { - let encoded_message = encode(&[Token::String(message.to_owned())]); - let hashed_message = keccak256(encoded_message); - wallet.sign_message(hashed_message).await.unwrap() - } - - pub async fn generate_signatures(&self) -> Result, WalletError> { - let message = std::env::var("SIGNATURE_VERIFICATION_MESSAGE").unwrap(); - let signature_futures: Vec<_> = self - .signing_wallets - .iter() - .map(|wallet| Self::sign_message(wallet, &message)) - .collect(); - - Ok(join_all(signature_futures).await) - } - pub async fn submit_proof_of_address_ownership( &self, address_ownership_proofs: Vec, @@ -128,30 +96,3 @@ impl SummaSigner { Ok(()) } } - -#[cfg(test)] -mod test { - use super::*; - use ethers::{types::Address, utils::Anvil}; - - #[tokio::test] - async fn test_sign_message() { - let anvil = Anvil::new().spawn(); - - let signer = SummaSigner::new( - //Account #1 - &vec!["0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"], - "0xde9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0", - 31337, - anvil.endpoint().as_str(), - //Verifier deployment is not necessary for this test - Address::random(), - ); - - let signatures = signer.generate_signatures().await.unwrap(); - assert_eq!(signatures.len(), 1); - //Signature produced by the account #1 - assert_eq!(signatures[0].to_string(), "089b32327d332c295dc3b8873c205b72153211de6dc1c51235782b091cefb9d06d6df2661b86a7d441cd322f125b84901486b150e684221a7b7636eb8182af551b"); - drop(anvil); - } -} diff --git a/backend/src/tests.rs b/backend/src/tests.rs index 834bbefe..faf41cee 100644 --- a/backend/src/tests.rs +++ b/backend/src/tests.rs @@ -9,15 +9,19 @@ use ethers::{ }; use tokio::time; +use crate::contracts::generated::{ + inclusion_verifier::InclusionVerifier, solvency_verifier::SolvencyVerifier, + summa_contract::Summa, +}; use crate::contracts::mock::mock_erc20::{MockERC20, MOCKERC20_ABI, MOCKERC20_BYTECODE}; -// Setup test conditions on the anvil instance -pub async fn initialize_anvil() -> ( +// Setup test environment on the anvil instance +pub async fn initialize_test_env() -> ( AnvilInstance, H160, H160, Arc, LocalWallet>>, - MockERC20, LocalWallet>>, + Summa, LocalWallet>>, ) { let anvil: ethers::utils::AnvilInstance = Anvil::new() .mnemonic("test test test test test test test test test test test junk") @@ -67,13 +71,37 @@ pub async fn initialize_anvil() -> ( time::sleep(Duration::from_millis(500)).await; - (anvil, cex_addr_1, cex_addr_2, client, mock_erc20) + // Deploy verifier contracts before deploy Summa contract + let solvency_verifer_contract = SolvencyVerifier::deploy(Arc::clone(&client), ()) + .unwrap() + .send() + .await + .unwrap(); + + let inclusion_verifer_contract = InclusionVerifier::deploy(Arc::clone(&client), ()) + .unwrap() + .send() + .await + .unwrap(); + + // Deploy Summa contract + let summa_contract = Summa::deploy( + Arc::clone(&client), + ( + solvency_verifer_contract.address(), + inclusion_verifer_contract.address(), + ), + ) + .unwrap() + .send() + .await + .unwrap(); + + (anvil, cex_addr_1, cex_addr_2, client, summa_contract) } #[cfg(test)] mod test { - use std::sync::Arc; - use ethers::{ abi::AbiEncode, types::{Bytes, U256}, @@ -81,43 +109,15 @@ mod test { }; use crate::apis::{address_ownership::AddressOwnership, round::Round}; - use crate::contracts::generated::{ - inclusion_verifier::InclusionVerifier, - solvency_verifier::SolvencyVerifier, - summa_contract::{ - AddressOwnershipProof, AddressOwnershipProofSubmittedFilter, Asset, - SolvencyProofSubmittedFilter, Summa, - }, + use crate::contracts::generated::summa_contract::{ + AddressOwnershipProof, AddressOwnershipProofSubmittedFilter, Asset, + SolvencyProofSubmittedFilter, }; - use crate::tests::initialize_anvil; + use crate::tests::initialize_test_env; #[tokio::test] async fn test_round_features() { - let (anvil, cex_addr_1, cex_addr_2, client, _mock_erc20) = initialize_anvil().await; - - let solvency_verifer_contract = SolvencyVerifier::deploy(Arc::clone(&client), ()) - .unwrap() - .send() - .await - .unwrap(); - - let inclusion_verifer_contract = InclusionVerifier::deploy(Arc::clone(&client), ()) - .unwrap() - .send() - .await - .unwrap(); - - let summa_contract = Summa::deploy( - Arc::clone(&client), - ( - solvency_verifer_contract.address(), - inclusion_verifer_contract.address(), - ), - ) - .unwrap() - .send() - .await - .unwrap(); + let (anvil, cex_addr_1, cex_addr_2, _, summa_contract) = initialize_test_env().await; let mut address_ownership_client = AddressOwnership::new( "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", @@ -226,27 +226,25 @@ mod test { // Test inclusion proof let inclusion_proof = round.get_proof_of_inclusion(0).unwrap(); let proof = Bytes::from(inclusion_proof.get_proof().clone()); - let public_input_vec = inclusion_proof.get_public_inputs(); - - // Adjust for endianness - let mut leaf_hash = public_input_vec[0][0].to_bytes(); - let mut root_hash = public_input_vec[0][1].to_bytes(); - leaf_hash.reverse(); - root_hash.reverse(); - - let public_inputs = vec![ - U256::from_big_endian(&leaf_hash), - U256::from_big_endian(&root_hash), - ]; - - // Ensure the root hash matches the one from the contract - let onchain_mstroot = summa_contract.mst_roots(U256::from(1)).await.unwrap(); - assert_eq!(onchain_mstroot, U256::from_big_endian(&root_hash)); + let public_inputs: Vec = 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(); // Verify inclusion proof with onchain function - let _result = summa_contract + let verified = summa_contract .verify_inclusion_proof(proof, public_inputs, U256::from(1)) - .await; + .await + .unwrap(); + + assert_eq!(verified, true); drop(anvil); } diff --git a/zk_prover/src/circuits/utils.rs b/zk_prover/src/circuits/utils.rs index 8c52ab12..96bb2bcb 100644 --- a/zk_prover/src/circuits/utils.rs +++ b/zk_prover/src/circuits/utils.rs @@ -71,7 +71,7 @@ pub fn generate_setup_artifacts + CircuitExt>( } } None => { - let timer = start_timer!(|| "Creating params"); + let timer = start_timer!(|| "None Creating params"); params = ParamsKZG::::setup(k, OsRng); end_timer!(timer); } diff --git a/zk_prover/src/merkle_sum_tree/utils/generate_leaf_hash.rs b/zk_prover/src/merkle_sum_tree/utils/generate_leaf_hash.rs new file mode 100644 index 00000000..8effd32a --- /dev/null +++ b/zk_prover/src/merkle_sum_tree/utils/generate_leaf_hash.rs @@ -0,0 +1,17 @@ +use halo2_proofs::halo2curves::bn256::Fr as Fp; +use num_bigint::BigUint; + +use crate::merkle_sum_tree::Entry; + +pub fn generate_leaf_hash(user_name: String, balances: Vec) -> Fp +where + [usize; N_ASSETS + 1]: Sized, +{ + // Convert usize to BigInt for the `Entry` struct + let balances_big_uint: Vec = balances.into_iter().map(BigUint::from).collect(); + + let entry: Entry = + Entry::new(user_name, balances_big_uint.try_into().unwrap()).unwrap(); + + entry.compute_leaf().hash +} diff --git a/zk_prover/src/merkle_sum_tree/utils/mod.rs b/zk_prover/src/merkle_sum_tree/utils/mod.rs index 53d4db35..6c77a9b9 100644 --- a/zk_prover/src/merkle_sum_tree/utils/mod.rs +++ b/zk_prover/src/merkle_sum_tree/utils/mod.rs @@ -2,6 +2,7 @@ mod build_tree; mod create_middle_node; mod create_proof; mod csv_parser; +mod generate_leaf_hash; mod hash; mod index_of; mod operation_helpers; @@ -10,6 +11,7 @@ mod proof_verification; pub use build_tree::build_merkle_tree_from_entries; pub use create_proof::create_proof; pub use csv_parser::parse_csv_to_entries; +pub use generate_leaf_hash::generate_leaf_hash; pub use hash::{poseidon_entry, poseidon_node}; pub use index_of::index_of; pub use operation_helpers::*;