Procházet zdrojové kódy

validator: apply miner-claimable fee accounting

brid před 1 týdnem
rodič
revize
060655d4ec

+ 1 - 1
script/research/gg/src/main.rs

@@ -156,7 +156,7 @@ fn main() -> Result<()> {
                     let file = file?;
                     let bytes = base64::decode(read_to_string(file.path())?.trim()).unwrap();
                     let tx = deserialize_async(&bytes).await?;
-                    apply_transaction(&overlay, 0, pow_target, &tx, &mut tree).await?;
+                    apply_transaction(&overlay, 0, pow_target, &tx, &mut tree, false).await?;
                     genesis_block.txs.push(tx);
                 }
 

+ 6 - 5
src/validator/consensus.rs

@@ -21,7 +21,7 @@ use std::{
     str::FromStr,
 };
 
-use darkfi_sdk::{crypto::MerkleTree, tx::TransactionHash};
+use darkfi_sdk::{crypto::MerkleTree, fee::accumulate_fee, tx::TransactionHash};
 use darkfi_serial::{async_trait, SerialDecodable, SerialEncodable};
 use kvdb_overlay::DatabaseOverlayStateDiff;
 use num_bigint::BigUint;
@@ -750,7 +750,7 @@ impl Fork {
     }
 
     /// Auxiliary function to retrieve unproposed valid transactions,
-    /// along with their total gas used and total paid fees. Erroneous
+    /// along with their total gas used and total miner-claimable fees. Erroneous
     /// transactions will be removed from the database.
     ///
     /// Note: Always remember to purge new trees from the database if
@@ -770,7 +770,7 @@ impl Fork {
 
         // Total gas accumulators
         let mut total_gas_used = 0_u64;
-        let mut total_gas_paid = 0_u64;
+        let mut total_miner_claimable = 0_u64;
 
         // Map of ZK proof verifying keys for the current transaction
         // batch.
@@ -834,7 +834,8 @@ impl Fork {
 
             // Update accumulated total gas
             total_gas_used = total_gas_used.saturating_add(tx_gas_used);
-            total_gas_paid = total_gas_paid.saturating_add(gas_data.paid);
+            total_miner_claimable =
+                accumulate_fee(total_miner_claimable, gas_data.miner_claimable)?;
 
             // Push the tx hash into the unproposed transactions vector
             unproposed_txs.push(tx);
@@ -843,7 +844,7 @@ impl Fork {
         // Remove erroneous transactions from mempool
         self.blockchain.remove_pending_txs_hashes(&erroneous_txs)?;
 
-        Ok((unproposed_txs, total_gas_used, total_gas_paid))
+        Ok((unproposed_txs, total_gas_used, total_miner_claimable))
     }
 
     /// Auxiliary function to create a full clone using

+ 40 - 2
src/validator/fees.rs

@@ -16,9 +16,12 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use darkfi_serial::{async_trait, SerialDecodable, SerialEncodable};
+use darkfi_sdk::{crypto::MONEY_CONTRACT_ID, fee::accumulate_fee};
+use darkfi_serial::{async_trait, deserialize, serialize, SerialDecodable, SerialEncodable};
 
-use crate::zkas::ZkBinary;
+use crate::{blockchain::BlockchainOverlayPtr, zkas::ZkBinary, Error, Result};
+
+const MONEY_CONTRACT_FEES_TREE: &str = "fees";
 
 /// Fixed fee for verifying a Schnorr signature over the Pallas curve.
 pub const PALLAS_SCHNORR_VERIFY_GAS: u64 = 1850;
@@ -57,6 +60,38 @@ pub fn circuit_gas_use(zkbin: &ZkBinary) -> u64 {
     VERIFY_GAS_PER_ROW.saturating_mul(rows)
 }
 
+/// Add miner-claimable fees to the existing money contract height accumulator.
+///
+/// This is intentionally narrow: it can only update the money contract's `fees`
+/// tree at the current block height key, and it requires that the accumulator
+/// entry already exists.
+pub fn add_miner_claimable_fee(
+    overlay: &BlockchainOverlayPtr,
+    verifying_block_height: u32,
+    miner_claimable_fee: u64,
+) -> Result<()> {
+    let (overlay, fees_tree) = {
+        let blockchain = overlay.lock().unwrap();
+        let fees_tree =
+            blockchain.contracts.lookup(&MONEY_CONTRACT_ID, MONEY_CONTRACT_FEES_TREE)?;
+        (blockchain.overlay.clone(), blake3::Hash::from(fees_tree).to_string())
+    };
+
+    let key = serialize(&verifying_block_height);
+    let mut overlay = overlay.lock().unwrap();
+    let Some(existing_value) = overlay.get(&fees_tree, &key)? else {
+        return Err(Error::DatabaseError(format!(
+            "Money fee accumulator for height {verifying_block_height} not found"
+        )))
+    };
+
+    let existing_value: u64 = deserialize(&existing_value)?;
+    let updated_value = accumulate_fee(existing_value, miner_claimable_fee)?;
+
+    overlay.insert(&fees_tree, &key, &serialize(&updated_value))?;
+    Ok(())
+}
+
 /// Auxiliary struct representing the full gas usage breakdown of a
 /// transaction.
 ///
@@ -74,6 +109,8 @@ pub struct GasData {
     pub deployments: u64,
     /// Transaction paid fee
     pub paid: u64,
+    /// Transaction miner-claimable fee after burn
+    pub miner_claimable: u64,
 }
 
 impl GasData {
@@ -98,6 +135,7 @@ impl std::fmt::Debug for GasData {
             .field("signatures", &self.signatures)
             .field("deployments", &self.deployments)
             .field("paid", &self.paid)
+            .field("miner_claimable", &self.miner_claimable)
             .finish()
     }
 }

+ 10 - 3
src/validator/mod.rs

@@ -354,8 +354,15 @@ impl Validator {
         // Validate and insert each block
         for (index, block) in blocks.iter().enumerate() {
             // Verify block
-            match verify_checkpoint_block(&overlay, &diffs, block, &headers[index], module.target)
-                .await
+            match verify_checkpoint_block(
+                &overlay,
+                &diffs,
+                block,
+                &headers[index],
+                module.target,
+                self.verify_fees,
+            )
+            .await
             {
                 Ok(()) => { /* Do nothing */ }
                 // Skip already existing block
@@ -545,7 +552,7 @@ impl Validator {
     /// to the database, and a boolean called `verify_fees` to
     /// overwrite the nodes configured `verify_fees` flag.
     ///
-    /// Returns the total gas used and total paid fees for the given
+    /// Returns the total gas used and total miner-claimable fees for the given
     /// transactions.
     ///
     /// Note: This function should only be used in tests and always

+ 374 - 96
src/validator/verification.rs

@@ -26,8 +26,9 @@ use darkfi_sdk::{
     },
     dark_tree::dark_forest_leaf_vec_integrity_check,
     deploy::DeployParamsV1,
-    fee::minimum_fee,
+    fee::{accumulate_fee, burn_fee, miner_claimable_fee, minimum_fee},
     pasta::pallas,
+    tx::TransactionHash,
 };
 use darkfi_serial::{deserialize_async, serialize_async, AsyncDecodable, AsyncEncodable};
 use kvdb_overlay::DatabaseOverlayStateDiff;
@@ -47,7 +48,7 @@ use crate::{
     util::time::Timestamp,
     validator::{
         consensus::{Consensus, Fork, Proposal, BLOCK_GAS_LIMIT},
-        fees::{circuit_gas_use, GasData, PALLAS_SCHNORR_VERIFY_GAS},
+        fees::{add_miner_claimable_fee, circuit_gas_use, GasData, PALLAS_SCHNORR_VERIFY_GAS},
         pow::PoWModule,
     },
     zk::VerifyingKey,
@@ -324,6 +325,7 @@ pub async fn verify_checkpoint_block(
     block: &BlockInfo,
     header: &HeaderHash,
     block_target: u32,
+    verify_fees: bool,
 ) -> Result<()> {
     let block_hash = block.hash();
     debug!(target: "validator::verification::verify_checkpoint_block", "Validating block {block_hash}");
@@ -349,7 +351,8 @@ pub async fn verify_checkpoint_block(
     let mut tree = MerkleTree::new(1);
     let txs = &block.txs[..block.txs.len() - 1];
     if let Err(e) =
-        apply_transactions(overlay, block.header.height, block_target, txs, &mut tree).await
+        apply_transactions(overlay, block.header.height, block_target, txs, &mut tree, verify_fees)
+            .await
     {
         warn!(
             target: "validator::verification::verify_checkpoint_block",
@@ -406,6 +409,122 @@ pub fn verify_producer_signature(block: &BlockInfo, public_key: &PublicKey) -> R
     Ok(())
 }
 
+fn extract_fee_call(
+    tx_hash: &TransactionHash,
+    tx: &Transaction,
+    verify_fee: bool,
+) -> Result<Option<(usize, u64)>> {
+    if !verify_fee {
+        return Ok(None)
+    }
+
+    let mut fee_call = None;
+
+    for (call_idx, call) in tx.calls.iter().enumerate() {
+        if !call.data.is_money_fee() {
+            continue
+        }
+
+        if fee_call.is_some() {
+            error!(
+                target: "validator::verification::extract_fee_call",
+                "[VALIDATOR] Transaction {tx_hash} contains multiple fee payment calls"
+            );
+            return Err(TxVerifyFailed::InvalidFee.into())
+        }
+
+        let fee = match call.data.money_fee_value() {
+            Ok(fee) => fee,
+            Err(e) => {
+                error!(
+                    target: "validator::verification::extract_fee_call",
+                    "[VALIDATOR] Failed parsing tx {tx_hash} fee call: {e}"
+                );
+                return Err(TxVerifyFailed::InvalidFee.into())
+            }
+        };
+
+        if fee == 0 {
+            error!(
+                target: "validator::verification::extract_fee_call",
+                "[VALIDATOR] Transaction {tx_hash} contains zero fee payment"
+            );
+            return Err(TxVerifyFailed::InvalidFee.into())
+        }
+
+        fee_call = Some((call_idx, fee));
+    }
+
+    if fee_call.is_none() {
+        error!(
+            target: "validator::verification::extract_fee_call",
+            "[VALIDATOR] Transaction {tx_hash} does not contain fee payment call"
+        );
+        return Err(TxVerifyFailed::InvalidFee.into())
+    }
+
+    Ok(fee_call)
+}
+
+fn calculate_miner_claimable_fee(
+    tx_hash: &TransactionHash,
+    total_gas_used: u64,
+    fee: u64,
+) -> Result<u64> {
+    let required_fee = match minimum_fee(total_gas_used) {
+        Ok(fee) => fee,
+        Err(e) => {
+            error!(
+                target: "validator::verification::calculate_miner_claimable_fee",
+                "[VALIDATOR] Failed calculating tx {tx_hash} fee: {e}"
+            );
+            return Err(TxVerifyFailed::InvalidFee.into())
+        }
+    };
+
+    if required_fee > fee {
+        error!(
+            target: "validator::verification::calculate_miner_claimable_fee",
+            "[VALIDATOR] Transaction {tx_hash} has insufficient fee. \
+             Required: {required_fee}, Paid: {fee}"
+        );
+        return Err(TxVerifyFailed::InsufficientFee.into())
+    }
+
+    let burned_fee = match burn_fee(required_fee) {
+        Ok(fee) => fee,
+        Err(e) => {
+            error!(
+                target: "validator::verification::calculate_miner_claimable_fee",
+                "[VALIDATOR] Failed calculating tx {tx_hash} burned fee: {e}"
+            );
+            return Err(TxVerifyFailed::InvalidFee.into())
+        }
+    };
+
+    let claimable_fee = match miner_claimable_fee(fee, burned_fee) {
+        Ok(fee) => fee,
+        Err(e) => {
+            error!(
+                target: "validator::verification::calculate_miner_claimable_fee",
+                "[VALIDATOR] Failed calculating tx {tx_hash} miner-claimable fee: {e}"
+            );
+            return Err(TxVerifyFailed::InvalidFee.into())
+        }
+    };
+
+    let tip = fee - required_fee;
+
+    debug!(target: "validator::verification::calculate_miner_claimable_fee", "The fee paid for transaction {tx_hash}: {fee}");
+    debug!(
+        target: "validator::verification::calculate_miner_claimable_fee",
+        "The fee split for transaction {tx_hash}: required={required_fee}, \
+         burned={burned_fee}, miner_claimable={claimable_fee}, tip={tip}"
+    );
+
+    Ok(claimable_fee)
+}
+
 /// Verify provided producer [`Transaction`].
 ///
 /// Verify WASM execution, signatures, and ZK proofs and apply it to
@@ -670,38 +789,7 @@ pub async fn verify_transaction(
     // Table of public keys used for signature verification
     let mut sig_table = vec![];
 
-    // Index of the Fee-paying call
-    let mut fee_call_idx = 0;
-
-    if verify_fee {
-        // Verify that there is a single money fee call in the
-        // transaction.
-        let mut found_fee = false;
-        for (call_idx, call) in tx.calls.iter().enumerate() {
-            if !call.data.is_money_fee() {
-                continue
-            }
-
-            if found_fee {
-                error!(
-                    target: "validator::verification::verify_transcation",
-                    "[VALIDATOR] Transaction {tx_hash} contains multiple fee payment calls"
-                );
-                return Err(TxVerifyFailed::InvalidFee.into())
-            }
-
-            found_fee = true;
-            fee_call_idx = call_idx;
-        }
-
-        if !found_fee {
-            error!(
-                target: "validator::verification::verify_transcation",
-                "[VALIDATOR] Transaction {tx_hash} does not contain fee payment call"
-            );
-            return Err(TxVerifyFailed::InvalidFee.into())
-        }
-    }
+    let fee_call = extract_fee_call(&tx_hash, tx, verify_fee)?;
 
     // Write the transaction calls payload data
     let mut payload = vec![];
@@ -792,14 +880,13 @@ pub async fn verify_transaction(
             // existing circuit. Might be a smart idea to do so in
             // order to have to care less about being able to verify
             // historical txs.
-            if inner_vk_map.contains_key(zkas_ns.as_str()) {
-                continue
-            }
-
             let (zkbin, vk) =
                 overlay.lock().unwrap().contracts.get_zkas(&call.data.contract_id, zkas_ns)?;
 
-            inner_vk_map.insert(zkas_ns.to_string(), vk);
+            if !inner_vk_map.contains_key(zkas_ns.as_str()) {
+                inner_vk_map.insert(zkas_ns.to_string(), vk);
+            }
+
             circuits_to_verify.push(zkbin);
         }
 
@@ -888,45 +975,14 @@ pub async fn verify_transaction(
         return Err(TxVerifyFailed::GasLimitExceeded.into())
     }
 
-    if verify_fee {
-        // Extract the paid fee from the fee call.
-        let fee = match tx.calls[fee_call_idx].data.money_fee_value() {
-            Ok(v) => v,
-            Err(e) => {
-                error!(
-                    target: "validator::verification::verify_transaction",
-                    "[VALIDATOR] Failed parsing tx {tx_hash} fee call: {e}"
-                );
-                return Err(TxVerifyFailed::InvalidFee.into())
-            }
-        };
-
-        // Compute the required fee for this transaction
-        let required_fee = match minimum_fee(total_gas_used) {
-            Ok(fee) => fee,
-            Err(e) => {
-                error!(
-                    target: "validator::verification::verify_transaction",
-                    "[VALIDATOR] Failed calculating tx {tx_hash} fee: {e}"
-                );
-                return Err(TxVerifyFailed::InvalidFee.into())
-            }
-        };
-
-        // Check that enough fee has been paid for the used gas in this
-        // transaction.
-        if required_fee > fee {
-            error!(
-                target: "validator::verification::verify_transaction",
-                "[VALIDATOR] Transaction {tx_hash} has insufficient fee. Required: {required_fee}, Paid: {fee}"
-            );
-            return Err(TxVerifyFailed::InsufficientFee.into())
-        }
-        debug!(target: "validator::verification::verify_transaction", "The gas paid for transaction {tx_hash}: {}", gas_data.paid);
-
-        // Store paid fee
+    let miner_claimable = if let Some((_, fee)) = fee_call {
+        let claimable_fee = calculate_miner_claimable_fee(&tx_hash, total_gas_used, fee)?;
         gas_data.paid = fee;
-    }
+        gas_data.miner_claimable = claimable_fee;
+        Some(claimable_fee)
+    } else {
+        None
+    };
 
     // When we're done looping and executing over the tx's contract
     // calls and (optionally) made sure that enough fee was paid, we
@@ -960,6 +1016,10 @@ pub async fn verify_transaction(
     }
     debug!(target: "validator::verification::verify_transaction", "ZK proof verification successful");
 
+    if let Some(claimable_fee) = miner_claimable {
+        add_miner_claimable_fee(overlay, verifying_block_height, claimable_fee)?;
+    }
+
     // Append hash to merkle tree
     append_tx_to_merkle_tree(tree, tx);
 
@@ -976,14 +1036,37 @@ pub async fn apply_transaction(
     block_target: u32,
     tx: &Transaction,
     tree: &mut MerkleTree,
-) -> Result<()> {
+    verify_fee: bool,
+) -> Result<GasData> {
     let tx_hash = tx.hash();
     debug!(target: "validator::verification::apply_transaction", "Applying transaction {tx_hash}");
 
+    if verify_fee {
+        dark_forest_leaf_vec_integrity_check(
+            &tx.calls,
+            Some(MIN_TX_CALLS + 1),
+            Some(MAX_TX_CALLS),
+        )?;
+    } else {
+        dark_forest_leaf_vec_integrity_check(&tx.calls, Some(MIN_TX_CALLS), Some(MAX_TX_CALLS))?;
+    }
+
+    let fee_call = extract_fee_call(&tx_hash, tx, verify_fee)?;
+
+    let mut gas_data = GasData::default();
+
     // Write the transaction calls payload data
     let mut payload = vec![];
     tx.calls.encode_async(&mut payload).await?;
 
+    // Define a buffer in case we want to use a different payload in a
+    // specific call.
+    let mut _call_payload = vec![];
+
+    // We'll also take note of all the circuits in a Vec so we can
+    // calculate their verification cost.
+    let mut circuits_to_verify = vec![];
+
     // Create tx-local state
     let tx_local_state = Arc::new(Mutex::new(TxLocalState::new()));
 
@@ -991,6 +1074,26 @@ pub async fn apply_transaction(
     for (idx, call) in tx.calls.iter().enumerate() {
         debug!(target: "validator::verification::apply_transaction", "Executing contract call {idx}");
 
+        // Transaction call must contain a function code.
+        let Some(func) = call.data.data.first() else {
+            error!(target: "validator::verification::apply_transaction", "Call contains no data");
+            return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
+        };
+
+        if call.data.is_money_pow_reward() {
+            error!(target: "validator::verification::apply_transaction", "Reward transaction detected");
+            return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
+        }
+
+        // Check if its the fee call so we only pass its payload.
+        let (call_idx, call_payload) = if call.data.is_money_fee() {
+            _call_payload = vec![];
+            vec![call.clone()].encode_async(&mut _call_payload).await?;
+            (0_u8, &_call_payload)
+        } else {
+            (idx as u8, &payload)
+        };
+
         debug!(target: "validator::verification::apply_transaction", "Instantiating WASM runtime");
         let wasm = overlay.lock().unwrap().contracts.get(call.data.contract_id)?;
 
@@ -1002,15 +1105,38 @@ pub async fn apply_transaction(
             verifying_block_height,
             block_target,
             tx_hash,
-            idx as u8,
+            call_idx,
         )?;
 
+        debug!(target: "validator::verification::apply_transaction", "Executing \"metadata\" call");
+        let metadata = runtime.metadata(call_payload)?;
+
+        // Decode the metadata retrieved from the execution.
+        let mut decoder = Cursor::new(&metadata);
+        let zkp_pub: Vec<(String, Vec<pallas::Base>)> =
+            AsyncDecodable::decode_async(&mut decoder).await?;
+        let _: Vec<PublicKey> = AsyncDecodable::decode_async(&mut decoder).await?;
+
+        if decoder.position() != metadata.len() as u64 {
+            error!(
+                target: "validator::verification::apply_transaction",
+                "[VALIDATOR] Failed decoding entire metadata buffer for {tx_hash}:{idx}"
+            );
+            return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
+        }
+
+        for (zkas_ns, _) in &zkp_pub {
+            let (zkbin, _) =
+                overlay.lock().unwrap().contracts.get_zkas(&call.data.contract_id, zkas_ns)?;
+            circuits_to_verify.push(zkbin);
+        }
+
         // Run the "exec" function. We keep the returned state update
         // in a buffer, prefixed by the call function ID, enforcing the
         // state update function in the contract.
         debug!(target: "validator::verification::apply_transaction", "Executing \"exec\" call");
-        let mut state_update = vec![call.data.data[0]];
-        state_update.append(&mut runtime.exec(&payload)?);
+        let mut state_update = vec![*func];
+        state_update.append(&mut runtime.exec(call_payload)?);
         debug!(target: "validator::verification::apply_transaction", "Successfully executed \"exec\" call");
 
         // If that was successful, we apply the state update in the
@@ -1038,24 +1164,61 @@ pub async fn apply_transaction(
                 verifying_block_height,
                 block_target,
                 tx_hash,
-                idx as u8,
+                call_idx,
             )?;
 
             deploy_runtime.deploy(&deploy_params.ix)?;
+
+            let deploy_gas_used = deploy_runtime.gas_used();
+            debug!(target: "validator::verification::apply_transaction", "The gas used for deployment call {call:?} of transaction {tx_hash}: {deploy_gas_used}");
+            gas_data.deployments = gas_data.deployments.saturating_add(deploy_gas_used);
         }
+
+        let wasm_gas_used = runtime.gas_used();
+        debug!(target: "validator::verification::apply_transaction", "The gas used for WASM call {call:?} of transaction {tx_hash}: {wasm_gas_used}");
+        gas_data.wasm = gas_data.wasm.saturating_add(wasm_gas_used);
+    }
+
+    gas_data.signatures = PALLAS_SCHNORR_VERIFY_GAS
+        .saturating_mul(tx.signatures.len() as u64)
+        .saturating_add(serialize_async(tx).await.len() as u64);
+    debug!(target: "validator::verification::apply_transaction", "The gas used for signature of transaction {tx_hash}: {}", gas_data.signatures);
+
+    for zkbin in circuits_to_verify.iter() {
+        let zk_circuit_gas_used = circuit_gas_use(zkbin);
+        debug!(target: "validator::verification::apply_transaction", "The gas used for ZK circuit in namespace {} of transaction {tx_hash}: {zk_circuit_gas_used}", zkbin.namespace);
+        gas_data.zk_circuits = gas_data.zk_circuits.saturating_add(zk_circuit_gas_used);
+    }
+
+    let total_gas_used = gas_data.total_gas_used();
+
+    if total_gas_used > TX_GAS_LIMIT {
+        error!(
+            target: "validator::verification::apply_transaction",
+            "[VALIDATOR] Transaction {tx_hash} exceeds TX_GAS_LIMIT: \
+             {total_gas_used} > {TX_GAS_LIMIT}"
+        );
+        return Err(TxVerifyFailed::GasLimitExceeded.into())
+    }
+
+    if let Some((_, fee)) = fee_call {
+        let claimable_fee = calculate_miner_claimable_fee(&tx_hash, total_gas_used, fee)?;
+        gas_data.paid = fee;
+        gas_data.miner_claimable = claimable_fee;
+        add_miner_claimable_fee(overlay, verifying_block_height, claimable_fee)?;
     }
 
     // Append hash to merkle tree
     append_tx_to_merkle_tree(tree, tx);
 
     debug!(target: "validator::verification::apply_transaction", "Transaction {tx_hash} applied successfully");
-    Ok(())
+    Ok(gas_data)
 }
 
 /// Verify a set of [`Transaction`] in sequence and apply them if all
 /// are valid. In case any of the transactions fail, they will be
 /// returned to the caller as an error. If all transactions are valid,
-/// the function will return the total gas used and total paid fees
+/// the function will return the total gas used and total miner-claimable fees
 /// from all the transactions. Additionally, their hash is appended to
 /// the provided Merkle tree.
 ///
@@ -1079,7 +1242,7 @@ pub async fn verify_transactions(
 
     // Total gas accumulators
     let mut total_gas_used = 0_u64;
-    let mut total_gas_paid = 0_u64;
+    let mut total_miner_claimable = 0_u64;
 
     // Map of ZK proof verifying keys for the current transaction batch
     let mut vks: HashMap<[u8; 32], HashMap<String, VerifyingKey>> = HashMap::new();
@@ -1135,14 +1298,14 @@ pub async fn verify_transactions(
 
         // Update accumulated total gas
         total_gas_used = total_gas_used.saturating_add(tx_gas_used);
-        total_gas_paid = total_gas_paid.saturating_add(gas_data.paid);
+        total_miner_claimable = accumulate_fee(total_miner_claimable, gas_data.miner_claimable)?;
     }
 
     if !erroneous_txs.is_empty() {
         return Err(TxVerifyFailed::ErroneousTxs(erroneous_txs).into())
     }
 
-    Ok((total_gas_used, total_gas_paid))
+    Ok((total_gas_used, total_miner_claimable))
 }
 
 /// Apply given set of [`Transaction`] in sequence, without formal
@@ -1155,34 +1318,64 @@ async fn apply_transactions(
     block_target: u32,
     txs: &[Transaction],
     tree: &mut MerkleTree,
-) -> Result<()> {
+    verify_fees: bool,
+) -> Result<(u64, u64)> {
     debug!(target: "validator::verification::apply_transactions", "Applying {} transactions", txs.len());
     if txs.is_empty() {
-        return Ok(())
+        return Ok((0, 0))
     }
 
     // Tracker for failed txs
     let mut erroneous_txs = vec![];
+    let mut total_gas_used = 0_u64;
+    let mut total_miner_claimable = 0_u64;
 
     // Iterate over transactions and attempt to apply them
     for tx in txs {
         overlay.lock().unwrap().checkpoint();
-        if let Err(e) =
-            apply_transaction(overlay, verifying_block_height, block_target, tx, tree).await
+        let gas_data = match apply_transaction(
+            overlay,
+            verifying_block_height,
+            block_target,
+            tx,
+            tree,
+            verify_fees,
+        )
+        .await
         {
-            warn!(target: "validator::verification::apply_transactions", "Transaction apply failed: {e}");
+            Ok(gas_values) => gas_values,
+            Err(e) => {
+                warn!(target: "validator::verification::apply_transactions", "Transaction apply failed: {e}");
+                erroneous_txs.push(tx.clone());
+                overlay.lock().unwrap().revert_to_checkpoint();
+                continue
+            }
+        };
+
+        let tx_gas_used = gas_data.total_gas_used();
+        let accumulated_gas_usage = total_gas_used.saturating_add(tx_gas_used);
+        if accumulated_gas_usage > BLOCK_GAS_LIMIT {
+            warn!(
+                target: "validator::verification::apply_transactions",
+                "Transaction {} exceeds configured transaction gas limit: \
+                 {accumulated_gas_usage} - {BLOCK_GAS_LIMIT}",
+                tx.hash()
+            );
             erroneous_txs.push(tx.clone());
             overlay.lock().unwrap().revert_to_checkpoint();
-        };
+            break
+        }
+
+        total_gas_used = total_gas_used.saturating_add(tx_gas_used);
+        total_miner_claimable = accumulate_fee(total_miner_claimable, gas_data.miner_claimable)?;
     }
 
     if !erroneous_txs.is_empty() {
         return Err(TxVerifyFailed::ErroneousTxs(erroneous_txs).into())
     }
 
-    Ok(())
+    Ok((total_gas_used, total_miner_claimable))
 }
-
 /// Verify given [`Proposal`] against provided consensus state.
 ///
 /// A proposal is considered valid when the following rules apply:
@@ -1279,3 +1472,88 @@ pub async fn verify_fork_proposal(
 
     Ok(())
 }
+
+#[cfg(test)]
+mod tests {
+    use darkfi_sdk::{
+        crypto::{DAO_CONTRACT_ID, MONEY_CONTRACT_ID},
+        dark_tree::DarkLeaf,
+        fee::{burn_fee, minimum_fee},
+        tx::ContractCall,
+    };
+    use darkfi_serial::serialize;
+
+    use super::*;
+
+    fn leaf(call: ContractCall) -> DarkLeaf<ContractCall> {
+        DarkLeaf { data: call, parent_index: None, children_indexes: vec![] }
+    }
+
+    fn fee_call(fee: u64) -> DarkLeaf<ContractCall> {
+        let mut data = vec![0x00];
+        data.extend(serialize(&fee));
+        leaf(ContractCall { contract_id: *MONEY_CONTRACT_ID, data })
+    }
+
+    fn short_fee_call() -> DarkLeaf<ContractCall> {
+        leaf(ContractCall { contract_id: *MONEY_CONTRACT_ID, data: vec![0x00] })
+    }
+
+    fn non_fee_call() -> DarkLeaf<ContractCall> {
+        leaf(ContractCall { contract_id: *DAO_CONTRACT_ID, data: vec![0x00] })
+    }
+
+    fn tx(calls: Vec<DarkLeaf<ContractCall>>) -> Transaction {
+        Transaction { calls, proofs: vec![], signatures: vec![] }
+    }
+
+    #[test]
+    fn fee_call_extraction_is_disabled_when_fee_checks_are_disabled() {
+        let tx = tx(vec![]);
+        assert_eq!(extract_fee_call(&tx.hash(), &tx, false).unwrap(), None);
+    }
+
+    #[test]
+    fn fee_call_extraction_allows_one_fee_call_with_other_calls() {
+        let tx = tx(vec![non_fee_call(), fee_call(42)]);
+
+        assert_eq!(extract_fee_call(&tx.hash(), &tx, true).unwrap(), Some((1, 42)));
+    }
+
+    #[test]
+    fn fee_call_extraction_rejects_missing_duplicate_malformed_and_zero_fee() {
+        let missing = tx(vec![non_fee_call()]);
+        assert!(extract_fee_call(&missing.hash(), &missing, true).is_err());
+
+        let duplicate = tx(vec![fee_call(1), fee_call(2)]);
+        assert!(extract_fee_call(&duplicate.hash(), &duplicate, true).is_err());
+
+        let malformed = tx(vec![short_fee_call()]);
+        assert!(extract_fee_call(&malformed.hash(), &malformed, true).is_err());
+
+        let zero = tx(vec![fee_call(0)]);
+        assert!(extract_fee_call(&zero.hash(), &zero, true).is_err());
+    }
+
+    #[test]
+    fn miner_claimable_fee_split_handles_exact_fee_and_tip() {
+        let tx_hash = TransactionHash::none();
+        let gas = 10;
+        let required = minimum_fee(gas).unwrap();
+        let burned = burn_fee(required).unwrap();
+
+        assert_eq!(required, 50);
+        assert_eq!(burned, 37);
+        assert_eq!(calculate_miner_claimable_fee(&tx_hash, gas, required).unwrap(), 13);
+        assert_eq!(calculate_miner_claimable_fee(&tx_hash, gas, required + 7).unwrap(), 20);
+    }
+
+    #[test]
+    fn miner_claimable_fee_split_rejects_insufficient_fee() {
+        let tx_hash = TransactionHash::none();
+        let gas = 10;
+        let required = minimum_fee(gas).unwrap();
+
+        assert!(calculate_miner_claimable_fee(&tx_hash, gas, required - 1).is_err());
+    }
+}