diff --git a/Cargo.lock b/Cargo.lock index 72d9482..1aebfcb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1202,20 +1202,17 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" name = "client" version = "1.1.0" dependencies = [ - "anyhow", - "axum", "bincode", "bitcoin", - "bitcoin_hashes 0.16.0", - "esplora-client", + "getrandom 0.2.15", "hex", - "lazy_static", - "lib", "serde", - "sha2 0.10.8", - "tokio", - "zkcoins-program", - "zkcoins-prover", + "serde-wasm-bindgen", + "serde_json", + "shared", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] @@ -2358,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" @@ -2844,16 +2847,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" @@ -2956,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" @@ -4569,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" @@ -4648,6 +4662,27 @@ 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", + "serde", + "sha2 0.10.8", + "shared", + "tokio", + "tower-http", + "zkcoins-program", + "zkcoins-prover", +] + [[package]] name = "sha2" version = "0.10.8" @@ -4698,6 +4733,19 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shared" +version = "1.1.0" +dependencies = [ + "bincode", + "bitcoin", + "hex", + "lazy_static", + "serde", + "sha2 0.10.8", + "zkcoins-program", +] + [[package]] name = "shlex" version = "1.3.0" @@ -5729,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" @@ -5886,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" @@ -6528,7 +6607,7 @@ version = "0.1.0" dependencies = [ "bincode", "derive_builder", - "lib", + "lazy_static", "rand", "serde", "sha2 0.11.0-pre.3", @@ -6540,7 +6619,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 8238e8b..1226dc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,9 @@ members = [ "program", "script", - "client", - "lib" -] + "server", + "client", + "shared"] resolver = "2" [workspace.dependencies] @@ -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 debd89e..55a02da 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -3,22 +3,44 @@ name = "client" version.workspace = true edition.workspace = true +[lib] +crate-type = ["cdylib"] + +[features] +console_error_panic_hook = [] + [dependencies] -bitcoin = { workspace = true } -bitcoin_hashes = { version = "0.16.0", features = ["std"] } -sha2 = { workspace = true } -serde = { workspace = true } -bincode = { workspace = true } +wasm-bindgen = "0.2" +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" -tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "net", "time"] } -esplora-client = { git = "https://github.com/BitVM/rust-esplora-client", branch = "master" } -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 } +[dependencies.getrandom] +version = "0.2.15" +features = ["js"] -[features] -default = [] +[package.metadata.wasm-pack.profile.release] +wasm-opt = false diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..dd03df5 --- /dev/null +++ b/client/index.html @@ -0,0 +1,159 @@ + + + + + + zkCoins Wallet + + + +

zkCoins Wallet

+ +
+ +
+

Create New Account

+ +
+
+ + +
+

Send Coins

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

Debug Output

+

+            
+
+ + + + + diff --git a/client/readme.md b/client/readme.md new file mode 100644 index 0000000..150a2bb --- /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 +``` + diff --git a/client/src/lib.rs b/client/src/lib.rs new file mode 100644 index 0000000..865914d --- /dev/null +++ b/client/src/lib.rs @@ -0,0 +1,278 @@ +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] +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())) +} + +#[derive(Serialize, Deserialize)] +struct StorableClientAccountData { + address_hex: String, + num_pubkeys: u32, + xpriv_str: String, +} + +#[wasm_bindgen] +pub fn create_and_store_new_account() -> 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/client/src/wallet.rs b/client/src/wallet.rs deleted file mode 100644 index eab4e5d..0000000 --- a/client/src/wallet.rs +++ /dev/null @@ -1,587 +0,0 @@ -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 serde::{Deserialize, Serialize}; -use zkcoins_program::merkle::CommitmentMerkleProofs; -use zkcoins_program::{ - calculate_coin_identifier, hash, AccountState, Coin, CoinTemplate, 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; -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, - pub coin: Coin, - pub inclusion_proof: InclusionProof, - pub commitment: Commitment, -} - -#[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 } - } -} - -#[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, -} - -impl Account { - /// 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, - next_public_key: PublicKey, - coin_templates: Vec, - ) -> Result, String> { - let mut next_account_state = self.state.clone(); - next_account_state.balance = self.get_balance(); - for coin_template in &coin_templates { - // Apply all coins. - next_account_state.balance = if let Some(balance) = next_account_state.balance.checked_sub(coin_template.amount) { - balance - } else { - return Err("Balance too small to create Coin. This should have been checked beforehand and is a bug :(".to_owned()); - }; - } - - let next_account_state_hash = next_account_state.hash(); - let coins = coin_templates.into_iter().enumerate().map(|(i, template)| { - Coin::new( - template, - calculate_coin_identifier(next_account_state_hash, i as u32), - ) - }); - // Set the next public key. - next_account_state.public_key = next_public_key.serialize().to_vec(); - Ok(coins.collect()) - } - - pub fn get_balance(&self) -> Amount { - self.coin_queue - .iter() - .fold(self.state.balance, |acc, x| acc + x.coin.amount) - } -} - -#[derive(Serialize, Deserialize)] -pub struct Wallet { - accounts: HashMap, - #[serde(skip)] - prover: Prover, - master_private_key: PrivateKey, - state: Arc>, -} - -fn generate_public_key(private_key: &Xpriv, index: u32) -> Result { - // WARNING: LEAKING THE MASTER PUBLIC KEY IS EQUIVALENT TO LEAKING THE PRIVATE KEY! - if let Ok(x) = Xpub::from_priv(&SECP256K1, private_key).derive_pub(&SECP256K1, &[ChildNumber::Normal {index}]) { - Ok(x.public_key) - } else { - return Err("Failed to derive first pubkey".to_owned()); - } -} - -impl Wallet { - /// Get the keypair to the pubkey this account commited to (which is derived key num_pubkeys - - /// 1) - fn private_key(&self, account: &Account) -> Result { - if let Some(index) = account.num_pubkeys.checked_sub(1) { - if let Ok(x) = account.private_key.derive_priv(&SECP256K1, &[ChildNumber::Normal {index}]) { - Ok(x.private_key) - } else { - return Err("Unable to derive private key for account".to_owned()); - } - } else { - return Err("This account was never commited to.".to_owned()); - } - } - - pub fn new(state: Arc>) -> Result { - 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 { - accounts, - prover, - master_private_key: if let Ok(private_key) = PrivateKey::new_master(Network::Bitcoin, &seed) { - private_key - } else { - return Err("Failed to create private key.".to_owned()); - }, - state, - }; - wallet.create_account()?; - Ok(wallet) - } - - pub fn create_account(&mut self) -> Result { - let private_key = if let Ok(private_key) = self.master_private_key.derive_priv( - &SECP256K1, - &[ChildNumber::Hardened {index: self.accounts.len() as u32}] - ) { - private_key - } else { - return Err("Unable to derive a private key for the account".to_owned()); - }; - - let account = if let Ok(public_key) = generate_public_key(&private_key, 0) { - AccountState::new(public_key.serialize().to_vec()) - } else { - return Err("Unable to generate a public key for the account".to_owned()); - }; - - 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(), - }, - ); - Ok(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 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)), - _ => Err("No account with this address".to_string()), - } - } - - pub fn get_addresses(&self) -> Vec
{ - self.accounts.keys().cloned().collect::>() - } - - pub fn receive_coin(&mut self, coin_proof: CoinProof) -> Result<(), String> { - // Deserialze proof data - let proof_data = coin_proof.proof.public_values.clone().read::(); - - // 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. - assert!(coin_proof - .inclusion_proof - .verify(coin_proof.coin.identifier, proof_data.output_coins_root)); - - // Get the recipient account - let mut account = if let Some(account) = self.accounts.remove(&coin_proof.coin.recipient) { - account - } else { - return Err("No account in wallet is the recipient of the coin.".to_owned()); - }; - - // 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 - // same account state hash -> what is stored for the public key has to be the preimage to - // the coin identifier) - //let _ = self.prover.update_account( - // &account.state, - // &None, - // account.proof.clone(), - // vec![proof.clone()], - // // Note: account public_key is not updated when only receiving. - // &account.state.public_key, - //); - - // TODO: Make sure the coin_queue doesn't include this coin already. - account.coin_queue.push(coin_proof); - self.accounts.insert(account.state.owner, account); - Ok(()) - } - - /// Get all required merkle proofs from the state for the public key and the previous proof - pub fn get_merkle_proofs( - &self, - mut previous_proof: Proof, - public_key: PublicKey, - state: &MutexGuard<'_, State>, - ) -> Result { - let account_merkle_proofs = if let Some(commitment_proof) = state.get_commitment_proof(&public_key) { - commitment_proof - } else { - return Err("Unable to get merkle proofs for provided public key".to_owned()); - }; - - let proof_data = previous_proof.public_values.read::(); - let previous_root = proof_data.commitment_history_root; - let previous_root_proof = if let Some(mmr_inclusion_proof) = state.get_mmr_inclusion_proof(previous_root) { - mmr_inclusion_proof - } else { - return Err("Unable to get mmr inclusion proof for the previous root".to_owned()); - }; - - if hash_concat(&proof_data.account_state_hash, - &proof_data.output_coins_root) != account_merkle_proofs.0 { - return Err("Commitment is not hash(hash(account_state) || out_coins_root)".to_owned()); - } - - let proofs = CommitmentMerkleProofs { - commitment_root: account_merkle_proofs.2, - commitment_proof: account_merkle_proofs.1, - commitment_root_history_proof: account_merkle_proofs.3, - commitment_root_mmr_sibling: state.prev_mmr_root, - previous_root_history_proof: previous_root_proof, - commitment_account_state_hash: proof_data.account_state_hash, - commitment_out_coins_root: proof_data.output_coins_root, - }; - - if !proofs.verify_previous_root(previous_root, state.mmr.root()) { - return Err("Previous root history proof verification failed.".to_owned()); - } - Ok(proofs) - } - - pub fn send_coins( - &mut self, - invoices: Vec, - account_address: &Address, - ) -> Result, String> { - let state = &self.state.lock().unwrap(); - // Check if the account balance is enough - 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()); - } - - let mut account = self - .accounts - .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)?; - - // Create the coin templates. - let mut coin_templates = vec![]; - for invoice in invoices { - coin_templates.push(CoinTemplate::new(invoice.recipient, invoice.amount)); - } - - let mut coin_history_proofs = vec![]; - let mut coin_non_inclusion_proofs = vec![]; - 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.public_key.clone(), - state, - )?); - let key = coin_proof.coin.identifier; - coin_non_inclusion_proofs.push({ - if let Ok(non_inclusion_proof) = account.coin_history.generate_non_inclusion_proof(key) { - non_inclusion_proof - } else { - return Err("Should provide an inclusion proof".to_owned()); - } - }); - coin_inclusion_proofs.push(coin_proof.inclusion_proof.clone()); - in_coins.push(coin_proof.coin.clone()); - if let Err(_) = account.coin_history.insert(key, key) { - return Err("Coin should not exist in coin history tree".to_owned()); - } - } - 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()) - // 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.) - .in_coins(in_coins) - .in_coins_inclusion_proofs(coin_inclusion_proofs) - .in_coin_proofs_history_proofs(coin_history_proofs) - .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 mut out_coins_tree = SparseMerkleTree::new(); - let mut current_root = DEFAULT_HASHES[0]; - if current_root != out_coins_tree.root() { - return Err("Empty tree has an unexpected root.".to_owned()); - } - - let mut out_coin_proofs = vec![]; - for coin in &out_coins { - let non_inclusion_proof = if let Ok(non_inclusion_proof) = out_coins_tree.generate_non_inclusion_proof(coin.identifier) { - non_inclusion_proof - } else { - return Err("Coin should not exist in tree yet".to_owned()); - }; - out_coin_proofs.push(non_inclusion_proof.clone()); - out_coins_tree - .insert(coin.identifier, coin.identifier)?; - current_root = non_inclusion_proof.insert(coin.identifier); - if current_root != out_coins_tree.root() { - return Err("Roots deviate after inserting manually and updating with non_inclusion_proof".to_owned()); - } - } - - let proof_hints_builder = proof_hints_builder - .out_coins(out_coins.clone()) - .out_coin_proofs(out_coin_proofs); - - // TODO(Refactor): Don't use take and move this up into the loop - let received_proofs = take(&mut account.coin_queue) - .into_iter() - .map(|x| x.proof) - .collect(); - - let proof = match account.proof.take() { - Some(account_proof) => { - let account_commitment_public_key = if let Some(index) = account.num_pubkeys.checked_sub(1) { - generate_public_key(&account.private_key, index)? - } else { - return Err("account num_pubkey is 0 although there is a previous proof. This is a bug.".to_owned()); - }; - proof_hints_builder.prev_proof_history_proofs(Some(self.get_merkle_proofs( - account_proof.clone(), - account_commitment_public_key, - state, - )?)); - proof_hints_builder.proof_type(ProofType::AccountUpdateProof); - self.prover.update_account(proof_hints_builder, account_proof, received_proofs)? - } - _ => { - self.prover.create_account(proof_hints_builder, received_proofs)? - } - }; - - // Update account. - account.state.balance = balance - invoiced_amount; - account.state.public_key = next_public_key.serialize().to_vec(); - account.num_pubkeys += 1; - account.proof = Some(proof.clone()); - - let private_key = self.private_key(&account)?; - let commitment = if let Ok(commitment) = Commitment::new( - &private_key, - hash_concat(&account.state.hash(), &out_coins_tree.root()).to_vec() - ) { - commitment - } else { - return Err("Unable to create commitment".to_owned()); - }; - - // Insert account back into wallet. - self.accounts.insert(*account_address, account); - let public_values = bincode::deserialize::(&proof.public_values.to_vec()).unwrap(); - if public_values.output_coins_root != out_coins_tree.root() { - return Err("The simulated out_coins_tree root does not match the commited output_coins_root".to_owned()); - } - - // Create the coin_proofs to be distributed to recipients - let mut coin_proofs = vec![]; - for coin in out_coins { - coin_proofs.push(CoinProof { - proof: proof.clone(), - inclusion_proof: out_coins_tree.generate_inclusion_proof(&coin.identifier)?.0, - coin, - commitment: commitment.clone(), - }); - } - - Ok(coin_proofs) - } - - pub fn get_minting_account_address(&mut self) -> Result { - let seed = include_bytes!("../minting_secret.bin"); - //rng.fill_bytes(&mut seed); - - let private_key = if let Ok(private_key) = PrivateKey::new_master(Network::Bitcoin, seed) { - private_key - } else { - return Err("Failed to create private key.".to_owned()); - }; - let minting_account_address = hash(private_key.to_string().as_bytes()); - if self.accounts.get(&minting_account_address).is_some() { - return Ok(minting_account_address); - } - - 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); - Ok(minting_account_address) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::state::State; - use tokio::time::Instant; - use zkcoins_program::MINTING_ADDRESS; - - #[test] - fn test_wallet_operations() { - let state = Arc::new(Mutex::new(State::new())); - let mut wallet = Wallet::new(state).unwrap(); - assert_eq!( - MINTING_ADDRESS, wallet.get_minting_account_address().unwrap(), - "Minting address in wallet and program are different" - ); - let account_2 = wallet.create_account().unwrap(); - let account_1 = wallet.create_account().unwrap(); - - assert_eq!( - wallet.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); - - let account_2_invoice = Invoice::new(100, account_2); - let account_1_invoice = Invoice::new(100, account_1); - - 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( - &coin_proofs - .iter() - .map(|x| x.commitment.clone()) - .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); - } - - #[test] - fn test_create_minting_account() { - let state = Arc::new(Mutex::new(State::new())); - let mut wallet = Wallet::new(state).unwrap(); - assert_eq!( - wallet.get_minting_account_address().unwrap(), - MINTING_ADDRESS, - "Minting address in program not the same as in wallet" - ); - assert_eq!( - wallet.get_account_balance(&MINTING_ADDRESS).unwrap(), - 10_000 - ); - } -} 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 abc6ff3..241cb14 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -1,16 +1,72 @@ -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::{ - sparse_merkle_tree::{InclusionProof, NonInclusionProof, DEFAULT_HASHES}, - Amount, HashDigest, PublicKey, +use merkle::{ + 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 +#[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 { @@ -83,6 +139,8 @@ pub struct AccountState { impl AccountState { 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. // 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/program/src/main.rs b/program/src/main.rs index 986a4e7..96beb91 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 { @@ -120,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.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 98% rename from lib/src/merkle_mountain_range.rs rename to program/src/merkle/merkle_mountain_range.rs index 0e92c68..c43854e 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)] @@ -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. /// @@ -207,6 +213,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 91% rename from lib/src/lib.rs rename to program/src/merkle/mod.rs index 82367c0..03b2e55 100644 --- a/lib/src/lib.rs +++ b/program/src/merkle/mod.rs @@ -1,4 +1,5 @@ use sha2::{Digest, Sha256}; + pub mod merkle_mountain_range; pub mod sparse_merkle_tree; @@ -7,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/lib/src/sparse_merkle_tree.rs b/program/src/merkle/sparse_merkle_tree.rs similarity index 98% rename from lib/src/sparse_merkle_tree.rs rename to program/src/merkle/sparse_merkle_tree.rs index 7da6cef..f1d17ed 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; @@ -48,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()); @@ -98,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) @@ -146,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) @@ -233,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 { @@ -309,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], @@ -428,7 +435,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 new file mode 100644 index 0000000..bb8d2ec --- /dev/null +++ b/server/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "server" +version.workspace = true +edition.workspace = true + +[dependencies] +bitcoin = { workspace = true } +bitcoin_hashes = { version = "0.16.0", features = ["std"] } +sha2 = { workspace = true } +serde = { workspace = true } +bincode = { workspace = true } +hex = "0.4.3" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "net", "time"] } +esplora-client = { git = "https://github.com/BitVM/rust-esplora-client", branch = "master" } +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 } +tower-http = { version = "0.5", features = ["fs"] } + + +[features] +default = [] 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/server/src/account_server.rs b/server/src/account_server.rs new file mode 100644 index 0000000..c39b5d4 --- /dev/null +++ b/server/src/account_server.rs @@ -0,0 +1,614 @@ +use std::sync::{Arc, Mutex, MutexGuard}; +use std::{collections::HashMap, mem::take}; + +use bitcoin::secp256k1::PublicKey; +use serde::{Deserialize, Serialize}; +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::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, + 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: zkcoins_program::PublicKey, + coin_templates: Vec, + ) -> Result, String> { + 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 = if let Some(balance) = next_account_state.balance.checked_sub(coin_template.amount) { + balance + } else { + return Err("Balance too small to create Coin. This should have been checked beforehand and is a bug :(".to_owned()); + }; + } + + let next_account_state_hash = next_account_state.hash(); + let coins = coin_templates.into_iter().enumerate().map(|(i, template)| { + Coin::new( + template, + calculate_coin_identifier(next_account_state_hash, i as u32), + ) + }); + // Set the next public key. + next_account_state.public_key = next_public_key.serialize().to_vec(); + Ok(coins.collect()) + } + + pub fn get_balance(&self) -> Amount { + self.coin_queue + .iter() + .fold(self.balance, |acc, x| acc + x.coin.amount) + } +} + +pub struct AccountServer { + accounts: HashMap, + prover: Prover, + state: Arc>, +} + +impl AccountServer { + // TODO: Move to client. + /// Get the keypair to the pubkey this account commited to (which is derived key num_pubkeys - + /// 1) + + pub fn new(state: Arc>) -> Self { + let accounts = HashMap::new(); + let prover = Prover::new(); + + AccountServer { + accounts, + prover, + state, + } + } + + 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.balance, |acc, x| acc + x.coin.amount)), + _ => Err("No account with this address".to_string()), + } + } + + pub fn get_addresses(&self) -> Vec
{ + self.accounts.keys().cloned().collect::>() + } + + pub fn receive_coin(&mut self, coin_proof: CoinProof) -> Result<(), String> { + // Deserialze proof data + let proof_data = coin_proof.proof.public_values.clone().read::(); + + // Verify the inclusion of the coin in the proof. + // TODO: Return an err and also verify the proof verification itself. (Dry-run the + // aggregation) + // TODO: Verify that the commitment was not included in our state. + coin_proof.inclusion_proof.verify(coin_proof.coin.identifier, proof_data.output_coins_root)?; + + // Get the recipient account + let mut account = self + .accounts + .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 + // same account state hash -> what is stored for the public key has to be the preimage to + // the coin identifier) + //let _ = self.prover.update_account( + // &account.state, + // &None, + // account.proof.clone(), + // vec![proof.clone()], + // // Note: account public_key is not updated when only receiving. + // &account.state.public_key, + //); + + // TODO: Make sure the coin_queue doesn't include this coin already. + let address = coin_proof.coin.recipient; + account.coin_queue.push(coin_proof); + self.accounts.insert(address, account); + Ok(()) + } + + /// Get all required merkle proofs from the state for the public key and the previous proof + pub fn get_merkle_proofs( + &self, + mut previous_proof: Proof, + public_key: PublicKey, + state: &MutexGuard<'_, State>, + ) -> Result { + let account_merkle_proofs = if let Some(commitment_proof) = state.get_commitment_proof(&public_key) { + commitment_proof + } else { + return Err("Unable to get merkle proofs for provided public key".to_owned()); + }; + + let proof_data = previous_proof.public_values.read::(); + let previous_root = proof_data.commitment_history_root; + let previous_root_proof = if let Some(mmr_inclusion_proof) = state.get_mmr_inclusion_proof(previous_root) { + mmr_inclusion_proof + } else { + return Err("Unable to get mmr inclusion proof for the previous root".to_owned()); + }; + + if hash_concat(&proof_data.account_state_hash, + &proof_data.output_coins_root) != account_merkle_proofs.0 { + return Err("Commitment is not hash(hash(account_state) || out_coins_root)".to_owned()); + } + + let proofs = CommitmentMerkleProofs { + commitment_root: account_merkle_proofs.2, + commitment_proof: account_merkle_proofs.1, + commitment_root_history_proof: account_merkle_proofs.3, + commitment_root_mmr_sibling: state.prev_mmr_root, + previous_root_history_proof: previous_root_proof, + commitment_account_state_hash: proof_data.account_state_hash, + commitment_out_coins_root: proof_data.output_coins_root, + }; + + if !proofs.verify_previous_root(previous_root, state.mmr.root()) { + return Err("Previous root history proof verification failed.".to_owned()); + } + Ok(proofs) + } + + pub fn send_coins( + &mut self, + invoices: Vec, + 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 invoiced_amount = invoices.iter().fold(0, |acc, x| acc + x.amount); + if balance < invoiced_amount { + return Err("Insufficient funds".into()); + } + + let mut account = self + .accounts + .remove(&account_address) + .unwrap_or_else(|| unreachable!()); + + // 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 { + coin_templates.push(CoinTemplate::new(invoice.recipient, invoice.amount)); + } + + let mut coin_history_proofs = vec![]; + let mut coin_non_inclusion_proofs = vec![]; + let mut coin_inclusion_proofs = vec![]; + let mut in_coins = vec![]; + for coin_proof in &account.coin_queue { + coin_history_proofs.push({ + if let Some(commitment) = &coin_proof.commitment { + self.get_merkle_proofs( + coin_proof.proof.clone(), + commitment.public_key, + state, + )? + } else { + return Err("Coin is missing commitment".to_owned()); + } + }); + let key = coin_proof.coin.identifier; + coin_non_inclusion_proofs.push({ + if let Ok(non_inclusion_proof) = account.coin_history.generate_non_inclusion_proof(key) { + non_inclusion_proof + } else { + return Err("Should provide an inclusion proof".to_owned()); + } + }); + coin_inclusion_proofs.push(coin_proof.inclusion_proof.clone()); + in_coins.push(coin_proof.coin.clone()); + if let Err(_) = account.coin_history.insert(key, key) { + return Err("Coin should not exist in coin history tree".to_owned()); + } + } + let mut proof_hints_builder = ProgramInputsBuilder::default(); + let proof_hints_builder = proof_hints_builder + .account_state(AccountState { + owner: account_address, + balance: account.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.) + .in_coins(in_coins) + .in_coins_inclusion_proofs(coin_inclusion_proofs) + .in_coin_proofs_history_proofs(coin_history_proofs) + .in_coin_proofs_non_inclusion_proofs(coin_non_inclusion_proofs) + .current_history_root(state.mmr.root()); + + let out_coins = account.create_coins( + account_address, + next_public_key, + public_key.serialize().to_vec(), + coin_templates, + )?; + let mut out_coins_tree = SparseMerkleTree::new(); + let mut current_root = DEFAULT_HASHES[0]; + if current_root != out_coins_tree.root() { + return Err("Empty tree has an unexpected root.".to_owned()); + } + + let mut out_coin_proofs = vec![]; + for coin in &out_coins { + let non_inclusion_proof = if let Ok(non_inclusion_proof) = out_coins_tree.generate_non_inclusion_proof(coin.identifier) { + non_inclusion_proof + } else { + return Err("Coin should not exist in tree yet".to_owned()); + }; + out_coin_proofs.push(non_inclusion_proof.clone()); + out_coins_tree.insert(coin.identifier, coin.identifier)?; + current_root = non_inclusion_proof.insert(coin.identifier); + if current_root != out_coins_tree.root() { + return Err("Roots deviate after inserting manually and updating with non_inclusion_proof".to_owned()); + } + } + + let proof_hints_builder = proof_hints_builder + .out_coins(out_coins.clone()) + .out_coin_proofs(out_coin_proofs); + + // TODO(Refactor): Don't use take and move this up into the loop + let received_proofs = take(&mut account.coin_queue) + .into_iter() + .map(|x| x.proof) + .collect(); + + let proof = match account.proof.take() { + Some(account_proof) => { + 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, + state, + )?)); + proof_hints_builder.proof_type(ProofType::AccountUpdateProof); + self.prover.update_account(proof_hints_builder, account_proof, received_proofs)? + } + _ => { + self.prover.create_account(proof_hints_builder, received_proofs)? + } + }; + + // Update account. + account.balance = balance - invoiced_amount; + account.proof = Some(proof.clone()); + + // Insert account back into database. + self.accounts.insert(account_address, account); + let public_values = bincode::deserialize::(&proof.public_values.to_vec()).unwrap(); + if public_values.output_coins_root != out_coins_tree.root() { + return Err("The simulated out_coins_tree root does not match the commited output_coins_root".to_owned()); + } + + // Create the coin_proofs to be distributed to recipients + let mut coin_proofs = vec![]; + for coin in out_coins { + coin_proofs.push(CoinProof { + proof: proof.clone(), + inclusion_proof: out_coins_tree.generate_inclusion_proof(&coin.identifier)?.0, + coin, + // 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) -> Result { + if self + .accounts + .get(&zkcoins_program::MINTING_ADDRESS) + .is_some() + { + Ok(zkcoins_program::MINTING_ADDRESS) + } else { + Err("Minting account not creaputed".into()) + } + } +} + +#[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 + ); + } +} diff --git a/client/src/main.rs b/server/src/main.rs similarity index 90% rename from client/src/main.rs rename to server/src/main.rs index e2822c2..38d39f3 100644 --- a/client/src/main.rs +++ b/server/src/main.rs @@ -1,11 +1,10 @@ -mod commitment; mod publisher; mod scanner; mod server; -mod wallet; +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; @@ -21,7 +20,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 +71,16 @@ 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)).unwrap(); + // Create a new AccountServer instance with a reference to the state + let account_server = account_server::AccountServer::new(Arc::clone(&state)); - // Call create_account without arguments - wallet.create_account().unwrap(); + // 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/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 65% rename from client/src/server.rs rename to server/src/server.rs index 36ee5c3..6551d43 100644 --- a/client/src/server.rs +++ b/server/src/server.rs @@ -1,24 +1,34 @@ -use axum::body::Bytes; use axum::{ + body::Bytes, extract::{Json, Path, State}, http::{header, StatusCode}, response::IntoResponse, routing::{get, post}, Router, }; +use bitcoin::{bip32::Xpriv, Network}; use serde::{Deserialize, Serialize}; +use shared::{ClientAccount, Invoice}; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use tokio::net::TcpListener; +use zkcoins_program::hash; use zkcoins_prover::Proof; -use crate::commitment::Commitment; +use crate::account_server::{AccountServer, CoinProof}; use crate::publisher::create_and_broadcast_inscription; -use crate::wallet::{CoinProof, Invoice, Wallet}; 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 { @@ -30,11 +40,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,10 +96,10 @@ pub struct SendCoinResponse { // Handler functions for our REST API async fn get_balance_handler( - State((wallet, _)): State<(Arc>, Arc)>, + State(state): State, axum::extract::Query(params): axum::extract::Query>, ) -> impl IntoResponse { - let wallet = wallet.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") { @@ -113,28 +126,20 @@ 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 })), } } else { - // If no address is provided, return total wallet balance (for backward compatibility) - ( - StatusCode::OK, - Json(BalanceResponse { - balance: wallet.get_balance(), - }), - ) + (StatusCode::NOT_FOUND, Json(BalanceResponse { balance: 0 })) } } -async fn get_address_handler( - State((wallet, _)): State<(Arc>, Arc)>, -) -> impl IntoResponse { - let wallet = wallet.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 = wallet + let hex_addresses: Vec = account_server .get_addresses() .iter() .map(|addr| format!("0x{}", hex::encode(addr))) @@ -146,16 +151,14 @@ async fn get_address_handler( } async fn receive_coin_handler( - State((wallet, _)): 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 wallet = wallet.lock().unwrap(); - match wallet.receive_coin( - coin_proof - ) { + let mut account_server = state.account_server.lock().unwrap(); + match account_server.receive_coin(coin_proof) { Ok(_) => Json(SendCoinResponse { success: true, proof_id: None, @@ -177,7 +180,7 @@ async fn receive_coin_handler( } async fn send_coin_handler( - State((wallet, proof_store)): State<(Arc>, Arc)>, + State(state): State, Json(request): Json, ) -> impl IntoResponse { // Create converted addresses (from_address and to_address) @@ -222,17 +225,23 @@ async fn send_coin_handler( ); } - // Acquire the wallet lock only for the duration of sending coins. + // 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 wallet_guard = wallet.lock().unwrap(); - wallet_guard.send_coins(vec![Invoice::new(request.amount, to_address)], &from_address) + let mut account_server_lock = state.account_server.lock().unwrap(); + account_server_lock.send_coins( + vec![Invoice::new(request.amount, to_address)], + from_address, + request.public_key, + request.next_public_key, + ) }; - // 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).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,9 +254,9 @@ 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()); + let proof_id = state.proof_store.add_proof(coin_proofs.pop().unwrap()); ( StatusCode::OK, Json(SendCoinResponse { @@ -266,11 +275,8 @@ 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(state): State, Json(request): Json, ) -> impl IntoResponse { let account_address_vec = match hex::decode(request.account_address.trim_start_matches("0x")) { @@ -285,7 +291,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); @@ -299,18 +305,50 @@ async fn mint_handler( ); } - // Acquire the wallet lock only for the duration of Minting coins. + // 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 = state.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 account_server lock only for the duration of sending 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(vec![Invoice::new(request.amount, account_address)], minting_address) + 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)], + minting_address, + 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) => { - let commitment_data = - bincode::serialize(&coin_proofs[0].commitment).expect("Failed to serialize commitment"); + // Increment num_pubkeys *after* successful send and before await + { + 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; + } 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"); println!( "Sending commitment data with size: {} bytes", @@ -318,13 +356,14 @@ 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 { 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 { @@ -345,10 +384,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(); @@ -375,38 +414,58 @@ async fn get_proof_handler( } // 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(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 minting_account = { + 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 + ); + Arc::new(Mutex::new(ClientAccount { + address: hash(private_key.to_string().as_bytes()), + num_pubkeys: 0, + private_key, + })) + }; + + // 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() .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); // 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 - 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?; @@ -416,7 +475,7 @@ pub async fn start_rest_server(wallet: Wallet, addr: &str) -> anyhow::Result<()> // 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) => { @@ -435,3 +494,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 96% rename from client/src/state.rs rename to server/src/state.rs index f4e2aa2..479aba8 100644 --- a/client/src/state.rs +++ b/server/src/state.rs @@ -1,13 +1,11 @@ -use crate::commitment::Commitment; +use shared::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,14 +143,14 @@ 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)?; // 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(()) } @@ -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 @@ -295,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()); diff --git a/shared/Cargo.toml b/shared/Cargo.toml new file mode 100644 index 0000000..4d5b830 --- /dev/null +++ b/shared/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "shared" +version.workspace = true +edition.workspace = true + +[dependencies] +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/client/src/commitment.rs b/shared/src/commitment.rs similarity index 95% rename from client/src/commitment.rs rename to shared/src/commitment.rs index d95d8cf..5255512 100644 --- a/client/src/commitment.rs +++ b/shared/src/commitment.rs @@ -1,12 +1,13 @@ -use crate::wallet::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}; 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 { @@ -34,7 +35,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(), diff --git a/shared/src/lib.rs b/shared/src/lib.rs new file mode 100644 index 0000000..2b5d906 --- /dev/null +++ b/shared/src/lib.rs @@ -0,0 +1,92 @@ +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, 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, 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 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, + ) + .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 + } +}