Browse Source

contract/money: burn part of the paid fee in FeeV1

brid 1 day ago
parent
commit
5586a0f59e

+ 5 - 5
bin/drk/src/money.rs

@@ -53,7 +53,7 @@ use darkfi_sdk::{
         BaseBlind, FuncId, MerkleNode, MerkleTree, ScalarBlind, MONEY_CONTRACT_ID,
     },
     dark_tree::DarkLeaf,
-    fee::minimum_fee,
+    fee::{burn_fee, minimum_fee, MONEY_FEE_CALLDATA_PREFIX_LEN},
     pasta::pallas,
     ContractCall,
 };
@@ -123,8 +123,6 @@ pub const MONEY_ALIASES_COL_TOKEN_ID: &str = "token_id";
 
 pub const BALANCE_BASE10_DECIMALS: usize = 8;
 
-const MONEY_FEE_PREFIX_LEN: usize = 9;
-
 fn parse_money_function(data: &[u8]) -> Result<MoneyFunction> {
     let Some(func) = data.first() else {
         return Err(Error::ParseFailed("money call data is empty"))
@@ -134,11 +132,11 @@ fn parse_money_function(data: &[u8]) -> Result<MoneyFunction> {
 }
 
 async fn parse_money_fee_params(data: &[u8]) -> Result<MoneyFeeParamsV1> {
-    if data.len() < MONEY_FEE_PREFIX_LEN {
+    if data.len() < MONEY_FEE_CALLDATA_PREFIX_LEN {
         return Err(Error::ParseFailed("money fee call data is too short"))
     }
 
-    Ok(deserialize_async(&data[MONEY_FEE_PREFIX_LEN..]).await?)
+    Ok(deserialize_async(&data[MONEY_FEE_CALLDATA_PREFIX_LEN..]).await?)
 }
 
 impl Drk {
@@ -1312,6 +1310,7 @@ impl Drk {
         let fee_call_fee = minimum_fee(FEE_CALL_GAS)?;
         let tx_fee = self.get_tx_fee(tx, false).await?;
         let required_fee = fee_call_fee.checked_add(tx_fee).ok_or(Error::AdditionOverflow)?;
+        let burned_fee = burn_fee(required_fee)?;
 
         // Knowing the total gas, we can now find an OwnCoin of enough value
         // so that we can create a valid Money::Fee call.
@@ -1405,6 +1404,7 @@ impl Drk {
         // Encode the contract call
         let mut data = vec![MoneyFunction::FeeV1 as u8];
         required_fee.encode_async(&mut data).await?;
+        burned_fee.encode_async(&mut data).await?;
         params.encode_async(&mut data).await?;
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
 

+ 7 - 2
src/contract/money/src/client/fee_v1.rs

@@ -37,8 +37,13 @@ use crate::{
     model::{CoinAttributes, Nullifier},
 };
 
-/// Fixed gas used by the fee call.
-/// This is the minimum gas any fee-paying transaction will use.
+/// Estimated gas used by the fee call. Actual usage depends on chain
+/// state (tree depths), so it cannot be exact. It must stay an upper
+/// bound: over-estimating is valid since the burn is enforced as a
+/// floor, under-estimating makes the transaction invalid.
+// TODO: If the fee call outgrows this estimate (deeper trees, gas
+// metering changes), fee verification will reject transactions and
+// the constant has to be bumped.
 pub const FEE_CALL_GAS: u64 = 42_000_000;
 
 /// Private values related to the Fee call

+ 1 - 1
src/contract/money/src/client/pow_reward_v1.rs

@@ -65,7 +65,7 @@ pub struct PoWRewardCallBuilder {
     pub signature_keypair: Keypair,
     /// Rewarded block height
     pub block_height: u32,
-    /// Rewarded block transactions paid fees
+    /// Miner-claimable fees accumulated for the rewarded height
     pub fees: u64,
     /// Optional recipient's public key, in case we want to mint to a different address
     pub recipient: Option<PublicKey>,

+ 2 - 2
src/contract/money/src/entrypoint.rs

@@ -181,8 +181,8 @@ fn init_contract(cid: ContractId, _ix: &[u8]) -> ContractResult {
         wasm::db::db_init(cid, MONEY_CONTRACT_TOKEN_FREEZE_TREE)?;
     }
 
-    // Set up a database tree to hold the fees paid for each block
-    // k=height_bytes, v=fees_paid_bytes
+    // Set up a database tree to hold miner-claimable fees for each block.
+    // k=height_bytes, v=miner_claimable_fees_bytes
     if wasm::db::db_lookup(cid, MONEY_CONTRACT_FEES_TREE).is_err() {
         let fees_db = wasm::db::db_init(cid, MONEY_CONTRACT_FEES_TREE)?;
         // Initialize the first two accumulators

+ 23 - 18
src/contract/money/src/entrypoint/fee_v1.rs

@@ -28,6 +28,7 @@ use darkfi_sdk::{
     },
     dark_tree::DarkLeaf,
     error::{ContractError, ContractResult},
+    fee::MONEY_FEE_CALLDATA_PREFIX_LEN,
     msg,
     pasta::pallas,
     wasm::{
@@ -50,18 +51,17 @@ use crate::{
     MONEY_CONTRACT_NULLIFIER_ROOTS_TREE, MONEY_CONTRACT_ZKAS_FEE_NS_V1,
 };
 
-const MONEY_FEE_PREFIX_LEN: usize = 9;
-
-fn parse_fee_call_data(data: &[u8]) -> Result<(u64, MoneyFeeParamsV1), ContractError> {
-    if data.len() < MONEY_FEE_PREFIX_LEN {
+fn parse_fee_call_data(data: &[u8]) -> Result<(u64, u64, MoneyFeeParamsV1), ContractError> {
+    if data.len() < MONEY_FEE_CALLDATA_PREFIX_LEN {
         msg!("[FeeV1] Error: Fee call data is too short");
         return Err(MoneyError::InvalidFeeCall.into())
     }
 
-    let fee = deserialize(&data[1..MONEY_FEE_PREFIX_LEN])?;
-    let params = deserialize(&data[MONEY_FEE_PREFIX_LEN..])?;
+    let fee = deserialize(&data[1..9])?;
+    let burned_fee = deserialize(&data[9..MONEY_FEE_CALLDATA_PREFIX_LEN])?;
+    let params = deserialize(&data[MONEY_FEE_CALLDATA_PREFIX_LEN..])?;
 
-    Ok((fee, params))
+    Ok((fee, burned_fee, params))
 }
 
 /// `get_metadata` function for `Money::FeeV1`
@@ -71,7 +71,7 @@ pub(crate) fn money_fee_get_metadata_v1(
     calls: Vec<DarkLeaf<ContractCall>>,
 ) -> Result<Vec<u8>, ContractError> {
     let self_ = &calls[call_idx].data;
-    let (_, params) = parse_fee_call_data(&self_.data)?;
+    let (_, _, params) = parse_fee_call_data(&self_.data)?;
 
     // Public inputs for the ZK proofs we have to verify
     let mut zk_public_inputs: Vec<(String, Vec<pallas::Base>)> = vec![];
@@ -115,14 +115,19 @@ pub(crate) fn money_fee_process_instruction_v1(
     calls: Vec<DarkLeaf<ContractCall>>,
 ) -> Result<Vec<u8>, ContractError> {
     let self_ = &calls[call_idx];
-    let (fee, params) = parse_fee_call_data(&self_.data.data)?;
+    let (paid_fee, burned_fee, params) = parse_fee_call_data(&self_.data.data)?;
 
     // We should have _some_ fee paid...
-    if fee == 0 {
+    if paid_fee == 0 {
         msg!("[FeeV1] Error: Paid fee is 0");
         return Err(MoneyError::InsufficientFee.into())
     }
 
+    let Some(miner_claimable_fee) = paid_fee.checked_sub(burned_fee) else {
+        msg!("[FeeV1] Error: Burned fee exceeds paid fee");
+        return Err(MoneyError::InvalidFeeCall.into())
+    };
+
     // Access the necessary databases where there is information to
     // validate this state transition.
     let coins_db = db_lookup(cid, MONEY_CONTRACT_COINS_TREE)?;
@@ -195,8 +200,8 @@ pub(crate) fn money_fee_process_instruction_v1(
     // Subtract the output value commitment
     valcom_total -= params.output.value_commit;
 
-    // Now subtract the fee from the accumulator
-    valcom_total -= pedersen_commitment_u64(fee, params.fee_value_blind);
+    // Now subtract the paid fee from the accumulator.
+    valcom_total -= pedersen_commitment_u64(paid_fee, params.fee_value_blind);
 
     // If the accumulator is not back in its initial; state, that means there
     // is a value mismatch betweeen inputs and outputs.
@@ -205,15 +210,15 @@ pub(crate) fn money_fee_process_instruction_v1(
         return Err(MoneyError::ValueMismatch.into())
     }
 
-    // Accumulate the height paid fee
+    // Accumulate the height miner-claimable fee.
     let verifying_block_height = wasm::util::get_verifying_block_height()?;
-    let Some(paid_fee) = db_get(fees_db, &serialize(&verifying_block_height))? else {
+    let Some(accumulated_fees) = db_get(fees_db, &serialize(&verifying_block_height))? else {
         msg!("[FeeV1] Error: Block height fees accumulator not found");
         return Err(MoneyError::PoWRewardCallMissingFeesAccumulator.into())
     };
-    let paid_fee: u64 = deserialize(&paid_fee)?;
-    let Some(paid_fee) = paid_fee.checked_add(fee) else {
-        msg!("[FeeV1] Error: Could not compute paid fee");
+    let accumulated_fees: u64 = deserialize(&accumulated_fees)?;
+    let Some(accumulated_fees) = accumulated_fees.checked_add(miner_claimable_fee) else {
+        msg!("[FeeV1] Error: Could not compute accumulated miner-claimable fee");
         return Err(MoneyError::ValueMismatch.into())
     };
 
@@ -223,7 +228,7 @@ pub(crate) fn money_fee_process_instruction_v1(
         coin: params.output.coin,
         tx_local: params.output.tx_local,
         height: verifying_block_height,
-        fee: paid_fee,
+        fee: accumulated_fees,
     };
     // and return it
     Ok(serialize(&update))

+ 7 - 5
src/contract/money/src/entrypoint/pow_reward_v1.rs

@@ -118,17 +118,19 @@ pub(crate) fn money_pow_reward_process_instruction_v1(
         return Err(MoneyError::TransferClearInputNonNativeToken.into())
     }
 
-    // Grab the currect height accumulated fees
+    // Grab the current height accumulated miner-claimable fees.
     let fees_db = wasm::db::db_lookup(cid, MONEY_CONTRACT_FEES_TREE)?;
-    let Some(paid_fee) = wasm::db::db_get(fees_db, &serialize(&verifying_block_height))? else {
+    let fees = wasm::db::db_get(fees_db, &serialize(&verifying_block_height))?;
+    let Some(miner_claimable_fees) = fees else {
         msg!("[PoWRewardV1] Error: Block height fees accumulator not found");
         return Err(MoneyError::PoWRewardCallMissingFeesAccumulator.into())
     };
-    let paid_fee: u64 = deserialize(&paid_fee)?;
+    let miner_claimable_fees: u64 = deserialize(&miner_claimable_fees)?;
 
     // Verify reward value matches the expected one for this block height,
-    // including the paid fees.
-    let Some(expected_reward) = expected_reward(verifying_block_height).checked_add(paid_fee)
+    // including miner-claimable fees accumulated by FeeV1.
+    let Some(expected_reward) =
+        expected_reward(verifying_block_height).checked_add(miner_claimable_fees)
     else {
         msg!("[PoWRewardV1] Error: Could not compute expected reward");
         return Err(MoneyError::ValueMismatch.into())

+ 1 - 1
src/contract/money/src/model/mod.rs

@@ -194,7 +194,7 @@ pub struct MoneyFeeUpdateV1 {
     pub tx_local: bool,
     /// Block height the fee was verified against
     pub height: u32,
-    /// Height accumulated fee paid
+    /// Height accumulated miner-claimable fee
     pub fee: u64,
 }
 

+ 2 - 1
src/contract/money/tests/dep8.rs

@@ -39,7 +39,7 @@ use darkfi_sdk::{
         contract_id::MONEY_CONTRACT_ID, note::AeadEncryptedNote, BaseBlind, FuncId, MerkleNode,
         MerkleTree, ScalarBlind, SecretKey,
     },
-    fee::minimum_fee,
+    fee::{burn_fee, minimum_fee},
     pasta::pallas,
     ContractCall,
 };
@@ -270,6 +270,7 @@ fn dep8() -> Result<()> {
         // Encode the contract call
         let mut data = vec![MoneyFunction::FeeV1 as u8];
         required_fee.encode_async(&mut data).await?;
+        burn_fee(required_fee)?.encode_async(&mut data).await?;
         fee_call_params.encode_async(&mut data).await?;
         let fee_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
 

+ 4 - 1
src/contract/test-harness/src/money_fee.rs

@@ -37,7 +37,7 @@ use darkfi_sdk::{
         contract_id::MONEY_CONTRACT_ID, note::AeadEncryptedNote, BaseBlind, Blind, FuncId,
         ScalarBlind, SecretKey,
     },
-    fee::minimum_fee,
+    fee::{burn_fee, minimum_fee},
     pasta::pallas,
     ContractCall,
 };
@@ -146,6 +146,7 @@ impl TestHarness {
 
         let mut data = vec![MoneyFunction::FeeV1 as u8];
         required_fee.encode(&mut data)?;
+        burn_fee(required_fee)?.encode(&mut data)?;
         params.encode(&mut data)?;
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
         let mut tx_builder =
@@ -203,6 +204,7 @@ impl TestHarness {
         // Compute the required fee
         let fee_gas = gas_used.checked_add(FEE_CALL_GAS).ok_or(darkfi::Error::AdditionOverflow)?;
         let required_fee = minimum_fee(fee_gas)?;
+        let burned_fee = burn_fee(required_fee)?;
 
         // Knowing the total gas, we can now find an OwnCoin of enough
         // value so that we can create a valid Money::Fee call.
@@ -296,6 +298,7 @@ impl TestHarness {
         // Encode the contract call
         let mut data = vec![MoneyFunction::FeeV1 as u8];
         required_fee.encode(&mut data)?;
+        burned_fee.encode(&mut data)?;
         params.encode(&mut data)?;
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
 

+ 7 - 2
src/sdk/python/src/contract/money/mod.rs

@@ -19,7 +19,7 @@
 use std::fmt::Write;
 
 use darkfi_money_contract::{model as money_model, MoneyFunction};
-use darkfi_sdk::crypto::util::FieldElemAsStr;
+use darkfi_sdk::{crypto::util::FieldElemAsStr, fee::MONEY_FEE_CALLDATA_PREFIX_LEN};
 use darkfi_serial::deserialize;
 use pyo3::{
     prelude::{PyAnyMethods, PyDictMethods, PyModule, PyModuleMethods},
@@ -71,7 +71,12 @@ pub fn decode_money_function_params(
 ) -> darkfi::Result<Box<dyn FunctionParams>> {
     let res: Box<dyn FunctionParams> = match MoneyFunction::try_from(function_index)? {
         MoneyFunction::FeeV1 => {
-            let params: money_model::MoneyFeeParamsV1 = deserialize(&data[9..])?;
+            if data.len() < MONEY_FEE_CALLDATA_PREFIX_LEN {
+                return Err(darkfi::Error::ParseFailed("money fee call data is too short"))
+            }
+
+            let params: money_model::MoneyFeeParamsV1 =
+                deserialize(&data[MONEY_FEE_CALLDATA_PREFIX_LEN..])?;
             Box::new(params)
         }
         MoneyFunction::GenesisMintV1 => {

+ 7 - 1
src/sdk/src/fee.rs

@@ -21,6 +21,10 @@ use crate::error::{FeeError, FeeResult};
 /// Fixed consensus fee charged per gas unit for the initial fee-burning testnet.
 pub const FEE_PER_GAS: u64 = 5;
 
+/// Length of the `Money::FeeV1` calldata prefix:
+/// `[function_id][paid_fee][burned_fee]`.
+pub const MONEY_FEE_CALLDATA_PREFIX_LEN: usize = 17;
+
 /// Numerator for the mandatory burn ratio applied to the minimum fee.
 pub const BURN_NUM: u64 = 3;
 
@@ -60,7 +64,9 @@ pub fn burn_fee(minimum_fee: u64) -> FeeResult<u64> {
     burn_fee_with_constants(minimum_fee, BURN_NUM, BURN_DEN)
 }
 
-/// Compute the miner-claimable fee from the paid fee and mandatory burn.
+/// Compute the miner-claimable fee from the paid fee and the declared burn.
+/// Note the declared burn may exceed the mandatory burn, so this is only
+/// the mandatory minimum when the burn is declared exactly.
 pub fn miner_claimable_fee(paid_fee: u64, burned_fee: u64) -> FeeResult<u64> {
     paid_fee.checked_sub(burned_fee).ok_or(FeeError::InsufficientFee)
 }

+ 29 - 8
src/sdk/src/tx.rs

@@ -27,10 +27,17 @@ use darkfi_serial::{deserialize, SerialDecodable, SerialEncodable};
 
 use super::{
     crypto::{ContractId, SecretKey},
+    fee::MONEY_FEE_CALLDATA_PREFIX_LEN,
     ContractError, FeeError, FeeResult, GenericResult,
 };
 use crate::crypto::{DAO_CONTRACT_ID, DEPLOYOOOR_CONTRACT_ID, MONEY_CONTRACT_ID};
 
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub struct MoneyFeeValues {
+    pub paid_fee: u64,
+    pub burned_fee: u64,
+}
+
 #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, SerialEncodable, SerialDecodable)]
 // We have to introduce a type rather than using an alias so we can implement Display
 pub struct TransactionHash(pub [u8; 32]);
@@ -89,13 +96,22 @@ impl ContractCall {
         self.matches_contract_call_type(*MONEY_CONTRACT_ID, 0x00)
     }
 
-    /// Returns the paid native token fee encoded in a `Money::FeeV1` call.
-    pub fn money_fee_value(&self) -> FeeResult<u64> {
-        if !self.is_money_fee() || self.data.len() < 9 {
+    /// Returns the public fee values encoded in a `Money::FeeV1` call.
+    pub fn money_fee_values(&self) -> FeeResult<MoneyFeeValues> {
+        if !self.is_money_fee() || self.data.len() < MONEY_FEE_CALLDATA_PREFIX_LEN {
             return Err(FeeError::InvalidFeeCall)
         }
 
-        deserialize(&self.data[1..9]).map_err(|_| FeeError::InvalidFeeCall)
+        let paid_fee = deserialize(&self.data[1..9]).map_err(|_| FeeError::InvalidFeeCall)?;
+        let burned_fee = deserialize(&self.data[9..MONEY_FEE_CALLDATA_PREFIX_LEN])
+            .map_err(|_| FeeError::InvalidFeeCall)?;
+
+        Ok(MoneyFeeValues { paid_fee, burned_fee })
+    }
+
+    /// Returns the paid native token fee encoded in a `Money::FeeV1` call.
+    pub fn money_fee_value(&self) -> FeeResult<u64> {
+        Ok(self.money_fee_values()?.paid_fee)
     }
 
     /// Returns true if call is a money genesis mint.
@@ -177,21 +193,25 @@ mod tests {
     use crate::crypto::DAO_CONTRACT_ID;
 
     #[test]
-    fn money_fee_value_extracts_paid_fee() {
-        let fee = 20_u64;
+    fn money_fee_values_extract_public_values() {
+        let paid_fee = 20_u64;
+        let burned_fee = 15_u64;
         let mut data = vec![0x00];
-        data.extend(serialize(&fee));
+        data.extend(serialize(&paid_fee));
+        data.extend(serialize(&burned_fee));
         data.extend([0xab, 0xcd]);
 
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
 
-        assert_eq!(call.money_fee_value().unwrap(), fee);
+        assert_eq!(call.money_fee_values().unwrap(), MoneyFeeValues { paid_fee, burned_fee });
+        assert_eq!(call.money_fee_value().unwrap(), paid_fee);
     }
 
     #[test]
     fn money_fee_value_rejects_wrong_contract() {
         let mut data = vec![0x00];
         data.extend(serialize(&20_u64));
+        data.extend(serialize(&15_u64));
 
         let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
 
@@ -202,6 +222,7 @@ mod tests {
     fn money_fee_value_rejects_wrong_function() {
         let mut data = vec![0x01];
         data.extend(serialize(&20_u64));
+        data.extend(serialize(&15_u64));
 
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
 

+ 41 - 3
src/validator/verification.rs

@@ -26,7 +26,7 @@ use darkfi_sdk::{
     },
     dark_tree::dark_forest_leaf_vec_integrity_check,
     deploy::DeployParamsV1,
-    fee::minimum_fee,
+    fee::{burn_fee, minimum_fee},
     pasta::pallas,
 };
 use darkfi_serial::{deserialize_async, serialize_async, AsyncDecodable, AsyncEncodable};
@@ -889,8 +889,8 @@ pub async fn verify_transaction(
     }
 
     if verify_fee {
-        // Extract the paid fee from the fee call.
-        let fee = match tx.calls[fee_call_idx].data.money_fee_value() {
+        // Extract the public fee values from the fee call.
+        let fee_values = match tx.calls[fee_call_idx].data.money_fee_values() {
             Ok(v) => v,
             Err(e) => {
                 error!(
@@ -901,6 +901,20 @@ pub async fn verify_transaction(
             }
         };
 
+        // The paid fee must be nonzero, and the burned fee declared in
+        // calldata cannot exceed the paid fee.
+        if fee_values.paid_fee == 0 || fee_values.burned_fee > fee_values.paid_fee {
+            error!(
+                target: "validator::verification::verify_transaction",
+                "[VALIDATOR] Transaction {tx_hash} fee call declares invalid public fee values: paid {}, burned {}",
+                fee_values.paid_fee,
+                fee_values.burned_fee
+            );
+            return Err(TxVerifyFailed::InvalidFee.into())
+        }
+
+        let fee = fee_values.paid_fee;
+
         // Compute the required fee for this transaction
         let required_fee = match minimum_fee(total_gas_used) {
             Ok(fee) => fee,
@@ -922,6 +936,30 @@ pub async fn verify_transaction(
             );
             return Err(TxVerifyFailed::InsufficientFee.into())
         }
+
+        // Check that the burned fee declared in the fee call is not less
+        // than the mandatory burn for this transaction's required fee.
+        // Over-declaring is allowed so that conservative fee estimates,
+        // which cannot predict final gas exactly, remain valid.
+        let expected_burned_fee = match burn_fee(required_fee) {
+            Ok(fee) => fee,
+            Err(e) => {
+                error!(
+                    target: "validator::verification::verify_transaction",
+                    "[VALIDATOR] Failed calculating tx {tx_hash} burned fee: {e}"
+                );
+                return Err(TxVerifyFailed::InvalidFee.into())
+            }
+        };
+
+        if fee_values.burned_fee < expected_burned_fee {
+            error!(
+                target: "validator::verification::verify_transaction",
+                "[VALIDATOR] Transaction {tx_hash} burned fee is below the mandatory burn. Mandatory: {expected_burned_fee}, Declared: {}",
+                fee_values.burned_fee
+            );
+            return Err(TxVerifyFailed::InvalidFee.into())
+        }
         debug!(target: "validator::verification::verify_transaction", "The gas paid for transaction {tx_hash}: {}", gas_data.paid);
 
         // Store paid fee