mirror of
https://gitlab.torproject.org/tpo/core/tor.git
synced 2024-11-24 20:33:31 +01:00
rust: Add new ProtoverVote type and refactor functions to methods.
This adds a new type for votes upon `protover::ProtoEntry`s (technically, on `protover::UnvalidatedProtoEntry`s, because the C code does not validate based upon currently known protocols when voting, in order to maintain future-compatibility), and converts several functions which would have operated on this datatype into methods for ease-of-use and readability. This also fixes a behavioural differentce to the C version of protover_compute_vote(). The C version of protover_compute_vote() calls expand_protocol_list() which checks if there would be too many subprotocols *or* expanded individual version numbers, i.e. more than MAX_PROTOCOLS_TO_EXPAND, and does this *per vote* (but only in compute_vote(), everywhere else in the C seems to only care about the number of subprotocols, not the number of individual versions). We need to match its behaviour in Rust and ensure we're not allowing more than it would to get the votes to match. * ADD new `protover::ProtoverVote` datatype. * REMOVE the `protover::compute_vote()` function and refactor it into an equivalent-in-behaviour albeit more memory-efficient voting algorithm based on the new underlying `protover::protoset::ProtoSet` datatype, as `ProtoverVote::compute()`. * REMOVE the `protover::write_vote_to_string()` function, since this functionality is now generated by the impl_to_string_for_proto_entry!() macro for both `ProtoEntry` and `UnvalidatedProtoEntry` (the latter of which is the correct type to return from a voting protocol instance, since the entity voting may not know of all protocols being voted upon or known about by other voting parties). * FIXES part of #24031: https://bugs.torproject.org/24031 rust: Fix a difference in compute_vote() behaviour to C version.
This commit is contained in:
parent
26bafb3c33
commit
9abbd23df7
@ -270,6 +270,15 @@ impl UnvalidatedProtoEntry {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
let mut total: usize = 0;
|
||||
|
||||
for (_, versions) in self.iter() {
|
||||
total += versions.len();
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
/// Determine if we support every protocol a client supports, and if not,
|
||||
/// determine which protocols we do not have support for.
|
||||
///
|
||||
@ -467,62 +476,82 @@ impl From<ProtoEntry> for UnvalidatedProtoEntry {
|
||||
}
|
||||
}
|
||||
|
||||
/// Protocol voting implementation.
|
||||
/// A mapping of protocols to a count of how many times each of their `Version`s
|
||||
/// were voted for or supported.
|
||||
///
|
||||
/// Given a list of strings describing protocol versions, return a new
|
||||
/// string encoding all of the protocols that are listed by at
|
||||
/// least threshold of the inputs.
|
||||
/// # Warning
|
||||
///
|
||||
/// The string is sorted according to the following conventions:
|
||||
/// - Protocols names are alphabetized
|
||||
/// - Protocols are in order low to high
|
||||
/// - Individual and ranges are listed together. For example,
|
||||
/// "3, 5-10,13"
|
||||
/// - All entries are unique
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use protover::compute_vote;
|
||||
///
|
||||
/// let protos = vec![String::from("Link=3-4"), String::from("Link=3")];
|
||||
/// let vote = compute_vote(protos, 2);
|
||||
/// assert_eq!("Link=3", vote)
|
||||
/// ```
|
||||
pub fn compute_vote(
|
||||
list_of_proto_strings: Vec<String>,
|
||||
threshold: i32,
|
||||
) -> String {
|
||||
let empty = String::from("");
|
||||
/// The "protocols" are *not* guaranteed to be known/supported `Protocol`s, in
|
||||
/// order to allow new subprotocols to be introduced even if Directory
|
||||
/// Authorities don't yet know of them.
|
||||
pub struct ProtoverVote( HashMap<UnknownProtocol, HashMap<Version, usize>> );
|
||||
|
||||
if list_of_proto_strings.is_empty() {
|
||||
return empty;
|
||||
impl Default for ProtoverVote {
|
||||
fn default() -> ProtoverVote {
|
||||
ProtoverVote( HashMap::new() )
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for ProtoverVote {
|
||||
type Item = (UnknownProtocol, HashMap<Version, usize>);
|
||||
type IntoIter = hash_map::IntoIter<UnknownProtocol, HashMap<Version, usize>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoverVote {
|
||||
pub fn entry(&mut self, key: UnknownProtocol)
|
||||
-> hash_map::Entry<UnknownProtocol, HashMap<Version, usize>>
|
||||
{
|
||||
self.0.entry(key)
|
||||
}
|
||||
|
||||
// all_count is a structure to represent the count of the number of
|
||||
// supported versions for a specific protocol. For example, in JSON format:
|
||||
// {
|
||||
// "FirstSupportedProtocol": {
|
||||
// "1": "3",
|
||||
// "2": "1"
|
||||
// }
|
||||
// }
|
||||
// means that FirstSupportedProtocol has three votes which support version
|
||||
// 1, and one vote that supports version 2
|
||||
let mut all_count: HashMap<String, HashMap<Version, usize>> =
|
||||
HashMap::new();
|
||||
/// Protocol voting implementation.
|
||||
///
|
||||
/// Given a slice of `UnvalidatedProtoEntry`s and a vote `threshold`, return
|
||||
/// a new `UnvalidatedProtoEntry` encoding all of the protocols that are
|
||||
/// listed by at least `threshold` of the inputs.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use protover::ProtoverVote;
|
||||
/// use protover::UnvalidatedProtoEntry;
|
||||
///
|
||||
/// let protos: &[UnvalidatedProtoEntry] = &["Link=3-4".parse().unwrap(),
|
||||
/// "Link=3".parse().unwrap()];
|
||||
/// let vote = ProtoverVote::compute(protos, &2);
|
||||
/// assert_eq!("Link=3", vote.to_string());
|
||||
/// ```
|
||||
// C_RUST_COUPLED: /src/or/protover.c protover_compute_vote
|
||||
pub fn compute(proto_entries: &[UnvalidatedProtoEntry], threshold: &usize) -> UnvalidatedProtoEntry {
|
||||
let mut all_count: ProtoverVote = ProtoverVote::default();
|
||||
let mut final_output: UnvalidatedProtoEntry = UnvalidatedProtoEntry::default();
|
||||
|
||||
if proto_entries.is_empty() {
|
||||
return final_output;
|
||||
}
|
||||
|
||||
// parse and collect all of the protos and their versions and collect them
|
||||
for vote in list_of_proto_strings {
|
||||
let this_vote: HashMap<String, Versions> =
|
||||
match parse_protocols_from_string_with_no_validation(&vote) {
|
||||
Ok(result) => result,
|
||||
Err(_) => continue,
|
||||
};
|
||||
for (protocol, versions) in this_vote {
|
||||
let supported_vers: &mut HashMap<Version, usize> =
|
||||
all_count.entry(protocol).or_insert(HashMap::new());
|
||||
for vote in proto_entries {
|
||||
// C_RUST_DIFFERS: This doesn't actually differ, bu this check on
|
||||
// the total is here to make it match. Because the C version calls
|
||||
// expand_protocol_list() which checks if there would be too many
|
||||
// subprotocols *or* individual version numbers, i.e. more than
|
||||
// MAX_PROTOCOLS_TO_EXPAND, and does this *per vote*, we need to
|
||||
// match it's behaviour and ensure we're not allowing more than it
|
||||
// would.
|
||||
if vote.len() > MAX_PROTOCOLS_TO_EXPAND {
|
||||
continue;
|
||||
}
|
||||
|
||||
for version in versions.0 {
|
||||
for (protocol, versions) in vote.iter() {
|
||||
let supported_vers: &mut HashMap<Version, usize> =
|
||||
all_count.entry(protocol.clone()).or_insert(HashMap::new());
|
||||
|
||||
for version in versions.clone().expand() {
|
||||
let counter: &mut usize =
|
||||
supported_vers.entry(version).or_insert(0);
|
||||
*counter += 1;
|
||||
@ -530,57 +559,19 @@ pub fn compute_vote(
|
||||
}
|
||||
}
|
||||
|
||||
let mut final_output: HashMap<String, String> =
|
||||
HashMap::with_capacity(get_supported_protocols().split(" ").count());
|
||||
for (protocol, mut versions) in all_count {
|
||||
// Go through and remove versions that are less than the threshold
|
||||
versions.retain(|_, count| *count as usize >= *threshold);
|
||||
|
||||
// Go through and remove verstions that are less than the threshold
|
||||
for (protocol, versions) in all_count {
|
||||
let mut meets_threshold = HashSet::new();
|
||||
for (version, count) in versions {
|
||||
if count >= threshold as usize {
|
||||
meets_threshold.insert(version);
|
||||
if versions.len() > 0 {
|
||||
let voted_versions: Vec<Version> = versions.keys().cloned().collect();
|
||||
let voted_protoset: ProtoSet = ProtoSet::from(voted_versions);
|
||||
|
||||
final_output.insert(protocol, voted_protoset);
|
||||
}
|
||||
}
|
||||
|
||||
// For each protocol, compress its version list into the expected
|
||||
// protocol version string format
|
||||
let contracted = contract_protocol_list(&meets_threshold);
|
||||
if !contracted.is_empty() {
|
||||
final_output.insert(protocol, contracted);
|
||||
final_output
|
||||
}
|
||||
}
|
||||
|
||||
write_vote_to_string(&final_output)
|
||||
}
|
||||
|
||||
/// Return a String comprised of protocol entries in alphabetical order
|
||||
///
|
||||
/// # Inputs
|
||||
///
|
||||
/// * `vote`, a `HashMap` comprised of keys and values, both which are strings.
|
||||
/// The keys are the protocol names while values are a string representation of
|
||||
/// the supported versions.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `String` whose value is series of pairs, comprising of the protocol name
|
||||
/// and versions that it supports. The string takes the following format:
|
||||
///
|
||||
/// "first_protocol_name=1,2-5, second_protocol_name=4,5"
|
||||
///
|
||||
/// Sorts the keys in alphabetical order and creates the expected subprotocol
|
||||
/// entry format.
|
||||
///
|
||||
fn write_vote_to_string(vote: &HashMap<String, String>) -> String {
|
||||
let mut keys: Vec<&String> = vote.keys().collect();
|
||||
keys.sort();
|
||||
|
||||
let mut output = Vec::new();
|
||||
for key in keys {
|
||||
// TODO error in indexing here?
|
||||
output.push(format!("{}={}", key, vote[key]));
|
||||
}
|
||||
output.join(" ")
|
||||
}
|
||||
|
||||
/// Returns a boolean indicating whether the given protocol and version is
|
||||
|
Loading…
Reference in New Issue
Block a user