diff --git a/Cargo.toml b/Cargo.toml index 092ab44..0937a4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,46 +1,9 @@ -[package] -name = "zkbitcoin" -version = "0.1.0" -edition = "2021" +[workspace] +members = [ + "crates/*", +] -[dependencies] -anyhow = "1.0.75" -base64 = "0.21.5" -bitcoin = { version = "0.31.0", features = [ - "serde", - "bitcoinconsensus", -], git = "https://github.com/mimoo/rust-bitcoin/", branch = "mimoo/fix_0_31" } -bitcoincore-rpc = "0.18" -clap = { version = "4.4.10", features = ["derive", "env"] } -env_logger = "0.10.1" -frost-secp256k1-tr = { git = "https://github.com/mimoo/frost", branch = "mimoo/fix5" } -futures = "0.3.30" -hex = "0.4.3" -home = "0.5.9" -itertools = "0.12.0" -jsonrpsee = { version = "0.21.0", features = ["server"] } -jsonrpsee-core = "0.21.0" -jsonrpsee-http-server = "0.15.1" -jsonrpsee-types = "0.21.0" -log = "0.4.20" -num-bigint = "0.4.4" -num-traits = "0.2.17" -rand = "0.8.5" -rand_chacha = "0.3.1" -reqwest = { version = "0.11", features = ["stream"] } -secp256k1 = "0.28.0" -serde = { version = "1.0", features = ["derive"] } -serde_json = { version = "1.0" } -sha3 = "0.10.8" -sha256 = "1.4.0" -tempdir = "0.3.7" -tokio = { version = "1.34", features = [ - "fs", - "rt", - "rt-multi-thread", - "macros", -] } -tokio-stream = "0.1.14" +resolver = "2" [patch.crates-io] # see docs/serialization.md diff --git a/crates/zkbtc-admin/Cargo.toml b/crates/zkbtc-admin/Cargo.toml new file mode 100644 index 0000000..a323e15 --- /dev/null +++ b/crates/zkbtc-admin/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "zkbtc-admin" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.75" +bitcoin = { version = "0.31.0", features = [ + "serde", + "bitcoinconsensus", +], git = "https://github.com/mimoo/rust-bitcoin/", branch = "mimoo/fix_0_31" } +clap = { version = "4.4.10", features = ["derive", "env"] } +env_logger = "0.10.1" +hex = "0.4.3" +log = "0.4.20" +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0" } +tempdir = "0.3.7" +tokio = { version = "1.34", features = [ + "fs", + "rt", + "rt-multi-thread", + "macros", +] } +zkbitcoin-core = { path = "../zkbtc-core"} diff --git a/crates/zkbtc-admin/src/cli/mod.rs b/crates/zkbtc-admin/src/cli/mod.rs new file mode 100644 index 0000000..9733c92 --- /dev/null +++ b/crates/zkbtc-admin/src/cli/mod.rs @@ -0,0 +1,54 @@ +use clap::{Parser, Subcommand}; +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +pub struct Cli { + #[command(subcommand)] + pub command: Commands, +} + +#[derive(Subcommand)] +pub enum Commands { + /// Generates an MPC committee via a trusted dealer. + /// Ideally this is just used for testing as it is more secure to do a DKG. + GenerateCommittee { + /// Number of nodes in the committee. + #[arg(short, long)] + num: u16, + + /// Minimum number of committee member required for a signature. + #[arg(short, long)] + threshold: u16, + + /// Output directory to write the committee configuration files to. + #[arg(short, long)] + output_dir: String, + }, + + /// Starts an MPC node given a configuration + StartCommitteeNode { + /// The address to run the node on. + #[arg(short, long)] + address: Option, + + /// The path to the node's key package. + #[arg(short, long)] + key_path: String, + + /// The path to the MPC committee public key package. + #[arg(short, long)] + publickey_package_path: String, + }, + + /// Starts an orchestrator + StartOrchestrator { + /// The address to run the node on. + #[arg(short, long)] + address: Option, + + #[arg(short, long)] + publickey_package_path: String, + + #[arg(short, long)] + committee_cfg_path: String, + }, +} diff --git a/crates/zkbtc-admin/src/main.rs b/crates/zkbtc-admin/src/main.rs new file mode 100644 index 0000000..dfe4558 --- /dev/null +++ b/crates/zkbtc-admin/src/main.rs @@ -0,0 +1,159 @@ +mod cli; + +use crate::cli::*; +use anyhow::Result; +use clap::Parser; +use log::info; +use std::path::PathBuf; +use zkbitcoin_core::{ + committee::orchestrator::{run_server, CommitteeConfig, Member}, + constants::{ZKBITCOIN_FEE_PUBKEY, ZKBITCOIN_PUBKEY}, + frost, taproot_addr_from, +}; + +#[tokio::main] +async fn main() -> Result<()> { + // init default log level to info (unless RUST_LOG is set) + env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); + + // debug info + info!( + "- zkbitcoin_address: {}", + taproot_addr_from(ZKBITCOIN_PUBKEY).unwrap().to_string() + ); + info!( + "- zkbitcoin_fund_address: {}", + taproot_addr_from(ZKBITCOIN_FEE_PUBKEY).unwrap().to_string() + ); + + // parse CLI + let cli = Cli::parse(); + match &cli.command { + Commands::GenerateCommittee { + num, + threshold, + output_dir, + } => { + let output_dir = PathBuf::from(output_dir); + + // deal until we get a public key starting with 0x02 + let (mut key_packages, mut pubkey_package) = + frost::gen_frost_keys(*num, *threshold).unwrap(); + let mut pubkey = pubkey_package.verifying_key().to_owned(); + loop { + if pubkey.serialize()[0] == 2 { + break; + } + (key_packages, pubkey_package) = frost::gen_frost_keys(*num, *threshold).unwrap(); + pubkey = pubkey_package.verifying_key().to_owned(); + } + + // all key packages + { + for (id, key_package) in key_packages.values().enumerate() { + let filename = format!("key-{id}.json"); + + let path = output_dir.join(filename); + std::fs::create_dir_all(path.clone().parent().unwrap()) + .expect("Couldn't create directory"); + let file = std::fs::File::create(&path) + .expect("couldn't create file given output dir"); + serde_json::to_writer_pretty(file, key_package).unwrap(); + } + } + + // public key package + { + let path = output_dir.join("publickey-package.json"); + let file = + std::fs::File::create(&path).expect("couldn't create file given output dir"); + serde_json::to_writer_pretty(file, &pubkey_package).unwrap(); + } + + // create the committee-cfg.json file + { + let ip = "http://127.0.0.1:889"; + let committee_cfg = CommitteeConfig { + threshold: *threshold as usize, + members: key_packages + .iter() + .enumerate() + .map(|(id, (member_id, _))| { + ( + *member_id, + Member { + address: format!("{}{}", ip, id), + }, + ) + }) + .collect(), + }; + let path = output_dir.join("committee-cfg.json"); + let file = + std::fs::File::create(&path).expect("couldn't create file given output dir"); + serde_json::to_writer_pretty(file, &committee_cfg).unwrap(); + } + } + + Commands::StartCommitteeNode { + address, + key_path, + publickey_package_path, + } => { + let key_package = { + let full_path = PathBuf::from(key_path); + let file = std::fs::File::open(full_path).expect("file not found"); + let key: frost::KeyPackage = + serde_json::from_reader(file).expect("error while reading file"); + key + }; + + let pubkey_package = { + let full_path = PathBuf::from(publickey_package_path); + let file = std::fs::File::open(full_path).expect("file not found"); + let publickey_package: frost::PublicKeyPackage = + serde_json::from_reader(file).expect("error while reading file"); + publickey_package + }; + + zkbitcoin_core::committee::node::run_server( + address.as_deref(), + key_package, + pubkey_package, + ) + .await + .unwrap(); + } + + Commands::StartOrchestrator { + address, + publickey_package_path, + committee_cfg_path, + } => { + let pubkey_package = { + let full_path = PathBuf::from(publickey_package_path); + let file = std::fs::File::open(full_path).expect("file not found"); + let publickey_package: frost::PublicKeyPackage = + serde_json::from_reader(file).expect("error while reading file"); + publickey_package + }; + + let committee_cfg = { + let full_path = PathBuf::from(committee_cfg_path); + let file = std::fs::File::open(full_path).expect("file not found"); + let publickey_package: CommitteeConfig = + serde_json::from_reader(file).expect("error while reading file"); + publickey_package + }; + + // sanity check (unfortunately the publickey_package doesn't contain this info) + assert!(committee_cfg.threshold > 0); + + run_server(address.as_deref(), pubkey_package, committee_cfg) + .await + .unwrap(); + } + } + + Ok(()) +} diff --git a/crates/zkbtc-core/Cargo.toml b/crates/zkbtc-core/Cargo.toml new file mode 100644 index 0000000..bd9fbe1 --- /dev/null +++ b/crates/zkbtc-core/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "zkbitcoin-core" +version = "0.1.0" +edition = "2021" + + +[dependencies] +anyhow = "1.0.75" +base64 = "0.21.5" +bitcoin = { version = "0.31.0", features = [ + "serde", + "bitcoinconsensus", +], git = "https://github.com/mimoo/rust-bitcoin/", branch = "mimoo/fix_0_31" } +bitcoincore-rpc = "0.18" +env_logger = "0.10.1" +frost-secp256k1-tr = { git = "https://github.com/mimoo/frost", branch = "mimoo/fix5" } +futures = "0.3.30" +hex = "0.4.3" +home = "0.5.9" +itertools = "0.12.0" +jsonrpsee = { version = "0.21.0", features = ["server"] } +jsonrpsee-core = "0.21.0" +jsonrpsee-http-server = "0.15.1" +jsonrpsee-types = "0.21.0" +log = "0.4.20" +num-bigint = "0.4.4" +num-traits = "0.2.17" +rand = "0.8.5" +rand_chacha = "0.3.1" +reqwest = { version = "0.11", features = ["stream"] } +secp256k1 = "0.28.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0" } +sha3 = "0.10.8" +sha256 = "1.4.0" +tempdir = "0.3.7" +tokio = { version = "1.34", features = [ + "fs", + "rt", + "rt-multi-thread", + "macros", +] } +tokio-stream = "0.1.14" diff --git a/src/alice_sign_tx.rs b/crates/zkbtc-core/src/alice_sign_tx.rs similarity index 100% rename from src/alice_sign_tx.rs rename to crates/zkbtc-core/src/alice_sign_tx.rs diff --git a/src/bob_request.rs b/crates/zkbtc-core/src/bob_request.rs similarity index 99% rename from src/bob_request.rs rename to crates/zkbtc-core/src/bob_request.rs index 16d26b3..7208e19 100644 --- a/src/bob_request.rs +++ b/crates/zkbtc-core/src/bob_request.rs @@ -363,7 +363,7 @@ impl BobRequest { .input .iter() .find(|tx| tx.previous_output.txid == self.zkapp_tx.txid()) - .context("the transaction ID that was passed in the request does not exist")? + .context("the zkapp transaction ID could not be found in the transaction's inputs")? .previous_output; Ok(outpoint) diff --git a/src/committee/mod.rs b/crates/zkbtc-core/src/committee/mod.rs similarity index 100% rename from src/committee/mod.rs rename to crates/zkbtc-core/src/committee/mod.rs diff --git a/src/committee/node.rs b/crates/zkbtc-core/src/committee/node.rs similarity index 100% rename from src/committee/node.rs rename to crates/zkbtc-core/src/committee/node.rs diff --git a/src/committee/orchestrator.rs b/crates/zkbtc-core/src/committee/orchestrator.rs similarity index 100% rename from src/committee/orchestrator.rs rename to crates/zkbtc-core/src/committee/orchestrator.rs diff --git a/src/constants.rs b/crates/zkbtc-core/src/constants.rs similarity index 100% rename from src/constants.rs rename to crates/zkbtc-core/src/constants.rs diff --git a/src/frost.rs b/crates/zkbtc-core/src/frost.rs similarity index 100% rename from src/frost.rs rename to crates/zkbtc-core/src/frost.rs diff --git a/src/json_rpc_stuff.rs b/crates/zkbtc-core/src/json_rpc_stuff.rs similarity index 100% rename from src/json_rpc_stuff.rs rename to crates/zkbtc-core/src/json_rpc_stuff.rs diff --git a/src/lib.rs b/crates/zkbtc-core/src/lib.rs similarity index 100% rename from src/lib.rs rename to crates/zkbtc-core/src/lib.rs diff --git a/src/mpc_sign_tx.rs b/crates/zkbtc-core/src/mpc_sign_tx.rs similarity index 100% rename from src/mpc_sign_tx.rs rename to crates/zkbtc-core/src/mpc_sign_tx.rs diff --git a/src/plonk.rs b/crates/zkbtc-core/src/plonk.rs similarity index 100% rename from src/plonk.rs rename to crates/zkbtc-core/src/plonk.rs diff --git a/src/snarkjs.rs b/crates/zkbtc-core/src/snarkjs.rs similarity index 100% rename from src/snarkjs.rs rename to crates/zkbtc-core/src/snarkjs.rs diff --git a/src/srs.rs b/crates/zkbtc-core/src/srs.rs similarity index 100% rename from src/srs.rs rename to crates/zkbtc-core/src/srs.rs diff --git a/crates/zkbtc/Cargo.toml b/crates/zkbtc/Cargo.toml new file mode 100644 index 0000000..54ca3c8 --- /dev/null +++ b/crates/zkbtc/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "zkbtc" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.75" +bitcoin = { version = "0.31.0", features = [ + "serde", + "bitcoinconsensus", +], git = "https://github.com/mimoo/rust-bitcoin/", branch = "mimoo/fix_0_31" } +clap = { version = "4.4.10", features = ["derive", "env"] } +env_logger = "0.10.1" +hex = "0.4.3" +log = "0.4.20" +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0" } +tempdir = "0.3.7" +tokio = { version = "1.34", features = [ + "fs", + "rt", + "rt-multi-thread", + "macros", +] } +zkbitcoin-core = { path = "../zkbtc-core"} diff --git a/crates/zkbtc/src/cli/mod.rs b/crates/zkbtc/src/cli/mod.rs new file mode 100644 index 0000000..e76fc9c --- /dev/null +++ b/crates/zkbtc/src/cli/mod.rs @@ -0,0 +1,109 @@ +use clap::{Parser, Subcommand}; +use std::path::PathBuf; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +pub struct Cli { + #[command(subcommand)] + pub command: Commands, +} + +#[derive(Subcommand)] +pub enum Commands { + /// Deploy a zkapp on Bitcoin. + DeployZkapp { + /// The wallet name of the RPC full node. + #[arg(env = "RPC_WALLET")] + wallet: Option, + + /// The `http(s)://address:port`` of the RPC full node. + #[arg(env = "RPC_ADDRESS")] + address: Option, + + /// The `user:password`` of the RPC full node. + #[arg(env = "RPC_AUTH")] + auth: Option, + + /// The path to the Circom circuit to deploy. + #[arg(short, long)] + circom_circuit_path: PathBuf, + + /// Optionally, an initial state for stateful zkapps. + #[arg(short, long)] + initial_state: Option, + + /// The amount in satoshis to send to the zkapp. + #[arg(short, long)] + satoshi_amount: u64, + }, + + /// Use a zkapp on Bitcoin. + UseZkapp { + /// The wallet name of the RPC full node. + #[arg(env = "RPC_WALLET")] + wallet: Option, + + /// The `http(s)://address:port`` of the RPC full node. + #[arg(env = "RPC_ADDRESS")] + address: Option, + + /// The `user:password`` of the RPC full node. + #[arg(env = "RPC_AUTH")] + auth: Option, + + /// The address of the orchestrator. + #[arg(env = "ENDPOINT")] + orchestrator_address: Option, + + /// The transaction ID that deployed the zkapp. + #[arg(short, long)] + txid: String, + + /// The address of the recipient. + #[arg(short, long)] + recipient_address: String, + + /// The path to the circom circuit to use. + #[arg(short, long)] + circom_circuit_path: PathBuf, + + /// A JSON string of the proof inputs. + /// For stateful zkapps, we expect at least `amount_in` and `amount_out`. + #[arg(short, long)] + proof_inputs: Option, + }, + + /// Check the status of a zkapp on Bitcoin. + GetZkapp { + /// The transaction ID that deployed the zkapp. + #[arg(required = true)] + txid: String, + + /// The wallet name of the RPC full node. + #[arg(env = "RPC_WALLET")] + wallet: Option, + + /// The `http(s)://address:port`` of the RPC full node. + #[arg(env = "RPC_ADDRESS")] + address: Option, + + /// The `user:password`` of the RPC full node. + #[arg(env = "RPC_AUTH")] + auth: Option, + }, + + /// Get list of deployed zkapps on Bitcoin. + ListZkapps { + /// The wallet name of the RPC full node. + #[arg(env = "RPC_WALLET")] + wallet: Option, + + /// The `http(s)://address:port`` of the RPC full node. + #[arg(env = "RPC_ADDRESS")] + address: Option, + + /// The `user:password`` of the RPC full node. + #[arg(env = "RPC_AUTH")] + auth: Option, + }, +} diff --git a/crates/zkbtc/src/main.rs b/crates/zkbtc/src/main.rs new file mode 100644 index 0000000..a22bb5c --- /dev/null +++ b/crates/zkbtc/src/main.rs @@ -0,0 +1,239 @@ +mod cli; + +use crate::cli::*; +use anyhow::{ensure, Context, Result}; +use bitcoin::{Address, Txid}; +use clap::Parser; +use log::info; +use std::{collections::HashMap, env, str::FromStr}; +use tempdir::TempDir; +use zkbitcoin_core::{ + alice_sign_tx::generate_and_broadcast_transaction, + bob_request::{fetch_smart_contract, send_bob_request, BobRequest}, + constants::{ + BITCOIN_JSON_RPC_VERSION, ORCHESTRATOR_ADDRESS, ZKBITCOIN_FEE_PUBKEY, ZKBITCOIN_PUBKEY, + }, + get_network, + json_rpc_stuff::{ + scan_txout_set, send_raw_transaction, sign_transaction, RpcCtx, TransactionOrHex, + }, + snarkjs::{self, CompilationResult}, + taproot_addr_from, +}; + +#[tokio::main] +async fn main() -> Result<()> { + // init default log level to info (unless RUST_LOG is set) + env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); + + // debug info + info!( + "- zkbitcoin_address: {}", + taproot_addr_from(ZKBITCOIN_PUBKEY).unwrap().to_string() + ); + info!( + "- zkbitcoin_fund_address: {}", + taproot_addr_from(ZKBITCOIN_FEE_PUBKEY).unwrap().to_string() + ); + + // parse CLI + let cli = Cli::parse(); + match &cli.command { + // Alice's command + Commands::DeployZkapp { + wallet, + address, + auth, + circom_circuit_path, + initial_state, + satoshi_amount, + } => { + let ctx = RpcCtx::new( + Some(BITCOIN_JSON_RPC_VERSION), + wallet.clone(), + address.clone(), + auth.clone(), + None, + ); + + let circom_circuit_path = env::current_dir()?.join(circom_circuit_path); + + // compile to get VK (and its digest) + let (vk, vk_hash) = { + let tmp_dir = TempDir::new("zkbitcoin_").context("couldn't create tmp dir")?; + let CompilationResult { + verifier_key, + circuit_r1cs_path: _, + prover_key_path: _, + } = snarkjs::compile(&tmp_dir, &circom_circuit_path).await?; + let vk_hash = verifier_key.hash(); + (verifier_key, vk_hash) + }; + + // sanity check + let num_public_inputs = vk.nPublic; + ensure!( + num_public_inputs > 0, + "the circuit must have at least one public input (the txid)" + ); + + info!( + "deploying circuit {} with {num_public_inputs} public inputs", + hex::encode(&vk_hash) + ); + + // sanity check for stateful zkapps + if num_public_inputs > 1 { + let double_state_len = vk.nPublic - 3; /* txid, amount_in, amount_out */ + let state_len = double_state_len.checked_div(2).context("the VK")?; + { + // TODO: does checked_div errors if its not a perfect division? + assert_eq!(state_len * 2, double_state_len); + } + + // for now we only state of a single element + ensure!( + state_len == 1, + "we only allow states of a single field element" + ); + + // check that the circuit makes sense for a stateful zkapp + ensure!(num_public_inputs == 3 /* txid, amount_in, amount_out */ + state_len * 2, "the circuit passed does not expect the right number of public inputs for a stateful zkapp"); + + // parse initial state + ensure!( + initial_state.is_some(), + "an initial state should be passed for a stateful zkapp" + ); + } + + // generate and broadcast deploy transaction + let txid = generate_and_broadcast_transaction( + &ctx, + &vk_hash, + initial_state.as_ref(), + *satoshi_amount, + ) + .await?; + + info!("- txid broadcast to the network: {txid}"); + info!("- on an explorer: https://blockstream.info/testnet/tx/{txid}"); + } + + // Bob's command + Commands::UseZkapp { + wallet, + address, + auth, + orchestrator_address, + txid, + recipient_address, + circom_circuit_path, + proof_inputs, + } => { + let rpc_ctx = RpcCtx::new( + Some(BITCOIN_JSON_RPC_VERSION), + wallet.clone(), + address.clone(), + auth.clone(), + None, + ); + + // parse circom circuit path + let circom_circuit_path = env::current_dir()?.join(circom_circuit_path); + + // parse proof inputs + let proof_inputs: HashMap> = if let Some(s) = &proof_inputs { + serde_json::from_str(s)? + } else { + HashMap::new() + }; + + // parse Bob address + let bob_address = Address::from_str(recipient_address) + .unwrap() + .require_network(get_network()) + .unwrap(); + + // parse transaction ID + let txid = Txid::from_str(txid)?; + + // create bob request + let bob_request = BobRequest::new( + &rpc_ctx, + bob_address, + txid, + &circom_circuit_path, + proof_inputs, + ) + .await?; + + // send bob's request to the orchestartor. + let address = orchestrator_address + .as_deref() + .unwrap_or(ORCHESTRATOR_ADDRESS); + let bob_response = send_bob_request(address, bob_request) + .await + .context("error while sending request to orchestrator")?; + + // sign it + let (signed_tx_hex, _signed_tx) = sign_transaction( + &rpc_ctx, + TransactionOrHex::Transaction(&bob_response.unlocked_tx), + ) + .await?; + + // broadcast transaction + let txid = send_raw_transaction(&rpc_ctx, TransactionOrHex::Hex(signed_tx_hex)).await?; + + // print useful msg + info!("- txid broadcast to the network: {txid}"); + info!("- on an explorer: https://blockstream.info/testnet/tx/{txid}"); + } + + Commands::GetZkapp { + wallet, + address, + auth, + txid, + } => { + let ctx = RpcCtx::new( + Some(BITCOIN_JSON_RPC_VERSION), + wallet.clone(), + address.clone(), + auth.clone(), + None, + ); + + // extract smart contract + let zkapp = fetch_smart_contract(&ctx, Txid::from_str(txid)?).await?; + + println!("{zkapp}"); + } + + Commands::ListZkapps { + wallet, + address, + auth, + } => { + let mut rpc_ctx = RpcCtx::new( + Some(BITCOIN_JSON_RPC_VERSION), + wallet.clone(), + address.clone(), + auth.clone(), + None, + ); + rpc_ctx.timeout = std::time::Duration::from_secs(20); // scan takes 13s from what I can see + let zkbitcoin_addr = taproot_addr_from(ZKBITCOIN_PUBKEY).unwrap(); + let res = scan_txout_set(&rpc_ctx, &zkbitcoin_addr.to_string()).await?; + for unspent in &res.unspents { + let txid = unspent.txid; + if let Ok(zkapp) = fetch_smart_contract(&rpc_ctx, txid).await { + println!("{zkapp}"); + } + } + } + } + + Ok(()) +} diff --git a/src/bin/zkbtc.rs b/src/bin/zkbtc.rs deleted file mode 100644 index 976afe6..0000000 --- a/src/bin/zkbtc.rs +++ /dev/null @@ -1,514 +0,0 @@ -use std::{collections::HashMap, env, path::PathBuf, str::FromStr}; - -use anyhow::{ensure, Context, Result}; -use bitcoin::{Address, Txid}; -use clap::{Parser, Subcommand}; -use log::info; -use tempdir::TempDir; -use zkbitcoin::{ - alice_sign_tx::generate_and_broadcast_transaction, - bob_request::{fetch_smart_contract, send_bob_request, BobRequest}, - committee::orchestrator::{CommitteeConfig, Member}, - constants::{ - BITCOIN_JSON_RPC_VERSION, ORCHESTRATOR_ADDRESS, ZKBITCOIN_FEE_PUBKEY, ZKBITCOIN_PUBKEY, - }, - frost, get_network, - json_rpc_stuff::{ - scan_txout_set, send_raw_transaction, sign_transaction, RpcCtx, TransactionOrHex, - }, - snarkjs::{self, CompilationResult}, - taproot_addr_from, -}; - -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Cli { - #[command(subcommand)] - command: Commands, -} - -#[derive(Subcommand)] -enum Commands { - /// Deploy a zkapp on Bitcoin. - DeployZkapp { - /// The wallet name of the RPC full node. - #[arg(env = "RPC_WALLET")] - wallet: Option, - - /// The `http(s)://address:port`` of the RPC full node. - #[arg(env = "RPC_ADDRESS")] - address: Option, - - /// The `user:password`` of the RPC full node. - #[arg(env = "RPC_AUTH")] - auth: Option, - - /// The path to the Circom circuit to deploy. - #[arg(short, long)] - circom_circuit_path: PathBuf, - - /// Optionally, an initial state for stateful zkapps. - #[arg(short, long)] - initial_state: Option, - - /// The amount in satoshis to send to the zkapp. - #[arg(short, long)] - satoshi_amount: u64, - }, - - /// Use a zkapp on Bitcoin. - UseZkapp { - /// The wallet name of the RPC full node. - #[arg(env = "RPC_WALLET")] - wallet: Option, - - /// The `http(s)://address:port`` of the RPC full node. - #[arg(env = "RPC_ADDRESS")] - address: Option, - - /// The `user:password`` of the RPC full node. - #[arg(env = "RPC_AUTH")] - auth: Option, - - /// The address of the orchestrator. - #[arg(env = "ENDPOINT")] - orchestrator_address: Option, - - /// The transaction ID that deployed the zkapp. - #[arg(short, long)] - txid: String, - - /// The address of the recipient. - #[arg(short, long)] - recipient_address: String, - - /// The path to the circom circuit to use. - #[arg(short, long)] - circom_circuit_path: PathBuf, - - /// A JSON string of the proof inputs. - /// For stateful zkapps, we expect at least `amount_in` and `amount_out`. - #[arg(short, long)] - proof_inputs: Option, - }, - - /// Check the status of a zkapp on Bitcoin. - GetZkapp { - /// The transaction ID that deployed the zkapp. - #[arg(required = true)] - txid: String, - - /// The wallet name of the RPC full node. - #[arg(env = "RPC_WALLET")] - wallet: Option, - - /// The `http(s)://address:port`` of the RPC full node. - #[arg(env = "RPC_ADDRESS")] - address: Option, - - /// The `user:password`` of the RPC full node. - #[arg(env = "RPC_AUTH")] - auth: Option, - }, - - /// Get list of deployed zkapps on Bitcoin. - ListZkapps { - /// The wallet name of the RPC full node. - #[arg(env = "RPC_WALLET")] - wallet: Option, - - /// The `http(s)://address:port`` of the RPC full node. - #[arg(env = "RPC_ADDRESS")] - address: Option, - - /// The `user:password`` of the RPC full node. - #[arg(env = "RPC_AUTH")] - auth: Option, - }, - - /// Generates an MPC committee via a trusted dealer. - /// Ideally this is just used for testing as it is more secure to do a DKG. - GenerateCommittee { - /// Number of nodes in the committee. - #[arg(short, long)] - num: u16, - - /// Minimum number of committee member required for a signature. - #[arg(short, long)] - threshold: u16, - - /// Output directory to write the committee configuration files to. - #[arg(short, long)] - output_dir: String, - }, - - /// Starts an MPC node given a configuration - StartCommitteeNode { - /// The address to run the node on. - #[arg(short, long)] - address: Option, - - /// The path to the node's key package. - #[arg(short, long)] - key_path: String, - - /// The path to the MPC committee public key package. - #[arg(short, long)] - publickey_package_path: String, - }, - - /// Starts an orchestrator - StartOrchestrator { - /// The address to run the node on. - #[arg(short, long)] - address: Option, - - #[arg(short, long)] - publickey_package_path: String, - - #[arg(short, long)] - committee_cfg_path: String, - }, -} - -#[tokio::main] -async fn main() -> Result<()> { - // init default log level to info (unless RUST_LOG is set) - env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); - - // debug info - info!( - "- zkbitcoin_address: {}", - taproot_addr_from(ZKBITCOIN_PUBKEY).unwrap().to_string() - ); - info!( - "- zkbitcoin_fund_address: {}", - taproot_addr_from(ZKBITCOIN_FEE_PUBKEY).unwrap().to_string() - ); - - // parse CLI - let cli = Cli::parse(); - match &cli.command { - // Alice's command - Commands::DeployZkapp { - wallet, - address, - auth, - circom_circuit_path, - initial_state, - satoshi_amount, - } => { - let ctx = RpcCtx::new( - Some(BITCOIN_JSON_RPC_VERSION), - wallet.clone(), - address.clone(), - auth.clone(), - None, - ); - - let circom_circuit_path = env::current_dir()?.join(circom_circuit_path); - - // compile to get VK (and its digest) - let (vk, vk_hash) = { - let tmp_dir = TempDir::new("zkbitcoin_").context("couldn't create tmp dir")?; - let CompilationResult { - verifier_key, - circuit_r1cs_path: _, - prover_key_path: _, - } = snarkjs::compile(&tmp_dir, &circom_circuit_path).await?; - let vk_hash = verifier_key.hash(); - (verifier_key, vk_hash) - }; - - // sanity check - let num_public_inputs = vk.nPublic; - ensure!( - num_public_inputs > 0, - "the circuit must have at least one public input (the txid)" - ); - - info!( - "deploying circuit {} with {num_public_inputs} public inputs", - hex::encode(vk_hash) - ); - - // sanity check for stateful zkapps - if num_public_inputs > 1 { - let double_state_len = vk.nPublic - 3; /* txid, amount_in, amount_out */ - let state_len = double_state_len.checked_div(2).context("the VK")?; - { - // TODO: does checked_div errors if its not a perfect division? - assert_eq!(state_len * 2, double_state_len); - } - - // for now we only state of a single element - ensure!( - state_len == 1, - "we only allow states of a single field element" - ); - - // check that the circuit makes sense for a stateful zkapp - ensure!(num_public_inputs == 3 /* txid, amount_in, amount_out */ + state_len * 2, "the circuit passed does not expect the right number of public inputs for a stateful zkapp"); - - // parse initial state - ensure!( - initial_state.is_some(), - "an initial state should be passed for a stateful zkapp" - ); - } - - // generate and broadcast deploy transaction - let txid = generate_and_broadcast_transaction( - &ctx, - &vk_hash, - initial_state.as_ref(), - *satoshi_amount, - ) - .await?; - - info!("- txid broadcast to the network: {txid}"); - info!("- on an explorer: https://blockstream.info/testnet/tx/{txid}"); - } - - // Bob's command - Commands::UseZkapp { - wallet, - address, - auth, - orchestrator_address, - txid, - recipient_address, - circom_circuit_path, - proof_inputs, - } => { - let rpc_ctx = RpcCtx::new( - Some(BITCOIN_JSON_RPC_VERSION), - wallet.clone(), - address.clone(), - auth.clone(), - None, - ); - - // parse circom circuit path - let circom_circuit_path = env::current_dir()?.join(circom_circuit_path); - - // parse proof inputs - let proof_inputs: HashMap> = if let Some(s) = &proof_inputs { - serde_json::from_str(s)? - } else { - HashMap::new() - }; - - // parse Bob address - let bob_address = Address::from_str(recipient_address) - .unwrap() - .require_network(get_network()) - .unwrap(); - - // parse transaction ID - let txid = Txid::from_str(txid)?; - - // create bob request - let bob_request = BobRequest::new( - &rpc_ctx, - bob_address, - txid, - &circom_circuit_path, - proof_inputs, - ) - .await?; - - // send bob's request to the orchestartor. - let address = orchestrator_address - .as_deref() - .unwrap_or(ORCHESTRATOR_ADDRESS); - let bob_response = send_bob_request(address, bob_request) - .await - .context("error while sending request to orchestrator")?; - - // sign it - let (signed_tx_hex, _signed_tx) = sign_transaction( - &rpc_ctx, - TransactionOrHex::Transaction(&bob_response.unlocked_tx), - ) - .await?; - - // broadcast transaction - let txid = send_raw_transaction(&rpc_ctx, TransactionOrHex::Hex(signed_tx_hex)).await?; - - // print useful msg - info!("- txid broadcast to the network: {txid}"); - info!("- on an explorer: https://blockstream.info/testnet/tx/{txid}"); - } - - Commands::GetZkapp { - wallet, - address, - auth, - txid, - } => { - let ctx = RpcCtx::new( - Some(BITCOIN_JSON_RPC_VERSION), - wallet.clone(), - address.clone(), - auth.clone(), - None, - ); - - // extract smart contract - let zkapp = fetch_smart_contract(&ctx, Txid::from_str(txid)?).await?; - - println!("{zkapp}"); - } - - Commands::ListZkapps { - wallet, - address, - auth, - } => { - let mut rpc_ctx = RpcCtx::new( - Some(BITCOIN_JSON_RPC_VERSION), - wallet.clone(), - address.clone(), - auth.clone(), - None, - ); - rpc_ctx.timeout = std::time::Duration::from_secs(20); // scan takes 13s from what I can see - let zkbitcoin_addr = taproot_addr_from(ZKBITCOIN_PUBKEY).unwrap(); - let res = scan_txout_set(&rpc_ctx, &zkbitcoin_addr.to_string()).await?; - for unspent in &res.unspents { - let txid = unspent.txid; - if let Ok(zkapp) = fetch_smart_contract(&rpc_ctx, txid).await { - println!("{zkapp}"); - } - } - } - - Commands::GenerateCommittee { - num, - threshold, - output_dir, - } => { - let output_dir = PathBuf::from(output_dir); - - // deal until we get a public key starting with 0x02 - let (mut key_packages, mut pubkey_package) = - frost::gen_frost_keys(*num, *threshold).unwrap(); - let mut pubkey = pubkey_package.verifying_key().to_owned(); - loop { - if pubkey.serialize()[0] == 2 { - break; - } - (key_packages, pubkey_package) = frost::gen_frost_keys(*num, *threshold).unwrap(); - pubkey = pubkey_package.verifying_key().to_owned(); - } - - // all key packages - { - for (id, key_package) in key_packages.values().enumerate() { - let filename = format!("key-{id}.json"); - - let path = output_dir.join(filename); - std::fs::create_dir_all(path.clone().parent().unwrap()) - .expect("Couldn't create directory"); - let file = std::fs::File::create(&path) - .expect("couldn't create file given output dir"); - serde_json::to_writer_pretty(file, key_package).unwrap(); - } - } - - // public key package - { - let path = output_dir.join("publickey-package.json"); - let file = - std::fs::File::create(path).expect("couldn't create file given output dir"); - serde_json::to_writer_pretty(file, &pubkey_package).unwrap(); - } - - // create the committee-cfg.json file - { - let ip = "http://127.0.0.1:889"; - let committee_cfg = CommitteeConfig { - threshold: *threshold as usize, - members: key_packages - .iter() - .enumerate() - .map(|(id, (member_id, _))| { - ( - *member_id, - Member { - address: format!("{}{}", ip, id), - }, - ) - }) - .collect(), - }; - let path = output_dir.join("committee-cfg.json"); - let file = - std::fs::File::create(path).expect("couldn't create file given output dir"); - serde_json::to_writer_pretty(file, &committee_cfg).unwrap(); - } - } - - Commands::StartCommitteeNode { - address, - key_path, - publickey_package_path, - } => { - let key_package = { - let full_path = PathBuf::from(key_path); - let file = std::fs::File::open(full_path).expect("file not found"); - let key: frost::KeyPackage = - serde_json::from_reader(file).expect("error while reading file"); - key - }; - - let pubkey_package = { - let full_path = PathBuf::from(publickey_package_path); - let file = std::fs::File::open(full_path).expect("file not found"); - let publickey_package: frost::PublicKeyPackage = - serde_json::from_reader(file).expect("error while reading file"); - publickey_package - }; - - zkbitcoin::committee::node::run_server(address.as_deref(), key_package, pubkey_package) - .await - .unwrap(); - } - - Commands::StartOrchestrator { - address, - publickey_package_path, - committee_cfg_path, - } => { - let pubkey_package = { - let full_path = PathBuf::from(publickey_package_path); - let file = std::fs::File::open(full_path).expect("file not found"); - let publickey_package: frost::PublicKeyPackage = - serde_json::from_reader(file).expect("error while reading file"); - publickey_package - }; - - let committee_cfg = { - let full_path = PathBuf::from(committee_cfg_path); - let file = std::fs::File::open(full_path).expect("file not found"); - let publickey_package: CommitteeConfig = - serde_json::from_reader(file).expect("error while reading file"); - publickey_package - }; - - // sanity check (unfortunately the publickey_package doesn't contain this info) - assert!(committee_cfg.threshold > 0); - - zkbitcoin::committee::orchestrator::run_server( - address.as_deref(), - pubkey_package, - committee_cfg, - ) - .await - .unwrap(); - } - } - - Ok(()) -}