/* 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(())
}
}
}