Prechádzať zdrojové kódy

drk: added fee call to transfer

skoupidi 2 rokov pred
rodič
commit
07606d27e4

+ 2 - 2
bin/darkfid/src/error.rs

@@ -23,7 +23,7 @@ use darkfi::rpc::jsonrpc::{ErrorCode::ServerError, JsonError, JsonResult};
 pub enum RpcError {
 pub enum RpcError {
     // Transaction-related errors
     // Transaction-related errors
     TxSimulationFail = -32110,
     TxSimulationFail = -32110,
-    TxBroadcastFail = -32111,
+    TxGasCalculationFail = -32111,
 
 
     // State-related errors,
     // State-related errors,
     NotSynced = -32120,
     NotSynced = -32120,
@@ -43,7 +43,7 @@ fn to_tuple(e: RpcError) -> (i32, String) {
     let msg = match e {
     let msg = match e {
         // Transaction-related errors
         // Transaction-related errors
         RpcError::TxSimulationFail => "Failed simulating transaction state change",
         RpcError::TxSimulationFail => "Failed simulating transaction state change",
-        RpcError::TxBroadcastFail => "Failed broadcasting transaction",
+        RpcError::TxGasCalculationFail => "Failed to calculate transaction's gas",
         // State-related errors
         // State-related errors
         RpcError::NotSynced => "Blockchain is not synced",
         RpcError::NotSynced => "Blockchain is not synced",
         RpcError::UnknownBlockHeight => "Did not find block height",
         RpcError::UnknownBlockHeight => "Did not find block height",

+ 1 - 0
bin/darkfid/src/rpc.rs

@@ -73,6 +73,7 @@ impl RequestHandler for Darkfid {
             "tx.broadcast" => self.tx_broadcast(req.id, req.params).await,
             "tx.broadcast" => self.tx_broadcast(req.id, req.params).await,
             "tx.pending" => self.tx_pending(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.clean_pending" => self.tx_pending(req.id, req.params).await,
+            "tx.calculate_gas" => self.tx_calculate_gas(req.id, req.params).await,
 
 
             // ==============
             // ==============
             // Invalid method
             // Invalid method

+ 54 - 3
bin/darkfid/src/rpc_tx.rs

@@ -17,7 +17,7 @@
  */
  */
 
 
 use darkfi_serial::deserialize_async;
 use darkfi_serial::deserialize_async;
-use log::error;
+use log::{error, warn};
 use tinyjson::JsonValue;
 use tinyjson::JsonValue;
 
 
 use darkfi::{
 use darkfi::{
@@ -135,8 +135,7 @@ impl Darkfid {
 
 
         self.p2p.broadcast(&tx).await;
         self.p2p.broadcast(&tx).await;
         if self.p2p.hosts().channels().await.is_empty() {
         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();
         let tx_hash = tx.hash().to_string();
@@ -209,4 +208,56 @@ impl Darkfid {
 
 
         JsonResponse::new(JsonValue::Array(pending_txs), id).into()
         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::<Vec<JsonValue>>().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::<String>().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::<bool>().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()
+    }
 }
 }

+ 0 - 1
bin/drk/money.sql

@@ -1,6 +1,5 @@
 -- Wallet definitions for this contract.
 -- Wallet definitions for this contract.
 -- We store data that is needed to be able to receive and send tokens.
 -- 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
 -- Arbitrary info that is potentially useful
 CREATE TABLE IF NOT EXISTS BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o_money_info (
 CREATE TABLE IF NOT EXISTS BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o_money_info (

+ 233 - 76
bin/drk/src/money.rs

@@ -22,12 +22,22 @@ use lazy_static::lazy_static;
 use rand::rngs::OsRng;
 use rand::rngs::OsRng;
 use rusqlite::types::Value;
 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::{
 use darkfi_money_contract::{
-    client::{MoneyNote, OwnCoin},
+    client::{
+        compute_remainder_blind,
+        fee_v1::{create_fee_proof, FeeCallInput, FeeCallOutput, FEE_CALL_GAS},
+        MoneyNote, OwnCoin,
+    },
     model::{
     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,
     MoneyFunction,
 };
 };
@@ -38,8 +48,9 @@ use darkfi_sdk::{
         ScalarBlind, SecretKey, MONEY_CONTRACT_ID,
         ScalarBlind, SecretKey, MONEY_CONTRACT_ID,
     },
     },
     pasta::pallas,
     pasta::pallas,
+    ContractCall,
 };
 };
-use darkfi_serial::{deserialize_async, serialize_async};
+use darkfi_serial::{deserialize_async, serialize_async, AsyncEncodable};
 
 
 use crate::{
 use crate::{
     convert_named_params,
     convert_named_params,
@@ -401,93 +412,122 @@ impl Drk {
         };
         };
 
 
         let mut owncoins = Vec::with_capacity(rows.len());
         let mut owncoins = Vec::with_capacity(rows.len());
+        for row in rows {
+            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<Vec<OwnCoin>> {
+        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 {
         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?;
+            owncoins.push(self.parse_coin_record(&row).await?.0)
+        }
 
 
-            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;
+        Ok(owncoins)
+    }
 
 
-            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?;
+    /// 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::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::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 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 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 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 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 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 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 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 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 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 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 secret_bytes) = row[9] else {
-                return Err(Error::ParseFailed("[get_coins] Secret bytes parsing failed"))
-            };
-            let secret: SecretKey = deserialize_async(secret_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?;
 
 
-            // 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 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 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 secret_bytes) = row[9] else {
+            return Err(Error::ParseFailed("[parse_coin_record] Secret bytes parsing failed"))
+        };
+        let secret: SecretKey = deserialize_async(secret_bytes).await?;
 
 
-            let Value::Blob(ref memo) = row[12] else {
-                return Err(Error::ParseFailed("[get_coins] Memo parsing failed"))
-            };
+        // 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 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 };
+        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?;
 
 
-            owncoins.push((owncoin, is_spent))
-        }
+        let Value::Blob(ref memo) = row[12] else {
+            return Err(Error::ParseFailed("[parse_coin_record] Memo parsing failed"))
+        };
 
 
-        Ok(owncoins)
+        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.
     /// Create an alias record for provided Token ID.
@@ -642,7 +682,10 @@ impl Drk {
         match MoneyFunction::try_from(data[0])? {
         match MoneyFunction::try_from(data[0])? {
             MoneyFunction::FeeV1 => {
             MoneyFunction::FeeV1 => {
                 println!("[apply_tx_money_data] Found Money::FeeV1 call");
                 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 => {
             MoneyFunction::GenesisMintV1 => {
                 println!("[apply_tx_money_data] Found Money::GenesisMintV1 call");
                 println!("[apply_tx_money_data] Found Money::GenesisMintV1 call");
@@ -871,4 +914,118 @@ impl Drk {
         // Else parse input
         // Else parse input
         Ok(TokenId::from_str(input.as_str())?)
         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<Proof>, Vec<SecretKey>)> {
+        // 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(&note, &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]))
+    }
 }
 }

+ 18 - 4
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<BlockInfo> {
     async fn get_block_by_height(&self, height: u32) -> Result<BlockInfo> {
         let req = JsonRequest::new(
         let req = JsonRequest::new(
             "blockchain.get_block",
             "blockchain.get_block",
@@ -293,7 +293,7 @@ impl Drk {
     }
     }
 
 
     /// Broadcast a given transaction to darkfid and forward onto the network.
     /// 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<String> {
     pub async fn broadcast_tx(&self, tx: &Transaction) -> Result<String> {
         println!("Broadcasting transaction...");
         println!("Broadcasting transaction...");
 
 
@@ -314,7 +314,7 @@ impl Drk {
         Ok(txid)
         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<Option<Transaction>> {
     pub async fn get_tx(&self, tx_hash: &TransactionHash) -> Result<Option<Transaction>> {
         let tx_hash_str = tx_hash.to_string();
         let tx_hash_str = tx_hash.to_string();
         let req = JsonRequest::new(
         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<bool> {
     pub async fn simulate_tx(&self, tx: &Transaction) -> Result<bool> {
         let tx_str = base64::encode(&serialize_async(tx).await);
         let tx_str = base64::encode(&serialize_async(tx).await);
         let req =
         let req =
@@ -361,4 +361,18 @@ impl Drk {
 
 
         Ok(ret)
         Ok(ret)
     }
     }
+
+    /// Queries darkfid for given transaction's gas.
+    pub async fn get_tx_gas(&self, tx: &Transaction, include_fee: bool) -> Result<u64> {
+        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::<f64>().unwrap() as u64;
+
+        Ok(gas)
+    }
 }
 }

+ 49 - 24
bin/drk/src/transfer.rs

@@ -24,12 +24,11 @@ use darkfi::{
     Error, Result,
     Error, Result,
 };
 };
 use darkfi_money_contract::{
 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::{
 use darkfi_sdk::{
-    crypto::{contract_id::MONEY_CONTRACT_ID, FuncId, Keypair, PublicKey},
+    crypto::{contract_id::MONEY_CONTRACT_ID, Keypair, PublicKey},
     tx::ContractCall,
     tx::ContractCall,
 };
 };
 use darkfi_serial::AsyncEncodable;
 use darkfi_serial::AsyncEncodable;
@@ -44,13 +43,8 @@ impl Drk {
         token_id: TokenId,
         token_id: TokenId,
         recipient: PublicKey,
         recipient: PublicKey,
     ) -> Result<Transaction> {
     ) -> Result<Transaction> {
-        // First get all unspent OwnCoins to see what our balance is.
-        let owncoins = self.get_coins(false).await?;
-        let mut owncoins: Vec<OwnCoin> = 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() {
         if owncoins.is_empty() {
             return Err(Error::Custom(format!("Did not find any coins with token ID: {token_id}")))
             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 secret = self.default_secret().await?;
         let keypair = Keypair::new(secret);
         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
         // 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.
         // the circuit objects and proving keys so we can build the transaction.
         // We also do this through the RPC.
         // 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)
         let Some(mint_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1)
         else {
         else {
@@ -91,38 +83,71 @@ impl Drk {
             return Err(Error::Custom("Burn circuit not found".to_string()))
             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 mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?;
         let burn_zkbin = ZkBinary::decode(&burn_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 mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin);
         let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_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 mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit);
         let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit);
         let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit);
+        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
 
 
         // Building transaction parameters
         // Building transaction parameters
         let (params, secrets, spent_coins) = make_transfer_call(
         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,
             burn_pk,
         )?;
         )?;
 
 
-        // Encode and sign the transaction
+        // Encode the call
         let mut data = vec![MoneyFunction::TransferV1 as u8];
         let mut data = vec![MoneyFunction::TransferV1 as u8];
         params.encode_async(&mut data).await?;
         params.encode_async(&mut data).await?;
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
+
+        // Create the TransactionBuilder containing the `Transfer` call
         let mut tx_builder =
         let mut tx_builder =
             TransactionBuilder::new(ContractCallLeaf { call, proofs: secrets.proofs }, vec![])?;
             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 mut tx = tx_builder.build()?;
         let sigs = tx.create_sigs(&secrets.signature_secrets)?;
         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)
         Ok(tx)
     }
     }

+ 35 - 35
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
 Note: List is not exhaustive. Missing functionalities that are not part
 of the guide can be added for future regressions.
 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               |
 
 

+ 39 - 3
src/validator/mod.rs

@@ -16,7 +16,7 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
  */
 
 
-use std::sync::Arc;
+use std::{collections::HashMap, sync::Arc};
 
 
 use darkfi_sdk::crypto::MerkleTree;
 use darkfi_sdk::crypto::MerkleTree;
 use log::{debug, error, info, warn};
 use log::{debug, error, info, warn};
@@ -30,6 +30,7 @@ use crate::{
     },
     },
     error::TxVerifyFailed,
     error::TxVerifyFailed,
     tx::Transaction,
     tx::Transaction,
+    zk::VerifyingKey,
     Error, Result,
     Error, Result,
 };
 };
 
 
@@ -45,7 +46,7 @@ use pow::PoWModule;
 pub mod verification;
 pub mod verification;
 use verification::{
 use verification::{
     verify_block, verify_checkpoint_block, verify_genesis_block, verify_producer_transaction,
     verify_block, verify_checkpoint_block, verify_genesis_block, verify_producer_transaction,
-    verify_transactions,
+    verify_transaction, verify_transactions,
 };
 };
 
 
 /// Fee calculation helpers
 /// Fee calculation helpers
@@ -53,7 +54,7 @@ pub mod fees;
 
 
 /// Helper utilities
 /// Helper utilities
 pub mod utils;
 pub mod utils;
-use utils::{block_rank, deploy_native_contracts};
+use utils::{best_fork_index, block_rank, deploy_native_contracts};
 
 
 /// Configuration for initializing [`Validator`]
 /// Configuration for initializing [`Validator`]
 #[derive(Clone)]
 #[derive(Clone)]
@@ -127,6 +128,41 @@ impl Validator {
         Ok(state)
         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<u64> {
+        // 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<String, VerifyingKey>> = 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,
     /// The node retrieves a transaction, validates its state transition,
     /// and appends it to the pending txs store.
     /// and appends it to the pending txs store.
     pub async fn append_tx(&self, tx: &Transaction, write: bool) -> Result<()> {
     pub async fn append_tx(&self, tx: &Transaction, write: bool) -> Result<()> {