Skip to main content

fastcrypto_tbls/
nodes.rs

1// Copyright (c) 2022, Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::ecies;
5use crate::types::ShareIndex;
6use fastcrypto::error::{FastCryptoError, FastCryptoResult};
7use fastcrypto::groups::GroupElement;
8use fastcrypto::hash::{Blake2b256, Digest, HashFunction};
9use serde::{Deserialize, Serialize};
10use tracing::debug;
11
12pub type PartyId = u16;
13
14/// Public parameters of a party.
15#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
16pub struct Node<G: GroupElement> {
17    pub id: PartyId,
18    pub pk: ecies::PublicKey<G>,
19    pub weight: u16, // May be zero after reduce()
20}
21
22/// Wrapper for a set of nodes.
23#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
24pub struct Nodes<G: GroupElement> {
25    nodes: Vec<Node<G>>, // Party ids are 0..len(nodes)-1 (inclusive)
26    total_weight: u16,   // Share ids are 1..total_weight (inclusive)
27    // Next two fields are used to map share ids to party ids.
28    accumulated_weights: Vec<u16>, // Accumulated sum of all nodes' weights. Used to map share ids to party ids.
29    nodes_with_nonzero_weight: Vec<u16>, // Indexes of nodes with non-zero weight
30}
31
32impl<G: GroupElement + Serialize> Nodes<G> {
33    // We don't expect to have more than 1000 nodes (simplifies some checks).
34    const MAX_NODES: usize = 1000;
35
36    /// Create a new set of nodes. Nodes must have consecutive ids starting from 0.
37    pub fn new(nodes: Vec<Node<G>>) -> FastCryptoResult<Self> {
38        let mut nodes = nodes;
39        nodes.sort_by_key(|n| n.id);
40        // Check all ids are consecutive and start from 0
41        if (0..nodes.len()).any(|i| (nodes[i].id as usize) != i) {
42            return Err(FastCryptoError::InvalidInput);
43        }
44        // Make sure we never overflow in the functions below.
45        if nodes.is_empty() || nodes.len() > Self::MAX_NODES {
46            return Err(FastCryptoError::InvalidInput);
47        }
48        // Make sure we never overflow in the functions below, as we don't expect to have more than u16::MAX total weight.
49        let total_weight = nodes.iter().map(|n| n.weight as u32).sum::<u32>();
50        if total_weight > u16::MAX as u32 || total_weight == 0 {
51            return Err(FastCryptoError::InvalidInput);
52        }
53        let total_weight = total_weight as u16;
54
55        // We use the next two to map share ids to party ids.
56        let accumulated_weights = Self::get_accumulated_weights(&nodes);
57        let nodes_with_nonzero_weight = Self::filter_nonzero_weights(&nodes);
58
59        Ok(Self {
60            nodes,
61            total_weight,
62            accumulated_weights,
63            nodes_with_nonzero_weight,
64        })
65    }
66
67    fn filter_nonzero_weights(nodes: &[Node<G>]) -> Vec<u16> {
68        nodes
69            .iter()
70            .enumerate()
71            .filter_map(|(i, n)| if n.weight > 0 { Some(i as u16) } else { None })
72            .collect::<Vec<_>>()
73    }
74
75    fn get_accumulated_weights(nodes: &[Node<G>]) -> Vec<u16> {
76        nodes
77            .iter()
78            .filter_map(|n| if n.weight > 0 { Some(n.weight) } else { None })
79            .scan(0, |accumulated_weight, weight| {
80                *accumulated_weight += weight;
81                Some(*accumulated_weight)
82            })
83            .collect::<Vec<_>>()
84    }
85
86    /// Total weight of the nodes.
87    pub fn total_weight(&self) -> u16 {
88        self.total_weight
89    }
90
91    /// Number of nodes.
92    pub fn num_nodes(&self) -> usize {
93        self.nodes.len()
94    }
95
96    /// Get an iterator on the share ids.
97    pub fn share_ids_iter(&self) -> impl Iterator<Item = ShareIndex> {
98        (1..=self.total_weight).map(|i| ShareIndex::new(i).expect("nonzero"))
99    }
100
101    /// Get the node corresponding to a share id.
102    pub fn share_id_to_node(&self, share_id: &ShareIndex) -> FastCryptoResult<&Node<G>> {
103        let nonzero_node_id = self
104            .accumulated_weights
105            .binary_search(&share_id.get())
106            .unwrap_or_else(|i| i);
107        match self.nodes_with_nonzero_weight.get(nonzero_node_id) {
108            Some(node_id) => self.node_id_to_node(*node_id),
109            None => Err(FastCryptoError::InvalidInput),
110        }
111    }
112
113    pub fn node_id_to_node(&self, party_id: PartyId) -> FastCryptoResult<&Node<G>> {
114        self.nodes
115            .get(party_id as usize)
116            .ok_or(FastCryptoError::InvalidInput)
117    }
118
119    /// Get the share ids of a node (ordered). Returns error if the node does not exist.
120    pub fn share_ids_of(&self, id: PartyId) -> FastCryptoResult<Vec<ShareIndex>> {
121        // Check if the input is valid.
122        self.node_id_to_node(id)?;
123
124        // TODO: [perf opt] Cache this or impl differently.
125        Ok(self
126            .share_ids_iter()
127            .filter(|share_id| self.share_id_to_node(share_id).expect("valid share id").id == id)
128            .collect::<Vec<_>>())
129    }
130
131    /// Get an iterator on the nodes.
132    pub fn iter(&self) -> impl Iterator<Item = &Node<G>> {
133        self.nodes.iter()
134    }
135
136    // Used for logging.
137    pub fn hash(&self) -> Digest<32> {
138        let mut hash = Blake2b256::default();
139        hash.update(bcs::to_bytes(&self.nodes).expect("should serialize"));
140        hash.finalize()
141    }
142
143    /// Create a new set of nodes. Nodes must have consecutive ids starting from 0.
144    /// Reduces weights up to an allowed delta in the original total weight.
145    /// Finds the largest d such that:
146    /// - The new threshold is ceil(t / d)
147    /// - The new weights are all divided by d (floor division)
148    /// - The precision loss, counted as the sum of the remainders of the division by d, is at most
149    ///   the allowed delta
150    /// In practice, allowed delta will be the extra liveness we would assume above 2f+1.
151    ///
152    /// total_weight_lower_bound allows limiting the level of reduction (e.g., in benchmarks). To
153    /// get the best results, set it to 1.
154    pub fn new_reduced(
155        nodes_vec: Vec<Node<G>>,
156        t: u16,
157        allowed_delta: u16,
158        total_weight_lower_bound: u16,
159    ) -> FastCryptoResult<(Self, u16)> {
160        let n = Self::new(nodes_vec)?; // checks the input, etc
161        assert!(total_weight_lower_bound <= n.total_weight && total_weight_lower_bound > 0);
162        let mut max_d = 1;
163        for d in 2..=40 {
164            // Break if we reached the lower bound.
165            // U16 is safe here since total_weight is u16.
166            let new_total_weight = n.nodes.iter().map(|n| n.weight / d).sum::<u16>();
167            if new_total_weight < total_weight_lower_bound {
168                break;
169            }
170            // Compute the precision loss.
171            // U16 is safe here since total_weight is u16.
172            let delta = n.nodes.iter().map(|n| n.weight % d).sum::<u16>();
173            if delta <= allowed_delta {
174                max_d = d;
175            }
176        }
177        debug!(
178            "Nodes::reduce reducing from {} with max_d {}, allowed_delta {}, total_weight_lower_bound {}",
179            n.total_weight, max_d, allowed_delta, total_weight_lower_bound
180        );
181
182        let nodes = n
183            .nodes
184            .iter()
185            .map(|n| Node {
186                id: n.id,
187                pk: n.pk.clone(),
188                weight: n.weight / max_d,
189            })
190            .collect::<Vec<_>>();
191        let accumulated_weights = Self::get_accumulated_weights(&nodes);
192        let nodes_with_nonzero_weight = Self::filter_nonzero_weights(&nodes);
193        // U16 is safe here since the original total_weight is u16.
194        let total_weight = nodes.iter().map(|n| n.weight).sum::<u16>();
195        let new_t = t / max_d + (t % max_d != 0) as u16;
196        Ok((
197            Self {
198                nodes,
199                total_weight,
200                accumulated_weights,
201                nodes_with_nonzero_weight,
202            },
203            new_t,
204        ))
205    }
206}