From 07606d27e4af3fc80faf2b13e5e0ab93ab3353a4 Mon Sep 17 00:00:00 2001 From: skoupidi Date: Thu, 2 May 2024 16:07:30 +0300 Subject: [PATCH] drk: added fee call to transfer --- bin/darkfid/src/error.rs | 4 +- bin/darkfid/src/rpc.rs | 1 + bin/darkfid/src/rpc_tx.rs | 57 ++- bin/drk/money.sql | 1 - bin/drk/src/money.rs | 335 +++++++++++++----- bin/drk/src/rpc.rs | 22 +- bin/drk/src/transfer.rs | 73 ++-- .../localnet/darkfid-single-node/README.md | 70 ++-- src/validator/mod.rs | 42 ++- 9 files changed, 444 insertions(+), 161 deletions(-) diff --git a/bin/darkfid/src/error.rs b/bin/darkfid/src/error.rs index 80b623977..d06146853 100644 --- a/bin/darkfid/src/error.rs +++ b/bin/darkfid/src/error.rs @@ -23,7 +23,7 @@ use darkfi::rpc::jsonrpc::{ErrorCode::ServerError, JsonError, JsonResult}; pub enum RpcError { // Transaction-related errors TxSimulationFail = -32110, - TxBroadcastFail = -32111, + TxGasCalculationFail = -32111, // State-related errors, NotSynced = -32120, @@ -43,7 +43,7 @@ fn to_tuple(e: RpcError) -> (i32, String) { let msg = match e { // Transaction-related errors RpcError::TxSimulationFail => "Failed simulating transaction state change", - RpcError::TxBroadcastFail => "Failed broadcasting transaction", + RpcError::TxGasCalculationFail => "Failed to calculate transaction's gas", // State-related errors RpcError::NotSynced => "Blockchain is not synced", RpcError::UnknownBlockHeight => "Did not find block height", diff --git a/bin/darkfid/src/rpc.rs b/bin/darkfid/src/rpc.rs index 17e9f2726..5fce9728e 100644 --- a/bin/darkfid/src/rpc.rs +++ b/bin/darkfid/src/rpc.rs @@ -73,6 +73,7 @@ impl RequestHandler for Darkfid { "tx.broadcast" => self.tx_broadcast(req.id, req.params).await, "tx.pending" => self.tx_pending(req.id, req.params).await, "tx.clean_pending" => self.tx_pending(req.id, req.params).await, + "tx.calculate_gas" => self.tx_calculate_gas(req.id, req.params).await, // ============== // Invalid method diff --git a/bin/darkfid/src/rpc_tx.rs b/bin/darkfid/src/rpc_tx.rs index 88f097a09..dd4b91a20 100644 --- a/bin/darkfid/src/rpc_tx.rs +++ b/bin/darkfid/src/rpc_tx.rs @@ -17,7 +17,7 @@ */ use darkfi_serial::deserialize_async; -use log::error; +use log::{error, warn}; use tinyjson::JsonValue; use darkfi::{ @@ -135,8 +135,7 @@ impl Darkfid { self.p2p.broadcast(&tx).await; if self.p2p.hosts().channels().await.is_empty() { - error!(target: "darkfid::rpc::tx_broadcast", "Failed broadcasting tx, no connected channels"); - return server_error(RpcError::TxBroadcastFail, id, None) + warn!(target: "darkfid::rpc::tx_broadcast", "No connected channels to broadcast tx"); } let tx_hash = tx.hash().to_string(); @@ -209,4 +208,56 @@ impl Darkfid { JsonResponse::new(JsonValue::Array(pending_txs), id).into() } + + // RPCAPI: + // Compute provided transaction's total gas, against current best fork. + // Returns the gas value if the transaction is valid, otherwise, a corresponding + // error. + // + // --> {"jsonrpc": "2.0", "method": "tx.calculate_gas", "params": ["base64encodedTX", "include_fee"], "id": 1} + // <-- {"jsonrpc": "2.0", "result": true, "id": 1} + pub async fn tx_calculate_gas(&self, id: u16, params: JsonValue) -> JsonResult { + let params = params.get::>().unwrap(); + if params.len() != 2 || !params[0].is_string() || !params[1].is_bool() { + return JsonError::new(InvalidParams, None, id).into() + } + + if !*self.validator.synced.read().await { + error!(target: "darkfid::rpc::tx_calculate_gas", "Blockchain is not synced"); + return server_error(RpcError::NotSynced, id, None) + } + + // Try to deserialize the transaction + let tx_enc = params[0].get::().unwrap().trim(); + let tx_bytes = match base64::decode(tx_enc) { + Some(v) => v, + None => { + error!(target: "darkfid::rpc::tx_calculate_gas", "Failed decoding base64 transaction"); + return server_error(RpcError::ParseError, id, None) + } + }; + + let tx: Transaction = match deserialize_async(&tx_bytes).await { + Ok(v) => v, + Err(e) => { + error!(target: "darkfid::rpc::tx_calculate_gas", "Failed deserializing bytes into Transaction: {}", e); + return server_error(RpcError::ParseError, id, None) + } + }; + + // Parse the include fee flag + let include_fee = params[1].get::().unwrap(); + + // Simulate state transition + let result = self.validator.calculate_gas(&tx, *include_fee).await; + if result.is_err() { + error!( + target: "darkfid::rpc::tx_calculate_gas", "Failed to validate state transition: {}", + result.err().unwrap() + ); + return server_error(RpcError::TxGasCalculationFail, id, None) + }; + + JsonResponse::new(JsonValue::Number(result.unwrap() as f64), id).into() + } } diff --git a/bin/drk/money.sql b/bin/drk/money.sql index 3e9da5403..8a2f8e4fe 100644 --- a/bin/drk/money.sql +++ b/bin/drk/money.sql @@ -1,6 +1,5 @@ -- Wallet definitions for this contract. -- We store data that is needed to be able to receive and send tokens. --- TODO: The tables should be prefixed with ContractId to prevent collision -- Arbitrary info that is potentially useful CREATE TABLE IF NOT EXISTS BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o_money_info ( diff --git a/bin/drk/src/money.rs b/bin/drk/src/money.rs index d504735d5..9d1cdbd7c 100644 --- a/bin/drk/src/money.rs +++ b/bin/drk/src/money.rs @@ -22,12 +22,22 @@ use lazy_static::lazy_static; use rand::rngs::OsRng; use rusqlite::types::Value; -use darkfi::{zk::halo2::Field, Error, Result}; +use darkfi::{ + tx::Transaction, + zk::{halo2::Field, proof::ProvingKey, Proof}, + zkas::ZkBinary, + Error, Result, +}; use darkfi_money_contract::{ - client::{MoneyNote, OwnCoin}, + client::{ + compute_remainder_blind, + fee_v1::{create_fee_proof, FeeCallInput, FeeCallOutput, FEE_CALL_GAS}, + MoneyNote, OwnCoin, + }, model::{ - Coin, MoneyGenesisMintParamsV1, MoneyPoWRewardParamsV1, MoneyTokenFreezeParamsV1, - MoneyTokenMintParamsV1, MoneyTransferParamsV1, Nullifier, TokenId, DARK_TOKEN_ID, + Coin, Input, MoneyFeeParamsV1, MoneyGenesisMintParamsV1, MoneyPoWRewardParamsV1, + MoneyTokenFreezeParamsV1, MoneyTokenMintParamsV1, MoneyTransferParamsV1, Nullifier, Output, + TokenId, DARK_TOKEN_ID, }, MoneyFunction, }; @@ -38,8 +48,9 @@ use darkfi_sdk::{ ScalarBlind, SecretKey, MONEY_CONTRACT_ID, }, pasta::pallas, + ContractCall, }; -use darkfi_serial::{deserialize_async, serialize_async}; +use darkfi_serial::{deserialize_async, serialize_async, AsyncEncodable}; use crate::{ convert_named_params, @@ -401,95 +412,124 @@ impl Drk { }; let mut owncoins = Vec::with_capacity(rows.len()); - for row in rows { - let Value::Blob(ref coin_bytes) = row[0] else { - return Err(Error::ParseFailed("[get_coins] Coin bytes parsing failed")) - }; - let coin: Coin = deserialize_async(coin_bytes).await?; - - let Value::Integer(is_spent) = row[1] else { - return Err(Error::ParseFailed("[get_coins] Is spent parsing failed")) - }; - let Ok(is_spent) = u64::try_from(is_spent) else { - return Err(Error::ParseFailed("[get_coins] Is spent parsing failed")) - }; - let is_spent = is_spent > 0; - - let Value::Blob(ref value_bytes) = row[2] else { - return Err(Error::ParseFailed("[get_coins] Value bytes parsing failed")) - }; - let value: u64 = deserialize_async(value_bytes).await?; - - let Value::Blob(ref token_id_bytes) = row[3] else { - return Err(Error::ParseFailed("[get_coins] Token ID bytes parsing failed")) - }; - let token_id: TokenId = deserialize_async(token_id_bytes).await?; - - let Value::Blob(ref spend_hook_bytes) = row[4] else { - return Err(Error::ParseFailed("[get_coins] Spend hook bytes parsing failed")) - }; - let spend_hook: pallas::Base = deserialize_async(spend_hook_bytes).await?; - - let Value::Blob(ref user_data_bytes) = row[5] else { - return Err(Error::ParseFailed("[get_coins] User data bytes parsing failed")) - }; - let user_data: pallas::Base = deserialize_async(user_data_bytes).await?; - - let Value::Blob(ref coin_blind_bytes) = row[6] else { - return Err(Error::ParseFailed("[get_coins] Coin blind bytes parsing failed")) - }; - let coin_blind: BaseBlind = deserialize_async(coin_blind_bytes).await?; - - let Value::Blob(ref value_blind_bytes) = row[7] else { - return Err(Error::ParseFailed("[get_coins] Value blind bytes parsing failed")) - }; - let value_blind: ScalarBlind = deserialize_async(value_blind_bytes).await?; - - let Value::Blob(ref token_blind_bytes) = row[8] else { - return Err(Error::ParseFailed("[get_coins] Token blind bytes parsing failed")) - }; - let token_blind: BaseBlind = deserialize_async(token_blind_bytes).await?; - - let Value::Blob(ref secret_bytes) = row[9] else { - return Err(Error::ParseFailed("[get_coins] Secret bytes parsing failed")) - }; - let secret: SecretKey = deserialize_async(secret_bytes).await?; - - // TODO: Remove from SQL store, can be derived ondemand - let Value::Blob(ref nullifier_bytes) = row[10] else { - return Err(Error::ParseFailed("[get_coins] Nullifier bytes parsing failed")) - }; - let _nullifier: Nullifier = deserialize_async(nullifier_bytes).await?; - - let Value::Blob(ref leaf_position_bytes) = row[11] else { - return Err(Error::ParseFailed("[get_coins] Leaf position bytes parsing failed")) - }; - let leaf_position: bridgetree::Position = - deserialize_async(leaf_position_bytes).await?; - - let Value::Blob(ref memo) = row[12] else { - return Err(Error::ParseFailed("[get_coins] Memo parsing failed")) - }; - - let note = MoneyNote { - value, - token_id, - spend_hook: spend_hook.into(), - user_data, - coin_blind, - value_blind, - token_blind, - memo: memo.clone(), - }; - let owncoin = OwnCoin { coin, note, secret, leaf_position }; - - owncoins.push((owncoin, is_spent)) + owncoins.push(self.parse_coin_record(&row).await?) } Ok(owncoins) } + /// Fetch provided token unspend balances from the wallet. + pub async fn get_token_coins(&self, token_id: &TokenId) -> Result> { + let query = self.wallet.query_multiple( + &MONEY_COINS_TABLE, + &[], + convert_named_params! {(MONEY_COINS_COL_IS_SPENT, false), (MONEY_TOKENS_COL_TOKEN_ID, serialize_async(token_id).await), (MONEY_COINS_COL_SPEND_HOOK, serialize_async(&FuncId::none()).await)}, + ) + .await; + + let rows = match query { + Ok(r) => r, + Err(e) => { + return Err(Error::RusqliteError(format!( + "[get_coins] Coins retrieval failed: {e:?}" + ))) + } + }; + + let mut owncoins = Vec::with_capacity(rows.len()); + for row in rows { + owncoins.push(self.parse_coin_record(&row).await?.0) + } + + Ok(owncoins) + } + + /// Auxiliary function to parse a `MONEY_COINS_TABLE` record. + /// The boolean in the returned tuple notes if the coin was marked as spent. + async fn parse_coin_record(&self, row: &[Value]) -> Result<(OwnCoin, bool)> { + let Value::Blob(ref coin_bytes) = row[0] else { + return Err(Error::ParseFailed("[parse_coin_record] Coin bytes parsing failed")) + }; + let coin: Coin = deserialize_async(coin_bytes).await?; + + let Value::Integer(is_spent) = row[1] else { + return Err(Error::ParseFailed("[parse_coin_record] Is spent parsing failed")) + }; + let Ok(is_spent) = u64::try_from(is_spent) else { + return Err(Error::ParseFailed("[parse_coin_record] Is spent parsing failed")) + }; + let is_spent = is_spent > 0; + + let Value::Blob(ref value_bytes) = row[2] else { + return Err(Error::ParseFailed("[parse_coin_record] Value bytes parsing failed")) + }; + let value: u64 = deserialize_async(value_bytes).await?; + + let Value::Blob(ref token_id_bytes) = row[3] else { + return Err(Error::ParseFailed("[parse_coin_record] Token ID bytes parsing failed")) + }; + let token_id: TokenId = deserialize_async(token_id_bytes).await?; + + let Value::Blob(ref spend_hook_bytes) = row[4] else { + return Err(Error::ParseFailed("[parse_coin_record] Spend hook bytes parsing failed")) + }; + let spend_hook: pallas::Base = deserialize_async(spend_hook_bytes).await?; + + let Value::Blob(ref user_data_bytes) = row[5] else { + return Err(Error::ParseFailed("[parse_coin_record] User data bytes parsing failed")) + }; + let user_data: pallas::Base = deserialize_async(user_data_bytes).await?; + + let Value::Blob(ref coin_blind_bytes) = row[6] else { + return Err(Error::ParseFailed("[parse_coin_record] Coin blind bytes parsing failed")) + }; + let coin_blind: BaseBlind = deserialize_async(coin_blind_bytes).await?; + + let Value::Blob(ref value_blind_bytes) = row[7] else { + return Err(Error::ParseFailed("[parse_coin_record] Value blind bytes parsing failed")) + }; + let value_blind: ScalarBlind = deserialize_async(value_blind_bytes).await?; + + let Value::Blob(ref token_blind_bytes) = row[8] else { + return Err(Error::ParseFailed("[parse_coin_record] Token blind bytes parsing failed")) + }; + let token_blind: BaseBlind = deserialize_async(token_blind_bytes).await?; + + let Value::Blob(ref secret_bytes) = row[9] else { + return Err(Error::ParseFailed("[parse_coin_record] Secret bytes parsing failed")) + }; + let secret: SecretKey = deserialize_async(secret_bytes).await?; + + // TODO: Remove from SQL store, can be derived ondemand + let Value::Blob(ref nullifier_bytes) = row[10] else { + return Err(Error::ParseFailed("[parse_coin_record] Nullifier bytes parsing failed")) + }; + let _nullifier: Nullifier = deserialize_async(nullifier_bytes).await?; + + let Value::Blob(ref leaf_position_bytes) = row[11] else { + return Err(Error::ParseFailed("[parse_coin_record] Leaf position bytes parsing failed")) + }; + let leaf_position: bridgetree::Position = deserialize_async(leaf_position_bytes).await?; + + let Value::Blob(ref memo) = row[12] else { + return Err(Error::ParseFailed("[parse_coin_record] Memo parsing failed")) + }; + + let note = MoneyNote { + value, + token_id, + spend_hook: spend_hook.into(), + user_data, + coin_blind, + value_blind, + token_blind, + memo: memo.clone(), + }; + + Ok((OwnCoin { coin, note, secret, leaf_position }, is_spent)) + } + /// Create an alias record for provided Token ID. pub async fn add_alias(&self, alias: String, token_id: TokenId) -> WalletDbResult<()> { println!("Generating alias {alias} for Token: {token_id}"); @@ -642,7 +682,10 @@ impl Drk { match MoneyFunction::try_from(data[0])? { MoneyFunction::FeeV1 => { println!("[apply_tx_money_data] Found Money::FeeV1 call"); - // TODO: implement + let params: MoneyFeeParamsV1 = deserialize_async(&data[9..]).await?; + nullifiers.push(params.input.nullifier); + coins.push(params.output.coin); + notes.push(params.output.note); } MoneyFunction::GenesisMintV1 => { println!("[apply_tx_money_data] Found Money::GenesisMintV1 call"); @@ -871,4 +914,118 @@ impl Drk { // Else parse input Ok(TokenId::from_str(input.as_str())?) } + + /// Create and append a `Money::Fee` call to a given [`Transaction`]. + /// + /// Optionally takes a set of spent coins in order not to reuse them here. + /// + /// Returns the `Fee` call, and all necessary data and parameters related. + pub async fn append_fee_call( + &self, + tx: &Transaction, + public_key: PublicKey, + money_merkle_tree: &MerkleTree, + fee_pk: &ProvingKey, + fee_zkbin: &ZkBinary, + spent_coins: Option<&[OwnCoin]>, + ) -> Result<(ContractCall, Vec, Vec)> { + // First we verify the fee-less transaction to see how much gas it uses for execution + // and verification. + let gas_used = FEE_CALL_GAS + self.get_tx_gas(tx, false).await?; + + // Knowing the total gas, we can now find an OwnCoin of enough value + // so that we can create a valid Money::Fee call. + let mut available_coins = self.get_token_coins(&DARK_TOKEN_ID).await?; + available_coins.retain(|x| x.note.value > gas_used); + if let Some(spent_coins) = spent_coins { + available_coins.retain(|x| !spent_coins.contains(x)); + } + if available_coins.is_empty() { + return Err(Error::Custom("Not enough native tokens to pay for fees".to_string())) + } + + let coin = &available_coins[0]; + let change_value = coin.note.value - gas_used; + + // Input and output setup + let input = FeeCallInput { + coin: coin.clone(), + merkle_path: money_merkle_tree.witness(coin.leaf_position, 0).unwrap(), + user_data_blind: BaseBlind::random(&mut OsRng), + }; + + let output = FeeCallOutput { + public_key, + value: change_value, + token_id: coin.note.token_id, + blind: BaseBlind::random(&mut OsRng), + spend_hook: FuncId::none(), + user_data: pallas::Base::ZERO, + }; + + // Create blinding factors + let token_blind = BaseBlind::random(&mut OsRng); + let input_value_blind = ScalarBlind::random(&mut OsRng); + let fee_value_blind = ScalarBlind::random(&mut OsRng); + let output_value_blind = compute_remainder_blind(&[input_value_blind], &[fee_value_blind]); + + // Create an ephemeral signing key + let signature_secret = SecretKey::random(&mut OsRng); + + // Create the actual fee proof + let (proof, public_inputs) = create_fee_proof( + fee_zkbin, + fee_pk, + &input, + input_value_blind, + &output, + output_value_blind, + output.spend_hook, + output.user_data, + output.blind, + token_blind, + signature_secret, + )?; + + // Encrypted note for the output + let note = MoneyNote { + coin_blind: output.blind, + value: output.value, + token_id: output.token_id, + spend_hook: output.spend_hook, + user_data: output.user_data, + value_blind: output_value_blind, + token_blind, + memo: vec![], + }; + + let encrypted_note = AeadEncryptedNote::encrypt(¬e, &output.public_key, &mut OsRng)?; + + let params = MoneyFeeParamsV1 { + input: Input { + value_commit: public_inputs.input_value_commit, + token_commit: public_inputs.token_commit, + nullifier: public_inputs.nullifier, + merkle_root: public_inputs.merkle_root, + user_data_enc: public_inputs.input_user_data_enc, + signature_public: public_inputs.signature_public, + }, + output: Output { + value_commit: public_inputs.output_value_commit, + token_commit: public_inputs.token_commit, + coin: public_inputs.output_coin, + note: encrypted_note, + }, + fee_value_blind, + token_blind, + }; + + // Encode the contract call + let mut data = vec![MoneyFunction::FeeV1 as u8]; + gas_used.encode_async(&mut data).await?; + params.encode_async(&mut data).await?; + let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data }; + + Ok((call, vec![proof], vec![signature_secret])) + } } diff --git a/bin/drk/src/rpc.rs b/bin/drk/src/rpc.rs index 456c160cb..c14e74345 100644 --- a/bin/drk/src/rpc.rs +++ b/bin/drk/src/rpc.rs @@ -278,7 +278,7 @@ impl Drk { } } - // Queries darkfid for a block with given height + // Queries darkfid for a block with given height. async fn get_block_by_height(&self, height: u32) -> Result { let req = JsonRequest::new( "blockchain.get_block", @@ -293,7 +293,7 @@ impl Drk { } /// Broadcast a given transaction to darkfid and forward onto the network. - /// Returns the transaction ID upon success + /// Returns the transaction ID upon success. pub async fn broadcast_tx(&self, tx: &Transaction) -> Result { println!("Broadcasting transaction..."); @@ -314,7 +314,7 @@ impl Drk { Ok(txid) } - /// Queries darkfid for a tx with given hash + /// Queries darkfid for a tx with given hash. pub async fn get_tx(&self, tx_hash: &TransactionHash) -> Result> { let tx_hash_str = tx_hash.to_string(); let req = JsonRequest::new( @@ -333,7 +333,7 @@ impl Drk { } } - /// Simulate the transaction with the state machine + /// Simulate the transaction with the state machine. pub async fn simulate_tx(&self, tx: &Transaction) -> Result { let tx_str = base64::encode(&serialize_async(tx).await); let req = @@ -361,4 +361,18 @@ impl Drk { Ok(ret) } + + /// Queries darkfid for given transaction's gas. + pub async fn get_tx_gas(&self, tx: &Transaction, include_fee: bool) -> Result { + let params = JsonValue::Array(vec![ + JsonValue::String(base64::encode(&serialize_async(tx).await)), + JsonValue::Boolean(include_fee), + ]); + let req = JsonRequest::new("tx.calculate_gas", params); + let rep = self.rpc_client.as_ref().unwrap().request(req).await?; + + let gas = *rep.get::().unwrap() as u64; + + Ok(gas) + } } diff --git a/bin/drk/src/transfer.rs b/bin/drk/src/transfer.rs index 495fda7ba..51045c590 100644 --- a/bin/drk/src/transfer.rs +++ b/bin/drk/src/transfer.rs @@ -24,12 +24,11 @@ use darkfi::{ Error, Result, }; use darkfi_money_contract::{ - client::{transfer_v1::make_transfer_call, OwnCoin}, - model::TokenId, - MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1, + client::transfer_v1::make_transfer_call, model::TokenId, MoneyFunction, + MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_FEE_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1, }; use darkfi_sdk::{ - crypto::{contract_id::MONEY_CONTRACT_ID, FuncId, Keypair, PublicKey}, + crypto::{contract_id::MONEY_CONTRACT_ID, Keypair, PublicKey}, tx::ContractCall, }; use darkfi_serial::AsyncEncodable; @@ -44,13 +43,8 @@ impl Drk { token_id: TokenId, recipient: PublicKey, ) -> Result { - // First get all unspent OwnCoins to see what our balance is. - let owncoins = self.get_coins(false).await?; - let mut owncoins: Vec = owncoins.iter().map(|x| x.0.clone()).collect(); - // We're only interested in the ones for the token_id we're sending - // And the ones not owned by some protocol (meaning spend-hook should be 0) - owncoins.retain(|x| x.note.token_id == token_id); - owncoins.retain(|x| x.note.spend_hook == FuncId::none()); + // First get all unspent OwnCoins to see what our balance is + let owncoins = self.get_token_coins(&token_id).await?; if owncoins.is_empty() { return Err(Error::Custom(format!("Did not find any coins with token ID: {token_id}"))) } @@ -74,12 +68,10 @@ impl Drk { let secret = self.default_secret().await?; let keypair = Keypair::new(secret); - let contract_id = *MONEY_CONTRACT_ID; - // Now we need to do a lookup for the zkas proof bincodes, and create // the circuit objects and proving keys so we can build the transaction. // We also do this through the RPC. - let zkas_bins = self.lookup_zkas(&contract_id).await?; + let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?; let Some(mint_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1) else { @@ -91,38 +83,71 @@ impl Drk { return Err(Error::Custom("Burn circuit not found".to_string())) }; + let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1) + else { + return Err(Error::Custom("Fee circuit not found".to_string())) + }; + let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?; let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?; + let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?; let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin); let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin); + let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin); - // Creating Mint and Burn circuit proving keys + // Creating Mint, Burn and Fee circuits proving keys let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit); let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit); + let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit); // Building transaction parameters let (params, secrets, spent_coins) = make_transfer_call( - keypair, recipient, amount, token_id, owncoins, tree, mint_zkbin, mint_pk, burn_zkbin, + keypair, + recipient, + amount, + token_id, + owncoins, + tree.clone(), + mint_zkbin, + mint_pk, + burn_zkbin, burn_pk, )?; - // Encode and sign the transaction + // Encode the call let mut data = vec![MoneyFunction::TransferV1 as u8]; params.encode_async(&mut data).await?; let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data }; + + // Create the TransactionBuilder containing the `Transfer` call let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs: secrets.proofs }, vec![])?; + + // We first have to execute the fee-less tx to gather its used gas, and then we feed + // it into the fee-creating function. + // We also tell it about any spent coins so we don't accidentally reuse them in the + // fee call. + // TODO: We have to build a proper coin selection algorithm so that we can utilize + // the Money::Transfer to merge any coins which would give us a coin with enough + // value for paying the transaction fee. let mut tx = tx_builder.build()?; let sigs = tx.create_sigs(&secrets.signature_secrets)?; - tx.signatures = vec![sigs]; + tx.signatures.push(sigs); - // We need to mark the coins we've spent in our wallet - for spent_coin in spent_coins { - if let Err(e) = self.mark_spent_coin(&spent_coin.coin).await { - return Err(Error::Custom(format!("Mark spent coin {spent_coin:?} failed: {e:?}"))) - }; - } + let (fee_call, fee_proofs, fee_secrets) = self + .append_fee_call(&tx, keypair.public, &tree, &fee_pk, &fee_zkbin, Some(&spent_coins)) + .await?; + + // Append the fee call to the transaction + tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?; + + // Now build the actual transaction and sign it with all necessary keys. + let mut tx = tx_builder.build()?; + let sigs = tx.create_sigs(&secrets.signature_secrets)?; + tx.signatures.push(sigs); + let sigs = tx.create_sigs(&fee_secrets)?; + tx.signatures.push(sigs); Ok(tx) } diff --git a/contrib/localnet/darkfid-single-node/README.md b/contrib/localnet/darkfid-single-node/README.md index e709cea4a..6d60f0de1 100644 --- a/contrib/localnet/darkfid-single-node/README.md +++ b/contrib/localnet/darkfid-single-node/README.md @@ -40,39 +40,39 @@ testnet user guide. Note: List is not exhaustive. Missing functionalities that are not part of the guide can be added for future regressions. -| # | Description | Command | Status | -|----|---------------------------|--------------------------------------------------|-------------------------| -| 0 | Initialization | wallet --initialize | Pass | -| 1 | Key generation | wallet --keygen | Pass | -| 2 | Set default wallet | wallet --default-address {ADDR_ID} | Pass | -| 3 | Default address retrieval | wallet --address | Pass | -| 4 | Block scanning | scan | Pass | -| 5 | Block subscribing | subscribe | Pass | -| 6 | Balance retrieval | wallet --balance | Pass | -| 7 | Aliases retrieval | alias show | Pass | -| 8 | Mint auth generation | token generate-mint | Pass | -| 9 | Mint auths retrieval | token list | Pass | -| 10 | Alias add | alias add {ALIAS} {TOKEN} | Pass | -| 11 | Aliases retrieval | alias show | Pass | -| 12 | Mint generation | token mint {ALIAS} {AMOUNT} {ADDR} | Failure: disabled | -| 13 | Transfer | transfer {AMOUNT} {ALIAS} {ADDR} | Failure: fee is missing | -| 14 | Coins retrieval | wallet --coins | Pass | -| 15 | OTC initialization | otc init -v {AMOUNT}:{AMOUNT} -t {ALIAS}:{ALIAS} | Failure: needs #12 | -| 16 | OTC join | otc join | Failure: needs #15 | -| 17 | OTC sign | otc sign | Failure: needs #16 | -| 18 | DAO create | dao create {LIMIT} {QUORUM} {RATIO} {TOKEN} | Failure: needs #12 | -| 19 | DAO view | dao view | Failure: needs #18 | -| 20 | DAO import | dao import | Failure: needs #18 | -| 21 | DAO list | dao sign | Failure: needs #18 | -| 22 | DAO mint | dao mint {DAO} | Failure: needs #18 | -| 23 | DAO balance | dao balance {DAO} | Failure: needs #18 | -| 24 | DAO propose | dao propose {DAO} {ADDR} {AMOUNT} {TOKEN} | Failure: needs #18 | -| 25 | DAO proposals retrieval | dao proposals {DAO} | Failure: needs #24 | -| 26 | DAO proposal retrieval | dao proposal {DAO} {PROPOSAL_ID} | Failure: needs #24 | -| 27 | DAO vote | dao vote {DAO} {PROPOSAL_ID} {VOTE} {WEIGHT} | Failure: needs #24 | -| 28 | DAO proposal execution | dao exec {DAO} {PROPOSAL_ID} | Failure: needs #27 | -| 29 | Coins unspend | unspend {COIN} | Pass | -| 30 | Transaction inspect | inspect | Pass | -| 31 | Transaction simulate | explorer simulate-tx | Pass | -| 31 | Transaction broadcast | broadcast | Pass | +| # | Description | Command | Status | +|----|---------------------------|--------------------------------------------------|--------------------| +| 0 | Initialization | wallet --initialize | Pass | +| 1 | Key generation | wallet --keygen | Pass | +| 2 | Set default wallet | wallet --default-address {ADDR_ID} | Pass | +| 3 | Default address retrieval | wallet --address | Pass | +| 4 | Block scanning | scan | Pass | +| 5 | Block subscribing | subscribe | Pass | +| 6 | Balance retrieval | wallet --balance | Pass | +| 7 | Aliases retrieval | alias show | Pass | +| 8 | Mint auth generation | token generate-mint | Pass | +| 9 | Mint auths retrieval | token list | Pass | +| 10 | Alias add | alias add {ALIAS} {TOKEN} | Pass | +| 11 | Aliases retrieval | alias show | Pass | +| 12 | Mint generation | token mint {ALIAS} {AMOUNT} {ADDR} | Failure: disabled | +| 13 | Transfer | transfer {AMOUNT} {ALIAS} {ADDR} | Pass | +| 14 | Coins retrieval | wallet --coins | Pass | +| 15 | OTC initialization | otc init -v {AMOUNT}:{AMOUNT} -t {ALIAS}:{ALIAS} | Failure: needs #12 | +| 16 | OTC join | otc join | Failure: needs #15 | +| 17 | OTC sign | otc sign | Failure: needs #16 | +| 18 | DAO create | dao create {LIMIT} {QUORUM} {RATIO} {TOKEN} | Failure: needs #12 | +| 19 | DAO view | dao view | Failure: needs #18 | +| 20 | DAO import | dao import | Failure: needs #18 | +| 21 | DAO list | dao sign | Failure: needs #18 | +| 22 | DAO mint | dao mint {DAO} | Failure: needs #18 | +| 23 | DAO balance | dao balance {DAO} | Failure: needs #18 | +| 24 | DAO propose | dao propose {DAO} {ADDR} {AMOUNT} {TOKEN} | Failure: needs #18 | +| 25 | DAO proposals retrieval | dao proposals {DAO} | Failure: needs #24 | +| 26 | DAO proposal retrieval | dao proposal {DAO} {PROPOSAL_ID} | Failure: needs #24 | +| 27 | DAO vote | dao vote {DAO} {PROPOSAL_ID} {VOTE} {WEIGHT} | Failure: needs #24 | +| 28 | DAO proposal execution | dao exec {DAO} {PROPOSAL_ID} | Failure: needs #27 | +| 29 | Coins unspend | unspend {COIN} | Pass | +| 30 | Transaction inspect | inspect | Pass | +| 31 | Transaction simulate | explorer simulate-tx | Pass | +| 31 | Transaction broadcast | broadcast | Pass | diff --git a/src/validator/mod.rs b/src/validator/mod.rs index 8c9fdeae3..5a4db5527 100644 --- a/src/validator/mod.rs +++ b/src/validator/mod.rs @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use darkfi_sdk::crypto::MerkleTree; use log::{debug, error, info, warn}; @@ -30,6 +30,7 @@ use crate::{ }, error::TxVerifyFailed, tx::Transaction, + zk::VerifyingKey, Error, Result, }; @@ -45,7 +46,7 @@ use pow::PoWModule; pub mod verification; use verification::{ verify_block, verify_checkpoint_block, verify_genesis_block, verify_producer_transaction, - verify_transactions, + verify_transaction, verify_transactions, }; /// Fee calculation helpers @@ -53,7 +54,7 @@ pub mod fees; /// Helper utilities pub mod utils; -use utils::{block_rank, deploy_native_contracts}; +use utils::{best_fork_index, block_rank, deploy_native_contracts}; /// Configuration for initializing [`Validator`] #[derive(Clone)] @@ -127,6 +128,41 @@ impl Validator { Ok(state) } + /// Auxiliary function to compute provided transaction's total gas, + /// against current best fork. + /// The function takes a boolean called `verify_fee` to overwrite + /// the nodes configured `verify_fees` flag. + pub async fn calculate_gas(&self, tx: &Transaction, verify_fee: bool) -> Result { + // Grab the best fork to verify against + let forks = self.consensus.forks.read().await; + let fork = &forks[best_fork_index(&forks)?]; + let overlay = fork.overlay.lock().unwrap().full_clone()?; + let next_block_height = fork.get_next_block_height()?; + drop(forks); + + // Map of ZK proof verifying keys for the transaction + let mut vks: HashMap<[u8; 32], HashMap> = HashMap::new(); + for call in &tx.calls { + vks.insert(call.data.contract_id.to_bytes(), HashMap::new()); + } + + // Verify transaction to grab the gas used + let verify_result = verify_transaction( + &overlay, + next_block_height, + tx, + &mut MerkleTree::new(1), + &mut vks, + verify_fee, + ) + .await; + + // Purge new trees + overlay.lock().unwrap().overlay.lock().unwrap().purge_new_trees()?; + + verify_result + } + /// The node retrieves a transaction, validates its state transition, /// and appends it to the pending txs store. pub async fn append_tx(&self, tx: &Transaction, write: bool) -> Result<()> {