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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+ }
+}