/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2022 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use std::{ io::{stdin, Read}, process::exit, }; use clap::{Parser, Subcommand}; use darkfi_sdk::crypto::{ pedersen::{pedersen_commitment_base, pedersen_commitment_u64}, schnorr, schnorr::SchnorrSecret, PublicKey, SecretKey, TokenId, }; use darkfi_serial::{deserialize, serialize, SerialDecodable, SerialEncodable}; use halo2_proofs::arithmetic::Field; use rand::rngs::OsRng; use url::Url; use darkfi::{ cli_desc, crypto::{ burn_proof::{create_burn_proof, verify_burn_proof}, mint_proof::{create_mint_proof, verify_mint_proof}, note::{EncryptedNote, Note}, proof::{ProvingKey, VerifyingKey}, types::{ DrkCoinBlind, DrkSerial, DrkSpendHook, DrkUserData, DrkUserDataBlind, DrkValueBlind, }, BurnRevealedValues, MintRevealedValues, Proof, }, rpc::client::RpcClient, tx::{ partial::{PartialTransaction, PartialTransactionInput}, Transaction, TransactionInput, TransactionOutput, }, util::{ cli::{fg_green, fg_red, progress_bar}, parse::encode_base10, }, zk::circuit::{BurnContract, MintContract}, Result, }; mod cli_util; use cli_util::{parse_token_pair, parse_value_pair}; mod rpc; use rpc::Rpc; #[derive(Parser)] #[clap(name = "darkotc", about = cli_desc!(), version)] #[clap(arg_required_else_help(true))] struct Args { #[clap(short, parse(from_occurrences))] /// Increase verbosity (-vvv supported) verbose: u8, #[clap(short, long, default_value = "tcp://127.0.0.1:8340")] /// darkfid JSON-RPC endpoint endpoint: Url, #[clap(subcommand)] command: Subcmd, } #[derive(Subcommand)] enum Subcmd { /// Initialize an atomic swap Init { #[clap(short, long)] /// Pair of token IDs to swap: token_to_send:token_to_recv token_pair: String, #[clap(short, long)] /// Pair of values to swap: value_to_send:value_to_recv value_pair: String, }, /// Inspect partial swap data from stdin. InspectPartial, /// Join two partial swap data files and build a tx Join { data0: String, data1: String }, /// Sign a transaction given from stdin. SignTx, } #[derive(SerialEncodable, SerialDecodable)] /// Half of the swap data, includes the coin that is supposed to be received, /// and the coin that is supposed to be sent. struct PartialSwapData { /// Mint proof of coin to be received mint_proof: Proof, /// Public values for the mint proof mint_revealed: MintRevealedValues, /// Value of the coin to be received mint_value: u64, /// Token ID of the coin to be received mint_token: TokenId, /// Blinding factor for the minted value pedersen commitment mint_value_blind: DrkValueBlind, /// Blinding factor for the minted token ID pedersen commitment mint_token_blind: DrkValueBlind, /// Burn proof of the coin to be sent burn_proof: Proof, /// Public values for the burn proof burn_revealed: BurnRevealedValues, /// Value of the coin to be sent burn_value: u64, /// Token ID of the coin to be sent burn_token: TokenId, /// Blinding factor for the burned value pedersen commitment burn_value_blind: DrkValueBlind, /// Blinding factor for the burned token ID pedersen commitment burn_token_blind: DrkValueBlind, /// Encrypted note encrypted_note: EncryptedNote, } #[derive(SerialEncodable, SerialDecodable)] /// Full swap data, containing two instances of `PartialSwapData`, which /// represent an atomic swap. struct SwapData { swap0: PartialSwapData, swap1: PartialSwapData, } async fn init_swap( endpoint: Url, token_pair: (TokenId, TokenId), value_pair: (u64, u64), ) -> Result { let rpc_client = match RpcClient::new(endpoint).await { Ok(v) => v, Err(e) => { eprintln!("Error: Failed connecting to darkfid JSON-RPC endpoint."); return Err(e) } }; let rpc = Rpc { rpc_client }; // TODO: Implement metadata for decimals, don't hardcode. let vp = value_pair; // Connect to darkfid and see if there's available funds. let balance = rpc.balance_of(token_pair.0).await?; if balance < vp.0 { eprintln!( "Error: There's not enough balance for token \"{}\" in your wallet.", token_pair.0 ); eprintln!("Available balance is {} ({})", encode_base10(balance, 8), balance); exit(1); } // If there's not enough funds in a single coin, mint a single new coin // with the funds. We do this to minimize the size of the swap transaction. // i.e. 2 inputs and 2 outputs. // TODO: Implement ^ // TODO: Maybe this should be done by the user beforehand? // Find a coin to spend. We can find multiple, but we'll pick the first one. let coins = rpc.get_coins_valtok(vp.0, token_pair.0).await?; if coins.is_empty() { eprintln!("Error: Did not manage to find a coin with enough value to spend."); exit(1); } // Fetch our default address let our_addr = rpc.wallet_address().await?; let our_pubk = match PublicKey::try_from(our_addr) { Ok(v) => v, Err(e) => { eprintln!("Error converting our address into PublicKey: {}", e); exit(1); } }; // Build ZK proving keys let pb = progress_bar("Building proving key for the Mint contract"); let mint_pk = ProvingKey::build(11, &MintContract::default()); pb.finish(); let pb = progress_bar("Building proving key for the Burn contract"); let burn_pk = ProvingKey::build(11, &BurnContract::default()); pb.finish(); // The coin we want to receive let recv_value_blind = DrkValueBlind::random(&mut OsRng); let recv_token_blind = DrkValueBlind::random(&mut OsRng); let recv_coin_blind = DrkCoinBlind::random(&mut OsRng); let recv_serial = DrkSerial::random(&mut OsRng); // Spend hook and user data disabled let spend_hook = DrkSpendHook::from(0); let user_data = DrkUserData::from(0); let pb = progress_bar("Building Mint proof for the receiving coin"); let (mint_proof, mint_revealed) = create_mint_proof( &mint_pk, vp.1, token_pair.1, recv_value_blind, recv_token_blind, recv_serial, spend_hook, user_data, recv_coin_blind, our_pubk, )?; pb.finish(); // The coin we are spending. let coin = coins[0].clone(); let pb = progress_bar("Building Burn proof for the spending coin"); let signature_secret = SecretKey::random(&mut OsRng); let merkle_path = match rpc.get_merkle_path(usize::from(coin.leaf_position)).await { Ok(v) => v, Err(e) => { eprintln!("Failed to get Merkle path for our coin from darkfid RPC: {}", e); exit(1); } }; // Spend hook and user data disabled let spend_hook = DrkSpendHook::from(0); let user_data = DrkUserData::from(0); let user_data_blind = DrkUserDataBlind::random(&mut OsRng); let (burn_proof, burn_revealed) = create_burn_proof( &burn_pk, vp.0, token_pair.0, coin.note.value_blind, coin.note.token_blind, coin.note.serial, spend_hook, user_data, user_data_blind, coin.note.coin_blind, coin.secret, coin.leaf_position, merkle_path, signature_secret, )?; pb.finish(); // Create encrypted note let note = Note { serial: recv_serial, value: vp.1, token_id: token_pair.1, coin_blind: recv_coin_blind, value_blind: recv_value_blind, token_blind: recv_token_blind, // Here we store our secret key we used for signing memo: serialize(&signature_secret), }; let encrypted_note = note.encrypt(&our_pubk)?; // Pack proofs together with pedersen commitment openings so // counterparty can verify correctness. let partial_swap_data = PartialSwapData { mint_proof, mint_revealed, mint_value: vp.1, mint_token: token_pair.1, mint_value_blind: recv_value_blind, mint_token_blind: recv_token_blind, burn_proof, burn_value: vp.0, burn_token: token_pair.0, burn_revealed, burn_value_blind: coin.note.value_blind, burn_token_blind: coin.note.token_blind, encrypted_note, }; Ok(partial_swap_data) } fn inspect_partial(data: &str) -> Result<()> { let bytes = match bs58::decode(data).into_vec() { Ok(v) => v, Err(e) => { eprintln!("Error decoding base58 data from input: {}", e); exit(1); } }; let sd: PartialSwapData = match deserialize(&bytes) { Ok(v) => v, Err(e) => { eprintln!("Error deserializing partial swap data into struct: {}", e); exit(1); } }; eprintln!("Successfully decoded partial swap data"); // Build ZK verifying keys let pb = progress_bar("Building verifying key for the Mint contract"); let mint_vk = VerifyingKey::build(11, &MintContract::default()); pb.finish(); let pb = progress_bar("Building verifying key for the Burn contract"); let burn_vk = VerifyingKey::build(11, &BurnContract::default()); pb.finish(); let pb = progress_bar("Verifying Burn proof"); let burn_valid = verify_burn_proof(&burn_vk, &sd.burn_proof, &sd.burn_revealed).is_ok(); pb.finish(); let pb = progress_bar("Verifying Mint proof"); let mint_valid = verify_mint_proof(&mint_vk, &sd.mint_proof, &sd.mint_revealed).is_ok(); pb.finish(); eprintln!(" Verifying Pedersen commitments"); let burn_value_valid = pedersen_commitment_u64(sd.burn_value, sd.burn_value_blind) == sd.burn_revealed.value_commit; let burn_token_valid = pedersen_commitment_base(sd.burn_token.inner(), sd.burn_token_blind) == sd.burn_revealed.token_commit; let mint_value_valid = pedersen_commitment_u64(sd.mint_value, sd.mint_value_blind) == sd.mint_revealed.value_commit; let mint_token_valid = pedersen_commitment_base(sd.mint_token.inner(), sd.mint_token_blind) == sd.mint_revealed.token_commit; let mut valid = true; eprintln!("Summary:"); eprint!(" Burn proof: "); if burn_valid { eprintln!("{}", fg_green("VALID")); } else { eprintln!("{}", fg_red("INVALID")); valid = false; } eprint!(" Burn proof value commitment: "); if burn_value_valid { eprintln!("{}", fg_green("VALID")); } else { eprintln!("{}", fg_red("INVALID")); valid = false; } eprint!(" Burn proof token commitment: "); if burn_token_valid { eprintln!("{}", fg_green("VALID")); } else { eprintln!("{}", fg_red("INVALID")); valid = false; } eprint!(" Mint proof: "); if mint_valid { eprintln!("{}", fg_green("VALID")); } else { eprintln!("{}", fg_red("INVALID")); valid = false; } eprint!(" Mint proof value commitment: "); if mint_value_valid { eprintln!("{}", fg_green("VALID")); } else { eprintln!("{}", fg_red("INVALID")); valid = false; } eprint!(" Mint proof token commitment: "); if mint_token_valid { eprintln!("{}", fg_green("VALID")); } else { eprintln!("{}", fg_red("INVALID")); valid = false; } eprintln!("========================================"); eprintln!("Mint: {} {}", encode_base10(sd.mint_value, 8), sd.mint_token); eprintln!("Burn: {} {}", encode_base10(sd.burn_value, 8), sd.burn_token); eprint!("\nThe ZK proofs and commitments inspected are "); if !valid { println!("{}", fg_red("NOT VALID")); exit(1); } else { eprintln!("{}", fg_green("VALID")); } Ok(()) } async fn join(endpoint: Url, d0: PartialSwapData, d1: PartialSwapData) -> Result { eprintln!("Joining data into a transaction"); let input0 = PartialTransactionInput { burn_proof: d0.burn_proof, revealed: d0.burn_revealed }; let input1 = PartialTransactionInput { burn_proof: d1.burn_proof, revealed: d1.burn_revealed }; let inputs = vec![input0, input1]; let output0 = TransactionOutput { mint_proof: d0.mint_proof, revealed: d0.mint_revealed, enc_note: d0.encrypted_note.clone(), }; let output1 = TransactionOutput { mint_proof: d1.mint_proof, revealed: d1.mint_revealed, enc_note: d1.encrypted_note.clone(), }; let outputs = vec![output0, output1]; let partial_tx = PartialTransaction { clear_inputs: vec![], inputs, outputs }; let unsigned_tx_data = serialize(&partial_tx); let mut inputs = vec![]; let mut signed: bool; eprint!("Trying to decrypt the note of the first half... "); let rpc_client = RpcClient::new(endpoint.clone()).await?; let rpc = Rpc { rpc_client }; let note = match rpc.decrypt_note(&d0.encrypted_note).await { Ok(v) => v, Err(_) => None, }; if let Some(note) = note { eprintln!("{}", fg_green("Success")); let signature = try_sign_tx(¬e, &unsigned_tx_data[..])?; let input = TransactionInput::from_partial(partial_tx.inputs[0].clone(), signature); inputs.push(input); signed = true; } else { eprintln!("{}", fg_red("Failure")); let signature = schnorr::Signature::dummy(); let input = TransactionInput::from_partial(partial_tx.inputs[0].clone(), signature); inputs.push(input); signed = false; } // If we have signed, we shouldn't have to look in the other one, but we might // be sending to ourself for some reason. eprint!("Trying to decrypt the note of the second half... "); let rpc_client = RpcClient::new(endpoint).await?; let rpc = Rpc { rpc_client }; let note = match rpc.decrypt_note(&d1.encrypted_note).await { Ok(v) => v, Err(_) => None, }; if let Some(note) = note { eprintln!("{}", fg_green("Success")); let signature = try_sign_tx(¬e, &unsigned_tx_data[..])?; let input = TransactionInput::from_partial(partial_tx.inputs[1].clone(), signature); inputs.push(input); signed = true; } else { eprintln!("{}", fg_red("Failure")); let signature = schnorr::Signature::dummy(); let input = TransactionInput::from_partial(partial_tx.inputs[1].clone(), signature); inputs.push(input); if !signed { eprintln!("Error: Failed to sign transaction!"); exit(1); } } if !signed { eprintln!("Error: Failed to sign transaction!"); exit(1); } let tx = Transaction { clear_inputs: vec![], inputs, outputs: partial_tx.outputs }; Ok(tx) } async fn sign_tx(endpoint: Url, data: &str) -> Result { eprintln!("Trying to sign transaction"); let mut tx: Transaction = deserialize(&bs58::decode(data).into_vec()?)?; // We assume our input and our output are in the same index, since this // transaction contains 2 inputs and 2 outputs, and one of each is ours, // and one of each is the other party's. So we go on and sign the input // index of the output index we can decrypt the note for. let mut idx_to_sign = 0; let mut signature = schnorr::Signature::dummy(); eprintln!("Looking for an encrypted note we can decrypt..."); let mut found_secret = false; for (i, output) in tx.outputs.iter().enumerate() { // TODO: FIXME: Consider not closing the RPC on failure. let rpc = Rpc { rpc_client: RpcClient::new(endpoint.clone()).await? }; let note = match rpc.decrypt_note(&output.enc_note).await { Ok(v) => v, Err(_) => continue, }; if let Some(note) = note { eprintln!("Successfully decrypted note in output {}", i); eprintln!("Creating signature..."); let mut unsigned_tx_data = vec![]; let _ = tx.encode_without_signature(&mut unsigned_tx_data)?; signature = try_sign_tx(¬e, &unsigned_tx_data[..])?; found_secret = true; idx_to_sign = i; break } eprintln!("Failed to find a note to decrypt. Signing failed."); exit(1); } if !found_secret { eprintln!("Error: Did not manage to sign transaction. Couldn't find any secret keys."); exit(1); } tx.inputs[idx_to_sign].signature = signature; Ok(tx) } fn try_sign_tx(note: &Note, tx_data: &[u8]) -> Result { if note.memo.len() != 32 { eprintln!("Error: The note memo is not 32 bytes"); exit(1); } let secret = match SecretKey::from_bytes(note.memo.clone().try_into().unwrap()) { Ok(v) => v, Err(e) => { eprintln!("Did not manage to cast bytes into SecretKey: {}", e); exit(1); } }; eprintln!("Signing transaction..."); let signature = secret.sign(&mut OsRng, tx_data); Ok(signature) } #[async_std::main] async fn main() -> Result<()> { let args = Args::parse(); match args.command { Subcmd::Init { token_pair, value_pair } => { let token_pair = parse_token_pair(&token_pair)?; let value_pair = parse_value_pair(&value_pair)?; eprintln!("Creating half of an atomic swap"); eprintln!("Send: {} {} tokens.", encode_base10(value_pair.0, 8), token_pair.0); eprintln!("Recv: {} {} tokens.", encode_base10(value_pair.1, 8), token_pair.1); let swap_data = init_swap(args.endpoint, token_pair, value_pair).await?; println!("{}", bs58::encode(serialize(&swap_data)).into_string()); Ok(()) } Subcmd::InspectPartial => { let mut buf = String::new(); stdin().read_to_string(&mut buf)?; inspect_partial(buf.trim()) } Subcmd::Join { data0, data1 } => { let d0 = std::fs::read_to_string(data0)?; let d1 = std::fs::read_to_string(data1)?; let d0 = deserialize(&bs58::decode(&d0.trim()).into_vec()?)?; let d1 = deserialize(&bs58::decode(&d1.trim()).into_vec()?)?; let tx = join(args.endpoint, d0, d1).await?; println!("{}", bs58::encode(&serialize(&tx)).into_string()); eprintln!("Successfully signed transaction"); Ok(()) } Subcmd::SignTx => { let mut buf = String::new(); stdin().read_to_string(&mut buf)?; let tx = sign_tx(args.endpoint, buf.trim()).await?; println!("{}", bs58::encode(&serialize(&tx)).into_string()); eprintln!("Successfully signed transaction"); Ok(()) } } }