Просмотр исходного кода

validator/consensus: gas-based limit for unproposed transactions retrieval

Gas-based limit implementation to retrieve unproposed transactions. The limit is maintained by the constant 'GAS_LIMIT_UNPROPOSED_TXS', currently calculated as the product of the average total gas used and a gas limit multiplier equivalent to the existing TX_CAP value (i.e, 50).

The average total gas was obtained through analysis of empirical transaction test data, using a gas analysis tool.

The aim of this approach is to allow a gradual and controlled transition towards an optimal gas-based system. This minimizes potential adverse effects brought by changes to TX_CAP's implementation and provides the benefits of using gas to limit the number of txs received. This implementation will be fine-tuned until the discovery of the most efficient formula for determining the unproposed transactions' gas limit.

Tests have been added to verify the implementation's correctness by running transactions against it. To run the tests, run the following command from the bin/darkfid directory:

cargo test --release --bin darkfid tests::unproposed_txs
kalm 2 лет назад
Родитель
Сommit
cff9d0e7f5

+ 1 - 1
bin/darkfid/src/task/miner.rs

@@ -356,7 +356,7 @@ async fn generate_next_block(
     let next_block_height = last_proposal.block.header.height + 1;
 
     // Grab forks' unproposed transactions
-    let (mut txs, fees) = extended_fork
+    let (mut txs, fees, _) = extended_fork
         .unproposed_txs(&extended_fork.blockchain, next_block_height, block_target, verify_fees)
         .await?;
 

+ 2 - 0
bin/darkfid/src/tests/mod.rs

@@ -32,6 +32,8 @@ mod forks;
 
 mod sync_forks;
 
+mod unproposed_txs;
+
 async fn sync_blocks_real(ex: Arc<Executor<'static>>) -> Result<()> {
     init_logger();
 

+ 161 - 0
bin/darkfid/src/tests/unproposed_txs.rs

@@ -0,0 +1,161 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//! Test cases for unproprosed transactions.
+//!
+//! The following are supported test cases:
+//! - Verifying the processing of unproposed transactions that are within the unproposed transactions gas limit.
+//! - Verifying the processing of unproposed transactions that exceed the unproposed transactions gas limit.
+//!
+//! The tests were written with a 'GAS_LIMIT_UNPROPOSED_TXS' set to `23_822_290 * 50`. The number `23_822_290` is derived
+//! from the average gas used per transaction, yielding an overall limit of 1_191_114_500 for the pool
+//! of unproposed transactions.
+//!
+//! Please update the test to reflect any changes to the unproposed transactions gas limit value.
+
+use darkfi::Result;
+use std::sync::Arc;
+
+use crate::tests::{Harness, HarnessConfig};
+use darkfi::validator::{consensus::GAS_LIMIT_UNPROPOSED_TXS, utils::best_fork_index};
+use darkfi_contract_test_harness::{init_logger, Holder, TestHarness};
+use darkfi_sdk::{crypto::BaseBlind, num_traits::One};
+use num_bigint::BigUint;
+use rand::rngs::OsRng;
+use smol::Executor;
+
+/// Simulates the processing of a specified number of unproposed transactions, returning
+/// the total number of unproposed transactions and gas used.
+async fn simulate_unproposed_txs(num_txs: u64, ex: Arc<Executor<'static>>) -> Result<(u64, u64)> {
+    init_logger();
+
+    // Set current block height used to create and retrieve unproposed transactions
+    let current_block_height = 1;
+
+    // Create chain test harness configuration
+    let pow_target = 90;
+    let pow_fixed_difficulty = Some(BigUint::one());
+    let config = HarnessConfig {
+        pow_target,
+        pow_fixed_difficulty: pow_fixed_difficulty.clone(),
+        finalization_threshold: 6,
+    };
+
+    // Create chain test harness using created configuration
+    let blockchain_test_harness = Harness::new(config, false, &ex).await?;
+
+    // Get validator and generate the fork
+    let validator = blockchain_test_harness.alice.validator.clone();
+    validator.consensus.generate_empty_fork().await?;
+
+    // Create contract test harness
+    const HOLDERS: [Holder; 1] = [Holder::Alice];
+    let mut contract_test_harness = TestHarness::new(&HOLDERS, false).await?;
+
+    // Create and add pending transactions
+    for counter in 0..num_txs {
+        let (tx, _, _, _) = contract_test_harness
+            .token_mint(
+                counter + 1,
+                &Holder::Alice,
+                &Holder::Alice,
+                BaseBlind::random(&mut OsRng),
+                None,
+                None,
+                current_block_height,
+            )
+            .await?;
+        validator.append_tx(&tx, true).await?;
+    }
+
+    // Obtain fork
+    let forks = validator.consensus.forks.read().await;
+    let best_fork = &forks[best_fork_index(&forks)?];
+
+    // Retrieve unproposed transactions
+    let (tx, _, total_gas_used) = best_fork
+        .unproposed_txs(
+            &best_fork.clone().blockchain,
+            current_block_height,
+            validator.consensus.module.read().await.target,
+            false,
+        )
+        .await?;
+
+    Ok((tx.len() as u64, total_gas_used))
+}
+
+/// Tests the processing of unproposed transactions within `GAS_LIMIT_UNPROPOSED_TXS`.
+///
+/// Note: In this test scenario, the mempool is populated with 5 pending transactions that each use roughly 9_851_908 gas,
+/// falling within `GAS_LIMIT_UNPROPOSED_TXS`.
+#[test]
+fn test_unproposed_txs_within_gas_limit() -> Result<()> {
+    let ex = Arc::new(Executor::new());
+    let (signal, shutdown) = smol::channel::unbounded::<()>();
+
+    easy_parallel::Parallel::new().each(0..1, |_| smol::block_on(ex.run(shutdown.recv()))).finish(
+        || {
+            smol::block_on(async {
+                // Receive number of unproposed txs within gas limit
+                let (num_unproposed_txs, _) = simulate_unproposed_txs(5, ex.clone()).await.unwrap();
+
+                // Shutdown spawned nodes
+                signal.send(()).await.unwrap();
+
+                // Verify test result
+                assert_eq!(num_unproposed_txs, 5);
+            });
+        },
+    );
+
+    Ok(())
+}
+
+/// Tests the processing of unproposed transactions with a mempool of transactions that collectively exceed `GAS_LIMIT_UNPROPOSED_TXS`.
+///
+/// Note: In this test scenario, the mempool is populated with 135 pending transactions, with an average gas usage of 9_851_647 gas.
+/// The total estimated gas usage of these transactions exceeds `GAS_LIMIT_UNPROPOSED_TXS`.
+#[test]
+fn test_unproposed_txs_exceeding_gas_limit() -> Result<()> {
+    let avg_gas_usage = 9_851_647;
+    let min_expected = GAS_LIMIT_UNPROPOSED_TXS / avg_gas_usage;
+    let ex = Arc::new(Executor::new());
+    let (signal, shutdown) = smol::channel::unbounded::<()>();
+
+    easy_parallel::Parallel::new().each(0..1, |_| smol::block_on(ex.run(shutdown.recv()))).finish(
+        || {
+            smol::block_on(async {
+                // Receive total gas used by simulating a number of transactions that will exceed gas limit
+                let (num_unproposed_txs, total_gas_used) =
+                    simulate_unproposed_txs(135, ex.clone()).await.unwrap();
+
+                // Shutdown spawned nodes
+                signal.send(()).await.unwrap();
+
+                // Verify min expected test result
+                assert!(num_unproposed_txs >= min_expected);
+
+                // Verify test result falls within gas limit
+                assert!(total_gas_used <= GAS_LIMIT_UNPROPOSED_TXS);
+            });
+        },
+    );
+
+    Ok(())
+}

+ 32 - 15
src/validator/consensus.rs

@@ -20,7 +20,7 @@ use std::collections::{HashMap, HashSet};
 
 use darkfi_sdk::{crypto::MerkleTree, tx::TransactionHash};
 use darkfi_serial::{async_trait, SerialDecodable, SerialEncodable};
-use log::{debug, info};
+use log::{debug, info, warn};
 use num_bigint::BigUint;
 use sled_overlay::database::SledDbOverlayState;
 use smol::lock::RwLock;
@@ -44,6 +44,15 @@ use crate::{
 /// Block/proposal maximum transactions, exluding producer transaction
 pub const TXS_CAP: usize = 50;
 
+/// Average amount of gas consumed during transaction execution, derived by the Gas Analyzer
+const GAS_TX_AVG: u64 = 23_822_290;
+
+/// Multiplier used to calculate the gas limit for unproposed transactions
+const GAS_LIMIT_MULTIPLIER_UNPROPOSED_TXS: u64 = 50;
+
+/// Gas limit for unproposed transactions
+pub const GAS_LIMIT_UNPROPOSED_TXS: u64 = GAS_TX_AVG * GAS_LIMIT_MULTIPLIER_UNPROPOSED_TXS;
+
 /// This struct represents the information required by the consensus algorithm
 pub struct Consensus {
     /// Canonical (finalized) blockchain
@@ -611,24 +620,25 @@ impl Fork {
     }
 
     /// Auxiliary function to retrieve unproposed valid transactions,
-    /// along with their total paid fees.
+    /// along with their total paid fees and total gas used.
     pub async fn unproposed_txs(
         &self,
         blockchain: &Blockchain,
         verifying_block_height: u32,
         block_target: u32,
         verify_fees: bool,
-    ) -> Result<(Vec<Transaction>, u64)> {
+    ) -> Result<(Vec<Transaction>, u64, u64)> {
         // Check if our mempool is not empty
         if self.mempool.is_empty() {
-            return Ok((vec![], 0))
+            return Ok((vec![], 0, 0))
         }
 
         // Transactions Merkle tree
         let mut tree = MerkleTree::new(1);
 
-        // Gas accumulator
-        let mut gas_paid = 0;
+        // Total gas accumulators
+        let mut total_gas_paid = 0;
+        let mut total_gas_used = 0;
 
         // Map of ZK proof verifying keys for the current transaction batch
         let mut vks: HashMap<[u8; 32], HashMap<String, VerifyingKey>> = HashMap::new();
@@ -658,7 +668,7 @@ impl Fork {
 
             // Verify the transaction against current state
             overlay.lock().unwrap().checkpoint();
-            match verify_transaction(
+            let (tx_gas_used, tx_gas_paid) = match verify_transaction(
                 &overlay,
                 verifying_block_height,
                 block_target,
@@ -669,25 +679,32 @@ impl Fork {
             )
             .await
             {
-                Ok((_, gas)) => gas_paid += gas,
+                Ok(gas_values) => gas_values,
                 Err(e) => {
                     debug!(target: "validator::consensus::unproposed_txs", "Transaction verification failed: {}", e);
                     overlay.lock().unwrap().revert_to_checkpoint()?;
                     continue
                 }
-            }
+            };
 
-            // Push the tx hash into the unproposed transactions vector
-            unproposed_txs.push(unproposed_tx);
+            // Calculate current accumulated gas usage
+            let accumulated_gas_usage = total_gas_used + tx_gas_used;
 
-            // Check limit
-            // TODO: here we can use gas instead of the TXS_cap limit
-            if unproposed_txs.len() == TXS_CAP {
+            // Check gas limit - if accumulated gas used exceeds it, break out of loop
+            if accumulated_gas_usage > GAS_LIMIT_UNPROPOSED_TXS {
+                warn!(target: "validator::consensus::unproposed_txs", "Retrieving transaction {} would exceed configured unproposed transaction gas limit: {} - {}", tx, accumulated_gas_usage, GAS_LIMIT_UNPROPOSED_TXS);
                 break
             }
+
+            // Update accumulated total gas
+            total_gas_paid += tx_gas_paid;
+            total_gas_used += tx_gas_used;
+
+            // Push the tx hash into the unproposed transactions vector
+            unproposed_txs.push(unproposed_tx);
         }
 
-        Ok((unproposed_txs, gas_paid))
+        Ok((unproposed_txs, total_gas_paid, total_gas_used))
     }
 
     /// Auxiliary function to create a full clone using BlockchainOverlay::full_clone.

+ 25 - 6
src/validator/verification.rs

@@ -518,7 +518,7 @@ async fn apply_producer_transaction(
     // Append hash to merkle tree
     append_tx_to_merkle_tree(tree, tx);
 
-    debug!(target: "validator::verification::apply_producer_transaction", "Pruducer transaction {} executed successfully", tx_hash);
+    debug!(target: "validator::verification::apply_producer_transaction", "Producer transaction {} executed successfully", tx_hash);
 
     Ok(signature_public_key)
 }
@@ -689,22 +689,37 @@ pub async fn verify_transaction(
 
             deploy_runtime.deploy(&deploy_params.ix)?;
 
-            // Append the used gas
-            gas_used += deploy_runtime.gas_used();
+            let deploy_gas_used = deploy_runtime.gas_used();
+            debug!(target: "validator::verification::verify_transaction", "The gas used for deployment call {:?} of transaction {}: {}", call, tx_hash, deploy_gas_used);
+
+            // Append the used deployment gas
+            gas_used += deploy_gas_used;
         }
 
         // At this point we're done with the call and move on to the next one.
         // Accumulate the WASM gas used.
-        gas_used += runtime.gas_used();
+        let wasm_gas_used = runtime.gas_used();
+        debug!(target: "validator::verification::verify_transaction", "The gas used for WASM call {:?} of transaction {}: {}", call, tx_hash, wasm_gas_used);
+
+        // Append the used wasm gas
+        gas_used += wasm_gas_used;
     }
 
     // The signature fee is tx_size + fixed_sig_fee * n_signatures
-    gas_used += (PALLAS_SCHNORR_SIGNATURE_FEE * tx.signatures.len() as u64) +
+    let signature_fee = (PALLAS_SCHNORR_SIGNATURE_FEE * tx.signatures.len() as u64) +
         serialize_async(tx).await.len() as u64;
+    debug!(target: "validator::verification::verify_transaction", "The gas used for signature of transaction {}: {}", tx_hash, signature_fee);
+
+    // Append the used signature gas
+    gas_used += signature_fee;
 
     // The ZK circuit fee is calculated using a function in validator/fees.rs
     for zkbin in circuits_to_verify.iter() {
-        gas_used += circuit_gas_use(zkbin);
+        let zk_circuit_gas_used = circuit_gas_use(zkbin);
+        debug!(target: "validator::verification::verify_transaction", "The gas used for ZK circuit in namespace {} of transaction {}: {}", zkbin.namespace, tx_hash, zk_circuit_gas_used);
+
+        // Append the used zk circuit gas
+        gas_used += zk_circuit_gas_used;
     }
 
     if verify_fee {
@@ -730,6 +745,9 @@ pub async fn verify_transaction(
             );
             return Err(TxVerifyFailed::InsufficientFee.into())
         }
+        debug!(target: "validator::verification::verify_transaction", "The gas paid for transaction {}: {}", tx_hash, gas_paid);
+
+        // Store paid fee
         gas_paid = fee;
     }
 
@@ -768,6 +786,7 @@ pub async fn verify_transaction(
     // Append hash to merkle tree
     append_tx_to_merkle_tree(tree, tx);
 
+    debug!(target: "validator::verification::verify_transaction", "The total gas used for transaction {}: {}", tx_hash, gas_used);
     debug!(target: "validator::verification::verify_transaction", "Transaction {} verified successfully", tx_hash);
     Ok((gas_used, gas_paid))
 }