Skip to main content

dusk_consensus/user/
sortition.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4//
5// Copyright (c) DUSK NETWORK. All rights reserved.
6
7use node_data::StepName;
8use node_data::bls::PublicKeyBytes;
9use node_data::ledger::Seed;
10use num_bigint::BigInt;
11use num_bigint::Sign::Plus;
12use sha3::{Digest, Sha3_256};
13
14use crate::config::{
15    PROPOSAL_COMMITTEE_CREDITS, RATIFICATION_COMMITTEE_CREDITS,
16    VALIDATION_COMMITTEE_CREDITS,
17};
18
19#[derive(Debug, Clone, Default, Eq, Hash, PartialEq)]
20pub struct Config {
21    seed: Seed,
22    round: u64,
23    pub step: u8,
24    committee_credits: usize,
25    exclusion: Vec<PublicKeyBytes>,
26}
27
28impl Config {
29    pub fn new(
30        seed: Seed,
31        round: u64,
32        iteration: u8,
33        step: StepName,
34        exclusion: Vec<PublicKeyBytes>,
35    ) -> Config {
36        let committee_credits = match step {
37            StepName::Proposal => PROPOSAL_COMMITTEE_CREDITS,
38            StepName::Ratification => RATIFICATION_COMMITTEE_CREDITS,
39            StepName::Validation => VALIDATION_COMMITTEE_CREDITS,
40        };
41        let step = step.to_step(iteration);
42        Self {
43            seed,
44            round,
45            step,
46            committee_credits,
47            exclusion,
48        }
49    }
50
51    pub fn committee_credits(&self) -> usize {
52        self.committee_credits
53    }
54
55    pub fn step(&self) -> u8 {
56        self.step
57    }
58
59    pub fn round(&self) -> u64 {
60        self.round
61    }
62
63    pub fn exclusion(&self) -> &Vec<PublicKeyBytes> {
64        &self.exclusion
65    }
66}
67
68// The deterministic procedure requires the set of active stakes,
69// ordered in ascending order from oldest to newest, the latest global seed,
70// current consensus round and current consensus step.
71
72pub fn create_sortition_hash(cfg: &Config, counter: u32) -> [u8; 32] {
73    let mut hasher = Sha3_256::new();
74
75    // write input message
76    hasher.update(&cfg.seed.inner()[..]);
77    hasher.update(cfg.step.to_le_bytes());
78    hasher.update(counter.to_le_bytes());
79
80    // read hash digest
81    let reader = hasher.finalize();
82    reader.as_slice().try_into().expect("Wrong length")
83}
84
85/// Generate a score from the given hash and total stake weight
86pub fn generate_sortition_score(
87    hash: [u8; 32],
88    total_weight: &BigInt,
89) -> BigInt {
90    let num = BigInt::from_bytes_be(Plus, hash.as_slice());
91    num % total_weight
92}
93
94#[cfg(test)]
95mod tests {
96
97    use dusk_bytes::DeserializableSlice;
98    use dusk_core::signatures::bls::{
99        PublicKey as BlsPublicKey, SecretKey as BlsSecretKey,
100    };
101    use node_data::ledger::Seed;
102
103    use super::*;
104    use crate::user::committee::Committee;
105    use crate::user::provisioners::{DUSK, Provisioners};
106    use crate::user::sortition::Config;
107
108    impl Config {
109        pub fn raw(
110            seed: Seed,
111            round: u64,
112            step: u8,
113            committee_credits: usize,
114            exclusion: Vec<PublicKeyBytes>,
115        ) -> Config {
116            Self {
117                seed,
118                round,
119                step,
120                committee_credits,
121                exclusion,
122            }
123        }
124    }
125
126    #[test]
127    pub fn test_sortition_hash() {
128        let hash = [
129            74, 64, 238, 174, 226, 52, 11, 105, 93, 251, 204, 6, 137, 176, 14,
130            96, 77, 139, 92, 76, 7, 178, 38, 16, 132, 233, 13, 180, 78, 206,
131            204, 31,
132        ];
133
134        assert_eq!(
135            create_sortition_hash(
136                &Config::raw(Seed::from([3; 48]), 10, 3, 0, vec![]),
137                1
138            )[..],
139            hash[..],
140        );
141    }
142
143    #[test]
144    pub fn test_generate_sortition_score() {
145        let dataset = vec![
146            ([3; 48], 123342342, 80689917),
147            ([4; 48], 44443333, 20330495),
148        ];
149
150        for (seed, total_weight, expected_score) in dataset {
151            let hash = create_sortition_hash(
152                &Config::raw(Seed::from(seed), 10, 3, 0, vec![]),
153                1,
154            );
155
156            let total_weight = BigInt::from(total_weight);
157            let res = generate_sortition_score(hash, &total_weight);
158
159            assert_eq!(res, BigInt::from(expected_score));
160        }
161    }
162
163    #[test]
164    fn test_deterministic_sortition_1() {
165        let p = generate_provisioners(5);
166
167        let committee_credits = 64;
168
169        // Execute sortition with specific config
170        let cfg = Config::raw(Seed::default(), 1, 1, 64, vec![]);
171
172        let committee = Committee::new(&p, &cfg);
173
174        // Verify expected committee credits
175        assert_eq!(
176            committee_credits,
177            committee.get_occurrences().iter().sum::<usize>()
178        );
179
180        // Verify expected distribution
181        assert_eq!(vec![4, 29, 9, 22], committee.get_occurrences());
182    }
183
184    #[test]
185    fn test_deterministic_sortition_2() {
186        let p = generate_provisioners(5);
187
188        let committee_credits = 45;
189        let cfg = Config::raw(
190            Seed::from([3u8; 48]),
191            7777,
192            8,
193            committee_credits,
194            vec![],
195        );
196
197        let committee = Committee::new(&p, &cfg);
198        assert_eq!(
199            committee_credits,
200            committee.get_occurrences().iter().sum::<usize>()
201        );
202        assert_eq!(vec![6, 13, 11, 15], committee.get_occurrences());
203    }
204
205    #[test]
206    fn test_deterministic_sortition_2_exclusion() {
207        let p = generate_provisioners(5);
208
209        let seed = Seed::from([3u8; 48]);
210        let round = 7777;
211        let committee_credits = 45;
212        let iteration = 2;
213        let relative_step = 2;
214        let step = iteration * 3 + relative_step;
215
216        let cfg = Config::raw(seed, round, step, committee_credits, vec![]);
217        let generator = p.get_generator(iteration, seed, round);
218        let committee = Committee::new(&p, &cfg);
219
220        committee
221            .iter()
222            .find(|&p| p.bytes() == &generator)
223            .expect("Generator to be included");
224        assert_eq!(
225            committee_credits,
226            committee.get_occurrences().iter().sum::<usize>()
227        );
228        assert_eq!(vec![6, 13, 11, 15], committee.get_occurrences());
229
230        // Run the same extraction, with the generator excluded
231        let cfg =
232            Config::raw(seed, round, step, committee_credits, vec![generator]);
233        let committee = Committee::new(&p, &cfg);
234
235        assert!(
236            committee
237                .iter()
238                .find(|&p| p.bytes() == &generator)
239                .is_none(),
240            "Generator to be excluded"
241        );
242        assert_eq!(
243            committee_credits,
244            committee.get_occurrences().iter().sum::<usize>()
245        );
246        assert_eq!(vec![8, 13, 24], committee.get_occurrences());
247    }
248
249    #[test]
250    fn test_quorum() {
251        let p = generate_provisioners(5);
252
253        let cfg = Config::raw(Seed::default(), 7777, 8, 64, vec![]);
254
255        let c = Committee::new(&p, &cfg);
256        assert_eq!(c.super_majority_quorum(), 43);
257    }
258
259    #[test]
260    fn test_intersect() {
261        let p = generate_provisioners(10);
262
263        let cfg = Config::raw(Seed::default(), 1, 3, 200, vec![]);
264        // println!("{:#?}", p);
265
266        let c = Committee::new(&p, &cfg);
267        // println!("{:#?}", c);
268
269        let max_bitset = (2_i32.pow((c.size()) as u32) - 1) as u64;
270        println!("max_bitset: {} / {:#064b} ", max_bitset, max_bitset);
271
272        for bitset in 0..max_bitset {
273            //println!("bitset: {:#064b}", bitset);
274            let result = c.intersect(bitset);
275            assert_eq!(
276                c.bits(&result),
277                bitset,
278                "testing with  bitset:{}",
279                bitset
280            );
281        }
282    }
283
284    fn generate_provisioners(n: usize) -> Provisioners {
285        let sks = [
286            "7f6f2ccdb23f2abb7b69278e947c01c6160a31cf02c19d06d0f6e5ab1d768b15",
287            "611830d3641a68f94a690dcc25d1f4b0dac948325ac18f6dd32564371735f32c",
288            "1fbec814b18b1d4c3eaa7cec41007e04bf0a98453b06ec7582aa29882c52eb3e",
289            "ecd9c4a53ea15f18447b08fb96a13c5ab7dc7d24067b102fcbaaf7b39ca52e2d",
290            "e463bcb1a6e57288ffd4671503082fa8656e3eacb78fb1925f8a7c76400e8e15",
291            "7a19fb2d099a9557f7c10c2efbb8b101d9e0ec85610d5c74a887d1d4fb8d2827",
292            "4dbad51eb408af559dd91bbbed8dbeae0a2c89e0e05f0cce87c98652a8437f1f",
293            "befba86ae9e0c207865f7e24e8349d4ecdbc8b0f4632842499a0dfa60568e20a",
294            "b260b8a10343bf5a5dacb4f1d32d06c4fdddc9981a3619fbc0a5cd9eb30f3334",
295            "87a9779748888da5d96bbbce041b5109c6ffc0c4f30561c0170384a5922d9e21",
296        ];
297        let sks: Vec<_> = sks
298            .iter()
299            .take(n)
300            .map(|hex| hex::decode(hex).expect("valid hex"))
301            .map(|data| {
302                BlsSecretKey::from_slice(&data[..]).expect("valid secret key")
303            })
304            .collect();
305
306        let mut p = Provisioners::empty();
307        for (i, sk) in sks.iter().enumerate().skip(1) {
308            let stake_value = 1000 * (i) as u64 * DUSK;
309            let stake_pk =
310                node_data::bls::PublicKey::new(BlsPublicKey::from(sk));
311            p.add_provisioner_with_value(stake_pk, stake_value);
312        }
313        p
314    }
315}