فهرست منبع

validator/consensus: bound in-memory forks with a configurable param

skoupidi 5 ماه پیش
والد
کامیت
eb7ee7dddc

+ 9 - 0
bin/darkfid/darkfid_config.toml

@@ -20,6 +20,9 @@ database = "~/.local/share/darkfi/darkfid/testnet"
 # Confirmation threshold, denominated by number of blocks
 threshold = 6
 
+# Max in-memory forks to maintain
+max_forks = 32
+
 # PoW block production target, in seconds
 pow_target = 120
 
@@ -215,6 +218,9 @@ database = "~/.local/share/darkfi/darkfid/mainnet"
 # Confirmation threshold, denominated by number of blocks
 threshold = 11
 
+# Max in-memory forks to maintain
+max_forks = 32
+
 # PoW block production target, in seconds
 pow_target = 120
 
@@ -404,6 +410,9 @@ database = "~/.local/share/darkfi/darkfid/localnet"
 # Confirmation threshold, denominated by number of blocks
 threshold = 3
 
+# Max in-memory forks to maintain
+max_forks = 8
+
 # PoW block production target, in seconds
 pow_target = 10
 

+ 5 - 0
bin/darkfid/src/main.rs

@@ -99,6 +99,10 @@ pub struct BlockchainNetwork {
     /// Confirmation threshold, denominated by number of blocks
     threshold: usize,
 
+    #[structopt(long, default_value = "8")]
+    /// Max in-memory forks to maintain
+    max_forks: usize,
+
     #[structopt(long, default_value = "120")]
     /// PoW block production target, in seconds
     pow_target: u32,
@@ -187,6 +191,7 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
 
     let config = ValidatorConfig {
         confirmation_threshold: blockchain_config.threshold,
+        max_forks: blockchain_config.max_forks,
         pow_target: blockchain_config.pow_target,
         pow_fixed_difficulty,
         genesis_block,

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

@@ -60,6 +60,7 @@ pub struct HarnessConfig {
     pub pow_target: u32,
     pub pow_fixed_difficulty: Option<BigUint>,
     pub confirmation_threshold: usize,
+    pub max_forks: usize,
     pub alice_url: String,
     pub bob_url: String,
 }
@@ -102,6 +103,7 @@ impl Harness {
         // don't get circular dependencies.
         let validator_config = ValidatorConfig {
             confirmation_threshold: config.confirmation_threshold,
+            max_forks: config.max_forks,
             pow_target: config.pow_target,
             pow_fixed_difficulty: config.pow_fixed_difficulty.clone(),
             genesis_block,

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

@@ -53,6 +53,7 @@ async fn sync_blocks_real(ex: Arc<Executor<'static>>) -> Result<()> {
         pow_target,
         pow_fixed_difficulty: pow_fixed_difficulty.clone(),
         confirmation_threshold: 3,
+        max_forks: 8,
         alice_url: "tcp+tls://127.0.0.1:18340".to_string(),
         bob_url: "tcp+tls://127.0.0.1:18341".to_string(),
     };
@@ -254,6 +255,7 @@ fn darkfid_programmatic_control() -> Result<()> {
                     overlay.lock().unwrap().contracts.update_state_monotree(&diff).unwrap();
                 let config = darkfi::validator::ValidatorConfig {
                     confirmation_threshold: 1,
+                    max_forks: 8,
                     pow_target: 20,
                     pow_fixed_difficulty: Some(BigUint::one()),
                     genesis_block,

+ 1 - 0
bin/darkfid/src/tests/sync_forks.rs

@@ -37,6 +37,7 @@ async fn sync_forks_real(ex: Arc<Executor<'static>>) -> Result<()> {
         pow_target,
         pow_fixed_difficulty: pow_fixed_difficulty.clone(),
         confirmation_threshold: 6,
+        max_forks: 8,
         alice_url: "tcp+tls://127.0.0.1:18440".to_string(),
         bob_url: "tcp+tls://127.0.0.1:18441".to_string(),
     };

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

@@ -55,6 +55,7 @@ async fn simulate_unproposed_txs(
         pow_target,
         pow_fixed_difficulty: pow_fixed_difficulty.clone(),
         confirmation_threshold: 6,
+        max_forks: 8,
         alice_url,
         bob_url,
     };

+ 3 - 0
contrib/localnet/darkfid-five-nodes/darkfid0.toml

@@ -17,6 +17,9 @@ database = "darkfid0"
 # Confirmation threshold, denominated by number of blocks
 threshold = 6
 
+# Max in-memory forks to maintain
+max_forks = 8
+
 # PoW block production target, in seconds
 pow_target = 60
 

+ 3 - 0
contrib/localnet/darkfid-five-nodes/darkfid1.toml

@@ -17,6 +17,9 @@ database = "darkfid1"
 # Confirmation threshold, denominated by number of blocks
 threshold = 6
 
+# Max in-memory forks to maintain
+max_forks = 8
+
 # PoW block production target, in seconds
 pow_target = 60
 

+ 3 - 0
contrib/localnet/darkfid-five-nodes/darkfid2.toml

@@ -17,6 +17,9 @@ database = "darkfid2"
 # Confirmation threshold, denominated by number of blocks
 threshold = 6
 
+# Max in-memory forks to maintain
+max_forks = 8
+
 # PoW block production target, in seconds
 pow_target = 60
 

+ 3 - 0
contrib/localnet/darkfid-five-nodes/darkfid3.toml

@@ -17,6 +17,9 @@ database = "darkfid3"
 # Confirmation threshold, denominated by number of blocks
 threshold = 6
 
+# Max in-memory forks to maintain
+max_forks = 8
+
 # PoW block production target, in seconds
 pow_target = 60
 

+ 3 - 0
contrib/localnet/darkfid-five-nodes/darkfid4.toml

@@ -17,6 +17,9 @@ database = "darkfid4"
 # Confirmation threshold, denominated by number of blocks
 threshold = 6
 
+# Max in-memory forks to maintain
+max_forks = 8
+
 # PoW block production target, in seconds
 pow_target = 60
 

+ 3 - 0
contrib/localnet/darkfid-single-node/darkfid.toml

@@ -17,6 +17,9 @@ database = "darkfid"
 # Confirmation threshold, denominated by number of blocks
 threshold = 1
 
+# Max in-memory forks to maintain
+max_forks = 8
+
 # PoW block production target, in seconds
 pow_target = 10
 

+ 3 - 0
contrib/localnet/darkfid-small/darkfid0.toml

@@ -17,6 +17,9 @@ database = "darkfid0"
 # Confirmation threshold, denominated by number of blocks
 threshold = 6
 
+# Max in-memory forks to maintain
+max_forks = 8
+
 # PoW block production target, in seconds
 pow_target = 20
 

+ 3 - 0
contrib/localnet/darkfid-small/darkfid1.toml

@@ -25,6 +25,9 @@ database = "darkfid1"
 # Confirmation threshold, denominated by number of blocks
 threshold = 6
 
+# Max in-memory forks to maintain
+max_forks = 8
+
 # PoW block production target, in seconds
 pow_target = 20
 

+ 3 - 0
contrib/localnet/darkfid-small/darkfid2.toml

@@ -17,6 +17,9 @@ database = "darkfid2"
 # Confirmation threshold, denominated by number of blocks
 threshold = 6
 
+# Max in-memory forks to maintain
+max_forks = 8
+
 # PoW block production target, in seconds
 pow_target = 20
 

+ 1 - 0
src/contract/test-harness/src/lib.rs

@@ -186,6 +186,7 @@ impl Wallet {
         // Create the `Validator` instance
         let validator_config = ValidatorConfig {
             confirmation_threshold: 3,
+            max_forks: 8,
             pow_target: POW_TARGET,
             pow_fixed_difficulty: Some(BigUint::from(1_u8)),
             genesis_block,

+ 50 - 13
src/validator/consensus.rs

@@ -33,7 +33,7 @@ use crate::{
     tx::{Transaction, MAX_TX_CALLS},
     validator::{
         pow::{PoWModule, RANDOMX_KEY_CHANGE_DELAY, RANDOMX_KEY_CHANGING_HEIGHT},
-        utils::{best_fork_index, block_rank, find_extended_fork_index},
+        utils::{best_fork_index, block_rank, find_extended_fork_index, worst_fork_index},
         verification::{verify_proposal, verify_transaction},
     },
     zk::VerifyingKey,
@@ -51,6 +51,8 @@ pub struct Consensus {
     pub confirmation_threshold: usize,
     /// Fork chains containing block proposals
     pub forks: Vec<Fork>,
+    /// Max in-memory forks to maintain.
+    max_forks: usize,
     /// Canonical blockchain PoW module state
     pub module: PoWModule,
 }
@@ -60,15 +62,19 @@ impl Consensus {
     pub fn new(
         blockchain: Blockchain,
         confirmation_threshold: usize,
+        max_forks: usize,
         pow_target: u32,
         pow_fixed_difficulty: Option<BigUint>,
     ) -> Result<Self> {
+        let max_forks = if max_forks == 0 { 1 } else { max_forks };
         let module = PoWModule::new(blockchain.clone(), pow_target, pow_fixed_difficulty, None)?;
 
-        Ok(Self { blockchain, confirmation_threshold, forks: vec![], module })
+        Ok(Self { blockchain, confirmation_threshold, forks: vec![], max_forks, module })
     }
 
-    /// Generate a new empty fork.
+    /// Try to generate a new empty fork. If the forks bound has been
+    /// reached, try to replace the worst ranking one with the new
+    /// empty fork.
     pub async fn generate_empty_fork(&mut self) -> Result<()> {
         debug!(target: "validator::consensus::generate_empty_fork", "Generating new empty fork...");
         // Check if we already have an empty fork
@@ -79,14 +85,48 @@ impl Consensus {
             }
         }
         let fork = Fork::new(self.blockchain.clone(), self.module.clone()).await?;
-        self.forks.push(fork);
+        self.push_fork(fork);
         debug!(target: "validator::consensus::generate_empty_fork", "Fork generated!");
 
         Ok(())
     }
 
-    /// Given a proposal, the node verifys it and finds which fork it extends.
-    /// If the proposal extends the canonical blockchain, a new fork chain is created.
+    /// Auxiliary function to push a fork into the forks vector
+    /// respecting the bounding confirguration. The fork will be
+    /// inserted iff the bound has not be reached or it ranks higher
+    /// than the lowest ranking existing fork.
+    fn push_fork(&mut self, fork: Fork) {
+        // Check if we have reached the bound
+        if self.forks.len() < self.max_forks {
+            self.forks.push(fork);
+            return
+        }
+
+        // Grab worst fork. We don't care about competing forks since
+        // any of them can be replaced. It's safe to unwrap here since
+        // we already checked forks length. `best_fork_index` returns
+        // an error iff we pass an empty forks vector.
+        let index = worst_fork_index(&self.forks).unwrap();
+
+        // Check if the provided one ranks lower
+        if fork.targets_rank < self.forks[index].targets_rank {
+            return
+        }
+
+        // Break tie using their hash distances rank
+        if fork.targets_rank == self.forks[index].targets_rank &&
+            fork.hashes_rank <= self.forks[index].hashes_rank
+        {
+            return
+        }
+
+        // Replace the current worst fork with the provided one
+        self.forks[index] = fork;
+    }
+
+    /// Given a proposal, the node verifys it and finds which fork it
+    /// extends. If the proposal extends the canonical blockchain, a
+    /// new fork chain is created.
     pub async fn append_proposal(&mut self, proposal: &Proposal, verify_fees: bool) -> Result<()> {
         debug!(target: "validator::consensus::append_proposal", "Appending proposal {}", proposal.hash);
 
@@ -115,11 +155,8 @@ impl Consensus {
         // Append proposal to the fork
         fork.append_proposal(proposal).await?;
 
-        // TODO: to keep memory usage low, we should only append forks that
-        // are higher ranking than our current best one
-
-        // If a fork index was found, replace forks with the mutated one,
-        // otherwise push the new fork.
+        // If a fork index was found, replace fork with the mutated
+        // one, otherwise try to push the new fork.
         match index {
             Some(i) => {
                 if i < self.forks.len() &&
@@ -127,11 +164,11 @@ impl Consensus {
                 {
                     self.forks[i] = fork;
                 } else {
-                    self.forks.push(fork);
+                    self.push_fork(fork);
                 }
             }
             None => {
-                self.forks.push(fork);
+                self.push_fork(fork);
             }
         }
 

+ 3 - 0
src/validator/mod.rs

@@ -67,6 +67,8 @@ use utils::{best_fork_index, block_rank, deploy_native_contracts};
 pub struct ValidatorConfig {
     /// Currently configured confirmation security threshold
     pub confirmation_threshold: usize,
+    /// Currently configured max in-memory forks to maintain.
+    pub max_forks: usize,
     /// Currently configured PoW target
     pub pow_target: u32,
     /// Optional fixed difficulty, for testing purposes
@@ -124,6 +126,7 @@ impl Validator {
         let consensus = Consensus::new(
             blockchain.clone(),
             config.confirmation_threshold,
+            config.max_forks,
             config.pow_target,
             config.pow_fixed_difficulty.clone(),
         )?;

+ 54 - 5
src/validator/utils.rs

@@ -237,11 +237,10 @@ pub fn find_extended_fork_index(forks: &[Fork], proposal: &Proposal) -> Result<(
 
 /// Auxiliary function to find best ranked fork.
 ///
-/// The best ranked fork is the one with the highest sum of
-/// its blocks squared mining target distances, from max 32
-/// bytes int. In case of a tie, the fork with the highest
-/// sum of its blocks squared RandomX hash number distances,
-/// from max 32 bytes int, wins.
+/// The best ranked fork is the one with the highest sum of its blocks
+/// squared mining target distances, from max 32 bytes int. In case of
+/// a tie, the fork with the highest sum of its blocks squared RandomX
+/// hash number distances, from max 32 bytes int, wins.
 pub fn best_fork_index(forks: &[Fork]) -> Result<usize> {
     // Check if node has any forks
     if forks.is_empty() {
@@ -285,3 +284,53 @@ pub fn best_fork_index(forks: &[Fork]) -> Result<usize> {
 
     Ok(best_index)
 }
+
+/// Auxiliary function to find worst ranked fork.
+///
+/// The worst ranked fork is the one with the lowest sum of its blocks
+/// squared mining target distances, from max 32 bytes int. In case of
+/// a tie, the fork with the lowest sum of its blocks squared RandomX
+/// hash number distances, from max 32 bytes int, wins.
+pub fn worst_fork_index(forks: &[Fork]) -> Result<usize> {
+    // Check if node has any forks
+    if forks.is_empty() {
+        return Err(Error::ForksNotFound)
+    }
+
+    // Find the worst ranked forks
+    let mut worst = &BigUint::from(0u64);
+    let mut indexes = vec![];
+    for (f_index, fork) in forks.iter().enumerate() {
+        let rank = &fork.targets_rank;
+
+        // Fork ranks higher that current worst
+        if rank > worst {
+            continue
+        }
+
+        // Fork has same rank as current worst
+        if rank == worst {
+            indexes.push(f_index);
+            continue
+        }
+
+        // Fork ranks lower that current worst
+        worst = rank;
+        indexes = vec![f_index];
+    }
+
+    // If a single worst ranking fork exists, return it
+    if indexes.len() == 1 {
+        return Ok(indexes[0])
+    }
+
+    // Break tie using their hash distances rank
+    let mut worst_index = indexes[0];
+    for index in &indexes[1..] {
+        if forks[*index].hashes_rank < forks[worst_index].hashes_rank {
+            worst_index = *index;
+        }
+    }
+
+    Ok(worst_index)
+}