From badaae30bc48528e70c49afce1f0056ab48e8b6b Mon Sep 17 00:00:00 2001 From: robinlinus Date: Mon, 28 Apr 2025 10:50:26 -0700 Subject: [PATCH 01/21] initial commit --- client/{src => }/index.html | 0 {client => server}/Cargo.toml | 0 {client => server}/minting_secret.bin | 0 {client => server}/src/commitment.rs | 0 {client => server}/src/main.rs | 0 {client => server}/src/publisher.rs | 0 {client => server}/src/scanner.rs | 0 {client => server}/src/server.rs | 39 ++++++++++++++++----------- {client => server}/src/state.rs | 0 {client => server}/src/wallet.rs | 2 -- 10 files changed, 23 insertions(+), 18 deletions(-) rename client/{src => }/index.html (100%) rename {client => server}/Cargo.toml (100%) rename {client => server}/minting_secret.bin (100%) rename {client => server}/src/commitment.rs (100%) rename {client => server}/src/main.rs (100%) rename {client => server}/src/publisher.rs (100%) rename {client => server}/src/scanner.rs (100%) rename {client => server}/src/server.rs (93%) rename {client => server}/src/state.rs (100%) rename {client => server}/src/wallet.rs (99%) diff --git a/client/src/index.html b/client/index.html similarity index 100% rename from client/src/index.html rename to client/index.html diff --git a/client/Cargo.toml b/server/Cargo.toml similarity index 100% rename from client/Cargo.toml rename to server/Cargo.toml diff --git a/client/minting_secret.bin b/server/minting_secret.bin similarity index 100% rename from client/minting_secret.bin rename to server/minting_secret.bin diff --git a/client/src/commitment.rs b/server/src/commitment.rs similarity index 100% rename from client/src/commitment.rs rename to server/src/commitment.rs diff --git a/client/src/main.rs b/server/src/main.rs similarity index 100% rename from client/src/main.rs rename to server/src/main.rs diff --git a/client/src/publisher.rs b/server/src/publisher.rs similarity index 100% rename from client/src/publisher.rs rename to server/src/publisher.rs diff --git a/client/src/scanner.rs b/server/src/scanner.rs similarity index 100% rename from client/src/scanner.rs rename to server/src/scanner.rs diff --git a/client/src/server.rs b/server/src/server.rs similarity index 93% rename from client/src/server.rs rename to server/src/server.rs index dbe36d7..01c7b0e 100644 --- a/client/src/server.rs +++ b/server/src/server.rs @@ -153,9 +153,7 @@ async fn receive_coin_handler( match bincode::deserialize::(&body) { Ok(coin_proof) => { let mut wallet = wallet.lock().unwrap(); - match wallet.receive_coin( - coin_proof - ) { + match wallet.receive_coin(coin_proof) { Ok(_) => Json(SendCoinResponse { success: true, proof_id: None, @@ -225,14 +223,17 @@ async fn send_coin_handler( // Acquire the wallet lock only for the duration of sending coins. let send_result = { let mut wallet_guard = wallet.lock().unwrap(); - wallet_guard.send_coins(vec![Invoice::new(request.amount, to_address)], &from_address) + wallet_guard.send_coins( + vec![Invoice::new(request.amount, to_address)], + &from_address, + ) }; // Now that the wallet lock is dropped, we can await safely. match send_result { Ok(mut coin_proofs) => { - let commitment_data = - bincode::serialize(&coin_proofs[0].commitment).expect("Failed to serialize commitment"); + let commitment_data = bincode::serialize(&coin_proofs[0].commitment) + .expect("Failed to serialize commitment"); println!( "Sending commitment data with size: {} bytes", @@ -245,7 +246,7 @@ async fn send_coin_handler( { eprintln!("Error broadcasting inscription: {}", err); } - + // TODO: Handle all the coins_proofs let proof_id = proof_store.add_proof(coin_proofs.pop().unwrap()); ( @@ -266,7 +267,6 @@ async fn send_coin_handler( } } - // TODO: This has to be eventually replaced by a faucet (or some way for wallets to share the // minting account) async fn mint_handler( @@ -285,7 +285,7 @@ async fn mint_handler( ) } }; - + let mut account_address = [0u8; 32]; if account_address_vec.len() == 32 { account_address.copy_from_slice(&account_address_vec); @@ -303,14 +303,17 @@ async fn mint_handler( let send_result = { let mut wallet_guard = wallet.lock().unwrap(); let minting_address = &wallet_guard.get_minting_account_address(); - wallet_guard.send_coins(vec![Invoice::new(request.amount, account_address)], minting_address) + wallet_guard.send_coins( + vec![Invoice::new(request.amount, account_address)], + minting_address, + ) }; // Now that the wallet lock is dropped, we can await safely. match send_result { Ok(mut coin_proofs) => { - let commitment_data = - bincode::serialize(&coin_proofs[0].commitment).expect("Failed to serialize commitment"); + let commitment_data = bincode::serialize(&coin_proofs[0].commitment) + .expect("Failed to serialize commitment"); println!( "Sending commitment data with size: {} bytes", @@ -323,7 +326,7 @@ async fn mint_handler( { eprintln!("Error broadcasting inscription: {}", err); } - + let proof_id = proof_store.add_proof(coin_proofs.pop().unwrap()); ( StatusCode::OK, @@ -393,10 +396,10 @@ pub async fn start_rest_server(wallet: Wallet, addr: &str) -> anyhow::Result<()> // Create a router for API endpoints let api_routes = Router::new() .route("/balance", get(get_balance_handler)) - .route("/address", get(get_address_handler)) - .route("/receive", post(receive_coin_handler)) .route("/send", post(send_coin_handler)) - .route("/proof/:id", get(get_proof_handler)) + // .route("/address", get(get_address_handler)) + // .route("/receive", post(receive_coin_handler)) + // .route("/proof/:id", get(get_proof_handler)) .route("/mint", post(mint_handler)) .with_state(state); @@ -435,3 +438,7 @@ async fn serve_index() -> impl IntoResponse { } } } + +// http://myserver.com//balance +// http://myserver.com//send +// http://myserver.com//sign diff --git a/client/src/state.rs b/server/src/state.rs similarity index 100% rename from client/src/state.rs rename to server/src/state.rs diff --git a/client/src/wallet.rs b/server/src/wallet.rs similarity index 99% rename from client/src/wallet.rs rename to server/src/wallet.rs index 657361c..1c6e529 100644 --- a/client/src/wallet.rs +++ b/server/src/wallet.rs @@ -1,5 +1,3 @@ -use std::fs::File; -use std::io::Write; use std::sync::{Arc, Mutex, MutexGuard}; use std::{collections::HashMap, mem::take}; From eeb7c5eea497d5b29d72f182d9affee9d2162a6f Mon Sep 17 00:00:00 2001 From: robinlinus Date: Mon, 28 Apr 2025 19:00:29 -0700 Subject: [PATCH 02/21] Add wasm dependencies to client --- client/Cargo.toml | 10 + client/index.html | 562 +----------------------------------------- client/readme.md | 14 ++ client/src/index.html | 550 +++++++++++++++++++++++++++++++++++++++++ client/src/lib.rs | 6 + 5 files changed, 592 insertions(+), 550 deletions(-) create mode 100644 client/Cargo.toml create mode 100644 client/readme.md create mode 100644 client/src/index.html create mode 100644 client/src/lib.rs diff --git a/client/Cargo.toml b/client/Cargo.toml new file mode 100644 index 0000000..49c223d --- /dev/null +++ b/client/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "client" +version.workspace = true +edition.workspace = true + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "0.2" \ No newline at end of file diff --git a/client/index.html b/client/index.html index ee9c828..82fa50b 100644 --- a/client/index.html +++ b/client/index.html @@ -1,550 +1,12 @@ - - - - - - zkCoins Wallet - - - -

zkCoins Wallet

- -
- -
-

Select Account

-
- -
- -
- - -
-

Account Details

-
- Address: Not selected -
-
- Balance: - -
-
- - -
-

Coin Faucet

-
- - -
-
- - - -
-
- -
- - -
-

Send Coins

-
- - -
-
- - -
- -
- -
- - -
-

Receive Coins

-
- - -
- -
-
-
- - - - + + + +

Hello, World!

\ No newline at end of file diff --git a/client/readme.md b/client/readme.md new file mode 100644 index 0000000..df8651e --- /dev/null +++ b/client/readme.md @@ -0,0 +1,14 @@ +# zkCoins Wallet + +## Client + +```bash + wasm-pack build --target web +``` + +## Server + +```bash + python3 -m http.server 8000 +``` + diff --git a/client/src/index.html b/client/src/index.html new file mode 100644 index 0000000..ee9c828 --- /dev/null +++ b/client/src/index.html @@ -0,0 +1,550 @@ + + + + + + zkCoins Wallet + + + +

zkCoins Wallet

+ +
+ +
+

Select Account

+
+ +
+ +
+ + +
+

Account Details

+
+ Address: Not selected +
+
+ Balance: - +
+
+ + +
+

Coin Faucet

+
+ + +
+
+ + + +
+
+ +
+ + +
+

Send Coins

+
+ + +
+
+ + +
+ +
+ +
+ + +
+

Receive Coins

+
+ + +
+ +
+
+
+ + + + diff --git a/client/src/lib.rs b/client/src/lib.rs new file mode 100644 index 0000000..0151f41 --- /dev/null +++ b/client/src/lib.rs @@ -0,0 +1,6 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub fn greet(name: &str) -> String { + format!("Hello, {}!", name) +} From 73a44b9b4208f384125dafdd687d19c6e584d975 Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 29 Apr 2025 13:52:49 +0200 Subject: [PATCH 03/21] chore: Fix workspace names --- Cargo.lock | 35 +++++++++++++++++++++-------------- Cargo.toml | 5 +++-- server/Cargo.toml | 2 +- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 72d9482..dd46fe5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1202,20 +1202,7 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" name = "client" version = "1.1.0" dependencies = [ - "anyhow", - "axum", - "bincode", - "bitcoin", - "bitcoin_hashes 0.16.0", - "esplora-client", - "hex", - "lazy_static", - "lib", - "serde", - "sha2 0.10.8", - "tokio", - "zkcoins-program", - "zkcoins-prover", + "wasm-bindgen", ] [[package]] @@ -4648,6 +4635,26 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "server" +version = "1.1.0" +dependencies = [ + "anyhow", + "axum", + "bincode", + "bitcoin", + "bitcoin_hashes 0.16.0", + "esplora-client", + "hex", + "lazy_static", + "lib", + "serde", + "sha2 0.10.8", + "tokio", + "zkcoins-program", + "zkcoins-prover", +] + [[package]] name = "sha2" version = "0.10.8" diff --git a/Cargo.toml b/Cargo.toml index 8238e8b..7720364 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,8 +2,9 @@ members = [ "program", "script", - "client", - "lib" + "lib", + "server", + "client" ] resolver = "2" diff --git a/server/Cargo.toml b/server/Cargo.toml index debd89e..0ee1909 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "client" +name = "server" version.workspace = true edition.workspace = true From 693b240fa54f9b0246e56a98b3ac862ed8ddfc23 Mon Sep 17 00:00:00 2001 From: robinlinus Date: Wed, 30 Apr 2025 20:51:06 -0700 Subject: [PATCH 04/21] Implement Schnorr signing in wasm --- client/Cargo.toml | 11 +++++++++- client/index.html | 12 +++++++---- client/readme.md | 2 +- client/src/lib.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/client/Cargo.toml b/client/Cargo.toml index 49c223d..db5c59e 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -7,4 +7,13 @@ edition.workspace = true crate-type = ["cdylib"] [dependencies] -wasm-bindgen = "0.2" \ No newline at end of file +wasm-bindgen = "0.2" +bitcoin = { workspace = true } +hex = "0.4.3" + +[dependencies.getrandom] +version = "0.2.15" +features = ["js"] + +[package.metadata.wasm-pack.profile.release] +wasm-opt = false \ No newline at end of file diff --git a/client/index.html b/client/index.html index 82fa50b..6aaf181 100644 --- a/client/index.html +++ b/client/index.html @@ -1,12 +1,16 @@ - - -

Hello, World!

\ No newline at end of file +

Hello, World!

+ diff --git a/client/readme.md b/client/readme.md index df8651e..150a2bb 100644 --- a/client/readme.md +++ b/client/readme.md @@ -9,6 +9,6 @@ ## Server ```bash - python3 -m http.server 8000 + python3 -m http.server ``` diff --git a/client/src/lib.rs b/client/src/lib.rs index 0151f41..4af5f9b 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1,6 +1,57 @@ +use bitcoin::{ + key::Secp256k1, + secp256k1::{Message, SecretKey}, +}; +use hex; use wasm_bindgen::prelude::*; +/// A simple greeting function. #[wasm_bindgen] pub fn greet(name: &str) -> String { format!("Hello, {}!", name) } + +/// Signs a 32-byte hash using a Schnorr signature over a given 32-byte private key. +/// +/// The function takes two hex-encoded strings: +/// - `private_key_hex`: The private key in hex (must be 32 bytes). +/// - `hash_hex`: The message hash to sign in hex (must be 32 bytes). +/// +/// Returns a hex-encoded Schnorr signature on success or an error as a `JsValue` on failure. +#[wasm_bindgen] +pub fn sign_schnorr(private_key_hex: &str, hash_hex: &str) -> Result { + // Decode the private key from hex. + let priv_key_bytes = hex::decode(private_key_hex) + .map_err(|e| JsValue::from_str(&format!("Invalid hex in private key: {}", e)))?; + if priv_key_bytes.len() != 32 { + return Err(JsValue::from_str("Private key must be 32 bytes in hex")); + } + + // Create the secret key. + let secret_key = SecretKey::from_slice(&priv_key_bytes) + .map_err(|e| JsValue::from_str(&format!("Invalid secret key: {}", e)))?; + + // Decode the message hash from hex. + let hash_bytes = hex::decode(hash_hex) + .map_err(|e| JsValue::from_str(&format!("Invalid hex in message hash: {}", e)))?; + if hash_bytes.len() != 32 { + return Err(JsValue::from_str("Hash must be 32 bytes in hex")); + } + + // Construct the message from the 32-byte hash. + // Note: `from_slice` is deprecated; use `from_digest_slice` instead. + let msg = Message::from_digest_slice(&hash_bytes) + .map_err(|e| JsValue::from_str(&format!("Invalid message: {}", e)))?; + + // Create a signing-only secp256k1 context. + let secp = Secp256k1::signing_only(); + + // Convert the secret key into a keypair. + let keypair = bitcoin::secp256k1::Keypair::from_secret_key(&secp, &secret_key); + + // Sign the message using the keypair. + let sig = secp.sign_schnorr_no_aux_rand(&msg, &keypair); + + // Return the signature as a hex string. + Ok(hex::encode(sig.as_ref())) +} From 27bca174feff58181ebea18feefdbf740d04b749 Mon Sep 17 00:00:00 2001 From: Lukas Date: Thu, 1 May 2025 15:36:04 +0200 Subject: [PATCH 05/21] Rework Server API to use keys provided by client --- program/src/lib.rs | 4 +- server/src/server.rs | 2 +- server/src/wallet.rs | 456 ++++++++++++++++++++++--------------------- 3 files changed, 241 insertions(+), 221 deletions(-) diff --git a/program/src/lib.rs b/program/src/lib.rs index abc6ff3..d44d0fa 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -82,7 +82,9 @@ pub struct AccountState { } impl AccountState { - pub fn new(initial_public_key: PublicKey) -> Self { + pub fn new(initial_public_key: lib::PublicKey) -> Self { + // TODO: The randomness here is annoying why do we not hash the public key directly and + // skip the first one in the commitments? // We add random bytes to the public key as a blinding factor for the address. // This ensures that the on-chain commited public keys can not be linked to the address. let mut rng = rand::thread_rng(); diff --git a/server/src/server.rs b/server/src/server.rs index 01c7b0e..aabee5b 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -16,7 +16,7 @@ use zkcoins_prover::Proof; use crate::commitment::Commitment; use crate::publisher::create_and_broadcast_inscription; -use crate::wallet::{CoinProof, Invoice, Wallet}; +use crate::wallet::{CoinProof, Invoice, Server}; use crate::NETWORK_CONFIG; // Response types for our API diff --git a/server/src/wallet.rs b/server/src/wallet.rs index 1c6e529..76a25f2 100644 --- a/server/src/wallet.rs +++ b/server/src/wallet.rs @@ -35,7 +35,7 @@ pub struct CoinProof { pub proof: Proof, pub coin: Coin, pub inclusion_proof: InclusionProof, - pub commitment: Commitment, + pub commitment: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Copy)] @@ -50,26 +50,119 @@ impl Invoice { } } +pub struct ClientAccount { + pub address: HashDigest, + pub num_pubkeys: u32, + private_key: PrivateKey +} + +// TODO Move to tests or client +pub fn new_master_private_key() -> PrivateKey { + let mut rng = OsRng; + let mut seed = [0u8; 32]; + rng.fill_bytes(&mut seed); + PrivateKey::new_master(Network::Bitcoin, &seed).expect("Failed to create private key.") +} + +// TODO: Move this to client +impl ClientAccount { + fn current_private_key(&self) -> SecretKey { + self + .private_key + .derive_priv( + &SECP256K1, + &[ChildNumber::Normal { + index: self + .num_pubkeys + .checked_sub(1) + .expect("This account was never commited to."), + }], + ) + .expect("Unable to derive private key for account") + .private_key + } + + pub fn sign_proof(&self, proof: &mut CoinProof) { + let proof_data = bincode::deserialize::(&proof.proof.public_values.clone().to_vec()).expect("Proof is invalid"); + proof.commitment = Some(Commitment::new(&self.current_private_key(), hash_concat(&proof_data.account_state_hash, &proof_data.output_coins_root).to_vec()).expect("Should be able to create commitment")); + } + + // TODO: Remove the server as an argument and send a REST request, fill in the commitment and + // send it back to server. + pub fn send_coins(&mut self, server: &mut Server, invoices: Vec) -> Result, String> { + let result = server.send_coins(invoices, self.address, Self::generate_public_key(&self.private_key, self.num_pubkeys),Self::generate_public_key(&self.private_key, self.num_pubkeys + 1)); + self.num_pubkeys += 1; + match result { + Ok(mut proofs) => { + for proof in &mut proofs { + self.sign_proof(proof); + } + Ok(proofs) + } + Err(_) => result, + } + } + + pub fn generate_public_key(private_key: &Xpriv, index: u32) -> PublicKey { + // WARNING: LEAKING THE MASTER PUBLIC KEY IS EQUIVALENT TO LEAKING THE PRIVATE KEY! + Xpub::from_priv(&SECP256K1, private_key) + .derive_pub(&SECP256K1, &[ChildNumber::Normal { index }]) + .expect("Failed to derive first pubkey") + .public_key + } + + pub fn new(private_key: Xpriv) -> Self { + //let private_key = master_private_key + // .derive_priv( + // &SECP256K1, + // &[ChildNumber::Hardened { + // index: self.accounts.len() as u32, + // }], + // ) + // .expect("Unable to derive a private key for the account"); + + let account = AccountState::new(Self::generate_public_key(&private_key, 0).serialize().to_vec()); + + let address = account.owner; + ClientAccount { + address, + num_pubkeys: 0, + private_key, + } + } +} + #[derive(Serialize, Deserialize, Debug)] pub struct Account { - pub state: AccountState, pub proof: Option, - pub num_pubkeys: u32, - private_key: PrivateKey, pub coin_queue: Vec, pub coin_history: SparseMerkleTree, + pub balance: u64, } impl Account { + pub fn new() -> Self { + Account { + proof: None, + coin_queue: vec![], + coin_history: SparseMerkleTree::new(), + balance: 0, + } + } /// Uses the coin_template and next_public_key to create the next account_state and generates a /// Coin with filled in identifier (as it commits to the next account state hash). pub fn create_coins( &self, + address: HashDigest, next_public_key: PublicKey, + public_key: lib::PublicKey, coin_templates: Vec, ) -> Vec { - let mut next_account_state = self.state.clone(); - next_account_state.balance = self.get_balance(); + let mut next_account_state = AccountState { + owner: address, + balance: self.get_balance(), + public_key, + }; for coin_template in &coin_templates { // Apply all coins. next_account_state.balance = next_account_state @@ -93,113 +186,45 @@ impl Account { pub fn get_balance(&self) -> Amount { self.coin_queue .iter() - .fold(self.state.balance, |acc, x| acc + x.coin.amount) + .fold(self.balance, |acc, x| acc + x.coin.amount) } } -#[derive(Serialize, Deserialize)] -pub struct Wallet { +pub struct Server { accounts: HashMap, - #[serde(skip)] prover: Prover, - master_private_key: PrivateKey, state: Arc>, } -fn generate_public_key(private_key: &Xpriv, index: u32) -> PublicKey { - // WARNING: LEAKING THE MASTER PUBLIC KEY IS EQUIVALENT TO LEAKING THE PRIVATE KEY! - Xpub::from_priv(&SECP256K1, private_key) - .derive_pub(&SECP256K1, &[ChildNumber::Normal { index }]) - .expect("Failed to derive first pubkey") - .public_key -} -impl Wallet { +impl Server { + // TODO: Move to client. /// Get the keypair to the pubkey this account commited to (which is derived key num_pubkeys - /// 1) - fn private_key(&self, account: &Account) -> SecretKey { - account - .private_key - .derive_priv( - &SECP256K1, - &[ChildNumber::Normal { - index: account - .num_pubkeys - .checked_sub(1) - .expect("This account was never commited to."), - }], - ) - .expect("Unable to derive private key for account") - .private_key - } pub fn new(state: Arc>) -> Self { let accounts = HashMap::new(); let prover = Prover::new(); - - let mut rng = OsRng; - let mut seed = [0u8; 32]; - rng.fill_bytes(&mut seed); - - let mut wallet = Wallet { + + Server { accounts, prover, - master_private_key: PrivateKey::new_master(Network::Bitcoin, &seed) - .expect("Failed to create private key."), state, - }; - wallet.create_account(); - wallet - } - - pub fn create_account(&mut self) -> Address { - let private_key = self - .master_private_key - .derive_priv( - &SECP256K1, - &[ChildNumber::Hardened { - index: self.accounts.len() as u32, - }], - ) - .expect("Unable to derive a private key for the account"); - - let account = AccountState::new(generate_public_key(&private_key, 0).serialize().to_vec()); - - let address = account.owner; - - // Initialize account. - self.accounts.insert( - account.owner, - Account { - state: account, - proof: None, - num_pubkeys: 0, - coin_queue: vec![], - private_key, - coin_history: SparseMerkleTree::new(), - }, - ); - address - } - - pub fn import_account(&mut self, account: Account) { - self.accounts.insert(account.state.owner, account); + } } - pub fn get_balance(&self) -> Amount { - let mut balance = 0_u64; - for account in self.accounts.values() { - balance += account.state.balance; - } - balance + pub fn import_account(&mut self, address: HashDigest, account: Account) { + self.accounts.insert(address, account); } + // TODO: User needs to provide a signature and the salt and the secret information for the + // address to authenticate. pub fn get_account_balance(&self, account_address: &Address) -> Result { match self.accounts.get(account_address) { Some(account) => Ok(account .coin_queue .iter() - .fold(account.state.balance, |acc, x| acc + x.coin.amount)), + .fold(account.balance, |acc, x| acc + x.coin.amount)), _ => Err("No account with this address".to_string()), } } @@ -224,8 +249,7 @@ impl Wallet { // TODO: Return an error instead let mut account = self .accounts - .remove(&coin_proof.coin.recipient) - .expect("No account in wallet is the recipient of the coin."); + .remove(&coin_proof.coin.recipient).unwrap_or_else(|| Account::new()); // Check if we could generate updated account proof. (e.g. the coin is valid) // TODO: Check if the public key is not included in our accumulator yet (or belongs to the @@ -241,8 +265,9 @@ impl Wallet { //); // TODO: Make sure the coin_queue doesn't include this coin already. + let address = coin_proof.coin.recipient.clone(); account.coin_queue.push(coin_proof); - self.accounts.insert(account.state.owner, account); + self.accounts.insert(address, account); Ok(()) } @@ -291,11 +316,13 @@ impl Wallet { pub fn send_coins( &mut self, invoices: Vec, - account_address: &Address, + account_address: Address, + public_key: PublicKey, + next_public_key: PublicKey ) -> Result, String> { let state = &self.state.lock().unwrap(); // Check if the account balance is enough - let balance = self.get_account_balance(account_address)?; + let balance = self.get_account_balance(&account_address)?; let invoiced_amount = invoices.iter().fold(0, |acc, x| acc + x.amount); if balance < invoiced_amount { return Err("Insufficient funds".into()); @@ -303,12 +330,11 @@ impl Wallet { let mut account = self .accounts - .remove(account_address) + .remove(&account_address) .unwrap_or_else(|| unreachable!()); - // Derive a new public key from the private key. - let next_public_key = generate_public_key(&account.private_key, account.num_pubkeys + 1); - + // TODO: Copy this over to the client because they too have to check that the + // out_coins_tree is correct and only contains the coins from the invoices. // Create the coin templates. let mut coin_templates = vec![]; for invoice in invoices { @@ -322,7 +348,7 @@ impl Wallet { for coin_proof in &account.coin_queue { coin_history_proofs.push(self.get_merkle_proofs( coin_proof.proof.clone(), - coin_proof.commitment.public_key.clone(), + coin_proof.commitment.clone().expect("Coin is missing commitment").public_key.clone(), state, )); coin_non_inclusion_proofs.push( @@ -340,8 +366,12 @@ impl Wallet { } let mut proof_hints_builder = ProgramInputsBuilder::default(); let proof_hints_builder = proof_hints_builder - .account_state(account.state.clone()) - .next_public_key(next_public_key.serialize().to_vec()) + .account_state(AccountState { + owner: account_address.clone(), + balance, + public_key: public_key.serialize().to_vec(), + }) + .next_public_key(next_public_key.clone().serialize().to_vec()) // Create the coin. (In case of multiple coins adjust AccountState.create_coin to apply // all coin templates first and then create the identifier from the final account // state.) @@ -351,7 +381,7 @@ impl Wallet { .in_coin_proofs_non_inclusion_proofs(coin_non_inclusion_proofs) .current_history_root(state.mmr.root()); - let out_coins = account.create_coins(next_public_key, coin_templates); + let out_coins = account.create_coins(account_address.clone(), next_public_key, public_key.serialize().to_vec(), coin_templates); let mut out_coins_tree = SparseMerkleTree::new(); let mut current_root = DEFAULT_HASHES[0]; assert_eq!( @@ -389,8 +419,7 @@ impl Wallet { let proof = match account.proof.take() { Some(account_proof) => { - let account_commitment_public_key = - generate_public_key(&account.private_key, account.num_pubkeys.checked_sub(1).expect("account num_pubkey is 0 although there is a previous proof. This is a bug.")); + let account_commitment_public_key = public_key; proof_hints_builder.prev_proof_history_proofs(Some(self.get_merkle_proofs( account_proof.clone(), account_commitment_public_key, @@ -409,20 +438,19 @@ impl Wallet { }; // Update account. - account.state.balance = balance - invoiced_amount; - account.state.public_key = next_public_key.serialize().to_vec(); - account.num_pubkeys += 1; + account.balance = balance - invoiced_amount; account.proof = Some(proof.clone()); - let private_key = self.private_key(&account); - let commitment = Commitment::new( - &private_key, - hash_concat(&account.state.hash(), &out_coins_tree.root()).to_vec(), - ) - .expect("Unable to create commitment"); + //// TODO: Move this to the client + //let private_key = self.private_key(&account); + //let commitment = Commitment::new( + // &private_key, + // hash_concat(&account.state.hash(), &out_coins_tree.root()).to_vec(), + //) + //.expect("Unable to create commitment"); - // Insert account back into wallet. - self.accounts.insert(*account_address, account); + // Insert account back into database. + self.accounts.insert(account_address, account); assert_eq!( out_coins_tree.root(), bincode::deserialize::(&proof.public_values.to_vec()) @@ -441,46 +469,29 @@ impl Wallet { .unwrap() .0, coin, - commitment: commitment.clone(), + // User will fill in the commitment and send back this proof to the server. + commitment: None, }); } Ok(coin_proofs) } - pub fn get_minting_account_address(&mut self) -> HashDigest { - let mut rng = OsRng; - let mut seed = include_bytes!("../minting_secret.bin"); - //rng.fill_bytes(&mut seed); + // TODO: Move this to tests + pub fn minting_account_private_key() -> Xpriv { + let secret= include_bytes!("../minting_secret.bin"); + PrivateKey::new_master(Network::Bitcoin, secret).expect("Failed to create private key.") + } - let private_key = - PrivateKey::new_master(Network::Bitcoin, seed).expect("Failed to create private key."); - let minting_account_address = hash(private_key.to_string().as_bytes()); + // TODO: Do not use private key here anymore but store address as a static const + pub fn get_minting_account_address(&mut self) -> Result { + let minting_account_address = hash(Self::minting_account_private_key().to_string().as_bytes()); if self.accounts.get(&minting_account_address).is_some() { - return minting_account_address; + println!("Set MINTING_ADDRESS to {:?}", minting_account_address); + Ok(minting_account_address) + } else { + Err("Minting account not creaputed".into()) } - - let minting_account_state = AccountState { - owner: hash(private_key.to_string().as_bytes()), - balance: 10_000, - public_key: generate_public_key(&private_key, 0).serialize().to_vec(), - }; - - println!("Set MINTING_ADDRESS to {:?}", minting_account_state.owner); - let minting_account_address = minting_account_state.owner.clone(); - - // Create account with custom balance. - //let proof = prover.create_account(&minting_account_state); - let minting_account = Account { - state: minting_account_state, - proof: None, - num_pubkeys: 0, - coin_queue: vec![], - private_key, - coin_history: SparseMerkleTree::new(), - }; - self.import_account(minting_account); - minting_account_address } } @@ -488,101 +499,108 @@ impl Wallet { mod tests { use super::*; use crate::state::State; - use tokio::time::Instant; use zkcoins_program::MINTING_ADDRESS; + fn create_minting_account() -> ClientAccount { + ClientAccount::new(Server::minting_account_private_key()) + } + #[test] fn test_wallet_operations() { let state = Arc::new(Mutex::new(State::new())); - let mut wallet = Wallet::new(state); - let minting_address = wallet.get_minting_account_address(); + let mut server = Server::new(state); + let mut minting_account = create_minting_account(); + server.import_account(minting_account.address, Account { + proof: None, + coin_queue: vec![], + coin_history: SparseMerkleTree::new(), + balance: 10_000, + }); assert_eq!( - MINTING_ADDRESS, minting_address, + MINTING_ADDRESS, server.get_minting_account_address().unwrap(), "Minting address in wallet and program are different" ); - let account_2 = wallet.create_account(); - let account_1 = wallet.create_account(); + let mut account_1 = ClientAccount::new(Xpriv::new_master(Network::Signet, &[1u8; 32]).unwrap()); + let mut account_2 = ClientAccount::new(Xpriv::new_master(Network::Signet, &[2u8; 32]).unwrap()); assert_eq!( - wallet.get_account_balance(&MINTING_ADDRESS).unwrap(), + server.get_account_balance(&MINTING_ADDRESS).unwrap(), 10_000 ); - assert_eq!(wallet.get_account_balance(&account_1).unwrap(), 0); - assert_eq!(wallet.get_account_balance(&account_2).unwrap(), 0); + assert_eq!(server.get_account_balance(&account_1.address).unwrap(), 0); + assert_eq!(server.get_account_balance(&account_2.address).unwrap(), 0); - let account_2_invoice = Invoice::new(100, account_2); - let account_1_invoice = Invoice::new(100, account_1); + let account_2_invoice = Invoice::new(100, account_2.address); + let account_1_invoice = Invoice::new(100, account_1.address); - let mut coin_proofs = wallet - .send_coins(vec![account_2_invoice, account_1_invoice], &MINTING_ADDRESS) - .expect("Unable to send coin"); - wallet.state.lock().unwrap().update( + let coin_proofs = minting_account.send_coins(&mut server,vec![account_2_invoice, account_1_invoice]).unwrap(); + server.state.lock().unwrap().update( &coin_proofs .iter() - .map(|x| x.commitment.clone()) + .map(|x| x.commitment.clone().unwrap()) .collect::>(), ); - wallet - .receive_coin(coin_proofs.pop().unwrap()) - .expect("Unable to receive coin"); - wallet - .receive_coin(coin_proofs.pop().unwrap()) - .expect("Unable to receive coin"); - assert_eq!(wallet.get_account_balance(&account_1).unwrap(), 100); - assert_eq!(wallet.get_account_balance(&account_2).unwrap(), 100); - println!("Minting succesfull"); - - let mut coin_proofs = wallet - .send_coins(vec![account_1_invoice], &account_2) - .expect("Unable to send coin"); - wallet.state.lock().unwrap().update( - &coin_proofs - .iter() - .map(|x| x.commitment.clone()) - .collect::>(), - ); - assert_eq!(wallet.get_account_balance(&account_1).unwrap(), 100); - assert_eq!(wallet.get_account_balance(&account_2).unwrap(), 0); - - wallet - .receive_coin(coin_proofs.pop().unwrap()) - .expect("Unable to receive coin"); - assert_eq!(wallet.get_account_balance(&account_1).unwrap(), 200); - assert_eq!(wallet.get_account_balance(&account_2).unwrap(), 0); - - // Send with timer - let start = Instant::now(); - let mut coin_proofs = wallet - .send_coins(vec![account_2_invoice], &account_1) - .expect("Unable to send coin"); - let duration = start.elapsed(); - wallet.state.lock().unwrap().update( - &coin_proofs - .iter() - .map(|x| x.commitment.clone()) - .collect::>(), - ); - println!("TIME ELAPSED FOR ONE RECURSIVE SEND: {:?}", duration); - wallet - .receive_coin(coin_proofs.pop().unwrap()) - .expect("Unable to receive coin"); - assert_eq!(wallet.get_account_balance(&account_1).unwrap(), 100); - assert_eq!(wallet.get_account_balance(&account_2).unwrap(), 100); + //server + // .receive_coin(coin_proofs.pop().unwrap()) + // .expect("Unable to receive coin"); + //server + // .receive_coin(coin_proofs.pop().unwrap()) + // .expect("Unable to receive coin"); + //assert_eq!(server.get_account_balance(&account_1).unwrap(), 100); + //assert_eq!(server.get_account_balance(&account_2).unwrap(), 100); + //println!("Minting succesfull"); + + //let mut coin_proofs = server + // .send_coins(vec![account_1_invoice], &account_2, generate_public_key(&server.accounts.get(account_2).private_key, account.num_pubkeys + 1); + // .expect("Unable to send coin"); + //server.state.lock().unwrap().update( + // &coin_proofs + // .iter() + // .map(|x| x.commitment.clone()) + // .collect::>(), + //); + //assert_eq!(server.get_account_balance(&account_1).unwrap(), 100); + //assert_eq!(server.get_account_balance(&account_2).unwrap(), 0); + + //server + // .receive_coin(coin_proofs.pop().unwrap()) + // .expect("Unable to receive coin"); + //assert_eq!(server.get_account_balance(&account_1).unwrap(), 200); + //assert_eq!(server.get_account_balance(&account_2).unwrap(), 0); + + //// Send with timer + //let start = Instant::now(); + //let mut coin_proofs = server + // .send_coins(vec![account_2_invoice], &account_1) + // .expect("Unable to send coin"); + //let duration = start.elapsed(); + //server.state.lock().unwrap().update( + // &coin_proofs + // .iter() + // .map(|x| x.commitment.clone()) + // .collect::>(), + //); + //println!("TIME ELAPSED FOR ONE RECURSIVE SEND: {:?}", duration); + //server + // .receive_coin(coin_proofs.pop().unwrap()) + // .expect("Unable to receive coin"); + //assert_eq!(server.get_account_balance(&account_1).unwrap(), 100); + //assert_eq!(server.get_account_balance(&account_2).unwrap(), 100); } - #[test] - fn test_create_minting_account() { - let state = Arc::new(Mutex::new(State::new())); - let mut wallet = Wallet::new(state); - assert_eq!( - wallet.get_minting_account_address(), - MINTING_ADDRESS, - "Minting address in program not the same as in wallet" - ); - assert_eq!( - wallet.get_account_balance(&MINTING_ADDRESS).unwrap(), - 10_000 - ); - } + //#[test] + //fn test_create_minting_account() { + // let state = Arc::new(Mutex::new(State::new())); + // let mut wallet = Server::new(state); + // assert_eq!( + // wallet.get_minting_account_address(), + // MINTING_ADDRESS, + // "Minting address in program not the same as in wallet" + // ); + // assert_eq!( + // wallet.get_account_balance(&MINTING_ADDRESS).unwrap(), + // 10_000 + // ); + //} } From e1fbb35425422ef90ba44365158f11dfa01eeb3d Mon Sep 17 00:00:00 2001 From: Lukas Date: Thu, 1 May 2025 16:05:40 +0200 Subject: [PATCH 06/21] Fix compilation issues --- Cargo.lock | 3 +++ server/src/main.rs | 6 ++--- server/src/server.rs | 34 ++++++++++++++------------- server/src/wallet.rs | 55 +++++++++++++++++++++++--------------------- 4 files changed, 53 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dd46fe5..184a95d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1202,6 +1202,9 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" name = "client" version = "1.1.0" dependencies = [ + "bitcoin", + "getrandom 0.2.15", + "hex", "wasm-bindgen", ] diff --git a/server/src/main.rs b/server/src/main.rs index 1d674ed..f367130 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -73,10 +73,10 @@ async fn main() -> Result<(), Box> { )); // Create a new wallet instance with a reference to the state - let mut wallet = wallet::Wallet::new(Arc::clone(&state)); + let mut wallet = wallet::Server::new(Arc::clone(&state)); - // Call create_account without arguments - wallet.create_account(); + // TODO: Create minting account + //wallet.create_account(); // Spawn the wallet server as a separate task tokio::spawn(async move { diff --git a/server/src/server.rs b/server/src/server.rs index aabee5b..8069aa7 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -83,7 +83,7 @@ pub struct SendCoinResponse { // Handler functions for our REST API async fn get_balance_handler( - State((wallet, _)): State<(Arc>, Arc)>, + State((wallet, _)): State<(Arc>, Arc)>, axum::extract::Query(params): axum::extract::Query>, ) -> impl IntoResponse { let wallet = wallet.lock().unwrap(); @@ -118,18 +118,13 @@ async fn get_balance_handler( Err(_) => (StatusCode::NOT_FOUND, Json(BalanceResponse { balance: 0 })), } } else { - // If no address is provided, return total wallet balance (for backward compatibility) - ( - StatusCode::OK, - Json(BalanceResponse { - balance: wallet.get_balance(), - }), - ) + // TODO: Return an error + todo!() } } async fn get_address_handler( - State((wallet, _)): State<(Arc>, Arc)>, + State((wallet, _)): State<(Arc>, Arc)>, ) -> impl IntoResponse { let wallet = wallet.lock().unwrap(); @@ -146,7 +141,7 @@ async fn get_address_handler( } async fn receive_coin_handler( - State((wallet, _)): State<(Arc>, Arc)>, + State((wallet, _)): State<(Arc>, Arc)>, body: Bytes, // Accept raw binary data instead of multipart ) -> impl IntoResponse { // Try to deserialize the binary data as a CoinProof @@ -175,7 +170,7 @@ async fn receive_coin_handler( } async fn send_coin_handler( - State((wallet, proof_store)): State<(Arc>, Arc)>, + State((wallet, proof_store)): State<(Arc>, Arc)>, Json(request): Json, ) -> impl IntoResponse { // Create converted addresses (from_address and to_address) @@ -220,12 +215,15 @@ async fn send_coin_handler( ); } + // TODO: Provide the correct public keys from the client // Acquire the wallet lock only for the duration of sending coins. let send_result = { let mut wallet_guard = wallet.lock().unwrap(); wallet_guard.send_coins( vec![Invoice::new(request.amount, to_address)], - &from_address, + from_address, + todo!(), + todo!(), ) }; @@ -270,7 +268,7 @@ async fn send_coin_handler( // TODO: This has to be eventually replaced by a faucet (or some way for wallets to share the // minting account) async fn mint_handler( - State((wallet, proof_store)): State<(Arc>, Arc)>, + State((wallet, proof_store)): State<(Arc>, Arc)>, Json(request): Json, ) -> impl IntoResponse { let account_address_vec = match hex::decode(request.account_address.trim_start_matches("0x")) { @@ -299,13 +297,16 @@ async fn mint_handler( ); } + // TODO: Fill in the correct public keys for the minting account (create it at server start) // Acquire the wallet lock only for the duration of Minting coins. let send_result = { let mut wallet_guard = wallet.lock().unwrap(); - let minting_address = &wallet_guard.get_minting_account_address(); + let minting_address = wallet_guard.get_minting_account_address().unwrap(); wallet_guard.send_coins( vec![Invoice::new(request.amount, account_address)], minting_address, + todo!(), + todo!(), ) }; @@ -348,7 +349,7 @@ async fn mint_handler( // New handler to get a binary proof by ID async fn get_proof_handler( - State((_, proof_store)): State<(Arc>, Arc)>, + State((_, proof_store)): State<(Arc>, Arc)>, Path(id): Path, ) -> impl IntoResponse { match proof_store.get_proof(id) { @@ -377,8 +378,9 @@ async fn get_proof_handler( } } +// TODO: Create a minting account that is stored in Server // Function to start the REST API server -pub async fn start_rest_server(wallet: Wallet, addr: &str) -> anyhow::Result<()> { +pub async fn start_rest_server(wallet: Server, addr: &str) -> anyhow::Result<()> { // Parse the address string into a SocketAddr let socket_addr = addr .parse::() diff --git a/server/src/wallet.rs b/server/src/wallet.rs index 76a25f2..aa45ec0 100644 --- a/server/src/wallet.rs +++ b/server/src/wallet.rs @@ -477,18 +477,10 @@ impl Server { Ok(coin_proofs) } - // TODO: Move this to tests - pub fn minting_account_private_key() -> Xpriv { - let secret= include_bytes!("../minting_secret.bin"); - PrivateKey::new_master(Network::Bitcoin, secret).expect("Failed to create private key.") - } - - // TODO: Do not use private key here anymore but store address as a static const pub fn get_minting_account_address(&mut self) -> Result { - let minting_account_address = hash(Self::minting_account_private_key().to_string().as_bytes()); - if self.accounts.get(&minting_account_address).is_some() { - println!("Set MINTING_ADDRESS to {:?}", minting_account_address); - Ok(minting_account_address) + println!("Set MINTING_ADDRESS to {:?}", &zkcoins_program::MINTING_ADDRESS); + if self.accounts.get(&zkcoins_program::MINTING_ADDRESS).is_some() { + Ok(zkcoins_program::MINTING_ADDRESS) } else { Err("Minting account not creaputed".into()) } @@ -502,7 +494,9 @@ mod tests { use zkcoins_program::MINTING_ADDRESS; fn create_minting_account() -> ClientAccount { - ClientAccount::new(Server::minting_account_private_key()) + let secret= include_bytes!("../minting_secret.bin"); + let private_key = PrivateKey::new_master(Network::Bitcoin, secret).expect("Failed to create private key."); + ClientAccount::new(private_key) } #[test] @@ -510,6 +504,7 @@ mod tests { let state = Arc::new(Mutex::new(State::new())); let mut server = Server::new(state); let mut minting_account = create_minting_account(); + println!("minting account address: {:?}", minting_account.address); server.import_account(minting_account.address, Account { proof: None, coin_queue: vec![], @@ -589,18 +584,26 @@ mod tests { //assert_eq!(server.get_account_balance(&account_2).unwrap(), 100); } - //#[test] - //fn test_create_minting_account() { - // let state = Arc::new(Mutex::new(State::new())); - // let mut wallet = Server::new(state); - // assert_eq!( - // wallet.get_minting_account_address(), - // MINTING_ADDRESS, - // "Minting address in program not the same as in wallet" - // ); - // assert_eq!( - // wallet.get_account_balance(&MINTING_ADDRESS).unwrap(), - // 10_000 - // ); - //} + #[test] + fn test_create_minting_account() { + let state = Arc::new(Mutex::new(State::new())); + let mut server = Server::new(state); + let mut minting_account = create_minting_account(); + println!("minting account address: {:?}", minting_account.address); + server.import_account(minting_account.address, Account { + proof: None, + coin_queue: vec![], + coin_history: SparseMerkleTree::new(), + balance: 10_000, + }); + assert_eq!( + server.get_minting_account_address(), + MINTING_ADDRESS, + "Minting address in program not the same as in wallet" + ); + assert_eq!( + server.get_account_balance(&MINTING_ADDRESS).unwrap(), + 10_000 + ); + } } From dc57f47a84f8fcdff227e264d7123f7eeb841af1 Mon Sep 17 00:00:00 2001 From: robinlinus Date: Thu, 1 May 2025 07:25:25 -0700 Subject: [PATCH 07/21] Fix nonce reuse in signing --- server/src/commitment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/commitment.rs b/server/src/commitment.rs index d95d8cf..30dfe6a 100644 --- a/server/src/commitment.rs +++ b/server/src/commitment.rs @@ -34,7 +34,7 @@ impl Commitment { let keypair = Keypair::from_secret_key(&SECP256K1, secret_key); // Sign the message using Schnorr signature with the keypair - let signature = SECP256K1.sign_schnorr_with_aux_rand(&msg, &keypair, &[0; 32]); + let signature = SECP256K1.sign_schnorr_no_aux_rand(&msg, &keypair); Ok(Self { public_key: keypair.public_key(), From b3a5c838d8298594d731536a8a27ba82e78cdfe1 Mon Sep 17 00:00:00 2001 From: Lukas Date: Fri, 2 May 2025 12:12:42 +0200 Subject: [PATCH 08/21] Fix minting account test --- server/src/wallet.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/server/src/wallet.rs b/server/src/wallet.rs index aa45ec0..83980ff 100644 --- a/server/src/wallet.rs +++ b/server/src/wallet.rs @@ -478,7 +478,6 @@ impl Server { } pub fn get_minting_account_address(&mut self) -> Result { - println!("Set MINTING_ADDRESS to {:?}", &zkcoins_program::MINTING_ADDRESS); if self.accounts.get(&zkcoins_program::MINTING_ADDRESS).is_some() { Ok(zkcoins_program::MINTING_ADDRESS) } else { @@ -496,7 +495,12 @@ mod tests { fn create_minting_account() -> ClientAccount { let secret= include_bytes!("../minting_secret.bin"); let private_key = PrivateKey::new_master(Network::Bitcoin, secret).expect("Failed to create private key."); - ClientAccount::new(private_key) + println!("Set MINTING_ADDRESS to {:?}", &zkcoins_program::MINTING_ADDRESS); + ClientAccount { + address: hash(private_key.to_string().as_bytes()), + num_pubkeys: 0, + private_key, + } } #[test] @@ -588,7 +592,7 @@ mod tests { fn test_create_minting_account() { let state = Arc::new(Mutex::new(State::new())); let mut server = Server::new(state); - let mut minting_account = create_minting_account(); + let minting_account = create_minting_account(); println!("minting account address: {:?}", minting_account.address); server.import_account(minting_account.address, Account { proof: None, @@ -597,9 +601,9 @@ mod tests { balance: 10_000, }); assert_eq!( - server.get_minting_account_address(), + server.get_minting_account_address().unwrap(), MINTING_ADDRESS, - "Minting address in program not the same as in wallet" + "Minting address is not stored in server." ); assert_eq!( server.get_account_balance(&MINTING_ADDRESS).unwrap(), From 6e60f3674691644093e08562740d54e41dcb059e Mon Sep 17 00:00:00 2001 From: Lukas Date: Fri, 2 May 2025 12:22:52 +0200 Subject: [PATCH 09/21] Fix wallet operations test --- server/src/wallet.rs | 243 ++++++++++++++++++++++++++----------------- 1 file changed, 150 insertions(+), 93 deletions(-) diff --git a/server/src/wallet.rs b/server/src/wallet.rs index 83980ff..8834ae7 100644 --- a/server/src/wallet.rs +++ b/server/src/wallet.rs @@ -53,7 +53,7 @@ impl Invoice { pub struct ClientAccount { pub address: HashDigest, pub num_pubkeys: u32, - private_key: PrivateKey + private_key: PrivateKey, } // TODO Move to tests or client @@ -67,8 +67,7 @@ pub fn new_master_private_key() -> PrivateKey { // TODO: Move this to client impl ClientAccount { fn current_private_key(&self) -> SecretKey { - self - .private_key + self.private_key .derive_priv( &SECP256K1, &[ChildNumber::Normal { @@ -83,14 +82,35 @@ impl ClientAccount { } pub fn sign_proof(&self, proof: &mut CoinProof) { - let proof_data = bincode::deserialize::(&proof.proof.public_values.clone().to_vec()).expect("Proof is invalid"); - proof.commitment = Some(Commitment::new(&self.current_private_key(), hash_concat(&proof_data.account_state_hash, &proof_data.output_coins_root).to_vec()).expect("Should be able to create commitment")); + let proof_data = + bincode::deserialize::(&proof.proof.public_values.clone().to_vec()) + .expect("Proof is invalid"); + proof.commitment = Some( + Commitment::new( + &self.current_private_key(), + hash_concat( + &proof_data.account_state_hash, + &proof_data.output_coins_root, + ) + .to_vec(), + ) + .expect("Should be able to create commitment"), + ); } - + // TODO: Remove the server as an argument and send a REST request, fill in the commitment and // send it back to server. - pub fn send_coins(&mut self, server: &mut Server, invoices: Vec) -> Result, String> { - let result = server.send_coins(invoices, self.address, Self::generate_public_key(&self.private_key, self.num_pubkeys),Self::generate_public_key(&self.private_key, self.num_pubkeys + 1)); + pub fn send_coins( + &mut self, + server: &mut Server, + invoices: Vec, + ) -> Result, String> { + let result = server.send_coins( + invoices, + self.address, + Self::generate_public_key(&self.private_key, self.num_pubkeys), + Self::generate_public_key(&self.private_key, self.num_pubkeys + 1), + ); self.num_pubkeys += 1; match result { Ok(mut proofs) => { @@ -104,11 +124,11 @@ impl ClientAccount { } pub fn generate_public_key(private_key: &Xpriv, index: u32) -> PublicKey { - // WARNING: LEAKING THE MASTER PUBLIC KEY IS EQUIVALENT TO LEAKING THE PRIVATE KEY! - Xpub::from_priv(&SECP256K1, private_key) - .derive_pub(&SECP256K1, &[ChildNumber::Normal { index }]) - .expect("Failed to derive first pubkey") - .public_key + // WARNING: LEAKING THE MASTER PUBLIC KEY IS EQUIVALENT TO LEAKING THE PRIVATE KEY! + Xpub::from_priv(&SECP256K1, private_key) + .derive_pub(&SECP256K1, &[ChildNumber::Normal { index }]) + .expect("Failed to derive first pubkey") + .public_key } pub fn new(private_key: Xpriv) -> Self { @@ -121,7 +141,11 @@ impl ClientAccount { // ) // .expect("Unable to derive a private key for the account"); - let account = AccountState::new(Self::generate_public_key(&private_key, 0).serialize().to_vec()); + let account = AccountState::new( + Self::generate_public_key(&private_key, 0) + .serialize() + .to_vec(), + ); let address = account.owner; ClientAccount { @@ -196,7 +220,6 @@ pub struct Server { state: Arc>, } - impl Server { // TODO: Move to client. /// Get the keypair to the pubkey this account commited to (which is derived key num_pubkeys - @@ -205,7 +228,7 @@ impl Server { pub fn new(state: Arc>) -> Self { let accounts = HashMap::new(); let prover = Prover::new(); - + Server { accounts, prover, @@ -249,7 +272,8 @@ impl Server { // TODO: Return an error instead let mut account = self .accounts - .remove(&coin_proof.coin.recipient).unwrap_or_else(|| Account::new()); + .remove(&coin_proof.coin.recipient) + .unwrap_or_else(|| Account::new()); // Check if we could generate updated account proof. (e.g. the coin is valid) // TODO: Check if the public key is not included in our accumulator yet (or belongs to the @@ -318,7 +342,7 @@ impl Server { invoices: Vec, account_address: Address, public_key: PublicKey, - next_public_key: PublicKey + next_public_key: PublicKey, ) -> Result, String> { let state = &self.state.lock().unwrap(); // Check if the account balance is enough @@ -346,11 +370,18 @@ impl Server { let mut coin_inclusion_proofs = vec![]; let mut in_coins = vec![]; for coin_proof in &account.coin_queue { - coin_history_proofs.push(self.get_merkle_proofs( - coin_proof.proof.clone(), - coin_proof.commitment.clone().expect("Coin is missing commitment").public_key.clone(), - state, - )); + coin_history_proofs.push( + self.get_merkle_proofs( + coin_proof.proof.clone(), + coin_proof + .commitment + .clone() + .expect("Coin is missing commitment") + .public_key + .clone(), + state, + ), + ); coin_non_inclusion_proofs.push( account .coin_history @@ -368,7 +399,7 @@ impl Server { let proof_hints_builder = proof_hints_builder .account_state(AccountState { owner: account_address.clone(), - balance, + balance: account.balance, public_key: public_key.serialize().to_vec(), }) .next_public_key(next_public_key.clone().serialize().to_vec()) @@ -381,7 +412,12 @@ impl Server { .in_coin_proofs_non_inclusion_proofs(coin_non_inclusion_proofs) .current_history_root(state.mmr.root()); - let out_coins = account.create_coins(account_address.clone(), next_public_key, public_key.serialize().to_vec(), coin_templates); + let out_coins = account.create_coins( + account_address.clone(), + next_public_key, + public_key.serialize().to_vec(), + coin_templates, + ); let mut out_coins_tree = SparseMerkleTree::new(); let mut current_root = DEFAULT_HASHES[0]; assert_eq!( @@ -478,7 +514,11 @@ impl Server { } pub fn get_minting_account_address(&mut self) -> Result { - if self.accounts.get(&zkcoins_program::MINTING_ADDRESS).is_some() { + if self + .accounts + .get(&zkcoins_program::MINTING_ADDRESS) + .is_some() + { Ok(zkcoins_program::MINTING_ADDRESS) } else { Err("Minting account not creaputed".into()) @@ -488,14 +528,20 @@ impl Server { #[cfg(test)] mod tests { + use std::time::Instant; + use super::*; use crate::state::State; use zkcoins_program::MINTING_ADDRESS; fn create_minting_account() -> ClientAccount { - let secret= include_bytes!("../minting_secret.bin"); - let private_key = PrivateKey::new_master(Network::Bitcoin, secret).expect("Failed to create private key."); - println!("Set MINTING_ADDRESS to {:?}", &zkcoins_program::MINTING_ADDRESS); + let secret = include_bytes!("../minting_secret.bin"); + let private_key = PrivateKey::new_master(Network::Bitcoin, secret) + .expect("Failed to create private key."); + println!( + "Set MINTING_ADDRESS to {:?}", + &zkcoins_program::MINTING_ADDRESS + ); ClientAccount { address: hash(private_key.to_string().as_bytes()), num_pubkeys: 0, @@ -509,30 +555,38 @@ mod tests { let mut server = Server::new(state); let mut minting_account = create_minting_account(); println!("minting account address: {:?}", minting_account.address); - server.import_account(minting_account.address, Account { - proof: None, - coin_queue: vec![], - coin_history: SparseMerkleTree::new(), - balance: 10_000, - }); + server.import_account( + minting_account.address, + Account { + proof: None, + coin_queue: vec![], + coin_history: SparseMerkleTree::new(), + balance: 10_000, + }, + ); assert_eq!( - MINTING_ADDRESS, server.get_minting_account_address().unwrap(), + MINTING_ADDRESS, + server.get_minting_account_address().unwrap(), "Minting address in wallet and program are different" ); - let mut account_1 = ClientAccount::new(Xpriv::new_master(Network::Signet, &[1u8; 32]).unwrap()); - let mut account_2 = ClientAccount::new(Xpriv::new_master(Network::Signet, &[2u8; 32]).unwrap()); + let mut account_1 = + ClientAccount::new(Xpriv::new_master(Network::Signet, &[1u8; 32]).unwrap()); + let mut account_2 = + ClientAccount::new(Xpriv::new_master(Network::Signet, &[2u8; 32]).unwrap()); assert_eq!( server.get_account_balance(&MINTING_ADDRESS).unwrap(), 10_000 ); - assert_eq!(server.get_account_balance(&account_1.address).unwrap(), 0); - assert_eq!(server.get_account_balance(&account_2.address).unwrap(), 0); + assert!(server.get_account_balance(&account_1.address).is_err()); + assert!(server.get_account_balance(&account_2.address).is_err()); let account_2_invoice = Invoice::new(100, account_2.address); let account_1_invoice = Invoice::new(100, account_1.address); - let coin_proofs = minting_account.send_coins(&mut server,vec![account_2_invoice, account_1_invoice]).unwrap(); + let mut coin_proofs = minting_account + .send_coins(&mut server, vec![account_2_invoice, account_1_invoice]) + .unwrap(); server.state.lock().unwrap().update( &coin_proofs .iter() @@ -540,52 +594,52 @@ mod tests { .collect::>(), ); - //server - // .receive_coin(coin_proofs.pop().unwrap()) - // .expect("Unable to receive coin"); - //server - // .receive_coin(coin_proofs.pop().unwrap()) - // .expect("Unable to receive coin"); - //assert_eq!(server.get_account_balance(&account_1).unwrap(), 100); - //assert_eq!(server.get_account_balance(&account_2).unwrap(), 100); - //println!("Minting succesfull"); - - //let mut coin_proofs = server - // .send_coins(vec![account_1_invoice], &account_2, generate_public_key(&server.accounts.get(account_2).private_key, account.num_pubkeys + 1); - // .expect("Unable to send coin"); - //server.state.lock().unwrap().update( - // &coin_proofs - // .iter() - // .map(|x| x.commitment.clone()) - // .collect::>(), - //); - //assert_eq!(server.get_account_balance(&account_1).unwrap(), 100); - //assert_eq!(server.get_account_balance(&account_2).unwrap(), 0); - - //server - // .receive_coin(coin_proofs.pop().unwrap()) - // .expect("Unable to receive coin"); - //assert_eq!(server.get_account_balance(&account_1).unwrap(), 200); - //assert_eq!(server.get_account_balance(&account_2).unwrap(), 0); - - //// Send with timer - //let start = Instant::now(); - //let mut coin_proofs = server - // .send_coins(vec![account_2_invoice], &account_1) - // .expect("Unable to send coin"); - //let duration = start.elapsed(); - //server.state.lock().unwrap().update( - // &coin_proofs - // .iter() - // .map(|x| x.commitment.clone()) - // .collect::>(), - //); - //println!("TIME ELAPSED FOR ONE RECURSIVE SEND: {:?}", duration); - //server - // .receive_coin(coin_proofs.pop().unwrap()) - // .expect("Unable to receive coin"); - //assert_eq!(server.get_account_balance(&account_1).unwrap(), 100); - //assert_eq!(server.get_account_balance(&account_2).unwrap(), 100); + server + .receive_coin(coin_proofs.pop().unwrap()) + .expect("Unable to receive coin"); + server + .receive_coin(coin_proofs.pop().unwrap()) + .expect("Unable to receive coin"); + assert_eq!(server.get_account_balance(&account_1.address).unwrap(), 100); + assert_eq!(server.get_account_balance(&account_2.address).unwrap(), 100); + println!("Minting succesfull"); + + let mut coin_proofs = account_2 + .send_coins(&mut server, vec![account_1_invoice]) + .expect("Unable to send coin"); + server.state.lock().unwrap().update( + &coin_proofs + .iter() + .map(|x| x.commitment.clone().unwrap()) + .collect::>(), + ); + assert_eq!(server.get_account_balance(&account_1.address).unwrap(), 100); + assert_eq!(server.get_account_balance(&account_2.address).unwrap(), 0); + + server + .receive_coin(coin_proofs.pop().unwrap()) + .expect("Unable to receive coin"); + assert_eq!(server.get_account_balance(&account_1.address).unwrap(), 200); + assert_eq!(server.get_account_balance(&account_2.address).unwrap(), 0); + + // Send with timer + let start = Instant::now(); + let mut coin_proofs = account_1 + .send_coins(&mut server, vec![account_2_invoice]) + .expect("Unable to send coin"); + let duration = start.elapsed(); + server.state.lock().unwrap().update( + &coin_proofs + .iter() + .map(|x| x.commitment.clone().unwrap()) + .collect::>(), + ); + println!("TIME ELAPSED FOR ONE RECURSIVE SEND: {:?}", duration); + server + .receive_coin(coin_proofs.pop().unwrap()) + .expect("Unable to receive coin"); + assert_eq!(server.get_account_balance(&account_1.address).unwrap(), 100); + assert_eq!(server.get_account_balance(&account_2.address).unwrap(), 100); } #[test] @@ -594,12 +648,15 @@ mod tests { let mut server = Server::new(state); let minting_account = create_minting_account(); println!("minting account address: {:?}", minting_account.address); - server.import_account(minting_account.address, Account { - proof: None, - coin_queue: vec![], - coin_history: SparseMerkleTree::new(), - balance: 10_000, - }); + server.import_account( + minting_account.address, + Account { + proof: None, + coin_queue: vec![], + coin_history: SparseMerkleTree::new(), + balance: 10_000, + }, + ); assert_eq!( server.get_minting_account_address().unwrap(), MINTING_ADDRESS, From cc6294b55cb5dd87f07e3c800086da54e80920a2 Mon Sep 17 00:00:00 2001 From: Lukas Date: Fri, 2 May 2025 12:36:16 +0200 Subject: [PATCH 10/21] Small TODO cleanup --- server/src/server.rs | 2 +- server/src/wallet.rs | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/server/src/server.rs b/server/src/server.rs index 8069aa7..360dd8c 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -411,7 +411,7 @@ pub async fn start_rest_server(wallet: Server, addr: &str) -> anyhow::Result<()> .fallback(serve_index); // Use our custom serve_index handler as fallback // Run the server - println!("Wallet REST server started at {}", socket_addr); + println!("REST server started at {}", socket_addr); let listener = TcpListener::bind(socket_addr).await?; axum::serve(listener, app).await?; diff --git a/server/src/wallet.rs b/server/src/wallet.rs index 8834ae7..9a83396 100644 --- a/server/src/wallet.rs +++ b/server/src/wallet.rs @@ -13,7 +13,7 @@ use bitcoin::{ use serde::{Deserialize, Serialize}; use zkcoins_program::merkle::CommitmentMerkleProofs; use zkcoins_program::{ - calculate_coin_identifier, hash, AccountState, Coin, CoinTemplate, ProgramInputsBuilder, + calculate_coin_identifier, AccountState, Coin, CoinTemplate, ProgramInputsBuilder, ProofData, ProofType, }; use zkcoins_prover::{Proof, Prover}; @@ -263,13 +263,12 @@ impl Server { // Verify the inclusion of the coin in the proof. // TODO: Return an err and also verify the proof verification itself. (Dry-run the // aggregation) - // TOOD: Verify that the commitment was not included in our state. + // TODO: Verify that the commitment was not included in our state. assert!(coin_proof .inclusion_proof .verify(coin_proof.coin.identifier, proof_data.output_coins_root)); // Get the recipient account - // TODO: Return an error instead let mut account = self .accounts .remove(&coin_proof.coin.recipient) @@ -477,14 +476,6 @@ impl Server { account.balance = balance - invoiced_amount; account.proof = Some(proof.clone()); - //// TODO: Move this to the client - //let private_key = self.private_key(&account); - //let commitment = Commitment::new( - // &private_key, - // hash_concat(&account.state.hash(), &out_coins_tree.root()).to_vec(), - //) - //.expect("Unable to create commitment"); - // Insert account back into database. self.accounts.insert(account_address, account); assert_eq!( From 2bc05e36234ef9c05c812a5d894a7e312e2a09ca Mon Sep 17 00:00:00 2001 From: Lukas Date: Fri, 2 May 2025 12:43:10 +0200 Subject: [PATCH 11/21] Rename Wallet to AccountServer --- server/src/{wallet.rs => account_server.rs} | 12 ++++++------ server/src/main.rs | 2 +- server/src/server.rs | 16 ++++++++-------- 3 files changed, 15 insertions(+), 15 deletions(-) rename server/src/{wallet.rs => account_server.rs} (99%) diff --git a/server/src/wallet.rs b/server/src/account_server.rs similarity index 99% rename from server/src/wallet.rs rename to server/src/account_server.rs index 9a83396..31c61fd 100644 --- a/server/src/wallet.rs +++ b/server/src/account_server.rs @@ -102,7 +102,7 @@ impl ClientAccount { // send it back to server. pub fn send_coins( &mut self, - server: &mut Server, + server: &mut AccountServer, invoices: Vec, ) -> Result, String> { let result = server.send_coins( @@ -214,13 +214,13 @@ impl Account { } } -pub struct Server { +pub struct AccountServer { accounts: HashMap, prover: Prover, state: Arc>, } -impl Server { +impl AccountServer { // TODO: Move to client. /// Get the keypair to the pubkey this account commited to (which is derived key num_pubkeys - /// 1) @@ -229,7 +229,7 @@ impl Server { let accounts = HashMap::new(); let prover = Prover::new(); - Server { + AccountServer { accounts, prover, state, @@ -543,7 +543,7 @@ mod tests { #[test] fn test_wallet_operations() { let state = Arc::new(Mutex::new(State::new())); - let mut server = Server::new(state); + let mut server = AccountServer::new(state); let mut minting_account = create_minting_account(); println!("minting account address: {:?}", minting_account.address); server.import_account( @@ -636,7 +636,7 @@ mod tests { #[test] fn test_create_minting_account() { let state = Arc::new(Mutex::new(State::new())); - let mut server = Server::new(state); + let mut server = AccountServer::new(state); let minting_account = create_minting_account(); println!("minting account address: {:?}", minting_account.address); server.import_account( diff --git a/server/src/main.rs b/server/src/main.rs index f367130..fc04612 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -73,7 +73,7 @@ async fn main() -> Result<(), Box> { )); // Create a new wallet instance with a reference to the state - let mut wallet = wallet::Server::new(Arc::clone(&state)); + let mut wallet = wallet::AccountServer::new(Arc::clone(&state)); // TODO: Create minting account //wallet.create_account(); diff --git a/server/src/server.rs b/server/src/server.rs index 360dd8c..abb0885 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -16,7 +16,7 @@ use zkcoins_prover::Proof; use crate::commitment::Commitment; use crate::publisher::create_and_broadcast_inscription; -use crate::wallet::{CoinProof, Invoice, Server}; +use crate::wallet::{CoinProof, Invoice, AccountServer}; use crate::NETWORK_CONFIG; // Response types for our API @@ -83,7 +83,7 @@ pub struct SendCoinResponse { // Handler functions for our REST API async fn get_balance_handler( - State((wallet, _)): State<(Arc>, Arc)>, + State((wallet, _)): State<(Arc>, Arc)>, axum::extract::Query(params): axum::extract::Query>, ) -> impl IntoResponse { let wallet = wallet.lock().unwrap(); @@ -124,7 +124,7 @@ async fn get_balance_handler( } async fn get_address_handler( - State((wallet, _)): State<(Arc>, Arc)>, + State((wallet, _)): State<(Arc>, Arc)>, ) -> impl IntoResponse { let wallet = wallet.lock().unwrap(); @@ -141,7 +141,7 @@ async fn get_address_handler( } async fn receive_coin_handler( - State((wallet, _)): State<(Arc>, Arc)>, + State((wallet, _)): State<(Arc>, Arc)>, body: Bytes, // Accept raw binary data instead of multipart ) -> impl IntoResponse { // Try to deserialize the binary data as a CoinProof @@ -170,7 +170,7 @@ async fn receive_coin_handler( } async fn send_coin_handler( - State((wallet, proof_store)): State<(Arc>, Arc)>, + State((wallet, proof_store)): State<(Arc>, Arc)>, Json(request): Json, ) -> impl IntoResponse { // Create converted addresses (from_address and to_address) @@ -268,7 +268,7 @@ async fn send_coin_handler( // TODO: This has to be eventually replaced by a faucet (or some way for wallets to share the // minting account) async fn mint_handler( - State((wallet, proof_store)): State<(Arc>, Arc)>, + State((wallet, proof_store)): State<(Arc>, Arc)>, Json(request): Json, ) -> impl IntoResponse { let account_address_vec = match hex::decode(request.account_address.trim_start_matches("0x")) { @@ -349,7 +349,7 @@ async fn mint_handler( // New handler to get a binary proof by ID async fn get_proof_handler( - State((_, proof_store)): State<(Arc>, Arc)>, + State((_, proof_store)): State<(Arc>, Arc)>, Path(id): Path, ) -> impl IntoResponse { match proof_store.get_proof(id) { @@ -380,7 +380,7 @@ async fn get_proof_handler( // TODO: Create a minting account that is stored in Server // Function to start the REST API server -pub async fn start_rest_server(wallet: Server, addr: &str) -> anyhow::Result<()> { +pub async fn start_rest_server(wallet: AccountServer, addr: &str) -> anyhow::Result<()> { // Parse the address string into a SocketAddr let socket_addr = addr .parse::() From 7a492713a037b1330bb22ddb1e5d3fde2d7e0a71 Mon Sep 17 00:00:00 2001 From: Lukas Date: Fri, 2 May 2025 12:55:01 +0200 Subject: [PATCH 12/21] Rename all wallet instances to account server --- server/src/account_server.rs | 3 +- server/src/commitment.rs | 4 +-- server/src/main.rs | 14 +++++----- server/src/server.rs | 53 ++++++++++++++++++------------------ 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/server/src/account_server.rs b/server/src/account_server.rs index 31c61fd..904e3e5 100644 --- a/server/src/account_server.rs +++ b/server/src/account_server.rs @@ -519,6 +519,7 @@ impl AccountServer { #[cfg(test)] mod tests { + use zkcoins_program::hash; use std::time::Instant; use super::*; @@ -558,7 +559,7 @@ mod tests { assert_eq!( MINTING_ADDRESS, server.get_minting_account_address().unwrap(), - "Minting address in wallet and program are different" + "Minting address in server and program are different" ); let mut account_1 = ClientAccount::new(Xpriv::new_master(Network::Signet, &[1u8; 32]).unwrap()); diff --git a/server/src/commitment.rs b/server/src/commitment.rs index 30dfe6a..cad3653 100644 --- a/server/src/commitment.rs +++ b/server/src/commitment.rs @@ -1,6 +1,6 @@ -use crate::wallet::SECP256K1; +use crate::account_server::SECP256K1; use bitcoin::secp256k1::{ - self, schnorr::Signature, Keypair, Message, PublicKey, Secp256k1, SecretKey, + self, schnorr::Signature, Keypair, Message, PublicKey, Secp256k1, SecretKey }; use lib::HashDigest; use serde::{Deserialize, Serialize}; diff --git a/server/src/main.rs b/server/src/main.rs index fc04612..70133db 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -2,7 +2,7 @@ mod commitment; mod publisher; mod scanner; mod server; -mod wallet; +mod account_server; mod state; use crate::commitment::Commitment; @@ -21,7 +21,7 @@ use std::io::{Read, Write}; const SMT_PATH: &str = "smt.bin"; const MMR_PATH: &str = "mmr.bin"; const LATEST_BLOCK_PATH: &str = "latest_block.bin"; -const WALLET_SERVER_ADDR: &str = "127.0.0.1:4242"; +const ACCOUNT_SERVER_ADDR: &str = "127.0.0.1:4242"; //const START_BLOCK_HASH: &str = "0000028de422efcd9539a657fa2b39acecb524e3f6ccb553c536ee7955934ab6"; const START_BLOCK_HASH: &str = "000000e1f8bafb4d8932dd0ae215945b725fa631e5243e9aba5bb291c1b9da4a"; @@ -72,16 +72,16 @@ async fn main() -> Result<(), Box> { } )); - // Create a new wallet instance with a reference to the state - let mut wallet = wallet::AccountServer::new(Arc::clone(&state)); + // Create a new AccountServer instance with a reference to the state + let mut account_server = account_server::AccountServer::new(Arc::clone(&state)); // TODO: Create minting account //wallet.create_account(); - // Spawn the wallet server as a separate task + // Spawn the account_server as a separate task tokio::spawn(async move { - if let Err(e) = start_rest_server(wallet, WALLET_SERVER_ADDR).await { - eprintln!("Wallet server error: {}", e); + if let Err(e) = start_rest_server(account_server, ACCOUNT_SERVER_ADDR).await { + eprintln!("Account server error: {}", e); } }); diff --git a/server/src/server.rs b/server/src/server.rs index abb0885..dfc98e5 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -14,9 +14,9 @@ use std::sync::{Arc, Mutex}; use tokio::net::TcpListener; use zkcoins_prover::Proof; +use crate::account_server::{AccountServer, CoinProof, Invoice}; use crate::commitment::Commitment; use crate::publisher::create_and_broadcast_inscription; -use crate::wallet::{CoinProof, Invoice, AccountServer}; use crate::NETWORK_CONFIG; // Response types for our API @@ -83,10 +83,10 @@ pub struct SendCoinResponse { // Handler functions for our REST API async fn get_balance_handler( - State((wallet, _)): State<(Arc>, Arc)>, + State((account_server, _)): State<(Arc>, Arc)>, axum::extract::Query(params): axum::extract::Query>, ) -> impl IntoResponse { - let wallet = wallet.lock().unwrap(); + let account_server = account_server.lock().unwrap(); // Check if an address parameter was provided if let Some(address_hex) = params.get("address") { @@ -113,7 +113,7 @@ async fn get_balance_handler( } // Get balance for the specific account - match wallet.get_account_balance(&address) { + match account_server.get_account_balance(&address) { Ok(balance) => (StatusCode::OK, Json(BalanceResponse { balance })), Err(_) => (StatusCode::NOT_FOUND, Json(BalanceResponse { balance: 0 })), } @@ -124,12 +124,12 @@ async fn get_balance_handler( } async fn get_address_handler( - State((wallet, _)): State<(Arc>, Arc)>, + State((account_server, _)): State<(Arc>, Arc)>, ) -> impl IntoResponse { - let wallet = wallet.lock().unwrap(); + let account_server = account_server.lock().unwrap(); // Convert addresses to hex strings - let hex_addresses: Vec = wallet + let hex_addresses: Vec = account_server .get_addresses() .iter() .map(|addr| format!("0x{}", hex::encode(addr))) @@ -141,14 +141,14 @@ async fn get_address_handler( } async fn receive_coin_handler( - State((wallet, _)): State<(Arc>, Arc)>, + State((account_server, _)): State<(Arc>, Arc)>, body: Bytes, // Accept raw binary data instead of multipart ) -> impl IntoResponse { // Try to deserialize the binary data as a CoinProof match bincode::deserialize::(&body) { Ok(coin_proof) => { - let mut wallet = wallet.lock().unwrap(); - match wallet.receive_coin(coin_proof) { + let mut account_server = account_server.lock().unwrap(); + match account_server.receive_coin(coin_proof) { Ok(_) => Json(SendCoinResponse { success: true, proof_id: None, @@ -170,7 +170,7 @@ async fn receive_coin_handler( } async fn send_coin_handler( - State((wallet, proof_store)): State<(Arc>, Arc)>, + State((account_server, proof_store)): State<(Arc>, Arc)>, Json(request): Json, ) -> impl IntoResponse { // Create converted addresses (from_address and to_address) @@ -216,10 +216,10 @@ async fn send_coin_handler( } // TODO: Provide the correct public keys from the client - // Acquire the wallet lock only for the duration of sending coins. + // Acquire the account_server lock only for the duration of sending coins. let send_result = { - let mut wallet_guard = wallet.lock().unwrap(); - wallet_guard.send_coins( + let mut account_server_lock = account_server.lock().unwrap(); + account_server_lock.send_coins( vec![Invoice::new(request.amount, to_address)], from_address, todo!(), @@ -227,7 +227,7 @@ async fn send_coin_handler( ) }; - // Now that the wallet lock is dropped, we can await safely. + // Now that the account_server lock is dropped, we can await safely. match send_result { Ok(mut coin_proofs) => { let commitment_data = bincode::serialize(&coin_proofs[0].commitment) @@ -265,10 +265,9 @@ async fn send_coin_handler( } } -// TODO: This has to be eventually replaced by a faucet (or some way for wallets to share the -// minting account) +// TODO: This has to be eventually replaced by a faucet (or some way for account servers to share the minting account) async fn mint_handler( - State((wallet, proof_store)): State<(Arc>, Arc)>, + State((account_server, proof_store)): State<(Arc>, Arc)>, Json(request): Json, ) -> impl IntoResponse { let account_address_vec = match hex::decode(request.account_address.trim_start_matches("0x")) { @@ -298,11 +297,11 @@ async fn mint_handler( } // TODO: Fill in the correct public keys for the minting account (create it at server start) - // Acquire the wallet lock only for the duration of Minting coins. + // Acquire the account_server lock only for the duration of Minting coins. let send_result = { - let mut wallet_guard = wallet.lock().unwrap(); - let minting_address = wallet_guard.get_minting_account_address().unwrap(); - wallet_guard.send_coins( + let mut account_server_guard = account_server.lock().unwrap(); + let minting_address = account_server_guard.get_minting_account_address().unwrap(); + account_server_guard.send_coins( vec![Invoice::new(request.amount, account_address)], minting_address, todo!(), @@ -310,7 +309,7 @@ async fn mint_handler( ) }; - // Now that the wallet lock is dropped, we can await safely. + // Now that the account_server lock is dropped, we can await safely. match send_result { Ok(mut coin_proofs) => { let commitment_data = bincode::serialize(&coin_proofs[0].commitment) @@ -380,20 +379,20 @@ async fn get_proof_handler( // TODO: Create a minting account that is stored in Server // Function to start the REST API server -pub async fn start_rest_server(wallet: AccountServer, addr: &str) -> anyhow::Result<()> { +pub async fn start_rest_server(account_server: AccountServer, addr: &str) -> anyhow::Result<()> { // Parse the address string into a SocketAddr let socket_addr = addr .parse::() .map_err(|e| anyhow::anyhow!("Failed to parse address: {}", e))?; - // Wrap the wallet in an Arc for thread-safe sharing - let shared_wallet = Arc::new(Mutex::new(wallet)); + // Wrap the account_server in an Arc for thread-safe sharing + let shared_account_server = Arc::new(Mutex::new(account_server)); // Create a proof store let proof_store = Arc::new(ProofStore::new()); // Create the combined state - let state = (shared_wallet, proof_store); + let state = (shared_account_server, proof_store); // Create a router for API endpoints let api_routes = Router::new() From 9f3766d3a169797b1f9a2ed56b947be195f2c0d0 Mon Sep 17 00:00:00 2001 From: Lukas Date: Fri, 2 May 2025 14:12:31 +0200 Subject: [PATCH 13/21] update handlers to use client account pubkeys --- server/src/account_server.rs | 40 +++++++++------------- server/src/server.rs | 66 +++++++++++++++++++++++------------- 2 files changed, 58 insertions(+), 48 deletions(-) diff --git a/server/src/account_server.rs b/server/src/account_server.rs index 904e3e5..a0e5d63 100644 --- a/server/src/account_server.rs +++ b/server/src/account_server.rs @@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize}; use zkcoins_program::merkle::CommitmentMerkleProofs; use zkcoins_program::{ calculate_coin_identifier, AccountState, Coin, CoinTemplate, ProgramInputsBuilder, - ProofData, ProofType, + ProofData, ProofType, hash }; use zkcoins_prover::{Proof, Prover}; @@ -26,10 +26,11 @@ use crate::commitment::Commitment; use crate::state::State; use lib::sparse_merkle_tree::{InclusionProof, SparseMerkleTree, DEFAULT_HASHES}; use lib::{hash_concat, Amount, HashDigest}; + lazy_static! { pub static ref SECP256K1: Secp256k1 = Secp256k1::new(); -} +} #[derive(Serialize, Deserialize, Debug, Clone)] pub struct CoinProof { pub proof: Proof, @@ -53,7 +54,7 @@ impl Invoice { pub struct ClientAccount { pub address: HashDigest, pub num_pubkeys: u32, - private_key: PrivateKey, + pub private_key: PrivateKey, } // TODO Move to tests or client @@ -108,8 +109,8 @@ impl ClientAccount { let result = server.send_coins( invoices, self.address, - Self::generate_public_key(&self.private_key, self.num_pubkeys), - Self::generate_public_key(&self.private_key, self.num_pubkeys + 1), + self.generate_public_key(self.num_pubkeys), + self.generate_public_key(self.num_pubkeys + 1), ); self.num_pubkeys += 1; match result { @@ -123,36 +124,27 @@ impl ClientAccount { } } - pub fn generate_public_key(private_key: &Xpriv, index: u32) -> PublicKey { + pub fn generate_public_key(&self, index: u32) -> PublicKey { // WARNING: LEAKING THE MASTER PUBLIC KEY IS EQUIVALENT TO LEAKING THE PRIVATE KEY! - Xpub::from_priv(&SECP256K1, private_key) + Xpub::from_priv(&SECP256K1, &self.private_key) .derive_pub(&SECP256K1, &[ChildNumber::Normal { index }]) .expect("Failed to derive first pubkey") .public_key } pub fn new(private_key: Xpriv) -> Self { - //let private_key = master_private_key - // .derive_priv( - // &SECP256K1, - // &[ChildNumber::Hardened { - // index: self.accounts.len() as u32, - // }], - // ) - // .expect("Unable to derive a private key for the account"); - + let mut client_account = ClientAccount { + address: [0u8; 32], + num_pubkeys: 0, + private_key, + }; let account = AccountState::new( - Self::generate_public_key(&private_key, 0) + client_account.generate_public_key(0) .serialize() .to_vec(), ); - - let address = account.owner; - ClientAccount { - address, - num_pubkeys: 0, - private_key, - } + client_account.address = account.owner; + client_account } } diff --git a/server/src/server.rs b/server/src/server.rs index dfc98e5..a61eaa6 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -1,11 +1,7 @@ -use axum::body::Bytes; use axum::{ - extract::{Json, Path, State}, - http::{header, StatusCode}, - response::IntoResponse, - routing::{get, post}, - Router, + body::Bytes, extract::{Json, Path, State}, http::{header, StatusCode}, response::IntoResponse, routing::{get, post}, Router }; +use bitcoin::Network; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::net::SocketAddr; @@ -13,9 +9,11 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use tokio::net::TcpListener; use zkcoins_prover::Proof; +use zkcoins_program::hash; -use crate::account_server::{AccountServer, CoinProof, Invoice}; -use crate::commitment::Commitment; +use crate::account_server::{ + AccountServer, ClientAccount, CoinProof, Invoice, PrivateKey, +}; use crate::publisher::create_and_broadcast_inscription; use crate::NETWORK_CONFIG; @@ -30,11 +28,14 @@ pub struct AddressesResponse { addresses: Vec, } +// TODO: Send multiple coins at once. #[derive(Deserialize)] pub struct SendCoinRequest { account_address: String, recipient: String, amount: u64, + public_key: bitcoin::secp256k1::PublicKey, + next_public_key: bitcoin::secp256k1::PublicKey, } #[derive(Deserialize)] @@ -83,7 +84,7 @@ pub struct SendCoinResponse { // Handler functions for our REST API async fn get_balance_handler( - State((account_server, _)): State<(Arc>, Arc)>, + State((account_server, _, _)): State<(Arc>, Arc, Arc>)>, axum::extract::Query(params): axum::extract::Query>, ) -> impl IntoResponse { let account_server = account_server.lock().unwrap(); @@ -118,8 +119,7 @@ async fn get_balance_handler( Err(_) => (StatusCode::NOT_FOUND, Json(BalanceResponse { balance: 0 })), } } else { - // TODO: Return an error - todo!() + (StatusCode::NOT_FOUND, Json(BalanceResponse { balance: 0 })) } } @@ -170,7 +170,7 @@ async fn receive_coin_handler( } async fn send_coin_handler( - State((account_server, proof_store)): State<(Arc>, Arc)>, + State((account_server, proof_store, _)): State<(Arc>, Arc, Arc>)>, Json(request): Json, ) -> impl IntoResponse { // Create converted addresses (from_address and to_address) @@ -222,8 +222,8 @@ async fn send_coin_handler( account_server_lock.send_coins( vec![Invoice::new(request.amount, to_address)], from_address, - todo!(), - todo!(), + request.public_key, + request.next_public_key, ) }; @@ -267,7 +267,7 @@ async fn send_coin_handler( // TODO: This has to be eventually replaced by a faucet (or some way for account servers to share the minting account) async fn mint_handler( - State((account_server, proof_store)): State<(Arc>, Arc)>, + State((account_server, proof_store, minting_account)): State<(Arc>, Arc, Arc>)>, Json(request): Json, ) -> impl IntoResponse { let account_address_vec = match hex::decode(request.account_address.trim_start_matches("0x")) { @@ -296,22 +296,26 @@ async fn mint_handler( ); } - // TODO: Fill in the correct public keys for the minting account (create it at server start) - // Acquire the account_server lock only for the duration of Minting coins. + // TODO: Why does this mess up the handler for axum? Better to just use a struct and claim a + // mutex of the entire thing. + let mut minting_account = minting_account.lock().unwrap(); + + // Acquire the wallet lock only for the duration of Minting coins. let send_result = { let mut account_server_guard = account_server.lock().unwrap(); let minting_address = account_server_guard.get_minting_account_address().unwrap(); account_server_guard.send_coins( vec![Invoice::new(request.amount, account_address)], minting_address, - todo!(), - todo!(), + minting_account.generate_public_key(minting_account.num_pubkeys), + minting_account.generate_public_key(minting_account.num_pubkeys + 1), ) }; - // Now that the account_server lock is dropped, we can await safely. + // Now that the wallet lock is dropped, we can await safely. match send_result { Ok(mut coin_proofs) => { + minting_account.num_pubkeys += 1; let commitment_data = bincode::serialize(&coin_proofs[0].commitment) .expect("Failed to serialize commitment"); @@ -377,7 +381,6 @@ async fn get_proof_handler( } } -// TODO: Create a minting account that is stored in Server // Function to start the REST API server pub async fn start_rest_server(account_server: AccountServer, addr: &str) -> anyhow::Result<()> { // Parse the address string into a SocketAddr @@ -391,8 +394,23 @@ pub async fn start_rest_server(account_server: AccountServer, addr: &str) -> any // Create a proof store let proof_store = Arc::new(ProofStore::new()); - // Create the combined state - let state = (shared_account_server, proof_store); + let minting_account = { + let secret = include_bytes!("../minting_secret.bin"); + let private_key = PrivateKey::new_master(Network::Bitcoin, secret) + .expect("Failed to create private key."); + println!( + "Set MINTING_ADDRESS to {:?}", + &zkcoins_program::MINTING_ADDRESS + ); + Arc::new(Mutex::new(ClientAccount { + address: hash(private_key.to_string().as_bytes()), + num_pubkeys: 0, + private_key, + })) + }; + + // Create the combined state. TODO: Make this a struct + let state = (shared_account_server, proof_store, minting_account); // Create a router for API endpoints let api_routes = Router::new() @@ -442,4 +460,4 @@ async fn serve_index() -> impl IntoResponse { // http://myserver.com//balance // http://myserver.com//send -// http://myserver.com//sign +// http://myserver.com//sign) From e627da54735d14e1ed8054ad65abb9af36363509 Mon Sep 17 00:00:00 2001 From: robinlinus Date: Sun, 4 May 2025 17:52:45 -0700 Subject: [PATCH 14/21] Fix mint_handler requirements for an Axum handler --- server/src/server.rs | 71 +++++++++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/server/src/server.rs b/server/src/server.rs index a61eaa6..d1a97d5 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -1,5 +1,10 @@ use axum::{ - body::Bytes, extract::{Json, Path, State}, http::{header, StatusCode}, response::IntoResponse, routing::{get, post}, Router + body::Bytes, + extract::{Json, Path, State}, + http::{header, StatusCode}, + response::IntoResponse, + routing::{get, post}, + Router, }; use bitcoin::Network; use serde::{Deserialize, Serialize}; @@ -8,12 +13,10 @@ use std::net::SocketAddr; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use tokio::net::TcpListener; -use zkcoins_prover::Proof; use zkcoins_program::hash; +use zkcoins_prover::Proof; -use crate::account_server::{ - AccountServer, ClientAccount, CoinProof, Invoice, PrivateKey, -}; +use crate::account_server::{AccountServer, ClientAccount, CoinProof, Invoice, PrivateKey}; use crate::publisher::create_and_broadcast_inscription; use crate::NETWORK_CONFIG; @@ -84,7 +87,11 @@ pub struct SendCoinResponse { // Handler functions for our REST API async fn get_balance_handler( - State((account_server, _, _)): State<(Arc>, Arc, Arc>)>, + State((account_server, _, _)): State<( + Arc>, + Arc, + Arc>, + )>, axum::extract::Query(params): axum::extract::Query>, ) -> impl IntoResponse { let account_server = account_server.lock().unwrap(); @@ -170,7 +177,11 @@ async fn receive_coin_handler( } async fn send_coin_handler( - State((account_server, proof_store, _)): State<(Arc>, Arc, Arc>)>, + State((account_server, proof_store, _)): State<( + Arc>, + Arc, + Arc>, + )>, Json(request): Json, ) -> impl IntoResponse { // Create converted addresses (from_address and to_address) @@ -265,9 +276,12 @@ async fn send_coin_handler( } } -// TODO: This has to be eventually replaced by a faucet (or some way for account servers to share the minting account) async fn mint_handler( - State((account_server, proof_store, minting_account)): State<(Arc>, Arc, Arc>)>, + State((account_server, proof_store, minting_account)): State<( + Arc>, + Arc, + Arc>, + )>, Json(request): Json, ) -> impl IntoResponse { let account_address_vec = match hex::decode(request.account_address.trim_start_matches("0x")) { @@ -296,26 +310,48 @@ async fn mint_handler( ); } - // TODO: Why does this mess up the handler for axum? Better to just use a struct and claim a - // mutex of the entire thing. - let mut minting_account = minting_account.lock().unwrap(); + // Generate keys and get necessary info while holding the minting_account lock briefly + let (minting_pubkey, next_minting_pubkey, num_pubkeys_before_mint) = { + let minting_account_guard = minting_account.lock().unwrap(); + let current_num_pubkeys = minting_account_guard.num_pubkeys; + ( + minting_account_guard.generate_public_key(current_num_pubkeys), + minting_account_guard.generate_public_key(current_num_pubkeys + 1), + current_num_pubkeys, + ) + // minting_account_guard is dropped here, releasing the lock before any .await + }; - // Acquire the wallet lock only for the duration of Minting coins. + // Acquire the account_server lock only for the duration of sending coins. let send_result = { let mut account_server_guard = account_server.lock().unwrap(); let minting_address = account_server_guard.get_minting_account_address().unwrap(); account_server_guard.send_coins( vec![Invoice::new(request.amount, account_address)], minting_address, - minting_account.generate_public_key(minting_account.num_pubkeys), - minting_account.generate_public_key(minting_account.num_pubkeys + 1), + minting_pubkey, // Use the generated keys + next_minting_pubkey, // Use the generated keys ) + // account_server_guard is dropped here }; - // Now that the wallet lock is dropped, we can await safely. + // Now that the locks are dropped, we can await safely. match send_result { Ok(mut coin_proofs) => { - minting_account.num_pubkeys += 1; + // Increment num_pubkeys *after* successful send and before await + { + let mut minting_account_guard = minting_account.lock().unwrap(); + // Ensure we only increment if the send was successful and based on the state *before* the send + if minting_account_guard.num_pubkeys == num_pubkeys_before_mint { + minting_account_guard.num_pubkeys += 1; + } else { + // This case might indicate a race condition or unexpected state change. + // Handle appropriately, maybe log an error or return a specific response. + eprintln!("Warning: num_pubkeys changed unexpectedly during mint operation."); + } + // minting_account_guard is dropped here + } + let commitment_data = bincode::serialize(&coin_proofs[0].commitment) .expect("Failed to serialize commitment"); @@ -325,6 +361,7 @@ async fn mint_handler( ); println!("Commitment data hex: {}", hex::encode(&commitment_data)); + // This await is now safe because no locks are held across it if let Err(err) = create_and_broadcast_inscription(&commitment_data, &NETWORK_CONFIG).await { From 480c44232d92558d679556625c7ddea725507c19 Mon Sep 17 00:00:00 2001 From: robinlinus Date: Mon, 5 May 2025 09:13:09 -0700 Subject: [PATCH 15/21] Refactor state into a struct to simplify handlers --- server/src/server.rs | 62 +++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/server/src/server.rs b/server/src/server.rs index d1a97d5..9bdc581 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -20,6 +20,14 @@ use crate::account_server::{AccountServer, ClientAccount, CoinProof, Invoice, Pr use crate::publisher::create_and_broadcast_inscription; use crate::NETWORK_CONFIG; +// Define a struct for our application state +#[derive(Clone)] +struct AppState { + account_server: Arc>, + proof_store: Arc, + minting_account: Arc>, +} + // Response types for our API #[derive(Serialize)] pub struct BalanceResponse { @@ -87,14 +95,10 @@ pub struct SendCoinResponse { // Handler functions for our REST API async fn get_balance_handler( - State((account_server, _, _)): State<( - Arc>, - Arc, - Arc>, - )>, + State(state): State, axum::extract::Query(params): axum::extract::Query>, ) -> impl IntoResponse { - let account_server = account_server.lock().unwrap(); + let account_server = state.account_server.lock().unwrap(); // Check if an address parameter was provided if let Some(address_hex) = params.get("address") { @@ -130,10 +134,8 @@ async fn get_balance_handler( } } -async fn get_address_handler( - State((account_server, _)): State<(Arc>, Arc)>, -) -> impl IntoResponse { - let account_server = account_server.lock().unwrap(); +async fn get_address_handler(State(state): State) -> impl IntoResponse { + let account_server = state.account_server.lock().unwrap(); // Convert addresses to hex strings let hex_addresses: Vec = account_server @@ -148,13 +150,13 @@ async fn get_address_handler( } async fn receive_coin_handler( - State((account_server, _)): State<(Arc>, Arc)>, + State(state): State, body: Bytes, // Accept raw binary data instead of multipart ) -> impl IntoResponse { // Try to deserialize the binary data as a CoinProof match bincode::deserialize::(&body) { Ok(coin_proof) => { - let mut account_server = account_server.lock().unwrap(); + let mut account_server = state.account_server.lock().unwrap(); match account_server.receive_coin(coin_proof) { Ok(_) => Json(SendCoinResponse { success: true, @@ -177,11 +179,7 @@ async fn receive_coin_handler( } async fn send_coin_handler( - State((account_server, proof_store, _)): State<( - Arc>, - Arc, - Arc>, - )>, + State(state): State, Json(request): Json, ) -> impl IntoResponse { // Create converted addresses (from_address and to_address) @@ -229,7 +227,7 @@ async fn send_coin_handler( // TODO: Provide the correct public keys from the client // Acquire the account_server lock only for the duration of sending coins. let send_result = { - let mut account_server_lock = account_server.lock().unwrap(); + let mut account_server_lock = state.account_server.lock().unwrap(); account_server_lock.send_coins( vec![Invoice::new(request.amount, to_address)], from_address, @@ -257,7 +255,7 @@ async fn send_coin_handler( } // TODO: Handle all the coins_proofs - let proof_id = proof_store.add_proof(coin_proofs.pop().unwrap()); + let proof_id = state.proof_store.add_proof(coin_proofs.pop().unwrap()); ( StatusCode::OK, Json(SendCoinResponse { @@ -277,11 +275,7 @@ async fn send_coin_handler( } async fn mint_handler( - State((account_server, proof_store, minting_account)): State<( - Arc>, - Arc, - Arc>, - )>, + State(state): State, Json(request): Json, ) -> impl IntoResponse { let account_address_vec = match hex::decode(request.account_address.trim_start_matches("0x")) { @@ -312,7 +306,7 @@ async fn mint_handler( // Generate keys and get necessary info while holding the minting_account lock briefly let (minting_pubkey, next_minting_pubkey, num_pubkeys_before_mint) = { - let minting_account_guard = minting_account.lock().unwrap(); + let minting_account_guard = state.minting_account.lock().unwrap(); let current_num_pubkeys = minting_account_guard.num_pubkeys; ( minting_account_guard.generate_public_key(current_num_pubkeys), @@ -324,7 +318,7 @@ async fn mint_handler( // Acquire the account_server lock only for the duration of sending coins. let send_result = { - let mut account_server_guard = account_server.lock().unwrap(); + let mut account_server_guard = state.account_server.lock().unwrap(); let minting_address = account_server_guard.get_minting_account_address().unwrap(); account_server_guard.send_coins( vec![Invoice::new(request.amount, account_address)], @@ -340,7 +334,7 @@ async fn mint_handler( Ok(mut coin_proofs) => { // Increment num_pubkeys *after* successful send and before await { - let mut minting_account_guard = minting_account.lock().unwrap(); + let mut minting_account_guard = state.minting_account.lock().unwrap(); // Ensure we only increment if the send was successful and based on the state *before* the send if minting_account_guard.num_pubkeys == num_pubkeys_before_mint { minting_account_guard.num_pubkeys += 1; @@ -368,7 +362,7 @@ async fn mint_handler( eprintln!("Error broadcasting inscription: {}", err); } - let proof_id = proof_store.add_proof(coin_proofs.pop().unwrap()); + let proof_id = state.proof_store.add_proof(coin_proofs.pop().unwrap()); ( StatusCode::OK, Json(SendCoinResponse { @@ -389,10 +383,10 @@ async fn mint_handler( // New handler to get a binary proof by ID async fn get_proof_handler( - State((_, proof_store)): State<(Arc>, Arc)>, + State(state): State, Path(id): Path, ) -> impl IntoResponse { - match proof_store.get_proof(id) { + match state.proof_store.get_proof(id) { Some(proof_with_commitment) => { // Serialize the proof and commitment together to binary let binary_data = bincode::serialize(&proof_with_commitment).unwrap_or_default(); @@ -446,8 +440,12 @@ pub async fn start_rest_server(account_server: AccountServer, addr: &str) -> any })) }; - // Create the combined state. TODO: Make this a struct - let state = (shared_account_server, proof_store, minting_account); + // Create the combined state using the AppState struct + let state = AppState { + account_server: shared_account_server, + proof_store, + minting_account, + }; // Create a router for API endpoints let api_routes = Router::new() From 052d43aa1b17eba0baf5cff8790b8e5ff59e5066 Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 6 May 2025 14:26:09 +0200 Subject: [PATCH 16/21] Refactor: Move lib module into zkcoins_program --- Cargo.lock | 14 +---- Cargo.toml | 1 - lib/Cargo.toml | 11 ---- program/Cargo.toml | 2 +- program/src/lib.rs | 60 ++++++++++++++++++- program/src/main.rs | 7 +-- program/src/merkle.rs | 59 ------------------ .../src/merkle}/merkle_mountain_range.rs | 6 +- lib/src/lib.rs => program/src/merkle/mod.rs | 4 ++ .../src/merkle}/sparse_merkle_tree.rs | 5 +- script/Cargo.toml | 1 - server/Cargo.toml | 1 - server/src/account_server.rs | 10 ++-- server/src/commitment.rs | 2 +- server/src/state.rs | 15 ++--- 15 files changed, 84 insertions(+), 114 deletions(-) delete mode 100644 lib/Cargo.toml delete mode 100644 program/src/merkle.rs rename {lib/src => program/src/merkle}/merkle_mountain_range.rs (99%) rename lib/src/lib.rs => program/src/merkle/mod.rs (84%) rename {lib/src => program/src/merkle}/sparse_merkle_tree.rs (99%) diff --git a/Cargo.lock b/Cargo.lock index 184a95d..d425727 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2834,16 +2834,6 @@ dependencies = [ "spin", ] -[[package]] -name = "lib" -version = "1.1.0" -dependencies = [ - "bincode", - "lazy_static", - "serde", - "sha2 0.11.0-pre.3", -] - [[package]] name = "libc" version = "0.2.170" @@ -4650,7 +4640,6 @@ dependencies = [ "esplora-client", "hex", "lazy_static", - "lib", "serde", "sha2 0.10.8", "tokio", @@ -6538,7 +6527,7 @@ version = "0.1.0" dependencies = [ "bincode", "derive_builder", - "lib", + "lazy_static", "rand", "serde", "sha2 0.11.0-pre.3", @@ -6550,7 +6539,6 @@ name = "zkcoins-prover" version = "1.1.0" dependencies = [ "bitcoin", - "lib", "sp1-build", "sp1-sdk", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 7720364..bc62932 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ members = [ "program", "script", - "lib", "server", "client" ] diff --git a/lib/Cargo.toml b/lib/Cargo.toml deleted file mode 100644 index 4617d4f..0000000 --- a/lib/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "lib" -version.workspace = true -edition.workspace = true - -[dependencies] -serde = { workspace = true } -lazy_static = { workspace = true } -bincode = { workspace = true } -# Use patched version from sp1: https://docs.succinct.xyz/docs/sp1/writing-programs/patched-crates -sha2 = { git = "https://github.com/sp1-patches/RustCrypto-hashes", package = "sha2" } \ No newline at end of file diff --git a/program/Cargo.toml b/program/Cargo.toml index dc10b4a..ebe2cb6 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -8,7 +8,7 @@ sp1-zkvm = { version="4.0.0", features = ["verify"] } bincode = { workspace = true } serde = { workspace = true } rand = { workspace = true } -lib = { path = "../lib" } +lazy_static = { workspace = true } derive_builder = "0.20.2" # Use patched version from sp1: https://docs.succinct.xyz/docs/sp1/writing-programs/patched-crates sha2 = { git = "https://github.com/sp1-patches/RustCrypto-hashes", package = "sha2" } diff --git a/program/src/lib.rs b/program/src/lib.rs index d44d0fa..cbfff68 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -1,16 +1,70 @@ -use merkle::CommitmentMerkleProofs; +use merkle::{hash_concat, merkle_mountain_range::MMRProof}; use rand::Rng; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use derive_builder::Builder; -use lib::{ +use merkle::{ sparse_merkle_tree::{InclusionProof, NonInclusionProof, DEFAULT_HASHES}, Amount, HashDigest, PublicKey, }; pub mod merkle; +/// All three proofs that have to be checked per coin or previous account state proof +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CommitmentMerkleProofs { + // Root of the commitment tree. + pub commitment_root: HashDigest, + // Proves that commitment is included in commitment tree. + pub commitment_proof: InclusionProof, + // Proves that the commitment root is included in the commitment history tree. + pub commitment_root_history_proof: MMRProof, + pub commitment_root_mmr_sibling: HashDigest, + // Proves that the previous commitment history root is included in the commitment history tree. + // This proof is different from commitment_root_history_proof and commitmentProof because we + // store tuples of (SMTRoot, MMRRoot) in the MMR. + pub previous_root_history_proof: (HashDigest, MMRProof), + // The commitment is hash(hash(account_state) || out_coins_root) + pub commitment_account_state_hash: HashDigest, + pub commitment_out_coins_root: HashDigest, +} + +impl CommitmentMerkleProofs { + fn verify_commitment_root(&self, commitment_history_root: HashDigest) -> bool { + self.commitment_root_history_proof.verify( + hash_concat(&self.commitment_root, &self.commitment_root_mmr_sibling), + commitment_history_root, + ) + } + + fn commitment(&self) -> HashDigest { + hash_concat( + &self.commitment_account_state_hash, + &self.commitment_out_coins_root, + ) + } + + pub fn verify_commitment(&self, commitment_history_root: HashDigest) -> bool { + let valid_smt_in_history = self.verify_commitment_root(commitment_history_root); + let valid_commitment_in_smt = self + .commitment_proof + .verify(self.commitment(), self.commitment_root); + valid_smt_in_history && valid_commitment_in_smt + } + + pub fn verify_previous_root( + &self, + previous_root: HashDigest, + commitment_history_root: HashDigest, + ) -> bool { + self.previous_root_history_proof.1.verify( + hash_concat(&self.previous_root_history_proof.0, &previous_root), + commitment_history_root, + ) + } +} + pub const MINTING_ADDRESS: HashDigest = [44, 153, 26, 227, 141, 88, 195, 127, 88, 144, 228, 143, 121, 49, 51, 158, 111, 205, 183, 53, 133, 35, 183, 240, 183, 165, 104, 116, 66, 228, 94, 242]; pub fn hash(data: &[u8]) -> HashDigest { @@ -82,7 +136,7 @@ pub struct AccountState { } impl AccountState { - pub fn new(initial_public_key: lib::PublicKey) -> Self { + pub fn new(initial_public_key: merkle::PublicKey) -> Self { // TODO: The randomness here is annoying why do we not hash the public key directly and // skip the first one in the commitments? // We add random bytes to the public key as a blinding factor for the address. diff --git a/program/src/main.rs b/program/src/main.rs index 986a4e7..823664d 100644 --- a/program/src/main.rs +++ b/program/src/main.rs @@ -1,11 +1,10 @@ #![no_main] sp1_zkvm::entrypoint!(main); -use lib::sparse_merkle_tree::{InclusionProof, DEFAULT_HASHES}; -use lib::HashDigest; use sha2::{Digest, Sha256}; -use zkcoins_program::merkle::CommitmentMerkleProofs; -use zkcoins_program::{AccountState, Coin, ProofData, ProofType}; +use zkcoins_program::merkle::sparse_merkle_tree::{InclusionProof, DEFAULT_HASHES}; +use zkcoins_program::merkle::HashDigest; +use zkcoins_program::{AccountState, Coin, CommitmentMerkleProofs, ProofData, ProofType}; use zkcoins_program::{ProgramInputs, MINTING_ADDRESS}; fn verify_proof(public_values: Vec, vkey: [u32; 8]) -> ProofData { diff --git a/program/src/merkle.rs b/program/src/merkle.rs deleted file mode 100644 index 48f24af..0000000 --- a/program/src/merkle.rs +++ /dev/null @@ -1,59 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use lib::merkle_mountain_range::MMRProof; -use lib::sparse_merkle_tree::InclusionProof; -use lib::{hash_concat, HashDigest}; - -/// All three proofs that have to be checked per coin or previous account state proof -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct CommitmentMerkleProofs { - // Root of the commitment tree. - pub commitment_root: HashDigest, - // Proves that commitment is included in commitment tree. - pub commitment_proof: InclusionProof, - // Proves that the commitment root is included in the commitment history tree. - pub commitment_root_history_proof: MMRProof, - pub commitment_root_mmr_sibling: HashDigest, - // Proves that the previous commitment history root is included in the commitment history tree. - // This proof is different from commitment_root_history_proof and commitmentProof because we - // store tuples of (SMTRoot, MMRRoot) in the MMR. - pub previous_root_history_proof: (HashDigest, MMRProof), - // The commitment is hash(hash(account_state) || out_coins_root) - pub commitment_account_state_hash: HashDigest, - pub commitment_out_coins_root: HashDigest, -} - -impl CommitmentMerkleProofs { - fn verify_commitment_root(&self, commitment_history_root: HashDigest) -> bool { - self.commitment_root_history_proof.verify( - hash_concat(&self.commitment_root, &self.commitment_root_mmr_sibling), - commitment_history_root, - ) - } - - fn commitment(&self) -> HashDigest { - hash_concat( - &self.commitment_account_state_hash, - &self.commitment_out_coins_root, - ) - } - - pub fn verify_commitment(&self, commitment_history_root: HashDigest) -> bool { - let valid_smt_in_history = self.verify_commitment_root(commitment_history_root); - let valid_commitment_in_smt = self - .commitment_proof - .verify(self.commitment(), self.commitment_root); - valid_smt_in_history && valid_commitment_in_smt - } - - pub fn verify_previous_root( - &self, - previous_root: HashDigest, - commitment_history_root: HashDigest, - ) -> bool { - self.previous_root_history_proof.1.verify( - hash_concat(&self.previous_root_history_proof.0, &previous_root), - commitment_history_root, - ) - } -} diff --git a/lib/src/merkle_mountain_range.rs b/program/src/merkle/merkle_mountain_range.rs similarity index 99% rename from lib/src/merkle_mountain_range.rs rename to program/src/merkle/merkle_mountain_range.rs index 0e92c68..5c0ccfe 100644 --- a/lib/src/merkle_mountain_range.rs +++ b/program/src/merkle/merkle_mountain_range.rs @@ -1,8 +1,8 @@ -use crate::{hash_concat, HashDigest, ZERO_HASH}; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; use std::io; +use super::{hash_concat, HashDigest, ZERO_HASH}; + pub type MerklePath = Vec; #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] @@ -207,6 +207,8 @@ impl MerkleMountainRange { #[cfg(test)] mod tests { + use sha2::{Digest, Sha256}; + use super::*; /// Helper to convert a string into a 32-byte hash using SHA256. diff --git a/lib/src/lib.rs b/program/src/merkle/mod.rs similarity index 84% rename from lib/src/lib.rs rename to program/src/merkle/mod.rs index 82367c0..fb4c31e 100644 --- a/lib/src/lib.rs +++ b/program/src/merkle/mod.rs @@ -1,4 +1,8 @@ +use merkle_mountain_range::MMRProof; use sha2::{Digest, Sha256}; +use serde::{Deserialize, Serialize}; +use sparse_merkle_tree::InclusionProof; + pub mod merkle_mountain_range; pub mod sparse_merkle_tree; diff --git a/lib/src/sparse_merkle_tree.rs b/program/src/merkle/sparse_merkle_tree.rs similarity index 99% rename from lib/src/sparse_merkle_tree.rs rename to program/src/merkle/sparse_merkle_tree.rs index 7da6cef..be354c5 100644 --- a/lib/src/sparse_merkle_tree.rs +++ b/program/src/merkle/sparse_merkle_tree.rs @@ -1,4 +1,3 @@ -use crate::{hash_concat, HashDigest, ZERO_HASH}; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -6,6 +5,8 @@ use std::collections::HashMap; use std::fs::File; use std::io::{self, Read, Write}; +use super::{HashDigest, ZERO_HASH, hash_concat}; + /// The tree depth. For a 256-bit key space, depth is 256. pub const TREE_DEPTH: usize = 256; @@ -428,7 +429,7 @@ pub fn load_merkle_tree(path: &str) -> io::Result { #[cfg(test)] mod tests { - use crate::HASH_SIZE; + use super::super::HASH_SIZE; use super::*; diff --git a/script/Cargo.toml b/script/Cargo.toml index d39664e..8009ced 100644 --- a/script/Cargo.toml +++ b/script/Cargo.toml @@ -9,7 +9,6 @@ bitcoin = { workspace = true } zkcoins-program = { path = "../program" } sp1-sdk = { workspace = true } tracing = "0.1.40" -lib = { path = "../lib" } [build-dependencies] sp1-build = "4.0.0" diff --git a/server/Cargo.toml b/server/Cargo.toml index 0ee1909..2f455ad 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -16,7 +16,6 @@ axum = { version = "0.7.9", features = ["json", "multipart"] } anyhow = "1.0" zkcoins-prover = { path = "../script/" } zkcoins-program = { path = "../program/" } -lib = { path = "../lib" } lazy_static = { workspace = true } diff --git a/server/src/account_server.rs b/server/src/account_server.rs index a0e5d63..8c63544 100644 --- a/server/src/account_server.rs +++ b/server/src/account_server.rs @@ -11,10 +11,10 @@ use bitcoin::{ Network, }; use serde::{Deserialize, Serialize}; -use zkcoins_program::merkle::CommitmentMerkleProofs; +use zkcoins_program::merkle::sparse_merkle_tree::{InclusionProof, SparseMerkleTree, DEFAULT_HASHES}; +use zkcoins_program::merkle::{hash_concat, Amount, HashDigest}; use zkcoins_program::{ - calculate_coin_identifier, AccountState, Coin, CoinTemplate, ProgramInputsBuilder, - ProofData, ProofType, hash + calculate_coin_identifier, AccountState, Coin, CoinTemplate, CommitmentMerkleProofs, ProgramInputsBuilder, ProofData, ProofType }; use zkcoins_prover::{Proof, Prover}; @@ -24,8 +24,6 @@ use lazy_static::lazy_static; use crate::commitment::Commitment; use crate::state::State; -use lib::sparse_merkle_tree::{InclusionProof, SparseMerkleTree, DEFAULT_HASHES}; -use lib::{hash_concat, Amount, HashDigest}; lazy_static! { pub static ref SECP256K1: Secp256k1 = Secp256k1::new(); @@ -171,7 +169,7 @@ impl Account { &self, address: HashDigest, next_public_key: PublicKey, - public_key: lib::PublicKey, + public_key: zkcoins_program::merkle::PublicKey, coin_templates: Vec, ) -> Vec { let mut next_account_state = AccountState { diff --git a/server/src/commitment.rs b/server/src/commitment.rs index cad3653..a5ab4cd 100644 --- a/server/src/commitment.rs +++ b/server/src/commitment.rs @@ -2,9 +2,9 @@ use crate::account_server::SECP256K1; use bitcoin::secp256k1::{ self, schnorr::Signature, Keypair, Message, PublicKey, Secp256k1, SecretKey }; -use lib::HashDigest; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; +use zkcoins_program::merkle::HashDigest; use std::fmt; /// A commitment consisting of a public key, a Schnorr signature, and a message. diff --git a/server/src/state.rs b/server/src/state.rs index f4e2aa2..7d99fee 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -1,13 +1,11 @@ use crate::commitment::Commitment; use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; -use lib::merkle_mountain_range::MMRProof; -use lib::merkle_mountain_range::MerkleMountainRange; -use lib::sparse_merkle_tree::InclusionProof; -use lib::sparse_merkle_tree::SparseMerkleTree; -use lib::{HashDigest, ZERO_HASH}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; +use zkcoins_program::merkle::merkle_mountain_range::{MMRProof, MerkleMountainRange}; +use zkcoins_program::merkle::sparse_merkle_tree::{load_merkle_tree, save_merkle_tree, InclusionProof, SparseMerkleTree}; +use zkcoins_program::merkle::{HashDigest, ZERO_HASH}; use std::collections::HashMap; use std::io; @@ -145,7 +143,7 @@ impl State { /// Saves the state to two files: one for the SMT and one for the MMR. pub fn save_to_files(&self, smt_path: &str, mmr_path: &str) -> io::Result<()> { // Save SMT - lib::sparse_merkle_tree::save_merkle_tree(&self.smt, smt_path)?; + save_merkle_tree(&self.smt, smt_path)?; // Save MMR self.mmr.save_to_file(mmr_path)?; @@ -160,7 +158,7 @@ impl State { /// Loads the state from two files: one for the SMT and one for the MMR. pub fn load_from_files(smt_path: &str, mmr_path: &str) -> io::Result { // Load SMT - let smt = lib::sparse_merkle_tree::load_merkle_tree(smt_path)?; + let smt = load_merkle_tree(smt_path)?; // Load MMR let mmr = MerkleMountainRange::load_from_file(mmr_path)?; @@ -194,8 +192,7 @@ mod tests { use super::*; use bitcoin::hashes::Hash; use bitcoin::secp256k1::{Secp256k1, SecretKey}; - use lib::hash_concat; - use lib::HASH_SIZE; + use zkcoins_program::merkle::{hash_concat, HASH_SIZE}; use std::str::FromStr; // Helper function to create a test commitment with a given message From 33f2aed97652ee50102e9ccebf6962520a1677c4 Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 6 May 2025 14:31:07 +0200 Subject: [PATCH 17/21] chore: Clippy fix --- client/src/lib.rs | 1 - program/src/main.rs | 2 +- program/src/merkle/merkle_mountain_range.rs | 6 ++++++ program/src/merkle/mod.rs | 3 --- program/src/merkle/sparse_merkle_tree.rs | 20 +++++++++++++------- server/src/account_server.rs | 11 +++++------ server/src/main.rs | 2 +- server/src/state.rs | 4 ++-- 8 files changed, 28 insertions(+), 21 deletions(-) diff --git a/client/src/lib.rs b/client/src/lib.rs index 4af5f9b..ae4ed51 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -2,7 +2,6 @@ use bitcoin::{ key::Secp256k1, secp256k1::{Message, SecretKey}, }; -use hex; use wasm_bindgen::prelude::*; /// A simple greeting function. diff --git a/program/src/main.rs b/program/src/main.rs index 823664d..96beb91 100644 --- a/program/src/main.rs +++ b/program/src/main.rs @@ -119,7 +119,7 @@ pub fn main() { .next() .expect("Missing coin proof history proofs"), commitment_history_root, - &coin, + coin, inclusion_proofs.next().expect("Missing coin inclusion proof"), ); diff --git a/program/src/merkle/merkle_mountain_range.rs b/program/src/merkle/merkle_mountain_range.rs index 5c0ccfe..c43854e 100644 --- a/program/src/merkle/merkle_mountain_range.rs +++ b/program/src/merkle/merkle_mountain_range.rs @@ -52,6 +52,12 @@ pub struct MerkleMountainRange { levels: Vec>, } +impl Default for MerkleMountainRange { + fn default() -> Self { + Self::new() + } +} + impl MerkleMountainRange { /// Create a new, empty Merkle tree. /// diff --git a/program/src/merkle/mod.rs b/program/src/merkle/mod.rs index fb4c31e..5fd037e 100644 --- a/program/src/merkle/mod.rs +++ b/program/src/merkle/mod.rs @@ -1,7 +1,4 @@ -use merkle_mountain_range::MMRProof; use sha2::{Digest, Sha256}; -use serde::{Deserialize, Serialize}; -use sparse_merkle_tree::InclusionProof; pub mod merkle_mountain_range; pub mod sparse_merkle_tree; diff --git a/program/src/merkle/sparse_merkle_tree.rs b/program/src/merkle/sparse_merkle_tree.rs index be354c5..f1d17ed 100644 --- a/program/src/merkle/sparse_merkle_tree.rs +++ b/program/src/merkle/sparse_merkle_tree.rs @@ -49,8 +49,8 @@ impl InclusionProof { let mut current_hash = hash_concat(&leaf, &self.key); let mut siblings = self.siblings.clone(); // Start with the leaf hash and work our way up to the root - while !siblings.is_empty() { - let sibling = siblings.pop().unwrap(); + while let Some(sibling) = siblings.pop() { + // Get the bit at this level (from most significant to least) let branch = get_bit(&self.key, siblings.len()); @@ -99,8 +99,8 @@ impl NonInclusionProof { hash_concat(&self.leaf.1, &self.leaf.0) }; // Reconstruct the root by combining the siblings - while !siblings.is_empty() { - let sibling = siblings.pop().unwrap(); + while let Some(sibling) = siblings.pop() { + // Combine the current hash with its sibling in the correct order current_hash = if get_bit(&self.leaf.0, siblings.len()) { hash_concat(&sibling, ¤t_hash) @@ -147,8 +147,8 @@ impl NonInclusionProof { } }; // Hash through previous siblings - while !siblings.is_empty() { - let sibling = siblings.pop().unwrap(); + while let Some(sibling) = siblings.pop() { + // Combine children in the correct order. current_hash = if get_bit(&self.key, siblings.len()) { hash_concat(&sibling, ¤t_hash) @@ -234,6 +234,12 @@ pub struct SparseMerkleTree { leaf_values: HashMap<[u8; 32], HashDigest>, } +impl Default for SparseMerkleTree { + fn default() -> Self { + Self::new() + } +} + impl SparseMerkleTree { /// Creates a new sparse Merkle tree with the specified depth. pub fn new() -> Self { @@ -310,7 +316,7 @@ impl SparseMerkleTree { let mut sibling_leaf = (key, DEFAULT_HASHES[TREE_DEPTH]); - if let None = self.nodes.get(&(0, [0; 32])) { + if self.nodes.get(&(0, [0; 32])).is_none() { return Ok(NonInclusionProof { key, root: DEFAULT_HASHES[0], diff --git a/server/src/account_server.rs b/server/src/account_server.rs index 8c63544..8f14c7f 100644 --- a/server/src/account_server.rs +++ b/server/src/account_server.rs @@ -262,7 +262,7 @@ impl AccountServer { let mut account = self .accounts .remove(&coin_proof.coin.recipient) - .unwrap_or_else(|| Account::new()); + .unwrap_or_else(Account::new); // Check if we could generate updated account proof. (e.g. the coin is valid) // TODO: Check if the public key is not included in our accumulator yet (or belongs to the @@ -278,7 +278,7 @@ impl AccountServer { //); // TODO: Make sure the coin_queue doesn't include this coin already. - let address = coin_proof.coin.recipient.clone(); + let address = coin_proof.coin.recipient; account.coin_queue.push(coin_proof); self.accounts.insert(address, account); Ok(()) @@ -366,8 +366,7 @@ impl AccountServer { .commitment .clone() .expect("Coin is missing commitment") - .public_key - .clone(), + .public_key, state, ), ); @@ -387,7 +386,7 @@ impl AccountServer { let mut proof_hints_builder = ProgramInputsBuilder::default(); let proof_hints_builder = proof_hints_builder .account_state(AccountState { - owner: account_address.clone(), + owner: account_address, balance: account.balance, public_key: public_key.serialize().to_vec(), }) @@ -402,7 +401,7 @@ impl AccountServer { .current_history_root(state.mmr.root()); let out_coins = account.create_coins( - account_address.clone(), + account_address, next_public_key, public_key.serialize().to_vec(), coin_templates, diff --git a/server/src/main.rs b/server/src/main.rs index 70133db..72fa2cb 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -73,7 +73,7 @@ async fn main() -> Result<(), Box> { )); // Create a new AccountServer instance with a reference to the state - let mut account_server = account_server::AccountServer::new(Arc::clone(&state)); + let account_server = account_server::AccountServer::new(Arc::clone(&state)); // TODO: Create minting account //wallet.create_account(); diff --git a/server/src/state.rs b/server/src/state.rs index 7d99fee..5d1f93e 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -150,7 +150,7 @@ impl State { // Save prev_mmr_root to a separate file let prev_root_path = format!("{}.prev_root", mmr_path); - std::fs::write(prev_root_path, &self.prev_mmr_root)?; + std::fs::write(prev_root_path, self.prev_mmr_root)?; Ok(()) } @@ -292,7 +292,7 @@ mod tests { std::fs::remove_file(temp_smt_path).ok(); std::fs::remove_file(temp_mmr_path).ok(); // Also remove the prev_root file - std::fs::remove_file(&format!("{}.prev_root", temp_mmr_path)).ok(); + std::fs::remove_file(format!("{}.prev_root", temp_mmr_path)).ok(); // Verify the loaded state has the same roots assert_eq!(original_state.smt.root(), loaded_state.smt.root()); From 170c3cfde09be3c9f9da8215b07ee7ab9924f5c6 Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 6 May 2025 15:28:41 +0200 Subject: [PATCH 18/21] Refactor: Create shared workspace client/server --- Cargo.lock | 17 ++ Cargo.toml | 4 +- client/Cargo.toml | 4 +- client/src/lib.rs | 4 +- program/src/lib.rs | 8 +- program/src/merkle/mod.rs | 3 - server/Cargo.toml | 1 + server/src/account_server.rs | 420 ++++++++++----------------- server/src/main.rs | 3 +- server/src/server.rs | 7 +- server/src/state.rs | 2 +- shared/Cargo.toml | 14 + {server => shared}/src/commitment.rs | 3 +- shared/src/lib.rs | 107 +++++++ 14 files changed, 306 insertions(+), 291 deletions(-) create mode 100644 shared/Cargo.toml rename {server => shared}/src/commitment.rs (98%) create mode 100644 shared/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d425727..641279a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1202,9 +1202,11 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" name = "client" version = "1.1.0" dependencies = [ + "bincode", "bitcoin", "getrandom 0.2.15", "hex", + "shared", "wasm-bindgen", ] @@ -4642,6 +4644,7 @@ dependencies = [ "lazy_static", "serde", "sha2 0.10.8", + "shared", "tokio", "zkcoins-program", "zkcoins-prover", @@ -4697,6 +4700,20 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shared" +version = "1.1.0" +dependencies = [ + "bincode", + "bitcoin", + "hex", + "lazy_static", + "serde", + "sha2 0.10.8", + "zkcoins-program", + "zkcoins-prover", +] + [[package]] name = "shlex" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index bc62932..2564c1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,8 +3,8 @@ members = [ "program", "script", "server", - "client" -] + "client", + "shared"] resolver = "2" [workspace.dependencies] diff --git a/client/Cargo.toml b/client/Cargo.toml index db5c59e..d6f20e2 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -9,6 +9,8 @@ crate-type = ["cdylib"] [dependencies] wasm-bindgen = "0.2" bitcoin = { workspace = true } +bincode = { workspace = true } +shared = { path = "../shared/" } hex = "0.4.3" [dependencies.getrandom] @@ -16,4 +18,4 @@ version = "0.2.15" features = ["js"] [package.metadata.wasm-pack.profile.release] -wasm-opt = false \ No newline at end of file +wasm-opt = false diff --git a/client/src/lib.rs b/client/src/lib.rs index ae4ed51..6dc9bcd 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1,7 +1,7 @@ use bitcoin::{ - key::Secp256k1, - secp256k1::{Message, SecretKey}, + bip32::{ChildNumber, Xpriv}, key::{rand::{rngs::OsRng, RngCore}, Secp256k1}, secp256k1::{Message, SecretKey}, Network }; +use shared::{Address, CoinProof, ProofData, SECP256K1}; use wasm_bindgen::prelude::*; /// A simple greeting function. diff --git a/program/src/lib.rs b/program/src/lib.rs index cbfff68..241cb14 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -5,10 +5,12 @@ use sha2::{Digest, Sha256}; use derive_builder::Builder; use merkle::{ - sparse_merkle_tree::{InclusionProof, NonInclusionProof, DEFAULT_HASHES}, - Amount, HashDigest, PublicKey, + sparse_merkle_tree::{InclusionProof, NonInclusionProof, DEFAULT_HASHES}, HashDigest }; +pub type Amount = u64; +pub type PublicKey = Vec; + pub mod merkle; /// All three proofs that have to be checked per coin or previous account state proof @@ -136,7 +138,7 @@ pub struct AccountState { } impl AccountState { - pub fn new(initial_public_key: merkle::PublicKey) -> Self { + pub fn new(initial_public_key: PublicKey) -> Self { // TODO: The randomness here is annoying why do we not hash the public key directly and // skip the first one in the commitments? // We add random bytes to the public key as a blinding factor for the address. diff --git a/program/src/merkle/mod.rs b/program/src/merkle/mod.rs index 5fd037e..03b2e55 100644 --- a/program/src/merkle/mod.rs +++ b/program/src/merkle/mod.rs @@ -8,9 +8,6 @@ pub const HASH_SIZE: usize = 32; pub type HashDigest = [u8; HASH_SIZE]; pub const ZERO_HASH: HashDigest = [0u8; HASH_SIZE]; -pub type Amount = u64; -pub type PublicKey = Vec; - /// Compute the SHA256 hash of the concatenation of two 32-byte arrays. pub fn hash_concat(left: &HashDigest, right: &HashDigest) -> HashDigest { let mut hasher = Sha256::new(); diff --git a/server/Cargo.toml b/server/Cargo.toml index 2f455ad..1026564 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -16,6 +16,7 @@ axum = { version = "0.7.9", features = ["json", "multipart"] } anyhow = "1.0" zkcoins-prover = { path = "../script/" } zkcoins-program = { path = "../program/" } +shared = { path = "../shared/" } lazy_static = { workspace = true } diff --git a/server/src/account_server.rs b/server/src/account_server.rs index 8f14c7f..26b237d 100644 --- a/server/src/account_server.rs +++ b/server/src/account_server.rs @@ -11,141 +11,15 @@ use bitcoin::{ Network, }; use serde::{Deserialize, Serialize}; +use shared::{Address, CoinProof, Invoice}; use zkcoins_program::merkle::sparse_merkle_tree::{InclusionProof, SparseMerkleTree, DEFAULT_HASHES}; -use zkcoins_program::merkle::{hash_concat, Amount, HashDigest}; +use zkcoins_program::merkle::{hash_concat,HashDigest}; use zkcoins_program::{ - calculate_coin_identifier, AccountState, Coin, CoinTemplate, CommitmentMerkleProofs, ProgramInputsBuilder, ProofData, ProofType + calculate_coin_identifier, AccountState, Amount, Coin, CoinTemplate, CommitmentMerkleProofs, ProgramInputsBuilder, ProofData, ProofType }; use zkcoins_prover::{Proof, Prover}; - -pub type Address = HashDigest; -pub type PrivateKey = bitcoin::bip32::Xpriv; -use lazy_static::lazy_static; - -use crate::commitment::Commitment; use crate::state::State; -lazy_static! { - pub static ref SECP256K1: Secp256k1 = Secp256k1::new(); - -} -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct CoinProof { - pub proof: Proof, - pub coin: Coin, - pub inclusion_proof: InclusionProof, - pub commitment: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy)] -pub struct Invoice { - pub amount: Amount, - pub recipient: Address, -} - -impl Invoice { - pub fn new(amount: Amount, recipient: HashDigest) -> Self { - Invoice { amount, recipient } - } -} - -pub struct ClientAccount { - pub address: HashDigest, - pub num_pubkeys: u32, - pub private_key: PrivateKey, -} - -// TODO Move to tests or client -pub fn new_master_private_key() -> PrivateKey { - let mut rng = OsRng; - let mut seed = [0u8; 32]; - rng.fill_bytes(&mut seed); - PrivateKey::new_master(Network::Bitcoin, &seed).expect("Failed to create private key.") -} - -// TODO: Move this to client -impl ClientAccount { - fn current_private_key(&self) -> SecretKey { - self.private_key - .derive_priv( - &SECP256K1, - &[ChildNumber::Normal { - index: self - .num_pubkeys - .checked_sub(1) - .expect("This account was never commited to."), - }], - ) - .expect("Unable to derive private key for account") - .private_key - } - - pub fn sign_proof(&self, proof: &mut CoinProof) { - let proof_data = - bincode::deserialize::(&proof.proof.public_values.clone().to_vec()) - .expect("Proof is invalid"); - proof.commitment = Some( - Commitment::new( - &self.current_private_key(), - hash_concat( - &proof_data.account_state_hash, - &proof_data.output_coins_root, - ) - .to_vec(), - ) - .expect("Should be able to create commitment"), - ); - } - - // TODO: Remove the server as an argument and send a REST request, fill in the commitment and - // send it back to server. - pub fn send_coins( - &mut self, - server: &mut AccountServer, - invoices: Vec, - ) -> Result, String> { - let result = server.send_coins( - invoices, - self.address, - self.generate_public_key(self.num_pubkeys), - self.generate_public_key(self.num_pubkeys + 1), - ); - self.num_pubkeys += 1; - match result { - Ok(mut proofs) => { - for proof in &mut proofs { - self.sign_proof(proof); - } - Ok(proofs) - } - Err(_) => result, - } - } - - pub fn generate_public_key(&self, index: u32) -> PublicKey { - // WARNING: LEAKING THE MASTER PUBLIC KEY IS EQUIVALENT TO LEAKING THE PRIVATE KEY! - Xpub::from_priv(&SECP256K1, &self.private_key) - .derive_pub(&SECP256K1, &[ChildNumber::Normal { index }]) - .expect("Failed to derive first pubkey") - .public_key - } - - pub fn new(private_key: Xpriv) -> Self { - let mut client_account = ClientAccount { - address: [0u8; 32], - num_pubkeys: 0, - private_key, - }; - let account = AccountState::new( - client_account.generate_public_key(0) - .serialize() - .to_vec(), - ); - client_account.address = account.owner; - client_account - } -} - #[derive(Serialize, Deserialize, Debug)] pub struct Account { pub proof: Option, @@ -169,7 +43,7 @@ impl Account { &self, address: HashDigest, next_public_key: PublicKey, - public_key: zkcoins_program::merkle::PublicKey, + public_key: zkcoins_program::PublicKey, coin_templates: Vec, ) -> Vec { let mut next_account_state = AccountState { @@ -506,146 +380,146 @@ impl AccountServer { } } -#[cfg(test)] -mod tests { - use zkcoins_program::hash; - use std::time::Instant; - - use super::*; - use crate::state::State; - use zkcoins_program::MINTING_ADDRESS; - - fn create_minting_account() -> ClientAccount { - let secret = include_bytes!("../minting_secret.bin"); - let private_key = PrivateKey::new_master(Network::Bitcoin, secret) - .expect("Failed to create private key."); - println!( - "Set MINTING_ADDRESS to {:?}", - &zkcoins_program::MINTING_ADDRESS - ); - ClientAccount { - address: hash(private_key.to_string().as_bytes()), - num_pubkeys: 0, - private_key, - } - } - - #[test] - fn test_wallet_operations() { - let state = Arc::new(Mutex::new(State::new())); - let mut server = AccountServer::new(state); - let mut minting_account = create_minting_account(); - println!("minting account address: {:?}", minting_account.address); - server.import_account( - minting_account.address, - Account { - proof: None, - coin_queue: vec![], - coin_history: SparseMerkleTree::new(), - balance: 10_000, - }, - ); - assert_eq!( - MINTING_ADDRESS, - server.get_minting_account_address().unwrap(), - "Minting address in server and program are different" - ); - let mut account_1 = - ClientAccount::new(Xpriv::new_master(Network::Signet, &[1u8; 32]).unwrap()); - let mut account_2 = - ClientAccount::new(Xpriv::new_master(Network::Signet, &[2u8; 32]).unwrap()); - - assert_eq!( - server.get_account_balance(&MINTING_ADDRESS).unwrap(), - 10_000 - ); - assert!(server.get_account_balance(&account_1.address).is_err()); - assert!(server.get_account_balance(&account_2.address).is_err()); - - let account_2_invoice = Invoice::new(100, account_2.address); - let account_1_invoice = Invoice::new(100, account_1.address); - - let mut coin_proofs = minting_account - .send_coins(&mut server, vec![account_2_invoice, account_1_invoice]) - .unwrap(); - server.state.lock().unwrap().update( - &coin_proofs - .iter() - .map(|x| x.commitment.clone().unwrap()) - .collect::>(), - ); - - server - .receive_coin(coin_proofs.pop().unwrap()) - .expect("Unable to receive coin"); - server - .receive_coin(coin_proofs.pop().unwrap()) - .expect("Unable to receive coin"); - assert_eq!(server.get_account_balance(&account_1.address).unwrap(), 100); - assert_eq!(server.get_account_balance(&account_2.address).unwrap(), 100); - println!("Minting succesfull"); - - let mut coin_proofs = account_2 - .send_coins(&mut server, vec![account_1_invoice]) - .expect("Unable to send coin"); - server.state.lock().unwrap().update( - &coin_proofs - .iter() - .map(|x| x.commitment.clone().unwrap()) - .collect::>(), - ); - assert_eq!(server.get_account_balance(&account_1.address).unwrap(), 100); - assert_eq!(server.get_account_balance(&account_2.address).unwrap(), 0); - - server - .receive_coin(coin_proofs.pop().unwrap()) - .expect("Unable to receive coin"); - assert_eq!(server.get_account_balance(&account_1.address).unwrap(), 200); - assert_eq!(server.get_account_balance(&account_2.address).unwrap(), 0); - - // Send with timer - let start = Instant::now(); - let mut coin_proofs = account_1 - .send_coins(&mut server, vec![account_2_invoice]) - .expect("Unable to send coin"); - let duration = start.elapsed(); - server.state.lock().unwrap().update( - &coin_proofs - .iter() - .map(|x| x.commitment.clone().unwrap()) - .collect::>(), - ); - println!("TIME ELAPSED FOR ONE RECURSIVE SEND: {:?}", duration); - server - .receive_coin(coin_proofs.pop().unwrap()) - .expect("Unable to receive coin"); - assert_eq!(server.get_account_balance(&account_1.address).unwrap(), 100); - assert_eq!(server.get_account_balance(&account_2.address).unwrap(), 100); - } - - #[test] - fn test_create_minting_account() { - let state = Arc::new(Mutex::new(State::new())); - let mut server = AccountServer::new(state); - let minting_account = create_minting_account(); - println!("minting account address: {:?}", minting_account.address); - server.import_account( - minting_account.address, - Account { - proof: None, - coin_queue: vec![], - coin_history: SparseMerkleTree::new(), - balance: 10_000, - }, - ); - assert_eq!( - server.get_minting_account_address().unwrap(), - MINTING_ADDRESS, - "Minting address is not stored in server." - ); - assert_eq!( - server.get_account_balance(&MINTING_ADDRESS).unwrap(), - 10_000 - ); - } -} +//#[cfg(test)] +//mod tests { +// use zkcoins_program::hash; +// use std::time::Instant; +// +// use super::*; +// use crate::state::State; +// use zkcoins_program::MINTING_ADDRESS; +// +// fn create_minting_account() -> ClientAccount { +// let secret = include_bytes!("../minting_secret.bin"); +// let private_key = Xpriv::new_master(Network::Bitcoin, secret) +// .expect("Failed to create private key."); +// println!( +// "Set MINTING_ADDRESS to {:?}", +// &zkcoins_program::MINTING_ADDRESS +// ); +// ClientAccount { +// address: hash(private_key.to_string().as_bytes()), +// num_pubkeys: 0, +// private_key, +// } +// } +// +// #[test] +// fn test_wallet_operations() { +// let state = Arc::new(Mutex::new(State::new())); +// let mut server = AccountServer::new(state); +// let mut minting_account = create_minting_account(); +// println!("minting account address: {:?}", minting_account.address); +// server.import_account( +// minting_account.address, +// Account { +// proof: None, +// coin_queue: vec![], +// coin_history: SparseMerkleTree::new(), +// balance: 10_000, +// }, +// ); +// assert_eq!( +// MINTING_ADDRESS, +// server.get_minting_account_address().unwrap(), +// "Minting address in server and program are different" +// ); +// let mut account_1 = +// ClientAccount::new(Xpriv::new_master(Network::Signet, &[1u8; 32]).unwrap()); +// let mut account_2 = +// ClientAccount::new(Xpriv::new_master(Network::Signet, &[2u8; 32]).unwrap()); +// +// assert_eq!( +// server.get_account_balance(&MINTING_ADDRESS).unwrap(), +// 10_000 +// ); +// assert!(server.get_account_balance(&account_1.address).is_err()); +// assert!(server.get_account_balance(&account_2.address).is_err()); +// +// let account_2_invoice = Invoice::new(100, account_2.address); +// let account_1_invoice = Invoice::new(100, account_1.address); +// +// let mut coin_proofs = minting_account +// .send_coins(&mut server, vec![account_2_invoice, account_1_invoice]) +// .unwrap(); +// server.state.lock().unwrap().update( +// &coin_proofs +// .iter() +// .map(|x| x.commitment.clone().unwrap()) +// .collect::>(), +// ); +// +// server +// .receive_coin(coin_proofs.pop().unwrap()) +// .expect("Unable to receive coin"); +// server +// .receive_coin(coin_proofs.pop().unwrap()) +// .expect("Unable to receive coin"); +// assert_eq!(server.get_account_balance(&account_1.address).unwrap(), 100); +// assert_eq!(server.get_account_balance(&account_2.address).unwrap(), 100); +// println!("Minting succesfull"); +// +// let mut coin_proofs = account_2 +// .send_coins(&mut server, vec![account_1_invoice]) +// .expect("Unable to send coin"); +// server.state.lock().unwrap().update( +// &coin_proofs +// .iter() +// .map(|x| x.commitment.clone().unwrap()) +// .collect::>(), +// ); +// assert_eq!(server.get_account_balance(&account_1.address).unwrap(), 100); +// assert_eq!(server.get_account_balance(&account_2.address).unwrap(), 0); +// +// server +// .receive_coin(coin_proofs.pop().unwrap()) +// .expect("Unable to receive coin"); +// assert_eq!(server.get_account_balance(&account_1.address).unwrap(), 200); +// assert_eq!(server.get_account_balance(&account_2.address).unwrap(), 0); +// +// // Send with timer +// let start = Instant::now(); +// let mut coin_proofs = account_1 +// .send_coins(&mut server, vec![account_2_invoice]) +// .expect("Unable to send coin"); +// let duration = start.elapsed(); +// server.state.lock().unwrap().update( +// &coin_proofs +// .iter() +// .map(|x| x.commitment.clone().unwrap()) +// .collect::>(), +// ); +// println!("TIME ELAPSED FOR ONE RECURSIVE SEND: {:?}", duration); +// server +// .receive_coin(coin_proofs.pop().unwrap()) +// .expect("Unable to receive coin"); +// assert_eq!(server.get_account_balance(&account_1.address).unwrap(), 100); +// assert_eq!(server.get_account_balance(&account_2.address).unwrap(), 100); +// } +// +// #[test] +// fn test_create_minting_account() { +// let state = Arc::new(Mutex::new(State::new())); +// let mut server = AccountServer::new(state); +// let minting_account = create_minting_account(); +// println!("minting account address: {:?}", minting_account.address); +// server.import_account( +// minting_account.address, +// Account { +// proof: None, +// coin_queue: vec![], +// coin_history: SparseMerkleTree::new(), +// balance: 10_000, +// }, +// ); +// assert_eq!( +// server.get_minting_account_address().unwrap(), +// MINTING_ADDRESS, +// "Minting address is not stored in server." +// ); +// assert_eq!( +// server.get_account_balance(&MINTING_ADDRESS).unwrap(), +// 10_000 +// ); +// } +//} diff --git a/server/src/main.rs b/server/src/main.rs index 72fa2cb..38d39f3 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,11 +1,10 @@ -mod commitment; mod publisher; mod scanner; mod server; mod account_server; mod state; -use crate::commitment::Commitment; +use shared::commitment::Commitment; use crate::publisher::EsploraConfig; use crate::scanner::scan_for_inscriptions; use crate::server::start_rest_server; diff --git a/server/src/server.rs b/server/src/server.rs index 9bdc581..026b7ac 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -6,8 +6,9 @@ use axum::{ routing::{get, post}, Router, }; -use bitcoin::Network; +use bitcoin::{bip32::Xpriv, Network}; use serde::{Deserialize, Serialize}; +use shared::{ClientAccount, CoinProof, Invoice}; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::atomic::{AtomicU64, Ordering}; @@ -16,7 +17,7 @@ use tokio::net::TcpListener; use zkcoins_program::hash; use zkcoins_prover::Proof; -use crate::account_server::{AccountServer, ClientAccount, CoinProof, Invoice, PrivateKey}; +use crate::account_server::{AccountServer}; use crate::publisher::create_and_broadcast_inscription; use crate::NETWORK_CONFIG; @@ -427,7 +428,7 @@ pub async fn start_rest_server(account_server: AccountServer, addr: &str) -> any let minting_account = { let secret = include_bytes!("../minting_secret.bin"); - let private_key = PrivateKey::new_master(Network::Bitcoin, secret) + let private_key = Xpriv::new_master(Network::Bitcoin, secret) .expect("Failed to create private key."); println!( "Set MINTING_ADDRESS to {:?}", diff --git a/server/src/state.rs b/server/src/state.rs index 5d1f93e..479aba8 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -1,4 +1,4 @@ -use crate::commitment::Commitment; +use shared::commitment::Commitment; use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; use serde::{Deserialize, Serialize}; diff --git a/shared/Cargo.toml b/shared/Cargo.toml new file mode 100644 index 0000000..95d250e --- /dev/null +++ b/shared/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "shared" +version.workspace = true +edition.workspace = true + +[dependencies] +zkcoins-prover = { path = "../script/" } +zkcoins-program = { path = "../program/" } +lazy_static = { workspace = true } +bitcoin = { workspace = true } +sha2 = { workspace = true } +serde = { workspace = true } +bincode = { workspace = true } +hex = "0.4.3" diff --git a/server/src/commitment.rs b/shared/src/commitment.rs similarity index 98% rename from server/src/commitment.rs rename to shared/src/commitment.rs index a5ab4cd..5255512 100644 --- a/server/src/commitment.rs +++ b/shared/src/commitment.rs @@ -1,4 +1,3 @@ -use crate::account_server::SECP256K1; use bitcoin::secp256k1::{ self, schnorr::Signature, Keypair, Message, PublicKey, Secp256k1, SecretKey }; @@ -7,6 +6,8 @@ use sha2::{Digest, Sha256}; use zkcoins_program::merkle::HashDigest; use std::fmt; +use crate::SECP256K1; + /// A commitment consisting of a public key, a Schnorr signature, and a message. #[derive(Clone, Serialize, Deserialize)] pub struct Commitment { diff --git a/shared/src/lib.rs b/shared/src/lib.rs new file mode 100644 index 0000000..d1dadb2 --- /dev/null +++ b/shared/src/lib.rs @@ -0,0 +1,107 @@ +use bitcoin::{bip32::{ChildNumber, Xpriv, Xpub}, key::{rand::{rngs::OsRng, RngCore}, Secp256k1}, secp256k1::{All, PublicKey, SecretKey}, Network}; +use commitment::Commitment; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use zkcoins_program::{merkle::{hash_concat, sparse_merkle_tree::InclusionProof, HashDigest}, AccountState, Amount, Coin}; +use zkcoins_prover::Proof; + +pub mod commitment; +pub use zkcoins_program::ProofData; + +lazy_static! { + pub static ref SECP256K1: Secp256k1 = Secp256k1::new(); + +} + +pub type Address = HashDigest; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CoinProof { + pub proof: Proof, + pub coin: Coin, + pub inclusion_proof: InclusionProof, + pub commitment: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +pub struct Invoice { + pub amount: Amount, + pub recipient: Address, +} + +impl Invoice { + pub fn new(amount: Amount, recipient: HashDigest) -> Self { + Invoice { amount, recipient } + } +} + +// TODO: Eventually move all of this to the client directly +pub struct ClientAccount { + pub address: Address, + pub num_pubkeys: u32, + pub private_key: Xpriv, +} + +pub fn new_master_private_key() -> Xpriv { + let mut rng = OsRng; + let mut seed = [0u8; 32]; + rng.fill_bytes(&mut seed); + Xpriv::new_master(Network::Bitcoin, &seed).expect("Failed to create private key.") +} + +impl ClientAccount { + fn current_private_key(&self) -> SecretKey { + self.private_key + .derive_priv( + &SECP256K1, + &[ChildNumber::Normal { + index: self + .num_pubkeys + .checked_sub(1) + .expect("This account was never commited to."), + }], + ) + .expect("Unable to derive private key for account") + .private_key + } + + pub fn sign_proof(&self, proof: &mut CoinProof) { + let proof_data = + bincode::deserialize::(&proof.proof.public_values.clone().to_vec()) + .expect("Proof is invalid"); + proof.commitment = Some( + Commitment::new( + &self.current_private_key(), + hash_concat( + &proof_data.account_state_hash, + &proof_data.output_coins_root, + ) + .to_vec(), + ) + .expect("Should be able to create commitment"), + ); + } + + pub fn generate_public_key(&self, index: u32) -> PublicKey { + // WARNING: LEAKING THE MASTER PUBLIC KEY IS EQUIVALENT TO LEAKING THE PRIVATE KEY! + Xpub::from_priv(&SECP256K1, &self.private_key) + .derive_pub(&SECP256K1, &[ChildNumber::Normal { index }]) + .expect("Failed to derive first pubkey") + .public_key + } + + pub fn new(private_key: Xpriv) -> Self { + let mut client_account = ClientAccount { + address: [0u8; 32], + num_pubkeys: 0, + private_key, + }; + let account = AccountState::new( + client_account.generate_public_key(0) + .serialize() + .to_vec(), + ); + client_account.address = account.owner; + client_account + } +} From 3659940edf31a86d12ef22d8f612a3e65dff298b Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 6 May 2025 15:29:34 +0200 Subject: [PATCH 19/21] chore: Clippy fix --- client/src/lib.rs | 3 +-- server/src/account_server.rs | 12 ++---------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/client/src/lib.rs b/client/src/lib.rs index 6dc9bcd..30630da 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1,7 +1,6 @@ use bitcoin::{ - bip32::{ChildNumber, Xpriv}, key::{rand::{rngs::OsRng, RngCore}, Secp256k1}, secp256k1::{Message, SecretKey}, Network + key::Secp256k1, secp256k1::{Message, SecretKey} }; -use shared::{Address, CoinProof, ProofData, SECP256K1}; use wasm_bindgen::prelude::*; /// A simple greeting function. diff --git a/server/src/account_server.rs b/server/src/account_server.rs index 26b237d..b6128e4 100644 --- a/server/src/account_server.rs +++ b/server/src/account_server.rs @@ -1,18 +1,10 @@ use std::sync::{Arc, Mutex, MutexGuard}; use std::{collections::HashMap, mem::take}; -use bitcoin::{ - bip32::{ChildNumber, Xpriv, Xpub}, - key::{ - rand::{rngs::OsRng, RngCore}, - Secp256k1, - }, - secp256k1::{All, PublicKey, SecretKey}, - Network, -}; +use bitcoin::secp256k1::PublicKey; use serde::{Deserialize, Serialize}; use shared::{Address, CoinProof, Invoice}; -use zkcoins_program::merkle::sparse_merkle_tree::{InclusionProof, SparseMerkleTree, DEFAULT_HASHES}; +use zkcoins_program::merkle::sparse_merkle_tree::{SparseMerkleTree, DEFAULT_HASHES}; use zkcoins_program::merkle::{hash_concat,HashDigest}; use zkcoins_program::{ calculate_coin_identifier, AccountState, Amount, Coin, CoinTemplate, CommitmentMerkleProofs, ProgramInputsBuilder, ProofData, ProofType From 2176eb671a483b3d9b1d33c5a29da7a2d3e23b06 Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 6 May 2025 15:42:38 +0200 Subject: [PATCH 20/21] tests: No longer use ClientAccount --- server/src/account_server.rs | 394 ++++++++++++++++++++++------------- 1 file changed, 251 insertions(+), 143 deletions(-) diff --git a/server/src/account_server.rs b/server/src/account_server.rs index b6128e4..eaf56fe 100644 --- a/server/src/account_server.rs +++ b/server/src/account_server.rs @@ -11,6 +11,14 @@ use zkcoins_program::{ }; use zkcoins_prover::{Proof, Prover}; use crate::state::State; +use bitcoin::{ + bip32::{ChildNumber, Xpriv, Xpub}, + key::Secp256k1, + secp256k1::{All, PublicKey as BitcoinPublicKey, SecretKey}, + Network, +}; +use shared::{commitment::Commitment}; +use lazy_static::lazy_static; #[derive(Serialize, Deserialize, Debug)] pub struct Account { @@ -372,146 +380,246 @@ impl AccountServer { } } -//#[cfg(test)] -//mod tests { -// use zkcoins_program::hash; -// use std::time::Instant; -// -// use super::*; -// use crate::state::State; -// use zkcoins_program::MINTING_ADDRESS; -// -// fn create_minting_account() -> ClientAccount { -// let secret = include_bytes!("../minting_secret.bin"); -// let private_key = Xpriv::new_master(Network::Bitcoin, secret) -// .expect("Failed to create private key."); -// println!( -// "Set MINTING_ADDRESS to {:?}", -// &zkcoins_program::MINTING_ADDRESS -// ); -// ClientAccount { -// address: hash(private_key.to_string().as_bytes()), -// num_pubkeys: 0, -// private_key, -// } -// } -// -// #[test] -// fn test_wallet_operations() { -// let state = Arc::new(Mutex::new(State::new())); -// let mut server = AccountServer::new(state); -// let mut minting_account = create_minting_account(); -// println!("minting account address: {:?}", minting_account.address); -// server.import_account( -// minting_account.address, -// Account { -// proof: None, -// coin_queue: vec![], -// coin_history: SparseMerkleTree::new(), -// balance: 10_000, -// }, -// ); -// assert_eq!( -// MINTING_ADDRESS, -// server.get_minting_account_address().unwrap(), -// "Minting address in server and program are different" -// ); -// let mut account_1 = -// ClientAccount::new(Xpriv::new_master(Network::Signet, &[1u8; 32]).unwrap()); -// let mut account_2 = -// ClientAccount::new(Xpriv::new_master(Network::Signet, &[2u8; 32]).unwrap()); -// -// assert_eq!( -// server.get_account_balance(&MINTING_ADDRESS).unwrap(), -// 10_000 -// ); -// assert!(server.get_account_balance(&account_1.address).is_err()); -// assert!(server.get_account_balance(&account_2.address).is_err()); -// -// let account_2_invoice = Invoice::new(100, account_2.address); -// let account_1_invoice = Invoice::new(100, account_1.address); -// -// let mut coin_proofs = minting_account -// .send_coins(&mut server, vec![account_2_invoice, account_1_invoice]) -// .unwrap(); -// server.state.lock().unwrap().update( -// &coin_proofs -// .iter() -// .map(|x| x.commitment.clone().unwrap()) -// .collect::>(), -// ); -// -// server -// .receive_coin(coin_proofs.pop().unwrap()) -// .expect("Unable to receive coin"); -// server -// .receive_coin(coin_proofs.pop().unwrap()) -// .expect("Unable to receive coin"); -// assert_eq!(server.get_account_balance(&account_1.address).unwrap(), 100); -// assert_eq!(server.get_account_balance(&account_2.address).unwrap(), 100); -// println!("Minting succesfull"); -// -// let mut coin_proofs = account_2 -// .send_coins(&mut server, vec![account_1_invoice]) -// .expect("Unable to send coin"); -// server.state.lock().unwrap().update( -// &coin_proofs -// .iter() -// .map(|x| x.commitment.clone().unwrap()) -// .collect::>(), -// ); -// assert_eq!(server.get_account_balance(&account_1.address).unwrap(), 100); -// assert_eq!(server.get_account_balance(&account_2.address).unwrap(), 0); -// -// server -// .receive_coin(coin_proofs.pop().unwrap()) -// .expect("Unable to receive coin"); -// assert_eq!(server.get_account_balance(&account_1.address).unwrap(), 200); -// assert_eq!(server.get_account_balance(&account_2.address).unwrap(), 0); -// -// // Send with timer -// let start = Instant::now(); -// let mut coin_proofs = account_1 -// .send_coins(&mut server, vec![account_2_invoice]) -// .expect("Unable to send coin"); -// let duration = start.elapsed(); -// server.state.lock().unwrap().update( -// &coin_proofs -// .iter() -// .map(|x| x.commitment.clone().unwrap()) -// .collect::>(), -// ); -// println!("TIME ELAPSED FOR ONE RECURSIVE SEND: {:?}", duration); -// server -// .receive_coin(coin_proofs.pop().unwrap()) -// .expect("Unable to receive coin"); -// assert_eq!(server.get_account_balance(&account_1.address).unwrap(), 100); -// assert_eq!(server.get_account_balance(&account_2.address).unwrap(), 100); -// } -// -// #[test] -// fn test_create_minting_account() { -// let state = Arc::new(Mutex::new(State::new())); -// let mut server = AccountServer::new(state); -// let minting_account = create_minting_account(); -// println!("minting account address: {:?}", minting_account.address); -// server.import_account( -// minting_account.address, -// Account { -// proof: None, -// coin_queue: vec![], -// coin_history: SparseMerkleTree::new(), -// balance: 10_000, -// }, -// ); -// assert_eq!( -// server.get_minting_account_address().unwrap(), -// MINTING_ADDRESS, -// "Minting address is not stored in server." -// ); -// assert_eq!( -// server.get_account_balance(&MINTING_ADDRESS).unwrap(), -// 10_000 -// ); -// } -//} +#[cfg(test)] +mod tests { + use zkcoins_program::hash; + use std::time::Instant; + + use super::*; + use crate::state::State; + use zkcoins_program::MINTING_ADDRESS; + use bitcoin::{ + bip32::{ChildNumber, Xpriv, Xpub}, + key::Secp256k1, + secp256k1::{All, PublicKey as BitcoinPublicKey, SecretKey}, + Network, + }; + use shared::{commitment::Commitment, ProofData}; + use lazy_static::lazy_static; + + lazy_static! { + static ref SECP256K1_TEST_CTX: Secp256k1 = Secp256k1::new(); + } + + // Fixed seed for deterministic address generation in tests for generic accounts + const TEST_ACCOUNT_RANDOM_SEED_FOR_ADDRESS: [u8; 32] = [1u8; 32]; + + fn generate_test_public_key(private_key: &Xpriv, index: u32) -> BitcoinPublicKey { + Xpub::from_priv(&SECP256K1_TEST_CTX, private_key) + .derive_pub(&SECP256K1_TEST_CTX, &[ChildNumber::Normal { index }]) + .expect("Failed to derive public key for test") + .public_key + } + + fn derive_test_secret_key(private_key: &Xpriv, index: u32) -> SecretKey { + private_key + .derive_priv( + &SECP256K1_TEST_CTX, + &[ChildNumber::Normal { index }], + ) + .expect("Unable to derive private key for test") + .private_key + } + + struct TestAccountData { + xpriv: Xpriv, + address: Address, + num_pubkeys: u32, + } + + impl TestAccountData { + fn new_minting_account() -> Self { + let secret = include_bytes!("../minting_secret.bin"); + let xpriv = Xpriv::new_master(Network::Bitcoin, secret) + .expect("Failed to create private key for minting account."); + + TestAccountData { + xpriv, + address: MINTING_ADDRESS, + num_pubkeys: 0, + } + } + + fn new_generic(seed: &[u8; 32], network: Network) -> Self { + let xpriv = Xpriv::new_master(network, seed) + .expect("Failed to create private key for generic account."); + + let initial_pk_bytes = generate_test_public_key(&xpriv, 0).serialize().to_vec(); + // Deterministic address generation for tests, mimicking AccountState::new but with fixed randomness + let address = zkcoins_program::hash(&[initial_pk_bytes, TEST_ACCOUNT_RANDOM_SEED_FOR_ADDRESS.to_vec()].concat()); + + TestAccountData { + xpriv, + address, + num_pubkeys: 0, + } + } + + fn execute_send_coins( + &mut self, + server: &mut AccountServer, + invoices: Vec, + ) -> Result, String> { + let current_pk = generate_test_public_key(&self.xpriv, self.num_pubkeys); + let next_pk = generate_test_public_key(&self.xpriv, self.num_pubkeys + 1); + + let mut coin_proofs = server.send_coins( + invoices, + self.address, + current_pk, + next_pk, + )?; + + // The key used for the commitment corresponds to current_pk + let signing_secret_key = derive_test_secret_key(&self.xpriv, self.num_pubkeys); + + self.num_pubkeys += 1; // Increment after deriving signing key for current op, before it's used for next op + + for cp in &mut coin_proofs { + let proof_data = bincode::deserialize::(&cp.proof.public_values.to_vec()) + .expect("ProofData deserialization failed in test"); + let commitment_hash_input = zkcoins_program::merkle::hash_concat( + &proof_data.account_state_hash, + &proof_data.output_coins_root, + ); + cp.commitment = Some( + Commitment::new( + &signing_secret_key, + commitment_hash_input.to_vec(), + ) + .expect("Failed to create commitment for coin proof in test"), + ); + } + Ok(coin_proofs) + } + } + + #[test] + fn test_wallet_operations() { + let state_arc = Arc::new(Mutex::new(State::new())); + let mut server = AccountServer::new(Arc::clone(&state_arc)); + + let mut minting_account_data = TestAccountData::new_minting_account(); + println!("minting account address: {:?}", minting_account_data.address); + server.import_account( + minting_account_data.address, + Account { + proof: None, + coin_queue: vec![], + coin_history: SparseMerkleTree::new(), + balance: 10_000, + }, + ); + assert_eq!( + MINTING_ADDRESS, + server.get_minting_account_address().unwrap(), + "Minting address in server and program are different" + ); + + let mut account_1_data = + TestAccountData::new_generic(&[1u8; 32], Network::Signet); + let mut account_2_data = + TestAccountData::new_generic(&[2u8; 32], Network::Signet); + + assert_eq!( + server.get_account_balance(&MINTING_ADDRESS).unwrap(), + 10_000 + ); + assert!(server.get_account_balance(&account_1_data.address).is_err()); + assert!(server.get_account_balance(&account_2_data.address).is_err()); + + // Note: Invoices use addresses. + let account_2_invoice = Invoice::new(100, account_2_data.address); + let account_1_invoice = Invoice::new(100, account_1_data.address); + + let mut coin_proofs = minting_account_data + .execute_send_coins(&mut server, vec![account_2_invoice.clone(), account_1_invoice.clone()]) + .unwrap(); + + state_arc.lock().unwrap().update( + &coin_proofs + .iter() + .map(|x| x.commitment.clone().unwrap()) + .collect::>(), + ); + + server + .receive_coin(coin_proofs.pop().unwrap()) // Order might matter if tied to invoice order + .expect("Unable to receive coin for account_1_invoice"); // Assuming account_1_invoice was last in vec or order doesn't strictly map here + server + .receive_coin(coin_proofs.pop().unwrap()) + .expect("Unable to receive coin for account_2_invoice"); + + assert_eq!(server.get_account_balance(&account_1_data.address).unwrap(), 100); + assert_eq!(server.get_account_balance(&account_2_data.address).unwrap(), 100); + println!("Minting successful"); + + let mut coin_proofs_from_acc2 = account_2_data + .execute_send_coins(&mut server, vec![account_1_invoice.clone()]) // account_2 sends to account_1 + .expect("Unable to send coin from account_2"); + + state_arc.lock().unwrap().update( + &coin_proofs_from_acc2 + .iter() + .map(|x| x.commitment.clone().unwrap()) + .collect::>(), + ); + // Balances before receiving the new coin by account_1 + assert_eq!(server.get_account_balance(&account_1_data.address).unwrap(), 100); + assert_eq!(server.get_account_balance(&account_2_data.address).unwrap(), 0); // account_2's balance reduced after send + + server + .receive_coin(coin_proofs_from_acc2.pop().unwrap()) + .expect("Unable to receive coin by account_1 from account_2"); + assert_eq!(server.get_account_balance(&account_1_data.address).unwrap(), 200); + assert_eq!(server.get_account_balance(&account_2_data.address).unwrap(), 0); + + // Send with timer + let start_time = Instant::now(); + let mut coin_proofs_from_acc1 = account_1_data + .execute_send_coins(&mut server, vec![account_2_invoice.clone()]) // account_1 sends to account_2 + .expect("Unable to send coin from account_1"); + let duration = start_time.elapsed(); + + state_arc.lock().unwrap().update( + &coin_proofs_from_acc1 + .iter() + .map(|x| x.commitment.clone().unwrap()) + .collect::>(), + ); + println!("TIME ELAPSED FOR ONE RECURSIVE SEND: {:?}", duration); + server + .receive_coin(coin_proofs_from_acc1.pop().unwrap()) + .expect("Unable to receive coin by account_2 from account_1"); + assert_eq!(server.get_account_balance(&account_1_data.address).unwrap(), 100); // 200 - 100 + assert_eq!(server.get_account_balance(&account_2_data.address).unwrap(), 100); // 0 + 100 + } + + #[test] + fn test_create_minting_account() { + let state_arc = Arc::new(Mutex::new(State::new())); + let mut server = AccountServer::new(state_arc); + + let minting_account_data = TestAccountData::new_minting_account(); + println!("minting account address: {:?}", minting_account_data.address); + + server.import_account( + minting_account_data.address, // This is MINTING_ADDRESS + Account { + proof: None, + coin_queue: vec![], + coin_history: SparseMerkleTree::new(), + balance: 10_000, + }, + ); + assert_eq!( + server.get_minting_account_address().unwrap(), + MINTING_ADDRESS, + "Minting address is not stored in server correctly." + ); + assert_eq!( + server.get_account_balance(&MINTING_ADDRESS).unwrap(), + 10_000 + ); + } +} From 719756a5b4068bd95379f2e4ef5f2eb0be58b640 Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 6 May 2025 17:01:34 +0200 Subject: [PATCH 21/21] WIP: AI generated client example --- Cargo.lock | 65 +++++++++- Cargo.toml | 3 +- client/Cargo.toml | 27 ++++- client/index.html | 173 ++++++++++++++++++++++++--- client/src/lib.rs | 223 +++++++++++++++++++++++++++++++++++ server/Cargo.toml | 1 + server/src/account_server.rs | 21 ++-- server/src/server.rs | 7 +- shared/Cargo.toml | 5 +- shared/src/lib.rs | 35 ++---- 10 files changed, 501 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 641279a..1aebfcb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1206,8 +1206,13 @@ dependencies = [ "bitcoin", "getrandom 0.2.15", "hex", + "serde", + "serde-wasm-bindgen", + "serde_json", "shared", "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] @@ -2350,6 +2355,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + [[package]] name = "httparse" version = "1.10.1" @@ -2938,6 +2949,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -4551,6 +4572,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" version = "1.0.219" @@ -4646,6 +4678,7 @@ dependencies = [ "sha2 0.10.8", "shared", "tokio", + "tower-http", "zkcoins-program", "zkcoins-prover", ] @@ -4711,7 +4744,6 @@ dependencies = [ "serde", "sha2 0.10.8", "zkcoins-program", - "zkcoins-prover", ] [[package]] @@ -5745,6 +5777,31 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.9.0", + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -5902,6 +5959,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" version = "1.0.18" diff --git a/Cargo.toml b/Cargo.toml index 2564c1b..1226dc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ serde = { version = "1.0", features = ["derive"] } rand = "0.8" blake3 = "1.6.1" lazy_static = "1.5.0" -bitcoin = { version = "0.32.5", features = ["std", "rand", "serde", "rand-std"] } +bitcoin = { version = "0.32.5", default-features = false, features = ["std", "rand", "serde", "rand-std"] } sp1-sdk = "4.0.0" [profile.dev] @@ -24,7 +24,6 @@ opt-level = 3 version = "1.1.0" edition = "2021" - [patch.crates-io] sp1-zkvm = { git = "https://github.com/succinctlabs/sp1", tag = "v4.1.2" } sp1-lib = { git = "https://github.com/succinctlabs/sp1", tag = "v4.1.2" } diff --git a/client/Cargo.toml b/client/Cargo.toml index d6f20e2..55a02da 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -6,9 +6,34 @@ edition.workspace = true [lib] crate-type = ["cdylib"] +[features] +console_error_panic_hook = [] + [dependencies] wasm-bindgen = "0.2" -bitcoin = { workspace = true } +web-sys = { version = "0.3", features = [ + "Window", + "Request", + "RequestInit", + "RequestMode", + "Headers", + "Response", + "Storage", + "console", + "Document", + "Element", + "HtmlElement", + "Node", + "Text", + "Performance", + "PerformanceTiming", + "Location" +] } +wasm-bindgen-futures = "0.4" +serde = { workspace = true } +serde_json = "1.0" +serde-wasm-bindgen = "0.6" +bitcoin = { workspace = true, default-features = false, features = ["std", "rand", "serde", "rand-std"] } bincode = { workspace = true } shared = { path = "../shared/" } hex = "0.4.3" diff --git a/client/index.html b/client/index.html index 6aaf181..dd03df5 100644 --- a/client/index.html +++ b/client/index.html @@ -1,16 +1,159 @@ - - -

Hello, World!

+ + + + + + zkCoins Wallet + + + +

zkCoins Wallet

+ +
+ +
+

Create New Account

+ +
+
+ + +
+

Send Coins

+
+ + +
+
+ + +
+ +
+
+ + +
+

Debug Output

+

+            
+
+ + + + diff --git a/client/src/lib.rs b/client/src/lib.rs index 30630da..865914d 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -2,6 +2,13 @@ use bitcoin::{ key::Secp256k1, secp256k1::{Message, SecretKey} }; use wasm_bindgen::prelude::*; +use web_sys::{window}; +use serde::{Serialize, Deserialize}; +use shared::{ClientAccount, new_master_private_key}; +use bitcoin::bip32::Xpriv; +use core::str::FromStr; +use wasm_bindgen_futures::JsFuture; +use web_sys::{Request, RequestInit, RequestMode, Headers, Response}; /// A simple greeting function. #[wasm_bindgen] @@ -53,3 +60,219 @@ pub fn sign_schnorr(private_key_hex: &str, hash_hex: &str) -> Result Result { + // It's good practice to set up a panic hook for easier debugging in WASM. + // This should ideally be called once when the WASM module initializes. + // For simplicity in this function, we'll call it here. + // If you have a central initialization, move it there. + #[cfg(feature = "console_error_panic_hook")] + set_panic_hook(); + + let window = window().ok_or_else(|| JsValue::from_str("Failed to obtain window object"))?; + let local_storage = window.local_storage() + .map_err(|e| JsValue::from_str(&format!("Error accessing localStorage: {:?}", e)))? + .ok_or_else(|| JsValue::from_str("localStorage is not available in this browser"))?; + + // Generate a new master private key. + // new_master_private_key internally uses OsRng, which for wasm relies on getrandom's js feature. + let master_xpriv = new_master_private_key(); + + // Create a new ClientAccount. This also derives the address. + let client_account = ClientAccount::new(master_xpriv); + + // Prepare the data for serialization. + let storable_data = StorableClientAccountData { + address_hex: hex::encode(client_account.address), + num_pubkeys: client_account.num_pubkeys, // Initialized to 0 by ClientAccount::new + xpriv_str: client_account.private_key.to_string(), // Serialize Xpriv to its string representation + }; + + // Serialize to JSON. + let json_data = serde_json::to_string(&storable_data) + .map_err(|e| JsValue::from_str(&format!("Failed to serialize account data to JSON: {}", e)))?; + + // Define a key for localStorage. + let storage_key = format!("account_{}", storable_data.address_hex); + + // Store the JSON string in localStorage. + local_storage.set_item(&storage_key, &json_data) + .map_err(|e| JsValue::from_str(&format!("Failed to store account in localStorage: {:?}", e)))?; + + // Return the hex-encoded address of the newly created and stored account. + Ok(storable_data.address_hex) +} + +// Example of how you might retrieve it (optional, not requested but good for completeness) +/* +#[wasm_bindgen] +pub fn get_stored_account(address_hex: &str) -> Result { + #[cfg(feature = "console_error_panic_hook")] + set_panic_hook(); + + let window = window().ok_or_else(|| JsValue::from_str("Failed to obtain window object"))?; + let local_storage = window.local_storage() + .map_err(|e| JsValue::from_str(&format!("Error accessing localStorage: {:?}", e)))? + .ok_or_else(|| JsValue::from_str("localStorage is not available"))?; + + let storage_key = format!("account_{}", address_hex); + + match local_storage.get_item(&storage_key) { + Ok(Some(json_data)) => { + let storable_data: StorableClientAccountData = serde_json::from_str(&json_data) + .map_err(|e| JsValue::from_str(&format!("Failed to deserialize account data: {}", e)))?; + // For now, just returning the JsValue from the serialized struct. + // If you need to reconstruct ClientAccount, you'd use Xpriv::from_str(&storable_data.xpriv_str) + // and then create the ClientAccount if needed in Rust. + // Or, just return the fields as a JS object. + Ok(serde_wasm_bindgen::to_value(&storable_data)?) + } + Ok(None) => Err(JsValue::from_str(&format!("Account not found for address: {}", address_hex))), + Err(e) => Err(JsValue::from_str(&format!("Error retrieving account from localStorage: {:?}", e))), + } +} +*/ + +// Re-define PublicKey alias if not already globally available in this scope +// from the existing sign_schnorr function's imports. +// It seems `bitcoin::secp256k1::PublicKey` is directly used in shared::ClientAccount.generate_public_key +// so we can use that directly. Let's use an alias for clarity if needed. +type BitcoinSecp256k1PublicKey = bitcoin::secp256k1::PublicKey; + +#[derive(Serialize, Debug)] // Added Debug for logging potential errors +struct ClientSendCoinRequest { + account_address: String, // hex of sender + recipient: String, // hex of recipient + amount: u64, + public_key: BitcoinSecp256k1PublicKey, + next_public_key: BitcoinSecp256k1PublicKey, +} + +#[derive(Deserialize, Debug, Serialize)] // Added Serialize to return as JsValue easily +struct ClientSendCoinResponse { + success: bool, + proof_id: Option, +} + +#[wasm_bindgen] +pub async fn send_coins_from_browser( + sender_address_hex: String, + recipient_address_hex: String, + amount_str: String, // Amount as string to avoid JS number precision issues with u64 +) -> Result { + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); // Call panic hook + + let amount = amount_str.parse::().map_err(|e| { + JsValue::from_str(&format!("Invalid amount format: {}. Expected u64.", e)) + })?; + + let window = window().ok_or_else(|| JsValue::from_str("Failed to obtain window object"))?; + let local_storage = window + .local_storage() + .map_err(|e| JsValue::from_str(&format!("Error accessing localStorage: {:?}", e)))? + .ok_or_else(|| JsValue::from_str("localStorage is not available"))?; + + let storage_key = format!("account_{}", sender_address_hex); + let account_json_data = local_storage + .get_item(&storage_key) + .map_err(|e| JsValue::from_str(&format!("Error retrieving account: {:?}", e)))? + .ok_or_else(|| JsValue::from_str(&format!("Account {} not found in localStorage", sender_address_hex)))?; + + let mut storable_data: StorableClientAccountData = serde_json::from_str(&account_json_data) + .map_err(|e| JsValue::from_str(&format!("Failed to deserialize account data: {}", e)))?; + + let sender_xpriv = Xpriv::from_str(&storable_data.xpriv_str) + .map_err(|e| JsValue::from_str(&format!("Failed to parse Xpriv from stored string: {}", e)))?; + + // Decode sender_address_hex to bytes for the temporary ClientAccount struct if needed for its address field. + // shared::ClientAccount address field is [u8; 32]. + let mut sender_address_bytes = [0u8; 32]; + hex::decode_to_slice(&sender_address_hex, &mut sender_address_bytes) + .map_err(|e| JsValue::from_str(&format!("Failed to decode sender_address_hex: {}", e)))?; + + // Create a temporary ClientAccount to use its methods for key generation. + // Note: ClientAccount::new derives address differently. Here we assume the stored address_hex is the source of truth for the Address field. + let temp_client_account = ClientAccount { + address: sender_address_bytes, // Use the address this function was called with + num_pubkeys: storable_data.num_pubkeys, // This is not directly used by generate_public_key, index is passed + private_key: sender_xpriv, + }; + + let current_public_key = temp_client_account.generate_public_key(storable_data.num_pubkeys); + let next_public_key = temp_client_account.generate_public_key(storable_data.num_pubkeys + 1); + + let request_payload = ClientSendCoinRequest { + account_address: sender_address_hex.clone(), + recipient: recipient_address_hex, + amount, + public_key: current_public_key, + next_public_key, + }; + + let request_json = serde_json::to_string(&request_payload) + .map_err(|e| JsValue::from_str(&format!("Failed to serialize send_coins request: {}", e)))?; + + let opts = RequestInit::new(); + opts.set_method("POST"); + opts.set_mode(RequestMode::Cors); // Adjust if necessary + opts.set_body(&JsValue::from_str(&request_json)); + + let headers = Headers::new().map_err(|_| JsValue::from_str("Failed to create Headers"))?; + headers.set("Content-Type", "application/json").map_err(|_| JsValue::from_str("Failed to set Content-Type header"))?; + opts.set_headers(&headers); + + // Assuming your server is running on the same origin or configured for CORS. + // Adjust API_BASE_URL as needed. For now, using a relative path. + let url = "/api/send"; + let request = Request::new_with_str_and_init(url, &opts) + .map_err(|e| JsValue::from_str(&format!("Failed to create Request: {:?}", e)))?; + + let resp_value = JsFuture::from(window.fetch_with_request(&request)) + .await + .map_err(|e| JsValue::from_str(&format!("Fetch error: {:?}", e)))?; + + let resp: Response = resp_value + .dyn_into() + .map_err(|_| JsValue::from_str("Failed to convert JsValue to Response"))?; + + if !resp.ok() { + let error_text = JsFuture::from(resp.text().map_err(|_| JsValue::from_str("Failed to get response text"))?) + .await + .map_err(|_| JsValue::from_str("Failed to get error response text"))? + .as_string() + .unwrap_or_else(|| "Unknown error".to_string()); + return Err(JsValue::from_str(&format!( + "Server error: {} - {}", + resp.status(), + error_text + ))); + } + + let resp_json = JsFuture::from(resp.json().map_err(|_| JsValue::from_str("Failed to parse response as JSON"))?) + .await + .map_err(|_| JsValue::from_str("Failed to parse response as JSON"))?; + + let server_response: ClientSendCoinResponse = serde_wasm_bindgen::from_value(resp_json.clone()) + .map_err(|e| JsValue::from_str(&format!("Failed to deserialize server response: {}", e)))?; + + if server_response.success { + // Update the stored account data with the incremented num_pubkeys + storable_data.num_pubkeys += 1; + let updated_account_json_data = serde_json::to_string(&storable_data) + .map_err(|e| JsValue::from_str(&format!("Failed to serialize updated account data: {}", e)))?; + local_storage.set_item(&storage_key, &updated_account_json_data) + .map_err(|e| JsValue::from_str(&format!("Failed to update account in localStorage: {:?}", e)))?; + } + + // Return the server response as a JsValue + Ok(resp_json) +} diff --git a/server/Cargo.toml b/server/Cargo.toml index 1026564..bb8d2ec 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -18,6 +18,7 @@ zkcoins-prover = { path = "../script/" } zkcoins-program = { path = "../program/" } shared = { path = "../shared/" } lazy_static = { workspace = true } +tower-http = { version = "0.5", features = ["fs"] } [features] diff --git a/server/src/account_server.rs b/server/src/account_server.rs index eaf56fe..3317a0f 100644 --- a/server/src/account_server.rs +++ b/server/src/account_server.rs @@ -3,23 +3,26 @@ use std::{collections::HashMap, mem::take}; use bitcoin::secp256k1::PublicKey; use serde::{Deserialize, Serialize}; -use shared::{Address, CoinProof, Invoice}; -use zkcoins_program::merkle::sparse_merkle_tree::{SparseMerkleTree, DEFAULT_HASHES}; +use shared::{Address, Invoice}; +use zkcoins_program::merkle::sparse_merkle_tree::{SparseMerkleTree, DEFAULT_HASHES, InclusionProof}; use zkcoins_program::merkle::{hash_concat,HashDigest}; use zkcoins_program::{ calculate_coin_identifier, AccountState, Amount, Coin, CoinTemplate, CommitmentMerkleProofs, ProgramInputsBuilder, ProofData, ProofType }; use zkcoins_prover::{Proof, Prover}; use crate::state::State; -use bitcoin::{ - bip32::{ChildNumber, Xpriv, Xpub}, - key::Secp256k1, - secp256k1::{All, PublicKey as BitcoinPublicKey, SecretKey}, - Network, -}; -use shared::{commitment::Commitment}; +use bitcoin::bip32::Xpriv; +use shared::commitment::Commitment; use lazy_static::lazy_static; +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CoinProof { + pub proof: Proof, + pub coin: Coin, + pub inclusion_proof: InclusionProof, + pub commitment: Option, +} + #[derive(Serialize, Deserialize, Debug)] pub struct Account { pub proof: Option, diff --git a/server/src/server.rs b/server/src/server.rs index 026b7ac..6551d43 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -8,7 +8,7 @@ use axum::{ }; use bitcoin::{bip32::Xpriv, Network}; use serde::{Deserialize, Serialize}; -use shared::{ClientAccount, CoinProof, Invoice}; +use shared::{ClientAccount, Invoice}; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::atomic::{AtomicU64, Ordering}; @@ -17,7 +17,7 @@ use tokio::net::TcpListener; use zkcoins_program::hash; use zkcoins_prover::Proof; -use crate::account_server::{AccountServer}; +use crate::account_server::{AccountServer, CoinProof}; use crate::publisher::create_and_broadcast_inscription; use crate::NETWORK_CONFIG; @@ -461,6 +461,7 @@ pub async fn start_rest_server(account_server: AccountServer, addr: &str) -> any // Build our application with routes let app = Router::new() .nest("/api", api_routes) // Put all API routes under /api + .nest_service("/pkg", tower_http::services::ServeDir::new("client/www/pkg")) // Serve static files from www/pkg .fallback(serve_index); // Use our custom serve_index handler as fallback // Run the server @@ -474,7 +475,7 @@ pub async fn start_rest_server(account_server: AccountServer, addr: &str) -> any // Handler to serve the index.html file async fn serve_index() -> impl IntoResponse { let current_dir = std::env::current_dir().unwrap_or_default(); - let index_path = current_dir.join("src").join("index.html"); + let index_path = current_dir.join("client").join("www").join("index.html"); match tokio::fs::read_to_string(&index_path).await { Ok(content) => { diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 95d250e..4d5b830 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -4,11 +4,10 @@ version.workspace = true edition.workspace = true [dependencies] -zkcoins-prover = { path = "../script/" } zkcoins-program = { path = "../program/" } lazy_static = { workspace = true } bitcoin = { workspace = true } sha2 = { workspace = true } -serde = { workspace = true } -bincode = { workspace = true } +serde = { workspace = true } +bincode = { workspace = true } hex = "0.4.3" diff --git a/shared/src/lib.rs b/shared/src/lib.rs index d1dadb2..2b5d906 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -2,27 +2,17 @@ use bitcoin::{bip32::{ChildNumber, Xpriv, Xpub}, key::{rand::{rngs::OsRng, RngCo use commitment::Commitment; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; -use zkcoins_program::{merkle::{hash_concat, sparse_merkle_tree::InclusionProof, HashDigest}, AccountState, Amount, Coin}; -use zkcoins_prover::Proof; +use zkcoins_program::{merkle::{hash_concat, HashDigest}, AccountState, Amount}; pub mod commitment; pub use zkcoins_program::ProofData; lazy_static! { pub static ref SECP256K1: Secp256k1 = Secp256k1::new(); - } pub type Address = HashDigest; -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct CoinProof { - pub proof: Proof, - pub coin: Coin, - pub inclusion_proof: InclusionProof, - pub commitment: Option, -} - #[derive(Serialize, Deserialize, Debug, Clone, Copy)] pub struct Invoice { pub amount: Amount, @@ -65,21 +55,16 @@ impl ClientAccount { .private_key } - pub fn sign_proof(&self, proof: &mut CoinProof) { - let proof_data = - bincode::deserialize::(&proof.proof.public_values.clone().to_vec()) - .expect("Proof is invalid"); - proof.commitment = Some( - Commitment::new( - &self.current_private_key(), - hash_concat( - &proof_data.account_state_hash, - &proof_data.output_coins_root, - ) - .to_vec(), + pub fn create_commitment(&self, account_state_hash: &HashDigest, output_coins_root: &HashDigest) -> Commitment { + Commitment::new( + &self.current_private_key(), + hash_concat( + account_state_hash, + output_coins_root, ) - .expect("Should be able to create commitment"), - ); + .to_vec(), + ) + .expect("Should be able to create commitment") } pub fn generate_public_key(&self, index: u32) -> PublicKey {