darkfi_money_contract/client/transfer_v1/mod.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2024 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 <https://www.gnu.org/licenses/>.
*/
use darkfi::{zk::ProvingKey, zkas::ZkBinary, ClientFailed, Result};
use darkfi_sdk::{
crypto::{pasta_prelude::*, Blind, FuncId, Keypair, MerkleTree, PublicKey},
pasta::pallas,
};
use log::{debug, error};
use rand::rngs::OsRng;
use crate::{
client::OwnCoin,
error::MoneyError,
model::{MoneyTransferParamsV1, TokenId},
};
mod builder;
pub use builder::{
TransferCallBuilder, TransferCallClearInput, TransferCallInput, TransferCallOutput,
TransferCallSecrets,
};
pub(crate) mod proof;
/// Select coins from `coins` of at least `min_value` in total.
/// Different strategies can be used. This function uses the dumb strategy
/// of selecting coins until we reach `min_value`.
pub fn select_coins(coins: Vec<OwnCoin>, min_value: u64) -> Result<(Vec<OwnCoin>, u64)> {
let mut total_value = 0;
let mut selected = vec![];
for coin in coins {
if total_value >= min_value {
break
}
total_value += coin.note.value;
selected.push(coin);
}
if total_value < min_value {
error!(target: "contract::money::client::transfer::select_coins", "Not enough value to build tx inputs");
return Err(ClientFailed::NotEnoughValue(total_value).into())
}
let change_value = total_value - min_value;
Ok((selected, change_value))
}
/// Make a simple anonymous transfer call.
///
/// * `keypair`: Caller's keypair
/// * `recipient`: Recipient's public key
/// * `value`: Amount that we want to send to the recipient
/// * `token_id`: Token ID that we want to send to the recipient
/// * `coins`: Set of `OwnCoin` we're given to use in this builder
/// * `tree`: Merkle tree of coins used to create inclusion proofs
/// * `output_spend_hook: Optional contract spend hook to use in
/// the output, not applicable to the change
/// * `output_user_data: Optional user data to use in the output,
/// not applicable to the change
/// * `mint_zkbin`: `Mint_V1` zkas circuit ZkBinary
/// * `mint_pk`: Proving key for the `Mint_V1` zk circuit
/// * `burn_zkbin`: `Burn_V1` zkas circuit ZkBinary
/// * `burn_pk`: Proving key for the `Burn_V1` zk circuit
/// * `half_split`: Flag indicating to split the output coin into
/// two equal halves.
///
/// Returns a tuple of:
///
/// * The actual call data
/// * Secret values such as blinds
/// * A list of the spent coins
#[allow(clippy::too_many_arguments)]
pub fn make_transfer_call(
keypair: Keypair,
recipient: PublicKey,
value: u64,
token_id: TokenId,
coins: Vec<OwnCoin>,
tree: MerkleTree,
output_spend_hook: Option<FuncId>,
output_user_data: Option<pallas::Base>,
mint_zkbin: ZkBinary,
mint_pk: ProvingKey,
burn_zkbin: ZkBinary,
burn_pk: ProvingKey,
half_split: bool,
) -> Result<(MoneyTransferParamsV1, TransferCallSecrets, Vec<OwnCoin>)> {
debug!(target: "contract::money::client::transfer", "Building Money::TransferV1 contract call");
if value == 0 {
return Err(ClientFailed::InvalidAmount(value).into())
}
// Using integer division via `half_split` causes the evaluation of `1 / 2` which is equal to
// 0. This would cause us to send two outputs of 0 value which is not what we want.
if half_split && value == 1 {
return Err(ClientFailed::InvalidAmount(value).into())
}
if token_id.inner() == pallas::Base::ZERO {
return Err(ClientFailed::InvalidTokenId(token_id.to_string()).into())
}
if coins.is_empty() {
return Err(ClientFailed::VerifyError(MoneyError::TransferMissingInputs.to_string()).into())
}
// Ensure the coins given to us are all of the same token ID.
// The money contract base transfer doesn't allow conversions.
for coin in &coins {
if coin.note.token_id != token_id {
return Err(ClientFailed::InvalidTokenId(coin.note.token_id.to_string()).into())
}
}
let mut inputs = vec![];
let mut outputs = vec![];
let (spent_coins, change_value) = select_coins(coins, value)?;
for coin in spent_coins.iter() {
let input = TransferCallInput {
coin: coin.clone(),
merkle_path: tree.witness(coin.leaf_position, 0).unwrap(),
user_data_blind: Blind::random(&mut OsRng),
};
inputs.push(input);
}
// Check if we should split the output into two equal halves
if half_split {
// Integer division is safe here as we are dividing by a constant.
#[allow(clippy::integer_division)]
let mut half = value / 2;
// Add the first half
outputs.push(TransferCallOutput {
public_key: recipient,
value: half,
token_id,
spend_hook: output_spend_hook.unwrap_or(FuncId::none()),
user_data: output_user_data.unwrap_or(pallas::Base::ZERO),
blind: Blind::random(&mut OsRng),
});
// Handle the case where value is odd. If so, division by 2 will truncate the amount.
// e.g. in integer division, 3 / 2 == 1.
// Arithmetic side effects are safe here: no risk of overflow or panic.
#[allow(clippy::arithmetic_side_effects)]
if value % 2 != 0 {
half += 1;
}
outputs.push(TransferCallOutput {
public_key: recipient,
value: half,
token_id,
spend_hook: output_spend_hook.unwrap_or(FuncId::none()),
user_data: output_user_data.unwrap_or(pallas::Base::ZERO),
blind: Blind::random(&mut OsRng),
});
} else {
outputs.push(TransferCallOutput {
public_key: recipient,
value,
token_id,
spend_hook: output_spend_hook.unwrap_or(FuncId::none()),
user_data: output_user_data.unwrap_or(pallas::Base::ZERO),
blind: Blind::random(&mut OsRng),
});
}
if change_value > 0 {
outputs.push(TransferCallOutput {
public_key: keypair.public,
value: change_value,
token_id,
spend_hook: FuncId::none(),
user_data: pallas::Base::ZERO,
blind: Blind::random(&mut OsRng),
});
}
if inputs.is_empty() {
error!(target: "contract::money::client::transfer", "Error: No inputs selected");
return Err(ClientFailed::VerifyError(MoneyError::TransferMissingInputs.to_string()).into())
}
let xfer_builder = TransferCallBuilder {
clear_inputs: vec![],
inputs,
outputs,
mint_zkbin,
mint_pk,
burn_zkbin,
burn_pk,
};
let (params, secrets) = xfer_builder.build()?;
Ok((params, secrets, spent_coins))
}