mithril_stm/
eligibility_check.rs

1use crate::stm::Stake;
2#[cfg(any(feature = "num-integer-backend", target_family = "wasm", windows))]
3use {
4    num_bigint::{BigInt, Sign},
5    num_rational::Ratio,
6    num_traits::{One, Signed},
7    std::ops::Neg,
8};
9
10#[cfg(any(feature = "num-integer-backend", target_family = "wasm", windows))]
11/// Checks that ev is successful in the lottery. In particular, it compares the output of `phi`
12/// (a real) to the output of `ev` (a hash).  It uses the same technique used in the
13/// [Cardano ledger](https://github.com/input-output-hk/cardano-ledger/). In particular,
14/// `ev` is a natural in `[0,2^512]`, while `phi` is a floating point in `[0, 1]`, and so what
15/// this check does is verify whether `p < 1 - (1 - phi_f)^w`, with `p = ev / 2^512`.
16///
17/// The calculation is done using the following optimization:
18///
19/// let `q = 1 / (1 - p)` and `c = ln(1 - phi_f)`
20///
21/// then          `p < 1 - (1 - phi_f)^w`
22/// `<=> 1 / (1 - p) < exp(-w * c)`
23/// `<=> q           < exp(-w * c)`
24///
25/// This can be computed using the taylor expansion. Using error estimation, we can do
26/// an early stop, once we know that the result is either above or below.  We iterate 1000
27/// times. If no conclusive result has been reached, we return false.
28///
29/// Note that         1             1               evMax
30///             q = ----- = ------------------ = -------------
31///                 1 - p    1 - (ev / evMax)    (evMax - ev)
32///
33/// Used to determine winning lottery tickets.
34pub(crate) fn ev_lt_phi(phi_f: f64, ev: [u8; 64], stake: Stake, total_stake: Stake) -> bool {
35    // If phi_f = 1, then we automatically break with true
36    if (phi_f - 1.0).abs() < f64::EPSILON {
37        return true;
38    }
39
40    let ev_max = BigInt::from(2u8).pow(512);
41    let ev = BigInt::from_bytes_le(Sign::Plus, &ev);
42    let q = Ratio::new_raw(ev_max.clone(), ev_max - ev);
43
44    let c =
45        Ratio::from_float((1.0 - phi_f).ln()).expect("Only fails if the float is infinite or NaN.");
46    let w = Ratio::new_raw(BigInt::from(stake), BigInt::from(total_stake));
47    let x = (w * c).neg();
48    // Now we compute a taylor function that breaks when the result is known.
49    taylor_comparison(1000, q, x)
50}
51
52#[cfg(any(feature = "num-integer-backend", target_family = "wasm", windows))]
53/// Checks if cmp < exp(x). Uses error approximation for an early stop. Whenever the value being
54/// compared, `cmp`, is smaller (or greater) than the current approximation minus an `error_term`
55/// (plus an `error_term` respectively), then we stop approximating. The choice of the `error_term`
56/// is specific to our use case, and this function should not be used in other contexts without
57/// reconsidering the `error_term`. As a conservative value of the `error_term` we choose
58/// `new_x * M`, where `new_x` is the next term of the taylor expansion, and `M` is the largest
59/// value of `x` in a reasonable range. Note that `x >= 0`, given that `x = - w * c`, with
60/// `0 <= w <= 1` and `c < 0`, as `c` is defined as `c = ln(1.0 - phi_f)` with `phi_f \in (0,1)`.
61/// Therefore, a good integral bound is the maximum value that `|ln(1.0 - phi_f)|` can take with
62/// `phi_f \in [0, 0.95]` (if we expect to have `phi_f > 0.95` this bound should be extended),
63/// which is `3`. Hence, we set `M = 3`.
64#[allow(clippy::redundant_clone)]
65fn taylor_comparison(bound: usize, cmp: Ratio<BigInt>, x: Ratio<BigInt>) -> bool {
66    let mut new_x = x.clone();
67    let mut phi: Ratio<BigInt> = One::one();
68    let mut divisor: BigInt = One::one();
69    for _ in 0..bound {
70        phi += new_x.clone();
71
72        divisor += 1;
73        new_x = (new_x.clone() * x.clone()) / divisor.clone();
74        let error_term = new_x.clone().abs() * BigInt::from(3); // new_x * M
75
76        if cmp > (phi.clone() + error_term.clone()) {
77            return false;
78        } else if cmp < phi.clone() - error_term.clone() {
79            return true;
80        }
81    }
82    false
83}
84
85#[cfg(not(any(feature = "num-integer-backend", target_family = "wasm", windows)))]
86/// The crate `rug` has sufficient optimizations to not require a taylor approximation with early
87/// stop. The difference between the current implementation and the one using the optimization
88/// above is around 10% faster. We perform the computations with 117 significant bits of
89/// precision, since this is enough to represent the fraction of a single lovelace. We have that
90/// 1e6 lovelace equals 1 ada, and there is 45 billion ada in circulation. Meaning there are
91/// 4.5e16 lovelace, so 1e-17 is sufficient to represent fractions of the stake distribution. In
92/// order to keep the error in the 1e-17 range, we need to carry out the computations with 34
93/// decimal digits (in order to represent the 4.5e16 ada without any rounding errors, we need
94/// double that precision).
95pub(crate) fn ev_lt_phi(phi_f: f64, ev: [u8; 64], stake: Stake, total_stake: Stake) -> bool {
96    use rug::{integer::Order, ops::Pow, Float};
97
98    // If phi_f = 1, then we automatically break with true
99    if (phi_f - 1.0).abs() < f64::EPSILON {
100        return true;
101    }
102    let ev = rug::Integer::from_digits(&ev, Order::LsfLe);
103    let ev_max: Float = Float::with_val(117, 2).pow(512);
104    let q = ev / ev_max;
105
106    let w = Float::with_val(117, stake) / Float::with_val(117, total_stake);
107    let phi = Float::with_val(117, 1.0) - Float::with_val(117, 1.0 - phi_f).pow(w);
108
109    q < phi
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use num_bigint::{BigInt, Sign};
116    use num_rational::Ratio;
117    use proptest::prelude::*;
118    // Implementation of `ev_lt_phi` without approximation. We only get the precision of f64 here.
119    fn simple_ev_lt_phi(phi_f: f64, ev: [u8; 64], stake: Stake, total_stake: Stake) -> bool {
120        let ev_max = BigInt::from(2u8).pow(512);
121        let ev = BigInt::from_bytes_le(Sign::Plus, &ev);
122        let q = Ratio::new_raw(ev, ev_max);
123
124        let w = stake as f64 / total_stake as f64;
125        let phi = Ratio::from_float(1.0 - (1.0 - phi_f).powf(w)).unwrap();
126        q < phi
127    }
128
129    proptest! {
130        #![proptest_config(ProptestConfig::with_cases(50))]
131
132        #[test]
133        /// Checking the ev_lt_phi function.
134        fn test_precision_approximation(
135            phi_f in 0.01..0.5f64,
136            ev_1 in any::<[u8; 32]>(),
137            ev_2 in any::<[u8; 32]>(),
138            total_stake in 100_000_000..1_000_000_000u64,
139            stake in 1_000_000..50_000_000u64
140        ) {
141            let mut ev = [0u8; 64];
142            ev.copy_from_slice(&[&ev_1[..], &ev_2[..]].concat());
143
144            let quick_result = simple_ev_lt_phi(phi_f, ev, stake, total_stake);
145            let result = ev_lt_phi(phi_f, ev, stake, total_stake);
146            assert_eq!(quick_result, result);
147        }
148
149        #[cfg(any(feature = "num-integer-backend", target_family = "wasm", windows))]
150        #[test]
151        /// Checking the early break of Taylor computation
152        fn early_break_taylor(
153            x in -0.9..0.9f64,
154        ) {
155            let exponential = num_traits::float::Float::exp(x);
156            let cmp_n = Ratio::from_float(exponential - 2e-10_f64).unwrap();
157            let cmp_p = Ratio::from_float(exponential + 2e-10_f64).unwrap();
158            assert!(taylor_comparison(1000, cmp_n, Ratio::from_float(x).unwrap()));
159            assert!(!taylor_comparison(1000, cmp_p, Ratio::from_float(x).unwrap()));
160        }
161    }
162}