Przeglądaj źródła

validator/consensus/pid: sigmas caclulation foundation

aggstam 3 lat temu
rodzic
commit
9712efdfb1

+ 5 - 0
bin/darkfid2/src/tests/harness.rs

@@ -105,6 +105,11 @@ impl Harness {
             pallas::Base::ZERO,
             vec![previous_hash],
             vec![previous.header.previous.clone()],
+            0.0,
+            0.0,
+            0.0,
+            0,
+            0,
             pallas::Base::ZERO,
             pallas::Base::ZERO,
         );

+ 4 - 0
src/blockchain/slot_store.rs

@@ -38,6 +38,10 @@ pub fn validate_slot(
 ) -> Result<()> {
     let error = Err(Error::SlotIsInvalid(slot.id));
 
+    // TODO: Validate previous slot stuff
+    // slot.total_tokens = previous.total_tokens + previous.reward
+    // slot.previous_slot_err = previous.err;
+
     // Check slots are incremental (1)
     if slot.id <= previous.id {
         return error

+ 13 - 1
src/consensus/state.rs

@@ -165,7 +165,19 @@ impl ConsensusState {
     ) {
         let id = self.time_keeper.current_slot();
         let previous_eta = self.get_previous_eta();
-        let slot = Slot { id, previous_eta, fork_hashes, fork_previous_hashes, sigma1, sigma2 };
+        let slot = Slot::new(
+            id,
+            previous_eta,
+            fork_hashes,
+            fork_previous_hashes,
+            0.0,
+            0.0,
+            0.0,
+            0,
+            0,
+            sigma1,
+            sigma2,
+        );
         info!(target: "consensus::state", "generate_slot: {:?}", slot);
         self.slots.push(slot);
     }

+ 12 - 9
src/contract/test-harness/src/lib.rs

@@ -440,17 +440,20 @@ impl TestHarness {
         // We grab the genesis slot to generate slot
         // using same consensus parameters
         let genesis_block = self.genesis_block;
-        let fork_hashes = vec![genesis_block];
-        let fork_previous_hashes = vec![genesis_block];
         let genesis_slot = self.get_slot_by_slot(0).await?;
-        let slot = Slot {
+        let slot = Slot::new(
             id,
-            previous_eta: genesis_slot.previous_eta,
-            fork_hashes,
-            fork_previous_hashes,
-            sigma1: genesis_slot.sigma1,
-            sigma2: genesis_slot.sigma2,
-        };
+            genesis_slot.previous_eta,
+            vec![genesis_block],
+            vec![genesis_block],
+            0.0,
+            0.0,
+            0.0,
+            0,
+            0,
+            genesis_slot.sigma1,
+            genesis_slot.sigma2,
+        );
 
         // Store generated slot
         for wallet in self.holders.values() {

+ 4 - 0
src/error.rs

@@ -56,6 +56,10 @@ pub enum Error {
     #[error(transparent)]
     TryFromSliceError(#[from] std::array::TryFromSliceError),
 
+    #[cfg(feature = "dashu")]
+    #[error(transparent)]
+    DashuConversionError(#[from] dashu::base::error::ConversionError),
+
     #[cfg(feature = "dashu")]
     #[error(transparent)]
     DashuParseError(#[from] dashu::base::error::ParseError),

+ 41 - 2
src/sdk/src/blockchain.rs

@@ -32,6 +32,16 @@ pub struct Slot {
     /// Previous slot second to last proposal/block hashes,
     /// as observed by the validator
     pub fork_previous_hashes: Vec<blake3::Hash>,
+    /// Slot inverse probability `f` of becoming a block producer
+    pub f: f64,
+    /// Slot feedback error
+    pub error: f64,
+    /// Previous slot feedback error
+    pub previous_slot_error: f64,
+    /// Total tokens up until this slot
+    pub total_tokens: u64,
+    /// Slot reward
+    pub reward: u64,
     /// Slot sigma1
     pub sigma1: pallas::Base,
     /// Slot sigma2
@@ -44,16 +54,45 @@ impl Slot {
         previous_eta: pallas::Base,
         fork_hashes: Vec<blake3::Hash>,
         fork_previous_hashes: Vec<blake3::Hash>,
+        f: f64,
+        error: f64,
+        previous_slot_error: f64,
+        total_tokens: u64,
+        reward: u64,
         sigma1: pallas::Base,
         sigma2: pallas::Base,
     ) -> Self {
-        Self { id, previous_eta, fork_hashes, fork_previous_hashes, sigma1, sigma2 }
+        Self {
+            id,
+            previous_eta,
+            fork_hashes,
+            fork_previous_hashes,
+            f,
+            error,
+            previous_slot_error,
+            total_tokens,
+            reward,
+            sigma1,
+            sigma2,
+        }
     }
 }
 
 impl Default for Slot {
     /// Represents the genesis slot on current timestamp
     fn default() -> Self {
-        Self::new(0, pallas::Base::ZERO, vec![], vec![], pallas::Base::ZERO, pallas::Base::ZERO)
+        Self::new(
+            0,
+            pallas::Base::ZERO,
+            vec![],
+            vec![],
+            0.0,
+            0.0,
+            0.0,
+            0,
+            0,
+            pallas::Base::ZERO,
+            pallas::Base::ZERO,
+        )
     }
 }

+ 228 - 0
src/validator/consensus/float_10.rs

@@ -0,0 +1,228 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2023 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/>.
+ */
+
+use std::ops::{Add, AddAssign, Div, Mul, Sub};
+
+use darkfi_sdk::pasta::{group::ff::PrimeField, pallas};
+use dashu::{
+    base::Abs,
+    float::{round::mode::Zero, FBig, Repr},
+    integer::{IBig, Sign, UBig},
+};
+use lazy_static::lazy_static;
+
+const RADIX_BITS: usize = 76;
+const B: u64 = 10;
+
+/// Wrapper structure over a Base 10 [`dashu::float::FBig`]
+/// and Zero rounding mode.
+#[derive(Clone, PartialEq, PartialOrd, Debug)]
+pub struct Float10(FBig<Zero, B>);
+
+impl Float10 {
+    pub fn repr(&self) -> &Repr<B> {
+        self.0.repr()
+    }
+
+    pub fn abs(&self) -> Self {
+        Self(self.0.clone().abs())
+    }
+
+    pub fn powf(&self, exp: Self) -> Self {
+        Self(self.0.powf(&exp.0))
+    }
+
+    pub fn ln(&self) -> Self {
+        Self(self.0.ln())
+    }
+}
+
+impl Add for Float10 {
+    type Output = Self;
+
+    fn add(self, other: Self) -> Self {
+        Self(self.0 + other.0)
+    }
+}
+
+impl AddAssign for Float10 {
+    fn add_assign(&mut self, other: Self) {
+        *self = Self(self.0.clone() + other.0);
+    }
+}
+
+impl Sub for Float10 {
+    type Output = Self;
+
+    fn sub(self, other: Self) -> Self {
+        Self(self.0 - other.0)
+    }
+}
+
+impl Mul for Float10 {
+    type Output = Self;
+
+    fn mul(self, other: Self) -> Self {
+        Self(self.0 * other.0)
+    }
+}
+
+impl Div for Float10 {
+    type Output = Self;
+
+    fn div(self, other: Self) -> Self {
+        Self(self.0 / other.0)
+    }
+}
+
+impl std::fmt::Display for Float10 {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        write!(f, "{}", self.0)
+    }
+}
+
+impl TryFrom<&str> for Float10 {
+    type Error = crate::Error;
+
+    fn try_from(value: &str) -> Result<Self, Self::Error> {
+        Ok(Self(FBig::from_str_native(value)?.with_precision(RADIX_BITS).value()))
+    }
+}
+
+impl TryFrom<u64> for Float10 {
+    type Error = crate::Error;
+
+    fn try_from(value: u64) -> Result<Self, Self::Error> {
+        Ok(Self(FBig::try_from(value)?))
+    }
+}
+
+impl TryFrom<i64> for Float10 {
+    type Error = crate::Error;
+
+    fn try_from(value: i64) -> Result<Self, Self::Error> {
+        Ok(Self(FBig::try_from(value)?))
+    }
+}
+
+impl TryFrom<f64> for Float10 {
+    type Error = crate::Error;
+
+    fn try_from(value: f64) -> Result<Self, Self::Error> {
+        Ok(Self(FBig::try_from(value)?.with_base().value()))
+    }
+}
+
+// Commonly used Float10
+lazy_static! {
+    pub static ref FLOAT10_NEG_TWO: Float10 = Float10::try_from("-2").unwrap();
+    pub static ref FLOAT10_NEG_ONE: Float10 = Float10::try_from("-1").unwrap();
+    pub static ref FLOAT10_ZERO: Float10 = Float10::try_from("0").unwrap();
+    pub static ref FLOAT10_ONE: Float10 = Float10::try_from("1").unwrap();
+    pub static ref FLOAT10_TWO: Float10 = Float10::try_from("2").unwrap();
+    pub static ref FLOAT10_THREE: Float10 = Float10::try_from("3").unwrap();
+    pub static ref FLOAT10_FIVE: Float10 = Float10::try_from("5").unwrap();
+    pub static ref FLOAT10_NINE: Float10 = Float10::try_from("9").unwrap();
+    pub static ref FLOAT10_TEN: Float10 = Float10::try_from("10").unwrap();
+}
+
+// Utility functions
+/// Convert a Float10 to [`dashu::integer::IBig`].
+pub fn fbig2ibig(f: Float10) -> IBig {
+    let rad = IBig::try_from(10).unwrap();
+    let sig = f.repr().significand();
+    let exp = f.repr().exponent();
+
+    let val: IBig = if exp >= 0 {
+        sig.clone() * rad.pow(exp.unsigned_abs())
+    } else {
+        sig.clone() / rad.pow(exp.unsigned_abs())
+    };
+
+    val
+}
+
+/// Convert a Float10 to [`pallas::Base`].
+/// Note: negative values in pallas field don't wrap,
+/// and can't be converted back to original value.
+pub fn fbig2base(f: Float10) -> pallas::Base {
+    let val: IBig = fbig2ibig(f);
+    let (sign, word) = val.as_sign_words();
+    let mut words: [u64; 4] = [0, 0, 0, 0];
+    words[..word.len()].copy_from_slice(word);
+    match sign {
+        Sign::Positive => pallas::Base::from_raw(words),
+        Sign::Negative => pallas::Base::from_raw(words).neg(),
+    }
+}
+
+/// Convert a [`pallas::Base`] to [`dashu::integer::IBig`].
+/// Note: only zero and positive numbers conversion is supported.
+/// Used for testing purposes on non-negative values at the moment.
+pub fn base2ibig(base: pallas::Base) -> IBig {
+    let byts: [u8; 32] = base.to_repr();
+    let words: [u64; 4] = [
+        u64::from_le_bytes(byts[0..8].try_into().expect("")),
+        u64::from_le_bytes(byts[8..16].try_into().expect("")),
+        u64::from_le_bytes(byts[16..24].try_into().expect("")),
+        u64::from_le_bytes(byts[24..32].try_into().expect("")),
+    ];
+    let uparts = UBig::from_words(&words);
+    IBig::from_parts(Sign::Positive, uparts)
+}
+
+#[cfg(test)]
+mod tests {
+    use darkfi_sdk::pasta::pallas;
+    use dashu::integer::IBig;
+
+    use super::{base2ibig, fbig2base, fbig2ibig, Float10};
+
+    #[test]
+    fn dashu_fbig2ibig() {
+        let f = Float10::try_from("234234223.000").unwrap();
+        let i: IBig = fbig2ibig(f);
+        let sig = IBig::from(234234223);
+        assert_eq!(i, sig);
+    }
+
+    #[test]
+    fn dashu_test_base2ibig() {
+        let fbig: Float10 = Float10::try_from(
+            "289480223093290488558927462521719769633630564819415607159546767643499676303",
+        )
+        .unwrap();
+        let ibig = fbig2ibig(fbig.clone());
+        let res_base: pallas::Base = fbig2base(fbig.clone());
+        let res_ibig: IBig = base2ibig(res_base);
+        assert_eq!(res_ibig, ibig);
+    }
+
+    #[test]
+    fn dashu_test2_base2ibig() {
+        // Verify that field wrapping for negative values won't hold during conversions.
+        let fbig: Float10 = Float10::try_from(
+            "-20065240046497827215558476051577517633529246907153511707181011345840062564.87",
+        )
+        .unwrap();
+        let ibig = fbig2ibig(fbig.clone());
+        let res_base: pallas::Base = fbig2base(fbig.clone());
+        let res_ibig: IBig = base2ibig(res_base);
+        assert_ne!(res_ibig, ibig);
+    }
+}

+ 3 - 0
src/validator/consensus/mod.rs

@@ -21,6 +21,9 @@ use crate::{blockchain::Blockchain, util::time::TimeKeeper};
 /// DarkFi consensus PID controller
 pub mod pid;
 
+/// Base 10 big float implementation for high precision arithmetics
+pub mod float_10;
+
 /// This struct represents the information required by the consensus algorithm
 pub struct Consensus {
     /// Canonical (finalized) blockchain

+ 81 - 5
src/validator/consensus/pid.rs

@@ -21,12 +21,23 @@
 //! since we just want to simulate its functionality. After layout is
 //! complete, the proper pid functionality will be implemented.
 
-use darkfi_sdk::pasta::pallas;
+use darkfi_sdk::{blockchain::Slot, pasta::pallas};
+use lazy_static::lazy_static;
 
-/// Return 2-term target approximation sigma coefficients,
-/// corresponding to current slot consensus state.
-pub fn current_sigmas() -> (pallas::Base, pallas::Base) {
-    (pallas::Base::zero(), pallas::Base::zero())
+use super::float_10::{
+    fbig2base, Float10, FLOAT10_NEG_ONE, FLOAT10_NEG_TWO, FLOAT10_ONE, FLOAT10_TWO, FLOAT10_ZERO,
+};
+
+/// PID controller configuration
+const P: &str = "28948022309329048855892746252171976963363056481941560715954676764349967630337";
+lazy_static! {
+    static ref FIELD_P: Float10 = Float10::try_from(P).unwrap();
+    static ref KP: Float10 = Float10::try_from("0.18").unwrap();
+    static ref KI: Float10 = Float10::try_from("0.02").unwrap();
+    static ref KD: Float10 = Float10::try_from("-0.1").unwrap();
+    static ref MAX_F: Float10 = Float10::try_from("0.99").unwrap();
+    static ref MIN_F: Float10 = Float10::try_from("0.01").unwrap();
+    static ref EPSILON: Float10 = Float10::try_from("1").unwrap();
 }
 
 /// Return 2-term target approximation sigma coefficients,
@@ -34,3 +45,68 @@ pub fn current_sigmas() -> (pallas::Base, pallas::Base) {
 pub fn slot_sigmas() -> (pallas::Base, pallas::Base) {
     (pallas::Base::zero(), pallas::Base::zero())
 }
+
+/// Return 2-term target approximation sigma coefficients,
+/// corresponding to provided slot consensus state.
+pub fn sigmass(previous_slot: &Slot) -> (pallas::Base, pallas::Base) {
+    let f = calculate_f(previous_slot);
+    let total_tokens =
+        Float10::try_from(previous_slot.total_tokens + previous_slot.reward).unwrap();
+    calculate_sigmas(f, total_tokens)
+}
+
+/// Calculate the inverse probability `f` of becoming a block producer (winning the lottery)
+/// having all the tokens, represented as Float10.
+fn calculate_f(previous_slot: &Slot) -> Float10 {
+    // PID controller K values based on constants
+    let k1 = KP.clone() + KI.clone() + KD.clone();
+    let k2 = FLOAT10_NEG_ONE.clone() * KP.clone() + FLOAT10_NEG_TWO.clone() * KD.clone();
+    let k3 = KD.clone();
+
+    // Convert slot values to Float10
+    let previous_slot_f = Float10::try_from(previous_slot.f).unwrap();
+    let previous_slot_error = Float10::try_from(previous_slot.error).unwrap();
+    let previous_slot_previous_slot_error =
+        Float10::try_from(previous_slot.previous_slot_error).unwrap();
+
+    // Calculate feedback error based on previous block producers.
+    // We know how many producers existed in previous slot by
+    // the len of its fork hashes.
+    let feedback = Float10::try_from(previous_slot.fork_hashes.len() as u64).unwrap();
+    let error = FLOAT10_ONE.clone() - feedback;
+
+    // Calculate f
+    let mut f = previous_slot_f +
+        k1 * error +
+        k2 * previous_slot_error +
+        k3 * previous_slot_previous_slot_error;
+
+    // Boundaries control
+    if f <= *FLOAT10_ZERO {
+        f = MIN_F.clone()
+    } else if f >= *FLOAT10_ONE {
+        f = MAX_F.clone()
+    }
+
+    f
+}
+
+/// Return 2-term target approximation sigma coefficients,
+/// corresponding to provided `f` and `total_tokens` values.
+fn calculate_sigmas(f: Float10, total_tokens: Float10) -> (pallas::Base, pallas::Base) {
+    // Calculate `neg_c` value
+    let x = FLOAT10_ONE.clone() - f;
+    let c = x.ln();
+    let neg_c = FLOAT10_NEG_ONE.clone() * c;
+
+    // Calculate sigma 1
+    let sigma1_fbig = neg_c.clone() / (total_tokens.clone() + EPSILON.clone()) * FIELD_P.clone();
+    let sigma1 = fbig2base(sigma1_fbig);
+
+    // Calculate sigma 2
+    let sigma2_fbig = (neg_c / (total_tokens + EPSILON.clone())).powf(FLOAT10_TWO.clone()) *
+        (FIELD_P.clone() / FLOAT10_TWO.clone());
+    let sigma2 = fbig2base(sigma2_fbig);
+
+    (sigma1, sigma2)
+}

+ 5 - 0
tests/blockchain.rs

@@ -68,6 +68,11 @@ impl Harness {
             pallas::Base::ZERO,
             vec![previous_hash],
             vec![previous.header.previous.clone()],
+            0.0,
+            0.0,
+            0.0,
+            0,
+            0,
             pallas::Base::ZERO,
             pallas::Base::ZERO,
         );