Przeglądaj źródła

script/research/last_man_standing: created new dummy consensus state with its own pid controller

aggstam 3 lat temu
rodzic
commit
61525b3ed9

+ 1 - 0
script/research/last_man_standing/Cargo.toml

@@ -9,6 +9,7 @@ edition = "2021"
 
 [dependencies]
 async-std = "1.12.0"
+dashu = { version = "0.2.0", git = "https://github.com/ertosns/dashu" }
 darkfi = {path = "../../../", features = ["blockchain"]}
 darkfi-sdk = {path = "../../../src/sdk"}
 rand = "0.8.5"

+ 444 - 77
script/research/last_man_standing/src/main.rs

@@ -19,60 +19,366 @@
 use darkfi::{
     blockchain::Blockchain,
     consensus::{
-        constants::{EPOCH_LENGTH, TESTNET_GENESIS_HASH_BYTES},
+        constants,
         leadcoin::{LeadCoin, LeadCoinSecrets},
-        state::ConsensusState,
+        utils::fbig2base,
+        Float10,
     },
-    util::{async_util::sleep, time::Timestamp},
+    util::time::Timestamp,
     Result,
 };
-use darkfi_sdk::pasta::pallas;
+use darkfi_sdk::{
+    crypto::{constants::MERKLE_DEPTH, MerkleNode},
+    incrementalmerkletree::bridgetree::BridgeTree,
+    pasta::{group::ff::PrimeField, pallas},
+};
+use dashu::base::Abs;
 use rand::{thread_rng, Rng};
 
 // Simulation configuration
-const N: u64 = 10;
-const INIT_DISTRIBUTION: u64 = 1000;
+const NODES: u64 = 10;
+const SLOTS: u64 = 10;
+
+// PID controller configuration/constants
+#[derive(Clone)]
+struct PID {
+    pub dt: Float10,
+    pub _ti: Float10,
+    pub _td: Float10,
+    pub kp: Float10,
+    pub ki: Float10,
+    pub kd: Float10,
+    pub _pid_out_step: Float10,
+    pub max_der: Float10,
+    pub min_der: Float10,
+    pub max_f: Float10,
+    pub min_f: Float10,
+    pub deg_rate: Float10,
+}
+
+impl PID {
+    fn new() -> Self {
+        Self {
+            dt: Float10::from_str_native("0.1")
+                .unwrap()
+                .with_precision(constants::RADIX_BITS)
+                .value(),
+            _ti: constants::FLOAT10_ONE.clone(),
+            _td: constants::FLOAT10_ONE.clone(),
+            kp: Float10::from_str_native("0.1")
+                .unwrap()
+                .with_precision(constants::RADIX_BITS)
+                .value(),
+            ki: Float10::from_str_native("0.03")
+                .unwrap()
+                .with_precision(constants::RADIX_BITS)
+                .value(),
+            kd: constants::FLOAT10_ONE.clone(),
+            _pid_out_step: Float10::from_str_native("0.1")
+                .unwrap()
+                .with_precision(constants::RADIX_BITS)
+                .value(),
+            max_der: Float10::from_str_native("0.1")
+                .unwrap()
+                .with_precision(constants::RADIX_BITS)
+                .value(),
+            min_der: Float10::from_str_native("-0.1")
+                .unwrap()
+                .with_precision(constants::RADIX_BITS)
+                .value(),
+            max_f: Float10::from_str_native("0.99")
+                .unwrap()
+                .with_precision(constants::RADIX_BITS)
+                .value(),
+            min_f: Float10::from_str_native("0.05")
+                .unwrap()
+                .with_precision(constants::RADIX_BITS)
+                .value(),
+            deg_rate: Float10::from_str_native("0.9")
+                .unwrap()
+                .with_precision(constants::RADIX_BITS)
+                .value(),
+        }
+    }
+}
+
+/// Node consensus state
+struct ConsensusState {
+    /// Current slot
+    pub current_slot: u64,
+    /// Total sum of initial staking coins
+    pub initial_distribution: u64,
+    /// Competing coins
+    pub coins: Vec<LeadCoin>,
+    /// Coin commitments tree
+    pub coins_tree: BridgeTree<MerkleNode, MERKLE_DEPTH>,
+    /// Previous rounds leaders
+    pub leaders_history: Vec<i64>,
+    /// PID configuration
+    pub pid: PID,
+}
+
+impl ConsensusState {
+    fn pid_error(&self, feedback: Float10) -> Float10 {
+        let target = constants::FLOAT10_ONE.clone();
+        target - feedback
+    }
+
+    fn f_dif(&self) -> Float10 {
+        let last_round_leaders = *self.leaders_history.last().unwrap();
+        let previous_leaders = Float10::try_from(last_round_leaders)
+            .unwrap()
+            .with_precision(constants::RADIX_BITS)
+            .value();
+        self.pid_error(previous_leaders)
+    }
+
+    fn max_windowed_forks(&self) -> Float10 {
+        let mut max: i64 = 5;
+        let window_size = 10;
+        let len = self.leaders_history.len();
+        let window_begining = if len <= (window_size + 1) { 0 } else { len - (window_size + 1) };
+        for item in &self.leaders_history[window_begining..] {
+            if *item > max {
+                max = *item;
+            }
+        }
+
+        Float10::try_from(max).unwrap().with_precision(constants::RADIX_BITS).value()
+    }
+
+    fn tuned_kp(&self) -> Float10 {
+        (self.pid.kp.clone() * constants::FLOAT10_FIVE.clone()) / self.max_windowed_forks()
+    }
+
+    fn weighted_f_dif(&self) -> Float10 {
+        self.tuned_kp() * self.f_dif()
+    }
+
+    fn f_int(&self) -> Float10 {
+        let mut sum = constants::FLOAT10_ZERO.clone();
+        let lead_history_len = self.leaders_history.len();
+        let history_begin_index = if lead_history_len > 10 { lead_history_len - 10 } else { 0 };
+
+        for lf in &self.leaders_history[history_begin_index..] {
+            sum += self.pid_error(Float10::try_from(lf.clone()).unwrap()).abs();
+        }
+        sum
+    }
+
+    fn tuned_ki(&self) -> Float10 {
+        (self.pid.ki.clone() * constants::FLOAT10_FIVE.clone()) / self.max_windowed_forks()
+    }
+
+    fn weighted_f_int(&self) -> Float10 {
+        self.tuned_ki() * self.f_int()
+    }
+
+    fn f_der(&self) -> Float10 {
+        let len = self.leaders_history.len();
+        let last = Float10::try_from(self.leaders_history[len - 1] as i64)
+            .unwrap()
+            .with_precision(constants::RADIX_BITS)
+            .value();
+        let mut der = if len > 1 {
+            let second_to_last = Float10::try_from(self.leaders_history[len - 2] as i64)
+                .unwrap()
+                .with_precision(constants::RADIX_BITS)
+                .value();
+            (self.pid_error(second_to_last) - self.pid_error(last)) / self.pid.dt.clone()
+        } else {
+            self.pid_error(last) / self.pid.dt.clone()
+        };
+
+        der = if der > self.pid.max_der.clone() { self.pid.max_der.clone() } else { der };
+        der = if der < self.pid.min_der.clone() { self.pid.min_der.clone() } else { der };
+        der
+    }
+
+    fn weighted_f_der(&self) -> Float10 {
+        self.pid.kd.clone() * self.f_der()
+    }
+
+    fn zero_leads_len(&self) -> Float10 {
+        let mut count = constants::FLOAT10_ZERO.clone();
+        let hist_len = self.leaders_history.len();
+        for i in 1..hist_len {
+            if self.leaders_history[hist_len - i] == 0 {
+                count += constants::FLOAT10_ONE.clone();
+            } else {
+                break
+            }
+        }
+        count
+    }
+
+    /// Inverse probability of winning lottery having all the stake.    
+    fn win_inv_prob_with_full_stake(&self) -> Float10 {
+        let p = self.weighted_f_dif();
+        let i = self.weighted_f_int();
+        let d = self.weighted_f_der();
+        //println!("win_inv_prob_with_full_stake(): PID P: {:?}", p);
+        //println!("win_inv_prob_with_full_stake(): PID I: {:?}", i);
+        //println!("win_inv_prob_with_full_stake(): PID D: {:?}", d);
+        let f = p + i.clone() + d;
+        //println!("win_inv_prob_with_full_stake(): PID f: {}", f);
+        if f == constants::FLOAT10_ZERO.clone() {
+            return self.pid.min_f.clone()
+        } else if f >= constants::FLOAT10_ONE.clone() {
+            return self.pid.max_f.clone()
+        }
+        let hist_len = self.leaders_history.len();
+        if hist_len > 3 &&
+            self.leaders_history[hist_len - 1] == 0 &&
+            self.leaders_history[hist_len - 2] == 0 &&
+            self.leaders_history[hist_len - 3] == 0 &&
+            i == constants::FLOAT10_ZERO.clone()
+        {
+            return f * self.pid.deg_rate.clone().powf(self.zero_leads_len())
+        }
+        f
+    }
+
+    /// Leadership reward, assuming constant reward
+    /// TODO (res) implement reward mechanism with accord to DRK,DARK token-economics
+    fn reward(&self) -> u64 {
+        constants::REWARD
+    }
+
+    /// Network total stake, assuming constant reward.
+    /// Only used for fine-tuning. At genesis epoch first slot, of absolute index 0,
+    /// if no stake was distributed, the total stake would be 0.
+    /// To avoid division by zero, we asume total stake at first division is GENESIS_TOTAL_STAKE(1).
+    fn total_stake(&self) -> u64 {
+        let rewards = (self.current_slot - 1) * self.reward();
+        let total_stake = rewards + self.initial_distribution;
+        if total_stake == 0 {
+            return constants::GENESIS_TOTAL_STAKE
+        }
+        total_stake
+    }
+
+    /// Return 2-term target approximation sigma coefficients.
+    pub fn sigmas(&self) -> (pallas::Base, pallas::Base) {
+        let f = self.win_inv_prob_with_full_stake();
+        let total_stake = self.total_stake();
+        //println!("sigmas(): f: {}", f);
+        //println!("sigmas(): stake: {}", total_stake);
+        let one = constants::FLOAT10_ONE.clone();
+        let two = constants::FLOAT10_TWO.clone();
+        let field_p = Float10::from_str_native(constants::P)
+            .unwrap()
+            .with_precision(constants::RADIX_BITS)
+            .value();
+        let total_sigma =
+            Float10::try_from(total_stake).unwrap().with_precision(constants::RADIX_BITS).value();
+
+        let x = one - f;
+        let c = x.ln();
+
+        let sigma1_fbig = c.clone() / total_sigma.clone() * field_p.clone();
+        let sigma1 = fbig2base(sigma1_fbig);
+
+        let sigma2_fbig = (c / total_sigma).powf(two.clone()) * (field_p / two);
+        let sigma2 = fbig2base(sigma2_fbig);
+        (sigma1, sigma2)
+    }
+
+    /// Check that the participant/stakeholder coins win the slot lottery.
+    /// If the stakeholder has multiple competing winning coins, only the highest value
+    /// coin is selected, since the stakeholder can't give more than one proof per block/slot.
+    /// * 'sigma1', 'sigma2': slot sigmas
+    /// Returns: (check: bool, idx: usize) where idx is the winning coin's index
+    pub fn is_slot_leader(&mut self, sigma1: pallas::Base, sigma2: pallas::Base) -> (bool, usize) {
+        let mut won = false;
+        let mut highest_stake = 0;
+        let mut highest_stake_idx = 0;
+        let _total_stake = self.total_stake();
+        for (winning_idx, coin) in self.coins.iter().enumerate() {
+            //println!("is_slot_leader: coin stake: {:?}", coin.value);
+            //println!("is_slot_leader: total stake: {}", total_stake);
+            //println!("is_slot_leader: relative stake: {}", (coin.value as f64) / total_stake as f64);
+            let first_winning = coin.is_leader(sigma1, sigma2);
+            if first_winning && !won {
+                highest_stake_idx = winning_idx;
+            }
+
+            won |= first_winning;
+            if won && coin.value > highest_stake {
+                highest_stake = coin.value;
+                highest_stake_idx = winning_idx;
+            }
+        }
+
+        (won, highest_stake_idx)
+    }
+}
+
+/// Utility function to extract leader selection lottery randomness(eta),
+/// defined as the hash of the previous lead proof converted to pallas base.
+fn get_eta(blockchain: &Blockchain) -> pallas::Base {
+    let proof_tx_hash = blockchain.get_last_proof_hash().unwrap();
+    let mut bytes: [u8; 32] = *proof_tx_hash.as_bytes();
+    // read first 254 bits
+    bytes[30] = 0;
+    bytes[31] = 0;
+    pallas::Base::from_repr(bytes).unwrap()
+}
 
 // Generate N nodes states
 fn generate_nodes() -> Result<Vec<ConsensusState>> {
-    println!("Generating {N} nodes...");
-    let stake = INIT_DISTRIBUTION / N;
+    println!("Generating {NODES} nodes...");
+
+    // Generate a dummy DB to get initial coins eta from genesis block hash
+    let db = sled::Config::new().temporary(true).open()?;
+    let timestamp = Timestamp::current_time();
+    let blockchain = Blockchain::new(&db, timestamp, *constants::TESTNET_GENESIS_HASH_BYTES)?;
+
+    // Generate coins configuration
+    let mut stakes = vec![];
+    let mut initial_distribution = 0;
+    for _ in 0..NODES {
+        let stake = rand::thread_rng().gen_range(0..1000);
+        //let stake = 100;
+        initial_distribution += stake;
+        stakes.push(stake);
+    }
+    let slot = 0;
+    let eta = get_eta(&blockchain);
+    let pid = PID::new();
     let mut nodes = vec![];
-    for i in 0..N {
+    for i in 0..NODES {
         println!("Generating node {i}");
-        let db = sled::Config::new().temporary(true).open()?;
-        let timestamp = Timestamp::current_time();
-        let blockchain = Blockchain::new(&db, timestamp, *TESTNET_GENESIS_HASH_BYTES)?;
-        let mut node_state = ConsensusState::new(
-            blockchain,
-            timestamp,
-            timestamp,
-            *TESTNET_GENESIS_HASH_BYTES,
-            INIT_DISTRIBUTION,
-        )?;
-
         // Generate coin here to control stake
-        let slot = node_state.current_slot();
-        let eta = node_state.get_eta();
+        let mut coins_tree =
+            BridgeTree::<MerkleNode, MERKLE_DEPTH>::new(constants::EPOCH_LENGTH * 100);
         let mut rng = thread_rng();
-        let mut seeds: Vec<u64> = Vec::with_capacity(EPOCH_LENGTH);
-        for _ in 0..EPOCH_LENGTH {
+        let mut seeds: Vec<u64> = Vec::with_capacity(constants::EPOCH_LENGTH);
+        for _ in 0..constants::EPOCH_LENGTH {
             seeds.push(rng.gen());
         }
         let epoch_secrets = LeadCoinSecrets::generate();
         let coin = LeadCoin::new(
             eta,
-            stake,
+            stakes[i as usize],
             slot,
             epoch_secrets.secret_keys[0].inner(),
             epoch_secrets.merkle_roots[0],
             0,
             epoch_secrets.merkle_paths[0],
             pallas::Base::from(seeds[0]),
-            &mut node_state.coins_tree,
+            &mut coins_tree,
         );
-        node_state.coins.push(coin);
-        node_state.proposing = true;
+
+        let node_state = ConsensusState {
+            current_slot: slot,
+            initial_distribution,
+            coins: vec![coin],
+            coins_tree,
+            leaders_history: vec![0],
+            pid: pid.clone(),
+        };
+
         nodes.push(node_state);
     }
 
@@ -88,64 +394,125 @@ async fn main() -> Result<()> {
     // Generate nodes
     let mut nodes = generate_nodes()?;
 
-    // Skip genesis slot.
-    // Note: increase slot duration if nodes generation takes longer than it
-    let seconds_next_slot = nodes[0].next_n_slot_start(2).as_secs();
-    println!("Waiting for next slot ({seconds_next_slot} sec)");
-    sleep(seconds_next_slot).await;
-
-    // Playing lottery
-    let mut leaders = vec![];
-    let slot = nodes[0].current_slot();
-    let (sigma1, sigma2) = nodes[0].sigmas();
-    println!("Playing lottery for slot: {slot}");
-    for (i, node) in nodes.iter_mut().enumerate() {
-        let (won, _, _) = node.is_slot_leader(sigma1, sigma2);
-        if won {
-            leaders.push(i);
-        }
-    }
     // In real conditions, everyone waits until a leader arises, and then the "draft" period begins,
     // where other leaders can join/challenge the fight for leadership. If a leader submits a proof after
     // that window passes, it gets ignorred.
     // Note: This time window is the min slot time.
-    println!("Slot leaders: {:?}", leaders);
-    // If more than one leaders occur, we enter the last man standing mode,
-    // where they replay the lottery in specific time windows (rounds), until only one left.
-    // Rounds should be the same time window as the draft period.
-    // Also to "progress" to next round the node must have submitted proof for all the previous rounds.
-    if leaders.len() > 1 {
-        println!("Entering last man standing mode...");
-        let mut round = 0;
-        let mut survivors = vec![];
+
+    // Playing lottery for N slots
+    for slot in 1..SLOTS {
+        println!("Playing lottery for slot: {slot}");
+        // Updating nodes
+        for node in &mut nodes {
+            node.current_slot = slot;
+            // Clean leaders history
+            //node.leaders_history = vec![0];
+        }
+
+        // Start slot loop
+        let mut slot_leader: Option<usize> = None;
         loop {
-            println!("Round {round}, FIGHT!");            
-            let participants = if !survivors.is_empty() {
-                survivors.clone()
-            } else {
-                leaders.clone()
-            };
-            survivors = vec![];
-            for participant in &participants {
-                // We derive the new coin. In real conditions, slot sigmas should adapt on how many
-                // leaders/survivors we have seen on each round.
-                let mut coins_tree = nodes[*participant].coins_tree.clone();
-                nodes[*participant].coins[0] = nodes[*participant].coins[0].derive_coin(&mut coins_tree);
-                nodes[*participant].coins_tree = coins_tree;
-                let (won, _, _) = nodes[*participant].is_slot_leader(sigma1, sigma2);
+            // Check if slot leader was found
+            if let Some(leader) = slot_leader {
+                println!("Slot {slot} leader: {leader}");
+                // Rewarding leader
+                let mut coins_tree = nodes[leader].coins_tree.clone();
+                nodes[leader].coins[0] = nodes[leader].coins[0].derive_coin(&mut coins_tree);
+                nodes[leader].coins_tree = coins_tree;
+                break
+            }
+
+            // Draft round where everyone plays the lottery
+            let mut sigmas: Vec<(pallas::Base, pallas::Base)> = vec![];
+            let mut leaders = vec![];
+            for (i, node) in nodes.iter_mut().enumerate() {
+                // We verify all nodes will calculate the same sigmas
+                let (sigma1, sigma2) = node.sigmas();
+                for pair in &sigmas {
+                    if sigma1 != pair.0 && sigma2 != pair.1 {
+                        println!("ABORT, sigmas are wrong.");
+                        return Ok(())
+                    }
+                }
+                sigmas.push((sigma1, sigma2));
+                let (won, _) = node.is_slot_leader(sigma1, sigma2);
                 if won {
-                    survivors.push(*participant);
+                    leaders.push(i);
                 }
             }
-            println!("Round {round} survivors: {:?}", survivors);
-            if survivors.is_empty() {
-                println!("Survivors didn't win round, terminating last man standing mode");
-                break
-            } else if survivors.len() == 1 {
-                println!("Node {} is the last man standing!", survivors[0]);
-                break
+
+            // Check if single leader was found
+            if leaders.len() == 1 {
+                slot_leader = Some(leaders[0]);
+                continue
+            }
+
+            println!("Slot leaders: {:?}", leaders);
+
+            // Updated nodes leaders history
+            for node in &mut nodes {
+                node.leaders_history.push(leaders.len() as i64);
+            }
+
+            // If more than one leaders occur, we enter the last man standing mode,
+            // where they replay the lottery in specific time windows (rounds), until only one left.
+            // Rounds should be the same time window as the draft period.
+            // Also to "progress" to next round the node must have submitted proof for all the previous rounds.
+            if leaders.len() > 1 {
+                println!("Entering last man standing mode...");
+                let mut round = 0;
+                let mut survivors = vec![];
+                loop {
+                    println!("Round {round}, FIGHT!");
+                    // Sanity check: we verify all nodes will calculate the same sigmas for round validations
+                    let mut sigmas: Vec<(pallas::Base, pallas::Base)> = vec![];
+                    for node in &nodes {
+                        let (sigma1, sigma2) = node.sigmas();
+                        for pair in &sigmas {
+                            if sigma1 != pair.0 && sigma2 != pair.1 {
+                                println!("ABORT, sigmas are wrong.");
+                                return Ok(())
+                            }
+                        }
+                        sigmas.push((sigma1, sigma2));
+                    }
+
+                    // Now leaders/survivors can replay the lottery
+                    let participants =
+                        if !survivors.is_empty() { survivors.clone() } else { leaders.clone() };
+                    survivors = vec![];
+                    for participant in &participants {
+                        let (sigma1, sigma2) = nodes[*participant].sigmas();
+                        // Verify no shenanigans happen when recalculating sigmas
+                        if sigma1 != sigmas[*participant].0 && sigma2 != sigmas[*participant].1 {
+                            println!("ABORT, participant sigmas are wrong.");
+                            return Ok(())
+                        }
+
+                        let (won, _) = nodes[*participant].is_slot_leader(sigma1, sigma2);
+                        if won {
+                            survivors.push(*participant);
+                        }
+                    }
+
+                    // Updated nodes leaders history
+                    for node in &mut nodes {
+                        node.leaders_history.push(survivors.len() as i64);
+                    }
+
+                    println!("Round {round} survivors: {:?}", survivors);
+                    if survivors.is_empty() {
+                        println!("Survivors didn't win round, terminating last man standing mode");
+                        break
+                    } else if survivors.len() == 1 {
+                        println!("Node {} is the last man standing!", survivors[0]);
+                        slot_leader = Some(survivors[0]);
+                        break
+                    }
+
+                    round += 1;
+                }
             }
-            round += 1;
         }
     }