diff --git a/src/alice_sign_tx.rs b/src/alice_sign_tx.rs index 727f6dd..4037a60 100644 --- a/src/alice_sign_tx.rs +++ b/src/alice_sign_tx.rs @@ -16,7 +16,7 @@ use crate::{op_return_script_for, p2tr_script_to}; pub async fn generate_and_broadcast_transaction( ctx: &RpcCtx, vk_hash: &[u8; 32], - initial_state: Option<&String>, + initial_state: Option<&str>, satoshi_amount: u64, ) -> Result { // 1. create transaction based on VK + amount diff --git a/src/bin/zkbtc-admin.rs b/src/bin/zkbtc-admin.rs index 6448f80..a2c7e46 100644 --- a/src/bin/zkbtc-admin.rs +++ b/src/bin/zkbtc-admin.rs @@ -84,127 +84,136 @@ async fn main() -> Result<()> { 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(); - } - } + } => generate_committee(*num, *threshold, output_dir), 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(); - } + } => start_committee_node(address.as_deref(), key_path, publickey_package_path).await, 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( + start_orchestrator( address.as_deref(), - pubkey_package, - committee_cfg, + publickey_package_path, + committee_cfg_path, ) .await - .unwrap(); } } Ok(()) } + +fn generate_committee(num: u16, threshold: u16, output_dir: &str) { + 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(); + } +} + +async fn start_committee_node(address: Option<&str>, key_path: &str, publickey_package_path: &str) { + 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, key_package, pubkey_package) + .await + .unwrap(); +} + +async fn start_orchestrator( + address: Option<&str>, + publickey_package_path: &str, + committee_cfg_path: &str, +) { + 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, pubkey_package, committee_cfg) + .await + .unwrap(); +} diff --git a/src/bin/zkbtc.rs b/src/bin/zkbtc.rs index 0bb651d..c2c8ace 100644 --- a/src/bin/zkbtc.rs +++ b/src/bin/zkbtc.rs @@ -152,76 +152,21 @@ async fn main() -> Result<()> { initial_state, satoshi_amount, } => { - let ctx = RpcCtx::new( + let rpc_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(), + deploy_zkapp( + &rpc_ctx, + circom_circuit_path, + initial_state.as_deref(), *satoshi_amount, ) .await?; - - info!("- txid broadcast to the network: {txid}"); - info!("- on an explorer: https://blockstream.info/testnet/tx/{txid}"); } // Bob's command @@ -242,57 +187,16 @@ async fn main() -> Result<()> { 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( + use_zkapp( &rpc_ctx, - bob_address, + orchestrator_address.as_deref(), 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), + recipient_address, + circom_circuit_path, + proof_inputs.as_deref(), ) .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 { @@ -301,17 +205,14 @@ async fn main() -> Result<()> { auth, txid, } => { - let ctx = RpcCtx::new( + let rpc_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?; - + let zkapp = fetch_smart_contract(&rpc_ctx, Txid::from_str(txid)?).await?; println!("{zkapp}"); } @@ -341,3 +242,125 @@ async fn main() -> Result<()> { Ok(()) } + +async fn deploy_zkapp( + rpc_ctx: &RpcCtx, + circom_circuit_path: PathBuf, + initial_state: Option<&str>, + satoshi_amount: u64, +) -> Result<()> { + // 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(&rpc_ctx, &vk_hash, initial_state, satoshi_amount) + .await?; + + info!("- txid broadcast to the network: {txid}"); + info!("- on an explorer: https://blockstream.info/testnet/tx/{txid}"); + + Ok(()) +} + +async fn use_zkapp( + rpc_ctx: &RpcCtx, + orchestrator_address: Option<&str>, + txid: &str, + recipient_address: &str, + circom_circuit_path: PathBuf, + proof_inputs: Option<&str>, +) -> Result<()> { + // 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.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}"); + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index b4a60f7..afc8536 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,7 +97,7 @@ pub fn circom_field_from_bytes(bytes: &[u8]) -> anyhow::Result { pub fn op_return_script_for( vk_hash: &[u8; 32], - initial_state: Option<&String>, + initial_state: Option<&str>, ) -> anyhow::Result { let mut data = vk_hash.to_vec(); if let Some(initial_state) = initial_state {