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}