1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
pub mod community_advisors;
pub mod dreps;
pub mod proposers;
pub mod veterans;
pub mod voters;

use rust_decimal::Decimal;
pub type Funds = Decimal;
// Lets match to the same type as the funds, but naming it funds would be confusing
pub type Rewards = Decimal;
pub type VoteCount = HashMap<Identifier, HashSet<Hash>>;

use crate::types::proposal::FullProposalInfo;
use chain_impl_mockchain::certificate::ExternalProposalId;
use jormungandr_lib::crypto::{account::Identifier, hash::Hash};
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum Error {
    #[error("hash is not a valid blake2b256 hash")]
    InvalidHash(Vec<u8>),
}

/// Convert a slice of bytes that represent a valid `ExternalProposalId`, and returns an array of
/// the decoded 32-byte array.
pub(crate) fn chain_proposal_id_bytes(v: &[u8]) -> Result<[u8; 32], Error> {
    // the chain_proposal_id comes as hex-encoded string digest of a blake2b256 key
    // the first step is to decode the &str
    let chain_proposal_str =
        std::str::from_utf8(v).map_err(|_| Error::InvalidHash(v.to_owned()))?;
    // second step is to convert &str into a digest so that it can be converted into
    // [u8;32]
    let chain_proposal_id = ExternalProposalId::from_str(chain_proposal_str)
        .map_err(|_| Error::InvalidHash(v.to_owned()))?;
    let bytes: [u8; 32] = chain_proposal_id.into();
    Ok(bytes)
}

pub struct Threshold {
    total: usize,
    per_challenge: HashMap<i32, usize>,
    proposals_per_challenge: HashMap<i32, HashSet<Hash>>,
}

impl Threshold {
    pub fn new(
        total_threshold: usize,
        per_challenge: HashMap<i32, usize>,
        proposals: Vec<FullProposalInfo>,
    ) -> Result<Self, Error> {
        let proposals = proposals
            .into_iter()
            .map(|p| {
                let bytes = chain_proposal_id_bytes(&p.proposal.chain_proposal_id)?;
                Ok((p.proposal.challenge_id, Hash::from(bytes)))
            })
            .collect::<Result<Vec<_>, Error>>()?;
        Ok(Self {
            total: total_threshold,
            per_challenge,
            proposals_per_challenge: proposals.into_iter().fold(
                HashMap::new(),
                |mut acc, (challenge_id, hash)| {
                    acc.entry(challenge_id).or_default().insert(hash);
                    acc
                },
            ),
        })
    }

    fn filter(&self, votes: &HashSet<Hash>) -> bool {
        if votes.len() < self.total {
            return false;
        }

        for (challenge, threshold) in &self.per_challenge {
            let votes_in_challengs = self
                .proposals_per_challenge
                .get(challenge)
                .map(|props| votes.intersection(props).count())
                .unwrap_or_default();
            if votes_in_challengs < *threshold {
                return false;
            }
        }

        true
    }
}