diff --git a/script/research/last_man_standing/Cargo.toml b/script/research/last_man_standing/Cargo.toml index e61e55a12..b4af08f86 100644 --- a/script/research/last_man_standing/Cargo.toml +++ b/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" diff --git a/script/research/last_man_standing/src/main.rs b/script/research/last_man_standing/src/main.rs index fa7826a68..3bf4c36e1 100644 --- a/script/research/last_man_standing/src/main.rs +++ b/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, + /// Coin commitments tree + pub coins_tree: BridgeTree, + /// Previous rounds leaders + pub leaders_history: Vec, + /// 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> { - println!("Generating {N} nodes..."); - let stake = INIT_DISTRIBUTION / N; - let mut nodes = vec![]; - for i in 0..N { - 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, - )?; + 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..NODES { + println!("Generating node {i}"); // Generate coin here to control stake - let slot = node_state.current_slot(); - let eta = node_state.get_eta(); + let mut coins_tree = + BridgeTree::::new(constants::EPOCH_LENGTH * 100); let mut rng = thread_rng(); - let mut seeds: Vec = Vec::with_capacity(EPOCH_LENGTH); - for _ in 0..EPOCH_LENGTH { + let mut seeds: Vec = 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 = 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; } }