From 9570ec088345f5e1c0afbdade42c32ab47a1f647 Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Sun, 24 Sep 2023 15:15:51 -0500 Subject: [PATCH 01/76] wip --- crates/core/src/lib.rs | 1 + crates/core/src/topology_manager/mod.rs | 0 2 files changed, 1 insertion(+) create mode 100644 crates/core/src/topology_manager/mod.rs diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 02ddb6b48..76f831b4e 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -5,6 +5,7 @@ mod message; mod node; mod operations; mod resource_manager; +mod topology_manager; mod ring; mod router; mod runtime; diff --git a/crates/core/src/topology_manager/mod.rs b/crates/core/src/topology_manager/mod.rs new file mode 100644 index 000000000..e69de29bb From e51bce6d6f23dddf4e5a76e751a18befc7a4fbdd Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Wed, 27 Sep 2023 09:32:51 -0500 Subject: [PATCH 02/76] wip --- Cargo.lock | 111 ++++++++++++++++++ crates/core/Cargo.toml | 1 + crates/core/src/lib.rs | 2 +- crates/core/src/ring.rs | 2 +- crates/core/src/topology_manager/mod.rs | 62 ++++++++++ .../small_world_deviation_metric.rs | 91 ++++++++++++++ 6 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 crates/core/src/topology_manager/small_world_deviation_metric.rs diff --git a/Cargo.lock b/Cargo.lock index a4a7d79f0..9f87af2b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,6 +169,15 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arbitrary" version = "1.3.0" @@ -539,6 +548,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + [[package]] name = "byteorder" version = "1.4.3" @@ -1584,6 +1599,7 @@ dependencies = [ "futures", "itertools", "libp2p", + "nalgebra", "notify", "once_cell", "opentelemetry", @@ -2873,6 +2889,16 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef" +[[package]] +name = "matrixmultiply" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "md-5" version = "0.10.5" @@ -3027,6 +3053,33 @@ dependencies = [ "unsigned-varint", ] +[[package]] +name = "nalgebra" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "307ed9b18cc2423f29e83f84fd23a8e73628727990181f18641a8b5dc2ab1caa" +dependencies = [ + "approx", + "matrixmultiply", + "nalgebra-macros", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-macros" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91761aed67d03ad966ef783ae962ef9bbaca728d2dd7ceb7939ec110fffad998" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "netlink-packet-core" version = "0.4.2" @@ -3188,6 +3241,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -3209,6 +3271,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.16" @@ -3884,6 +3957,12 @@ dependencies = [ "getrandom 0.2.10", ] +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "rayon" version = "1.7.0" @@ -4257,6 +4336,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "safe_arch" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f398075ce1e6a179b46f51bd88d0598b92b00d3551f1a2d4ac49e771b56ac354" +dependencies = [ + "bytemuck", +] + [[package]] name = "same-file" version = "1.0.6" @@ -4490,6 +4578,19 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simba" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + [[package]] name = "simdutf8" version = "0.1.4" @@ -5867,6 +5968,16 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +[[package]] +name = "wide" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa469ffa65ef7e0ba0f164183697b89b854253fd31aeb92358b7b6155177d62f" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "widestring" version = "1.0.2" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 4d0f977b6..9bb28fbde 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -76,6 +76,7 @@ tracing-subscriber = { version = "0.3.16", optional = true } # internal deps freenet-stdlib = { path = "../../stdlib/rust", version = "0.0.5", features = ["net", "archive"] } +nalgebra = "0.32.3" [dev-dependencies] tracing = "0.1" diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 76f831b4e..dfb838d8b 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -5,12 +5,12 @@ mod message; mod node; mod operations; mod resource_manager; -mod topology_manager; mod ring; mod router; mod runtime; #[cfg(feature = "websocket")] pub mod server; +mod topology_manager; pub mod util; type DynError = Box; diff --git a/crates/core/src/ring.rs b/crates/core/src/ring.rs index 2a425c4e7..a430c0d46 100644 --- a/crates/core/src/ring.rs +++ b/crates/core/src/ring.rs @@ -391,7 +391,7 @@ impl Ring { /// An abstract location on the 1D ring, represented by a real number on the interal [0, 1] #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Copy)] -pub struct Location(f64); +pub struct Location(pub f64); impl Location { pub fn new(location: f64) -> Self { diff --git a/crates/core/src/topology_manager/mod.rs b/crates/core/src/topology_manager/mod.rs index e69de29bb..05c99b291 100644 --- a/crates/core/src/topology_manager/mod.rs +++ b/crates/core/src/topology_manager/mod.rs @@ -0,0 +1,62 @@ +mod small_world_deviation_metric; + +use std::collections::{BTreeMap, HashMap}; +use rand::Rng; +use crate::ring::*; + +pub(crate) struct TopologyManager {} + +impl TopologyManager { + + pub(crate) fn select_join_target_location(&self, peer_requests: PeerStatistics) -> Location { + let mut max_combined_rpm = 0.0; + let mut max_combined_rpm_location = None; + + for (_, peer_key_location_map) in peer_requests.0.iter() { + let mut peer_key_locations: Vec<_> = peer_key_location_map.iter().collect(); + peer_key_locations.sort_by_key(|(peer_key_location, _)| peer_key_location.location); + + let len = peer_key_locations.len(); + for i in 0..len { + let next_i = (i + 1) % len; // Wrap-around index + let combined_rpm = peer_key_locations[i].1.0 + peer_key_locations[next_i].1.0; + + if combined_rpm > max_combined_rpm { + max_combined_rpm = combined_rpm; + max_combined_rpm_location = Some(( + peer_key_locations[i].0.location, + peer_key_locations[next_i].0.location, + )); + } + } + } + + if let Some((location1, location2)) = max_combined_rpm_location { + let mut rng = rand::thread_rng(); + let random_location = if location1 < location2 { + rng.gen_range(location1.unwrap().0 .. location2.unwrap().0) + } else { + // Wrap-around case + let upper_weight = 1.0 - location1.unwrap().0; + let lower_weight = location2.unwrap().0; + let total_weight = upper_weight + lower_weight; + let random_weight = rng.gen_range(0.0..total_weight); + + if random_weight < upper_weight { + location1.unwrap().0 + random_weight + } else { + random_weight - upper_weight + } + }; + return Location(random_location); + } + + + Location::random() + } + +} + +pub struct RequestsPerMinute(f64); + +pub struct PeerStatistics(BTreeMap>); diff --git a/crates/core/src/topology_manager/small_world_deviation_metric.rs b/crates/core/src/topology_manager/small_world_deviation_metric.rs new file mode 100644 index 000000000..ef8dc1fd5 --- /dev/null +++ b/crates/core/src/topology_manager/small_world_deviation_metric.rs @@ -0,0 +1,91 @@ +extern crate nalgebra as na; + +/* +This module provides a metric called `SmallWorldDeviationMetric` to quantify +how well a given peer's connections in a peer-to-peer network approximate an +ideal small-world topology. The ideal topology is based on a 1D ring model +where the probability density of a connection at distance `r` is proportional +to `r^-1`. + +The metric is calculated as follows: +1. A normalization constant `C` is calculated to ensure that the total + probability of the ideal distribution is 1. +2. For a range of distances `X`, the ideal and actual proportions of peers + within `X` are calculated. +3. The area between the ideal and actual curves is calculated using trapezoidal + integration, yielding the `SmallWorldDeviationMetric`. + +A metric value close to 0 indicates that the actual distribution closely matches +the ideal. A positive value indicates a lack of long-range links, and a negative +value indicates a lack of short-range links. +*/ + +/// Calculate the normalization constant C for the ideal r^-1 distribution. +/// The integral is approximated using trapezoidal rule. +fn calculate_normalization_constant() -> f64 { + let mut sum = 0.0; + let step = 0.01; + let mut r = step; + while r <= 0.5 { + sum += 1.0 / r; + r += step; + } + sum *= step; // Multiply by step size to complete the trapezoidal rule + 1.0 / sum // Normalize so that total probability is 1 +} + +/// Calculate the ideal proportion of peers within distance X for the r^-1 distribution. +fn ideal_proportion_within_x(x: f64, c: f64) -> f64 { + c * (x.ln() - 0.01f64.ln()) +} + +/// Calculate the actual proportion of peers within distance X. +fn actual_proportion_within_x(connection_distances: &Vec, x: f64) -> f64 { + let count = connection_distances.iter().filter(|&&r| r <= x).count(); + count as f64 / connection_distances.len() as f64 +} + +/// Calculate the SmallWorldDeviationMetric as the area between the ideal and actual curves. +/// Uses trapezoidal rule for integration. +/// +/// Returns a metric where: +/// - 0.0 is ideal, indicating a perfect match with the ideal small-world topology. +/// - A negative value indicates the network is not clustered enough (lacks short-range links). +/// - A positive value indicates the network is too clustered (lacks long-range links). +pub(crate) fn small_world_deviation_metric(connection_distances: Vec) -> f64 { + let c = calculate_normalization_constant(); + let mut sum = 0.0; + let step = 0.01; + let mut x = step; + while x <= 0.5 { + let ideal = ideal_proportion_within_x(x, c); + let actual = actual_proportion_within_x(&connection_distances, x); + sum += actual - ideal; + x += step; + } + sum * step // Multiply by step size to complete the trapezoidal rule +} + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_small_world_deviation_metric() { + // Ideal case: distances drawn from an r^-1 distribution + let ideal_distances: Vec = vec![0.1, 0.2, 0.05, 0.4, 0.3]; // Replace with actual ideal distances + let metric_ideal = small_world_deviation_metric(ideal_distances); + assert!(metric_ideal.abs() < 0.1); // The metric should be close to zero for the ideal case + + // Non-ideal case 1: mostly short distances + let non_ideal_1: Vec = vec![0.01, 0.02, 0.03, 0.04, 0.05]; + let metric_non_ideal_1 = small_world_deviation_metric(non_ideal_1); + assert!(metric_non_ideal_1 > 0.1); // The metric should be significantly positive + + // Non-ideal case 2: mostly long distances + let non_ideal_2: Vec = vec![0.4, 0.45, 0.48, 0.49, 0.5]; + let metric_non_ideal_2 = small_world_deviation_metric(non_ideal_2); + assert!(metric_non_ideal_2 < -0.1); // The metric should be significantly negative + } +} From fa2009c4cf9e0e703c5a2e6f3cbc7f54a0e3213c Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Wed, 27 Sep 2023 16:51:14 -0500 Subject: [PATCH 03/76] wip --- crates/core/src/operations/join_ring.rs | 2 +- crates/core/src/topology_manager/mod.rs | 44 +------------------ .../small_world_deviation_metric.rs | 4 +- 3 files changed, 4 insertions(+), 46 deletions(-) diff --git a/crates/core/src/operations/join_ring.rs b/crates/core/src/operations/join_ring.rs index 050cbcbd3..0958d0186 100644 --- a/crates/core/src/operations/join_ring.rs +++ b/crates/core/src/operations/join_ring.rs @@ -924,7 +924,7 @@ mod messages { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub(crate) enum JoinRequest { StartReq { - target: PeerKeyLocation, + target: PeerKeyLocation, req_peer: PeerKey, hops_to_live: usize, max_hops_to_live: usize, diff --git a/crates/core/src/topology_manager/mod.rs b/crates/core/src/topology_manager/mod.rs index 05c99b291..649fefe4a 100644 --- a/crates/core/src/topology_manager/mod.rs +++ b/crates/core/src/topology_manager/mod.rs @@ -8,51 +8,9 @@ pub(crate) struct TopologyManager {} impl TopologyManager { + /// Identifies a location on the ring where pub(crate) fn select_join_target_location(&self, peer_requests: PeerStatistics) -> Location { - let mut max_combined_rpm = 0.0; - let mut max_combined_rpm_location = None; - - for (_, peer_key_location_map) in peer_requests.0.iter() { - let mut peer_key_locations: Vec<_> = peer_key_location_map.iter().collect(); - peer_key_locations.sort_by_key(|(peer_key_location, _)| peer_key_location.location); - - let len = peer_key_locations.len(); - for i in 0..len { - let next_i = (i + 1) % len; // Wrap-around index - let combined_rpm = peer_key_locations[i].1.0 + peer_key_locations[next_i].1.0; - - if combined_rpm > max_combined_rpm { - max_combined_rpm = combined_rpm; - max_combined_rpm_location = Some(( - peer_key_locations[i].0.location, - peer_key_locations[next_i].0.location, - )); - } - } - } - - if let Some((location1, location2)) = max_combined_rpm_location { - let mut rng = rand::thread_rng(); - let random_location = if location1 < location2 { - rng.gen_range(location1.unwrap().0 .. location2.unwrap().0) - } else { - // Wrap-around case - let upper_weight = 1.0 - location1.unwrap().0; - let lower_weight = location2.unwrap().0; - let total_weight = upper_weight + lower_weight; - let random_weight = rng.gen_range(0.0..total_weight); - - if random_weight < upper_weight { - location1.unwrap().0 + random_weight - } else { - random_weight - upper_weight - } - }; - return Location(random_location); - } - - Location::random() } } diff --git a/crates/core/src/topology_manager/small_world_deviation_metric.rs b/crates/core/src/topology_manager/small_world_deviation_metric.rs index ef8dc1fd5..6d45cc4c8 100644 --- a/crates/core/src/topology_manager/small_world_deviation_metric.rs +++ b/crates/core/src/topology_manager/small_world_deviation_metric.rs @@ -40,7 +40,7 @@ fn ideal_proportion_within_x(x: f64, c: f64) -> f64 { } /// Calculate the actual proportion of peers within distance X. -fn actual_proportion_within_x(connection_distances: &Vec, x: f64) -> f64 { +fn actual_proportion_within_x(connection_distances: &[f64], x: f64) -> f64 { let count = connection_distances.iter().filter(|&&r| r <= x).count(); count as f64 / connection_distances.len() as f64 } @@ -52,7 +52,7 @@ fn actual_proportion_within_x(connection_distances: &Vec, x: f64) -> f64 { /// - 0.0 is ideal, indicating a perfect match with the ideal small-world topology. /// - A negative value indicates the network is not clustered enough (lacks short-range links). /// - A positive value indicates the network is too clustered (lacks long-range links). -pub(crate) fn small_world_deviation_metric(connection_distances: Vec) -> f64 { +pub(crate) fn small_world_deviation_metric(connection_distances: &[f64]) -> f64 { let c = calculate_normalization_constant(); let mut sum = 0.0; let step = 0.01; From e6de98a3307fb2a764eba94609f69987574699ea Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Sat, 30 Sep 2023 21:57:28 -0500 Subject: [PATCH 04/76] wip --- Cargo.lock | 72 ++++++++++++++++- crates/core/Cargo.toml | 1 + ...ll_world_deviation_metric.rs => metric.rs} | 12 +-- crates/core/src/topology_manager/mod.rs | 22 +++-- .../src/topology_manager/small_world_rand.rs | 81 +++++++++++++++++++ 5 files changed, 171 insertions(+), 17 deletions(-) rename crates/core/src/topology_manager/{small_world_deviation_metric.rs => metric.rs} (88%) create mode 100644 crates/core/src/topology_manager/small_world_rand.rs diff --git a/Cargo.lock b/Cargo.lock index 9f87af2b8..cdc8f8391 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1599,7 +1599,7 @@ dependencies = [ "futures", "itertools", "libp2p", - "nalgebra", + "nalgebra 0.32.3", "notify", "once_cell", "opentelemetry", @@ -1614,6 +1614,7 @@ dependencies = [ "serde_json", "serde_with", "sqlx", + "statrs", "stretto", "thiserror", "tokio", @@ -3053,6 +3054,24 @@ dependencies = [ "unsigned-varint", ] +[[package]] +name = "nalgebra" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d506eb7e08d6329505faa8a3a00a5dcc6de9f76e0c77e4b75763ae3c770831ff" +dependencies = [ + "approx", + "matrixmultiply", + "nalgebra-macros 0.1.0", + "num-complex", + "num-rational", + "num-traits", + "rand", + "rand_distr", + "simba 0.6.0", + "typenum", +] + [[package]] name = "nalgebra" version = "0.32.3" @@ -3061,14 +3080,25 @@ checksum = "307ed9b18cc2423f29e83f84fd23a8e73628727990181f18641a8b5dc2ab1caa" dependencies = [ "approx", "matrixmultiply", - "nalgebra-macros", + "nalgebra-macros 0.2.1", "num-complex", "num-rational", "num-traits", - "simba", + "simba 0.8.1", "typenum", ] +[[package]] +name = "nalgebra-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01fcc0b8149b4632adc89ac3b7b31a12fb6099a0317a4eb2ebff574ef7de7218" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "nalgebra-macros" version = "0.2.1" @@ -3957,6 +3987,16 @@ dependencies = [ "getrandom 0.2.10", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand", +] + [[package]] name = "rawpointer" version = "0.2.1" @@ -4578,6 +4618,19 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simba" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b7840f121a46d63066ee7a99fc81dcabbc6105e437cae43528cea199b5a05f" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + [[package]] name = "simba" version = "0.8.1" @@ -4911,6 +4964,19 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "statrs" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d08e5e1748192713cc281da8b16924fb46be7b0c2431854eadc785823e5696e" +dependencies = [ + "approx", + "lazy_static", + "nalgebra 0.29.0", + "num-traits", + "rand", +] + [[package]] name = "stretto" version = "0.8.1" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 9bb28fbde..47c0859ad 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -83,6 +83,7 @@ tracing = "0.1" arbitrary = { version = "1", features = ["derive"] } itertools = "0.11" pico-args = "0.5" +statrs = "0.16.0" freenet-stdlib = { path = "../../stdlib/rust", version = "0.0.5", features = ["testing", "net"] } [features] diff --git a/crates/core/src/topology_manager/small_world_deviation_metric.rs b/crates/core/src/topology_manager/metric.rs similarity index 88% rename from crates/core/src/topology_manager/small_world_deviation_metric.rs rename to crates/core/src/topology_manager/metric.rs index 6d45cc4c8..c7ab7126f 100644 --- a/crates/core/src/topology_manager/small_world_deviation_metric.rs +++ b/crates/core/src/topology_manager/metric.rs @@ -52,7 +52,7 @@ fn actual_proportion_within_x(connection_distances: &[f64], x: f64) -> f64 { /// - 0.0 is ideal, indicating a perfect match with the ideal small-world topology. /// - A negative value indicates the network is not clustered enough (lacks short-range links). /// - A positive value indicates the network is too clustered (lacks long-range links). -pub(crate) fn small_world_deviation_metric(connection_distances: &[f64]) -> f64 { +pub(crate) fn measure_small_worldness(connection_distances: &[f64]) -> f64 { let c = calculate_normalization_constant(); let mut sum = 0.0; let step = 0.01; @@ -75,17 +75,17 @@ mod tests { fn test_small_world_deviation_metric() { // Ideal case: distances drawn from an r^-1 distribution let ideal_distances: Vec = vec![0.1, 0.2, 0.05, 0.4, 0.3]; // Replace with actual ideal distances - let metric_ideal = small_world_deviation_metric(ideal_distances); + let metric_ideal = measure_small_worldness(&ideal_distances); assert!(metric_ideal.abs() < 0.1); // The metric should be close to zero for the ideal case // Non-ideal case 1: mostly short distances - let non_ideal_1: Vec = vec![0.01, 0.02, 0.03, 0.04, 0.05]; - let metric_non_ideal_1 = small_world_deviation_metric(non_ideal_1); + let non_ideal_1: &[f64] = &[0.01, 0.02, 0.03, 0.04, 0.05]; + let metric_non_ideal_1 = measure_small_worldness(&non_ideal_1); assert!(metric_non_ideal_1 > 0.1); // The metric should be significantly positive // Non-ideal case 2: mostly long distances - let non_ideal_2: Vec = vec![0.4, 0.45, 0.48, 0.49, 0.5]; - let metric_non_ideal_2 = small_world_deviation_metric(non_ideal_2); + let non_ideal_2: &[f64] = &[0.4, 0.45, 0.48, 0.49, 0.5]; + let metric_non_ideal_2 = measure_small_worldness(&non_ideal_2); assert!(metric_non_ideal_2 < -0.1); // The metric should be significantly negative } } diff --git a/crates/core/src/topology_manager/mod.rs b/crates/core/src/topology_manager/mod.rs index 649fefe4a..4be4b7542 100644 --- a/crates/core/src/topology_manager/mod.rs +++ b/crates/core/src/topology_manager/mod.rs @@ -1,19 +1,25 @@ -mod small_world_deviation_metric; +mod metric; +mod small_world_rand; use std::collections::{BTreeMap, HashMap}; use rand::Rng; use crate::ring::*; -pub(crate) struct TopologyManager {} +/// Identifies a location on the ring where +pub(crate) fn select_join_target_location(my_location : &Location, peer_statistics: &PeerStatistics) -> Location { + let min_distance : f64 = peer_statistics.0.keys().map(|location| location.distance(my_location)).min().unwrap_or(Distance::new(0.5)).as_f64(); -impl TopologyManager { + let maximum_min_distance = 0.01; - /// Identifies a location on the ring where - pub(crate) fn select_join_target_location(&self, peer_requests: PeerStatistics) -> Location { - - } + let min_distance = if min_distance > maximum_min_distance { min_distance } else { maximum_min_distance }; + if peer_statistics.0.len() < 10 { + let dist = small_world_rand::random_link_distance(min_distance); + + return Location::new(0.0); + } + todo!() +} -} pub struct RequestsPerMinute(f64); diff --git a/crates/core/src/topology_manager/small_world_rand.rs b/crates/core/src/topology_manager/small_world_rand.rs new file mode 100644 index 000000000..370a7f7a3 --- /dev/null +++ b/crates/core/src/topology_manager/small_world_rand.rs @@ -0,0 +1,81 @@ +use rand::Rng; + +// Function to generate a random link distance based on Kleinberg's d^{-1} distribution +pub(crate) fn random_link_distance(d_min: f64) -> f64 { + let d_max = 0.5; + + // Generate a uniform random number between 0 and 1 + let u: f64 = rand::thread_rng().gen_range(0.0..1.0); + + // Correct Inverse CDF: F^{-1}(u) = d_min * (d_max / d_min).powf(u) + let d = d_min * (d_max / d_min).powf(u); + + return d; +} + +#[cfg(test)] +mod tests { + use crate::topology_manager::metric::measure_small_worldness; + + use super::*; + use statrs::distribution::*; + use tracing_subscriber::Layer; + + #[test] + fn chi_squared_test() { + let d_min = 0.01; + let d_max = 0.5; + let n = 10000; // Number of samples + let num_bins = 20; // Number of bins for histogram + let mut bins = vec![0; num_bins]; + + // Generate a bunch of link distances + for _ in 0..n { + let d = random_link_distance(d_min); + let bin_index = ((d - d_min) / (d_max - d_min) * (num_bins as f64)).floor() as usize; + if bin_index < num_bins { + bins[bin_index] += 1; + } + } + + // Perform chi-squared test + let mut expected_counts = vec![0.0; num_bins]; + for i in 0..num_bins { + let lower = d_min + (d_max - d_min) * (i as f64 / num_bins as f64); + let upper = d_min + (d_max - d_min) * ((i as f64 + 1.0) / num_bins as f64); + expected_counts[i] = ((upper - lower) / (upper.powf(-1.0) - lower.powf(-1.0))).floor() * n as f64 / num_bins as f64; + } + + let chi_squared = expected_counts.iter().zip(bins.iter()).map(|(&e, &o)| { + ((o as f64 - e) * (o as f64 - e)) / e + }).sum::(); + + // Degrees of freedom is num_bins - 1 + let dof = num_bins - 1; + let chi = ChiSquared::new(dof as f64).unwrap(); + + let p_value = 1.0 - chi.cdf(chi_squared); + + // Check if p_value is above 0.05, indicating that we fail to reject the null hypothesis + assert!(p_value > 0.05, "Chi-squared test failed, p_value = {}", p_value); + } + + #[test] + fn metric_test() { + let d_min = 0.01; + let d_max = 0.5; + let n = 1000; // Number of samples + let mut distances = vec![]; + // Generate a bunch of link distances + for _ in 0..n { + distances.push(random_link_distance(d_min)); + } + + let metric = measure_small_worldness(&distances); + + println!("Small-world deviation metric = {}", metric); + + // Check if metric is close to 0.0, indicating that the network is close to the ideal small-world topology + assert!(metric.abs() < 0.1, "Small-world deviation metric is too high, metric = {}", metric); + } +} From 47c305e0f34e6f1118f00d683953312b19681ccc Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Fri, 22 Sep 2023 16:50:36 +0200 Subject: [PATCH 05/76] Join executor to network ops --- Cargo.lock | 153 +++++----- crates/core/src/client_events.rs | 4 +- crates/core/src/client_events/combinator.rs | 12 +- crates/core/src/contract.rs | 51 ++-- crates/core/src/contract/executor.rs | 269 +++++++++++++----- crates/core/src/contract/handler.rs | 167 ++++++++--- crates/core/src/contract/in_memory.rs | 15 +- crates/core/src/contract/storages/rocks_db.rs | 6 +- crates/core/src/contract/storages/sqlite.rs | 13 +- crates/core/src/message.rs | 18 +- crates/core/src/node.rs | 268 ++++++++++------- crates/core/src/node/conn_manager.rs | 47 ++- .../core/src/node/conn_manager/p2p_protoc.rs | 63 +++- .../node/{event_listener.rs => event_log.rs} | 12 +- crates/core/src/node/in_memory_impl.rs | 58 ++-- crates/core/src/node/op_state.rs | 60 ++-- crates/core/src/node/p2p_impl.rs | 42 ++- crates/core/src/node/tests.rs | 2 +- crates/core/src/operations.rs | 46 ++- crates/core/src/operations/get.rs | 83 ++++-- crates/core/src/operations/join_ring.rs | 6 + crates/core/src/operations/op_trait.rs | 2 + crates/core/src/operations/put.rs | 57 +++- crates/core/src/operations/state_machine.rs | 110 ------- crates/core/src/operations/subscribe.rs | 20 +- crates/core/src/operations/update.rs | 6 +- crates/core/src/server/mod.rs | 2 +- stdlib | 2 +- 28 files changed, 1009 insertions(+), 585 deletions(-) rename crates/core/src/node/{event_listener.rs => event_log.rs} (96%) delete mode 100644 crates/core/src/operations/state_machine.rs diff --git a/Cargo.lock b/Cargo.lock index cdc8f8391..481c4a797 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,9 +87,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2135563fb5c609d2b2b87c1e8ce7bc41b0b45430fa9661f457981503dd5bf0" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" dependencies = [ "memchr", ] @@ -322,9 +322,9 @@ checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" [[package]] name = "atomic-waker" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" @@ -490,9 +490,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "199c42ab6972d92c9f8995f086273d25c42fc0f7b2a1fcefba465c1352d25ba5" +checksum = "0231f06152bf547e9c2b5194f247cd97aacf6dcd8b15d8e5ec0663f64580da87" dependencies = [ "arrayref", "arrayvec", @@ -705,9 +705,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.4" +version = "4.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136" +checksum = "824956d0dca8334758a5b7f7e50518d66ea319330cbceedcf76905c2f6ab30e3" dependencies = [ "clap_builder", "clap_derive", @@ -715,9 +715,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.4" +version = "4.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56" +checksum = "122ec64120a49b4563ccaedcbea7818d069ed8e9aa6d829b82d8a4128936b2ab" dependencies = [ "anstream", "anstyle", @@ -751,9 +751,9 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "concurrent-queue" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" dependencies = [ "crossbeam-utils", ] @@ -1095,9 +1095,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622178105f911d937a42cdb140730ba4a3ed2becd8ae6ce39c7d28b5d75d4588" +checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" dependencies = [ "cfg-if", "cpufeatures", @@ -1338,7 +1338,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" dependencies = [ - "curve25519-dalek 4.1.0", + "curve25519-dalek 4.1.1", "ed25519", "rand_core 0.6.4", "serde", @@ -1469,9 +1469,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fdev" @@ -2457,9 +2457,9 @@ dependencies = [ [[package]] name = "libp2p-core" -version = "0.40.0" +version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef7dd7b09e71aac9271c60031d0e558966cdb3253ba0308ab369bb2de80630d0" +checksum = "dd44289ab25e4c9230d9246c475a22241e301b23e8f4061d3bdef304a1a99713" dependencies = [ "either", "fnv", @@ -2592,7 +2592,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71ce70757f2c0d82e9a3ef738fb10ea0723d16cec37f078f719e2c247704c1bb" dependencies = [ "bytes", - "curve25519-dalek 4.1.0", + "curve25519-dalek 4.1.1", "futures", "libp2p-core", "libp2p-identity", @@ -2612,9 +2612,9 @@ dependencies = [ [[package]] name = "libp2p-ping" -version = "0.43.0" +version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd5ee3270229443a2b34b27ed0cb7470ef6b4a6e45e54e89a8771fa683bab48" +checksum = "e702d75cd0827dfa15f8fd92d15b9932abe38d10d21f47c50438c71dd1b5dae3" dependencies = [ "either", "futures", @@ -2671,9 +2671,9 @@ dependencies = [ [[package]] name = "libp2p-swarm" -version = "0.43.3" +version = "0.43.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28016944851bd73526d3c146aabf0fa9bbe27c558f080f9e5447da3a1772c01a" +checksum = "f0cf749abdc5ca1dce6296dc8ea0f012464dfcfd3ddd67ffc0cabd8241c4e1da" dependencies = [ "either", "fnv", @@ -2886,9 +2886,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "matchit" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "matrixmultiply" @@ -2902,10 +2902,11 @@ dependencies = [ [[package]] name = "md-5" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ + "cfg-if", "digest 0.10.7", ] @@ -3509,9 +3510,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" +checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" [[package]] name = "parking_lot" @@ -3590,9 +3591,9 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pest" -version = "2.7.3" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a4d085fd991ac8d5b05a147b437791b4260b76326baf0fc60cf7c9c27ecd33" +checksum = "c022f1e7b65d6a24c0dbbd5fb344c66881bc01f3e5ae74a1c8100f2f985d98a4" dependencies = [ "memchr", "thiserror", @@ -3601,9 +3602,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.3" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bee7be22ce7918f641a33f08e3f43388c7656772244e2bbb2477f44cc9021a" +checksum = "35513f630d46400a977c4cb58f78e1bfbe01434316e60c37d27b9ad6139c66d8" dependencies = [ "pest", "pest_generator", @@ -3611,9 +3612,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.3" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1511785c5e98d79a05e8a6bc34b4ac2168a0e3e92161862030ad84daa223141" +checksum = "bc9fc1b9e7057baba189b5c626e2d6f40681ae5b6eb064dc7c7834101ec8123a" dependencies = [ "pest", "pest_meta", @@ -3624,9 +3625,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.3" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42f0394d3123e33353ca5e1e89092e533d2cc490389f2bd6131c43c634ebc5f" +checksum = "1df74e9e7ec4053ceb980e7c0c8bd3594e977fde1af91daba9c928e8e8c6708d" dependencies = [ "once_cell", "pest", @@ -3905,9 +3906,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.10.4" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13f81c9a9d574310b8351f8666f5a93ac3b0069c45c28ad52c10291389a7cf9" +checksum = "2c78e758510582acc40acb90458401172d41f1016f8c9dde89e49677afb7eec1" dependencies = [ "bytes", "rand", @@ -4005,9 +4006,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" dependencies = [ "either", "rayon-core", @@ -4015,14 +4016,12 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", ] [[package]] @@ -4136,9 +4135,9 @@ dependencies = [ [[package]] name = "rend" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581008d2099240d37fb08d77ad713bcaec2c4d89d50b5b21a8bb1996bbab68ab" +checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd" dependencies = [ "bytecheck", ] @@ -4311,9 +4310,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.13" +version = "0.38.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" +checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" dependencies = [ "bitflags 2.4.0", "errno", @@ -4345,9 +4344,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.5" +version = "0.101.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" dependencies = [ "ring", "untrusted", @@ -4433,9 +4432,9 @@ checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6" [[package]] name = "semver" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" dependencies = [ "serde", ] @@ -4554,9 +4553,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -4678,9 +4677,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "snow" @@ -4691,7 +4690,7 @@ dependencies = [ "aes-gcm", "blake2", "chacha20poly1305 0.9.1", - "curve25519-dalek 4.1.0", + "curve25519-dalek 4.1.1", "rand_core 0.6.4", "ring", "rustc_version", @@ -5111,9 +5110,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", - "fastrand 2.0.0", + "fastrand 2.0.1", "redox_syscall 0.3.5", - "rustix 0.38.13", + "rustix 0.38.14", "windows-sys 0.48.0", ] @@ -5171,9 +5170,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" +checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" dependencies = [ "deranged", "itoa", @@ -5184,15 +5183,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] @@ -5255,9 +5254,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2dbec703c26b00d74844519606ef15d09a7d6857860f84ad223dec002ddea2" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", @@ -5267,9 +5266,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ "bytes", "futures-core", @@ -5528,9 +5527,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e862a1c4128df0112ab625f55cd5c934bcb4312ba80b39ae4b4835a3fd58e649" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" dependencies = [ "byteorder", "bytes", @@ -5713,9 +5712,9 @@ checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] name = "waker-fn" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" [[package]] name = "walkdir" @@ -6068,9 +6067,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] diff --git a/crates/core/src/client_events.rs b/crates/core/src/client_events.rs index 3195bdd42..b8574ed32 100644 --- a/crates/core/src/client_events.rs +++ b/crates/core/src/client_events.rs @@ -93,7 +93,7 @@ impl From for AuthToken { #[non_exhaustive] pub struct OpenRequest<'a> { - pub id: ClientId, + pub client_id: ClientId, pub request: Box>, pub notification_channel: Option>, pub token: Option, @@ -109,7 +109,7 @@ impl<'a> OpenRequest<'a> { pub fn new(id: ClientId, request: Box>) -> Self { Self { - id, + client_id: id, request, notification_channel: None, token: None, diff --git a/crates/core/src/client_events/combinator.rs b/crates/core/src/client_events/combinator.rs index 44ea7a2c8..f528c39ca 100644 --- a/crates/core/src/client_events/combinator.rs +++ b/crates/core/src/client_events/combinator.rs @@ -84,7 +84,7 @@ impl super::ClientEventsProxy for ClientEventsCombinator { .map(|res| { match res { Ok(OpenRequest { - id: external, + client_id: external, request, notification_channel, token, @@ -103,7 +103,7 @@ impl super::ClientEventsProxy for ClientEventsCombinator { }); Ok(OpenRequest { - id, + client_id: id, request, notification_channel, token, @@ -162,9 +162,9 @@ async fn client_fn( } client_msg = client.recv() => { match client_msg { - Ok(OpenRequest {id, request, notification_channel, token}) => { - tracing::debug!("received msg @ combinator from external id {id}, msg: {request}"); - if tx_host.send(Ok(OpenRequest { id, request, notification_channel, token })).await.is_err() { + Ok(OpenRequest { client_id, request, notification_channel, token }) => { + tracing::debug!("received msg @ combinator from external id {client_id}, msg: {request}"); + if tx_host.send(Ok(OpenRequest { client_id, request, notification_channel, token })).await.is_err() { break; } } @@ -308,7 +308,7 @@ mod test { .unwrap(); for i in 0..3 { - let OpenRequest { id, .. } = combinator.recv().await.unwrap(); + let OpenRequest { client_id: id, .. } = combinator.recv().await.unwrap(); eprintln!("received: {id:?}"); assert_eq!(ClientId::new(i), id); } diff --git a/crates/core/src/contract.rs b/crates/core/src/contract.rs index 2d6b48a9d..ef3d77fdd 100644 --- a/crates/core/src/contract.rs +++ b/crates/core/src/contract.rs @@ -7,30 +7,32 @@ mod handler; mod in_memory; pub mod storages; +pub(crate) use executor::{ + executor_channel, ExecutorToEventLoopChannel, NetworkEventListenerHalve, +}; pub(crate) use handler::{ - contract_handler_channel, CHSenderHalve, ContractHandler, ContractHandlerChannel, - ContractHandlerEvent, NetworkContractHandler, StoreResponse, + contract_handler_channel, ClientResponses, ClientResponsesSender, ContractHandler, + ContractHandlerEvent, ContractHandlerToEventLoopChannel, EventId, NetEventListener, + NetworkContractHandler, StoreResponse, }; #[cfg(test)] pub(crate) use in_memory::{MemoryContractHandler, MockRuntime}; -use executor::ContractExecutor; pub use executor::{Executor, ExecutorError, OperationMode}; +use executor::ContractExecutor; + pub(crate) async fn contract_handling<'a, CH>(mut contract_handler: CH) -> Result<(), ContractError> where CH: ContractHandler + Send + 'static, { loop { - let res = contract_handler.channel().recv_from_listener().await?; - match res { - ( - _id, - ContractHandlerEvent::GetQuery { - key, - fetch_contract, - }, - ) => { + let (id, event) = contract_handler.channel().recv_from_event_loop().await?; + match event { + ContractHandlerEvent::GetQuery { + key, + fetch_contract, + } => { match contract_handler .executor() .fetch_contract(key.clone(), fetch_contract) @@ -39,8 +41,8 @@ where Ok((state, contract)) => { contract_handler .channel() - .send_to_listener( - _id, + .send_to_event_loop( + id, ContractHandlerEvent::GetResponse { key, response: Ok(StoreResponse { @@ -55,8 +57,8 @@ where tracing::warn!("error while executing get contract query: {err}"); contract_handler .channel() - .send_to_listener( - _id, + .send_to_event_loop( + id, ContractHandlerEvent::GetResponse { key, response: Err(err.into()), @@ -66,30 +68,27 @@ where } } } - (id, ContractHandlerEvent::Cache(contract)) => { + ContractHandlerEvent::Cache(contract) => { match contract_handler.executor().store_contract(contract).await { Ok(_) => { contract_handler .channel() - .send_to_listener(id, ContractHandlerEvent::CacheResult(Ok(()))) + .send_to_event_loop(id, ContractHandlerEvent::CacheResult(Ok(()))) .await?; } Err(err) => { let err = ContractError::ContractRuntimeError(err); contract_handler .channel() - .send_to_listener(id, ContractHandlerEvent::CacheResult(Err(err))) + .send_to_event_loop(id, ContractHandlerEvent::CacheResult(Err(err))) .await?; } } } - ( - _id, - ContractHandlerEvent::PutQuery { - key: _key, - state: _state, - }, - ) => { + ContractHandlerEvent::PutQuery { + key: _key, + state: _state, + } => { // let _put_result = contract_handler // .handle_request(ClientRequest::Put { // contract: todo!(), diff --git a/crates/core/src/contract/executor.rs b/crates/core/src/contract/executor.rs index 1c1e73c48..01236c011 100644 --- a/crates/core/src/contract/executor.rs +++ b/crates/core/src/contract/executor.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::fmt::Display; -use std::hint::unreachable_unchecked; +use std::sync::Arc; use std::time::{Duration, Instant}; use blake3::traits::digest::generic_array::GenericArray; @@ -14,21 +14,24 @@ use freenet_stdlib::client_api::{ RequestError, }; use freenet_stdlib::prelude::*; -use tokio::sync::mpsc::UnboundedSender; +use tokio::sync::mpsc::{self}; +use crate::message::Transaction; +use crate::node::OpManager; #[cfg(any( not(feature = "local-mode"), feature = "network-mode", all(not(feature = "local-mode"), not(feature = "network-mode")) ))] use crate::operations::get::GetResult; +use crate::operations::{OpEnum, OpError}; use crate::runtime::{ ContractRuntimeInterface, ContractStore, DelegateRuntimeInterface, DelegateStore, Runtime, SecretsStore, StateStore, StateStoreError, }; use crate::{ client_events::{ClientId, HostResult}, - node::{NodeConfig, OpManager, P2pBridge}, + node::{NodeConfig, P2pBridge}, operations::{self, op_trait::Operation}, DynError, }; @@ -98,27 +101,90 @@ pub enum OperationMode { Network, } -#[cfg(any( - not(feature = "local-mode"), - feature = "network-mode", - all(not(feature = "local-mode"), not(feature = "network-mode")) -))] -// for now just mocking up requests to the network -async fn op_request(_request: M) -> Result -where - Op: Operation, - M: ComposeNetworkMessage, -{ +pub(crate) struct ExecutorToEventLoopChannel { + op_manager: Arc, + end: End, +} + +pub(crate) fn executor_channel( + op_manager: Arc, +) -> ( + ExecutorToEventLoopChannel, + ExecutorToEventLoopChannel, +) { + let (sender, _b) = mpsc::channel(1); + let listener_halve = ExecutorToEventLoopChannel { + op_manager: op_manager.clone(), + end: NetworkEventListenerHalve, + }; + let sender_halve = ExecutorToEventLoopChannel { + op_manager: op_manager.clone(), + end: ExecutorHalve { sender }, + }; + (listener_halve, sender_halve) +} + +#[cfg(test)] +pub(crate) fn executor_channel_test() -> ( + ExecutorToEventLoopChannel, + ExecutorToEventLoopChannel, +) { todo!() } +impl ExecutorToEventLoopChannel { + async fn send_to_event_loop(&mut self, message: T) -> Result<(), DynError> + where + T: ComposeNetworkMessage, + Op: Operation + Send + 'static, + { + let op = message.initiate_op(&self.op_manager); + self.end.sender.send(*op.id()).await?; + >::resume_op(op, &self.op_manager).await?; + Ok(()) + } +} + +impl ExecutorToEventLoopChannel { + pub async fn transaction_from_executor(&mut self) -> Transaction { + todo!() + } + + pub async fn response(&mut self, _result: OpEnum) { + todo!() + } +} + +impl Clone for ExecutorToEventLoopChannel { + fn clone(&self) -> Self { + todo!() + } +} + +pub(crate) struct NetworkEventListenerHalve; +pub(crate) struct ExecutorHalve { + sender: mpsc::Sender, +} + +mod sealed { + use super::{ExecutorHalve, NetworkEventListenerHalve}; + pub(crate) trait ChannelHalve {} + impl ChannelHalve for NetworkEventListenerHalve {} + impl ChannelHalve for ExecutorHalve {} +} + #[allow(unused)] +#[async_trait::async_trait] trait ComposeNetworkMessage where Self: Sized, - Op: Operation, + Op: Operation + Send + 'static, { - fn get_message(self, manager: &OpManager) -> Op::Message { + fn initiate_op(self, op_manager: &OpManager) -> Op { + todo!() + } + + async fn resume_op(op: Op, op_manager: &OpManager) -> Result { todo!() } } @@ -129,7 +195,21 @@ struct GetContract { fetch_contract: bool, } -impl ComposeNetworkMessage for GetContract {} +#[async_trait::async_trait] +impl ComposeNetworkMessage for GetContract { + fn initiate_op(self, op_manager: &OpManager) -> operations::get::GetOp { + operations::get::start_op(self.key, self.fetch_contract, &op_manager.ring.peer_key) + } + + async fn resume_op( + op: operations::get::GetOp, + op_manager: &OpManager, + ) -> Result { + let id = *>::id(&op); + operations::get::request_get(op_manager, op, None).await?; + Ok(id) + } +} #[allow(unused)] struct SubscribeContract { @@ -172,9 +252,90 @@ pub struct Executor { mode: OperationMode, runtime: R, state_store: StateStore, - update_notifications: HashMap)>>, + update_notifications: HashMap)>>, subscriber_summaries: HashMap>>>, delegate_attested_ids: HashMap>, + #[cfg(any( + not(feature = "local-mode"), + feature = "network-mode", + all(not(feature = "local-mode"), not(feature = "network-mode")) + ))] + event_loop_channel: Option>, +} + +#[cfg(any( + not(feature = "local-mode"), + feature = "network-mode", + all(not(feature = "local-mode"), not(feature = "network-mode")) +))] +impl Executor { + pub(crate) fn event_loop_channel( + &mut self, + channel: ExecutorToEventLoopChannel, + ) { + self.event_loop_channel = Some(channel); + } + + async fn subscribe(&mut self, key: ContractKey) -> Result<(), ExecutorError> { + #[cfg(any( + all(not(feature = "local-mode"), not(feature = "network-mode")), + all(feature = "local-mode", feature = "network-mode") + ))] + { + if self.mode == OperationMode::Local { + return Ok(()); + } + } + let request = SubscribeContract { key }; + let op: operations::subscribe::SubscribeOp = self + .op_request(request) + .await + .map_err(ExecutorError::other)?; + let _sub: operations::subscribe::SubscribeResult = + op.try_into().map_err(ExecutorError::other)?; + Ok(()) + } + + #[inline] + async fn local_state_or_from_network( + &mut self, + id: &ContractInstanceId, + ) -> Result, ExecutorError> { + if let Ok(contract) = self.state_store.get(&(*id).into()).await { + return Ok(Either::Left(contract)); + }; + let request: GetContract = GetContract { + key: (*id).into(), + fetch_contract: true, + }; + let op: operations::get::GetOp = self + .op_request(request) + .await + .map_err(ExecutorError::other)?; + let get_result: operations::get::GetResult = op.try_into().map_err(ExecutorError::other)?; + Ok(Either::Right(get_result)) + } + + // FIXME: must add suspension and resuming when doing this, + // otherwise it may be possible to end up in a deadlock waiting for a tree of contract + // dependencies to be resolved + async fn op_request(&mut self, request: M) -> Result + where + Op: Operation + Send + 'static, + M: ComposeNetworkMessage, + { + debug_assert!(self.event_loop_channel.is_some()); + let channel = match self.event_loop_channel.as_mut() { + Some(ch) => ch, + None => { + // Safety: this should be always set if network mode is ambiguous + // or using network mode unequivocally + unsafe { std::hint::unreachable_unchecked() } + } + }; + channel.send_to_event_loop(request).await?; + todo!() + } } impl Executor { @@ -241,6 +402,12 @@ impl Executor { update_notifications: HashMap::default(), subscriber_summaries: HashMap::default(), delegate_attested_ids: HashMap::default(), + #[cfg(any( + not(feature = "local-mode"), + feature = "network-mode", + all(not(feature = "local-mode"), not(feature = "network-mode")) + ))] + event_loop_channel: None, }) } @@ -309,7 +476,7 @@ impl Executor { &mut self, id: ClientId, req: ClientRequest<'a>, - updates: Option>>, + updates: Option>>, ) -> Response { match req { ClientRequest::ContractOp(op) => self.contract_requests(op, id, updates).await, @@ -329,7 +496,7 @@ impl Executor { &mut self, req: ContractRequest<'_>, cli_id: ClientId, - updates: Option>>, + updates: Option>>, ) -> Response { match req { ContractRequest::Put { @@ -594,6 +761,7 @@ impl Executor { let state = match self.local_state_or_from_network(&id).await? { Either::Left(state) => state, Either::Right(GetResult { state, contract }) => { + let contract = contract.unwrap(); // fixme: deal with unwrap self.verify_and_store_contract( state.clone(), contract, @@ -680,8 +848,10 @@ impl Executor { key: key.clone(), new_state, }; - let op: operations::update::UpdateOp = - op_request(request).await.map_err(ExecutorError::other)?; + let op: operations::update::UpdateOp = self + .op_request(request) + .await + .map_err(ExecutorError::other)?; let _update: operations::update::UpdateResult = op.try_into().map_err(ExecutorError::other)?; } @@ -789,6 +959,7 @@ impl Executor { .await? { Either::Right(GetResult { state, contract }) => { + let contract = contract.unwrap(); // fixme: deal with unwrap self.verify_and_store_contract( state, contract.clone(), @@ -803,29 +974,6 @@ impl Executor { } } - #[cfg(any( - not(feature = "local-mode"), - feature = "network-mode", - all(not(feature = "local-mode"), not(feature = "network-mode")) - ))] - async fn subscribe(&self, key: ContractKey) -> Result<(), ExecutorError> { - #[cfg(any( - all(not(feature = "local-mode"), not(feature = "network-mode")), - all(feature = "local-mode", feature = "network-mode") - ))] - { - if self.mode == OperationMode::Local { - return Ok(()); - } - } - let request = SubscribeContract { key }; - let op: operations::subscribe::SubscribeOp = - op_request(request).await.map_err(ExecutorError::other)?; - let _sub: operations::subscribe::SubscribeResult = - op.try_into().map_err(ExecutorError::other)?; - Ok(()) - } - async fn get_local_contract( &self, id: &ContractInstanceId, @@ -908,10 +1056,11 @@ impl Executor { *related = Some(state.into()); } Either::Right(result) => { + let contract = result.contract.unwrap(); // fixme: deal with unwrap trying_key = (*id).into(); - trying_params = result.contract.params(); + trying_params = contract.params(); trying_state = result.state; - trying_contract = Some(result.contract); + trying_contract = Some(contract); continue; } } @@ -1004,28 +1153,6 @@ impl Executor { Ok(()) } - #[cfg(any( - not(feature = "local-mode"), - feature = "network-mode", - all(not(feature = "local-mode"), not(feature = "network-mode")), - ))] - #[inline] - async fn local_state_or_from_network( - &self, - id: &ContractInstanceId, - ) -> Result, ExecutorError> { - if let Ok(contract) = self.state_store.get(&(*id).into()).await { - return Ok(Either::Left(contract)); - }; - let request: GetContract = GetContract { - key: (*id).into(), - fetch_contract: true, - }; - let op: operations::get::GetOp = op_request(request).await.map_err(ExecutorError::other)?; - let get_result: operations::get::GetResult = op.try_into().map_err(ExecutorError::other)?; - Ok(Either::Right(get_result)) - } - async fn get_contract_locally( &self, key: &ContractKey, @@ -1055,7 +1182,7 @@ impl Executor { &mut self, _id: ClientId, _req: ClientRequest<'a>, - _updates: Option>>, + _updates: Option>>, ) -> Response { todo!() } @@ -1077,7 +1204,7 @@ impl ContractExecutor for Executor { Err(err) => Err(err), Ok(_) => { // Safety: check `perform_contract_get` to indeed check this should never happen - unsafe { unreachable_unchecked() } + unsafe { std::hint::unreachable_unchecked() } } } } diff --git a/crates/core/src/contract/handler.rs b/crates/core/src/contract/handler.rs index b56d45f62..311916723 100644 --- a/crates/core/src/contract/handler.rs +++ b/crates/core/src/contract/handler.rs @@ -1,6 +1,7 @@ #![allow(unused)] // FIXME: remove this -use std::collections::VecDeque; +use std::collections::{BTreeMap, VecDeque}; +use std::hash::Hash; use std::marker::PhantomData; use std::sync::atomic::{AtomicU64, Ordering::SeqCst}; use std::time::{Duration, Instant}; @@ -9,12 +10,16 @@ use freenet_stdlib::client_api::{ClientError, ClientRequest, HostResponse}; use freenet_stdlib::prelude::*; use futures::{future::BoxFuture, FutureExt}; use serde::{Deserialize, Serialize}; -use tokio::sync::mpsc::{self, UnboundedSender}; +use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; +use super::executor::{ExecutorHalve, ExecutorToEventLoopChannel}; use super::{ executor::{ContractExecutor, Executor}, ContractError, }; +use crate::client_events::HostResult; +use crate::message::Transaction; +use crate::node::OpManager; use crate::{ client_events::ClientId, node::NodeConfig, @@ -24,18 +29,53 @@ use crate::{ pub const MAX_MEM_CACHE: i64 = 10_000_000; +pub(crate) struct ClientResponses(UnboundedReceiver<(ClientId, HostResult)>); + +impl ClientResponses { + pub fn channel() -> (Self, ClientResponsesSender) { + let (tx, rx) = mpsc::unbounded_channel(); + (Self(rx), ClientResponsesSender(tx)) + } +} + +impl std::ops::Deref for ClientResponses { + type Target = UnboundedReceiver<(ClientId, HostResult)>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for ClientResponses { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[derive(Clone)] +pub(crate) struct ClientResponsesSender(UnboundedSender<(ClientId, HostResult)>); + +impl std::ops::Deref for ClientResponsesSender { + type Target = UnboundedSender<(ClientId, HostResult)>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + pub(crate) trait ContractHandler { type Builder; type ContractExecutor: ContractExecutor; fn build( - channel: ContractHandlerChannel, + contract_handler_channel: ContractHandlerToEventLoopChannel, + executor_request_sender: ExecutorToEventLoopChannel, builder: Self::Builder, ) -> BoxFuture<'static, Result> where Self: Sized + 'static; - fn channel(&mut self) -> &mut ContractHandlerChannel; + fn channel(&mut self) -> &mut ContractHandlerToEventLoopChannel; /// # Arguments /// - updates: channel to send back updates from contracts to whoever is subscribed to the contract. @@ -51,7 +91,7 @@ pub(crate) trait ContractHandler { pub(crate) struct NetworkContractHandler { executor: Executor, - channel: ContractHandlerChannel, + channel: ContractHandlerToEventLoopChannel, } impl ContractHandler for NetworkContractHandler { @@ -59,20 +99,22 @@ impl ContractHandler for NetworkContractHandler { type ContractExecutor = Executor; fn build( - channel: ContractHandlerChannel, + channel: ContractHandlerToEventLoopChannel, + executor_request_sender: ExecutorToEventLoopChannel, config: Self::Builder, ) -> BoxFuture<'static, Result> where Self: Sized + 'static, { async { - let executor = Executor::from_config(config).await?; + let mut executor = Executor::from_config(config).await?; + executor.event_loop_channel(executor_request_sender); Ok(Self { executor, channel }) } .boxed() } - fn channel(&mut self) -> &mut ContractHandlerChannel { + fn channel(&mut self) -> &mut ContractHandlerToEventLoopChannel { &mut self.channel } @@ -103,7 +145,8 @@ impl ContractHandler for NetworkContractHandler { type ContractExecutor = Executor; fn build( - channel: ContractHandlerChannel, + channel: ContractHandlerToEventLoopChannel, + _executor_request_sender: ExecutorToEventLoopChannel, _builder: Self::Builder, ) -> BoxFuture<'static, Result> where @@ -116,7 +159,7 @@ impl ContractHandler for NetworkContractHandler { .boxed() } - fn channel(&mut self) -> &mut ContractHandlerChannel { + fn channel(&mut self) -> &mut ContractHandlerToEventLoopChannel { &mut self.channel } @@ -141,46 +184,66 @@ impl ContractHandler for NetworkContractHandler { } } -pub struct EventId(u64); +#[derive(Eq)] +pub(crate) struct EventId { + id: u64, + client_id: Option, +} + +impl EventId { + pub fn client_id(&self) -> Option { + self.client_id + } +} + +impl PartialEq for EventId { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Hash for EventId { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} /// A bidirectional channel which keeps track of the initiator half /// and sends the corresponding response to the listener of the operation. -pub(crate) struct ContractHandlerChannel { +pub(crate) struct ContractHandlerToEventLoopChannel { rx: mpsc::UnboundedReceiver, tx: mpsc::UnboundedSender, - //TODO: change queue to btree once pop_first is stabilized - // (https://github.com/rust-lang/rust/issues/62924) - queue: VecDeque<(u64, ContractHandlerEvent)>, + queue: BTreeMap, _halve: PhantomData, } -pub(crate) struct CHListenerHalve; -pub(crate) struct CHSenderHalve; +pub(crate) struct ContractHandlerHalve; +pub(crate) struct NetEventListener; mod sealed { - use super::{CHListenerHalve, CHSenderHalve}; + use super::{ContractHandlerHalve, NetEventListener}; pub(crate) trait ChannelHalve {} - impl ChannelHalve for CHListenerHalve {} - impl ChannelHalve for CHSenderHalve {} + impl ChannelHalve for ContractHandlerHalve {} + impl ChannelHalve for NetEventListener {} } pub(crate) fn contract_handler_channel() -> ( - ContractHandlerChannel, - ContractHandlerChannel, + ContractHandlerToEventLoopChannel, + ContractHandlerToEventLoopChannel, ) { let (notification_tx, notification_channel) = mpsc::unbounded_channel(); let (ch_tx, ch_listener) = mpsc::unbounded_channel(); ( - ContractHandlerChannel { + ContractHandlerToEventLoopChannel { rx: notification_channel, tx: ch_tx, - queue: VecDeque::new(), + queue: BTreeMap::new(), _halve: PhantomData, }, - ContractHandlerChannel { + ContractHandlerToEventLoopChannel { rx: ch_listener, tx: notification_tx, - queue: VecDeque::new(), + queue: BTreeMap::new(), _halve: PhantomData, }, ) @@ -195,18 +258,19 @@ static EV_ID: AtomicU64 = AtomicU64::new(0); // kind of event and can be optimized on a case basis const CH_EV_RESPONSE_TIME_OUT: Duration = Duration::from_secs(300); -impl ContractHandlerChannel { +impl ContractHandlerToEventLoopChannel { /// Send an event to the contract handler and receive a response event if successful. pub async fn send_to_handler( &mut self, ev: ContractHandlerEvent, + client_id: Option, ) -> Result { let id = EV_ID.fetch_add(1, SeqCst); self.tx - .send(InternalCHEvent { ev, id }) + .send(InternalCHEvent { ev, id, client_id }) .map_err(|err| ContractError::ChannelDropped(Box::new(err.0.ev)))?; - if let Ok(pos) = self.queue.binary_search_by_key(&id, |(k, _v)| *k) { - Ok(self.queue.remove(pos).unwrap().1) + if let Some(handler) = self.queue.remove(&id) { + Ok(handler) } else { let started_op = Instant::now(); loop { @@ -217,34 +281,46 @@ impl ContractHandlerChannel { if msg.id == id { return Ok(msg.ev); } else { - self.queue.push_front((id, msg.ev)); // should never be duplicates + self.queue.insert(id, msg.ev); // should never be duplicates } } tokio::time::sleep(Duration::from_nanos(100)).await; } } } + + // todo: use + pub async fn recv_from_handler(&mut self) -> (EventId, ContractHandlerEvent) { + todo!() + } } -impl ContractHandlerChannel { - pub async fn send_to_listener( +impl ContractHandlerToEventLoopChannel { + pub async fn send_to_event_loop( &self, id: EventId, ev: ContractHandlerEvent, ) -> Result<(), ContractError> { self.tx - .send(InternalCHEvent { ev, id: id.0 }) + .send(InternalCHEvent { + ev, + id: id.id, + client_id: id.client_id, + }) .map_err(|err| ContractError::ChannelDropped(Box::new(err.0.ev))) } - pub async fn recv_from_listener( + pub async fn recv_from_event_loop( &mut self, ) -> Result<(EventId, ContractHandlerEvent), ContractError> { - if let Some((id, ev)) = self.queue.pop_front() { - return Ok((EventId(id), ev)); - } if let Some(msg) = self.rx.recv().await { - return Ok((EventId(msg.id), msg.ev)); + return Ok(( + EventId { + id: msg.id, + client_id: msg.client_id, + }, + msg.ev, + )); } Err(ContractError::NoEvHandlerResponse) } @@ -259,6 +335,7 @@ pub(crate) struct StoreResponse { struct InternalCHEvent { ev: ContractHandlerEvent, id: u64, + client_id: Option, } #[derive(Debug)] @@ -288,6 +365,12 @@ pub(crate) enum ContractHandlerEvent { CacheResult(Result<(), ContractError>), } +impl ContractHandlerEvent { + pub async fn into_network_op(self, op_manager: &OpManager) -> Transaction { + todo!() + } +} + #[cfg(test)] pub mod test { use std::sync::Arc; @@ -313,12 +396,12 @@ pub mod test { Parameters::from(vec![]), ))); send_halve - .send_to_handler(ContractHandlerEvent::Cache(contract)) + .send_to_handler(ContractHandlerEvent::Cache(contract), None) .await }); let (id, ev) = - tokio::time::timeout(Duration::from_millis(100), rcv_halve.recv_from_listener()) + tokio::time::timeout(Duration::from_millis(100), rcv_halve.recv_from_event_loop()) .await??; if let ContractHandlerEvent::Cache(contract) = ev { @@ -329,7 +412,7 @@ pub mod test { )); tokio::time::timeout( Duration::from_millis(100), - rcv_halve.send_to_listener(id, ContractHandlerEvent::Cache(contract)), + rcv_halve.send_to_event_loop(id, ContractHandlerEvent::Cache(contract)), ) .await??; } else { diff --git a/crates/core/src/contract/in_memory.rs b/crates/core/src/contract/in_memory.rs index 9e6b6e09e..58fd9a5db 100644 --- a/crates/core/src/contract/in_memory.rs +++ b/crates/core/src/contract/in_memory.rs @@ -10,7 +10,8 @@ use futures::{future::BoxFuture, FutureExt}; use tokio::sync::mpsc::UnboundedSender; use super::{ - handler::{CHListenerHalve, ContractHandler, ContractHandlerChannel}, + executor::{ExecutorHalve, ExecutorToEventLoopChannel}, + handler::{ContractHandler, ContractHandlerHalve, ContractHandlerToEventLoopChannel}, storages::in_memory::MemKVStore, Executor, }; @@ -24,7 +25,7 @@ pub(crate) struct MemoryContractHandler where KVStore: StateStorage, { - channel: ContractHandlerChannel, + channel: ContractHandlerToEventLoopChannel, _kv_store: StateStore, _runtime: MockRuntime, } @@ -36,7 +37,10 @@ where { const MAX_MEM_CACHE: i64 = 10_000_000; - pub fn new(channel: ContractHandlerChannel, kv_store: KVStore) -> Self { + pub fn new( + channel: ContractHandlerToEventLoopChannel, + kv_store: KVStore, + ) -> Self { MemoryContractHandler { channel, _kv_store: StateStore::new(kv_store, 10_000_000).unwrap(), @@ -56,7 +60,8 @@ impl ContractHandler for MemoryContractHandler { type ContractExecutor = Executor; fn build( - channel: ContractHandlerChannel, + channel: ContractHandlerToEventLoopChannel, + _executor_request_sender: ExecutorToEventLoopChannel, _config: Self::Builder, ) -> BoxFuture<'static, Result> where @@ -66,7 +71,7 @@ impl ContractHandler for MemoryContractHandler { async move { Ok(MemoryContractHandler::new(channel, store)) }.boxed() } - fn channel(&mut self) -> &mut ContractHandlerChannel { + fn channel(&mut self) -> &mut ContractHandlerToEventLoopChannel { &mut self.channel } diff --git a/crates/core/src/contract/storages/rocks_db.rs b/crates/core/src/contract/storages/rocks_db.rs index 6a5d458c9..9d58c85d3 100644 --- a/crates/core/src/contract/storages/rocks_db.rs +++ b/crates/core/src/contract/storages/rocks_db.rs @@ -102,7 +102,8 @@ mod test { use crate::{ client_events::ClientId, contract::{ - contract_handler_channel, ContractHandler, MockRuntime, NetworkContractHandler, + contract_handler_channel, executor::executor_channel_test, ContractHandler, + MockRuntime, NetworkContractHandler, }, DynError, }; @@ -110,7 +111,8 @@ mod test { // Prepare and get handler for rocksdb async fn get_handler() -> Result, DynError> { let (_, ch_handler) = contract_handler_channel(); - let handler = NetworkContractHandler::build(ch_handler, ()).await?; + let (_, executor_sender) = executor_channel_test(); + let handler = NetworkContractHandler::build(ch_handler, executor_sender, ()).await?; Ok(handler) } diff --git a/crates/core/src/contract/storages/sqlite.rs b/crates/core/src/contract/storages/sqlite.rs index a674a5566..ec8bfbce5 100644 --- a/crates/core/src/contract/storages/sqlite.rs +++ b/crates/core/src/contract/storages/sqlite.rs @@ -142,15 +142,20 @@ mod test { use freenet_stdlib::client_api::ContractRequest; use freenet_stdlib::prelude::*; - use crate::contract::{ - contract_handler_channel, ContractHandler, MockRuntime, NetworkContractHandler, + use crate::{ + client_events::ClientId, + contract::{ + contract_handler_channel, executor::executor_channel_test, ContractHandler, + MockRuntime, NetworkContractHandler, + }, + DynError, }; - use crate::{client_events::ClientId, DynError}; // Prepare and get handler for an in-memory sqlite db async fn get_handler() -> Result, DynError> { let (_, ch_handler) = contract_handler_channel(); - let handler = NetworkContractHandler::build(ch_handler, ()).await?; + let (_, executor_sender) = executor_channel_test(); + let handler = NetworkContractHandler::build(ch_handler, executor_sender, ()).await?; Ok(handler) } diff --git a/crates/core/src/message.rs b/crates/core/src/message.rs index a3194ff4d..35d0acd6a 100644 --- a/crates/core/src/message.rs +++ b/crates/core/src/message.rs @@ -13,7 +13,10 @@ use uuid::{ use crate::{ node::{ConnectionError, PeerKey}, - operations::{get::GetMsg, join_ring::JoinRingMsg, put::PutMsg, subscribe::SubscribeMsg}, + operations::{ + get::GetMsg, join_ring::JoinRingMsg, put::PutMsg, subscribe::SubscribeMsg, + update::UpdateMsg, + }, ring::{Location, PeerKeyLocation}, }; pub(crate) use sealed_msg_type::{TransactionType, TransactionTypeId}; @@ -89,6 +92,8 @@ where } mod sealed_msg_type { + use crate::operations::update::UpdateMsg; + use super::*; pub(crate) trait SealedTxType { @@ -111,6 +116,7 @@ mod sealed_msg_type { Put, Get, Subscribe, + Update, Canceled, } @@ -136,7 +142,8 @@ mod sealed_msg_type { JoinRing -> JoinRingMsg, Put -> PutMsg, Get -> GetMsg, - Subscribe -> SubscribeMsg + Subscribe -> SubscribeMsg, + Update -> UpdateMsg }); } @@ -146,11 +153,12 @@ pub(crate) enum Message { Put(PutMsg), Get(GetMsg), Subscribe(SubscribeMsg), + Update(UpdateMsg), /// Failed a transaction, informing of cancellation. Canceled(Transaction), } -pub(crate) trait InnerMessage { +pub(crate) trait InnerMessage: Into { fn id(&self) -> &Transaction; } @@ -194,6 +202,7 @@ impl Message { Put(op) => op.id(), Get(op) => op.id(), Subscribe(op) => op.id(), + Update(_op) => todo!(), Canceled(tx) => tx, } } @@ -205,6 +214,7 @@ impl Message { Put(op) => op.target(), Get(op) => op.target(), Subscribe(op) => op.target(), + Update(_op) => todo!(), Canceled(_) => None, } } @@ -217,6 +227,7 @@ impl Message { Put(op) => op.terminal(), Get(op) => op.terminal(), Subscribe(op) => op.terminal(), + Update(_op) => todo!(), Canceled(_) => true, } } @@ -231,6 +242,7 @@ impl Display for Message { Put(msg) => msg.fmt(f)?, Get(msg) => msg.fmt(f)?, Subscribe(msg) => msg.fmt(f)?, + Update(_op) => todo!(), Canceled(msg) => msg.fmt(f)?, }; write!(f, "}}") diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index 070d1fb9d..489f5db70 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -21,15 +21,18 @@ use libp2p::{identity, multiaddr::Protocol, Multiaddr, PeerId}; #[cfg(test)] use self::in_memory_impl::NodeInMemory; use self::{ - event_listener::{EventListener, EventLog}, + event_log::{EventLog, EventLogListener}, p2p_impl::NodeP2P, }; use crate::{ - client_events::{BoxedClient, ClientEventsProxy, OpenRequest}, + client_events::{BoxedClient, ClientEventsProxy, ClientId, OpenRequest}, config::Config, config::GlobalExecutor, - contract::{ContractError, NetworkContractHandler, OperationMode}, - message::{InnerMessage, Message, NodeEvent, Transaction, TransactionType, TxType}, + contract::{ + ClientResponses, ClientResponsesSender, ContractError, ExecutorToEventLoopChannel, + NetworkContractHandler, NetworkEventListenerHalve, OperationMode, + }, + message::{InnerMessage, Message, Transaction, TransactionType, TxType}, operations::{ get, join_ring::{self, JoinRingMsg, JoinRingOp}, @@ -44,7 +47,7 @@ pub(crate) use conn_manager::{p2p_protoc::P2pBridge, ConnectionBridge, Connectio pub(crate) use op_state::OpManager; mod conn_manager; -mod event_listener; +mod event_log; #[cfg(test)] mod in_memory_impl; mod op_state; @@ -299,110 +302,132 @@ where } /// Process client events. -async fn client_event_handling(op_storage: Arc, mut client_events: ClientEv) -where +async fn client_event_handling( + op_storage: Arc, + mut client_events: ClientEv, + mut client_responses: ClientResponses, +) where ClientEv: ClientEventsProxy + Send + Sync + 'static, { loop { - // fixme: send back responses to client - let OpenRequest { - id: _id, request, .. - } = client_events.recv().await.unwrap(); // fixme: deal with this unwrap - if let ClientRequest::Disconnect { .. } = *request { - if let Err(err) = op_storage.notify_internal_op(NodeEvent::ShutdownNode).await { - tracing::error!("{}", err); + tokio::select! { + client_request = client_events.recv() => { + let req = match client_request { + Ok(req) => req, + Err(err) => { + tracing::debug!(error = %err, "client error"); + continue; + } + }; + if let ClientRequest::Disconnect { .. } = &*req.request { + // todo: notify executor of disconnect + continue; + } + process_open_request(req, op_storage.clone()).await; + } + res = client_responses.recv() => { + if let Some((cli_id, res)) = res { + if let Err(err) = client_events.send(cli_id, res).await { + tracing::error!("channel closed: {err}"); + break; + } + } } - break; } + } +} - let op_storage_cp = op_storage.clone(); - GlobalExecutor::spawn(async move { - match *request { - ClientRequest::ContractOp(ops) => match ops { - ContractRequest::Put { - state, +#[inline] +async fn process_open_request(request: OpenRequest<'static>, op_storage: Arc) { + // this will indirectly start actions on the local contract executor + let op_storage_cp = op_storage.clone(); + let fut = async move { + let client_id = request.client_id; + match *request.request { + ClientRequest::ContractOp(ops) => match ops { + ContractRequest::Put { + state, + contract, + related_contracts, + } => { + // Initialize a put op. + tracing::debug!( + "Received put from user event @ {}", + &op_storage_cp.ring.peer_key + ); + let op = put::start_op( contract, - related_contracts, - } => { - // Initialize a put op. - tracing::debug!( - "Received put from user event @ {}", - &op_storage_cp.ring.peer_key - ); - let op = put::start_op( - contract, - state, - op_storage_cp.ring.max_hops_to_live, - &op_storage_cp.ring.peer_key, - ); - if let Err(err) = put::request_put(&op_storage_cp, op).await { - tracing::error!("{}", err); - } - todo!("use `related_contracts`: {related_contracts:?}") - } - ContractRequest::Update { - key: _key, - data: _delta, - } => { - todo!() + state, + op_storage_cp.ring.max_hops_to_live, + &op_storage_cp.ring.peer_key, + ); + if let Err(err) = put::request_put(&op_storage_cp, op, Some(client_id)).await { + tracing::error!("{}", err); } - ContractRequest::Get { - key, - fetch_contract: contract, - } => { - // Initialize a get op. - tracing::debug!( - "Received get from user event @ {}", - &op_storage_cp.ring.peer_key - ); - let op = get::start_op(key, contract, &op_storage_cp.ring.peer_key); - if let Err(err) = get::request_get(&op_storage_cp, op).await { - tracing::error!("{}", err); - } + todo!("use `related_contracts`: {related_contracts:?}") + } + ContractRequest::Update { + key: _key, + data: _delta, + } => { + todo!() + } + ContractRequest::Get { + key, + fetch_contract: contract, + } => { + // Initialize a get op. + tracing::debug!( + "Received get from user event @ {}", + &op_storage_cp.ring.peer_key + ); + let op = get::start_op(key, contract, &op_storage_cp.ring.peer_key); + if let Err(err) = get::request_get(&op_storage_cp, op, Some(client_id)).await { + tracing::error!("{}", err); } - ContractRequest::Subscribe { key, .. } => { - // Initialize a subscribe op. - loop { - // FIXME: this will block the event loop until the subscribe op succeeds - // instead the op should be deferred for later execution - let op = subscribe::start_op(key.clone(), &op_storage_cp.ring.peer_key); - match subscribe::request_subscribe(&op_storage_cp, op).await { - Err(OpError::ContractError(ContractError::ContractNotFound( - key, - ))) => { - tracing::warn!("Trying to subscribe to a contract not present: {}, requesting it first", key); - let get_op = get::start_op( - key.clone(), - true, - &op_storage_cp.ring.peer_key, - ); - if let Err(err) = get::request_get(&op_storage_cp, get_op).await - { - tracing::error!("Failed getting the contract `{}` while previously trying to subscribe; bailing: {}", key, err); - tokio::time::sleep(Duration::from_secs(5)).await; - } - } - Err(err) => { - tracing::error!("{}", err); - break; + } + ContractRequest::Subscribe { key, .. } => { + // Initialize a subscribe op. + loop { + // FIXME: this will block the event loop until the subscribe op succeeds + // instead the op should be deferred for later execution + let op = subscribe::start_op(key.clone(), &op_storage_cp.ring.peer_key); + match subscribe::request_subscribe(&op_storage_cp, op, Some(client_id)) + .await + { + Err(OpError::ContractError(ContractError::ContractNotFound(key))) => { + tracing::warn!("Trying to subscribe to a contract not present: {}, requesting it first", key); + let get_op = + get::start_op(key.clone(), true, &op_storage_cp.ring.peer_key); + if let Err(err) = + get::request_get(&op_storage_cp, get_op, Some(client_id)).await + { + tracing::error!("Failed getting the contract `{}` while previously trying to subscribe; bailing: {}", key, err); + tokio::time::sleep(Duration::from_secs(5)).await; } - Ok(()) => break, } + Err(err) => { + tracing::error!("{}", err); + break; + } + Ok(()) => break, } - todo!() } - _ => { - tracing::error!("op not supported"); - } - }, - ClientRequest::DelegateOp(_op) => todo!("FIXME: delegate op"), - ClientRequest::Disconnect { .. } => unreachable!(), + todo!() + } _ => { tracing::error!("op not supported"); } + }, + ClientRequest::DelegateOp(_op) => todo!("FIXME: delegate op"), + ClientRequest::Disconnect { .. } => unreachable!(), + _ => { + tracing::error!("op not supported"); } - }); - } + } + }; + + GlobalExecutor::spawn(fut); } macro_rules! log_handling_msg { @@ -416,9 +441,24 @@ macro_rules! log_handling_msg { } #[inline(always)] -fn report_result(op_result: Result<(), OpError>) { - if let Err(err) = op_result { - tracing::debug!("Finished tx w/ error: {}", err) +async fn report_result( + op_result: Result, OpError>, + executor_callback: Option>, + client_req_handler_callback: Option<(ClientId, ClientResponsesSender)>, +) { + match op_result { + Ok(Some(res)) => { + if let Some((client_id, cb)) = client_req_handler_callback { + let _ = cb.send((client_id, res.to_host_result(client_id))); + } + if let Some(mut cb) = executor_callback { + cb.response(res).await; + } + } + Ok(None) => {} + Err(err) => { + tracing::debug!("Finished tx w/ error: {}", err) + } } } @@ -426,10 +466,14 @@ async fn process_message( msg: Result, op_storage: Arc, mut conn_manager: CB, - event_listener: Option>, + event_listener: Option>, + executor_callback: Option>, + client_req_handler_callback: Option, + client_id: Option, ) where CB: ConnectionBridge, { + let cli_req = client_id.zip(client_req_handler_callback); match msg { Ok(msg) => { if let Some(mut listener) = event_listener { @@ -442,23 +486,32 @@ async fn process_message( &op_storage, &mut conn_manager, op, + client_id, ) .await; - report_result(op_result); + report_result(op_result, executor_callback, cli_req).await; } Message::Put(op) => { log_handling_msg!("put", *op.id(), op_storage); - let op_result = - handle_op_request::(&op_storage, &mut conn_manager, op) - .await; - report_result(op_result); + let op_result = handle_op_request::( + &op_storage, + &mut conn_manager, + op, + client_id, + ) + .await; + report_result(op_result, executor_callback, cli_req).await; } Message::Get(op) => { log_handling_msg!("get", op.id(), op_storage); - let op_result = - handle_op_request::(&op_storage, &mut conn_manager, op) - .await; - report_result(op_result); + let op_result = handle_op_request::( + &op_storage, + &mut conn_manager, + op, + client_id, + ) + .await; + report_result(op_result, executor_callback, cli_req).await; } Message::Subscribe(op) => { log_handling_msg!("subscribe", op.id(), op_storage); @@ -466,15 +519,16 @@ async fn process_message( &op_storage, &mut conn_manager, op, + client_id, ) .await; - report_result(op_result); + report_result(op_result, executor_callback, cli_req).await; } _ => {} } } Err(err) => { - report_result(Err(err.into())); + report_result(Err(err.into()), executor_callback, cli_req).await; } } } diff --git a/crates/core/src/node/conn_manager.rs b/crates/core/src/node/conn_manager.rs index c589eadae..e1c6cf73d 100644 --- a/crates/core/src/node/conn_manager.rs +++ b/crates/core/src/node/conn_manager.rs @@ -1,10 +1,17 @@ //! Types and definitions to handle all socket communication for the peer nodes. +use std::ops::{Deref, DerefMut}; + +use either::Either; use libp2p::swarm::StreamUpgradeError; use serde::{Deserialize, Serialize}; +use tokio::sync::mpsc::{self, Receiver, Sender}; use super::PeerKey; -use crate::message::Message; +use crate::{ + client_events::ClientId, + message::{Message, NodeEvent}, +}; #[cfg(test)] pub(crate) mod in_memory; @@ -76,3 +83,41 @@ impl Clone for ConnectionError { } } } + +pub(super) struct EventLoopNotifications(Receiver), NodeEvent>>); + +impl EventLoopNotifications { + pub fn channel() -> (EventLoopNotifications, EventLoopNotificationsSender) { + let (notification_tx, notification_rx) = mpsc::channel(100); + ( + EventLoopNotifications(notification_rx), + EventLoopNotificationsSender(notification_tx), + ) + } +} + +impl Deref for EventLoopNotifications { + type Target = Receiver), NodeEvent>>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for EventLoopNotifications { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +pub(super) struct EventLoopNotificationsSender( + Sender), NodeEvent>>, +); + +impl Deref for EventLoopNotificationsSender { + type Target = Sender), NodeEvent>>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/crates/core/src/node/conn_manager/p2p_protoc.rs b/crates/core/src/node/conn_manager/p2p_protoc.rs index f17b4c65b..1f2573591 100644 --- a/crates/core/src/node/conn_manager/p2p_protoc.rs +++ b/crates/core/src/node/conn_manager/p2p_protoc.rs @@ -32,13 +32,15 @@ use libp2p::{ }, InboundUpgrade, Multiaddr, OutboundUpgrade, PeerId, Swarm, }; -use tokio::sync::mpsc::{channel, Receiver, Sender}; +use tokio::sync::mpsc::{self, Receiver, Sender}; use unsigned_varint::codec::UviBytes; -use super::{ConnectionBridge, ConnectionError}; +use super::{ConnectionBridge, ConnectionError, EventLoopNotifications}; use crate::{ + client_events::ClientId, config::{self, GlobalExecutor}, - message::{Message, NodeEvent, TransactionType}, + contract::{ClientResponsesSender, ExecutorToEventLoopChannel, NetworkEventListenerHalve}, + message::{Message, NodeEvent, Transaction, TransactionType}, node::{ handle_cancelled_op, join_ring_request, process_message, InitPeerNode, NodeBuilder, OpManager, PeerKey, @@ -199,7 +201,7 @@ impl P2pConnManager { swarm.add_external_address(remote_addr); } - let (tx_bridge_cmd, rx_bridge_cmd) = channel(100); + let (tx_bridge_cmd, rx_bridge_cmd) = mpsc::channel(100); let bridge = P2pBridge::new(tx_bridge_cmd); let gateways = config.get_gateways()?; @@ -222,15 +224,20 @@ impl P2pConnManager { pub async fn run_event_listener( mut self, op_manager: Arc, - mut notification_channel: Receiver>, + mut notification_channel: EventLoopNotifications, + mut executor_channel: ExecutorToEventLoopChannel, + cli_response_sender: ClientResponsesSender, ) -> Result<(), anyhow::Error> { use ConnMngrActions::*; + let mut pending_from_executor = HashSet::new(); + let mut tx_to_client: HashMap = HashMap::new(); + loop { - let net_msg = self.swarm.select_next_some().map(|event| match event { + let network_msg = self.swarm.select_next_some().map(|event| match event { SwarmEvent::Behaviour(NetEvent::Freenet(msg)) => { tracing::debug!("Message inbound: {:?}", msg); - Ok(Left(*msg)) + Ok(Left((*msg, None))) } SwarmEvent::ConnectionClosed { peer_id, .. } => { Ok(Right(ConnMngrActions::ConnectionClosed { @@ -299,9 +306,9 @@ impl P2pConnManager { } }); - let notification_msg = notification_channel.recv().map(|m| match m { + let notification_msg = notification_channel.0.recv().map(|m| match m { None => Ok(Right(ClosedChannel)), - Some(Left(msg)) => Ok(Left(msg)), + Some(Left((msg, cli_id))) => Ok(Left((msg, cli_id))), Some(Right(action)) => Ok(Right(NodeAction(action))), }); @@ -315,13 +322,24 @@ impl P2pConnManager { }); let msg: Result<_, ConnectionError> = tokio::select! { - msg = net_msg => { msg } + msg = network_msg => { msg } msg = notification_msg => { msg } msg = bridge_msg => { msg } + (event_id, contract_handler_event) = op_manager.recv_from_handler() => { + if let Some(client_id) = event_id.client_id() { + let transaction = contract_handler_event.into_network_op(&op_manager).await; + tx_to_client.insert(transaction, client_id); + } + continue; + } + id = executor_channel.transaction_from_executor() => { + pending_from_executor.insert(id); + continue; + } }; match msg { - Ok(Left(msg)) => { + Ok(Left((msg, client_id))) => { let cb = self.bridge.clone(); match msg { Message::Canceled(tx) => { @@ -356,11 +374,24 @@ impl P2pConnManager { continue; } msg => { + let executor_callback = pending_from_executor + .remove(msg.id()) + .then(|| executor_channel.clone()); + let pending_client_req = tx_to_client.get(msg.id()).copied(); + let client_req_handler_callback = if pending_client_req.is_some() { + debug_assert!(client_id.is_none()); + Some(cli_response_sender.clone()) + } else { + None + }; GlobalExecutor::spawn(process_message( Ok(msg), op_manager.clone(), cb, None, + executor_callback, + client_req_handler_callback, + client_id, )); } } @@ -415,7 +446,15 @@ impl P2pConnManager { } Err(err) => { let cb = self.bridge.clone(); - GlobalExecutor::spawn(process_message(Err(err), op_manager.clone(), cb, None)); + GlobalExecutor::spawn(process_message( + Err(err), + op_manager.clone(), + cb, + None, + None, + None, + None, + )); } Ok(Right(NoAction)) | Ok(Right(NodeAction(NodeEvent::ConfirmedInbound))) => {} } diff --git a/crates/core/src/node/event_listener.rs b/crates/core/src/node/event_log.rs similarity index 96% rename from crates/core/src/node/event_listener.rs rename to crates/core/src/node/event_log.rs index 944f1770f..7828c1139 100644 --- a/crates/core/src/node/event_listener.rs +++ b/crates/core/src/node/event_log.rs @@ -21,9 +21,9 @@ struct ListenerLogId(usize); /// /// This type then can emit it's own information to adjacent systems /// or is a no-op. -pub(crate) trait EventListener { +pub(crate) trait EventLogListener { fn event_received(&mut self, ev: EventLog); - fn trait_clone(&self) -> Box; + fn trait_clone(&self) -> Box; } #[allow(dead_code)] // fixme: remove this @@ -112,13 +112,13 @@ struct MessageLog { #[derive(Clone)] pub(super) struct EventRegister {} -impl EventListener for EventRegister { +impl EventLogListener for EventRegister { fn event_received(&mut self, _log: EventLog) { // let (_msg_log, _log_id) = create_log(log); // TODO: save log } - fn trait_clone(&self) -> Box { + fn trait_clone(&self) -> Box { Box::new(self.clone()) } } @@ -321,7 +321,7 @@ mod test_utils { } } - impl super::EventListener for TestEventListener { + impl super::EventLogListener for TestEventListener { fn event_received(&mut self, log: EventLog) { let tx = log.tx; let mut logs = self.logs.write(); @@ -331,7 +331,7 @@ mod test_utils { self.tx_log.entry(*tx).or_default().push(log_id); } - fn trait_clone(&self) -> Box { + fn trait_clone(&self) -> Box { Box::new(self.clone()) } } diff --git a/crates/core/src/node/in_memory_impl.rs b/crates/core/src/node/in_memory_impl.rs index aef809307..84b07142a 100644 --- a/crates/core/src/node/in_memory_impl.rs +++ b/crates/core/src/node/in_memory_impl.rs @@ -2,17 +2,22 @@ use std::{collections::HashMap, sync::Arc}; use either::Either; use freenet_stdlib::prelude::*; -use tokio::sync::mpsc::{self, Receiver}; use super::{ - client_event_handling, conn_manager::in_memory::MemoryConnManager, - event_listener::EventListener, handle_cancelled_op, join_ring_request, op_state::OpManager, + client_event_handling, + conn_manager::{in_memory::MemoryConnManager, EventLoopNotifications}, + event_log::EventLogListener, + handle_cancelled_op, join_ring_request, + op_state::OpManager, process_message, PeerKey, }; use crate::{ client_events::ClientEventsProxy, config::GlobalExecutor, - contract::{self, ContractError, ContractHandler, ContractHandlerEvent}, + contract::{ + self, executor_channel, ClientResponsesSender, ContractError, ContractHandler, + ContractHandlerEvent, ExecutorToEventLoopChannel, NetworkEventListenerHalve, + }, message::{Message, NodeEvent, TransactionType}, node::NodeBuilder, operations::OpError, @@ -24,17 +29,18 @@ pub(super) struct NodeInMemory { pub peer_key: PeerKey, pub op_storage: Arc, gateways: Vec, - notification_channel: Receiver>, + notification_channel: EventLoopNotifications, conn_manager: MemoryConnManager, - event_listener: Option>, + event_listener: Option>, is_gateway: bool, + _executor_listener: ExecutorToEventLoopChannel, } impl NodeInMemory { /// Buils an in-memory node. Does nothing upon construction, pub async fn build( builder: NodeBuilder<1>, - event_listener: Option>, + event_listener: Option>, ch_builder: CH::Builder, ) -> Result where @@ -46,10 +52,11 @@ impl NodeInMemory { let is_gateway = builder.local_ip.zip(builder.local_port).is_some(); let ring = Ring::new(&builder, &gateways)?; - let (notification_tx, notification_channel) = mpsc::channel(100); + let (notification_channel, notification_tx) = EventLoopNotifications::channel(); let (ops_ch_channel, ch_channel) = contract::contract_handler_channel(); let op_storage = Arc::new(OpManager::new(ring, notification_tx, ops_ch_channel)); - let contract_handler = CH::build(ch_channel, ch_builder) + let (_executor_listener, executor_sender) = executor_channel(op_storage.clone()); + let contract_handler = CH::build(ch_channel, executor_sender, ch_builder) .await .map_err(|e| anyhow::anyhow!(e))?; @@ -63,6 +70,7 @@ impl NodeInMemory { notification_channel, event_listener, is_gateway, + _executor_listener, }) } @@ -84,8 +92,13 @@ impl NodeInMemory { anyhow::bail!("requires at least one gateway"); } } - GlobalExecutor::spawn(client_event_handling(self.op_storage.clone(), user_events)); - self.run_event_listener().await + let (client_responses, cli_response_sender) = contract::ClientResponses::channel(); + GlobalExecutor::spawn(client_event_handling( + self.op_storage.clone(), + user_events, + client_responses, + )); + self.run_event_listener(cli_response_sender).await } pub async fn append_contracts<'a>( @@ -96,13 +109,16 @@ impl NodeInMemory { for (contract, state) in contracts { let key = contract.key(); self.op_storage - .notify_contract_handler(ContractHandlerEvent::Cache(contract.clone())) + .notify_contract_handler(ContractHandlerEvent::Cache(contract.clone()), None) .await?; self.op_storage - .notify_contract_handler(ContractHandlerEvent::PutQuery { - key: key.clone(), - state, - }) + .notify_contract_handler( + ContractHandlerEvent::PutQuery { + key: key.clone(), + state, + }, + None, + ) .await?; tracing::debug!( "Appended contract {} to peer {}", @@ -129,12 +145,15 @@ impl NodeInMemory { } /// Starts listening to incoming events. Will attempt to join the ring if any gateways have been provided. - async fn run_event_listener(&mut self) -> Result<(), anyhow::Error> { + async fn run_event_listener( + &mut self, + _client_responses: ClientResponsesSender, // fixme: use this + ) -> Result<(), anyhow::Error> { loop { let msg = tokio::select! { msg = self.conn_manager.recv() => { msg.map(Either::Left) } msg = self.notification_channel.recv() => if let Some(msg) = msg { - Ok(msg) + Ok(msg.map_left(|(msg, _cli_id)| msg)) } else { anyhow::bail!("notification channel shutdown, fatal error"); } @@ -198,6 +217,9 @@ impl NodeInMemory { op_storage, conn_manager, event_listener, + None, + None, + None, )); } } diff --git a/crates/core/src/node/op_state.rs b/crates/core/src/node/op_state.rs index 273ec2589..c3a5658f1 100644 --- a/crates/core/src/node/op_state.rs +++ b/crates/core/src/node/op_state.rs @@ -3,21 +3,21 @@ use std::{collections::BTreeMap, time::Instant}; use dashmap::DashMap; use either::Either; use parking_lot::RwLock; -use tokio::sync::{ - mpsc::{error::SendError, Sender}, - Mutex, -}; +use tokio::sync::{mpsc::error::SendError, Mutex}; use crate::{ - contract::{CHSenderHalve, ContractError, ContractHandlerChannel, ContractHandlerEvent}, - message::{Message, NodeEvent, Transaction, TransactionType}, + contract::{ + ContractError, ContractHandlerEvent, ContractHandlerToEventLoopChannel, NetEventListener, + }, + dev_tool::ClientId, + message::{Message, Transaction, TransactionType}, operations::{ get::GetOp, join_ring::JoinRingOp, put::PutOp, subscribe::SubscribeOp, OpEnum, OpError, }, ring::Ring, }; -use super::PeerKey; +use super::{conn_manager::EventLoopNotificationsSender, PeerKey}; /// Thread safe and friendly data structure to maintain state of the different operations /// and enable their execution. @@ -26,8 +26,9 @@ pub(crate) struct OpManager { put: DashMap, get: DashMap, subscribe: DashMap, - notification_channel: Sender>, - contract_handler: Mutex>, + to_event_listener: EventLoopNotificationsSender, + // todo: remove the need for a mutex here + ch_outbound: Mutex>, // FIXME: think of an optimal strategy to check for timeouts and clean up garbage _ops_ttl: RwLock>>, pub ring: Ring, @@ -42,10 +43,10 @@ macro_rules! check_id_op { } impl OpManager { - pub fn new( + pub(super) fn new( ring: Ring, - notification_channel: Sender>, - contract_handler: ContractHandlerChannel, + notification_channel: EventLoopNotificationsSender, + contract_handler: ContractHandlerToEventLoopChannel, ) -> Self { Self { join_ring: DashMap::default(), @@ -53,8 +54,8 @@ impl OpManager { get: DashMap::default(), subscribe: DashMap::default(), ring, - notification_channel, - contract_handler: Mutex::new(contract_handler), + to_event_listener: notification_channel, + ch_outbound: Mutex::new(contract_handler), _ops_ttl: RwLock::new(BTreeMap::new()), } } @@ -69,35 +70,41 @@ impl OpManager { &self, msg: Message, op: OpEnum, - ) -> Result<(), SendError> { + client_id: Option, + ) -> Result<(), SendError<(Message, Option)>> { // push back the state to the stack self.push(*msg.id(), op).expect("infallible"); - self.notification_channel - .send(Either::Left(msg)) + self.to_event_listener + .send(Either::Left((msg, client_id))) .await .map_err(|err| SendError(err.0.unwrap_left())) } - /// Send an internal message to this node event loop. - pub async fn notify_internal_op(&self, msg: NodeEvent) -> Result<(), SendError> { - self.notification_channel - .send(Either::Right(msg)) - .await - .map_err(|err| SendError(err.0.unwrap_right())) - } + // /// Send an internal message to this node event loop. + // pub async fn notify_internal_op(&self, msg: NodeEvent) -> Result<(), SendError> { + // self.to_event_listener + // .send(Either::Right(msg)) + // .await + // .map_err(|err| SendError(err.0.unwrap_right())) + // } /// Send an event to the contract handler and await a response event from it if successful. pub async fn notify_contract_handler( &self, msg: ContractHandlerEvent, + client_id: Option, ) -> Result { - self.contract_handler + self.ch_outbound .lock() .await - .send_to_handler(msg) + .send_to_handler(msg, client_id) .await } + pub async fn recv_from_handler(&self) -> (crate::contract::EventId, ContractHandlerEvent) { + todo!() + } + pub fn push(&self, id: Transaction, op: OpEnum) -> Result<(), OpError> { match op { OpEnum::JoinRing(tx) => { @@ -134,6 +141,7 @@ impl OpManager { .remove(id) .map(|(_k, v)| v) .map(OpEnum::Subscribe), + TransactionType::Update => todo!(), TransactionType::Canceled => unreachable!(), } } diff --git a/crates/core/src/node/p2p_impl.rs b/crates/core/src/node/p2p_impl.rs index 0f726c738..9c2b93a35 100644 --- a/crates/core/src/node/p2p_impl.rs +++ b/crates/core/src/node/p2p_impl.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use either::Either; use libp2p::{ core::{ muxing, @@ -11,16 +10,19 @@ use libp2p::{ identity::Keypair, noise, tcp, yamux, PeerId, Transport, }; -use tokio::sync::mpsc::{self, Receiver}; use super::{ - client_event_handling, conn_manager::p2p_protoc::P2pConnManager, join_ring_request, PeerKey, + client_event_handling, + conn_manager::{p2p_protoc::P2pConnManager, EventLoopNotifications}, + join_ring_request, PeerKey, }; use crate::{ client_events::combinator::ClientEventsCombinator, config::{self, GlobalExecutor}, - contract::{self, ContractHandler}, - message::{Message, NodeEvent}, + contract::{ + self, ClientResponsesSender, ContractHandler, ExecutorToEventLoopChannel, + NetworkEventListenerHalve, + }, node::NodeBuilder, ring::Ring, util::IterExt, @@ -31,10 +33,12 @@ use super::OpManager; pub(super) struct NodeP2P { pub(crate) peer_key: PeerKey, pub(crate) op_storage: Arc, - notification_channel: Receiver>, + notification_channel: EventLoopNotifications, pub(super) conn_manager: P2pConnManager, // event_listener: Option>, is_gateway: bool, + executor_listener: ExecutorToEventLoopChannel, + cli_response_sender: ClientResponsesSender, } impl NodeP2P { @@ -60,8 +64,14 @@ impl NodeP2P { } // start the p2p event loop + // todo: pass `cli_response_sender` self.conn_manager - .run_event_listener(self.op_storage.clone(), self.notification_channel) + .run_event_listener( + self.op_storage.clone(), + self.notification_channel, + self.executor_listener, + self.cli_response_sender, + ) .await } @@ -81,16 +91,22 @@ impl NodeP2P { }; let ring = Ring::new(&builder, &gateways)?; - let (notification_tx, notification_channel) = mpsc::channel(100); - let (ops_ch_channel, ch_channel) = contract::contract_handler_channel(); - let op_storage = Arc::new(OpManager::new(ring, notification_tx, ops_ch_channel)); - let contract_handler = CH::build(ch_channel, ch_builder) + let (notification_channel, notification_tx) = EventLoopNotifications::channel(); + let (ch_outbound, ch_inbound) = contract::contract_handler_channel(); + let (client_responses, cli_response_sender) = contract::ClientResponses::channel(); + let op_storage = Arc::new(OpManager::new(ring, notification_tx, ch_outbound)); + let (executor_listener, executor_sender) = contract::executor_channel(op_storage.clone()); + let contract_handler = CH::build(ch_inbound, executor_sender, ch_builder) .await .map_err(|e| anyhow::anyhow!(e))?; GlobalExecutor::spawn(contract::contract_handling(contract_handler)); let clients = ClientEventsCombinator::new(builder.clients); - GlobalExecutor::spawn(client_event_handling(op_storage.clone(), clients)); + GlobalExecutor::spawn(client_event_handling( + op_storage.clone(), + clients, + client_responses, + )); Ok(NodeP2P { peer_key, @@ -98,6 +114,8 @@ impl NodeP2P { notification_channel, op_storage, is_gateway: builder.location.is_some(), + executor_listener, + cli_response_sender, }) } diff --git a/crates/core/src/node/tests.rs b/crates/core/src/node/tests.rs index d36676cba..e4eba6473 100644 --- a/crates/core/src/node/tests.rs +++ b/crates/core/src/node/tests.rs @@ -17,7 +17,7 @@ use crate::{ client_events::test::MemoryEventsGen, config::GlobalExecutor, contract::MemoryContractHandler, - node::{event_listener::TestEventListener, InitPeerNode, NodeBuilder, NodeInMemory}, + node::{event_log::TestEventListener, InitPeerNode, NodeBuilder, NodeInMemory}, ring::{Distance, Location, PeerKeyLocation}, }; diff --git a/crates/core/src/operations.rs b/crates/core/src/operations.rs index 32957a6d7..9c9576331 100644 --- a/crates/core/src/operations.rs +++ b/crates/core/src/operations.rs @@ -1,14 +1,12 @@ use tokio::sync::mpsc::error::SendError; use self::op_trait::Operation; -use crate::operations::get::GetOp; -use crate::operations::put::PutOp; -use crate::operations::subscribe::SubscribeOp; use crate::{ + client_events::{ClientId, HostResult}, contract::ContractError, message::{InnerMessage, Message, Transaction, TransactionType}, node::{ConnectionBridge, ConnectionError, OpManager, PeerKey}, - operations::join_ring::JoinRingOp, + operations::{get::GetOp, join_ring::JoinRingOp, put::PutOp, subscribe::SubscribeOp}, ring::RingError, }; @@ -35,7 +33,8 @@ pub(crate) async fn handle_op_request( op_storage: &OpManager, conn_manager: &mut CB, msg: Op::Message, -) -> Result<(), OpError> + client_id: Option, +) -> Result, OpError> where Op: Operation, CB: ConnectionBridge, @@ -45,7 +44,8 @@ where let result = { let OpInitialization { sender: s, op } = Op::load_or_init(op_storage, &msg)?; sender = s; - op.process_message(conn_manager, op_storage, msg).await + op.process_message(conn_manager, op_storage, msg, client_id) + .await }; handle_op_result( op_storage, @@ -61,7 +61,7 @@ async fn handle_op_result( conn_manager: &mut CB, result: Result, sender: Option, -) -> Result<(), OpError> +) -> Result, OpError> where CB: ConnectionBridge, { @@ -69,7 +69,7 @@ where match result { Err((OpError::StatePushed, _)) => { // do nothing and continue, the operation will just continue later on - return Ok(()); + return Ok(None); } Err((err, tx_id)) => { if let Some(sender) = sender { @@ -88,6 +88,14 @@ where } op_storage.push(id, updated_state)?; } + + Ok(OperationResult { + return_msg: None, + state: Some(final_state), + }) if final_state.is_final() => { + // operation finished_completely with result + return Ok(Some(final_state)); + } Ok(OperationResult { return_msg: None, state: Some(updated_state), @@ -112,7 +120,7 @@ where // operation finished_completely } } - Ok(()) + Ok(None) } pub(crate) enum OpEnum { @@ -132,6 +140,20 @@ impl OpEnum { Subscribe(op) => *>::id(op), } } + + fn is_final(&self) -> bool { + match self { + OpEnum::JoinRing(op) if op.finished() => true, + OpEnum::Put(op) if op.finished() => true, + OpEnum::Get(op) if op.finished() => true, + OpEnum::Subscribe(op) if op.finished() => true, + _ => false, + } + } + + pub fn to_host_result(&self, _client_id: ClientId) -> HostResult { + todo!() + } } #[derive(Debug, thiserror::Error)] @@ -148,7 +170,7 @@ pub(crate) enum OpError { #[error("cannot perform a state transition from the current state with the provided input (tx: {0})")] InvalidStateTransition(Transaction), #[error("failed notifying back to the node message loop, channel closed")] - NotificationError(#[from] Box>), + NotificationError(#[from] Box)>>), #[error("unspected transaction type, trying to get a {0:?} from a {1:?}")] IncorrectTxType(TransactionType, TransactionType), #[error("op not present: {0}")] @@ -163,8 +185,8 @@ pub(crate) enum OpError { StatePushed, } -impl From> for OpError { - fn from(err: SendError) -> OpError { +impl From)>> for OpError { + fn from(err: SendError<(Message, Option)>) -> OpError { OpError::NotificationError(Box::new(err)) } } diff --git a/crates/core/src/operations/get.rs b/crates/core/src/operations/get.rs index 9c4288eea..8f5c3fc0c 100644 --- a/crates/core/src/operations/get.rs +++ b/crates/core/src/operations/get.rs @@ -5,6 +5,7 @@ use std::time::Duration; use freenet_stdlib::prelude::*; use crate::{ + client_events::ClientId, config::PEER_TIMEOUT, contract::{ContractError, ContractHandlerEvent, StoreResponse}, message::{InnerMessage, Message, Transaction, TxType}, @@ -28,20 +29,29 @@ const MAX_GET_RETRY_HOPS: usize = 1; pub(crate) struct GetOp { id: Transaction, state: Option, + result: Option, _ttl: Duration, } +impl GetOp { + pub(super) fn finished(&self) -> bool { + self.result.is_some() + } +} #[allow(dead_code)] pub(crate) struct GetResult { pub state: WrappedState, - pub contract: ContractContainer, + pub contract: Option, } impl TryFrom for GetResult { type Error = OpError; - fn try_from(_value: GetOp) -> Result { - todo!() + fn try_from(value: GetOp) -> Result { + match value.result { + Some(r) => Ok(r), + _ => todo!(), + } } } @@ -73,6 +83,7 @@ where op: Self { state: Some(GetState::ReceivedRequest), id: tx, + result: None, _ttl: PEER_TIMEOUT, }, sender, @@ -92,10 +103,12 @@ where conn_manager: &'a mut CB, op_storage: &'a OpManager, input: Self::Message, + client_id: Option, ) -> Pin> + Send + 'a>> { Box::pin(async move { let return_msg; let new_state; + let mut result = None; match input { GetMsg::RequestGet { @@ -156,6 +169,7 @@ where sender: op_storage.ring.own_location(), target: sender, // return to requester }), + None, self._ttl, ); } @@ -185,10 +199,13 @@ where response: value, key: returned_key, } = op_storage - .notify_contract_handler(ContractHandlerEvent::GetQuery { - key: key.clone(), - fetch_contract, - }) + .notify_contract_handler( + ContractHandlerEvent::GetQuery { + key: key.clone(), + fetch_contract, + }, + client_id, + ) .await? { match check_contract_found( @@ -308,13 +325,13 @@ where }; } GetMsg::ReturnGet { + id, key, value: StoreResponse { state: Some(value), contract, }, - id, sender, target, } => { @@ -331,12 +348,13 @@ where if let Some(contract) = &contract { // store contract first op_storage - .notify_contract_handler(ContractHandlerEvent::Cache( - contract.clone(), - )) + .notify_contract_handler( + ContractHandlerEvent::Cache(contract.clone()), + client_id, + ) .await?; let key = contract.key(); - tracing::debug!("Contract `{}` successfully put", key); + tracing::debug!("Contract `{}` successfully cached", key); } else { // no contract, consider this like an error ignoring the incoming update value tracing::warn!( @@ -347,6 +365,7 @@ where let op = GetOp { id, state: self.state, + result: None, _ttl: self._ttl, }; @@ -363,6 +382,7 @@ where target, }), OpEnum::Get(op), + None, ) .await?; return Err(OpError::StatePushed); @@ -370,10 +390,13 @@ where } op_storage - .notify_contract_handler(ContractHandlerEvent::PutQuery { - key: key.clone(), - state: value.clone(), - }) + .notify_contract_handler( + ContractHandlerEvent::PutQuery { + key: key.clone(), + state: value.clone(), + }, + client_id, + ) .await?; match self.state { @@ -384,10 +407,18 @@ where ); new_state = None; return_msg = None; + result = Some(GetResult { + state: value.clone(), + contract, + }); } else { tracing::debug!("Get response received for contract {}", key); new_state = None; return_msg = None; + result = Some(GetResult { + state: value.clone(), + contract, + }); } } Some(GetState::ReceivedRequest) => { @@ -410,7 +441,7 @@ where _ => return Err(OpError::UnexpectedOpState), } - build_op_result(self.id, new_state, return_msg, self._ttl) + build_op_result(self.id, new_state, return_msg, result, self._ttl) }) } } @@ -419,11 +450,13 @@ fn build_op_result( id: Transaction, state: Option, msg: Option, + result: Option, ttl: Duration, ) -> Result { let output_op = Some(GetOp { id, state, + result, _ttl: ttl, }); Ok(OperationResult { @@ -493,6 +526,7 @@ pub(crate) fn start_op(key: ContractKey, fetch_contract: bool, id: &PeerKey) -> GetOp { id, state, + result: None, _ttl: PEER_TIMEOUT, } } @@ -516,7 +550,11 @@ enum GetState { } /// Request to get the current value from a contract. -pub(crate) async fn request_get(op_storage: &OpManager, get_op: GetOp) -> Result<(), OpError> { +pub(crate) async fn request_get( + op_storage: &OpManager, + get_op: GetOp, + client_id: Option, +) -> Result<(), OpError> { let (target, id) = if let Some(GetState::PrepareRequest { key, id, .. }) = get_op.state.clone() { // the initial request must provide: @@ -554,20 +592,21 @@ pub(crate) async fn request_get(op_storage: &OpManager, get_op: GetOp) -> Result }); let msg = Some(GetMsg::RequestGet { + id, key, target, - id, fetch_contract, }); let op = GetOp { id, state: new_state, + result: None, _ttl: get_op._ttl, }; op_storage - .notify_op_change(msg.map(Message::from).unwrap(), OpEnum::Get(op)) + .notify_op_change(msg.map(Message::from).unwrap(), OpEnum::Get(op), client_id) .await?; } _ => return Err(OpError::InvalidStateTransition(get_op.id)), @@ -578,12 +617,12 @@ pub(crate) async fn request_get(op_storage: &OpManager, get_op: GetOp) -> Result mod messages { use std::fmt::Display; + use serde::{Deserialize, Serialize}; + use crate::{contract::StoreResponse, message::InnerMessage}; use super::*; - use serde::{Deserialize, Serialize}; - #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub(crate) enum GetMsg { /// Internal node call to route to a peer close to the contract. diff --git a/crates/core/src/operations/join_ring.rs b/crates/core/src/operations/join_ring.rs index 0958d0186..212d87baf 100644 --- a/crates/core/src/operations/join_ring.rs +++ b/crates/core/src/operations/join_ring.rs @@ -6,6 +6,7 @@ use super::{OpError, OperationResult}; use crate::operations::op_trait::Operation; use crate::operations::OpInitialization; use crate::{ + client_events::ClientId, config::PEER_TIMEOUT, message::{InnerMessage, Message, Transaction}, node::{ConnectionBridge, ConnectionError, OpManager, PeerKey}, @@ -32,6 +33,10 @@ impl JoinRingOp { pub fn has_backoff(&self) -> bool { self.backoff.is_some() } + + pub(super) fn finished(&self) -> bool { + todo!() + } } pub(crate) struct JoinRingResult {} @@ -89,6 +94,7 @@ impl Operation for JoinRingOp { conn_manager: &'a mut CB, op_storage: &'a OpManager, input: Self::Message, + _client_id: Option, ) -> Pin> + Send + 'a>> { Box::pin(async move { let mut return_msg = None; diff --git a/crates/core/src/operations/op_trait.rs b/crates/core/src/operations/op_trait.rs index 683d9df43..d5a4d63f5 100644 --- a/crates/core/src/operations/op_trait.rs +++ b/crates/core/src/operations/op_trait.rs @@ -5,6 +5,7 @@ use std::pin::Pin; use futures::Future; use crate::{ + client_events::ClientId, message::{InnerMessage, Transaction}, node::OpManager, operations::{OpError, OpInitialization, OperationResult}, @@ -33,5 +34,6 @@ where conn_manager: &'a mut CB, op_storage: &'a OpManager, input: Self::Message, + client_id: Option, ) -> Pin> + Send + 'a>>; } diff --git a/crates/core/src/operations/put.rs b/crates/core/src/operations/put.rs index 10f8b95fe..9c6a61a72 100644 --- a/crates/core/src/operations/put.rs +++ b/crates/core/src/operations/put.rs @@ -12,6 +12,7 @@ use freenet_stdlib::prelude::*; use super::{OpEnum, OpError, OperationResult}; use crate::{ + client_events::ClientId, config::PEER_TIMEOUT, contract::ContractHandlerEvent, message::{InnerMessage, Message, Transaction, TxType}, @@ -25,7 +26,15 @@ pub(crate) struct PutOp { state: Option, /// time left until time out, when this reaches zero it will be removed from the state _ttl: Duration, + done: bool, } + +impl PutOp { + pub(super) fn finished(&self) -> bool { + self.done + } +} + pub(crate) struct PutResult {} impl TryFrom for PutResult { @@ -61,6 +70,7 @@ impl Operation for PutOp { Ok(OpInitialization { op: Self { state: Some(PutState::ReceivedRequest), + done: false, id: tx, _ttl: PEER_TIMEOUT, }, @@ -80,10 +90,12 @@ impl Operation for PutOp { conn_manager: &'a mut CB, op_storage: &'a OpManager, input: Self::Message, + client_id: Option, ) -> Pin> + Send + 'a>> { Box::pin(async move { let return_msg; let new_state; + let mut done = false; match input { PutMsg::RequestPut { @@ -140,7 +152,7 @@ impl Operation for PutOp { .within_caching_distance(&Location::from(&key)) { tracing::debug!("Contract `{}` not cached @ peer {}", key, target.peer); - match try_to_cache_contract(op_storage, &contract, &key).await { + match try_to_cache_contract(op_storage, &contract, &key, client_id).await { Ok(_) => {} Err(err) => return Err(err), } @@ -156,7 +168,7 @@ impl Operation for PutOp { // after the contract has been cached, push the update query tracing::debug!("Attempting contract value update"); - let new_value = put_contract(op_storage, key.clone(), value).await?; + let new_value = put_contract(op_storage, key.clone(), value, client_id).await?; tracing::debug!("Contract successfully updated"); // if the change was successful, communicate this back to the requestor and broadcast the change conn_manager @@ -197,7 +209,7 @@ impl Operation for PutOp { ); match try_to_broadcast( - id, + (id, client_id), op_storage, self.state, broadcast_to, @@ -224,7 +236,8 @@ impl Operation for PutOp { let target = op_storage.ring.own_location(); tracing::debug!("Attempting contract value update"); - let new_value = put_contract(op_storage, key.clone(), new_value).await?; + let new_value = + put_contract(op_storage, key.clone(), new_value, client_id).await?; tracing::debug!("Contract successfully updated"); let broadcast_to = op_storage @@ -247,7 +260,7 @@ impl Operation for PutOp { ); match try_to_broadcast( - id, + (id, client_id), op_storage, self.state, broadcast_to, @@ -326,6 +339,7 @@ impl Operation for PutOp { tracing::debug!("Successfully updated value for {}", contract,); new_state = None; return_msg = None; + done = true; } _ => return Err(OpError::InvalidStateTransition(self.id)), }; @@ -355,7 +369,7 @@ impl Operation for PutOp { .ring .within_caching_distance(&Location::from(&key)); if !cached_contract && within_caching_dist { - match try_to_cache_contract(op_storage, &contract, &key).await { + match try_to_cache_contract(op_storage, &contract, &key, client_id).await { Ok(_) => {} Err(err) => return Err(err), } @@ -367,7 +381,7 @@ impl Operation for PutOp { }); } // after the contract has been cached, push the update query - let new_value = put_contract(op_storage, key, new_value).await?; + let new_value = put_contract(op_storage, key, new_value, client_id).await?; //update skip list skip_list.push(peer_loc.peer); @@ -391,7 +405,7 @@ impl Operation for PutOp { _ => return Err(OpError::UnexpectedOpState), } - build_op_result(self.id, new_state, return_msg, self._ttl) + build_op_result(self.id, new_state, return_msg, self._ttl, done) }) } } @@ -401,10 +415,12 @@ fn build_op_result( state: Option, msg: Option, ttl: Duration, + done: bool, ) -> Result { let output_op = Some(PutOp { id, state, + done, _ttl: ttl, }); Ok(OperationResult { @@ -417,10 +433,11 @@ async fn try_to_cache_contract<'a>( op_storage: &'a OpManager, contract: &ContractContainer, key: &ContractKey, + client_id: Option, ) -> Result<(), OpError> { // this node does not have the contract, so instead store the contract and execute the put op. let res = op_storage - .notify_contract_handler(ContractHandlerEvent::Cache(contract.clone())) + .notify_contract_handler(ContractHandlerEvent::Cache(contract.clone()), client_id) .await?; if let ContractHandlerEvent::CacheResult(Ok(_)) = res { op_storage.ring.contract_cached(key); @@ -435,7 +452,7 @@ async fn try_to_cache_contract<'a>( } async fn try_to_broadcast( - id: Transaction, + (id, client_id): (Transaction, Option), op_storage: &OpManager, state: Option, broadcast_to: Vec, @@ -471,10 +488,15 @@ async fn try_to_broadcast( let op = PutOp { id, state: new_state, + done: false, _ttl: ttl, }; op_storage - .notify_op_change(Message::from(return_msg.unwrap()), OpEnum::Put(op)) + .notify_op_change( + Message::from(return_msg.unwrap()), + OpEnum::Put(op), + client_id, + ) .await?; return Err(OpError::StatePushed); } @@ -509,6 +531,7 @@ pub(crate) fn start_op( PutOp { id, state, + done: false, _ttl: PEER_TIMEOUT, } } @@ -529,7 +552,11 @@ enum PutState { } /// Request to insert/update a value into a contract. -pub(crate) async fn request_put(op_storage: &OpManager, put_op: PutOp) -> Result<(), OpError> { +pub(crate) async fn request_put( + op_storage: &OpManager, + put_op: PutOp, + client_id: Option, +) -> Result<(), OpError> { let key = if let Some(PutState::PrepareRequest { contract, .. }) = put_op.state.clone() { contract.key() } else { @@ -570,11 +597,12 @@ pub(crate) async fn request_put(op_storage: &OpManager, put_op: PutOp) -> Result let op = PutOp { state: new_state, id, + done: false, _ttl: put_op._ttl, }; op_storage - .notify_op_change(msg.map(Message::from).unwrap(), OpEnum::Put(op)) + .notify_op_change(msg.map(Message::from).unwrap(), OpEnum::Put(op), client_id) .await?; } _ => return Err(OpError::InvalidStateTransition(put_op.id)), @@ -587,10 +615,11 @@ async fn put_contract( op_storage: &OpManager, key: ContractKey, state: WrappedState, + client_id: Option, ) -> Result { // after the contract has been cached, push the update query match op_storage - .notify_contract_handler(ContractHandlerEvent::PutQuery { key, state }) + .notify_contract_handler(ContractHandlerEvent::PutQuery { key, state }, client_id) .await { Ok(ContractHandlerEvent::PutResponse { diff --git a/crates/core/src/operations/state_machine.rs b/crates/core/src/operations/state_machine.rs deleted file mode 100644 index 8473d8dc5..000000000 --- a/crates/core/src/operations/state_machine.rs +++ /dev/null @@ -1,110 +0,0 @@ -//! Inspired by `rust-fsm`, bought in tree for modifying and tailoring it to -//! this application needs. - -use crate::message::Transaction; - -use super::OpError; - -pub(crate) trait StateMachineImpl { - /// The input alphabet. - type Input; - /// The set of possible states. - type State; - /// The output alphabet. - type Output; - - /// The transition fuction that outputs a new state based on the current - /// state and the provided input. Outputs `None` when there is no transition - /// for a given combination of the input and the state. - fn state_transition_from_input( - _state: Self::State, - _input: Self::Input, - ) -> Option { - None - } - - fn state_transition(_state: &mut Self::State, _input: &mut Self::Input) -> Option { - None - } - - /// The output function that outputs some value from the output alphabet - /// based on the current state and the given input. Outputs `None` when - /// there is no output for a given combination of the input and the state. - fn output_from_input(_state: Self::State, _input: Self::Input) -> Option { - None - } - - fn output_from_input_as_ref( - _state: &Self::State, - _input: &Self::Input, - ) -> Option { - None - } -} - -/// A convenience wrapper around the `StateMachine` trait that encapsulates the -/// state and transition and output function calls. -pub(crate) struct StateMachine { - state: Option, - pub id: Transaction, -} - -impl StateMachine -where - T: StateMachineImpl, -{ - /// Create a new instance of this wrapper which encapsulates the given - /// state. - pub fn from_state(state: T::State, id: Transaction) -> Self { - Self { - state: Some(state), - id, - } - } - - /// Consumes the provided input, gives an output and performs a state - /// transition. If a state transition with the current state and the - /// provided input is not allowed, returns an error. - /// - /// The consumed input is moved to the state, while the output production takes it by reference. - pub fn consume_to_state( - &mut self, - input: T::Input, - ) -> Result, OpError> { - let popped_state = self - .state - .take() - .ok_or(OpError::InvalidStateTransition(self.id))?; - let output = T::output_from_input_as_ref(&popped_state, &input); - if let Some(new_state) = T::state_transition_from_input(popped_state, input) { - self.state = Some(new_state); - Ok(output) - } else { - Err(OpError::InvalidStateTransition(self.id)) - } - } - - /// Semantically similar to [`Self::consume_to_state()`] with the exception that - /// the consumed input is moved to the output, while the state change takes it by reference. - pub fn consume_to_output( - &mut self, - mut input: T::Input, - ) -> Result, OpError> { - let mut popped_state = self - .state - .take() - .ok_or(OpError::InvalidStateTransition(self.id))?; - if let Some(new_state) = T::state_transition(&mut popped_state, &mut input) { - let output = T::output_from_input(popped_state, input); - self.state = Some(new_state); - Ok(output) - } else { - Err(OpError::InvalidStateTransition(self.id)) - } - } - - /// Returns the current state. - pub fn state(&mut self) -> &mut T::State { - self.state.as_mut().expect("infallible") - } -} diff --git a/crates/core/src/operations/subscribe.rs b/crates/core/src/operations/subscribe.rs index 80c85ac46..726eaad65 100644 --- a/crates/core/src/operations/subscribe.rs +++ b/crates/core/src/operations/subscribe.rs @@ -5,13 +5,13 @@ use std::time::Duration; use freenet_stdlib::prelude::*; use serde::{Deserialize, Serialize}; -use crate::operations::op_trait::Operation; -use crate::operations::OpInitialization; use crate::{ + client_events::ClientId, config::PEER_TIMEOUT, contract::ContractError, message::{Message, Transaction, TxType}, node::{ConnectionBridge, OpManager, PeerKey}, + operations::{op_trait::Operation, OpInitialization}, ring::{PeerKeyLocation, RingError}, }; @@ -27,6 +27,12 @@ pub(crate) struct SubscribeOp { _ttl: Duration, } +impl SubscribeOp { + pub fn finished(&self) -> bool { + todo!() + } +} + pub(crate) enum SubscribeResult {} impl TryFrom for SubscribeResult { @@ -84,6 +90,7 @@ impl Operation for SubscribeOp { conn_manager: &'a mut CB, op_storage: &'a OpManager, input: Self::Message, + client_id: Option, ) -> Pin> + Send + 'a>> { Box::pin(async move { let return_msg; @@ -247,6 +254,8 @@ impl Operation for SubscribeOp { sender.peer ); op_storage.ring.add_subscription(key); + let _ = client_id; + // todo: should inform back to the network event loop? match self.state { Some(SubscribeState::AwaitingResponse { .. }) => { @@ -312,6 +321,7 @@ enum SubscribeState { pub(crate) async fn request_subscribe( op_storage: &OpManager, sub_op: SubscribeOp, + client_id: Option, ) -> Result<(), OpError> { let (target, _id) = if let Some(SubscribeState::PrepareRequest { id, key }) = sub_op.state.clone() { @@ -344,7 +354,11 @@ pub(crate) async fn request_subscribe( _ttl: sub_op._ttl, }; op_storage - .notify_op_change(msg.map(Message::from).unwrap(), OpEnum::Subscribe(op)) + .notify_op_change( + msg.map(Message::from).unwrap(), + OpEnum::Subscribe(op), + client_id, + ) .await?; } _ => return Err(OpError::InvalidStateTransition(sub_op.id)), diff --git a/crates/core/src/operations/update.rs b/crates/core/src/operations/update.rs index 7ba93714c..49e5efe66 100644 --- a/crates/core/src/operations/update.rs +++ b/crates/core/src/operations/update.rs @@ -1,7 +1,7 @@ // TODO: complete update logic in the network pub(crate) use self::messages::UpdateMsg; -use crate::node::ConnectionBridge; +use crate::{client_events::ClientId, node::ConnectionBridge}; use super::{op_trait::Operation, OpError}; @@ -37,6 +37,7 @@ impl Operation for UpdateOp { _conn_manager: &'a mut CB, _op_storage: &'a crate::node::OpManager, _input: Self::Message, + _client_id: Option, ) -> std::pin::Pin< Box> + Send + 'a>, > { @@ -45,8 +46,11 @@ impl Operation for UpdateOp { } mod messages { + use serde::{Deserialize, Serialize}; + use crate::message::{InnerMessage, Transaction}; + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub(crate) enum UpdateMsg {} impl InnerMessage for UpdateMsg { diff --git a/crates/core/src/server/mod.rs b/crates/core/src/server/mod.rs index 48a1cb426..c46883ca9 100644 --- a/crates/core/src/server/mod.rs +++ b/crates/core/src/server/mod.rs @@ -105,7 +105,7 @@ pub mod local_node { } }; let OpenRequest { - id, + client_id: id, request, notification_channel, token, diff --git a/stdlib b/stdlib index 26c7acf86..2ee157f90 160000 --- a/stdlib +++ b/stdlib @@ -1 +1 @@ -Subproject commit 26c7acf8635b3ce8cef2568c159379b2ed800e73 +Subproject commit 2ee157f90449e85c0e43b552bb8653f8dd694e6b From 5e69981761eb2abf903f0beec0d597319ee2fe10 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Tue, 26 Sep 2023 16:13:33 +0200 Subject: [PATCH 06/76] Minor fixes to documentation --- .github/dependabot.yml | 4 ++-- .github/workflows/docs.yml | 1 + README.md | 26 +++++++++++++------------- docs/src/tutorial.md | 8 ++++---- stdlib | 2 +- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 51d4fa994..f37c6069f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,8 +3,8 @@ updates: - package-ecosystem: "cargo" directory: "/" schedule: - interval: "daily" - open-pull-requests-limit: 9999 + interval: "weekly" + open-pull-requests-limit: 50 - package-ecosystem: "github-actions" directory: "/" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d7d0a964a..5d1a458aa 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -13,6 +13,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + submodules: true - name: Setup Rust uses: ATiltedTree/setup-rust@v1 with: diff --git a/README.md b/README.md index 8b2af0877..f37e5db81 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ matrix - + docs.rs docs @@ -42,14 +42,14 @@ building decentralized applications using our SDK and testing them locally. Examples of what can be built on Freenet include: -* Decentralized email (with a gateway to legacy email via the @freenet.org +- Decentralized email (with a gateway to legacy email via the @freenet.org domain) -* Decentralized microblogging (think Twitter or Facebook) -* Instant Messaging (Whatsapp, Signal) -* Online Store (Amazon) -* Discussion (Reddit, HN) -* Video discovery (Youtube, TikTok) -* Search (Google, Bing) +- Decentralized microblogging (think Twitter or Facebook) +- Instant Messaging (Whatsapp, Signal) +- Online Store (Amazon) +- Discussion (Reddit, HN) +- Video discovery (Youtube, TikTok) +- Search (Google, Bing) All will be completely decentralized, scalable, and cryptographically secure. We want Freenet to be useful out-of-the-box, so we plan to provide reference @@ -61,7 +61,7 @@ Freenet is a decentralized key-value database. It uses the same [small world](https://freenetproject.org/assets/papers/lic.pdf) routing algorithm as the original Freenet design, but each key is a cryptographic contract implemented in [Web Assembly](https://webassembly.org/), and the value -associated with each contract is called its *state*. The role of the +associated with each contract is called its _state_. The role of the cryptographic contract is to specify what state is allowed for this contract, and how the state is modified. @@ -110,14 +110,14 @@ development. ### Supporting Freenet If you are in a position to fund our continued efforts please contact us on -[twitter](https://twitter.com/FreenetOrg) or by email at *ian at freenet dot -org*. +[twitter](https://twitter.com/FreenetOrg) or by email at _ian at freenet dot +org_. ## License This project is licensed under either of: -* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or +- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) -* MIT license ([LICENSE-MIT](LICENSE-MIT) or +- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index e6bc8acae..b979d6b56 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -175,7 +175,7 @@ applications and interfacing with your local node, so we will make our ```json { "dependencies": { - "@freenet/freenet-stdlib": "0.0.2" + "@freenetorg/freenet-stdlib": "0.0.6" } } ``` @@ -196,11 +196,11 @@ import { GetResponse, HostError, Key, - LocutusWsApi, + FreenetWsApi, PutResponse, UpdateNotification, UpdateResponse, -} from "@locutus/locutus-stdlib/webSocketInterface"; +} from "@freenetorg/freenet-stdlib/websocket-interface"; const handler = { onPut: (_response: PutResponse) => {}, @@ -212,7 +212,7 @@ const handler = { }; const API_URL = new URL(`ws://${location.host}/contract/command/`); -const locutusApi = new LocutusWsApi(API_URL, handler); +const locutusApi = new FreenetWsApi(API_URL, handler); const CONTRACT = "DCBi7HNZC3QUZRiZLFZDiEduv5KHgZfgBk8WwTiheGq1"; diff --git a/stdlib b/stdlib index 2ee157f90..32e0dd28e 160000 --- a/stdlib +++ b/stdlib @@ -1 +1 @@ -Subproject commit 2ee157f90449e85c0e43b552bb8653f8dd694e6b +Subproject commit 32e0dd28ef5c3d00f3020491926bb47a3cfe0eba From 25dc2dc2ec5340d45d84369cf356cdc078806b3d Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Tue, 26 Sep 2023 08:59:04 -0500 Subject: [PATCH 07/76] minor text change --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index f37e5db81..4208ea781 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,12 @@ Locutus was the working title used for this successor to the original Freenet, in March 2023 it was renamed to "Freenet" or "Freenet 2023", this repository was renamed from `locutus` to `freenet-core` in September 2023. +## What is Hyphanet? + +The original Freenet codebase is now called Hyphanet. It is still actively +developed by the same maintainers as before, and is available +[here](https://www.hyphanet.org). + ## Stay up to date [![Twitter From 6161c807fd70be4bad95586a1157413adeecd35e Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Tue, 26 Sep 2023 09:07:45 -0500 Subject: [PATCH 08/76] minor text change --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4208ea781..169a14378 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ systems, desktop and mobile. ## What is Locutus? Locutus was the working title used for this successor to the original Freenet, -in March 2023 it was renamed to "Freenet" or "Freenet 2023", this repository was +in March 2023 it was renamed to "Freenet", this repository was renamed from `locutus` to `freenet-core` in September 2023. ## What is Hyphanet? From 036c811441a44144e95cd30591994bc9300870dc Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Tue, 26 Sep 2023 16:30:58 +0200 Subject: [PATCH 09/76] Update dep for the microblogging app --- apps/freenet-microblogging/Cargo.lock | 36 ++++++++++++--------- apps/freenet-microblogging/Cargo.toml | 2 +- apps/freenet-microblogging/web/src/index.ts | 4 +-- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/apps/freenet-microblogging/Cargo.lock b/apps/freenet-microblogging/Cargo.lock index 4ac255d79..5cf3784e8 100644 --- a/apps/freenet-microblogging/Cargo.lock +++ b/apps/freenet-microblogging/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "aho-corasick" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2135563fb5c609d2b2b87c1e8ce7bc41b0b45430fa9661f457981503dd5bf0" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" dependencies = [ "memchr", ] @@ -76,9 +76,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "blake3" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "199c42ab6972d92c9f8995f086273d25c42fc0f7b2a1fcefba465c1352d25ba5" +checksum = "0231f06152bf547e9c2b5194f247cd97aacf6dcd8b15d8e5ec0663f64580da87" dependencies = [ "arrayref", "arrayvec", @@ -275,6 +275,8 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "freenet-macros" version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9fac5b43bee97b6ee49a376ca9e26eedf8bd581efcb176e4a534304f8b4279" dependencies = [ "proc-macro2", "quote", @@ -303,6 +305,8 @@ dependencies = [ [[package]] name = "freenet-stdlib" version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb83503ad71922ceeaa56737c0a4a9da11520aaaf8ad6a89f43edb1154f02764" dependencies = [ "arrayvec", "bincode", @@ -797,9 +801,9 @@ checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "semver" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" dependencies = [ "serde", ] @@ -875,9 +879,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -914,9 +918,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "spin" @@ -989,9 +993,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" +checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" dependencies = [ "deranged", "itoa", @@ -1002,15 +1006,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] diff --git a/apps/freenet-microblogging/Cargo.toml b/apps/freenet-microblogging/Cargo.toml index d7e19decb..c998161d5 100644 --- a/apps/freenet-microblogging/Cargo.toml +++ b/apps/freenet-microblogging/Cargo.toml @@ -14,7 +14,7 @@ panic = 'abort' strip = true [workspace.dependencies] -freenet-stdlib = { path = "../../stdlib/rust", default-features = false } +freenet-stdlib = { version = "0.0.5", default-features = false } #[target.wasm32-unknown-unknown] #rustflags = ["-C", "link-arg=--import-memory"] diff --git a/apps/freenet-microblogging/web/src/index.ts b/apps/freenet-microblogging/web/src/index.ts index 51429f047..de4d923fd 100644 --- a/apps/freenet-microblogging/web/src/index.ts +++ b/apps/freenet-microblogging/web/src/index.ts @@ -3,7 +3,7 @@ import { GetResponse, HostError, ContractKey, - LocutusWsApi, + FreenetWsApi, PutResponse, UpdateNotification, UpdateResponse, @@ -146,7 +146,7 @@ const handler = { }; const API_URL = new URL(`ws://${location.host}/contract/command`); -const locutusApi = new LocutusWsApi(API_URL, handler); +const locutusApi = new FreenetWsApi(API_URL, handler); async function loadState() { const key = ContractKey.fromInstanceId(MODEL_CONTRACT); From a050ffbc8e25d90dfa2510a9778c8e7ba502bed1 Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Tue, 26 Sep 2023 09:54:38 -0500 Subject: [PATCH 10/76] Update pt_sync.yml --- .github/workflows/pt_sync.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pt_sync.yml b/.github/workflows/pt_sync.yml index 0824252bd..7a2e8b568 100644 --- a/.github/workflows/pt_sync.yml +++ b/.github/workflows/pt_sync.yml @@ -2,7 +2,7 @@ name: Sync Issues with Pivotal Tracker on: issues: - types: [labeled] + types: [labeled, opened] jobs: sync: From 5c5288e20893816092a13b796bd0679067ed3d4a Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Tue, 26 Sep 2023 09:55:08 -0500 Subject: [PATCH 11/76] Update pt_sync.yml --- .github/workflows/pt_sync.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pt_sync.yml b/.github/workflows/pt_sync.yml index 7a2e8b568..c77492c41 100644 --- a/.github/workflows/pt_sync.yml +++ b/.github/workflows/pt_sync.yml @@ -15,5 +15,5 @@ jobs: PT_PROJECT_ID: ${{ secrets.PT_PROJECT_ID }} GH_REPOSITORY: ${{ github.repository }} GH_ACCESS_TOKEN: ${{ github.token }} - if: github.event.label.name == 'planned' + if: github.event.label.name == 'planned' || github.event.action == 'opened' run: python .github/scripts/pt_sync.py From 3420e0fe7cbaa0fe1b82594deeb80813c395ae0b Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Tue, 26 Sep 2023 17:28:31 +0200 Subject: [PATCH 12/76] Remove random bytes delegate request --- crates/core/src/runtime/delegate.rs | 30 +---------------------------- crates/core/src/runtime/util.rs | 8 -------- stdlib | 2 +- 3 files changed, 2 insertions(+), 38 deletions(-) diff --git a/crates/core/src/runtime/delegate.rs b/crates/core/src/runtime/delegate.rs index 32b60418f..368823ccd 100644 --- a/crates/core/src/runtime/delegate.rs +++ b/crates/core/src/runtime/delegate.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use wasmer::{Instance, TypedFunction}; use super::error::RuntimeInnerError; -use super::{util, ContractError, Runtime, RuntimeResult}; +use super::{ContractError, Runtime, RuntimeResult}; #[derive(Debug, Serialize, Deserialize)] pub enum Response { @@ -216,16 +216,6 @@ impl Runtime { context.user_response.insert(req_id, response); last_context = DelegateContext::new(bincode::serialize(&context).unwrap()); } - OutboundDelegateMsg::RandomBytesRequest(bytes) => { - let mut bytes = vec![0; bytes]; - util::generate_random_bytes(&mut bytes); - let inbound = InboundDelegateMsg::RandomBytes(bytes); - let new_outbound_msgs = - self.exec_inbound(params, attested, &inbound, process_func, instance)?; - for msg in new_outbound_msgs.into_iter() { - outbound_msgs.push_back(msg); - } - } OutboundDelegateMsg::ContextUpdated(context) => { last_context = context; } @@ -337,24 +327,6 @@ impl DelegateRuntimeInterface for Runtime { &mut results, )?; } - InboundDelegateMsg::RandomBytes(bytes) => { - let mut outbound = VecDeque::from(self.exec_inbound( - params, - attested, - &InboundDelegateMsg::RandomBytes(bytes), - &process_func, - &running.instance, - )?); - self.get_outbound( - delegate_key, - &running.instance, - &process_func, - params, - attested, - &mut outbound, - &mut results, - )?; - } InboundDelegateMsg::GetSecretRequest(GetSecretRequest { key: secret_key, .. }) => { diff --git a/crates/core/src/runtime/util.rs b/crates/core/src/runtime/util.rs index 5f964afcb..8b1378917 100644 --- a/crates/core/src/runtime/util.rs +++ b/crates/core/src/runtime/util.rs @@ -1,9 +1 @@ -use rand::{rngs::ThreadRng, Rng}; -#[inline] -pub fn generate_random_bytes(output: &mut [u8]) { - let mut rng = ThreadRng::default(); - for element in output { - *element = rng.gen(); - } -} diff --git a/stdlib b/stdlib index 32e0dd28e..ea8730491 160000 --- a/stdlib +++ b/stdlib @@ -1 +1 @@ -Subproject commit 32e0dd28ef5c3d00f3020491926bb47a3cfe0eba +Subproject commit ea8730491f87f3fd6b1c813e581a508dfbaf94a3 From 581078b41f35d1290d2e6692eefa5d606797f02c Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Wed, 27 Sep 2023 11:35:06 +0200 Subject: [PATCH 13/76] Update crates.io link --- README.md | 4 ++-- stdlib | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 169a14378..a32792b23 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ continuous integration status - - + Crates.io version diff --git a/stdlib b/stdlib index ea8730491..6cd278094 160000 --- a/stdlib +++ b/stdlib @@ -1 +1 @@ -Subproject commit ea8730491f87f3fd6b1c813e581a508dfbaf94a3 +Subproject commit 6cd278094b77f2cd11c0ca2c7e1458c6e78a5cf8 From f6e3fecd0188a1f3e9bdae55a23a3fcd1deb2a50 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Wed, 27 Sep 2023 11:53:50 +0200 Subject: [PATCH 14/76] More documentation renaming --- docs/src/README.md | 22 +++++++++++----------- docs/src/contract-interface.md | 2 +- docs/src/docker.md | 2 +- docs/src/glossary.md | 12 ++++++------ docs/src/tutorial.md | 20 ++++++++++---------- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/src/README.md b/docs/src/README.md index fc12a4f10..b5649bd9c 100644 --- a/docs/src/README.md +++ b/docs/src/README.md @@ -2,22 +2,22 @@ # Introduction -## What is Locutus? +## What is Freenet? -Locutus is a global, [observable](https://en.wikipedia.org/wiki/Small-world_network), decentralized key-value store. Values are arbitrary blocks of data, called the contract's "state." Keys are cryptographic contracts that specify: +Freenet is a global, [observable](https://en.wikipedia.org/wiki/Small-world_network), decentralized key-value store. Values are arbitrary blocks of data, called the contract's "state." Keys are cryptographic contracts that specify: - Whether a given state is permitted under this contract - How the state can be modified over time - How two valid states can be merged - How to efficiently synchronize a contract's state between peers -Locutus is a true decentralized peer-to-peer network, and is robust and scalable, through its use of a [small-world network](https://en.wikipedia.org/wiki/Small-world_network). +Freenet is a true decentralized peer-to-peer network, and is robust and scalable, through its use of a [small-world network](https://en.wikipedia.org/wiki/Small-world_network). -Applications on Locutus can be built in any language that is supported by web browsers, including JavaScript and WebAssembly. These applications are distributed over Locutus and can create, retrieve, and update contracts through a WebSocket connection to the local Locutus peer. +Applications on Freenet can be built in any language that is supported by web browsers, including JavaScript and WebAssembly. These applications are distributed over Freenet and can create, retrieve, and update contracts through a WebSocket connection to the local Freenet peer. ## Writing a Contract -Locutus contracts can be written in any language that compiles to WebAssembly. +Freenet contracts can be written in any language that compiles to WebAssembly. This includes [Rust](https://www.rust-lang.org/), and [AssemblyScript](https://www.assemblyscript.org/), among many others. @@ -27,17 +27,17 @@ A contract can be retrieved using a key, which is a cryptographic hash derived f ## Small world routing -Locutus peers self-organize into a [small-world network](https://en.wikipedia.org/wiki/Small-world_routing) to allow contracts to be found in a fast, scalable, and decentralized way. +Freenet peers self-organize into a [small-world network](https://en.wikipedia.org/wiki/Small-world_routing) to allow contracts to be found in a fast, scalable, and decentralized way. -Every peer in Locutus is assigned a number between 0 and 1 when it first joins the network, this is the peer's "location". The small world network topology ensures that peers with similar locations are more likely to be connected. +Every peer in Freenet is assigned a number between 0 and 1 when it first joins the network, this is the peer's "location". The small world network topology ensures that peers with similar locations are more likely to be connected. Contracts also have a location, which is derived from the contract's key. Peers cache contracts close to their locations. ## Writing an Application -Creating a decentralized application on Locutus is very similar to creating a normal web application. You can use familiar frameworks like React, Bootstrap, Angular, Vue.js, and so on. +Creating a decentralized application on Freenet is very similar to creating a normal web application. You can use familiar frameworks like React, Bootstrap, Angular, Vue.js, and so on. -The main difference is that instead of connecting to a REST API running on a server, the web application connects to the Locutus peer running on the local computer through a [WebSocket](https://en.wikipedia.org/wiki/WebSocket) connection. +The main difference is that instead of connecting to a REST API running on a server, the web application connects to the Freenet peer running on the local computer through a [WebSocket](https://en.wikipedia.org/wiki/WebSocket) connection. Through this the application can: @@ -51,11 +51,11 @@ Contracts are extremely flexible. they can be used to create decentralized data ## Delegate Ecosystem -Applications in Locutus don't need to be built from scratch, they can be built on top of components provided by us or others. +Applications in Freenet don't need to be built from scratch, they can be built on top of components provided by us or others. ### Reputation system -Allows users to build up reputation over time based on feedback from those they interact with. Think of the feedback system in services like Uber, but with Locutus it will be entirely decentralized and cryptographically secure. It can be used for things like spam prevention (with IM and email), or fraud prevention (with an online store). +Allows users to build up reputation over time based on feedback from those they interact with. Think of the feedback system in services like Uber, but with Freenet it will be entirely decentralized and cryptographically secure. It can be used for things like spam prevention (with IM and email), or fraud prevention (with an online store). This is conceptually similar to Freenet's [Web of Trust](http://www.draketo.de/english/freenet/friendly-communication-with-anonymity) plugin. diff --git a/docs/src/contract-interface.md b/docs/src/contract-interface.md index cfcd7bdb0..7e392aaaf 100644 --- a/docs/src/contract-interface.md +++ b/docs/src/contract-interface.md @@ -9,7 +9,7 @@ ## Interface -Locutus contracts must implement the contract interface from [stdlib/rust/src/contract_interface.rs](https://github.com/freenet/freenet-core/blob/main/stdlib/rust/src/contract_interface.rs): +Freenet contracts must implement the contract interface from [stdlib/rust/src/contract_interface.rs](https://github.com/freenet/freenet-core/blob/main/stdlib/rust/src/contract_interface.rs): ```rust,no_run,noplayground {{#include ../../stdlib/rust/src/contract_interface.rs:contractifce}} diff --git a/docs/src/docker.md b/docs/src/docker.md index fc8389592..cacf1bda2 100644 --- a/docs/src/docker.md +++ b/docs/src/docker.md @@ -22,7 +22,7 @@ cd docker docker compose build ``` -## Running Locutus Node from the docker image +## Running Freenet from the docker image Note: Currently the node will not pick up new contracts when they are published. Make sure the node is stopped and re-started after new contracts are added. diff --git a/docs/src/glossary.md b/docs/src/glossary.md index 84b67be45..9abc9e5b4 100644 --- a/docs/src/glossary.md +++ b/docs/src/glossary.md @@ -2,10 +2,10 @@ ## Application -Software that uses Locutus as a back-end. This includes native software -distributed independenly of Locutus but which uses Locutus as a back-end -(perhaps bundling Locutus), and [web applications](glossary#web-application) -that are distributed over Locutus and run in a web browser. +Software that uses Freenet as a back-end. This includes native software +distributed independenly of Freenet but which uses Freenet as a back-end +(perhaps bundling Freenet), and [web applications](glossary#web-application) +that are distributed over Freenet and run in a web browser. ## Contract @@ -24,7 +24,7 @@ the web proxy. For example, if the contract id is `6C2KyVMtqw8D5wWa8Y7e14VmDNXXXv9CQ3m44PC9YbD2` then visiting `http://localhost:PORT/contract/web/6C2KyVMtqw8D5wWa8Y7e14VmDNXXXv9CQ3m44PC9YbD2` -will cause the application/component to be retrieved from Locutus, decompressed, +will cause the application/component to be retrieved from Freenet, decompressed, and sent to the browser where it can execute. ## Contract State @@ -78,7 +78,7 @@ consistency](https://en.wikipedia.org/wiki/Eventual_consistency). ## Web Application -Software built on Locutus and distributed through Locutus. +Software built on Freenet and distributed through Freenet. Applications run in the browser and can be built with tools like React, TypeScript, and Vue.js. An application may use multiple components and diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index b979d6b56..bd4c03f95 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -15,16 +15,16 @@ Mac (for Windows see [here](https://rustup.rs)): curl https://sh.rustup.rs -sSf | sh ``` -### Locutus Dev Tool (LDT) +### Freenet Dev Tool (LDT) -Once you have a working installation of Cargo you can install the Locutus dev +Once you have a working installation of Cargo you can install the Freenet dev tools: ```bash cargo install freenet ``` -This command will install `fdev` (Locutus Dev Tool) and a working Freenet kernel that can +This command will install `fdev` (Freenet development tool) and a working Freenet kernel that can be used for local development. ### Node.js and TypeScript @@ -83,13 +83,13 @@ fdev new web-app ``` will create the skeleton for a web application and its container contract for -Locutus ready for development at the `my-app/web` directory. +Freenet ready for development at the `my-app/web` directory. ## Making a container contract The first thing that we need is to write the code for our container contract. This contract's role is to contain the web application code itself, allowing it -to be distributed over Locutus. +to be distributed over Freenet. The `new` command has created the source ready to be modified for us, in your favorite editor open the following file: @@ -122,7 +122,7 @@ That's a lot of information, let's unpack it: use freenet_stdlib::prelude::*; ``` -Here we are importing the necessary types and traits to write a Locutus contract +Here we are importing the necessary types and traits to write a Freenet contract successfully using Rust. ```rust,noplayground @@ -167,7 +167,7 @@ which does not do anything yet. To change this, we will start developing our web application. To do that, we can go and modify the code of the contract state, which in this -case is the web application. Locutus offers a standard library (stdlib) that can +case is the web application. Freenet offers a standard library (stdlib) that can be used with Typescript/JavaScript to facilitate the development of web applications and interfacing with your local node, so we will make our `package.json` contains the dependency: @@ -378,7 +378,7 @@ HTTP gateway access and then we can re-use it for publishing additional contracts. Currently, wep applications follow a standarized build procedure in case you use @@ -470,7 +470,7 @@ You should see some logs printed via the stdout of the process indicating that the node HTTP gateway is running. Once the HTTP gateway is running, we are ready to publish the contracts to our -local Locutus node: +local Freenet node: ```bash cd ../backend && fdev publish --code="./build/freenet/backend.wasm" --state="./build/freenet/contract-state" @@ -497,7 +497,7 @@ to new contracts, and evolving it over time. ## Limitations -- Publishing to the Locutus network is not yet supported. +- Publishing to the Freenet network is not yet supported. - Only Rust is currently supported for contract development, but we'll support more languages like [AssemblyScript](https://www.assemblyscript.org/) in the future. From 4262245bbeb9981dd19805b3568764d0e81d2788 Mon Sep 17 00:00:00 2001 From: Hector Santos Date: Tue, 26 Sep 2023 23:34:36 +0200 Subject: [PATCH 15/76] Add an email app guide --- apps/freenet-email-app/Makefile | 5 +- apps/freenet-email-app/README.md | 158 +++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 apps/freenet-email-app/README.md diff --git a/apps/freenet-email-app/Makefile b/apps/freenet-email-app/Makefile index d92b27611..9bbc387d1 100644 --- a/apps/freenet-email-app/Makefile +++ b/apps/freenet-email-app/Makefile @@ -16,8 +16,11 @@ endif .PHONY: build clean +all: \ + $(MAKE) build-tool \ + $(MAKE) build + build: \ - build-tool \ generate-id-manager-key \ build-inbox \ build-webapp \ diff --git a/apps/freenet-email-app/README.md b/apps/freenet-email-app/README.md new file mode 100644 index 000000000..b886e1e33 --- /dev/null +++ b/apps/freenet-email-app/README.md @@ -0,0 +1,158 @@ +# Freenet Email App Setup Guide + +Freenet Email App is a descentralized email application that runs on top of Freenet. + +## Introduction + +This guide will walk you through the setup and launch of the email application on Freenet. +Currently, Freenet is still under development, so the application will communicate with a +local node, which simulates a node on the network. + +## Prerequisites + +* Install the latest version of Rust and Cargo (for Windows + see [here](https://rustup.rs/)): + ```bash + curl https://sh.rustup.rs -sSf | sh + ``` +* Install the Locutus Dev Tool (LDT). Use cargo to install it: + ```bash + cargo install freenet + ``` + This command will install fdev (Locutus Dev Tool) and a working Freenet kernel that + can be used for local development. + +## Prepare the Freenet email contracts and delegates + +### Setup the identity management delegate + +This delegate is responsible for managing a user's contacts. It can store and +retrieve contact information, and can be used by other components to send messages +to contacts. + +This delegate is located inside the modules folder of freenet-core: + +- `freenet-core/` + - `modules/` + - `identity-management/` <-- here is located the identity management delegate + - `build/` <-- the generated folder that contains the compiled delegate binary with version + wasm code + - `src/` <-- this folder contains the source code of the delegate + - `Makefile` <-- this file contains the build instructions for the delegate + - ... + +To build the delegate, go to the `identity-management` folder and run the following command: + +```bash +make build +``` + +This command will compile the delegate and generate a binary file inside the `build/locutus/` folder. It +generates the build folder if it doesn't exist. In addition, the build command will generate: + +- `build/identity_management_code_hash` <-- this file contains the hash of the delegate's wasm code +- `build/identity-manager-key.private.pem` <-- this file contains a generated private key for the delegate +- `build/identity-manager-key.public.pem` <-- this file contains a generated public key for the delegate +- `build/identity-manager-params` <-- this file contains the parameters of the delegate, in that case, the delegate's + SecretKey. + +### Setup the anti-flood token system + +The Antiflood Token System (AFT) is a decentralized system aimed to provide a simple, but general purpose solution +to flooding, denial-of-service attacks, and spam. + +Is composed of one delegate, responsible for generating tokens, and a one contract that keeps track of the token +assignments. + +This system is located inside the modules folder of freenet-core: + +- `freenet-core/` + - `modules/` + - `antiflood-tokens/` <-- here is located the antiflood tokens system + - `contracts/` <-- this folder contains the contract source code + - `token-allocation-record/` <-- this folder contains the token allocation record contract + - `build/` <-- the generated folder that contains the compiled contract binary with version + wasm + code + - `src/` <-- this folder contains the source code of the contract + - ... + - `delegates/` <-- this folder contains the delegate source code + - `token-generator/` <-- this folder contains the token generator delegate + - `build/` <-- the generated folder that contains the compiled delegate binary with version + wasm + code + - `src/` <-- this folder contains the source code of the delegate + - ... + - `Makefile` <-- this file contains the build instructions for building the delegate and the contract + - ... + +To build the Antiflood Token System, go to the `antiflood-tokens` folder and run the following command: + +```bash +make build +``` + +This command will compile the contract and the delegate and generate a binary file inside the `build/locutus/` folder. +It +generates the build folder if it doesn't exist. + +- `contracts/token-allocation-record/build/identity_management_code_hash` <-- this file contains the hash of the + contract's wasm code +- `delegates/token-generator/build/token_generator_code_hash` <-- this file contains the hash of the delegate's wasm + code + +### Setup the app + +After building the previous delegates and contracts, what remains is to prepare the app, both the web +and the contract that will be responsible for maintaining the inboxes. + +The app is located inside the `apps/freenet-email-app` folder: + +- `freenet-core/` + - `apps/` + - `freenet-email-app/` <-- here is located the email app + - `contracts/` <-- this folder contains the contract source code + - `inbox/` <-- this folder contains the email inbox contract + - `build/` <-- the generated folder that contains the compiled contract binary with version + wasm + code + - `src/` <-- this folder contains the source code of the contract + - ... + - `web/` <-- this folder contains the web app source code, web app built with Rust Dioxus framework + - `build/` <-- the generated folder that contains the compiled web app binary with version + wasm + code + - `container/` <-- this folder contains the web contract container, a simple contract associated + with the web app, as the state. + - `src/` <-- this folder contains the source code of the web app + - ... + - `Makefile` <-- this file contains the build instructions for building and running the web app, and + the local node. + +To build the email application, go to the `apps/freenet-email-app` folder and run the following command: + +```bash +make build +``` + +This command will compile the inbox and web contracts and the delegate, generate a binary files +inside the respective `build/locutus/` folders and publish the web contract. It generates the build folder if it doesn't +exist. + +After building the app, what remains is to run the local node and the web app. To do that, run the following commanda: + +1. Run the local node: + ```bash + make run-node + ``` +2. Run the web app: + ```bash + make run-web + ``` + +During the development process, changes inside the web app will be automatically reloaded if it is running. + +## Troubleshooting + +### Freenet node throws an error when trying to find some contract or delegate + +If any of the delegates or contracts definition change, is necessary to rebuild each of them and restart the node. +Probably the contract or delegate hash has changed and the node is trying to find the old version. + +If the error persists, we recommend to make a clean build of all the delegates and contracts, and restart the node. + From d1a8a3ecfd6b8108155d28ef6b386ff93733c4f7 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Wed, 27 Sep 2023 11:51:04 +0200 Subject: [PATCH 16/76] Minor name corrections --- apps/freenet-email-app/README.md | 114 ++++++++++++++++--------------- 1 file changed, 58 insertions(+), 56 deletions(-) diff --git a/apps/freenet-email-app/README.md b/apps/freenet-email-app/README.md index b886e1e33..80a406b16 100644 --- a/apps/freenet-email-app/README.md +++ b/apps/freenet-email-app/README.md @@ -1,6 +1,6 @@ -# Freenet Email App Setup Guide +# Freenet Messaging App Setup Guide -Freenet Email App is a descentralized email application that runs on top of Freenet. +Freenet Messaging App is a descentralized messaging application that runs on top of Freenet. ## Introduction @@ -10,17 +10,16 @@ local node, which simulates a node on the network. ## Prerequisites -* Install the latest version of Rust and Cargo (for Windows +- Install the latest version of Rust and Cargo (for Windows see [here](https://rustup.rs/)): ```bash curl https://sh.rustup.rs -sSf | sh ``` -* Install the Locutus Dev Tool (LDT). Use cargo to install it: +- Install the Freeenet development tool (fdev) and a working Freenet kernel that can be used for local development. Use cargo to install it: ```bash cargo install freenet + cargo install fdev ``` - This command will install fdev (Locutus Dev Tool) and a working Freenet kernel that - can be used for local development. ## Prepare the Freenet email contracts and delegates @@ -33,12 +32,12 @@ to contacts. This delegate is located inside the modules folder of freenet-core: - `freenet-core/` - - `modules/` - - `identity-management/` <-- here is located the identity management delegate - - `build/` <-- the generated folder that contains the compiled delegate binary with version + wasm code - - `src/` <-- this folder contains the source code of the delegate - - `Makefile` <-- this file contains the build instructions for the delegate - - ... + - `modules/` + - `identity-management/` <-- here is located the identity management delegate + - `build/` <-- the generated folder that contains the compiled delegate binary with version + wasm code + - `src/` <-- this folder contains the source code of the delegate + - `Makefile` <-- this file contains the build instructions for the delegate + - ... To build the delegate, go to the `identity-management` folder and run the following command: @@ -66,22 +65,22 @@ assignments. This system is located inside the modules folder of freenet-core: - `freenet-core/` - - `modules/` - - `antiflood-tokens/` <-- here is located the antiflood tokens system - - `contracts/` <-- this folder contains the contract source code - - `token-allocation-record/` <-- this folder contains the token allocation record contract - - `build/` <-- the generated folder that contains the compiled contract binary with version + wasm - code - - `src/` <-- this folder contains the source code of the contract - - ... - - `delegates/` <-- this folder contains the delegate source code - - `token-generator/` <-- this folder contains the token generator delegate - - `build/` <-- the generated folder that contains the compiled delegate binary with version + wasm - code - - `src/` <-- this folder contains the source code of the delegate - - ... - - `Makefile` <-- this file contains the build instructions for building the delegate and the contract - - ... + - `modules/` + - `antiflood-tokens/` <-- here is located the antiflood tokens system + - `contracts/` <-- this folder contains the contract source code + - `token-allocation-record/` <-- this folder contains the token allocation record contract + - `build/` <-- the generated folder that contains the compiled contract binary with version + wasm + code + - `src/` <-- this folder contains the source code of the contract + - ... + - `delegates/` <-- this folder contains the delegate source code + - `token-generator/` <-- this folder contains the token generator delegate + - `build/` <-- the generated folder that contains the compiled delegate binary with version + wasm + code + - `src/` <-- this folder contains the source code of the delegate + - ... + - `Makefile` <-- this file contains the build instructions for building the delegate and the contract + - ... To build the Antiflood Token System, go to the `antiflood-tokens` folder and run the following command: @@ -90,8 +89,7 @@ make build ``` This command will compile the contract and the delegate and generate a binary file inside the `build/locutus/` folder. -It -generates the build folder if it doesn't exist. +It generates the build folder if it doesn't exist. - `contracts/token-allocation-record/build/identity_management_code_hash` <-- this file contains the hash of the contract's wasm code @@ -106,23 +104,23 @@ and the contract that will be responsible for maintaining the inboxes. The app is located inside the `apps/freenet-email-app` folder: - `freenet-core/` - - `apps/` - - `freenet-email-app/` <-- here is located the email app - - `contracts/` <-- this folder contains the contract source code - - `inbox/` <-- this folder contains the email inbox contract - - `build/` <-- the generated folder that contains the compiled contract binary with version + wasm - code - - `src/` <-- this folder contains the source code of the contract - - ... - - `web/` <-- this folder contains the web app source code, web app built with Rust Dioxus framework - - `build/` <-- the generated folder that contains the compiled web app binary with version + wasm - code - - `container/` <-- this folder contains the web contract container, a simple contract associated - with the web app, as the state. - - `src/` <-- this folder contains the source code of the web app - - ... - - `Makefile` <-- this file contains the build instructions for building and running the web app, and - the local node. + - `apps/` + - `freenet-email-app/` <-- here is located the email app + - `contracts/` <-- this folder contains the contract source code + - `inbox/` <-- this folder contains the email inbox contract + - `build/` <-- the generated folder that contains the compiled contract binary with version + wasm + code + - `src/` <-- this folder contains the source code of the contract + - ... + - `web/` <-- this folder contains the web app source code, web app built with Rust Dioxus framework + - `build/` <-- the generated folder that contains the compiled web app binary with version + wasm + code + - `container/` <-- this folder contains the web contract container, a simple contract associated + with the web app, as the state. + - `src/` <-- this folder contains the source code of the web app + - ... + - `Makefile` <-- this file contains the build instructions for building and running the web app, and + the local node. To build the email application, go to the `apps/freenet-email-app` folder and run the following command: @@ -131,22 +129,27 @@ make build ``` This command will compile the inbox and web contracts and the delegate, generate a binary files -inside the respective `build/locutus/` folders and publish the web contract. It generates the build folder if it doesn't +inside the respective `build/freenet/` folders and publish the web contract. It generates the build folder if it doesn't exist. -After building the app, what remains is to run the local node and the web app. To do that, run the following commanda: +After building the app, what remains is to run the local node and the web app. To do that, run the following command: 1. Run the local node: - ```bash - make run-node - ``` + ```bash + make run-node + ``` 2. Run the web app: - ```bash - make run-web - ``` + ```bash + make run-web + ``` During the development process, changes inside the web app will be automatically reloaded if it is running. +If you, instead, want to access the published app via the `build` command, go to your browser and write an URL like: +`http://localhost:50509/contract/web/5vavA8Wh7ZQRbqqNhtQvdszGFCrE9he67aLB4F3jGLws/` + +The hash may be different and you can get it when you run the build command. + ## Troubleshooting ### Freenet node throws an error when trying to find some contract or delegate @@ -155,4 +158,3 @@ If any of the delegates or contracts definition change, is necessary to rebuild Probably the contract or delegate hash has changed and the node is trying to find the old version. If the error persists, we recommend to make a clean build of all the delegates and contracts, and restart the node. - From eebcdc706e823277b064cba680c31407fb8ad725 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Wed, 27 Sep 2023 18:03:05 +0200 Subject: [PATCH 17/76] wip: fix app working --- Cargo.lock | 77 +++++----- Cargo.toml | 2 +- apps/freenet-email-app/Cargo.lock | 103 +++++++------ apps/freenet-email-app/Cargo.toml | 2 +- apps/freenet-email-app/README.md | 8 +- crates/core/Cargo.toml | 6 +- crates/core/src/bin/freenet.rs | 1 + crates/core/src/server/mod.rs | 1 - crates/fdev/Cargo.toml | 6 +- modules/antiflood-tokens/Cargo.lock | 142 +++++++++--------- modules/antiflood-tokens/Cargo.toml | 2 +- .../delegates/token-generator/Cargo.toml | 2 +- .../delegates/token-generator/src/lib.rs | 3 - .../delegates/token-generator/src/tests.rs | 2 +- ...ration_test.rs => integration_test.rs.bkp} | 0 .../antiflood-tokens/interfaces/src/lib.rs | 2 +- modules/identity-management/Cargo.lock | 42 +++--- modules/identity-management/Cargo.toml | 2 +- stdlib | 2 +- 19 files changed, 209 insertions(+), 196 deletions(-) rename modules/antiflood-tokens/delegates/token-generator/tests/{integration_test.rs => integration_test.rs.bkp} (100%) diff --git a/Cargo.lock b/Cargo.lock index 481c4a797..c3eb47808 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1475,7 +1475,7 @@ checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fdev" -version = "0.0.4" +version = "0.0.5" dependencies = [ "anyhow", "bincode", @@ -1494,7 +1494,7 @@ dependencies = [ "tar", "thiserror", "tokio", - "toml 0.8.0", + "toml 0.8.1", "tracing", "tracing-subscriber", "xz2", @@ -1546,13 +1546,12 @@ dependencies = [ [[package]] name = "flume" -version = "0.10.14" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ "futures-core", "futures-sink", - "pin-project", "spin 0.9.8", ] @@ -1573,7 +1572,7 @@ dependencies = [ [[package]] name = "freenet" -version = "0.0.4" +version = "0.0.5" dependencies = [ "anyhow", "arbitrary", @@ -1638,7 +1637,7 @@ dependencies = [ [[package]] name = "freenet-stdlib" -version = "0.0.5" +version = "0.0.6" dependencies = [ "arbitrary", "arrayvec", @@ -2671,9 +2670,9 @@ dependencies = [ [[package]] name = "libp2p-swarm" -version = "0.43.4" +version = "0.43.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0cf749abdc5ca1dce6296dc8ea0f012464dfcfd3ddd67ffc0cabd8241c4e1da" +checksum = "ab94183f8fc2325817835b57946deb44340c99362cd4606c0a5717299b2ba369" dependencies = [ "either", "fnv", @@ -4564,9 +4563,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -4756,9 +4755,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e58421b6bc416714d5115a2ca953718f6c621a51b68e4f4922aea5a4391a721" +checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33" dependencies = [ "sqlx-core", "sqlx-macros", @@ -4769,9 +4768,9 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4cef4251aabbae751a3710927945901ee1d97ee96d757f6880ebb9a79bfd53" +checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" dependencies = [ "ahash 0.8.3", "atoi", @@ -4812,9 +4811,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "208e3165167afd7f3881b16c1ef3f2af69fa75980897aac8874a0696516d12c2" +checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" dependencies = [ "proc-macro2", "quote", @@ -4825,9 +4824,9 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a4a8336d278c62231d87f24e8a7a74898156e34c1c18942857be2acb29c7dfc" +checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc" dependencies = [ "dotenvy", "either", @@ -4850,9 +4849,9 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca69bf415b93b60b80dc8fda3cb4ef52b2336614d8da2de5456cc942a110482" +checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" dependencies = [ "atoi", "base64 0.21.4", @@ -4892,9 +4891,9 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0db2df1b8731c3651e204629dd55e52adbae0462fa1bdcbed56a2302c18181e" +checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" dependencies = [ "atoi", "base64 0.21.4", @@ -4931,9 +4930,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4c21bf34c7cae5b283efb3ac1bcc7670df7561124dc2f8bdc0b59be40f79a2" +checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" dependencies = [ "atoi", "flume", @@ -5118,18 +5117,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.48" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.48" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", @@ -5288,9 +5287,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c226a7bba6d859b63c92c4b4fe69c5b6b72d0cb897dbc8e6012298e6154cb56e" +checksum = "1bc1433177506450fe920e46a4f9812d0c211f5dd556da10e731a0a3dfa151f0" dependencies = [ "indexmap 2.0.0", "serde", @@ -5310,9 +5309,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ff63e60a958cefbb518ae1fd6566af80d9d4be430a33f3723dfc47d1d411d95" +checksum = "ca676d9ba1a322c1b64eb8045a5ec5c0cfb0c9d08e15e9ff622589ad5221c8fe" dependencies = [ "indexmap 2.0.0", "serde", @@ -5828,9 +5827,9 @@ checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasm-encoder" -version = "0.33.1" +version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39de0723a53d3c8f54bed106cfbc0d06b3e4d945c5c5022115a61e3b29183ae" +checksum = "34180c89672b3e4825c3a8db4b61a674f1447afd5fe2445b2d22c3d8b6ea086c" dependencies = [ "leb128", ] @@ -5978,9 +5977,9 @@ dependencies = [ [[package]] name = "wast" -version = "65.0.1" +version = "65.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd8c1cbadf94a0b0d1071c581d3cfea1b7ed5192c79808dd15406e508dd0afb" +checksum = "a55a88724cf8c2c0ebbf32c8e8f4ac0d6aa7ba6d73a1cfd94b254aa8f894317e" dependencies = [ "leb128", "memchr", @@ -5990,9 +5989,9 @@ dependencies = [ [[package]] name = "wat" -version = "1.0.73" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3209e35eeaf483714f4c6be93f4a03e69aad5f304e3fa66afa7cb90fe1c8051f" +checksum = "d83e1a8d86d008adc7bafa5cf4332d448699a08fcf2a715a71fbb75e2c5ca188" dependencies = [ "wast", ] diff --git a/Cargo.toml b/Cargo.toml index 518338449..aaeee75d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ tracing = "0.1" tracing-subscriber = "0.3" wasmer = "4.2.0" -freenet-stdlib = "0.0.5" +freenet-stdlib = { path = "./stdlib/rust/", version = "0.0.6" } [profile.dev.package."*"] opt-level = 3 diff --git a/apps/freenet-email-app/Cargo.lock b/apps/freenet-email-app/Cargo.lock index 9336a3578..124fa2a08 100644 --- a/apps/freenet-email-app/Cargo.lock +++ b/apps/freenet-email-app/Cargo.lock @@ -131,9 +131,9 @@ dependencies = [ [[package]] name = "async-task" -version = "4.4.0" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" +checksum = "b9441c6b2fe128a7c2bf680a44c34d0df31ce09e5b7e401fcca3faa483dbc921" [[package]] name = "async-trait" @@ -172,9 +172,9 @@ dependencies = [ [[package]] name = "atomic-waker" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" @@ -267,17 +267,18 @@ dependencies = [ [[package]] name = "blocking" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" +checksum = "94c4ef1f913d78636d78d538eec1f18de81e481f44b1be0a81060090530846e1" dependencies = [ "async-channel", "async-lock", "async-task", - "atomic-waker", - "fastrand", + "fastrand 2.0.1", + "futures-io", "futures-lite", - "log", + "piper", + "tracing", ] [[package]] @@ -448,15 +449,14 @@ dependencies = [ [[package]] name = "cocoa-foundation" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "931d3837c286f56e3c58423ce4eba12d08db2374461a785c86f672b08b5650d6" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" dependencies = [ "bitflags", "block", "core-foundation", "core-graphics-types", - "foreign-types", "libc", "objc", ] @@ -479,9 +479,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" dependencies = [ "crossbeam-utils", ] @@ -1042,6 +1042,12 @@ dependencies = [ "instant", ] +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + [[package]] name = "fdeflate" version = "0.3.0" @@ -1200,9 +1206,9 @@ dependencies = [ [[package]] name = "freenet-stdlib" -version = "0.0.5" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb83503ad71922ceeaa56737c0a4a9da11520aaaf8ad6a89f43edb1154f02764" +checksum = "ebd193c150c4f7f9f44068ee7f5181d4c4374ceb1b37965e1edd968d3730f35a" dependencies = [ "arrayvec", "bincode", @@ -1294,7 +1300,7 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ - "fastrand", + "fastrand 1.9.0", "futures-core", "futures-io", "memchr", @@ -2377,9 +2383,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" +checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" [[package]] name = "parking_lot" @@ -2510,6 +2516,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand 2.0.1", + "futures-io", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -2954,9 +2971,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" dependencies = [ "serde", ] @@ -3104,9 +3121,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -3115,9 +3132,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -3187,9 +3204,9 @@ dependencies = [ [[package]] name = "smallbox" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4679d6eef28b85020158619fc09769de89e90886c5de7157587d87cb72648faa" +checksum = "d92359f97e6b417da4328a970cf04a044db104fbd57f7d72cb7ff665bb8806af" [[package]] name = "smallvec" @@ -3454,18 +3471,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.48" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.48" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", @@ -3484,9 +3501,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" +checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" dependencies = [ "deranged", "itoa 1.0.9", @@ -3497,15 +3514,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] @@ -3562,9 +3579,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2dbec703c26b00d74844519606ef15d09a7d6857860f84ad223dec002ddea2" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", @@ -3670,9 +3687,9 @@ dependencies = [ [[package]] name = "tungstenite" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e862a1c4128df0112ab625f55cd5c934bcb4312ba80b39ae4b4835a3fd58e649" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" dependencies = [ "byteorder", "bytes", @@ -3782,9 +3799,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "waker-fn" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" [[package]] name = "walkdir" diff --git a/apps/freenet-email-app/Cargo.toml b/apps/freenet-email-app/Cargo.toml index 70b6c3dd2..913d5fdee 100644 --- a/apps/freenet-email-app/Cargo.toml +++ b/apps/freenet-email-app/Cargo.toml @@ -20,5 +20,5 @@ serde = "1" serde_json = "1" once_cell = "1" -freenet-stdlib = { version = "0.0.5" } +freenet-stdlib = { version = "0.0.6" } freenet-aft-interface = { path = "../../modules/antiflood-tokens/interfaces" } diff --git a/apps/freenet-email-app/README.md b/apps/freenet-email-app/README.md index 80a406b16..2acc42a24 100644 --- a/apps/freenet-email-app/README.md +++ b/apps/freenet-email-app/README.md @@ -45,7 +45,7 @@ To build the delegate, go to the `identity-management` folder and run the follow make build ``` -This command will compile the delegate and generate a binary file inside the `build/locutus/` folder. It +This command will compile the delegate and generate a binary file inside the `build/freenet/` folder. It generates the build folder if it doesn't exist. In addition, the build command will generate: - `build/identity_management_code_hash` <-- this file contains the hash of the delegate's wasm code @@ -88,7 +88,7 @@ To build the Antiflood Token System, go to the `antiflood-tokens` folder and run make build ``` -This command will compile the contract and the delegate and generate a binary file inside the `build/locutus/` folder. +This command will compile the contract and the delegate and generate a binary file inside the `build/freenet/` folder. It generates the build folder if it doesn't exist. - `contracts/token-allocation-record/build/identity_management_code_hash` <-- this file contains the hash of the @@ -145,8 +145,8 @@ After building the app, what remains is to run the local node and the web app. T During the development process, changes inside the web app will be automatically reloaded if it is running. -If you, instead, want to access the published app via the `build` command, go to your browser and write an URL like: -`http://localhost:50509/contract/web/5vavA8Wh7ZQRbqqNhtQvdszGFCrE9he67aLB4F3jGLws/` +If you, instead, want to access the published app via the `build` command, go to your browser and write an URL like (with the node running): +`http://localhost:50509/contract/web/5zrr81Nbvk6PjkrXjXXFpDfrNZZvhx2JCc7BZTBHUKDo/` The hash may be different and you can get it when you run the build command. diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 47c0859ad..abca19737 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "freenet" -version = "0.0.4" +version = "0.0.5" edition = "2021" rust-version = "1.71.1" publish = true @@ -75,7 +75,7 @@ tracing-opentelemetry = { version = "0.21.0", optional = true } tracing-subscriber = { version = "0.3.16", optional = true } # internal deps -freenet-stdlib = { path = "../../stdlib/rust", version = "0.0.5", features = ["net", "archive"] } +freenet-stdlib = { workspace = true, features = ["net", "archive"] } nalgebra = "0.32.3" [dev-dependencies] @@ -84,7 +84,7 @@ arbitrary = { version = "1", features = ["derive"] } itertools = "0.11" pico-args = "0.5" statrs = "0.16.0" -freenet-stdlib = { path = "../../stdlib/rust", version = "0.0.5", features = ["testing", "net"] } +freenet-stdlib = { workspace = true, features = ["testing", "net"] } [features] default = ["websocket", "rocks_db", "trace"] diff --git a/crates/core/src/bin/freenet.rs b/crates/core/src/bin/freenet.rs index 34d9997a4..aef4f7f60 100644 --- a/crates/core/src/bin/freenet.rs +++ b/crates/core/src/bin/freenet.rs @@ -16,6 +16,7 @@ async fn run(config: NodeConfig) -> Result<(), DynError> { async fn run_local(config: NodeConfig) -> Result<(), DynError> { let port = config.port; let ip = config.address; + freenet::config::Config::set_op_mode(OperationMode::Local); let executor = Executor::from_config(config).await?; let socket: SocketAddr = (ip, port).into(); freenet::server::local_node::run_local_node(executor, socket).await diff --git a/crates/core/src/server/mod.rs b/crates/core/src/server/mod.rs index c46883ca9..fe3adcbf8 100644 --- a/crates/core/src/server/mod.rs +++ b/crates/core/src/server/mod.rs @@ -70,7 +70,6 @@ pub mod local_node { mut executor: Executor, socket: SocketAddr, ) -> Result<(), DynError> { - crate::config::Config::set_op_mode(crate::local_node::OperationMode::Local); match socket.ip() { IpAddr::V4(ip) if !ip.is_loopback() => { return Err(format!("invalid ip: {ip}, expecting localhost").into()) diff --git a/crates/fdev/Cargo.toml b/crates/fdev/Cargo.toml index dd8f1d6ec..cff6dec65 100644 --- a/crates/fdev/Cargo.toml +++ b/crates/fdev/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fdev" -version = "0.0.4" +version = "0.0.5" edition = "2021" rust-version = "1.71.1" publish = true @@ -31,5 +31,5 @@ toml = { version = "0.8", features = ["default", "preserve_order"] } xz2 = "0.1" # internal -freenet = { path = "../core", version = "0.0.4" } -freenet-stdlib = { path = "../../stdlib/rust", version = "0.0.5" } +freenet = { path = "../core", version = "0.0.5" } +freenet-stdlib = { workspace = true } diff --git a/modules/antiflood-tokens/Cargo.lock b/modules/antiflood-tokens/Cargo.lock index 014ba0ae6..bf70303ea 100644 --- a/modules/antiflood-tokens/Cargo.lock +++ b/modules/antiflood-tokens/Cargo.lock @@ -297,9 +297,9 @@ checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" [[package]] name = "atomic-waker" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" @@ -671,9 +671,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.4" +version = "4.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136" +checksum = "824956d0dca8334758a5b7f7e50518d66ea319330cbceedcf76905c2f6ab30e3" dependencies = [ "clap_builder", "clap_derive", @@ -681,9 +681,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.4" +version = "4.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56" +checksum = "122ec64120a49b4563ccaedcbea7818d069ed8e9aa6d829b82d8a4128936b2ab" dependencies = [ "anstream", "anstyle", @@ -717,9 +717,9 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "concurrent-queue" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" dependencies = [ "crossbeam-utils", ] @@ -1456,7 +1456,7 @@ dependencies = [ [[package]] name = "freenet" -version = "0.0.4" +version = "0.0.5" dependencies = [ "anyhow", "arrayvec", @@ -1477,7 +1477,7 @@ dependencies = [ "dashmap", "directories", "either", - "freenet-stdlib 0.0.5", + "freenet-stdlib 0.0.6", "futures", "itertools", "libp2p", @@ -1512,7 +1512,7 @@ dependencies = [ "bincode", "bs58", "chrono", - "freenet-stdlib 0.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "freenet-stdlib 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "rand", "rsa", "serde", @@ -1543,7 +1543,7 @@ dependencies = [ [[package]] name = "freenet-stdlib" -version = "0.0.5" +version = "0.0.6" dependencies = [ "arrayvec", "bincode", @@ -1575,9 +1575,9 @@ dependencies = [ [[package]] name = "freenet-stdlib" -version = "0.0.5" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb83503ad71922ceeaa56737c0a4a9da11520aaaf8ad6a89f43edb1154f02764" +checksum = "ebd193c150c4f7f9f44068ee7f5181d4c4374ceb1b37965e1edd968d3730f35a" dependencies = [ "arbitrary", "arrayvec", @@ -1607,7 +1607,7 @@ dependencies = [ "bincode", "chrono", "freenet-aft-interface", - "freenet-stdlib 0.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "freenet-stdlib 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "rsa", "serde", "serde_json", @@ -1624,7 +1624,7 @@ dependencies = [ "chrono", "freenet", "freenet-aft-interface", - "freenet-stdlib 0.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "freenet-stdlib 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "once_cell", "rand", "rand_chacha", @@ -2076,9 +2076,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "ad227c3af19d4914570ad36d30409928b75967c298feb9ea1969db3a610bb14e" dependencies = [ "equivalent", "hashbrown 0.14.0", @@ -2366,9 +2366,9 @@ dependencies = [ [[package]] name = "libp2p-core" -version = "0.40.0" +version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef7dd7b09e71aac9271c60031d0e558966cdb3253ba0308ab369bb2de80630d0" +checksum = "dd44289ab25e4c9230d9246c475a22241e301b23e8f4061d3bdef304a1a99713" dependencies = [ "either", "fnv", @@ -2521,9 +2521,9 @@ dependencies = [ [[package]] name = "libp2p-ping" -version = "0.43.0" +version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd5ee3270229443a2b34b27ed0cb7470ef6b4a6e45e54e89a8771fa683bab48" +checksum = "e702d75cd0827dfa15f8fd92d15b9932abe38d10d21f47c50438c71dd1b5dae3" dependencies = [ "either", "futures", @@ -2580,9 +2580,9 @@ dependencies = [ [[package]] name = "libp2p-swarm" -version = "0.43.3" +version = "0.43.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28016944851bd73526d3c146aabf0fa9bbe27c558f080f9e5447da3a1772c01a" +checksum = "ab94183f8fc2325817835b57946deb44340c99362cd4606c0a5717299b2ba369" dependencies = [ "either", "fnv", @@ -2778,9 +2778,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "matchit" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" @@ -3307,9 +3307,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" +checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" [[package]] name = "parking_lot" @@ -3388,9 +3388,9 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pest" -version = "2.7.3" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a4d085fd991ac8d5b05a147b437791b4260b76326baf0fc60cf7c9c27ecd33" +checksum = "c022f1e7b65d6a24c0dbbd5fb344c66881bc01f3e5ae74a1c8100f2f985d98a4" dependencies = [ "memchr", "thiserror", @@ -3399,9 +3399,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.3" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bee7be22ce7918f641a33f08e3f43388c7656772244e2bbb2477f44cc9021a" +checksum = "35513f630d46400a977c4cb58f78e1bfbe01434316e60c37d27b9ad6139c66d8" dependencies = [ "pest", "pest_generator", @@ -3409,9 +3409,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.3" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1511785c5e98d79a05e8a6bc34b4ac2168a0e3e92161862030ad84daa223141" +checksum = "bc9fc1b9e7057baba189b5c626e2d6f40681ae5b6eb064dc7c7834101ec8123a" dependencies = [ "pest", "pest_meta", @@ -3422,9 +3422,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.3" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42f0394d3123e33353ca5e1e89092e533d2cc490389f2bd6131c43c634ebc5f" +checksum = "1df74e9e7ec4053ceb980e7c0c8bd3594e977fde1af91daba9c928e8e8c6708d" dependencies = [ "once_cell", "pest", @@ -3910,9 +3910,9 @@ dependencies = [ [[package]] name = "rend" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581008d2099240d37fb08d77ad713bcaec2c4d89d50b5b21a8bb1996bbab68ab" +checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd" dependencies = [ "bytecheck", ] @@ -4099,9 +4099,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.5" +version = "0.101.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" dependencies = [ "ring", "untrusted", @@ -4178,9 +4178,9 @@ checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6" [[package]] name = "semver" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" dependencies = [ "serde", ] @@ -4269,7 +4269,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.0.0", + "indexmap 2.0.1", "serde", "serde_json", "serde_with_macros", @@ -4290,9 +4290,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -4301,9 +4301,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -4312,9 +4312,9 @@ dependencies = [ [[package]] name = "sharded-slab" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +checksum = "c1b21f559e07218024e7e9f90f96f601825397de0e25420135f7f952453fed0b" dependencies = [ "lazy_static", ] @@ -4588,18 +4588,18 @@ checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" [[package]] name = "thiserror" -version = "1.0.48" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.48" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", @@ -4640,9 +4640,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" +checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" dependencies = [ "deranged", "itoa", @@ -4653,15 +4653,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] @@ -4723,9 +4723,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2dbec703c26b00d74844519606ef15d09a7d6857860f84ad223dec002ddea2" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", @@ -4961,9 +4961,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e862a1c4128df0112ab625f55cd5c934bcb4312ba80b39ae4b4835a3fd58e649" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" dependencies = [ "byteorder", "bytes", @@ -5134,9 +5134,9 @@ checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] name = "waker-fn" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" [[package]] name = "walkdir" @@ -5250,9 +5250,9 @@ checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasm-encoder" -version = "0.33.1" +version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39de0723a53d3c8f54bed106cfbc0d06b3e4d945c5c5022115a61e3b29183ae" +checksum = "34180c89672b3e4825c3a8db4b61a674f1447afd5fe2445b2d22c3d8b6ea086c" dependencies = [ "leb128", ] @@ -5400,9 +5400,9 @@ dependencies = [ [[package]] name = "wast" -version = "65.0.1" +version = "65.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd8c1cbadf94a0b0d1071c581d3cfea1b7ed5192c79808dd15406e508dd0afb" +checksum = "a55a88724cf8c2c0ebbf32c8e8f4ac0d6aa7ba6d73a1cfd94b254aa8f894317e" dependencies = [ "leb128", "memchr", @@ -5412,9 +5412,9 @@ dependencies = [ [[package]] name = "wat" -version = "1.0.73" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3209e35eeaf483714f4c6be93f4a03e69aad5f304e3fa66afa7cb90fe1c8051f" +checksum = "d83e1a8d86d008adc7bafa5cf4332d448699a08fcf2a715a71fbb75e2c5ca188" dependencies = [ "wast", ] diff --git a/modules/antiflood-tokens/Cargo.toml b/modules/antiflood-tokens/Cargo.toml index bbdc2ab32..2de61b3d1 100644 --- a/modules/antiflood-tokens/Cargo.toml +++ b/modules/antiflood-tokens/Cargo.toml @@ -6,7 +6,7 @@ resolver = "2" bincode = { version = "1" } bs58 = "0.5" chrono = { version = "0.4.23", default-features = false } -freenet-stdlib = { version = "0.0.5" } +freenet-stdlib = { version = "0.0.6" } rsa = { version = "0.9.2", default-features = false, features = ["serde", "pem"] } serde = { version = "1" } serde_json = { version = "1" } diff --git a/modules/antiflood-tokens/delegates/token-generator/Cargo.toml b/modules/antiflood-tokens/delegates/token-generator/Cargo.toml index 66999778c..6c51293fb 100644 --- a/modules/antiflood-tokens/delegates/token-generator/Cargo.toml +++ b/modules/antiflood-tokens/delegates/token-generator/Cargo.toml @@ -27,7 +27,7 @@ chacha20poly1305 = "0.10" rand = { version = "0.8", features = ["std"] } rand_chacha = { version = "0.3" } freenet-stdlib = { workspace = true, features = ["testing"] } -freenet = { path = "../../../../crates/core", version = "0.0.4" } +freenet = { path = "../../../../crates/core", version = "0.0.5" } tracing-subscriber = { version = "0.3.16", features = ["env-filter", "fmt"] } [lib] diff --git a/modules/antiflood-tokens/delegates/token-generator/src/lib.rs b/modules/antiflood-tokens/delegates/token-generator/src/lib.rs index 022f0e441..8a5d383e0 100644 --- a/modules/antiflood-tokens/delegates/token-generator/src/lib.rs +++ b/modules/antiflood-tokens/delegates/token-generator/src/lib.rs @@ -62,9 +62,6 @@ impl DelegateInterface for TokenDelegate { InboundDelegateMsg::GetSecretResponse(GetSecretResponse { .. }) => Err( DelegateError::Other("unexpected message type: get secret".into()), ), - InboundDelegateMsg::RandomBytes(_) => Err(DelegateError::Other( - "unexpected message type: radom bytes".into(), - )), InboundDelegateMsg::GetSecretRequest(_) => unreachable!(), } } diff --git a/modules/antiflood-tokens/delegates/token-generator/src/tests.rs b/modules/antiflood-tokens/delegates/token-generator/src/tests.rs index f3dabe996..149bb92b9 100644 --- a/modules/antiflood-tokens/delegates/token-generator/src/tests.rs +++ b/modules/antiflood-tokens/delegates/token-generator/src/tests.rs @@ -12,7 +12,7 @@ mod token_assignment { .unwrap() .and_hms_opt(0, 0, 0) .unwrap(); - DateTime::::from_utc(naive, Utc) + DateTime::::from_naive_utc_and_offset(naive, Utc) } const TEST_TIER: Tier = Tier::Day1; diff --git a/modules/antiflood-tokens/delegates/token-generator/tests/integration_test.rs b/modules/antiflood-tokens/delegates/token-generator/tests/integration_test.rs.bkp similarity index 100% rename from modules/antiflood-tokens/delegates/token-generator/tests/integration_test.rs rename to modules/antiflood-tokens/delegates/token-generator/tests/integration_test.rs.bkp diff --git a/modules/antiflood-tokens/interfaces/src/lib.rs b/modules/antiflood-tokens/interfaces/src/lib.rs index e3f033f36..da7fd6d9b 100644 --- a/modules/antiflood-tokens/interfaces/src/lib.rs +++ b/modules/antiflood-tokens/interfaces/src/lib.rs @@ -314,7 +314,7 @@ fn get_date(y: i32, m: u32, d: u32) -> DateTime { .unwrap() .and_hms_opt(0, 0, 0) .unwrap(); - DateTime::::from_utc(naive, Utc) + DateTime::::from_naive_utc_and_offset(naive, Utc) } #[non_exhaustive] diff --git a/modules/identity-management/Cargo.lock b/modules/identity-management/Cargo.lock index 00f2eb3f6..cf8e849bb 100644 --- a/modules/identity-management/Cargo.lock +++ b/modules/identity-management/Cargo.lock @@ -350,9 +350,9 @@ dependencies = [ [[package]] name = "freenet-stdlib" -version = "0.0.5" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb83503ad71922ceeaa56737c0a4a9da11520aaaf8ad6a89f43edb1154f02764" +checksum = "ebd193c150c4f7f9f44068ee7f5181d4c4374ceb1b37965e1edd968d3730f35a" dependencies = [ "arrayvec", "bincode", @@ -587,9 +587,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "ad227c3af19d4914570ad36d30409928b75967c298feb9ea1969db3a610bb14e" dependencies = [ "equivalent", "hashbrown 0.14.0", @@ -874,9 +874,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" dependencies = [ "serde", ] @@ -931,7 +931,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.0.0", + "indexmap 2.0.1", "serde", "serde_json", "serde_with_macros", @@ -962,9 +962,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -973,9 +973,9 @@ dependencies = [ [[package]] name = "sharded-slab" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +checksum = "c1b21f559e07218024e7e9f90f96f601825397de0e25420135f7f952453fed0b" dependencies = [ "lazy_static", ] @@ -1040,18 +1040,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.48" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.48" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", @@ -1070,9 +1070,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" +checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" dependencies = [ "deranged", "itoa", @@ -1083,15 +1083,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] diff --git a/modules/identity-management/Cargo.toml b/modules/identity-management/Cargo.toml index 47302c3af..185a32542 100644 --- a/modules/identity-management/Cargo.toml +++ b/modules/identity-management/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] p384 = { version = "0.13", default-features = false, features = ["serde", "pem", "pkcs8", "arithmetic"] } -freenet-stdlib = { version = "0.0.5" } +freenet-stdlib = { version = "0.0.6" } serde = "1" serde_json = "1" diff --git a/stdlib b/stdlib index 6cd278094..5a8080f1a 160000 --- a/stdlib +++ b/stdlib @@ -1 +1 @@ -Subproject commit 6cd278094b77f2cd11c0ca2c7e1458c6e78a5cf8 +Subproject commit 5a8080f1a2b718c40728d257d5f2d474cc1d5a91 From 5a91db274893f5f0494c6df6197d6fac3fe4279c Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Thu, 28 Sep 2023 10:16:57 +0200 Subject: [PATCH 18/76] Messaging app runs --- Cargo.lock | 10 ++++++---- Cargo.toml | 3 ++- crates/core/Cargo.toml | 2 +- crates/fdev/Cargo.toml | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3eb47808..15b8632ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1475,7 +1475,7 @@ checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fdev" -version = "0.0.5" +version = "0.0.6" dependencies = [ "anyhow", "bincode", @@ -1572,7 +1572,7 @@ dependencies = [ [[package]] name = "freenet" -version = "0.0.5" +version = "0.0.6" dependencies = [ "anyhow", "arbitrary", @@ -1629,6 +1629,8 @@ dependencies = [ [[package]] name = "freenet-macros" version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9fac5b43bee97b6ee49a376ca9e26eedf8bd581efcb176e4a534304f8b4279" dependencies = [ "proc-macro2", "quote", @@ -1638,6 +1640,8 @@ dependencies = [ [[package]] name = "freenet-stdlib" version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd193c150c4f7f9f44068ee7f5181d4c4374ceb1b37965e1edd968d3730f35a" dependencies = [ "arbitrary", "arrayvec", @@ -1651,7 +1655,6 @@ dependencies = [ "futures", "js-sys", "once_cell", - "rand", "semver", "serde", "serde-wasm-bindgen 0.6.0", @@ -1665,7 +1668,6 @@ dependencies = [ "tracing", "tracing-subscriber", "wasm-bindgen", - "wasmer", "web-sys", "xz2", ] diff --git a/Cargo.toml b/Cargo.toml index aaeee75d9..ad4f03f87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,8 @@ tracing = "0.1" tracing-subscriber = "0.3" wasmer = "4.2.0" -freenet-stdlib = { path = "./stdlib/rust/", version = "0.0.6" } +# freenet-stdlib = { path = "./stdlib/rust/", version = "0.0.7" } +freenet-stdlib = { version = "0.0.6" } [profile.dev.package."*"] opt-level = 3 diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index abca19737..8717e59f7 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "freenet" -version = "0.0.5" +version = "0.0.6" edition = "2021" rust-version = "1.71.1" publish = true diff --git a/crates/fdev/Cargo.toml b/crates/fdev/Cargo.toml index cff6dec65..54ffc8b47 100644 --- a/crates/fdev/Cargo.toml +++ b/crates/fdev/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fdev" -version = "0.0.5" +version = "0.0.6" edition = "2021" rust-version = "1.71.1" publish = true @@ -31,5 +31,5 @@ toml = { version = "0.8", features = ["default", "preserve_order"] } xz2 = "0.1" # internal -freenet = { path = "../core", version = "0.0.5" } +freenet = { path = "../core", version = "0.0.6" } freenet-stdlib = { workspace = true } From 8072ca6d6df643c05e6564ee31620904afc1211c Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Thu, 28 Sep 2023 16:29:02 +0200 Subject: [PATCH 19/76] Package wasm with debug symbols if set --- crates/fdev/src/build.rs | 39 ++++++++++++++++++++++----------------- crates/fdev/src/config.rs | 6 ++++++ 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/crates/fdev/src/build.rs b/crates/fdev/src/build.rs index 8930ed50f..827f44a3f 100644 --- a/crates/fdev/src/build.rs +++ b/crates/fdev/src/build.rs @@ -31,34 +31,34 @@ pub fn build_package(cli_config: BuildToolCliConfig, cwd: &Path) -> Result<(), D fn compile_rust_wasm_lib(cli_config: &BuildToolCliConfig, work_dir: &Path) -> Result<(), DynError> { let package_type = cli_config.package_type; - const RUST_TARGET_ARGS: &[&str] = &["build", "--release", "--lib", "--target"]; + const RUST_TARGET_ARGS: &[&str] = &["build", "--lib", "--target"]; + let release: &[&str] = if cli_config.debug { + &[] + } else { + &["--release"] + }; let target = WASM_TARGET; + let cmd_args = cli_config + .features + .as_ref() + .iter() + .flat_map(|x| ["--features", x.as_str()]) + .chain(release.iter().copied()) + .collect::>(); use std::io::IsTerminal; let cmd_args = if std::io::stdout().is_terminal() && std::io::stderr().is_terminal() { RUST_TARGET_ARGS .iter() .copied() .chain([target, "--color", "always"]) - .chain( - cli_config - .features - .as_ref() - .iter() - .flat_map(|x| ["--features", x.as_str()]), - ) + .chain(cmd_args) .collect::>() } else { RUST_TARGET_ARGS .iter() .copied() .chain([target]) - .chain( - cli_config - .features - .as_ref() - .iter() - .flat_map(|x| ["--features", x.as_str()]), - ) + .chain(cmd_args) .collect::>() }; @@ -79,7 +79,7 @@ fn compile_rust_wasm_lib(cli_config: &BuildToolCliConfig, work_dir: &Path) -> Re fn get_out_lib( work_dir: &Path, - _cli_config: &BuildToolCliConfig, + cli_config: &BuildToolCliConfig, ) -> Result<(String, PathBuf), DynError> { const ERR: &str = "Cargo.toml definition incorrect"; @@ -100,6 +100,11 @@ fn get_out_lib( .as_str() .ok_or_else(|| Error::MissConfiguration(ERR.into()))? .replace('-', "_"); + let opt_dir = if !cli_config.debug { + "release" + } else { + "debug" + }; let output_lib = env::var("CARGO_TARGET_DIR") .map_err(|e| { println!("Missing environment variable `CARGO_TARGET_DIR"); @@ -107,7 +112,7 @@ fn get_out_lib( })? .parse::()? .join(target) - .join("release") + .join(opt_dir) .join(&package_name) .with_extension("wasm"); Ok((package_name, output_lib)) diff --git a/crates/fdev/src/config.rs b/crates/fdev/src/config.rs index 8d1554646..f1d63c703 100644 --- a/crates/fdev/src/config.rs +++ b/crates/fdev/src/config.rs @@ -105,8 +105,13 @@ pub struct BuildToolCliConfig { #[arg(long, value_parser = parse_version, default_value_t=Version::new(0, 0, 1))] pub(crate) version: Version, + /// Output object type. #[arg(long, value_enum, default_value_t=PackageType::default())] pub(crate) package_type: PackageType, + + /// Compile in debug mode instead of release. + #[arg(long)] + pub(crate) debug: bool, } #[derive(Default, Debug, Clone, Copy, ValueEnum)] @@ -131,6 +136,7 @@ impl Default for BuildToolCliConfig { features: None, version: Version::new(0, 0, 1), package_type: PackageType::default(), + debug: false, } } } From 7aa8868f78eeae7f561f95ad4f92eb72a7226f1c Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Fri, 29 Sep 2023 10:28:49 +0200 Subject: [PATCH 20/76] Fix issues with packaging webapps --- Cargo.lock | 9 +- Cargo.toml | 4 +- apps/freenet-microblogging/Makefile | 25 +--- apps/freenet-microblogging/web/package.json | 2 +- apps/freenet-microblogging/web/src/index.ts | 14 +- crates/core/Cargo.toml | 6 +- .../core/examples/freenet_microblogging_posts | Bin 230414 -> 0 bytes .../freenet_microblogging_posts_state | 10 -- .../core/examples/freenet_microblogging_web | Bin 78531 -> 0 bytes .../examples/freenet_microblogging_web_state | Bin 393696 -> 0 bytes crates/core/src/server/app_packaging.rs | 125 ++++++++++++++++++ crates/core/src/server/mod.rs | 3 + crates/core/src/server/path_handlers.rs | 7 +- crates/fdev/src/build.rs | 9 +- stdlib | 2 +- 15 files changed, 164 insertions(+), 52 deletions(-) delete mode 100644 crates/core/examples/freenet_microblogging_posts delete mode 100644 crates/core/examples/freenet_microblogging_posts_state delete mode 100644 crates/core/examples/freenet_microblogging_web delete mode 100644 crates/core/examples/freenet_microblogging_web_state create mode 100644 crates/core/src/server/app_packaging.rs diff --git a/Cargo.lock b/Cargo.lock index 15b8632ac..8b338aad2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1583,6 +1583,7 @@ dependencies = [ "bincode", "blake3", "bs58", + "byteorder", "bytes", "chacha20poly1305 0.10.1", "chrono", @@ -1615,6 +1616,7 @@ dependencies = [ "sqlx", "statrs", "stretto", + "tar", "thiserror", "tokio", "tower-http", @@ -1624,6 +1626,7 @@ dependencies = [ "unsigned-varint", "uuid", "wasmer", + "xz2", ] [[package]] @@ -1639,9 +1642,9 @@ dependencies = [ [[package]] name = "freenet-stdlib" -version = "0.0.6" +version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd193c150c4f7f9f44068ee7f5181d4c4374ceb1b37965e1edd968d3730f35a" +checksum = "6a408ccb448697333c9e3f8a935976c223db980d2fb6a076ae7c89851c6ed17a" dependencies = [ "arbitrary", "arrayvec", @@ -1661,7 +1664,6 @@ dependencies = [ "serde_bytes", "serde_json", "serde_with", - "tar", "thiserror", "tokio", "tokio-tungstenite", @@ -1669,7 +1671,6 @@ dependencies = [ "tracing-subscriber", "wasm-bindgen", "web-sys", - "xz2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ad4f03f87..3957663a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,8 +21,8 @@ tracing = "0.1" tracing-subscriber = "0.3" wasmer = "4.2.0" -# freenet-stdlib = { path = "./stdlib/rust/", version = "0.0.7" } -freenet-stdlib = { version = "0.0.6" } +# freenet-stdlib = { path = "./stdlib/rust/", version = "0.0.8" } +freenet-stdlib = { version = "0.0.7" } [profile.dev.package."*"] opt-level = 3 diff --git a/apps/freenet-microblogging/Makefile b/apps/freenet-microblogging/Makefile index f981e6280..e1b30e6c7 100644 --- a/apps/freenet-microblogging/Makefile +++ b/apps/freenet-microblogging/Makefile @@ -11,10 +11,10 @@ $(error CARGO_TARGET_DIR is not set) endif build: \ - webapp \ posts \ - publish-webapp \ - publish-posts + publish-posts \ + webapp \ + publish-webapp node: \ build-tool \ @@ -22,39 +22,26 @@ node: \ build-tool: cd $(FREENET_DIR)/crates/core && - cargo install --path --force $(FREENET_DIR)/crates/core - cargo install --path --force $(FREENET_DIR)/crates/fdev + cargo install --force --path $(FREENET_DIR)/crates/core + cargo install --force --path $(FREENET_DIR)/crates/fdev webapp: cd $(WEB_DIR) - npm i --force freenet-stdlib + npm i --force @freenetorg/freenet-stdlib npm run build - fdev build - cp ./build/freenet/freenet_microblogging_web ../../../crates/core/examples/ - cp ./build/freenet/contract-state ../../../crates/core/examples/freenet_microblogging_web_state publish-webapp: cd $(WEB_DIR) - fdev publish --code build/freenet/freenet_microblogging_web contract --state build/freenet/contract-state posts: cd $(POSTS_DIR) - fdev build - cp ./build/freenet/freenet_microblogging_posts ../../../../crates/core/examples/ - cp ./build/freenet/contract-state ../../../../crates/core/examples/freenet_microblogging_posts_state publish-posts: cd $(POSTS_DIR) - fdev publish --code build/freenet/freenet_microblogging_posts contract --state build/freenet/contract-state run-node: RUST_BACKTRACE=1 RUST_LOG=freenet=debug,locutus_core=debug,locutus_node=debug,info freenet local - -run: - cd $(FREENET_DIR) - cd crates/core/examples - cargo run --example contract_browsing diff --git a/apps/freenet-microblogging/web/package.json b/apps/freenet-microblogging/web/package.json index eacd1c714..4dda17a2e 100644 --- a/apps/freenet-microblogging/web/package.json +++ b/apps/freenet-microblogging/web/package.json @@ -1,7 +1,7 @@ { "dependencies": { + "@freenetorg/freenet-stdlib": "^0.0.8", "bootstrap": "5.3.1", - "freenet-stdlib": "file:../../../stdlib/typescript/dist/pack/freenet-stdlib.tgz", "module-alias": "^2.2.2" }, "main": "src/index.ts", diff --git a/apps/freenet-microblogging/web/src/index.ts b/apps/freenet-microblogging/web/src/index.ts index de4d923fd..2dd006b3c 100644 --- a/apps/freenet-microblogging/web/src/index.ts +++ b/apps/freenet-microblogging/web/src/index.ts @@ -12,10 +12,10 @@ import { UpdateData, DeltaUpdate, DelegateResponse, -} from "freenet-stdlib/websocket-interface"; +} from "@freenetorg/freenet-stdlib"; +import { UpdateDataType } from "@freenetorg/freenet-stdlib/common"; import "./scss/styles.scss"; -import { UpdateDataType } from "freenet-stdlib/common"; // import * as bootstrap from "bootstrap"; @@ -29,7 +29,7 @@ function getDocument(): Document { const DOCUMENT: Document = getDocument(); -const MODEL_CONTRACT = "Hz1TGDBXtD6c1E74shUWMm9EdXjDDbPY1JxdTZsK2xwc"; +const MODEL_CONTRACT = "AgWvW6kwUpMfZcuzSrVddzgfMK2uPPew2UtDCdob9bkj"; const KEY = ContractKey.fromInstanceId(MODEL_CONTRACT); function getState(hostResponse: GetResponse) { @@ -76,7 +76,7 @@ async function sendUpdate() { ); const update = new UpdateData(UpdateDataType.DeltaUpdate, delta); let updateRequest = new UpdateRequest(KEY, update); - await locutusApi.update(updateRequest); + await freenetApi.update(updateRequest); } } @@ -126,7 +126,7 @@ async function subscribeToUpdates() { KEY, new Array() ); - await locutusApi.subscribe(subscribe_request); + await freenetApi.subscribe(subscribe_request); } const handler = { @@ -146,14 +146,14 @@ const handler = { }; const API_URL = new URL(`ws://${location.host}/contract/command`); -const locutusApi = new FreenetWsApi(API_URL, handler); +const freenetApi = new FreenetWsApi(API_URL, handler); async function loadState() { const key = ContractKey.fromInstanceId(MODEL_CONTRACT); const fetchContract = false; const getRequest: GetRequest = new GetRequest(key, fetchContract); - await locutusApi.get(getRequest); + await freenetApi.get(getRequest); } window.addEventListener("load", function (_ev: Event) { diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 8717e59f7..a93e84875 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -21,6 +21,7 @@ arrayvec = { workspace = true } axum = { version = "0.6", default-features = false, features = ["ws", "tower-log", "matched-path", "headers", "query", "http1"] } bincode = "1" blake3 = { workspace = true } +byteorder = "1" bytes = "1" bs58 = "0.5" chacha20poly1305 = { workspace = true } @@ -66,6 +67,8 @@ ordered-float = "3.9.1" notify = "6" wasmer = { workspace = true, features = [ "sys"] } chrono = { workspace = true } +tar = { version = "0.4.38" } +xz2 = { version = "0.1" } # Tracing deps tracing = { version = "0.1", optional = true } @@ -75,8 +78,7 @@ tracing-opentelemetry = { version = "0.21.0", optional = true } tracing-subscriber = { version = "0.3.16", optional = true } # internal deps -freenet-stdlib = { workspace = true, features = ["net", "archive"] } -nalgebra = "0.32.3" +freenet-stdlib = { workspace = true, features = ["net"] } [dev-dependencies] tracing = "0.1" diff --git a/crates/core/examples/freenet_microblogging_posts b/crates/core/examples/freenet_microblogging_posts deleted file mode 100644 index 1237850b9dc13281fd6855386cca06ebb32fd1db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 230414 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 z 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 R_9Gu};dun`8Q@F6{{j6M-2eap diff --git a/crates/core/examples/freenet_microblogging_web_state b/crates/core/examples/freenet_microblogging_web_state deleted file mode 100644 index cf40dd146ae2979a278eac97f65dc5dc8973700a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 393696 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

T30D%u!8gNXo4F86pZ-jPLRF`R>9Q|%K1SSwO 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�Hq#nq``Z3%&?^~ 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

    &SHEf&0 zjg%ysmBPsGS{bV>uSrW4ePfHU#v#9C5hPFjfY-0teat9M$d#?gTN$v);LE;y%WiGX z65TLT%Rvv1)+4KUwEF?rY~xci-g#>SZWK zAFj>=g@NE)33pi{Re~qVWq;KSIN70V?+5hnIM0vqJ+3$96e^8SpjZI8JXw>oqc5NU zjV8OVL7JiQMxmKOPBGYWf1D9{)}k;c?+>dm9Y`9!6IIK_G2cR4IP_?v{(Hr=jR6iG z$ISvt32__`hCOAuN;BxwA(aQuUaQcZMBPJ<*_&oP?}R)iFnT?8ypn)9A2DhamG~7x;8PAXf_vpNd8&;Sy&Oq;|-cDcd}+%ZCD&E0oNp^b`J7LwMa z0=36!+cvmofp6FEeNzmz^^)Pm!n(vWf73<`$#+MIwuEkF$4S1!-ZM4eE_?fLHhLqTN^ zP8K6$LY&;PD5b9)<4R!42cK;Y7=BeS*bcl(XaMm=hTgo$@?>nqA-AtO*>nk}42Dt% zoc=cN>QQPpA5Xcg#*)|E4LD1I6pS@FU$6P;kw}+12%{422F6sE^RxbEnVxAag;vI~ zlmy_dTdiw>iY(IOCQ?;qB=Dic?e9U)TLHYJTJ25CKMVN$JW`hCJ2!|flSkn15RrZF3vJbM zD1|>B<4nPT<8IvJJUrFtUG* z+O(p<|K@Q*ulJ!zbTP}(Ysc*Hd(J{0PLdX!azzS10pbq&h#x`5I#@1Ig(f#=ZxKm5 z3ICWX=PziQF`ye&n6#PHVMEniFI&t%zMee>qVI@>W%9o$nj~&9TgW99sDy+>1~3Sz z4+QF;@#>*2t;l#x6ou}#Lrr;7^?Wdir8V;XUG!=RC7Jq|jDkAUF!ug=m@C0jKrXkk z$oo*|5XQhf0Kq~7_u0jAW%X|YA?>56&^Vx$Hit>8fOfjixv;XmfcOQZ1Icjdbr=+B zgKFpCY_k`d#Z1rP4byHr!rcoPdQw4_j`gr_torzl*z3z_rnS zyF;^Jc4T<_G&tJxRxFXegz(hOg3>>Nh0|8O^av`Ob+|4#;fw{B9*Cyq$?*wNZJ24K zr6>BggF}Y&-9Ct{tLs>9hL$qJ=cHAKfovbbTB#?#xwS|XP9E&SvYIZfyPPwXHIT7= zo*1^>a-fol*6HUu^hk&wkAnZBZv(`tAhE#51XARbUkPk}J7uvmNq0;`+SOd11?(OE z#nYT7^RYCE2-v2RrA?)t!v__>*cs&R{;$IExMxDj=Qauw35;D6Su^DZNj6uwz1MFurR+Nt<`jRzvG>|i*YiBajr}(x`;`;y(~E!=G9bP(rx$rU2`zb0$E*70EL1@JnCkt4$!fwO3Q=*E{^zzVErDOP(<#+*(3^Q zl#NW}fOzQ^r{@(yx6eg_Sud0KZ-L5q4`cUQ&3pClLY^CG>@IS<%WWJDVeHwmLcM`@ zYDx9Xe5`q1tiv6CzLpS$csD>80dg&NqS;?K2si6}IYoofCXjH%==5O4I9VO=?>)}} z8d^}Re2qv`V=u<@Qv+MZ_muM?*xLjHWf>m}R~SEur{f~s5X}#b`IE4#AJfgKKgBTji;~>a0c+9W=vj+DKv~|l{&{ei!%2LVJ9T8+gi-0sJ z0z9bFIkV$G10olo3qPyxJ<2;CZ$MH#rz+%h8 znIh|{j5kqn!j7n6zxI|iI9llONOqiP~BpBMcw*PnH1(B012sdtp?OC(gab%B81DmNtK z^G_@fv__wSs>>T2;1CkCRk*WG_BO#&mvP11j;53HV^)#-5uz zR3DV&G3F3o((GvxC`1cds?&#k0VgDCv^l&|k}@3rRf5@Gx-L>FV}H>-=yV5i^$zM( z*2F&lRmuERyzvV@L+_c#vib2qxF_*WIH^!w2y65emGY?l5!GO8A{*1A@T#Lom^$_Y zdGwXsFH-xYseP}~EBD7fb`7vFRolR2E2zVyA>+NE7p(%NUtA2WLy>Bx+WXsdr&Z)@ zMT1&pICs>M9-qvjs~tk?p<|hE@dc!e`SYlU!rOQ}{aLhvH2IXLv}N^5?FZ){SX$j! zojhxL5k#$*M6ov|@Zb+;+~nRF(^BBJqSkz>6sJREW7Xs4ojwox`3wa8iM5-}*%jrO z1TJg=8DcSSj={8EPqY;p!p1eDeH?9~ya=e_jS}6>B|nIJh6mJ@pb4K~X^vPNeiRv4 zh}*sR5e;|bMb-Zt%!9>*G&j$MN`~yTZCGSSryt1M=~)GD|`x9SGIRRc*orzu`TpGcs(F^;NRq zOvyiQOraJFSlgl89^ms*oYc}a)>w1f4&?B+0%|LA-CoY8x*K{FoREw$UbvmvojzJ* z*RGiQk%=Dym}H>~X43lp^RLh7n(yevW7-$} z9Gme7Z$MQRq;kMJe)`qQFq?(>c6Ut%Oo^F*o1e&#HS?S$m1oi@Pe!w3Ln!J#swE*x z2t;4!aP@K8l*(1@+a;O~UXU<}pbf8$-vPTKaXwzbCI z{6yar{;NN&@b1_R$|aBu;a2YwD7#KTe}0;FnQLwp*_*_^WbENAHu}u(FND3J3C6cG zb>Mjs9#?JwDJVvE?6mi-X(t1)QN7NaAH9D%T25dD1h5)7Lw@?8M*?P_XpmoBG*dH- z`XiGL$&Zs55MAn<>OQRm1nSlB=8}QY4sSF*^^rIuAVXXp6n_el90gR$lcp5sR{~(r z*-Ri3m#@ipZGqBQGh`c0hX=)&5yfdK`CeI%ovqzgTw|Tboh{JifZ`qoeI;`l&A&1D z|AU{5LCAn&Guqx4$|V9Ip6K%o8Ga6Z1|bNW(!=S)R1b;{W=$yJYonGJ>Gd0iCY`U} z3C#&Om4dU8R2$Uda@6zDN+KiOA~nH!Ag|K(kHtq{RtHxWoU(XbzLtY@2uQ=;ZXz&W zbL~g85qzCy(J!!w)qGr`j)=ys1SZo32t_Abc}m-*O9V%!-ij`vsWcu^VJ{~=NeL@B zdki3}oBQw-LzMKvU+8$ID*t*D4~B$BhTJ~f$2x*Z*cBS>3S;@Bg&C48&;E?MCtbA; zQ1yl2;olqQ9#Kct=L%SFJ>nuAo#Dhqza%&>%%(#(W@#L({gQOCOLV+EPKv>oU586i z?N(`oyTLT8jB=!ZH+bD&ND-b^-y9y zz!3L4YewH%)(yKW)z>ZwrwbP>tFpHCHQRCUlYUa%j|FD{%+IY$E>sBMd>!<3qz+TA z)@ZB{?$X}8uLOkZZv`o_7q-9J#lVrjkyezu_)~i6T%ZB`(d;53lBJp>xJng^wr2#y|qMU6;Lcs35&u#s;Wz^zm0? zOJ3bp4~=5NF!|<7r&5*Xj{y>9Ah?2?`>>9;iTTDt9M69R?S7{e$NCN|Z+&?!7p~Cu zTSI7Sn9CxS;WHeK6|~5D79es7)<;Tm6hjDb!) zpM=7^v{DCS0~^sgd}gSuWbFUdiYmg?jCBLoDs!pJ#pVcz&AD%9KxyWPS5_2l9qDJa zxjz+!{yRM9_#MdN7+lBHnrGiOTkD-gP%46WLTVU@NmoPGFk#)uN;5=-FD??e61U#r zm$)OfE2H$&%etBK5iyxk)7g4{iUA3=h%+=7Vaoc(D`0QgI09+R^Ge|cImSCpcE};` zEJ%IQQ*}It2jhD73yE?CphZV&D&iQG;733|@FZjI$H}pB#$9{vYi)02jjnOAjHPSB z>1>A8^vN#&=QUF|+VD2g2pK{0(RAI3p%g#}d=RI1_}U=svfBs%mv5f?__-@9Z57I) zCqFG24I4ohqH2^kcl|pO27JJJdFK$#qO8emynhs#G$5`x^QYo#5(12j(h))NDyoC8 zmvY{GdAbaA=5OiCCcqFQS28gL9B&%MyJQ_Q8tJH$ubsoU2+tPISza^?Asw5Zp$}M2 zni~5{BE~FIZCk2*{aVEePeju$`?3P^%iou`60qYcPAlt}c{53K?_um=3|EJKlT(ke zUmTC9nY~{fu{XtEei%h%pS^DsQ)kTMaI@vpvT{l6>59ie{Zvlme3!K{V#Q~0S%FS< z6N@Xtz2xRBP9ItZ_=^=hE_%dHrJA~?$lf4&`81IQ5opcG%JO}p>6Ed}ub{m@2D=R0 zb`p5kY+aC4lsC0fC%JDozR%Gr)*!eWQ@JCoZ*@7!&dHfU8b*y zbj!_1x}YtlFHE8kG7m0t5nFImd+=&tMOUhC)(gGx4wmK2OUf34R;pJub#8$14z$I_ zL6a+U5tQxuRJq3`2re*rb321HPFQLRM38jFD3qm|y`Qx^Icv70E`!%E9){X!gqc}( zc0MBX5@K-##aR!S+cwSqiv<|2u>9T&1hu$0CdLwSX~@n5IlH6j`&sg5!)Bn%LZlTT zxJGSii{*}Fj{-nd5lhaHRCbw_OaAvn7>qbFRhhh zn?ewuZL0RZ`@PJZS1;g28`#7C-2l7?Q(MAfXGvd^KnI==Q@$Ma-Y!640 z%R2q5{uYWZ3rFxr?yXo8n{G9~mSrhxeqv7kLMLO)l3=%GidCYVk~te8S(`9L-Fm9% z&D7hDpD6I0uvU}&DN3~~1_sJRXE;h{L+n9m0M^-9>rN9LPVXktXX(w4Rx)7H%bJEXr%+rf3}k3^X2^i4eBMz#Rru2hp8hBU>j(Q+>3=0s zlUQtAF!(jSx28s0ac6p6Ff$M9Zc)^I0N6>)Ym2L1ISI9$2=mAw@4_P zKlBU<#M|5%w5{ACm@k4D3YzHVG?N%9D|z*o4By6%AOt*0aF-)*A*9===<>rsS=1O( z4Ci^U*}{kshF20#X(9Ol*ew{l1^jYdX%y92Z*IWRa$5i$Yb87C>d5>CfgC~q`NTZe zU`OX=K8skdDQ~QtwAB{YBXyv%uR!U~)Ekh-8|NExIBx>l_3YV5w0Wu!WTs3ts*6mji;l585IMSjD)n*xoly5bMkm{SoF@+S|6D}#H$1?AKF@Xmnyq40K)X6nyYkjmoCotqmC@J~ z`%km2X2T46ak*SkuCNkJ#`%)7@)-W~qnc2h58RDlmdP>@0)8gY9Kmz&;9;M^p|$?X z&GiBc2W1Uq+Tk@Rs(4Nn@{ zQ2_;-H(^h^JRL14H7ZP%DjY?Gz32kE0PUTtHwHZs^T%cq^e$fAi`*jSKv{+Ps(ijs^3vX;e9rWLM9{8lgN`Cl4 zq~K`gxephb2*DAA_@cGHLT8^j?5;5^&`y2R<3z(UOSFy0e;VlB=d1BaP7Mokt(k?5 zfv7C!j%Q$(BQzLHgMRJ_2qIARvnrR60};Xb3!a@9oQS@>XJ8PF@59o)?$?{*vrt0+ z{-Izg82JlqoFT_tuy176-bpts^>mKcgQ(;h89HPPtoY`(9J_P7iRh{x4#N2>63N;;rt| z*2ZlgbHQMFPb2+G#S+-fdw$W%R)&N7_H%G$BjkrKIVKwW5lS*Fr#(mCQzo+K1Rbvb zI1?vw(_71eNV^hlCyPLygW;N9KX|L%_dSF{@X|LV%~IC%RQYoyq*K5b{H!8X9tX_-4p}{hcJNh_^uzES+lCH7v5rW39Cu*_TH47g`~0& zGNkNs^&GWSoVW4+2U}{Db?O?Qw3^P_hIf@lMTNCds1K(*sox0(koon-!q%#4^+n9B z^GoyE*1Qtst@FOC?+_3)(;2^eyTU+3q~;YG(Eobs+(c^Sx;$le%|uT&+$ti7eHwKq zA|g& z>_vEikh&5#aU16%a|oXmeH!M}&HTPLSWIa5E)R%`(jteYZH74x&5sAo2jabug$bIn z9(I#}P;!RzO;wBaPtH$NCZk>=hcQg<2>IcZ8K3xfXd4!qfF}VWekm6qfhaJY3~da?3d)0m={kVIcf&;Dc9d z*phi0#6_yN!c2!%=D9iLm`Dw-sEx~aTA)AQoeLU(-dn$Tf8RN!Bvw?=i<^0rW%qAa z;u`=s-%`=U-R0v8eWlxM!-#PwATzRwsf(6KlqfI$>u6nLmBJ*PU+H}T zZul3J>LutmDw7u7nL0IoLgK8SBiMq{>1h3y6e~^o0zutK4(4U*vaGk4=ugS_1 z>Bv;xapNhQNK1U@70*3jTw@miva#o%4FC|5M*@D*m-ZvdVBZiyR!XFomjP}UY*W5b zBU}NZE`m5odeJ#|%yGy9gY{fox0o%Ud9Rd(sb4w%=^rqx?=mky{LwRUbTwCb<84kn zDj1wf_Sq(xu$t`G;z3}JHGVasY35^(W|9D?c7S`u6@Ku??<@S97@~$}K4AQ&gvmWE zF+{_YYs1i{p&YfBO#6&KX#SDic2Hb$&#diuKb5Ub|c{L=|L_HK&awaq+nZtBVIb0aJ&%3o9KxtcjG4n){OD~!ru1FdXB&1GhT*59 zFFVU|xsBva*SfCC7<@d#IV8?kzkY95(5&i~|MlfiX-|>|_5?LyPv<&LnUNp%RL+vQ z3fZO0MSkek703Ua(CKOZx|SI}%OsYVW#BCB%P=Y#Wh}xU1=z^bKt_3<@j-<8E>h!= zuu@AfgD2pRREjKvXS+ed&&XJE69B?My@RIG&xJzB<_8gj_Y_z>r!rMmV_2|7ejv}Y zI2&Q3F1*TcV>j0f6Vjl{@w;nscd!IaHgc{UiS7D?3DOKy5R2Xzv5XM>`_~Q}^E%Ao zvGbdB7)z|arzQLC(S6*JHIjuy_#(|m7kxBUK9B{1Den~$mCd?9ETcXAQOW$Vv*r+N zScUBAmzqn1-V<_PP)023(-XsR%0(x<9DT`lSR}I;{P050{_~4Ho=|_YbJ0^kim31g zPMDT$MXKi~`sN@=uJR$2GL7R4-?tP~lM%m3JpZVVBJ4N#9%R=xbUXJWebQxcMFoek zH390*kwgWN4lMU79tbSKZ?>22?o;rT=gXF4!aA5v>u7hdz|yD0Bxz;jKR~vafv;X6 zz_ph&k4G|Mb^`UI8kjJjbM(y0O49hBC)mHh7g`IZo|DMJTp7u7|Nozy|LQ4M+dj*i zd0$lGdSqERjn^70vUimxk}IyADM=4Qbl>0hs5_EqcSb37_MW1M#+wt*$<* zbq-S}wx1S~T$d}Yig^|yCDUAx`Q`D(_lQ~THjq-j)ew^VA?@76kj$wht+@cRHzs>y zqA9e!r0yCN0054dTK zHSCDLcg3zQGFp3?J7S^*`g?Gv6MTo15L=Cuuz)yxgX&p=rn19~UUWwmEe1q)G)#M| zG9x>xa~bzb+gzYijRs;6&FJpoL%niMQ6 znxy9>ZuVB@zF?dEsQ*f}rnVAFhhZzk_^Y5S6rRBF3NmY1Ix;$_YeA6EobE zsrpn!Y+K7fX*p};C>`XOJai|o4mFd_oP&!>6^DiGs2r{*G0!vlhKYmS2`*|z^VP>0%VgFX?xz3$bk$Yl6JBr5 z2+a7Z>hogm&_`~wTTq!e{^;C~q}LtmqB=Er;bq`2u4Ccq6HufbxzAa+=j!SbXqOVL zIvetnS6jn*&QIk1T&3hkFISwM5Z-z$v}3fGXZTQ+c2$$bBfhW6knTXq?iiUVEcyIT zy~swChLVi@mX-y>YjHK4YwUpkkrF~hu;H8-lN$Guohs|PnnBS^QS^O-7&*+LfFeCN z|3|TPN`cn+R|;=Fn|8F5w~C*}dhHOBh+&DcUm<`22~&?~5qqkjK{P?Q%K&#uA?{#I zO3|payFGz#*kU@rqT;PMqbLN=Fzc^ePw_3shctx~bk!4XYQ5LJY2ux{^)LkP{hK1t zkQ7p}4_R?w4ermCQEH&M9-{mO+34&M-3>8S+i{nh-V(5ai^RJPe2; zU$G2m!{%9#RmEx6H@IebkjPJc-e<9fSTBDSLrX{b>AVB2C=s%2eJ6#%Au&&-aOvJn z=aBMHMfCjk1d=tA)F`PZ^)4NX2#XFh>iZG{k4sMw;v4ad=XlQw!%GumUgW`?juPh} zps-d+Ct19Hrlm6`rJ09 zTif{!vv<4JGZ>`_$WqBNXR0jfd!fO@QfzZ5w!(X+)9W}eY)Y8*@AA$0s?w=+%4s#s z&*QOXdv*ABi&@lqG{X%J=c#DHKE$yw7(oUqspQRJ%mmHd%O_67h1Af^j&HNrX8{YdM6Lft-$ ztXwb#a3(F(t+Z!Vl~75cm-6MZ?`CyOFDcHgd5d=2!bWj2-=<*=t9jg`GIL;M_4hHz z*94$E9*nCJ=imA#H=&g#2NoF1OddzUyM77*OG4O@X6SUWo|GXydfWMkY2iSO89M2@ zk)c?YN?SycTq`qQBPuq;30b_m^lYdjo{YAUO>rJ05zbm;^6WIJYn90|Da=%Y(Y}DM zp@Ms1*Eii*kJK9pdXfy=kGwHq=^dO-nGT02cMK5dbMceJU<_`9(edJ|UC*@w|2U@%i9VP$sf^7oIT#p07&e1*AtDAyUQ^|n!383rnQ7I#)Rz;+_qbXWLjexp)bh6Yf zU|2xMC{_t72)|^5@_NI!U^tnYpW&U?48{&MyRVQMDx#0w5?*ks&H`U813tR@Inv^s zPF&4F4+oA1xg$`od()wP`u!q-X?@&kQ@sR?_PXlry6~R5hb-`XzA3KfwJNzsHwanH z(QQq(#S8ED@Rxm<9*iF=vC;=XEE{vYc`rDyj=1obcN0T5RR3!Wf1G4?S=dXb9hk1h zP9Tc#RDcMY$Kdek+NpH%=O+g|ot_!}KaM zs)_KVpfV#r5~1Lx%9rgRk~kl^TOUAy`x2*NENW8^PD`i^BDzi-pPAjhh+_U;z>y=o zkT~8YzMT|;dPtQu=LSvCb0rAijr{J2lpQ&VSRn=w+AW_xT-WxKOpdzoiK7(Za$h-w zN#=>E4;5CYE|><4Cm4WH4S+)>&TD6cQ~Z}swlCfqd!~ceaJ@@C|IFs4=ZO_tAoZ)% z2W?2WfIs1Lo!Q_P!BtCJ2;$QYhz|&1$f`S{ICG#e*DkdindEFg``z{dughJcZ+HA^ zZqn^BllTD$AK8dHYJr`^rKZgm=LSf$^hV-Lg=B*F3;9G1%L zcZv+kjbZXKgyZwgH}Alr|1M(}$;dXe9<)FmKo+ru5@cR&<&|p?;NHU^m-H3bUHy=U zkk(w=U-I_fv%9oLi^^BUk*g*Pfhc4);!A-f5i8QZ-Bk^1iGsIx|G#Eo^(oS z@V!MTZrX3gi``ve z3dj#UDRtmJxPadLnQ_bU97e*0UDA?TMXpE&O%#KLY?@4Pc(c6R5Bf5;I+9r6NepTD zw%iJ|I&Pe++*mg)XvjuuvLKso^oT|08=uO;rAw9-!(1i|r7cq#bKh^MKa=vI0B{lg z7d5ULq>lKL58Ui~sVr%4h$rH2Fce32(TH@e+UrM)L78qPxpceqEl9xebaW|Ynuf;S z7dc3&ggfwgN$bpm!mV4Gvhs(n1^H5mo|Y&Q7KZ@>FZ*(yjW8&MvDfq8P9E*A0-0Y^ zYSo8?B%gkl*e37cs-H%4lfSLZQ?Dq=9%tOuEb_*8GOJ&p;2D=-#=SA$TinkTFXk={ zSiG995W4RNYucB?;=YQ)bRm1}0�Qwz8iVDI4_PbM~$1`ul5qQ}n4m*|}-cxZpAf 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�Pte98JC7;mrXfjpEw3I2FV4i#&hg5e_?ZphJLlO>y|2dy zan?U{1)MqlSZum#YOqAO5(A+t$l|c(jsmGjeD-`il=+6pOQ8emic;aW ze1sDN4)Be;cP^ZL9gR*(uJ(flCgGLZ+q(f*?kpD3CzE;?=5L@O6jzsreor@4uI+n~ zL&uoQYFlZvc4jCuQ6yNvo^#o{!$nYzL2=XJcEU*~_|luW`so6&?>DzsM>>K9k3ZlS z#{%ORXLQoBZ=|f5nMrqy!I`tpy~0QK75b#RK$Iy;qb9h^Tm!s2b4szUSwWq&OOBbF z7LK549Zw6IE;(3=eAhc_Vl|t{ioRQ}vvs7N+Xr(S!UsPHt?GxSYMG1z>&fP>p)1R`qJK>qI{CVC z;2z@kQM_s50U1-<+z1KZ(2OAGxc4H{oRPAIDW=6Y-4+Kbxdi3P-AJ&$&KDDqKp5?BobjFCAHi<+hGGWgJ6AeC@B*oo|-{VNxiXbb57r@6 zE+#5uH~xvq$r-nGV^;0vig#>-p6T9rzl`}hgQ=48nhibN2P5H{PAmkOi2t=JLdj>ltA*c_9gW;hJG zFVOBmKq4x&v>GP?;@AH9MC}OT)t~Pns5xkpMHijmr3MnSKxgSNE*E-x`X7HGv30w0 zFQ4V5CwAvoT2S^_)6c)}!|y z6K`F_lRejojtAM5{z)4}XPu%=)EnRPn5U-H8sS}ihcPesMjaJSZ|y1!gE#P*|0Ik& zCfB9NFGXdr$C2(;Vwt+C3cra&Jp2`KYFD3n@fHA;X4v1Bj(f^<_S)Z#!viCUba1 zizXHm3l8^Q1%mCLiz^;Om^ADX1523~?>HiO1Yn1vw^V62QnWOQL|wN)a;5;-oy{JHTq4`Myo zt>#}_eQr=^{xdYrIseOhC`nS9cAq-xG)SXFL;(=OnIdA;4#ga-z(LSLi&80G!sW&{ z+{H@$YMD_)bDx4G6FQ@ZA6t`JnMW{*eH+!@n#<{8C`qg>CJ-#;NTHtH$7f1{_8(N zr*^+~@)j90TYhI;bq z=6DA8${6m8{6`e;XITqs`=i8uqeBLnek^{{cP&S72tY&6-4zZz>`?9H@z<1yzuRiT z6NiVKl>spum>KH(VHqcMJX)nvX=b&ZpwBnN9hU{shr1jwSg%KP=1~ZYbrgl!an(s4 zu+ryW*mUZzbsuDF_=11yo*Q~iV0-z2^;%)@GlVy%R zfNB1l$M8A%UbEY|5@s$s6#N|_0v#rTp9llPXS|T5TL|CoR8<(-$>cv%bpy>cy|%mxg)#@*zL#=4NKNLrUs2L zdbN$PO|8YlIRM;JjX8b<?=ea%m|Dm_LO7dqgWL=)6!bp2o?Lh`tU?xi+ zQuRy>B2QM)Y65oSNIY_3sAURd9kj@J-(~B%lt>xg=W}c6BcTEco5!sByZ*)NqoC{0 zTc>pjxgps&2r;-33E^rQUxJDN#Q^OGDPoqyoPZLDDd*is(^exfsQeN!m%GCp0#83%k_!zz|tT-LSD2w8grF&Pc}WxC=#$HmXj}wN1B~ zFC1|oSMX^S*c%QmYn;SvcJz3nQZ#{wh#h3-8g~yqQMv`wKXN=7$wkLvj{wp?tT%L} z6b@X#$MU*hc}MEvwA>uI^l5L)WdIQKc&n{K)qDHd%AEeeQ+Zy{44^K+LOa{g=d}CA z$CaDyhO9^$+bhHy!{F!dy5I15P#759PLH@!)_FC~@HPp~PnTBVk{S zT$sDlfy_jdL;)NklsMvi^Kg=^7)#e4r z>FI*Qab}Q-Ar!#p0lW4K+T78#$PGD9-n1~ZtSZ0ZR+V|DyCI855h$~X_G%{oU|RQF z&a{>hDz4m4jy>YeiMwvV%hz3pNxvTW_qw*?YRIHLO{X|_+HEV#8O1JoS#KdD!f?2f zTz`{hEI62oq|8eV%LLz(rOS#r%T9yPKkS^!;ze#Y@M?H?!oNA4qC*mWKK=770K{Pi1LYH;(!|Je*VTPGT zh_D&H*IJ&%ghf%03K_K;Lk%KT4-f*q&_oW_yQt&DQG;dwhr{t5LL2JG5_&X6vumj; z^2e!n^FXQc4>KzTFN2H_^YUF?Mc!~Ap&2c{)tvbPgSeuR-thjA!lZSu@U)!8~brDKCs8i!DkMO6^SavV3og=JE zU~3Dx-YtTj)B}=5sh&u+>CK1*>*iyv2#YOqXDN%rl4vZIl`r(($XAT0yJSJ#+G=;8 zIkI$*XIlz_`uAAjOINU`ED!E{DSAwYK7RYH4I_sRB!t;ma$z|3=EfE315!%uw_d*$ z3(jzjc=k1y`1!mrmOUCvDhy5}d;^|0V1*3F8S8~a;?#;fSh2512`Ye_=9ljkkN^Yj zQste=jCANz-|3v3ors3U-=M-uD{~BNk^B1acO+VOf+V#oI|_9C6rESNtIL^QJMBsa zKNpU4U>2ASM1AK?ezi6(az6 z{_wvY2d?JR*kUewr_|A){h|(^2GFvmQYt#4@)(HW>9#%L$j?&sFfxhyLqPgSc94*> z=O#gSnHf~xebdhk2cB(2qT$P5@ zRnP|yKy20qYg2rQLkE^E8;ea+O)0nupV!}x`;%xg@);T1vi;Rt>gzy+dBrbh~S`XnM3cP13Ylvjr($%KLVyR%zg#y@2F*hA?=VbH@i z2F*|CEy$j9JcegGu5Eyq*>j@%yP}JCu6XT8U3N!cT0@hHcDtOyLX|R(tBh?iK4CdT zy|0~8xQtM*gU6j|peq}Nypx)w>XaAm@zTogR|R3;M!-tNVnFRQnBNaHxF54pVDG4W zP?jE6jeY(5pas)2fPqJn3~tuIOpj)0IczXJx$w!ap2?JQk0kReKq)a!(E-i+%0@=0 z!FIaAX@SP%^f=2OFAbu&tce@LuMv~|Mw~y1JfSA`3r0NSAr$mVnLwNOi*X>cqWt&DUm$ zXZXF)S)EJ zf0GCw&+b>r{NU9=;CW~h(2S;$r1YV!q(SWo>E;B9DkUgH*b0vG43w5lA%#6;a#kjMtuJCPfX$MG= zp3*esr4~#bSoq!1mVvy_EX29U65c;$4NrjVEz(_XmhP7ksC*D?Syh5hjx}emT?Wxg zB^Ox~@+{H5UdMVT29i?C7xKp%&40-JUS>WL++@7ZaPRNFOKm!jMxyZxi zM?_BJma-eH@HxF}zt8Mm{jGf?KLZoYVMvhofpCDpD=`(zXUg&wJD4Jd7T+bLPq$QfA4qnx$Ot1S>O9so~ z&!J+ACfD&|(LLZYEi@kk^5@hG^4gf}Z4FV-Dd_!D`sgq2jr#`(h^BvZn1eHsx8EVX8UM5ZVEK~2_~$QvkCIR{!+(78N*=_=oH9Us_?l*0lTc8Qo}PH! zb%BWtRlQRBWq?@SZy%A~G_?jHM@P8w-&+$j$UVO`cAwr4wMW3f#gd^a~##$_@B zNs1oY)H5fQr!8|M`!Go}#eGy>^wfsAmxGi|ymQM2i4|WP>-s&ALxEbCkpS;CwscmTvS`PEZ!yVpWV#h(C9y?n+0p!Kk4u4)|GOv6P-cYE4BC@SFwc>h_c ztY_#;dmiK+Q~(A4&)LB9>RXy-uwd=jSa6^Bxl+_EBcEhym~EHMNrhRTHaXTQ0bmR? z5~|a`#^a>mWQG#6AV^#i=M+1Iq6eTPiEJ&9EPkvn;qMr9_5KeHLG(r^*rRj3%a!`k z>cK4G_Wr$HQl9c!2iVArgA;V&P`me5w}kSK^wAzT2)iCQ^nFq7ZQB0KAb zksqUknUsQ*?wGMkc63t`zW;nz>96Y>^ZFw2b-9}No95_lB>?P$)}-IQJ8_C6JiA*C z$P3Me1oXvJ@J4WjymMJmOxMvtNJ^BQKtY}dFngxBPO4QM40*40)=M_!qC=UHMA8#d zYdXgO>-okPd@|ZN_4`y;h_x4jr)pV9&?6FAX##Rg=7Mb5!Sht+C+X3;1jt@ZBE4HNEAHbmXVXCx-}{I+(1bj66706)m@JC_dIiW2>bu>C`*RP7++9P7@bpj9RSz zL)lgdEMb{p{Xl{}4BYR`VLf%Mq=Z{U0tc+hO9Az4*Jw;gz>?)i_|Q+xWyFKMB|_4T ztLPx0&_>yaIS_4?jd&!fA`%2W9u`i)`Jg$PsZ|#Twe)G9z<*DDXje!PuYT_S!uiaQ}5QL6xD+Pc9iygPIRQSnOxbiurv*b8Vo5!c=wksAw}Ck zkp*amuu|0CvE@+*QcmeEo3ADCLPf+2_ljYK-+TW*Saht#;}m62_|=#`fsojEA$gZj zomxNvN!MKh+)^eF2!R5M|Fk`rPtWoEGoYP3nY9>0CqJbtuj5TEo=OnGXXOt! z2&I4|I{mNOQxr{-h!ii2CnVm{?^xymgD;EA`(auN(Il^-Rj4TC)CL!vpu4wjt>%=< zyL@2v(Qkq*86>M7+3#MCfYV`B7U#p+;yq`{+>H(-V3G{r4TtmhA@RPlG|d0zP{tPY zY>Z1ADEpt>#CrfjY$$h$vC-PW{PUx%c_DGhA}R^T9%5;C8r7ffsg&wrZsQj+`&_5( z(TCG{V2E{(U-?v(eXc1oP|N+nqj?%d9Utvql`>Q zYff!0j1^D5VS3ymLGUIXDlUIU)OGqHjZCSqiB-#GC|q`eS$Bp4I|YGO`~z5>|4%;b z&G7KLN9@3kJ)j4R>`%MD`NlM^qQLH9O3lB@|Bo+%;_5Gu^kA2LVavzC# zgBImlVPmTI@F^zFY=tp%U1`{s@V=m0bTeyj_KH35lgtu2*wwg8%ToIwwja%j z*D-nXza?%%MP;AXlIsYWHgr*BkO!(XqE*Od()6i$6#M>;(3`AuJn(+zT!xTuI!G5ExZw}jScqkL&)Hrdv~$c)S8*jSdA zQQvKhLf$`WHD`tk^p)3j7@?j7BT{$f@lv_BWK~l2sgF|#R_D4#MPr>#(4Owd9fI#O zLSCQ54N+zmsPXl>Nk#EBRP^c62em*s-r3QRyHF?`AR6Fhx;XK$v>##PqW=pp{kT&J za6h92d)w%k5c8)fAYASZQ5%}p(GDgvx4ScR2zkb45t?7hz{x@OqqNClH0bD*Eh4yT zB=q_Cy{UVfiz-n>p4BpHrvFa8c^T)=DXFM_&@He38X}eA1v{6pCVtrNyp^ThRBjd{ z!K`;Q#4wSXyo9UQxCtCG5!zXjX}HCAix)ZMRbU8diQzbF48CQ{aCtCwqqof+u02F`eu{$IV13?+g*hW9K@v7K2QLTK0+C@B>7IOA|o2Z7HG4j1KRbMH};Gg zCw^eD`JKVS)`5S@FDqRJa+^E`F>60_{G7s1!m=hEy&nazR~=!<^lN@Paq<63edEj+ zVv7bX6v>Pm31L0Avdm_ym#Zk*ok#6+t9)ie=>M+fV$&eDlfs&C84Y1*1-b`5JITPb zbE)c$^|=V=h+6CK6PXz7v+oIilA4#qrAT%F9i=JzT68X$-oNvrrkbpaBRF;>Pr`Zv{ z+*YfYD*r0GpyWwjt&+=608;29!^=;=R42f+{byHtiN0t=fgU}9CDe9K^Vned_ zO$?y?=6f{e5b`pA7yIcZQr!p%2H`JDBR%A)L3I6+oy*VDWu@ngWBRTg8l zMElGGW7M&)wt`^QIcw(U57mQOEXZR9?^I%Ug#h1KnIi%usUPKPh#zwx#!mQ62i+(E z=r9?uwRUm)gG1mST>8Zq_^&K`B<)7**E1X!1Zmp z8h|{oRj;bDqI3J#RYb+^!9?F7YrFklIJnj69jY|7awfO{EY>sn4aO5+yDEbUNf2W>3MEcmqoEjqu&s!)uOmVLt+dYtu5bQv9mUeoR8cZlcS-6(+A(Kx)PEI zy_E91LJ8}9zQDLV|0ZRlryP}<*K08 z95l+_YE|9kVZlzU7VV+fXzx0yuVQXDpUbT@XQo=$Uz`syA{<}Gz4JD)f^ANh4!%QC z9UmBFUAMPv&uLN~m49jCTWGHL*?>4^EG$p+W|#(mNL4AAU3d2$;ac{&Tk&f~&i=pl zJCi5<7g$$t{WCC#R2}y(0F>)WiU$&c1ns5&&7omHZeXUJTw%drV6LvvYw} zNdWKgF=Au%1nO65^G`%n>SsyF#$pN1R2;k24-t`R2T{s98<9}##spT|uJnoXprFL* z-A(Ww<2<^jEkE2qm)SP`IV;QaL3Y>jn?Nf0q==v_;OqQF;~06N+|oG{NxvvJ=2a50 zZTG~CM7vCG)uLS?>@iAq8QAJLK5dm`Eg}Ao=(bY#_;D+AbFxp8C|Kd(jd7YB@kuvx z6ai#0&ga?~he6>)8fvKAQ-JBljdUt;7Up z(B6X6`Kkl=i~bKH=GULAkZfq6YM&8Z*b57}h1uf>ye4QUfbJso$r|Wodo%e}5S{%; z@so}yx~|l8n5dy^hc}!LRqif3=Os_nKxu6uP6EuWDCw7z3-+_PoA&O8pe&3pDckVT zBwePXWcHPElLv-Cf8^2{b{Twx-q^Fet)q%belrHl#c~RjRWMbQQTtlBEL`2o3{P89 zZ6-&67$dfOK!!{OI5;MWq|kX5sdGGS9177DP-B#A5c-W%g z*!71%Kiu=9+pXN~1L{o zc6}{~*2vzLt9m^TFAu+~;rnFvLVp6Kfld(2>*4*gL-f66$#n93bP->rZb5s!HcUG4 z;kLJKZO~A;eteTu?1eG2yo^1DB|VzL1OAs9aC5m<)6s=c+FwYKQ!B#r3aUYS=%70; zPw*0bJt(r-+ebA@$M<$QzV4PR*!L1S2 zHxc(y1K=_Jw<+CF7HVH* zsh(gNMltqJiD4BkCOh?&oA&P_g(BF$OXVzk+6cpY`SKDKq;SIz+Ch3YxM4ZQ^;=wPe9TJ#eMdYmVDb1;K? zDUvy9xx8frEVYG%C;~l!J^rMXouWbKb%GSDeY5kDzJ}!?{HE~QSxXe+XQCdRo-Hyw zd<$A9{v=0g)n4%IoE?@9&u}Z=qIOn^vaw%6U9((~wgIjsg|TRcR5$-z1WxtA(x6m?F!Ax%MEVNjDL$&n))rFrFpb*ZYfPAMX3`SBx*tMTTD) zaSC43UXC^F9A9<7M$OSx?zwRW44hA@R$@IN;xh@{UTGg$I27*7g+Vp92doE0cJonD~$28B9%ejtpNu&(0Gxc$};B{8IbpyyJ|KUCln%$|I=dCgPB6WHZq& zzc`5E(>3!bE0wqZ^%13j5MtO8Ft4(3Or9!TEIoBxK;i7l4LRTL^Wfh~BcX-SD798c z>bVS(XuipgZf==rDQDB(etDlh6}bpbPkh z{UfjuH;?hj7MD-5IMy>tA1>lpRuXGG7$_9nFI<#&r>#!tn;AF%d^-u7A6e#!wN#rK ziW+?BL7n&`KejdtZA%;yR>@Y`6-&L`!M=T-PvFUeO-ReTk8sB8Isg1s6D%89W~aEg zXM=W`u=dLjNFT%wQVXh<8R~0b7Z>;C5qhL< ze_hL8P*?t9NIk+~qii1@BtN^XG-318(KYZY4A&cShe9FXM?{WlIx&qzS$HM^&}M~r zlwJJmmhKNMPV({FOv6goJy{Y^)(>l<$E5I1g>vLA-$rAb`clQTl+8n=Q2(ALo&n^! z?IvR)Q{)bXvm}%)3uM)_SNt6vG!l4(xTeimvmrLoAy(E>BJ8Hd1tTCiT zKsDChEKMEmY_1BP*3^V7NnIFVy##R}8)Ou$yjnsbQZp>=HuEPrYxk4dX~OwVmguRw^ya~mpB zL^=)n4lQrSK3ZUqeFzQn?uO?b!2*bv_hJ80PQ7?#B^&oU=cow*|9=O=u2s_|?v(92 z$~VBKEdfL9xcIb6)2+yMy@XxZ9ol9oV5 zjKM0}w|@-v6z!r*$dj;g-0y0I#5>wUSO#B}Qxn+%DWcDA(+gRA}tJxu48^`nYLm+cQ} zOt4Fr2|@voLkc9~%$z|5%ljmJM777ldI9~iCI0m!0m*JpiGLrqTNviEf zpODcIfA9G=@lja^ifNc(JA&~O87`+#)2#xdHdTKa#0dU(+QlFsWA3;FgH^Fw*7oFl z|GSqu>t+^r9=#)_ff?*{!?JP7W;o>dU=+oSA{|cFL*67y>ltaR!s63QzyJVlMJ|3& zCB5Vo+sqnUL?sI64K;ycH0qbYyg9Df!g6Y@8s}LC0v-d)=aA|B$Ra4twmUC(NRKu3 zvG8fg)1?L1i;??^*-=5Q3tvPTpZ;$|y^He0^}9gZnR$3fT(rhxZBUBwL?uT3OG9MI zROY`Rq$04JpqQb!y>atZL5?9n!T?4d*gAFgusYt_2eD?Js{c^iHJT$H87RY(;^ShA zC@5|$7Wq>DQ*Sv!F*OHMmo?-`xq99&014V28Q7e8i>3ejwH(1zlrn|Uiy4&P)MHt( z2E8tnP8>rSm&bDJK#af@94}i`lBpk`nSHR{w(4&VkXoD|@f%+DI8fM^mw9#LUxeE; zm<=3toyJ>pVnH8&i3Uj<>?Zxo7(Sp)PF@M1n2Xj-zTW}*=BH^&02cS9r}8J$0pSa8 z&e+;a52(M*^2|#=0JFhcQQ0mC4*eG0<8_8i5XZPRyb=eaFEPvbDp?%QH%&o*3)4>_G8THpMK*32%+Nbg!#$%L^m96dSrOiyHQgkhud9sdRc68xYKG zr1_u=t7Ox9CCB+?bt!7Fv^?>}Fy!l~ zZciN?@}J%{w+5o;M4$h^pka)+C!~Ta8O9=)+QUj7(YCHq4LGMahIX9`+={})2)bxm zvq*yxBg=j^wRQu8Z>wx}cQ)h2ejlB)nI{$!%c`56?;-d|s`n>w!)oKD0~MU&cKpJU zG3A#TWj9S{eXqQ(U@_hbSsS0;g_8ReP_n5{K#1iDq@aA7}Zi;M@)5=WW&u9m(9LmG#|!+oXS# zqXqBLY;EhQT)l&rg;(;3yGN~pe`}|JBSZy>z-d~?LWTMa z@O%bDhx+1NGvi>$-E{zf{p|O-#lgn{DwktM*nj@r)GU2z*ww<+=uCl>>Oh-*JbywaOd1D*dP*?hEXZ;L812%XCF~S zg+2e%HymZPZhW$I`F-#H-2lV1(QNm`4Ct(VgCqzCS( zhhAJTmlTQx;zpdca3lN4uA%=TOsux7&TE|y#>vW<$l&eE4)E7?Bl;G>S@g$vOLu@yN|3KtQ`>zC)O-0ZAu3bP z9s$RSl`T8;c1KbHwtk~j;=&Boux8+M^zH*iE|$@a)==!cw$4DFc(U~yLAgCvmS`xt zZEyhjj7Bw?@PE1DCr4J{Xl9^Vto`YqgTYfyW$4n@w>e0eGB`l`0}8tm4waj@1bh5n zycB&q33P4YGW9r8Q7iq%3JEvbXiF3y0YU}|LQOX|1PDmqz5NsvU#{LDF_57iM>8oh zZzoRQVF7vo^5|=dz3G1h7cl2Io4|0E49<`m0?>t$=T9vB!M5WGmFe=nL@LgbDa=lW zW3PA$-;GeA)n1^@?=;-5AzdPKNj;JHFTZ9Vb0s%HY(#foZ*-M2$e7AN-;$SrH&Eoj zfWwou!iVE1Ow%i0%Y(xZcY9WATcvO|jl$(T*v z@mZzt>A!XLA}k3BWf($ciVi5rC)H<-aawz<5uP3pkzkAveWmJTo-Tgm$Dg2FG?ecM zuK3j++^GQ9x9>VNTYbfTgoT*MUWpLI*;BlYG}mrGwaOA>(?GpvCf-f1W+6v@-es!+ zC8DVA@1-)DC_3d`n0;rjW>{d*`P70D&zcpyDso5MT6c97X3{|_B z><|oKe(3rcwz{&6K<-ybelof%+z5fQA5dCXZh$EtVO}uBI z(oV#YsBkjmrG$ql(;;7m)uoWkp7_olYxlTc5!verq?fhue?VWlEc9B} z=Rm8cDt%=7^vp}zHZLfyi(w$((OHKrXFl?-EK-|+4)-x1|Lw+6T_TeX&bZxn-&%q8h@ z24Wwl~Bbm7vn*HLt^{w+u?e<#@x;i=b(U7aM>d5`P(4h7c#mKzw%+kG$H{ILu`hN8MI#8WR^kE4objz8Dyvg!D`Kj-kRBO{S?^lXz zjK8zhuV3N*d-t=uQt(T2F!2g)PCMyH_|}V#^=QTc>6@AbNqU0BC$xC-;Ka+FO>^i} z^AKa5NBT*R<-l$%p9s$i=%=f=#9j1Xn~(Zwd#V=rR7vu`W$3MWbblpkrUot$Z6UdBcQ07>`N= zOBvI|NLT`(-SQdi|Np7|jUsfn{Eow0Q-QSQdA`6Y_ zu4Na=HwVOG*eniPK(@G8yYbMIY@*oDNL@)79ZPQtd~!XMd`lB`^y8k)xx(m~OR)pD zj~^{>HO1me}~uc*GH8w6pBa6H5p4b8$eaQ?j0iI!0_f)G$2r;8VhZJc48}Jt8?#+041<)?jzy0$BupVGBhUZdp#^^Jq~cF3R;m1j!d|Eh%W{yRS9L)f>Gn z$qmreIQ<63vr2}@~5oaJ>(?><*seEgqmsih)roh4& zXOnr+;{s=qc6wEqb_Wpu)d!q)|A#wUKjX+fgL`n|1>Gw7!4pzz+;Q%!1z zeTjV;(#eMwYQcS}(B&tp?z|hG5e7cf;BDi5(FAf@{4YC!Tqq+~q?<}=-GzG24B$me z0|;kck_bng)wKyL31}FxpxkTdHb~MTw_SaVd<5yzw3P)_63XQ^3YVDPo+;J~q&tme zjvvnFo;^MhH6sCU#$1|Y^7+?s5vtfend@s?LL31jTcQt|s}0@v9k1QHp4U#?Y@KWB zRJB4nF%a&yf`I+Q>E9NiUFe!EQda4f@Yr1dzD0?z&IfDtQ7D!o$5XI;63cTvW3fNA zi$DsjpmRT|@)-Y_PTOt*WfI>EnDx{JXAVaQ9~D|bgaKLH3%TqxJP3Y^iBX@QU7$u2 z&mFqSNFZTqlp&^lHI4dX%B7IG7MFVkQUFJZ#+EY6Et>aInvcV3-hTa`)UJ zQ;VH#Q+Gd}VXMwnrm{BQYp3&slok8KJgG&i%|Rt4C8j|o2E}b5Ho>!S=1X)!Y1}{W z=__DRc=I>6QWYv2ozzJ;Xt7mmUn<{O!-Of_0k&YrSCx0FY!f1CQbg)x`^XKZOkJd3 z+?(^e*_hKS2yHNpfq-(zUi~uZ)~Z0JSoU~hz{%`NcEZkw(QyPQJ~S#^N%9E9q#Ju+ zWDw)iGa8zNC@F53#?|MdQ06n8b|W)4A#!6RXL@IKMC}(?j?2yQ;)3Pnn99r;B#fa; zdZM3_SdBNMlR!3GI(WL9)LR;5ZN%y2CS5$QL9JYQHz-qa`y;CWX+5V(d3ibg`}i3H zuDPe}@NNV1U|DjM+apbY-rS8I!!l*XX!IY{OthG2qSCodE07Z-AAjXnVZzrHf6$RN ze&Q5yUdHuF8R9Xa84n;57EAxhoTNwFP2x6-mjXY(@Nt@Lt&tFg9Ipy)|0Vpu$(DjCf zzwYco`FT1zPpO>TQ{$#zWb*X{uZ9BaW z$Kn*6U*SdstpDi(&tA#9b4tI3M!PWo; zmwh7Xx9)OCmv+(P4cHf(gxu8c%PJB?S>hU4@o5FZKeov@7E55ODmmpq)E!<*ZMkN2 z+!os}vmC*&J9@M_-Hj_cubBU%b_>-^H1I zG|w9Nvhao`$U|?Nx$lDmk6VVQs?-G9v>Z`BPwW@Y99bwZT2oVJpOPA7;9I}n?Fa%NG3JNlz)vd|j!GDI%m|->)ORqJ z&9%C`Wiyd+jq8LfRA-KY#g%v-$dcOZ&bx%wQCInS`f8eJ7NGT%8%PA=%gr(8M8JJz=3Z)yqWPf(4SW|mNg(Dse;vOf=$(?b$e8kTd< zwB$1sKOPV$9*cxfJmp7YOo0BU$Q9+hawxHE?z%dt&pI_khm$fm@3LnzO?54W+HHbX!}9x8)RTS-#V?UJ&o*05X6I@{?R*|Y1+gTg+ht-y0#N67N_U3a| zmY*#y9U2>&_-qs2+4H5g615&*Zr2>0?-WE4|H8`;jsC=i=X}i-wU5#v>3QElUcOr! zS*${rj3-m9K#r_}U2GYpQflQ5VM@-*<)>|Sq?#W|eClZ@<@IXkZHSNo%}5a#NlP89 z!XciNJhD99Lk;_L_Oo*&jebf&2F<(wFG&2XBM*%!0|r;|sQ&9%6q6kZ4e@U(Ke5oPlKEeuKJwU?0 zt4ZbO%Tr32vE?BAmFm$|b7~zn{^$n$vYZ_B{PYP!lEFg86<60rr?fAZ@G%57%RB%Q z`?U0MAT`-tlAB1|e7V23W*U6ua#Xl$!HmEaudNkygGd1a-S+U$ewa*tCg*b?9zTsX zSbxhMv`Gs6~qgEs4si+b{!zy3JD6_q8XU zNW;q8O-jT@zMZ$QXs`R?wb2!Hx!aNw;YNyqm<1IkyAbmG#gb)Mm0KwL-5kjNc77c0 zAta#0LuR}NvS)4sS=uNDMF_J7wF1OV`UULDV=0ZcRo?znaS!i_f&c6ho6q@0xAS?{ zXsNpv9h~ayZw4FJrV!Pg{T3#3(F3S|$Nr>P=b;cjXREQvGHk8z25Wv=Q*FGS1^Aje zOEzs}lW;WI?64%4fp0t?#BP}70}|#95Fhg}WC(W`Hy?yMy*nE((#vE`D6CU=yvmu+ zMUNwp>&q{cD;FUfV}?_mox)x`76XNWbdZK&gsV)7eHwj1R7II{@odj8IIkIGD9+Wg zjSy~=-eBdlmG?@OaZj|vbQkWo87r(@o5Z~P)#%W?^w8lsI?qcr6S ztRLBGTO!4P9~WkH)su)_sz%S)AlNEdO-&H9wNJ}ntx%G!s05{94N`u=7 z@Px&-;Y|7En=e2?&wzr7$#~Z%C3T!ci-hybMyqe=`{6y7+}?8qB^#mu;=mD!V8OQ^ z6clO+ebNo^OT|*5 z;b|}Nm<+w(ga~IYhRlVP+7n}`>{~fCjok-0W7zt>FTad~{!xw0mN)vIv%$JQ=!!%p z>8{U>9c;GdyhK~tQ)DK7D4b-{#aEi?cbl{gg<_sQkbCuLzbO=hFO^OU8<_rXT@(br zXw9(Xk(#RFnV3K6tn0McPAYIoAn@ZMjTkv|k1;u)!znayhzuSN!Q+}^LG&HlG1TLd z1Mf-5yJ%;^PdvA|w{9AQe7Si2Cgup8AihnxkM7+Tz9NRdVT->v+=}F1rtodE!UjWs?HJ_3M=sXZrfB$ow)(if)MqrYb5W&X zGf-VwH~kBe?{W@w+p}!u3;>xadKaRT)rJ;Qhhb7Q4Eu6Bkb@+{)zh0tA@?P#_;2rxYNxTU8@>X)M(Ma*4k;IETbjPq{n`J^@ny-ln}>Qs*fXIN$Y zZ|AH7mxcpNQ0cLu(IHyK1OCPnSern_%+hMSHw6^WfLY}Fch1y8$qa5d8BndV!4L5J ze${zrpO01Wh(?a-Cd#nr69c#MXoMK{2RE;twgI~OwM&Aixf*pal6FMY2Aji&Qq0Zc z>i5%F{q@DVHmM0sZr*6}bzQwSa~Y|%6U6J$x{`^DD7DJ&B4)(G6cEAnb!@!bXIUYx z1%@jVumt}Y8RMJ2omgi66l1!42F_67L<2ide;TbRg}aCmP%D% z@`wu)tia%P?I`Jbg<{C%4{EHy;83yHu!Ba6KaA;^VJF>kv3p7pQF|dn-Hf)WBz7Gm zeZ6Pdgr%(ciiJhv_$vQTpD`oRZ|<^lqLrXMnx=o$VQav=^6TIYfC9ChfxV?p3|vN&)XvCEUON^HQ{P8JxMji@{!yP3B_o^)>!=s7H3}q(=?yC5x3NmG z<%uvswoBk;-@x==toBTSaiY%xr$s#Bhrq$_K|4XN$9M=TCv z(FbQp&Axu^NOhypB(IF-2dkr(VT7s!x(Ot0Ff3alkL16@hSBG^dooDYfGdgso6}MA zhuT!5=#9R5^1!pZe|ee)!<)nu&tQ-bHedJ?w4I`8L}aV!4r*~*1zB0ZyoJeMBs*S+ zf3DPZ`-ZnaghcH$OKWQ6B)ReS2cvSA)_BBX8-H@fch_sWAfe*`;tNZ>>azO{nd-e{ zw4!`il`Q}=9J~fo-DEPt|B=w&9vduaLkq588)BpFZ}E2|W-;Cb2gf`DUZ`C&)tvKA zAHDX=BqdDWc3f$bXDk@T;!twz?4CaT6jcI>9`DdAu$zg8>L4;}G+Mi6o$!Lr(FW@EdEPuLL?Lt2 zQ>%iZrT7~bE=bX>HTE7@Ukk6!6k;^f$8Y>r!a>DLK;oyjxV(O%VSCiD=;!OtN-j+Y z9hv+2#yhOFe-ga%M-29oe_XbbBii0Cdn?KFEms%v)w*Q|D|ZW72K4Llq8WN+|2oAM zE$I)-(XwePX=zLgqn6+0A?t`5EUcoBmd7x&6y?C7);qrWMw$;cgmm1Mlk><&8$9eW z+4}Oj!lroYNNn&xP_F1|Lmn9T`%=GtanSgn7Yg0I^=W**2;CS*;`Cu!^8#Ljcm_*7 zTc=Rvmpa(Rc#X%ePXOhMJeaT^#^EQ?gwTTj{Knm}Zo;Va^F^m}zT&XQ)s&f(pmp^F zGcvuCDhMvoL-Uor;Dh2q2ZnKysSs7#`9F7{tf6yzY6E&C5`rK9J}J@6NVw=sRH-D? zvPxugT@r9}Tft3(Us2C`P5f=Vie@A@pN^+Vi1}5B!Xjs67azvpSxm!TRpzf$7Gk&y zIMxn=bX(FE0!Ca!9GgYCCL^yJJy*jfX*yNdYkp|TuVn=nD+nQU-p);ZgfHx8)>~Q& zQ{;e2ON{&n+LR?YmB@VKAQfVU^dY7bQjWEZehn&YLanO~IjS~r1!uNgi+qrg)~9`c zwa^ea!HS|GNDqAMP^}Bf5%CGE+-p&pn}9tL@D{LOu%YsobG6E6p)6 zF9*{6TQGc(sb-jw&%ZV`cSiKsRh5FZBHz4XkWKR$d(%g&(C?$Ym-|lzZa&ys2k2>C z`Yhua63oJH^>GQ*q*WC>hWo!GyHxP*keZHiqRi6RJ~oZw07GHAz0BTHROPJy>E5Oek(nN8%rhLT-|2$)CXTO*%md>9|jDA*aZC?thIC7#|UOj z(*jj$?i3q(BR}w}xr{Bw=wCSasaVegUaIjzFMeodh9rIn4~UB?!gwzBtY( zD1eg}GebK|cgxkVei;}RD4m^>I@T1)v)dw$L|29uP4s!8uARcMpR-_s0rb?%I+5aL ziVHarSQ7s7X3`N4pC4IoG$>e=S#T1% z(h(P4aDrp+z^64Wa}Yk-t#NzJ<}a3HZlUt8w$vneY*=%5P5sLEY4|xN=(F`mWp{cS zjex3v+Xz&5{z`05zKX3hbuO4v9LeI2kSZ~+7)T=jiNFW}mo5D7vlCHqs*pezbv|Zz zx$~U*rmmzCW4KR3+!6=c|&Yu{qdPkvI0qVm+9xedBV#f`Cgr}!U={=s_#Y< z`(FMq-E(G?^V*$|;ZxdYQ)E)@kQAj!;u zzUB1_AX4_D;Ju(f4$%RpCKQ+ioINJ9G(xjE#WaW@&(6(m&x$3jZagMy7Bag)Nh92M zI0%cx2(?*uD+|s_5ge6xZ5$3*QA}leIgkv!4Jh0CY%mu$6IzXsgWF5nvsu&YC6X&Y zJswN-Yv~NdWGdAit`8GU3*c*?LcSNdn(5ylG1 zk}8g8tCHoLPLxrIl-#|NTJW6=iI?9{^84k+GA<;7e3ZV?_E#7OG?Lv%AGBXENQ9K5 zf;uB^HQEV2#EhXy0W+Ut3J8u$2jFtoVOjI?l3MGLR%&C@ms8@^djKuLXmdNib)Dv#t%&?$o6>492oE~>lz;bXfI07 zG^>X((!o?9(cQs_>ZYliQ?q^GEyFIG;1xyha(hcdCj#t{+RyYYh~i-RQrZNt$pQ&^ z)7<~%=eB+)DJ7bfJnKVY+zNrh>`;3k`rw7g5x^OQoLxv%^B+Q=YAgj5ePwnoxHs)> zd`CWZm+}v=owqz_Ti^>UG)N&>5Hm}Lfux39Y9|7hk8H^7q~+~KW^0#rm%2_XrZUn! zdINUR-OW{sj_|aT)=sM9B!bHS4+^J)Srr%a4tg{@td*2QM0*@bxBrz<3p7 zGBoZ_-|4Y}@P0XCT5*tH@YilpMQ#gj+W4<>Pi6=eF)W~aeNY7Vfu*Q-;sVL!?5YXS_5QZOT zTV4YmBKk*WnHr628{=&71V5DOySMqIJ2WO88+^3WPpml>(BDMZ?a6l)%sK;qE(?n7 z>cWkN5PRikTFHomDV8gw61xG^uvpy&%gI%1_#We}7#_#9shV%knQdxbhDv0V+uFk$ zGmnr!BxYs*L;@ke999wk~HBQk|CHM zh8-9rN&x_;G?_I=ZBJV{8}$=$mN#&FFa2{}12WQZJ`PjwZpmsi(`}ui>EQjj;j%_o ziu|GOeP7FmkS-hv4A+S$abmLX5FgyBXJ5HSlXe{4(S1?WGqdy_oRMoxXxfC9Qi=in z%^LAo^Femovf##;iWCb9XI`h4(})MG1z2(H`4DPKkRaCgpn}p=g9Ze+;5WP!deJk! zZG3^+jA671yBGg2tbM;6`tS+l_ zY0kW+UN_hU)m__Zi)j?|sn~vr4N@FN`%(bfeuuCTlP@29vuD^egHf?)51&3Wtb|41l-xU>dLVJnhw7eOAK|XJig%;eK(4 zroF>qC#1bZ$PF4ixhuy5VPe1+&fTis>bkX@kc2uU>$u?cHi}W0$QX>4#$6k2!}J8F zDWpqF^LF@(_o5#Yk_Ro(?3H+^%;^pWUi|vf4WcjjHc%RYzp?BNp&pl>Ygk*j<(*@B z-?`AZk-MO4XC&+w3^J7-`o#r*FFGrXKrp|TWinT6E4Q4;b5A0m>{bvO=jS>F&I~st z<+8kBn=D%@H^tj^X{NZ6P=ZGxK7n>^=b6Oox!Do-i#X-}n64CojJmvN3YP2~Ggmeb z5@9XvQT`J<{En=8bx2eq%YHMtd_}f5z72%dEI5&{$T_A+EvlNSElobR zPBsmpJJ~;s3_)3olE=IJ`&}8WdC{ag^a^(bT|Jz+F58|XQeHZ{Y^2$C_nXmq=Y@Y+ z0vV0Ix&nfARah0OP<-n7UCs}|@C2=DXa1`7nO|3Oxhh7sjnPn8Z5P@GPm2VOmT|Cu zmV`4+liIVMBeG-Ei?p}0MK9Y1lHtF@eHh~kim-D?Z?VqKqLNF!wSJLE1VFB2WG|97 zA*J#C7*sZtDJi6$)EHA0iCf^`)mE*{w@lW?$#2p#E^LzBAhOurHLg;g7(pySj1Y~* zUN`V1-GWrL)SdqK(V3hv{t-h2KrE~IuG3|ZqDYDvw>A$I)B=~UKHcJo{^=*;Jc=eQ z!p|%uNC4b%fFjS@7`d$bmWqv0xAveHHhf>FvZP-{h#H`88Q*@dYPONV3o+hY4dLVP z%8;?COr#f7>it{TrP#iEaqi<++XX)zu=0Ul4 z_Z3O&F+!?ZR#t%8D7hxRj5=@BTVzkrpIWKUWm`$0W1TCOzSGjVIklmD!G>LSepHZ{ z=dw)#y0Cir5GY+^!q&tw1hVUCjA9b+09fFN(V?d`EL5L;3|B^~2VsCE#L`LToo5y` z^_@)Jxa3<>9_dX1Vy$o%NBFAcyOhQG{u#n^vE@^&LG2LWd*OI*0WY>E?lV$|(|YLY z9|jJlaGwxG>4n>8F7IQ5cjh%@&}Yy)^8jL;@S86?F42}M1{xOy^pi9topjGirrxV~ zRfWt?G)k=!Oju{k(4l?H1_R1`f!3tq3RxZW4|3e}2|q*$Dz{h^t2&@(gj9fdO_lIn28Ow|~<( zHnz2x-Xxx+voz+U4n8M5^$UNrrKKa1?XAx+U4jxDfzJw4R~AstvuUylyTIuX?YbnY z+|b}bQL48VQm?3=dYvuq6zhtYU#wv>69WPk?I7=Snjhu_@E4rs-f)@aA*&DBZfoEt z|C~r(bye8JM1|}LS7}41?cRSm)ZY|zk6^$jKz~A}gAlhw`*@+WA&piiExKMd!ODH% zru7*aW4Bg9ajYx@mDJW{$)@yp00aHnUCvIOiZt@~l{-C-TG*+5i8P7RUS&}>3bu)Y zcWFV>Qj{cVtQa>m*Nw@Ej~0~7yMHK3EQ<2)sU&A*Qv>4)#lVed70(JzF|+Qs zG1!En^NUjODl-0vl z6^s5uov@^Vw?l;EJDTL!t~@}Qk2UHd6RTMu9GMo&EIfO?_I>Ys4*|}zz#uY)ob%vzfuST7vwAn-3$DiTd(jvi1!s*G z8R;IKvNQ~2T74*B3I=~+8cf`&R}i9y12ROSC{gokl)hs~l-CVFDTM$r1KI}@2#sPv z8;6PmD9mG3?me3i zEmGgvu}^@q#r-5XD9!cxLRnlKv`XY=c}L37yBF+2_8oeBFl0Wr2vH@b*CjtTH6!wbF{<7#PG!()yVVRg z`h)`o>{d8aM|c-X?cyXT>0S>-CT28Vgn`PJw9KZ#S_V$MvZa~GTAhe42uiYDohRdl zk3V;(KA>X3w5!h1xo^RVd3M*J_z={wcBF;qZ}5O=PVyjDz4eH_L5s=3(t|!Gx2nMW(P8Ah9>TS zEV$-G?P|2Q;Y~WGsfxh5NKZnP)q00F*u@w_^wt371*83CkplK z4D*QaiXu0tkr-fGv$SbhoWPuUGs2HPuOt&O zh!1X?2zJiQxXH%Je}oX{6Y`CVYJ!Zitxdv&yl|{4&nK>HCPPS)Xx_5T)_LN`~5XQ6O1JZ5x-2&XcfeF)5 zXqs}QQ*jFEA4(({Io`V_z@xP}U)M3cr4x3HO%R3%Zk{9sOOK0)TNpnkQ>&XT`N?5q zERCG4P|0AUCd_%ufBsQ^`T&1mcnQUW|KY9}*8#4i#Gte`VmTL8l_~Ra-u1F|+`Csr z^L94SNUfuAMU2d*$OeZ$=l#_aZzan+hs$mMow~Yq^cH_4VL>q%Otp$cv^ZiLg38V> zrC>ZLTT5@Va0b_rp!2FV4nT+w>h8->X*d01D!5!jW;+I6w<@`MVUmx&arL3C1_OLe zog30p%czCncrBn%VTRdri?%tsZjOR(!ZYgrs7vm4=A;vWo~Z6ZY~ zdK+@hx9JKqM5+?d3+B1bBlgyqk1>tsw`c9xk2A zZ6;j@x|H$h82Yf!77vq5Z0zW!CB}L997gzG$?BGrenWr zPT44&{*<8>Whlkx6juE??7lJ>0*e`Il4H;}pLIHMk0Z$;5MPJ?DvoaXLi_ftin=(D zmfE4QIq$a9NNI;yiK=SFNy@F6f(pQ#eKtQMuVz8h*S#LNp@8AW^O z_6QUM;RIsZ%BaAPV#BZwzzkLE8f;*>mjh}CrnB)o8Dxs+d6nY0;~6?amZkv1DA^bp zfj(WTrKlc2Qqn}17TvcTmRPtitqJ^=Dzt#f9XLv{aK>m`eN4a4C6VbWz>c9Mh?E)B zT4Cs`n&5HTP`HKb8(#Dn%5%@4+zJN~s6Yy;=S$~ueS%8&_t0u6zxgw~; zcqn}BT{_X0!8(Z4yUnc)x(^m*#&^Ngf6*Tw2yd)FURrW_H9*h3($IPjID6hwBYM%{ z?-{R*#E|)mCp@m2Tx0<>7Q!EATw%?E4*er_EzoD1iQjAQCb#WK1Y&h-xC^wn@4Kl| zlZbA8pvXMpHQ4zDFtq()5j7slKkTpcF7zWZ9jUa{uLCL)Mg2-IzzlO;+yLfebPa=8 z&0~KBkx9UJjIP08iv3zbJqV&>mwcK)z&hM}c`(00&>++%n8+@IzsLfhv}5RPLIFdOJA(@Q7eerjo@pT+Uw73 zni@L9%iy9k@EVJ$!HK9XI&{GAVvSclGV^MslG+j0aHklj^PAayHCXb^(tZ~R)h!AEBVNak>4{uF3rliox-7J0fh<|ad&GC?L+QAqtNNpi_bIZ_G6}fjgVb@0#VOC)K z|2RyP!;0k)YYKZpb5Uu2LitZrf16j&VeB;?=;JkqKO@crmg-(@cJ#zdIce^I@qY$& zaFufE2^Rj;zS6Yr8hfcutR^#gntX&1xI!-cDQcQUeB;k{@DPV4T1<{yL(lt$QBED< zOUOt|;zp#z`B}z}jN6ib=v!k8`4Ivj(B-%Hrme_$2N%!f$_Q#on;H{ga~l<|l=@=w zfEkp^6?40+3K)*OhFL=0 zrSnbo7!`)-zd8gkE8l^K#Ayd;Bd%iKi80{l-fsGTtcx0}=#7vJ@GTBumg4xvOns zL4PT?vXTw|s*PluowkSowE$QWA21yT<+hYxy;(|Xv(#YTG#J#X-Lo^4Ks$YQU`KU7 zZ))y}@Ed*YuSNlGV$SyXZXp5P%-_ISW`o4$>LLf`8;Y!jIgNnA@`4r@+iNYi#e4Fe zsw6aMp}1n~sV%ME&|zlH?hNB4e}NJB}vOiBZn6;jG(uQG^Q^_-8q zUE!8@C-mU~tBz#c$Y4dxZ21lS5CIIH7L|ILs~x|^K*?Vm>t}xdp5D}$AhnCpGr%w zeS`mvW$MU-RcQCo-;Y{~DS+YYdH8*VutK_Pu2v)dJ4|BOTncn%Dw(d?B|mDSO7Ol4&d6^yvv<$ewwc z2F|)I8SNndq&%_gesrkVg_(Z@f}ES+=FagZnZepQ2)jQ)KSr6;5&z1GGQ?3vyY;9L z1&3mD9C4el?{!(qSB>raPWd@w1pWvf~G(c}`0t{gpuG=EG=oT3XL? zs!VZGXXONmY5_X;fBZF07lDt+3iJMB34TtSVwGUl>74F$DB0(SmGD(^m8Wt$cT>gW zU^KF%DkPZu!EK=af|N$`l3}DVIO^Je<*_trkE;nF_=08t?4g=Xos0U=GVq7plX)E~ zm3=Ji<()%RN8M^GoQN9sDpyM^4E5Xr24K5;{3zAebADvDku{(}vh4*}q!Ftya=z0c;07*M9C}BnfHoCj+$K zMmsfppiRiCfS0EIq1xoEwJeI|y9)ZpFs4Ox0>L{|+06lxY@zkfS(!-S#B+ijK2&XU z@(!6EwHgm-uI}tP3@6ct6nkfmp0u!3VVejz=(-J<$fIGx?8&=f4ekibW0q4V->hqX zgGrI`y*6LGwr(}eCEJUlJPGklPOI>_p4OjnMzdyV zC<~pt^`YTjeH2I!`0Fpu?G_+1p`koW)#sr*mLBoj* zqY;Qtu6RPUZJfTHq-OV}N1168B|fbr^UAm5RhP95+8&Nn)2u21F~VV=I+ra(5oV*> zVN@tQ4BSKUBayqZMYHuIH!|HQ<}-DZcMBj+nmAq~wgD0BChQ9mqGuBfsy56A_y)={ zyeM}7&~^apPR~;DZXiy?lIzM%hK9PA`35Tjza`*8M#ZjU)3_2+4wlB6`s?%0C@uc< zY>9mS>#}O^UI)MDO*)mvd(PQd5SP=Q34i)*1|@uDBwB*&v7XHwA31HGH_LH2XN0bWWF>{>eH-(>vtxLRDwF*2PZt^+}X z0+xuQfsO(J){;MXO|;x6If)4)F=0RP$#n60wj=D2Ry`NQt?conWZxT_OAm-S@)s%nWht-^(|oZiHVm?~!o!v*6b z&fYgRi7eW{Z5dwaY2e=$-KEof3B17{gw8P7!%{Ji>}_NP)wG?bR6PbY0+_?>#Ktq zs2}<5eo;1oLrNi9NJtzqjS%3DI87hu!bx=l&|X{fXOeY#q~iT8!lY1Vx(De=<}e=3 zTLwYTvnW-Qp1i>^J28qYvTF>~u9zE|{7&PyooxNGdd=om_OKF0%CLW9VGf-81sY&_ zH9;@rhDnsdJc~Pyh*aK_*_Prr!<9;NIrWQ3UT7CxxEcr|({G9@PbW!(Qbs3;ehz-Qtz!JPuA)w0M zY>`ZfGYhSHbc*sO{L!H_P&qDO>k{jz6eRSX?<1p$_@c#Lh8AQoj$G!WpX*nFTmk9j zJaF{Q{qr`oy}RDNZ~cSv9E}#zdoqKjYf6ByW|(LoZ;{GIKoRAVzSjYg|F2Q7hTbcw z>z2?w$M6XbIV;8t2+Kmq=HT2D59=Yj7n`Ig^j4@O$3HTYQ=vBYO77BVieKCQnDc)7 zhuEjU5WF?3wvbxp)-~M;{Lmzc2f!WsVc^m~vg?{=0tr0yogN5%%Q5@0$nt&}%Vs9t z435yTxC-1K^wgvd|~BN&P;`(l+Iq@gMh+mz(ecUXcC}=^-ZK;R_6dl+Z>7k24#t^XnQ# zlq?e#uax!jHh$t*=Q-^|^7a}_qSd0MW8MIAv5lJ!eZvt?U4hn&k&eATVgl$nWiZb% zMzEL%Akh>8f@|I)MAA}+~;4MPO9>lo}dLLgGu)p7ZLqh5@4`xyuB18dY8ucZa6-+ z3jG_dc)AR9pmPNoqsOXCLhJSLnrP2{10*VV%r#VBq(^`=eW5HV>+?T^1Iem4sxw%A zS^!d}{)Yk&ikrJhFv$D>KsZCj(r!Xh!tq4OSrdl@T`zYmLSFeO$0esJAe(c!D8|-u zP3R+YEwf3IToBv5JR;csSRW3F)=ZxaTfB|0$WiJ{@5a^L|NWQ@8Q?i~`lzSQQiE?0 zoj{?4CKVTb9M^~53M3Mn0f3-*Wc2Ibx)LZQKMeqM{eTi_@$jw-ESa>~n~NFjI+7Yx z6AfXweKH5JN@O8&qI>ZHLBZ|b3Yo!PsV*tE9jg&}J$N(Ct(g_m8v~9tOJ{blskF zQ*|=E?5?PH`3af zZf1POSJMiLuagLDM0`0#OAsDUhKnJL;?M{7x#B*$Bjht>fsd64zpob{^CA)8Y>6r) zYzz@~!lioP-0>_%6W8Bn^Wk+oK}w>diFoBfv0dtId} zMNNI44SX_C;S|~m^dnz~hdD3Iz>7$soEtnQR%Zh-|4^7scm3Z8qYQJh?9XOCg9qgy zAuy|!C~%n1aW#~m2q=;=UCa>~iqMnaXo0l@^5m<^8s@$tb3#x4@CjJXmsw*-%**{KN=y9g z#$IFd`D?fF{~)6h)_j#&3zw4Q_KA6cS$9BXx^5k!fWVm{J!3sI6A6LGPSq2l9bbfe zE;+Ya9lCyNO6pYtZUk(l+3rJ6#i%+uT=P|As`1qvzXY=?byco}xMtqK)^0G!?Uh_P zaG9;>X$7_KC^YH(jVo3dDYFz3wl`wHeYEr_IS#w-$OW7T!+*p|{pK%npSTy$i|E@n zaO&JK>?7Ty38n3h%9Unj_53lfSz!ApEnr z!q~*33gkR|t-Hz3{0^Yq4w$jiW-AB=`2Q$TvHE?LvvLZCOCcrw*ef^SFVDK+MtG*T zy4n+2T+AL^>ZcI{gK0ONvO67sZotg$cllgUvcR zUD3_l*$xhKnkZ7U>B|PE*tqII%59Q0%7Yyit#S2DyOIlp1zIRB z@Q2}|&`RQv&;mbwydezAseT+uew@txMhj_cM)~n~-f9yfEDn-!c|f^Tl_6$J@c_;n zi9v7}Wzh$18dCVc01l3)#WpS9im8ZaW(tEQDBF6BerqB5#0vZ^7D3ER`}tkijeSj2 zb6GnMuO-me%B+1!uZ5%91P6-I2;BqG3dD6Ie7G4MgQT(V;0K9m%9@F-wEJN$G;U)t zyzn`v=Bw881d=ohIoj9x9NpWtYit83<|4e)GX>>k`Cf4XK!_q49(POAi`*X=cvk74 z&lK0PoT`=KiB83xqq}`YgXW}kV5Hk!YVFkzwglm^5-0AiISn@bAL<|~(KUE{ELFAm z{-j&F6E4)bIvF5bGrW(keXyNROc)o`bObokp3nI-Nu@APpHXhPyhD2)Q%Bq+ax$>g zkjg|N+aAM{Pk_u|jBg#1dh}z3KPTwEo``r*9k|BOj|;GOb#6Cl}WagJ5>OHFjV+s{eYwDE?}*h-Z8yhA~G z=`93s7ZjEZY?Z3CFM!OkSBxbfyr@)3NII;8adG~%y+Z^leh-Bk>-9eHx6NI`MIjU? zv9Rc0QU^Pi5C9S$Hw8U!k;5Ahv-4BADeh^UQ8<=B^%_|t9kCnmYHS#*2B}8O`n}F>6gqv`F%o zbL4p^WUP@}5`l3|zq*mQx|M6s>~XPpXvh0{1po@zFsxc$a>XAoA+4q#Q-w9%JUpF{ zV6j#`a|E;mvWp9H*R2=Et=|N~;Xmx4CmIA=)$rz`apu+%^y|^}b^y*=1-twUO!cXrD;gZ`K@z>Q*9MhQoZ&}IM zUX^EU-Fk)zNH*Fd*+F`Xu=VO(Tv1a9L6qBDhw8uuO_v-3WZI;5if?yj6_0+B7H3YP zaK2IzB6&X#4G)!hHIh58pi6;spfbTBaqR3;1pB27ftD;}sE_YN2Y%X7jd5VO_*s0S zG-76 zPw${9@pQJ516`0faoYXbDXG5wCvmK`BFP$q+5k%D`CnfGz}*QB=5T-cyB%UTwPV(( zD^hkb!^stQ*ULY~E&D~1X7tfdQYgxDt|heN6k(i7uV}VBO9y^juz1SXt!AIfNAf3o zUTkqb8~{*wbD*tD0DB<~F%6q1y0Q zwP6cy12{5qhy7AsZNKi)rHe4@uI=J#tcZ>I7@;pS7YXT~y;Mh3@RJ?1L*FZeJwNA- z7LMgTzH_TTTKF8&Lxwd4nQ~|G^1+U4%Xb&B3rPh9UU%#X-`g;kKC`_3-?0vm>Xj>B zBSL-I#;%jn#gII)K?*7iiZL07km7-0IwGLWXiSt*a_bjvt#i$NjgZ_x-A6xlxa>gG znVhbEt@_@#@-G~a*j*ip{XmT_0cmZF#a)(P%`JSHbxiFU4$~boTKg+@ zNGf5S%MCgPCgPkxh>V?eb_zM3t0=;B^o@~69u$>a_s;Q4^42pzdSxJ{Nbt@UHL4Zq z$m2SH1FY$^hZWu=pwq5?KUS4<(qHM>qaG!tWe}6~rZuQSq@MDkjbhh{L&}lMezGTI z^SBu|*oh0qB*M{l<`*>}r^N5EY$E{biiN}Gy+SBMPNMQWNrI7HGd0spmA~pN$k(bt zOTr*9V7;AN>;&E}I1>vE5NVzQRp;sWEf#DanlrEaz|5g_ar|0mzl8=MS8?pPk*qrp zwt}qOomuPQik9W*tOud9?gXWk5}+OBiwj^${Nj}AK^X6*!5p}`*cZeA6w+$eUbcU(;jLwpkl zK&BAu%q`~=yOx2xH&7s#H`N0!WM7jMMBXo6R(grj9Ry$S^y&F` zO$<%(c|W+a*z|gsM1ztOqTujcE(F3>$3ev@E&AVum0*{m92Y&Mpd7;`A4fbDnz#j_ z^wtKZruqvZX~e}!?vv2F$|+HDEN@i2hZrV;#M6x3Z$#RR3}b-@x%2-1;{!B4$({x~ zCAQOzj`??2t7P^BNwO(rj|%1l0a0Wr26;3Jj)ZNO&M|+o7|n#I$9$Mx8l3;hKAtP` z^0vOCKEx1@M|FseDsZ0&#MAW zr?ZZV4_gR~a3_Y1T*{a|Ru#owvSg%$iE{x#Ox*%3BQZ-bC^3l@(8im+P?<;RUj1h+ z?+!0~N3_jGqf$6zBBM|RRTF^8{+i%(1fWRx#P&g#hYb`0ci)GcWltwm5ZN?CZ(X&0 zRJ^j7M$AhHiUx{Ig_buWx4`a_;Q#mv3lK=$O))8TLChT``VU-H(kndboX%4Nangy1f z*0-rHtWL9j#O~=EP4=h;M1e3iQgFBmXNssVH#8P$2Ee+quH4=2f;_cansmepVms03 zzzt-;+7xJ`RZE`Bm5pg|&*P8(Qa(<6;6BoG7pw`AzG$ZPdz!EtH*%dAZ!k3+jjP^l zuh)S_w@w`#RZ3h|C4*0umv%>_$hcgHmUR3+2{<-`bfl{KNDBK&)Oz$XKjzi0MABv1 zXdZQ8Q?SX6QvCPL0)EVF`lAOiVoM!HOsl+uz2jVPo`#~A%E?JUHuWC?qKAFfVNc`B zdSt1K9#MaPt)x#E9sol?yuZ@6ZjPUQJm&z}xfaSh*NlED2oY>Zk3!TvGTHc?@xD&B z5u*eWvvc(j3wdOnen&or#R3`}W~ETh?hYn4g4V~5z2m3*c~Gikx_KxEHTWCe`^_mr zMbp+_OX~3~8ENNSUGYP){myJGcSs{R7>5D?gN1VJ91_M~#W&Y)#VleX?=0ye*zsnj z&zJ?9YEpOx;KJr<15IGykip3#cPZEfFr5msJU&d6ifB;}i$2X_y~ZFCo~T0clHS!q z)#Z4umQSis4Z790d%Hr4-EfCF_JT*pO2vF$45F*VoYYk_nRwi!#qT=Y1C4Zill$9x zR#A;L<&P*sC0)1oX+erX=?r5dT>^c#c6>XIhdAfH*nSh43SlkLjL%l9Mp;OsyB(Rc zK{l8jx&zgMs4MFyx=7&-zsm(}-|UjX5_tMB)I9*2v35A&tNCjTNd7fz;$@q_NZVL* z5~Lzw@Sl1&*T*=Hu4PNoNw=+hKE;>t78YX!Tknk&;zYV@(d}+MEUFeJ7?+<_GI=l@ zsJ6#<`wiYyy4)qiJZrgvo`XSnA$pubRp5WD8cN89Q|fXiB9f36;t}t<+ceGA?6~;K zA>c|wnaX_41I+p~ke3y+bPd!!+@w*1xK3WYdNz>ClMrVOP#Mes*GfZgEsy1b2Zo}> z_1^7&?Rcj4fwxLxBQi$R;T*JWAUcelYkMCTm0%s3hnA`U)$4}abQG-_o8()g@IgSr zrsC@P^}_D5scsQvtCL8!I}MJ~KLf)4Vwv4JHu=C**F9En#h&}x54jk+1;RKL zWo5)G5hXJQaxPiJXZ1p_yKpc%I7~rTCf z*glN@rqiWkG`YRKkZGRrzs6;t4!*&|@w%t!t+uOe+K<<3L%YP{um;Q5j*@_t#A#&V zP+S3foZH$FHoeOde7ercrUhws2n+IQ@O(|yQXq&n?;&}-JMGv*yCfOqAgx_#ht$%q z12R)EY}??z$9(Yplj<;?org32ojmW`m(aU&04quLpj&PLN41@pSIPh#Z!)1nH+*8x zU`)koW%yhjO<_3_Hu3Vdr~7u?PdfZJqAIy4tylmDH&GzdDW=&@msM}kJn(r~} zm8VN|?sn|cz72%vA*fUHIKwQzJ28I}3KW%jPwCy3Emjzew{{q79l7&g8w%)vVt>)_|jLK)f1c`u+ zz}igrlnK@GF&&H$Xb^(dUf+E=-cPk1ttrreKlDBKRf>iP97-?9-m{n*OL6|9CPJB= zd@YuQE5K_h*Ht(?y&lu{_|DYKq>+gua|D(r`+Z$;a@kqFe@`9d94OD;Va&H{?-$)= z2Mf##Q%D!iu*L|q8oJ9Wmibx341y)4WVrgSFr;7t?nL|y)tA(Q%wJbcMBqj`45Lk( zzACR9=^c`2P}fj+gdi`fjRovP# zQcs#ydcLT^Te7;80@^-$ulx0XMxC16IkEUAdlA8MXFxif$ArY%_`1GAkBrZuKR|^o z=JJ6EM7ukuJ4K%Oc1Ox|kGo@m4@#&mqf!%R^ZE8hReaT~q_oayEa(-ab(?79ins`4 za8U9@^lo7o94rF4lE;NS$BuuT9T1k0OeaPLA|^{Si5eEGGoj&RX|q$t)xu65 z;9E=J9DLp7O}G%KG#L(6xlFC6ye|IP{zN&2I`efuOo3Fh62H2s19<`+jgZdFYch0t z_gf7W6IF)Gx6e>?RuZ?bVg7BGW$Pzq47maVvcmZj;@FxSyk;g6f%`O2pWY6HTlJ;!%!U-M&k1DRv)=TC5KgI4Jyyhf4Hyv#yj*$K2b6YCDCNX-= z>rU0-u*pmNBU4juZLm1IXWB#}48Q)d)kj)!Y$Mh@*d5_I%ARC@B&!Tqk%mwPJx3IO z@zFKXPR%)9+@sLhSwFs(%g+gnq5>x~A6CRKNbL8}h&(4o*Q^}tk?-w}D`;H;w)ka5 zgqlDC3|e~h?{#=m5B8jL?Dz;Cr(3*yY@Rz|k+2Rp9MJu0zYAL&Iwe zL7?h@D5Q(BF?hbu2EJ1=J!zWRJoDcJ&dq$zcn(dBdg^Y#HMz0dJaL0W)SucGZy`z6{p3QQ{0=E{VILuiH&H zZeAw>UmL2=iPfV7AUZQKhngX=TM-+6*g0ttYxzu3PKLs?uQz(Q!kzRx&4C zc!)T47z=B^7ra5vEYc-287D1FYb*Z zy2V+dt|6f3hIR%Zhu}~JDvM)6_RPyp^GYaO?+)l&S20B5mL{i@v%}W`h>BJ?YBeQe zoct5;JKv&#ts#KG)-dP`i6K?dbQIMy7tCtfxTM(}y~b9z{KX9WdD+a6LzUD>8Z#rV zbB0P#g-c@-!+8|gkmF*pkFy@xdzKDD%Y^47-QqW|ly6Q3TlHAAWdE3B3I56Z!~lJT zUTt?PzAVdNHM-Nt1ShOEdo{DZ}E0nC$ zK$it;Tx*ZW0Ail6xSkf=*wCbjQT;BKDM`Rkl8@7eqK5rTlVDkjN6-i8?zcdDRq-gb zk_q<^ezYl5Q4}4Mi{{~#KBUnUY@vgYJpIQ@JA%}iL?eklZoDw63C#*R$vD+|HQ zjJ(k%Y{PBz37JjcZHS)m5Mt1+6-MZ=g5Efg0(9NI&5p=h#o+I8|D=?xh~TrPl*>8l zG#jn`ajM*6>c9vsI{7E?hhkgR)^e@zhILidl0vFt&5?8%-tH)Y3}riQ&@TA*w7mFa zH?6>g#5s4+q}S#W7n;jpz_#8?wSMzjgKVq%{0hlzF9d|ku4UZYW%QFc#f36mc>JrM z;xlX-HIi%Uf=q12MCJGeu~p{o7zydZ_w(5_M*(i;6yAU^pUBAtlYM*XGIe)#UYroQ zzLkA=2MRLDe_qTRr=h6Pk6cO%>-A2~}hB~(m1BYuZS z<~L(wK?rwAm~Kv~^BcPO(|4v65R%N(_k0(n`BYmK|}`7A;o{enu4x zZG(ikAi6LqAnN}+^{b9{vP;|i9d0#rk4)swnbg{ELL#5J`M2@?(5*RE(E7JZk*&lX zzn|qDzRg^epp%t!D0HwXwc$F|ArX1JUH4vWGQc^iC{ea_UV-M24n1kr4r&(dpjtxglNwh_09q;l$eWo^0ft^0Ve3txMN+o#2#bZ#$3!kLkp``Njir{9NGi-Jy`e*M7^_}DWAwV4b36fIpXLB_jiXaLJLOQam0)eG zy_?4Ns}~Fb4uM4VN{9ynjX=PP*ObMCCnuk7hSlw>doA9) zq5N*zl`CS)Q^l^kD(5I(?d)Q1E;HQOqP5PCI`!3-SFP72?3IWZGMl7RRkL)$r)79c84l~ypv*@b7P;>If@?~Vj;QQvy zw$(Y{HO#0}+966;4t8=a434~50rN8resIzBW&faO!(|t+geTaqVj4jJM6kG}hb-UY zczi7`wDM0(a^(yiu>~cRX})sqaqM1bqDX8}BSWXrD-_apSrzQ;R;R!)OckzZb>rzu5BcQ=l1i2j}EKvU4?bfUS|?UQAO^ zbg+;rxHV@m{qujG92^RP&JB?9fNhar5y^78s%-b!y*|#21w7`LtKfJppb!Tp#X~+# zsHyt{)NQ~ZO}@C{wN%lBg*MeM8}dvh?pn8d=vo>!m|*f0LY!r&q0E90ws6jK*()%< zEg>SCxF67_aQpL^pcvW3s+{M#HGAhMi!o)} zeChvjn;h2Em9bhW+!OeZ^Zdq?dMj&+2bmhHN~q4ZEN*3sS4bZrLnJImC$RrN4V(Mp z#@V=U+`!H*=qrn#(_Bu1Z0dU9G;Lg+j<9os-P@5eLG2r)C}YIh{QWhPS0{VV8g3l^ zt#7q1@Bh|L-pbS!q~9yKGaL;jL)m|!HGE2AuNUt=_>}2pWeH&3%|zT0G^~p_vVgiu zb;*#Sfub#2oYi8AE7943$6AT3-p?XWUNx#(X(;CCEv?p+e}|-#@&_+Yj`%C5T6mxY z-29mrM7_+YHIKN}uuBwGtyMc9++QcXJCq0+dqE25VTGG7S^kRsd!vHU=y5{Q-$N{uEYaZNw8@TO-z#i8AqOqw;y05aztT}HM=l|Ys@6R zHi3h5-J}A2zX|-TTr}aC+-%~egSQMiYWZP8mo%%1S@e zk73c=x5Wc%OZ6c)Pyf&L&9X(Api~*CMQ#|g2XEnwQ`oo z&f0v>>?ERq<-}kzbFe-GIkU z64263e@k|r^o+Q|#2SWS4u2uj$1dopc6pHx0%ujWf?yK77=d!_R^%XeAMH!Cz<><3 zD$J7}r~eW_U6f|bB?=6@z+c8(j}TOIqNKW?+OWdfT9Fu^?R{wi-j>l|JBC^8;^HTD zsKIwZpD}{+>_kNio`oCptF2|VDr(M4DpNxd3KGwfbF+GJgY;rO$T&7&!Dg5w03&~Z zcZTa^Nb;+T?xYgWY&`lzhR57zm`iJ#^Hh2yQc+ZMk4h`JDdy=Qhb8>2;Ot*M>k?9`OQOh_lA~ZUg4lH*X+Fef zoKDN~*K0B@5mK4hQU?^Aw{)t;9@dIWD9_73sfTLd`);zPCC;y{shuH5dLu-K zeIdY2h{ITeVf?59dp-Et5Q-djUVG+}=?UDyYR-Lsb@#QWe3f-$SAdngaus4J{S)2D zsFHsu>R=Yal9b%S2EPh4DkHjun2^D5b}1UE071AG*DV|0EbX!$F)JYv_QUPm6%5ee zKtl`mDN5OOQs+wm%KjI{1V9OW!;WR=mCqOvYCY%*hav_{ADq^^$+G8T+2*&Y`}gs< zuO+Uo8xVIQ1lPh%5J!kAu92E2K>%^G@tV(WK*YMEWNDjC*{ttsXXH1IrKKXjLdw2w zKVPkwa;YM6lgLl2T(2n0&G3NNM75+;vrvS1{Ahty2I=_T|3(>3m>y z^-BXGh_`QmdF_PCkLYy^k8rGey~>5dQc zX)oYMY0Y#`2{UV6I&V?K&)siOSJVZ%jLAQfA`!T3#bBI&8xRSe8B}4+JrX*GCTCRv z?u2t>byVfIWADndmagl-wb{bhYEiW-bx(~0Z^h67!>Ls?F7~c0JuO|JosL*-D=8F1 zeNp6um&fkhM78q%#6JHf?sSE=lgl9;4u($}b?E{+Xh=>0Tp`L?CMA#i$Weo`{X`WSARA2#YY(9)L;?v)JeCjxFlS4T!6-bQym z&|z`Ohw(5?naOc6L$|ExhourmB@Bk}5uEj{gYd8gAt}a}q^3POP{;inF!RJ&zs0P? z`-G?6%uptaCFku(tVPoO`Q_D1K@({vmuQ%N&-4(EHls=KH{>#mgP)^;`?CqD0XP-X zNo=8~6IHV=DsgxJmVW4?+hgGF!$)GrkDZ+ZTStm*^&|-J^7%1z%Pe`zZaPXmE@V%n zZ+JszB9;_)kD+vBqVlo6+lYE3-zNy3MDimW zMvL89TjwIqNoN^N;2SsTxWcT!{h(6cduj@5%(SF>lz$C!>=D2RRIOsR*k&*%8noGD zi@PU!6A8rIVgGmj`A9L z;OIjLLglI=?-{)CKJm)xbu|x+_l>LF43zgz?MUp|Dg!j{OyXcfn3^ zsPug6@|Mg$6J=3K?zTybm^PwC7)aZ5AqtP_L&wklulWdPD4BS&F>0V? z_~$Phi@2bfLJ%xa7&T%pUgI+$zWJD!AN(~StBbJ;ncmd2+^_nuN)bpffH)PvLfIpw z`h&~@76i2Mb7aMD-19-vWMb@@UQo2%8Ir+hT##^C+St4xrrZ}ED9jPnW?B8?3j~`7+=--BO&ETs|&}-5J_-SivyBk zNx6OMI8*y5**klX49=1r0`Ucs8A)@4Czx~L)bxi#jfuetV)aO5p+!{S! zr=&jSdI;)0E9`d>;-tzOqMbF8{zm**s1CN{kFu!?^S5&5J4j+A*4n!4p+q=XlG~i5 zfFp3?b~L-#kXVD6-%4fCKcaU2wv*XfZZB<9@ji(MMp#&FidaKY%3)$NC&P-4gx3XO z>_n+YWU6Fb7(aJ^&|JejpmV}Wlj7%KcJ$E81mV7_4N@P5lgm%bWF^4H9LuGxh0AbT z>i(M7S4%ovO=yw>+OBZOjh6;__omqjsEeFdE=Q7xGeb-Mo{!o)1o4P86~n>S3H0}qhp2e!xfiRYTiuu zOJJ=ot6P~=D>ScH>dM_Qlx_zkO}&{=0B84aO}_;r+xt#%GmzaFJi~o2e$PlX>H-p4 z6(ZmukpM)E={xzY+YfOGUPQB8#0rOCRyA=+=DMW2 z8bYW%k-II1ygI~56n(MZ8^=g;Q%L#xt0Z$quFOH%N2FNVgX%B!KtDybivyB-+D3oE zY_oNG?eYflY!xjphq_B+47_y6N)EXT)}urCola%A&r+LmZpHq}dSSziDd)olu&Q ze9ezK+GMB(&-`YLH+M?F{q1DI$lrl{=4)zP3OeSxg8%wTwFQUtv`y|?4q>z zFY@#w)nOvgdJ0K8X?R)^ZlufA7izcqqIbp9%X6%1=0J>SjS)ixEFSLt{JC&nAQ`oA-T) zec;JmdEh!cAuD{zgr&rt^|ww+C9s*phv8UXh%&e)dAnvcnf6~SBD)Z-I?s+MO- zwIM$}}H6A&BEPuabw85&J?2bH|v4rXwBu&!@pb@S@ zB}KK)QeWGjxF;IY3i==j&k}K+*4+RNMHOKU=3NKF+Iic(6T`@G1ess`5u2LJRpQh4|$wZ0L z>jOWsV~v=G|4Yoj*lkKXmzM63zB$)7O9Y_k#M=aJ%QQvzeX;9rrfc2-O5_4*+EbUK zUZb83%p#n%fhHAOym!1W@(T#l`X2dOS0q4Vs?xsK7$N5WE>HO-Yg7A*u)cmzERzWl z0JQ20U^)604q&&X!2G#tF zOtH9D@I>jw`oFi+_S@&iu6Lk!r7yD=T0HtcB5&LQUtLq$qy!7$#776ruSo6de!uzA z-fNTsi{-6Wmx|u8y|v+rCq_%!$tvt7F7wdD4WXczNnc@6I9Hd4mV)OhCS9pn!7QAH z*azmcaGvF3FrexSGeUhnUinh_6>0Opud}Qg(8JVnSU9J;vw)!}Vl)eY_h<>a$bpKX z0h4t6c6@>PIdMMre=M)qm2Q-~uNqFSK)ZYh!(jM?PYi>3rq&u!3mqIfXuJgfc@ zIT)Xf!oql4C!d)ouJmwcbefPJ_=|iTEkeqH_S9H+tFlWZdd(D;SM54}5HxQzp=9)g z_9wbbFW~P|>*KNZh$`+oc}9nS!O{bj8VjAyR@~At9oyvPJ)=JBv#hwjJuG@k^^DMBp*# zgze-STy&PkqbqMq%;_4Y_zgY{;l1-5>5GB^j;$E;Z&9n!^5;z$RK*}Y3Wi7!L2l90 z!#T*)UKKWwcE>-meh5bA*S!8=!J8;O{`VIeny_!a5r!92rttV%oP#clyLlRAEro;D zo-niNs+q4batns74<_g`YNmN+-OaOsMi<+}$$pxcFq`T)=x+|}90#%Fp3}6jW>myPNqbAvUnwj@bCS~fN!m5~5rg&p>RxN_Q}Ao_(Z(eP z38Jp}Cr=bS<;;1AXKQYf&Cz)a*P3x;zvDRd+&Uq<8OgG9|CI zuQzR9l{5QDZP#HmSHz-?mQf60XonTK?4C+0Y3Aj{AZI8E;L*;y>{)^dysdgN2Q_wde}sICY8c>40K_%yd=87>1Ca8=e-$p^ZO)L@(2r^x zv&YMago3!quSqMZwXNC4j@JScH~{S8p(bEaa~cN6_&%e10GjlT`LhJj!C&kIKfn}G zh~f+iz3gHaF8vjs(Pq@o(N!+aae^-X^mORFUf<8a4a_5i)*(Ba4G!;XJWgkKL@%{Nm`g$4;vBCUCpY3(x^ESt zBOMALxep8!t^5}4p;H8StfiC00IBx#+7v(Xv!E}=CY3H|OxmJR`=`^q%WW3~!oll* zL50unf54$OkYEB<7~vRJ%7~nA%;pCDD30R|2F&PLqjjwH3J#9YU3Vn`sgLiivOBWw z>QzdTY#yP}nzPCaS*_R6gxDapaZ|fR%C*>f6|O$4q?) z1|5M^Q`73h;~^Zb>n6DB(Ymt1fJac&u%G$gr@z^ zT`RDdT)U_AY-yzxY^D-9eKwZX3dLPJ*T}`@8!y0PN*|h!)$MCbeK}YJMKZk_btQaj zWc&7iT&$8a^Zhz>K6VIVeJJPK$!{c1{6w@$?!5+|@?c?Wbo79ZT$g&&?=-XupbU12 zwkO>v4PnfmkUt4dbPD56ka;EeQV0!CYRB6I{EU2+N}=6&8txaYjtnxVzcM%=O0%y% zgingGs&sLg0oH0aw2(r2Js=i8{BHIzwe|^&WluOdm;KJ|(H!|`NGVBxNP)F)$eo9aSyT6A(k95iV`uCBWg>EK1twv^Q0P}mS?W?Oz_IhsM^T(xW;+7)U z_J0_u>PX}L7Y?D(S9D8#9`QJ!qay5mwoT9ieD@-Dy;uQRj!Ifahee{Fud&$Xhev2+ zKTqOHou)WOVG|=VZh+xl{An&>*G-!|W~u_rgep{)VhqfZh?0Odk594$^Ljb(voH0# zY`?RtUAJ@#L^_B=Amo&F=78GHN?|SYk4Df#+3Sh8S)-5f2fY?)Rs6#ICr^@aIZRZ} zm;zKpx+IeM39&?#!5SP`cHZTMAL16C4X|E5{=Leb9N^rH-rzwV+xt=G{4T$L`G>4t z_i}{YH*Yzi;wMpytTkebFYgEmrndS7&-ce8M*EzEsb5}zc;rIJ35=1;y5n=uVe9Qy zCf4~+MM)1bekzOdDM$AFA`=MX?v)R_+HapiVLZEqGMYn@u9YJ!eXWf52xnn`{u^Cv z;BBc>}Lv4r{KANZ?djX zlP%)0HMVs_lojv6Xltj3XgF+SQX*PXQM;c#fyLbflwWbQo8HLkcHUL5wBPj9`@1!K z^15?-KOB()l`Ad>48HE2%2GPhBbNdoNjGgApJsxLnV`Dc20UrqO#p1r<+-rGc55YI zUCnH@NZ92jkUB`rO*h#9Yr~ujJ@a_CxRGw{qrA3zYd3q_ukNtn_#yy#o2U;wwp{u* zwT8ZgXlsxYm)DMDY6c*^iUbF8?<_;)2Nx9d#|8X2i?^mnmwsFA9y;mT_wVt?M^qOP0s@Yey z9PNWgE=q#u<2iX71*t5H!V!t%lxx=9LiXV-Y-Tn-l44*!%Htuj?8h$2zUpeP!~AxJ z_!t+RO486djcW!pTDoqB&hUAh6ESiAND5x@chB+#fuAU&TFp3~8vZTLh5hbL6W~5l z?j8YK{+f}=+O;U;-^}L>0lSqOh2|C7%n~cgI)7{?_sP@+`iq+uy0IHLy@71mCAf~; zYLhZ)5Clg4TF^pG=RKcW)doOZjmekns=qkb!VFkmlCW;BL<9tgmvu>N6LcHy=rLV? zOO4vRdEe<+88Z5Wg_%C%J#x`|>cR-`V&Yfr-9p_dBgL|&lG-SDpvXnvoY+CJYo9uk69vLBGtTmn!lXP!`lyGe$&~NG+NrMKq$3tTFkmtDe^s0 zKyV?0Yv1yJjJM6*PRF~ozfp@D!R-8GUnkz;;&bp-QHg5(~h0m1hOH^3lG zlML%9hSb-m5PXm_xc=T*YXR1f7Nl_jRQnBn65OQ3hEV}aRr_h|JdTCKXj=M8mEUK@ zbh$I2bDTp+MFl8Y$}ro+`uI*cK!1xMKKj3qm7)KgHQy*7b-=~dPj~@8#~P?fwL$;s#wlj7JkGoipYn zDpE@H^+%4y&U(=7u;=60veR6sac$E;(f^L`@q+91#>Dj6pC7~^#6iNZRqlH~BWQbA zm_l4GGQ8=GtjLea*s&6UV3gbtmVzkcBfdEs&jV$W?3MOxDqXwlFkODE$6 z-ek8RY}VHUGkZFT0GFGFC3rzhO-32ciK`5mxs}5Sbk1aG&EO2ywc$u$d@MB{zuF1&e^&7VTNk#cm8qPi)^gx_Th@;9+UE$I7`(%OxctvV z5HjOSKKg1(n~`~{=Ta6mOU#ItifoCRWiOAygndi6P}WP88cXMstEpt@X+87$pbE%O z#zE@v-nTsjd4Su@>rnY;4)GC5L^&BF@K;vwlGpih=EXiAal>02j=%#>1NFyOtHMX( z3GGM!(cP9Qx!wKIu=$0doXc)3xIMS}i?Ep`1)Pi%WUru))&o+LqXkLL*Z zviaSVkWfM@KGEmP?kTHMOILyGm*lhPzx2xAtI+G2BpR**mfhk-d!EvSNLsmtZ9?@5 zYL6&Zg$9X|SI4`21y_}oZzt8pwQtrFW?|wgq(Fjv6olk|hGxRD3Bq}-|9%dXS4OVU zukEorp1xFN-;V^)5o4=U*D;5~jt?mCtxsXo+%%EqtP!!e@_sm3PC0WufUV4SN#DzUKx(jzurT1U0i6fGc=dtun2{f61D(h4AJ=KG0!OvG z^JR_n&bKC2%?x(()xbAgmt*aOB%rp1TJr=ZCx?QSzKiWy+zO{hNk#M3Vlk|}z18P) z2&CabquMf$-TPW<_pf|c&#}2XA!*t(ZB=?^?+Ii23n21Co&6QQB?K7C)S#*8PiwVOK*38w+xzoG2Ed{rQ?Z3)Wj@PtL)y8(VhN9yg zwflTi=yGutS%_?NE~lgZ^AIT~_4=snrh$+a>XPQOBjxAtIk7a+SuMDhrkD1`>IrnX zG=d2w5P&~E^I}bRdF4m(e*!aZuqHsKc}LNzpQgj<{JMniM^~PmJ#N~1EWGA@b^iqn z*@spab16+en{EiFAJuzJS>vF@N~=g70oL(?du}z^P_%sQ-@^7~8A7U1|FtJ2UF^N~ z=v_AY<=YItO+wD=LL|&a+ETJF4fuaS z6u%3>EbgG7@8Zfhxx`DcOu5xtd+>6m^%1I_b>&!XAfs7#Ce*=v}{(9IA0RM$(Nr{(kD>u$s@f5yp z3Z|e5w!jhCu1{Gn;F+!WwcLH=@`6#Kr{fPQA9GiFge!k>!gs)bF`YnB|4BDX{K182 zqc2?@DAkE0c{p=xj?qAkjF(ChBJBfml~eJ`qOvzhD7hOb#M@uPv3MB~*?~OXY7;pO z5j@2lKjD0dLtEgSl&Z4WBVgS+QZTBLm9q5yg=Z=0XYyaeBOsOMS>Bs=0^49EZz3&5 z2RvMSjX;?%g1?{_Gz6gP`FQi8AoEsBZ1nkILclhsK+xLtb)TO`Kwbd{ROF9sFIDAF z2Mp?$>3^CaXP!T@)4MyK^JUK=3>WG%x-Sj9K-J+kk`hEU;c)M*QOHAI&(SKI^g7NC zd7tc7dh16Ko%<_>IToEr_#DXyYedB!M7kwR3mlZ!Wsw7H(3Wg<;#VB6x``WG@1Ty^ zvQ&0-*KvYzY=_$uWfa6((2Vg{G#sVrSD6@h?{+P#58O6-lQ7C6WR}(&u6(61XndvV zgN0G32m+KM-oS%@i;re2S~S*su@5%?1F-JG&2vKef&`#=sw2-NHtXr{8~`KsoGh@e zm_hj{z3kw{iLK?;V_N_H+vrMlpy?}-LQ=0{v?d%NDt0G|Hz)+40&OB^o`qSKXka6s43F)-6>;FF&ypMID zz`dub61x?7mKl8uX0?&f+tD3Jx84as$T@re z^-Z2G-_kyOErbs1fP;Av$|XMA?o4El@8Ep1E5!iAhlY-0S`3Ip03fX!h}n6U6Qj;C zvw{>8AOMp9(@bw@PN14ileS)qw3~S$uk5hb|ESxI2|D#E)HWj4d5qcTK2c29_qr+@ zPUX5TAL^bRLO|;hNUTMg7(&Zx{_Ui%i@@zSG7hEp(r5MPl_i%N@F;DO z!}L3^?Oc-Ket`K%^h@HW8gD1ZXkCW96e@OcL`$$T{@UVd*lVpCo8++H->M~K zM!0<0%dnRQ6<5K4`UwREW z7=ILXKfjd?HN=fEwBI3$ZUgo_)L8sUZH|e17O>~0F*}y?U$1rdZnCMphhr6LvlFVC z`(82KbGJzet@PJo>vHW8SU9)?kaR%QotZ-3^^aW#iH@Q`X;WdNJV9 z#gN!i)VNgLmS+=={|O5dFhv7;^QL#hXNV@4{q+hRY+tlS;fI8*!sC^^}RxzuwdxbW5C!;?6R#g=+T$ zIwBn#r>nMA&&!C9ND|>{(}AzN6hwuUcB}Ntp)ezfOJT|Lc&CeTO zw;W5eM@-D|7FufXDT&+)k-ZR60$sMl9QCNr35INYTMk%?Ho6|gW-4K$?2;d34Eb4t ze3B|+PhQ+W*&naoL++`;0Z@4|CEQjWH0fa2Ru!LWf5Y5wZC(ylto;k?74Uh-v{})R zjub^jc$*Fr&wvpQ-zj_!Z#4$dm3;k2#ZNTSb7{REOk-QX$q2C~X2r{E4WW(bidsV~ zOfj6U*~|NEbQRtukyQ|JPah9W}zN4-M3~* znC0>LBDyP!lzrHHNLeHsLXEEm6sxF82%gj9gZi`RF+gaHdy2)ML1Ziyk(pn|CN0!} z_0xaoe|M6P52BD|DoTiVFwcSwPGz z^d#yj(rWK|S$mOP_{yQsgq4aFMyqEXO`70T#}|wW(a~T-0?QsMnB2tAqdfyhiikB< zd9k^ysmOXMde<%7g?H%np@5cRhN9e4?;S3uTOExB#n%9y%QREiisG}@X4jHWrHX9A z#L$nb6r3EB)OQ|6fdQps)V(M&Ea@nwFk7VAJ5K++Qs-lZ21{8ZR(fO2dCfchq> z3*Eo0JTZ!`7JXZoL0XSLf!lU7=%15c^esmj)^A>Zf zz5Fg}|Ee71Ng823U$_SM^2)PVjkH$$`h@_@c`YsoY~)4<-1fV{?z*Mt1{Lm2geZe1 zYmVmTyce&PigTet1=?4L6&n`SH}GKiFg*Z!Cp(uWDLIDdd$1XNUwVPYLsuM$=EG7&BIW>CMO*fUQpyozRz>EbggJs0?;^N3m63^}3C z5Q^`_&Ys7+@D}8qSp)cn@TXp3m%uzSD}Src4eyL)Nasdq_1dzRsoNif%PNvYlr4Hy zV{JDKCk`^Q=0SR@AM2xJ-dg!#mw zBh><=OYUCCDaaLa$JAjib6CWC0f8OZlCwSfza&Mnl76K+!pSRNsnJ>@oXGba z11#aOnb%c{h4;m()zN9NdZuZ~cqqukA#h^qe~$beA_ugB1=j2N2)8H80_h@zIOvy! zewh>HF%?OB;IR~mZ!BxIhs9(fEbvqiql)fbfM3|5fG^ys|3!s#Ebt*b=q6&ie0)IK zP~{~y#~3le?+zgdj{X{@SR|~`??Lnc4D^1@#TG(9VAi4#ya9APi^E1ieFo{O26AAy zD*iFG+O$EjG|XIc`!X#Q^X(3k6RzWfPxQn)$O zAe0t4K>v^10bS-LZF#tS$er;028zI)>E;Rf({?`=)q2GyUJz#uPPIfE>GBFB25uVgl9Hnfl z8|Y-@{Yvl4K5Z26ZuIPXHp&+#wxaKz>Ua3~YYp_F3c;Nexlt+@av*<%)cV1@|&}8 zjF72x=A^+MrLczWe}@SP*EU3Bh38OqC<(<~B(-Fi$w#d&0>E7CXvM~dVSLG6XIlGK;!~CZq>0X}z|XVjB$$-hHGSUmiK`Lc z0uX>BQYW{d5Fk(H%53k#bF8@BT;1$le~}9sH{?0w0-Q8Ut^77n>|~L@DX69!l=DU9 zChpFT1z>@U=n~$!nP_RmrD|5_kyI=mn~d5oyPXe~1gF=6d%u=S z*gz^Cp4cf4@`Llfp7v+R7I&|d*ut`vzBx?kF@J%EWnza0ivSC#kZA>=s(H2}+6tDj zT8Aem$d%RT)>xKRBi#&%a+ZQDkWPpoOr;b7-d_i7_mt! zOdJFYcx85n5FieX;trEZN?eX5Jh~}HGE!}W3|L#s4sj^sOht228uKR%UZj*5COIe} zesT)QEIC;kky6&nV?USjDzXYAN!VSsI6+k?WljA~<`phXy|{wo*!t|BqSYcX-hCQS z2BTQi{Hm7eR(F<$=a)(08$M?6C3ro89g;|wrFgRr@pILMu0wJU z74i;0+dU_1kq_CQC;J~xk>W$tMKX)YN=OP$6wWl>3Le;KUUv+NEH{s6bPZJ-B(B z_+bi%m4UBSgMz+p`_9;RUj5GH%8IN!2^Hn-&m?+C*~m)#o`gDSrhW8Vu?5_8%KQdzN7vxH}q8*|qRqVqwc!BS6w$*yryf zZ5$})*SBlVNvrP&&8}m51F~CCA52)*HZgAJTvX1)xo0U6gLB4VkA6}S*;W%VRLtZuXS+bEfA6$G z;?gxnLf=y}gHSS7|G_5S1$qXl0#!#PfrH*)9Fy1Z>5Ys^!p+9MZ~vDwuY6Rzc78RR z9v}e8ARu-Z4#B2=?63np)KBEj6*|}~)-d2XDw5`k+4geLO`ZwtLD|HE4BEp1IF=~CD(w%jDDZ!tDxB9|m z!20D63s+1@hs}QX^Z&A#kv2G^Qo_+_Fv%Ew2<5N&*@%Yd38qFDEhMla4HZ%g*WRxL zH-F(#Lm2(P@H6ZYePT&EWm$UMZM{@8BMpVoSRdXk&}q4t7)qAw9|YPXB1Juayp#)g zx)4FEm$q;#9@f4XmSnq@sH22F0XIQQ>GwNEohQCyq>sg?j_7GtnO&I^1n6tzf`*-2 z=axL4es^E&>;_3lAEZh~Hmw55#R4&ly3`wl-q+CU%%>l~+Dw_EzjmPuictQ^!ng=Z zK%}Qm>~?xtj#ij+U8|l=UpD1glE;Sq;>cth?itoCtvOwXyd}$X=i3sM(ccq?0wYlc z1HJ2&O42;bDMy{(Oo_|lMOWo@Ho%TmF=XvK9)Y9POm!k0}7_yu=CrIxYLzJ(78Cee!VMmVfiAE{cz#PZw4_g5mpIAkZ?Isr|;U5LB|q<}dq4i=yXORS{c8 zC@VkUEZEbS zC#!8eZ#DX<{!k!-^{KDTk8hmmD|jGVJkP$)e-mHzy#}eQ-@QFI`%3~GLk+GhE`&2K z>y3rT(3NEx>cDr|6MYAZo}J0cI6u}|ZV;5>sSv)t8*iFl9PtXsO|4+(f^b5DD?7Ga zugs~%Hf3bhU$@GJX}476xnrm6EL9T%-3N>3L@WW1OcMy3DP=T>5hJc{RFv!}Vjy$} zei1%u(`em)6fIbU`&+gMBBNkwRwmHkNp4VOPG@i`i(PHQJYBlAQq-TM_>PVwleUqu zL;cz4_2#|f#@+)MP(k&f8df7Z4I-D=cfWy@9w5d5qGY#(fi8 z>wL;=PL_AE$}Z<0EoP$Ip9%>OVyXNHuVCJgIr!u*X0Dnx*9-vAh%n5{r(q%e^xl~9 zvL=RDr|(?O8uE~~4|{+~1WasPAT{M6WLMEKt-kZOk470_hBx9;7*9DaltR7#xtpvh zYEVg}{Hd*LxaKCMwAmHaqHI7w7EDa?b)H6a*Fb(le33E(!vh`8U5O5zzF@NJ37AfO zJ+?u<@}h>X_qwph!=(E#*d1i&j<_e&9Zt}BLCqkvik8&#*rikBRzaO_jkKA4TGZ`X zyrQ*7madqbuEjK``@Ce0WEcf9D^uHH0#CUN(W_gdYIwtsvHvmRJ7&=1YixvzU|p~a zzs);DNe2qtuO$nrQpzYUyHekQFkwGavKkMjeuIIQE=Vgb;*yu{ThZOaX9gz-ZN{uE_^Zke+cc*Wa8+ zyOi=iR`PE09QqB*;N6epC>ZuJET!LEs9L*e*n7t|B@hbJ;+oKPHIzYl?$Z7cRV^F?8V zIR&%eNTKC;X(u7T%|4f{V=UOb8F@4vS4gWlPN)OW!F6DLns)mt^%S&!gg zXaSu~&*Ju?L>v@p^XG^xlmHTHF>@vlSTDK|Uv^3mE-k7lI%8SIT6ozAtoF+hCy!-G z>oTnDY)P|QhVk6Si+EOa56PmgrIhvh7H*k$369#16_vYR!K^7!W3=Dzn>}u7e%Ok{ zQFCA9`|5ZNUVJ)KIq@ zT!Bk$ZV({AOfqo2`H`6ds8)C1T_G#i&ktE6PizkzI@Q+^<8`xByZ2xkAz~7 zyV^kxRZt_~>NaMz%RVW%&c|+MT?~c-5YvvS>o+Dg9$)<5*#nbD_hf~BA53uFDGTKg z333sGNGPBb=U))qZw{vw0;E`yk1kX&rsA3{!ZJ1iUfOrT%4IQM#kgku82OG^=>M7Q4wu^*6r9!yexBx43c*F5j&R#ozS zj9o01Gt-fiU1y{V7k8#mGu0dn@)2O1!~V3b;RC^6$qtg8 zl^M0hT|9wo+0TvYHU{LFothaQ7Z0+WvhWOqZsN_^dsv2eg4Xm)-L}$n=Wv40xrH*i z?_-?^%p)LaM|JrDt4RIV{k&qup?xMR_eEMJrvrkc0d+DtgE`5LUHVD$M(4||i)1n+Hqa^w8rz?}6i z$nG6!UzZ%8U;)x9d0vyjU3n|bBgfpi9+z`?uLNw06&6{s!FbwViqb|78K%1_=-WOL z7+JfixQc$X(E;iZ)L^|&mHOQvG4?PQ3FLuZSLJ~r-u;;L{*tZ%&jzfou-W4@w%BbU zwF5-m+~&aJsnz71BCF39J$I2-B^Hpu40Mz2-H=t!q;*R&KPCA)Q2dpAK(%)Ak)s|Y zz1**CJoO+2w#x$MvI=tBimNgt@j~t=mdr@#qhX7s?F?CcEaIjK2PB?NBz`6c!Xwz< zXxC!4DRt(S`+&Yl>d*|CXI&%tLh<0GPzKtrbhSP84Ss`}fG3;~M@#xe*<`A$QoHA| z_6h)7OPe@+9w161HSw7h6B2B`G37hg4rU>5Uj(&d<7 zxN600{hP5B7~nd|c;ISHzHRkJpR{G+*=oh<_H)N@_*o!#a;vPQo@yg zD1>a9o;#cL7xukg)~IV*dT6RfT%+!85PRd6tI#N*fPbrSlIPCxzL27-!azVLS|oT)AvIHY|LKV&AnK=yRL?i~4^k^THyCF5B;SFiJcy-+b? z^gW(~_z7M&&Y5TNU>cdT56EQ9%XtA={jQ!^{voYB%0R=4hR{2el^Ni}M(pJ0!-(J+;c42qJa)ROKs+ zz_`I1A=&2!y!Cz*mL0Y~OUCI>{82g(YjPK!7!v_#wbGa7stV%%c7Pj-nc#JhpMMxL z7s)2?`Q1ZdBe%&Zr9qp%|nb!8_s!NL&Xf3$y+Pe z2-uTLoTfsaVJ{DBQG^Z0Mzi&r85@HR_m|F$Us5KTSRzVMQ)8T|M%=lBnhdVcZ>18M z+-h!%?f>@2YI_tL@l7=%COt^%T;w`WP@E1&p`6aXAxUJ*fLaD&Z|vtTxw~{A zNb248_lRILz4r_|K7@ZO%4zr4c)qv3Oltkn-2vpzwM3r5RTbMyqI-!5ta@}cM^ zIs;gBAH7s_PQ?bKt`Mj`RMwjqC`X&fqjH8{-*#J~gJ!FRmlM%{IXv95$y%N?$OYms zwT#LQmc|`Je%#hiDNzuLEsV%R^eFKu`jdY1f{wF1=g`7{ZDjcH&ak}+p8Vlx@*rf_ zSr}w1B~7rX^QeaIsB_3*;l3bB$sp5zjib3%w8}80EffwV?B(h7(GJ|4ny8tFEs1A* zE^4Gpw)L9xh=Ik6&&*7Zu>@sbZvck?lLbA}m`%YGBkgSiVES9h^@IvTdK%pfn?`t3 z7(mfJ0LzmaxceiG(dtcz9QPa>uH?pj)eE#^zEK#8{|^3k!mfBg1XE{IXS||C7>|B> z55)P8y46IwwEyl)$)^h%Vv?XTIngK5p=|S6=iUeFhQ`v8%~k4_pIw|WK_6avrnYVuf2$=A)6tQuM{JM{sUT&Zz&#Q!}V!%||R3*~X zRPz8G4AxDVwt}L=L;tQ_zCX=-KMZs>rI$Gd^B8?k!Sz3x%{}z^y0dE$8dr(n42AOG zKhEAL1V<4L@aW60v%0=309GPE07gsI?vy<=w z3+}s4Q^=-(b#e_GC5N6FBgViUff1@9o)HDHTHvfb?7(Klec><`j+>Gy$h21I!(Uog zyzxM_hxB7N9ud>pGJ9=&)GWUbg+XCRb0&79JK4cC-@mF0Yw4g`ysp!ziuWb>(;s7Z z3H166bDngTY~$S&oLFpNe6F%zKfu+6!HULL^FvFe8FzXP}-4hOPkV zl!J$=v|%{2w`Fmg(JBML?z{2&@Uey&i(zgn$sp3efkCah?iA%!bS* zQjus#g&dGKd5a5=I1mxqVo)nV1GsX0KbcF({@EJ|{swo?>1GJovLeetf@rZ=_<4-W zHQXw7{F^QR6Ok>vXwQ0*2g#AAIsW0Vs$Bwn@$0335Zd3EvJ$?`w&*}*%ypUV9}dIhckh4c>>Xy`%W0P zdAEOzV9}pih~nvqnC&BuJWq^H}=BfIPuO-0n#Ny0FSc>vqp&k&U&o}>JHS;%96#b zd`V9ldu|HuRfakAEE1rMvH#Q+wpP+|-);y@@mGH*}Z4EeTC6HIB z7)ctuF$X=_HT2TSj79RHbXdra5WjD#^^{nn0x`a8h*N=O5wwAs{i;v}ccyLAP>Pl8 z)1_pDY?aBZ`))dD>l-;SH{2K1p@SQ+Yj%j5nQ{X-kB$ApL}6l!1dzU=!>plxD1o(2 zBb-*~F@|eL6^H*_s*EJrHj5u!#YY-P_36ouzP>KXb*Y5s!&Cg{i<2kWv-PzJYm$yj zsM{LZ&eLk@TT;|W6g}!ULoN!qU_mFZupSefJtU2ap9AuuDG(VWl*4@slPJ}Y-v!}w zpeQ2H_4^wGfQtN@h{wf)gAxye-p+=}i@IExlZhvY`>nL+{52)^j{vumWBQ;8>c2@WR)~ z%r;%vkGl#QPWU3g=iw|znBs%N!&2HQmUgDJgcZy}G}MBt46=nK$a4E(g%~TQlZzmq zgR56bj0iR!K!%Q1*+_-)lh{De>dM3%)EN8racGf*iN`;7!QDJ-Z%I0Ko*W#^BxD1F z3WGiJwEL4ER`+qcz2q_W=b}jxcUk(C&D%bMO^&Pn?XacsgL$1RAY9uvm5Ydnmu&V7 z6&}LccHAieKUJKr25vmmD1)rZPt_Pimm^&>-a4x)dPJnQphmXAWT_m=sF1%`>qiE$ ziX0zX984`K0_3=hERlEPMP@lM?t1sq#RS9TG}H4uPmRWC=f8GY1OhkGSfFKD|XQXC+_QBP{dc~63XLZ;vlq5 zsAF7BMjL}clE+ zg!RwfQzB*QEIj#NgxPcX?9oZhmoyUog=Km6Y+jjC1T?cOL-4!#HW86tW#mWd7p)~8 zIxqC*4vGJzKhe@e0Z_ruz=f$l!LvjLNH;lowFS8~)Q!wJL1krf+G5 zLf)O^SDAJT=~jK^6h`n4A@=Uv9Ht+;8UlW?GCseZ=myH31dQw6%6BhI&s$R{I-}KB zFt}L99ro9a6V4NT=2h;6M?5`geQTF(6jHxFryFE{v0!bV&3k~4Fh`O3vFIuNpcU1@ z3Ey|aXbdI|H;uo$o(jcSHjkQ=E~NMwrp%hIt~vtWaIn=eF8D zkf=0vr_YXe)?@+h)Ak+_&_6Z2r~ZAvWNg@vqn^dkro5KS66-r>Yi9N72J&yd?!1g& zZQ<;eO`BEQTVv2>R_n1awPn2~lw%arjH0l3lspD#toX3rLiA&^?e!qb*SScg?|<7$ zY}1A#VsTan?A@oLZu0I#1Uv>sHGXNtGmKh&U1(lXpN1+oGIMsxCp=)i2PTvmP ziu3kTWW9dA*>Q}y(8#dAtaKRhlJlrUz-Qb_bLrsjNIy@SsD<`&i%K zG_h(Xkc<@mpOhPI$TZ4WnxPaf#<|v|oV$0<6ta0Jv+f=*hAn7m$w;p4*ZmOa)Pk8v znf0NQY{@LKAHQ{h?lNhRVwW{?dH?zSt5jRPjT#r!?Dmsd(_If({S4vajfg*ck`aSa zL3>qTzTUe4IV%!Ch~b^aV;Dd-oq}nABW`qCRR2n8?>s)qjgxWJ*vzr8>h?1=J736u zuhPG9d2?NIpZr+^;QF=3OToZ#_6KopyXLp7w%A(}9jWT3U@}kdd@tm7bC*)xa;n0{ za1OkFH4ra3+f&PE1~C>Rq-8qxmWrW0=nYFCdJ`lZ9q!9@4N3;>HoI(s`GR>RDQl4|I_0?f|rK{TRz z3_`3xCuMY}ii@rJJk}7cEm){)L;IUwms9&!`0g$DEhAQdHb8jAW04ey^3(az`Jj4% z+!6JAYY$Q!rja{Ug)@|qooFjxsgb0)z|Rn*f(+J+V7nAi_K6%~GN&iRdo7XsF|hp- z{BKw9|E)}kU1GK}GV3NgkjY_3%d)F+>u5{^`oZD^uB}W&h1>f*V!%#W#dXc()#*l29ezVT+ zzU21`CFn8QDk-NiR^OHIeR49d)P@_b{1}808?UL!63DwC5A`{%cb&fkoG+aP4zu`p z!E1>j#4oAqu6VTM)F;X4EXB*nR<`$_pT*eZ5@U+k&D0T-w{W5u{o@fc-J+7VE*{f&&^lp*E5i&`eUa$6i}r8jrcZLwL93-_Gjw z0_prqwa5%%G!LQltM&*KBmzJs!FOQY`m57fAIBIPgYKm&fnZ*|Bx}YW5jV1{$z#4x zkQJx-3}A&=ENqWr$%%pM?C(AkiJ#IsB|U`41w#+h=a@-ILapD@TqK0B9IaUH+My%C z#|z5K1+}3CzMnSWOuE497HUN5*@|n4mRxl23_>~YzQ63?T^SEEg`_lNIgEK=qmT|U zsBavBc;tx`u*mcxT zLDtvUOv!ErxhyiQgtXGw$2CKQ+s@_V&v>(uH0(->W|_o@p&M9b14e78lr^ndx}eab z*n{?13N{g}W-4n}bPhbKu}bGfB=6gx%TpnC&argx)0BnUQ+oXy!7F~Zem#v`ox=m` z7j&_0eY1(^)>NOi#*EC7NWX(X06gdN32Rc@`=e(YmVlG~Ov$On8c?2&Ot?qwr88E4 z73UbqD1w}U%a@@TmBE1@Xe*H?=C36A8q?4lSu3Wt);^YN#ikTcqd1AQPgm&c2eyO|odV`&4P|_!-h~2p$Qd*0j!;RK7&gJ40=?6Uz zgnfCNJn9Ot82_%}0jITveTiTv?uebk1Ja*z(dzpy<2XHwhih!zx1Qnc0xLAd7Z6f0 z2GCajEl7RVu@Cgk>w)0FXqD@ZgjsJAni|q`7hujCq<*r(uxlEoW4&PSa{?VB1>0?K z^H~O+P03F2s+8Qly6ZIjRh9kO^#o-0MH@1|ZLA`V(_9zrfBWHk0kaDd(z_RRkr1swoU1JfKd0cnf}Q-`DPPvy64o7 zw4g|iD!{h}l+0Td38Vz)c+$A$Hz=3rlGBD`Plpil2G{dGlL zlsV59J%+;C{jcSMyde|e5y;XHrB7U!I)(bXuUGv$$FWoC6JDjA@1I|a*^dz~KYZdy zBSE*Gg5m|)4|sv|evF|siVkj1yuE`(b(G4>J}!|^oS{$PExW|U%f>rCGfp#^hSdKV z?Rwiv8Fk`?A&p4L>z51i>|dLO>4kgyuiVac7cEd^ZAX8XGPGhGio+C;Xca_A^H<`C z<+jlOJO#O~MrY&6xWpjZWYJ}p?=i+idj-Hx?3u2ggE6H!YTBj>#3xA&eB=lMKj}}P`2WZ!0s&i7 zGpskvaQ(CelHW!lQ|Y>_oTv1@rvv!uI$?Xk;b6OD=nbAi@v+7chp;J5>T(m6@Xb(?AhllWpJ8weRK&AqF{tp|ID-uYT;D zWAcA&6m)Uv*%@uur4eA|41M;j}tWDj#CjlwYrT{_U zI0Mo4-yCeFMRq`fyZGm_;s(SMzmDi;}}&@QB>HGtn8y6EuS(=RbutunSO$o zR-4pvze(T{d7|OZ!7GjrT`zVA7elF-x02uYi~#?M<>@SSP`skmo}7C_%hrEdr=;+N zoH&XgqnkMn``P>~Lgekk9Xhb!Oe!XgtzafvzH}`87QWA*nqEoH37crfb`lwNleX_d zeM>tTBpDC7Q7v4Z4evIi5>^HlBFpcDJ6Ysy_!WfIDTd>zq5>a{Hm_o^2~^3ojJYYO z?4?u&hy2za02H^ibtpoDAWBX?+38b61cgSh;ZPNUoC1RW5;UHJIWNcjS-8u_4S9>S z)~waE9MtiVS^vf-J~ZOetD7!yJ$t@p1kX|87Ww;Q`pi9P`~2a{c53kej2}1DPKwO! zh1>%aWoZIW@n}58 zxU(}xat_}O%S&YxATcPTSLh-3f9ENA86UOg>#UgW3)h_3vNMCZrK~m74h&k<_a?i% zZFrC5u}bJehpH1d82Q^n*I~&?S9A z2Ob2NW1X#iTVVKKsUL)T$R(>s*Aa?c6a-I+M6Nbc9W2 zIr#tUV`d>zBApyKikHD#^Xz(!*r-{sL&qh#BGX~zf36?c3|mVPgV6}YpOqr%7b*0E zPazi4&W>R`5O70r#`v}TqH0y68*~k2Nw6~ zu7WYLU!QUNKgwR${+};m-#)WFj_t3>X`ug{Gxo&$^4W<9CQk2JBMLxB`oe0Fs8eOB zNd!JmgB=^+v>j3c&e|C(1#L_7Fq@2?b_q5^K*Y#{%T-fPu& z2AVY|RkusQE`?9>6^kpCnf=_;7iopCEmTKNxt&yKnBj-A!7;jTIS`&g+~d*@bj*D} zBPck~7`JE1%j9&~j_iecg4!C7!#(Lq{hRy>Y=o)%r9Mn{t7Et!A#8+b#_gMY%F^ovg5Eao&EdoD#EAqAvC#igTLKp|WsFI6kD3x7CMq6QEWxXtN( z;9=irK+9$&i-sN)zE{!8c52Pocsq7VBb(kMKA5)*UW2_YW(1`bTa&{o<|jX>w=-U%NOm2mD2SONuc0@K5AEcf8!wU=B(XFYggjAKiR1@Ksta2_#(27fzUF|2 zL{tZ-lgaihcO7)mP&#WnV_G&B*UCHi&KZ^*?QALx@;P+%KZo=z#0=pi^&C|l91`Q7 zT+rXbjb<=%jv+!z41`ICQCojfs0Ako@S!5mzo0E`evuIv-&&&7Hh?` z`V_eYWDqQKv4vZ>h!x|gIk-&^o2aUzV_=@V*YFzpOVYLhY!Y*4PD}2!`Ej>hpfk|7 z@okCBbrq`;N89MPE)`yOV^hVATBL-o3il8CINl7OdVPABF%I%K2dYI;HB&dD-3K zQNh;C^qhpzSZ^<`->Q z**Td6kU3j4Pd@>;^fdtkLM49zk*ZBT^?`#K(INk}Uvm$9<cP)lUbikoUnOAW|uDLYg1`$IXWGUWm#2t=ygAwe4JeyYg)5I?|HxpQ42-@cNo# z&3pmMRvqcLVJyP1%>3Ri^{@opylYovHOf-Fv6m-J_O`{$1#dnK4c%&w(pEM4k%)E* zr?N%F^3)M3T-IP5;D3jm0(`-UAr8!$GBGpgpV@%Z)>@O*Ie2*tuD`YK@RP>T1v)|0 z1^X}+hQYWj$g1&hO8fHb*H!>?>0*>=M;wMnRw^jVf9Z}D_%jm|pEbPte^Qd(1v{w@CC zRJ9V&j(kTQkaToi7aBbBHsjyD@52kxKb!$OwJz5}>_RY8eKRO> zUH-qeTJG;7LQ&-jXM^=hh7uC}xu5S<9Ld*^21P4o+j~mqa>x(LP##Nf$~%Q>Pbb?2 z;0u4!1pLEc*9difDaMFnC9f_^Gmj%9Kk4XLOeiCMTYNDJ(}b33HY5{MPR7kwdPe;= zgqm^?bQz47T3Mhhoa~jt@!_J;8xM(DV*Uvfc`K-o^AUm$_}|_fPOw6t#~+n9bk3{K6!g=p+tzoH>Ezd!HQgb9x0Ga%I$VE z@xGs06Li;(MxyJ&F^iVM!39E0WL56ISZcz+*Z$pW=yV87b+$i;9wJAaJ)H<6Bdf|l zlGVEV${gv^tT)E}62+`PN3kL34sg6b;*xb$fDZs?DgTL*qHs~ftUjHq?p}fcW;>y+ zxFB~>SLliH{BorJC%Ovc3PppOe-ABoTSL-A(L(h(<@F9si@||pY;?T9cO_uYm61lb zvxx-D`tg0VKdcvn-LO55fX>=em{7R2z0hn}!deuP>5)}8oO5FnxMgzZBYMHq?YmWO znu4mDa7`wxkmEe)4Tmxqu1IF$Ft)X$wmoyYeu%`#NrncJA8wV9F3{9)PksCUR?>Lf zYZc+(h0bs2^F{H?3@1MYLEa5D51abFWbw*Q$(jY(u|A}3cmQyVyDv~npfqyLQG{htK>rXK1L1UJ8n!zyH3H~>wA7$VcaaEMxQGWq@}mr0HoaM2I_IN~ zP^s+k8=13Eg%1;y`Gw=QhsJ*xiJ>Kl8jLI-ubDZN&(pT!uU2rdT6~$h8imELHz2#3 zDs&u$R|+`*g38Ida3Tf2$2sbZqcgo?Tio*tXB>)8hVSF(j`g|cDWW~RjguQ(MIkb~ zCthU5CP|lS?kW7UHK`$zNRi5r57+i7r1R;f@d?7D9wA9?2}s42bb;-DV{*I@2)itg zILD=Z{X{PB_B+K|AeS~2t}|V;1D!uIOVrn zw9xtV0*5LC)uB01Gf8&cySK?aTHgfNSXTQ*RX!1eW;i;eI1~53t`hkU;tiG5jhr}r z^-l@6?^%jb$9p55f7k0Q5358aGi8u$ImH?KbRht6CU8tD@(0cjxhx$xPY6=0fbOf7 zT=i{m@rL?hv9S+KV^T(KW6_$D`dwRdN(b(f`K&25rpnU~*TeV!$)O3IVndl3t~Lj(PtvMH3?oq`xy@)?w^mljOScBR=KU9`{nLN%LkYj=!PLy#|owIx|tL+%hSp z(`FT45M869x7KnB`8qKhC8mU9m$sm(UcL0E+b-h{KC&SlD&jTTn}JZ>3~GMc)Q>#G z*Eh$xajxvwQ?^e$B*%2o1GE$H6376lI`g}{l4Mi6gy=IXI^FAB&Jb=JyqadgpJ;nJ z`rAfw(}uAm6gu5<907}fq;0<7HV7H%v=^63bAt1Tg3@Olc#`oAr@bWm0Si9?^W2H) zfHER=%zScYtmp>|Bu=K?!+%aSvMzn*8AqzGRW8!xpI%vl44^b+1doi2U?igdcw|+h zoi&4fP&}0jIAcjrEgRIkX1;drd;ecjaax}`5(&u4wnk+xS+r5jfprry%t_vKrDR3aIX+y@h1m- zh1at8M2ki2Y=f12Zhi>Auz7tB8pgIJb7)e2!389p;goOk-QAW+)(PPMKHm^PP-H77 zq9{PD(t;73&MMgQR@>U2^PlgV1>m2xP%95~r zq`P{-7NgSE1@ZFk4SKStZSC8zGqXS`Lm=8onP!!a*BzCQAwEmox|Rh2l2%BEOEy8l zdt4vFeHluN_MOX-vfsbom`mKwoYv}Jh0EGe4oURQ(j0*Kn zQ6Eh`+5%IW99)y_egM(MYj9So!)8s&LZ~!EhA`NEG;d<+=PdV$b|_@ZS96QdaTgPq z1$6K6&Ih%po)mGw`_hkW>&zVXT!9DyP_XmkIAad4(eIbFDJc+48bk8f|M>RmAtJ*q zxeS_LaGJ{^YL6vdqH0-y;(qhFLWy|{jL2^PIRS&kcmg*Tu6a0+5%Zvf9U1Yj!%%9c z!=}jJ>$Z)@#)>2>x}bw0X=DX|%4x-EEc0Gv*cuG1RbaVG5t|3{r{_S0)Js@RU2>yN z3768qS+hSFPx@QY5~;M%_`_Q6po?b$z;t6<^2Su&4#6 zs+b-w?k#9XQg&Z4kjfc$HZ7xN`mNG=FSA@ojW+veb0L>t!TLKfad=-s)ub|=eEbI( zr`O?12|B114v>d@#pyx#nvcOGTJ&~;XgfvMD-b(!&C*q~%8r^^EpO|AyjhNEn1B31=m zYDp?z_v@ZW6((Hx;rnIag6IWx!IwLCw**b;Lj%`mx8C#d`-^bMeZz#>2lJ(LJv5au zJ8@x14C`Wp5TQu3Ny=}t&rJPQ9y}zTw>&AOf-l8E&Bwnzgs--wbrnzLd<_v7tks<|-Y)^L5xeihB3#NpRX~o5WB^2!$HO93k zw1%T*zY^-~&iiohS0faVeeiCqYBVcAdYeg%Z`<5gWK>E@(*%)Y z9e-~bW#r-^-EPnA5FJ6=Tp(XTEE6AXMr(I48YFXajBmM(H%n1z!~^B9i7|u1bq6kl zeXh&aP2;L`$rY9(+ZhxpohNq!vh&WBWI?TJL5+gY)zLr6Mf7b#Q_H9)5|~^K!1!Wv zO19p6@ln|Iv~34NJujdGP=f?HR%y!L{O{!Mb3tgp+2)bh4<-XPV-P`l_{fV^_^(1) z0h*RSMf?DV$9pW648Z><7@`b1zo3`~#4f zHCd&vW!k7rLJNWC?bx>d2QnTW3c$x)3Y|IW)`pp{zNl4d+~*psQ@K&tsqrlI_OfAH z{OYKO7(6qoP5Z-wSi1614BzERtW5AFhdfX323M>!n^GTVzvFf|NiwCr1cT{Zhh`%< zqmafF!d-Cx0|7KDzD3N}c)eI-_kLMYnBKhBI`>!wdPf~serKkX=M&Yeke^Que!w%)6P6QsN*cRboAlc-K z$ijRm#zlbRD{evY(r2Vr3xy)IBKv?7@M?o)c|majuJJJAWC`vMVfG1lrD97Vwdb4J z#yupQ{=>G*En9TwQnsWnf)_hqZ@+XmG;ej-^!VIBZyhNg{aBc>$ysfM8^JHnRBRe0 z?E?Ei^}=9xWZy8eZ}4d9z05{>hh(oh2kHgNI`ToU$mJ zIBT34+LEm8ybeslh35;)w_^}g{PD491-?IA$&SsGI&5BC9R$R!CO{Gg_1m3G=Nr_m zIH)vZzf+wm!o$$w)iD?pi#`&ldCy_;iK}6fo0d&q#ti(hKYw5^F-}jv^R|kri%ao006$oKm%90Vs!ukrGprN ZtjGZX001^Iy@C`MJ1_$P00004Sy~zOLreew diff --git a/crates/core/src/server/app_packaging.rs b/crates/core/src/server/app_packaging.rs new file mode 100644 index 000000000..ce241ca4b --- /dev/null +++ b/crates/core/src/server/app_packaging.rs @@ -0,0 +1,125 @@ +//! Helper functions and types for dealing with HTTP gateway compatible contracts. +use std::{ + io::{Cursor, Read}, + path::Path, +}; + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use tar::{Archive, Builder}; +use xz2::read::{XzDecoder, XzEncoder}; + +#[derive(Debug, thiserror::Error)] +pub enum WebContractError { + #[error("unpacking error: {0}")] + UnpackingError(Box), + #[error(transparent)] + StoringError(std::io::Error), + #[error("file not found: {0}")] + FileNotFound(String), +} + +#[non_exhaustive] +pub struct WebApp { + pub metadata: Vec, + pub web: Vec, +} + +impl WebApp { + pub fn from_data( + metadata: Vec, + web: Builder>>, + ) -> Result { + let buf = web.into_inner().unwrap().into_inner(); + let mut encoder = XzEncoder::new(Cursor::new(buf), 6); + let mut compressed = vec![]; + encoder.read_to_end(&mut compressed).unwrap(); + Ok(Self { + metadata, + web: compressed, + }) + } + + pub fn pack(mut self) -> std::io::Result> { + let mut output = Vec::with_capacity( + self.metadata.len() + self.web.len() + (std::mem::size_of::() * 2), + ); + output.write_u64::(self.metadata.len() as u64)?; + output.append(&mut self.metadata); + output.write_u64::(self.web.len() as u64)?; + output.append(&mut self.web); + Ok(output) + } + + pub fn unpack(&mut self, dst: impl AsRef) -> Result<(), WebContractError> { + let mut decoded_web = self.decode_web(); + decoded_web + .unpack(dst) + .map_err(WebContractError::StoringError)?; + Ok(()) + } + + pub fn get_file(&mut self, path: &str) -> Result, WebContractError> { + let mut decoded_web = self.decode_web(); + for e in decoded_web + .entries() + .map_err(|e| WebContractError::UnpackingError(Box::new(e)))? + { + let mut e = e.map_err(|e| WebContractError::UnpackingError(Box::new(e)))?; + if e.path() + .ok() + .filter(|p| p.to_string_lossy() == path) + .is_some() + { + let mut bytes = vec![]; + e.read_to_end(&mut bytes) + .map_err(|e| WebContractError::UnpackingError(Box::new(e)))?; + return Ok(bytes); + } + } + Err(WebContractError::FileNotFound(path.to_owned())) + } + + fn decode_web(&self) -> Archive> { + let decoder = XzDecoder::new(self.web.as_slice()); + Archive::new(decoder) + } +} + +impl<'a> TryFrom<&'a [u8]> for WebApp { + type Error = WebContractError; + + fn try_from(state: &'a [u8]) -> Result { + const MAX_METADATA_SIZE: u64 = 1024; + const MAX_WEB_SIZE: u64 = 1024 * 1024 * 100; + // Decompose the state and extract the compressed web interface + let mut state = Cursor::new(state); + + let metadata_size = state + .read_u64::() + .map_err(|e| WebContractError::UnpackingError(Box::new(e)))?; + if metadata_size > MAX_METADATA_SIZE { + return Err(WebContractError::UnpackingError( + format!("Exceeded metadata size of 1kB: {} bytes", metadata_size).into(), + )); + } + let mut metadata = vec![0; metadata_size as usize]; + state + .read_exact(&mut metadata) + .map_err(|e| WebContractError::UnpackingError(Box::new(e)))?; + + let web_size = state + .read_u64::() + .map_err(|e| WebContractError::UnpackingError(Box::new(e)))?; + if web_size > MAX_WEB_SIZE { + return Err(WebContractError::UnpackingError( + format!("Exceeded packed web size of 100MB: {} bytes", web_size).into(), + )); + } + let mut web = vec![0; web_size as usize]; + state + .read_exact(&mut web) + .map_err(|e| WebContractError::UnpackingError(Box::new(e)))?; + + Ok(Self { metadata, web }) + } +} diff --git a/crates/core/src/server/mod.rs b/crates/core/src/server/mod.rs index fe3adcbf8..730c2a4de 100644 --- a/crates/core/src/server/mod.rs +++ b/crates/core/src/server/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod app_packaging; pub(crate) mod errors; mod http_gateway; pub(crate) mod path_handlers; @@ -9,6 +10,8 @@ use freenet_stdlib::{ use crate::client_events::{AuthToken, ClientId, HostResult}; +pub use app_packaging::WebApp; + #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub(crate) enum ClientConnection { diff --git a/crates/core/src/server/path_handlers.rs b/crates/core/src/server/path_handlers.rs index de6d62516..02b341d7c 100644 --- a/crates/core/src/server/path_handlers.rs +++ b/crates/core/src/server/path_handlers.rs @@ -7,15 +7,16 @@ use bytes::Bytes; use freenet_stdlib::{ client_api::{ClientRequest, ContractRequest, ContractResponse, HostResponse}, prelude::*, - web::{WebApp, WebContractError}, }; use tokio::{fs::File, io::AsyncReadExt, sync::mpsc}; use crate::client_events::AuthToken; use super::{ - errors::WebSocketApiError, http_gateway::HttpGatewayRequest, ClientConnection, - HostCallbackResult, + app_packaging::{WebApp, WebContractError}, + errors::WebSocketApiError, + http_gateway::HttpGatewayRequest, + ClientConnection, HostCallbackResult, }; const ALPHABET: &str = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; diff --git a/crates/fdev/src/build.rs b/crates/fdev/src/build.rs index 827f44a3f..1fd431ba7 100644 --- a/crates/fdev/src/build.rs +++ b/crates/fdev/src/build.rs @@ -1,3 +1,4 @@ +use freenet::server::WebApp; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use std::{ @@ -10,8 +11,6 @@ use std::{ }; use tar::Builder; -use freenet_stdlib::web::WebApp; - use crate::{ config::{BuildToolCliConfig, PackageType}, util::pipe_std_streams, @@ -137,6 +136,7 @@ mod contract { compile_contract(&config, &cli_config, cwd)?; match config.contract.c_type.unwrap_or(ContractType::Standard) { ContractType::WebApp => { + println!("Packaging standard Freenet web app contract type"); let embedded = if let Some(d) = config.webapp.as_ref().and_then(|a| a.dependencies.as_ref()) { let deps = include_deps(d)?; @@ -146,7 +146,10 @@ mod contract { }; build_web_state(&config, embedded, cwd)? } - ContractType::Standard => build_generic_state(&mut config, cwd)?, + ContractType::Standard => { + println!("Packaging generic contract type"); + build_generic_state(&mut config, cwd)? + } } Ok(()) } diff --git a/stdlib b/stdlib index 5a8080f1a..3284f0f9d 160000 --- a/stdlib +++ b/stdlib @@ -1 +1 @@ -Subproject commit 5a8080f1a2b718c40728d257d5f2d474cc1d5a91 +Subproject commit 3284f0f9d6fb0e14e562be2fd1f842ed46b360a7 From d49b8c70a735816d61839d64566c409a648ff3c1 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Fri, 29 Sep 2023 10:43:18 +0200 Subject: [PATCH 21/76] Update tutorial --- docs/src/tutorial.md | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index bd4c03f95..9f7ba6df9 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -1,6 +1,6 @@ # Getting Started -This tutorial will show you how to build decentralized software on Freenet. +This tutorial will show you how to build decentralized software on Freenet. For a similar working and up to date example check the `freenet-microblogging` app (located in under the `apps/freenet-microblogging` directory in the `freenet-core` repository). @@ -15,7 +15,7 @@ Mac (for Windows see [here](https://rustup.rs)): curl https://sh.rustup.rs -sSf | sh ``` -### Freenet Dev Tool (LDT) +### Freenet development tool (fdev) Once you have a working installation of Cargo you can install the Freenet dev tools: @@ -200,19 +200,21 @@ import { PutResponse, UpdateNotification, UpdateResponse, + DelegateResponse, } from "@freenetorg/freenet-stdlib/websocket-interface"; const handler = { - onPut: (_response: PutResponse) => {}, - onGet: (_response: GetResponse) => {}, - onUpdate: (_up: UpdateResponse) => {}, - onUpdateNotification: (_notif: UpdateNotification) => {}, + onContractPut: (_response: PutResponse) => {}, + onContractGet: (_response: GetResponse) => {}, + onContractUpdate: (_up: UpdateResponse) => {}, + onContractUpdateNotification: (_notif: UpdateNotification) => {}, + onDelegateResponse: (_response: DelegateResponse) => {}, onErr: (err: HostError) => {}, onOpen: () => {}, }; const API_URL = new URL(`ws://${location.host}/contract/command/`); -const locutusApi = new FreenetWsApi(API_URL, handler); +const freenetApi = new FreenetWsApi(API_URL, handler); const CONTRACT = "DCBi7HNZC3QUZRiZLFZDiEduv5KHgZfgBk8WwTiheGq1"; @@ -221,7 +223,7 @@ async function loadState() { key: Key.fromSpec(CONTRACT), fetch_contract: false, }; - await locutusApi.get(getRequest); + await freenetApi.get(getRequest); } ``` @@ -238,7 +240,7 @@ const handler = { }; const API_URL = new URL(`ws://${location.host}/contract/command/`); -const locutusApi = new LocutusWsApi(API_URL, handler); +const freenetApi = new FreenetWsApi(API_URL, handler); ``` This type provides a convenient interface to the WebSocket API. It receives an @@ -253,7 +255,7 @@ async function loadState() { key: Key.fromSpec(CONTRACT), fetch_contract: false, }; - await locutusApi.get(getRequest); + await freenetApi.get(getRequest); } ``` @@ -331,9 +333,10 @@ render them in our browser. We can do that, for example, using the API: ```typescript function getUpdateNotification(notification: UpdateNotification) { let decoder = new TextDecoder("utf8"); - let updatesBox = document.getElementById("updates") as HTMLPreElement; - let newUpdate = decoder.decode(Uint8Array.from(notification.update)); - let newUpdateJson = JSON.parse(newUpdate); + let updatesBox = DOCUMENT.getElementById("updates") as HTMLPreElement; + let delta = notification.update?.updateData as DeltaUpdate; + let newUpdate = decoder.decode(Uint8Array.from(delta.delta)); + let newUpdateJson = JSON.parse(newUpdate.replace("\x00", "")); updatesBox.textContent = updatesBox.textContent + newUpdateJson; } ``` From 6b042f957cb66a732450fe7bc524bf5ceca7829b Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Sat, 30 Sep 2023 22:05:27 -0500 Subject: [PATCH 22/76] top: add topology manager --- crates/core/src/lib.rs | 2 +- crates/core/src/operations/join_ring.rs | 2 +- crates/core/src/ring.rs | 15 +- .../{topology_manager => topology}/metric.rs | 28 ++- crates/core/src/topology/mod.rs | 96 +++++++++++ crates/core/src/topology/simulation/mod.rs | 160 ++++++++++++++++++ .../small_world_rand.rs | 62 ++++--- crates/core/src/topology_manager/mod.rs | 26 --- 8 files changed, 329 insertions(+), 62 deletions(-) rename crates/core/src/{topology_manager => topology}/metric.rs (80%) create mode 100644 crates/core/src/topology/mod.rs create mode 100644 crates/core/src/topology/simulation/mod.rs rename crates/core/src/{topology_manager => topology}/small_world_rand.rs (64%) delete mode 100644 crates/core/src/topology_manager/mod.rs diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index dfb838d8b..561fddd26 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -10,7 +10,7 @@ mod router; mod runtime; #[cfg(feature = "websocket")] pub mod server; -mod topology_manager; +mod topology; pub mod util; type DynError = Box; diff --git a/crates/core/src/operations/join_ring.rs b/crates/core/src/operations/join_ring.rs index 212d87baf..ca20df520 100644 --- a/crates/core/src/operations/join_ring.rs +++ b/crates/core/src/operations/join_ring.rs @@ -930,7 +930,7 @@ mod messages { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub(crate) enum JoinRequest { StartReq { - target: PeerKeyLocation, + target: PeerKeyLocation, req_peer: PeerKey, hops_to_live: usize, max_hops_to_live: usize, diff --git a/crates/core/src/ring.rs b/crates/core/src/ring.rs index a430c0d46..b7d871884 100644 --- a/crates/core/src/ring.rs +++ b/crates/core/src/ring.rs @@ -28,6 +28,9 @@ use freenet_stdlib::prelude::ContractKey; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; +use rand::prelude::*; +use std::{cmp::Ordering, hash::Hash}; + use crate::node::{self, NodeBuilder, PeerKey}; #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -391,7 +394,7 @@ impl Ring { /// An abstract location on the 1D ring, represented by a real number on the interal [0, 1] #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Copy)] -pub struct Location(pub f64); +pub struct Location(f64); impl Location { pub fn new(location: f64) -> Self { @@ -402,6 +405,11 @@ impl Location { Location(location) } + /// Returns a new location rounded to ensure it is between 0.0 and 1.0 + pub fn new_rounded(location: f64) -> Self { + Self::new(location.rem_euclid(1.0)) + } + /// Returns a new random location. pub fn random() -> Self { use rand::prelude::*; @@ -418,6 +426,10 @@ impl Location { Distance::new(1.0f64 - d) } } + + pub fn as_f64(&self) -> f64 { + self.0 + } } /// Ensure at compile time locations can only be constructed from well formed contract keys @@ -508,6 +520,7 @@ impl PartialEq for Distance { } } +#[allow(clippy::incorrect_partial_ord_impl_on_ord_type)] impl PartialOrd for Distance { fn partial_cmp(&self, other: &Self) -> Option { self.0.partial_cmp(&other.0) diff --git a/crates/core/src/topology_manager/metric.rs b/crates/core/src/topology/metric.rs similarity index 80% rename from crates/core/src/topology_manager/metric.rs rename to crates/core/src/topology/metric.rs index c7ab7126f..d6676a401 100644 --- a/crates/core/src/topology_manager/metric.rs +++ b/crates/core/src/topology/metric.rs @@ -20,6 +20,8 @@ the ideal. A positive value indicates a lack of long-range links, and a negative value indicates a lack of short-range links. */ +use crate::ring::Distance; + /// Calculate the normalization constant C for the ideal r^-1 distribution. /// The integral is approximated using trapezoidal rule. fn calculate_normalization_constant() -> f64 { @@ -40,8 +42,8 @@ fn ideal_proportion_within_x(x: f64, c: f64) -> f64 { } /// Calculate the actual proportion of peers within distance X. -fn actual_proportion_within_x(connection_distances: &[f64], x: f64) -> f64 { - let count = connection_distances.iter().filter(|&&r| r <= x).count(); +fn actual_proportion_within_x(connection_distances: &[Distance], x: &Distance) -> f64 { + let count = connection_distances.iter().filter(|&&r| r <= *x).count(); count as f64 / connection_distances.len() as f64 } @@ -52,39 +54,49 @@ fn actual_proportion_within_x(connection_distances: &[f64], x: f64) -> f64 { /// - 0.0 is ideal, indicating a perfect match with the ideal small-world topology. /// - A negative value indicates the network is not clustered enough (lacks short-range links). /// - A positive value indicates the network is too clustered (lacks long-range links). -pub(crate) fn measure_small_worldness(connection_distances: &[f64]) -> f64 { +pub(crate) fn measure_small_worldness(connection_distances: &[Distance]) -> f64 { let c = calculate_normalization_constant(); let mut sum = 0.0; let step = 0.01; let mut x = step; while x <= 0.5 { let ideal = ideal_proportion_within_x(x, c); - let actual = actual_proportion_within_x(&connection_distances, x); + let actual = actual_proportion_within_x(&connection_distances, &Distance::new(x)); sum += actual - ideal; x += step; } sum * step // Multiply by step size to complete the trapezoidal rule } - #[cfg(test)] mod tests { + use itertools::Itertools; + use super::*; #[test] fn test_small_world_deviation_metric() { // Ideal case: distances drawn from an r^-1 distribution - let ideal_distances: Vec = vec![0.1, 0.2, 0.05, 0.4, 0.3]; // Replace with actual ideal distances + let ideal_distances: Vec = vec![0.1, 0.2, 0.05, 0.4, 0.3] + .iter() + .map(|d| Distance::new(*d)) + .collect_vec(); let metric_ideal = measure_small_worldness(&ideal_distances); assert!(metric_ideal.abs() < 0.1); // The metric should be close to zero for the ideal case // Non-ideal case 1: mostly short distances - let non_ideal_1: &[f64] = &[0.01, 0.02, 0.03, 0.04, 0.05]; + let non_ideal_1: Vec = vec![0.01, 0.02, 0.03, 0.04, 0.05] + .iter() + .map(|d| Distance::new(*d)) + .collect_vec(); let metric_non_ideal_1 = measure_small_worldness(&non_ideal_1); assert!(metric_non_ideal_1 > 0.1); // The metric should be significantly positive // Non-ideal case 2: mostly long distances - let non_ideal_2: &[f64] = &[0.4, 0.45, 0.48, 0.49, 0.5]; + let non_ideal_2: Vec = vec![0.4, 0.45, 0.48, 0.49, 0.5] + .iter() + .map(|d| Distance::new(*d)) + .collect_vec(); let metric_non_ideal_2 = measure_small_worldness(&non_ideal_2); assert!(metric_non_ideal_2 < -0.1); // The metric should be significantly negative } diff --git a/crates/core/src/topology/mod.rs b/crates/core/src/topology/mod.rs new file mode 100644 index 000000000..490b59cc4 --- /dev/null +++ b/crates/core/src/topology/mod.rs @@ -0,0 +1,96 @@ +#![allow(unused_variables, dead_code)] + +mod metric; +mod simulation; +mod small_world_rand; + +use crate::ring::*; + +const DEFAULT_MIN_DISTANCE: f64 = 0.01; + +pub(crate) enum TopologyStrategy { + Simple, + SmallWorld, + LoadBalancing, +} + +pub(crate) struct JoinTargetInfo { + pub target: Location, + pub threshold: Distance, + pub strategy: TopologyStrategy, +} + +impl TopologyStrategy { + pub(crate) fn select_join_target_location( + &self, + my_location: &Location, + peer_statistics: &PeerStatistics, + ) -> JoinTargetInfo { + match self { + TopologyStrategy::Simple => { + random_strategy(my_location, peer_statistics, TopologyStrategy::Simple) + } + TopologyStrategy::SmallWorld => small_world_metric_strategy( + my_location, + peer_statistics, + TopologyStrategy::SmallWorld, + ), + TopologyStrategy::LoadBalancing => load_balancing_strategy( + my_location, + peer_statistics, + TopologyStrategy::LoadBalancing, + ), + } + } +} + +pub(crate) fn random_strategy( + my_location: &Location, + peer_statistics: &PeerStatistics, + strategy: TopologyStrategy, +) -> JoinTargetInfo { + unimplemented!() +} + +pub(crate) fn small_world_metric_strategy( + my_location: &Location, + peer_statistics: &PeerStatistics, + strategy: TopologyStrategy, +) -> JoinTargetInfo { + unimplemented!() +} + +pub(crate) fn load_balancing_strategy( + my_location: &Location, + peer_statistics: &PeerStatistics, + strategy: TopologyStrategy, +) -> JoinTargetInfo { + unimplemented!() +} + +pub(crate) fn select_strategy(num_neighbors: usize) -> TopologyStrategy { + if num_neighbors < 10 { + TopologyStrategy::Simple + } else { + // Randomly select between the three strategies based on your criteria + unimplemented!() + } +} + +pub(crate) struct RequestsPerMinute(f64); + +pub(crate) struct PeerInfo { + pub location: Location, + pub requests_per_minute: RequestsPerMinute, + pub strategy: TopologyStrategy, +} + +pub(crate) struct PeerStatistics { + pub(crate) peers: Vec, +} + +impl PeerStatistics { + pub(crate) fn new() -> Self { + Self { peers: Vec::new() } + } +} diff --git a/crates/core/src/topology/simulation/mod.rs b/crates/core/src/topology/simulation/mod.rs new file mode 100644 index 000000000..2368e8d47 --- /dev/null +++ b/crates/core/src/topology/simulation/mod.rs @@ -0,0 +1,160 @@ +use crate::network_sim::Location; +use std::collections::{HashMap, HashSet}; +use tracing::{debug, info, warn, instrument}; + +use super::*; + +#[derive(Debug)] +struct SimulatedNetwork { + nodes: Vec, + connections: HashMap>, + requests: HashMap>, // origin node -> destination node -> requests +} + +impl SimulatedNetwork { + fn new() -> Self { + Self { + nodes: Vec::new(), + connections: HashMap::new(), + requests: HashMap::new(), + } + } + + fn add_node(&mut self) -> NodeRef { + let index = self.nodes.len(); + let node = SimulatedNode { + location: Location::random(), + index, + }; + debug!("Adding node {:?}", node); + self.nodes.push(node); + NodeRef { index } + } + + fn connect(&mut self, a: NodeRef, b: NodeRef) { + info!("Connecting {:?} and {:?}", a, b); + self.connections.entry(a).or_default().insert(b); + self.connections.entry(b).or_default().insert(a); + } + + fn disconnect(&mut self, a: NodeRef, b: NodeRef) { + info!("Disconnecting {:?} and {:?}", a, b); + self.connections.entry(a).or_default().remove(&b); + self.connections.entry(b).or_default().remove(&a); + } + + fn route(&self, source: &NodeRef, destination: Location) -> Result, RouteError> { + info!("Routing from {:?} to {:?}", source, destination); + let mut current = *source; // Dereference to copy + let mut visited = Vec::new(); + let mut recent_nodes = Vec::new(); // To track the sequence of last N nodes + + loop { + debug!("Current node: {:?}", current); + + // Check if we've reached the destination + if self.nodes[current.index].location == destination { + info!("Reached destination"); + return Ok(visited); + } + + // Get current node's distance to the destination + let current_distance = self.nodes[current.index] + .location + .distance(destination) + .as_f64(); + debug!("Current distance to destination: {}", current_distance); + + // Find the closest connected node to the destination that hasn't been visited + let closest_connections = match self.connections.get(¤t) { + Some(connections) => connections, + None => { + // Handle the None case here. For example, you might want to return agit n Err value or break the loop. + return Err(RouteError::NoRoute); + } + }; + let (closest, closest_distance) = closest_connections + .iter() + .map(|&neighbor| { + let distance = self.nodes[neighbor.index] + .location + .distance(destination) + .as_f64(); + (neighbor, distance) + }) + .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)) + .unwrap_or((current, current_distance)); + + debug!( + "Closest node: {:?}, distance: {}", + closest, closest_distance + ); + + // Check for a loop by looking at the sequence of last N nodes + recent_nodes.push(closest); + if recent_nodes.len() > 10 { + // Keep only the last 10 visited nodes in the list + recent_nodes.remove(0); + } + if recent_nodes.len() > 2 && recent_nodes.first() == recent_nodes.last() { + warn!("Loop detected"); + return Err(RouteError::Loop); + } + + // Update visited nodes and current node + visited.push(closest); + current = closest; + } + } + + fn join( + &self, + source: &NodeRef, + target: Location, + tolerance: Distance, + ) -> Option> { + info!("Joining via {:?} with target {:?}", source, target); + let join_route = self.route(source, target); + let join_route: Vec = match join_route { + Ok(route) => route, + Err(e) => { + warn!("Error joining network: {:?}", e); + return Option::None; + } + }; + + let mut joiners = Vec::new(); + + for node in join_route.iter() { + let node_location = self.nodes[node.index].location; + let distance = node_location.distance(target); + if distance < tolerance { + info!("Found node {:?} within tolerance", node); + joiners.push(node.clone()); + } + } + + Option::Some(joiners) + } +} + +#[derive(Debug)] +enum RouteError { + NoRoute, + Loop, + DeadEnd, +} + +#[derive(Debug)] +struct SimulatedNode { + index: usize, + location: Location, +} + +#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] +struct NodeRef { + index: usize, +} + +#[cfg(test)] +mod tests; diff --git a/crates/core/src/topology_manager/small_world_rand.rs b/crates/core/src/topology/small_world_rand.rs similarity index 64% rename from crates/core/src/topology_manager/small_world_rand.rs rename to crates/core/src/topology/small_world_rand.rs index 370a7f7a3..c7d27a953 100644 --- a/crates/core/src/topology_manager/small_world_rand.rs +++ b/crates/core/src/topology/small_world_rand.rs @@ -1,70 +1,78 @@ use rand::Rng; +use crate::ring::Distance; + // Function to generate a random link distance based on Kleinberg's d^{-1} distribution -pub(crate) fn random_link_distance(d_min: f64) -> f64 { +pub(crate) fn random_link_distance(d_min: Distance) -> Distance { let d_max = 0.5; // Generate a uniform random number between 0 and 1 let u: f64 = rand::thread_rng().gen_range(0.0..1.0); - + // Correct Inverse CDF: F^{-1}(u) = d_min * (d_max / d_min).powf(u) - let d = d_min * (d_max / d_min).powf(u); - - return d; + let d = d_min.as_f64() * (d_max / d_min.as_f64()).powf(u); + + Distance::new(d) } #[cfg(test)] mod tests { - use crate::topology_manager::metric::measure_small_worldness; + use crate::topology::metric::measure_small_worldness; use super::*; use statrs::distribution::*; - use tracing_subscriber::Layer; - + #[test] fn chi_squared_test() { let d_min = 0.01; let d_max = 0.5; - let n = 10000; // Number of samples - let num_bins = 20; // Number of bins for histogram + let n = 10000; // Number of samples + let num_bins = 20; // Number of bins for histogram let mut bins = vec![0; num_bins]; - + // Generate a bunch of link distances for _ in 0..n { - let d = random_link_distance(d_min); + let d = random_link_distance(Distance::new(d_min)).as_f64(); let bin_index = ((d - d_min) / (d_max - d_min) * (num_bins as f64)).floor() as usize; if bin_index < num_bins { bins[bin_index] += 1; } } - + // Perform chi-squared test let mut expected_counts = vec![0.0; num_bins]; for i in 0..num_bins { let lower = d_min + (d_max - d_min) * (i as f64 / num_bins as f64); let upper = d_min + (d_max - d_min) * ((i as f64 + 1.0) / num_bins as f64); - expected_counts[i] = ((upper - lower) / (upper.powf(-1.0) - lower.powf(-1.0))).floor() * n as f64 / num_bins as f64; + expected_counts[i] = ((upper - lower) / (upper.powf(-1.0) - lower.powf(-1.0))).floor() + * n as f64 + / num_bins as f64; } - - let chi_squared = expected_counts.iter().zip(bins.iter()).map(|(&e, &o)| { - ((o as f64 - e) * (o as f64 - e)) / e - }).sum::(); - + + let chi_squared = expected_counts + .iter() + .zip(bins.iter()) + .map(|(&e, &o)| ((o as f64 - e) * (o as f64 - e)) / e) + .sum::(); + // Degrees of freedom is num_bins - 1 let dof = num_bins - 1; let chi = ChiSquared::new(dof as f64).unwrap(); let p_value = 1.0 - chi.cdf(chi_squared); - + // Check if p_value is above 0.05, indicating that we fail to reject the null hypothesis - assert!(p_value > 0.05, "Chi-squared test failed, p_value = {}", p_value); + assert!( + p_value > 0.05, + "Chi-squared test failed, p_value = {}", + p_value + ); } #[test] fn metric_test() { - let d_min = 0.01; - let d_max = 0.5; - let n = 1000; // Number of samples + let d_min = Distance::new(0.01); + let n = 1000; // Number of samples let mut distances = vec![]; // Generate a bunch of link distances for _ in 0..n { @@ -76,6 +84,10 @@ mod tests { println!("Small-world deviation metric = {}", metric); // Check if metric is close to 0.0, indicating that the network is close to the ideal small-world topology - assert!(metric.abs() < 0.1, "Small-world deviation metric is too high, metric = {}", metric); + assert!( + metric.abs() < 0.1, + "Small-world deviation metric is too high, metric = {}", + metric + ); } } diff --git a/crates/core/src/topology_manager/mod.rs b/crates/core/src/topology_manager/mod.rs deleted file mode 100644 index 4be4b7542..000000000 --- a/crates/core/src/topology_manager/mod.rs +++ /dev/null @@ -1,26 +0,0 @@ -mod metric; -mod small_world_rand; - -use std::collections::{BTreeMap, HashMap}; -use rand::Rng; -use crate::ring::*; - -/// Identifies a location on the ring where -pub(crate) fn select_join_target_location(my_location : &Location, peer_statistics: &PeerStatistics) -> Location { - let min_distance : f64 = peer_statistics.0.keys().map(|location| location.distance(my_location)).min().unwrap_or(Distance::new(0.5)).as_f64(); - - let maximum_min_distance = 0.01; - - let min_distance = if min_distance > maximum_min_distance { min_distance } else { maximum_min_distance }; - if peer_statistics.0.len() < 10 { - let dist = small_world_rand::random_link_distance(min_distance); - - return Location::new(0.0); - } - todo!() -} - - -pub struct RequestsPerMinute(f64); - -pub struct PeerStatistics(BTreeMap>); From 8720017e15cbd1e935296b527958de9e30fcd71a Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Fri, 6 Oct 2023 12:08:13 -0500 Subject: [PATCH 23/76] top: testing simulation --- crates/core/src/topology/mod.rs | 52 ++-- crates/core/src/topology/simulation/mod.rs | 162 +------------ .../core/src/topology/simulation/network.rs | 223 ++++++++++++++++++ .../core/src/topology/simulation/simulator.rs | 1 + crates/core/src/topology/simulation/tests.rs | 133 +++++++++++ 5 files changed, 395 insertions(+), 176 deletions(-) create mode 100644 crates/core/src/topology/simulation/network.rs create mode 100644 crates/core/src/topology/simulation/simulator.rs create mode 100644 crates/core/src/topology/simulation/tests.rs diff --git a/crates/core/src/topology/mod.rs b/crates/core/src/topology/mod.rs index 490b59cc4..1a26756a3 100644 --- a/crates/core/src/topology/mod.rs +++ b/crates/core/src/topology/mod.rs @@ -4,12 +4,16 @@ mod metric; mod simulation; mod small_world_rand; +use rand::Rng; + use crate::ring::*; +use self::small_world_rand::random_link_distance; + const DEFAULT_MIN_DISTANCE: f64 = 0.01; pub(crate) enum TopologyStrategy { - Simple, + Random, SmallWorld, LoadBalancing, } @@ -27,19 +31,13 @@ impl TopologyStrategy { peer_statistics: &PeerStatistics, ) -> JoinTargetInfo { match self { - TopologyStrategy::Simple => { - random_strategy(my_location, peer_statistics, TopologyStrategy::Simple) + TopologyStrategy::Random => random_strategy(my_location, peer_statistics), + TopologyStrategy::SmallWorld => { + small_world_metric_strategy(my_location, peer_statistics) + } + TopologyStrategy::LoadBalancing => { + load_balancing_strategy(my_location, peer_statistics) } - TopologyStrategy::SmallWorld => small_world_metric_strategy( - my_location, - peer_statistics, - TopologyStrategy::SmallWorld, - ), - TopologyStrategy::LoadBalancing => load_balancing_strategy( - my_location, - peer_statistics, - TopologyStrategy::LoadBalancing, - ), } } } @@ -47,15 +45,34 @@ impl TopologyStrategy { pub(crate) fn random_strategy( my_location: &Location, peer_statistics: &PeerStatistics, - strategy: TopologyStrategy, ) -> JoinTargetInfo { - unimplemented!() + // Determine distance to closest neighboring peer + let mut min_distance = Distance::new(DEFAULT_MIN_DISTANCE); + for peer in peer_statistics.peers.iter() { + let distance = my_location.distance(&peer.location); + if distance < min_distance { + min_distance = distance; + } + } + let distance_to_target = random_link_distance(min_distance); + + let direction = if rand::thread_rng().gen_bool(0.5) { + 1.0 + } else { + -1.0 + }; + let target = Location::new_rounded(my_location.as_f64() * direction); + let threshold = Distance::new(distance_to_target.as_f64() / 2.0); + JoinTargetInfo { + target, + threshold, + strategy: TopologyStrategy::Random, + } } pub(crate) fn small_world_metric_strategy( my_location: &Location, peer_statistics: &PeerStatistics, - strategy: TopologyStrategy, ) -> JoinTargetInfo { unimplemented!() } @@ -63,14 +80,13 @@ pub(crate) fn small_world_metric_strategy( pub(crate) fn load_balancing_strategy( my_location: &Location, peer_statistics: &PeerStatistics, - strategy: TopologyStrategy, ) -> JoinTargetInfo { unimplemented!() } pub(crate) fn select_strategy(num_neighbors: usize) -> TopologyStrategy { if num_neighbors < 10 { - TopologyStrategy::Simple + TopologyStrategy::Random } else { // Randomly select between the three strategies based on your criteria unimplemented!() diff --git a/crates/core/src/topology/simulation/mod.rs b/crates/core/src/topology/simulation/mod.rs index 2368e8d47..7a0a6f090 100644 --- a/crates/core/src/topology/simulation/mod.rs +++ b/crates/core/src/topology/simulation/mod.rs @@ -1,160 +1,6 @@ use crate::network_sim::Location; -use std::collections::{HashMap, HashSet}; -use tracing::{debug, info, warn, instrument}; +use std::collections::{HashMap, HashSet, LinkedList}; +use tracing::{debug, info, instrument, warn}; -use super::*; - -#[derive(Debug)] -struct SimulatedNetwork { - nodes: Vec, - connections: HashMap>, - requests: HashMap>, // origin node -> destination node -> requests -} - -impl SimulatedNetwork { - fn new() -> Self { - Self { - nodes: Vec::new(), - connections: HashMap::new(), - requests: HashMap::new(), - } - } - - fn add_node(&mut self) -> NodeRef { - let index = self.nodes.len(); - let node = SimulatedNode { - location: Location::random(), - index, - }; - debug!("Adding node {:?}", node); - self.nodes.push(node); - NodeRef { index } - } - - fn connect(&mut self, a: NodeRef, b: NodeRef) { - info!("Connecting {:?} and {:?}", a, b); - self.connections.entry(a).or_default().insert(b); - self.connections.entry(b).or_default().insert(a); - } - - fn disconnect(&mut self, a: NodeRef, b: NodeRef) { - info!("Disconnecting {:?} and {:?}", a, b); - self.connections.entry(a).or_default().remove(&b); - self.connections.entry(b).or_default().remove(&a); - } - - fn route(&self, source: &NodeRef, destination: Location) -> Result, RouteError> { - info!("Routing from {:?} to {:?}", source, destination); - let mut current = *source; // Dereference to copy - let mut visited = Vec::new(); - let mut recent_nodes = Vec::new(); // To track the sequence of last N nodes - - loop { - debug!("Current node: {:?}", current); - - // Check if we've reached the destination - if self.nodes[current.index].location == destination { - info!("Reached destination"); - return Ok(visited); - } - - // Get current node's distance to the destination - let current_distance = self.nodes[current.index] - .location - .distance(destination) - .as_f64(); - debug!("Current distance to destination: {}", current_distance); - - // Find the closest connected node to the destination that hasn't been visited - let closest_connections = match self.connections.get(¤t) { - Some(connections) => connections, - None => { - // Handle the None case here. For example, you might want to return agit n Err value or break the loop. - return Err(RouteError::NoRoute); - } - }; - let (closest, closest_distance) = closest_connections - .iter() - .map(|&neighbor| { - let distance = self.nodes[neighbor.index] - .location - .distance(destination) - .as_f64(); - (neighbor, distance) - }) - .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)) - .unwrap_or((current, current_distance)); - - debug!( - "Closest node: {:?}, distance: {}", - closest, closest_distance - ); - - // Check for a loop by looking at the sequence of last N nodes - recent_nodes.push(closest); - if recent_nodes.len() > 10 { - // Keep only the last 10 visited nodes in the list - recent_nodes.remove(0); - } - if recent_nodes.len() > 2 && recent_nodes.first() == recent_nodes.last() { - warn!("Loop detected"); - return Err(RouteError::Loop); - } - - // Update visited nodes and current node - visited.push(closest); - current = closest; - } - } - - fn join( - &self, - source: &NodeRef, - target: Location, - tolerance: Distance, - ) -> Option> { - info!("Joining via {:?} with target {:?}", source, target); - let join_route = self.route(source, target); - let join_route: Vec = match join_route { - Ok(route) => route, - Err(e) => { - warn!("Error joining network: {:?}", e); - return Option::None; - } - }; - - let mut joiners = Vec::new(); - - for node in join_route.iter() { - let node_location = self.nodes[node.index].location; - let distance = node_location.distance(target); - if distance < tolerance { - info!("Found node {:?} within tolerance", node); - joiners.push(node.clone()); - } - } - - Option::Some(joiners) - } -} - -#[derive(Debug)] -enum RouteError { - NoRoute, - Loop, - DeadEnd, -} - -#[derive(Debug)] -struct SimulatedNode { - index: usize, - location: Location, -} - -#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] -struct NodeRef { - index: usize, -} - -#[cfg(test)] -mod tests; +mod network; +mod simulator; diff --git a/crates/core/src/topology/simulation/network.rs b/crates/core/src/topology/simulation/network.rs new file mode 100644 index 000000000..f4cbd20aa --- /dev/null +++ b/crates/core/src/topology/simulation/network.rs @@ -0,0 +1,223 @@ +use crate::ring::Distance; + +use super::*; + +#[derive(Debug)] +struct Network { + current_time: u64, + nodes: Vec, + connections: HashMap>, + requests: HashMap)>, // origin node -> destination node -> requests +} + +const MAX_EVENTS_TO_TRACK: usize = 100; + +#[derive(Debug)] +struct EventStatTracker { + recent_event_times: LinkedList, +} + +impl EventStatTracker { + fn new() -> Self { + Self { + recent_event_times: LinkedList::new(), + } + } + + fn get_event_times(&self) -> &LinkedList { + &self.recent_event_times + } + + fn add_event(&mut self, time: u64) { + self.recent_event_times.push_back(time); + // if there are recent_event_times remove the oldest event times if the exceed MAX_EVENTS_TO_TRACK + while self.recent_event_times.len() > MAX_EVENTS_TO_TRACK { + self.recent_event_times.pop_front(); + } + } +} + +impl Network { + fn tick(&mut self) { + self.current_time += 1; + } + + fn new() -> Self { + Self { + current_time: 0, + nodes: Vec::new(), + connections: HashMap::new(), + requests: HashMap::new(), + } + } + + fn add_node(&mut self) -> NodeRef { + let index = self.nodes.len(); + let node = SimulatedNode { + location: Location::random(), + index, + }; + debug!("Adding node {:?}", node); + self.nodes.push(node); + NodeRef { index } + } + + fn connect(&mut self, a: NodeRef, b: NodeRef) { + // throw an error if a == b + assert!(a != b, "Cannot connect a node to itself"); + + info!("Connecting {:?} and {:?}", a, b); + self.connections.entry(a).or_default().insert(b); + self.connections.entry(b).or_default().insert(a); + } + + fn disconnect(&mut self, a: NodeRef, b: NodeRef) { + info!("Disconnecting {:?} and {:?}", a, b); + self.connections.entry(a).or_default().remove(&b); + self.connections.entry(b).or_default().remove(&a); + } + + fn route(&self, source: &NodeRef, destination: Location) -> Result, RouteError> { + info!("Routing from {:?} to {:?}", source, destination); + let mut current = *source; // Dereference to copy + let mut visited = Vec::new(); + let mut recent_nodes = Vec::new(); // To track the sequence of last N nodes + + loop { + debug!("Current node: {:?}", current); + + // Check if we've reached the destination + if self.nodes[current.index].location == destination { + info!("Reached destination"); + return Ok(visited); + } + + // Get current node's distance to the destination + let current_distance = self.nodes[current.index] + .location + .distance(destination) + .as_f64(); + debug!("Current distance to destination: {}", current_distance); + + // Find the closest connected node to the destination that hasn't been visited + let closest_connections = match self.connections.get(¤t) { + Some(connections) => connections, + None => { + // Handle the None case here. For example, you might want to return agit n Err value or break the loop. + return Err(RouteError::NoRoute); + } + }; + let (closest, closest_distance) = closest_connections + .iter() + .map(|&neighbor| { + let distance = self.nodes[neighbor.index] + .location + .distance(destination) + .as_f64(); + (neighbor, distance) + }) + .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)) + .unwrap_or((current, current_distance)); + + debug!( + "Closest node: {:?}, distance: {}", + closest, closest_distance + ); + + // Check for a loop by looking at the sequence of last N nodes + recent_nodes.push(closest); + if recent_nodes.len() > 10 { + // Keep only the last 10 visited nodes in the list + recent_nodes.remove(0); + } + if recent_nodes.len() > 2 && recent_nodes.first() == recent_nodes.last() { + warn!("Loop detected"); + return Err(RouteError::Loop); + } + + // Update visited nodes and current node + visited.push(closest); + current = closest; + } + } + + fn record_request(&mut self, a: &NodeRef, b: &NodeRef) { + let (count, dest_map) = self + .requests + .entry(*a) + .or_insert((EventStatTracker::new(), HashMap::new())); + count.add_event(self.current_time); + dest_map + .entry(*b) + .or_insert(EventStatTracker::new()) + .add_event(self.current_time); + } + + fn reset_recorded_requests(&mut self) { + self.requests.clear(); + } + + fn get_join_peers( + &self, + source: &NodeRef, + target: Location, + tolerance: Distance, + ) -> Option> { + info!("Joining via {:?} with target {:?}", source, target); + let join_route = self.route(source, target); + let join_route: Vec = match join_route { + Ok(route) => route, + Err(e) => { + warn!("Error joining network: {:?}", e); + return Option::None; + } + }; + + let mut joiners = Vec::new(); + + for node in join_route.iter() { + let node_location = self.nodes[node.index].location; + let distance = node_location.distance(target); + if distance < tolerance { + info!("Found node {:?} within tolerance", node); + joiners.push(*node); + } + } + + Option::Some(joiners) + } + + fn join(&mut self, node: NodeRef, target: Location, tolerance: Distance) { + info!("Joining {:?} with target {:?}", node, target); + let joiners = self.get_join_peers(&node, target, tolerance); + let joiners: Vec = match joiners { + Some(joiners) => joiners, + None => { + warn!("Error joining network"); + return; + } + }; + + for joiner in joiners.iter() { + self.connect(node, *joiner); + } + } +} + +#[derive(Debug)] +enum RouteError { + NoRoute, + Loop, + DeadEnd, +} + +#[derive(Debug)] +struct SimulatedNode { + index: usize, + location: Location, +} + +#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] +struct NodeRef { + index: usize, +} diff --git a/crates/core/src/topology/simulation/simulator.rs b/crates/core/src/topology/simulation/simulator.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/crates/core/src/topology/simulation/simulator.rs @@ -0,0 +1 @@ + diff --git a/crates/core/src/topology/simulation/tests.rs b/crates/core/src/topology/simulation/tests.rs new file mode 100644 index 000000000..b8a057704 --- /dev/null +++ b/crates/core/src/topology/simulation/tests.rs @@ -0,0 +1,133 @@ +use super::*; + +fn setup() { + // Initialize the tracer + tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) // Adjust the level here + .try_init() + .unwrap_or(()); +} +#[test] +fn test_add_node() { + setup(); + let mut net = SimulatedNetwork::new(); + let node_ref = net.add_node(); + assert_eq!(node_ref.index, 0); + assert_eq!(net.nodes.len(), 1); +} + +#[test] +fn test_connect() { + setup(); + let mut net = SimulatedNetwork::new(); + let a = net.add_node(); + let b = net.add_node(); + net.connect(a, b); + assert!(net.connections.get(&a).unwrap().contains(&b)); + assert!(net.connections.get(&b).unwrap().contains(&a)); +} + +#[test] +fn test_disconnect() { + setup(); + let mut net = SimulatedNetwork::new(); + let a = net.add_node(); + let b = net.add_node(); + net.connect(a, b); + net.disconnect(a, b); + assert!(!net.connections.get(&a).unwrap().contains(&b)); + assert!(!net.connections.get(&b).unwrap().contains(&a)); +} + +#[test] +fn test_route_success() { + setup(); + let mut net = SimulatedNetwork::new(); + let a = net.add_node(); + let b = net.add_node(); + net.connect(a, b); + + // Mock a destination location that's the same as node b's location + let destination = net.nodes[b.index].location; + + let result = net.route(&a, destination); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), vec![b]); +} + +#[test] +fn test_route_loop_error() { + setup(); + + let mut net = SimulatedNetwork::new(); + let a = net.add_node(); + let b = net.add_node(); + let c = net.add_node(); + net.connect(a, b); + net.connect(b, c); + net.connect(c, a); + + // Mock a destination location that's not reachable + let destination = Location::random(); // Assume random will not match any node + + let result = net.route(&a, destination); + assert!(matches!(result, Err(RouteError::Loop))); +} + +#[test] +fn test_route_dead_end() { + setup(); + let mut net = SimulatedNetwork::new(); + let a = net.add_node(); + let destination = net.nodes[a.index].location; + + let result = net.route(&a, destination); + assert!(result.is_ok()); + assert!(result.unwrap().is_empty()); // Since there's no closer node +} +#[test] +fn test_join_success() { + let mut net = SimulatedNetwork::new(); + let a = net.add_node(); + let b = net.add_node(); + let c = net.add_node(); + net.connect(a, b); + net.connect(b, c); + + let destination = net.nodes[c.index].location; + let tolerance = Distance::new(0.4); // Changed to a valid value + + let join_result = net.get_join_peers(&a, destination, tolerance); + + assert!(join_result.is_some()); + assert_eq!(join_result.unwrap(), vec![b, c]); +} + +#[test] +fn test_join_failure() { + let mut net = SimulatedNetwork::new(); + let a = net.add_node(); + + let destination = Location::random(); + let tolerance = Distance::new(0.4); // Changed to a valid value + + let join_result = net.get_join_peers(&a, destination, tolerance); + + assert!(join_result.is_none()); +} + +#[test] +fn test_join_with_tolerance() { + let mut net = SimulatedNetwork::new(); + let a = net.add_node(); + let b = net.add_node(); + net.connect(a, b); + + let destination = net.nodes[b.index].location; + let tolerance = Distance::new(0.1); + + let join_result = net.get_join_peers(&a, destination, tolerance); + + assert!(join_result.is_some()); + assert_eq!(join_result.unwrap(), vec![b]); +} From 188be5778db0c3bc5dd4b3f38430c835900bcd19 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Mon, 2 Oct 2023 15:56:17 +0200 Subject: [PATCH 24/76] Add submodules to audit action --- .github/workflows/audit.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index e709d18ed..b182c4037 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -11,6 +11,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: true - uses: actions-rs/audit-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} From 3e8fa067d080ae2601d5c0521ce3f5ad4a6bde5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Oct 2023 19:59:13 +0000 Subject: [PATCH 25/76] Bump styfle/cancel-workflow-action from 0.11.0 to 0.12.0 Bumps [styfle/cancel-workflow-action](https://github.com/styfle/cancel-workflow-action) from 0.11.0 to 0.12.0. - [Release notes](https://github.com/styfle/cancel-workflow-action/releases) - [Commits](https://github.com/styfle/cancel-workflow-action/compare/0.11.0...0.12.0) --- updated-dependencies: - dependency-name: styfle/cancel-workflow-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7491944f3..ae257aaf9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.11.0 + uses: styfle/cancel-workflow-action@0.12.0 with: access_token: ${{ github.token }} @@ -63,7 +63,7 @@ jobs: steps: - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.11.0 + uses: styfle/cancel-workflow-action@0.12.0 with: access_token: ${{ github.token }} From 41528d4d109e194ae9d4ab06ed5d5e68ee561a0f Mon Sep 17 00:00:00 2001 From: Nacho Duart Date: Tue, 10 Oct 2023 07:51:05 +0200 Subject: [PATCH 26/76] #186148680 - Layer1 contract abstractions (#861) * Change wasm linear mem manipulation * Avoid wasm entry function name collision By requiring a feature to be enabled * Add submodules to audit action * Update stdlib * Update deps * Fix clippy issues --- Cargo.lock | 256 +++++++++--------- Cargo.toml | 4 +- .../contracts/inbox/Cargo.toml | 3 +- .../web/container/Cargo.toml | 4 + .../contracts/posts/Cargo.toml | 4 + .../web/container/Cargo.toml | 3 + crates/core/src/contract/storages/rocks_db.rs | 2 +- crates/core/src/ring.rs | 10 +- crates/core/src/router/isotonic_estimator.rs | 2 +- crates/core/src/runtime/error.rs | 4 +- crates/core/src/runtime/wasm_runtime.rs | 12 +- crates/fdev/src/build.rs | 33 ++- crates/fdev/src/config.rs | 9 + .../token-allocation-record/Cargo.toml | 4 + .../delegates/token-generator/Cargo.toml | 4 + modules/identity-management/Cargo.toml | 1 + modules/identity-management/src/lib.rs | 6 +- stdlib | 2 +- tests/test-contract-1/Cargo.lock | 4 +- tests/test-contract-1/Cargo.toml | 4 +- tests/test-contract-2/Cargo.lock | 4 +- tests/test-contract-2/Cargo.toml | 2 + tests/test-delegate-1/Cargo.lock | 4 +- tests/test-delegate-1/Cargo.toml | 4 +- 24 files changed, 206 insertions(+), 179 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b338aad2..8891bc286 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,9 +87,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] @@ -117,9 +117,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.5.0" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" dependencies = [ "anstyle", "anstyle-parse", @@ -131,15 +131,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" dependencies = [ "utf8parse", ] @@ -155,9 +155,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "2.1.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", "windows-sys 0.48.0", @@ -266,7 +266,7 @@ dependencies = [ "log", "parking", "polling", - "rustix 0.37.23", + "rustix 0.37.24", "slab", "socket2 0.4.9", "waker-fn", @@ -289,7 +289,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -449,7 +449,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -556,9 +556,9 @@ checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" @@ -705,9 +705,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.5" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824956d0dca8334758a5b7f7e50518d66ea319330cbceedcf76905c2f6ab30e3" +checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" dependencies = [ "clap_builder", "clap_derive", @@ -715,9 +715,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.5" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122ec64120a49b4563ccaedcbea7818d069ed8e9aa6d829b82d8a4128936b2ab" +checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" dependencies = [ "anstream", "anstyle", @@ -734,7 +734,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -1067,9 +1067,9 @@ dependencies = [ [[package]] name = "curl-sys" -version = "0.4.66+curl-8.3.0" +version = "0.4.67+curl-8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70c44a72e830f0e40ad90dda8a6ab6ed6314d39776599a58a2e5e37fbc6db5b9" +checksum = "3cc35d066510b197a0f72de863736641539957628c8a42e70e27c66849e77c34" dependencies = [ "cc", "libc", @@ -1118,7 +1118,7 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -1142,7 +1142,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -1153,7 +1153,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -1163,7 +1163,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.0", + "hashbrown 0.14.1", "lock_api", "once_cell", "parking_lot_core", @@ -1248,7 +1248,7 @@ checksum = "53e0efad4403bfc52dc201159c4b842a246a14b98c64b55dfd0f2d89729dfeb8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -1301,7 +1301,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -1405,7 +1405,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -1416,25 +1416,14 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" dependencies = [ - "errno-dragonfly", "libc", "windows-sys 0.48.0", ] -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "etcetera" version = "0.8.0" @@ -1494,7 +1483,7 @@ dependencies = [ "tar", "thiserror", "tokio", - "toml 0.8.1", + "toml 0.8.2", "tracing", "tracing-subscriber", "xz2", @@ -1631,20 +1620,16 @@ dependencies = [ [[package]] name = "freenet-macros" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e9fac5b43bee97b6ee49a376ca9e26eedf8bd581efcb176e4a534304f8b4279" +version = "0.0.5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] name = "freenet-stdlib" -version = "0.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a408ccb448697333c9e3f8a935976c223db980d2fb6a076ae7c89851c6ed17a" +version = "0.0.8" dependencies = [ "arbitrary", "arrayvec", @@ -1658,6 +1643,7 @@ dependencies = [ "futures", "js-sys", "once_cell", + "rand", "semver", "serde", "serde-wasm-bindgen 0.6.0", @@ -1670,6 +1656,7 @@ dependencies = [ "tracing", "tracing-subscriber", "wasm-bindgen", + "wasmer", "web-sys", ] @@ -1771,7 +1758,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -1914,9 +1901,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" dependencies = [ "ahash 0.8.3", "allocator-api2", @@ -1928,7 +1915,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.0", + "hashbrown 0.14.1", ] [[package]] @@ -2169,12 +2156,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.1", "serde", ] @@ -2363,9 +2350,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.148" +version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "libloading" @@ -2379,9 +2366,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libp2p" @@ -2535,12 +2522,13 @@ dependencies = [ [[package]] name = "libp2p-identity" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686e73aff5e23efbb99bc85340ea6fd8686986aa7b283a881ba182cfca535ca9" +checksum = "57bf6e730ec5e7022958da53ffb03b326e681b7316939012ae9b3c7449a812d4" dependencies = [ "bs58", "ed25519-dalek", + "hkdf", "log", "multihash", "quick-protobuf", @@ -2704,7 +2692,7 @@ dependencies = [ "proc-macro-warning", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -2807,9 +2795,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.7" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" [[package]] name = "lock_api" @@ -2914,9 +2902,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.3" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memmap2" @@ -3317,9 +3305,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", "libm", @@ -3622,7 +3610,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -3659,7 +3647,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -3770,7 +3758,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -3805,14 +3793,14 @@ checksum = "3d1eaa7fa0aa1929ffdf7eeb6eac234dde6268914a14ad44d23521ab6a9b258e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] name = "proc-macro2" -version = "1.0.67" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] @@ -3837,7 +3825,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -4081,14 +4069,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.5" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +checksum = "d119d7c7ca818f8a53c300863d4f87566aac09943aef5b355bb83969dae75d87" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.8", - "regex-syntax 0.7.5", + "regex-automata 0.4.1", + "regex-syntax 0.8.0", ] [[package]] @@ -4102,13 +4090,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.8" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +checksum = "465c6fc0621e4abc4187a2bda0937bfd4f722c2730b29562e19689ea796c9a4b" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.5", + "regex-syntax 0.8.0", ] [[package]] @@ -4119,9 +4107,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +checksum = "c3cbb081b9784b07cceb8824c8583f86db4814d172ab043f3c23f7dc600bf83d" [[package]] name = "region" @@ -4298,9 +4286,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.23" +version = "0.37.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +checksum = "4279d76516df406a8bd37e7dff53fd37d1a093f997a3c34a5c21658c126db06d" dependencies = [ "bitflags 1.3.2", "errno", @@ -4312,14 +4300,14 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.14" +version = "0.38.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" +checksum = "5a74ee2d7c2581cd139b42447d7d9389b889bdaad3a73f1ebb16f2a3237bb19c" dependencies = [ "bitflags 2.4.0", "errno", "libc", - "linux-raw-sys 0.4.7", + "linux-raw-sys 0.4.10", "windows-sys 0.48.0", ] @@ -4434,9 +4422,9 @@ checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6" [[package]] name = "semver" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" dependencies = [ "serde", ] @@ -4489,7 +4477,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -4534,7 +4522,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.0.0", + "indexmap 2.0.2", "serde", "serde_json", "serde_with_macros", @@ -4550,7 +4538,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -4577,9 +4565,9 @@ dependencies = [ [[package]] name = "sharded-slab" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] @@ -4791,7 +4779,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.0.0", + "indexmap 2.0.2", "log", "memchr", "once_cell", @@ -5034,9 +5022,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.37" +version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ "proc-macro2", "quote", @@ -5114,7 +5102,7 @@ dependencies = [ "cfg-if", "fastrand 2.0.1", "redox_syscall 0.3.5", - "rustix 0.38.14", + "rustix 0.38.18", "windows-sys 0.48.0", ] @@ -5135,7 +5123,7 @@ checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -5215,9 +5203,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.32.0" +version = "1.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" dependencies = [ "backtrace", "bytes", @@ -5240,7 +5228,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -5290,11 +5278,11 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc1433177506450fe920e46a4f9812d0c211f5dd556da10e731a0a3dfa151f0" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.0.2", "serde", "serde_spanned", "toml_datetime", @@ -5312,11 +5300,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca676d9ba1a322c1b64eb8045a5ec5c0cfb0c9d08e15e9ff622589ad5221c8fe" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.0.2", "serde", "serde_spanned", "toml_datetime", @@ -5397,7 +5385,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -5772,7 +5760,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", "wasm-bindgen-shared", ] @@ -5817,7 +5805,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5839,9 +5827,9 @@ dependencies = [ [[package]] name = "wasmer" -version = "4.2.0" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7954f3bdeb6cc9b46ddfbab7d795fc6a249a79f51c102d95679390526cf43d8" +checksum = "0e626f958755a90a6552b9528f59b58a62ae288e6c17fcf40e99495bc33c60f0" dependencies = [ "bytes", "cfg-if", @@ -5868,9 +5856,9 @@ dependencies = [ [[package]] name = "wasmer-compiler" -version = "4.2.0" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c72380ddff4d9710366527100183df6f943caab5b63adc7cac6d90bacdf4628" +checksum = "848e1922694cf97f4df680a0534c9d72c836378b5eb2313c1708fe1a75b40044" dependencies = [ "backtrace", "bytes", @@ -5895,9 +5883,9 @@ dependencies = [ [[package]] name = "wasmer-compiler-cranelift" -version = "4.2.0" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957adcc170f007bf26293c7cd95352c1e471d7bf4295a1dfda796702a79d9648" +checksum = "3d96bce6fad15a954edcfc2749b59e47ea7de524b6ef3df392035636491a40b4" dependencies = [ "cranelift-codegen", "cranelift-entity", @@ -5914,9 +5902,9 @@ dependencies = [ [[package]] name = "wasmer-derive" -version = "4.2.0" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cd92754ce26eec6136ae6282d96ca59632bd47237ecba82824de16e34b3ce6b" +checksum = "7f08f80d166a9279671b7af7a09409c28ede2e0b4e3acabbf0e3cb22c8038ba7" dependencies = [ "proc-macro-error", "proc-macro2", @@ -5926,9 +5914,9 @@ dependencies = [ [[package]] name = "wasmer-types" -version = "4.2.0" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93ca35c9a184e7d561f496c5d6da835c8309a8f76fcca1384f5800127395735" +checksum = "ae2c892882f0b416783fb4310e5697f5c30587f6f9555f9d4f2be85ab39d5d3d" dependencies = [ "bytecheck", "enum-iterator", @@ -5942,9 +5930,9 @@ dependencies = [ [[package]] name = "wasmer-vm" -version = "4.2.0" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200ff711048c0a2cb045d4984a673a1edea18f1e14f024694a43f4951922998b" +checksum = "7c0a9a57b627fb39e5a491058d4365f099bc9b140031c000fded24a3306d9480" dependencies = [ "backtrace", "cc", @@ -5980,9 +5968,9 @@ dependencies = [ [[package]] name = "wast" -version = "65.0.2" +version = "66.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55a88724cf8c2c0ebbf32c8e8f4ac0d6aa7ba6d73a1cfd94b254aa8f894317e" +checksum = "0da7529bb848d58ab8bf32230fc065b363baee2bd338d5e58c589a1e7d83ad07" dependencies = [ "leb128", "memchr", @@ -5992,9 +5980,9 @@ dependencies = [ [[package]] name = "wat" -version = "1.0.74" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83e1a8d86d008adc7bafa5cf4332d448699a08fcf2a715a71fbb75e2c5ca188" +checksum = "4780374047c65b6b6e86019093fe80c18b66825eb684df778a4e068282a780e7" dependencies = [ "wast", ] @@ -6245,9 +6233,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" +checksum = "037711d82167854aff2018dfd193aa0fef5370f456732f0d5a0c59b0f1b4b907" dependencies = [ "memchr", ] @@ -6373,5 +6361,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] diff --git a/Cargo.toml b/Cargo.toml index 3957663a0..940caf6eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,8 +21,8 @@ tracing = "0.1" tracing-subscriber = "0.3" wasmer = "4.2.0" -# freenet-stdlib = { path = "./stdlib/rust/", version = "0.0.8" } -freenet-stdlib = { version = "0.0.7" } +freenet-stdlib = { path = "./stdlib/rust/", version = "0.0.8", features = ["unstable"] } +# freenet-stdlib = { version = "0.0.7" } [profile.dev.package."*"] opt-level = 3 diff --git a/apps/freenet-email-app/contracts/inbox/Cargo.toml b/apps/freenet-email-app/contracts/inbox/Cargo.toml index 73a932bd1..5fe8c9bd9 100644 --- a/apps/freenet-email-app/contracts/inbox/Cargo.toml +++ b/apps/freenet-email-app/contracts/inbox/Cargo.toml @@ -19,6 +19,7 @@ thiserror = "1" crate-type = ["cdylib", "rlib"] [features] -default = [] +default = ["freenet-main-contract"] contract = [] +freenet-main-contract = [] wasmbind = ["chrono/wasmbind"] diff --git a/apps/freenet-email-app/web/container/Cargo.toml b/apps/freenet-email-app/web/container/Cargo.toml index 2bbe54a41..678295d24 100644 --- a/apps/freenet-email-app/web/container/Cargo.toml +++ b/apps/freenet-email-app/web/container/Cargo.toml @@ -11,3 +11,7 @@ freenet-stdlib = { workspace = true } [lib] crate-type = ["cdylib"] + +[features] +default = ["freenet-main-contract"] +freenet-main-contract = [] diff --git a/apps/freenet-microblogging/contracts/posts/Cargo.toml b/apps/freenet-microblogging/contracts/posts/Cargo.toml index 37ff5aeae..c267e68b2 100644 --- a/apps/freenet-microblogging/contracts/posts/Cargo.toml +++ b/apps/freenet-microblogging/contracts/posts/Cargo.toml @@ -20,3 +20,7 @@ crate-type = ["cdylib"] [build-dependencies] serde = "1" serde_json = "1" + +[features] +default = ["freenet-main-contract"] +freenet-main-contract = [] diff --git a/apps/freenet-microblogging/web/container/Cargo.toml b/apps/freenet-microblogging/web/container/Cargo.toml index a70fbdf08..71a3bad3c 100644 --- a/apps/freenet-microblogging/web/container/Cargo.toml +++ b/apps/freenet-microblogging/web/container/Cargo.toml @@ -11,3 +11,6 @@ freenet-stdlib = { workspace = true } [lib] crate-type = ["cdylib"] + +[features] +freenet-main-contract = [] diff --git a/crates/core/src/contract/storages/rocks_db.rs b/crates/core/src/contract/storages/rocks_db.rs index 9d58c85d3..44d66de43 100644 --- a/crates/core/src/contract/storages/rocks_db.rs +++ b/crates/core/src/contract/storages/rocks_db.rs @@ -48,7 +48,7 @@ impl StateStorage for RocksDb { "failed getting contract: `{key}` {}", key.encoded_code_hash() .map(|ch| format!("(with code hash: `{ch}`)")) - .unwrap_or(String::new()) + .unwrap_or_default() ); Ok(None) } diff --git a/crates/core/src/ring.rs b/crates/core/src/ring.rs index b7d871884..dff723b75 100644 --- a/crates/core/src/ring.rs +++ b/crates/core/src/ring.rs @@ -11,7 +11,6 @@ //! - final location use std::{ - borrow::Borrow, collections::BTreeMap, convert::TryFrom, fmt::Display, @@ -438,7 +437,7 @@ impl From<&ContractKey> for Location { fn from(key: &ContractKey) -> Self { let mut value = 0.0; let mut divisor = 256.0; - for byte in key.borrow().bytes().iter().take(7) { + for byte in key.bytes().iter().take(7) { value += *byte as f64 / divisor; divisor *= 256.0; } @@ -472,7 +471,7 @@ impl Ord for Location { impl PartialOrd for Location { fn partial_cmp(&self, other: &Self) -> Option { - self.0.partial_cmp(&other.0) + Some(self.cmp(other)) } } @@ -523,7 +522,7 @@ impl PartialEq for Distance { #[allow(clippy::incorrect_partial_ord_impl_on_ord_type)] impl PartialOrd for Distance { fn partial_cmp(&self, other: &Self) -> Option { - self.0.partial_cmp(&other.0) + Some(self.cmp(other)) } } @@ -531,7 +530,8 @@ impl Eq for Distance {} impl Ord for Distance { fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.partial_cmp(other) + self.0 + .partial_cmp(&other.0) .expect("always should return a cmp value") } } diff --git a/crates/core/src/router/isotonic_estimator.rs b/crates/core/src/router/isotonic_estimator.rs index a8ef32180..300854e11 100644 --- a/crates/core/src/router/isotonic_estimator.rs +++ b/crates/core/src/router/isotonic_estimator.rs @@ -103,7 +103,7 @@ impl IsotonicEstimator { self.peer_adjustments .entry(event.peer) - .or_insert_with(Adjustment::default) + .or_default() .add(adjustment); } } diff --git a/crates/core/src/runtime/error.rs b/crates/core/src/runtime/error.rs index d7c4a24ec..d45cd86e2 100644 --- a/crates/core/src/runtime/error.rs +++ b/crates/core/src/runtime/error.rs @@ -82,7 +82,7 @@ macro_rules! impl_err { } impl_err!(Box); -impl_err!(freenet_stdlib::buf::Error); +impl_err!(freenet_stdlib::memory::buf::Error); impl_err!(std::io::Error); impl_err!(secrets_store::SecretStoreError); impl_err!(bincode::Error); @@ -100,7 +100,7 @@ pub(crate) enum RuntimeInnerError { Any(#[from] Box), #[error(transparent)] - BufferError(#[from] freenet_stdlib::buf::Error), + BufferError(#[from] freenet_stdlib::memory::buf::Error), #[error(transparent)] IOError(#[from] std::io::Error), diff --git a/crates/core/src/runtime/wasm_runtime.rs b/crates/core/src/runtime/wasm_runtime.rs index aa762e9aa..7c5d32e83 100644 --- a/crates/core/src/runtime/wasm_runtime.rs +++ b/crates/core/src/runtime/wasm_runtime.rs @@ -1,7 +1,10 @@ use std::{collections::HashMap, sync::atomic::AtomicI64}; use freenet_stdlib::{ - buf::{BufferBuilder, BufferMut}, + memory::{ + buf::{BufferBuilder, BufferMut}, + WasmLinearMem, + }, prelude::*, }; use wasmer::{imports, Bytes, Imports, Instance, Memory, MemoryType, Module, Store, TypedFunction}; @@ -148,7 +151,7 @@ impl Runtime { let data = data.as_ref(); let initiate_buffer: TypedFunction = instance .exports - .get_typed_function(&self.wasm_store, "initiate_buffer")?; + .get_typed_function(&self.wasm_store, "__frnt__initiate_buffer")?; let builder_ptr = initiate_buffer.call(&mut self.wasm_store, data.len() as u32)?; let linear_mem = self.linear_mem(instance)?; unsafe { @@ -166,10 +169,7 @@ impl Runtime { .map(Ok) .unwrap_or_else(|| instance.exports.get_memory("memory"))? .view(&self.wasm_store); - Ok(WasmLinearMem { - start_ptr: memory.data_ptr() as *const _, - size: memory.data_size(), - }) + Ok(unsafe { WasmLinearMem::new(memory.data_ptr() as *const _, memory.data_size()) }) } pub(super) fn prepare_contract_call( diff --git a/crates/fdev/src/build.rs b/crates/fdev/src/build.rs index 1fd431ba7..a02b90949 100644 --- a/crates/fdev/src/build.rs +++ b/crates/fdev/src/build.rs @@ -28,39 +28,42 @@ pub fn build_package(cli_config: BuildToolCliConfig, cwd: &Path) -> Result<(), D } } -fn compile_rust_wasm_lib(cli_config: &BuildToolCliConfig, work_dir: &Path) -> Result<(), DynError> { - let package_type = cli_config.package_type; - const RUST_TARGET_ARGS: &[&str] = &["build", "--lib", "--target"]; +fn compile_options(cli_config: &BuildToolCliConfig) -> impl Iterator { let release: &[&str] = if cli_config.debug { &[] } else { &["--release"] }; - let target = WASM_TARGET; - let cmd_args = cli_config - .features - .as_ref() - .iter() - .flat_map(|x| ["--features", x.as_str()]) - .chain(release.iter().copied()) - .collect::>(); + let feature_list = cli_config.features.iter().flat_map(|s| { + s.split(',') + .filter(|p| *p != cli_config.package_type.feature()) + }); + let features = ["--features", cli_config.package_type.feature()] + .into_iter() + .chain(feature_list); + features.chain(release.iter().copied()) +} + +fn compile_rust_wasm_lib(cli_config: &BuildToolCliConfig, work_dir: &Path) -> Result<(), DynError> { + const RUST_TARGET_ARGS: &[&str] = &["build", "--lib", "--target"]; use std::io::IsTerminal; let cmd_args = if std::io::stdout().is_terminal() && std::io::stderr().is_terminal() { RUST_TARGET_ARGS .iter() .copied() - .chain([target, "--color", "always"]) - .chain(cmd_args) + .chain([WASM_TARGET, "--color", "always"]) + .chain(compile_options(cli_config)) .collect::>() } else { RUST_TARGET_ARGS .iter() .copied() - .chain([target]) - .chain(cmd_args) + .chain([WASM_TARGET]) + .chain(compile_options(cli_config)) .collect::>() }; + let package_type = cli_config.package_type; println!("Compiling {package_type} with rust"); let child = Command::new("cargo") .args(&cmd_args) diff --git a/crates/fdev/src/config.rs b/crates/fdev/src/config.rs index f1d63c703..f7dee00ad 100644 --- a/crates/fdev/src/config.rs +++ b/crates/fdev/src/config.rs @@ -121,6 +121,15 @@ pub(crate) enum PackageType { Delegate, } +impl PackageType { + pub fn feature(&self) -> &'static str { + match self { + PackageType::Contract => "freenet-main-contract", + PackageType::Delegate => "freenet-main-delegate", + } + } +} + impl Display for PackageType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/modules/antiflood-tokens/contracts/token-allocation-record/Cargo.toml b/modules/antiflood-tokens/contracts/token-allocation-record/Cargo.toml index bd159a3e4..8195c6cf9 100644 --- a/modules/antiflood-tokens/contracts/token-allocation-record/Cargo.toml +++ b/modules/antiflood-tokens/contracts/token-allocation-record/Cargo.toml @@ -15,3 +15,7 @@ freenet-aft-interface = { path = "../../interfaces" } [lib] crate-type = ["cdylib", "rlib"] + +[features] +default = ["freenet-main-contract"] +freenet-main-contract = [] diff --git a/modules/antiflood-tokens/delegates/token-generator/Cargo.toml b/modules/antiflood-tokens/delegates/token-generator/Cargo.toml index 6c51293fb..51e50fe2f 100644 --- a/modules/antiflood-tokens/delegates/token-generator/Cargo.toml +++ b/modules/antiflood-tokens/delegates/token-generator/Cargo.toml @@ -32,3 +32,7 @@ tracing-subscriber = { version = "0.3.16", features = ["env-filter", "fmt"] } [lib] crate-type = ["cdylib"] + +[features] +default = ["freenet-main-delegate"] +freenet-main-delegate = [] diff --git a/modules/identity-management/Cargo.toml b/modules/identity-management/Cargo.toml index 185a32542..0422370ad 100644 --- a/modules/identity-management/Cargo.toml +++ b/modules/identity-management/Cargo.toml @@ -23,3 +23,4 @@ crate-type = ["cdylib", "rlib"] [features] default = [] contract = ["freenet-stdlib/contract"] +freenet-main-delegate = [] diff --git a/modules/identity-management/src/lib.rs b/modules/identity-management/src/lib.rs index 4131e15a2..63f5d87a7 100644 --- a/modules/identity-management/src/lib.rs +++ b/modules/identity-management/src/lib.rs @@ -122,7 +122,7 @@ impl DelegateInterface for IdentityManagement { let msg = IdentityMsg::try_from(&*payload)?; let action = match msg { IdentityMsg::CreateIdentity { alias, key, extra } => { - #[cfg(all(target_family = "wasm", feature = "contract"))] + #[cfg(feature = "contract")] { freenet_stdlib::log::info(&format!( "create alias new {alias} for {}", @@ -136,7 +136,7 @@ impl DelegateInterface for IdentityManagement { serde_json::to_vec(&IdentityMsg::DeleteIdentity { alias }).unwrap() } IdentityMsg::Init => { - #[cfg(all(target_family = "wasm", feature = "contract"))] + #[cfg(feature = "contract")] { freenet_stdlib::log::info(&format!( "initialize secret {}", @@ -165,7 +165,7 @@ impl DelegateInterface for IdentityManagement { context, .. }) => { - #[cfg(all(target_family = "wasm", feature = "contract"))] + #[cfg(feature = "contract")] { freenet_stdlib::log::info(&format!( "got request for {}", diff --git a/stdlib b/stdlib index 3284f0f9d..0cd6ba8c9 160000 --- a/stdlib +++ b/stdlib @@ -1 +1 @@ -Subproject commit 3284f0f9d6fb0e14e562be2fd1f842ed46b360a7 +Subproject commit 0cd6ba8c9a51ae03d24deef8ce35a88241de57f4 diff --git a/tests/test-contract-1/Cargo.lock b/tests/test-contract-1/Cargo.lock index e61c89005..fe1ff01f1 100644 --- a/tests/test-contract-1/Cargo.lock +++ b/tests/test-contract-1/Cargo.lock @@ -241,7 +241,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "freenet-macros" -version = "0.0.4" +version = "0.0.5" dependencies = [ "proc-macro2", "quote", @@ -250,7 +250,7 @@ dependencies = [ [[package]] name = "freenet-stdlib" -version = "0.0.5" +version = "0.0.8" dependencies = [ "arrayvec", "bincode", diff --git a/tests/test-contract-1/Cargo.toml b/tests/test-contract-1/Cargo.toml index 0b893647f..7004d1e52 100644 --- a/tests/test-contract-1/Cargo.toml +++ b/tests/test-contract-1/Cargo.toml @@ -9,7 +9,9 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -freenet-stdlib = { path = "../../stdlib/rust" } +freenet-stdlib = { path = "../../stdlib/rust", features = ["contract"] } [features] +default = ["freenet-main-contract"] +freenet-main-contract = [] trace = ["freenet-stdlib/trace"] diff --git a/tests/test-contract-2/Cargo.lock b/tests/test-contract-2/Cargo.lock index c576a3e27..58690d79b 100644 --- a/tests/test-contract-2/Cargo.lock +++ b/tests/test-contract-2/Cargo.lock @@ -241,7 +241,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "freenet-macros" -version = "0.0.4" +version = "0.0.5" dependencies = [ "proc-macro2", "quote", @@ -250,7 +250,7 @@ dependencies = [ [[package]] name = "freenet-stdlib" -version = "0.0.5" +version = "0.0.8" dependencies = [ "arrayvec", "bincode", diff --git a/tests/test-contract-2/Cargo.toml b/tests/test-contract-2/Cargo.toml index a19f879b6..e451b24fd 100644 --- a/tests/test-contract-2/Cargo.toml +++ b/tests/test-contract-2/Cargo.toml @@ -12,4 +12,6 @@ crate-type = ["cdylib"] freenet-stdlib = { path = "../../stdlib/rust", features = ["contract"] } [features] +default = ["freenet-main-contract"] +freenet-main-contract = [] trace = ["freenet-stdlib/trace"] diff --git a/tests/test-delegate-1/Cargo.lock b/tests/test-delegate-1/Cargo.lock index 540307e54..e4698056d 100644 --- a/tests/test-delegate-1/Cargo.lock +++ b/tests/test-delegate-1/Cargo.lock @@ -241,7 +241,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "freenet-macros" -version = "0.0.4" +version = "0.0.5" dependencies = [ "proc-macro2", "quote", @@ -250,7 +250,7 @@ dependencies = [ [[package]] name = "freenet-stdlib" -version = "0.0.5" +version = "0.0.8" dependencies = [ "arrayvec", "bincode", diff --git a/tests/test-delegate-1/Cargo.toml b/tests/test-delegate-1/Cargo.toml index bb5146773..9e9e48a6e 100644 --- a/tests/test-delegate-1/Cargo.toml +++ b/tests/test-delegate-1/Cargo.toml @@ -11,10 +11,12 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -freenet-stdlib = { path = "../../stdlib/rust" } +freenet-stdlib = { path = "../../stdlib/rust", features = ["contract"]} serde = "1" serde_json = "1" bincode = "1" [features] +default = ["freenet-main-delegate"] +freenet-main-delegate = [] trace = ["freenet-stdlib/trace"] From 4c02d43b4cb2069fcf2efa49e7d3134e715af048 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Fri, 6 Oct 2023 04:10:46 +0000 Subject: [PATCH 27/76] fix: upgrade bootstrap from 5.3.1 to 5.3.2 Snyk has created this PR to upgrade bootstrap from 5.3.1 to 5.3.2. See this package in npm: https://www.npmjs.com/package/bootstrap See this project in Snyk: https://app.snyk.io/org/sanity/project/6842b9f8-922d-49c7-be06-12eb35a08371?utm_source=github&utm_medium=referral&page=upgrade-pr --- apps/freenet-microblogging/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/freenet-microblogging/web/package.json b/apps/freenet-microblogging/web/package.json index 4dda17a2e..66299730e 100644 --- a/apps/freenet-microblogging/web/package.json +++ b/apps/freenet-microblogging/web/package.json @@ -1,7 +1,7 @@ { "dependencies": { "@freenetorg/freenet-stdlib": "^0.0.8", - "bootstrap": "5.3.1", + "bootstrap": "5.3.2", "module-alias": "^2.2.2" }, "main": "src/index.ts", From ec08c9216c7b38812098761e79caa2164f6a4595 Mon Sep 17 00:00:00 2001 From: Nacho Duart Date: Tue, 10 Oct 2023 07:59:00 +0200 Subject: [PATCH 28/76] Update stdlib ver (#866) * Add submodules to audit action * Bump styfle/cancel-workflow-action from 0.11.0 to 0.12.0 Bumps [styfle/cancel-workflow-action](https://github.com/styfle/cancel-workflow-action) from 0.11.0 to 0.12.0. - [Release notes](https://github.com/styfle/cancel-workflow-action/releases) - [Commits](https://github.com/styfle/cancel-workflow-action/compare/0.11.0...0.12.0) --- updated-dependencies: - dependency-name: styfle/cancel-workflow-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Update stdlib commit --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From ec01e01f591433538b4c64f74cc23d4141105b6b Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Wed, 11 Oct 2023 01:52:50 -0500 Subject: [PATCH 29/76] Propagate router estimation errors (#867) * propagate estimation errors * fix build * format --- crates/core/src/router.rs | 27 +++++++++++++++++--- crates/core/src/router/isotonic_estimator.rs | 10 +++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/crates/core/src/router.rs b/crates/core/src/router.rs index 9d6ca0689..46f229aaa 100644 --- a/crates/core/src/router.rs +++ b/crates/core/src/router.rs @@ -5,7 +5,7 @@ mod util; use crate::ring::{Location, PeerKeyLocation}; use isotonic_estimator::{EstimatorType, IsotonicEstimator, IsotonicEvent}; use serde::Serialize; -use std::time::Duration; +use std::{fmt, time::Duration}; use util::{Mean, TransferSpeed}; #[derive(Debug, Clone, Serialize)] @@ -196,15 +196,24 @@ impl Router { let time_to_response_start_estimate = self .response_start_time_estimator .estimate_retrieval_time(peer, contract_location) - .unwrap(); + .map_err(|e| { + RoutingError::EstimationError(format!( + "Response Start Time Estimation failed: {}", + e + )) + })?; let failure_estimate = self .failure_estimator .estimate_retrieval_time(peer, contract_location) - .unwrap(); + .map_err(|e| { + RoutingError::EstimationError(format!("Failure Estimation failed: {}", e)) + })?; let transfer_rate_estimate = self .transfer_rate_estimator .estimate_retrieval_time(peer, contract_location) - .unwrap(); + .map_err(|e| { + RoutingError::EstimationError(format!("Transfer Rate Estimation failed: {}", e)) + })?; // This is a fairly naive approach, assuming that the cost of a failure is a multiple // of the cost of success. @@ -233,6 +242,16 @@ impl Router { #[derive(Debug)] pub(crate) enum RoutingError { InsufficientDataError, + EstimationError(String), +} + +impl fmt::Display for RoutingError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RoutingError::InsufficientDataError => write!(f, "Insufficient data provided"), + RoutingError::EstimationError(err_msg) => write!(f, "Estimation error: {}", err_msg), + } + } } #[derive(Debug, Clone, Copy, Serialize)] diff --git a/crates/core/src/router/isotonic_estimator.rs b/crates/core/src/router/isotonic_estimator.rs index 300854e11..e3670607e 100644 --- a/crates/core/src/router/isotonic_estimator.rs +++ b/crates/core/src/router/isotonic_estimator.rs @@ -1,7 +1,7 @@ use crate::ring::{Distance, Location, PeerKeyLocation}; use pav_regression::pav::{IsotonicRegression, Point}; use serde::Serialize; -use std::collections::HashMap; +use std::{collections::HashMap, fmt}; const MIN_POINTS_FOR_REGRESSION: usize = 5; @@ -160,6 +160,14 @@ pub(crate) enum EstimationError { InsufficientData, // Error indicating that there is not enough data for estimation } +impl fmt::Display for EstimationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + EstimationError::InsufficientData => write!(f, "Insufficient data for estimation"), + } + } +} + /// A routing event is a single request to a peer for a contract, and some value indicating /// the result of the request, such as the time it took to retrieve the contract. #[derive(Debug, Clone)] From c6200c576c9f9e1b1f6d034d47111d9b90e8aeea Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Wed, 11 Oct 2023 09:24:33 -0500 Subject: [PATCH 30/76] top: clean up simulator --- crates/core/src/topology/simulation/mod.rs | 203 +++++++++++++++- .../core/src/topology/simulation/network.rs | 223 ------------------ .../core/src/topology/simulation/simulator.rs | 1 - 3 files changed, 198 insertions(+), 229 deletions(-) delete mode 100644 crates/core/src/topology/simulation/network.rs delete mode 100644 crates/core/src/topology/simulation/simulator.rs diff --git a/crates/core/src/topology/simulation/mod.rs b/crates/core/src/topology/simulation/mod.rs index 7a0a6f090..484c3fb24 100644 --- a/crates/core/src/topology/simulation/mod.rs +++ b/crates/core/src/topology/simulation/mod.rs @@ -1,6 +1,199 @@ -use crate::network_sim::Location; -use std::collections::{HashMap, HashSet, LinkedList}; -use tracing::{debug, info, instrument, warn}; +mod event_stat_tracker; -mod network; -mod simulator; +use crate::ring::Distance; +use super::*; +use event_stat_tracker::EventStatTracker; + + +#[derive(Debug)] +struct SimulatedNetwork { + current_time: u64, + nodes: Vec, + connections: HashMap>, + requests: HashMap)>, // origin node -> destination node -> requests +} + +impl SimulatedNetwork { + pub(crate) fn tick(&mut self) { + self.current_time += 1; + } + + pub(crate) fn new() -> Self { + Self { + current_time: 0, + nodes: Vec::new(), + connections: HashMap::new(), + requests: HashMap::new(), + } + } + + fn add_node(&mut self) -> NodeRef { + let index = self.nodes.len(); + let node = SimulatedNode { + location: Location::random(), + index, + }; + debug!("Adding node {:?}", node); + self.nodes.push(node); + NodeRef { index } + } + + fn connect(&mut self, a: NodeRef, b: NodeRef) { + // throw an error if a == b + assert!(a != b, "Cannot connect a node to itself"); + + info!("Connecting {:?} and {:?}", a, b); + self.connections.entry(a).or_default().insert(b); + self.connections.entry(b).or_default().insert(a); + } + + fn disconnect(&mut self, a: NodeRef, b: NodeRef) { + info!("Disconnecting {:?} and {:?}", a, b); + self.connections.entry(a).or_default().remove(&b); + self.connections.entry(b).or_default().remove(&a); + } + + fn route(&self, source: &NodeRef, destination: Location) -> Result, RouteError> { + info!("Routing from {:?} to {:?}", source, destination); + let mut current = *source; // Dereference to copy + let mut visited = Vec::new(); + let mut recent_nodes = Vec::new(); // To track the sequence of last N nodes + + loop { + debug!("Current node: {:?}", current); + + // Check if we've reached the destination + if self.nodes[current.index].location == destination { + info!("Reached destination"); + return Ok(visited); + } + + // Get current node's distance to the destination + let current_distance = self.nodes[current.index] + .location + .distance(destination) + .as_f64(); + debug!("Current distance to destination: {}", current_distance); + + // Find the closest connected node to the destination that hasn't been visited + let closest_connections = match self.connections.get(¤t) { + Some(connections) => connections, + None => { + // Handle the None case here. For example, you might want to return agit n Err value or break the loop. + return Err(RouteError::NoRoute); + } + }; + let (closest, closest_distance) = closest_connections + .iter() + .map(|&neighbor| { + let distance = self.nodes[neighbor.index] + .location + .distance(destination) + .as_f64(); + (neighbor, distance) + }) + .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)) + .unwrap_or((current, current_distance)); + + debug!( + "Closest node: {:?}, distance: {}", + closest, closest_distance + ); + + // Check for a loop by looking at the sequence of last N nodes + recent_nodes.push(closest); + if recent_nodes.len() > 10 { + // Keep only the last 10 visited nodes in the list + recent_nodes.remove(0); + } + if recent_nodes.len() > 2 && recent_nodes.first() == recent_nodes.last() { + warn!("Loop detected"); + return Err(RouteError::Loop); + } + + // Update visited nodes and current node + visited.push(closest); + current = closest; + } + } + + fn record_request(&mut self, a: &NodeRef, b: &NodeRef) { + let (count, dest_map) = self + .requests + .entry(*a) + .or_insert((EventStatTracker::new(), HashMap::new())); + count.add_event(self.current_time); + dest_map + .entry(*b) + .or_insert(EventStatTracker::new()) + .add_event(self.current_time); + } + + fn reset_recorded_requests(&mut self) { + self.requests.clear(); + } + + fn get_join_peers( + &self, + source: &NodeRef, + target: Location, + tolerance: Distance, + ) -> Option> { + info!("Joining via {:?} with target {:?}", source, target); + let join_route = self.route(source, target); + let join_route: Vec = match join_route { + Ok(route) => route, + Err(e) => { + warn!("Error joining network: {:?}", e); + return Option::None; + } + }; + + let mut joiners = Vec::new(); + + for node in join_route.iter() { + let node_location = self.nodes[node.index].location; + let distance = node_location.distance(target); + if distance < tolerance { + info!("Found node {:?} within tolerance", node); + joiners.push(*node); + } + } + + Option::Some(joiners) + } + + fn join(&mut self, node: NodeRef, target: Location, tolerance: Distance) { + info!("Joining {:?} with target {:?}", node, target); + let joiners = self.get_join_peers(&node, target, tolerance); + let joiners: Vec = match joiners { + Some(joiners) => joiners, + None => { + warn!("Error joining network"); + return; + } + }; + + for joiner in joiners.iter() { + self.connect(node, *joiner); + } + } +} + +#[derive(Debug)] +enum RouteError { + NoRoute, + Loop, + DeadEnd, +} + +#[derive(Debug)] +struct SimulatedNode { + index: usize, + location: Location, +} + +#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] +struct NodeRef { + index: usize, +} diff --git a/crates/core/src/topology/simulation/network.rs b/crates/core/src/topology/simulation/network.rs deleted file mode 100644 index f4cbd20aa..000000000 --- a/crates/core/src/topology/simulation/network.rs +++ /dev/null @@ -1,223 +0,0 @@ -use crate::ring::Distance; - -use super::*; - -#[derive(Debug)] -struct Network { - current_time: u64, - nodes: Vec, - connections: HashMap>, - requests: HashMap)>, // origin node -> destination node -> requests -} - -const MAX_EVENTS_TO_TRACK: usize = 100; - -#[derive(Debug)] -struct EventStatTracker { - recent_event_times: LinkedList, -} - -impl EventStatTracker { - fn new() -> Self { - Self { - recent_event_times: LinkedList::new(), - } - } - - fn get_event_times(&self) -> &LinkedList { - &self.recent_event_times - } - - fn add_event(&mut self, time: u64) { - self.recent_event_times.push_back(time); - // if there are recent_event_times remove the oldest event times if the exceed MAX_EVENTS_TO_TRACK - while self.recent_event_times.len() > MAX_EVENTS_TO_TRACK { - self.recent_event_times.pop_front(); - } - } -} - -impl Network { - fn tick(&mut self) { - self.current_time += 1; - } - - fn new() -> Self { - Self { - current_time: 0, - nodes: Vec::new(), - connections: HashMap::new(), - requests: HashMap::new(), - } - } - - fn add_node(&mut self) -> NodeRef { - let index = self.nodes.len(); - let node = SimulatedNode { - location: Location::random(), - index, - }; - debug!("Adding node {:?}", node); - self.nodes.push(node); - NodeRef { index } - } - - fn connect(&mut self, a: NodeRef, b: NodeRef) { - // throw an error if a == b - assert!(a != b, "Cannot connect a node to itself"); - - info!("Connecting {:?} and {:?}", a, b); - self.connections.entry(a).or_default().insert(b); - self.connections.entry(b).or_default().insert(a); - } - - fn disconnect(&mut self, a: NodeRef, b: NodeRef) { - info!("Disconnecting {:?} and {:?}", a, b); - self.connections.entry(a).or_default().remove(&b); - self.connections.entry(b).or_default().remove(&a); - } - - fn route(&self, source: &NodeRef, destination: Location) -> Result, RouteError> { - info!("Routing from {:?} to {:?}", source, destination); - let mut current = *source; // Dereference to copy - let mut visited = Vec::new(); - let mut recent_nodes = Vec::new(); // To track the sequence of last N nodes - - loop { - debug!("Current node: {:?}", current); - - // Check if we've reached the destination - if self.nodes[current.index].location == destination { - info!("Reached destination"); - return Ok(visited); - } - - // Get current node's distance to the destination - let current_distance = self.nodes[current.index] - .location - .distance(destination) - .as_f64(); - debug!("Current distance to destination: {}", current_distance); - - // Find the closest connected node to the destination that hasn't been visited - let closest_connections = match self.connections.get(¤t) { - Some(connections) => connections, - None => { - // Handle the None case here. For example, you might want to return agit n Err value or break the loop. - return Err(RouteError::NoRoute); - } - }; - let (closest, closest_distance) = closest_connections - .iter() - .map(|&neighbor| { - let distance = self.nodes[neighbor.index] - .location - .distance(destination) - .as_f64(); - (neighbor, distance) - }) - .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)) - .unwrap_or((current, current_distance)); - - debug!( - "Closest node: {:?}, distance: {}", - closest, closest_distance - ); - - // Check for a loop by looking at the sequence of last N nodes - recent_nodes.push(closest); - if recent_nodes.len() > 10 { - // Keep only the last 10 visited nodes in the list - recent_nodes.remove(0); - } - if recent_nodes.len() > 2 && recent_nodes.first() == recent_nodes.last() { - warn!("Loop detected"); - return Err(RouteError::Loop); - } - - // Update visited nodes and current node - visited.push(closest); - current = closest; - } - } - - fn record_request(&mut self, a: &NodeRef, b: &NodeRef) { - let (count, dest_map) = self - .requests - .entry(*a) - .or_insert((EventStatTracker::new(), HashMap::new())); - count.add_event(self.current_time); - dest_map - .entry(*b) - .or_insert(EventStatTracker::new()) - .add_event(self.current_time); - } - - fn reset_recorded_requests(&mut self) { - self.requests.clear(); - } - - fn get_join_peers( - &self, - source: &NodeRef, - target: Location, - tolerance: Distance, - ) -> Option> { - info!("Joining via {:?} with target {:?}", source, target); - let join_route = self.route(source, target); - let join_route: Vec = match join_route { - Ok(route) => route, - Err(e) => { - warn!("Error joining network: {:?}", e); - return Option::None; - } - }; - - let mut joiners = Vec::new(); - - for node in join_route.iter() { - let node_location = self.nodes[node.index].location; - let distance = node_location.distance(target); - if distance < tolerance { - info!("Found node {:?} within tolerance", node); - joiners.push(*node); - } - } - - Option::Some(joiners) - } - - fn join(&mut self, node: NodeRef, target: Location, tolerance: Distance) { - info!("Joining {:?} with target {:?}", node, target); - let joiners = self.get_join_peers(&node, target, tolerance); - let joiners: Vec = match joiners { - Some(joiners) => joiners, - None => { - warn!("Error joining network"); - return; - } - }; - - for joiner in joiners.iter() { - self.connect(node, *joiner); - } - } -} - -#[derive(Debug)] -enum RouteError { - NoRoute, - Loop, - DeadEnd, -} - -#[derive(Debug)] -struct SimulatedNode { - index: usize, - location: Location, -} - -#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] -struct NodeRef { - index: usize, -} diff --git a/crates/core/src/topology/simulation/simulator.rs b/crates/core/src/topology/simulation/simulator.rs deleted file mode 100644 index 8b1378917..000000000 --- a/crates/core/src/topology/simulation/simulator.rs +++ /dev/null @@ -1 +0,0 @@ - From c94e203edbafaf784ffcfee1e2bf629b61f57d47 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Wed, 11 Oct 2023 12:50:30 +0200 Subject: [PATCH 31/76] Connect router and feed live events --- Cargo.lock | 12 ++ crates/core/Cargo.toml | 1 + crates/core/src/contract/executor.rs | 10 +- crates/core/src/message.rs | 22 +-- crates/core/src/node.rs | 57 ++++-- .../core/src/node/conn_manager/p2p_protoc.rs | 71 ++++++-- crates/core/src/node/op_state.rs | 5 + crates/core/src/node/p2p_impl.rs | 18 +- crates/core/src/operations.rs | 63 ++++--- crates/core/src/operations/get.rs | 169 ++++++++++++++---- crates/core/src/operations/join_ring.rs | 60 +++++-- crates/core/src/operations/op_trait.rs | 8 +- crates/core/src/operations/put.rs | 141 ++++++++++++--- crates/core/src/operations/subscribe.rs | 64 +++---- crates/core/src/operations/update.rs | 4 +- crates/core/src/ring.rs | 17 +- crates/core/src/router.rs | 33 ++-- crates/core/src/router/isotonic_estimator.rs | 14 +- crates/core/src/router/util.rs | 6 +- 19 files changed, 546 insertions(+), 229 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8891bc286..1c7f4501f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1195,6 +1195,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "delegate" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee5df75c70b95bd3aacc8e2fd098797692fb1d54121019c4de481e42f04c8a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "der" version = "0.7.8" @@ -1582,6 +1593,7 @@ dependencies = [ "crossbeam", "ctrlc", "dashmap", + "delegate", "directories", "either", "freenet-stdlib", diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index a93e84875..2e05202c5 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -31,6 +31,7 @@ config = { version = "0.13.0", features = [ "toml" ] } crossbeam = "0.8.2" ctrlc = { version = "3.4", features = ["termination"] } dashmap = "^5.5" +delegate = "0.10" directories = "5" either = { workspace = true , features = ["serde"] } futures = "0.3.21" diff --git a/crates/core/src/contract/executor.rs b/crates/core/src/contract/executor.rs index 01236c011..b6a86baf2 100644 --- a/crates/core/src/contract/executor.rs +++ b/crates/core/src/contract/executor.rs @@ -31,7 +31,7 @@ use crate::runtime::{ }; use crate::{ client_events::{ClientId, HostResult}, - node::{NodeConfig, P2pBridge}, + node::NodeConfig, operations::{self, op_trait::Operation}, DynError, }; @@ -136,7 +136,7 @@ impl ExecutorToEventLoopChannel { async fn send_to_event_loop(&mut self, message: T) -> Result<(), DynError> where T: ComposeNetworkMessage, - Op: Operation + Send + 'static, + Op: Operation + Send + 'static, { let op = message.initiate_op(&self.op_manager); self.end.sender.send(*op.id()).await?; @@ -178,7 +178,7 @@ mod sealed { trait ComposeNetworkMessage where Self: Sized, - Op: Operation + Send + 'static, + Op: Operation + Send + 'static, { fn initiate_op(self, op_manager: &OpManager) -> Op { todo!() @@ -205,7 +205,7 @@ impl ComposeNetworkMessage for GetContract { op: operations::get::GetOp, op_manager: &OpManager, ) -> Result { - let id = *>::id(&op); + let id = *op.id(); operations::get::request_get(op_manager, op, None).await?; Ok(id) } @@ -321,7 +321,7 @@ impl Executor { // dependencies to be resolved async fn op_request(&mut self, request: M) -> Result where - Op: Operation + Send + 'static, + Op: Operation + Send + 'static, M: ComposeNetworkMessage, { debug_assert!(self.event_loop_channel.is_some()); diff --git a/crates/core/src/message.rs b/crates/core/src/message.rs index 35d0acd6a..4f4bd3b7a 100644 --- a/crates/core/src/message.rs +++ b/crates/core/src/message.rs @@ -1,9 +1,6 @@ //! Main message type which encapsulated all the messaging between nodes. -use std::{ - fmt::Display, - time::{Duration, SystemTime}, -}; +use std::{fmt::Display, time::Duration}; use serde::{Deserialize, Serialize}; use uuid::{ @@ -45,13 +42,7 @@ static UUID_CONTEXT: Context = Context::new(14); impl Transaction { pub fn new(ty: TransactionTypeId, initial_peer: &PeerKey) -> Transaction { // using v1 UUID to keep to keep track of the creation ts - let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("infallible"); - let now_secs = now.as_secs(); - let now_nanos = now.as_nanos(); - let now_nanos = now_nanos - (now_secs as u128 * 1_000_000_000); - let ts = Timestamp::from_unix(&UUID_CONTEXT, now_secs, now_nanos as u32); + let ts: Timestamp = uuid::timestamp::Timestamp::now(&UUID_CONTEXT); // event in the net this UUID should be unique since peer keys are unique // however some id collision may be theoretically possible if two transactions @@ -61,8 +52,8 @@ impl Transaction { let b = &mut [0; 6]; b.copy_from_slice(&initial_peer.to_bytes()[0..6]); let id = Uuid::new_v1(ts, b); - // 2 word size for 64-bits platforms most likely since msg type - // probably will be aligned to 64 bytes + + // 3 word size for 64-bits platforms Self { id, ty } } @@ -231,6 +222,11 @@ impl Message { Canceled(_) => true, } } + + pub fn track_stats(&self) -> bool { + use Message::*; + !matches!(self, JoinRing(_) | Subscribe(_) | Canceled(_)) + } } impl Display for Message { diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index 489f5db70..fc38d6dcf 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -36,14 +36,15 @@ use crate::{ operations::{ get, join_ring::{self, JoinRingMsg, JoinRingOp}, - put, subscribe, OpEnum, OpError, + put, subscribe, OpEnum, OpError, OpOutcome, }, ring::{Location, PeerKeyLocation}, + router::{RouteEvent, RouteOutcome}, util::{ExponentialBackoff, IterExt}, }; use crate::operations::handle_op_request; -pub(crate) use conn_manager::{p2p_protoc::P2pBridge, ConnectionBridge, ConnectionError}; +pub(crate) use conn_manager::{ConnectionBridge, ConnectionError}; pub(crate) use op_state::OpManager; mod conn_manager; @@ -440,19 +441,53 @@ macro_rules! log_handling_msg { }; } -#[inline(always)] async fn report_result( op_result: Result, OpError>, + op_storage: &OpManager, executor_callback: Option>, client_req_handler_callback: Option<(ClientId, ClientResponsesSender)>, ) { match op_result { - Ok(Some(res)) => { + Ok(Some(op_res)) => { if let Some((client_id, cb)) = client_req_handler_callback { - let _ = cb.send((client_id, res.to_host_result(client_id))); + let _ = cb.send((client_id, op_res.to_host_result(client_id))); + } + // check operations.rs:handle_op_result to see what's the meaning of each state + // in case more cases want to be handled when feeding information to the OpManager + + match op_res.outcome() { + OpOutcome::ContractOpSuccess { + target_peer, + contract_location, + first_response_time, + payload_size, + payload_transfer_time, + } => { + op_storage.ring.routing_finished(RouteEvent { + peer: *target_peer, + contract_location, + outcome: RouteOutcome::Success { + time_to_response_start: first_response_time, + payload_size, + payload_transfer_time, + }, + }); + } + // todo: handle failures, need to track timeouts and other potential failures + // OpOutcome::ContractOpFailure { + // target_peer: Some(target_peer), + // contract_location, + // } => { + // op_storage.ring.routing_finished(RouteEvent { + // peer: *target_peer, + // contract_location, + // outcome: RouteOutcome::Failure, + // }); + // } + OpOutcome::Incomplete | OpOutcome::Irrelevant => {} } if let Some(mut cb) = executor_callback { - cb.response(res).await; + cb.response(op_res).await; } } Ok(None) => {} @@ -489,7 +524,7 @@ async fn process_message( client_id, ) .await; - report_result(op_result, executor_callback, cli_req).await; + report_result(op_result, &op_storage, executor_callback, cli_req).await; } Message::Put(op) => { log_handling_msg!("put", *op.id(), op_storage); @@ -500,7 +535,7 @@ async fn process_message( client_id, ) .await; - report_result(op_result, executor_callback, cli_req).await; + report_result(op_result, &op_storage, executor_callback, cli_req).await; } Message::Get(op) => { log_handling_msg!("get", op.id(), op_storage); @@ -511,7 +546,7 @@ async fn process_message( client_id, ) .await; - report_result(op_result, executor_callback, cli_req).await; + report_result(op_result, &op_storage, executor_callback, cli_req).await; } Message::Subscribe(op) => { log_handling_msg!("subscribe", op.id(), op_storage); @@ -522,13 +557,13 @@ async fn process_message( client_id, ) .await; - report_result(op_result, executor_callback, cli_req).await; + report_result(op_result, &op_storage, executor_callback, cli_req).await; } _ => {} } } Err(err) => { - report_result(Err(err.into()), executor_callback, cli_req).await; + report_result(Err(err.into()), &op_storage, executor_callback, cli_req).await; } } } diff --git a/crates/core/src/node/conn_manager/p2p_protoc.rs b/crates/core/src/node/conn_manager/p2p_protoc.rs index 1f2573591..4eee493ad 100644 --- a/crates/core/src/node/conn_manager/p2p_protoc.rs +++ b/crates/core/src/node/conn_manager/p2p_protoc.rs @@ -62,6 +62,7 @@ fn config_behaviour( local_key: &Keypair, gateways: &[InitPeerNode], _public_addr: &Option, + op_manager: Arc, ) -> NetBehaviour { let routing_table: HashMap<_, _> = gateways .iter() @@ -101,6 +102,7 @@ fn config_behaviour( connected: HashMap::new(), openning_connection: HashSet::new(), inbound: VecDeque::new(), + op_manager, }, } } @@ -177,6 +179,7 @@ impl P2pConnManager { pub fn build( transport: transport::Boxed<(PeerId, muxing::StreamMuxerBox)>, config: &NodeBuilder, + op_manager: Arc, ) -> Result { // We set a global executor which is virtually the Tokio multi-threaded executor // to reuse it's thread pool and scheduler in order to drive futures. @@ -191,7 +194,12 @@ impl P2pConnManager { let builder = SwarmBuilder::with_executor( transport, - config_behaviour(&config.local_key, &config.remote_nodes, &public_addr), + config_behaviour( + &config.local_key, + &config.remote_nodes, + &public_addr, + op_manager, + ), PeerId::from(config.local_key.public()), global_executor, ); @@ -505,6 +513,7 @@ pub(in crate::node) struct FreenetBehaviour { routing_table: HashMap>, connected: HashMap, openning_connection: HashSet, + op_manager: Arc, } impl NetworkBehaviour for FreenetBehaviour { @@ -526,7 +535,7 @@ impl NetworkBehaviour for FreenetBehaviour { .entry(peer_id) .or_default() .insert(remote_addr.clone()); - Ok(Handler::new()) + Ok(Handler::new(self.op_manager.clone())) } fn handle_established_outbound_connection( @@ -543,7 +552,7 @@ impl NetworkBehaviour for FreenetBehaviour { .entry(peer_id) .or_default() .insert(addr.clone()); - Ok(Handler::new()) + Ok(Handler::new(self.op_manager.clone())) } fn on_connection_handler_event( @@ -634,6 +643,7 @@ pub(in crate::node) struct Handler { uniq_conn_id: UniqConnId, protocol_status: ProtocolStatus, pending: Vec, + op_manager: Arc, } #[allow(dead_code)] @@ -668,6 +678,7 @@ enum SubstreamState { PendingFlush { conn_id: UniqConnId, substream: FreenetStream, + op_id: Option, }, /// Waiting for an answer back from the remote. WaitingMsg { @@ -685,13 +696,14 @@ impl SubstreamState { } impl Handler { - fn new() -> Self { + fn new(op_manager: Arc) -> Self { Self { substreams: vec![], keep_alive: KeepAlive::Until(Instant::now() + config::PEER_TIMEOUT), uniq_conn_id: 0, protocol_status: ProtocolStatus::Unconfirmed, pending: Vec::new(), + op_manager, } } @@ -831,17 +843,31 @@ impl ConnectionHandler for Handler { } _ => break, }, - Left(msg) => match Sink::start_send(Pin::new(&mut substream), msg) { - Ok(()) => { - stream = SubstreamState::PendingFlush { substream, conn_id }; + Left(msg) => { + let op_id = msg.id(); + if msg.track_stats() { + if let Some(mut op) = self.op_manager.pop(op_id) { + op.record_transfer(); + let _ = self.op_manager.push(*op_id, op); + } } - Err(err) => { - let event = ConnectionHandlerEvent::NotifyBehaviour( - HandlerEvent::Inbound(Right(NodeEvent::Error(err))), - ); - return Poll::Ready(event); + let op_id = *op_id; + match Sink::start_send(Pin::new(&mut substream), msg) { + Ok(()) => { + stream = SubstreamState::PendingFlush { + substream, + conn_id, + op_id: Some(op_id), + }; + } + Err(err) => { + let event = ConnectionHandlerEvent::NotifyBehaviour( + HandlerEvent::Inbound(Right(NodeEvent::Error(err))), + ); + return Poll::Ready(event); + } } - }, + } }, Poll::Pending => { stream = SubstreamState::PendingSend { @@ -861,14 +887,24 @@ impl ConnectionHandler for Handler { SubstreamState::PendingFlush { mut substream, conn_id, + op_id, } => match Sink::poll_flush(Pin::new(&mut substream), cx) { Poll::Ready(Ok(())) => { + if let Some(op_id) = op_id { + if let Some(mut op) = self.op_manager.pop(&op_id) { + op.record_transfer(); + let _ = self.op_manager.push(op_id, op); + } + } stream = SubstreamState::WaitingMsg { substream, conn_id }; continue; } Poll::Pending => { - self.substreams - .push(SubstreamState::PendingFlush { substream, conn_id }); + self.substreams.push(SubstreamState::PendingFlush { + substream, + conn_id, + op_id, + }); break; } Poll::Ready(Err(err)) => { @@ -883,6 +919,11 @@ impl ConnectionHandler for Handler { conn_id, } => match Stream::poll_next(Pin::new(&mut substream), cx) { Poll::Ready(Some(Ok(msg))) => { + let op_id = msg.id(); + if let Some(mut op) = self.op_manager.pop(op_id) { + op.record_transfer(); + let _ = self.op_manager.push(*op_id, op); + } if !msg.terminal() { // received a message, the other peer is waiting for an answer self.substreams diff --git a/crates/core/src/node/op_state.rs b/crates/core/src/node/op_state.rs index c3a5658f1..047bcc1cc 100644 --- a/crates/core/src/node/op_state.rs +++ b/crates/core/src/node/op_state.rs @@ -34,6 +34,7 @@ pub(crate) struct OpManager { pub ring: Ring, } +#[cfg(debug_assertions)] macro_rules! check_id_op { ($get_ty:expr, $var:path) => { if !matches!($get_ty, $var) { @@ -108,18 +109,22 @@ impl OpManager { pub fn push(&self, id: Transaction, op: OpEnum) -> Result<(), OpError> { match op { OpEnum::JoinRing(tx) => { + #[cfg(debug_assertions)] check_id_op!(id.tx_type(), TransactionType::JoinRing); self.join_ring.insert(id, *tx); } OpEnum::Put(tx) => { + #[cfg(debug_assertions)] check_id_op!(id.tx_type(), TransactionType::Put); self.put.insert(id, tx); } OpEnum::Get(tx) => { + #[cfg(debug_assertions)] check_id_op!(id.tx_type(), TransactionType::Get); self.get.insert(id, tx); } OpEnum::Subscribe(tx) => { + #[cfg(debug_assertions)] check_id_op!(id.tx_type(), TransactionType::Subscribe); self.subscribe.insert(id, tx); } diff --git a/crates/core/src/node/p2p_impl.rs b/crates/core/src/node/p2p_impl.rs index 9c2b93a35..4e44ae477 100644 --- a/crates/core/src/node/p2p_impl.rs +++ b/crates/core/src/node/p2p_impl.rs @@ -32,7 +32,7 @@ use super::OpManager; pub(super) struct NodeP2P { pub(crate) peer_key: PeerKey, - pub(crate) op_storage: Arc, + pub(crate) op_manager: Arc, notification_channel: EventLoopNotifications, pub(super) conn_manager: P2pConnManager, // event_listener: Option>, @@ -54,7 +54,7 @@ impl NodeP2P { None, self.peer_key, gateway, - &self.op_storage, + &self.op_manager, &mut self.conn_manager.bridge, ) .await?; @@ -67,7 +67,7 @@ impl NodeP2P { // todo: pass `cli_response_sender` self.conn_manager .run_event_listener( - self.op_storage.clone(), + self.op_manager.clone(), self.notification_channel, self.executor_listener, self.cli_response_sender, @@ -85,11 +85,6 @@ impl NodeP2P { let peer_key = PeerKey::from(builder.local_key.public()); let gateways = builder.get_gateways()?; - let conn_manager = { - let transport = Self::config_transport(&builder.local_key)?; - P2pConnManager::build(transport, &builder)? - }; - let ring = Ring::new(&builder, &gateways)?; let (notification_channel, notification_tx) = EventLoopNotifications::channel(); let (ch_outbound, ch_inbound) = contract::contract_handler_channel(); @@ -100,6 +95,11 @@ impl NodeP2P { .await .map_err(|e| anyhow::anyhow!(e))?; + let conn_manager = { + let transport = Self::config_transport(&builder.local_key)?; + P2pConnManager::build(transport, &builder, op_storage.clone())? + }; + GlobalExecutor::spawn(contract::contract_handling(contract_handler)); let clients = ClientEventsCombinator::new(builder.clients); GlobalExecutor::spawn(client_event_handling( @@ -112,7 +112,7 @@ impl NodeP2P { peer_key, conn_manager, notification_channel, - op_storage, + op_manager: op_storage, is_gateway: builder.location.is_some(), executor_listener, cli_response_sender, diff --git a/crates/core/src/operations.rs b/crates/core/src/operations.rs index 9c9576331..53e953442 100644 --- a/crates/core/src/operations.rs +++ b/crates/core/src/operations.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use tokio::sync::mpsc::error::SendError; use self::op_trait::Operation; @@ -6,8 +8,7 @@ use crate::{ contract::ContractError, message::{InnerMessage, Message, Transaction, TransactionType}, node::{ConnectionBridge, ConnectionError, OpManager, PeerKey}, - operations::{get::GetOp, join_ring::JoinRingOp, put::PutOp, subscribe::SubscribeOp}, - ring::RingError, + ring::{Location, PeerKeyLocation, RingError}, }; pub(crate) mod get; @@ -36,7 +37,7 @@ pub(crate) async fn handle_op_request( client_id: Option, ) -> Result, OpError> where - Op: Operation, + Op: Operation, CB: ConnectionBridge, { let sender; @@ -92,7 +93,7 @@ where Ok(OperationResult { return_msg: None, state: Some(final_state), - }) if final_state.is_final() => { + }) if final_state.finalized() => { // operation finished_completely with result return Ok(Some(final_state)); } @@ -101,7 +102,7 @@ where state: Some(updated_state), }) => { // interim state - let id = OpEnum::id::(&updated_state); + let id = *updated_state.id(); op_storage.push(id, updated_state)?; } Ok(OperationResult { @@ -131,23 +132,17 @@ pub(crate) enum OpEnum { } impl OpEnum { - fn id(&self) -> Transaction { - use OpEnum::*; - match self { - JoinRing(op) => *>::id(op), - Put(op) => *>::id(op), - Get(op) => *>::id(op), - Subscribe(op) => *>::id(op), - } - } - - fn is_final(&self) -> bool { - match self { - OpEnum::JoinRing(op) if op.finished() => true, - OpEnum::Put(op) if op.finished() => true, - OpEnum::Get(op) if op.finished() => true, - OpEnum::Subscribe(op) if op.finished() => true, - _ => false, + delegate::delegate! { + to match self { + OpEnum::JoinRing(op) => op, + OpEnum::Put(op) => op, + OpEnum::Get(op) => op, + OpEnum::Subscribe(op) => op, + } { + pub fn id(&self) -> &Transaction; + pub fn outcome(&self) -> OpOutcome; + pub fn finalized(&self) -> bool; + pub fn record_transfer(&mut self); } } @@ -156,6 +151,30 @@ impl OpEnum { } } +pub(crate) enum OpOutcome<'a> { + /// An op which involves a contract completed successfully. + ContractOpSuccess { + target_peer: &'a PeerKeyLocation, + contract_location: Location, + /// Time the operation took to initiate. + first_response_time: Duration, + /// Size of the payload (contract, state, etc.) in bytes. + payload_size: usize, + /// Transfer time of the payload. + payload_transfer_time: Duration, + }, + // todo: handle failures stats when it does not complete successfully + // /// An op which involves a contract completed unsuccessfully. + // ContractOpFailure { + // target_peer: Option<&'a PeerKeyLocation>, + // contract_location: Location, + // }, + /// In transit contract operation. + Incomplete, + /// This operation stats are not relevant for this peer. + Irrelevant, +} + #[derive(Debug, thiserror::Error)] pub(crate) enum OpError { #[error(transparent)] diff --git a/crates/core/src/operations/get.rs b/crates/core/src/operations/get.rs index 8f5c3fc0c..e8227e5d0 100644 --- a/crates/core/src/operations/get.rs +++ b/crates/core/src/operations/get.rs @@ -1,6 +1,6 @@ -use std::future::Future; use std::pin::Pin; use std::time::Duration; +use std::{future::Future, time::Instant}; use freenet_stdlib::prelude::*; @@ -15,7 +15,7 @@ use crate::{ DynError, }; -use super::{OpEnum, OpError, OperationResult}; +use super::{OpEnum, OpError, OpOutcome, OperationResult}; pub(crate) use self::messages::GetMsg; @@ -30,15 +30,97 @@ pub(crate) struct GetOp { id: Transaction, state: Option, result: Option, + stats: Option, _ttl: Duration, } + +struct GetStats { + caching_peer: Option, + contract_location: Location, + /// (start, end) + first_response_time: Option<(Instant, Option)>, + /// (start, end) + transfer_time: Option<(Instant, Option)>, + step: RecordingStats, +} + +/// While timing, at what particular step we are now. +#[derive(Clone, Copy, Default)] +enum RecordingStats { + #[default] + Uninitialized, + InitGet, + TransferNotStarted, + TransferStarted, + Completed, +} + impl GetOp { - pub(super) fn finished(&self) -> bool { - self.result.is_some() + pub(super) fn outcome(&self) -> OpOutcome { + if let Some(( + GetResult { state, contract }, + GetStats { + caching_peer: Some(target_peer), + contract_location, + first_response_time: Some((response_start, Some(response_end))), + transfer_time: Some((transfer_start, Some(transfer_end))), + .. + }, + )) = self.result.as_ref().zip(self.stats.as_ref()) + { + let payload_size = state.size() + + contract + .as_ref() + .map(|c| c.data().len()) + .unwrap_or_default(); + OpOutcome::ContractOpSuccess { + target_peer, + contract_location: *contract_location, + payload_size, + first_response_time: *response_end - *response_start, + payload_transfer_time: *transfer_end - *transfer_start, + } + } else { + OpOutcome::Incomplete + } + } + + pub(super) fn finalized(&self) -> bool { + self.stats + .as_ref() + .map(|s| s.transfer_time.is_some()) + .unwrap_or(false) + } + + pub(super) fn record_transfer(&mut self) { + if let Some(stats) = self.stats.as_mut() { + match stats.step { + RecordingStats::Uninitialized => { + stats.first_response_time = Some((Instant::now(), None)); + stats.step = RecordingStats::InitGet; + } + RecordingStats::InitGet => { + if let Some((_, e)) = stats.first_response_time.as_mut() { + *e = Some(Instant::now()); + } + stats.step = RecordingStats::TransferNotStarted; + } + RecordingStats::TransferNotStarted => { + stats.transfer_time = Some((Instant::now(), None)); + stats.step = RecordingStats::TransferStarted; + } + RecordingStats::TransferStarted => { + if let Some((_, e)) = stats.transfer_time.as_mut() { + *e = Some(Instant::now()); + } + stats.step = RecordingStats::Completed; + } + RecordingStats::Completed => {} + } + } } } -#[allow(dead_code)] pub(crate) struct GetResult { pub state: WrappedState, pub contract: Option, @@ -55,10 +137,7 @@ impl TryFrom for GetResult { } } -impl Operation for GetOp -where - CB: std::marker::Send, -{ +impl Operation for GetOp { type Message = GetMsg; type Result = GetResult; @@ -74,7 +153,7 @@ where let result = match op_storage.pop(msg.id()) { Some(OpEnum::Get(get_op)) => { Ok(OpInitialization { op: get_op, sender }) - // was an existing operation, the other peer messaged back + // was an existing operation, other peer messaged back } Some(_) => return Err(OpError::OpNotPresent(tx)), None => { @@ -84,6 +163,7 @@ where state: Some(GetState::ReceivedRequest), id: tx, result: None, + stats: None, // don't care about stats in target peers _ttl: PEER_TIMEOUT, }, sender, @@ -98,7 +178,7 @@ where &self.id } - fn process_message<'a>( + fn process_message<'a, CB: ConnectionBridge>( self, conn_manager: &'a mut CB, op_storage: &'a OpManager, @@ -109,6 +189,7 @@ where let return_msg; let new_state; let mut result = None; + let mut stats = self.stats; match input { GetMsg::RequestGet { @@ -124,6 +205,13 @@ where )); tracing::debug!("Seek contract {} @ {} (tx: {})", key, target.peer, id); new_state = self.state; + stats = Some(GetStats { + contract_location: Location::from(&key), + caching_peer: None, + transfer_time: None, + first_response_time: None, + step: Default::default(), + }); return_msg = Some(GetMsg::SeekNode { key, id, @@ -142,6 +230,10 @@ where htl, } => { let is_cached_contract = op_storage.ring.is_contract_cached(&key); + if let Some(s) = stats.as_mut() { + s.caching_peer = Some(target); + } + if !is_cached_contract { tracing::warn!( "Contract `{}` not found while processing a get request at node @ {}", @@ -170,6 +262,7 @@ where target: sender, // return to requester }), None, + stats, self._ttl, ); } @@ -274,6 +367,7 @@ where fetch_contract, .. }) => { + // todo: register in the stats for the outcome of the op that failed to get a response from this peer if retries < MAX_RETRIES { // no response received from this peer, so skip it in the next iteration skip_list.push(target.peer); @@ -367,6 +461,7 @@ where state: self.state, result: None, _ttl: self._ttl, + stats, }; op_storage @@ -441,7 +536,7 @@ where _ => return Err(OpError::UnexpectedOpState), } - build_op_result(self.id, new_state, return_msg, result, self._ttl) + build_op_result(self.id, new_state, return_msg, result, stats, self._ttl) }) } } @@ -451,12 +546,14 @@ fn build_op_result( state: Option, msg: Option, result: Option, + stats: Option, ttl: Duration, ) -> Result { let output_op = Some(GetOp { id, state, result, + stats, _ttl: ttl, }); Ok(OperationResult { @@ -510,14 +607,11 @@ fn check_contract_found( } } -pub(crate) fn start_op(key: ContractKey, fetch_contract: bool, id: &PeerKey) -> GetOp { - tracing::debug!( - "Requesting get contract {} @ loc({})", - key, - Location::from(&key) - ); +pub(crate) fn start_op(key: ContractKey, fetch_contract: bool, this_peer: &PeerKey) -> GetOp { + let contract_location = Location::from(&key); + tracing::debug!("Requesting get contract {} @ loc({contract_location})", key,); - let id = Transaction::new(::tx_type_id(), id); + let id = Transaction::new(::tx_type_id(), this_peer); let state = Some(GetState::PrepareRequest { key, id, @@ -527,11 +621,17 @@ pub(crate) fn start_op(key: ContractKey, fetch_contract: bool, id: &PeerKey) -> id, state, result: None, + stats: Some(GetStats { + contract_location, + caching_peer: None, + transfer_time: None, + first_response_time: None, + step: Default::default(), + }), _ttl: PEER_TIMEOUT, } } -#[derive(PartialEq, Eq, Debug, Clone)] enum GetState { /// A new petition for a get op. ReceivedRequest, @@ -555,19 +655,18 @@ pub(crate) async fn request_get( get_op: GetOp, client_id: Option, ) -> Result<(), OpError> { - let (target, id) = if let Some(GetState::PrepareRequest { key, id, .. }) = get_op.state.clone() - { + let (target, id) = if let Some(GetState::PrepareRequest { key, id, .. }) = &get_op.state { // the initial request must provide: // - a location in the network where the contract resides // - and the key of the contract value to get ( op_storage .ring - .closest_caching(&key, 1, &[]) + .closest_caching(key, 1, &[]) .into_iter() .next() .ok_or(RingError::EmptyRing)?, - id, + *id, ) } else { return Err(OpError::UnexpectedOpState); @@ -578,7 +677,7 @@ pub(crate) async fn request_get( id ); - match get_op.state.clone() { + match get_op.state { Some(GetState::PrepareRequest { fetch_contract, key, @@ -591,22 +690,26 @@ pub(crate) async fn request_get( fetch_contract, }); - let msg = Some(GetMsg::RequestGet { + let msg = GetMsg::RequestGet { id, key, target, fetch_contract, - }); + }; let op = GetOp { id, state: new_state, result: None, + stats: get_op.stats.map(|mut s| { + s.caching_peer = Some(target); + s + }), _ttl: get_op._ttl, }; op_storage - .notify_op_change(msg.map(Message::from).unwrap(), OpEnum::Get(op), client_id) + .notify_op_change(Message::from(msg), OpEnum::Get(op), client_id) .await?; } _ => return Err(OpError::InvalidStateTransition(get_op.id)), @@ -625,11 +728,6 @@ mod messages { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub(crate) enum GetMsg { - /// Internal node call to route to a peer close to the contract. - FetchRouting { - id: Transaction, - target: PeerKeyLocation, - }, RequestGet { id: Transaction, target: PeerKeyLocation, @@ -656,7 +754,6 @@ mod messages { impl InnerMessage for GetMsg { fn id(&self) -> &Transaction { match self { - Self::FetchRouting { id, .. } => id, Self::RequestGet { id, .. } => id, Self::SeekNode { id, .. } => id, Self::ReturnGet { id, .. } => id, @@ -674,7 +771,6 @@ mod messages { pub fn target(&self) -> Option<&PeerKeyLocation> { match self { - Self::FetchRouting { target, .. } => Some(target), Self::SeekNode { target, .. } => Some(target), Self::RequestGet { target, .. } => Some(target), Self::ReturnGet { target, .. } => Some(target), @@ -683,7 +779,7 @@ mod messages { pub fn terminal(&self) -> bool { use GetMsg::*; - matches!(self, ReturnGet { .. } | SeekNode { .. }) + matches!(self, ReturnGet { .. }) } } @@ -691,7 +787,6 @@ mod messages { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let id = self.id(); match self { - Self::FetchRouting { .. } => write!(f, "FetchRouting(id: {id})"), Self::RequestGet { .. } => write!(f, "RequestGet(id: {id})"), Self::SeekNode { .. } => write!(f, "SeekNode(id: {id})"), Self::ReturnGet { .. } => write!(f, "ReturnGet(id: {id})"), diff --git a/crates/core/src/operations/join_ring.rs b/crates/core/src/operations/join_ring.rs index ca20df520..876549dec 100644 --- a/crates/core/src/operations/join_ring.rs +++ b/crates/core/src/operations/join_ring.rs @@ -2,7 +2,7 @@ use futures::Future; use std::pin::Pin; use std::{collections::HashSet, time::Duration}; -use super::{OpError, OperationResult}; +use super::{OpError, OpOutcome, OperationResult}; use crate::operations::op_trait::Operation; use crate::operations::OpInitialization; use crate::{ @@ -34,9 +34,15 @@ impl JoinRingOp { self.backoff.is_some() } - pub(super) fn finished(&self) -> bool { - todo!() + pub(super) fn outcome(&self) -> OpOutcome { + OpOutcome::Irrelevant + } + + pub(super) fn finalized(&self) -> bool { + matches!(self.state, Some(JRState::Connected)) } + + pub(super) fn record_transfer(&mut self) {} } pub(crate) struct JoinRingResult {} @@ -49,7 +55,7 @@ impl TryFrom for JoinRingResult { } } -impl Operation for JoinRingOp { +impl Operation for JoinRingOp { type Message = JoinRingMsg; type Result = JoinRingResult; @@ -89,7 +95,7 @@ impl Operation for JoinRingOp { &self.id } - fn process_message<'a>( + fn process_message<'a, CB: ConnectionBridge>( self, conn_manager: &'a mut CB, op_storage: &'a OpManager, @@ -292,9 +298,11 @@ impl Operation for JoinRingOp { } _ => return Err(OpError::InvalidStateTransition(self.id)), }; - if let Some(state) = new_state.clone() { + if let Some(state) = new_state { if state.is_connected() { new_state = None; + } else { + new_state = Some(state); } }; } @@ -447,9 +455,11 @@ impl Operation for JoinRingOp { } _ => return Err(OpError::InvalidStateTransition(self.id)), } - if let Some(state) = new_state.clone() { + if let Some(state) = new_state { if state.is_connected() { new_state = None; + } else { + new_state = Some(state) } }; } @@ -471,7 +481,7 @@ impl Operation for JoinRingOp { } _ => return Err(OpError::InvalidStateTransition(self.id)), } - if let Some(state) = new_state.clone() { + if let Some(state) = new_state { if !state.is_connected() { return Err(OpError::InvalidStateTransition(id)); } else { @@ -494,7 +504,7 @@ impl Operation for JoinRingOp { } _ => return Err(OpError::InvalidStateTransition(self.id)), }; - if let Some(state) = new_state.clone() { + if let Some(state) = new_state { if !state.is_connected() { return Err(OpError::InvalidStateTransition(id)); } else { @@ -626,7 +636,6 @@ mod states { } } -#[derive(Debug, Clone)] enum JRState { Initializing, Connecting(ConnectionInfo), @@ -704,21 +713,23 @@ pub(crate) async fn join_ring_request( tx: Transaction, op_storage: &OpManager, conn_manager: &mut CB, - mut join_op: JoinRingOp, + join_op: JoinRingOp, ) -> Result<(), OpError> where CB: ConnectionBridge, { + let JoinRingOp { + id, + state, + backoff, + _ttl, + .. + } = join_op; let ConnectionInfo { gateway, this_peer, max_hops_to_live, - } = join_op - .state - .as_mut() - .expect("Infallible") - .clone() - .try_unwrap_connecting()?; + } = state.expect("infallible").try_unwrap_connecting()?; tracing::info!( "Joining ring via {} (at {}) (tx: {})", @@ -738,7 +749,20 @@ where }, }); conn_manager.send(&gateway.peer, join_req).await?; - op_storage.push(tx, OpEnum::JoinRing(Box::new(join_op)))?; + op_storage.push( + tx, + OpEnum::JoinRing(Box::new(JoinRingOp { + id, + state: Some(JRState::Connecting(ConnectionInfo { + gateway, + this_peer, + max_hops_to_live, + })), + gateway: Box::new(gateway), + backoff, + _ttl, + })), + )?; Ok(()) } diff --git a/crates/core/src/operations/op_trait.rs b/crates/core/src/operations/op_trait.rs index d5a4d63f5..646fec42b 100644 --- a/crates/core/src/operations/op_trait.rs +++ b/crates/core/src/operations/op_trait.rs @@ -7,11 +7,11 @@ use futures::Future; use crate::{ client_events::ClientId, message::{InnerMessage, Transaction}, - node::OpManager, + node::{ConnectionBridge, OpManager}, operations::{OpError, OpInitialization, OperationResult}, }; -pub(crate) trait Operation +pub(crate) trait Operation where Self: Sized + TryInto, { @@ -24,12 +24,10 @@ where msg: &Self::Message, ) -> Result, OpError>; - // fn new(transaction: Transaction, builder: Self::Builder) -> Self; - fn id(&self) -> &Transaction; #[allow(clippy::type_complexity)] - fn process_message<'a>( + fn process_message<'a, CB: ConnectionBridge>( self, conn_manager: &'a mut CB, op_storage: &'a OpManager, diff --git a/crates/core/src/operations/put.rs b/crates/core/src/operations/put.rs index 9c6a61a72..60e20361e 100644 --- a/crates/core/src/operations/put.rs +++ b/crates/core/src/operations/put.rs @@ -2,15 +2,15 @@ //! a given radius will cache a copy of the contract and it's current value, //! as well as will broadcast updates to the contract value to all subscribers. -use std::collections::HashSet; use std::future::Future; use std::pin::Pin; use std::time::Duration; +use std::{collections::HashSet, time::Instant}; pub(crate) use self::messages::PutMsg; use freenet_stdlib::prelude::*; -use super::{OpEnum, OpError, OperationResult}; +use super::{OpEnum, OpError, OpOutcome, OperationResult}; use crate::{ client_events::ClientId, config::PEER_TIMEOUT, @@ -24,28 +24,104 @@ use crate::{ pub(crate) struct PutOp { id: Transaction, state: Option, + stats: Option, /// time left until time out, when this reaches zero it will be removed from the state _ttl: Duration, - done: bool, } impl PutOp { - pub(super) fn finished(&self) -> bool { - self.done + pub(super) fn outcome(&self) -> OpOutcome { + match &self.stats { + Some(PutStats { + contract_location, + payload_size, + // first_response_time: Some((response_start, Some(response_end))), + transfer_time: Some((transfer_start, Some(transfer_end))), + target: Some(target), + .. + }) => { + let payload_transfer_time = *transfer_end - *transfer_start; + // todo: check if this is correct + // in puts both times are equivalent since when the transfer is initialized + // it already contains the payload + let first_response_time = payload_transfer_time.clone(); + OpOutcome::ContractOpSuccess { + target_peer: target, + contract_location: *contract_location, + payload_size: *payload_size, + payload_transfer_time, + first_response_time, + } + } + Some(_) => OpOutcome::Incomplete, + None => OpOutcome::Irrelevant, + } + } + + pub(super) fn finalized(&self) -> bool { + self.stats + .as_ref() + .map(|s| matches!(s.step, RecordingStats::Completed)) + .unwrap_or(false) + } + + pub(super) fn record_transfer(&mut self) { + if let Some(stats) = self.stats.as_mut() { + match stats.step { + RecordingStats::Uninitialized => { + stats.transfer_time = Some((Instant::now(), None)); + stats.step = RecordingStats::InitPut; + } + RecordingStats::InitPut => { + if let Some((_, e)) = stats.transfer_time.as_mut() { + *e = Some(Instant::now()); + } + stats.step = RecordingStats::Completed; + } + RecordingStats::Completed => {} + } + } } } +struct PutStats { + contract_location: Location, + payload_size: usize, + // /// (start, end) + // first_response_time: Option<(Instant, Option)>, + /// (start, end) + transfer_time: Option<(Instant, Option)>, + target: Option, + step: RecordingStats, +} + +/// While timing, at what particular step we are now. +#[derive(Clone, Copy, Default)] +enum RecordingStats { + #[default] + Uninitialized, + InitPut, + Completed, +} + pub(crate) struct PutResult {} impl TryFrom for PutResult { type Error = OpError; - fn try_from(_value: PutOp) -> Result { - todo!() + fn try_from(op: PutOp) -> Result { + if let Some(true) = op + .stats + .map(|s| matches!(s.step, RecordingStats::Completed)) + { + Ok(PutResult {}) + } else { + Err(OpError::UnexpectedOpState) + } } } -impl Operation for PutOp { +impl Operation for PutOp { type Message = PutMsg; type Result = PutResult; @@ -70,7 +146,7 @@ impl Operation for PutOp { Ok(OpInitialization { op: Self { state: Some(PutState::ReceivedRequest), - done: false, + stats: None, // don't care for stats in the target peers id: tx, _ttl: PEER_TIMEOUT, }, @@ -85,7 +161,7 @@ impl Operation for PutOp { &self.id } - fn process_message<'a>( + fn process_message<'a, CB: ConnectionBridge>( self, conn_manager: &'a mut CB, op_storage: &'a OpManager, @@ -95,7 +171,7 @@ impl Operation for PutOp { Box::pin(async move { let return_msg; let new_state; - let mut done = false; + let stats = self.stats; match input { PutMsg::RequestPut { @@ -339,7 +415,6 @@ impl Operation for PutOp { tracing::debug!("Successfully updated value for {}", contract,); new_state = None; return_msg = None; - done = true; } _ => return Err(OpError::InvalidStateTransition(self.id)), }; @@ -405,7 +480,7 @@ impl Operation for PutOp { _ => return Err(OpError::UnexpectedOpState), } - build_op_result(self.id, new_state, return_msg, self._ttl, done) + build_op_result(self.id, new_state, return_msg, self._ttl, stats) }) } } @@ -415,12 +490,12 @@ fn build_op_result( state: Option, msg: Option, ttl: Duration, - done: bool, + stats: Option, ) -> Result { let output_op = Some(PutOp { id, state, - done, + stats, _ttl: ttl, }); Ok(OperationResult { @@ -488,7 +563,7 @@ async fn try_to_broadcast( let op = PutOp { id, state: new_state, - done: false, + stats: None, _ttl: ttl, }; op_storage @@ -514,15 +589,15 @@ pub(crate) fn start_op( peer: &PeerKey, ) -> PutOp { let key = contract.key(); + let contract_location = Location::from(&key); tracing::debug!( - "Requesting put to contract {} @ loc({})", + "Requesting put to contract {} @ loc({contract_location})", key, - Location::from(&key) ); let id = Transaction::new(::tx_type_id(), peer); + let payload_size = contract.data().len(); let state = Some(PutState::PrepareRequest { - id, contract, value, htl, @@ -531,16 +606,21 @@ pub(crate) fn start_op( PutOp { id, state, - done: false, + stats: Some(PutStats { + contract_location, + payload_size, + target: None, + // first_response_time: None, + transfer_time: None, + step: Default::default(), + }), _ttl: PEER_TIMEOUT, } } -#[derive(PartialEq, Eq, Debug, Clone)] enum PutState { ReceivedRequest, PrepareRequest { - id: Transaction, contract: ContractContainer, value: WrappedState, htl: usize, @@ -554,10 +634,10 @@ enum PutState { /// Request to insert/update a value into a contract. pub(crate) async fn request_put( op_storage: &OpManager, - put_op: PutOp, + mut put_op: PutOp, client_id: Option, ) -> Result<(), OpError> { - let key = if let Some(PutState::PrepareRequest { contract, .. }) = put_op.state.clone() { + let key = if let Some(PutState::PrepareRequest { contract, .. }) = &put_op.state { contract.key() } else { return Err(OpError::UnexpectedOpState); @@ -576,8 +656,11 @@ pub(crate) async fn request_put( .ok_or(RingError::EmptyRing)?; let id = put_op.id; + if let Some(stats) = &mut put_op.stats { + stats.target = Some(target); + } - match put_op.state.clone() { + match put_op.state { Some(PutState::PrepareRequest { contract, value, @@ -586,23 +669,23 @@ pub(crate) async fn request_put( }) => { let key = contract.key(); let new_state = Some(PutState::AwaitingResponse { contract: key }); - let msg = Some(PutMsg::RequestPut { + let msg = PutMsg::RequestPut { id, contract, value, htl, target, - }); + }; let op = PutOp { state: new_state, id, - done: false, + stats: put_op.stats, _ttl: put_op._ttl, }; op_storage - .notify_op_change(msg.map(Message::from).unwrap(), OpEnum::Put(op), client_id) + .notify_op_change(Message::from(msg), OpEnum::Put(op), client_id) .await?; } _ => return Err(OpError::InvalidStateTransition(put_op.id)), diff --git a/crates/core/src/operations/subscribe.rs b/crates/core/src/operations/subscribe.rs index 726eaad65..8bc768bfb 100644 --- a/crates/core/src/operations/subscribe.rs +++ b/crates/core/src/operations/subscribe.rs @@ -15,7 +15,7 @@ use crate::{ ring::{PeerKeyLocation, RingError}, }; -use super::{OpEnum, OpError, OperationResult}; +use super::{OpEnum, OpError, OpOutcome, OperationResult}; pub(crate) use self::messages::SubscribeMsg; @@ -28,9 +28,15 @@ pub(crate) struct SubscribeOp { } impl SubscribeOp { - pub fn finished(&self) -> bool { - todo!() + pub(super) fn outcome(&self) -> OpOutcome { + OpOutcome::Irrelevant + } + + pub(super) fn finalized(&self) -> bool { + matches!(self.state, Some(SubscribeState::Completed)) } + + pub(super) fn record_transfer(&mut self) {} } pub(crate) enum SubscribeResult {} @@ -43,7 +49,7 @@ impl TryFrom for SubscribeResult { } } -impl Operation for SubscribeOp { +impl Operation for SubscribeOp { type Message = SubscribeMsg; type Result = SubscribeResult; @@ -85,7 +91,7 @@ impl Operation for SubscribeOp { &self.id } - fn process_message<'a>( + fn process_message<'a, CB: ConnectionBridge>( self, conn_manager: &'a mut CB, op_storage: &'a OpManager, @@ -300,7 +306,6 @@ pub(crate) fn start_op(key: ContractKey, peer: &PeerKey) -> SubscribeOp { } } -#[derive(PartialEq, Eq, Debug, Clone)] enum SubscribeState { /// Prepare the request to subscribe. PrepareRequest { @@ -323,42 +328,39 @@ pub(crate) async fn request_subscribe( sub_op: SubscribeOp, client_id: Option, ) -> Result<(), OpError> { - let (target, _id) = - if let Some(SubscribeState::PrepareRequest { id, key }) = sub_op.state.clone() { - if !op_storage.ring.is_contract_cached(&key) { - return Err(OpError::ContractError(ContractError::ContractNotFound(key))); - } - ( - op_storage - .ring - .closest_caching(&key, 1, &[]) - .into_iter() - .next() - .ok_or(RingError::EmptyRing)?, - id, - ) - } else { - return Err(OpError::UnexpectedOpState); - }; - - match sub_op.state.clone() { + let (target, _id) = if let Some(SubscribeState::PrepareRequest { id, key }) = &sub_op.state { + if !op_storage.ring.is_contract_cached(key) { + return Err(OpError::ContractError(ContractError::ContractNotFound( + key.clone(), + ))); + } + ( + op_storage + .ring + .closest_caching(key, 1, &[]) + .into_iter() + .next() + .ok_or(RingError::EmptyRing)?, + *id, + ) + } else { + return Err(OpError::UnexpectedOpState); + }; + + match sub_op.state { Some(SubscribeState::PrepareRequest { id, key, .. }) => { let new_state = Some(SubscribeState::AwaitingResponse { skip_list: vec![], retries: 0, }); - let msg = Some(SubscribeMsg::RequestSub { id, key, target }); + let msg = SubscribeMsg::RequestSub { id, key, target }; let op = SubscribeOp { id, state: new_state, _ttl: sub_op._ttl, }; op_storage - .notify_op_change( - msg.map(Message::from).unwrap(), - OpEnum::Subscribe(op), - client_id, - ) + .notify_op_change(Message::from(msg), OpEnum::Subscribe(op), client_id) .await?; } _ => return Err(OpError::InvalidStateTransition(sub_op.id)), diff --git a/crates/core/src/operations/update.rs b/crates/core/src/operations/update.rs index 49e5efe66..42339191f 100644 --- a/crates/core/src/operations/update.rs +++ b/crates/core/src/operations/update.rs @@ -17,7 +17,7 @@ impl TryFrom for UpdateResult { } } -impl Operation for UpdateOp { +impl Operation for UpdateOp { type Message = UpdateMsg; type Result = UpdateResult; @@ -32,7 +32,7 @@ impl Operation for UpdateOp { todo!() } - fn process_message<'a>( + fn process_message<'a, CB: ConnectionBridge>( self, _conn_manager: &'a mut CB, _op_storage: &'a crate::node::OpManager, diff --git a/crates/core/src/ring.rs b/crates/core/src/ring.rs index dff723b75..32d5c39c8 100644 --- a/crates/core/src/ring.rs +++ b/crates/core/src/ring.rs @@ -10,6 +10,7 @@ //! - next node //! - final location +use std::hash::Hash; use std::{ collections::BTreeMap, convert::TryFrom, @@ -27,10 +28,10 @@ use freenet_stdlib::prelude::ContractKey; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; -use rand::prelude::*; -use std::{cmp::Ordering, hash::Hash}; - -use crate::node::{self, NodeBuilder, PeerKey}; +use crate::{ + node::{self, NodeBuilder, PeerKey}, + router::Router, +}; #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] /// The location of a peer in the ring. This location allows routing towards the peer. @@ -68,6 +69,7 @@ pub(crate) struct Ring { pub peer_key: PeerKey, max_connections: usize, min_connections: usize, + router: Arc>, connections_by_location: Arc>>, location_for_peer: Arc>>, /// contracts in the ring cached by this node @@ -146,11 +148,14 @@ impl Ring { Self::MAX_CONNECTIONS }; + let router = Router::new(&[]); + let ring = Ring { rnd_if_htl_above, max_hops_to_live, max_connections, min_connections, + router: Arc::new(RwLock::new(router)), connections_by_location: Arc::new(RwLock::new(BTreeMap::new())), location_for_peer: Arc::new(RwLock::new(BTreeMap::new())), cached_contracts: DashSet::new(), @@ -321,6 +326,10 @@ impl Ring { iter.collect() } + pub fn routing_finished(&self, event: crate::router::RouteEvent) { + self.router.write().add_event(event); + } + /// Get a random peer from the known ring connections. pub fn random_peer(&self, filter_fn: F) -> Option where diff --git a/crates/core/src/router.rs b/crates/core/src/router.rs index 46f229aaa..bfa079bc7 100644 --- a/crates/core/src/router.rs +++ b/crates/core/src/router.rs @@ -17,7 +17,7 @@ pub(crate) struct Router { } impl Router { - fn new(history: &[RouteEvent]) -> Self { + pub fn new(history: &[RouteEvent]) -> Self { let failure_outcomes: Vec = history .iter() .map(|re| IsotonicEvent { @@ -107,7 +107,7 @@ impl Router { } } - fn add_event(&mut self, event: RouteEvent) { + pub fn add_event(&mut self, event: RouteEvent) { match event.outcome { RouteOutcome::Success { time_to_response_start, @@ -143,14 +143,11 @@ impl Router { } } - pub(crate) fn select_peer<'a, I>( + pub fn select_peer<'a>( &self, - peers: I, + peers: impl IntoIterator, contract_location: &Location, - ) -> Option<&'a PeerKeyLocation> - where - I: IntoIterator, - { + ) -> Option<&'a PeerKeyLocation> { if !self.has_sufficient_historical_data() { // Find the peer with the minimum distance to the contract location, // ignoring peers with no location @@ -240,7 +237,7 @@ impl Router { } #[derive(Debug)] -pub(crate) enum RoutingError { +enum RoutingError { InsufficientDataError, EstimationError(String), } @@ -255,22 +252,22 @@ impl fmt::Display for RoutingError { } #[derive(Debug, Clone, Copy, Serialize)] -pub(crate) struct RoutingPrediction { - pub failure_probability: f64, - pub xfer_speed: TransferSpeed, - pub time_to_response_start: f64, - pub expected_total_time: f64, +struct RoutingPrediction { + failure_probability: f64, + xfer_speed: TransferSpeed, + time_to_response_start: f64, + expected_total_time: f64, } #[derive(Debug, Clone, Copy, Serialize)] pub(crate) struct RouteEvent { - peer: PeerKeyLocation, - contract_location: Location, - outcome: RouteOutcome, + pub peer: PeerKeyLocation, + pub contract_location: Location, + pub outcome: RouteOutcome, } #[derive(Debug, Clone, Copy, Serialize)] -pub(crate) enum RouteOutcome { +pub enum RouteOutcome { Success { time_to_response_start: Duration, payload_size: usize, diff --git a/crates/core/src/router/isotonic_estimator.rs b/crates/core/src/router/isotonic_estimator.rs index e3670607e..2ba5d5dd8 100644 --- a/crates/core/src/router/isotonic_estimator.rs +++ b/crates/core/src/router/isotonic_estimator.rs @@ -13,9 +13,9 @@ const MIN_POINTS_FOR_REGRESSION: usize = 5; /// outcome of the peer's previous requests. #[derive(Debug, Clone, Serialize)] -pub(crate) struct IsotonicEstimator { - pub(crate) global_regression: IsotonicRegression, - pub(crate) peer_adjustments: HashMap, +pub(super) struct IsotonicEstimator { + pub global_regression: IsotonicRegression, + pub peer_adjustments: HashMap, } impl IsotonicEstimator { @@ -148,7 +148,7 @@ impl IsotonicEstimator { } } -pub(crate) enum EstimatorType { +pub(super) enum EstimatorType { /// Where the estimated value is expected to increase as distance increases Positive, /// Where the estimated value is expected to decrease as distance increases @@ -156,7 +156,7 @@ pub(crate) enum EstimatorType { } #[derive(Debug, PartialEq, Eq)] -pub(crate) enum EstimationError { +pub(super) enum EstimationError { InsufficientData, // Error indicating that there is not enough data for estimation } @@ -171,7 +171,7 @@ impl fmt::Display for EstimationError { /// A routing event is a single request to a peer for a contract, and some value indicating /// the result of the request, such as the time it took to retrieve the contract. #[derive(Debug, Clone)] -pub(crate) struct IsotonicEvent { +pub(super) struct IsotonicEvent { pub peer: PeerKeyLocation, pub contract_location: Location, /// The result of the routing event, which is used to train the estimator, typically the time @@ -187,7 +187,7 @@ impl IsotonicEvent { } #[derive(Debug, Clone, Serialize)] -pub(crate) struct Adjustment { +pub(super) struct Adjustment { sum: f64, count: u64, } diff --git a/crates/core/src/router/util.rs b/crates/core/src/router/util.rs index f8fe151e0..0f78072d0 100644 --- a/crates/core/src/router/util.rs +++ b/crates/core/src/router/util.rs @@ -3,7 +3,7 @@ use std::time::Duration; use serde::Serialize; #[derive(Debug, Clone, Serialize)] -pub(crate) struct Mean { +pub(super) struct Mean { sum: f64, count: u64, } @@ -35,8 +35,8 @@ impl Default for Mean { } #[derive(Debug, Clone, Copy, Serialize)] -pub(crate) struct TransferSpeed { - pub(crate) bytes_per_second: f64, +pub(super) struct TransferSpeed { + pub bytes_per_second: f64, } impl TransferSpeed { From 7493178febd90136146a0d186926597895cbe87c Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Thu, 12 Oct 2023 14:10:23 +0200 Subject: [PATCH 32/76] Log route events and reload router periodically --- crates/core/src/config.rs | 28 +- crates/core/src/node.rs | 80 ++++-- crates/core/src/node/conn_manager.rs | 8 + .../core/src/node/conn_manager/p2p_protoc.rs | 11 +- crates/core/src/node/event_log.rs | 239 +++++++++++++----- crates/core/src/node/in_memory_impl.rs | 18 +- crates/core/src/node/p2p_impl.rs | 32 ++- crates/core/src/node/tests.rs | 4 +- crates/core/src/operations/join_ring.rs | 10 + crates/core/src/operations/put.rs | 61 ++--- crates/core/src/ring.rs | 28 +- crates/core/src/router.rs | 46 ++-- crates/core/src/router/isotonic_estimator.rs | 15 +- crates/core/src/router/util.rs | 2 +- 14 files changed, 405 insertions(+), 177 deletions(-) diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index 855de5d37..f8b8155dd 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -86,6 +86,7 @@ pub struct ConfigPaths { secrets_dir: PathBuf, db_dir: PathBuf, app_data_dir: PathBuf, + event_log: PathBuf, } impl ConfigPaths { @@ -122,12 +123,21 @@ impl ConfigPaths { fs::create_dir_all(db_dir.join("local"))?; } + let event_log = app_data_dir.join("_EVENT_LOG"); + if !event_log.exists() { + fs::write(&event_log, [])?; + let mut local_file = event_log.clone(); + local_file.set_file_name("_EVENT_LOG_LOCAL"); + fs::write(local_file, [])?; + } + Ok(Self { contracts_dir, delegates_dir, secrets_dir, db_dir, app_data_dir, + event_log, }) } } @@ -176,8 +186,24 @@ impl Config { } } + pub fn event_log(&self) -> PathBuf { + if self.local_mode.load(std::sync::atomic::Ordering::SeqCst) { + let mut local_file = self.config_paths.event_log.clone(); + local_file.set_file_name("_EVENT_LOG_LOCAL"); + local_file + } else { + self.config_paths.event_log.to_owned() + } + } + pub fn get_static_conf() -> &'static Config { - CONFIG.get_or_init(|| Config::load_conf().expect("Failed to load configuration")) + CONFIG.get_or_init(|| match Config::load_conf() { + Ok(config) => config, + Err(err) => { + tracing::error!("failed while loading configuration: {err}"); + panic!("Failed while loading configuration") + } + }) } fn load_conf() -> std::io::Result { diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index fc38d6dcf..a690b41a6 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -20,10 +20,7 @@ use libp2p::{identity, multiaddr::Protocol, Multiaddr, PeerId}; #[cfg(test)] use self::in_memory_impl::NodeInMemory; -use self::{ - event_log::{EventLog, EventLogListener}, - p2p_impl::NodeP2P, -}; +use self::{event_log::EventLog, p2p_impl::NodeP2P}; use crate::{ client_events::{BoxedClient, ClientEventsProxy, ClientId, OpenRequest}, config::Config, @@ -45,6 +42,9 @@ use crate::{ use crate::operations::handle_op_request; pub(crate) use conn_manager::{ConnectionBridge, ConnectionError}; +#[cfg(test)] +pub(crate) use event_log::test_utils::TestEventListener; +pub(crate) use event_log::{EventLogRegister, EventRegister}; pub(crate) use op_state::OpManager; mod conn_manager; @@ -184,7 +184,11 @@ impl NodeBuilder { /// Builds a node using the default backend connection manager. pub async fn build(self, config: NodeConfig) -> Result { - let node = NodeP2P::build::(self, config).await?; + let event_log = event_log::EventRegister::new(); + let node = NodeP2P::build::( + self, event_log, config, + ) + .await?; Ok(Node(node)) } @@ -446,6 +450,7 @@ async fn report_result( op_storage: &OpManager, executor_callback: Option>, client_req_handler_callback: Option<(ClientId, ClientResponsesSender)>, + event_listener: &mut Box, ) { match op_result { Ok(Some(op_res)) => { @@ -463,7 +468,7 @@ async fn report_result( payload_size, payload_transfer_time, } => { - op_storage.ring.routing_finished(RouteEvent { + let event = RouteEvent { peer: *target_peer, contract_location, outcome: RouteOutcome::Success { @@ -471,7 +476,14 @@ async fn report_result( payload_size, payload_transfer_time, }, - }); + }; + if let Err(err) = event_listener + .event_received(EventLog::route_event(op_res.id(), op_storage, &event)) + .await + { + tracing::warn!("failed logging event: {err}"); + } + op_storage.ring.routing_finished(event); } // todo: handle failures, need to track timeouts and other potential failures // OpOutcome::ContractOpFailure { @@ -501,7 +513,7 @@ async fn process_message( msg: Result, op_storage: Arc, mut conn_manager: CB, - event_listener: Option>, + mut event_listener: Box, executor_callback: Option>, client_req_handler_callback: Option, client_id: Option, @@ -511,8 +523,11 @@ async fn process_message( let cli_req = client_id.zip(client_req_handler_callback); match msg { Ok(msg) => { - if let Some(mut listener) = event_listener { - listener.event_received(EventLog::new(&msg, &op_storage)); + if let Err(err) = event_listener + .event_received(EventLog::from_msg(&msg, &op_storage)) + .await + { + tracing::warn!("failed logging event: {err}"); } match msg { Message::JoinRing(op) => { @@ -524,7 +539,14 @@ async fn process_message( client_id, ) .await; - report_result(op_result, &op_storage, executor_callback, cli_req).await; + report_result( + op_result, + &op_storage, + executor_callback, + cli_req, + &mut event_listener, + ) + .await; } Message::Put(op) => { log_handling_msg!("put", *op.id(), op_storage); @@ -535,7 +557,14 @@ async fn process_message( client_id, ) .await; - report_result(op_result, &op_storage, executor_callback, cli_req).await; + report_result( + op_result, + &op_storage, + executor_callback, + cli_req, + &mut event_listener, + ) + .await; } Message::Get(op) => { log_handling_msg!("get", op.id(), op_storage); @@ -546,7 +575,14 @@ async fn process_message( client_id, ) .await; - report_result(op_result, &op_storage, executor_callback, cli_req).await; + report_result( + op_result, + &op_storage, + executor_callback, + cli_req, + &mut event_listener, + ) + .await; } Message::Subscribe(op) => { log_handling_msg!("subscribe", op.id(), op_storage); @@ -557,13 +593,27 @@ async fn process_message( client_id, ) .await; - report_result(op_result, &op_storage, executor_callback, cli_req).await; + report_result( + op_result, + &op_storage, + executor_callback, + cli_req, + &mut event_listener, + ) + .await; } _ => {} } } Err(err) => { - report_result(Err(err.into()), &op_storage, executor_callback, cli_req).await; + report_result( + Err(err.into()), + &op_storage, + executor_callback, + cli_req, + &mut event_listener, + ) + .await; } } } diff --git a/crates/core/src/node/conn_manager.rs b/crates/core/src/node/conn_manager.rs index e1c6cf73d..5ad9e25c6 100644 --- a/crates/core/src/node/conn_manager.rs +++ b/crates/core/src/node/conn_manager.rs @@ -27,6 +27,14 @@ pub(crate) type ConnResult = std::result::Result; pub(crate) trait ConnectionBridge: Send + Sync { async fn add_connection(&mut self, peer: PeerKey) -> ConnResult<()>; + // todo: LRU connection drop IF we can connect to other peer + // at least have a minimum of connection time alive to consider dropping it + + // If we get a join request and are at MAX_CONNECTIONS: + // 1. Ensure it's been N minutes since the last peer removal. + // 2. Drop the peer with the fewest outbound requests/minute that's at least M minutes old. + // This promotes peer turnover to prevent network stagnation. + async fn drop_connection(&mut self, peer: &PeerKey) -> ConnResult<()>; async fn send(&self, target: &PeerKey, msg: Message) -> ConnResult<()>; diff --git a/crates/core/src/node/conn_manager/p2p_protoc.rs b/crates/core/src/node/conn_manager/p2p_protoc.rs index 4eee493ad..bc996135d 100644 --- a/crates/core/src/node/conn_manager/p2p_protoc.rs +++ b/crates/core/src/node/conn_manager/p2p_protoc.rs @@ -42,8 +42,8 @@ use crate::{ contract::{ClientResponsesSender, ExecutorToEventLoopChannel, NetworkEventListenerHalve}, message::{Message, NodeEvent, Transaction, TransactionType}, node::{ - handle_cancelled_op, join_ring_request, process_message, InitPeerNode, NodeBuilder, - OpManager, PeerKey, + handle_cancelled_op, join_ring_request, process_message, EventLogRegister, InitPeerNode, + NodeBuilder, OpManager, PeerKey, }, operations::OpError, ring::PeerKeyLocation, @@ -173,6 +173,7 @@ pub(in crate::node) struct P2pConnManager { conn_bridge_rx: Receiver, /// last valid observed public address public_addr: Option, + event_listener: Box, } impl P2pConnManager { @@ -180,6 +181,7 @@ impl P2pConnManager { transport: transport::Boxed<(PeerId, muxing::StreamMuxerBox)>, config: &NodeBuilder, op_manager: Arc, + event_listener: &dyn EventLogRegister, ) -> Result { // We set a global executor which is virtually the Tokio multi-threaded executor // to reuse it's thread pool and scheduler in order to drive futures. @@ -219,6 +221,7 @@ impl P2pConnManager { bridge, conn_bridge_rx: rx_bridge_cmd, public_addr, + event_listener: event_listener.trait_clone(), }) } @@ -396,7 +399,7 @@ impl P2pConnManager { Ok(msg), op_manager.clone(), cb, - None, + self.event_listener.trait_clone(), executor_callback, client_req_handler_callback, client_id, @@ -458,7 +461,7 @@ impl P2pConnManager { Err(err), op_manager.clone(), cb, - None, + self.event_listener.trait_clone(), None, None, None, diff --git a/crates/core/src/node/event_log.rs b/crates/core/src/node/event_log.rs index 7828c1139..479927890 100644 --- a/crates/core/src/node/event_log.rs +++ b/crates/core/src/node/event_log.rs @@ -1,11 +1,20 @@ use freenet_stdlib::prelude::*; +use futures::{future::BoxFuture, FutureExt}; +use serde::{Deserialize, Serialize}; +use tokio::{ + fs::OpenOptions, + sync::mpsc::{self}, +}; use super::PeerKey; use crate::{ + config::GlobalExecutor, contract::StoreResponse, message::{Message, Transaction}, operations::{get::GetMsg, join_ring::JoinRingMsg, put::PutMsg}, ring::{Location, PeerKeyLocation}, + router::RouteEvent, + DynError, }; #[cfg(test)] @@ -21,9 +30,15 @@ struct ListenerLogId(usize); /// /// This type then can emit it's own information to adjacent systems /// or is a no-op. -pub(crate) trait EventLogListener { - fn event_received(&mut self, ev: EventLog); - fn trait_clone(&self) -> Box; +pub(crate) trait EventLogRegister: std::any::Any + Send + Sync + 'static { + fn event_received<'a>(&'a mut self, ev: EventLog) -> BoxFuture<'a, Result<(), DynError>>; + fn trait_clone(&self) -> Box; + fn as_any(&self) -> &dyn std::any::Any + where + Self: Sized, + { + self as _ + } } #[allow(dead_code)] // fixme: remove this @@ -34,7 +49,19 @@ pub(crate) struct EventLog<'a> { } impl<'a> EventLog<'a> { - pub fn new(msg: &'a Message, op_storage: &'a OpManager) -> Self { + pub fn route_event( + tx: &'a Transaction, + op_storage: &'a OpManager, + route_event: &RouteEvent, + ) -> Self { + EventLog { + tx, + peer_id: &op_storage.ring.peer_key, + kind: EventKind::Route(route_event.clone()), + } + } + + pub fn from_msg(msg: &'a Message, op_storage: &'a OpManager) -> Self { let kind = match msg { Message::JoinRing(JoinRingMsg::Connected { sender, target, .. }) => { EventKind::Connected { @@ -47,47 +74,37 @@ impl<'a> EventLog<'a> { contract, target, .. }) => { let key = contract.key(); - EventKind::Put( - PutEvent::Request { - performer: target.peer, - key, - }, - *msg.id(), - ) + EventKind::Put(PutEvent::Request { + performer: target.peer, + key, + }) } - Message::Put(PutMsg::SuccessfulUpdate { new_value, .. }) => EventKind::Put( - PutEvent::PutSuccess { + Message::Put(PutMsg::SuccessfulUpdate { new_value, .. }) => { + EventKind::Put(PutEvent::PutSuccess { requester: op_storage.ring.peer_key, value: new_value.clone(), - }, - *msg.id(), - ), + }) + } Message::Put(PutMsg::Broadcasting { new_value, broadcast_to, key, .. - }) => EventKind::Put( - PutEvent::BroadcastEmitted { - broadcast_to: broadcast_to.clone(), - key: key.clone(), - value: new_value.clone(), - }, - *msg.id(), - ), + }) => EventKind::Put(PutEvent::BroadcastEmitted { + broadcast_to: broadcast_to.clone(), + key: key.clone(), + value: new_value.clone(), + }), Message::Put(PutMsg::BroadcastTo { sender, new_value, key, .. - }) => EventKind::Put( - PutEvent::BroadcastReceived { - requester: sender.peer, - key: key.clone(), - value: new_value.clone(), - }, - *msg.id(), - ), + }) => EventKind::Put(PutEvent::BroadcastReceived { + requester: sender.peer, + key: key.clone(), + value: new_value.clone(), + }), Message::Get(GetMsg::ReturnGet { key, value: StoreResponse { state: Some(_), .. }, @@ -103,41 +120,136 @@ impl<'a> EventLog<'a> { } } -#[cfg(test)] -struct MessageLog { +#[derive(Serialize, Deserialize)] +struct LogMessage { + tx: Transaction, peer_id: PeerKey, kind: EventKind, } #[derive(Clone)] -pub(super) struct EventRegister {} +pub(crate) struct EventRegister { + log_sender: mpsc::Sender, +} -impl EventLogListener for EventRegister { - fn event_received(&mut self, _log: EventLog) { - // let (_msg_log, _log_id) = create_log(log); - // TODO: save log +impl EventRegister { + pub fn new() -> Self { + let (log_sender, log_recv) = mpsc::channel(1000); + GlobalExecutor::spawn(Self::record_logs(log_recv)); + Self { log_sender } } - fn trait_clone(&self) -> Box { + async fn record_logs(mut log_recv: mpsc::Receiver) { + use tokio::io::AsyncWriteExt; + let event_log_path = crate::config::Config::get_static_conf().event_log(); + let mut event_log = match OpenOptions::new().write(true).open(&event_log_path).await { + Ok(file) => file, + Err(err) => { + tracing::error!("failed openning log file {:?} with: {err}", event_log_path); + panic!("failed openning log file"); // todo: propagate this to the main thread + } + }; + let mut num_written = 0; + let mut buf = vec![]; + while let Some(log) = log_recv.recv().await { + if let Err(err) = bincode::serialize_into(&mut buf, &log) { + tracing::error!("failed serializing log: {err}"); + panic!("failed serializing log"); + } + buf.push(b'\n'); + num_written += 1; + if num_written == 100 { + if let Err(err) = event_log.write_all(&buf).await { + tracing::error!("failed writting to event log: {err}"); + panic!("failed writting event log"); + } + num_written = 0; + buf.clear(); + } + } + } + + pub async fn get_router_events(max_event_number: usize) -> Result, DynError> { + use tokio::io::AsyncReadExt; + const BUF_SIZE: usize = 4096; + const MAX_EVENT_HISTORY: usize = 10_000; + let event_num = max_event_number.min(MAX_EVENT_HISTORY); + + let event_log_path = crate::config::Config::get_static_conf().event_log(); + let mut event_log = OpenOptions::new().read(true).open(event_log_path).await?; + + let mut buf = [0; BUF_SIZE]; + let mut records = Vec::with_capacity(event_num); + let mut partial_record = vec![]; + let mut record_start = 0; + + while records.len() < event_num { + let bytes_read = event_log.read(&mut buf).await?; + if bytes_read == 0 { + break; // EOF + } + + let mut found_newline = false; + for (i, byte) in buf.iter().enumerate().take(bytes_read) { + if byte == &b'\n' { + found_newline = true; + let rec = &buf[record_start..i]; + let deser_record: LogMessage = if partial_record.is_empty() { + record_start = i + 1; + bincode::deserialize(rec)? + } else { + partial_record.extend(rec); + let rec = bincode::deserialize(&partial_record)?; + partial_record.clear(); + rec + }; + if let EventKind::Route(outcome) = deser_record.kind { + records.push(outcome); + } + } + if records.len() == event_num { + break; // Reached the desired event number + } + } + if !found_newline { + break; // No more data to read, and event_num not reached + } + } + Ok(records) + } +} + +impl EventLogRegister for EventRegister { + fn event_received<'a>(&'a mut self, log: EventLog) -> BoxFuture<'a, Result<(), DynError>> { + let log_msg = LogMessage { + tx: *log.tx, + kind: log.kind, + peer_id: *log.peer_id, + }; + async { Ok(self.log_sender.send(log_msg).await?) }.boxed() + } + + fn trait_clone(&self) -> Box { Box::new(self.clone()) } } -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Serialize, Deserialize)] enum EventKind { Connected { loc: Location, from: PeerKey, to: PeerKeyLocation, }, - Put(PutEvent, Transaction), + Put(PutEvent), Get { key: ContractKey, }, + Route(RouteEvent), Unknown, } -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] enum PutEvent { Request { performer: PeerKey, @@ -166,7 +278,7 @@ enum PutEvent { } #[cfg(test)] -mod test_utils { +pub(super) mod test_utils { use std::{ collections::HashMap, sync::{ @@ -183,22 +295,11 @@ mod test_utils { static LOG_ID: AtomicUsize = AtomicUsize::new(0); - #[inline] - fn create_log(log: EventLog) -> (MessageLog, ListenerLogId) { - let log_id = ListenerLogId(LOG_ID.fetch_add(1, SeqCst)); - let EventLog { peer_id, kind, .. } = log; - let msg_log = MessageLog { - peer_id: *peer_id, - kind, - }; - (msg_log, log_id) - } - #[derive(Clone)] pub(crate) struct TestEventListener { node_labels: Arc>, tx_log: Arc>>, - logs: Arc>>, + logs: Arc>>, } impl TestEventListener { @@ -228,7 +329,7 @@ mod test_utils { ) -> bool { let logs = self.logs.read(); let put_ops = logs.iter().filter_map(|l| match &l.kind { - EventKind::Put(ev, id) => Some((id, ev)), + EventKind::Put(ev) => Some((&l.tx, ev)), _ => None, }); let put_ops: HashMap<_, Vec<_>> = put_ops.fold(HashMap::new(), |mut acc, (id, ev)| { @@ -266,8 +367,8 @@ mod test_utils { pub fn contract_broadcasted(&self, for_key: &ContractKey) -> bool { let logs = self.logs.read(); let put_broadcast_ops = logs.iter().filter_map(|l| match &l.kind { - EventKind::Put(ev @ PutEvent::BroadcastEmitted { .. }, id) - | EventKind::Put(ev @ PutEvent::BroadcastReceived { .. }, id) => Some((id, ev)), + EventKind::Put(ev @ PutEvent::BroadcastEmitted { .. }) + | EventKind::Put(ev @ PutEvent::BroadcastReceived { .. }) => Some((&l.tx, ev)), _ => None, }); let put_broadcast_by_tx: HashMap<_, Vec<_>> = @@ -319,19 +420,31 @@ mod test_utils { .collect::>() .into_iter() } + + fn create_log(log: EventLog) -> (LogMessage, ListenerLogId) { + let log_id = ListenerLogId(LOG_ID.fetch_add(1, SeqCst)); + let EventLog { peer_id, kind, .. } = log; + let msg_log = LogMessage { + tx: *log.tx, + peer_id: *peer_id, + kind, + }; + (msg_log, log_id) + } } - impl super::EventLogListener for TestEventListener { - fn event_received(&mut self, log: EventLog) { + impl super::EventLogRegister for TestEventListener { + fn event_received<'a>(&'a mut self, log: EventLog) -> BoxFuture<'a, Result<(), DynError>> { let tx = log.tx; let mut logs = self.logs.write(); - let (msg_log, log_id) = create_log(log); + let (msg_log, log_id) = Self::create_log(log); logs.push(msg_log); std::mem::drop(logs); self.tx_log.entry(*tx).or_default().push(log_id); + async { Ok(()) }.boxed() } - fn trait_clone(&self) -> Box { + fn trait_clone(&self) -> Box { Box::new(self.clone()) } } diff --git a/crates/core/src/node/in_memory_impl.rs b/crates/core/src/node/in_memory_impl.rs index 84b07142a..13a12491a 100644 --- a/crates/core/src/node/in_memory_impl.rs +++ b/crates/core/src/node/in_memory_impl.rs @@ -6,10 +6,9 @@ use freenet_stdlib::prelude::*; use super::{ client_event_handling, conn_manager::{in_memory::MemoryConnManager, EventLoopNotifications}, - event_log::EventLogListener, handle_cancelled_op, join_ring_request, op_state::OpManager, - process_message, PeerKey, + process_message, EventLogRegister, PeerKey, }; use crate::{ client_events::ClientEventsProxy, @@ -31,16 +30,16 @@ pub(super) struct NodeInMemory { gateways: Vec, notification_channel: EventLoopNotifications, conn_manager: MemoryConnManager, - event_listener: Option>, + event_listener: Box, is_gateway: bool, _executor_listener: ExecutorToEventLoopChannel, } impl NodeInMemory { /// Buils an in-memory node. Does nothing upon construction, - pub async fn build( + pub async fn build( builder: NodeBuilder<1>, - event_listener: Option>, + event_listener: EL, ch_builder: CH::Builder, ) -> Result where @@ -51,7 +50,7 @@ impl NodeInMemory { let gateways = builder.get_gateways()?; let is_gateway = builder.local_ip.zip(builder.local_port).is_some(); - let ring = Ring::new(&builder, &gateways)?; + let ring = Ring::new::<1, EL>(&builder, &gateways)?; let (notification_channel, notification_tx) = EventLoopNotifications::channel(); let (ops_ch_channel, ch_channel) = contract::contract_handler_channel(); let op_storage = Arc::new(OpManager::new(ring, notification_tx, ops_ch_channel)); @@ -68,7 +67,7 @@ impl NodeInMemory { op_storage, gateways, notification_channel, - event_listener, + event_listener: Box::new(event_listener), is_gateway, _executor_listener, }) @@ -207,10 +206,7 @@ impl NodeInMemory { let op_storage = self.op_storage.clone(); let conn_manager = self.conn_manager.clone(); - let event_listener = self - .event_listener - .as_ref() - .map(|listener| listener.trait_clone()); + let event_listener = self.event_listener.trait_clone(); GlobalExecutor::spawn(process_message( msg, diff --git a/crates/core/src/node/p2p_impl.rs b/crates/core/src/node/p2p_impl.rs index 4e44ae477..e3400dfd7 100644 --- a/crates/core/src/node/p2p_impl.rs +++ b/crates/core/src/node/p2p_impl.rs @@ -14,7 +14,7 @@ use libp2p::{ use super::{ client_event_handling, conn_manager::{p2p_protoc::P2pConnManager, EventLoopNotifications}, - join_ring_request, PeerKey, + join_ring_request, EventLogRegister, PeerKey, }; use crate::{ client_events::combinator::ClientEventsCombinator, @@ -35,7 +35,6 @@ pub(super) struct NodeP2P { pub(crate) op_manager: Arc, notification_channel: EventLoopNotifications, pub(super) conn_manager: P2pConnManager, - // event_listener: Option>, is_gateway: bool, executor_listener: ExecutorToEventLoopChannel, cli_response_sender: ClientResponsesSender, @@ -75,8 +74,9 @@ impl NodeP2P { .await } - pub(crate) async fn build( + pub(crate) async fn build( builder: NodeBuilder, + event_listener: EL, ch_builder: CH::Builder, ) -> Result where @@ -85,7 +85,8 @@ impl NodeP2P { let peer_key = PeerKey::from(builder.local_key.public()); let gateways = builder.get_gateways()?; - let ring = Ring::new(&builder, &gateways)?; + let event_listener: Box = Box::new(event_listener); + let ring = Ring::new::(&builder, &gateways)?; let (notification_channel, notification_tx) = EventLoopNotifications::channel(); let (ch_outbound, ch_inbound) = contract::contract_handler_channel(); let (client_responses, cli_response_sender) = contract::ClientResponses::channel(); @@ -97,7 +98,7 @@ impl NodeP2P { let conn_manager = { let transport = Self::config_transport(&builder.local_key)?; - P2pConnManager::build(transport, &builder, op_storage.clone())? + P2pConnManager::build(transport, &builder, op_storage.clone(), &*event_listener)? }; GlobalExecutor::spawn(contract::contract_handling(contract_handler)); @@ -152,7 +153,7 @@ mod test { client_events::test::MemoryEventsGen, config::GlobalExecutor, contract::MemoryContractHandler, - node::{tests::get_free_port, InitPeerNode}, + node::{event_log, tests::get_free_port, InitPeerNode}, ring::Location, }; @@ -208,7 +209,14 @@ mod test { .with_ip(Ipv4Addr::LOCALHOST) .with_port(peer1_port) .with_key(peer1_key); - let mut peer1 = Box::new(NodeP2P::build::(config, ()).await?); + let mut peer1 = Box::new( + NodeP2P::build::( + config, + event_log::TestEventListener::new(), + (), + ) + .await?, + ); peer1.conn_manager.listen_on()?; ping_ev_loop(&mut peer1).await.unwrap(); Ok::<_, anyhow::Error>(()) @@ -219,9 +227,13 @@ mod test { let user_events = MemoryEventsGen::new(receiver2, PeerKey::from(peer2_id)); let mut config = NodeBuilder::new([Box::new(user_events)]); config.add_gateway(peer1_config.clone()); - let mut peer2 = NodeP2P::build::(config, ()) - .await - .unwrap(); + let mut peer2 = NodeP2P::build::( + config, + event_log::TestEventListener::new(), + (), + ) + .await + .unwrap(); // wait a bit to make sure the first peer is up and listening tokio::time::sleep(Duration::from_millis(10)).await; peer2 diff --git a/crates/core/src/node/tests.rs b/crates/core/src/node/tests.rs index e4eba6473..88b9d1d68 100644 --- a/crates/core/src/node/tests.rs +++ b/crates/core/src/node/tests.rs @@ -205,9 +205,9 @@ impl SimNetwork { self.event_listener .add_node(label.clone(), PeerKey::from(id)); - let node = NodeInMemory::build::( + let node = NodeInMemory::build::( config, - Some(Box::new(self.event_listener.clone())), + self.event_listener.clone(), (), ) .await diff --git a/crates/core/src/operations/join_ring.rs b/crates/core/src/operations/join_ring.rs index 876549dec..3a88cb3ed 100644 --- a/crates/core/src/operations/join_ring.rs +++ b/crates/core/src/operations/join_ring.rs @@ -126,6 +126,7 @@ impl Operation for JoinRingOp { ); let new_location = Location::random(); + // FIXME: don't try to forward to peers which have already been tried (add a rejected_by list) let accepted_by = if op_storage.ring.should_accept(&new_location) { tracing::debug!("Accepting connection from {}", req_peer,); HashSet::from_iter([this_node_loc]) @@ -951,6 +952,15 @@ mod messages { } } + /* + + Peer A ---> Peer B (forward) ----> Peer C + |----- (forward) ---------> Peer D + + + Peer A ---> Peer B (forward) ----> Peer C ----> Peer D + + */ #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub(crate) enum JoinRequest { StartReq { diff --git a/crates/core/src/operations/put.rs b/crates/core/src/operations/put.rs index 60e20361e..27f91e33c 100644 --- a/crates/core/src/operations/put.rs +++ b/crates/core/src/operations/put.rs @@ -31,31 +31,32 @@ pub(crate) struct PutOp { impl PutOp { pub(super) fn outcome(&self) -> OpOutcome { - match &self.stats { - Some(PutStats { - contract_location, - payload_size, - // first_response_time: Some((response_start, Some(response_end))), - transfer_time: Some((transfer_start, Some(transfer_end))), - target: Some(target), - .. - }) => { - let payload_transfer_time = *transfer_end - *transfer_start; - // todo: check if this is correct - // in puts both times are equivalent since when the transfer is initialized - // it already contains the payload - let first_response_time = payload_transfer_time.clone(); - OpOutcome::ContractOpSuccess { - target_peer: target, - contract_location: *contract_location, - payload_size: *payload_size, - payload_transfer_time, - first_response_time, - } - } - Some(_) => OpOutcome::Incomplete, - None => OpOutcome::Irrelevant, - } + // todo: track in the future + // match &self.stats { + // Some(PutStats { + // contract_location, + // payload_size, + // // first_response_time: Some((response_start, Some(response_end))), + // transfer_time: Some((transfer_start, Some(transfer_end))), + // target: Some(target), + // .. + // }) => { + // let payload_transfer_time: Duration = *transfer_end - *transfer_start; + // // in puts both times are equivalent since when the transfer is initialized + // // it already contains the payload + // let first_response_time = payload_transfer_time; + // OpOutcome::ContractOpSuccess { + // target_peer: target, + // contract_location: *contract_location, + // payload_size: *payload_size, + // payload_transfer_time, + // first_response_time, + // } + // } + // Some(_) => OpOutcome::Incomplete, + // None => OpOutcome::Irrelevant, + // } + OpOutcome::Irrelevant } pub(super) fn finalized(&self) -> bool { @@ -85,8 +86,8 @@ impl PutOp { } struct PutStats { - contract_location: Location, - payload_size: usize, + // contract_location: Location, + // payload_size: usize, // /// (start, end) // first_response_time: Option<(Instant, Option)>, /// (start, end) @@ -596,7 +597,7 @@ pub(crate) fn start_op( ); let id = Transaction::new(::tx_type_id(), peer); - let payload_size = contract.data().len(); + // let payload_size = contract.data().len(); let state = Some(PutState::PrepareRequest { contract, value, @@ -607,8 +608,8 @@ pub(crate) fn start_op( id, state, stats: Some(PutStats { - contract_location, - payload_size, + // contract_location, + // payload_size, target: None, // first_response_time: None, transfer_time: None, diff --git a/crates/core/src/ring.rs b/crates/core/src/ring.rs index 32d5c39c8..f400a4e5a 100644 --- a/crates/core/src/ring.rs +++ b/crates/core/src/ring.rs @@ -20,6 +20,7 @@ use std::{ atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst}, Arc, }, + time::Duration, }; use anyhow::bail; @@ -29,7 +30,8 @@ use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use crate::{ - node::{self, NodeBuilder, PeerKey}, + config::GlobalExecutor, + node::{self, EventLogRegister, EventRegister, NodeBuilder, PeerKey}, router::Router, }; @@ -115,7 +117,7 @@ impl Ring { /// connection of a peer in the network). const MAX_HOPS_TO_LIVE: usize = 10; - pub fn new( + pub fn new( config: &NodeBuilder, gateways: &[PeerKeyLocation], ) -> Result { @@ -148,14 +150,15 @@ impl Ring { Self::MAX_CONNECTIONS }; - let router = Router::new(&[]); + let router = Arc::new(RwLock::new(Router::new(&[]))); + GlobalExecutor::spawn(Self::refresh_router::(router.clone())); let ring = Ring { rnd_if_htl_above, max_hops_to_live, max_connections, min_connections, - router: Arc::new(RwLock::new(router)), + router, connections_by_location: Arc::new(RwLock::new(BTreeMap::new())), location_for_peer: Arc::new(RwLock::new(BTreeMap::new())), cached_contracts: DashSet::new(), @@ -181,6 +184,21 @@ impl Ring { Ok(ring) } + async fn refresh_router(router: Arc>) { + let mut interval = tokio::time::interval(Duration::from_secs(60 * 5)); + interval.tick().await; + loop { + interval.tick().await; + let history = if std::any::type_name::() == std::any::type_name::() { + EventRegister::get_router_events(10_000).await.unwrap() + } else { + vec![] + }; + let router_ref = &mut *router.write(); + *router_ref = Router::new(&history); + } + } + #[inline(always)] /// Return if a location is within appropiate caching distance. pub fn within_caching_distance(&self, _loc: &Location) -> bool { @@ -581,7 +599,7 @@ mod test { let (_, receiver) = channel((0, peer_key)); let user_events = MemoryEventsGen::new(receiver, peer_key); let config = NodeBuilder::new([Box::new(user_events)]); - let ring = Ring::new(&config, &[]).unwrap(); + let ring = Ring::new::<1, node::TestEventListener>(&config, &[]).unwrap(); fn build_pk(loc: Location) -> PeerKeyLocation { PeerKeyLocation { diff --git a/crates/core/src/router.rs b/crates/core/src/router.rs index bfa079bc7..ce3624d44 100644 --- a/crates/core/src/router.rs +++ b/crates/core/src/router.rs @@ -4,10 +4,11 @@ mod util; use crate::ring::{Location, PeerKeyLocation}; use isotonic_estimator::{EstimatorType, IsotonicEstimator, IsotonicEvent}; -use serde::Serialize; -use std::{fmt, time::Duration}; +use serde::{Deserialize, Serialize}; +use std::time::Duration; use util::{Mean, TransferSpeed}; +// Important: Need to periodically rebuild the Router using `history` for better predictions. #[derive(Debug, Clone, Serialize)] pub(crate) struct Router { response_start_time_estimator: IsotonicEstimator, @@ -193,23 +194,23 @@ impl Router { let time_to_response_start_estimate = self .response_start_time_estimator .estimate_retrieval_time(peer, contract_location) - .map_err(|e| { - RoutingError::EstimationError(format!( - "Response Start Time Estimation failed: {}", - e - )) + .map_err(|source| RoutingError::EstimationError { + estimation: "start time", + source, })?; let failure_estimate = self .failure_estimator .estimate_retrieval_time(peer, contract_location) - .map_err(|e| { - RoutingError::EstimationError(format!("Failure Estimation failed: {}", e)) + .map_err(|source| RoutingError::EstimationError { + estimation: "failure", + source, })?; let transfer_rate_estimate = self .transfer_rate_estimator .estimate_retrieval_time(peer, contract_location) - .map_err(|e| { - RoutingError::EstimationError(format!("Transfer Rate Estimation failed: {}", e)) + .map_err(|source| RoutingError::EstimationError { + estimation: "transfer rate", + source, })?; // This is a fairly naive approach, assuming that the cost of a failure is a multiple @@ -236,19 +237,16 @@ impl Router { } } -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] enum RoutingError { + #[error("Insufficient data provided")] InsufficientDataError, - EstimationError(String), -} - -impl fmt::Display for RoutingError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - RoutingError::InsufficientDataError => write!(f, "Insufficient data provided"), - RoutingError::EstimationError(err_msg) => write!(f, "Estimation error: {}", err_msg), - } - } + #[error("failed {estimation} estimation: {source}")] + EstimationError { + estimation: &'static str, + #[source] + source: isotonic_estimator::EstimationError, + }, } #[derive(Debug, Clone, Copy, Serialize)] @@ -259,14 +257,14 @@ struct RoutingPrediction { expected_total_time: f64, } -#[derive(Debug, Clone, Copy, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct RouteEvent { pub peer: PeerKeyLocation, pub contract_location: Location, pub outcome: RouteOutcome, } -#[derive(Debug, Clone, Copy, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum RouteOutcome { Success { time_to_response_start: Duration, diff --git a/crates/core/src/router/isotonic_estimator.rs b/crates/core/src/router/isotonic_estimator.rs index 2ba5d5dd8..d987917e1 100644 --- a/crates/core/src/router/isotonic_estimator.rs +++ b/crates/core/src/router/isotonic_estimator.rs @@ -1,7 +1,7 @@ use crate::ring::{Distance, Location, PeerKeyLocation}; use pav_regression::pav::{IsotonicRegression, Point}; use serde::Serialize; -use std::{collections::HashMap, fmt}; +use std::collections::HashMap; const MIN_POINTS_FOR_REGRESSION: usize = 5; @@ -155,17 +155,10 @@ pub(super) enum EstimatorType { Negative, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, thiserror::Error)] pub(super) enum EstimationError { - InsufficientData, // Error indicating that there is not enough data for estimation -} - -impl fmt::Display for EstimationError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - EstimationError::InsufficientData => write!(f, "Insufficient data for estimation"), - } - } + #[error("Insufficient data for estimation")] + InsufficientData, } /// A routing event is a single request to a peer for a contract, and some value indicating diff --git a/crates/core/src/router/util.rs b/crates/core/src/router/util.rs index 0f78072d0..7aeca20a6 100644 --- a/crates/core/src/router/util.rs +++ b/crates/core/src/router/util.rs @@ -2,7 +2,7 @@ use std::time::Duration; use serde::Serialize; -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Copy, Serialize)] pub(super) struct Mean { sum: f64, count: u64, From 3b8a0de4fb226357698290eb680968a81553365f Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Thu, 12 Oct 2023 16:42:29 +0200 Subject: [PATCH 33/76] Use router to route requests. --- crates/core/src/operations/get.rs | 12 +++++--- crates/core/src/operations/join_ring.rs | 14 +++------- crates/core/src/operations/put.rs | 6 ++-- crates/core/src/operations/subscribe.rs | 12 +++++--- crates/core/src/ring.rs | 37 +++++++++---------------- crates/core/src/router.rs | 4 ++- 6 files changed, 39 insertions(+), 46 deletions(-) diff --git a/crates/core/src/operations/get.rs b/crates/core/src/operations/get.rs index e8227e5d0..56d03bc11 100644 --- a/crates/core/src/operations/get.rs +++ b/crates/core/src/operations/get.rs @@ -268,8 +268,12 @@ impl Operation for GetOp { } let new_htl = htl - 1; - let new_target = - op_storage.ring.closest_caching(&key, 1, &[sender.peer])[0]; + let Some(new_target) = + op_storage.ring.closest_caching(&key, &[sender.peer]) + else { + tracing::warn!("no peer found while trying getting contract {key}"); + return Err(OpError::RingError(RingError::NoCachingPeers(key))); + }; continue_seeking( conn_manager, @@ -373,7 +377,7 @@ impl Operation for GetOp { skip_list.push(target.peer); if let Some(target) = op_storage .ring - .closest_caching(&key, 1, skip_list.as_slice()) + .closest_caching(&key, skip_list.as_slice()) .into_iter() .next() { @@ -662,7 +666,7 @@ pub(crate) async fn request_get( ( op_storage .ring - .closest_caching(key, 1, &[]) + .closest_caching(key, &[]) .into_iter() .next() .ok_or(RingError::EmptyRing)?, diff --git a/crates/core/src/operations/join_ring.rs b/crates/core/src/operations/join_ring.rs index 3a88cb3ed..32415cade 100644 --- a/crates/core/src/operations/join_ring.rs +++ b/crates/core/src/operations/join_ring.rs @@ -795,14 +795,8 @@ where "Selecting close peer to forward request (requester: {})", req_peer.peer ); - ring.routing( - &new_peer_loc.location.unwrap(), - Some(&req_peer.peer), - 1, - &[], - ) - .pop() - .filter(|&pkl| pkl.peer != new_peer_loc.peer) + ring.routing(&new_peer_loc.location.unwrap(), Some(&req_peer.peer), &[]) + .and_then(|pkl| (pkl.peer != new_peer_loc.peer).then_some(pkl)) }; if let Some(forward_to) = forward_to { @@ -953,10 +947,10 @@ mod messages { } /* - + Peer A ---> Peer B (forward) ----> Peer C |----- (forward) ---------> Peer D - + Peer A ---> Peer B (forward) ----> Peer C ----> Peer D diff --git a/crates/core/src/operations/put.rs b/crates/core/src/operations/put.rs index 27f91e33c..181a1ffc6 100644 --- a/crates/core/src/operations/put.rs +++ b/crates/core/src/operations/put.rs @@ -651,7 +651,7 @@ pub(crate) async fn request_put( // - and the value to put let target = op_storage .ring - .closest_caching(&key, 1, &[sender.peer]) + .closest_caching(&key, &[sender.peer]) .into_iter() .next() .ok_or(RingError::EmptyRing)?; @@ -737,9 +737,9 @@ async fn forward_changes( { let key = contract.key(); let contract_loc = Location::from(&key); - let forward_to = op_storage.ring.closest_caching(&key, 1, skip_list); + let forward_to = op_storage.ring.closest_caching(&key, skip_list); let own_loc = op_storage.ring.own_location().location.expect("infallible"); - for peer in forward_to { + if let Some(peer) = forward_to { let other_loc = peer.location.as_ref().expect("infallible"); let other_distance = contract_loc.distance(other_loc); let self_distance = contract_loc.distance(own_loc); diff --git a/crates/core/src/operations/subscribe.rs b/crates/core/src/operations/subscribe.rs index 8bc768bfb..84b03563d 100644 --- a/crates/core/src/operations/subscribe.rs +++ b/crates/core/src/operations/subscribe.rs @@ -146,8 +146,12 @@ impl Operation for SubscribeOp { tracing::info!("Contract {} not found while processing info", key); tracing::info!("Trying to found the contract from another node"); - let new_target = - op_storage.ring.closest_caching(&key, 1, &[sender.peer])[0]; + let Some(new_target) = + op_storage.ring.closest_caching(&key, &[sender.peer]) + else { + tracing::warn!("no peer found while trying getting contract {key}"); + return Err(OpError::RingError(RingError::NoCachingPeers(key))); + }; let new_htl = htl + 1; if new_htl > MAX_RETRIES { @@ -220,7 +224,7 @@ impl Operation for SubscribeOp { skip_list.push(sender.peer); if let Some(target) = op_storage .ring - .closest_caching(&key, 1, skip_list.as_slice()) + .closest_caching(&key, skip_list.as_slice()) .into_iter() .next() { @@ -337,7 +341,7 @@ pub(crate) async fn request_subscribe( ( op_storage .ring - .closest_caching(key, 1, &[]) + .closest_caching(key, &[]) .into_iter() .next() .ok_or(RingError::EmptyRing)?, diff --git a/crates/core/src/ring.rs b/crates/core/src/ring.rs index f400a4e5a..bd31be024 100644 --- a/crates/core/src/ring.rs +++ b/crates/core/src/ring.rs @@ -304,30 +304,25 @@ impl Ring { Some(conn_by_dist[idx]) } - /// Return the closest peers to a contract location which are caching it, - /// excluding whichever peers in the skip list. + /// Return the most optimal peer caching a given contract. #[inline] pub fn closest_caching( &self, contract_key: &ContractKey, - n: usize, skip_list: &[PeerKey], - ) -> Vec { - // Right now we return just the closest known peers to that location. - // In the future this may change to the ones closest which are actually already caching it. - self.routing(&Location::from(contract_key), None, n, skip_list) + ) -> Option { + self.routing(&Location::from(contract_key), None, skip_list) } - /// Find the closest number of peers to a given location. Result is returned sorted by proximity. + /// Route an op to the most optimal target. pub fn routing( &self, target: &Location, requesting: Option<&PeerKey>, - n: usize, skip_list: &[PeerKey], - ) -> Vec { + ) -> Option { let connections = self.connections_by_location.read(); - let mut conn_by_dist: Vec<_> = connections + let peers = connections .iter() .filter(|(_, pkloc)| { if let Some(requester) = requesting { @@ -337,11 +332,9 @@ impl Ring { } !skip_list.contains(&pkloc.peer) }) - .map(|(loc, peer)| (loc.distance(target), (loc, peer))) - .collect(); - conn_by_dist.sort_by_key(|&(dist, _)| dist); - let iter = conn_by_dist.into_iter().map(|(_, v)| *v.1).take(n); - iter.collect() + .map(|(_, peer)| peer); + let router = &*self.router.read(); + router.select_peer(peers, target).cloned() } pub fn routing_finished(&self, event: crate::router::RouteEvent) { @@ -617,32 +610,28 @@ mod test { assert_eq!( Location(0.0), - ring.routing(&Location(0.9), None, 1, &[]) - .first() + ring.routing(&Location(0.9), None, &[]) .unwrap() .location .unwrap() ); assert_eq!( Location(0.0), - ring.routing(&Location(0.1), None, 1, &[]) - .first() + ring.routing(&Location(0.1), None, &[]) .unwrap() .location .unwrap() ); assert_eq!( Location(0.5), - ring.routing(&Location(0.41), None, 1, &[]) - .first() + ring.routing(&Location(0.41), None, &[]) .unwrap() .location .unwrap() ); assert_eq!( Location(0.3), - ring.routing(&Location(0.39), None, 1, &[]) - .first() + ring.routing(&Location(0.39), None, &[]) .unwrap() .location .unwrap() diff --git a/crates/core/src/router.rs b/crates/core/src/router.rs index ce3624d44..1662169dc 100644 --- a/crates/core/src/router.rs +++ b/crates/core/src/router.rs @@ -8,7 +8,9 @@ use serde::{Deserialize, Serialize}; use std::time::Duration; use util::{Mean, TransferSpeed}; -// Important: Need to periodically rebuild the Router using `history` for better predictions. +/// # Usage +/// Important when using this type: +/// Need to periodically rebuild the Router using `history` for better predictions. #[derive(Debug, Clone, Serialize)] pub(crate) struct Router { response_start_time_estimator: IsotonicEstimator, From e7a1194d5f7ad194738328311f0969cd8938468f Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Thu, 12 Oct 2023 17:25:23 +0200 Subject: [PATCH 34/76] Truncate record when maxed --- crates/core/src/node/event_log.rs | 89 ++++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 6 deletions(-) diff --git a/crates/core/src/node/event_log.rs b/crates/core/src/node/event_log.rs index 479927890..47f08d930 100644 --- a/crates/core/src/node/event_log.rs +++ b/crates/core/src/node/event_log.rs @@ -1,3 +1,5 @@ +use std::{io, path::Path}; + use freenet_stdlib::prelude::*; use futures::{future::BoxFuture, FutureExt}; use serde::{Deserialize, Serialize}; @@ -140,32 +142,107 @@ impl EventRegister { } async fn record_logs(mut log_recv: mpsc::Receiver) { + const MAX_LOG_RECORDS: usize = 100_000; + + async fn num_lines(path: &Path) -> io::Result { + use tokio::fs::File; + use tokio::io::{AsyncBufReadExt, BufReader}; + + let file = File::open(path).await.expect("Failed to open log file"); + let reader = BufReader::new(file); + let mut num_lines = 0; + let mut lines = reader.lines(); + while lines.next_line().await?.is_some() { + num_lines += 1; + } + Ok(num_lines) + } + + async fn truncate_lines( + file: &mut tokio::fs::File, + lines_to_keep: usize, + ) -> Result<(), Box> { + use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncSeekExt}; + + file.seek(io::SeekFrom::Start(0)).await?; + let file_metadata = file.metadata().await?; + let file_size = file_metadata.len(); + let mut reader = tokio::io::BufReader::new(file); + + let mut buffer = Vec::with_capacity(file_size as usize); + let mut lines_count = 0; + + let mut line = Vec::new(); + let mut discard_bytes = 0; + + while lines_count < lines_to_keep { + let bytes_read = reader.read_until(b'\n', &mut line).await?; + if bytes_read == 0 { + // EOF + break; + } + lines_count += 1; + discard_bytes += bytes_read; + line.clear(); + } + + // Copy the rest of the file to the buffer + while let Ok(bytes_read) = reader.read_buf(&mut buffer).await { + if bytes_read == 0 { + // EOF + break; + } + } + + // Seek back to the beginning and write the remaining content let file = reader.into_inner(); + let file = reader.into_inner(); + file.seek(io::SeekFrom::Start(0)).await?; + file.write_all(&buffer).await?; + + // Truncate the file to the new size + file.set_len(file_size - discard_bytes as u64).await?; + file.seek(io::SeekFrom::End(0)).await?; + Ok(()) + } + use tokio::io::AsyncWriteExt; let event_log_path = crate::config::Config::get_static_conf().event_log(); let mut event_log = match OpenOptions::new().write(true).open(&event_log_path).await { Ok(file) => file, Err(err) => { - tracing::error!("failed openning log file {:?} with: {err}", event_log_path); - panic!("failed openning log file"); // todo: propagate this to the main thread + tracing::error!("Failed openning log file {:?} with: {err}", event_log_path); + panic!("Failed openning log file"); // todo: propagate this to the main thread } }; let mut num_written = 0; let mut buf = vec![]; while let Some(log) = log_recv.recv().await { if let Err(err) = bincode::serialize_into(&mut buf, &log) { - tracing::error!("failed serializing log: {err}"); - panic!("failed serializing log"); + tracing::error!("Failed serializing log: {err}"); + panic!("Failed serializing log"); } buf.push(b'\n'); num_written += 1; if num_written == 100 { if let Err(err) = event_log.write_all(&buf).await { - tracing::error!("failed writting to event log: {err}"); - panic!("failed writting event log"); + tracing::error!("Failed writting to event log: {err}"); + panic!("Failed writting event log"); } num_written = 0; buf.clear(); } + + // Check the number of lines and truncate if needed + let num_lines = num_lines(event_log_path.as_path()) + .await + .expect("non IO error"); + if num_lines > MAX_LOG_RECORDS { + let truncate_to = num_lines - MAX_LOG_RECORDS + 900; // make some extra space removing 1000 old records + if let Err(err) = truncate_lines(&mut event_log, truncate_to).await { + tracing::error!("Failed truncating log file: {:?}", err); + panic!("Failed truncating log file"); + } + } } } From ebe304e0d3e44af81a039d4c2412bc397818d528 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Fri, 13 Oct 2023 08:58:35 +0200 Subject: [PATCH 35/76] Don't load old records Don't block while writing to log file --- crates/core/src/config.rs | 4 +- crates/core/src/contract/executor.rs | 2 +- crates/core/src/contract/in_memory.rs | 2 +- crates/core/src/contract/storages/rocks_db.rs | 2 +- crates/core/src/contract/storages/sqlite.rs | 2 +- crates/core/src/node.rs | 2 +- crates/core/src/node/event_log.rs | 70 +++++++++++++++---- crates/fdev/src/commands.rs | 6 +- crates/fdev/src/local_node/state.rs | 2 +- 9 files changed, 68 insertions(+), 24 deletions(-) diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index f8b8155dd..8a36a3b1e 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -149,7 +149,7 @@ impl Config { pub fn set_op_mode(mode: OperationMode) { let local_mode = matches!(mode, OperationMode::Local); - Self::get_static_conf() + Self::conf() .local_mode .store(local_mode, std::sync::atomic::Ordering::SeqCst); } @@ -196,7 +196,7 @@ impl Config { } } - pub fn get_static_conf() -> &'static Config { + pub fn conf() -> &'static Config { CONFIG.get_or_init(|| match Config::load_conf() { Ok(config) => config, Err(err) => { diff --git a/crates/core/src/contract/executor.rs b/crates/core/src/contract/executor.rs index b6a86baf2..a708ca866 100644 --- a/crates/core/src/contract/executor.rs +++ b/crates/core/src/contract/executor.rs @@ -342,7 +342,7 @@ impl Executor { pub async fn from_config(config: NodeConfig) -> Result { const MAX_SIZE: i64 = 10 * 1024 * 1024; const MAX_MEM_CACHE: u32 = 10_000_000; - let static_conf = crate::config::Config::get_static_conf(); + let static_conf = crate::config::Config::conf(); let state_store = StateStore::new(Storage::new().await?, MAX_MEM_CACHE).unwrap(); diff --git a/crates/core/src/contract/in_memory.rs b/crates/core/src/contract/in_memory.rs index 58fd9a5db..f390c7625 100644 --- a/crates/core/src/contract/in_memory.rs +++ b/crates/core/src/contract/in_memory.rs @@ -46,7 +46,7 @@ where _kv_store: StateStore::new(kv_store, 10_000_000).unwrap(), _runtime: MockRuntime { contract_store: ContractStore::new( - Config::get_static_conf().contracts_dir(), + Config::conf().contracts_dir(), Self::MAX_MEM_CACHE, ) .unwrap(), diff --git a/crates/core/src/contract/storages/rocks_db.rs b/crates/core/src/contract/storages/rocks_db.rs index 44d66de43..90850728f 100644 --- a/crates/core/src/contract/storages/rocks_db.rs +++ b/crates/core/src/contract/storages/rocks_db.rs @@ -9,7 +9,7 @@ pub struct RocksDb(DB); impl RocksDb { #[cfg_attr(feature = "sqlite", allow(unused))] pub async fn new() -> Result { - let path = Config::get_static_conf().db_dir().join("freenet.db"); + let path = Config::conf().db_dir().join("freenet.db"); tracing::info!("loading contract store from {path:?}"); let mut opts = Options::default(); diff --git a/crates/core/src/contract/storages/sqlite.rs b/crates/core/src/contract/storages/sqlite.rs index ec8bfbce5..253181b5f 100644 --- a/crates/core/src/contract/storages/sqlite.rs +++ b/crates/core/src/contract/storages/sqlite.rs @@ -18,7 +18,7 @@ static POOL: Lazy = Lazy::new(|| { let opts = if cfg!(test) { SqliteConnectOptions::from_str("sqlite::memory:").unwrap() } else { - let conn_str = Config::get_static_conf().db_dir().join("freenet.db"); + let conn_str = Config::conf().db_dir().join("freenet.db"); tracing::info!("loading contract store from {conn_str:?}"); SqliteConnectOptions::new() .create_if_missing(true) diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index a690b41a6..aa71a80ec 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -115,7 +115,7 @@ pub struct NodeBuilder { impl NodeBuilder { pub fn new(clients: [BoxedClient; CLIENTS]) -> NodeBuilder { - let local_key = if let Some(key) = &Config::get_static_conf().local_peer_keypair { + let local_key = if let Some(key) = &Config::conf().local_peer_keypair { key.clone() } else { identity::Keypair::generate_ed25519() diff --git a/crates/core/src/node/event_log.rs b/crates/core/src/node/event_log.rs index 47f08d930..e7d6bcee7 100644 --- a/crates/core/src/node/event_log.rs +++ b/crates/core/src/node/event_log.rs @@ -1,5 +1,6 @@ -use std::{io, path::Path}; +use std::{io, path::Path, time::SystemTime}; +use chrono::{DateTime, Utc}; use freenet_stdlib::prelude::*; use futures::{future::BoxFuture, FutureExt}; use serde::{Deserialize, Serialize}; @@ -125,6 +126,7 @@ impl<'a> EventLog<'a> { #[derive(Serialize, Deserialize)] struct LogMessage { tx: Transaction, + datetime: DateTime, peer_id: PeerKey, kind: EventKind, } @@ -134,15 +136,20 @@ pub(crate) struct EventRegister { log_sender: mpsc::Sender, } +/// Records from a new session must have higher than this ts. +static NEW_RECORDS_TS: std::sync::OnceLock = std::sync::OnceLock::new(); + impl EventRegister { pub fn new() -> Self { let (log_sender, log_recv) = mpsc::channel(1000); + NEW_RECORDS_TS.set(SystemTime::now()).expect("non set"); GlobalExecutor::spawn(Self::record_logs(log_recv)); Self { log_sender } } async fn record_logs(mut log_recv: mpsc::Receiver) { const MAX_LOG_RECORDS: usize = 100_000; + const BATCH_SIZE: usize = 100; async fn num_lines(path: &Path) -> io::Result { use tokio::fs::File; @@ -206,7 +213,7 @@ impl EventRegister { } use tokio::io::AsyncWriteExt; - let event_log_path = crate::config::Config::get_static_conf().event_log(); + let event_log_path = crate::config::Config::conf().event_log(); let mut event_log = match OpenOptions::new().write(true).open(&event_log_path).await { Ok(file) => file, Err(err) => { @@ -215,21 +222,47 @@ impl EventRegister { } }; let mut num_written = 0; - let mut buf = vec![]; + let mut batch_buf = Vec::with_capacity(BATCH_SIZE * 1024); + let mut log_batch = Vec::with_capacity(BATCH_SIZE); while let Some(log) = log_recv.recv().await { - if let Err(err) = bincode::serialize_into(&mut buf, &log) { - tracing::error!("Failed serializing log: {err}"); - panic!("Failed serializing log"); + log_batch.push(log); + + if log_batch.len() >= BATCH_SIZE { + let moved_batch = std::mem::replace(&mut log_batch, Vec::with_capacity(BATCH_SIZE)); + let serialization_task = tokio::task::spawn_blocking(move || { + let mut batch_serialized_data = Vec::new(); + for log_item in &moved_batch { + if let Err(err) = + bincode::serialize_into(&mut batch_serialized_data, log_item) + { + // Handle the error appropriately + tracing::error!("Failed serializing log: {err}"); + return Err(err); + } + batch_serialized_data.push(b'\n'); + } + Ok(batch_serialized_data) + }); + + match serialization_task.await { + Ok(Ok(mut serialized_data)) => { + batch_buf.append(&mut serialized_data); + num_written += log_batch.len(); + log_batch.clear(); // Clear the batch for new data + } + _ => { + panic!("Failed serializing log"); + } + } } - buf.push(b'\n'); - num_written += 1; - if num_written == 100 { - if let Err(err) = event_log.write_all(&buf).await { + + if num_written >= BATCH_SIZE { + if let Err(err) = event_log.write_all(&batch_buf).await { tracing::error!("Failed writting to event log: {err}"); panic!("Failed writting event log"); } num_written = 0; - buf.clear(); + batch_buf.clear(); } // Check the number of lines and truncate if needed @@ -252,7 +285,7 @@ impl EventRegister { const MAX_EVENT_HISTORY: usize = 10_000; let event_num = max_event_number.min(MAX_EVENT_HISTORY); - let event_log_path = crate::config::Config::get_static_conf().event_log(); + let event_log_path = crate::config::Config::conf().event_log(); let mut event_log = OpenOptions::new().read(true).open(event_log_path).await?; let mut buf = [0; BUF_SIZE]; @@ -260,6 +293,12 @@ impl EventRegister { let mut partial_record = vec![]; let mut record_start = 0; + let new_records_ts = NEW_RECORDS_TS + .get() + .expect("set on initialization") + .duration_since(std::time::UNIX_EPOCH) + .expect("should be older than unix epoch") + .as_secs() as i64; while records.len() < event_num { let bytes_read = event_log.read(&mut buf).await?; if bytes_read == 0 { @@ -281,7 +320,10 @@ impl EventRegister { rec }; if let EventKind::Route(outcome) = deser_record.kind { - records.push(outcome); + let record_ts = deser_record.datetime.timestamp(); + if record_ts > new_records_ts { + records.push(outcome); + } } } if records.len() == event_num { @@ -299,6 +341,7 @@ impl EventRegister { impl EventLogRegister for EventRegister { fn event_received<'a>(&'a mut self, log: EventLog) -> BoxFuture<'a, Result<(), DynError>> { let log_msg = LogMessage { + datetime: Utc::now(), tx: *log.tx, kind: log.kind, peer_id: *log.peer_id, @@ -502,6 +545,7 @@ pub(super) mod test_utils { let log_id = ListenerLogId(LOG_ID.fetch_add(1, SeqCst)); let EventLog { peer_id, kind, .. } = log; let msg_log = LogMessage { + datetime: Utc::now(), tx: *log.tx, peer_id: *peer_id, kind, diff --git a/crates/fdev/src/commands.rs b/crates/fdev/src/commands.rs index 957b24e51..2043ca7b9 100644 --- a/crates/fdev/src/commands.rs +++ b/crates/fdev/src/commands.rs @@ -156,13 +156,13 @@ async fn execute_command( ) -> Result<(), DynError> { let contracts_data_path = other .contract_data_dir - .unwrap_or_else(|| Config::get_static_conf().contracts_dir()); + .unwrap_or_else(|| Config::conf().contracts_dir()); let delegates_data_path = other .delegate_data_dir - .unwrap_or_else(|| Config::get_static_conf().delegates_dir()); + .unwrap_or_else(|| Config::conf().delegates_dir()); let secrets_data_path = other .secret_data_dir - .unwrap_or_else(|| Config::get_static_conf().secrets_dir()); + .unwrap_or_else(|| Config::conf().secrets_dir()); let contract_store = ContractStore::new(contracts_data_path, DEFAULT_MAX_CONTRACT_SIZE)?; let delegate_store = DelegateStore::new(delegates_data_path, DEFAULT_MAX_DELEGATE_SIZE)?; diff --git a/crates/fdev/src/local_node/state.rs b/crates/fdev/src/local_node/state.rs index 4501ccc0f..0ded9ce22 100644 --- a/crates/fdev/src/local_node/state.rs +++ b/crates/fdev/src/local_node/state.rs @@ -21,7 +21,7 @@ impl AppState { const MAX_MEM_CACHE: u32 = 10_000_000; pub async fn new(config: &LocalNodeCliConfig) -> Result { - let contract_dir = Config::get_static_conf().contracts_dir(); + let contract_dir = Config::conf().contracts_dir(); let contract_store = ContractStore::new(contract_dir, config.max_contract_size)?; let state_store = StateStore::new(Storage::new().await?, Self::MAX_MEM_CACHE).unwrap(); Ok(AppState { From 4d1202da953b87512eb2ecc25f11b8fe8d790fe1 Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Wed, 18 Oct 2023 18:52:20 -0500 Subject: [PATCH 36/76] top: sim testing --- crates/core/src/topology/simulation/mod.rs | 65 ++++++++++++++------ crates/core/src/topology/simulation/tests.rs | 27 +++++--- 2 files changed, 64 insertions(+), 28 deletions(-) diff --git a/crates/core/src/topology/simulation/mod.rs b/crates/core/src/topology/simulation/mod.rs index 484c3fb24..80fc863ca 100644 --- a/crates/core/src/topology/simulation/mod.rs +++ b/crates/core/src/topology/simulation/mod.rs @@ -1,15 +1,42 @@ mod event_stat_tracker; +mod tests; use crate::ring::Distance; use super::*; use event_stat_tracker::EventStatTracker; +use std::collections::{HashMap, HashSet}; +use tracing::{debug, info, warn}; +#[derive(Debug)] +struct Connections(HashMap>); + +impl Connections { + fn new() -> Self { + Self(HashMap::new()) + } + + fn connect(&mut self, a: NodeRef, b: NodeRef) { + // throw an error if a == b + assert!(a != b, "Cannot connect a node to itself"); + + info!("Connecting {:?} and {:?}", a, b); + self.0.entry(a).or_default().insert(b); + self.0.entry(b).or_default().insert(a); + } + + fn disconnect(&mut self, a: NodeRef, b: NodeRef) { + info!("Disconnecting {:?} and {:?}", a, b); + self.0.entry(a).or_default().remove(&b); + self.0.entry(b).or_default().remove(&a); + } + +} #[derive(Debug)] struct SimulatedNetwork { current_time: u64, nodes: Vec, - connections: HashMap>, + connections: Connections, requests: HashMap)>, // origin node -> destination node -> requests } @@ -22,11 +49,13 @@ impl SimulatedNetwork { Self { current_time: 0, nodes: Vec::new(), - connections: HashMap::new(), + connections: Connections::new(), requests: HashMap::new(), } } + pub(crate) + fn add_node(&mut self) -> NodeRef { let index = self.nodes.len(); let node = SimulatedNode { @@ -38,21 +67,6 @@ impl SimulatedNetwork { NodeRef { index } } - fn connect(&mut self, a: NodeRef, b: NodeRef) { - // throw an error if a == b - assert!(a != b, "Cannot connect a node to itself"); - - info!("Connecting {:?} and {:?}", a, b); - self.connections.entry(a).or_default().insert(b); - self.connections.entry(b).or_default().insert(a); - } - - fn disconnect(&mut self, a: NodeRef, b: NodeRef) { - info!("Disconnecting {:?} and {:?}", a, b); - self.connections.entry(a).or_default().remove(&b); - self.connections.entry(b).or_default().remove(&a); - } - fn route(&self, source: &NodeRef, destination: Location) -> Result, RouteError> { info!("Routing from {:?} to {:?}", source, destination); let mut current = *source; // Dereference to copy @@ -76,7 +90,7 @@ impl SimulatedNetwork { debug!("Current distance to destination: {}", current_distance); // Find the closest connected node to the destination that hasn't been visited - let closest_connections = match self.connections.get(¤t) { + let closest_connections = match self.connections.0.get(¤t) { Some(connections) => connections, None => { // Handle the None case here. For example, you might want to return agit n Err value or break the loop. @@ -175,9 +189,22 @@ impl SimulatedNetwork { }; for joiner in joiners.iter() { - self.connect(node, *joiner); + self.connections.connect(node, *joiner); } } + + pub(crate) fn add_and_assimilate_node(&mut self, strategy: TopologyStrategy, peer_statistics: &PeerStatistics) -> NodeRef { + let new_node = self.add_node(); + + let my_location = &self.nodes[new_node.index].location; + + let JoinTargetInfo { target, threshold, .. } = strategy.select_join_target_location(my_location, peer_statistics); + + self.join(new_node, target, threshold); + + new_node + } + } #[derive(Debug)] diff --git a/crates/core/src/topology/simulation/tests.rs b/crates/core/src/topology/simulation/tests.rs index b8a057704..53d3f5e1a 100644 --- a/crates/core/src/topology/simulation/tests.rs +++ b/crates/core/src/topology/simulation/tests.rs @@ -1,4 +1,4 @@ -use super::*; + fn setup() { // Initialize the tracer @@ -7,6 +7,14 @@ fn setup() { .try_init() .unwrap_or(()); } + +#[test] +fn it_ticks_the_network() { + let mut network = SimulatedNetwork::new(); + network.tick(); + assert_eq!(network.current_time, 1); +} + #[test] fn test_add_node() { setup(); @@ -22,9 +30,9 @@ fn test_connect() { let mut net = SimulatedNetwork::new(); let a = net.add_node(); let b = net.add_node(); - net.connect(a, b); - assert!(net.connections.get(&a).unwrap().contains(&b)); - assert!(net.connections.get(&b).unwrap().contains(&a)); + net.connections.connect(a, b); + assert!(net.connections.0.get(&a).unwrap().contains(&b)); + assert!(net.connections.0.get(&b).unwrap().contains(&a)); } #[test] @@ -33,10 +41,10 @@ fn test_disconnect() { let mut net = SimulatedNetwork::new(); let a = net.add_node(); let b = net.add_node(); - net.connect(a, b); - net.disconnect(a, b); - assert!(!net.connections.get(&a).unwrap().contains(&b)); - assert!(!net.connections.get(&b).unwrap().contains(&a)); + net.connections.connect(a, b); + net.connections.disconnect(a, b); + assert!(!net.connections.0.get(&a).unwrap().contains(&b)); + assert!(!net.connections.0.get(&b).unwrap().contains(&a)); } #[test] @@ -45,7 +53,7 @@ fn test_route_success() { let mut net = SimulatedNetwork::new(); let a = net.add_node(); let b = net.add_node(); - net.connect(a, b); + net.connections.connect(a, b); // Mock a destination location that's the same as node b's location let destination = net.nodes[b.index].location; @@ -131,3 +139,4 @@ fn test_join_with_tolerance() { assert!(join_result.is_some()); assert_eq!(join_result.unwrap(), vec![b]); } + From 312218716aa4a9e4fbf9361cc98e49dba06f8509 Mon Sep 17 00:00:00 2001 From: KristijanZic Date: Mon, 16 Oct 2023 01:07:11 +0200 Subject: [PATCH 37/76] Bump up freenet version to "0.0.6" for freenet-token-generator --- modules/antiflood-tokens/delegates/token-generator/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/antiflood-tokens/delegates/token-generator/Cargo.toml b/modules/antiflood-tokens/delegates/token-generator/Cargo.toml index 51e50fe2f..0545452ef 100644 --- a/modules/antiflood-tokens/delegates/token-generator/Cargo.toml +++ b/modules/antiflood-tokens/delegates/token-generator/Cargo.toml @@ -18,7 +18,7 @@ chrono = { workspace = true, features = ["alloc", "serde"] } freenet-stdlib = { workspace = true, features = ["contract"] } [target.'cfg(not(target_family = "wasm"))'.dependencies] -chrono = { workspace = true, features = ["clock", "alloc", "serde"]} +chrono = { workspace = true, features = ["clock", "alloc", "serde"] } [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] arbitrary = "1" @@ -27,7 +27,7 @@ chacha20poly1305 = "0.10" rand = { version = "0.8", features = ["std"] } rand_chacha = { version = "0.3" } freenet-stdlib = { workspace = true, features = ["testing"] } -freenet = { path = "../../../../crates/core", version = "0.0.5" } +freenet = { path = "../../../../crates/core", version = "0.0.6" } tracing-subscriber = { version = "0.3.16", features = ["env-filter", "fmt"] } [lib] From 5a319457dd29bcbd210f51f7b93f23cebe83aa4c Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Mon, 16 Oct 2023 09:51:57 +0200 Subject: [PATCH 38/76] Properly concantenate compiler features --- crates/fdev/src/build.rs | 43 ++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/crates/fdev/src/build.rs b/crates/fdev/src/build.rs index a02b90949..a8c491d43 100644 --- a/crates/fdev/src/build.rs +++ b/crates/fdev/src/build.rs @@ -28,38 +28,61 @@ pub fn build_package(cli_config: BuildToolCliConfig, cwd: &Path) -> Result<(), D } } -fn compile_options(cli_config: &BuildToolCliConfig) -> impl Iterator { +fn compile_options(cli_config: &BuildToolCliConfig) -> impl Iterator { let release: &[&str] = if cli_config.debug { &[] } else { &["--release"] }; - let feature_list = cli_config.features.iter().flat_map(|s| { - s.split(',') - .filter(|p| *p != cli_config.package_type.feature()) - }); - let features = ["--features", cli_config.package_type.feature()] + let feature_list = cli_config + .features + .iter() + .flat_map(|s| { + s.split(',') + .filter(|p| *p != cli_config.package_type.feature()) + }) + .chain([cli_config.package_type.feature()]); + let features = [ + "--features".to_string(), + feature_list.collect::>().join(","), + ]; + features .into_iter() - .chain(feature_list); - features.chain(release.iter().copied()) + .chain(release.iter().map(|s| s.to_string())) +} + +#[test] +fn test_get_compile_options() { + let config = BuildToolCliConfig { + features: Some("contract".into()), + version: semver::Version::new(0, 0, 1), + package_type: PackageType::Contract, + debug: false, + }; + let opts: Vec<_> = compile_options(&config).collect(); + assert_eq!( + opts, + vec!["--features", "contract,freenet-main-contract", "--release"] + ); } fn compile_rust_wasm_lib(cli_config: &BuildToolCliConfig, work_dir: &Path) -> Result<(), DynError> { const RUST_TARGET_ARGS: &[&str] = &["build", "--lib", "--target"]; use std::io::IsTerminal; + let comp_opts = compile_options(cli_config).collect::>(); let cmd_args = if std::io::stdout().is_terminal() && std::io::stderr().is_terminal() { RUST_TARGET_ARGS .iter() .copied() .chain([WASM_TARGET, "--color", "always"]) - .chain(compile_options(cli_config)) + .chain(comp_opts.iter().map(|s| s.as_str())) .collect::>() } else { RUST_TARGET_ARGS .iter() .copied() .chain([WASM_TARGET]) - .chain(compile_options(cli_config)) + .chain(comp_opts.iter().map(|s| s.as_str())) .collect::>() }; From d2b2c1fd5b0a3169de151a661cf70bed58319b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristijan=20=C5=BDic?= Date: Mon, 16 Oct 2023 13:25:06 +0200 Subject: [PATCH 39/76] Rename fdev strings from 'Locutus' to 'Freenet' (#871) * Rename fdev clap name from 'Locutus' to 'Freenet' * Finish renaming all fdev strings from "Locutus" to "Freenet" * Bump up fdev version to 0.0.6 --- crates/fdev/src/config.rs | 12 ++++++------ crates/fdev/src/inspect.rs | 2 +- crates/fdev/src/local_node.rs | 4 ++-- crates/fdev/src/local_node/user_events.rs | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/fdev/src/config.rs b/crates/fdev/src/config.rs index f7dee00ad..ae51df979 100644 --- a/crates/fdev/src/config.rs +++ b/crates/fdev/src/config.rs @@ -6,9 +6,9 @@ use freenet::dev_tool::OperationMode; use semver::Version; #[derive(clap::Parser, Clone)] -#[clap(name = "Locutus Development Tool")] +#[clap(name = "Freenet Development Tool")] #[clap(author = "The Freenet Project Inc.")] -#[clap(version = "0.0.3")] +#[clap(version = "0.0.6")] pub struct Config { #[clap(subcommand)] pub sub_command: SubCommand, @@ -18,13 +18,13 @@ pub struct Config { #[derive(clap::Parser, Clone)] pub struct BaseConfig { - /// Overrides the default data directory where Locutus contract files are stored. + /// Overrides the default data directory where Freenet contract files are stored. #[arg(long)] pub(crate) contract_data_dir: Option, - /// Overrides the default data directory where Locutus delegate files are stored. + /// Overrides the default data directory where Freenet delegate files are stored. #[arg(long)] pub(crate) delegate_data_dir: Option, - /// Overrides the default data directory where Locutus secret files are stored. + /// Overrides the default data directory where Freenet secret files are stored. #[arg(long)] pub(crate) secret_data_dir: Option, /// Node operation mode. @@ -154,7 +154,7 @@ fn parse_version(src: &str) -> Result { Version::parse(src).map_err(|e| e.to_string()) } -/// Create a new Locutus contract and/or app. +/// Create a new Freenet contract and/or app. #[derive(clap::Parser, Clone)] pub struct NewPackageCliConfig { #[arg(id = "type", value_enum)] diff --git a/crates/fdev/src/inspect.rs b/crates/fdev/src/inspect.rs index bec5adee1..7da43cab1 100644 --- a/crates/fdev/src/inspect.rs +++ b/crates/fdev/src/inspect.rs @@ -19,7 +19,7 @@ enum FileType { Contract, } -/// Inspect the packaged WASM code for Locutus. +/// Inspect the packaged WASM code for Freenet. #[derive(clap::Parser, Clone)] struct CodeInspection {} diff --git a/crates/fdev/src/local_node.rs b/crates/fdev/src/local_node.rs index 98b8c36aa..d7ccaa88c 100644 --- a/crates/fdev/src/local_node.rs +++ b/crates/fdev/src/local_node.rs @@ -42,9 +42,9 @@ pub enum DeserializationFmt { MessagePack, } -/// A CLI utility for testing out contracts against a Locutus local node. +/// A CLI utility for testing out contracts against a Freenet local node. #[derive(clap::Parser, Clone)] -#[clap(name = "Locutus Local Development Node Environment")] +#[clap(name = "Freenet Local Development Node Environment")] #[clap(author = "The Freenet Project Inc.")] #[clap(group( ArgGroup::new("output") diff --git a/crates/fdev/src/local_node/user_events.rs b/crates/fdev/src/local_node/user_events.rs index 2767cf3c4..044bf420c 100644 --- a/crates/fdev/src/local_node/user_events.rs +++ b/crates/fdev/src/local_node/user_events.rs @@ -19,7 +19,7 @@ use crate::{util, CommandSender, DynError}; use super::{state::AppState, DeserializationFmt, LocalNodeCliConfig}; -const HELP: &str = "Locutus Contract Development Environment +const HELP: &str = "Freenet Contract Development Environment SUBCOMMANDS: help Print this message From 3fc1c8b1dea14725119fbf6216125cb2cb4c8a07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristijan=20=C5=BDic?= Date: Wed, 18 Oct 2023 15:01:43 +0200 Subject: [PATCH 40/76] Rename fdev variable prefixes from "locutus" to "freenet" (#873) --- crates/fdev/src/commands.rs | 2 +- crates/fdev/src/local_node.rs | 2 +- crates/fdev/src/new_package.rs | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/fdev/src/commands.rs b/crates/fdev/src/commands.rs index 2043ca7b9..a61a0c508 100644 --- a/crates/fdev/src/commands.rs +++ b/crates/fdev/src/commands.rs @@ -8,7 +8,7 @@ use freenet_stdlib::{ client_api::{ClientRequest, ContractRequest, DelegateRequest}, prelude::*, }; -// use locutus_runtime::{ +// use freenet_runtime::{ // ContractContainer, ContractInstanceId, ContractStore, DelegateContainer, DelegateStore, // Parameters, SecretsStore, StateStore, // }; diff --git a/crates/fdev/src/local_node.rs b/crates/fdev/src/local_node.rs index d7ccaa88c..131c8ec86 100644 --- a/crates/fdev/src/local_node.rs +++ b/crates/fdev/src/local_node.rs @@ -83,6 +83,6 @@ pub struct LocalNodeCliConfig { #[clap(long, requires = "fmt")] pub(crate) terminal_output: bool, /// Max contract size - #[clap(long, env = "LOCUTUS_MAX_CONTRACT_SIZE", default_value_t = DEFAULT_MAX_CONTRACT_SIZE)] + #[clap(long, env = "FREENET_MAX_CONTRACT_SIZE", default_value_t = DEFAULT_MAX_CONTRACT_SIZE)] pub(crate) max_contract_size: i64, } diff --git a/crates/fdev/src/new_package.rs b/crates/fdev/src/new_package.rs index 00364e3cc..e708dc7fc 100644 --- a/crates/fdev/src/new_package.rs +++ b/crates/fdev/src/new_package.rs @@ -25,7 +25,7 @@ pub fn create_new_package(config: NewPackageCliConfig) -> Result<(), DynError> { fn create_view_package(cwd: &Path) -> Result<(), DynError> { create_rust_crate(cwd, ContractKind::WebApp)?; create_web_init_files(cwd)?; - let locutus_file_config = BuildToolConfig { + let freenet_file_config = BuildToolConfig { contract: Contract { c_type: Some(ContractType::WebApp), lang: Some(SupportedContractLangs::Rust), @@ -43,7 +43,7 @@ fn create_view_package(cwd: &Path) -> Result<(), DynError> { }), state: None, }; - let serialized = toml::to_string(&locutus_file_config)?.into_bytes(); + let serialized = toml::to_string(&freenet_file_config)?.into_bytes(); let path = cwd.join("freenet").with_extension("toml"); let mut file = File::create(path)?; file.write_all(&serialized)?; @@ -52,7 +52,7 @@ fn create_view_package(cwd: &Path) -> Result<(), DynError> { fn create_regular_contract(cwd: &Path) -> Result<(), DynError> { create_rust_crate(cwd, ContractKind::Contract)?; - let locutus_file_config = BuildToolConfig { + let freenet_file_config = BuildToolConfig { contract: Contract { c_type: Some(ContractType::Standard), lang: Some(SupportedContractLangs::Rust), @@ -61,7 +61,7 @@ fn create_regular_contract(cwd: &Path) -> Result<(), DynError> { webapp: None, state: None, }; - let serialized = toml::to_string(&locutus_file_config)?.into_bytes(); + let serialized = toml::to_string(&freenet_file_config)?.into_bytes(); let path = cwd.join("freenet").with_extension("toml"); let mut file = File::create(path)?; file.write_all(&serialized)?; From c6ca732157546621544c4a3328ba0cd79ebbba4c Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Thu, 19 Oct 2023 12:45:13 -0500 Subject: [PATCH 41/76] top: add connection connection evaluator --- .../core/src/topology/connection_evaluator.rs | 89 +++++++ crates/core/src/topology/mod.rs | 8 +- crates/core/src/topology/simulation/mod.rs | 226 ------------------ crates/core/src/topology/simulation/tests.rs | 142 ----------- 4 files changed, 91 insertions(+), 374 deletions(-) create mode 100644 crates/core/src/topology/connection_evaluator.rs delete mode 100644 crates/core/src/topology/simulation/mod.rs delete mode 100644 crates/core/src/topology/simulation/tests.rs diff --git a/crates/core/src/topology/connection_evaluator.rs b/crates/core/src/topology/connection_evaluator.rs new file mode 100644 index 000000000..d5e3c2f25 --- /dev/null +++ b/crates/core/src/topology/connection_evaluator.rs @@ -0,0 +1,89 @@ +use std::collections::VecDeque; +use std::time::{Duration, Instant}; + +/// `ConnectionEvaluator` is used to evaluate connection scores within a specified time window. +/// +/// The evaluator records scores and determines whether a given score is better (i.e., lower) than +/// any other scores within a predefined time window. A score is considered better if it's lower +/// than all other scores in the time window, or if no scores were recorded within the window's +/// duration. +/// +/// In the Freenet context, this will be used to titrate the rate of new connection requests accepted +/// by a node. The node will only accept a new connection if the score of the connection is better +/// than all other scores within the time window. +struct ConnectionEvaluator { + scores: VecDeque<(Instant, f64)>, + window_duration: Duration, +} + +impl ConnectionEvaluator { + pub fn new(window_duration: Duration) -> Self { + ConnectionEvaluator { + scores: VecDeque::new(), + window_duration, + } + } + + pub fn record(&mut self, score: f64) -> bool { + self.record_with_current_time(score, Instant::now()) + } + + fn record_with_current_time(&mut self, score: f64, current_time: Instant) -> bool { + self.remove_outdated_scores(current_time); + + let is_better = self.scores.is_empty() || self.scores.iter().all(|&(_, s)| score < s); + self.scores.push_back((current_time, score)); + + is_better + } + + fn remove_outdated_scores(&mut self, current_time: Instant) { + while let Some(&(time, _)) = self.scores.front() { + if current_time.duration_since(time) > self.window_duration { + self.scores.pop_front(); + } else { + break; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_record_first_score() { + let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(10)); + assert_eq!(evaluator.record(5.0), true); + } + + #[test] + fn test_record_within_window_duration() { + let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(10)); + + let start_time = Instant::now(); + evaluator.record_with_current_time(5.0, start_time); + assert_eq!(evaluator.record_with_current_time(6.0, start_time + Duration::from_secs(5)), false); + } + + #[test] + fn test_record_outside_window_duration() { + let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(10)); + + let start_time = Instant::now(); + evaluator.record_with_current_time(5.0, start_time); + assert_eq!(evaluator.record_with_current_time(6.0, start_time + Duration::from_secs(11)), true); + } + + #[test] + fn test_remove_outdated_scores() { + let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(10)); + + let start_time = Instant::now(); + evaluator.record_with_current_time(5.0, start_time); + evaluator.record_with_current_time(6.0, start_time + Duration::from_secs(5)); + evaluator.record_with_current_time(4.5, start_time + Duration::from_secs(11)); + assert_eq!(evaluator.scores.len(), 2); + } +} diff --git a/crates/core/src/topology/mod.rs b/crates/core/src/topology/mod.rs index 1a26756a3..45a27bf58 100644 --- a/crates/core/src/topology/mod.rs +++ b/crates/core/src/topology/mod.rs @@ -1,8 +1,8 @@ #![allow(unused_variables, dead_code)] mod metric; -mod simulation; mod small_world_rand; +mod connection_evaluator; use rand::Rng; @@ -10,11 +10,10 @@ use crate::ring::*; use self::small_world_rand::random_link_distance; -const DEFAULT_MIN_DISTANCE: f64 = 0.01; +const DEFAULT_MIN_DISTANCE: f64 = 1.0 / 1_000.0; pub(crate) enum TopologyStrategy { Random, - SmallWorld, LoadBalancing, } @@ -32,9 +31,6 @@ impl TopologyStrategy { ) -> JoinTargetInfo { match self { TopologyStrategy::Random => random_strategy(my_location, peer_statistics), - TopologyStrategy::SmallWorld => { - small_world_metric_strategy(my_location, peer_statistics) - } TopologyStrategy::LoadBalancing => { load_balancing_strategy(my_location, peer_statistics) } diff --git a/crates/core/src/topology/simulation/mod.rs b/crates/core/src/topology/simulation/mod.rs deleted file mode 100644 index 80fc863ca..000000000 --- a/crates/core/src/topology/simulation/mod.rs +++ /dev/null @@ -1,226 +0,0 @@ -mod event_stat_tracker; -mod tests; - -use crate::ring::Distance; -use super::*; -use event_stat_tracker::EventStatTracker; -use std::collections::{HashMap, HashSet}; -use tracing::{debug, info, warn}; - -#[derive(Debug)] -struct Connections(HashMap>); - -impl Connections { - fn new() -> Self { - Self(HashMap::new()) - } - - fn connect(&mut self, a: NodeRef, b: NodeRef) { - // throw an error if a == b - assert!(a != b, "Cannot connect a node to itself"); - - info!("Connecting {:?} and {:?}", a, b); - self.0.entry(a).or_default().insert(b); - self.0.entry(b).or_default().insert(a); - } - - fn disconnect(&mut self, a: NodeRef, b: NodeRef) { - info!("Disconnecting {:?} and {:?}", a, b); - self.0.entry(a).or_default().remove(&b); - self.0.entry(b).or_default().remove(&a); - } - -} - -#[derive(Debug)] -struct SimulatedNetwork { - current_time: u64, - nodes: Vec, - connections: Connections, - requests: HashMap)>, // origin node -> destination node -> requests -} - -impl SimulatedNetwork { - pub(crate) fn tick(&mut self) { - self.current_time += 1; - } - - pub(crate) fn new() -> Self { - Self { - current_time: 0, - nodes: Vec::new(), - connections: Connections::new(), - requests: HashMap::new(), - } - } - - pub(crate) - - fn add_node(&mut self) -> NodeRef { - let index = self.nodes.len(); - let node = SimulatedNode { - location: Location::random(), - index, - }; - debug!("Adding node {:?}", node); - self.nodes.push(node); - NodeRef { index } - } - - fn route(&self, source: &NodeRef, destination: Location) -> Result, RouteError> { - info!("Routing from {:?} to {:?}", source, destination); - let mut current = *source; // Dereference to copy - let mut visited = Vec::new(); - let mut recent_nodes = Vec::new(); // To track the sequence of last N nodes - - loop { - debug!("Current node: {:?}", current); - - // Check if we've reached the destination - if self.nodes[current.index].location == destination { - info!("Reached destination"); - return Ok(visited); - } - - // Get current node's distance to the destination - let current_distance = self.nodes[current.index] - .location - .distance(destination) - .as_f64(); - debug!("Current distance to destination: {}", current_distance); - - // Find the closest connected node to the destination that hasn't been visited - let closest_connections = match self.connections.0.get(¤t) { - Some(connections) => connections, - None => { - // Handle the None case here. For example, you might want to return agit n Err value or break the loop. - return Err(RouteError::NoRoute); - } - }; - let (closest, closest_distance) = closest_connections - .iter() - .map(|&neighbor| { - let distance = self.nodes[neighbor.index] - .location - .distance(destination) - .as_f64(); - (neighbor, distance) - }) - .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)) - .unwrap_or((current, current_distance)); - - debug!( - "Closest node: {:?}, distance: {}", - closest, closest_distance - ); - - // Check for a loop by looking at the sequence of last N nodes - recent_nodes.push(closest); - if recent_nodes.len() > 10 { - // Keep only the last 10 visited nodes in the list - recent_nodes.remove(0); - } - if recent_nodes.len() > 2 && recent_nodes.first() == recent_nodes.last() { - warn!("Loop detected"); - return Err(RouteError::Loop); - } - - // Update visited nodes and current node - visited.push(closest); - current = closest; - } - } - - fn record_request(&mut self, a: &NodeRef, b: &NodeRef) { - let (count, dest_map) = self - .requests - .entry(*a) - .or_insert((EventStatTracker::new(), HashMap::new())); - count.add_event(self.current_time); - dest_map - .entry(*b) - .or_insert(EventStatTracker::new()) - .add_event(self.current_time); - } - - fn reset_recorded_requests(&mut self) { - self.requests.clear(); - } - - fn get_join_peers( - &self, - source: &NodeRef, - target: Location, - tolerance: Distance, - ) -> Option> { - info!("Joining via {:?} with target {:?}", source, target); - let join_route = self.route(source, target); - let join_route: Vec = match join_route { - Ok(route) => route, - Err(e) => { - warn!("Error joining network: {:?}", e); - return Option::None; - } - }; - - let mut joiners = Vec::new(); - - for node in join_route.iter() { - let node_location = self.nodes[node.index].location; - let distance = node_location.distance(target); - if distance < tolerance { - info!("Found node {:?} within tolerance", node); - joiners.push(*node); - } - } - - Option::Some(joiners) - } - - fn join(&mut self, node: NodeRef, target: Location, tolerance: Distance) { - info!("Joining {:?} with target {:?}", node, target); - let joiners = self.get_join_peers(&node, target, tolerance); - let joiners: Vec = match joiners { - Some(joiners) => joiners, - None => { - warn!("Error joining network"); - return; - } - }; - - for joiner in joiners.iter() { - self.connections.connect(node, *joiner); - } - } - - pub(crate) fn add_and_assimilate_node(&mut self, strategy: TopologyStrategy, peer_statistics: &PeerStatistics) -> NodeRef { - let new_node = self.add_node(); - - let my_location = &self.nodes[new_node.index].location; - - let JoinTargetInfo { target, threshold, .. } = strategy.select_join_target_location(my_location, peer_statistics); - - self.join(new_node, target, threshold); - - new_node - } - -} - -#[derive(Debug)] -enum RouteError { - NoRoute, - Loop, - DeadEnd, -} - -#[derive(Debug)] -struct SimulatedNode { - index: usize, - location: Location, -} - -#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] -struct NodeRef { - index: usize, -} diff --git a/crates/core/src/topology/simulation/tests.rs b/crates/core/src/topology/simulation/tests.rs deleted file mode 100644 index 53d3f5e1a..000000000 --- a/crates/core/src/topology/simulation/tests.rs +++ /dev/null @@ -1,142 +0,0 @@ - - -fn setup() { - // Initialize the tracer - tracing_subscriber::fmt() - .with_max_level(tracing::Level::DEBUG) // Adjust the level here - .try_init() - .unwrap_or(()); -} - -#[test] -fn it_ticks_the_network() { - let mut network = SimulatedNetwork::new(); - network.tick(); - assert_eq!(network.current_time, 1); -} - -#[test] -fn test_add_node() { - setup(); - let mut net = SimulatedNetwork::new(); - let node_ref = net.add_node(); - assert_eq!(node_ref.index, 0); - assert_eq!(net.nodes.len(), 1); -} - -#[test] -fn test_connect() { - setup(); - let mut net = SimulatedNetwork::new(); - let a = net.add_node(); - let b = net.add_node(); - net.connections.connect(a, b); - assert!(net.connections.0.get(&a).unwrap().contains(&b)); - assert!(net.connections.0.get(&b).unwrap().contains(&a)); -} - -#[test] -fn test_disconnect() { - setup(); - let mut net = SimulatedNetwork::new(); - let a = net.add_node(); - let b = net.add_node(); - net.connections.connect(a, b); - net.connections.disconnect(a, b); - assert!(!net.connections.0.get(&a).unwrap().contains(&b)); - assert!(!net.connections.0.get(&b).unwrap().contains(&a)); -} - -#[test] -fn test_route_success() { - setup(); - let mut net = SimulatedNetwork::new(); - let a = net.add_node(); - let b = net.add_node(); - net.connections.connect(a, b); - - // Mock a destination location that's the same as node b's location - let destination = net.nodes[b.index].location; - - let result = net.route(&a, destination); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), vec![b]); -} - -#[test] -fn test_route_loop_error() { - setup(); - - let mut net = SimulatedNetwork::new(); - let a = net.add_node(); - let b = net.add_node(); - let c = net.add_node(); - net.connect(a, b); - net.connect(b, c); - net.connect(c, a); - - // Mock a destination location that's not reachable - let destination = Location::random(); // Assume random will not match any node - - let result = net.route(&a, destination); - assert!(matches!(result, Err(RouteError::Loop))); -} - -#[test] -fn test_route_dead_end() { - setup(); - let mut net = SimulatedNetwork::new(); - let a = net.add_node(); - let destination = net.nodes[a.index].location; - - let result = net.route(&a, destination); - assert!(result.is_ok()); - assert!(result.unwrap().is_empty()); // Since there's no closer node -} -#[test] -fn test_join_success() { - let mut net = SimulatedNetwork::new(); - let a = net.add_node(); - let b = net.add_node(); - let c = net.add_node(); - net.connect(a, b); - net.connect(b, c); - - let destination = net.nodes[c.index].location; - let tolerance = Distance::new(0.4); // Changed to a valid value - - let join_result = net.get_join_peers(&a, destination, tolerance); - - assert!(join_result.is_some()); - assert_eq!(join_result.unwrap(), vec![b, c]); -} - -#[test] -fn test_join_failure() { - let mut net = SimulatedNetwork::new(); - let a = net.add_node(); - - let destination = Location::random(); - let tolerance = Distance::new(0.4); // Changed to a valid value - - let join_result = net.get_join_peers(&a, destination, tolerance); - - assert!(join_result.is_none()); -} - -#[test] -fn test_join_with_tolerance() { - let mut net = SimulatedNetwork::new(); - let a = net.add_node(); - let b = net.add_node(); - net.connect(a, b); - - let destination = net.nodes[b.index].location; - let tolerance = Distance::new(0.1); - - let join_result = net.get_join_peers(&a, destination, tolerance); - - assert!(join_result.is_some()); - assert_eq!(join_result.unwrap(), vec![b]); -} - From c9c9d765392b84f2f63a1c7ceeea01bd178a22e6 Mon Sep 17 00:00:00 2001 From: Nacho Duart Date: Thu, 19 Oct 2023 10:27:23 +0200 Subject: [PATCH 42/76] Fix API breaking changes from libp2p (#874) Semver violation update --- Cargo.lock | 47 +++-------------------------------------------- stdlib | 2 +- 2 files changed, 4 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1c7f4501f..c93781dc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1600,7 +1600,6 @@ dependencies = [ "futures", "itertools", "libp2p", - "nalgebra 0.32.3", "notify", "once_cell", "opentelemetry", @@ -3065,29 +3064,13 @@ checksum = "d506eb7e08d6329505faa8a3a00a5dcc6de9f76e0c77e4b75763ae3c770831ff" dependencies = [ "approx", "matrixmultiply", - "nalgebra-macros 0.1.0", + "nalgebra-macros", "num-complex", "num-rational", "num-traits", "rand", "rand_distr", - "simba 0.6.0", - "typenum", -] - -[[package]] -name = "nalgebra" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307ed9b18cc2423f29e83f84fd23a8e73628727990181f18641a8b5dc2ab1caa" -dependencies = [ - "approx", - "matrixmultiply", - "nalgebra-macros 0.2.1", - "num-complex", - "num-rational", - "num-traits", - "simba 0.8.1", + "simba", "typenum", ] @@ -3102,17 +3085,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "nalgebra-macros" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91761aed67d03ad966ef783ae962ef9bbaca728d2dd7ceb7939ec110fffad998" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "netlink-packet-core" version = "0.4.2" @@ -4632,19 +4604,6 @@ dependencies = [ "wide", ] -[[package]] -name = "simba" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae" -dependencies = [ - "approx", - "num-complex", - "num-traits", - "paste", - "wide", -] - [[package]] name = "simdutf8" version = "0.1.4" @@ -4973,7 +4932,7 @@ checksum = "2d08e5e1748192713cc281da8b16924fb46be7b0c2431854eadc785823e5696e" dependencies = [ "approx", "lazy_static", - "nalgebra 0.29.0", + "nalgebra", "num-traits", "rand", ] diff --git a/stdlib b/stdlib index 0cd6ba8c9..f36475537 160000 --- a/stdlib +++ b/stdlib @@ -1 +1 @@ -Subproject commit 0cd6ba8c9a51ae03d24deef8ce35a88241de57f4 +Subproject commit f36475537a1fa2e1f19721bbfac452ce508f02b9 From daa435159713f4bad646f692ba764b4275347602 Mon Sep 17 00:00:00 2001 From: Nacho Duart Date: Thu, 19 Oct 2023 10:27:23 +0200 Subject: [PATCH 43/76] Fix API breaking changes from libp2p (#874) Semver violation update --- Cargo.lock | 36 ++------------ crates/core/Cargo.toml | 5 +- .../core/src/node/conn_manager/p2p_protoc.rs | 47 ++++++------------- crates/core/src/node/p2p_impl.rs | 6 +-- 4 files changed, 22 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c93781dc4..6219936fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -944,15 +944,6 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", -] - [[package]] name = "crossbeam" version = "0.8.2" @@ -1534,16 +1525,6 @@ dependencies = [ "rustc_version", ] -[[package]] -name = "flate2" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "flume" version = "0.11.0" @@ -1600,6 +1581,7 @@ dependencies = [ "futures", "itertools", "libp2p", + "libp2p-identity", "notify", "once_cell", "opentelemetry", @@ -2396,7 +2378,6 @@ dependencies = [ "libp2p-autonat", "libp2p-connection-limits", "libp2p-core", - "libp2p-deflate", "libp2p-dns", "libp2p-identify", "libp2p-identity", @@ -2483,17 +2464,6 @@ dependencies = [ "void", ] -[[package]] -name = "libp2p-deflate" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc29d4b2f967505282d5a37a7bbdb98cef00662368fefc390bbd99063bd24b26" -dependencies = [ - "flate2", - "futures", - "libp2p-core", -] - [[package]] name = "libp2p-dns" version = "0.40.0" @@ -2533,9 +2503,9 @@ dependencies = [ [[package]] name = "libp2p-identity" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bf6e730ec5e7022958da53ffb03b326e681b7316939012ae9b3c7449a812d4" +checksum = "cdd6317441f361babc74c2989c6484eb0726045399b6648de039e1805ea96972" dependencies = [ "bs58", "ed25519-dalek", diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 2e05202c5..9e0142774 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -37,7 +37,6 @@ either = { workspace = true , features = ["serde"] } futures = "0.3.21" libp2p = { version = "0.52.3", features = [ "autonat", - "deflate", "dns", "identify", "noise", @@ -45,8 +44,10 @@ libp2p = { version = "0.52.3", features = [ "tcp", "tokio", "yamux", - "macros" + "macros", + "ed25519" ], default-features = false } +libp2p-identity = { version = "0.2.7", features = ["ed25519", "rand"]} once_cell = "1" parking_lot = "0.12.0" rand = { workspace = true } diff --git a/crates/core/src/node/conn_manager/p2p_protoc.rs b/crates/core/src/node/conn_manager/p2p_protoc.rs index bc996135d..bc43a77b6 100644 --- a/crates/core/src/node/conn_manager/p2p_protoc.rs +++ b/crates/core/src/node/conn_manager/p2p_protoc.rs @@ -5,7 +5,6 @@ use std::{ pin::Pin, sync::Arc, task::Poll, - time::Instant, }; use asynchronous_codec::{BytesMut, Framed}; @@ -26,9 +25,9 @@ use libp2p::{ self, dial_opts::DialOpts, handler::{DialUpgradeError, FullyNegotiatedInbound, FullyNegotiatedOutbound}, - ConnectionHandler, ConnectionHandlerEvent, ConnectionId, FromSwarm, KeepAlive, - NetworkBehaviour, NotifyHandler, Stream as NegotiatedSubstream, SubstreamProtocol, - SwarmBuilder, SwarmEvent, ToSwarm, + Config as SwarmConfig, ConnectionHandler, ConnectionHandlerEvent, ConnectionId, FromSwarm, + KeepAlive, NetworkBehaviour, NotifyHandler, Stream as NegotiatedSubstream, + SubstreamProtocol, SwarmEvent, ToSwarm, }, InboundUpgrade, Multiaddr, OutboundUpgrade, PeerId, Swarm, }; @@ -194,19 +193,20 @@ impl P2pConnManager { None }; - let builder = SwarmBuilder::with_executor( + let behaviour = config_behaviour( + &config.local_key, + &config.remote_nodes, + &public_addr, + op_manager, + ); + let mut swarm = Swarm::new( transport, - config_behaviour( - &config.local_key, - &config.remote_nodes, - &public_addr, - op_manager, - ), + behaviour, PeerId::from(config.local_key.public()), - global_executor, + SwarmConfig::with_executor(global_executor) + .with_idle_connection_timeout(config::PEER_TIMEOUT), ); - let mut swarm = builder.build(); for remote_addr in config.remote_nodes.iter().filter_map(|r| r.addr.clone()) { swarm.add_external_address(remote_addr); } @@ -642,7 +642,6 @@ pub(in crate::node) enum HandlerEvent { /// Handles the connection with a given peer. pub(in crate::node) struct Handler { substreams: Vec, - keep_alive: KeepAlive, uniq_conn_id: UniqConnId, protocol_status: ProtocolStatus, pending: Vec, @@ -692,17 +691,10 @@ enum SubstreamState { ReportError { error: ConnectionError }, } -impl SubstreamState { - fn is_free(&self) -> bool { - matches!(self, SubstreamState::FreeStream { .. }) - } -} - impl Handler { fn new(op_manager: Arc) -> Self { Self { substreams: vec![], - keep_alive: KeepAlive::Until(Instant::now() + config::PEER_TIMEOUT), uniq_conn_id: 0, protocol_status: ProtocolStatus::Unconfirmed, pending: Vec::new(), @@ -782,7 +774,7 @@ impl ConnectionHandler for Handler { } fn connection_keep_alive(&self) -> KeepAlive { - self.keep_alive + KeepAlive::Yes } fn poll(&mut self, cx: &mut std::task::Context<'_>) -> std::task::Poll { @@ -808,10 +800,6 @@ impl ConnectionHandler for Handler { self.substreams .push(SubstreamState::AwaitingFirst { conn_id }); self.pending.push(*msg); - if self.substreams.is_empty() { - self.keep_alive = - KeepAlive::Until(Instant::now() + config::PEER_TIMEOUT); - } return Poll::Ready(event); } SubstreamState::AwaitingFirst { conn_id } => { @@ -967,13 +955,6 @@ impl ConnectionHandler for Handler { } } - if self.substreams.is_empty() || self.substreams.iter().all(|s| s.is_free()) { - // We destroyed all substreams in this iteration or all substreams are free - self.keep_alive = KeepAlive::Until(Instant::now() + config::PEER_TIMEOUT); - } else { - self.keep_alive = KeepAlive::Yes; - } - Poll::Pending } diff --git a/crates/core/src/node/p2p_impl.rs b/crates/core/src/node/p2p_impl.rs index e3400dfd7..a1fc3684d 100644 --- a/crates/core/src/node/p2p_impl.rs +++ b/crates/core/src/node/p2p_impl.rs @@ -5,8 +5,7 @@ use libp2p::{ muxing, transport::{self, upgrade}, }, - deflate, - dns::TokioDnsConfig, + dns, identity::Keypair, noise, tcp, yamux, PeerId, Transport, }; @@ -131,11 +130,10 @@ impl NodeP2P { local_key: &Keypair, ) -> std::io::Result> { let tcp = tcp::tokio::Transport::new(tcp::Config::new().nodelay(true).port_reuse(true)); - let with_dns = TokioDnsConfig::system(tcp)?; + let with_dns = dns::tokio::Transport::system(tcp)?; Ok(with_dns .upgrade(upgrade::Version::V1) .authenticate(noise::Config::new(local_key).unwrap()) - .apply(deflate::DeflateConfig::default()) .multiplex(yamux::Config::default()) .timeout(config::PEER_TIMEOUT) .map(|(peer, muxer), _| (peer, muxing::StreamMuxerBox::new(muxer))) From cc40ebabf4538bc9236a0be3ad1326cbf89d4533 Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Sat, 21 Oct 2023 09:01:11 -0500 Subject: [PATCH 44/76] Generate documentation per-release --- .github/workflows/docs.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5d1a458aa..6b8dd8ee3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,6 +4,7 @@ on: workflow_dispatch: push: branches: [main] + tags: [v*] jobs: deploy: @@ -14,6 +15,7 @@ jobs: with: fetch-depth: 0 submodules: true + ref: ${{ github.event.release.tag_name || 'main' }} - name: Setup Rust uses: ATiltedTree/setup-rust@v1 with: @@ -34,8 +36,17 @@ jobs: else echo "Docs folder not found." fi + - name: Prepare Deploy Folder + run: | + mkdir -p deploy + if [[ $GITHUB_REF == refs/heads/main ]]; then + mv docs/book deploy/ + else + version=${GITHUB_REF#refs/tags/v} + mv docs/book deploy/$version + fi - name: Deploy uses: JamesIves/github-pages-deploy-action@v4 with: - folder: docs/book + folder: deploy single-commit: true From f9958b2317b68435b36d82188d46597bce3e6d26 Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Sat, 21 Oct 2023 09:05:00 -0500 Subject: [PATCH 45/76] Create docs-retro.yml --- .github/workflows/docs-retro.yml | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/workflows/docs-retro.yml diff --git a/.github/workflows/docs-retro.yml b/.github/workflows/docs-retro.yml new file mode 100644 index 000000000..14d3dfa6a --- /dev/null +++ b/.github/workflows/docs-retro.yml @@ -0,0 +1,58 @@ +name: Retroactive Documentation Deployment + +on: + workflow_dispatch: # Allows manual triggering of the workflow + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v2 + with: + fetch-depth: 0 # Get the entire history so all tags are available + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + + - name: Cache Dependencies + uses: actions/cache@v2 + with: + path: ~/.cargo + key: cargo-cache-${{ hashFiles('**/Cargo.lock') }} + + - name: Install dependencies + run: | + cargo install mdbook mdbook-mermaid mdbook-toc + + - name: Generate and Deploy Documentation + run: | + mkdir -p deploy + tags=$(git tag -l 'v*') # Assumes tag format is v + for tag in $tags; do + git checkout $tag + version=${tag#v} + deploy_dir="deploy/$version" + if [[ ! -d $deploy_dir ]]; then + if [[ -d "docs" ]]; then + cd docs + mdbook build + mv book ../$deploy_dir + cd .. + else + echo "Docs folder not found for tag $tag." + fi + else + echo "Documentation already exists for tag $tag." + fi + done + git checkout main # Return to main branch before deployment + + - name: Deploy + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: deploy + single-commit: true From 3a82b4a08214f1199be39df8c896b3e2886b027e Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Sat, 21 Oct 2023 09:19:01 -0500 Subject: [PATCH 46/76] Update docs-retro.yml --- .github/workflows/docs-retro.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/docs-retro.yml b/.github/workflows/docs-retro.yml index 14d3dfa6a..6e348e0fb 100644 --- a/.github/workflows/docs-retro.yml +++ b/.github/workflows/docs-retro.yml @@ -32,9 +32,13 @@ jobs: run: | mkdir -p deploy tags=$(git tag -l 'v*') # Assumes tag format is v + latest_version="" for tag in $tags; do git checkout $tag version=${tag#v} + if [[ "$version" > "$latest_version" ]]; then + latest_version=$version + fi deploy_dir="deploy/$version" if [[ ! -d $deploy_dir ]]; then if [[ -d "docs" ]]; then @@ -50,9 +54,26 @@ jobs: fi done git checkout main # Return to main branch before deployment + # Build and move the latest version documentation to root directory + git checkout "v$latest_version" + if [[ -d "docs" ]]; then + cd docs + mdbook build + mv book ../deploy_latest + cd .. + else + echo "Docs folder not found for the latest tag v$latest_version." + fi - name: Deploy uses: JamesIves/github-pages-deploy-action@v4 with: folder: deploy single-commit: true + + - name: Deploy Latest Documentation + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: deploy_latest + target-folder: . + single-commit: true From f4aa11fb8f02aebb2fa5842df1609605042596a1 Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Sat, 21 Oct 2023 09:22:59 -0500 Subject: [PATCH 47/76] Update docs.yml --- .github/workflows/docs.yml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6b8dd8ee3..5d1a458aa 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,7 +4,6 @@ on: workflow_dispatch: push: branches: [main] - tags: [v*] jobs: deploy: @@ -15,7 +14,6 @@ jobs: with: fetch-depth: 0 submodules: true - ref: ${{ github.event.release.tag_name || 'main' }} - name: Setup Rust uses: ATiltedTree/setup-rust@v1 with: @@ -36,17 +34,8 @@ jobs: else echo "Docs folder not found." fi - - name: Prepare Deploy Folder - run: | - mkdir -p deploy - if [[ $GITHUB_REF == refs/heads/main ]]; then - mv docs/book deploy/ - else - version=${GITHUB_REF#refs/tags/v} - mv docs/book deploy/$version - fi - name: Deploy uses: JamesIves/github-pages-deploy-action@v4 with: - folder: deploy + folder: docs/book single-commit: true From 93e7787a09f3d154bff58b86b1617d2209cd541a Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Sat, 21 Oct 2023 09:27:41 -0500 Subject: [PATCH 48/76] Update docs.yml --- .github/workflows/docs.yml | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5d1a458aa..f76dca7c2 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -14,19 +14,23 @@ jobs: with: fetch-depth: 0 submodules: true + - name: Setup Rust uses: ATiltedTree/setup-rust@v1 with: rust-version: stable + - name: Cache Dependencies uses: actions/cache@v3 with: path: ~/.cargo key: cargo-cache-${{ hashFiles('Cargo.lock') }} + - name: Install dependencies run: | cargo install mdbook mdbook-mermaid mdbook-toc - - name: Build MDBook + + - name: Build MDBook for HEAD run: | if [ -d "docs" ]; then cd docs @@ -34,6 +38,26 @@ jobs: else echo "Docs folder not found." fi + + - name: Checkout latest release + id: latest_release + run: | + LATEST_TAG=$(git describe --tags --abbrev=0) + echo "::set-output name=tag::${LATEST_TAG}" + git checkout $LATEST_TAG + + - name: Create release subdirectory + run: mkdir -p docs/release + + - name: Build MDBook for release + run: | + if [ -d "docs" ]; then + cd docs + mdbook build -d release + else + echo "Docs folder not found." + fi + - name: Deploy uses: JamesIves/github-pages-deploy-action@v4 with: From a19dce097a2202c80f5d30f3f7e5f491d5098bfc Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Sat, 21 Oct 2023 09:31:21 -0500 Subject: [PATCH 49/76] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a32792b23..b4d2540d4 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ # Freenet -The Internet has grown increasingly centralized over the past 25 years, such +The Internet has grown increasingly centralized over the past two decades, such that a handful of companies now effectively control the Internet infrastructure. The public square is privately owned, threatening freedom of speech and democracy. From d0591881e3150e74a431018099550f3af52ff59c Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Sat, 21 Oct 2023 09:35:35 -0500 Subject: [PATCH 50/76] Update docs.yml --- .github/workflows/docs.yml | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f76dca7c2..5d1a458aa 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -14,23 +14,19 @@ jobs: with: fetch-depth: 0 submodules: true - - name: Setup Rust uses: ATiltedTree/setup-rust@v1 with: rust-version: stable - - name: Cache Dependencies uses: actions/cache@v3 with: path: ~/.cargo key: cargo-cache-${{ hashFiles('Cargo.lock') }} - - name: Install dependencies run: | cargo install mdbook mdbook-mermaid mdbook-toc - - - name: Build MDBook for HEAD + - name: Build MDBook run: | if [ -d "docs" ]; then cd docs @@ -38,26 +34,6 @@ jobs: else echo "Docs folder not found." fi - - - name: Checkout latest release - id: latest_release - run: | - LATEST_TAG=$(git describe --tags --abbrev=0) - echo "::set-output name=tag::${LATEST_TAG}" - git checkout $LATEST_TAG - - - name: Create release subdirectory - run: mkdir -p docs/release - - - name: Build MDBook for release - run: | - if [ -d "docs" ]; then - cd docs - mdbook build -d release - else - echo "Docs folder not found." - fi - - name: Deploy uses: JamesIves/github-pages-deploy-action@v4 with: From 9a2e146a334d625907bdc13752ebd9b9937a3e58 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 11:13:26 +0200 Subject: [PATCH 51/76] Bump actions/checkout from 2 to 4 (#875) Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docs-retro.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs-retro.yml b/.github/workflows/docs-retro.yml index 6e348e0fb..fa8486ace 100644 --- a/.github/workflows/docs-retro.yml +++ b/.github/workflows/docs-retro.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 # Get the entire history so all tags are available From 1076669e8d6cf531dfb7d3f575f9780996badeab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 11:13:53 +0200 Subject: [PATCH 52/76] Bump actions/cache from 2 to 3 (#876) Bumps [actions/cache](https://github.com/actions/cache) from 2 to 3. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docs-retro.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs-retro.yml b/.github/workflows/docs-retro.yml index fa8486ace..94ff73ce0 100644 --- a/.github/workflows/docs-retro.yml +++ b/.github/workflows/docs-retro.yml @@ -19,7 +19,7 @@ jobs: toolchain: stable - name: Cache Dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.cargo key: cargo-cache-${{ hashFiles('**/Cargo.lock') }} From bb0dd3f460ab7a030c2b98155f9d4040f17ede51 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Fri, 13 Oct 2023 16:32:38 +0200 Subject: [PATCH 53/76] Make sim network compile --- crates/core/src/node/event_log.rs | 6 +- crates/core/src/node/tests.rs | 158 ++++++++++++++---------- crates/core/src/operations/get.rs | 26 ++-- crates/core/src/operations/join_ring.rs | 2 +- crates/core/src/operations/put.rs | 14 +-- crates/core/src/operations/subscribe.rs | 4 +- 6 files changed, 119 insertions(+), 91 deletions(-) diff --git a/crates/core/src/node/event_log.rs b/crates/core/src/node/event_log.rs index e7d6bcee7..f27e3d8bc 100644 --- a/crates/core/src/node/event_log.rs +++ b/crates/core/src/node/event_log.rs @@ -411,13 +411,13 @@ pub(super) mod test_utils { use parking_lot::RwLock; use super::*; - use crate::{message::TxType, ring::Distance}; + use crate::{message::TxType, node::tests::NodeLabel, ring::Distance}; static LOG_ID: AtomicUsize = AtomicUsize::new(0); #[derive(Clone)] pub(crate) struct TestEventListener { - node_labels: Arc>, + node_labels: Arc>, tx_log: Arc>>, logs: Arc>>, } @@ -431,7 +431,7 @@ pub(super) mod test_utils { } } - pub fn add_node(&mut self, label: String, peer: PeerKey) { + pub fn add_node(&mut self, label: NodeLabel, peer: PeerKey) { self.node_labels.insert(label, peer); } diff --git a/crates/core/src/node/tests.rs b/crates/core/src/node/tests.rs index 88b9d1d68..86919eb44 100644 --- a/crates/core/src/node/tests.rs +++ b/crates/core/src/node/tests.rs @@ -2,6 +2,7 @@ use std::{ collections::{HashMap, HashSet}, fmt::Write, net::{Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener}, + sync::Arc, time::{Duration, Instant}, }; @@ -44,12 +45,12 @@ pub fn get_dynamic_port() -> u16 { /// A simulated in-memory network topology. pub(crate) struct SimNetwork { - pub labels: HashMap, + pub labels: HashMap, pub event_listener: TestEventListener, - usr_ev_controller: Sender<(EventId, PeerKey)>, + user_ev_controller: Sender<(EventId, PeerKey)>, receiver_ch: Receiver<(EventId, PeerKey)>, gateways: Vec<(NodeInMemory, GatewayConfig)>, - nodes: Vec<(NodeInMemory, String)>, + nodes: Vec<(NodeInMemory, NodeLabel)>, ring_max_htl: usize, rnd_if_htl_above: usize, max_connections: usize, @@ -58,6 +59,43 @@ pub(crate) struct SimNetwork { pub(crate) type EventId = usize; +#[derive(PartialEq, Eq, Hash, Clone)] +pub(crate) struct NodeLabel(Arc); + +impl NodeLabel { + fn gateway(id: usize) -> Self { + Self(format!("gateway-{id}").into()) + } + + fn node(id: usize) -> Self { + Self(format!("node-{id}").into()) + } + + fn is_gateway(&self) -> bool { + self.0.starts_with("gateway") + } +} + +impl std::fmt::Display for NodeLabel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::ops::Deref for NodeLabel { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.0.deref() + } +} + +impl<'a> From<&'a str> for NodeLabel { + fn from(value: &'a str) -> Self { + Self(value.to_string().into()) + } +} + #[derive(Clone)] pub(crate) struct NodeSpecification { /// Pair of contract and the initial value @@ -69,7 +107,7 @@ pub(crate) struct NodeSpecification { #[derive(Clone)] struct GatewayConfig { - label: String, + label: NodeLabel, port: u16, id: PeerId, location: Location, @@ -85,12 +123,12 @@ impl SimNetwork { min_connections: usize, ) -> Self { assert!(gateways > 0 && nodes > 0); - let (usr_ev_controller, _rcv_copy) = channel((0, PeerKey::random())); + let (user_ev_controller, receiver_ch) = channel((0, PeerKey::random())); let mut net = Self { event_listener: TestEventListener::new(), labels: HashMap::new(), - usr_ev_controller, - receiver_ch: _rcv_copy, + user_ev_controller, + receiver_ch, gateways: Vec::with_capacity(gateways), nodes: Vec::with_capacity(nodes), ring_max_htl, @@ -98,17 +136,17 @@ impl SimNetwork { max_connections, min_connections, }; - net.build_gateways(gateways); + net.build_gateways(gateways).await; net.build_nodes(nodes).await; net } #[instrument(skip(self))] - fn build_gateways(&mut self, num: usize) { + async fn build_gateways(&mut self, num: usize) { info!("Building {} gateways", num); let mut configs = Vec::with_capacity(num); for node_no in 0..num { - let label = format!("gateway-{}", node_no); + let label = NodeLabel::gateway(node_no); let pair = identity::Keypair::generate_ed25519(); let id = pair.public().to_peer_id(); let port = get_free_port().unwrap(); @@ -141,31 +179,29 @@ impl SimNetwork { )); } - // FIXME: - // for (mut this_node, this_config) in configs { - // for GatewayConfig { - // port, id, location, .. - // } in configs.iter().filter_map(|(_, config)| { - // if this_config.label != config.label { - // Some(config) - // } else { - // None - // } - // }) { - // this_node.add_gateway( - // InitPeerNode::new(*id, *location) - // .listening_ip(Ipv6Addr::LOCALHOST) - // .listening_port(*port), - // ); - // } - - // let gateway = NodeInMemory::::build::( - // this_node, - // Some(Box::new(self.event_listener.clone())), - // ) - // .unwrap(); - // self.gateways.push((gateway, this_config)); - // } + let gateways: Vec<_> = configs.iter().map(|(_, gw)| gw.clone()).collect(); + for (mut this_node, this_config) in configs { + for GatewayConfig { + port, id, location, .. + } in gateways + .iter() + .filter(|config| this_config.label != config.label) + { + this_node.add_gateway( + InitPeerNode::new(*id, *location) + .listening_ip(Ipv6Addr::LOCALHOST) + .listening_port(*port), + ); + } + let gateway = NodeInMemory::build::( + this_node, + self.event_listener.clone(), + (), + ) + .await + .unwrap(); + self.gateways.push((gateway, this_config)); + } } #[instrument(skip(self))] @@ -178,7 +214,7 @@ impl SimNetwork { .collect(); for node_no in 0..num { - let label = format!("node-{}", node_no); + let label = NodeLabel::node(node_no); let pair = identity::Keypair::generate_ed25519(); let id = pair.public().to_peer_id(); @@ -205,7 +241,7 @@ impl SimNetwork { self.event_listener .add_node(label.clone(), PeerKey::from(id)); - let node = NodeInMemory::build::( + let node = NodeInMemory::build::( config, self.event_listener.clone(), (), @@ -220,7 +256,7 @@ impl SimNetwork { self.build_with_specs(HashMap::new()).await } - pub async fn build_with_specs(&mut self, mut specs: HashMap) { + pub async fn build_with_specs(&mut self, mut specs: HashMap) { let mut gw_not_init = self.gateways.len(); let gw = self.gateways.drain(..).map(|(n, c)| (n, c.label)); for (node, label) in gw.chain(self.nodes.drain(..)).collect::>() { @@ -237,7 +273,7 @@ impl SimNetwork { fn initialize_peer( &mut self, mut peer: NodeInMemory, - label: String, + label: NodeLabel, node_specs: Option, ) { let mut user_events = MemoryEventsGen::new(self.receiver_ch.clone(), peer.peer_key); @@ -257,23 +293,20 @@ impl SimNetwork { }); } - pub fn get_locations_by_node(&self) -> HashMap { - let mut locations_by_node: HashMap = HashMap::new(); + pub fn get_locations_by_node(&self) -> HashMap { + let mut locations_by_node: HashMap = HashMap::new(); // Get node and gateways location by label for (node, label) in &self.nodes { - locations_by_node.insert(label.to_string(), node.op_storage.ring.own_location()); + locations_by_node.insert(label.clone(), node.op_storage.ring.own_location()); } for (node, config) in &self.gateways { - locations_by_node.insert( - config.label.to_string(), - node.op_storage.ring.own_location(), - ); + locations_by_node.insert(config.label.clone(), node.op_storage.ring.own_location()); } locations_by_node } - pub fn connected(&self, peer: &str) -> bool { + pub fn connected(&self, peer: &NodeLabel) -> bool { if let Some(key) = self.labels.get(peer) { self.event_listener.is_connected(key) } else { @@ -281,7 +314,12 @@ impl SimNetwork { } } - pub fn has_put_contract(&self, peer: &str, key: &ContractKey, value: &WrappedState) -> bool { + pub fn has_put_contract( + &self, + peer: &NodeLabel, + key: &ContractKey, + value: &WrappedState, + ) -> bool { if let Some(pk) = self.labels.get(peer) { self.event_listener.has_put_contract(pk, key, value) } else { @@ -289,7 +327,7 @@ impl SimNetwork { } } - pub fn has_got_contract(&self, peer: &str, key: &ContractKey) -> bool { + pub fn has_got_contract(&self, peer: &NodeLabel, key: &ContractKey) -> bool { if let Some(pk) = self.labels.get(peer) { self.event_listener.has_got_contract(pk, key) } else { @@ -311,7 +349,7 @@ impl SimNetwork { /// Returns the connectivity in the network per peer (that is all the connections /// this peers has registered). - pub fn node_connectivity(&self) -> HashMap> { + pub fn node_connectivity(&self) -> HashMap> { let mut peers_connections = HashMap::with_capacity(self.labels.len()); let key_to_label: HashMap<_, _> = self.labels.iter().map(|(k, v)| (v, k)).collect(); for (label, key) in &self.labels { @@ -328,7 +366,7 @@ impl SimNetwork { pub async fn trigger_event( &self, - label: &str, + label: &NodeLabel, event_id: EventId, await_for: Option, ) -> Result<(), anyhow::Error> { @@ -336,7 +374,7 @@ impl SimNetwork { .labels .get(label) .ok_or_else(|| anyhow::anyhow!("node not found"))?; - self.usr_ev_controller + self.user_ev_controller .send((event_id, *peer)) .expect("node listeners disconnected"); if let Some(sleep_time) = await_for { @@ -375,7 +413,7 @@ pub(crate) async fn check_connectivity( let elapsed = Instant::now(); while elapsed.elapsed() < time_out && connected.len() < num_nodes { for node in 0..num_nodes { - if !connected.contains(&node) && sim_nodes.connected(&format!("node-{}", node)) { + if !connected.contains(&node) && sim_nodes.connected(&NodeLabel::node(node)) { connected.insert(node); } } @@ -409,13 +447,7 @@ pub(crate) async fn check_connectivity( let mut connections_per_peer: Vec<_> = node_connectivity .iter() .map(|(k, v)| (k, v.len())) - .filter_map(|(k, v)| { - if !k.starts_with("gateway") { - Some(v) - } else { - None - } - }) + .filter_map(|(k, v)| if !k.is_gateway() { Some(v) } else { None }) .collect(); // ensure at least some normal nodes have more than one connection @@ -433,12 +465,12 @@ pub(crate) async fn check_connectivity( Ok(()) } -fn pretty_print_connections(conns: &HashMap>) -> String { +fn pretty_print_connections(conns: &HashMap>) -> String { let mut connections = String::from("Node connections:\n"); let mut conns = conns.iter().collect::>(); - conns.sort_by(|a, b| a.0.cmp(b.0)); + conns.sort_by(|(a, _), (b, _)| a.cmp(b)); for (peer, conns) in conns { - if peer.starts_with("gateway") { + if peer.is_gateway() { continue; } writeln!(&mut connections, "{peer}:").unwrap(); diff --git a/crates/core/src/operations/get.rs b/crates/core/src/operations/get.rs index 56d03bc11..33f8af4a4 100644 --- a/crates/core/src/operations/get.rs +++ b/crates/core/src/operations/get.rs @@ -807,7 +807,6 @@ mod test { use super::*; use crate::node::tests::{check_connectivity, NodeSpecification, SimNetwork}; - #[ignore] #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn successful_get_op_between_nodes() -> Result<(), anyhow::Error> { const NUM_NODES: usize = 1usize; @@ -840,10 +839,7 @@ mod test { contract_subscribers: HashMap::new(), }; - let get_specs = HashMap::from_iter([ - ("node-0".to_string(), node_0), - ("gateway-0".to_string(), gw_0), - ]); + let get_specs = HashMap::from_iter([("node-0".into(), node_0), ("gateway-0".into(), gw_0)]); // establish network let mut sim_nodes = SimNetwork::new(NUM_GW, NUM_NODES, 3, 2, 4, 2).await; @@ -852,10 +848,10 @@ mod test { // trigger get @ node-0, which does not own the contract sim_nodes - .trigger_event("node-0", 1, Some(Duration::from_millis(100))) + .trigger_event(&"node-0".into(), 1, Some(Duration::from_millis(100))) .await?; tokio::time::sleep(Duration::from_millis(100)).await; - assert!(sim_nodes.has_got_contract("node-0", &key)); + assert!(sim_nodes.has_got_contract(&"node-0".into(), &key)); Ok(()) } @@ -882,7 +878,7 @@ mod test { contract_subscribers: HashMap::new(), }; - let get_specs = HashMap::from_iter([("node-1".to_string(), node_1)]); + let get_specs = HashMap::from_iter([("node-1".into(), node_1)]); // establish network let mut sim_nodes = SimNetwork::new(NUM_GW, NUM_NODES, 3, 2, 4, 2).await; @@ -891,9 +887,9 @@ mod test { // trigger get @ node-1, which does not own the contract sim_nodes - .trigger_event("node-1", 1, Some(Duration::from_millis(100))) + .trigger_event(&"node-1".into(), 1, Some(Duration::from_millis(100))) .await?; - assert!(!sim_nodes.has_got_contract("node-1", &key)); + assert!(!sim_nodes.has_got_contract(&"node-1".into(), &key)); Ok(()) } @@ -940,9 +936,9 @@ mod test { }; let get_specs = HashMap::from_iter([ - ("node-0".to_string(), node_0), - ("node-1".to_string(), node_1), - ("gateway-0".to_string(), gw_0), + ("node-0".into(), node_0), + ("node-1".into(), node_1), + ("gateway-0".into(), gw_0), ]); // establish network @@ -951,9 +947,9 @@ mod test { check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(3)).await?; sim_nodes - .trigger_event("node-0", 1, Some(Duration::from_millis(500))) + .trigger_event(&"node-0".into(), 1, Some(Duration::from_millis(500))) .await?; - assert!(sim_nodes.has_got_contract("node-0", &key)); + assert!(sim_nodes.has_got_contract(&"node-0".into(), &key)); Ok(()) } } diff --git a/crates/core/src/operations/join_ring.rs b/crates/core/src/operations/join_ring.rs index 32415cade..936751a87 100644 --- a/crates/core/src/operations/join_ring.rs +++ b/crates/core/src/operations/join_ring.rs @@ -1006,7 +1006,7 @@ mod test { let mut sim_nodes = SimNetwork::new(1, 1, 1, 1, 2, 2).await; sim_nodes.build().await; tokio::time::sleep(Duration::from_secs(3)).await; - assert!(sim_nodes.connected("node-0")); + assert!(sim_nodes.connected(&"node-0".into())); } /// Once a gateway is left without remaining open slots, ensure forwarding connects diff --git a/crates/core/src/operations/put.rs b/crates/core/src/operations/put.rs index 181a1ffc6..4a157ac2d 100644 --- a/crates/core/src/operations/put.rs +++ b/crates/core/src/operations/put.rs @@ -918,8 +918,8 @@ mod test { let mut sim_nodes = SimNetwork::new(NUM_GW, NUM_NODES, 3, 2, 4, 2).await; let mut locations = sim_nodes.get_locations_by_node(); - let node0_loc = locations.remove("node-0").unwrap(); - let node1_loc = locations.remove("node-1").unwrap(); + let node0_loc = locations.remove(&"node-0".into()).unwrap(); + let node1_loc = locations.remove(&"node-1".into()).unwrap(); // both own the contract, and one triggers an update let node_0 = NodeSpecification { @@ -961,9 +961,9 @@ mod test { // establish network let put_specs = HashMap::from_iter([ - ("node-0".to_string(), node_0), - ("node-1".to_string(), node_1), - ("gateway-0".to_string(), gw_0), + ("node-0".into(), node_0), + ("node-1".into(), node_1), + ("gateway-0".into(), gw_0), ]); sim_nodes.build_with_specs(put_specs).await; @@ -972,9 +972,9 @@ mod test { // trigger the put op @ gw-0, this sim_nodes - .trigger_event("gateway-0", 1, Some(Duration::from_secs(3))) + .trigger_event(&"gateway-0".into(), 1, Some(Duration::from_secs(3))) .await?; - assert!(sim_nodes.has_put_contract("gateway-0", &key, &new_value)); + assert!(sim_nodes.has_put_contract(&"gateway-0".into(), &key, &new_value)); assert!(sim_nodes.event_listener.contract_broadcasted(&key)); Ok(()) } diff --git a/crates/core/src/operations/subscribe.rs b/crates/core/src/operations/subscribe.rs index 84b03563d..5b02883cb 100644 --- a/crates/core/src/operations/subscribe.rs +++ b/crates/core/src/operations/subscribe.rs @@ -506,8 +506,8 @@ mod test { }; let subscribe_specs = HashMap::from_iter([ - ("node-0".to_string(), first_node), - ("node-1".to_string(), second_node), + ("node-0".into(), first_node), + ("node-1".into(), second_node), ]); let mut sim_nodes = SimNetwork::new(NUM_GW, NUM_NODES, 3, 2, 4, 2).await; sim_nodes.build_with_specs(subscribe_specs).await; From e6590d6d91536958a5443175b696aace776b4e16 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Sun, 15 Oct 2023 20:53:16 +0200 Subject: [PATCH 54/76] Add mock executor impl --- crates/core/src/client_events.rs | 66 +++++---- crates/core/src/contract.rs | 41 +++--- crates/core/src/contract/executor.rs | 139 ++++++++++++------ crates/core/src/contract/handler.rs | 8 +- crates/core/src/contract/in_memory.rs | 31 ++-- crates/core/src/contract/storages/rocks_db.rs | 7 +- crates/core/src/contract/storages/sqlite.rs | 7 +- crates/core/src/lib.rs | 2 +- crates/core/src/node.rs | 75 +++++----- crates/core/src/node/p2p_impl.rs | 4 +- crates/core/src/node/tests.rs | 7 +- crates/core/src/operations.rs | 3 + crates/core/src/operations/get.rs | 36 ++++- crates/core/src/operations/join_ring.rs | 24 ++- crates/core/src/operations/put.rs | 11 +- crates/core/src/operations/subscribe.rs | 11 +- crates/core/src/ring.rs | 3 +- crates/core/src/runtime/mod.rs | 2 +- crates/fdev/src/commands.rs | 14 +- crates/fdev/src/local_node/state.rs | 11 +- 20 files changed, 309 insertions(+), 193 deletions(-) diff --git a/crates/core/src/client_events.rs b/crates/core/src/client_events.rs index b8574ed32..a6aca2218 100644 --- a/crates/core/src/client_events.rs +++ b/crates/core/src/client_events.rs @@ -145,8 +145,8 @@ pub(crate) mod test { // FIXME: remove unused #![allow(unused)] - use std::collections::HashMap; use std::sync::Arc; + use std::{collections::HashMap, time::Duration}; use freenet_stdlib::client_api::ContractRequest; use freenet_stdlib::prelude::*; @@ -264,37 +264,39 @@ pub(crate) mod test { impl ClientEventsProxy for MemoryEventsGen { fn recv(&mut self) -> BoxFuture<'_, Result, ClientError>> { - // async move { - // loop { - // if self.signal.changed().await.is_ok() { - // let (ev_id, pk) = *self.signal.borrow(); - // if pk == self.id && !self.random { - // let res = OpenRequest { - // id: ClientId(1), - // request: self - // .generate_deterministic_event(&ev_id) - // .expect("event not found"), - // notification_channel: None, - // }; - // return Ok(res); - // } else if pk == self.id { - // let res = OpenRequest { - // id: ClientId(1), - // request: self.generate_rand_event(), - // notification_channel: None, - // }; - // return Ok(res); - // } - // } else { - // log::debug!("sender half of user event gen dropped"); - // // probably the process finished, wait for a bit and then kill the thread - // tokio::time::sleep(Duration::from_secs(1)).await; - // panic!("finished orphan background thread"); - // } - // } - // } - // .boxed() - todo!("fixme") + async { + loop { + if self.signal.changed().await.is_ok() { + let (ev_id, pk) = *self.signal.borrow(); + if pk == self.id && !self.random { + let res = OpenRequest { + client_id: ClientId::FIRST, + request: self + .generate_deterministic_event(&ev_id) + .expect("event not found") + .into(), + notification_channel: None, + token: None, + }; + return Ok(res.into_owned()); + } else if pk == self.id { + let res = OpenRequest { + client_id: ClientId::FIRST, + request: self.generate_rand_event().into(), + notification_channel: None, + token: None, + }; + return Ok(res.into_owned()); + } + } else { + tracing::debug!("sender half of user event gen dropped"); + // probably the process finished, wait for a bit and then kill the thread + tokio::time::sleep(Duration::from_secs(1)).await; + panic!("finished orphan background thread"); + } + } + } + .boxed() } fn send( diff --git a/crates/core/src/contract.rs b/crates/core/src/contract.rs index ef3d77fdd..419282147 100644 --- a/crates/core/src/contract.rs +++ b/crates/core/src/contract.rs @@ -1,4 +1,5 @@ use crate::runtime::ContractError as ContractRtError; +use either::Either; use freenet_stdlib::prelude::*; mod executor; @@ -23,6 +24,7 @@ pub use executor::{Executor, ExecutorError, OperationMode}; use executor::ContractExecutor; pub(crate) async fn contract_handling<'a, CH>(mut contract_handler: CH) -> Result<(), ContractError> +// todo: remove result where CH: ContractHandler + Send + 'static, { @@ -85,30 +87,21 @@ where } } } - ContractHandlerEvent::PutQuery { - key: _key, - state: _state, - } => { - // let _put_result = contract_handler - // .handle_request(ClientRequest::Put { - // contract: todo!(), - // state: _state, - // }.into()) - // .await - // .map(|r| { - // let _r = r.unwrap_put(); - // unimplemented!(); - // }); - // contract_handler - // .channel() - // .send_to_listener( - // _id, - // ContractHandlerEvent::PushResponse { - // new_value: put_result, - // }, - // ) - // .await?; - todo!("perform put request"); + ContractHandlerEvent::PutQuery { key, state } => { + let put_result = contract_handler + .executor() + .upsert_contract_state(key, Either::Left(state)) + .await + .map_err(Into::into); + contract_handler + .channel() + .send_to_event_loop( + id, + ContractHandlerEvent::PutResponse { + new_value: put_result, + }, + ) + .await?; } _ => unreachable!(), } diff --git a/crates/core/src/contract/executor.rs b/crates/core/src/contract/executor.rs index a708ca866..e201cc444 100644 --- a/crates/core/src/contract/executor.rs +++ b/crates/core/src/contract/executor.rs @@ -237,6 +237,11 @@ pub(crate) trait ContractExecutor: Send + Sync + 'static { &mut self, contract: ContractContainer, ) -> Result<(), crate::runtime::ContractError>; + async fn upsert_contract_state( + &mut self, + key: ContractKey, + state: Either>, + ) -> Result; } /// A WASM executor which will run any contracts, delegates, etc. registered. @@ -338,8 +343,46 @@ impl Executor { } } -impl Executor { - pub async fn from_config(config: NodeConfig) -> Result { +impl Executor { + pub async fn new( + state_store: StateStore, + ctrl_handler: impl FnOnce(), + mode: OperationMode, + runtime: R, + ) -> Result { + ctrl_handler(); + + Ok(Self { + #[cfg(any( + all(feature = "local-mode", feature = "network-mode"), + all(not(feature = "local-mode"), not(feature = "network-mode")), + ))] + mode, + runtime, + state_store, + update_notifications: HashMap::default(), + subscriber_summaries: HashMap::default(), + delegate_attested_ids: HashMap::default(), + #[cfg(any( + not(feature = "local-mode"), + feature = "network-mode", + all(not(feature = "local-mode"), not(feature = "network-mode")) + ))] + event_loop_channel: None, + }) + } + + async fn get_stores( + config: &NodeConfig, + ) -> Result< + ( + ContractStore, + DelegateStore, + SecretsStore, + StateStore, + ), + DynError, + > { const MAX_SIZE: i64 = 10 * 1024 * 1024; const MAX_MEM_CACHE: u32 = 10_000_000; let static_conf = crate::config::Config::conf(); @@ -367,50 +410,26 @@ impl Executor { .unwrap_or_else(|| static_conf.secrets_dir()); let secret_store = SecretsStore::new(secrets_dir)?; + Ok((contract_store, delegate_store, secret_store, state_store)) + } +} + +impl Executor { + pub async fn from_config(config: NodeConfig) -> Result { + let (contract_store, delegate_store, secret_store, state_store) = + Self::get_stores(&config).await?; + let rt = Runtime::build(contract_store, delegate_store, secret_store, false).unwrap(); Executor::new( - contract_store, - delegate_store, - secret_store, state_store, || { crate::util::set_cleanup_on_exit().unwrap(); }, OperationMode::Local, + rt, ) .await } - #[allow(unused_variables)] - pub async fn new( - contract_store: ContractStore, - delegate_store: DelegateStore, - secret_store: SecretsStore, - contract_state: StateStore, - ctrl_handler: impl FnOnce(), - mode: OperationMode, - ) -> Result { - ctrl_handler(); - - Ok(Self { - #[cfg(any( - all(feature = "local-mode", feature = "network-mode"), - all(not(feature = "local-mode"), not(feature = "network-mode")), - ))] - mode, - runtime: Runtime::build(contract_store, delegate_store, secret_store, false).unwrap(), - state_store: contract_state, - update_notifications: HashMap::default(), - subscriber_summaries: HashMap::default(), - delegate_attested_ids: HashMap::default(), - #[cfg(any( - not(feature = "local-mode"), - feature = "network-mode", - all(not(feature = "local-mode"), not(feature = "network-mode")) - ))] - event_loop_channel: None, - }) - } - pub fn register_contract_notifier( &mut self, key: ContractKey, @@ -1174,8 +1193,22 @@ impl Executor { #[cfg(test)] impl Executor { - pub async fn new_mock() -> Result { - todo!() + pub async fn new_mock(test: &str) -> Result { + let tmp_path = std::env::temp_dir().join(format!("freenet-executor-{test}")); + + let contracts_data_dir = tmp_path.join("contracts"); + let contract_store = ContractStore::new(contracts_data_dir, u16::MAX as i64)?; + + let state_store = StateStore::new(Storage::new().await?, u16::MAX as u32).unwrap(); + + let executor = Executor::new( + state_store, + || {}, + OperationMode::Local, + super::MockRuntime { contract_store }, + ) + .await?; + Ok(executor) } pub async fn handle_request<'a>( @@ -1215,6 +1248,14 @@ impl ContractExecutor for Executor { ) -> Result<(), crate::runtime::ContractError> { self.runtime.contract_store.store_contract(contract) } + + async fn upsert_contract_state( + &mut self, + _key: ContractKey, + _state: Either>, + ) -> Result { + todo!() + } } #[cfg(test)] @@ -1254,12 +1295,28 @@ impl ContractExecutor for Executor { self.runtime.contract_store.store_contract(contract)?; Ok(()) } + + async fn upsert_contract_state( + &mut self, + _key: ContractKey, + state: Either>, + ) -> Result { + // todo: instead allow to perform mutations per contract based on incoming value so we can track + // state values over the network + match state { + Either::Left(state) => Ok(state), + Either::Right(delta) => Ok(WrappedState::from(delta.as_ref())), + } + } } #[cfg(test)] mod test { use super::*; - use crate::runtime::{ContractStore, StateStore}; + use crate::{ + contract::MockRuntime, + runtime::{ContractStore, StateStore}, + }; #[tokio::test(flavor = "multi_thread")] async fn local_node_handle() -> Result<(), Box> { @@ -1270,14 +1327,12 @@ mod test { let state_store = StateStore::new(Storage::new().await?, MAX_MEM_CACHE).unwrap(); let mut counter = 0; Executor::new( - contract_store, - DelegateStore::default(), - SecretsStore::default(), state_store, || { counter += 1; }, OperationMode::Local, + MockRuntime { contract_store }, ) .await .expect("local node with handle"); diff --git a/crates/core/src/contract/handler.rs b/crates/core/src/contract/handler.rs index 311916723..eca64c1a0 100644 --- a/crates/core/src/contract/handler.rs +++ b/crates/core/src/contract/handler.rs @@ -141,19 +141,19 @@ impl ContractHandler for NetworkContractHandler { #[cfg(test)] impl ContractHandler for NetworkContractHandler { - type Builder = (); + type Builder = String; type ContractExecutor = Executor; fn build( channel: ContractHandlerToEventLoopChannel, _executor_request_sender: ExecutorToEventLoopChannel, - _builder: Self::Builder, + test: Self::Builder, ) -> BoxFuture<'static, Result> where Self: Sized + 'static, { - async { - let executor = Executor::new_mock().await?; + async move { + let executor = Executor::new_mock(&test).await?; Ok(Self { executor, channel }) } .boxed() diff --git a/crates/core/src/contract/in_memory.rs b/crates/core/src/contract/in_memory.rs index f390c7625..5374e65f2 100644 --- a/crates/core/src/contract/in_memory.rs +++ b/crates/core/src/contract/in_memory.rs @@ -15,7 +15,7 @@ use super::{ storages::in_memory::MemKVStore, Executor, }; -use crate::{config::Config, DynError}; +use crate::DynError; pub(crate) struct MockRuntime { pub contract_store: ContractStore, @@ -27,7 +27,7 @@ where { channel: ContractHandlerToEventLoopChannel, _kv_store: StateStore, - _runtime: MockRuntime, + runtime: Executor, } impl MemoryContractHandler @@ -35,40 +35,39 @@ where KVStore: StateStorage + Send + Sync + 'static, ::Error: Into>, { - const MAX_MEM_CACHE: i64 = 10_000_000; - - pub fn new( + pub async fn new( channel: ContractHandlerToEventLoopChannel, kv_store: KVStore, + data_dir: &str, ) -> Self { + // let rt = MockRuntime { + // contract_store: ContractStore::new( + // Config::conf().contracts_dir(), + // Self::MAX_MEM_CACHE, + // ) + // .unwrap(); MemoryContractHandler { channel, _kv_store: StateStore::new(kv_store, 10_000_000).unwrap(), - _runtime: MockRuntime { - contract_store: ContractStore::new( - Config::conf().contracts_dir(), - Self::MAX_MEM_CACHE, - ) - .unwrap(), - }, + runtime: Executor::new_mock(data_dir).await.unwrap(), } } } impl ContractHandler for MemoryContractHandler { - type Builder = (); + type Builder = String; type ContractExecutor = Executor; fn build( channel: ContractHandlerToEventLoopChannel, _executor_request_sender: ExecutorToEventLoopChannel, - _config: Self::Builder, + config: Self::Builder, ) -> BoxFuture<'static, Result> where Self: Sized + 'static, { let store = MemKVStore::new(); - async move { Ok(MemoryContractHandler::new(channel, store)) }.boxed() + async move { Ok(MemoryContractHandler::new(channel, store, &config).await) }.boxed() } fn channel(&mut self) -> &mut ContractHandlerToEventLoopChannel { @@ -85,7 +84,7 @@ impl ContractHandler for MemoryContractHandler { } fn executor(&mut self) -> &mut Self::ContractExecutor { - todo!() + &mut self.runtime } } diff --git a/crates/core/src/contract/storages/rocks_db.rs b/crates/core/src/contract/storages/rocks_db.rs index 90850728f..69d5be411 100644 --- a/crates/core/src/contract/storages/rocks_db.rs +++ b/crates/core/src/contract/storages/rocks_db.rs @@ -109,10 +109,11 @@ mod test { }; // Prepare and get handler for rocksdb - async fn get_handler() -> Result, DynError> { + async fn get_handler(test: &str) -> Result, DynError> { let (_, ch_handler) = contract_handler_channel(); let (_, executor_sender) = executor_channel_test(); - let handler = NetworkContractHandler::build(ch_handler, executor_sender, ()).await?; + let handler = + NetworkContractHandler::build(ch_handler, executor_sender, test.to_string()).await?; Ok(handler) } @@ -120,7 +121,7 @@ mod test { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn contract_handler() -> Result<(), DynError> { // Create a rocksdb handler and initialize the database - let mut handler = get_handler().await?; + let mut handler = get_handler("contract_handler").await?; // Generate a contract let contract_bytes = b"Test contract value".to_vec(); diff --git a/crates/core/src/contract/storages/sqlite.rs b/crates/core/src/contract/storages/sqlite.rs index 253181b5f..a51249ef8 100644 --- a/crates/core/src/contract/storages/sqlite.rs +++ b/crates/core/src/contract/storages/sqlite.rs @@ -152,10 +152,11 @@ mod test { }; // Prepare and get handler for an in-memory sqlite db - async fn get_handler() -> Result, DynError> { + async fn get_handler(test: &str) -> Result, DynError> { let (_, ch_handler) = contract_handler_channel(); let (_, executor_sender) = executor_channel_test(); - let handler = NetworkContractHandler::build(ch_handler, executor_sender, ()).await?; + let handler = + NetworkContractHandler::build(ch_handler, executor_sender, test.to_owned()).await?; Ok(handler) } @@ -163,7 +164,7 @@ mod test { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn contract_handler() -> Result<(), DynError> { // Create a sqlite handler and initialize the database - let mut handler = get_handler().await?; + let mut handler = get_handler("contract_handler").await?; // Generate a contract let contract_bytes = b"test contract value".to_vec(); diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 561fddd26..9a4b59790 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -38,5 +38,5 @@ pub mod dev_tool { pub use crate::config::Config; pub use client_events::{ClientEventsProxy, ClientId, OpenRequest}; pub use contract::{storages::Storage, Executor, OperationMode}; - pub use runtime::{ContractStore, DelegateStore, SecretsStore, StateStore}; + pub use runtime::{ContractStore, DelegateStore, Runtime, SecretsStore, StateStore}; } diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index aa71a80ec..909279c5f 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -629,51 +629,48 @@ where CM: ConnectionBridge + Send + Sync, { tracing::warn!("Failed tx `{}`, potentially attempting a retry", tx); - match tx.tx_type() { - TransactionType::JoinRing => { - const MSG: &str = "Fatal error: unable to connect to the network"; - // the attempt to join the network failed, this could be a fatal error since the node - // is useless without connecting to the network, we will retry with exponential backoff - match op_storage.pop(&tx) { - Some(OpEnum::JoinRing(op)) if op.has_backoff() => { - if let JoinRingOp { - backoff: Some(backoff), - gateway, - .. - } = *op - { - if cfg!(test) { - join_ring_request(None, peer_key, &gateway, op_storage, conn_manager) - .await?; - } else { - join_ring_request( - Some(backoff), - peer_key, - &gateway, - op_storage, - conn_manager, - ) + if let TransactionType::JoinRing = tx.tx_type() { + const MSG: &str = "Fatal error: unable to connect to the network"; + // the attempt to join the network failed, this could be a fatal error since the node + // is useless without connecting to the network, we will retry with exponential backoff + match op_storage.pop(&tx) { + Some(OpEnum::JoinRing(op)) if op.has_backoff() => { + if let JoinRingOp { + backoff: Some(backoff), + gateway, + .. + } = *op + { + if cfg!(test) { + join_ring_request(None, peer_key, &gateway, op_storage, conn_manager) .await?; - } - } - } - None | Some(OpEnum::JoinRing(_)) => { - let rand_gw = gateways - .shuffle() - .take(1) - .next() - .expect("at least one gateway"); - if !cfg!(test) { - tracing::error!("{}", MSG); } else { - tracing::debug!("{}", MSG); + join_ring_request( + Some(backoff), + peer_key, + &gateway, + op_storage, + conn_manager, + ) + .await?; } - join_ring_request(None, peer_key, rand_gw, op_storage, conn_manager).await?; } - _ => {} } + None | Some(OpEnum::JoinRing(_)) => { + let rand_gw = gateways + .shuffle() + .take(1) + .next() + .expect("at least one gateway"); + if !cfg!(test) { + tracing::error!("{}", MSG); + } else { + tracing::debug!("{}", MSG); + } + join_ring_request(None, peer_key, rand_gw, op_storage, conn_manager).await?; + } + _ => {} } - _ => unreachable!(), } Ok(()) } diff --git a/crates/core/src/node/p2p_impl.rs b/crates/core/src/node/p2p_impl.rs index a1fc3684d..256a8c913 100644 --- a/crates/core/src/node/p2p_impl.rs +++ b/crates/core/src/node/p2p_impl.rs @@ -211,7 +211,7 @@ mod test { NodeP2P::build::( config, event_log::TestEventListener::new(), - (), + "ping-listener".into(), ) .await?, ); @@ -228,7 +228,7 @@ mod test { let mut peer2 = NodeP2P::build::( config, event_log::TestEventListener::new(), - (), + "ping-dialer".into(), ) .await .unwrap(); diff --git a/crates/core/src/node/tests.rs b/crates/core/src/node/tests.rs index 86919eb44..d01e01fef 100644 --- a/crates/core/src/node/tests.rs +++ b/crates/core/src/node/tests.rs @@ -45,6 +45,7 @@ pub fn get_dynamic_port() -> u16 { /// A simulated in-memory network topology. pub(crate) struct SimNetwork { + name: String, pub labels: HashMap, pub event_listener: TestEventListener, user_ev_controller: Sender<(EventId, PeerKey)>, @@ -115,6 +116,7 @@ struct GatewayConfig { impl SimNetwork { pub async fn new( + name: &str, gateways: usize, nodes: usize, ring_max_htl: usize, @@ -125,6 +127,7 @@ impl SimNetwork { assert!(gateways > 0 && nodes > 0); let (user_ev_controller, receiver_ch) = channel((0, PeerKey::random())); let mut net = Self { + name: name.into(), event_listener: TestEventListener::new(), labels: HashMap::new(), user_ev_controller, @@ -196,7 +199,7 @@ impl SimNetwork { let gateway = NodeInMemory::build::( this_node, self.event_listener.clone(), - (), + format!("{}-{label}", self.name, label = this_config.label), ) .await .unwrap(); @@ -244,7 +247,7 @@ impl SimNetwork { let node = NodeInMemory::build::( config, self.event_listener.clone(), - (), + format!("{}-{label}", self.name), ) .await .unwrap(); diff --git a/crates/core/src/operations.rs b/crates/core/src/operations.rs index 53e953442..aefe865e3 100644 --- a/crates/core/src/operations.rs +++ b/crates/core/src/operations.rs @@ -9,6 +9,7 @@ use crate::{ message::{InnerMessage, Message, Transaction, TransactionType}, node::{ConnectionBridge, ConnectionError, OpManager, PeerKey}, ring::{Location, PeerKeyLocation, RingError}, + DynError, }; pub(crate) mod get; @@ -183,6 +184,8 @@ pub(crate) enum OpError { RingError(#[from] RingError), #[error(transparent)] ContractError(#[from] ContractError), + #[error(transparent)] + ExecutorError(DynError), #[error("unexpected operation state")] UnexpectedOpState, diff --git a/crates/core/src/operations/get.rs b/crates/core/src/operations/get.rs index 33f8af4a4..fb553f746 100644 --- a/crates/core/src/operations/get.rs +++ b/crates/core/src/operations/get.rs @@ -293,8 +293,8 @@ impl Operation for GetOp { return_msg = None; new_state = None; } else if let ContractHandlerEvent::GetResponse { - response: value, key: returned_key, + response: value, } = op_storage .notify_contract_handler( ContractHandlerEvent::GetQuery { @@ -330,10 +330,17 @@ impl Operation for GetOp { Some(GetState::ReceivedRequest) => { tracing::debug!("Returning contract {} to {}", key, sender.peer); new_state = None; + let value = match value { + Ok(res) => res, + Err(err) => { + tracing::error!("error: {err}"); + return Err(OpError::ExecutorError(err)); + } + }; return_msg = Some(GetMsg::ReturnGet { id, key, - value: value.unwrap(), + value, sender: target, target: sender, }); @@ -842,7 +849,16 @@ mod test { let get_specs = HashMap::from_iter([("node-0".into(), node_0), ("gateway-0".into(), gw_0)]); // establish network - let mut sim_nodes = SimNetwork::new(NUM_GW, NUM_NODES, 3, 2, 4, 2).await; + let mut sim_nodes = SimNetwork::new( + "successful_get_op_between_nodes", + NUM_GW, + NUM_NODES, + 3, + 2, + 4, + 2, + ) + .await; sim_nodes.build_with_specs(get_specs).await; check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(3)).await?; @@ -881,7 +897,8 @@ mod test { let get_specs = HashMap::from_iter([("node-1".into(), node_1)]); // establish network - let mut sim_nodes = SimNetwork::new(NUM_GW, NUM_NODES, 3, 2, 4, 2).await; + let mut sim_nodes = + SimNetwork::new("get_contract_not_found", NUM_GW, NUM_NODES, 3, 2, 4, 2).await; sim_nodes.build_with_specs(get_specs).await; check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(3)).await?; @@ -942,7 +959,16 @@ mod test { ]); // establish network - let mut sim_nodes = SimNetwork::new(NUM_GW, NUM_NODES, 3, 2, 4, 3).await; + let mut sim_nodes = SimNetwork::new( + "get_contract_found_after_retry", + NUM_GW, + NUM_NODES, + 3, + 2, + 4, + 3, + ) + .await; sim_nodes.build_with_specs(get_specs).await; check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(3)).await?; diff --git a/crates/core/src/operations/join_ring.rs b/crates/core/src/operations/join_ring.rs index 936751a87..261597409 100644 --- a/crates/core/src/operations/join_ring.rs +++ b/crates/core/src/operations/join_ring.rs @@ -1003,7 +1003,7 @@ mod test { #[ignore] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn one_node_connects_to_gw() { - let mut sim_nodes = SimNetwork::new(1, 1, 1, 1, 2, 2).await; + let mut sim_nodes = SimNetwork::new("join_one_node_connects_to_gw", 1, 1, 1, 1, 2, 2).await; sim_nodes.build().await; tokio::time::sleep(Duration::from_secs(3)).await; assert!(sim_nodes.connected(&"node-0".into())); @@ -1015,7 +1015,16 @@ mod test { async fn forward_connection_to_node() -> Result<(), anyhow::Error> { const NUM_NODES: usize = 10usize; const NUM_GW: usize = 1usize; - let mut sim_nodes = SimNetwork::new(NUM_GW, NUM_NODES, 3, 2, 4, 2).await; + let mut sim_nodes = SimNetwork::new( + "join_forward_connection_to_node", + NUM_GW, + NUM_NODES, + 3, + 2, + 4, + 2, + ) + .await; sim_nodes.build().await; check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(3)).await } @@ -1026,7 +1035,16 @@ mod test { async fn all_nodes_should_connect() -> Result<(), anyhow::Error> { const NUM_NODES: usize = 10usize; const NUM_GW: usize = 1usize; - let mut sim_nodes = SimNetwork::new(NUM_GW, NUM_NODES, 3, 2, 1000, 2).await; + let mut sim_nodes = SimNetwork::new( + "join_all_nodes_should_connect", + NUM_GW, + NUM_NODES, + 3, + 2, + 1000, + 2, + ) + .await; sim_nodes.build().await; check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(10)).await } diff --git a/crates/core/src/operations/put.rs b/crates/core/src/operations/put.rs index 4a157ac2d..cf712d67b 100644 --- a/crates/core/src/operations/put.rs +++ b/crates/core/src/operations/put.rs @@ -916,7 +916,16 @@ mod test { let contract_val: WrappedState = gen.arbitrary()?; let new_value = WrappedState::new(Vec::from_iter(gen.arbitrary::<[u8; 20]>().unwrap())); - let mut sim_nodes = SimNetwork::new(NUM_GW, NUM_NODES, 3, 2, 4, 2).await; + let mut sim_nodes = SimNetwork::new( + "successful_put_op_between_nodes", + NUM_GW, + NUM_NODES, + 3, + 2, + 4, + 2, + ) + .await; let mut locations = sim_nodes.get_locations_by_node(); let node0_loc = locations.remove(&"node-0".into()).unwrap(); let node1_loc = locations.remove(&"node-1".into()).unwrap(); diff --git a/crates/core/src/operations/subscribe.rs b/crates/core/src/operations/subscribe.rs index 5b02883cb..14de3c74e 100644 --- a/crates/core/src/operations/subscribe.rs +++ b/crates/core/src/operations/subscribe.rs @@ -509,7 +509,16 @@ mod test { ("node-0".into(), first_node), ("node-1".into(), second_node), ]); - let mut sim_nodes = SimNetwork::new(NUM_GW, NUM_NODES, 3, 2, 4, 2).await; + let mut sim_nodes = SimNetwork::new( + "successful_subscribe_op_between_nodes", + NUM_GW, + NUM_NODES, + 3, + 2, + 4, + 2, + ) + .await; sim_nodes.build_with_specs(subscribe_specs).await; check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(3)).await?; diff --git a/crates/core/src/ring.rs b/crates/core/src/ring.rs index bd31be024..a5378be51 100644 --- a/crates/core/src/ring.rs +++ b/crates/core/src/ring.rs @@ -484,7 +484,8 @@ impl Eq for Location {} impl Ord for Location { fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.partial_cmp(other) + self.0 + .partial_cmp(&other.0) .expect("always should return a cmp value") } } diff --git a/crates/core/src/runtime/mod.rs b/crates/core/src/runtime/mod.rs index 72a6a7a6e..590417d5d 100644 --- a/crates/core/src/runtime/mod.rs +++ b/crates/core/src/runtime/mod.rs @@ -21,4 +21,4 @@ pub(crate) use error::RuntimeResult; pub use secrets_store::SecretsStore; pub use state_store::StateStore; pub(crate) use state_store::{StateStorage, StateStoreError}; -pub(crate) use wasm_runtime::{ContractExecError, Runtime}; +pub use wasm_runtime::{ContractExecError, Runtime}; diff --git a/crates/fdev/src/commands.rs b/crates/fdev/src/commands.rs index a61a0c508..b323044b2 100644 --- a/crates/fdev/src/commands.rs +++ b/crates/fdev/src/commands.rs @@ -167,16 +167,10 @@ async fn execute_command( let contract_store = ContractStore::new(contracts_data_path, DEFAULT_MAX_CONTRACT_SIZE)?; let delegate_store = DelegateStore::new(delegates_data_path, DEFAULT_MAX_DELEGATE_SIZE)?; let secret_store = SecretsStore::new(secrets_data_path)?; - let state_store = StateStore::new(Storage::new().await?, MAX_MEM_CACHE).unwrap(); - let mut executor = Executor::new( - contract_store, - delegate_store, - secret_store, - state_store, - || {}, - OperationMode::Local, - ) - .await?; + let state_store = StateStore::new(Storage::new().await?, MAX_MEM_CACHE)?; + let rt = + freenet::dev_tool::Runtime::build(contract_store, delegate_store, secret_store, false)?; + let mut executor = Executor::new(state_store, || {}, OperationMode::Local, rt).await?; executor .handle_request(ClientId::FIRST, request, None) diff --git a/crates/fdev/src/local_node/state.rs b/crates/fdev/src/local_node/state.rs index 0ded9ce22..9a25ff836 100644 --- a/crates/fdev/src/local_node/state.rs +++ b/crates/fdev/src/local_node/state.rs @@ -24,17 +24,22 @@ impl AppState { let contract_dir = Config::conf().contracts_dir(); let contract_store = ContractStore::new(contract_dir, config.max_contract_size)?; let state_store = StateStore::new(Storage::new().await?, Self::MAX_MEM_CACHE).unwrap(); + let rt = freenet::dev_tool::Runtime::build( + contract_store, + DelegateStore::default(), + SecretsStore::default(), + false, + ) + .unwrap(); Ok(AppState { local_node: Arc::new(RwLock::new( Executor::new( - contract_store, - DelegateStore::default(), - SecretsStore::default(), state_store, || { freenet::util::set_cleanup_on_exit().unwrap(); }, OperationMode::Local, + rt, ) .await?, )), From aea5907fc8e9228abc0c7ea84bfa3b9a82f7c843 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Mon, 16 Oct 2023 15:04:51 +0200 Subject: [PATCH 55/76] Get the `get` sim network tests to pass --- crates/core/src/bin/freenet.rs | 13 +--- crates/core/src/client_events.rs | 11 ++- crates/core/src/config.rs | 14 ++++ crates/core/src/contract.rs | 10 ++- crates/core/src/contract/executor.rs | 20 +++-- crates/core/src/contract/handler.rs | 51 ++++++++++++- crates/core/src/node.rs | 18 +++-- .../core/src/node/conn_manager/p2p_protoc.rs | 1 + crates/core/src/node/in_memory_impl.rs | 23 +++--- crates/core/src/node/tests.rs | 65 ++++++++++++----- crates/core/src/operations/get.rs | 12 +-- crates/core/src/operations/join_ring.rs | 73 ++++++++++--------- crates/core/src/operations/put.rs | 44 ++++++++--- crates/core/src/ring.rs | 1 - crates/core/src/runtime/delegate.rs | 2 +- crates/core/src/runtime/error.rs | 4 +- crates/core/src/runtime/state_store.rs | 11 +++ crates/core/src/runtime/tests/mod.rs | 2 +- crates/fdev/src/main.rs | 5 +- 19 files changed, 266 insertions(+), 114 deletions(-) diff --git a/crates/core/src/bin/freenet.rs b/crates/core/src/bin/freenet.rs index aef4f7f60..da78c34eb 100644 --- a/crates/core/src/bin/freenet.rs +++ b/crates/core/src/bin/freenet.rs @@ -1,8 +1,6 @@ use clap::Parser; use freenet::local_node::{Executor, NodeConfig, OperationMode}; use std::net::SocketAddr; -use tracing::metadata::LevelFilter; -use tracing_subscriber::EnvFilter; type DynError = Box; @@ -23,16 +21,7 @@ async fn run_local(config: NodeConfig) -> Result<(), DynError> { } fn main() -> Result<(), DynError> { - tracing_subscriber::fmt() - .with_level(true) - .with_file(true) - .with_line_number(true) - .with_env_filter( - EnvFilter::builder() - .with_default_directive(LevelFilter::INFO.into()) - .from_env_lossy(), - ) - .init(); + freenet::config::set_logger(); let config = NodeConfig::parse(); let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(4) diff --git a/crates/core/src/client_events.rs b/crates/core/src/client_events.rs index a6aca2218..22137b264 100644 --- a/crates/core/src/client_events.rs +++ b/crates/core/src/client_events.rs @@ -99,6 +99,16 @@ pub struct OpenRequest<'a> { pub token: Option, } +impl Display for OpenRequest<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "client request {{ client: {}, req: {} }}", + &self.client_id, &*self.request + ) + } +} + impl<'a> OpenRequest<'a> { pub fn into_owned(self) -> OpenRequest<'static> { OpenRequest { @@ -289,7 +299,6 @@ pub(crate) mod test { return Ok(res.into_owned()); } } else { - tracing::debug!("sender half of user event gen dropped"); // probably the process finished, wait for a bit and then kill the thread tokio::time::sleep(Duration::from_secs(1)).await; panic!("finished orphan background thread"); diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index 8a36a3b1e..f5cd1167a 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -323,6 +323,20 @@ impl libp2p::swarm::Executor for GlobalExecutor { } } +pub fn set_logger() { + tracing_subscriber::fmt() + .with_level(true) + .with_file(true) + .with_line_number(true) + .with_env_filter( + tracing_subscriber::EnvFilter::builder() + .with_default_directive(tracing_subscriber::filter::LevelFilter::DEBUG.into()) + .from_env_lossy() + .add_directive("stretto=off".parse().unwrap()), + ) + .init(); +} + pub(super) mod tracer { use super::*; diff --git a/crates/core/src/contract.rs b/crates/core/src/contract.rs index 419282147..f4198e5a7 100644 --- a/crates/core/src/contract.rs +++ b/crates/core/src/contract.rs @@ -23,6 +23,7 @@ pub use executor::{Executor, ExecutorError, OperationMode}; use executor::ContractExecutor; +#[tracing::instrument(skip_all)] pub(crate) async fn contract_handling<'a, CH>(mut contract_handler: CH) -> Result<(), ContractError> // todo: remove result where @@ -30,6 +31,7 @@ where { loop { let (id, event) = contract_handler.channel().recv_from_event_loop().await?; + tracing::debug!(%event, "got contract handling event"); match event { ContractHandlerEvent::GetQuery { key, @@ -87,10 +89,14 @@ where } } } - ContractHandlerEvent::PutQuery { key, state } => { + ContractHandlerEvent::PutQuery { + key, + state, + parameters, + } => { let put_result = contract_handler .executor() - .upsert_contract_state(key, Either::Left(state)) + .upsert_contract_state(key, Either::Left(state), parameters) .await .map_err(Into::into); contract_handler diff --git a/crates/core/src/contract/executor.rs b/crates/core/src/contract/executor.rs index e201cc444..e5fc49686 100644 --- a/crates/core/src/contract/executor.rs +++ b/crates/core/src/contract/executor.rs @@ -241,6 +241,7 @@ pub(crate) trait ContractExecutor: Send + Sync + 'static { &mut self, key: ContractKey, state: Either>, + params: Option>, ) -> Result; } @@ -1193,8 +1194,8 @@ impl Executor { #[cfg(test)] impl Executor { - pub async fn new_mock(test: &str) -> Result { - let tmp_path = std::env::temp_dir().join(format!("freenet-executor-{test}")); + pub async fn new_mock(data_dir: &str) -> Result { + let tmp_path = std::env::temp_dir().join(format!("freenet-executor-{data_dir}")); let contracts_data_dir = tmp_path.join("contracts"); let contract_store = ContractStore::new(contracts_data_dir, u16::MAX as i64)?; @@ -1253,6 +1254,7 @@ impl ContractExecutor for Executor { &mut self, _key: ContractKey, _state: Either>, + _params: Option>, ) -> Result { todo!() } @@ -1298,14 +1300,20 @@ impl ContractExecutor for Executor { async fn upsert_contract_state( &mut self, - _key: ContractKey, + key: ContractKey, state: Either>, + params: Option>, ) -> Result { // todo: instead allow to perform mutations per contract based on incoming value so we can track // state values over the network - match state { - Either::Left(state) => Ok(state), - Either::Right(delta) => Ok(WrappedState::from(delta.as_ref())), + match (state, params) { + (Either::Left(state), Some(params)) => { + self.state_store + .store(key, state.clone(), params.into_owned()) + .await?; + return Ok(state); + } + _ => todo!(), } } } diff --git a/crates/core/src/contract/handler.rs b/crates/core/src/contract/handler.rs index eca64c1a0..e4b94593d 100644 --- a/crates/core/src/contract/handler.rs +++ b/crates/core/src/contract/handler.rs @@ -147,13 +147,13 @@ impl ContractHandler for NetworkContractHandler { fn build( channel: ContractHandlerToEventLoopChannel, _executor_request_sender: ExecutorToEventLoopChannel, - test: Self::Builder, + data_dir: Self::Builder, ) -> BoxFuture<'static, Result> where Self: Sized + 'static, { async move { - let executor = Executor::new_mock(&test).await?; + let executor = Executor::new_mock(&data_dir).await?; Ok(Self { executor, channel }) } .boxed() @@ -344,6 +344,7 @@ pub(crate) enum ContractHandlerEvent { PutQuery { key: ContractKey, state: WrappedState, + parameters: Option>, }, /// The response to a push query. PutResponse { @@ -365,6 +366,52 @@ pub(crate) enum ContractHandlerEvent { CacheResult(Result<(), ContractError>), } +impl std::fmt::Display for ContractHandlerEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ContractHandlerEvent::PutQuery { + key, + state, + parameters, + } => { + if let Some(params) = parameters { + write!(f, "put query {{ {key}, params: {:?} }}", params.as_ref()) + } else { + write!(f, "put query {{ {key} }}") + } + } + ContractHandlerEvent::PutResponse { new_value } => match new_value { + Ok(v) => { + write!(f, "put query response {{ {v} }}",) + } + Err(e) => { + write!(f, "put query failed {{ {e} }}",) + } + }, + ContractHandlerEvent::GetQuery { + key, + fetch_contract, + } => { + write!(f, "get query {{ {key}, fetch contract: {fetch_contract} }}",) + } + ContractHandlerEvent::GetResponse { key, response } => match response { + Ok(v) => { + write!(f, "get query response {{ {key} }}",) + } + Err(e) => { + write!(f, "get query failed {{ {key} }}",) + } + }, + ContractHandlerEvent::Cache(container) => { + write!(f, "caching {{ {} }}", container.key()) + } + ContractHandlerEvent::CacheResult(r) => { + write!(f, "caching result {{ {} }}", r.is_ok()) + } + } + } +} + impl ContractHandlerEvent { pub async fn into_network_op(self, op_manager: &OpManager) -> Transaction { todo!() diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index 909279c5f..28c6b74c3 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -307,6 +307,7 @@ where } /// Process client events. +#[tracing::instrument(skip_all)] async fn client_event_handling( op_storage: Arc, mut client_events: ClientEv, @@ -318,7 +319,10 @@ async fn client_event_handling( tokio::select! { client_request = client_events.recv() => { let req = match client_request { - Ok(req) => req, + Ok(req) => { + tracing::debug!(%req, "got client request event"); + req + } Err(err) => { tracing::debug!(error = %err, "client error"); continue; @@ -332,6 +336,9 @@ async fn client_event_handling( } res = client_responses.recv() => { if let Some((cli_id, res)) = res { + if let Ok(res) = &res { + tracing::debug!(%res, "sending client response"); + } if let Err(err) = client_events.send(cli_id, res).await { tracing::error!("channel closed: {err}"); break; @@ -438,9 +445,9 @@ async fn process_open_request(request: OpenRequest<'static>, op_storage: Arc { tracing::debug!( - concat!("Handling ", $op, " get request @ {} (tx: {})"), + tx = %$id, + concat!("Handling ", $op, " get request @ {}"), $op_storage.ring.peer_key, - $id ); }; } @@ -504,11 +511,12 @@ async fn report_result( } Ok(None) => {} Err(err) => { - tracing::debug!("Finished tx w/ error: {}", err) + tracing::debug!("Finished transaction with error: {}", err) } } } +#[tracing::instrument(name = "process_network_message", skip_all)] async fn process_message( msg: Result, op_storage: Arc, @@ -628,7 +636,7 @@ async fn handle_cancelled_op( where CM: ConnectionBridge + Send + Sync, { - tracing::warn!("Failed tx `{}`, potentially attempting a retry", tx); + tracing::warn!(%tx, "Failed transaction, potentially attempting a retry"); if let TransactionType::JoinRing = tx.tx_type() { const MSG: &str = "Fatal error: unable to connect to the network"; // the attempt to join the network failed, this could be a fatal error since the node diff --git a/crates/core/src/node/conn_manager/p2p_protoc.rs b/crates/core/src/node/conn_manager/p2p_protoc.rs index bc43a77b6..cef6b86ee 100644 --- a/crates/core/src/node/conn_manager/p2p_protoc.rs +++ b/crates/core/src/node/conn_manager/p2p_protoc.rs @@ -232,6 +232,7 @@ impl P2pConnManager { Ok(()) } + #[tracing::instrument(name = "network_event_listener", skip_all)] pub async fn run_event_listener( mut self, op_manager: Arc, diff --git a/crates/core/src/node/in_memory_impl.rs b/crates/core/src/node/in_memory_impl.rs index 13a12491a..704ab10ea 100644 --- a/crates/core/src/node/in_memory_impl.rs +++ b/crates/core/src/node/in_memory_impl.rs @@ -15,7 +15,8 @@ use crate::{ config::GlobalExecutor, contract::{ self, executor_channel, ClientResponsesSender, ContractError, ContractHandler, - ContractHandlerEvent, ExecutorToEventLoopChannel, NetworkEventListenerHalve, + ContractHandlerEvent, ExecutorToEventLoopChannel, MemoryContractHandler, + NetworkEventListenerHalve, }, message::{Message, NodeEvent, TransactionType}, node::NodeBuilder, @@ -37,14 +38,11 @@ pub(super) struct NodeInMemory { impl NodeInMemory { /// Buils an in-memory node. Does nothing upon construction, - pub async fn build( + pub async fn build( builder: NodeBuilder<1>, event_listener: EL, - ch_builder: CH::Builder, - ) -> Result - where - CH: ContractHandler + Send + Sync + 'static, - { + ch_builder: String, + ) -> Result { let peer_key = PeerKey::from(builder.local_key.public()); let conn_manager = MemoryConnManager::new(peer_key); let gateways = builder.get_gateways()?; @@ -55,9 +53,10 @@ impl NodeInMemory { let (ops_ch_channel, ch_channel) = contract::contract_handler_channel(); let op_storage = Arc::new(OpManager::new(ring, notification_tx, ops_ch_channel)); let (_executor_listener, executor_sender) = executor_channel(op_storage.clone()); - let contract_handler = CH::build(ch_channel, executor_sender, ch_builder) - .await - .map_err(|e| anyhow::anyhow!(e))?; + let contract_handler = + MemoryContractHandler::build(ch_channel, executor_sender, ch_builder) + .await + .map_err(|e| anyhow::anyhow!(e))?; GlobalExecutor::spawn(contract::contract_handling(contract_handler)); @@ -106,7 +105,8 @@ impl NodeInMemory { contract_subscribers: HashMap>, ) -> Result<(), ContractError> { for (contract, state) in contracts { - let key = contract.key(); + let key: ContractKey = contract.key(); + let parameters = contract.params(); self.op_storage .notify_contract_handler(ContractHandlerEvent::Cache(contract.clone()), None) .await?; @@ -115,6 +115,7 @@ impl NodeInMemory { ContractHandlerEvent::PutQuery { key: key.clone(), state, + parameters: Some(parameters), }, None, ) diff --git a/crates/core/src/node/tests.rs b/crates/core/src/node/tests.rs index d01e01fef..c4ca5e028 100644 --- a/crates/core/src/node/tests.rs +++ b/crates/core/src/node/tests.rs @@ -17,7 +17,6 @@ use tracing::{info, instrument}; use crate::{ client_events::test::MemoryEventsGen, config::GlobalExecutor, - contract::MemoryContractHandler, node::{event_log::TestEventListener, InitPeerNode, NodeBuilder, NodeInMemory}, ring::{Distance, Location, PeerKeyLocation}, }; @@ -43,21 +42,6 @@ pub fn get_dynamic_port() -> u16 { rand::thread_rng().gen_range(FIRST_DYNAMIC_PORT..LAST_DYNAMIC_PORT) } -/// A simulated in-memory network topology. -pub(crate) struct SimNetwork { - name: String, - pub labels: HashMap, - pub event_listener: TestEventListener, - user_ev_controller: Sender<(EventId, PeerKey)>, - receiver_ch: Receiver<(EventId, PeerKey)>, - gateways: Vec<(NodeInMemory, GatewayConfig)>, - nodes: Vec<(NodeInMemory, NodeLabel)>, - ring_max_htl: usize, - rnd_if_htl_above: usize, - max_connections: usize, - min_connections: usize, -} - pub(crate) type EventId = usize; #[derive(PartialEq, Eq, Hash, Clone)] @@ -93,6 +77,16 @@ impl std::ops::Deref for NodeLabel { impl<'a> From<&'a str> for NodeLabel { fn from(value: &'a str) -> Self { + assert!(value.starts_with("gateway-") || value.starts_with("node-")); + let mut parts = value.split('-'); + assert!(parts.next().is_some()); + assert!(parts + .next() + .map(|s| s.parse::()) + .transpose() + .expect("should be an u16") + .is_some()); + assert!(parts.next().is_none()); Self(value.to_string().into()) } } @@ -114,6 +108,22 @@ struct GatewayConfig { location: Location, } +/// A simulated in-memory network topology. +pub(crate) struct SimNetwork { + name: String, + debug: bool, + pub labels: HashMap, + pub event_listener: TestEventListener, + user_ev_controller: Sender<(EventId, PeerKey)>, + receiver_ch: Receiver<(EventId, PeerKey)>, + gateways: Vec<(NodeInMemory, GatewayConfig)>, + nodes: Vec<(NodeInMemory, NodeLabel)>, + ring_max_htl: usize, + rnd_if_htl_above: usize, + max_connections: usize, + min_connections: usize, +} + impl SimNetwork { pub async fn new( name: &str, @@ -128,6 +138,7 @@ impl SimNetwork { let (user_ev_controller, receiver_ch) = channel((0, PeerKey::random())); let mut net = Self { name: name.into(), + debug: false, event_listener: TestEventListener::new(), labels: HashMap::new(), user_ev_controller, @@ -144,6 +155,11 @@ impl SimNetwork { net } + #[allow(unused)] + pub fn debug(&mut self) { + self.debug = true; + } + #[instrument(skip(self))] async fn build_gateways(&mut self, num: usize) { info!("Building {} gateways", num); @@ -196,7 +212,7 @@ impl SimNetwork { .listening_port(*port), ); } - let gateway = NodeInMemory::build::( + let gateway = NodeInMemory::build( this_node, self.event_listener.clone(), format!("{}-{label}", self.name, label = this_config.label), @@ -209,6 +225,7 @@ impl SimNetwork { #[instrument(skip(self))] async fn build_nodes(&mut self, num: usize) { + info!("Building {} regular nodes", num); let gateways: Vec<_> = self .gateways .iter() @@ -244,7 +261,7 @@ impl SimNetwork { self.event_listener .add_node(label.clone(), PeerKey::from(id)); - let node = NodeInMemory::build::( + let node = NodeInMemory::build( config, self.event_listener.clone(), format!("{}-{label}", self.name), @@ -387,6 +404,18 @@ impl SimNetwork { } } +impl Drop for SimNetwork { + fn drop(&mut self) { + if !self.debug { + for label in self.labels.keys() { + let p = std::env::temp_dir() + .join(format!("freenet-executor-{sim}-{label}", sim = self.name)); + let _ = std::fs::remove_dir_all(p); + } + } + } +} + fn group_locations_in_buckets( locs: impl IntoIterator, scale: i32, diff --git a/crates/core/src/operations/get.rs b/crates/core/src/operations/get.rs index fb553f746..30e0d8fc7 100644 --- a/crates/core/src/operations/get.rs +++ b/crates/core/src/operations/get.rs @@ -203,7 +203,7 @@ impl Operation for GetOp { self.state, Some(GetState::AwaitingResponse { .. }) )); - tracing::debug!("Seek contract {} @ {} (tx: {})", key, target.peer, id); + tracing::debug!(tx = %id, "Seek contract {} @ {}", key, target.peer); new_state = self.state; stats = Some(GetStats { contract_location: Location::from(&key), @@ -495,11 +495,13 @@ impl Operation for GetOp { } } + let parameters = contract.as_ref().map(|c| c.params()); op_storage .notify_contract_handler( ContractHandlerEvent::PutQuery { key: key.clone(), state: value.clone(), + parameters, }, client_id, ) @@ -683,9 +685,9 @@ pub(crate) async fn request_get( return Err(OpError::UnexpectedOpState); }; tracing::debug!( - "Preparing get contract request to {} (tx: {})", + tx = %id, + "Preparing get contract request to {}", target.peer, - id ); match get_op.state { @@ -866,12 +868,11 @@ mod test { sim_nodes .trigger_event(&"node-0".into(), 1, Some(Duration::from_millis(100))) .await?; - tokio::time::sleep(Duration::from_millis(100)).await; + tokio::time::sleep(Duration::from_millis(200)).await; assert!(sim_nodes.has_got_contract(&"node-0".into(), &key)); Ok(()) } - #[ignore] #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn contract_not_found() -> Result<(), anyhow::Error> { const NUM_NODES: usize = 2usize; @@ -910,7 +911,6 @@ mod test { Ok(()) } - #[ignore] #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn contract_found_after_retry() -> Result<(), anyhow::Error> { const NUM_NODES: usize = 2usize; diff --git a/crates/core/src/operations/join_ring.rs b/crates/core/src/operations/join_ring.rs index 261597409..03c5ba263 100644 --- a/crates/core/src/operations/join_ring.rs +++ b/crates/core/src/operations/join_ring.rs @@ -119,6 +119,7 @@ impl Operation for JoinRingOp { } => { // likely a gateway which accepts connections tracing::debug!( + tx = %id, "Initial join request received from {} with HTL {} @ {}", req_peer, hops_to_live, @@ -128,10 +129,10 @@ impl Operation for JoinRingOp { let new_location = Location::random(); // FIXME: don't try to forward to peers which have already been tried (add a rejected_by list) let accepted_by = if op_storage.ring.should_accept(&new_location) { - tracing::debug!("Accepting connection from {}", req_peer,); + tracing::debug!(tx = %id, "Accepting connection from {}", req_peer,); HashSet::from_iter([this_node_loc]) } else { - tracing::debug!("Rejecting connection from peer {}", req_peer); + tracing::debug!(tx = %id, "Rejecting connection from peer {}", req_peer); HashSet::new() }; @@ -151,9 +152,9 @@ impl Operation for JoinRingOp { .await? { tracing::debug!( - "Awaiting proxy response from @ {} (tx: {})", + tx = %id, + "Awaiting proxy response from @ {}", this_node_loc.peer, - id ); updated_state.add_new_proxy(accepted_by)?; // awaiting responses from proxies @@ -162,6 +163,7 @@ impl Operation for JoinRingOp { } else { if !accepted_by.is_empty() { tracing::debug!( + tx = %id, "OC received at gateway {} from requesting peer {}", this_node_loc.peer, req_peer @@ -196,6 +198,7 @@ impl Operation for JoinRingOp { } => { let own_loc = op_storage.ring.own_location(); tracing::debug!( + tx = %id, "Proxy join request received from {} to join new peer {} with HTL {} @ {}", sender.peer, joiner.peer, @@ -206,10 +209,11 @@ impl Operation for JoinRingOp { .ring .should_accept(&joiner.location.ok_or(ConnectionError::LocationUnknown)?) { - tracing::debug!("Accepting proxy connection from {}", joiner.peer); + tracing::debug!(tx = %id, "Accepting proxy connection from {}", joiner.peer); HashSet::from_iter([own_loc]) } else { tracing::debug!( + tx = %id, "Not accepting new proxy connection for sender {}", joiner.peer ); @@ -261,6 +265,7 @@ impl Operation for JoinRingOp { if match_target { new_state = Some(JRState::OCReceived); tracing::debug!( + tx = %id, "Sending response to join request with all the peers that accepted \ connection from gateway {} to peer {}", sender.peer, @@ -284,6 +289,7 @@ impl Operation for JoinRingOp { // connections alive and prune any dead connections new_state = Some(JRState::Connected); tracing::debug!( + tx = %id, "Sending response to join request with all the peers that accepted \ connection from proxy peer {} to proxy peer {}", sender.peer, @@ -319,7 +325,7 @@ impl Operation for JoinRingOp { }, .. } => { - tracing::debug!("Join response received from {}", sender.peer); + tracing::debug!(tx = %id, "Join response received from {}", sender.peer); // Set the given location let pk_loc = PeerKeyLocation { @@ -331,6 +337,7 @@ impl Operation for JoinRingOp { Some(JRState::Connecting(ConnectionInfo { gateway, .. })) => { if !accepted_by.clone().is_empty() { tracing::debug!( + tx = %id, "OC received and acknowledged at requesting peer {} from gateway {}", your_peer_id, gateway.peer @@ -372,17 +379,17 @@ impl Operation for JoinRingOp { target, msg: JoinResponse::Proxy { mut accepted_by }, } => { - tracing::debug!("Received proxy join at @ {}", target.peer); + tracing::debug!(tx = %id, "Received proxy join at @ {}", target.peer); match self.state { Some(JRState::Initializing) => { // the sender of the response is the target of the request and // is only a completed tx if it accepted the connection if accepted_by.contains(&sender) { tracing::debug!( - "Return to {}, connected at proxy {} (tx: {})", + tx = %id, + "Return to {}, connected at proxy {}", target.peer, sender.peer, - id ); new_state = Some(JRState::Connected); } else { @@ -423,6 +430,7 @@ impl Operation for JoinRingOp { if is_target_peer { tracing::debug!( + tx = %id, "Sending response to join request with all the peers that accepted \ connection from gateway {} to peer {}", target.peer, @@ -440,6 +448,7 @@ impl Operation for JoinRingOp { }); } else { tracing::debug!( + tx = %id, "Sending response to join request with all the peers that accepted \ connection from proxy peer {} to proxy peer {}", target.peer, @@ -472,7 +481,7 @@ impl Operation for JoinRingOp { } => { match self.state { Some(JRState::OCReceived) => { - tracing::debug!("Acknowledge connected at gateway"); + tracing::debug!(tx = %id, "Acknowledge connected at gateway"); new_state = Some(JRState::Connected); return_msg = Some(JoinRingMsg::Connected { id, @@ -491,7 +500,7 @@ impl Operation for JoinRingOp { sender.location.ok_or(ConnectionError::LocationUnknown)?, sender.peer, ); - tracing::debug!("Opened connection with peer {}", by_peer.peer); + tracing::debug!(tx = %id, "Opened connection with peer {}", by_peer.peer); new_state = None; } }; @@ -499,7 +508,7 @@ impl Operation for JoinRingOp { JoinRingMsg::Connected { target, sender, id } => { match self.state { Some(JRState::OCReceived) => { - tracing::debug!("Acknowledge connected at peer"); + tracing::debug!(tx = %id, "Acknowledge connected at peer {}", target.peer); new_state = Some(JRState::Connected); return_msg = None; } @@ -510,6 +519,7 @@ impl Operation for JoinRingOp { return Err(OpError::InvalidStateTransition(id)); } else { tracing::info!( + tx = %id, "Successfully completed connection @ {}, new location = {:?}", target.peer, op_storage.ring.own_location().location @@ -567,14 +577,14 @@ fn try_proxy_connection( ) -> (Option, Option) { let new_state = if accepted_by.contains(own_loc) { tracing::debug!( - "Return to {}, connected at proxy {} (tx: {})", + tx = %id, + "Return to {}, connected at proxy {}", sender.peer, own_loc.peer, - id ); Some(JRState::Connected) } else { - tracing::debug!("Failed to connect at proxy {}", sender.peer); + tracing::debug!(tx = %id, "Failed to connect at proxy {}", sender.peer); None }; let return_msg = Some(JoinRingMsg::Response { @@ -593,12 +603,13 @@ async fn propagate_oc_to_accepted_peers( other_peer: &PeerKeyLocation, msg: JoinRingMsg, ) -> Result<(), OpError> { + let id = msg.id(); if op_storage.ring.should_accept( &other_peer .location .ok_or(ConnectionError::LocationUnknown)?, ) { - tracing::info!("Established connection to {}", other_peer.peer); + tracing::info!(tx = %id, "Establishing connection to {}", other_peer.peer); conn_manager.add_connection(other_peer.peer).await?; op_storage.ring.add_connection( other_peer @@ -612,7 +623,7 @@ async fn propagate_oc_to_accepted_peers( let _ = conn_manager.send(&other_peer.peer, msg.into()).await; } } else { - tracing::debug!("Not accepting connection to {}", other_peer.peer); + tracing::debug!(tx = %id, "Not accepting connection to {}", other_peer.peer); } Ok(()) @@ -690,7 +701,7 @@ pub(crate) fn initial_request( max_hops_to_live: usize, id: Transaction, ) -> JoinRingOp { - tracing::debug!("Connecting to gw {} from {}", gateway.peer, this_peer); + tracing::debug!(tx = %id, "Connecting to gw {} from {}", gateway.peer, this_peer); let state = JRState::Connecting(ConnectionInfo { gateway, this_peer, @@ -733,10 +744,10 @@ where } = state.expect("infallible").try_unwrap_connecting()?; tracing::info!( - "Joining ring via {} (at {}) (tx: {})", + tx = %id, + "Joining ring via {} (at {})", gateway.peer, gateway.location.ok_or(ConnectionError::LocationUnknown)?, - tx ); conn_manager.add_connection(gateway.peer).await?; @@ -786,12 +797,14 @@ where let forward_to = if left_htl >= ring.rnd_if_htl_above { tracing::debug!( + tx = %id, "Randomly selecting peer to forward JoinRequest (requester: {})", req_peer.peer ); ring.random_peer(|p| p.peer != req_peer.peer) } else { tracing::debug!( + tx = %id, "Selecting close peer to forward request (requester: {})", req_peer.peer ); @@ -809,6 +822,7 @@ where }, }); tracing::debug!( + tx = %id, "Forwarding JoinRequest from sender {} to {}", req_peer.peer, forward_to.peer @@ -825,11 +839,11 @@ where } else { if num_accepted != 0 { tracing::warn!( - "Unable to forward, will only be connected to one peer (tx: {})", - id + tx = %id, + "Unable to forward, will only be connected to one peer", ); } else { - tracing::warn!("Unable to forward or accept any connections (tx: {})", id); + tracing::warn!(tx = %id, "Unable to forward or accept any connections"); } Ok(None) } @@ -946,15 +960,6 @@ mod messages { } } - /* - - Peer A ---> Peer B (forward) ----> Peer C - |----- (forward) ---------> Peer D - - - Peer A ---> Peer B (forward) ----> Peer C ----> Peer D - - */ #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub(crate) enum JoinRequest { StartReq { @@ -1000,7 +1005,6 @@ mod test { use crate::node::tests::{check_connectivity, SimNetwork}; /// Given a network of one node and one gateway test that both are connected. - #[ignore] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn one_node_connects_to_gw() { let mut sim_nodes = SimNetwork::new("join_one_node_connects_to_gw", 1, 1, 1, 1, 2, 2).await; @@ -1013,6 +1017,7 @@ mod test { #[ignore] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn forward_connection_to_node() -> Result<(), anyhow::Error> { + // crate::config::set_logger(); const NUM_NODES: usize = 10usize; const NUM_GW: usize = 1usize; let mut sim_nodes = SimNetwork::new( @@ -1030,8 +1035,8 @@ mod test { } /// Given a network of N peers all nodes should have connections. - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] #[ignore] + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn all_nodes_should_connect() -> Result<(), anyhow::Error> { const NUM_NODES: usize = 10usize; const NUM_GW: usize = 1usize; diff --git a/crates/core/src/operations/put.rs b/crates/core/src/operations/put.rs index cf712d67b..14836a177 100644 --- a/crates/core/src/operations/put.rs +++ b/crates/core/src/operations/put.rs @@ -245,7 +245,9 @@ impl Operation for PutOp { // after the contract has been cached, push the update query tracing::debug!("Attempting contract value update"); - let new_value = put_contract(op_storage, key.clone(), value, client_id).await?; + let parameters = contract.params(); + let new_value = + put_contract(op_storage, key.clone(), value, parameters, client_id).await?; tracing::debug!("Contract successfully updated"); // if the change was successful, communicate this back to the requestor and broadcast the change conn_manager @@ -291,7 +293,7 @@ impl Operation for PutOp { self.state, broadcast_to, key.clone(), - new_value, + (contract.params(), new_value), self._ttl, ) .await @@ -307,14 +309,21 @@ impl Operation for PutOp { id, key, new_value, + parameters, sender, sender_subscribers, } => { let target = op_storage.ring.own_location(); tracing::debug!("Attempting contract value update"); - let new_value = - put_contract(op_storage, key.clone(), new_value, client_id).await?; + let new_value = put_contract( + op_storage, + key.clone(), + new_value, + parameters.clone(), + client_id, + ) + .await?; tracing::debug!("Contract successfully updated"); let broadcast_to = op_storage @@ -342,7 +351,7 @@ impl Operation for PutOp { self.state, broadcast_to, key, - new_value, + (parameters, new_value), self._ttl, ) .await @@ -360,6 +369,7 @@ impl Operation for PutOp { mut broadcasted_to, key, new_value, + parameters, } => { let sender = op_storage.ring.own_location(); let msg = PutMsg::BroadcastTo { @@ -368,6 +378,7 @@ impl Operation for PutOp { new_value: new_value.clone(), sender, sender_subscribers: broadcast_to.clone(), + parameters, }; let mut broadcasting = Vec::with_capacity(broadcast_to.len()); @@ -457,7 +468,9 @@ impl Operation for PutOp { }); } // after the contract has been cached, push the update query - let new_value = put_contract(op_storage, key, new_value, client_id).await?; + let new_value = + put_contract(op_storage, key, new_value, contract.params(), client_id) + .await?; //update skip list skip_list.push(peer_loc.peer); @@ -533,7 +546,7 @@ async fn try_to_broadcast( state: Option, broadcast_to: Vec, key: ContractKey, - new_value: WrappedState, + (parameters, new_value): (Parameters<'static>, WrappedState), ttl: Duration, ) -> Result<(Option, Option), OpError> { let new_state; @@ -556,6 +569,7 @@ async fn try_to_broadcast( return_msg = Some(PutMsg::Broadcasting { id, new_value, + parameters, broadcasted_to: 0, broadcast_to, key, @@ -699,11 +713,19 @@ async fn put_contract( op_storage: &OpManager, key: ContractKey, state: WrappedState, + parameters: Parameters<'static>, client_id: Option, ) -> Result { // after the contract has been cached, push the update query match op_storage - .notify_contract_handler(ContractHandlerEvent::PutQuery { key, state }, client_id) + .notify_contract_handler( + ContractHandlerEvent::PutQuery { + key, + state, + parameters: Some(parameters), + }, + client_id, + ) .await { Ok(ContractHandlerEvent::PutResponse { @@ -771,7 +793,7 @@ mod messages { use crate::message::InnerMessage; use serde::{Deserialize, Serialize}; - #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] + #[derive(Debug, Serialize, Deserialize, Clone)] pub(crate) enum PutMsg { /// Initialize the put operation by routing the value RouteValue { @@ -824,6 +846,8 @@ mod messages { broadcast_to: Vec, key: ContractKey, new_value: WrappedState, + #[serde(deserialize_with = "Parameters::deser_params")] + parameters: Parameters<'static>, }, /// Broadcasting a change to a peer, which then will relay the changes to other peers. BroadcastTo { @@ -831,6 +855,8 @@ mod messages { sender: PeerKeyLocation, key: ContractKey, new_value: WrappedState, + #[serde(deserialize_with = "Parameters::deser_params")] + parameters: Parameters<'static>, sender_subscribers: Vec, }, } diff --git a/crates/core/src/ring.rs b/crates/core/src/ring.rs index a5378be51..86337d016 100644 --- a/crates/core/src/ring.rs +++ b/crates/core/src/ring.rs @@ -233,7 +233,6 @@ impl Ring { /// Returns this node location in the ring, if any (must have join the ring already). pub fn own_location(&self) -> PeerKeyLocation { - tracing::debug!("Getting loc for peer {}", self.peer_key); let location = f64::from_le_bytes(self.own_location.load(SeqCst).to_le_bytes()); let location = if (location - -1f64).abs() < f64::EPSILON { None diff --git a/crates/core/src/runtime/delegate.rs b/crates/core/src/runtime/delegate.rs index 368823ccd..6c22407a2 100644 --- a/crates/core/src/runtime/delegate.rs +++ b/crates/core/src/runtime/delegate.rs @@ -417,7 +417,7 @@ mod test { name: &str, ) -> Result<(DelegateContainer, Runtime), Box> { const TEST_PREFIX: &str = "delegate-api"; - let _ = tracing_subscriber::fmt().with_env_filter("info").try_init(); + // let _ = tracing_subscriber::fmt().with_env_filter("info").try_init(); let contracts_dir = super::super::tests::test_dir(TEST_PREFIX); let delegates_dir = super::super::tests::test_dir(TEST_PREFIX); let secrets_dir = super::super::tests::test_dir(TEST_PREFIX); diff --git a/crates/core/src/runtime/error.rs b/crates/core/src/runtime/error.rs index d45cd86e2..a18b84064 100644 --- a/crates/core/src/runtime/error.rs +++ b/crates/core/src/runtime/error.rs @@ -2,6 +2,8 @@ use std::fmt::Display; use freenet_stdlib::prelude::{ContractKey, DelegateKey, SecretsId}; +use crate::DynError; + use super::{delegate, secrets_store, wasm_runtime, DelegateExecError}; pub type RuntimeResult = std::result::Result; @@ -97,7 +99,7 @@ impl_err!(wasmer::RuntimeError); #[derive(thiserror::Error, Debug)] pub(crate) enum RuntimeInnerError { #[error(transparent)] - Any(#[from] Box), + Any(#[from] DynError), #[error(transparent)] BufferError(#[from] freenet_stdlib::memory::buf::Error), diff --git a/crates/core/src/runtime/state_store.rs b/crates/core/src/runtime/state_store.rs index ea507f249..334deab2f 100644 --- a/crates/core/src/runtime/state_store.rs +++ b/crates/core/src/runtime/state_store.rs @@ -11,6 +11,17 @@ pub enum StateStoreError { MissingContract(ContractKey), } +impl From for crate::runtime::ContractError { + fn from(value: StateStoreError) -> Self { + match value { + StateStoreError::Any(err) => crate::runtime::ContractError::from(err), + err @ StateStoreError::MissingContract(_) => { + crate::runtime::ContractError::from(Into::::into(format!("{err}"))) + } + } + } +} + #[async_trait::async_trait] #[allow(clippy::type_complexity)] pub trait StateStorage { diff --git a/crates/core/src/runtime/tests/mod.rs b/crates/core/src/runtime/tests/mod.rs index 0cb8bfacf..32d0bf913 100644 --- a/crates/core/src/runtime/tests/mod.rs +++ b/crates/core/src/runtime/tests/mod.rs @@ -62,7 +62,7 @@ pub(crate) fn get_test_module(name: &str) -> Result, Box Result<(ContractStore, ContractKey), Box> { - let _ = tracing_subscriber::fmt().with_env_filter("info").try_init(); + // let _ = tracing_subscriber::fmt().with_env_filter("info").try_init(); let mut store = ContractStore::new(test_dir("contract"), 10_000)?; let contract_bytes = WrappedContract::new( Arc::new(ContractCode::from(get_test_module(name)?)), diff --git a/crates/fdev/src/main.rs b/crates/fdev/src/main.rs index 9f290c0f7..7c9eeb6b6 100644 --- a/crates/fdev/src/main.rs +++ b/crates/fdev/src/main.rs @@ -2,7 +2,6 @@ use std::borrow::Cow; use clap::Parser; use freenet_stdlib::client_api::ClientRequest; -use tracing_subscriber::EnvFilter; mod build; mod commands; @@ -35,9 +34,7 @@ enum Error { #[tokio::main] async fn main() -> Result<(), anyhow::Error> { - tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .init(); + freenet::config::set_logger(); let cwd = std::env::current_dir()?; let config = Config::parse(); freenet::config::Config::set_op_mode(config.additional.mode); From a0574f3bb0396353714560522297cd7e947dcad0 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Tue, 17 Oct 2023 12:32:09 +0200 Subject: [PATCH 56/76] Multiple fixes around join ring op retries --- Cargo.lock | 303 +++++++++++++----- crates/core/Cargo.toml | 3 +- crates/core/src/contract.rs | 1 - crates/core/src/contract/executor.rs | 4 +- crates/core/src/contract/handler.rs | 58 ++-- crates/core/src/contract/in_memory.rs | 1 - crates/core/src/contract/storages/rocks_db.rs | 92 ------ crates/core/src/contract/storages/sqlite.rs | 93 ------ crates/core/src/lib.rs | 1 - crates/core/src/message.rs | 41 ++- crates/core/src/node.rs | 75 ++--- .../core/src/node/conn_manager/p2p_protoc.rs | 1 - crates/core/src/node/event_log.rs | 5 +- crates/core/src/node/in_memory_impl.rs | 23 +- crates/core/src/node/op_state.rs | 3 +- crates/core/src/node/p2p_impl.rs | 1 - crates/core/src/node/tests.rs | 38 ++- crates/core/src/operations.rs | 4 +- crates/core/src/operations/get.rs | 14 +- crates/core/src/operations/join_ring.rs | 147 ++++++--- crates/core/src/operations/put.rs | 11 +- crates/core/src/operations/subscribe.rs | 10 +- crates/core/src/ring.rs | 91 ++---- crates/core/src/runtime/contract_store.rs | 2 +- crates/core/src/runtime/wasm_runtime.rs | 1 - crates/core/src/server/mod.rs | 2 +- crates/fdev/src/build.rs | 1 + crates/fdev/src/new_package.rs | 2 +- 28 files changed, 484 insertions(+), 544 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6219936fa..3df61cda9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,7 +259,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ "async-lock", - "autocfg", + "autocfg 1.1.0", "cfg-if", "concurrent-queue", "futures-lite", @@ -316,9 +316,12 @@ dependencies = [ [[package]] name = "atomic" -version = "0.5.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] [[package]] name = "atomic-waker" @@ -326,6 +329,15 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.1.0", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -743,6 +755,15 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "colorchoice" version = "1.0.0" @@ -830,7 +851,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80128832c58ea9cbd041d2a759ec449224487b2c1e400453d99d244eead87a8e" dependencies = [ - "autocfg", + "autocfg 1.1.0", "cfg-if", "libc", "scopeguard", @@ -985,7 +1006,7 @@ version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ - "autocfg", + "autocfg 1.1.0", "cfg-if", "crossbeam-utils", "memoffset 0.9.0", @@ -1590,7 +1611,7 @@ dependencies = [ "parking_lot", "pav_regression", "pico-args", - "rand", + "rand 0.8.5", "rocksdb", "serde", "serde_json", @@ -1605,8 +1626,8 @@ dependencies = [ "tracing", "tracing-opentelemetry", "tracing-subscriber", + "ulid", "unsigned-varint", - "uuid", "wasmer", "xz2", ] @@ -1636,7 +1657,7 @@ dependencies = [ "futures", "js-sys", "once_cell", - "rand", + "rand 0.8.5", "semver", "serde", "serde-wasm-bindgen 0.6.0", @@ -1662,6 +1683,12 @@ dependencies = [ "libc", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "funty" version = "2.0.0" @@ -2133,7 +2160,26 @@ dependencies = [ "rtnetlink", "system-configuration", "tokio", - "windows 0.34.0", + "windows", +] + +[[package]] +name = "igd-next" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e065e90a518ab5fedf79aa1e4b784e10f8e484a834f6bda85c42633a2cb7af" +dependencies = [ + "async-trait", + "attohttpc", + "bytes", + "futures", + "http", + "hyper", + "log", + "rand 0.8.5", + "tokio", + "url", + "xmltree", ] [[package]] @@ -2142,7 +2188,7 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ - "autocfg", + "autocfg 1.1.0", "hashbrown 0.12.3", "serde", ] @@ -2421,7 +2467,7 @@ dependencies = [ "libp2p-swarm", "log", "quick-protobuf", - "rand", + "rand 0.8.5", ] [[package]] @@ -2456,7 +2502,7 @@ dependencies = [ "parking_lot", "pin-project", "quick-protobuf", - "rand", + "rand 0.8.5", "rw-stream-sink", "smallvec", "thiserror", @@ -2513,7 +2559,7 @@ dependencies = [ "log", "multihash", "quick-protobuf", - "rand", + "rand 0.8.5", "sha2", "thiserror", "zeroize", @@ -2532,7 +2578,7 @@ dependencies = [ "libp2p-identity", "libp2p-swarm", "log", - "rand", + "rand 0.8.5", "smallvec", "socket2 0.5.4", "tokio", @@ -2572,7 +2618,7 @@ dependencies = [ "multihash", "once_cell", "quick-protobuf", - "rand", + "rand 0.8.5", "sha2", "snow", "static_assertions", @@ -2595,7 +2641,7 @@ dependencies = [ "libp2p-identity", "libp2p-swarm", "log", - "rand", + "rand 0.8.5", "void", ] @@ -2615,7 +2661,8 @@ dependencies = [ "log", "parking_lot", "quinn", - "rand", + "rand 0.8.5", + "ring", "rustls", "socket2 0.5.4", "thiserror", @@ -2635,7 +2682,7 @@ dependencies = [ "libp2p-identity", "libp2p-swarm", "log", - "rand", + "rand 0.8.5", "smallvec", "void", ] @@ -2657,7 +2704,7 @@ dependencies = [ "log", "multistream-select", "once_cell", - "rand", + "rand 0.8.5", "smallvec", "tokio", "void", @@ -2786,7 +2833,7 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ - "autocfg", + "autocfg 1.1.0", "scopeguard", ] @@ -2911,7 +2958,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" dependencies = [ - "autocfg", + "autocfg 1.1.0", ] [[package]] @@ -2920,7 +2967,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ - "autocfg", + "autocfg 1.1.0", ] [[package]] @@ -3194,7 +3241,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ - "autocfg", + "autocfg 1.1.0", "num-integer", "num-traits", ] @@ -3211,7 +3258,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -3231,7 +3278,7 @@ version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ - "autocfg", + "autocfg 1.1.0", "num-traits", ] @@ -3241,7 +3288,7 @@ version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" dependencies = [ - "autocfg", + "autocfg 1.1.0", "num-integer", "num-traits", ] @@ -3263,7 +3310,7 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ - "autocfg", + "autocfg 1.1.0", "libm", ] @@ -3405,7 +3452,7 @@ dependencies = [ "opentelemetry_api", "ordered-float 3.9.1", "percent-encoding", - "rand", + "rand 0.8.5", "regex", "thiserror", "tokio", @@ -3655,7 +3702,7 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ - "autocfg", + "autocfg 1.1.0", "bitflags 1.3.2", "cfg-if", "concurrent-queue", @@ -3855,7 +3902,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c78e758510582acc40acb90458401172d41f1016f8c9dde89e49677afb7eec1" dependencies = [ "bytes", - "rand", + "rand 0.8.5", "ring", "rustc-hash", "rustls", @@ -3893,6 +3940,25 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.8", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi", +] + [[package]] name = "rand" version = "0.8.5" @@ -3900,10 +3966,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.3.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -3916,13 +3992,19 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.5.1" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" dependencies = [ - "getrandom 0.1.16", + "rand_core 0.4.2", ] +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "rand_core" version = "0.6.4" @@ -3933,20 +4015,67 @@ dependencies = [ ] [[package]] -name = "rand_distr" -version = "0.4.3" +name = "rand_hc" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" dependencies = [ - "num-traits", - "rand", + "rand_core 0.3.1", ] [[package]] -name = "rawpointer" -version = "0.2.1" +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] [[package]] name = "rayon" @@ -3980,6 +4109,15 @@ dependencies = [ "yasna", ] +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -4557,23 +4695,10 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" dependencies = [ - "digest 0.10.7", + "digest", "rand_core 0.6.4", ] -[[package]] -name = "simba" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0b7840f121a46d63066ee7a99fc81dcabbc6105e437cae43528cea199b5a05f" -dependencies = [ - "approx", - "num-complex", - "num-traits", - "paste", - "wide", -] - [[package]] name = "simdutf8" version = "0.1.4" @@ -4586,7 +4711,7 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ - "autocfg", + "autocfg 1.1.0", ] [[package]] @@ -4621,7 +4746,7 @@ dependencies = [ "aes-gcm", "blake2", "chacha20poly1305 0.9.1", - "curve25519-dalek 4.1.1", + "curve25519-dalek", "rand_core 0.6.4", "ring", "rustc_version", @@ -4808,7 +4933,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.5", "rsa", "serde", "sha1", @@ -4847,7 +4972,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand", + "rand 0.8.5", "serde", "serde_json", "sha1", @@ -4919,7 +5044,7 @@ dependencies = [ "crossbeam-channel", "futures", "parking_lot", - "rand", + "rand 0.8.5", "seahash", "thiserror", "tracing", @@ -5420,7 +5545,7 @@ dependencies = [ "idna 0.2.3", "ipnet", "lazy_static", - "rand", + "rand 0.8.5", "smallvec", "socket2 0.4.9", "thiserror", @@ -5430,6 +5555,31 @@ dependencies = [ "url", ] +[[package]] +name = "trust-dns-proto" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "559ac980345f7f5020883dd3bcacf176355225e01916f8c2efecad7534f682c6" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner 0.6.0", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.4.0", + "ipnet", + "once_cell", + "rand 0.8.5", + "smallvec", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + [[package]] name = "trust-dns-resolver" version = "0.22.0" @@ -5442,6 +5592,7 @@ dependencies = [ "lazy_static", "lru-cache", "parking_lot", + "rand 0.8.5", "resolv-conf", "smallvec", "thiserror", @@ -5468,7 +5619,7 @@ dependencies = [ "http", "httparse", "log", - "rand", + "rand 0.8.5", "sha1", "thiserror", "url", @@ -5487,6 +5638,18 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +[[package]] +name = "ulid" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e95a59b292ca0cf9b45be2e52294d1ca6cb24eb11b08ef4376f73f1a00c549" +dependencies = [ + "chrono", + "lazy_static", + "rand 0.6.5", + "serde", +] + [[package]] name = "unicase" version = "2.7.0" @@ -5610,12 +5773,7 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" name = "uuid" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" -dependencies = [ - "atomic", - "getrandom 0.2.10", - "serde", -] +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" [[package]] name = "valuable" @@ -6206,8 +6364,9 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" dependencies = [ - "curve25519-dalek 3.2.0", - "rand_core 0.5.1", + "curve25519-dalek", + "rand_core 0.6.4", + "serde", "zeroize", ] @@ -6272,7 +6431,7 @@ dependencies = [ "nohash-hasher", "parking_lot", "pin-project", - "rand", + "rand 0.8.5", "static_assertions", ] diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 9e0142774..965d770dd 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -59,7 +59,8 @@ thiserror = "1" tokio = { version = "1", features = ["rt-multi-thread", "sync", "macros", "fs"] } tower-http = { version = "0.4", features = ["trace", "fs"] } unsigned-varint = "0.7" -uuid = { version = "1", features = ["serde", "v4", "v1"] } +# uuid = { version = "1", features = ["serde", "v4", "v1"] } +ulid = { version = "0.4", features = ["serde"] } sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls"], optional = true } # TODO(kakoc): clang should be installed for rocksdb; write about that in prerequisites/dev guide rocksdb = { version = "0.21.0", default-features = false, optional = true } diff --git a/crates/core/src/contract.rs b/crates/core/src/contract.rs index f4198e5a7..28bf08907 100644 --- a/crates/core/src/contract.rs +++ b/crates/core/src/contract.rs @@ -25,7 +25,6 @@ use executor::ContractExecutor; #[tracing::instrument(skip_all)] pub(crate) async fn contract_handling<'a, CH>(mut contract_handler: CH) -> Result<(), ContractError> -// todo: remove result where CH: ContractHandler + Send + 'static, { diff --git a/crates/core/src/contract/executor.rs b/crates/core/src/contract/executor.rs index e5fc49686..c37c5c5f8 100644 --- a/crates/core/src/contract/executor.rs +++ b/crates/core/src/contract/executor.rs @@ -197,8 +197,8 @@ struct GetContract { #[async_trait::async_trait] impl ComposeNetworkMessage for GetContract { - fn initiate_op(self, op_manager: &OpManager) -> operations::get::GetOp { - operations::get::start_op(self.key, self.fetch_contract, &op_manager.ring.peer_key) + fn initiate_op(self, _op_manager: &OpManager) -> operations::get::GetOp { + operations::get::start_op(self.key, self.fetch_contract) } async fn resume_op( diff --git a/crates/core/src/contract/handler.rs b/crates/core/src/contract/handler.rs index e4b94593d..c2dcc92f7 100644 --- a/crates/core/src/contract/handler.rs +++ b/crates/core/src/contract/handler.rs @@ -289,7 +289,6 @@ impl ContractHandlerToEventLoopChannel { } } - // todo: use pub async fn recv_from_handler(&mut self) -> (EventId, ContractHandlerEvent) { todo!() } @@ -424,55 +423,56 @@ pub mod test { use crate::runtime::ContractStore; use freenet_stdlib::{ - client_api::{ClientRequest, HostResponse}, + client_api::{ClientRequest, ContractRequest, HostResponse}, prelude::*, }; use super::*; use crate::{config::GlobalExecutor, contract::MockRuntime}; - #[ignore] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn channel_test() -> Result<(), anyhow::Error> { let (mut send_halve, mut rcv_halve) = contract_handler_channel(); + let contract = ContractContainer::Wasm(ContractWasmAPIVersion::V1(WrappedContract::new( + Arc::new(ContractCode::from(vec![0, 1, 2, 3])), + Parameters::from(vec![4, 5]), + ))); + + let contract_cp = contract.clone(); let h = GlobalExecutor::spawn(async move { - let contract = - ContractContainer::Wasm(ContractWasmAPIVersion::V1(WrappedContract::new( - Arc::new(ContractCode::from(vec![0, 1, 2, 3])), - Parameters::from(vec![]), - ))); send_halve - .send_to_handler(ContractHandlerEvent::Cache(contract), None) + .send_to_handler(ContractHandlerEvent::Cache(contract_cp), None) .await }); - let (id, ev) = tokio::time::timeout(Duration::from_millis(100), rcv_halve.recv_from_event_loop()) .await??; - if let ContractHandlerEvent::Cache(contract) = ev { - let data: Vec = contract.data(); - assert_eq!(data, vec![0, 1, 2, 3]); - let contract = ContractContainer::Wasm(ContractWasmAPIVersion::V1( - WrappedContract::new(Arc::new(ContractCode::from(data)), Parameters::from(vec![])), - )); - tokio::time::timeout( - Duration::from_millis(100), - rcv_halve.send_to_event_loop(id, ContractHandlerEvent::Cache(contract)), - ) - .await??; - } else { + let ContractHandlerEvent::Cache(contract) = ev else { anyhow::bail!("invalid event"); - } - - if let ContractHandlerEvent::Cache(contract) = h.await?? { - let data: Vec = contract.data(); - assert_eq!(data, vec![0, 1, 2, 3]); - } else { + }; + assert_eq!(contract.data(), vec![0, 1, 2, 3]); + + tokio::time::timeout( + Duration::from_millis(100), + rcv_halve.send_to_event_loop(id, ContractHandlerEvent::Cache(contract)), + ) + .await??; + let ContractHandlerEvent::Cache(contract) = h.await?? else { anyhow::bail!("invalid event!"); - } + }; + assert_eq!(contract.data(), vec![0, 1, 2, 3]); Ok(()) } + + // Prepare and get handler for an in-memory sqlite db + async fn get_handler(test: &str) -> Result, DynError> { + let (_, ch_handler) = contract_handler_channel(); + let (_, executor_sender) = super::super::executor::executor_channel_test(); + let handler = + NetworkContractHandler::build(ch_handler, executor_sender, test.to_owned()).await?; + Ok(handler) + } } diff --git a/crates/core/src/contract/in_memory.rs b/crates/core/src/contract/in_memory.rs index 5374e65f2..8fa030941 100644 --- a/crates/core/src/contract/in_memory.rs +++ b/crates/core/src/contract/in_memory.rs @@ -88,7 +88,6 @@ impl ContractHandler for MemoryContractHandler { } } -#[ignore] #[test] fn serialization() -> Result<(), anyhow::Error> { let bytes = crate::util::test::random_bytes_1024(); diff --git a/crates/core/src/contract/storages/rocks_db.rs b/crates/core/src/contract/storages/rocks_db.rs index 69d5be411..5c77e20d6 100644 --- a/crates/core/src/contract/storages/rocks_db.rs +++ b/crates/core/src/contract/storages/rocks_db.rs @@ -92,95 +92,3 @@ impl StateStorage for RocksDb { } } } - -#[cfg(test)] -mod test { - use std::sync::Arc; - - use freenet_stdlib::{client_api::ContractRequest, prelude::*}; - - use crate::{ - client_events::ClientId, - contract::{ - contract_handler_channel, executor::executor_channel_test, ContractHandler, - MockRuntime, NetworkContractHandler, - }, - DynError, - }; - - // Prepare and get handler for rocksdb - async fn get_handler(test: &str) -> Result, DynError> { - let (_, ch_handler) = contract_handler_channel(); - let (_, executor_sender) = executor_channel_test(); - let handler = - NetworkContractHandler::build(ch_handler, executor_sender, test.to_string()).await?; - Ok(handler) - } - - #[ignore] - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn contract_handler() -> Result<(), DynError> { - // Create a rocksdb handler and initialize the database - let mut handler = get_handler("contract_handler").await?; - - // Generate a contract - let contract_bytes = b"Test contract value".to_vec(); - let contract: ContractContainer = - ContractContainer::Wasm(ContractWasmAPIVersion::V1(WrappedContract::new( - Arc::new(ContractCode::from(contract_bytes.clone())), - Parameters::from(vec![]), - ))); - - // Get contract parts - let state = WrappedState::new(contract_bytes.clone()); - handler - .handle_request( - ContractRequest::Put { - contract: contract.clone(), - state: state.clone(), - related_contracts: Default::default(), - } - .into(), - ClientId::FIRST, - None, - ) - .await? - .unwrap_put(); - let (get_result_value, _) = handler - .handle_request( - ContractRequest::Get { - key: contract.key().clone(), - fetch_contract: false, - } - .into(), - ClientId::FIRST, - None, - ) - .await? - .unwrap_get(); - assert_eq!(state, get_result_value); - - // Update the contract state with a new delta - let delta = StateDelta::from(b"New test contract value".to_vec()); - handler - .handle_request( - ContractRequest::Update { - key: contract.key().clone(), - data: delta.into(), - } - .into(), - ClientId::FIRST, - None, - ) - .await?; - // let (new_get_result_value, _) = handler - // .handle_request(ContractOps::Get { - // key: *contract.key(), - // contract: false, - // }) - // .await? - // .unwrap_summary(); - // assert_eq!(delta, new_get_result_value); - todo!("get summary and compare with delta"); - } -} diff --git a/crates/core/src/contract/storages/sqlite.rs b/crates/core/src/contract/storages/sqlite.rs index a51249ef8..e70f3bc71 100644 --- a/crates/core/src/contract/storages/sqlite.rs +++ b/crates/core/src/contract/storages/sqlite.rs @@ -134,96 +134,3 @@ pub enum SqlDbError { #[error(transparent)] StateStore(#[from] StateStoreError), } - -#[cfg(test)] -mod test { - use std::sync::Arc; - - use freenet_stdlib::client_api::ContractRequest; - use freenet_stdlib::prelude::*; - - use crate::{ - client_events::ClientId, - contract::{ - contract_handler_channel, executor::executor_channel_test, ContractHandler, - MockRuntime, NetworkContractHandler, - }, - DynError, - }; - - // Prepare and get handler for an in-memory sqlite db - async fn get_handler(test: &str) -> Result, DynError> { - let (_, ch_handler) = contract_handler_channel(); - let (_, executor_sender) = executor_channel_test(); - let handler = - NetworkContractHandler::build(ch_handler, executor_sender, test.to_owned()).await?; - Ok(handler) - } - - #[ignore] - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn contract_handler() -> Result<(), DynError> { - // Create a sqlite handler and initialize the database - let mut handler = get_handler("contract_handler").await?; - - // Generate a contract - let contract_bytes = b"test contract value".to_vec(); - let contract: ContractContainer = - ContractContainer::Wasm(ContractWasmAPIVersion::V1(WrappedContract::new( - Arc::new(ContractCode::from(contract_bytes.clone())), - Parameters::from(vec![]), - ))); - - // Get contract parts - let state = WrappedState::new(contract_bytes.clone()); - handler - .handle_request( - ContractRequest::Put { - contract: contract.clone(), - state: state.clone(), - related_contracts: Default::default(), - } - .into(), - ClientId::FIRST, - None, - ) - .await? - .unwrap_put(); - let (get_result_value, _) = handler - .handle_request( - ContractRequest::Get { - key: contract.key().clone(), - fetch_contract: false, - } - .into(), - ClientId::FIRST, - None, - ) - .await? - .unwrap_get(); - assert_eq!(state, get_result_value); - - // Update the contract state with a new delta - let delta = StateDelta::from(b"New test contract value".to_vec()); - handler - .handle_request( - ContractRequest::Update { - key: contract.key().clone(), - data: delta.into(), - } - .into(), - ClientId::FIRST, - None, - ) - .await?; - // let (new_get_result_value, _) = handler - // .handle_request(ContractOps::Get { - // key: *contract.key(), - // contract: false, - // }) - // .await? - // .unwrap_summary(); - // assert_eq!(delta, new_get_result_value); - todo!("get summary and compare with delta"); - } -} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 9a4b59790..fa71d6a6d 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -25,7 +25,6 @@ pub mod local_node { /// Exports to build a running network simulation. pub mod network_sim { - // todo: streamline this use super::*; pub use client_events::{ClientEventsProxy, ClientId, OpenRequest}; pub use node::{InitPeerNode, NodeBuilder, NodeConfig}; diff --git a/crates/core/src/message.rs b/crates/core/src/message.rs index 4f4bd3b7a..551339abb 100644 --- a/crates/core/src/message.rs +++ b/crates/core/src/message.rs @@ -3,10 +3,7 @@ use std::{fmt::Display, time::Duration}; use serde::{Deserialize, Serialize}; -use uuid::{ - v1::{Context, Timestamp}, - Uuid, -}; +use ulid::Ulid; use crate::{ node::{ConnectionError, PeerKey}, @@ -19,7 +16,7 @@ use crate::{ pub(crate) use sealed_msg_type::{TransactionType, TransactionTypeId}; /// An transaction is a unique, universal and efficient identifier for any -/// roundtrip transaction as it is broadcasted around the F2 network. +/// roundtrip transaction as it is broadcasted around the Freenet network. /// /// The identifier conveys all necessary information to identify and classify the /// transaction: @@ -31,27 +28,14 @@ pub(crate) use sealed_msg_type::{TransactionType, TransactionTypeId}; /// A transaction may span different messages sent across the network. #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy)] pub(crate) struct Transaction { - /// UUID V1, can retrieve timestamp information later to check for possible out of time - /// expired transactions which have been clean up already. - id: Uuid, + id: Ulid, ty: TransactionTypeId, } -static UUID_CONTEXT: Context = Context::new(14); - impl Transaction { - pub fn new(ty: TransactionTypeId, initial_peer: &PeerKey) -> Transaction { - // using v1 UUID to keep to keep track of the creation ts - let ts: Timestamp = uuid::timestamp::Timestamp::now(&UUID_CONTEXT); - - // event in the net this UUID should be unique since peer keys are unique - // however some id collision may be theoretically possible if two transactions - // are created at the same exact time and the first 6 bytes of the key coincide; - // in practice the chance of this happening is astronomically low - - let b = &mut [0; 6]; - b.copy_from_slice(&initial_peer.to_bytes()[0..6]); - let id = Uuid::new_v1(ts, b); + pub fn new() -> Transaction { + let ty = ::tx_type_id(); + let id = Ulid::new(); // 3 word size for 64-bits platforms Self { id, ty } @@ -111,6 +95,19 @@ mod sealed_msg_type { Canceled, } + impl Display for TransactionType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TransactionType::JoinRing => write!(f, "join ring"), + TransactionType::Put => write!(f, "put"), + TransactionType::Get => write!(f, "get"), + TransactionType::Subscribe => write!(f, "subscribe"), + TransactionType::Update => write!(f, "update"), + TransactionType::Canceled => write!(f, "canceled"), + } + } + } + macro_rules! transaction_type_enumeration { (decl struct { $( $var:tt -> $ty:tt),+ }) => { $( diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index 28c6b74c3..f9b2e1156 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -29,7 +29,7 @@ use crate::{ ClientResponses, ClientResponsesSender, ContractError, ExecutorToEventLoopChannel, NetworkContractHandler, NetworkEventListenerHalve, OperationMode, }, - message::{InnerMessage, Message, Transaction, TransactionType, TxType}, + message::{InnerMessage, Message, Transaction, TransactionType}, operations::{ get, join_ring::{self, JoinRingMsg, JoinRingOp}, @@ -37,13 +37,11 @@ use crate::{ }, ring::{Location, PeerKeyLocation}, router::{RouteEvent, RouteOutcome}, - util::{ExponentialBackoff, IterExt}, + util::ExponentialBackoff, }; use crate::operations::handle_op_request; pub(crate) use conn_manager::{ConnectionBridge, ConnectionError}; -#[cfg(test)] -pub(crate) use event_log::test_utils::TestEventListener; pub(crate) use event_log::{EventLogRegister, EventRegister}; pub(crate) use op_state::OpManager; @@ -77,7 +75,6 @@ pub struct Node(NodeP2P); impl Node { pub async fn run(self) -> Result<(), Box> { - //TODO: Initialize tracer self.0.run_node().await?; Ok(()) } @@ -284,7 +281,7 @@ async fn join_ring_request( where CM: ConnectionBridge + Send + Sync, { - let tx_id = Transaction::new(::tx_type_id(), &peer_key); + let tx_id = Transaction::new::(); let mut op = join_ring::initial_request(peer_key, *gateway, op_storage.ring.max_hops_to_live, tx_id); if let Some(mut backoff) = backoff { @@ -295,10 +292,7 @@ where ); if backoff.sleep_async().await.is_none() { tracing::error!("Max number of retries reached"); - return Err(OpError::MaxRetriesExceeded( - tx_id, - format!("{:?}", tx_id.tx_type()), - )); + return Err(OpError::MaxRetriesExceeded(tx_id, tx_id.tx_type())); } op.backoff = Some(backoff); } @@ -367,12 +361,7 @@ async fn process_open_request(request: OpenRequest<'static>, op_storage: Arc, op_storage: Arc, op_storage: Arc { tracing::warn!("Trying to subscribe to a contract not present: {}, requesting it first", key); - let get_op = - get::start_op(key.clone(), true, &op_storage_cp.ring.peer_key); + let get_op = get::start_op(key.clone(), true); if let Err(err) = get::request_get(&op_storage_cp, get_op, Some(client_id)).await { @@ -446,7 +434,7 @@ macro_rules! log_handling_msg { ($op:expr, $id:expr, $op_storage:ident) => { tracing::debug!( tx = %$id, - concat!("Handling ", $op, " get request @ {}"), + concat!("Handling ", $op, " request @ {}"), $op_storage.ring.peer_key, ); }; @@ -629,53 +617,27 @@ async fn process_message( async fn handle_cancelled_op( tx: Transaction, peer_key: PeerKey, - gateways: impl Iterator, op_storage: &OpManager, conn_manager: &mut CM, ) -> Result<(), OpError> where CM: ConnectionBridge + Send + Sync, { - tracing::warn!(%tx, "Failed transaction, potentially attempting a retry"); if let TransactionType::JoinRing = tx.tx_type() { - const MSG: &str = "Fatal error: unable to connect to the network"; // the attempt to join the network failed, this could be a fatal error since the node // is useless without connecting to the network, we will retry with exponential backoff match op_storage.pop(&tx) { Some(OpEnum::JoinRing(op)) if op.has_backoff() => { - if let JoinRingOp { - backoff: Some(backoff), - gateway, - .. - } = *op - { - if cfg!(test) { - join_ring_request(None, peer_key, &gateway, op_storage, conn_manager) - .await?; - } else { - join_ring_request( - Some(backoff), - peer_key, - &gateway, - op_storage, - conn_manager, - ) - .await?; - } - } + let JoinRingOp { + gateway, backoff, .. + } = *op; + let backoff = backoff.expect("infallible"); + tracing::warn!("Retry connecting to gateway {}", gateway.peer); + join_ring_request(Some(backoff), peer_key, &gateway, op_storage, conn_manager) + .await?; } - None | Some(OpEnum::JoinRing(_)) => { - let rand_gw = gateways - .shuffle() - .take(1) - .next() - .expect("at least one gateway"); - if !cfg!(test) { - tracing::error!("{}", MSG); - } else { - tracing::debug!("{}", MSG); - } - join_ring_request(None, peer_key, rand_gw, op_storage, conn_manager).await?; + Some(OpEnum::JoinRing(_)) => { + return Err(OpError::MaxRetriesExceeded(tx, tx.tx_type())); } _ => {} } @@ -693,6 +655,7 @@ impl PeerKey { PeerKey::from(Keypair::generate_ed25519().public()) } + #[cfg(test)] pub fn to_bytes(self) -> Vec { self.0.to_bytes() } diff --git a/crates/core/src/node/conn_manager/p2p_protoc.rs b/crates/core/src/node/conn_manager/p2p_protoc.rs index cef6b86ee..18d0ca3de 100644 --- a/crates/core/src/node/conn_manager/p2p_protoc.rs +++ b/crates/core/src/node/conn_manager/p2p_protoc.rs @@ -359,7 +359,6 @@ impl P2pConnManager { let res = handle_cancelled_op( tx, op_manager.ring.peer_key, - self.gateways.iter(), &op_manager, &mut self.bridge, ) diff --git a/crates/core/src/node/event_log.rs b/crates/core/src/node/event_log.rs index f27e3d8bc..b9caa4b6f 100644 --- a/crates/core/src/node/event_log.rs +++ b/crates/core/src/node/event_log.rs @@ -411,7 +411,7 @@ pub(super) mod test_utils { use parking_lot::RwLock; use super::*; - use crate::{message::TxType, node::tests::NodeLabel, ring::Distance}; + use crate::{node::tests::NodeLabel, ring::Distance}; static LOG_ID: AtomicUsize = AtomicUsize::new(0); @@ -570,12 +570,11 @@ pub(super) mod test_utils { } } - #[ignore] #[test] fn test_get_connections() -> Result<(), anyhow::Error> { let peer_id = PeerKey::random(); let loc = Location::try_from(0.5)?; - let tx = Transaction::new(::tx_type_id(), &peer_id); + let tx = Transaction::new::(); let locations = [ (PeerKey::random(), Location::try_from(0.5)?), (PeerKey::random(), Location::try_from(0.75)?), diff --git a/crates/core/src/node/in_memory_impl.rs b/crates/core/src/node/in_memory_impl.rs index 704ab10ea..e445eb401 100644 --- a/crates/core/src/node/in_memory_impl.rs +++ b/crates/core/src/node/in_memory_impl.rs @@ -145,6 +145,7 @@ impl NodeInMemory { } /// Starts listening to incoming events. Will attempt to join the ring if any gateways have been provided. + #[tracing::instrument(name = "memory_event_listener", skip_all)] async fn run_event_listener( &mut self, _client_responses: ClientResponsesSender, // fixme: use this @@ -164,7 +165,6 @@ impl NodeInMemory { let res = handle_cancelled_op( tx, self.peer_key, - self.gateways.iter(), &self.op_storage, &mut self.conn_manager, ) @@ -174,15 +174,18 @@ impl NodeInMemory { if tx_type == TransactionType::JoinRing && !self.is_gateway => { tracing::warn!("Retrying joining the ring with an other peer"); - let gateway = self.gateways.iter().shuffle().next().unwrap(); - join_ring_request( - None, - self.peer_key, - gateway, - &self.op_storage, - &mut self.conn_manager, - ) - .await? + if let Some(gateway) = self.gateways.iter().shuffle().next() { + join_ring_request( + None, + self.peer_key, + gateway, + &self.op_storage, + &mut self.conn_manager, + ) + .await? + } else { + anyhow::bail!("requires at least one gateway"); + } } Err(err) => return Err(anyhow::anyhow!(err)), Ok(_) => {} diff --git a/crates/core/src/node/op_state.rs b/crates/core/src/node/op_state.rs index 047bcc1cc..8b7860e91 100644 --- a/crates/core/src/node/op_state.rs +++ b/crates/core/src/node/op_state.rs @@ -62,8 +62,7 @@ impl OpManager { } /// An early, fast path, return for communicating back changes of on-going operations - /// in the node to the main message handler receiving loop, without any transmission in - /// the network whatsoever. + /// in the node to the main message handler, without any transmission in the network whatsoever. /// /// Useful when transitioning between states that do not require any network communication /// with other nodes, like intermediate states before returning. diff --git a/crates/core/src/node/p2p_impl.rs b/crates/core/src/node/p2p_impl.rs index 256a8c913..b6a3c5b35 100644 --- a/crates/core/src/node/p2p_impl.rs +++ b/crates/core/src/node/p2p_impl.rs @@ -62,7 +62,6 @@ impl NodeP2P { } // start the p2p event loop - // todo: pass `cli_response_sender` self.conn_manager .run_event_listener( self.op_manager.clone(), diff --git a/crates/core/src/node/tests.rs b/crates/core/src/node/tests.rs index c4ca5e028..0ec5df0ed 100644 --- a/crates/core/src/node/tests.rs +++ b/crates/core/src/node/tests.rs @@ -122,6 +122,7 @@ pub(crate) struct SimNetwork { rnd_if_htl_above: usize, max_connections: usize, min_connections: usize, + init_backoff: Duration, } impl SimNetwork { @@ -149,6 +150,7 @@ impl SimNetwork { rnd_if_htl_above, max_connections, min_connections, + init_backoff: Duration::from_millis(1), }; net.build_gateways(gateways).await; net.build_nodes(nodes).await; @@ -272,11 +274,11 @@ impl SimNetwork { } } - pub async fn build(&mut self) { - self.build_with_specs(HashMap::new()).await + pub async fn start(&mut self) { + self.start_with_spec(HashMap::new()).await } - pub async fn build_with_specs(&mut self, mut specs: HashMap) { + pub async fn start_with_spec(&mut self, mut specs: HashMap) { let mut gw_not_init = self.gateways.len(); let gw = self.gateways.drain(..).map(|(n, c)| (n, c.label)); for (node, label) in gw.chain(self.nodes.drain(..)).collect::>() { @@ -284,8 +286,9 @@ impl SimNetwork { self.initialize_peer(node, label, node_spec); if gw_not_init != 0 { gw_not_init -= 1; + tokio::time::sleep(self.init_backoff).await; } else { - tokio::time::sleep(Duration::from_millis(1)).await; + tokio::time::sleep(self.init_backoff).await; } } } @@ -302,6 +305,7 @@ impl SimNetwork { user_events.request_contracts(specs.non_owned_contracts); user_events.generate_events(specs.events_to_generate); } + tracing::debug!(peer = %label, "initializing"); self.labels.insert(label, peer.peer_key); GlobalExecutor::spawn(async move { if let Some(specs) = node_specs { @@ -369,17 +373,16 @@ impl SimNetwork { /// Returns the connectivity in the network per peer (that is all the connections /// this peers has registered). - pub fn node_connectivity(&self) -> HashMap> { + pub fn node_connectivity(&self) -> HashMap)> { let mut peers_connections = HashMap::with_capacity(self.labels.len()); let key_to_label: HashMap<_, _> = self.labels.iter().map(|(k, v)| (v, k)).collect(); for (label, key) in &self.labels { - peers_connections.insert( - label.clone(), - self.event_listener - .connections(*key) - .map(|(k, d)| (key_to_label[&k].clone(), d)) - .collect::>(), - ); + let conns = self + .event_listener + .connections(*key) + .map(|(k, d)| (key_to_label[&k].clone(), d)) + .collect::>(); + peers_connections.insert(label.clone(), (*key, conns)); } peers_connections } @@ -478,7 +481,7 @@ pub(crate) async fn check_connectivity( let mut connections_per_peer: Vec<_> = node_connectivity .iter() - .map(|(k, v)| (k, v.len())) + .map(|(k, v)| (k, v.1.len())) .filter_map(|(k, v)| if !k.is_gateway() { Some(v) } else { None }) .collect(); @@ -497,15 +500,17 @@ pub(crate) async fn check_connectivity( Ok(()) } -fn pretty_print_connections(conns: &HashMap>) -> String { +fn pretty_print_connections( + conns: &HashMap)>, +) -> String { let mut connections = String::from("Node connections:\n"); let mut conns = conns.iter().collect::>(); conns.sort_by(|(a, _), (b, _)| a.cmp(b)); - for (peer, conns) in conns { + for (peer, (key, conns)) in conns { if peer.is_gateway() { continue; } - writeln!(&mut connections, "{peer}:").unwrap(); + writeln!(&mut connections, "{peer} ({key}):").unwrap(); for (conn, dist) in conns { let dist = dist.as_f64(); writeln!(&mut connections, " {conn} (dist: {dist:.3})").unwrap(); @@ -514,7 +519,6 @@ fn pretty_print_connections(conns: &HashMap Result<(), anyhow::Error> { let locations = vec![0.5356, 0.5435, 0.5468, 0.5597, 0.6745, 0.7309, 0.7412]; diff --git a/crates/core/src/operations.rs b/crates/core/src/operations.rs index aefe865e3..a2ee51e35 100644 --- a/crates/core/src/operations.rs +++ b/crates/core/src/operations.rs @@ -197,8 +197,8 @@ pub(crate) enum OpError { IncorrectTxType(TransactionType, TransactionType), #[error("op not present: {0}")] OpNotPresent(Transaction), - #[error("max number of retries for tx {0} of op type {1} reached")] - MaxRetriesExceeded(Transaction, String), + #[error("max number of retries for tx {0} of op type `{1}` reached")] + MaxRetriesExceeded(Transaction, TransactionType), // user for control flow /// This is used as an early interrumpt of an op update when an op diff --git a/crates/core/src/operations/get.rs b/crates/core/src/operations/get.rs index 30e0d8fc7..0c5129470 100644 --- a/crates/core/src/operations/get.rs +++ b/crates/core/src/operations/get.rs @@ -8,7 +8,7 @@ use crate::{ client_events::ClientId, config::PEER_TIMEOUT, contract::{ContractError, ContractHandlerEvent, StoreResponse}, - message::{InnerMessage, Message, Transaction, TxType}, + message::{InnerMessage, Message, Transaction}, node::{ConnectionBridge, OpManager, PeerKey}, operations::{op_trait::Operation, OpInitialization}, ring::{Location, PeerKeyLocation, RingError}, @@ -409,7 +409,7 @@ impl Operation for GetOp { "Failed getting a value for contract {}, reached max retries", key ); - return Err(OpError::MaxRetriesExceeded(id, "get".to_owned())); + return Err(OpError::MaxRetriesExceeded(id, id.tx_type())); } } Some(GetState::ReceivedRequest) => { @@ -620,11 +620,11 @@ fn check_contract_found( } } -pub(crate) fn start_op(key: ContractKey, fetch_contract: bool, this_peer: &PeerKey) -> GetOp { +pub(crate) fn start_op(key: ContractKey, fetch_contract: bool) -> GetOp { let contract_location = Location::from(&key); tracing::debug!("Requesting get contract {} @ loc({contract_location})", key,); - let id = Transaction::new(::tx_type_id(), this_peer); + let id = Transaction::new::(); let state = Some(GetState::PrepareRequest { key, id, @@ -861,7 +861,7 @@ mod test { 2, ) .await; - sim_nodes.build_with_specs(get_specs).await; + sim_nodes.start_with_spec(get_specs).await; check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(3)).await?; // trigger get @ node-0, which does not own the contract @@ -900,7 +900,7 @@ mod test { // establish network let mut sim_nodes = SimNetwork::new("get_contract_not_found", NUM_GW, NUM_NODES, 3, 2, 4, 2).await; - sim_nodes.build_with_specs(get_specs).await; + sim_nodes.start_with_spec(get_specs).await; check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(3)).await?; // trigger get @ node-1, which does not own the contract @@ -969,7 +969,7 @@ mod test { 3, ) .await; - sim_nodes.build_with_specs(get_specs).await; + sim_nodes.start_with_spec(get_specs).await; check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(3)).await?; sim_nodes diff --git a/crates/core/src/operations/join_ring.rs b/crates/core/src/operations/join_ring.rs index 03c5ba263..11ab357f5 100644 --- a/crates/core/src/operations/join_ring.rs +++ b/crates/core/src/operations/join_ring.rs @@ -103,7 +103,7 @@ impl Operation for JoinRingOp { _client_id: Option, ) -> Pin> + Send + 'a>> { Box::pin(async move { - let mut return_msg = None; + let return_msg; let mut new_state = None; match input { @@ -333,45 +333,76 @@ impl Operation for JoinRingOp { peer: your_peer_id, }; - match self.state { - Some(JRState::Connecting(ConnectionInfo { gateway, .. })) => { - if !accepted_by.clone().is_empty() { - tracing::debug!( - tx = %id, - "OC received and acknowledged at requesting peer {} from gateway {}", - your_peer_id, - gateway.peer - ); - new_state = Some(JRState::OCReceived); - return_msg = Some(JoinRingMsg::Response { + // fixme: remove + tracing::debug!( + "accepted by: \nstate: {:?} \nlist: {:?}", + self.state, + accepted_by + ); + let Some(JRState::Connecting(ConnectionInfo { gateway, .. })) = self.state + else { + return Err(OpError::InvalidStateTransition(self.id)); + }; + if !accepted_by.is_empty() { + tracing::debug!( + tx = %id, + "OC received and acknowledged at requesting peer {} from gateway {}", + your_peer_id, + gateway.peer + ); + new_state = Some(JRState::OCReceived); + return_msg = Some(JoinRingMsg::Response { + id, + msg: JoinResponse::ReceivedOC { by_peer: pk_loc }, + sender: pk_loc, + target: sender, + }); + tracing::debug!( + tx = %id, + this_peer = %your_peer_id, + location = %your_location, + "Updating assigned location" + ); + op_storage.ring.update_location(Some(your_location)); + + for other_peer in accepted_by { + let _ = propagate_oc_to_accepted_peers( + conn_manager, + op_storage, + sender, + &other_peer, + JoinRingMsg::Response { id, - msg: JoinResponse::ReceivedOC { by_peer: pk_loc }, + target: other_peer, sender: pk_loc, - target: sender, - }); - } + msg: JoinResponse::ReceivedOC { by_peer: pk_loc }, + }, + ) + .await; } - _ => return Err(OpError::InvalidStateTransition(self.id)), - }; - - op_storage.ring.update_location(Some(your_location)); - - for other_peer in accepted_by { - let _ = propagate_oc_to_accepted_peers( - conn_manager, - op_storage, - sender, - &other_peer, - JoinRingMsg::Response { - id, - target: other_peer, - sender: pk_loc, - msg: JoinResponse::ReceivedOC { by_peer: pk_loc }, - }, - ) - .await; + } else { + // no connections accepted, failed + tracing::debug!( + tx = %id, + peer = %your_peer_id, + "No accepted connections, failed" + ); + let op = JoinRingOp { + id, + state: None, + gateway: self.gateway, + backoff: self.backoff, + _ttl: self._ttl, + }; + op_storage + .notify_op_change( + Message::Canceled(id), + OpEnum::JoinRing(op.into()), + None, + ) + .await?; + return Err(OpError::StatePushed); } - op_storage.ring.update_location(Some(your_location)); } JoinRingMsg::Response { id, @@ -520,9 +551,9 @@ impl Operation for JoinRingOp { } else { tracing::info!( tx = %id, - "Successfully completed connection @ {}, new location = {:?}", + assigned_location = ?op_storage.ring.own_location().location, + "Successfully completed connection @ {}", target.peer, - op_storage.ring.own_location().location ); conn_manager.add_connection(sender.peer).await?; op_storage.ring.add_connection( @@ -648,6 +679,7 @@ mod states { } } +#[derive(Debug)] enum JRState { Initializing, Connecting(ConnectionInfo), @@ -791,22 +823,36 @@ async fn forward_conn( where CM: ConnectionBridge, { - if left_htl == 0 || (ring.num_connections() == 0 && num_accepted == 0) { + if left_htl == 0 { + tracing::debug!( + tx = %id, + requester = %req_peer.peer, + "Couldn't forward join petition, no hops left or enough connections", + ); + return Ok(None); + } + + if ring.num_connections() == 0 { + tracing::warn!( + tx = %id, + requester = %req_peer.peer, + "Couldn't forward join petition, not enough connections", + ); return Ok(None); } let forward_to = if left_htl >= ring.rnd_if_htl_above { tracing::debug!( tx = %id, - "Randomly selecting peer to forward JoinRequest (requester: {})", - req_peer.peer + requester = %req_peer.peer, + "Randomly selecting peer to forward JoinRequest", ); - ring.random_peer(|p| p.peer != req_peer.peer) + ring.random_peer(|p| p != &req_peer.peer) } else { tracing::debug!( tx = %id, - "Selecting close peer to forward request (requester: {})", - req_peer.peer + requester = %req_peer.peer, + "Selecting close peer to forward request", ); ring.routing(&new_peer_loc.location.unwrap(), Some(&req_peer.peer), &[]) .and_then(|pkl| (pkl.peer != new_peer_loc.peer).then_some(pkl)) @@ -840,7 +886,7 @@ where if num_accepted != 0 { tracing::warn!( tx = %id, - "Unable to forward, will only be connected to one peer", + "Unable to forward, will only be connecting to one peer", ); } else { tracing::warn!(tx = %id, "Unable to forward or accept any connections"); @@ -1008,29 +1054,28 @@ mod test { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn one_node_connects_to_gw() { let mut sim_nodes = SimNetwork::new("join_one_node_connects_to_gw", 1, 1, 1, 1, 2, 2).await; - sim_nodes.build().await; + sim_nodes.start().await; tokio::time::sleep(Duration::from_secs(3)).await; assert!(sim_nodes.connected(&"node-0".into())); } /// Once a gateway is left without remaining open slots, ensure forwarding connects - #[ignore] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn forward_connection_to_node() -> Result<(), anyhow::Error> { - // crate::config::set_logger(); + crate::config::set_logger(); const NUM_NODES: usize = 10usize; const NUM_GW: usize = 1usize; let mut sim_nodes = SimNetwork::new( "join_forward_connection_to_node", NUM_GW, NUM_NODES, - 3, + 4, 2, 4, 2, ) .await; - sim_nodes.build().await; + sim_nodes.start().await; check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(3)).await } @@ -1050,7 +1095,7 @@ mod test { 2, ) .await; - sim_nodes.build().await; + sim_nodes.start().await; check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(10)).await } } diff --git a/crates/core/src/operations/put.rs b/crates/core/src/operations/put.rs index 14836a177..72102ce40 100644 --- a/crates/core/src/operations/put.rs +++ b/crates/core/src/operations/put.rs @@ -15,7 +15,7 @@ use crate::{ client_events::ClientId, config::PEER_TIMEOUT, contract::ContractHandlerEvent, - message::{InnerMessage, Message, Transaction, TxType}, + message::{InnerMessage, Message, Transaction }, node::{ConnectionBridge, OpManager, PeerKey}, operations::{op_trait::Operation, OpInitialization}, ring::{Location, PeerKeyLocation, RingError}, @@ -601,7 +601,6 @@ pub(crate) fn start_op( contract: ContractContainer, value: WrappedState, htl: usize, - peer: &PeerKey, ) -> PutOp { let key = contract.key(); let contract_location = Location::from(&key); @@ -610,7 +609,7 @@ pub(crate) fn start_op( key, ); - let id = Transaction::new(::tx_type_id(), peer); + let id = Transaction::new::(); // let payload_size = contract.data().len(); let state = Some(PutState::PrepareRequest { contract, @@ -929,9 +928,9 @@ mod test { use super::*; use crate::node::tests::{check_connectivity, NodeSpecification, SimNetwork}; - #[ignore] #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn successful_put_op_between_nodes() -> Result<(), anyhow::Error> { + crate::config::set_logger(); const NUM_NODES: usize = 2usize; const NUM_GW: usize = 1usize; @@ -1001,11 +1000,11 @@ mod test { ("gateway-0".into(), gw_0), ]); - sim_nodes.build_with_specs(put_specs).await; + sim_nodes.start_with_spec(put_specs).await; tokio::time::sleep(Duration::from_secs(5)).await; check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(3)).await?; - // trigger the put op @ gw-0, this + // trigger the put op @ gw-0 sim_nodes .trigger_event(&"gateway-0".into(), 1, Some(Duration::from_secs(3))) .await?; diff --git a/crates/core/src/operations/subscribe.rs b/crates/core/src/operations/subscribe.rs index 14de3c74e..28f1276bc 100644 --- a/crates/core/src/operations/subscribe.rs +++ b/crates/core/src/operations/subscribe.rs @@ -9,7 +9,7 @@ use crate::{ client_events::ClientId, config::PEER_TIMEOUT, contract::ContractError, - message::{Message, Transaction, TxType}, + message::{Message, Transaction}, node::{ConnectionBridge, OpManager, PeerKey}, operations::{op_trait::Operation, OpInitialization}, ring::{PeerKeyLocation, RingError}, @@ -245,7 +245,7 @@ impl Operation for SubscribeOp { retries: retries + 1, }); } else { - return Err(OpError::MaxRetriesExceeded(id, "sub".to_owned())); + return Err(OpError::MaxRetriesExceeded(id, id.tx_type())); } } _ => return Err(OpError::InvalidStateTransition(self.id)), @@ -300,8 +300,8 @@ fn build_op_result( }) } -pub(crate) fn start_op(key: ContractKey, peer: &PeerKey) -> SubscribeOp { - let id = Transaction::new(::tx_type_id(), peer); +pub(crate) fn start_op(key: ContractKey) -> SubscribeOp { + let id = Transaction::new::(); let state = Some(SubscribeState::PrepareRequest { id, key }); SubscribeOp { id, @@ -519,7 +519,7 @@ mod test { 2, ) .await; - sim_nodes.build_with_specs(subscribe_specs).await; + sim_nodes.start_with_spec(subscribe_specs).await; check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(3)).await?; Ok(()) diff --git a/crates/core/src/ring.rs b/crates/core/src/ring.rs index 86337d016..559cc678f 100644 --- a/crates/core/src/ring.rs +++ b/crates/core/src/ring.rs @@ -343,13 +343,32 @@ impl Ring { /// Get a random peer from the known ring connections. pub fn random_peer(&self, filter_fn: F) -> Option where - F: FnMut(&&PeerKeyLocation) -> bool, + F: Fn(&PeerKey) -> bool, { - self.connections_by_location - .read() - .values() - .find(filter_fn) - .copied() + use rand::Rng; + let peers = &*self.location_for_peer.read(); + let amount = peers.len(); + if amount == 0 { + return None; + } + let mut rng = rand::thread_rng(); + let mut attempts = 0; + loop { + if attempts >= amount { + return None; + } + let selected = rng.gen_range(0..amount); + let (peer, loc) = peers.iter().nth(selected).expect("infallible"); + if !filter_fn(peer) { + attempts += 1; + continue; + } else { + return Some(PeerKeyLocation { + peer: *peer, + location: Some(*loc), + }); + } + } } /// Will return an error in case the max number of subscribers has been added. @@ -466,8 +485,7 @@ impl From<&ContractKey> for Location { impl Display for Location { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.0.to_string().as_str())?; - Ok(()) + write!(f, "{}", self.0) } } @@ -569,10 +587,7 @@ pub(crate) enum RingError { #[cfg(test)] mod test { use super::*; - use crate::client_events::test::MemoryEventsGen; - use tokio::sync::watch::channel; - #[ignore] #[test] fn location_dist() { let l0 = Location(0.); @@ -583,58 +598,4 @@ mod test { let l1 = Location(0.50); assert!(l0.distance(l1) == Distance(0.25)); } - - #[ignore] - #[test] - fn find_closest() { - let peer_key: PeerKey = PeerKey::random(); - - let (_, receiver) = channel((0, peer_key)); - let user_events = MemoryEventsGen::new(receiver, peer_key); - let config = NodeBuilder::new([Box::new(user_events)]); - let ring = Ring::new::<1, node::TestEventListener>(&config, &[]).unwrap(); - - fn build_pk(loc: Location) -> PeerKeyLocation { - PeerKeyLocation { - peer: PeerKey::random(), - location: Some(loc), - } - } - - { - let conns = &mut *ring.connections_by_location.write(); - conns.insert(Location(0.3), build_pk(Location(0.3))); - conns.insert(Location(0.5), build_pk(Location(0.5))); - conns.insert(Location(0.0), build_pk(Location(0.0))); - } - - assert_eq!( - Location(0.0), - ring.routing(&Location(0.9), None, &[]) - .unwrap() - .location - .unwrap() - ); - assert_eq!( - Location(0.0), - ring.routing(&Location(0.1), None, &[]) - .unwrap() - .location - .unwrap() - ); - assert_eq!( - Location(0.5), - ring.routing(&Location(0.41), None, &[]) - .unwrap() - .location - .unwrap() - ); - assert_eq!( - Location(0.3), - ring.routing(&Location(0.39), None, &[]) - .unwrap() - .location - .unwrap() - ); - } } diff --git a/crates/core/src/runtime/contract_store.rs b/crates/core/src/runtime/contract_store.rs index bc9883a35..4204ef549 100644 --- a/crates/core/src/runtime/contract_store.rs +++ b/crates/core/src/runtime/contract_store.rs @@ -51,7 +51,7 @@ pub struct ContractStore { key_to_code_part: Arc>, } // TODO: add functionality to delete old contracts which have not been used for a while -// to keep the total speed used under a configured threshold +// to keep the total space used under a configured threshold static LOCK_FILE_PATH: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); static KEY_FILE_PATH: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); diff --git a/crates/core/src/runtime/wasm_runtime.rs b/crates/core/src/runtime/wasm_runtime.rs index 7c5d32e83..b7d2a6446 100644 --- a/crates/core/src/runtime/wasm_runtime.rs +++ b/crates/core/src/runtime/wasm_runtime.rs @@ -209,7 +209,6 @@ impl Runtime { let module = if let Some(module) = self.delegate_modules.get(key) { module } else { - // FIXME let delegate = self .delegate_store .fetch_delegate(key, params) diff --git a/crates/core/src/server/mod.rs b/crates/core/src/server/mod.rs index 730c2a4de..e9a56b4cf 100644 --- a/crates/core/src/server/mod.rs +++ b/crates/core/src/server/mod.rs @@ -130,7 +130,7 @@ pub mod local_node { if let Some(cause) = cause { tracing::info!("disconnecting cause: {cause}"); } - // todo: token must live for a bit to allow reconnections + // fixme: token must live for a bit to allow reconnections if let Some(rm_token) = gw .attested_contracts .iter() diff --git a/crates/fdev/src/build.rs b/crates/fdev/src/build.rs index a8c491d43..d16a6ef90 100644 --- a/crates/fdev/src/build.rs +++ b/crates/fdev/src/build.rs @@ -51,6 +51,7 @@ fn compile_options(cli_config: &BuildToolCliConfig) -> impl Iterator Result<(), DynError> { Error::CommandFailed("npm") })?; pipe_std_streams(child)?; - // todo: change pacakge.json: + // todo: change package.json: // - include dependencies: freenet-stdlib let child = Command::new(TSC) From 184dcddc8d7149d6c3a7bf92b600e822bbd39318 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Tue, 17 Oct 2023 14:50:48 +0200 Subject: [PATCH 57/76] Actually return back the populated connection list --- crates/core/src/message.rs | 14 +++---- crates/core/src/node.rs | 5 +-- .../core/src/node/conn_manager/p2p_protoc.rs | 2 +- crates/core/src/node/in_memory_impl.rs | 2 +- crates/core/src/operations.rs | 2 +- crates/core/src/operations/join_ring.rs | 37 ++++++++++--------- crates/core/src/ring.rs | 20 +++++++--- 7 files changed, 46 insertions(+), 36 deletions(-) diff --git a/crates/core/src/message.rs b/crates/core/src/message.rs index 551339abb..f284f7503 100644 --- a/crates/core/src/message.rs +++ b/crates/core/src/message.rs @@ -142,8 +142,8 @@ pub(crate) enum Message { Get(GetMsg), Subscribe(SubscribeMsg), Update(UpdateMsg), - /// Failed a transaction, informing of cancellation. - Canceled(Transaction), + /// Failed a transaction, informing of abortion. + Aborted(Transaction), } pub(crate) trait InnerMessage: Into { @@ -191,7 +191,7 @@ impl Message { Get(op) => op.id(), Subscribe(op) => op.id(), Update(_op) => todo!(), - Canceled(tx) => tx, + Aborted(tx) => tx, } } @@ -203,7 +203,7 @@ impl Message { Get(op) => op.target(), Subscribe(op) => op.target(), Update(_op) => todo!(), - Canceled(_) => None, + Aborted(_) => None, } } @@ -216,13 +216,13 @@ impl Message { Get(op) => op.terminal(), Subscribe(op) => op.terminal(), Update(_op) => todo!(), - Canceled(_) => true, + Aborted(_) => true, } } pub fn track_stats(&self) -> bool { use Message::*; - !matches!(self, JoinRing(_) | Subscribe(_) | Canceled(_)) + !matches!(self, JoinRing(_) | Subscribe(_) | Aborted(_)) } } @@ -236,7 +236,7 @@ impl Display for Message { Get(msg) => msg.fmt(f)?, Subscribe(msg) => msg.fmt(f)?, Update(_op) => todo!(), - Canceled(msg) => msg.fmt(f)?, + Aborted(msg) => msg.fmt(f)?, }; write!(f, "}}") } diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index f9b2e1156..7617d8680 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -286,10 +286,7 @@ where join_ring::initial_request(peer_key, *gateway, op_storage.ring.max_hops_to_live, tx_id); if let Some(mut backoff) = backoff { // backoff to retry later in case it failed - tracing::warn!( - "Performing a new join attempt, attempt number: {}", - backoff.retries() - ); + tracing::warn!("Performing a new join, attempt {}", backoff.retries() + 1); if backoff.sleep_async().await.is_none() { tracing::error!("Max number of retries reached"); return Err(OpError::MaxRetriesExceeded(tx_id, tx_id.tx_type())); diff --git a/crates/core/src/node/conn_manager/p2p_protoc.rs b/crates/core/src/node/conn_manager/p2p_protoc.rs index 18d0ca3de..38d1f194b 100644 --- a/crates/core/src/node/conn_manager/p2p_protoc.rs +++ b/crates/core/src/node/conn_manager/p2p_protoc.rs @@ -354,7 +354,7 @@ impl P2pConnManager { Ok(Left((msg, client_id))) => { let cb = self.bridge.clone(); match msg { - Message::Canceled(tx) => { + Message::Aborted(tx) => { let tx_type = tx.tx_type(); let res = handle_cancelled_op( tx, diff --git a/crates/core/src/node/in_memory_impl.rs b/crates/core/src/node/in_memory_impl.rs index e445eb401..bdbc8b1b5 100644 --- a/crates/core/src/node/in_memory_impl.rs +++ b/crates/core/src/node/in_memory_impl.rs @@ -160,7 +160,7 @@ impl NodeInMemory { } }; - if let Ok(Either::Left(Message::Canceled(tx))) = msg { + if let Ok(Either::Left(Message::Aborted(tx))) = msg { let tx_type = tx.tx_type(); let res = handle_cancelled_op( tx, diff --git a/crates/core/src/operations.rs b/crates/core/src/operations.rs index a2ee51e35..410bff877 100644 --- a/crates/core/src/operations.rs +++ b/crates/core/src/operations.rs @@ -75,7 +75,7 @@ where } Err((err, tx_id)) => { if let Some(sender) = sender { - conn_manager.send(&sender, Message::Canceled(tx_id)).await?; + conn_manager.send(&sender, Message::Aborted(tx_id)).await?; } return Err(err); } diff --git a/crates/core/src/operations/join_ring.rs b/crates/core/src/operations/join_ring.rs index 11ab357f5..d33fd98ed 100644 --- a/crates/core/src/operations/join_ring.rs +++ b/crates/core/src/operations/join_ring.rs @@ -17,8 +17,6 @@ use crate::{ pub(crate) use self::messages::{JoinRequest, JoinResponse, JoinRingMsg}; -const MAX_JOIN_RETRIES: usize = 3; - pub(crate) struct JoinRingOp { id: Transaction, state: Option, @@ -132,7 +130,7 @@ impl Operation for JoinRingOp { tracing::debug!(tx = %id, "Accepting connection from {}", req_peer,); HashSet::from_iter([this_node_loc]) } else { - tracing::debug!(tx = %id, "Rejecting connection from peer {}", req_peer); + tracing::debug!(tx = %id, at_peer = %this_node_loc.peer, "Rejecting connection from peer {}", req_peer); HashSet::new() }; @@ -334,16 +332,13 @@ impl Operation for JoinRingOp { }; // fixme: remove - tracing::debug!( - "accepted by: \nstate: {:?} \nlist: {:?}", - self.state, - accepted_by - ); + tracing::debug!("accepted by state: {:?} ", self.state,); let Some(JRState::Connecting(ConnectionInfo { gateway, .. })) = self.state else { return Err(OpError::InvalidStateTransition(self.id)); }; if !accepted_by.is_empty() { + tracing::debug!("accepted by list: {:?} ", accepted_by); tracing::debug!( tx = %id, "OC received and acknowledged at requesting peer {} from gateway {}", @@ -385,7 +380,7 @@ impl Operation for JoinRingOp { tracing::debug!( tx = %id, peer = %your_peer_id, - "No accepted connections, failed" + "Failed to establish any connections, aborting" ); let op = JoinRingOp { id, @@ -396,7 +391,7 @@ impl Operation for JoinRingOp { }; op_storage .notify_op_change( - Message::Canceled(id), + Message::Aborted(id), OpEnum::JoinRing(op.into()), None, ) @@ -472,7 +467,7 @@ impl Operation for JoinRingOp { target: state_target, sender: target, msg: JoinResponse::AcceptedBy { - peers: accepted_by, + peers: previously_accepted, your_location: new_location, your_peer_id: new_peer_id, }, @@ -490,7 +485,9 @@ impl Operation for JoinRingOp { id, target: state_target, sender: target, - msg: JoinResponse::Proxy { accepted_by }, + msg: JoinResponse::Proxy { + accepted_by: previously_accepted, + }, }); } } @@ -733,19 +730,25 @@ pub(crate) fn initial_request( max_hops_to_live: usize, id: Transaction, ) -> JoinRingOp { + const MAX_JOIN_RETRIES: usize = 3; tracing::debug!(tx = %id, "Connecting to gw {} from {}", gateway.peer, this_peer); let state = JRState::Connecting(ConnectionInfo { gateway, this_peer, max_hops_to_live, }); + let ceiling = if cfg!(test) { + Duration::from_secs(1) + } else { + Duration::from_secs(120) + }; JoinRingOp { id, state: Some(state), gateway: Box::new(gateway), backoff: Some(ExponentialBackoff::new( Duration::from_secs(1), - Duration::from_secs(120), + ceiling, MAX_JOIN_RETRIES, )), _ttl: PEER_TIMEOUT, @@ -1069,14 +1072,14 @@ mod test { "join_forward_connection_to_node", NUM_GW, NUM_NODES, - 4, - 2, - 4, + 3, 2, + 8, + 5, ) .await; sim_nodes.start().await; - check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(3)).await + check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(10)).await } /// Given a network of N peers all nodes should have connections. diff --git a/crates/core/src/ring.rs b/crates/core/src/ring.rs index 559cc678f..df2b8686e 100644 --- a/crates/core/src/ring.rs +++ b/crates/core/src/ring.rs @@ -251,23 +251,27 @@ impl Ring { /// # Panic /// Will panic if the node checking for this condition has no location assigned. pub fn should_accept(&self, location: &Location) -> bool { + let cbl = &*self.connections_by_location.read(); let open_conn = self.open_connections.fetch_add(1, SeqCst) + 1; let my_location = &self .own_location() .location .expect("this node has no location assigned!"); - let cbl = &*self.connections_by_location.read(); let accepted = if location == my_location || cbl.contains_key(location) { false } else if open_conn < self.min_connections { true } else if open_conn >= self.max_connections { + tracing::debug!(peer = %self.peer_key, "max connections reached"); false } else { - my_location.distance(location) - < self - .median_distance_to(my_location) - .unwrap_or(Distance(0.5)) + let median_distance = self + .median_distance_to(my_location) + .unwrap_or(Distance(0.5)); + let dist_to_loc = my_location.distance(location); + let is_lower_than_median = dist_to_loc < median_distance; + tracing::debug!("dist to connection loc: {dist_to_loc}, median dist: {median_distance}, accepting: {is_lower_than_median}"); + is_lower_than_median }; if !accepted { self.open_connections.fetch_sub(1, SeqCst); @@ -574,6 +578,12 @@ impl Ord for Distance { } } +impl Display for Distance { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + #[derive(thiserror::Error, Debug)] pub(crate) enum RingError { #[error(transparent)] From da8a1d70d88f72efb81d4aacd210c7a953b4528e Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Wed, 18 Oct 2023 15:13:21 +0200 Subject: [PATCH 58/76] Fixes to event log and sim connectivity report --- crates/core/src/config.rs | 23 +- crates/core/src/node.rs | 23 +- .../core/src/node/conn_manager/in_memory.rs | 64 ++++-- .../core/src/node/conn_manager/p2p_protoc.rs | 80 ++++++- crates/core/src/node/event_log.rs | 208 +++++++++++++----- crates/core/src/node/in_memory_impl.rs | 7 +- crates/core/src/node/p2p_impl.rs | 6 +- crates/core/src/node/tests.rs | 139 ++++++------ crates/core/src/operations/get.rs | 32 +-- crates/core/src/operations/join_ring.rs | 28 +-- crates/core/src/operations/put.rs | 24 +- crates/core/src/operations/subscribe.rs | 8 +- 12 files changed, 419 insertions(+), 223 deletions(-) diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index f5cd1167a..463414add 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -324,17 +324,18 @@ impl libp2p::swarm::Executor for GlobalExecutor { } pub fn set_logger() { - tracing_subscriber::fmt() - .with_level(true) - .with_file(true) - .with_line_number(true) - .with_env_filter( - tracing_subscriber::EnvFilter::builder() - .with_default_directive(tracing_subscriber::filter::LevelFilter::DEBUG.into()) - .from_env_lossy() - .add_directive("stretto=off".parse().unwrap()), - ) - .init(); + let sub = tracing_subscriber::fmt().with_level(true).with_env_filter( + tracing_subscriber::EnvFilter::builder() + .with_default_directive(tracing_subscriber::filter::LevelFilter::DEBUG.into()) + .from_env_lossy() + .add_directive("stretto=off".parse().unwrap()), + ); + + if cfg!(any(test, debug_assertions)) { + sub.with_file(true).with_line_number(true).init(); + } else { + sub.init(); + } } pub(super) mod tracer { diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index 7617d8680..ebc372f60 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -15,6 +15,7 @@ use std::{ time::Duration, }; +use either::Either; use freenet_stdlib::client_api::{ClientRequest, ContractRequest}; use libp2p::{identity, multiaddr::Protocol, Multiaddr, PeerId}; @@ -469,12 +470,13 @@ async fn report_result( payload_transfer_time, }, }; - if let Err(err) = event_listener - .event_received(EventLog::route_event(op_res.id(), op_storage, &event)) - .await - { - tracing::warn!("failed logging event: {err}"); - } + event_listener + .register_events(Either::Left(EventLog::route_event( + op_res.id(), + op_storage, + &event, + ))) + .await; op_storage.ring.routing_finished(event); } // todo: handle failures, need to track timeouts and other potential failures @@ -516,12 +518,9 @@ async fn process_message( let cli_req = client_id.zip(client_req_handler_callback); match msg { Ok(msg) => { - if let Err(err) = event_listener - .event_received(EventLog::from_msg(&msg, &op_storage)) - .await - { - tracing::warn!("failed logging event: {err}"); - } + event_listener + .register_events(EventLog::from_inbound_msg(&msg, &op_storage)) + .await; match msg { Message::JoinRing(op) => { log_handling_msg!("join", op.id(), op_storage); diff --git a/crates/core/src/node/conn_manager/in_memory.rs b/crates/core/src/node/conn_manager/in_memory.rs index ca62c9144..f1fbda01f 100644 --- a/crates/core/src/node/conn_manager/in_memory.rs +++ b/crates/core/src/node/conn_manager/in_memory.rs @@ -8,23 +8,33 @@ use std::{ use crossbeam::channel::{self, Receiver, Sender}; use once_cell::sync::OnceCell; -use parking_lot::Mutex; use rand::{prelude::StdRng, thread_rng, Rng, SeedableRng}; +use tokio::sync::Mutex; use super::{ConnectionBridge, ConnectionError, PeerKey}; -use crate::{config::GlobalExecutor, message::Message}; +use crate::{ + config::GlobalExecutor, + message::Message, + node::{event_log::EventLog, EventLogRegister, OpManager}, +}; static NETWORK_WIRES: OnceCell<(Sender, Receiver)> = OnceCell::new(); pub(in crate::node) struct MemoryConnManager { pub transport: InMemoryTransport, + log_register: Arc>>, + op_manager: Arc, msg_queue: Arc>>, peer: PeerKey, } impl MemoryConnManager { - pub fn new(peer: PeerKey) -> Self { + pub fn new( + peer: PeerKey, + log_register: Box, + op_manager: Arc, + ) -> Self { let transport = InMemoryTransport::new(peer); let msg_queue = Arc::new(Mutex::new(Vec::new())); @@ -33,21 +43,21 @@ impl MemoryConnManager { GlobalExecutor::spawn(async move { // evaluate the messages as they arrive loop { - let msg = { tr_cp.msg_stack_queue.lock().pop() }; - if let Some(msg) = msg { - let msg_data: Message = - bincode::deserialize_from(Cursor::new(msg.data)).unwrap(); - if let Some(mut queue) = msg_queue_cp.try_lock() { - queue.push(msg_data); - std::mem::drop(queue); - } + let Some(msg) = tr_cp.msg_stack_queue.lock().await.pop() else { + tokio::time::sleep(Duration::from_millis(10)).await; + continue; + }; + let msg_data: Message = bincode::deserialize_from(Cursor::new(msg.data)).unwrap(); + if let Ok(mut queue) = msg_queue_cp.try_lock() { + queue.push(msg_data); } - tokio::time::sleep(Duration::from_millis(10)).await; } }); Self { transport, + log_register: Arc::new(Mutex::new(log_register)), + op_manager, msg_queue, peer, } @@ -55,22 +65,27 @@ impl MemoryConnManager { pub async fn recv(&self) -> Result { loop { - if let Some(mut queue) = self.msg_queue.try_lock() { - let msg = queue.pop(); - std::mem::drop(queue); - if let Some(msg) = msg { - return Ok(msg); - } - } - tokio::time::sleep(Duration::from_millis(10)).await; + let Some(msg) = self.msg_queue.try_lock().ok().and_then(|mut l| l.pop()) else { + tokio::time::sleep(Duration::from_millis(10)).await; + continue; + }; + return Ok(msg); } } } impl Clone for MemoryConnManager { fn clone(&self) -> Self { + let log_register = loop { + if let Ok(lr) = self.log_register.try_lock() { + break lr.trait_clone(); + } + std::thread::sleep(Duration::from_nanos(50)); + }; Self { transport: self.transport.clone(), + log_register: Arc::new(Mutex::new(log_register)), + op_manager: self.op_manager.clone(), msg_queue: self.msg_queue.clone(), peer: self.peer, } @@ -80,6 +95,11 @@ impl Clone for MemoryConnManager { #[async_trait::async_trait] impl ConnectionBridge for MemoryConnManager { async fn send(&self, target: &PeerKey, msg: Message) -> super::ConnResult<()> { + self.log_register + .try_lock() + .expect("unique lock") + .register_events(EventLog::from_outbound_msg(&msg, &self.op_manager)) + .await; let msg = bincode::serialize(&msg)?; self.transport.send(*target, msg); Ok(()) @@ -139,7 +159,7 @@ impl InMemoryTransport { delayed.entry(msg.target).or_default().push(msg); tokio::time::sleep(Duration::from_millis(10)).await; } else { - rcv_msg_c.lock().push(msg); + rcv_msg_c.lock().await.push(msg); } } Ok(msg) => { @@ -156,7 +176,7 @@ impl InMemoryTransport { && !delayed.is_empty()) || delayed.len() == MAX_DELAYED_MSG { - let mut queue = rcv_msg_c.lock(); + let mut queue = rcv_msg_c.lock().await; for (_, msgs) in delayed.drain() { queue.extend(msgs); } diff --git a/crates/core/src/node/conn_manager/p2p_protoc.rs b/crates/core/src/node/conn_manager/p2p_protoc.rs index 38d1f194b..674c7f235 100644 --- a/crates/core/src/node/conn_manager/p2p_protoc.rs +++ b/crates/core/src/node/conn_manager/p2p_protoc.rs @@ -5,6 +5,7 @@ use std::{ pin::Pin, sync::Arc, task::Poll, + time::Duration, }; use asynchronous_codec::{BytesMut, Framed}; @@ -31,7 +32,10 @@ use libp2p::{ }, InboundUpgrade, Multiaddr, OutboundUpgrade, PeerId, Swarm, }; -use tokio::sync::mpsc::{self, Receiver, Sender}; +use tokio::sync::{ + mpsc::{self, Receiver, Sender}, + Mutex, +}; use unsigned_varint::codec::UviBytes; use super::{ConnectionBridge, ConnectionError, EventLoopNotifications}; @@ -41,8 +45,8 @@ use crate::{ contract::{ClientResponsesSender, ExecutorToEventLoopChannel, NetworkEventListenerHalve}, message::{Message, NodeEvent, Transaction, TransactionType}, node::{ - handle_cancelled_op, join_ring_request, process_message, EventLogRegister, InitPeerNode, - NodeBuilder, OpManager, PeerKey, + event_log::EventLog, handle_cancelled_op, join_ring_request, process_message, + EventLogRegister, InitPeerNode, NodeBuilder, OpManager, PeerKey, }, operations::OpError, ring::PeerKeyLocation, @@ -117,19 +121,73 @@ fn multiaddr_from_connection(conn: (IpAddr, u16)) -> Multiaddr { type P2pBridgeEvent = Either<(PeerKey, Box), NodeEvent>; -#[derive(Clone)] pub(crate) struct P2pBridge { active_net_connections: Arc>, accepted_peers: Arc>, ev_listener_tx: Sender, + op_manager: Arc, + log_register: Arc>>, } impl P2pBridge { - fn new(sender: Sender) -> Self { + fn new( + sender: Sender, + op_manager: Arc, + event_register: EL, + ) -> Self + where + EL: EventLogRegister, + { Self { active_net_connections: Arc::new(DashMap::new()), accepted_peers: Arc::new(DashSet::new()), ev_listener_tx: sender, + op_manager, + log_register: Arc::new(Mutex::new(Box::new(event_register))), + } + } +} + +#[cfg(any(debug_assertions, test))] +static CONTESTED_BRIDGE_CLONES: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); + +#[cfg(any(debug_assertions, test))] +static TOTAL_BRIDGE_CLONES: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + +impl Clone for P2pBridge { + fn clone(&self) -> Self { + let log_register = loop { + if let Ok(lr) = self.log_register.try_lock() { + #[cfg(any(debug_assertions, test))] + { + TOTAL_BRIDGE_CLONES.fetch_add(1, std::sync::atomic::Ordering::AcqRel); + } + break lr.trait_clone(); + } + #[cfg(any(debug_assertions, test))] + { + let contested = + CONTESTED_BRIDGE_CLONES.fetch_add(1, std::sync::atomic::Ordering::AcqRel); + + if contested % 100 == 0 { + let total = TOTAL_BRIDGE_CLONES.load(std::sync::atomic::Ordering::Acquire); + if total > 0 { + let threshold = (total as f64 * 0.01) as usize; + if contested / total > threshold { + tracing::debug!("p2p bridge clone contested more than 1% of the time"); + } + } + } + } + std::thread::sleep(Duration::from_nanos(50)); + }; + Self { + active_net_connections: self.active_net_connections.clone(), + accepted_peers: self.accepted_peers.clone(), + ev_listener_tx: self.ev_listener_tx.clone(), + op_manager: self.op_manager.clone(), + log_register: Arc::new(Mutex::new(log_register)), } } } @@ -157,6 +215,10 @@ impl ConnectionBridge for P2pBridge { } async fn send(&self, target: &PeerKey, msg: Message) -> super::ConnResult<()> { + self.log_register + .lock() + .await + .register_events(EventLog::from_outbound_msg(&msg, &self.op_manager)); self.ev_listener_tx .send(Left((*target, Box::new(msg)))) .await @@ -180,7 +242,7 @@ impl P2pConnManager { transport: transport::Boxed<(PeerId, muxing::StreamMuxerBox)>, config: &NodeBuilder, op_manager: Arc, - event_listener: &dyn EventLogRegister, + event_listener: impl EventLogRegister + Clone, ) -> Result { // We set a global executor which is virtually the Tokio multi-threaded executor // to reuse it's thread pool and scheduler in order to drive futures. @@ -197,7 +259,7 @@ impl P2pConnManager { &config.local_key, &config.remote_nodes, &public_addr, - op_manager, + op_manager.clone(), ); let mut swarm = Swarm::new( transport, @@ -212,7 +274,7 @@ impl P2pConnManager { } let (tx_bridge_cmd, rx_bridge_cmd) = mpsc::channel(100); - let bridge = P2pBridge::new(tx_bridge_cmd); + let bridge = P2pBridge::new(tx_bridge_cmd, op_manager, event_listener.clone()); let gateways = config.get_gateways()?; Ok(P2pConnManager { @@ -221,7 +283,7 @@ impl P2pConnManager { bridge, conn_bridge_rx: rx_bridge_cmd, public_addr, - event_listener: event_listener.trait_clone(), + event_listener: Box::new(event_listener), }) } diff --git a/crates/core/src/node/event_log.rs b/crates/core/src/node/event_log.rs index b9caa4b6f..08f0ecab0 100644 --- a/crates/core/src/node/event_log.rs +++ b/crates/core/src/node/event_log.rs @@ -1,6 +1,7 @@ use std::{io, path::Path, time::SystemTime}; use chrono::{DateTime, Utc}; +use either::Either; use freenet_stdlib::prelude::*; use futures::{future::BoxFuture, FutureExt}; use serde::{Deserialize, Serialize}; @@ -14,8 +15,8 @@ use crate::{ config::GlobalExecutor, contract::StoreResponse, message::{Message, Transaction}, - operations::{get::GetMsg, join_ring::JoinRingMsg, put::PutMsg}, - ring::{Location, PeerKeyLocation}, + operations::{get::GetMsg, join_ring, put::PutMsg}, + ring::PeerKeyLocation, router::RouteEvent, DynError, }; @@ -34,7 +35,10 @@ struct ListenerLogId(usize); /// This type then can emit it's own information to adjacent systems /// or is a no-op. pub(crate) trait EventLogRegister: std::any::Any + Send + Sync + 'static { - fn event_received<'a>(&'a mut self, ev: EventLog) -> BoxFuture<'a, Result<(), DynError>>; + fn register_events<'a>( + &'a mut self, + events: Either, Vec>>, + ) -> BoxFuture<'a, ()>; fn trait_clone(&self) -> Box; fn as_any(&self) -> &dyn std::any::Any where @@ -44,7 +48,6 @@ pub(crate) trait EventLogRegister: std::any::Any + Send + Sync + 'static { } } -#[allow(dead_code)] // fixme: remove this pub(crate) struct EventLog<'a> { tx: &'a Transaction, peer_id: &'a PeerKey, @@ -64,15 +67,76 @@ impl<'a> EventLog<'a> { } } - pub fn from_msg(msg: &'a Message, op_storage: &'a OpManager) -> Self { + pub fn from_outbound_msg( + msg: &'a Message, + op_storage: &'a OpManager, + ) -> Either> { let kind = match msg { - Message::JoinRing(JoinRingMsg::Connected { sender, target, .. }) => { - EventKind::Connected { - loc: target.location.unwrap(), - from: target.peer, - to: *sender, + Message::JoinRing(join_ring::JoinRingMsg::Response { + msg: + join_ring::JoinResponse::AcceptedBy { + peers, + your_location, + your_peer_id, + }, + .. + }) => { + let this_peer = op_storage.ring.own_location(); + if peers.contains(&this_peer) { + EventKind::Connected { + this: this_peer, + connected: PeerKeyLocation { + peer: *your_peer_id, + location: Some(*your_location), + }, + } + } else { + EventKind::Ignored } } + _ => EventKind::Ignored, + }; + Either::Left(EventLog { + tx: msg.id(), + peer_id: &op_storage.ring.peer_key, + kind, + }) + } + + pub fn from_inbound_msg( + msg: &'a Message, + op_storage: &'a OpManager, + ) -> Either> { + let kind = match msg { + Message::JoinRing(join_ring::JoinRingMsg::Response { + msg: + join_ring::JoinResponse::AcceptedBy { + peers, + your_location, + your_peer_id, + }, + .. + }) => { + return Either::Right( + peers + .iter() + .map(|peer| { + let kind: EventKind = EventKind::Connected { + this: PeerKeyLocation { + peer: *your_peer_id, + location: Some(*your_location), + }, + connected: *peer, + }; + EventLog { + tx: msg.id(), + peer_id: &op_storage.ring.peer_key, + kind, + } + }) + .collect(), + ); + } Message::Put(PutMsg::RequestPut { contract, target, .. }) => { @@ -113,13 +177,13 @@ impl<'a> EventLog<'a> { value: StoreResponse { state: Some(_), .. }, .. }) => EventKind::Get { key: key.clone() }, - _ => EventKind::Unknown, + _ => EventKind::Ignored, }; - EventLog { + Either::Left(EventLog { tx: msg.id(), peer_id: &op_storage.ring.peer_key, kind, - } + }) } } @@ -339,14 +403,35 @@ impl EventRegister { } impl EventLogRegister for EventRegister { - fn event_received<'a>(&'a mut self, log: EventLog) -> BoxFuture<'a, Result<(), DynError>> { - let log_msg = LogMessage { - datetime: Utc::now(), - tx: *log.tx, - kind: log.kind, - peer_id: *log.peer_id, - }; - async { Ok(self.log_sender.send(log_msg).await?) }.boxed() + fn register_events<'a>( + &'a mut self, + logs: Either, Vec>>, + ) -> BoxFuture<'a, ()> { + async { + match logs { + Either::Left(log) => { + let log_msg = LogMessage { + datetime: Utc::now(), + tx: *log.tx, + kind: log.kind, + peer_id: *log.peer_id, + }; + let _ = self.log_sender.send(log_msg).await; + } + Either::Right(logs) => { + for log in logs { + let log_msg = LogMessage { + datetime: Utc::now(), + tx: *log.tx, + kind: log.kind, + peer_id: *log.peer_id, + }; + let _ = self.log_sender.send(log_msg).await; + } + } + } + } + .boxed() } fn trait_clone(&self) -> Box { @@ -355,18 +440,18 @@ impl EventLogRegister for EventRegister { } #[derive(Serialize, Deserialize)] +// todo: make this take by ref instead enum EventKind { Connected { - loc: Location, - from: PeerKey, - to: PeerKeyLocation, + this: PeerKeyLocation, + connected: PeerKeyLocation, }, Put(PutEvent), Get { key: ContractKey, }, Route(RouteEvent), - Unknown, + Ignored, } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] @@ -408,7 +493,7 @@ pub(super) mod test_utils { }; use dashmap::DashMap; - use parking_lot::RwLock; + use parking_lot::Mutex; use super::*; use crate::{node::tests::NodeLabel, ring::Distance}; @@ -419,7 +504,7 @@ pub(super) mod test_utils { pub(crate) struct TestEventListener { node_labels: Arc>, tx_log: Arc>>, - logs: Arc>>, + logs: Arc>>, } impl TestEventListener { @@ -427,7 +512,7 @@ pub(super) mod test_utils { TestEventListener { node_labels: Arc::new(DashMap::new()), tx_log: Arc::new(DashMap::new()), - logs: Arc::new(RwLock::new(Vec::new())), + logs: Arc::new(Mutex::new(Vec::new())), } } @@ -436,7 +521,7 @@ pub(super) mod test_utils { } pub fn is_connected(&self, peer: &PeerKey) -> bool { - let logs = self.logs.read(); + let logs = self.logs.lock(); logs.iter() .any(|log| &log.peer_id == peer && matches!(log.kind, EventKind::Connected { .. })) } @@ -447,7 +532,7 @@ pub(super) mod test_utils { for_key: &ContractKey, expected_value: &WrappedState, ) -> bool { - let logs = self.logs.read(); + let logs = self.logs.lock(); let put_ops = logs.iter().filter_map(|l| match &l.kind { EventKind::Put(ev) => Some((&l.tx, ev)), _ => None, @@ -485,7 +570,7 @@ pub(super) mod test_utils { /// The contract was broadcasted from one peer to an other successfully. pub fn contract_broadcasted(&self, for_key: &ContractKey) -> bool { - let logs = self.logs.read(); + let logs = self.logs.lock(); let put_broadcast_ops = logs.iter().filter_map(|l| match &l.kind { EventKind::Put(ev @ PutEvent::BroadcastEmitted { .. }) | EventKind::Put(ev @ PutEvent::BroadcastReceived { .. }) => Some((&l.tx, ev)), @@ -518,7 +603,7 @@ pub(super) mod test_utils { } pub fn has_got_contract(&self, peer: &PeerKey, expected_key: &ContractKey) -> bool { - let logs = self.logs.read(); + let logs = self.logs.lock(); logs.iter().any(|log| { &log.peer_id == peer && matches!(log.kind, EventKind::Get { ref key } if key == expected_key ) @@ -527,12 +612,18 @@ pub(super) mod test_utils { /// Unique connections for a given peer and their relative distance to other peers. pub fn connections(&self, peer: PeerKey) -> impl Iterator { - let logs = self.logs.read(); + let logs = self.logs.lock(); logs.iter() .filter_map(|l| { - if let EventKind::Connected { loc, from, to } = l.kind { - if from == peer { - return Some((to.peer, loc.distance(to.location.unwrap()))); + if let EventKind::Connected { this, connected } = l.kind { + if this.peer == peer { + return Some(( + connected.peer, + connected + .location + .expect("set location") + .distance(this.location.unwrap()), + )); } } None @@ -555,14 +646,28 @@ pub(super) mod test_utils { } impl super::EventLogRegister for TestEventListener { - fn event_received<'a>(&'a mut self, log: EventLog) -> BoxFuture<'a, Result<(), DynError>> { - let tx = log.tx; - let mut logs = self.logs.write(); - let (msg_log, log_id) = Self::create_log(log); - logs.push(msg_log); - std::mem::drop(logs); - self.tx_log.entry(*tx).or_default().push(log_id); - async { Ok(()) }.boxed() + fn register_events<'a>( + &'a mut self, + logs: Either, Vec>>, + ) -> BoxFuture<'a, ()> { + match logs { + Either::Left(log) => { + let tx = log.tx; + let (msg_log, log_id) = Self::create_log(log); + self.logs.lock().push(msg_log); + self.tx_log.entry(*tx).or_default().push(log_id); + } + Either::Right(logs) => { + let logs_list = &mut *self.logs.lock(); + for log in logs { + let tx = log.tx; + let (msg_log, log_id) = Self::create_log(log); + logs_list.push(msg_log); + self.tx_log.entry(*tx).or_default().push(log_id); + } + } + } + async {}.boxed() } fn trait_clone(&self) -> Box { @@ -572,9 +677,10 @@ pub(super) mod test_utils { #[test] fn test_get_connections() -> Result<(), anyhow::Error> { + use crate::ring::Location; let peer_id = PeerKey::random(); let loc = Location::try_from(0.5)?; - let tx = Transaction::new::(); + let tx = Transaction::new::(); let locations = [ (PeerKey::random(), Location::try_from(0.5)?), (PeerKey::random(), Location::try_from(0.75)?), @@ -583,18 +689,20 @@ pub(super) mod test_utils { let mut listener = TestEventListener::new(); locations.iter().for_each(|(other, location)| { - listener.event_received(EventLog { + listener.register_events(Either::Left(EventLog { tx: &tx, peer_id: &peer_id, kind: EventKind::Connected { - loc, - from: peer_id, - to: PeerKeyLocation { + this: PeerKeyLocation { + peer: peer_id, + location: Some(loc), + }, + connected: PeerKeyLocation { peer: *other, location: Some(*location), }, }, - }); + })); }); let distances: Vec<_> = listener.connections(peer_id).collect(); diff --git a/crates/core/src/node/in_memory_impl.rs b/crates/core/src/node/in_memory_impl.rs index bdbc8b1b5..da2d4ce75 100644 --- a/crates/core/src/node/in_memory_impl.rs +++ b/crates/core/src/node/in_memory_impl.rs @@ -43,8 +43,8 @@ impl NodeInMemory { event_listener: EL, ch_builder: String, ) -> Result { + let event_listener = Box::new(event_listener); let peer_key = PeerKey::from(builder.local_key.public()); - let conn_manager = MemoryConnManager::new(peer_key); let gateways = builder.get_gateways()?; let is_gateway = builder.local_ip.zip(builder.local_port).is_some(); @@ -58,6 +58,9 @@ impl NodeInMemory { .await .map_err(|e| anyhow::anyhow!(e))?; + let conn_manager = + MemoryConnManager::new(peer_key, event_listener.trait_clone(), op_storage.clone()); + GlobalExecutor::spawn(contract::contract_handling(contract_handler)); Ok(NodeInMemory { @@ -66,7 +69,7 @@ impl NodeInMemory { op_storage, gateways, notification_channel, - event_listener: Box::new(event_listener), + event_listener, is_gateway, _executor_listener, }) diff --git a/crates/core/src/node/p2p_impl.rs b/crates/core/src/node/p2p_impl.rs index b6a3c5b35..2ea78a3bb 100644 --- a/crates/core/src/node/p2p_impl.rs +++ b/crates/core/src/node/p2p_impl.rs @@ -72,18 +72,18 @@ impl NodeP2P { .await } - pub(crate) async fn build( + pub(crate) async fn build( builder: NodeBuilder, event_listener: EL, ch_builder: CH::Builder, ) -> Result where CH: ContractHandler + Send + Sync + 'static, + EL: EventLogRegister + Clone, { let peer_key = PeerKey::from(builder.local_key.public()); let gateways = builder.get_gateways()?; - let event_listener: Box = Box::new(event_listener); let ring = Ring::new::(&builder, &gateways)?; let (notification_channel, notification_tx) = EventLoopNotifications::channel(); let (ch_outbound, ch_inbound) = contract::contract_handler_channel(); @@ -96,7 +96,7 @@ impl NodeP2P { let conn_manager = { let transport = Self::config_transport(&builder.local_key)?; - P2pConnManager::build(transport, &builder, op_storage.clone(), &*event_listener)? + P2pConnManager::build(transport, &builder, op_storage.clone(), event_listener)? }; GlobalExecutor::spawn(contract::contract_handling(contract_handler)); diff --git a/crates/core/src/node/tests.rs b/crates/core/src/node/tests.rs index 0ec5df0ed..d9f0d3416 100644 --- a/crates/core/src/node/tests.rs +++ b/crates/core/src/node/tests.rs @@ -56,6 +56,7 @@ impl NodeLabel { Self(format!("node-{id}").into()) } + #[allow(dead_code)] fn is_gateway(&self) -> bool { self.0.starts_with("gateway") } @@ -360,15 +361,19 @@ impl SimNetwork { } /// Builds an histogram of the distribution in the ring of each node relative to each other. - pub fn ring_distribution(&self, scale: i32) -> impl Iterator { + pub fn ring_distribution(&self, scale: i32) -> Vec<(f64, usize)> { let mut all_dists = Vec::with_capacity(self.labels.len()); for (.., key) in &self.labels { all_dists.push(self.event_listener.connections(*key)); } - group_locations_in_buckets( + let mut dist_buckets = group_locations_in_buckets( all_dists.into_iter().flatten().map(|(_, l)| l.as_f64()), scale, ) + .collect::>(); + dist_buckets + .sort_by(|(d0, _), (d1, _)| d0.partial_cmp(d1).unwrap_or(std::cmp::Ordering::Equal)); + dist_buckets } /// Returns the connectivity in the network per peer (that is all the connections @@ -405,6 +410,72 @@ impl SimNetwork { } Ok(()) } + + pub async fn check_connectivity(&self, time_out: Duration) -> Result<(), anyhow::Error> { + let num_nodes = self.nodes.capacity(); + let mut connected = HashSet::new(); + let elapsed = Instant::now(); + while elapsed.elapsed() < time_out && connected.len() < num_nodes { + for node in 0..num_nodes { + if !connected.contains(&node) && self.connected(&NodeLabel::node(node)) { + connected.insert(node); + } + } + } + tokio::time::sleep(Duration::from_millis(1_000)).await; + let expected = HashSet::from_iter(0..num_nodes); + let mut missing: Vec<_> = expected + .difference(&connected) + .map(|n| format!("node-{}", n)) + .collect(); + + let node_connectivity = self.node_connectivity(); + let connections = pretty_print_connections(&node_connectivity); + tracing::info!("Number of simulated nodes: {num_nodes}"); + tracing::info!("{connections}"); + + if !missing.is_empty() { + missing.sort(); + tracing::error!("Nodes without connection: {:?}", missing); + tracing::error!("Total nodes without connection: {:?}", missing.len()); + anyhow::bail!("found disconnected nodes"); + } + + tracing::info!( + "Required time for connecting all peers: {} secs", + elapsed.elapsed().as_secs() + ); + + let hist = self.ring_distribution(1); + tracing::info!("Ring distribution: {:?}", hist); + + Ok(()) + } + + pub fn connections_per_peer(&self) -> Result<(), anyhow::Error> { + let num_nodes = self.nodes.capacity(); + let node_connectivity = self.node_connectivity(); + + let mut connections_per_peer: Vec<_> = node_connectivity + .iter() + .map(|(k, v)| (k, v.1.len())) + .filter_map(|(k, v)| if !k.is_gateway() { Some(v) } else { None }) + .collect(); + + // ensure at least normal nodes have more than one connection + connections_per_peer.sort_unstable_by_key(|num_conn| *num_conn); + if *connections_per_peer.iter().last().unwrap() < 2 { + anyhow::bail!("low connectivy; some nodes didn't connect beyond the gateway"); + } + + // ensure the average number of connections per peer is above N + let avg_connections: usize = connections_per_peer.iter().sum::() / num_nodes; + tracing::info!("Average connections: {}", avg_connections); + if avg_connections < 1 { + anyhow::bail!("average number of connections is low"); + } + Ok(()) + } } impl Drop for SimNetwork { @@ -439,67 +510,6 @@ fn group_locations_in_buckets( .map(move |(k, v)| ((k as f64 / (10.0f64).powi(scale)), v)) } -pub(crate) async fn check_connectivity( - sim_nodes: &SimNetwork, - num_nodes: usize, - time_out: Duration, -) -> Result<(), anyhow::Error> { - let mut connected = HashSet::new(); - let elapsed = Instant::now(); - while elapsed.elapsed() < time_out && connected.len() < num_nodes { - for node in 0..num_nodes { - if !connected.contains(&node) && sim_nodes.connected(&NodeLabel::node(node)) { - connected.insert(node); - } - } - } - tokio::time::sleep(Duration::from_millis(1_000)).await; - let expected = HashSet::from_iter(0..num_nodes); - let mut missing: Vec<_> = expected - .difference(&connected) - .map(|n| format!("node-{}", n)) - .collect(); - - let node_connectivity = sim_nodes.node_connectivity(); - let connections = pretty_print_connections(&node_connectivity); - tracing::info!("{connections}"); - - if !missing.is_empty() { - missing.sort(); - tracing::error!("Nodes without connection: {:?}", missing); - tracing::error!("Total nodes without connection: {:?}", missing.len()); - anyhow::bail!("found disconnected nodes"); - } - - tracing::info!( - "Required time for connecting all peers: {} secs", - elapsed.elapsed().as_secs() - ); - - let hist: Vec<_> = sim_nodes.ring_distribution(1).collect(); - tracing::info!("Ring distribution: {:?}", hist); - - let mut connections_per_peer: Vec<_> = node_connectivity - .iter() - .map(|(k, v)| (k, v.1.len())) - .filter_map(|(k, v)| if !k.is_gateway() { Some(v) } else { None }) - .collect(); - - // ensure at least some normal nodes have more than one connection - connections_per_peer.sort_unstable_by_key(|num_conn| *num_conn); - if *connections_per_peer.iter().last().unwrap() < 1 { - anyhow::bail!("low connectivy; nodes didn't connect beyond the gateway"); - } - - // ensure the average number of connections per peer is above N - let avg_connections: usize = connections_per_peer.iter().sum::() / num_nodes; - tracing::info!("Average connections: {}", avg_connections); - if avg_connections < 1 { - anyhow::bail!("average number of connections is low"); - } - Ok(()) -} - fn pretty_print_connections( conns: &HashMap)>, ) -> String { @@ -507,9 +517,6 @@ fn pretty_print_connections( let mut conns = conns.iter().collect::>(); conns.sort_by(|(a, _), (b, _)| a.cmp(b)); for (peer, (key, conns)) in conns { - if peer.is_gateway() { - continue; - } writeln!(&mut connections, "{peer} ({key}):").unwrap(); for (conn, dist) in conns { let dist = dist.as_f64(); diff --git a/crates/core/src/operations/get.rs b/crates/core/src/operations/get.rs index 0c5129470..aa2731816 100644 --- a/crates/core/src/operations/get.rs +++ b/crates/core/src/operations/get.rs @@ -814,7 +814,7 @@ mod test { use std::collections::HashMap; use super::*; - use crate::node::tests::{check_connectivity, NodeSpecification, SimNetwork}; + use crate::node::tests::{NodeSpecification, SimNetwork}; #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn successful_get_op_between_nodes() -> Result<(), anyhow::Error> { @@ -851,7 +851,7 @@ mod test { let get_specs = HashMap::from_iter([("node-0".into(), node_0), ("gateway-0".into(), gw_0)]); // establish network - let mut sim_nodes = SimNetwork::new( + let mut sim_nw = SimNetwork::new( "successful_get_op_between_nodes", NUM_GW, NUM_NODES, @@ -861,15 +861,15 @@ mod test { 2, ) .await; - sim_nodes.start_with_spec(get_specs).await; - check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(3)).await?; + sim_nw.start_with_spec(get_specs).await; + sim_nw.check_connectivity(Duration::from_secs(3)).await?; // trigger get @ node-0, which does not own the contract - sim_nodes + sim_nw .trigger_event(&"node-0".into(), 1, Some(Duration::from_millis(100))) .await?; tokio::time::sleep(Duration::from_millis(200)).await; - assert!(sim_nodes.has_got_contract(&"node-0".into(), &key)); + assert!(sim_nw.has_got_contract(&"node-0".into(), &key)); Ok(()) } @@ -898,16 +898,16 @@ mod test { let get_specs = HashMap::from_iter([("node-1".into(), node_1)]); // establish network - let mut sim_nodes = + let mut sim_nw = SimNetwork::new("get_contract_not_found", NUM_GW, NUM_NODES, 3, 2, 4, 2).await; - sim_nodes.start_with_spec(get_specs).await; - check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(3)).await?; + sim_nw.start_with_spec(get_specs).await; + sim_nw.check_connectivity(Duration::from_secs(3)).await?; // trigger get @ node-1, which does not own the contract - sim_nodes + sim_nw .trigger_event(&"node-1".into(), 1, Some(Duration::from_millis(100))) .await?; - assert!(!sim_nodes.has_got_contract(&"node-1".into(), &key)); + assert!(!sim_nw.has_got_contract(&"node-1".into(), &key)); Ok(()) } @@ -959,7 +959,7 @@ mod test { ]); // establish network - let mut sim_nodes = SimNetwork::new( + let mut sim_nw = SimNetwork::new( "get_contract_found_after_retry", NUM_GW, NUM_NODES, @@ -969,13 +969,13 @@ mod test { 3, ) .await; - sim_nodes.start_with_spec(get_specs).await; - check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(3)).await?; + sim_nw.start_with_spec(get_specs).await; + sim_nw.check_connectivity(Duration::from_secs(3)).await?; - sim_nodes + sim_nw .trigger_event(&"node-0".into(), 1, Some(Duration::from_millis(500))) .await?; - assert!(sim_nodes.has_got_contract(&"node-0".into(), &key)); + assert!(sim_nw.has_got_contract(&"node-0".into(), &key)); Ok(()) } } diff --git a/crates/core/src/operations/join_ring.rs b/crates/core/src/operations/join_ring.rs index d33fd98ed..c8f70ba2e 100644 --- a/crates/core/src/operations/join_ring.rs +++ b/crates/core/src/operations/join_ring.rs @@ -432,13 +432,13 @@ impl Operation for JoinRingOp { Some(JRState::AwaitingProxyResponse { accepted_by: mut previously_accepted, new_peer_id, - target: state_target, + target: original_target, new_location, }) => { // Check if the response reached the target node and if the request // has been accepted by any node let is_accepted = !accepted_by.is_empty(); - let is_target_peer = new_peer_id == state_target.peer; + let is_target_peer = new_peer_id == original_target.peer; if is_accepted { previously_accepted.extend(accepted_by.drain()); @@ -460,11 +460,11 @@ impl Operation for JoinRingOp { "Sending response to join request with all the peers that accepted \ connection from gateway {} to peer {}", target.peer, - state_target.peer + original_target.peer ); return_msg = Some(JoinRingMsg::Response { id, - target: state_target, + target: original_target, sender: target, msg: JoinResponse::AcceptedBy { peers: previously_accepted, @@ -478,12 +478,12 @@ impl Operation for JoinRingOp { "Sending response to join request with all the peers that accepted \ connection from proxy peer {} to proxy peer {}", target.peer, - state_target.peer + original_target.peer ); return_msg = Some(JoinRingMsg::Response { id, - target: state_target, + target: original_target, sender: target, msg: JoinResponse::Proxy { accepted_by: previously_accepted, @@ -1051,7 +1051,7 @@ mod messages { mod test { use std::time::Duration; - use crate::node::tests::{check_connectivity, SimNetwork}; + use crate::node::tests::SimNetwork; /// Given a network of one node and one gateway test that both are connected. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -1068,18 +1068,18 @@ mod test { crate::config::set_logger(); const NUM_NODES: usize = 10usize; const NUM_GW: usize = 1usize; - let mut sim_nodes = SimNetwork::new( + let mut sim_nw = SimNetwork::new( "join_forward_connection_to_node", NUM_GW, NUM_NODES, 3, 2, - 8, 5, + 3, ) .await; - sim_nodes.start().await; - check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(10)).await + sim_nw.start().await; + sim_nw.check_connectivity(Duration::from_secs(10)).await } /// Given a network of N peers all nodes should have connections. @@ -1088,7 +1088,7 @@ mod test { async fn all_nodes_should_connect() -> Result<(), anyhow::Error> { const NUM_NODES: usize = 10usize; const NUM_GW: usize = 1usize; - let mut sim_nodes = SimNetwork::new( + let mut sim_nw = SimNetwork::new( "join_all_nodes_should_connect", NUM_GW, NUM_NODES, @@ -1098,7 +1098,7 @@ mod test { 2, ) .await; - sim_nodes.start().await; - check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(10)).await + sim_nw.start().await; + sim_nw.check_connectivity(Duration::from_secs(10)).await } } diff --git a/crates/core/src/operations/put.rs b/crates/core/src/operations/put.rs index 72102ce40..b1c00fa67 100644 --- a/crates/core/src/operations/put.rs +++ b/crates/core/src/operations/put.rs @@ -15,7 +15,7 @@ use crate::{ client_events::ClientId, config::PEER_TIMEOUT, contract::ContractHandlerEvent, - message::{InnerMessage, Message, Transaction }, + message::{InnerMessage, Message, Transaction}, node::{ConnectionBridge, OpManager, PeerKey}, operations::{op_trait::Operation, OpInitialization}, ring::{Location, PeerKeyLocation, RingError}, @@ -597,11 +597,7 @@ async fn try_to_broadcast( Ok((new_state, return_msg)) } -pub(crate) fn start_op( - contract: ContractContainer, - value: WrappedState, - htl: usize, -) -> PutOp { +pub(crate) fn start_op(contract: ContractContainer, value: WrappedState, htl: usize) -> PutOp { let key = contract.key(); let contract_location = Location::from(&key); tracing::debug!( @@ -926,7 +922,7 @@ mod test { use freenet_stdlib::prelude::*; use super::*; - use crate::node::tests::{check_connectivity, NodeSpecification, SimNetwork}; + use crate::node::tests::{NodeSpecification, SimNetwork}; #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn successful_put_op_between_nodes() -> Result<(), anyhow::Error> { @@ -941,7 +937,7 @@ mod test { let contract_val: WrappedState = gen.arbitrary()?; let new_value = WrappedState::new(Vec::from_iter(gen.arbitrary::<[u8; 20]>().unwrap())); - let mut sim_nodes = SimNetwork::new( + let mut sim_nw = SimNetwork::new( "successful_put_op_between_nodes", NUM_GW, NUM_NODES, @@ -951,7 +947,7 @@ mod test { 2, ) .await; - let mut locations = sim_nodes.get_locations_by_node(); + let mut locations = sim_nw.get_locations_by_node(); let node0_loc = locations.remove(&"node-0".into()).unwrap(); let node1_loc = locations.remove(&"node-1".into()).unwrap(); @@ -1000,16 +996,16 @@ mod test { ("gateway-0".into(), gw_0), ]); - sim_nodes.start_with_spec(put_specs).await; + sim_nw.start_with_spec(put_specs).await; tokio::time::sleep(Duration::from_secs(5)).await; - check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(3)).await?; + sim_nw.check_connectivity(Duration::from_secs(3)).await?; // trigger the put op @ gw-0 - sim_nodes + sim_nw .trigger_event(&"gateway-0".into(), 1, Some(Duration::from_secs(3))) .await?; - assert!(sim_nodes.has_put_contract(&"gateway-0".into(), &key, &new_value)); - assert!(sim_nodes.event_listener.contract_broadcasted(&key)); + assert!(sim_nw.has_put_contract(&"gateway-0".into(), &key, &new_value)); + assert!(sim_nw.event_listener.contract_broadcasted(&key)); Ok(()) } } diff --git a/crates/core/src/operations/subscribe.rs b/crates/core/src/operations/subscribe.rs index 28f1276bc..3f443ce3a 100644 --- a/crates/core/src/operations/subscribe.rs +++ b/crates/core/src/operations/subscribe.rs @@ -469,7 +469,7 @@ mod test { use freenet_stdlib::client_api::ContractRequest; use super::*; - use crate::node::tests::{check_connectivity, NodeSpecification, SimNetwork}; + use crate::node::tests::{NodeSpecification, SimNetwork}; #[ignore] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -509,7 +509,7 @@ mod test { ("node-0".into(), first_node), ("node-1".into(), second_node), ]); - let mut sim_nodes = SimNetwork::new( + let mut sim_nw = SimNetwork::new( "successful_subscribe_op_between_nodes", NUM_GW, NUM_NODES, @@ -519,8 +519,8 @@ mod test { 2, ) .await; - sim_nodes.start_with_spec(subscribe_specs).await; - check_connectivity(&sim_nodes, NUM_NODES, Duration::from_secs(3)).await?; + sim_nw.start_with_spec(subscribe_specs).await; + sim_nw.check_connectivity(Duration::from_secs(3)).await?; Ok(()) } From 78d614ea14569b464106a1de5491466b681f391d Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Thu, 19 Oct 2023 13:19:12 +0200 Subject: [PATCH 59/76] Cleanup a bunch of unnecessary code and todos --- Cargo.lock | 9 ++ apps/freenet-email-app/web/src/api.rs | 2 +- crates/core/Cargo.toml | 2 +- crates/core/src/client_events/websocket.rs | 2 +- crates/core/src/config.rs | 17 ++- crates/core/src/contract.rs | 2 +- crates/core/src/contract/executor.rs | 13 +- crates/core/src/contract/handler.rs | 68 ++++----- crates/core/src/contract/in_memory.rs | 31 +--- .../core/src/contract/storages/in_memory.rs | 41 ------ crates/core/src/contract/storages/mod.rs | 3 - crates/core/src/node.rs | 6 +- .../core/src/node/conn_manager/p2p_protoc.rs | 5 +- crates/core/src/node/in_memory_impl.rs | 2 +- .../node/{op_state.rs => op_state_manager.rs} | 9 +- crates/core/src/node/tests.rs | 65 ++++++--- crates/core/src/operations/get.rs | 2 +- crates/core/src/operations/join_ring.rs | 118 ++++++++++----- crates/core/src/ring.rs | 1 + crates/fdev/src/build.rs | 134 ++++++++---------- crates/fdev/src/inspect.rs | 2 - crates/fdev/src/local_node/commands.rs | 10 -- crates/fdev/src/new_package.rs | 4 +- 23 files changed, 267 insertions(+), 281 deletions(-) delete mode 100644 crates/core/src/contract/storages/in_memory.rs rename crates/core/src/node/{op_state.rs => op_state_manager.rs} (97%) diff --git a/Cargo.lock b/Cargo.lock index 3df61cda9..b3fb5b4ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3483,6 +3483,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-float" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "536900a8093134cf9ccf00a27deb3532421099e958d9dd431135d0c7543ca1e8" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-multimap" version = "0.4.3" diff --git a/apps/freenet-email-app/web/src/api.rs b/apps/freenet-email-app/web/src/api.rs index 8a5e21b00..642ba9ab3 100644 --- a/apps/freenet-email-app/web/src/api.rs +++ b/apps/freenet-email-app/web/src/api.rs @@ -39,7 +39,7 @@ impl WebApi { #[cfg(all(not(target_family = "wasm"), feature = "use-node"))] fn new() -> Result { - todo!() + unimplemented!() } #[cfg(all(target_family = "wasm", feature = "use-node"))] diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 965d770dd..ab104a28f 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -66,7 +66,7 @@ sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls"], optiona rocksdb = { version = "0.21.0", default-features = false, optional = true } pav_regression = "0.4.0" itertools = "0.11" -ordered-float = "3.9.1" +ordered-float = "4.1.1" notify = "6" wasmer = { workspace = true, features = [ "sys"] } chrono = { workspace = true } diff --git a/crates/core/src/client_events/websocket.rs b/crates/core/src/client_events/websocket.rs index c2840438a..47f7b4b12 100644 --- a/crates/core/src/client_events/websocket.rs +++ b/crates/core/src/client_events/websocket.rs @@ -474,7 +474,7 @@ impl ClientEventsProxy for WebSocketProxy { break Ok(reply.into_owned()); } } else { - todo!() + break Err(ClientError::from(ErrorKind::ChannelClosed)); } } } diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index 463414add..f12094421 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -143,10 +143,6 @@ impl ConfigPaths { } impl Config { - pub fn set_from_cli() -> Result<(), DynError> { - todo!() - } - pub fn set_op_mode(mode: OperationMode) { let local_mode = matches!(mode, OperationMode::Local); Self::conf() @@ -324,6 +320,19 @@ impl libp2p::swarm::Executor for GlobalExecutor { } pub fn set_logger() { + static LOGGER_SET: AtomicBool = AtomicBool::new(false); + if LOGGER_SET + .compare_exchange( + false, + true, + std::sync::atomic::Ordering::Acquire, + std::sync::atomic::Ordering::SeqCst, + ) + .is_err() + { + return; + } + let sub = tracing_subscriber::fmt().with_level(true).with_env_filter( tracing_subscriber::EnvFilter::builder() .with_default_directive(tracing_subscriber::filter::LevelFilter::DEBUG.into()) diff --git a/crates/core/src/contract.rs b/crates/core/src/contract.rs index 28bf08907..b018490fd 100644 --- a/crates/core/src/contract.rs +++ b/crates/core/src/contract.rs @@ -13,7 +13,7 @@ pub(crate) use executor::{ }; pub(crate) use handler::{ contract_handler_channel, ClientResponses, ClientResponsesSender, ContractHandler, - ContractHandlerEvent, ContractHandlerToEventLoopChannel, EventId, NetEventListener, + ContractHandlerEvent, ContractHandlerToEventLoopChannel, EventId, NetEventListenerHalve, NetworkContractHandler, StoreResponse, }; #[cfg(test)] diff --git a/crates/core/src/contract/executor.rs b/crates/core/src/contract/executor.rs index c37c5c5f8..82526414d 100644 --- a/crates/core/src/contract/executor.rs +++ b/crates/core/src/contract/executor.rs @@ -124,14 +124,6 @@ pub(crate) fn executor_channel( (listener_halve, sender_halve) } -#[cfg(test)] -pub(crate) fn executor_channel_test() -> ( - ExecutorToEventLoopChannel, - ExecutorToEventLoopChannel, -) { - todo!() -} - impl ExecutorToEventLoopChannel { async fn send_to_event_loop(&mut self, message: T) -> Result<(), DynError> where @@ -1200,6 +1192,7 @@ impl Executor { let contracts_data_dir = tmp_path.join("contracts"); let contract_store = ContractStore::new(contracts_data_dir, u16::MAX as i64)?; + // uses inmemory SQLite let state_store = StateStore::new(Storage::new().await?, u16::MAX as u32).unwrap(); let executor = Executor::new( @@ -1218,7 +1211,7 @@ impl Executor { _req: ClientRequest<'a>, _updates: Option>>, ) -> Response { - todo!() + unreachable!() } } @@ -1313,7 +1306,7 @@ impl ContractExecutor for Executor { .await?; return Ok(state); } - _ => todo!(), + _ => unreachable!(), } } } diff --git a/crates/core/src/contract/handler.rs b/crates/core/src/contract/handler.rs index c2dcc92f7..20a90c702 100644 --- a/crates/core/src/contract/handler.rs +++ b/crates/core/src/contract/handler.rs @@ -1,6 +1,5 @@ #![allow(unused)] // FIXME: remove this - -use std::collections::{BTreeMap, VecDeque}; +use std::collections::BTreeMap; use std::hash::Hash; use std::marker::PhantomData; use std::sync::atomic::{AtomicU64, Ordering::SeqCst}; @@ -19,15 +18,7 @@ use super::{ }; use crate::client_events::HostResult; use crate::message::Transaction; -use crate::node::OpManager; -use crate::{ - client_events::ClientId, - node::NodeConfig, - runtime::{ContractStore, Runtime, StateStorage, StateStore}, - DynError, -}; - -pub const MAX_MEM_CACHE: i64 = 10_000_000; +use crate::{client_events::ClientId, node::NodeConfig, runtime::Runtime, DynError}; pub(crate) struct ClientResponses(UnboundedReceiver<(ClientId, HostResult)>); @@ -188,12 +179,23 @@ impl ContractHandler for NetworkContractHandler { pub(crate) struct EventId { id: u64, client_id: Option, + transaction: Option, } impl EventId { pub fn client_id(&self) -> Option { self.client_id } + + pub fn transaction(&self) -> Option { + self.transaction + } + + // FIXME: this should be used somewhere to inform than an event is pending + // a transaction resolution + pub fn with_transaction(&mut self, transaction: Transaction) { + self.transaction = Some(transaction); + } } impl PartialEq for EventId { @@ -218,17 +220,17 @@ pub(crate) struct ContractHandlerToEventLoopChannel { } pub(crate) struct ContractHandlerHalve; -pub(crate) struct NetEventListener; +pub(crate) struct NetEventListenerHalve; mod sealed { - use super::{ContractHandlerHalve, NetEventListener}; + use super::{ContractHandlerHalve, NetEventListenerHalve}; pub(crate) trait ChannelHalve {} impl ChannelHalve for ContractHandlerHalve {} - impl ChannelHalve for NetEventListener {} + impl ChannelHalve for NetEventListenerHalve {} } pub(crate) fn contract_handler_channel() -> ( - ContractHandlerToEventLoopChannel, + ContractHandlerToEventLoopChannel, ContractHandlerToEventLoopChannel, ) { let (notification_tx, notification_channel) = mpsc::unbounded_channel(); @@ -258,7 +260,7 @@ static EV_ID: AtomicU64 = AtomicU64::new(0); // kind of event and can be optimized on a case basis const CH_EV_RESPONSE_TIME_OUT: Duration = Duration::from_secs(300); -impl ContractHandlerToEventLoopChannel { +impl ContractHandlerToEventLoopChannel { /// Send an event to the contract handler and receive a response event if successful. pub async fn send_to_handler( &mut self, @@ -289,7 +291,7 @@ impl ContractHandlerToEventLoopChannel { } } - pub async fn recv_from_handler(&mut self) -> (EventId, ContractHandlerEvent) { + pub async fn recv_from_handler(&mut self) -> EventId { todo!() } } @@ -317,6 +319,7 @@ impl ContractHandlerToEventLoopChannel { EventId { id: msg.id, client_id: msg.client_id, + transaction: None, }, msg.ev, )); @@ -369,9 +372,7 @@ impl std::fmt::Display for ContractHandlerEvent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ContractHandlerEvent::PutQuery { - key, - state, - parameters, + key, parameters, .. } => { if let Some(params) = parameters { write!(f, "put query {{ {key}, params: {:?} }}", params.as_ref()) @@ -394,10 +395,10 @@ impl std::fmt::Display for ContractHandlerEvent { write!(f, "get query {{ {key}, fetch contract: {fetch_contract} }}",) } ContractHandlerEvent::GetResponse { key, response } => match response { - Ok(v) => { + Ok(_) => { write!(f, "get query response {{ {key} }}",) } - Err(e) => { + Err(_) => { write!(f, "get query failed {{ {key} }}",) } }, @@ -411,24 +412,14 @@ impl std::fmt::Display for ContractHandlerEvent { } } -impl ContractHandlerEvent { - pub async fn into_network_op(self, op_manager: &OpManager) -> Transaction { - todo!() - } -} - #[cfg(test)] pub mod test { use std::sync::Arc; - use crate::runtime::ContractStore; - use freenet_stdlib::{ - client_api::{ClientRequest, ContractRequest, HostResponse}, - prelude::*, - }; + use freenet_stdlib::prelude::*; use super::*; - use crate::{config::GlobalExecutor, contract::MockRuntime}; + use crate::config::GlobalExecutor; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn channel_test() -> Result<(), anyhow::Error> { @@ -466,13 +457,4 @@ pub mod test { Ok(()) } - - // Prepare and get handler for an in-memory sqlite db - async fn get_handler(test: &str) -> Result, DynError> { - let (_, ch_handler) = contract_handler_channel(); - let (_, executor_sender) = super::super::executor::executor_channel_test(); - let handler = - NetworkContractHandler::build(ch_handler, executor_sender, test.to_owned()).await?; - Ok(handler) - } } diff --git a/crates/core/src/contract/in_memory.rs b/crates/core/src/contract/in_memory.rs index 8fa030941..1fc7149ac 100644 --- a/crates/core/src/contract/in_memory.rs +++ b/crates/core/src/contract/in_memory.rs @@ -1,7 +1,4 @@ -use crate::{ - client_events::ClientId, - runtime::{ContractStore, StateStorage, StateStore}, -}; +use crate::{client_events::ClientId, runtime::ContractStore}; use freenet_stdlib::{ client_api::{ClientError, ClientRequest, HostResponse}, prelude::WrappedContract, @@ -12,7 +9,6 @@ use tokio::sync::mpsc::UnboundedSender; use super::{ executor::{ExecutorHalve, ExecutorToEventLoopChannel}, handler::{ContractHandler, ContractHandlerHalve, ContractHandlerToEventLoopChannel}, - storages::in_memory::MemKVStore, Executor, }; use crate::DynError; @@ -21,34 +17,18 @@ pub(crate) struct MockRuntime { pub contract_store: ContractStore, } -pub(crate) struct MemoryContractHandler -where - KVStore: StateStorage, -{ +pub(crate) struct MemoryContractHandler { channel: ContractHandlerToEventLoopChannel, - _kv_store: StateStore, runtime: Executor, } -impl MemoryContractHandler -where - KVStore: StateStorage + Send + Sync + 'static, - ::Error: Into>, -{ +impl MemoryContractHandler { pub async fn new( channel: ContractHandlerToEventLoopChannel, - kv_store: KVStore, data_dir: &str, ) -> Self { - // let rt = MockRuntime { - // contract_store: ContractStore::new( - // Config::conf().contracts_dir(), - // Self::MAX_MEM_CACHE, - // ) - // .unwrap(); MemoryContractHandler { channel, - _kv_store: StateStore::new(kv_store, 10_000_000).unwrap(), runtime: Executor::new_mock(data_dir).await.unwrap(), } } @@ -66,8 +46,7 @@ impl ContractHandler for MemoryContractHandler { where Self: Sized + 'static, { - let store = MemKVStore::new(); - async move { Ok(MemoryContractHandler::new(channel, store, &config).await) }.boxed() + async move { Ok(MemoryContractHandler::new(channel, &config).await) }.boxed() } fn channel(&mut self) -> &mut ContractHandlerToEventLoopChannel { @@ -80,7 +59,7 @@ impl ContractHandler for MemoryContractHandler { _client_id: ClientId, _updates: Option>>, ) -> BoxFuture<'static, Result> { - todo!() + unreachable!() } fn executor(&mut self) -> &mut Self::ContractExecutor { diff --git a/crates/core/src/contract/storages/in_memory.rs b/crates/core/src/contract/storages/in_memory.rs deleted file mode 100644 index c9c66280e..000000000 --- a/crates/core/src/contract/storages/in_memory.rs +++ /dev/null @@ -1,41 +0,0 @@ -use dashmap::DashMap; -use freenet_stdlib::prelude::*; - -use crate::runtime::StateStorage; - -#[derive(Default, Clone)] -pub(crate) struct MemKVStore(DashMap); - -#[async_trait::async_trait] -impl StateStorage for MemKVStore { - type Error = String; - - async fn store(&mut self, _key: ContractKey, _state: WrappedState) -> Result<(), Self::Error> { - todo!() - } - - async fn get(&self, _key: &ContractKey) -> Result, Self::Error> { - todo!() - } - - async fn store_params( - &mut self, - _key: ContractKey, - _state: Parameters<'static>, - ) -> Result<(), Self::Error> { - todo!() - } - - async fn get_params<'a>( - &'a self, - _key: &'a ContractKey, - ) -> Result>, Self::Error> { - todo!() - } -} - -impl MemKVStore { - pub fn new() -> Self { - Self::default() - } -} diff --git a/crates/core/src/contract/storages/mod.rs b/crates/core/src/contract/storages/mod.rs index 48bc02864..e55e61c94 100644 --- a/crates/core/src/contract/storages/mod.rs +++ b/crates/core/src/contract/storages/mod.rs @@ -13,6 +13,3 @@ use self::rocks_db::RocksDb; #[cfg(all(feature = "rocks_db", not(feature = "sqlite")))] pub type Storage = RocksDb; - -#[cfg(test)] -pub(crate) mod in_memory; diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index ebc372f60..510b44d91 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -44,13 +44,13 @@ use crate::{ use crate::operations::handle_op_request; pub(crate) use conn_manager::{ConnectionBridge, ConnectionError}; pub(crate) use event_log::{EventLogRegister, EventRegister}; -pub(crate) use op_state::OpManager; +pub(crate) use op_state_manager::OpManager; mod conn_manager; mod event_log; #[cfg(test)] mod in_memory_impl; -mod op_state; +mod op_state_manager; mod p2p_impl; #[cfg(test)] pub(crate) mod tests; @@ -388,7 +388,7 @@ async fn process_open_request(request: OpenRequest<'static>, op_storage: Arc { // Initialize a subscribe op. loop { - // FIXME: this will block the event loop until the subscribe op succeeds + // FIXME: this will block the loop until the subscribe op succeeds // instead the op should be deferred for later execution let op = subscribe::start_op(key.clone()); match subscribe::request_subscribe(&op_storage_cp, op, Some(client_id)) diff --git a/crates/core/src/node/conn_manager/p2p_protoc.rs b/crates/core/src/node/conn_manager/p2p_protoc.rs index 674c7f235..d7c0c0b64 100644 --- a/crates/core/src/node/conn_manager/p2p_protoc.rs +++ b/crates/core/src/node/conn_manager/p2p_protoc.rs @@ -399,9 +399,8 @@ impl P2pConnManager { msg = network_msg => { msg } msg = notification_msg => { msg } msg = bridge_msg => { msg } - (event_id, contract_handler_event) = op_manager.recv_from_handler() => { - if let Some(client_id) = event_id.client_id() { - let transaction = contract_handler_event.into_network_op(&op_manager).await; + event_id = op_manager.recv_from_handler() => { + if let Some((client_id, transaction)) = event_id.client_id().zip(event_id.transaction()) { tx_to_client.insert(transaction, client_id); } continue; diff --git a/crates/core/src/node/in_memory_impl.rs b/crates/core/src/node/in_memory_impl.rs index da2d4ce75..47246894d 100644 --- a/crates/core/src/node/in_memory_impl.rs +++ b/crates/core/src/node/in_memory_impl.rs @@ -7,7 +7,7 @@ use super::{ client_event_handling, conn_manager::{in_memory::MemoryConnManager, EventLoopNotifications}, handle_cancelled_op, join_ring_request, - op_state::OpManager, + op_state_manager::OpManager, process_message, EventLogRegister, PeerKey, }; use crate::{ diff --git a/crates/core/src/node/op_state.rs b/crates/core/src/node/op_state_manager.rs similarity index 97% rename from crates/core/src/node/op_state.rs rename to crates/core/src/node/op_state_manager.rs index 8b7860e91..addb61193 100644 --- a/crates/core/src/node/op_state.rs +++ b/crates/core/src/node/op_state_manager.rs @@ -7,7 +7,8 @@ use tokio::sync::{mpsc::error::SendError, Mutex}; use crate::{ contract::{ - ContractError, ContractHandlerEvent, ContractHandlerToEventLoopChannel, NetEventListener, + ContractError, ContractHandlerEvent, ContractHandlerToEventLoopChannel, + NetEventListenerHalve, }, dev_tool::ClientId, message::{Message, Transaction, TransactionType}, @@ -28,7 +29,7 @@ pub(crate) struct OpManager { subscribe: DashMap, to_event_listener: EventLoopNotificationsSender, // todo: remove the need for a mutex here - ch_outbound: Mutex>, + ch_outbound: Mutex>, // FIXME: think of an optimal strategy to check for timeouts and clean up garbage _ops_ttl: RwLock>>, pub ring: Ring, @@ -47,7 +48,7 @@ impl OpManager { pub(super) fn new( ring: Ring, notification_channel: EventLoopNotificationsSender, - contract_handler: ContractHandlerToEventLoopChannel, + contract_handler: ContractHandlerToEventLoopChannel, ) -> Self { Self { join_ring: DashMap::default(), @@ -101,7 +102,7 @@ impl OpManager { .await } - pub async fn recv_from_handler(&self) -> (crate::contract::EventId, ContractHandlerEvent) { + pub async fn recv_from_handler(&self) -> crate::contract::EventId { todo!() } diff --git a/crates/core/src/node/tests.rs b/crates/core/src/node/tests.rs index d9f0d3416..4bccb574b 100644 --- a/crates/core/src/node/tests.rs +++ b/crates/core/src/node/tests.rs @@ -56,10 +56,13 @@ impl NodeLabel { Self(format!("node-{id}").into()) } - #[allow(dead_code)] fn is_gateway(&self) -> bool { self.0.starts_with("gateway") } + + pub fn is_node(&self) -> bool { + self.0.starts_with("node") + } } impl std::fmt::Display for NodeLabel { @@ -158,6 +161,10 @@ impl SimNetwork { net } + pub fn with_start_backoff(&mut self, value: Duration) { + self.init_backoff = value; + } + #[allow(unused)] pub fn debug(&mut self) { self.debug = true; @@ -285,12 +292,8 @@ impl SimNetwork { for (node, label) in gw.chain(self.nodes.drain(..)).collect::>() { let node_spec = specs.remove(&label); self.initialize_peer(node, label, node_spec); - if gw_not_init != 0 { - gw_not_init -= 1; - tokio::time::sleep(self.init_backoff).await; - } else { - tokio::time::sleep(self.init_backoff).await; - } + gw_not_init = gw_not_init.saturating_sub(1); + tokio::time::sleep(self.init_backoff).await; } } @@ -411,6 +414,8 @@ impl SimNetwork { Ok(()) } + /// Checks that all peers in the network have acquired at least one connection to any + /// other peers. pub async fn check_connectivity(&self, time_out: Duration) -> Result<(), anyhow::Error> { let num_nodes = self.nodes.capacity(); let mut connected = HashSet::new(); @@ -452,27 +457,55 @@ impl SimNetwork { Ok(()) } - pub fn connections_per_peer(&self) -> Result<(), anyhow::Error> { + /// Recommended to calling after `check_connectivity` to ensure enough time + /// elapsed for all peers to become connected. + /// + /// Checks that there is a good connectivity over the simulated network, + /// meaning that: + /// + /// - at least 50% of the peers have more than the minimum connections + /// - + pub fn network_connectivity_quality(&self) -> Result<(), anyhow::Error> { + const HIGHER_THAN_MIN_THRESHOLD: f64 = 0.5; let num_nodes = self.nodes.capacity(); + let min_connections_threshold = (num_nodes as f64 * HIGHER_THAN_MIN_THRESHOLD) as usize; let node_connectivity = self.node_connectivity(); let mut connections_per_peer: Vec<_> = node_connectivity .iter() .map(|(k, v)| (k, v.1.len())) - .filter_map(|(k, v)| if !k.is_gateway() { Some(v) } else { None }) + .filter(|&(k, _)| !k.is_gateway()) + .map(|(_, v)| v) .collect(); - // ensure at least normal nodes have more than one connection + // ensure at least "most" normal nodes have more than one connection connections_per_peer.sort_unstable_by_key(|num_conn| *num_conn); - if *connections_per_peer.iter().last().unwrap() < 2 { - anyhow::bail!("low connectivy; some nodes didn't connect beyond the gateway"); + if connections_per_peer[min_connections_threshold] < self.min_connections { + tracing::error!( + "Low connectivity; more than {:.0}% of the nodes don't have more than minimum connections", + HIGHER_THAN_MIN_THRESHOLD * 100.0 + ); + anyhow::bail!("low connectivity"); + } else { + let idx = connections_per_peer[min_connections_threshold..] + .iter() + .position(|num_conn| *num_conn < self.min_connections) + .unwrap_or_else(|| connections_per_peer[min_connections_threshold..].len() - 1) + + (min_connections_threshold - 1); + let percentile = idx as f64 / connections_per_peer.len() as f64 * 100.0; + tracing::info!("{percentile:.0}% nodes have higher than required minimum connections"); } - // ensure the average number of connections per peer is above N + // ensure the average number of connections per peer is above the mean between max and min connections + let expected_avg_connections = + ((self.max_connections - self.min_connections) / 2) + self.min_connections; let avg_connections: usize = connections_per_peer.iter().sum::() / num_nodes; - tracing::info!("Average connections: {}", avg_connections); - if avg_connections < 1 { - anyhow::bail!("average number of connections is low"); + tracing::info!( + "Average connections: {avg_connections} (expected > {expected_avg_connections})" + ); + if avg_connections < expected_avg_connections { + tracing::error!("Average number of connections ({avg_connections}) is low (< {expected_avg_connections})"); + anyhow::bail!("low average number of connections"); } Ok(()) } diff --git a/crates/core/src/operations/get.rs b/crates/core/src/operations/get.rs index aa2731816..296716230 100644 --- a/crates/core/src/operations/get.rs +++ b/crates/core/src/operations/get.rs @@ -132,7 +132,7 @@ impl TryFrom for GetResult { fn try_from(value: GetOp) -> Result { match value.result { Some(r) => Ok(r), - _ => todo!(), + _ => Err(OpError::UnexpectedOpState), } } } diff --git a/crates/core/src/operations/join_ring.rs b/crates/core/src/operations/join_ring.rs index c8f70ba2e..0039e6b6f 100644 --- a/crates/core/src/operations/join_ring.rs +++ b/crates/core/src/operations/join_ring.rs @@ -43,16 +43,37 @@ impl JoinRingOp { pub(super) fn record_transfer(&mut self) {} } +/// Not really used since client requests will never interact with this directly. pub(crate) struct JoinRingResult {} impl TryFrom for JoinRingResult { type Error = OpError; fn try_from(_value: JoinRingOp) -> Result { - todo!() + Ok(Self {}) } } +/* +Will need to do some changes for when we perform parallel joins. + +t0: (#1 join attempt) +joiner: + 1. dont knows location + 3. knows location + n. connected to the ring +gateway: + 2. assigned_location: None; assigned location to `joiner` based on IP and communicate to joiner + 4. forward to N peers + ... + +(2 join subsequently to acquire more/better connections) +join: + 1. knows location +gateway: + 2. assigned_location: Some(loc) +*/ + impl Operation for JoinRingOp { type Message = JoinRingMsg; type Result = JoinRingResult; @@ -110,8 +131,9 @@ impl Operation for JoinRingOp { msg: JoinRequest::StartReq { target: this_node_loc, - req_peer, + joiner, hops_to_live, + assigned_location, .. }, } => { @@ -119,24 +141,25 @@ impl Operation for JoinRingOp { tracing::debug!( tx = %id, "Initial join request received from {} with HTL {} @ {}", - req_peer, + joiner, hops_to_live, this_node_loc.peer ); - let new_location = Location::random(); + // todo: location should be based on your public IP + let new_location = assigned_location.unwrap_or_else(Location::random); // FIXME: don't try to forward to peers which have already been tried (add a rejected_by list) let accepted_by = if op_storage.ring.should_accept(&new_location) { - tracing::debug!(tx = %id, "Accepting connection from {}", req_peer,); + tracing::debug!(tx = %id, "Accepting connection from {}", joiner,); HashSet::from_iter([this_node_loc]) } else { - tracing::debug!(tx = %id, at_peer = %this_node_loc.peer, "Rejecting connection from peer {}", req_peer); + tracing::debug!(tx = %id, at_peer = %this_node_loc.peer, "Rejecting connection from peer {}", joiner); HashSet::new() }; let new_peer_loc = PeerKeyLocation { location: Some(new_location), - peer: req_peer, + peer: joiner, }; if let Some(mut updated_state) = forward_conn( id, @@ -164,7 +187,7 @@ impl Operation for JoinRingOp { tx = %id, "OC received at gateway {} from requesting peer {}", this_node_loc.peer, - req_peer + joiner ); new_state = Some(JRState::OCReceived); } else { @@ -176,10 +199,10 @@ impl Operation for JoinRingOp { msg: JoinResponse::AcceptedBy { peers: accepted_by, your_location: new_location, - your_peer_id: req_peer, + your_peer_id: joiner, }, target: PeerKeyLocation { - peer: req_peer, + peer: joiner, location: Some(new_location), }, }); @@ -681,7 +704,7 @@ enum JRState { Initializing, Connecting(ConnectionInfo), AwaitingProxyResponse { - /// Could be either the requester or nodes which have been previously forwarded to + /// Could be either the joiner or nodes which have been previously forwarded to target: PeerKeyLocation, accepted_by: HashSet, new_location: Location, @@ -786,11 +809,13 @@ where ); conn_manager.add_connection(gateway.peer).await?; + let assigned_location = op_storage.ring.own_location().location; let join_req = Message::from(messages::JoinRingMsg::Request { id: tx, msg: messages::JoinRequest::StartReq { target: gateway, - req_peer: this_peer, + joiner: this_peer, + assigned_location, hops_to_live: max_hops_to_live, max_hops_to_live, }, @@ -819,7 +844,7 @@ async fn forward_conn( ring: &Ring, conn_manager: &mut CM, req_peer: PeerKeyLocation, - new_peer_loc: PeerKeyLocation, + joiner: PeerKeyLocation, left_htl: usize, num_accepted: usize, ) -> Result, OpError> @@ -829,7 +854,7 @@ where if left_htl == 0 { tracing::debug!( tx = %id, - requester = %req_peer.peer, + joiner = %joiner.peer, "Couldn't forward join petition, no hops left or enough connections", ); return Ok(None); @@ -838,7 +863,7 @@ where if ring.num_connections() == 0 { tracing::warn!( tx = %id, - requester = %req_peer.peer, + joiner = %joiner.peer, "Couldn't forward join petition, not enough connections", ); return Ok(None); @@ -847,25 +872,26 @@ where let forward_to = if left_htl >= ring.rnd_if_htl_above { tracing::debug!( tx = %id, - requester = %req_peer.peer, + joiner = %joiner.peer, "Randomly selecting peer to forward JoinRequest", ); ring.random_peer(|p| p != &req_peer.peer) } else { tracing::debug!( tx = %id, - requester = %req_peer.peer, + joiner = %joiner.peer, "Selecting close peer to forward request", ); - ring.routing(&new_peer_loc.location.unwrap(), Some(&req_peer.peer), &[]) - .and_then(|pkl| (pkl.peer != new_peer_loc.peer).then_some(pkl)) + // FIXME: target the `desired_location` + ring.routing(&joiner.location.unwrap(), Some(&req_peer.peer), &[]) + .and_then(|pkl| (pkl.peer != joiner.peer).then_some(pkl)) }; if let Some(forward_to) = forward_to { let forwarded = Message::from(JoinRingMsg::Request { id, msg: JoinRequest::Proxy { - joiner: new_peer_loc, + joiner, hops_to_live: left_htl.min(ring.max_hops_to_live) - 1, sender: ring.own_location(), }, @@ -881,8 +907,8 @@ where let new_state = JRState::AwaitingProxyResponse { target: req_peer, accepted_by: HashSet::new(), - new_location: new_peer_loc.location.unwrap(), - new_peer_id: new_peer_loc.peer, + new_location: joiner.location.unwrap(), + new_peer_id: joiner.peer, }; Ok(Some(new_state)) } else { @@ -943,7 +969,10 @@ mod messages { Response { sender, .. } => Some(&sender.peer), Connected { sender, .. } => Some(&sender.peer), Request { - msg: JoinRequest::StartReq { req_peer, .. }, + msg: + JoinRequest::StartReq { + joiner: req_peer, .. + }, .. } => Some(req_peer), _ => None, @@ -1004,7 +1033,7 @@ mod messages { .. } => write!(f, "RouteValue(id: {id})"), Self::Connected { .. } => write!(f, "Connected(id: {id})"), - _ => todo!(), + _ => unimplemented!(), } } } @@ -1013,7 +1042,8 @@ mod messages { pub(crate) enum JoinRequest { StartReq { target: PeerKeyLocation, - req_peer: PeerKey, + joiner: PeerKey, + assigned_location: Option, hops_to_live: usize, max_hops_to_live: usize, }, @@ -1065,40 +1095,54 @@ mod test { /// Once a gateway is left without remaining open slots, ensure forwarding connects #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn forward_connection_to_node() -> Result<(), anyhow::Error> { - crate::config::set_logger(); - const NUM_NODES: usize = 10usize; + // crate::config::set_logger(); + const NUM_NODES: usize = 3usize; const NUM_GW: usize = 1usize; let mut sim_nw = SimNetwork::new( "join_forward_connection_to_node", NUM_GW, NUM_NODES, - 3, 2, - 5, - 3, + 1, + 2, + 1, ) .await; + // sim_nw.with_start_backoff(Duration::from_millis(100)); sim_nw.start().await; - sim_nw.check_connectivity(Duration::from_secs(10)).await + sim_nw.check_connectivity(Duration::from_secs(5)).await?; + let some_forwarded = sim_nw + .node_connectivity() + .into_iter() + .flat_map(|(_this, (_, conns))| conns.into_keys()) + .any(|c| c.is_node()); + assert!( + some_forwarded, + "didn't find any connection succesfully forwarded" + ); + Ok(()) } - /// Given a network of N peers all nodes should have connections. + /// Given a network of N peers all good connectivity #[ignore] #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn all_nodes_should_connect() -> Result<(), anyhow::Error> { + async fn network_should_achieve_good_connectivity() -> Result<(), anyhow::Error> { + crate::config::set_logger(); const NUM_NODES: usize = 10usize; - const NUM_GW: usize = 1usize; + const NUM_GW: usize = 2usize; let mut sim_nw = SimNetwork::new( "join_all_nodes_should_connect", NUM_GW, NUM_NODES, + 5, 3, - 2, - 1000, + 6, 2, ) .await; + sim_nw.with_start_backoff(Duration::from_millis(200)); sim_nw.start().await; - sim_nw.check_connectivity(Duration::from_secs(10)).await + sim_nw.check_connectivity(Duration::from_secs(10)).await?; + sim_nw.network_connectivity_quality() } } diff --git a/crates/core/src/ring.rs b/crates/core/src/ring.rs index df2b8686e..9f429e8b6 100644 --- a/crates/core/src/ring.rs +++ b/crates/core/src/ring.rs @@ -265,6 +265,7 @@ impl Ring { tracing::debug!(peer = %self.peer_key, "max connections reached"); false } else { + // todo: in the future maybe use the `small worldness` metric to decide let median_distance = self .median_distance_to(my_location) .unwrap_or(Distance(0.5)); diff --git a/crates/fdev/src/build.rs b/crates/fdev/src/build.rs index d16a6ef90..58b797a07 100644 --- a/crates/fdev/src/build.rs +++ b/crates/fdev/src/build.rs @@ -220,7 +220,7 @@ mod contract { pub lang: Option, pub typescript: Option, #[serde(rename = "state-sources")] - pub state_sources: Option, + pub state_sources: Sources, pub metadata: Option, pub dependencies: Option, } @@ -228,7 +228,6 @@ mod contract { #[derive(Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub(crate) enum SupportedWebLangs { - Javascript, Typescript, } @@ -243,6 +242,11 @@ mod contract { embedded_deps: EmbeddedDeps, cwd: &Path, ) -> Result<(), DynError> { + let Some(web_config) = &config.webapp else { + println!("No webapp config found."); + return Ok(()); + }; + let metadata = if let Some(md) = config.webapp.as_ref().and_then(|a| a.metadata.as_ref()) { let mut buf = vec![]; File::open(md)?.read_to_end(&mut buf)?; @@ -252,73 +256,68 @@ mod contract { }; let mut archive: Builder>> = Builder::new(Cursor::new(Vec::new())); - if let Some(web_config) = &config.webapp { - println!("Bundling webapp contract state"); - match &web_config.lang { - Some(SupportedWebLangs::Typescript) => { - let child = Command::new("npm") - .args(["install"]) + + println!("Bundling webapp contract state"); + match &web_config.lang { + Some(SupportedWebLangs::Typescript) => { + let child = Command::new("npm") + .args(["install"]) + .current_dir(cwd) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|e| { + eprintln!("Error while installing npm packages: {e}"); + Error::CommandFailed("npm") + })?; + pipe_std_streams(child)?; + let webpack = web_config + .typescript + .as_ref() + .map(|c| c.webpack) + .unwrap_or_default(); + use std::io::IsTerminal; + if webpack { + let cmd_args: &[&str] = + if std::io::stdout().is_terminal() && std::io::stderr().is_terminal() { + &["--color"] + } else { + &[] + }; + let child = Command::new("webpack") + .args(cmd_args) .current_dir(cwd) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .map_err(|e| { - eprintln!("Error while installing npm packages: {e}"); - Error::CommandFailed("npm") + eprintln!("Error while executing webpack command: {e}"); + Error::CommandFailed("tsc") })?; pipe_std_streams(child)?; - let webpack = web_config - .typescript - .as_ref() - .map(|c| c.webpack) - .unwrap_or_default(); - use std::io::IsTerminal; - if webpack { - let cmd_args: &[&str] = - if std::io::stdout().is_terminal() && std::io::stderr().is_terminal() { - &["--color"] - } else { - &[] - }; - let child = Command::new("webpack") - .args(cmd_args) - .current_dir(cwd) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .map_err(|e| { - eprintln!("Error while executing webpack command: {e}"); - Error::CommandFailed("tsc") - })?; - pipe_std_streams(child)?; - println!("Compiled input using webpack"); - } else { - let cmd_args: &[&str] = - if std::io::stdout().is_terminal() && std::io::stderr().is_terminal() { - &["--pretty"] - } else { - &[] - }; - let child = Command::new("tsc") - .args(cmd_args) - .current_dir(cwd) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .map_err(|e| { - eprintln!("Error while executing command tsc: {e}"); - Error::CommandFailed("tsc") - })?; - pipe_std_streams(child)?; - println!("Compiled input using tsc"); - } + println!("Compiled input using webpack"); + } else { + let cmd_args: &[&str] = + if std::io::stdout().is_terminal() && std::io::stderr().is_terminal() { + &["--pretty"] + } else { + &[] + }; + let child = Command::new("tsc") + .args(cmd_args) + .current_dir(cwd) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|e| { + eprintln!("Error while executing command tsc: {e}"); + Error::CommandFailed("tsc") + })?; + pipe_std_streams(child)?; + println!("Compiled input using tsc"); } - Some(SupportedWebLangs::Javascript) => todo!(), - None => {} } - } else { - println!("No webapp config found."); - return Ok(()); + None => {} } let build_state = |sources: &Sources| -> Result<(), DynError> { @@ -393,15 +392,8 @@ mod contract { Ok(()) }; - if let Some(sources) = config - .webapp - .as_ref() - .and_then(|a| a.state_sources.as_ref()) - { - build_state(sources) - } else { - todo!() - } + let sources = &web_config.state_sources; + build_state(sources) } fn build_generic_state(config: &mut BuildToolConfig, cwd: &Path) -> Result<(), DynError> { @@ -603,10 +595,10 @@ mod contract { webapp: Some(WebAppContract { lang: Some(SupportedWebLangs::Typescript), typescript: Some(TypescriptConfig { webpack: true }), - state_sources: Some(Sources { + state_sources: Sources { source_dirs: Some(vec!["dist".into()]), files: None, - }), + }, metadata: None, dependencies: Some( toml::toml! { diff --git a/crates/fdev/src/inspect.rs b/crates/fdev/src/inspect.rs index 7da43cab1..623fe63b1 100644 --- a/crates/fdev/src/inspect.rs +++ b/crates/fdev/src/inspect.rs @@ -16,7 +16,6 @@ pub struct InspectCliConfig { enum FileType { Code(CodeInspection), Delegate, - Contract, } /// Inspect the packaged WASM code for Freenet. @@ -47,7 +46,6 @@ delegate API version: {version} "# ); } - FileType::Contract => todo!(), } Ok(()) diff --git a/crates/fdev/src/local_node/commands.rs b/crates/fdev/src/local_node/commands.rs index 00962506e..e479a98fe 100644 --- a/crates/fdev/src/local_node/commands.rs +++ b/crates/fdev/src/local_node/commands.rs @@ -75,7 +75,6 @@ async fn execute_command( })) => { println!("current state for {key}:"); app.printout_deser(state.as_ref())?; - todo!() } Err(err) => { println!("error: {err}"); @@ -85,15 +84,6 @@ async fn execute_command( } _ => unreachable!(), }, - ClientRequest::DelegateOp(op) => { - match node.handle_request(ClientId::FIRST, op.into(), None).await { - Ok(_res) => todo!(), - Err(err) => { - println!("error: {err}"); - } - } - } - ClientRequest::Disconnect { .. } => return Ok(true), _ => { tracing::error!("op not supported"); return Err("op not support".into()); diff --git a/crates/fdev/src/new_package.rs b/crates/fdev/src/new_package.rs index 000104933..44485f0d2 100644 --- a/crates/fdev/src/new_package.rs +++ b/crates/fdev/src/new_package.rs @@ -34,10 +34,10 @@ fn create_view_package(cwd: &Path) -> Result<(), DynError> { webapp: Some(WebAppContract { lang: Some(SupportedWebLangs::Typescript), typescript: Some(TypescriptConfig { webpack: true }), - state_sources: Some(Sources { + state_sources: Sources { source_dirs: Some(vec![PathBuf::from("dist")]), files: None, - }), + }, metadata: None, dependencies: None, }), From fe4e8f351313799622bf61ccd49e969a90641414 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Thu, 19 Oct 2023 14:21:09 +0200 Subject: [PATCH 60/76] Add rand bytes func --- .../core/src/node/conn_manager/in_memory.rs | 13 +--- crates/core/src/runtime/native_api.rs | 73 +++++++++++++------ crates/core/src/runtime/wasm_runtime.rs | 3 +- 3 files changed, 54 insertions(+), 35 deletions(-) diff --git a/crates/core/src/node/conn_manager/in_memory.rs b/crates/core/src/node/conn_manager/in_memory.rs index f1fbda01f..abcc136eb 100644 --- a/crates/core/src/node/conn_manager/in_memory.rs +++ b/crates/core/src/node/conn_manager/in_memory.rs @@ -8,7 +8,7 @@ use std::{ use crossbeam::channel::{self, Receiver, Sender}; use once_cell::sync::OnceCell; -use rand::{prelude::StdRng, thread_rng, Rng, SeedableRng}; +use rand::{prelude::StdRng, seq::SliceRandom, thread_rng, Rng, SeedableRng}; use tokio::sync::Mutex; use super::{ConnectionBridge, ConnectionError, PeerKey}; @@ -180,7 +180,8 @@ impl InMemoryTransport { for (_, msgs) in delayed.drain() { queue.extend(msgs); } - Self::shuffle(&mut queue); + let queue = &mut queue; + queue.shuffle(&mut thread_rng()); } } tracing::error!("Stopped receiving messages in {}", interface_peer); @@ -204,12 +205,4 @@ impl InMemoryTransport { tracing::error!("Network shutdown") } } - - fn shuffle(iter: &mut [T]) { - let mut rng = thread_rng(); - for i in (1..(iter.len() - 1)).rev() { - let idx = rng.gen_range(0..=i); - iter.swap(idx, i); - } - } } diff --git a/crates/core/src/runtime/native_api.rs b/crates/core/src/runtime/native_api.rs index 475b6bd63..c061fd78d 100644 --- a/crates/core/src/runtime/native_api.rs +++ b/crates/core/src/runtime/native_api.rs @@ -19,56 +19,81 @@ fn compute_ptr(ptr: i64, start_ptr: i64) -> *mut T { (start_ptr + ptr) as _ } -pub(crate) mod time { +pub(crate) mod log { + use wasmer::Function; + use super::*; - use chrono::{DateTime, Utc as UtcOriginal}; pub(crate) fn prepare_export(store: &mut wasmer::Store, imports: &mut Imports) { - let utc_now = Function::new_typed(store, utc_now); + let utc_now = Function::new_typed(store, info); imports.register_namespace( - "freenet_time", - [("__frnt__time__utc_now".to_owned(), utc_now.into())], + "freenet_log", + [("__frnt__logger__info".to_owned(), utc_now.into())], ); } - fn utc_now(id: i64, ptr: i64) { + // TODO: this API right now is just a patch, ideally we want to impl a tracing subscriber + // that can be used in wasm and that under the hood will just pass data to the host via + // functions like this in a structured way + fn info(id: i64, ptr: i64, len: i32) { if id == -1 { panic!("unset module id"); } let info = MEM_ADDR.get(&id).expect("instance mem space not recorded"); - let now = UtcOriginal::now(); - let ptr = compute_ptr::>(ptr, info.start_ptr); - // eprintln!("{ptr:p} ({}) outside", ptr as i64); - unsafe { - ptr.write(now); - }; + let ptr = compute_ptr::(ptr, info.start_ptr); + let msg = + unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(ptr, len as _)) }; + tracing::info!(target: "contract", contract = %info.value().key(), "{msg}"); } } -pub(crate) mod log { - use wasmer::Function; +pub(crate) mod rand { + use ::rand::{thread_rng, RngCore}; use super::*; pub(crate) fn prepare_export(store: &mut wasmer::Store, imports: &mut Imports) { - let utc_now = Function::new_typed(store, info); + let rand_bytes = Function::new_typed(store, rand_bytes); imports.register_namespace( - "freenet_logger", - [("__frnt__logger__info".to_owned(), utc_now.into())], + "freenet_rand", + [("__frnt__rand__rand_bytes".to_owned(), rand_bytes.into())], ); } - // TODO: this API right now is just a patch, ideally we want to impl a tracing subscriber - // that can be used in wasm and that under the hood will just pass data to the host via - // functions like this in a structured way - fn info(id: i64, ptr: i64, len: i32) { + fn rand_bytes(id: i64, ptr: i64, len: u32) { if id == -1 { panic!("unset module id"); } let info = MEM_ADDR.get(&id).expect("instance mem space not recorded"); let ptr = compute_ptr::(ptr, info.start_ptr); - let msg = - unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(ptr, len as _)) }; - tracing::info!(target: "contract", contract = %info.value().key(), "{msg}"); + let slice = unsafe { &mut *std::ptr::slice_from_raw_parts_mut(ptr, len as usize) }; + let mut rng = thread_rng(); + rng.fill_bytes(slice); + } +} + +pub(crate) mod time { + use super::*; + use chrono::{DateTime, Utc as UtcOriginal}; + + pub(crate) fn prepare_export(store: &mut wasmer::Store, imports: &mut Imports) { + let utc_now = Function::new_typed(store, utc_now); + imports.register_namespace( + "freenet_time", + [("__frnt__time__utc_now".to_owned(), utc_now.into())], + ); + } + + fn utc_now(id: i64, ptr: i64) { + if id == -1 { + panic!("unset module id"); + } + let info = MEM_ADDR.get(&id).expect("instance mem space not recorded"); + let now = UtcOriginal::now(); + let ptr = compute_ptr::>(ptr, info.start_ptr); + // eprintln!("{ptr:p} ({}) outside", ptr as i64); + unsafe { + ptr.write(now); + }; } } diff --git a/crates/core/src/runtime/wasm_runtime.rs b/crates/core/src/runtime/wasm_runtime.rs index b7d2a6446..4c9d2db29 100644 --- a/crates/core/src/runtime/wasm_runtime.rs +++ b/crates/core/src/runtime/wasm_runtime.rs @@ -127,8 +127,9 @@ impl Runtime { } else { (None, imports! {}) }; - native_api::time::prepare_export(&mut store, &mut top_level_imports); native_api::log::prepare_export(&mut store, &mut top_level_imports); + native_api::rand::prepare_export(&mut store, &mut top_level_imports); + native_api::time::prepare_export(&mut store, &mut top_level_imports); Ok(Self { wasm_store: store, From 5405eff4d8d210f32360032fccb481b282793a50 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Thu, 19 Oct 2023 17:28:40 +0200 Subject: [PATCH 61/76] Rename join ring to connect --- crates/core/src/message.rs | 19 +- crates/core/src/node.rs | 15 +- crates/core/src/node/event_log.rs | 12 +- crates/core/src/node/op_state_manager.rs | 30 +-- crates/core/src/operations.rs | 6 +- .../operations/{join_ring.rs => connect.rs} | 181 +++++++++--------- crates/core/src/operations/get.rs | 22 +-- crates/core/src/operations/put.rs | 24 +-- crates/core/src/operations/subscribe.rs | 42 ++-- crates/core/src/operations/update.rs | 35 +++- 10 files changed, 212 insertions(+), 174 deletions(-) rename crates/core/src/operations/{join_ring.rs => connect.rs} (91%) diff --git a/crates/core/src/message.rs b/crates/core/src/message.rs index f284f7503..ef8046198 100644 --- a/crates/core/src/message.rs +++ b/crates/core/src/message.rs @@ -8,8 +8,7 @@ use ulid::Ulid; use crate::{ node::{ConnectionError, PeerKey}, operations::{ - get::GetMsg, join_ring::JoinRingMsg, put::PutMsg, subscribe::SubscribeMsg, - update::UpdateMsg, + connect::ConnectMsg, get::GetMsg, put::PutMsg, subscribe::SubscribeMsg, update::UpdateMsg, }, ring::{Location, PeerKeyLocation}, }; @@ -127,7 +126,7 @@ mod sealed_msg_type { } transaction_type_enumeration!(decl struct { - JoinRing -> JoinRingMsg, + JoinRing -> ConnectMsg, Put -> PutMsg, Get -> GetMsg, Subscribe -> SubscribeMsg, @@ -137,7 +136,7 @@ mod sealed_msg_type { #[derive(Debug, Serialize, Deserialize, Clone)] pub(crate) enum Message { - JoinRing(JoinRingMsg), + JoinRing(ConnectMsg), Put(PutMsg), Get(GetMsg), Subscribe(SubscribeMsg), @@ -148,6 +147,10 @@ pub(crate) enum Message { pub(crate) trait InnerMessage: Into { fn id(&self) -> &Transaction; + + fn target(&self) -> Option<&PeerKeyLocation>; + + fn terminal(&self) -> bool; } /// Internal node events emitted to the event loop. @@ -190,7 +193,7 @@ impl Message { Put(op) => op.id(), Get(op) => op.id(), Subscribe(op) => op.id(), - Update(_op) => todo!(), + Update(op) => op.id(), Aborted(tx) => tx, } } @@ -202,7 +205,7 @@ impl Message { Put(op) => op.target(), Get(op) => op.target(), Subscribe(op) => op.target(), - Update(_op) => todo!(), + Update(op) => op.target(), Aborted(_) => None, } } @@ -215,7 +218,7 @@ impl Message { Put(op) => op.terminal(), Get(op) => op.terminal(), Subscribe(op) => op.terminal(), - Update(_op) => todo!(), + Update(op) => op.terminal(), Aborted(_) => true, } } @@ -235,7 +238,7 @@ impl Display for Message { Put(msg) => msg.fmt(f)?, Get(msg) => msg.fmt(f)?, Subscribe(msg) => msg.fmt(f)?, - Update(_op) => todo!(), + Update(msg) => msg.fmt(f)?, Aborted(msg) => msg.fmt(f)?, }; write!(f, "}}") diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index 510b44d91..39bd2c9e2 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -32,9 +32,8 @@ use crate::{ }, message::{InnerMessage, Message, Transaction, TransactionType}, operations::{ - get, - join_ring::{self, JoinRingMsg, JoinRingOp}, - put, subscribe, OpEnum, OpError, OpOutcome, + connect::{self, ConnectMsg, ConnectOp}, + get, put, subscribe, OpEnum, OpError, OpOutcome, }, ring::{Location, PeerKeyLocation}, router::{RouteEvent, RouteOutcome}, @@ -282,9 +281,9 @@ async fn join_ring_request( where CM: ConnectionBridge + Send + Sync, { - let tx_id = Transaction::new::(); + let tx_id = Transaction::new::(); let mut op = - join_ring::initial_request(peer_key, *gateway, op_storage.ring.max_hops_to_live, tx_id); + connect::initial_request(peer_key, *gateway, op_storage.ring.max_hops_to_live, tx_id); if let Some(mut backoff) = backoff { // backoff to retry later in case it failed tracing::warn!("Performing a new join, attempt {}", backoff.retries() + 1); @@ -294,7 +293,7 @@ where } op.backoff = Some(backoff); } - join_ring::join_ring_request(tx_id, op_storage, conn_manager, op).await?; + connect::join_ring_request(tx_id, op_storage, conn_manager, op).await?; Ok(()) } @@ -524,7 +523,7 @@ async fn process_message( match msg { Message::JoinRing(op) => { log_handling_msg!("join", op.id(), op_storage); - let op_result = handle_op_request::( + let op_result = handle_op_request::( &op_storage, &mut conn_manager, op, @@ -624,7 +623,7 @@ where // is useless without connecting to the network, we will retry with exponential backoff match op_storage.pop(&tx) { Some(OpEnum::JoinRing(op)) if op.has_backoff() => { - let JoinRingOp { + let ConnectOp { gateway, backoff, .. } = *op; let backoff = backoff.expect("infallible"); diff --git a/crates/core/src/node/event_log.rs b/crates/core/src/node/event_log.rs index 08f0ecab0..e2a99705c 100644 --- a/crates/core/src/node/event_log.rs +++ b/crates/core/src/node/event_log.rs @@ -15,7 +15,7 @@ use crate::{ config::GlobalExecutor, contract::StoreResponse, message::{Message, Transaction}, - operations::{get::GetMsg, join_ring, put::PutMsg}, + operations::{connect, get::GetMsg, put::PutMsg}, ring::PeerKeyLocation, router::RouteEvent, DynError, @@ -72,9 +72,9 @@ impl<'a> EventLog<'a> { op_storage: &'a OpManager, ) -> Either> { let kind = match msg { - Message::JoinRing(join_ring::JoinRingMsg::Response { + Message::JoinRing(connect::ConnectMsg::Response { msg: - join_ring::JoinResponse::AcceptedBy { + connect::ConnectResponse::AcceptedBy { peers, your_location, your_peer_id, @@ -108,9 +108,9 @@ impl<'a> EventLog<'a> { op_storage: &'a OpManager, ) -> Either> { let kind = match msg { - Message::JoinRing(join_ring::JoinRingMsg::Response { + Message::JoinRing(connect::ConnectMsg::Response { msg: - join_ring::JoinResponse::AcceptedBy { + connect::ConnectResponse::AcceptedBy { peers, your_location, your_peer_id, @@ -680,7 +680,7 @@ pub(super) mod test_utils { use crate::ring::Location; let peer_id = PeerKey::random(); let loc = Location::try_from(0.5)?; - let tx = Transaction::new::(); + let tx = Transaction::new::(); let locations = [ (PeerKey::random(), Location::try_from(0.5)?), (PeerKey::random(), Location::try_from(0.75)?), diff --git a/crates/core/src/node/op_state_manager.rs b/crates/core/src/node/op_state_manager.rs index addb61193..6aeafec07 100644 --- a/crates/core/src/node/op_state_manager.rs +++ b/crates/core/src/node/op_state_manager.rs @@ -13,7 +13,8 @@ use crate::{ dev_tool::ClientId, message::{Message, Transaction, TransactionType}, operations::{ - get::GetOp, join_ring::JoinRingOp, put::PutOp, subscribe::SubscribeOp, OpEnum, OpError, + connect::ConnectOp, get::GetOp, put::PutOp, subscribe::SubscribeOp, update::UpdateOp, + OpEnum, OpError, }, ring::Ring, }; @@ -23,10 +24,11 @@ use super::{conn_manager::EventLoopNotificationsSender, PeerKey}; /// Thread safe and friendly data structure to maintain state of the different operations /// and enable their execution. pub(crate) struct OpManager { - join_ring: DashMap, + join_ring: DashMap, put: DashMap, get: DashMap, subscribe: DashMap, + update: DashMap, to_event_listener: EventLoopNotificationsSender, // todo: remove the need for a mutex here ch_outbound: Mutex>, @@ -55,6 +57,7 @@ impl OpManager { put: DashMap::default(), get: DashMap::default(), subscribe: DashMap::default(), + update: DashMap::default(), ring, to_event_listener: notification_channel, ch_outbound: Mutex::new(contract_handler), @@ -108,25 +111,30 @@ impl OpManager { pub fn push(&self, id: Transaction, op: OpEnum) -> Result<(), OpError> { match op { - OpEnum::JoinRing(tx) => { + OpEnum::JoinRing(op) => { #[cfg(debug_assertions)] check_id_op!(id.tx_type(), TransactionType::JoinRing); - self.join_ring.insert(id, *tx); + self.join_ring.insert(id, *op); } - OpEnum::Put(tx) => { + OpEnum::Put(op) => { #[cfg(debug_assertions)] check_id_op!(id.tx_type(), TransactionType::Put); - self.put.insert(id, tx); + self.put.insert(id, op); } - OpEnum::Get(tx) => { + OpEnum::Get(op) => { #[cfg(debug_assertions)] check_id_op!(id.tx_type(), TransactionType::Get); - self.get.insert(id, tx); + self.get.insert(id, op); } - OpEnum::Subscribe(tx) => { + OpEnum::Subscribe(op) => { #[cfg(debug_assertions)] check_id_op!(id.tx_type(), TransactionType::Subscribe); - self.subscribe.insert(id, tx); + self.subscribe.insert(id, op); + } + OpEnum::Update(op) => { + #[cfg(debug_assertions)] + check_id_op!(id.tx_type(), TransactionType::Update); + self.update.insert(id, op); } } Ok(()) @@ -146,7 +154,7 @@ impl OpManager { .remove(id) .map(|(_k, v)| v) .map(OpEnum::Subscribe), - TransactionType::Update => todo!(), + TransactionType::Update => self.update.remove(id).map(|(_k, v)| v).map(OpEnum::Update), TransactionType::Canceled => unreachable!(), } } diff --git a/crates/core/src/operations.rs b/crates/core/src/operations.rs index 410bff877..6c5b766e9 100644 --- a/crates/core/src/operations.rs +++ b/crates/core/src/operations.rs @@ -12,8 +12,8 @@ use crate::{ DynError, }; +pub(crate) mod connect; pub(crate) mod get; -pub(crate) mod join_ring; pub(crate) mod op_trait; pub(crate) mod put; pub(crate) mod subscribe; @@ -126,10 +126,11 @@ where } pub(crate) enum OpEnum { - JoinRing(Box), + JoinRing(Box), Put(put::PutOp), Get(get::GetOp), Subscribe(subscribe::SubscribeOp), + Update(update::UpdateOp), } impl OpEnum { @@ -139,6 +140,7 @@ impl OpEnum { OpEnum::Put(op) => op, OpEnum::Get(op) => op, OpEnum::Subscribe(op) => op, + OpEnum::Update(op) => op, } { pub fn id(&self) -> &Transaction; pub fn outcome(&self) -> OpOutcome; diff --git a/crates/core/src/operations/join_ring.rs b/crates/core/src/operations/connect.rs similarity index 91% rename from crates/core/src/operations/join_ring.rs rename to crates/core/src/operations/connect.rs index 0039e6b6f..0f34f3290 100644 --- a/crates/core/src/operations/join_ring.rs +++ b/crates/core/src/operations/connect.rs @@ -1,3 +1,4 @@ +//! Operation which seeks new connections in the ring. use futures::Future; use std::pin::Pin; use std::{collections::HashSet, time::Duration}; @@ -15,9 +16,9 @@ use crate::{ util::ExponentialBackoff, }; -pub(crate) use self::messages::{JoinRequest, JoinResponse, JoinRingMsg}; +pub(crate) use self::messages::{ConnectMsg, ConnectRequest, ConnectResponse}; -pub(crate) struct JoinRingOp { +pub(crate) struct ConnectOp { id: Transaction, state: Option, pub gateway: Box, @@ -27,7 +28,7 @@ pub(crate) struct JoinRingOp { _ttl: Duration, } -impl JoinRingOp { +impl ConnectOp { pub fn has_backoff(&self) -> bool { self.backoff.is_some() } @@ -44,12 +45,12 @@ impl JoinRingOp { } /// Not really used since client requests will never interact with this directly. -pub(crate) struct JoinRingResult {} +pub(crate) struct ConnectResult {} -impl TryFrom for JoinRingResult { +impl TryFrom for ConnectResult { type Error = OpError; - fn try_from(_value: JoinRingOp) -> Result { + fn try_from(_value: ConnectOp) -> Result { Ok(Self {}) } } @@ -74,9 +75,9 @@ gateway: 2. assigned_location: Some(loc) */ -impl Operation for JoinRingOp { - type Message = JoinRingMsg; - type Result = JoinRingResult; +impl Operation for ConnectOp { + type Message = ConnectMsg; + type Result = ConnectResult; fn load_or_init( op_storage: &OpManager, @@ -126,10 +127,10 @@ impl Operation for JoinRingOp { let mut new_state = None; match input { - JoinRingMsg::Request { + ConnectMsg::Request { id, msg: - JoinRequest::StartReq { + ConnectRequest::StartReq { target: this_node_loc, joiner, hops_to_live, @@ -193,10 +194,10 @@ impl Operation for JoinRingOp { } else { new_state = None } - return_msg = Some(JoinRingMsg::Response { + return_msg = Some(ConnectMsg::Response { id, sender: this_node_loc, - msg: JoinResponse::AcceptedBy { + msg: ConnectResponse::AcceptedBy { peers: accepted_by, your_location: new_location, your_peer_id: joiner, @@ -208,10 +209,10 @@ impl Operation for JoinRingOp { }); } } - JoinRingMsg::Request { + ConnectMsg::Request { id, msg: - JoinRequest::Proxy { + ConnectRequest::Proxy { sender, joiner, hops_to_live, @@ -292,11 +293,11 @@ impl Operation for JoinRingOp { sender.peer, target.peer ); - return_msg = Some(JoinRingMsg::Response { + return_msg = Some(ConnectMsg::Response { id, target, sender, - msg: JoinResponse::AcceptedBy { + msg: ConnectResponse::AcceptedBy { peers: accepted_by, your_location: new_location, your_peer_id: new_peer_id, @@ -316,11 +317,11 @@ impl Operation for JoinRingOp { sender.peer, own_loc.peer ); - return_msg = Some(JoinRingMsg::Response { + return_msg = Some(ConnectMsg::Response { id, target, sender, - msg: JoinResponse::Proxy { accepted_by }, + msg: ConnectResponse::Proxy { accepted_by }, }); } } @@ -335,11 +336,11 @@ impl Operation for JoinRingOp { }; } } - JoinRingMsg::Response { + ConnectMsg::Response { id, sender, msg: - JoinResponse::AcceptedBy { + ConnectResponse::AcceptedBy { peers: accepted_by, your_location, your_peer_id, @@ -369,9 +370,9 @@ impl Operation for JoinRingOp { gateway.peer ); new_state = Some(JRState::OCReceived); - return_msg = Some(JoinRingMsg::Response { + return_msg = Some(ConnectMsg::Response { id, - msg: JoinResponse::ReceivedOC { by_peer: pk_loc }, + msg: ConnectResponse::ReceivedOC { by_peer: pk_loc }, sender: pk_loc, target: sender, }); @@ -389,11 +390,11 @@ impl Operation for JoinRingOp { op_storage, sender, &other_peer, - JoinRingMsg::Response { + ConnectMsg::Response { id, target: other_peer, sender: pk_loc, - msg: JoinResponse::ReceivedOC { by_peer: pk_loc }, + msg: ConnectResponse::ReceivedOC { by_peer: pk_loc }, }, ) .await; @@ -405,7 +406,7 @@ impl Operation for JoinRingOp { peer = %your_peer_id, "Failed to establish any connections, aborting" ); - let op = JoinRingOp { + let op = ConnectOp { id, state: None, gateway: self.gateway, @@ -422,11 +423,11 @@ impl Operation for JoinRingOp { return Err(OpError::StatePushed); } } - JoinRingMsg::Response { + ConnectMsg::Response { id, sender, target, - msg: JoinResponse::Proxy { mut accepted_by }, + msg: ConnectResponse::Proxy { mut accepted_by }, } => { tracing::debug!(tx = %id, "Received proxy join at @ {}", target.peer); match self.state { @@ -445,8 +446,8 @@ impl Operation for JoinRingOp { tracing::debug!("Failed to connect at proxy {}", sender.peer); new_state = None; } - return_msg = Some(JoinRingMsg::Response { - msg: JoinResponse::Proxy { accepted_by }, + return_msg = Some(ConnectMsg::Response { + msg: ConnectResponse::Proxy { accepted_by }, sender, id, target, @@ -485,11 +486,11 @@ impl Operation for JoinRingOp { target.peer, original_target.peer ); - return_msg = Some(JoinRingMsg::Response { + return_msg = Some(ConnectMsg::Response { id, target: original_target, sender: target, - msg: JoinResponse::AcceptedBy { + msg: ConnectResponse::AcceptedBy { peers: previously_accepted, your_location: new_location, your_peer_id: new_peer_id, @@ -504,11 +505,11 @@ impl Operation for JoinRingOp { original_target.peer ); - return_msg = Some(JoinRingMsg::Response { + return_msg = Some(ConnectMsg::Response { id, target: original_target, sender: target, - msg: JoinResponse::Proxy { + msg: ConnectResponse::Proxy { accepted_by: previously_accepted, }, }); @@ -524,17 +525,17 @@ impl Operation for JoinRingOp { } }; } - JoinRingMsg::Response { + ConnectMsg::Response { id, sender, - msg: JoinResponse::ReceivedOC { by_peer }, + msg: ConnectResponse::ReceivedOC { by_peer }, target, } => { match self.state { Some(JRState::OCReceived) => { tracing::debug!(tx = %id, "Acknowledge connected at gateway"); new_state = Some(JRState::Connected); - return_msg = Some(JoinRingMsg::Connected { + return_msg = Some(ConnectMsg::Connected { id, sender: target, target: sender, @@ -556,7 +557,7 @@ impl Operation for JoinRingOp { } }; } - JoinRingMsg::Connected { target, sender, id } => { + ConnectMsg::Connected { target, sender, id } => { match self.state { Some(JRState::OCReceived) => { tracing::debug!(tx = %id, "Acknowledge connected at peer {}", target.peer); @@ -602,12 +603,12 @@ impl Operation for JoinRingOp { fn build_op_result( id: Transaction, state: Option, - msg: Option, + msg: Option, gateway: Box, backoff: Option, ttl: Duration, ) -> Result { - let output_op = Some(JoinRingOp { + let output_op = Some(ConnectOp { id, state, gateway, @@ -625,7 +626,7 @@ fn try_proxy_connection( sender: &PeerKeyLocation, own_loc: &PeerKeyLocation, accepted_by: HashSet, -) -> (Option, Option) { +) -> (Option, Option) { let new_state = if accepted_by.contains(own_loc) { tracing::debug!( tx = %id, @@ -638,8 +639,8 @@ fn try_proxy_connection( tracing::debug!(tx = %id, "Failed to connect at proxy {}", sender.peer); None }; - let return_msg = Some(JoinRingMsg::Response { - msg: JoinResponse::Proxy { accepted_by }, + let return_msg = Some(ConnectMsg::Response { + msg: ConnectResponse::Proxy { accepted_by }, sender: *own_loc, id: *id, target: *sender, @@ -652,7 +653,7 @@ async fn propagate_oc_to_accepted_peers( op_storage: &OpManager, sender: PeerKeyLocation, other_peer: &PeerKeyLocation, - msg: JoinRingMsg, + msg: ConnectMsg, ) -> Result<(), OpError> { let id = msg.id(); if op_storage.ring.should_accept( @@ -752,7 +753,7 @@ pub(crate) fn initial_request( gateway: PeerKeyLocation, max_hops_to_live: usize, id: Transaction, -) -> JoinRingOp { +) -> ConnectOp { const MAX_JOIN_RETRIES: usize = 3; tracing::debug!(tx = %id, "Connecting to gw {} from {}", gateway.peer, this_peer); let state = JRState::Connecting(ConnectionInfo { @@ -765,7 +766,7 @@ pub(crate) fn initial_request( } else { Duration::from_secs(120) }; - JoinRingOp { + ConnectOp { id, state: Some(state), gateway: Box::new(gateway), @@ -783,12 +784,12 @@ pub(crate) async fn join_ring_request( tx: Transaction, op_storage: &OpManager, conn_manager: &mut CB, - join_op: JoinRingOp, + join_op: ConnectOp, ) -> Result<(), OpError> where CB: ConnectionBridge, { - let JoinRingOp { + let ConnectOp { id, state, backoff, @@ -810,9 +811,9 @@ where conn_manager.add_connection(gateway.peer).await?; let assigned_location = op_storage.ring.own_location().location; - let join_req = Message::from(messages::JoinRingMsg::Request { + let join_req = Message::from(messages::ConnectMsg::Request { id: tx, - msg: messages::JoinRequest::StartReq { + msg: messages::ConnectRequest::StartReq { target: gateway, joiner: this_peer, assigned_location, @@ -823,7 +824,7 @@ where conn_manager.send(&gateway.peer, join_req).await?; op_storage.push( tx, - OpEnum::JoinRing(Box::new(JoinRingOp { + OpEnum::JoinRing(Box::new(ConnectOp { id, state: Some(JRState::Connecting(ConnectionInfo { gateway, @@ -888,9 +889,9 @@ where }; if let Some(forward_to) = forward_to { - let forwarded = Message::from(JoinRingMsg::Request { + let forwarded = Message::from(ConnectMsg::Request { id, - msg: JoinRequest::Proxy { + msg: ConnectRequest::Proxy { joiner, hops_to_live: left_htl.min(ring.max_hops_to_live) - 1, sender: ring.own_location(), @@ -934,16 +935,16 @@ mod messages { use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] - pub(crate) enum JoinRingMsg { + pub(crate) enum ConnectMsg { Request { id: Transaction, - msg: JoinRequest, + msg: ConnectRequest, }, Response { id: Transaction, sender: PeerKeyLocation, target: PeerKeyLocation, - msg: JoinResponse, + msg: ConnectResponse, }, Connected { id: Transaction, @@ -952,7 +953,7 @@ mod messages { }, } - impl InnerMessage for JoinRingMsg { + impl InnerMessage for ConnectMsg { fn id(&self) -> &Transaction { match self { Self::Request { id, .. } => id, @@ -960,31 +961,13 @@ mod messages { Self::Connected { id, .. } => id, } } - } - - impl JoinRingMsg { - pub fn sender(&self) -> Option<&PeerKey> { - use JoinRingMsg::*; - match self { - Response { sender, .. } => Some(&sender.peer), - Connected { sender, .. } => Some(&sender.peer), - Request { - msg: - JoinRequest::StartReq { - joiner: req_peer, .. - }, - .. - } => Some(req_peer), - _ => None, - } - } - pub fn target(&self) -> Option<&PeerKeyLocation> { - use JoinRingMsg::*; + fn target(&self) -> Option<&PeerKeyLocation> { + use ConnectMsg::*; match self { Response { target, .. } => Some(target), Request { - msg: JoinRequest::StartReq { target, .. }, + msg: ConnectRequest::StartReq { target, .. }, .. } => Some(target), Connected { target, .. } => Some(target), @@ -992,44 +975,62 @@ mod messages { } } - pub fn terminal(&self) -> bool { - use JoinRingMsg::*; + fn terminal(&self) -> bool { + use ConnectMsg::*; matches!( self, Response { - msg: JoinResponse::Proxy { .. }, + msg: ConnectResponse::Proxy { .. }, .. } | Connected { .. } ) } } - impl Display for JoinRingMsg { + impl ConnectMsg { + pub fn sender(&self) -> Option<&PeerKey> { + use ConnectMsg::*; + match self { + Response { sender, .. } => Some(&sender.peer), + Connected { sender, .. } => Some(&sender.peer), + Request { + msg: + ConnectRequest::StartReq { + joiner: req_peer, .. + }, + .. + } => Some(req_peer), + _ => None, + } + } + } + + impl Display for ConnectMsg { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let id = self.id(); match self { Self::Request { - msg: JoinRequest::StartReq { .. }, + msg: ConnectRequest::StartReq { .. }, .. } => write!(f, "StartRequest(id: {id})"), Self::Request { - msg: JoinRequest::Accepted { .. }, + msg: ConnectRequest::Accepted { .. }, .. } => write!(f, "RequestAccepted(id: {id})"), Self::Request { - msg: JoinRequest::Proxy { .. }, + msg: ConnectRequest::Proxy { .. }, .. } => write!(f, "ProxyRequest(id: {id})"), Self::Response { - msg: JoinResponse::AcceptedBy { .. }, + msg: ConnectResponse::AcceptedBy { .. }, .. } => write!(f, "RouteValue(id: {id})"), Self::Response { - msg: JoinResponse::ReceivedOC { .. }, + msg: ConnectResponse::ReceivedOC { .. }, .. } => write!(f, "RouteValue(id: {id})"), Self::Response { - msg: JoinResponse::Proxy { .. }, + msg: ConnectResponse::Proxy { .. }, .. } => write!(f, "RouteValue(id: {id})"), Self::Connected { .. } => write!(f, "Connected(id: {id})"), @@ -1039,7 +1040,7 @@ mod messages { } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] - pub(crate) enum JoinRequest { + pub(crate) enum ConnectRequest { StartReq { target: PeerKeyLocation, joiner: PeerKey, @@ -1062,7 +1063,7 @@ mod messages { } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] - pub(crate) enum JoinResponse { + pub(crate) enum ConnectResponse { AcceptedBy { peers: HashSet, your_location: Location, diff --git a/crates/core/src/operations/get.rs b/crates/core/src/operations/get.rs index 296716230..9a5733420 100644 --- a/crates/core/src/operations/get.rs +++ b/crates/core/src/operations/get.rs @@ -772,17 +772,8 @@ mod messages { Self::ReturnGet { id, .. } => id, } } - } - - impl GetMsg { - pub fn sender(&self) -> Option<&PeerKeyLocation> { - match self { - Self::SeekNode { target, .. } => Some(target), - _ => None, - } - } - pub fn target(&self) -> Option<&PeerKeyLocation> { + fn target(&self) -> Option<&PeerKeyLocation> { match self { Self::SeekNode { target, .. } => Some(target), Self::RequestGet { target, .. } => Some(target), @@ -790,12 +781,21 @@ mod messages { } } - pub fn terminal(&self) -> bool { + fn terminal(&self) -> bool { use GetMsg::*; matches!(self, ReturnGet { .. }) } } + impl GetMsg { + pub fn sender(&self) -> Option<&PeerKeyLocation> { + match self { + Self::SeekNode { target, .. } => Some(target), + _ => None, + } + } + } + impl Display for GetMsg { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let id = self.id(); diff --git a/crates/core/src/operations/put.rs b/crates/core/src/operations/put.rs index b1c00fa67..84ab12e7b 100644 --- a/crates/core/src/operations/put.rs +++ b/crates/core/src/operations/put.rs @@ -869,18 +869,8 @@ mod messages { Self::BroadcastTo { id, .. } => id, } } - } - - impl PutMsg { - pub fn sender(&self) -> Option<&PeerKeyLocation> { - match self { - Self::SeekNode { sender, .. } => Some(sender), - Self::BroadcastTo { sender, .. } => Some(sender), - _ => None, - } - } - pub fn target(&self) -> Option<&PeerKeyLocation> { + fn target(&self) -> Option<&PeerKeyLocation> { match self { Self::SeekNode { target, .. } => Some(target), Self::RequestPut { target, .. } => Some(target), @@ -888,7 +878,7 @@ mod messages { } } - pub fn terminal(&self) -> bool { + fn terminal(&self) -> bool { use PutMsg::*; matches!( self, @@ -897,6 +887,16 @@ mod messages { } } + impl PutMsg { + pub fn sender(&self) -> Option<&PeerKeyLocation> { + match self { + Self::SeekNode { sender, .. } => Some(sender), + Self::BroadcastTo { sender, .. } => Some(sender), + _ => None, + } + } + } + impl Display for PutMsg { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let id = self.id(); diff --git a/crates/core/src/operations/subscribe.rs b/crates/core/src/operations/subscribe.rs index 3f443ce3a..5dd708001 100644 --- a/crates/core/src/operations/subscribe.rs +++ b/crates/core/src/operations/subscribe.rs @@ -9,7 +9,7 @@ use crate::{ client_events::ClientId, config::PEER_TIMEOUT, contract::ContractError, - message::{Message, Transaction}, + message::{InnerMessage, Message, Transaction}, node::{ConnectionBridge, OpManager, PeerKey}, operations::{op_trait::Operation, OpInitialization}, ring::{PeerKeyLocation, RingError}, @@ -39,13 +39,16 @@ impl SubscribeOp { pub(super) fn record_transfer(&mut self) {} } -pub(crate) enum SubscribeResult {} +pub(crate) struct SubscribeResult {} impl TryFrom for SubscribeResult { type Error = OpError; - fn try_from(_value: SubscribeOp) -> Result { - todo!() + fn try_from(value: SubscribeOp) -> Result { + value + .finalized() + .then_some(SubscribeResult {}) + .ok_or(OpError::UnexpectedOpState) } } @@ -416,26 +419,8 @@ mod messages { Self::ReturnSub { id, .. } => id, } } - } - - impl SubscribeMsg { - pub(crate) fn id(&self) -> &Transaction { - match self { - Self::SeekNode { id, .. } => id, - Self::FetchRouting { id, .. } => id, - Self::RequestSub { id, .. } => id, - Self::ReturnSub { id, .. } => id, - } - } - - pub fn sender(&self) -> Option<&PeerKeyLocation> { - match self { - Self::ReturnSub { sender, .. } => Some(sender), - _ => None, - } - } - pub fn target(&self) -> Option<&PeerKeyLocation> { + fn target(&self) -> Option<&PeerKeyLocation> { match self { Self::SeekNode { target, .. } => Some(target), Self::ReturnSub { target, .. } => Some(target), @@ -443,12 +428,21 @@ mod messages { } } - pub fn terminal(&self) -> bool { + fn terminal(&self) -> bool { use SubscribeMsg::*; matches!(self, ReturnSub { .. } | SeekNode { .. }) } } + impl SubscribeMsg { + pub fn sender(&self) -> Option<&PeerKeyLocation> { + match self { + Self::ReturnSub { sender, .. } => Some(sender), + _ => None, + } + } + } + impl Display for SubscribeMsg { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let id = self.id(); diff --git a/crates/core/src/operations/update.rs b/crates/core/src/operations/update.rs index 42339191f..e85e495bb 100644 --- a/crates/core/src/operations/update.rs +++ b/crates/core/src/operations/update.rs @@ -3,10 +3,22 @@ pub(crate) use self::messages::UpdateMsg; use crate::{client_events::ClientId, node::ConnectionBridge}; -use super::{op_trait::Operation, OpError}; +use super::{op_trait::Operation, OpError, OpOutcome}; pub(crate) struct UpdateOp {} +impl UpdateOp { + pub fn outcome(&self) -> OpOutcome { + OpOutcome::Irrelevant + } + + pub fn finalized(&self) -> bool { + todo!() + } + + pub fn record_transfer(&mut self) {} +} + pub(crate) struct UpdateResult {} impl TryFrom for UpdateResult { @@ -46,9 +58,14 @@ impl Operation for UpdateOp { } mod messages { + use std::fmt::Display; + use serde::{Deserialize, Serialize}; - use crate::message::{InnerMessage, Transaction}; + use crate::{ + message::{InnerMessage, Transaction}, + ring::PeerKeyLocation, + }; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub(crate) enum UpdateMsg {} @@ -57,5 +74,19 @@ mod messages { fn id(&self) -> &Transaction { todo!() } + + fn target(&self) -> Option<&PeerKeyLocation> { + todo!() + } + + fn terminal(&self) -> bool { + todo!() + } + } + + impl Display for UpdateMsg { + fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + todo!() + } } } From e517445336748fcad4caeaefee567dd244195ed7 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Fri, 20 Oct 2023 11:57:55 +0200 Subject: [PATCH 62/76] Fix some issues with put op + remove some todos --- crates/core/src/client_events.rs | 2 +- crates/core/src/config.rs | 11 ++- crates/core/src/contract.rs | 3 +- crates/core/src/contract/executor.rs | 3 + crates/core/src/contract/handler.rs | 1 + crates/core/src/node.rs | 63 +++++++----- .../core/src/node/conn_manager/in_memory.rs | 50 +++++----- .../core/src/node/conn_manager/p2p_protoc.rs | 21 ++-- crates/core/src/node/event_log.rs | 27 ++++- crates/core/src/node/in_memory_impl.rs | 10 +- crates/core/src/node/op_state_manager.rs | 4 +- crates/core/src/node/tests.rs | 35 +++++-- crates/core/src/operations.rs | 4 +- crates/core/src/operations/connect.rs | 98 +++++++++---------- crates/core/src/operations/get.rs | 44 +++++---- crates/core/src/operations/put.rs | 55 ++++++++--- crates/core/src/operations/subscribe.rs | 47 ++++----- .../core/src/{runtime/mod.rs => runtime.rs} | 0 crates/core/src/{server/mod.rs => server.rs} | 0 crates/core/src/server/errors.rs | 17 +++- crates/core/src/server/path_handlers.rs | 6 +- 21 files changed, 323 insertions(+), 178 deletions(-) rename crates/core/src/{runtime/mod.rs => runtime.rs} (100%) rename crates/core/src/{server/mod.rs => server.rs} (100%) diff --git a/crates/core/src/client_events.rs b/crates/core/src/client_events.rs index 22137b264..af1085741 100644 --- a/crates/core/src/client_events.rs +++ b/crates/core/src/client_events.rs @@ -255,7 +255,7 @@ pub(crate) mod test { } else { // let contract_no = rng.gen_range(0..self.non_owned_contracts.len()); // self.non_owned_contracts[contract_no] - todo!("fixme") + todo!() }; break ContractRequest::Subscribe { key, summary: None }.into(); } diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index f12094421..07360c1a1 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -333,11 +333,18 @@ pub fn set_logger() { return; } + let filter = if cfg!(any(test, debug_assertions)) { + tracing_subscriber::filter::LevelFilter::DEBUG.into() + } else { + tracing_subscriber::filter::LevelFilter::INFO.into() + }; + let sub = tracing_subscriber::fmt().with_level(true).with_env_filter( tracing_subscriber::EnvFilter::builder() - .with_default_directive(tracing_subscriber::filter::LevelFilter::DEBUG.into()) + .with_default_directive(filter) .from_env_lossy() - .add_directive("stretto=off".parse().unwrap()), + .add_directive("stretto=off".parse().unwrap()) + .add_directive("sqlx=error".parse().unwrap()), ); if cfg!(any(test, debug_assertions)) { diff --git a/crates/core/src/contract.rs b/crates/core/src/contract.rs index b018490fd..1b621dfd8 100644 --- a/crates/core/src/contract.rs +++ b/crates/core/src/contract.rs @@ -91,11 +91,12 @@ where ContractHandlerEvent::PutQuery { key, state, + related_contracts, parameters, } => { let put_result = contract_handler .executor() - .upsert_contract_state(key, Either::Left(state), parameters) + .upsert_contract_state(key, Either::Left(state), related_contracts, parameters) .await .map_err(Into::into); contract_handler diff --git a/crates/core/src/contract/executor.rs b/crates/core/src/contract/executor.rs index 82526414d..864a6df18 100644 --- a/crates/core/src/contract/executor.rs +++ b/crates/core/src/contract/executor.rs @@ -233,6 +233,7 @@ pub(crate) trait ContractExecutor: Send + Sync + 'static { &mut self, key: ContractKey, state: Either>, + related_contracts: RelatedContracts<'static>, params: Option>, ) -> Result; } @@ -1247,6 +1248,7 @@ impl ContractExecutor for Executor { &mut self, _key: ContractKey, _state: Either>, + _related_contracts: RelatedContracts<'static>, _params: Option>, ) -> Result { todo!() @@ -1295,6 +1297,7 @@ impl ContractExecutor for Executor { &mut self, key: ContractKey, state: Either>, + _related_contracts: RelatedContracts<'static>, params: Option>, ) -> Result { // todo: instead allow to perform mutations per contract based on incoming value so we can track diff --git a/crates/core/src/contract/handler.rs b/crates/core/src/contract/handler.rs index 20a90c702..e259404bb 100644 --- a/crates/core/src/contract/handler.rs +++ b/crates/core/src/contract/handler.rs @@ -346,6 +346,7 @@ pub(crate) enum ContractHandlerEvent { PutQuery { key: ContractKey, state: WrappedState, + related_contracts: RelatedContracts<'static>, parameters: Option>, }, /// The response to a push query. diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index 39bd2c9e2..da34531b6 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -98,6 +98,10 @@ pub struct NodeBuilder { pub(crate) local_ip: Option, /// socket port to bind to the listener pub(crate) local_port: Option, + /// IP dialers should connect to + pub(crate) public_ip: Option, + /// socket port dialers should connect to + pub(crate) public_port: Option, /// At least an other running listener node is required for joining the network. /// Not necessary if this is an initial node. pub(crate) remote_nodes: Vec, @@ -122,6 +126,8 @@ impl NodeBuilder { remote_nodes: Vec::with_capacity(1), local_ip: None, local_port: None, + public_ip: None, + public_port: None, location: None, max_hops_to_live: None, rnd_if_htl_above: None, @@ -293,7 +299,7 @@ where } op.backoff = Some(backoff); } - connect::join_ring_request(tx_id, op_storage, conn_manager, op).await?; + connect::connect_request(tx_id, op_storage, conn_manager, op).await?; Ok(()) } @@ -343,9 +349,9 @@ async fn client_event_handling( #[inline] async fn process_open_request(request: OpenRequest<'static>, op_storage: Arc) { // this will indirectly start actions on the local contract executor - let op_storage_cp = op_storage.clone(); let fut = async move { let client_id = request.client_id; + let mut missing_contract = false; match *request.request { ClientRequest::ContractOp(ops) => match ops { ContractRequest::Put { @@ -356,13 +362,17 @@ async fn process_open_request(request: OpenRequest<'static>, op_storage: Arc, op_storage: Arc { // Initialize a subscribe op. loop { - // FIXME: this will block the loop until the subscribe op succeeds - // instead the op should be deferred for later execution let op = subscribe::start_op(key.clone()); - match subscribe::request_subscribe(&op_storage_cp, op, Some(client_id)) - .await - { - Err(OpError::ContractError(ContractError::ContractNotFound(key))) => { - tracing::warn!("Trying to subscribe to a contract not present: {}, requesting it first", key); + match subscribe::request_subscribe(&op_storage, op, Some(client_id)).await { + Err(OpError::ContractError(ContractError::ContractNotFound(key))) + if !missing_contract => + { + tracing::info!("Trying to subscribe to a contract not present: {key}, requesting it first"); + missing_contract = true; let get_op = get::start_op(key.clone(), true); if let Err(err) = - get::request_get(&op_storage_cp, get_op, Some(client_id)).await + get::request_get(&op_storage, get_op, Some(client_id)).await { - tracing::error!("Failed getting the contract `{}` while previously trying to subscribe; bailing: {}", key, err); - tokio::time::sleep(Duration::from_secs(5)).await; + tracing::error!("Failed getting the contract `{key}` while previously trying to subscribe; bailing: {err}"); + break; } } + Err(OpError::ContractError(ContractError::ContractNotFound(_))) => { + tokio::time::sleep(Duration::from_secs(1)).await + } Err(err) => { tracing::error!("{}", err); break; } - Ok(()) => break, + Ok(()) => { + if missing_contract { + tracing::debug!( + "Got back the missing contract ({key}) while subscribing" + ); + } + break; + } } } - todo!() } _ => { tracing::error!("op not supported"); @@ -423,7 +441,6 @@ async fn process_open_request(request: OpenRequest<'static>, op_storage: Arc { + Some(OpEnum::Connect(op)) if op.has_backoff() => { let ConnectOp { gateway, backoff, .. } = *op; @@ -631,7 +648,7 @@ where join_ring_request(Some(backoff), peer_key, &gateway, op_storage, conn_manager) .await?; } - Some(OpEnum::JoinRing(_)) => { + Some(OpEnum::Connect(_)) => { return Err(OpError::MaxRetriesExceeded(tx, tx.tx_type())); } _ => {} diff --git a/crates/core/src/node/conn_manager/in_memory.rs b/crates/core/src/node/conn_manager/in_memory.rs index abcc136eb..8953afc49 100644 --- a/crates/core/src/node/conn_manager/in_memory.rs +++ b/crates/core/src/node/conn_manager/in_memory.rs @@ -8,7 +8,7 @@ use std::{ use crossbeam::channel::{self, Receiver, Sender}; use once_cell::sync::OnceCell; -use rand::{prelude::StdRng, seq::SliceRandom, thread_rng, Rng, SeedableRng}; +use rand::{prelude::StdRng, seq::SliceRandom, Rng, SeedableRng}; use tokio::sync::Mutex; use super::{ConnectionBridge, ConnectionError, PeerKey}; @@ -34,23 +34,21 @@ impl MemoryConnManager { peer: PeerKey, log_register: Box, op_manager: Arc, + add_noise: bool, ) -> Self { - let transport = InMemoryTransport::new(peer); + let transport = InMemoryTransport::new(peer, add_noise); let msg_queue = Arc::new(Mutex::new(Vec::new())); let msg_queue_cp = msg_queue.clone(); - let tr_cp = transport.clone(); + let transport_cp = transport.clone(); GlobalExecutor::spawn(async move { // evaluate the messages as they arrive loop { - let Some(msg) = tr_cp.msg_stack_queue.lock().await.pop() else { - tokio::time::sleep(Duration::from_millis(10)).await; + let Some(msg) = transport_cp.msg_stack_queue.lock().await.pop() else { continue; }; let msg_data: Message = bincode::deserialize_from(Cursor::new(msg.data)).unwrap(); - if let Ok(mut queue) = msg_queue_cp.try_lock() { - queue.push(msg_data); - } + msg_queue_cp.lock().await.push(msg_data); } }); @@ -65,7 +63,9 @@ impl MemoryConnManager { pub async fn recv(&self) -> Result { loop { - let Some(msg) = self.msg_queue.try_lock().ok().and_then(|mut l| l.pop()) else { + let mut queue = self.msg_queue.lock().await; + let Some(msg) = queue.pop() else { + std::mem::drop(queue); tokio::time::sleep(Duration::from_millis(10)).await; continue; }; @@ -131,14 +131,13 @@ pub struct InMemoryTransport { } impl InMemoryTransport { - fn new(interface_peer: PeerKey) -> Self { + fn new(interface_peer: PeerKey, add_noise: bool) -> Self { let msg_stack_queue = Arc::new(Mutex::new(Vec::new())); - let (tx, rx) = NETWORK_WIRES.get_or_init(crossbeam::channel::unbounded); + let (network_tx, network_rx) = NETWORK_WIRES.get_or_init(crossbeam::channel::unbounded); // store messages incoming from the network in the msg stack - let rcv_msg_c = msg_stack_queue.clone(); - let rx = rx.clone(); - let tx_cp = tx.clone(); + let msg_stack_queue_cp = msg_stack_queue.clone(); + let network_tx_cp = network_tx.clone(); GlobalExecutor::spawn(async move { const MAX_DELAYED_MSG: usize = 10; let mut rng = StdRng::from_entropy(); @@ -146,25 +145,29 @@ impl InMemoryTransport { let mut delayed: HashMap<_, Vec<_>> = HashMap::with_capacity(MAX_DELAYED_MSG); let last_drain = Instant::now(); loop { - match rx.try_recv() { + match network_rx.try_recv() { Ok(msg) if msg.target == interface_peer => { tracing::trace!( "Inbound message received for peer {} from {}", interface_peer, msg.origin ); - if (rng.gen_bool(0.5) && delayed.len() < MAX_DELAYED_MSG) - || delayed.contains_key(&msg.target) - { + if rng.gen_bool(0.5) && delayed.len() < MAX_DELAYED_MSG && add_noise { delayed.entry(msg.target).or_default().push(msg); tokio::time::sleep(Duration::from_millis(10)).await; } else { - rcv_msg_c.lock().await.push(msg); + let mut queue = msg_stack_queue_cp.lock().await; + queue.push(msg); + if add_noise && rng.gen_bool(0.2) { + queue.shuffle(&mut rng); + } } } Ok(msg) => { // send back to the network since this msg belongs to other peer - tx_cp.send(msg).expect("failed to send msg back to network"); + network_tx_cp + .send(msg) + .expect("failed to send msg back to network"); tokio::time::sleep(Duration::from_nanos(1_000)).await } Err(channel::TryRecvError::Disconnected) => break, @@ -176,22 +179,21 @@ impl InMemoryTransport { && !delayed.is_empty()) || delayed.len() == MAX_DELAYED_MSG { - let mut queue = rcv_msg_c.lock().await; + let mut queue = msg_stack_queue_cp.lock().await; for (_, msgs) in delayed.drain() { queue.extend(msgs); } let queue = &mut queue; - queue.shuffle(&mut thread_rng()); + queue.shuffle(&mut rng); } } tracing::error!("Stopped receiving messages in {}", interface_peer); }); - let network = tx.clone(); Self { interface_peer, msg_stack_queue, - network, + network: network_tx.clone(), } } diff --git a/crates/core/src/node/conn_manager/p2p_protoc.rs b/crates/core/src/node/conn_manager/p2p_protoc.rs index d7c0c0b64..f8acf8202 100644 --- a/crates/core/src/node/conn_manager/p2p_protoc.rs +++ b/crates/core/src/node/conn_manager/p2p_protoc.rs @@ -64,7 +64,7 @@ const CURRENT_IDENTIFY_PROTOC_VER: &str = "/id/1.0.0"; fn config_behaviour( local_key: &Keypair, gateways: &[InitPeerNode], - _public_addr: &Option, + _private_addr: &Option, op_manager: Arc, ) -> NetBehaviour { let routing_table: HashMap<_, _> = gateways @@ -234,6 +234,7 @@ pub(in crate::node) struct P2pConnManager { conn_bridge_rx: Receiver, /// last valid observed public address public_addr: Option, + listening_addr: Option, event_listener: Box, } @@ -248,7 +249,14 @@ impl P2pConnManager { // to reuse it's thread pool and scheduler in order to drive futures. let global_executor = GlobalExecutor; - let public_addr = if let Some(conn) = config.local_ip.zip(config.local_port) { + let private_addr = if let Some(conn) = config.local_ip.zip(config.local_port) { + let public_addr = multiaddr_from_connection(conn); + Some(public_addr) + } else { + None + }; + + let public_addr = if let Some(conn) = config.public_ip.zip(config.public_port) { let public_addr = multiaddr_from_connection(conn); Some(public_addr) } else { @@ -258,7 +266,7 @@ impl P2pConnManager { let behaviour = config_behaviour( &config.local_key, &config.remote_nodes, - &public_addr, + &private_addr, op_manager.clone(), ); let mut swarm = Swarm::new( @@ -283,12 +291,13 @@ impl P2pConnManager { bridge, conn_bridge_rx: rx_bridge_cmd, public_addr, + listening_addr: private_addr, event_listener: Box::new(event_listener), }) } pub fn listen_on(&mut self) -> Result<(), anyhow::Error> { - if let Some(listening_addr) = &self.public_addr { + if let Some(listening_addr) = &self.listening_addr { self.swarm.listen_on(listening_addr.clone())?; } Ok(()) @@ -510,7 +519,7 @@ impl P2pConnManager { self.public_addr = Some(address); } Ok(Right(IsPrivatePeer(_peer))) => { - todo!("attempt hole punching") + todo!("this peer is private, attempt hole punching") } Ok(Right(ClosedChannel)) => { tracing::info!("Notification channel closed"); @@ -561,7 +570,7 @@ enum ConnMngrActions { }, /// Update self own public address, useful when communicating for first time UpdatePublicAddr(Multiaddr), - /// A peer which we attempted connection to is private, attempt hole-punching + /// This is private, so when establishing connections hole-punching should be performed IsPrivatePeer(PeerId), NodeAction(NodeEvent), ClosedChannel, diff --git a/crates/core/src/node/event_log.rs b/crates/core/src/node/event_log.rs index e2a99705c..b759ab921 100644 --- a/crates/core/src/node/event_log.rs +++ b/crates/core/src/node/event_log.rs @@ -15,7 +15,7 @@ use crate::{ config::GlobalExecutor, contract::StoreResponse, message::{Message, Transaction}, - operations::{connect, get::GetMsg, put::PutMsg}, + operations::{connect, get::GetMsg, put::PutMsg, subscribe::SubscribeMsg}, ring::PeerKeyLocation, router::RouteEvent, DynError, @@ -177,6 +177,15 @@ impl<'a> EventLog<'a> { value: StoreResponse { state: Some(_), .. }, .. }) => EventKind::Get { key: key.clone() }, + Message::Subscribe(SubscribeMsg::ReturnSub { + subscribed: true, + key, + sender, + .. + }) => EventKind::Subscribed { + key: key.clone(), + at: *sender, + }, _ => EventKind::Ignored, }; Either::Left(EventLog { @@ -451,6 +460,10 @@ enum EventKind { key: ContractKey, }, Route(RouteEvent), + Subscribed { + key: ContractKey, + at: PeerKeyLocation, + }, Ignored, } @@ -610,6 +623,18 @@ pub(super) mod test_utils { }) } + pub fn is_subscribed_to_contract( + &self, + peer: &PeerKey, + expected_key: &ContractKey, + ) -> bool { + let logs = self.logs.lock(); + logs.iter().any(|log| { + &log.peer_id == peer + && matches!(log.kind, EventKind::Subscribed { ref key, .. } if key == expected_key ) + }) + } + /// Unique connections for a given peer and their relative distance to other peers. pub fn connections(&self, peer: PeerKey) -> impl Iterator { let logs = self.logs.lock(); diff --git a/crates/core/src/node/in_memory_impl.rs b/crates/core/src/node/in_memory_impl.rs index 47246894d..d24652742 100644 --- a/crates/core/src/node/in_memory_impl.rs +++ b/crates/core/src/node/in_memory_impl.rs @@ -42,6 +42,7 @@ impl NodeInMemory { builder: NodeBuilder<1>, event_listener: EL, ch_builder: String, + add_noise: bool, ) -> Result { let event_listener = Box::new(event_listener); let peer_key = PeerKey::from(builder.local_key.public()); @@ -58,8 +59,12 @@ impl NodeInMemory { .await .map_err(|e| anyhow::anyhow!(e))?; - let conn_manager = - MemoryConnManager::new(peer_key, event_listener.trait_clone(), op_storage.clone()); + let conn_manager = MemoryConnManager::new( + peer_key, + event_listener.trait_clone(), + op_storage.clone(), + add_noise, + ); GlobalExecutor::spawn(contract::contract_handling(contract_handler)); @@ -118,6 +123,7 @@ impl NodeInMemory { ContractHandlerEvent::PutQuery { key: key.clone(), state, + related_contracts: RelatedContracts::default(), parameters: Some(parameters), }, None, diff --git a/crates/core/src/node/op_state_manager.rs b/crates/core/src/node/op_state_manager.rs index 6aeafec07..528833d81 100644 --- a/crates/core/src/node/op_state_manager.rs +++ b/crates/core/src/node/op_state_manager.rs @@ -111,7 +111,7 @@ impl OpManager { pub fn push(&self, id: Transaction, op: OpEnum) -> Result<(), OpError> { match op { - OpEnum::JoinRing(op) => { + OpEnum::Connect(op) => { #[cfg(debug_assertions)] check_id_op!(id.tx_type(), TransactionType::JoinRing); self.join_ring.insert(id, *op); @@ -146,7 +146,7 @@ impl OpManager { .join_ring .remove(id) .map(|(_k, v)| v) - .map(|op| OpEnum::JoinRing(Box::new(op))), + .map(|op| OpEnum::Connect(Box::new(op))), TransactionType::Put => self.put.remove(id).map(|(_k, v)| v).map(OpEnum::Put), TransactionType::Get => self.get.remove(id).map(|(_k, v)| v).map(OpEnum::Get), TransactionType::Subscribe => self diff --git a/crates/core/src/node/tests.rs b/crates/core/src/node/tests.rs index 4bccb574b..c1ad04b85 100644 --- a/crates/core/src/node/tests.rs +++ b/crates/core/src/node/tests.rs @@ -127,6 +127,7 @@ pub(crate) struct SimNetwork { max_connections: usize, min_connections: usize, init_backoff: Duration, + add_noise: bool, } impl SimNetwork { @@ -155,6 +156,7 @@ impl SimNetwork { max_connections, min_connections, init_backoff: Duration::from_millis(1), + add_noise: false, }; net.build_gateways(gateways).await; net.build_nodes(nodes).await; @@ -165,6 +167,12 @@ impl SimNetwork { self.init_backoff = value; } + /// Simulates network random behaviour, like messages arriving delayed or out of order, throttling etc. + #[allow(unused)] + pub fn with_noise(&mut self) { + self.add_noise = true; + } + #[allow(unused)] pub fn debug(&mut self) { self.debug = true; @@ -226,6 +234,7 @@ impl SimNetwork { this_node, self.event_listener.clone(), format!("{}-{label}", self.name, label = this_config.label), + self.add_noise, ) .await .unwrap(); @@ -275,6 +284,7 @@ impl SimNetwork { config, self.event_listener.clone(), format!("{}-{label}", self.name), + self.add_noise, ) .await .unwrap(); @@ -344,25 +354,33 @@ impl SimNetwork { pub fn has_put_contract( &self, - peer: &NodeLabel, + peer: impl Into, key: &ContractKey, value: &WrappedState, ) -> bool { - if let Some(pk) = self.labels.get(peer) { + if let Some(pk) = self.labels.get(&peer.into()) { self.event_listener.has_put_contract(pk, key, value) } else { panic!("peer not found"); } } - pub fn has_got_contract(&self, peer: &NodeLabel, key: &ContractKey) -> bool { - if let Some(pk) = self.labels.get(peer) { + pub fn has_got_contract(&self, peer: impl Into, key: &ContractKey) -> bool { + if let Some(pk) = self.labels.get(&peer.into()) { self.event_listener.has_got_contract(pk, key) } else { panic!("peer not found"); } } + pub fn is_subscribed_to_contract(&self, peer: impl Into, key: &ContractKey) -> bool { + if let Some(pk) = self.labels.get(&peer.into()) { + self.event_listener.is_subscribed_to_contract(pk, key) + } else { + panic!("peer not found"); + } + } + /// Builds an histogram of the distribution in the ring of each node relative to each other. pub fn ring_distribution(&self, scale: i32) -> Vec<(f64, usize)> { let mut all_dists = Vec::with_capacity(self.labels.len()); @@ -395,15 +413,20 @@ impl SimNetwork { peers_connections } + /// # Arguments + /// + /// - label: node for which to trigger the + /// - event_id: which event to trigger + /// - await_for: if set, wait for the duration before returning pub async fn trigger_event( &self, - label: &NodeLabel, + label: impl Into, event_id: EventId, await_for: Option, ) -> Result<(), anyhow::Error> { let peer = self .labels - .get(label) + .get(&label.into()) .ok_or_else(|| anyhow::anyhow!("node not found"))?; self.user_ev_controller .send((event_id, *peer)) diff --git a/crates/core/src/operations.rs b/crates/core/src/operations.rs index 6c5b766e9..cef8a097a 100644 --- a/crates/core/src/operations.rs +++ b/crates/core/src/operations.rs @@ -126,7 +126,7 @@ where } pub(crate) enum OpEnum { - JoinRing(Box), + Connect(Box), Put(put::PutOp), Get(get::GetOp), Subscribe(subscribe::SubscribeOp), @@ -136,7 +136,7 @@ pub(crate) enum OpEnum { impl OpEnum { delegate::delegate! { to match self { - OpEnum::JoinRing(op) => op, + OpEnum::Connect(op) => op, OpEnum::Put(op) => op, OpEnum::Get(op) => op, OpEnum::Subscribe(op) => op, diff --git a/crates/core/src/operations/connect.rs b/crates/core/src/operations/connect.rs index 0f34f3290..01962d34e 100644 --- a/crates/core/src/operations/connect.rs +++ b/crates/core/src/operations/connect.rs @@ -20,7 +20,7 @@ pub(crate) use self::messages::{ConnectMsg, ConnectRequest, ConnectResponse}; pub(crate) struct ConnectOp { id: Transaction, - state: Option, + state: Option, pub gateway: Box, /// keeps track of the number of retries and applies an exponential backoff cooldown period pub backoff: Option, @@ -38,7 +38,7 @@ impl ConnectOp { } pub(super) fn finalized(&self) -> bool { - matches!(self.state, Some(JRState::Connected)) + matches!(self.state, Some(ConnectState::Connected)) } pub(super) fn record_transfer(&mut self) {} @@ -86,11 +86,11 @@ impl Operation for ConnectOp { let sender; let tx = *msg.id(); match op_storage.pop(msg.id()) { - Some(OpEnum::JoinRing(join_op)) => { + Some(OpEnum::Connect(connect_op)) => { sender = msg.sender().cloned(); // was an existing operation, the other peer messaged back Ok(OpInitialization { - op: *join_op, + op: *connect_op, sender, }) } @@ -100,7 +100,7 @@ impl Operation for ConnectOp { Ok(OpInitialization { op: Self { id: tx, - state: Some(JRState::Initializing), + state: Some(ConnectState::Initializing), backoff: None, gateway: Box::new(op_storage.ring.own_location()), _ttl: PEER_TIMEOUT, @@ -141,7 +141,7 @@ impl Operation for ConnectOp { // likely a gateway which accepts connections tracing::debug!( tx = %id, - "Initial join request received from {} with HTL {} @ {}", + "Connection request received from {} with HTL {} @ {}", joiner, hops_to_live, this_node_loc.peer @@ -190,7 +190,7 @@ impl Operation for ConnectOp { this_node_loc.peer, joiner ); - new_state = Some(JRState::OCReceived); + new_state = Some(ConnectState::OCReceived); } else { new_state = None } @@ -221,7 +221,7 @@ impl Operation for ConnectOp { let own_loc = op_storage.ring.own_location(); tracing::debug!( tx = %id, - "Proxy join request received from {} to join new peer {} with HTL {} @ {}", + "Proxy connect request received from {} to ing new peer {} with HTL {} @ {}", sender.peer, joiner.peer, hops_to_live, @@ -259,7 +259,7 @@ impl Operation for ConnectOp { return_msg = None; } else { match self.state { - Some(JRState::Initializing) => { + Some(ConnectState::Initializing) => { let (state, msg) = try_proxy_connection( &id, &sender, @@ -269,7 +269,7 @@ impl Operation for ConnectOp { new_state = state; return_msg = msg; } - Some(JRState::AwaitingProxyResponse { + Some(ConnectState::AwaitingProxyResponse { accepted_by: mut previously_accepted, new_peer_id, target, @@ -285,7 +285,7 @@ impl Operation for ConnectOp { } if match_target { - new_state = Some(JRState::OCReceived); + new_state = Some(ConnectState::OCReceived); tracing::debug!( tx = %id, "Sending response to join request with all the peers that accepted \ @@ -309,10 +309,10 @@ impl Operation for ConnectOp { // is that we would end up with a dead connection; // this then must be dealed with by the normal mechanisms that keep // connections alive and prune any dead connections - new_state = Some(JRState::Connected); + new_state = Some(ConnectState::Connected); tracing::debug!( tx = %id, - "Sending response to join request with all the peers that accepted \ + "Sending response to connect request with all the peers that accepted \ connection from proxy peer {} to proxy peer {}", sender.peer, own_loc.peer @@ -347,7 +347,7 @@ impl Operation for ConnectOp { }, .. } => { - tracing::debug!(tx = %id, "Join response received from {}", sender.peer); + tracing::debug!(tx = %id, "Connect response received from {}", sender.peer); // Set the given location let pk_loc = PeerKeyLocation { @@ -357,7 +357,7 @@ impl Operation for ConnectOp { // fixme: remove tracing::debug!("accepted by state: {:?} ", self.state,); - let Some(JRState::Connecting(ConnectionInfo { gateway, .. })) = self.state + let Some(ConnectState::Connecting(ConnectionInfo { gateway, .. })) = self.state else { return Err(OpError::InvalidStateTransition(self.id)); }; @@ -369,7 +369,7 @@ impl Operation for ConnectOp { your_peer_id, gateway.peer ); - new_state = Some(JRState::OCReceived); + new_state = Some(ConnectState::OCReceived); return_msg = Some(ConnectMsg::Response { id, msg: ConnectResponse::ReceivedOC { by_peer: pk_loc }, @@ -416,7 +416,7 @@ impl Operation for ConnectOp { op_storage .notify_op_change( Message::Aborted(id), - OpEnum::JoinRing(op.into()), + OpEnum::Connect(op.into()), None, ) .await?; @@ -429,9 +429,9 @@ impl Operation for ConnectOp { target, msg: ConnectResponse::Proxy { mut accepted_by }, } => { - tracing::debug!(tx = %id, "Received proxy join at @ {}", target.peer); + tracing::debug!(tx = %id, "Received proxy connect at @ {}", target.peer); match self.state { - Some(JRState::Initializing) => { + Some(ConnectState::Initializing) => { // the sender of the response is the target of the request and // is only a completed tx if it accepted the connection if accepted_by.contains(&sender) { @@ -441,7 +441,7 @@ impl Operation for ConnectOp { target.peer, sender.peer, ); - new_state = Some(JRState::Connected); + new_state = Some(ConnectState::Connected); } else { tracing::debug!("Failed to connect at proxy {}", sender.peer); new_state = None; @@ -453,7 +453,7 @@ impl Operation for ConnectOp { target, }); } - Some(JRState::AwaitingProxyResponse { + Some(ConnectState::AwaitingProxyResponse { accepted_by: mut previously_accepted, new_peer_id, target: original_target, @@ -467,21 +467,21 @@ impl Operation for ConnectOp { if is_accepted { previously_accepted.extend(accepted_by.drain()); if is_target_peer { - new_state = Some(JRState::OCReceived); + new_state = Some(ConnectState::OCReceived); } else { // for proxies just consider the connection open directly // what would happen in case that the connection is not confirmed end-to-end // is that we would end up with a dead connection; // this then must be dealed with by the normal mechanisms that keep // connections alive and prune any dead connections - new_state = Some(JRState::Connected); + new_state = Some(ConnectState::Connected); } } if is_target_peer { tracing::debug!( tx = %id, - "Sending response to join request with all the peers that accepted \ + "Sending response to connect request with all the peers that accepted \ connection from gateway {} to peer {}", target.peer, original_target.peer @@ -499,7 +499,7 @@ impl Operation for ConnectOp { } else { tracing::debug!( tx = %id, - "Sending response to join request with all the peers that accepted \ + "Sending response to connect request with all the peers that accepted \ connection from proxy peer {} to proxy peer {}", target.peer, original_target.peer @@ -532,9 +532,9 @@ impl Operation for ConnectOp { target, } => { match self.state { - Some(JRState::OCReceived) => { + Some(ConnectState::OCReceived) => { tracing::debug!(tx = %id, "Acknowledge connected at gateway"); - new_state = Some(JRState::Connected); + new_state = Some(ConnectState::Connected); return_msg = Some(ConnectMsg::Connected { id, sender: target, @@ -559,9 +559,9 @@ impl Operation for ConnectOp { } ConnectMsg::Connected { target, sender, id } => { match self.state { - Some(JRState::OCReceived) => { + Some(ConnectState::OCReceived) => { tracing::debug!(tx = %id, "Acknowledge connected at peer {}", target.peer); - new_state = Some(JRState::Connected); + new_state = Some(ConnectState::Connected); return_msg = None; } _ => return Err(OpError::InvalidStateTransition(self.id)), @@ -602,7 +602,7 @@ impl Operation for ConnectOp { fn build_op_result( id: Transaction, - state: Option, + state: Option, msg: Option, gateway: Box, backoff: Option, @@ -617,7 +617,7 @@ fn build_op_result( }); Ok(OperationResult { return_msg: msg.map(Message::from), - state: output_op.map(|op| OpEnum::JoinRing(Box::new(op))), + state: output_op.map(|op| OpEnum::Connect(Box::new(op))), }) } @@ -626,7 +626,7 @@ fn try_proxy_connection( sender: &PeerKeyLocation, own_loc: &PeerKeyLocation, accepted_by: HashSet, -) -> (Option, Option) { +) -> (Option, Option) { let new_state = if accepted_by.contains(own_loc) { tracing::debug!( tx = %id, @@ -634,7 +634,7 @@ fn try_proxy_connection( sender.peer, own_loc.peer, ); - Some(JRState::Connected) + Some(ConnectState::Connected) } else { tracing::debug!(tx = %id, "Failed to connect at proxy {}", sender.peer); None @@ -685,7 +685,7 @@ mod states { use super::*; use std::fmt::Display; - impl Display for JRState { + impl Display for ConnectState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Initializing => write!(f, "Initializing"), @@ -701,7 +701,7 @@ mod states { } #[derive(Debug)] -enum JRState { +enum ConnectState { Initializing, Connecting(ConnectionInfo), AwaitingProxyResponse { @@ -722,7 +722,7 @@ struct ConnectionInfo { max_hops_to_live: usize, } -impl JRState { +impl ConnectState { fn try_unwrap_connecting(self) -> Result { if let Self::Connecting(conn_info) = self { Ok(conn_info) @@ -732,7 +732,7 @@ impl JRState { } fn is_connected(&self) -> bool { - matches!(self, JRState::Connected { .. }) + matches!(self, ConnectState::Connected { .. }) } fn add_new_proxy( @@ -755,8 +755,8 @@ pub(crate) fn initial_request( id: Transaction, ) -> ConnectOp { const MAX_JOIN_RETRIES: usize = 3; - tracing::debug!(tx = %id, "Connecting to gw {} from {}", gateway.peer, this_peer); - let state = JRState::Connecting(ConnectionInfo { + tracing::debug!(tx = %id, "Connecting to gateway {} from {}", gateway.peer, this_peer); + let state = ConnectState::Connecting(ConnectionInfo { gateway, this_peer, max_hops_to_live, @@ -780,7 +780,7 @@ pub(crate) fn initial_request( } /// Join ring routine, called upon performing a join operation for this node. -pub(crate) async fn join_ring_request( +pub(crate) async fn connect_request( tx: Transaction, op_storage: &OpManager, conn_manager: &mut CB, @@ -804,7 +804,7 @@ where tracing::info!( tx = %id, - "Joining ring via {} (at {})", + "Connecting to peer {} (at {})", gateway.peer, gateway.location.ok_or(ConnectionError::LocationUnknown)?, ); @@ -824,9 +824,9 @@ where conn_manager.send(&gateway.peer, join_req).await?; op_storage.push( tx, - OpEnum::JoinRing(Box::new(ConnectOp { + OpEnum::Connect(Box::new(ConnectOp { id, - state: Some(JRState::Connecting(ConnectionInfo { + state: Some(ConnectState::Connecting(ConnectionInfo { gateway, this_peer, max_hops_to_live, @@ -848,7 +848,7 @@ async fn forward_conn( joiner: PeerKeyLocation, left_htl: usize, num_accepted: usize, -) -> Result, OpError> +) -> Result, OpError> where CM: ConnectionBridge, { @@ -856,7 +856,7 @@ where tracing::debug!( tx = %id, joiner = %joiner.peer, - "Couldn't forward join petition, no hops left or enough connections", + "Couldn't forward connect petition, no hops left or enough connections", ); return Ok(None); } @@ -865,7 +865,7 @@ where tracing::warn!( tx = %id, joiner = %joiner.peer, - "Couldn't forward join petition, not enough connections", + "Couldn't forward connect petition, not enough connections", ); return Ok(None); } @@ -874,7 +874,7 @@ where tracing::debug!( tx = %id, joiner = %joiner.peer, - "Randomly selecting peer to forward JoinRequest", + "Randomly selecting peer to forward connect request", ); ring.random_peer(|p| p != &req_peer.peer) } else { @@ -899,13 +899,13 @@ where }); tracing::debug!( tx = %id, - "Forwarding JoinRequest from sender {} to {}", + "Forwarding connect request from sender {} to {}", req_peer.peer, forward_to.peer ); conn_manager.send(&forward_to.peer, forwarded).await?; // awaiting for responses from forward nodes - let new_state = JRState::AwaitingProxyResponse { + let new_state = ConnectState::AwaitingProxyResponse { target: req_peer, accepted_by: HashSet::new(), new_location: joiner.location.unwrap(), diff --git a/crates/core/src/operations/get.rs b/crates/core/src/operations/get.rs index 9a5733420..8c3cf4d70 100644 --- a/crates/core/src/operations/get.rs +++ b/crates/core/src/operations/get.rs @@ -236,6 +236,7 @@ impl Operation for GetOp { if !is_cached_contract { tracing::warn!( + tx = %id, "Contract `{}` not found while processing a get request at node @ {}", key, target.peer @@ -243,6 +244,7 @@ impl Operation for GetOp { if htl == 0 { tracing::warn!( + tx = %id, "The maximum HOPS number has been exceeded, sending the error \ back to the node @ {}", sender.peer @@ -271,7 +273,7 @@ impl Operation for GetOp { let Some(new_target) = op_storage.ring.closest_caching(&key, &[sender.peer]) else { - tracing::warn!("no peer found while trying getting contract {key}"); + tracing::warn!(tx = %id, "no peer found while trying getting contract {key}"); return Err(OpError::RingError(RingError::NoCachingPeers(key))); }; @@ -316,24 +318,25 @@ impl Operation for GetOp { Err(err) => return Err(err), } - tracing::debug!("Contract {returned_key} found @ peer {}", target.peer); + tracing::debug!(tx = %id, "Contract {returned_key} found @ peer {}", target.peer); match self.state { Some(GetState::AwaitingResponse { .. }) => { tracing::debug!( - "Completed operation, Get response received for contract {key}" + tx = %id, + "Completed operation, get response received for contract {key}" ); // Completed op new_state = None; return_msg = None; } Some(GetState::ReceivedRequest) => { - tracing::debug!("Returning contract {} to {}", key, sender.peer); + tracing::debug!(tx = %id, "Returning contract {} to {}", key, sender.peer); new_state = None; let value = match value { Ok(res) => res, Err(err) => { - tracing::error!("error: {err}"); + tracing::error!(tx = %id, "error: {err}"); return Err(OpError::ExecutorError(err)); } }; @@ -365,6 +368,7 @@ impl Operation for GetOp { } => { let this_loc = target; tracing::warn!( + tx = %id, "Neither contract or contract value for contract `{}` found at peer {}, \ retrying with other peers", key, @@ -406,6 +410,7 @@ impl Operation for GetOp { }); } else { tracing::error!( + tx = %id, "Failed getting a value for contract {}, reached max retries", key ); @@ -413,7 +418,7 @@ impl Operation for GetOp { } } Some(GetState::ReceivedRequest) => { - tracing::debug!("Returning contract {} to {}", key, sender.peer); + tracing::debug!(tx = %id, "Returning contract {} to {}", key, sender.peer); new_state = None; return_msg = Some(GetMsg::ReturnGet { id, @@ -459,10 +464,11 @@ impl Operation for GetOp { ) .await?; let key = contract.key(); - tracing::debug!("Contract `{}` successfully cached", key); + tracing::debug!(tx = %id, "Contract `{}` successfully cached", key); } else { // no contract, consider this like an error ignoring the incoming update value tracing::warn!( + tx = %id, "Contract not received from peer {} while requested", sender.peer ); @@ -501,6 +507,7 @@ impl Operation for GetOp { ContractHandlerEvent::PutQuery { key: key.clone(), state: value.clone(), + related_contracts: RelatedContracts::default(), parameters, }, client_id, @@ -511,6 +518,7 @@ impl Operation for GetOp { Some(GetState::AwaitingResponse { fetch_contract, .. }) => { if fetch_contract && contract.is_none() { tracing::error!( + tx = %id, "Get response received for contract {key}, but the contract wasn't returned" ); new_state = None; @@ -520,7 +528,7 @@ impl Operation for GetOp { contract, }); } else { - tracing::debug!("Get response received for contract {}", key); + tracing::debug!(tx = %id, "Get response received for contract {}", key); new_state = None; return_msg = None; result = Some(GetResult { @@ -530,7 +538,7 @@ impl Operation for GetOp { } } Some(GetState::ReceivedRequest) => { - tracing::debug!("Returning contract {} to {}", key, sender.peer); + tracing::debug!(tx = %id, "Returning contract {} to {}", key, sender.peer); new_state = None; return_msg = Some(GetMsg::ReturnGet { id, @@ -581,6 +589,7 @@ async fn continue_seeking( retry_msg: Message, ) -> Result<(), OpError> { tracing::info!( + tx = %retry_msg.id(), "Retrying to get the contract from node @ {}", new_target.peer ); @@ -600,6 +609,7 @@ fn check_contract_found( if returned_key != key { // shouldn't be a reachable path tracing::error!( + tx = %id, "contract retrieved ({}) and asked ({}) are not the same", returned_key, key @@ -622,9 +632,8 @@ fn check_contract_found( pub(crate) fn start_op(key: ContractKey, fetch_contract: bool) -> GetOp { let contract_location = Location::from(&key); - tracing::debug!("Requesting get contract {} @ loc({contract_location})", key,); - let id = Transaction::new::(); + tracing::debug!(tx = %id, "Requesting get contract {key} @ loc({contract_location})"); let state = Some(GetState::PrepareRequest { key, id, @@ -866,10 +875,9 @@ mod test { // trigger get @ node-0, which does not own the contract sim_nw - .trigger_event(&"node-0".into(), 1, Some(Duration::from_millis(100))) + .trigger_event("node-0", 1, Some(Duration::from_millis(50))) .await?; - tokio::time::sleep(Duration::from_millis(200)).await; - assert!(sim_nw.has_got_contract(&"node-0".into(), &key)); + assert!(sim_nw.has_got_contract("node-0", &key)); Ok(()) } @@ -905,9 +913,9 @@ mod test { // trigger get @ node-1, which does not own the contract sim_nw - .trigger_event(&"node-1".into(), 1, Some(Duration::from_millis(100))) + .trigger_event("node-1", 1, Some(Duration::from_millis(50))) .await?; - assert!(!sim_nw.has_got_contract(&"node-1".into(), &key)); + assert!(!sim_nw.has_got_contract("node-1", &key)); Ok(()) } @@ -973,9 +981,9 @@ mod test { sim_nw.check_connectivity(Duration::from_secs(3)).await?; sim_nw - .trigger_event(&"node-0".into(), 1, Some(Duration::from_millis(500))) + .trigger_event("node-0", 1, Some(Duration::from_millis(50))) .await?; - assert!(sim_nw.has_got_contract(&"node-0".into(), &key)); + assert!(sim_nw.has_got_contract("node-0", &key)); Ok(()) } } diff --git a/crates/core/src/operations/put.rs b/crates/core/src/operations/put.rs index 84ab12e7b..11bd6bbf9 100644 --- a/crates/core/src/operations/put.rs +++ b/crates/core/src/operations/put.rs @@ -178,6 +178,7 @@ impl Operation for PutOp { PutMsg::RequestPut { id, contract, + related_contracts, value, htl, target, @@ -198,6 +199,7 @@ impl Operation for PutOp { target, value, contract, + related_contracts, htl, skip_list: vec![sender.peer], }); @@ -210,6 +212,7 @@ impl Operation for PutOp { sender, value, contract, + related_contracts, htl, target, mut skip_list, @@ -246,8 +249,15 @@ impl Operation for PutOp { // after the contract has been cached, push the update query tracing::debug!("Attempting contract value update"); let parameters = contract.params(); - let new_value = - put_contract(op_storage, key.clone(), value, parameters, client_id).await?; + let new_value = put_contract( + op_storage, + key.clone(), + value, + related_contracts, + parameters, + client_id, + ) + .await?; tracing::debug!("Contract successfully updated"); // if the change was successful, communicate this back to the requestor and broadcast the change conn_manager @@ -320,6 +330,7 @@ impl Operation for PutOp { op_storage, key.clone(), new_value, + RelatedContracts::default(), parameters.clone(), client_id, ) @@ -468,9 +479,15 @@ impl Operation for PutOp { }); } // after the contract has been cached, push the update query - let new_value = - put_contract(op_storage, key, new_value, contract.params(), client_id) - .await?; + let new_value = put_contract( + op_storage, + key, + new_value, + RelatedContracts::default(), + contract.params(), + client_id, + ) + .await?; //update skip list skip_list.push(peer_loc.peer); @@ -597,7 +614,12 @@ async fn try_to_broadcast( Ok((new_state, return_msg)) } -pub(crate) fn start_op(contract: ContractContainer, value: WrappedState, htl: usize) -> PutOp { +pub(crate) fn start_op( + contract: ContractContainer, + related_contracts: RelatedContracts<'static>, + value: WrappedState, + htl: usize, +) -> PutOp { let key = contract.key(); let contract_location = Location::from(&key); tracing::debug!( @@ -609,6 +631,7 @@ pub(crate) fn start_op(contract: ContractContainer, value: WrappedState, htl: us // let payload_size = contract.data().len(); let state = Some(PutState::PrepareRequest { contract, + related_contracts, value, htl, }); @@ -632,6 +655,7 @@ enum PutState { ReceivedRequest, PrepareRequest { contract: ContractContainer, + related_contracts: RelatedContracts<'static>, value: WrappedState, htl: usize, }, @@ -675,13 +699,14 @@ pub(crate) async fn request_put( contract, value, htl, - .. + related_contracts, }) => { let key = contract.key(); let new_state = Some(PutState::AwaitingResponse { contract: key }); let msg = PutMsg::RequestPut { id, contract, + related_contracts, value, htl, target, @@ -708,6 +733,7 @@ async fn put_contract( op_storage: &OpManager, key: ContractKey, state: WrappedState, + related_contracts: RelatedContracts<'static>, parameters: Parameters<'static>, client_id: Option, ) -> Result { @@ -717,6 +743,7 @@ async fn put_contract( ContractHandlerEvent::PutQuery { key, state, + related_contracts, parameters: Some(parameters), }, client_id, @@ -800,6 +827,8 @@ mod messages { RequestPut { id: Transaction, contract: ContractContainer, + #[serde(deserialize_with = "RelatedContracts::deser_related_contracts")] + related_contracts: RelatedContracts<'static>, value: WrappedState, /// max hops to live htl: usize, @@ -828,6 +857,8 @@ mod messages { target: PeerKeyLocation, value: WrappedState, contract: ContractContainer, + #[serde(deserialize_with = "RelatedContracts::deser_related_contracts")] + related_contracts: RelatedContracts<'static>, /// max hops to live htl: usize, // FIXME: remove skip list once we deduplicate at top msg handling level @@ -926,7 +957,6 @@ mod test { #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn successful_put_op_between_nodes() -> Result<(), anyhow::Error> { - crate::config::set_logger(); const NUM_NODES: usize = 2usize; const NUM_GW: usize = 1usize; @@ -941,9 +971,9 @@ mod test { "successful_put_op_between_nodes", NUM_GW, NUM_NODES, - 3, 2, - 4, + 1, + 3, 2, ) .await; @@ -997,14 +1027,13 @@ mod test { ]); sim_nw.start_with_spec(put_specs).await; - tokio::time::sleep(Duration::from_secs(5)).await; sim_nw.check_connectivity(Duration::from_secs(3)).await?; // trigger the put op @ gw-0 sim_nw - .trigger_event(&"gateway-0".into(), 1, Some(Duration::from_secs(3))) + .trigger_event("gateway-0", 1, Some(Duration::from_millis(100))) .await?; - assert!(sim_nw.has_put_contract(&"gateway-0".into(), &key, &new_value)); + assert!(sim_nw.has_put_contract("gateway-0", &key, &new_value)); assert!(sim_nw.event_listener.contract_broadcasted(&key)); Ok(()) } diff --git a/crates/core/src/operations/subscribe.rs b/crates/core/src/operations/subscribe.rs index 5dd708001..c7dc67cf6 100644 --- a/crates/core/src/operations/subscribe.rs +++ b/crates/core/src/operations/subscribe.rs @@ -146,13 +146,13 @@ impl Operation for SubscribeOp { }; if !op_storage.ring.is_contract_cached(&key) { - tracing::info!("Contract {} not found while processing info", key); - tracing::info!("Trying to found the contract from another node"); + tracing::info!(tx = %id, "Contract {} not found while processing info", key); + tracing::info!(tx = %id, "Trying to found the contract from another node"); let Some(new_target) = op_storage.ring.closest_caching(&key, &[sender.peer]) else { - tracing::warn!("no peer found while trying getting contract {key}"); + tracing::warn!(tx = %id, "No peer found while trying getting contract {key}"); return Err(OpError::RingError(RingError::NoCachingPeers(key))); }; let new_htl = htl + 1; @@ -187,9 +187,9 @@ impl Operation for SubscribeOp { match self.state { Some(SubscribeState::ReceivedRequest) => { tracing::info!( - "Peer {} successfully subscribed to contract {}", + tx = %id, + "Peer {} successfully subscribed to contract {key}", subscriber.peer, - key ); new_state = Some(SubscribeState::Completed); // TODO review behaviour, if the contract is not cached should return subscribed false? @@ -212,8 +212,8 @@ impl Operation for SubscribeOp { id, } => { tracing::warn!( - "Contract `{}` not found at potential subscription provider {}", - key, + tx = %id, + "Contract `{key}` not found at potential subscription provider {}", sender.peer ); // will error out in case it has reached max number of retries @@ -261,14 +261,10 @@ impl Operation for SubscribeOp { target: _, id: _, } => { - tracing::warn!( - "Subscribed to `{}` not found at potential subscription provider {}", - key, - sender.peer - ); + tracing::warn!("Subscribed to `{key}` at provider {}", sender.peer); op_storage.ring.add_subscription(key); - let _ = client_id; // todo: should inform back to the network event loop? + let _ = client_id; match self.state { Some(SubscribeState::AwaitingResponse { .. }) => { @@ -465,9 +461,9 @@ mod test { use super::*; use crate::node::tests::{NodeSpecification, SimNetwork}; - #[ignore] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn successful_subscribe_op_between_nodes() -> Result<(), anyhow::Error> { + crate::config::set_logger(); const NUM_NODES: usize = 4usize; const NUM_GW: usize = 1usize; @@ -483,13 +479,6 @@ mod test { } .into(); let first_node = NodeSpecification { - owned_contracts: Vec::new(), - non_owned_contracts: vec![contract_key], - events_to_generate: HashMap::from_iter([(1, event)]), - contract_subscribers: HashMap::new(), - }; - - let second_node = NodeSpecification { owned_contracts: vec![( ContractContainer::Wasm(ContractWasmAPIVersion::V1(contract)), contract_val, @@ -498,6 +487,12 @@ mod test { events_to_generate: HashMap::new(), contract_subscribers: HashMap::new(), }; + let second_node = NodeSpecification { + owned_contracts: Vec::new(), + non_owned_contracts: vec![contract_key.clone()], + events_to_generate: HashMap::from_iter([(1, event)]), + contract_subscribers: HashMap::new(), + }; let subscribe_specs = HashMap::from_iter([ ("node-0".into(), first_node), @@ -507,15 +502,21 @@ mod test { "successful_subscribe_op_between_nodes", NUM_GW, NUM_NODES, + 4, 3, - 2, 4, 2, ) .await; + sim_nw.with_start_backoff(Duration::from_millis(150)); sim_nw.start_with_spec(subscribe_specs).await; sim_nw.check_connectivity(Duration::from_secs(3)).await?; - + sim_nw + .trigger_event("node-1", 1, Some(Duration::from_millis(50))) + .await?; + assert!(sim_nw.has_got_contract("node-1", &contract_key)); + tokio::time::sleep(Duration::from_secs(2)).await; + assert!(sim_nw.is_subscribed_to_contract("node-1", &contract_key)); Ok(()) } } diff --git a/crates/core/src/runtime/mod.rs b/crates/core/src/runtime.rs similarity index 100% rename from crates/core/src/runtime/mod.rs rename to crates/core/src/runtime.rs diff --git a/crates/core/src/server/mod.rs b/crates/core/src/server.rs similarity index 100% rename from crates/core/src/server/mod.rs rename to crates/core/src/server.rs diff --git a/crates/core/src/server/errors.rs b/crates/core/src/server/errors.rs index c63cfc2f4..f62c79467 100644 --- a/crates/core/src/server/errors.rs +++ b/crates/core/src/server/errors.rs @@ -1,6 +1,7 @@ use axum::http::StatusCode; use axum::response::{Html, IntoResponse, Response}; use freenet_stdlib::client_api::ErrorKind; +use freenet_stdlib::prelude::ContractKey; use std::fmt::{Display, Formatter}; #[derive(Debug)] @@ -15,14 +16,18 @@ pub(super) enum WebSocketApiError { AxumError { error: ErrorKind, }, + MissingContract { + key: ContractKey, + }, } impl WebSocketApiError { pub fn status_code(&self) -> StatusCode { match self { WebSocketApiError::InvalidParam { .. } => StatusCode::BAD_REQUEST, - WebSocketApiError::NodeError { .. } => StatusCode::BAD_GATEWAY, + WebSocketApiError::NodeError { .. } => StatusCode::INTERNAL_SERVER_ERROR, WebSocketApiError::AxumError { .. } => StatusCode::INTERNAL_SERVER_ERROR, + WebSocketApiError::MissingContract { .. } => StatusCode::NOT_FOUND, } } @@ -32,7 +37,8 @@ impl WebSocketApiError { format!("Invalid request params: {}", error_cause) } WebSocketApiError::NodeError { error_cause } => format!("Node error: {}", error_cause), - WebSocketApiError::AxumError { error } => format!("Axum error: {}", error), + WebSocketApiError::AxumError { error } => format!("Server error: {}", error), + WebSocketApiError::MissingContract { key } => format!("Missing contract {key}"), } } } @@ -61,7 +67,12 @@ impl IntoResponse for WebSocketApiError { { (StatusCode::NOT_FOUND, error_cause) } - WebSocketApiError::NodeError { error_cause } => (StatusCode::BAD_GATEWAY, error_cause), + WebSocketApiError::NodeError { error_cause } => { + (StatusCode::INTERNAL_SERVER_ERROR, error_cause) + } + err @ WebSocketApiError::MissingContract { .. } => { + (StatusCode::NOT_FOUND, err.error_message()) + } WebSocketApiError::AxumError { error } => { (StatusCode::INTERNAL_SERVER_ERROR, format!("{error}")) } diff --git a/crates/core/src/server/path_handlers.rs b/crates/core/src/server/path_handlers.rs index 02b341d7c..a7184b392 100644 --- a/crates/core/src/server/path_handlers.rs +++ b/crates/core/src/server/path_handlers.rs @@ -45,7 +45,9 @@ pub(super) async fn contract_home( let client_id = if let Some(HostCallbackResult::NewId { id }) = response_recv.recv().await { id } else { - todo!("this is an error"); + return Err(WebSocketApiError::NodeError { + error_cause: "Couldn't register new client in the node".into(), + }); }; request_sender .send(ClientConnection::Request { @@ -120,7 +122,7 @@ pub(super) async fn contract_home( web_body } None => { - todo!("error indicating the contract is not present"); + return Err(WebSocketApiError::MissingContract { key }); } }, Some(HostCallbackResult::Result { From d12c01b93b6fa61a083b203234e5887f9cf64198 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Fri, 20 Oct 2023 13:55:31 +0200 Subject: [PATCH 63/76] Fix issue of not caching contract after get --- crates/core/src/contract.rs | 6 ++- crates/core/src/node.rs | 4 +- crates/core/src/node/tests.rs | 2 +- crates/core/src/operations/get.rs | 57 +++++++++++++++++++------ crates/core/src/operations/put.rs | 2 +- crates/core/src/operations/subscribe.rs | 7 ++- 6 files changed, 55 insertions(+), 23 deletions(-) diff --git a/crates/core/src/contract.rs b/crates/core/src/contract.rs index 1b621dfd8..a8815f433 100644 --- a/crates/core/src/contract.rs +++ b/crates/core/src/contract.rs @@ -30,7 +30,7 @@ where { loop { let (id, event) = contract_handler.channel().recv_from_event_loop().await?; - tracing::debug!(%event, "got contract handling event"); + tracing::debug!(%event, "Got contract handling event"); match event { ContractHandlerEvent::GetQuery { key, @@ -42,6 +42,7 @@ where .await { Ok((state, contract)) => { + tracing::debug!("Fetched contract {key}"); contract_handler .channel() .send_to_event_loop( @@ -57,7 +58,7 @@ where .await?; } Err(err) => { - tracing::warn!("error while executing get contract query: {err}"); + tracing::warn!("Error while executing get contract query: {err}"); contract_handler .channel() .send_to_event_loop( @@ -126,4 +127,5 @@ pub(crate) enum ContractError { IOError(#[from] std::io::Error), #[error("no response received from handler")] NoEvHandlerResponse, + } diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index da34531b6..cafa2f7a6 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -413,7 +413,8 @@ async fn process_open_request(request: OpenRequest<'static>, op_storage: Arc { - tokio::time::sleep(Duration::from_secs(1)).await + tracing::warn!("Still waiting for {key} contract"); + tokio::time::sleep(Duration::from_secs(2)).await } Err(err) => { tracing::error!("{}", err); @@ -425,6 +426,7 @@ async fn process_open_request(request: OpenRequest<'static>, op_storage: Arc = expected .difference(&connected) diff --git a/crates/core/src/operations/get.rs b/crates/core/src/operations/get.rs index 8c3cf4d70..de16f53e3 100644 --- a/crates/core/src/operations/get.rs +++ b/crates/core/src/operations/get.rs @@ -29,7 +29,7 @@ const MAX_GET_RETRY_HOPS: usize = 1; pub(crate) struct GetOp { id: Transaction, state: Option, - result: Option, + pub(super) result: Option, stats: Option, _ttl: Duration, } @@ -86,10 +86,7 @@ impl GetOp { } pub(super) fn finalized(&self) -> bool { - self.stats - .as_ref() - .map(|s| s.transfer_time.is_some()) - .unwrap_or(false) + self.result.is_some() } pub(super) fn record_transfer(&mut self) { @@ -212,11 +209,13 @@ impl Operation for GetOp { first_response_time: None, step: Default::default(), }); + let own_loc = op_storage.ring.own_location(); return_msg = Some(GetMsg::SeekNode { key, id, target, - sender: op_storage.ring.own_location(), + skip_list: vec![own_loc.peer], + sender: own_loc, fetch_contract, htl: MAX_GET_RETRY_HOPS, }); @@ -227,12 +226,14 @@ impl Operation for GetOp { fetch_contract, sender, target, + mut skip_list, htl, } => { let is_cached_contract = op_storage.ring.is_contract_cached(&key); if let Some(s) = stats.as_mut() { s.caching_peer = Some(target); } + skip_list.push(target.peer); if !is_cached_contract { tracing::warn!( @@ -261,6 +262,7 @@ impl Operation for GetOp { contract: None, }, sender: op_storage.ring.own_location(), + updated_skip_list: skip_list, target: sender, // return to requester }), None, @@ -270,13 +272,11 @@ impl Operation for GetOp { } let new_htl = htl - 1; - let Some(new_target) = - op_storage.ring.closest_caching(&key, &[sender.peer]) + let Some(new_target) = op_storage.ring.closest_caching(&key, &skip_list) else { - tracing::warn!(tx = %id, "no peer found while trying getting contract {key}"); + tracing::warn!(tx = %id, "No other peers found while trying getting contract {key} @ {}", target.peer); return Err(OpError::RingError(RingError::NoCachingPeers(key))); }; - continue_seeking( conn_manager, &new_target, @@ -286,6 +286,7 @@ impl Operation for GetOp { fetch_contract, sender, target: new_target, + skip_list, htl: new_htl, }) .into(), @@ -344,6 +345,7 @@ impl Operation for GetOp { id, key, value, + updated_skip_list: vec![], sender: target, target: sender, }); @@ -364,7 +366,7 @@ impl Operation for GetOp { }, sender, target, - .. + updated_skip_list, } => { let this_loc = target; tracing::warn!( @@ -399,6 +401,7 @@ impl Operation for GetOp { sender: this_loc, fetch_contract, htl: MAX_GET_RETRY_HOPS, + skip_list: updated_skip_list, }); } else { return Err(RingError::NoCachingPeers(key).into()); @@ -427,6 +430,7 @@ impl Operation for GetOp { state: None, contract: None, }, + updated_skip_list, sender, target, }); @@ -444,7 +448,9 @@ impl Operation for GetOp { }, sender, target, + mut updated_skip_list, } => { + updated_skip_list.push(sender.peer); let require_contract = matches!( self.state, Some(GetState::AwaitingResponse { @@ -457,19 +463,28 @@ impl Operation for GetOp { if require_contract { if let Some(contract) = &contract { // store contract first - op_storage + let res = op_storage .notify_contract_handler( ContractHandlerEvent::Cache(contract.clone()), client_id, ) .await?; + match res { + ContractHandlerEvent::CacheResult(Ok(_)) => { + op_storage.ring.contract_cached(&key); + } + ContractHandlerEvent::CacheResult(Err(err)) => { + return Err(OpError::ContractError(err)); + } + _ => unreachable!(), + } let key = contract.key(); tracing::debug!(tx = %id, "Contract `{}` successfully cached", key); } else { // no contract, consider this like an error ignoring the incoming update value tracing::warn!( tx = %id, - "Contract not received from peer {} while requested", + "Contract not received from peer {} while required", sender.peer ); @@ -492,6 +507,7 @@ impl Operation for GetOp { }, sender, target, + updated_skip_list, }), OpEnum::Get(op), None, @@ -502,7 +518,7 @@ impl Operation for GetOp { } let parameters = contract.as_ref().map(|c| c.params()); - op_storage + let res = op_storage .notify_contract_handler( ContractHandlerEvent::PutQuery { key: key.clone(), @@ -513,6 +529,15 @@ impl Operation for GetOp { client_id, ) .await?; + match res { + ContractHandlerEvent::PutResponse { new_value: Ok(_) } => {} + ContractHandlerEvent::PutResponse { + new_value: Err(err), + } => { + return Err(OpError::ExecutorError(err)); + } + _ => unreachable!(), + } match self.state { Some(GetState::AwaitingResponse { fetch_contract, .. }) => { @@ -549,6 +574,7 @@ impl Operation for GetOp { }, sender, target, + updated_skip_list, }); } _ => return Err(OpError::InvalidStateTransition(self.id)), @@ -763,6 +789,8 @@ mod messages { target: PeerKeyLocation, sender: PeerKeyLocation, htl: usize, + // FIXME: remove skip list once we deduplicate at top msg handling level + skip_list: Vec, }, ReturnGet { id: Transaction, @@ -770,6 +798,7 @@ mod messages { value: StoreResponse, sender: PeerKeyLocation, target: PeerKeyLocation, + updated_skip_list: Vec, }, } diff --git a/crates/core/src/operations/put.rs b/crates/core/src/operations/put.rs index 11bd6bbf9..d3e216453 100644 --- a/crates/core/src/operations/put.rs +++ b/crates/core/src/operations/put.rs @@ -535,7 +535,7 @@ fn build_op_result( }) } -async fn try_to_cache_contract<'a>( +pub(super) async fn try_to_cache_contract<'a>( op_storage: &'a OpManager, contract: &ContractContainer, key: &ContractKey, diff --git a/crates/core/src/operations/subscribe.rs b/crates/core/src/operations/subscribe.rs index c7dc67cf6..114d64fbf 100644 --- a/crates/core/src/operations/subscribe.rs +++ b/crates/core/src/operations/subscribe.rs @@ -504,18 +504,17 @@ mod test { NUM_NODES, 4, 3, - 4, + 5, 2, ) .await; - sim_nw.with_start_backoff(Duration::from_millis(150)); sim_nw.start_with_spec(subscribe_specs).await; sim_nw.check_connectivity(Duration::from_secs(3)).await?; sim_nw - .trigger_event("node-1", 1, Some(Duration::from_millis(50))) + .trigger_event("node-1", 1, Some(Duration::from_secs(2))) .await?; assert!(sim_nw.has_got_contract("node-1", &contract_key)); - tokio::time::sleep(Duration::from_secs(2)).await; + tokio::time::sleep(Duration::from_secs(3)).await; assert!(sim_nw.is_subscribed_to_contract("node-1", &contract_key)); Ok(()) } From b566f52572a483815a6b6fa7cfe16ed410cc0ea0 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Mon, 23 Oct 2023 09:14:20 +0200 Subject: [PATCH 64/76] Pass subscribe op test --- crates/core/src/contract.rs | 6 +-- crates/core/src/contract/executor.rs | 1 + crates/core/src/contract/storages/sqlite.rs | 2 +- crates/core/src/node.rs | 24 ++++++++---- crates/core/src/operations/connect.rs | 2 +- crates/core/src/operations/get.rs | 9 ++--- crates/core/src/operations/subscribe.rs | 41 ++++++++++++++------- crates/core/src/runtime/error.rs | 6 +-- crates/core/src/runtime/secrets_store.rs | 2 +- crates/core/src/server/app_packaging.rs | 2 +- 10 files changed, 58 insertions(+), 37 deletions(-) diff --git a/crates/core/src/contract.rs b/crates/core/src/contract.rs index a8815f433..3a69540b4 100644 --- a/crates/core/src/contract.rs +++ b/crates/core/src/contract.rs @@ -81,6 +81,7 @@ where .await?; } Err(err) => { + tracing::error!("Error while caching: {err}"); let err = ContractError::ContractRuntimeError(err); contract_handler .channel() @@ -121,11 +122,10 @@ pub(crate) enum ContractError { ChannelDropped(Box), #[error("contract {0} not found in storage")] ContractNotFound(ContractKey), - #[error("")] - ContractRuntimeError(ContractRtError), #[error(transparent)] + ContractRuntimeError(ContractRtError), + #[error("{0}")] IOError(#[from] std::io::Error), #[error("no response received from handler")] NoEvHandlerResponse, - } diff --git a/crates/core/src/contract/executor.rs b/crates/core/src/contract/executor.rs index 864a6df18..58319f760 100644 --- a/crates/core/src/contract/executor.rs +++ b/crates/core/src/contract/executor.rs @@ -1191,6 +1191,7 @@ impl Executor { let tmp_path = std::env::temp_dir().join(format!("freenet-executor-{data_dir}")); let contracts_data_dir = tmp_path.join("contracts"); + std::fs::create_dir_all(&contracts_data_dir).expect("directory created"); let contract_store = ContractStore::new(contracts_data_dir, u16::MAX as i64)?; // uses inmemory SQLite diff --git a/crates/core/src/contract/storages/sqlite.rs b/crates/core/src/contract/storages/sqlite.rs index e70f3bc71..9d25f6c31 100644 --- a/crates/core/src/contract/storages/sqlite.rs +++ b/crates/core/src/contract/storages/sqlite.rs @@ -129,7 +129,7 @@ pub enum SqlDbError { SqliteError(#[from] sqlx::Error), #[error(transparent)] RuntimeError(#[from] ContractError), - #[error(transparent)] + #[error("{0}")] IOError(#[from] std::io::Error), #[error(transparent)] StateStore(#[from] StateStoreError), diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index cafa2f7a6..558b57839 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -18,6 +18,7 @@ use std::{ use either::Either; use freenet_stdlib::client_api::{ClientRequest, ContractRequest}; use libp2p::{identity, multiaddr::Protocol, Multiaddr, PeerId}; +use tracing::Instrument; #[cfg(test)] use self::in_memory_impl::NodeInMemory; @@ -30,7 +31,7 @@ use crate::{ ClientResponses, ClientResponsesSender, ContractError, ExecutorToEventLoopChannel, NetworkContractHandler, NetworkEventListenerHalve, OperationMode, }, - message::{InnerMessage, Message, Transaction, TransactionType}, + message::{Message, Transaction, TransactionType}, operations::{ connect::{self, ConnectMsg, ConnectOp}, get, put, subscribe, OpEnum, OpError, OpOutcome, @@ -426,7 +427,7 @@ async fn process_open_request(request: OpenRequest<'static>, op_storage: Arc, op_storage: Arc { tracing::debug!( @@ -515,8 +520,11 @@ async fn report_result( } } Ok(None) => {} + Err(OpError::OpNotPresent(tx)) => { + tracing::trace!(%tx, "Late message for finished transaction"); + } Err(err) => { - tracing::debug!("Finished transaction with error: {}", err) + tracing::debug!("Finished transaction with error: {err}") } } } @@ -541,7 +549,7 @@ async fn process_message( .await; match msg { Message::JoinRing(op) => { - log_handling_msg!("join", op.id(), op_storage); + // log_handling_msg!("join", op.id(), op_storage); let op_result = handle_op_request::( &op_storage, &mut conn_manager, @@ -559,7 +567,7 @@ async fn process_message( .await; } Message::Put(op) => { - log_handling_msg!("put", *op.id(), op_storage); + // log_handling_msg!("put", *op.id(), op_storage); let op_result = handle_op_request::( &op_storage, &mut conn_manager, @@ -577,7 +585,7 @@ async fn process_message( .await; } Message::Get(op) => { - log_handling_msg!("get", op.id(), op_storage); + // log_handling_msg!("get", op.id(), op_storage); let op_result = handle_op_request::( &op_storage, &mut conn_manager, @@ -595,7 +603,7 @@ async fn process_message( .await; } Message::Subscribe(op) => { - log_handling_msg!("subscribe", op.id(), op_storage); + // log_handling_msg!("subscribe", op.id(), op_storage); let op_result = handle_op_request::( &op_storage, &mut conn_manager, diff --git a/crates/core/src/operations/connect.rs b/crates/core/src/operations/connect.rs index 01962d34e..8f5c0543f 100644 --- a/crates/core/src/operations/connect.rs +++ b/crates/core/src/operations/connect.rs @@ -1111,7 +1111,7 @@ mod test { .await; // sim_nw.with_start_backoff(Duration::from_millis(100)); sim_nw.start().await; - sim_nw.check_connectivity(Duration::from_secs(5)).await?; + sim_nw.check_connectivity(Duration::from_secs(3)).await?; let some_forwarded = sim_nw .node_connectivity() .into_iter() diff --git a/crates/core/src/operations/get.rs b/crates/core/src/operations/get.rs index de16f53e3..39ec3c90e 100644 --- a/crates/core/src/operations/get.rs +++ b/crates/core/src/operations/get.rs @@ -534,6 +534,7 @@ impl Operation for GetOp { ContractHandlerEvent::PutResponse { new_value: Err(err), } => { + tracing::debug!(tx = %id, error = %err, "Failed put at executor"); return Err(OpError::ExecutorError(err)); } _ => unreachable!(), @@ -614,14 +615,12 @@ async fn continue_seeking( new_target: &PeerKeyLocation, retry_msg: Message, ) -> Result<(), OpError> { - tracing::info!( + tracing::debug!( tx = %retry_msg.id(), - "Retrying to get the contract from node @ {}", + "Forwarding get request to {}", new_target.peer ); - conn_manager.send(&new_target.peer, retry_msg).await?; - Ok(()) } @@ -1003,7 +1002,7 @@ mod test { 3, 2, 4, - 3, + 2, ) .await; sim_nw.start_with_spec(get_specs).await; diff --git a/crates/core/src/operations/subscribe.rs b/crates/core/src/operations/subscribe.rs index 114d64fbf..040c454c1 100644 --- a/crates/core/src/operations/subscribe.rs +++ b/crates/core/src/operations/subscribe.rs @@ -146,8 +146,7 @@ impl Operation for SubscribeOp { }; if !op_storage.ring.is_contract_cached(&key) { - tracing::info!(tx = %id, "Contract {} not found while processing info", key); - tracing::info!(tx = %id, "Trying to found the contract from another node"); + tracing::debug!(tx = %id, "Contract {} not found at {}, trying other peer", key, target.peer); let Some(new_target) = op_storage.ring.closest_caching(&key, &[sender.peer]) @@ -164,6 +163,7 @@ impl Operation for SubscribeOp { let mut new_skip_list = skip_list.clone(); new_skip_list.push(target.peer); + tracing::debug!(tx = %id, "Forward request to peer: {}", new_target.peer); // Retry seek node when the contract to subscribe has not been found in this node conn_manager .send( @@ -186,7 +186,7 @@ impl Operation for SubscribeOp { match self.state { Some(SubscribeState::ReceivedRequest) => { - tracing::info!( + tracing::debug!( tx = %id, "Peer {} successfully subscribed to contract {key}", subscriber.peer, @@ -258,20 +258,34 @@ impl Operation for SubscribeOp { subscribed: true, key, sender, - target: _, - id: _, + id, + target, + .. } => { - tracing::warn!("Subscribed to `{key}` at provider {}", sender.peer); - op_storage.ring.add_subscription(key); - // todo: should inform back to the network event loop? - let _ = client_id; - match self.state { Some(SubscribeState::AwaitingResponse { .. }) => { + tracing::debug!( + tx = %id, + target = ?target.peer, + this = ?op_storage.ring.own_location().peer, + "Subscribed to `{key}` at provider {}", sender.peer + ); + op_storage.ring.add_subscription(key); + // todo: should inform back to the network event loop? + let _ = client_id; new_state = None; return_msg = None; } - _ => return Err(OpError::InvalidStateTransition(self.id)), + state => { + tracing::error!( + tx = %id, + ?state, + target = ?target.peer, + this = ?op_storage.ring.own_location().peer, + "wrong state" + ); + return Err(OpError::InvalidStateTransition(self.id)); + } } } _ => return Err(OpError::UnexpectedOpState), @@ -288,9 +302,9 @@ fn build_op_result( msg: Option, ttl: Duration, ) -> Result { - let output_op = Some(SubscribeOp { + let output_op = state.map(|state| SubscribeOp { id, - state, + state: Some(state), _ttl: ttl, }); Ok(OperationResult { @@ -309,6 +323,7 @@ pub(crate) fn start_op(key: ContractKey) -> SubscribeOp { } } +#[derive(Debug)] enum SubscribeState { /// Prepare the request to subscribe. PrepareRequest { diff --git a/crates/core/src/runtime/error.rs b/crates/core/src/runtime/error.rs index a18b84064..7e855304b 100644 --- a/crates/core/src/runtime/error.rs +++ b/crates/core/src/runtime/error.rs @@ -8,7 +8,7 @@ use super::{delegate, secrets_store, wasm_runtime, DelegateExecError}; pub type RuntimeResult = std::result::Result; -#[derive(Debug)] +#[derive(thiserror::Error, Debug)] pub struct ContractError(Box); impl ContractError { @@ -65,8 +65,6 @@ impl Display for ContractError { } } -impl std::error::Error for ContractError {} - impl From for ContractError { fn from(err: RuntimeInnerError) -> Self { Self(Box::new(err)) @@ -104,7 +102,7 @@ pub(crate) enum RuntimeInnerError { #[error(transparent)] BufferError(#[from] freenet_stdlib::memory::buf::Error), - #[error(transparent)] + #[error("{0}")] IOError(#[from] std::io::Error), #[error(transparent)] diff --git a/crates/core/src/runtime/secrets_store.rs b/crates/core/src/runtime/secrets_store.rs index 486312892..092a7057d 100644 --- a/crates/core/src/runtime/secrets_store.rs +++ b/crates/core/src/runtime/secrets_store.rs @@ -54,7 +54,7 @@ impl StoreEntriesContainer for KeyToEncryptionMap { pub enum SecretStoreError { #[error("encryption error: {0}")] Encryption(EncryptionError), - #[error(transparent)] + #[error("{0}")] IO(#[from] std::io::Error), #[error("missing cipher")] MissingCipher, diff --git a/crates/core/src/server/app_packaging.rs b/crates/core/src/server/app_packaging.rs index ce241ca4b..5fa5744a0 100644 --- a/crates/core/src/server/app_packaging.rs +++ b/crates/core/src/server/app_packaging.rs @@ -12,7 +12,7 @@ use xz2::read::{XzDecoder, XzEncoder}; pub enum WebContractError { #[error("unpacking error: {0}")] UnpackingError(Box), - #[error(transparent)] + #[error("{0}")] StoringError(std::io::Error), #[error("file not found: {0}")] FileNotFound(String), From b254243f7b8b6d50addd7a8130748630d07f9eb2 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Tue, 24 Oct 2023 10:39:45 +0200 Subject: [PATCH 65/76] Handle concurrent same tx messages gracefully --- crates/core/src/message.rs | 20 +- crates/core/src/node.rs | 188 ++++++++++-------- .../core/src/node/conn_manager/p2p_protoc.rs | 8 +- crates/core/src/node/event_log.rs | 4 +- crates/core/src/node/in_memory_impl.rs | 2 +- crates/core/src/node/op_state_manager.rs | 46 +++-- crates/core/src/operations.rs | 8 +- crates/core/src/operations/connect.rs | 116 ++++++----- crates/core/src/operations/get.rs | 89 +++++---- crates/core/src/operations/op_trait.rs | 2 +- crates/core/src/operations/put.rs | 129 ++++++------ crates/core/src/operations/subscribe.rs | 81 ++++---- crates/core/src/operations/update.rs | 4 +- 13 files changed, 389 insertions(+), 308 deletions(-) diff --git a/crates/core/src/message.rs b/crates/core/src/message.rs index ef8046198..b35c821f3 100644 --- a/crates/core/src/message.rs +++ b/crates/core/src/message.rs @@ -86,7 +86,7 @@ mod sealed_msg_type { #[repr(u8)] #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)] pub(crate) enum TransactionType { - JoinRing, + Connect, Put, Get, Subscribe, @@ -97,7 +97,7 @@ mod sealed_msg_type { impl Display for TransactionType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - TransactionType::JoinRing => write!(f, "join ring"), + TransactionType::Connect => write!(f, "join ring"), TransactionType::Put => write!(f, "put"), TransactionType::Get => write!(f, "get"), TransactionType::Subscribe => write!(f, "subscribe"), @@ -126,7 +126,7 @@ mod sealed_msg_type { } transaction_type_enumeration!(decl struct { - JoinRing -> ConnectMsg, + Connect -> ConnectMsg, Put -> PutMsg, Get -> GetMsg, Subscribe -> SubscribeMsg, @@ -134,9 +134,9 @@ mod sealed_msg_type { }); } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize)] pub(crate) enum Message { - JoinRing(ConnectMsg), + Connect(ConnectMsg), Put(PutMsg), Get(GetMsg), Subscribe(SubscribeMsg), @@ -189,7 +189,7 @@ impl Message { pub fn id(&self) -> &Transaction { use Message::*; match self { - JoinRing(op) => op.id(), + Connect(op) => op.id(), Put(op) => op.id(), Get(op) => op.id(), Subscribe(op) => op.id(), @@ -201,7 +201,7 @@ impl Message { pub fn target(&self) -> Option<&PeerKeyLocation> { use Message::*; match self { - JoinRing(op) => op.target(), + Connect(op) => op.target(), Put(op) => op.target(), Get(op) => op.target(), Subscribe(op) => op.target(), @@ -214,7 +214,7 @@ impl Message { pub fn terminal(&self) -> bool { use Message::*; match self { - JoinRing(op) => op.terminal(), + Connect(op) => op.terminal(), Put(op) => op.terminal(), Get(op) => op.terminal(), Subscribe(op) => op.terminal(), @@ -225,7 +225,7 @@ impl Message { pub fn track_stats(&self) -> bool { use Message::*; - !matches!(self, JoinRing(_) | Subscribe(_) | Aborted(_)) + !matches!(self, Connect(_) | Subscribe(_) | Aborted(_)) } } @@ -234,7 +234,7 @@ impl Display for Message { use Message::*; write!(f, "Message {{")?; match self { - JoinRing(msg) => msg.fmt(f)?, + Connect(msg) => msg.fmt(f)?, Put(msg) => msg.fmt(f)?, Get(msg) => msg.fmt(f)?, Subscribe(msg) => msg.fmt(f)?, diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index 558b57839..6dd9b3cb5 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -44,7 +44,7 @@ use crate::{ use crate::operations::handle_op_request; pub(crate) use conn_manager::{ConnectionBridge, ConnectionError}; pub(crate) use event_log::{EventLogRegister, EventRegister}; -pub(crate) use op_state_manager::OpManager; +pub(crate) use op_state_manager::{OpManager, OpNotAvailable}; mod conn_manager; mod event_log; @@ -462,6 +462,7 @@ macro_rules! log_handling_msg { } async fn report_result( + tx: Option, op_result: Result, OpError>, op_storage: &OpManager, executor_callback: Option>, @@ -520,15 +521,30 @@ async fn report_result( } } Ok(None) => {} - Err(OpError::OpNotPresent(tx)) => { - tracing::trace!(%tx, "Late message for finished transaction"); - } Err(err) => { + // just mark the operation as completed so no redundant messages are processed for this transaction anymore + if let Some(tx) = tx { + op_storage.completed(tx); + } tracing::debug!("Finished transaction with error: {err}") } } } +macro_rules! handle_op_not_available { + ($op_result:ident) => { + if let Err(OpError::OpNotAvailable(state)) = &$op_result { + match state { + OpNotAvailable::Running => { + tokio::time::sleep(Duration::from_micros(1_000)).await; + continue; + } + OpNotAvailable::Completed => return, + } + } + }; +} + #[tracing::instrument(name = "process_network_message", skip_all)] async fn process_message( msg: Result, @@ -544,87 +560,99 @@ async fn process_message( let cli_req = client_id.zip(client_req_handler_callback); match msg { Ok(msg) => { + let tx = Some(*msg.id()); event_listener .register_events(EventLog::from_inbound_msg(&msg, &op_storage)) .await; - match msg { - Message::JoinRing(op) => { - // log_handling_msg!("join", op.id(), op_storage); - let op_result = handle_op_request::( - &op_storage, - &mut conn_manager, - op, - client_id, - ) - .await; - report_result( - op_result, - &op_storage, - executor_callback, - cli_req, - &mut event_listener, - ) - .await; - } - Message::Put(op) => { - // log_handling_msg!("put", *op.id(), op_storage); - let op_result = handle_op_request::( - &op_storage, - &mut conn_manager, - op, - client_id, - ) - .await; - report_result( - op_result, - &op_storage, - executor_callback, - cli_req, - &mut event_listener, - ) - .await; - } - Message::Get(op) => { - // log_handling_msg!("get", op.id(), op_storage); - let op_result = handle_op_request::( - &op_storage, - &mut conn_manager, - op, - client_id, - ) - .await; - report_result( - op_result, - &op_storage, - executor_callback, - cli_req, - &mut event_listener, - ) - .await; - } - Message::Subscribe(op) => { - // log_handling_msg!("subscribe", op.id(), op_storage); - let op_result = handle_op_request::( - &op_storage, - &mut conn_manager, - op, - client_id, - ) - .await; - report_result( - op_result, - &op_storage, - executor_callback, - cli_req, - &mut event_listener, - ) - .await; + loop { + match &msg { + Message::Connect(op) => { + // log_handling_msg!("join", op.id(), op_storage); + let op_result = handle_op_request::( + &op_storage, + &mut conn_manager, + op, + client_id, + ) + .await; + handle_op_not_available!(op_result); + break report_result( + tx, + op_result, + &op_storage, + executor_callback, + cli_req, + &mut event_listener, + ) + .await; + } + Message::Put(op) => { + // log_handling_msg!("put", *op.id(), op_storage); + let op_result = handle_op_request::( + &op_storage, + &mut conn_manager, + op, + client_id, + ) + .await; + handle_op_not_available!(op_result); + break report_result( + tx, + op_result, + &op_storage, + executor_callback, + cli_req, + &mut event_listener, + ) + .await; + } + Message::Get(op) => { + // log_handling_msg!("get", op.id(), op_storage); + let op_result = handle_op_request::( + &op_storage, + &mut conn_manager, + op, + client_id, + ) + .await; + handle_op_not_available!(op_result); + break report_result( + tx, + op_result, + &op_storage, + executor_callback, + cli_req, + &mut event_listener, + ) + .await; + } + Message::Subscribe(op) => { + // log_handling_msg!("subscribe", op.id(), op_storage); + let op_result = handle_op_request::( + &op_storage, + &mut conn_manager, + op, + client_id, + ) + .await; + handle_op_not_available!(op_result); + break report_result( + tx, + op_result, + &op_storage, + executor_callback, + cli_req, + &mut event_listener, + ) + .await; + } + _ => break, } - _ => {} } } Err(err) => { report_result( + None, Err(err.into()), &op_storage, executor_callback, @@ -645,11 +673,11 @@ async fn handle_cancelled_op( where CM: ConnectionBridge + Send + Sync, { - if let TransactionType::JoinRing = tx.tx_type() { + if let TransactionType::Connect = tx.tx_type() { // the attempt to join the network failed, this could be a fatal error since the node // is useless without connecting to the network, we will retry with exponential backoff match op_storage.pop(&tx) { - Some(OpEnum::Connect(op)) if op.has_backoff() => { + Ok(Some(OpEnum::Connect(op))) if op.has_backoff() => { let ConnectOp { gateway, backoff, .. } = *op; @@ -658,7 +686,7 @@ where join_ring_request(Some(backoff), peer_key, &gateway, op_storage, conn_manager) .await?; } - Some(OpEnum::Connect(_)) => { + Ok(Some(OpEnum::Connect(_))) => { return Err(OpError::MaxRetriesExceeded(tx, tx.tx_type())); } _ => {} diff --git a/crates/core/src/node/conn_manager/p2p_protoc.rs b/crates/core/src/node/conn_manager/p2p_protoc.rs index f8acf8202..d2c3f49ad 100644 --- a/crates/core/src/node/conn_manager/p2p_protoc.rs +++ b/crates/core/src/node/conn_manager/p2p_protoc.rs @@ -435,7 +435,7 @@ impl P2pConnManager { .await; match res { Err(OpError::MaxRetriesExceeded(_, _)) - if tx_type == TransactionType::JoinRing + if tx_type == TransactionType::Connect && self.public_addr.is_none() /* FIXME: this should be not a gateway instead */ => { tracing::warn!("Retrying joining the ring with an other peer"); @@ -907,7 +907,7 @@ impl ConnectionHandler for Handler { Left(msg) => { let op_id = msg.id(); if msg.track_stats() { - if let Some(mut op) = self.op_manager.pop(op_id) { + if let Ok(Some(mut op)) = self.op_manager.pop(op_id) { op.record_transfer(); let _ = self.op_manager.push(*op_id, op); } @@ -952,7 +952,7 @@ impl ConnectionHandler for Handler { } => match Sink::poll_flush(Pin::new(&mut substream), cx) { Poll::Ready(Ok(())) => { if let Some(op_id) = op_id { - if let Some(mut op) = self.op_manager.pop(&op_id) { + if let Ok(Some(mut op)) = self.op_manager.pop(&op_id) { op.record_transfer(); let _ = self.op_manager.push(op_id, op); } @@ -981,7 +981,7 @@ impl ConnectionHandler for Handler { } => match Stream::poll_next(Pin::new(&mut substream), cx) { Poll::Ready(Some(Ok(msg))) => { let op_id = msg.id(); - if let Some(mut op) = self.op_manager.pop(op_id) { + if let Ok(Some(mut op)) = self.op_manager.pop(op_id) { op.record_transfer(); let _ = self.op_manager.push(*op_id, op); } diff --git a/crates/core/src/node/event_log.rs b/crates/core/src/node/event_log.rs index b759ab921..b2bbd9b96 100644 --- a/crates/core/src/node/event_log.rs +++ b/crates/core/src/node/event_log.rs @@ -72,7 +72,7 @@ impl<'a> EventLog<'a> { op_storage: &'a OpManager, ) -> Either> { let kind = match msg { - Message::JoinRing(connect::ConnectMsg::Response { + Message::Connect(connect::ConnectMsg::Response { msg: connect::ConnectResponse::AcceptedBy { peers, @@ -108,7 +108,7 @@ impl<'a> EventLog<'a> { op_storage: &'a OpManager, ) -> Either> { let kind = match msg { - Message::JoinRing(connect::ConnectMsg::Response { + Message::Connect(connect::ConnectMsg::Response { msg: connect::ConnectResponse::AcceptedBy { peers, diff --git a/crates/core/src/node/in_memory_impl.rs b/crates/core/src/node/in_memory_impl.rs index d24652742..af7303320 100644 --- a/crates/core/src/node/in_memory_impl.rs +++ b/crates/core/src/node/in_memory_impl.rs @@ -180,7 +180,7 @@ impl NodeInMemory { .await; match res { Err(OpError::MaxRetriesExceeded(_, _)) - if tx_type == TransactionType::JoinRing && !self.is_gateway => + if tx_type == TransactionType::Connect && !self.is_gateway => { tracing::warn!("Retrying joining the ring with an other peer"); if let Some(gateway) = self.gateways.iter().shuffle().next() { diff --git a/crates/core/src/node/op_state_manager.rs b/crates/core/src/node/op_state_manager.rs index 528833d81..24750589c 100644 --- a/crates/core/src/node/op_state_manager.rs +++ b/crates/core/src/node/op_state_manager.rs @@ -1,6 +1,6 @@ use std::{collections::BTreeMap, time::Instant}; -use dashmap::DashMap; +use dashmap::{DashMap, DashSet}; use either::Either; use parking_lot::RwLock; use tokio::sync::{mpsc::error::SendError, Mutex}; @@ -21,6 +21,14 @@ use crate::{ use super::{conn_manager::EventLoopNotificationsSender, PeerKey}; +#[derive(Debug, thiserror::Error)] +pub(crate) enum OpNotAvailable { + #[error("operation running")] + Running, + #[error("operation completed")] + Completed, +} + /// Thread safe and friendly data structure to maintain state of the different operations /// and enable their execution. pub(crate) struct OpManager { @@ -34,6 +42,9 @@ pub(crate) struct OpManager { ch_outbound: Mutex>, // FIXME: think of an optimal strategy to check for timeouts and clean up garbage _ops_ttl: RwLock>>, + // todo: improve this when the anti-write amplification functionality is added + completed: DashSet, + in_progress: DashSet, pub ring: Ring, } @@ -61,6 +72,8 @@ impl OpManager { ring, to_event_listener: notification_channel, ch_outbound: Mutex::new(contract_handler), + completed: DashSet::new(), + in_progress: DashSet::new(), _ops_ttl: RwLock::new(BTreeMap::new()), } } @@ -84,14 +97,6 @@ impl OpManager { .map_err(|err| SendError(err.0.unwrap_left())) } - // /// Send an internal message to this node event loop. - // pub async fn notify_internal_op(&self, msg: NodeEvent) -> Result<(), SendError> { - // self.to_event_listener - // .send(Either::Right(msg)) - // .await - // .map_err(|err| SendError(err.0.unwrap_right())) - // } - /// Send an event to the contract handler and await a response event from it if successful. pub async fn notify_contract_handler( &self, @@ -110,10 +115,11 @@ impl OpManager { } pub fn push(&self, id: Transaction, op: OpEnum) -> Result<(), OpError> { + self.in_progress.remove(&id); match op { OpEnum::Connect(op) => { #[cfg(debug_assertions)] - check_id_op!(id.tx_type(), TransactionType::JoinRing); + check_id_op!(id.tx_type(), TransactionType::Connect); self.join_ring.insert(id, *op); } OpEnum::Put(op) => { @@ -140,9 +146,15 @@ impl OpManager { Ok(()) } - pub fn pop(&self, id: &Transaction) -> Option { - match id.tx_type() { - TransactionType::JoinRing => self + pub fn pop(&self, id: &Transaction) -> Result, OpNotAvailable> { + if self.completed.contains(id) { + return Err(OpNotAvailable::Completed); + } + if self.in_progress.contains(id) { + return Err(OpNotAvailable::Running); + } + let op = match id.tx_type() { + TransactionType::Connect => self .join_ring .remove(id) .map(|(_k, v)| v) @@ -156,7 +168,13 @@ impl OpManager { .map(OpEnum::Subscribe), TransactionType::Update => self.update.remove(id).map(|(_k, v)| v).map(OpEnum::Update), TransactionType::Canceled => unreachable!(), - } + }; + self.in_progress.insert(*id); + Ok(op) + } + + pub fn completed(&self, id: Transaction) { + self.completed.insert(id); } pub fn prune_connection(&self, peer: PeerKey) { diff --git a/crates/core/src/operations.rs b/crates/core/src/operations.rs index cef8a097a..786af2f0e 100644 --- a/crates/core/src/operations.rs +++ b/crates/core/src/operations.rs @@ -7,7 +7,7 @@ use crate::{ client_events::{ClientId, HostResult}, contract::ContractError, message::{InnerMessage, Message, Transaction, TransactionType}, - node::{ConnectionBridge, ConnectionError, OpManager, PeerKey}, + node::{ConnectionBridge, ConnectionError, OpManager, OpNotAvailable, PeerKey}, ring::{Location, PeerKeyLocation, RingError}, DynError, }; @@ -34,7 +34,7 @@ pub(crate) struct OpInitialization { pub(crate) async fn handle_op_request( op_storage: &OpManager, conn_manager: &mut CB, - msg: Op::Message, + msg: &Op::Message, client_id: Option, ) -> Result, OpError> where @@ -44,7 +44,7 @@ where let sender; let tx = *msg.id(); let result = { - let OpInitialization { sender: s, op } = Op::load_or_init(op_storage, &msg)?; + let OpInitialization { sender: s, op } = Op::load_or_init(op_storage, msg)?; sender = s; op.process_message(conn_manager, op_storage, msg, client_id) .await @@ -201,6 +201,8 @@ pub(crate) enum OpError { OpNotPresent(Transaction), #[error("max number of retries for tx {0} of op type `{1}` reached")] MaxRetriesExceeded(Transaction, TransactionType), + #[error("op not available")] + OpNotAvailable(#[from] OpNotAvailable), // user for control flow /// This is used as an early interrumpt of an op update when an op diff --git a/crates/core/src/operations/connect.rs b/crates/core/src/operations/connect.rs index 8f5c0543f..68607460b 100644 --- a/crates/core/src/operations/connect.rs +++ b/crates/core/src/operations/connect.rs @@ -86,7 +86,7 @@ impl Operation for ConnectOp { let sender; let tx = *msg.id(); match op_storage.pop(msg.id()) { - Some(OpEnum::Connect(connect_op)) => { + Ok(Some(OpEnum::Connect(connect_op))) => { sender = msg.sender().cloned(); // was an existing operation, the other peer messaged back Ok(OpInitialization { @@ -94,8 +94,11 @@ impl Operation for ConnectOp { sender, }) } - Some(_) => Err(OpError::OpNotPresent(tx)), - None => { + Ok(Some(op)) => { + let _ = op_storage.push(tx, op); + Err(OpError::OpNotPresent(tx)) + } + Ok(None) => { // new request to join this node, initialize the machine Ok(OpInitialization { op: Self { @@ -108,6 +111,7 @@ impl Operation for ConnectOp { sender: None, }) } + Err(err) => Err(err.into()), } } @@ -119,7 +123,7 @@ impl Operation for ConnectOp { self, conn_manager: &'a mut CB, op_storage: &'a OpManager, - input: Self::Message, + input: &'a Self::Message, _client_id: Option, ) -> Pin> + Send + 'a>> { Box::pin(async move { @@ -152,7 +156,7 @@ impl Operation for ConnectOp { // FIXME: don't try to forward to peers which have already been tried (add a rejected_by list) let accepted_by = if op_storage.ring.should_accept(&new_location) { tracing::debug!(tx = %id, "Accepting connection from {}", joiner,); - HashSet::from_iter([this_node_loc]) + HashSet::from_iter([*this_node_loc]) } else { tracing::debug!(tx = %id, at_peer = %this_node_loc.peer, "Rejecting connection from peer {}", joiner); HashSet::new() @@ -160,15 +164,15 @@ impl Operation for ConnectOp { let new_peer_loc = PeerKeyLocation { location: Some(new_location), - peer: joiner, + peer: *joiner, }; if let Some(mut updated_state) = forward_conn( - id, + *id, &op_storage.ring, conn_manager, new_peer_loc, new_peer_loc, - hops_to_live, + *hops_to_live, accepted_by.len(), ) .await? @@ -192,18 +196,19 @@ impl Operation for ConnectOp { ); new_state = Some(ConnectState::OCReceived); } else { + op_storage.completed(*id); new_state = None } return_msg = Some(ConnectMsg::Response { - id, - sender: this_node_loc, + id: *id, + sender: *this_node_loc, msg: ConnectResponse::AcceptedBy { peers: accepted_by, your_location: new_location, - your_peer_id: joiner, + your_peer_id: *joiner, }, target: PeerKeyLocation { - peer: joiner, + peer: *joiner, location: Some(new_location), }, }); @@ -243,12 +248,12 @@ impl Operation for ConnectOp { }; if let Some(mut updated_state) = forward_conn( - id, + *id, &op_storage.ring, conn_manager, - sender, - joiner, - hops_to_live, + *sender, + *joiner, + *hops_to_live, accepted_by.len(), ) .await? @@ -260,12 +265,8 @@ impl Operation for ConnectOp { } else { match self.state { Some(ConnectState::Initializing) => { - let (state, msg) = try_proxy_connection( - &id, - &sender, - &own_loc, - accepted_by.clone(), - ); + let (state, msg) = + try_proxy_connection(id, sender, &own_loc, accepted_by.clone()); new_state = state; return_msg = msg; } @@ -294,9 +295,9 @@ impl Operation for ConnectOp { target.peer ); return_msg = Some(ConnectMsg::Response { - id, + id: *id, target, - sender, + sender: *sender, msg: ConnectResponse::AcceptedBy { peers: accepted_by, your_location: new_location, @@ -318,9 +319,9 @@ impl Operation for ConnectOp { own_loc.peer ); return_msg = Some(ConnectMsg::Response { - id, + id: *id, target, - sender, + sender: *sender, msg: ConnectResponse::Proxy { accepted_by }, }); } @@ -330,6 +331,7 @@ impl Operation for ConnectOp { if let Some(state) = new_state { if state.is_connected() { new_state = None; + op_storage.completed(*id); } else { new_state = Some(state); } @@ -351,8 +353,8 @@ impl Operation for ConnectOp { // Set the given location let pk_loc = PeerKeyLocation { - location: Some(your_location), - peer: your_peer_id, + location: Some(*your_location), + peer: *your_peer_id, }; // fixme: remove @@ -371,10 +373,10 @@ impl Operation for ConnectOp { ); new_state = Some(ConnectState::OCReceived); return_msg = Some(ConnectMsg::Response { - id, + id: *id, msg: ConnectResponse::ReceivedOC { by_peer: pk_loc }, sender: pk_loc, - target: sender, + target: *sender, }); tracing::debug!( tx = %id, @@ -382,17 +384,17 @@ impl Operation for ConnectOp { location = %your_location, "Updating assigned location" ); - op_storage.ring.update_location(Some(your_location)); + op_storage.ring.update_location(Some(*your_location)); for other_peer in accepted_by { let _ = propagate_oc_to_accepted_peers( conn_manager, op_storage, - sender, - &other_peer, + *sender, + other_peer, ConnectMsg::Response { - id, - target: other_peer, + id: *id, + target: *other_peer, sender: pk_loc, msg: ConnectResponse::ReceivedOC { by_peer: pk_loc }, }, @@ -407,7 +409,7 @@ impl Operation for ConnectOp { "Failed to establish any connections, aborting" ); let op = ConnectOp { - id, + id: *id, state: None, gateway: self.gateway, backoff: self.backoff, @@ -415,7 +417,7 @@ impl Operation for ConnectOp { }; op_storage .notify_op_change( - Message::Aborted(id), + Message::Aborted(*id), OpEnum::Connect(op.into()), None, ) @@ -427,14 +429,14 @@ impl Operation for ConnectOp { id, sender, target, - msg: ConnectResponse::Proxy { mut accepted_by }, + msg: ConnectResponse::Proxy { accepted_by }, } => { tracing::debug!(tx = %id, "Received proxy connect at @ {}", target.peer); match self.state { Some(ConnectState::Initializing) => { // the sender of the response is the target of the request and // is only a completed tx if it accepted the connection - if accepted_by.contains(&sender) { + if accepted_by.contains(sender) { tracing::debug!( tx = %id, "Return to {}, connected at proxy {}", @@ -445,12 +447,15 @@ impl Operation for ConnectOp { } else { tracing::debug!("Failed to connect at proxy {}", sender.peer); new_state = None; + op_storage.completed(*id); } return_msg = Some(ConnectMsg::Response { - msg: ConnectResponse::Proxy { accepted_by }, - sender, - id, - target, + msg: ConnectResponse::Proxy { + accepted_by: accepted_by.clone(), + }, + sender: *sender, + id: *id, + target: *target, }); } Some(ConnectState::AwaitingProxyResponse { @@ -465,7 +470,7 @@ impl Operation for ConnectOp { let is_target_peer = new_peer_id == original_target.peer; if is_accepted { - previously_accepted.extend(accepted_by.drain()); + previously_accepted.extend(accepted_by.iter().copied()); if is_target_peer { new_state = Some(ConnectState::OCReceived); } else { @@ -487,9 +492,9 @@ impl Operation for ConnectOp { original_target.peer ); return_msg = Some(ConnectMsg::Response { - id, + id: *id, target: original_target, - sender: target, + sender: *target, msg: ConnectResponse::AcceptedBy { peers: previously_accepted, your_location: new_location, @@ -506,9 +511,9 @@ impl Operation for ConnectOp { ); return_msg = Some(ConnectMsg::Response { - id, + id: *id, target: original_target, - sender: target, + sender: *target, msg: ConnectResponse::Proxy { accepted_by: previously_accepted, }, @@ -520,6 +525,7 @@ impl Operation for ConnectOp { if let Some(state) = new_state { if state.is_connected() { new_state = None; + op_storage.completed(*id); } else { new_state = Some(state) } @@ -536,16 +542,16 @@ impl Operation for ConnectOp { tracing::debug!(tx = %id, "Acknowledge connected at gateway"); new_state = Some(ConnectState::Connected); return_msg = Some(ConnectMsg::Connected { - id, - sender: target, - target: sender, + id: *id, + sender: *target, + target: *sender, }); } _ => return Err(OpError::InvalidStateTransition(self.id)), } if let Some(state) = new_state { if !state.is_connected() { - return Err(OpError::InvalidStateTransition(id)); + return Err(OpError::InvalidStateTransition(*id)); } else { conn_manager.add_connection(sender.peer).await?; op_storage.ring.add_connection( @@ -554,6 +560,7 @@ impl Operation for ConnectOp { ); tracing::debug!(tx = %id, "Opened connection with peer {}", by_peer.peer); new_state = None; + op_storage.completed(*id); } }; } @@ -568,7 +575,7 @@ impl Operation for ConnectOp { }; if let Some(state) = new_state { if !state.is_connected() { - return Err(OpError::InvalidStateTransition(id)); + return Err(OpError::InvalidStateTransition(*id)); } else { tracing::info!( tx = %id, @@ -582,6 +589,7 @@ impl Operation for ConnectOp { sender.peer, ); new_state = None; + op_storage.completed(*id); } }; } @@ -934,7 +942,7 @@ mod messages { use crate::message::InnerMessage; use serde::{Deserialize, Serialize}; - #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] + #[derive(Debug, Serialize, Deserialize)] pub(crate) enum ConnectMsg { Request { id: Transaction, diff --git a/crates/core/src/operations/get.rs b/crates/core/src/operations/get.rs index 39ec3c90e..2306a4ab7 100644 --- a/crates/core/src/operations/get.rs +++ b/crates/core/src/operations/get.rs @@ -147,13 +147,16 @@ impl Operation for GetOp { sender = Some(peer_key_loc.peer); }; let tx = *msg.id(); - let result = match op_storage.pop(msg.id()) { - Some(OpEnum::Get(get_op)) => { + match op_storage.pop(msg.id()) { + Ok(Some(OpEnum::Get(get_op))) => { Ok(OpInitialization { op: get_op, sender }) // was an existing operation, other peer messaged back } - Some(_) => return Err(OpError::OpNotPresent(tx)), - None => { + Ok(Some(op)) => { + let _ = op_storage.push(tx, op); + Err(OpError::OpNotPresent(tx)) + } + Ok(None) => { // new request to get a value for a contract, initialize the machine Ok(OpInitialization { op: Self { @@ -166,9 +169,8 @@ impl Operation for GetOp { sender, }) } - }; - - result + Err(err) => Err(err.into()), + } } fn id(&self) -> &Transaction { @@ -179,7 +181,7 @@ impl Operation for GetOp { self, conn_manager: &'a mut CB, op_storage: &'a OpManager, - input: Self::Message, + input: &'a Self::Message, client_id: Option, ) -> Pin> + Send + 'a>> { Box::pin(async move { @@ -203,7 +205,7 @@ impl Operation for GetOp { tracing::debug!(tx = %id, "Seek contract {} @ {}", key, target.peer); new_state = self.state; stats = Some(GetStats { - contract_location: Location::from(&key), + contract_location: Location::from(key), caching_peer: None, transfer_time: None, first_response_time: None, @@ -211,12 +213,12 @@ impl Operation for GetOp { }); let own_loc = op_storage.ring.own_location(); return_msg = Some(GetMsg::SeekNode { - key, - id, - target, + key: key.clone(), + id: *id, + target: *target, skip_list: vec![own_loc.peer], sender: own_loc, - fetch_contract, + fetch_contract: *fetch_contract, htl: MAX_GET_RETRY_HOPS, }); } @@ -226,13 +228,19 @@ impl Operation for GetOp { fetch_contract, sender, target, - mut skip_list, + skip_list, htl, } => { + let htl = *htl; + let id = *id; + let key: ContractKey = key.clone(); + let fetch_contract = *fetch_contract; + let is_cached_contract = op_storage.ring.is_contract_cached(&key); if let Some(s) = stats.as_mut() { - s.caching_peer = Some(target); + s.caching_peer = Some(*target); } + let mut skip_list = skip_list.clone(); skip_list.push(target.peer); if !is_cached_contract { @@ -263,7 +271,7 @@ impl Operation for GetOp { }, sender: op_storage.ring.own_location(), updated_skip_list: skip_list, - target: sender, // return to requester + target: *sender, // return to requester }), None, stats, @@ -284,7 +292,7 @@ impl Operation for GetOp { id, key, fetch_contract, - sender, + sender: *sender, target: new_target, skip_list, htl: new_htl, @@ -346,8 +354,8 @@ impl Operation for GetOp { key, value, updated_skip_list: vec![], - sender: target, - target: sender, + sender: *target, + target: *sender, }); } _ => return Err(OpError::InvalidStateTransition(self.id)), @@ -390,21 +398,21 @@ impl Operation for GetOp { skip_list.push(target.peer); if let Some(target) = op_storage .ring - .closest_caching(&key, skip_list.as_slice()) + .closest_caching(key, skip_list.as_slice()) .into_iter() .next() { return_msg = Some(GetMsg::SeekNode { - id, - key, + id: *id, + key: key.clone(), target, - sender: this_loc, + sender: *this_loc, fetch_contract, htl: MAX_GET_RETRY_HOPS, - skip_list: updated_skip_list, + skip_list: updated_skip_list.clone(), }); } else { - return Err(RingError::NoCachingPeers(key).into()); + return Err(RingError::NoCachingPeers(key.clone()).into()); } new_state = Some(GetState::AwaitingResponse { skip_list, @@ -417,22 +425,22 @@ impl Operation for GetOp { "Failed getting a value for contract {}, reached max retries", key ); - return Err(OpError::MaxRetriesExceeded(id, id.tx_type())); + return Err(OpError::MaxRetriesExceeded(*id, id.tx_type())); } } Some(GetState::ReceivedRequest) => { tracing::debug!(tx = %id, "Returning contract {} to {}", key, sender.peer); new_state = None; return_msg = Some(GetMsg::ReturnGet { - id, - key, + id: *id, + key: key.clone(), value: StoreResponse { state: None, contract: None, }, - updated_skip_list, - sender, - target, + updated_skip_list: updated_skip_list.clone(), + sender: *sender, + target: *target, }); } _ => return Err(OpError::InvalidStateTransition(self.id)), @@ -448,8 +456,11 @@ impl Operation for GetOp { }, sender, target, - mut updated_skip_list, + updated_skip_list, } => { + let id = *id; + let key = key.clone(); + let mut updated_skip_list = updated_skip_list.clone(); updated_skip_list.push(sender.peer); let require_contract = matches!( self.state, @@ -505,8 +516,8 @@ impl Operation for GetOp { state: None, contract: None, }, - sender, - target, + sender: *sender, + target: *target, updated_skip_list, }), OpEnum::Get(op), @@ -551,7 +562,7 @@ impl Operation for GetOp { return_msg = None; result = Some(GetResult { state: value.clone(), - contract, + contract: contract.clone(), }); } else { tracing::debug!(tx = %id, "Get response received for contract {}", key); @@ -559,7 +570,7 @@ impl Operation for GetOp { return_msg = None; result = Some(GetResult { state: value.clone(), - contract, + contract: contract.clone(), }); } } @@ -573,8 +584,8 @@ impl Operation for GetOp { state: None, contract: None, }, - sender, - target, + sender: *sender, + target: *target, updated_skip_list, }); } @@ -773,7 +784,7 @@ mod messages { use super::*; - #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] + #[derive(Debug, Serialize, Deserialize)] pub(crate) enum GetMsg { RequestGet { id: Transaction, diff --git a/crates/core/src/operations/op_trait.rs b/crates/core/src/operations/op_trait.rs index 646fec42b..3c906106f 100644 --- a/crates/core/src/operations/op_trait.rs +++ b/crates/core/src/operations/op_trait.rs @@ -31,7 +31,7 @@ where self, conn_manager: &'a mut CB, op_storage: &'a OpManager, - input: Self::Message, + input: &'a Self::Message, client_id: Option, ) -> Pin> + Send + 'a>>; } diff --git a/crates/core/src/operations/put.rs b/crates/core/src/operations/put.rs index d3e216453..314dac994 100644 --- a/crates/core/src/operations/put.rs +++ b/crates/core/src/operations/put.rs @@ -136,13 +136,16 @@ impl Operation for PutOp { }; let tx = *msg.id(); - let result = match op_storage.pop(msg.id()) { - Some(OpEnum::Put(put_op)) => { + match op_storage.pop(msg.id()) { + Ok(Some(OpEnum::Put(put_op))) => { // was an existing operation, the other peer messaged back Ok(OpInitialization { op: put_op, sender }) } - Some(_) => return Err(OpError::OpNotPresent(tx)), - None => { + Ok(Some(op)) => { + let _ = op_storage.push(tx, op); + Err(OpError::OpNotPresent(tx)) + } + Ok(None) => { // new request to put a new value for a contract, initialize the machine Ok(OpInitialization { op: Self { @@ -154,8 +157,8 @@ impl Operation for PutOp { sender, }) } - }; - result + Err(err) => Err(err.into()), + } } fn id(&self) -> &Transaction { @@ -166,7 +169,7 @@ impl Operation for PutOp { self, conn_manager: &'a mut CB, op_storage: &'a OpManager, - input: Self::Message, + input: &'a Self::Message, client_id: Option, ) -> Pin> + Send + 'a>> { Box::pin(async move { @@ -187,20 +190,20 @@ impl Operation for PutOp { let key = contract.key(); tracing::debug!( - "Performing a RequestPut for contract {} from {} to {}", + "Rquesting put for contract {} from {} to {}", key, sender.peer, target.peer ); return_msg = Some(PutMsg::SeekNode { - id, + id: *id, sender, - target, - value, - contract, - related_contracts, - htl, + target: *target, + value: value.clone(), + contract: contract.clone(), + related_contracts: related_contracts.clone(), + htl: *htl, skip_list: vec![sender.peer], }); @@ -215,15 +218,16 @@ impl Operation for PutOp { related_contracts, htl, target, - mut skip_list, + skip_list, } => { let key = contract.key(); let is_cached_contract = op_storage.ring.is_contract_cached(&key); tracing::debug!( - "Performing a SeekNode at {}, trying put the contract {}", + tx = %id, + "Puttting contract {} at target peer {}", + key, target.peer, - key ); if !is_cached_contract @@ -231,45 +235,48 @@ impl Operation for PutOp { .ring .within_caching_distance(&Location::from(&key)) { - tracing::debug!("Contract `{}` not cached @ peer {}", key, target.peer); - match try_to_cache_contract(op_storage, &contract, &key, client_id).await { + tracing::debug!(tx = %id, "Contract `{}` not cached @ peer {}", key, target.peer); + match try_to_cache_contract(op_storage, contract, &key, client_id).await { Ok(_) => {} Err(err) => return Err(err), } } else if !is_cached_contract { + // FIXME // in this case forward to a closer node to the target location and just wait for a response // to give back to requesting peer - // FIXME tracing::warn!( + tx = %id, "Contract {} not found while processing info, forwarding", key ); } // after the contract has been cached, push the update query - tracing::debug!("Attempting contract value update"); + tracing::debug!(tx = %id, "Attempting contract value update"); let parameters = contract.params(); let new_value = put_contract( op_storage, key.clone(), - value, - related_contracts, + value.clone(), + related_contracts.clone(), parameters, client_id, ) .await?; - tracing::debug!("Contract successfully updated"); + tracing::debug!(tx = %id, "Contract successfully updated"); // if the change was successful, communicate this back to the requestor and broadcast the change conn_manager .send( &sender.peer, (PutMsg::SuccessfulUpdate { - id, + id: *id, new_value: new_value.clone(), }) .into(), ) .await?; + + let mut skip_list = skip_list.clone(); skip_list.push(target.peer); if let Some(new_htl) = htl.checked_sub(1) { @@ -277,9 +284,9 @@ impl Operation for PutOp { forward_changes( op_storage, conn_manager, - &contract, + contract, new_value.clone(), - id, + *id, new_htl, skip_list.as_slice(), ) @@ -292,13 +299,14 @@ impl Operation for PutOp { .map(|i| i.value().to_vec()) .unwrap_or_default(); tracing::debug!( + tx = %id, "Successfully updated a value for contract {} @ {:?}", key, target.location ); match try_to_broadcast( - (id, client_id), + (*id, client_id), op_storage, self.state, broadcast_to, @@ -329,7 +337,7 @@ impl Operation for PutOp { let new_value = put_contract( op_storage, key.clone(), - new_value, + new_value.clone(), RelatedContracts::default(), parameters.clone(), client_id, @@ -339,12 +347,12 @@ impl Operation for PutOp { let broadcast_to = op_storage .ring - .subscribers_of(&key) + .subscribers_of(key) .map(|i| { // Avoid already broadcast nodes and sender from broadcasting let mut subscribers: Vec = i.value().to_vec(); let mut avoid_list: HashSet = - sender_subscribers.into_iter().map(|pl| pl.peer).collect(); + sender_subscribers.iter().map(|pl| pl.peer).collect(); avoid_list.insert(sender.peer); subscribers.retain(|s| !avoid_list.contains(&s.peer)); subscribers @@ -357,12 +365,12 @@ impl Operation for PutOp { ); match try_to_broadcast( - (id, client_id), + (*id, client_id), op_storage, self.state, broadcast_to, - key, - (parameters, new_value), + key.clone(), + (parameters.clone(), new_value), self._ttl, ) .await @@ -376,25 +384,30 @@ impl Operation for PutOp { } PutMsg::Broadcasting { id, - mut broadcast_to, - mut broadcasted_to, + broadcast_to, + broadcasted_to, key, new_value, parameters, } => { let sender = op_storage.ring.own_location(); - let msg = PutMsg::BroadcastTo { - id, - key: key.clone(), - new_value: new_value.clone(), - sender, - sender_subscribers: broadcast_to.clone(), - parameters, - }; + let mut broadcasted_to = *broadcasted_to; let mut broadcasting = Vec::with_capacity(broadcast_to.len()); - for peer in &broadcast_to { - let f = conn_manager.send(&peer.peer, msg.clone().into()); + let mut filtered_broadcast = broadcast_to + .iter() + .filter(|pk| pk.peer != sender.peer) + .collect::>(); + for peer in filtered_broadcast.iter() { + let msg = PutMsg::BroadcastTo { + id: *id, + key: key.clone(), + new_value: new_value.clone(), + sender, + sender_subscribers: broadcast_to.clone(), + parameters: parameters.clone(), + }; + let f = conn_manager.send(&peer.peer, msg.into()); broadcasting.push(f); } let error_futures = futures::future::join_all(broadcasting) @@ -413,7 +426,7 @@ impl Operation for PutOp { let mut incorrect_results = 0; for (peer_num, err) in error_futures { // remove the failed peers in reverse order - let peer = broadcast_to.remove(peer_num); + let peer = filtered_broadcast.remove(peer_num); tracing::warn!( "failed broadcasting put change to {} with error {}; dropping connection", peer.peer, @@ -429,13 +442,15 @@ impl Operation for PutOp { ); // Subscriber nodes have been notified of the change, the operation is completed + op_storage.completed(*id); return_msg = None; new_state = None; } - PutMsg::SuccessfulUpdate { .. } => { + PutMsg::SuccessfulUpdate { id, .. } => { match self.state { Some(PutState::AwaitingResponse { contract, .. }) => { tracing::debug!("Successfully updated value for {}", contract,); + op_storage.completed(*id); new_state = None; return_msg = None; } @@ -451,7 +466,7 @@ impl Operation for PutOp { contract, new_value, htl, - mut skip_list, + skip_list, } => { let key = contract.key(); let peer_loc = op_storage.ring.own_location(); @@ -467,7 +482,7 @@ impl Operation for PutOp { .ring .within_caching_distance(&Location::from(&key)); if !cached_contract && within_caching_dist { - match try_to_cache_contract(op_storage, &contract, &key, client_id).await { + match try_to_cache_contract(op_storage, contract, &key, client_id).await { Ok(_) => {} Err(err) => return Err(err), } @@ -482,14 +497,15 @@ impl Operation for PutOp { let new_value = put_contract( op_storage, key, - new_value, + new_value.clone(), RelatedContracts::default(), contract.params(), client_id, ) .await?; - //update skip list + // update skip list + let mut skip_list = skip_list.clone(); skip_list.push(peer_loc.peer); // if successful, forward to the next closest peers (if any) @@ -497,14 +513,15 @@ impl Operation for PutOp { forward_changes( op_storage, conn_manager, - &contract, + contract, new_value, - id, + *id, new_htl, skip_list.as_slice(), ) .await; } + op_storage.completed(*id); return_msg = None; new_state = None; } @@ -815,7 +832,7 @@ mod messages { use crate::message::InnerMessage; use serde::{Deserialize, Serialize}; - #[derive(Debug, Serialize, Deserialize, Clone)] + #[derive(Debug, Serialize, Deserialize)] pub(crate) enum PutMsg { /// Initialize the put operation by routing the value RouteValue { @@ -1031,7 +1048,7 @@ mod test { // trigger the put op @ gw-0 sim_nw - .trigger_event("gateway-0", 1, Some(Duration::from_millis(100))) + .trigger_event("gateway-0", 1, Some(Duration::from_millis(150))) .await?; assert!(sim_nw.has_put_contract("gateway-0", &key, &new_value)); assert!(sim_nw.event_listener.contract_broadcasted(&key)); diff --git a/crates/core/src/operations/subscribe.rs b/crates/core/src/operations/subscribe.rs index 040c454c1..38d6b08e5 100644 --- a/crates/core/src/operations/subscribe.rs +++ b/crates/core/src/operations/subscribe.rs @@ -66,16 +66,19 @@ impl Operation for SubscribeOp { }; let id = *msg.id(); - let result = match op_storage.pop(msg.id()) { - Some(OpEnum::Subscribe(subscribe_op)) => { + match op_storage.pop(msg.id()) { + Ok(Some(OpEnum::Subscribe(subscribe_op))) => { // was an existing operation, the other peer messaged back Ok(OpInitialization { op: subscribe_op, sender, }) } - Some(_) => return Err(OpError::OpNotPresent(id)), - None => { + Ok(Some(op)) => { + let _ = op_storage.push(id, op); + Err(OpError::OpNotPresent(id)) + } + Ok(None) => { // new request to subcribe to a contract, initialize the machine Ok(OpInitialization { op: Self { @@ -86,8 +89,8 @@ impl Operation for SubscribeOp { sender, }) } - }; - result + Err(err) => Err(err.into()), + } } fn id(&self) -> &Transaction { @@ -98,7 +101,7 @@ impl Operation for SubscribeOp { self, conn_manager: &'a mut CB, op_storage: &'a OpManager, - input: Self::Message, + input: &'a Self::Message, client_id: Option, ) -> Pin> + Send + 'a>> { Box::pin(async move { @@ -115,9 +118,9 @@ impl Operation for SubscribeOp { let sender = op_storage.ring.own_location(); new_state = self.state; return_msg = Some(SubscribeMsg::SeekNode { - id, - key, - target, + id: *id, + key: key.clone(), + target: *target, subscriber: sender, skip_list: vec![sender.peer], htl: 0, @@ -136,23 +139,22 @@ impl Operation for SubscribeOp { OperationResult { return_msg: Some(Message::from(SubscribeMsg::ReturnSub { key: key.clone(), - id, + id: *id, subscribed: false, sender, - target: subscriber, + target: *subscriber, })), state: None, } }; - if !op_storage.ring.is_contract_cached(&key) { + if !op_storage.ring.is_contract_cached(key) { tracing::debug!(tx = %id, "Contract {} not found at {}, trying other peer", key, target.peer); - let Some(new_target) = - op_storage.ring.closest_caching(&key, &[sender.peer]) + let Some(new_target) = op_storage.ring.closest_caching(key, &[sender.peer]) else { tracing::warn!(tx = %id, "No peer found while trying getting contract {key}"); - return Err(OpError::RingError(RingError::NoCachingPeers(key))); + return Err(OpError::RingError(RingError::NoCachingPeers(key.clone()))); }; let new_htl = htl + 1; @@ -169,9 +171,9 @@ impl Operation for SubscribeOp { .send( &new_target.peer, (SubscribeMsg::SeekNode { - id, + id: *id, key: key.clone(), - subscriber, + subscriber: *subscriber, target: new_target, skip_list: new_skip_list.clone(), htl: new_htl, @@ -179,7 +181,7 @@ impl Operation for SubscribeOp { .into(), ) .await?; - } else if op_storage.ring.add_subscriber(&key, subscriber).is_err() { + } else if op_storage.ring.add_subscriber(key, *subscriber).is_err() { // max number of subscribers for this contract reached return Ok(return_err()); } @@ -191,15 +193,16 @@ impl Operation for SubscribeOp { "Peer {} successfully subscribed to contract {key}", subscriber.peer, ); - new_state = Some(SubscribeState::Completed); + new_state = None; // TODO review behaviour, if the contract is not cached should return subscribed false? return_msg = Some(SubscribeMsg::ReturnSub { - sender: target, - target: subscriber, - id, - key, + sender: *target, + target: *subscriber, + id: *id, + key: key.clone(), subscribed: true, }); + op_storage.completed(*id); } _ => return Err(OpError::InvalidStateTransition(self.id)), } @@ -227,28 +230,28 @@ impl Operation for SubscribeOp { skip_list.push(sender.peer); if let Some(target) = op_storage .ring - .closest_caching(&key, skip_list.as_slice()) + .closest_caching(key, skip_list.as_slice()) .into_iter() .next() { let subscriber = op_storage.ring.own_location(); return_msg = Some(SubscribeMsg::SeekNode { - id, - key, + id: *id, + key: key.clone(), subscriber, target, skip_list: vec![target.peer], htl: 0, }); } else { - return Err(RingError::NoCachingPeers(key).into()); + return Err(RingError::NoCachingPeers(key.clone()).into()); } new_state = Some(SubscribeState::AwaitingResponse { skip_list, retries: retries + 1, }); } else { - return Err(OpError::MaxRetriesExceeded(id, id.tx_type())); + return Err(OpError::MaxRetriesExceeded(*id, id.tx_type())); } } _ => return Err(OpError::InvalidStateTransition(self.id)), @@ -270,20 +273,15 @@ impl Operation for SubscribeOp { this = ?op_storage.ring.own_location().peer, "Subscribed to `{key}` at provider {}", sender.peer ); - op_storage.ring.add_subscription(key); - // todo: should inform back to the network event loop? + op_storage.ring.add_subscription(key.clone()); + // todo: should inform back to the network event loop in case a client + // is waiting for response let _ = client_id; - new_state = None; + new_state = Some(SubscribeState::Completed); return_msg = None; + op_storage.completed(*id); } - state => { - tracing::error!( - tx = %id, - ?state, - target = ?target.peer, - this = ?op_storage.ring.own_location().peer, - "wrong state" - ); + _other => { return Err(OpError::InvalidStateTransition(self.id)); } } @@ -393,7 +391,7 @@ mod messages { use super::*; - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] + #[derive(Debug, Serialize, Deserialize)] pub(crate) enum SubscribeMsg { FetchRouting { id: Transaction, @@ -478,7 +476,6 @@ mod test { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn successful_subscribe_op_between_nodes() -> Result<(), anyhow::Error> { - crate::config::set_logger(); const NUM_NODES: usize = 4usize; const NUM_GW: usize = 1usize; diff --git a/crates/core/src/operations/update.rs b/crates/core/src/operations/update.rs index e85e495bb..bfdae46a6 100644 --- a/crates/core/src/operations/update.rs +++ b/crates/core/src/operations/update.rs @@ -48,7 +48,7 @@ impl Operation for UpdateOp { self, _conn_manager: &'a mut CB, _op_storage: &'a crate::node::OpManager, - _input: Self::Message, + _input: &Self::Message, _client_id: Option, ) -> std::pin::Pin< Box> + Send + 'a>, @@ -67,7 +67,7 @@ mod messages { ring::PeerKeyLocation, }; - #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] + #[derive(Debug, Serialize, Deserialize)] pub(crate) enum UpdateMsg {} impl InnerMessage for UpdateMsg { From f0c5117cd987443f51b11b4531a7bbe20cb4e4ad Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Tue, 24 Oct 2023 11:46:10 +0200 Subject: [PATCH 66/76] Fix CI issues --- .github/workflows/ci.yml | 4 +- crates/core/Cargo.toml | 6 +-- crates/core/src/config.rs | 64 ++++++++++++++------------- crates/core/src/operations/connect.rs | 1 - crates/core/src/operations/get.rs | 2 +- crates/core/src/operations/put.rs | 2 +- 6 files changed, 41 insertions(+), 38 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae257aaf9..4c9589d5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,14 +10,14 @@ on: jobs: test_all: - name: Test features + name: Test runs-on: ubuntu-latest strategy: max-parallel: 1 matrix: - args: ["--no-default-features", "--features default"] + args: ["--no-default-features --features trace,websocket,sqlite"] env: FREENET_LOG: error CARGO_TARGET_DIR: ${{ github.workspace }}/target diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index ab104a28f..2a18499a6 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -74,7 +74,7 @@ tar = { version = "0.4.38" } xz2 = { version = "0.1" } # Tracing deps -tracing = { version = "0.1", optional = true } +tracing = { version = "0.1" } opentelemetry = { version = "0.20.0", default-features = false, features = ["rt-tokio", "trace"], optional = true } opentelemetry-jaeger = { version = "0.19.0", features = ["rt-tokio","collector_client", "isahc"], optional = true } tracing-opentelemetry = { version = "0.21.0", optional = true } @@ -92,11 +92,11 @@ statrs = "0.16.0" freenet-stdlib = { workspace = true, features = ["testing", "net"] } [features] -default = ["websocket", "rocks_db", "trace"] +default = ["trace", "websocket", "sqlite"] testing = ["arbitrary"] rocks_db = ["rocksdb"] sqlite = ["sqlx"] websocket = ["axum/ws"] -trace = ["tracing", "opentelemetry", "opentelemetry-jaeger", "tracing-opentelemetry", "tracing-subscriber"] +trace = ["opentelemetry", "opentelemetry-jaeger", "tracing-opentelemetry", "tracing-subscriber"] local-mode = [] network-mode = [] diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index 07360c1a1..b503edef2 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -52,6 +52,7 @@ pub(crate) struct WebSocketApiConfig { port: u16, } +#[cfg(feature = "websocket")] impl From for SocketAddr { fn from(val: WebSocketApiConfig) -> Self { (val.ip, val.port).into() @@ -320,44 +321,47 @@ impl libp2p::swarm::Executor for GlobalExecutor { } pub fn set_logger() { - static LOGGER_SET: AtomicBool = AtomicBool::new(false); - if LOGGER_SET - .compare_exchange( - false, - true, - std::sync::atomic::Ordering::Acquire, - std::sync::atomic::Ordering::SeqCst, - ) - .is_err() + #[cfg(feature = "trace")] { - return; - } + static LOGGER_SET: AtomicBool = AtomicBool::new(false); + if LOGGER_SET + .compare_exchange( + false, + true, + std::sync::atomic::Ordering::Acquire, + std::sync::atomic::Ordering::SeqCst, + ) + .is_err() + { + return; + } - let filter = if cfg!(any(test, debug_assertions)) { - tracing_subscriber::filter::LevelFilter::DEBUG.into() - } else { - tracing_subscriber::filter::LevelFilter::INFO.into() - }; - - let sub = tracing_subscriber::fmt().with_level(true).with_env_filter( - tracing_subscriber::EnvFilter::builder() - .with_default_directive(filter) - .from_env_lossy() - .add_directive("stretto=off".parse().unwrap()) - .add_directive("sqlx=error".parse().unwrap()), - ); - - if cfg!(any(test, debug_assertions)) { - sub.with_file(true).with_line_number(true).init(); - } else { - sub.init(); + let filter = if cfg!(any(test, debug_assertions)) { + tracing_subscriber::filter::LevelFilter::DEBUG.into() + } else { + tracing_subscriber::filter::LevelFilter::INFO.into() + }; + + let sub = tracing_subscriber::fmt().with_level(true).with_env_filter( + tracing_subscriber::EnvFilter::builder() + .with_default_directive(filter) + .from_env_lossy() + .add_directive("stretto=off".parse().unwrap()) + .add_directive("sqlx=error".parse().unwrap()), + ); + + if cfg!(any(test, debug_assertions)) { + sub.with_file(true).with_line_number(true).init(); + } else { + sub.init(); + } } } +#[cfg(feature = "trace")] pub(super) mod tracer { use super::*; - #[cfg(feature = "trace")] pub fn init_tracer() -> Result<(), opentelemetry::trace::TraceError> { use opentelemetry::{global, sdk::propagation::TraceContextPropagator}; use tracing_subscriber::layer::SubscriberExt; diff --git a/crates/core/src/operations/connect.rs b/crates/core/src/operations/connect.rs index 68607460b..5eb8eb7f2 100644 --- a/crates/core/src/operations/connect.rs +++ b/crates/core/src/operations/connect.rs @@ -1104,7 +1104,6 @@ mod test { /// Once a gateway is left without remaining open slots, ensure forwarding connects #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn forward_connection_to_node() -> Result<(), anyhow::Error> { - // crate::config::set_logger(); const NUM_NODES: usize = 3usize; const NUM_GW: usize = 1usize; let mut sim_nw = SimNetwork::new( diff --git a/crates/core/src/operations/get.rs b/crates/core/src/operations/get.rs index 2306a4ab7..97dff1e17 100644 --- a/crates/core/src/operations/get.rs +++ b/crates/core/src/operations/get.rs @@ -1020,7 +1020,7 @@ mod test { sim_nw.check_connectivity(Duration::from_secs(3)).await?; sim_nw - .trigger_event("node-0", 1, Some(Duration::from_millis(50))) + .trigger_event("node-0", 1, Some(Duration::from_millis(200))) .await?; assert!(sim_nw.has_got_contract("node-0", &key)); Ok(()) diff --git a/crates/core/src/operations/put.rs b/crates/core/src/operations/put.rs index 314dac994..c4480836d 100644 --- a/crates/core/src/operations/put.rs +++ b/crates/core/src/operations/put.rs @@ -1048,7 +1048,7 @@ mod test { // trigger the put op @ gw-0 sim_nw - .trigger_event("gateway-0", 1, Some(Duration::from_millis(150))) + .trigger_event("gateway-0", 1, Some(Duration::from_millis(200))) .await?; assert!(sim_nw.has_put_contract("gateway-0", &key, &new_value)); assert!(sim_nw.event_listener.contract_broadcasted(&key)); From 891ec7bb6ca427a509adc703c206c6c8da69d5b8 Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Tue, 24 Oct 2023 15:11:37 +0200 Subject: [PATCH 67/76] Add garbage op cleanup background task --- crates/core/src/message.rs | 14 +- .../core/src/node/conn_manager/p2p_protoc.rs | 21 +- crates/core/src/node/op_state_manager.rs | 190 ++++++++++++++---- crates/core/src/operations.rs | 16 +- crates/core/src/operations/connect.rs | 98 ++++----- crates/core/src/operations/get.rs | 67 +++--- crates/core/src/operations/op_trait.rs | 10 +- crates/core/src/operations/put.rs | 65 +++--- crates/core/src/operations/subscribe.rs | 70 ++++--- crates/core/src/operations/update.rs | 10 +- crates/core/src/ring.rs | 26 +-- 11 files changed, 369 insertions(+), 218 deletions(-) diff --git a/crates/core/src/message.rs b/crates/core/src/message.rs index b35c821f3..5ea63072e 100644 --- a/crates/core/src/message.rs +++ b/crates/core/src/message.rs @@ -51,6 +51,18 @@ impl Display for Transaction { } } +impl PartialOrd for Transaction { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Transaction { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.id.cmp(&other.id) + } +} + /// Get the transaction type associated to a given message type. pub(crate) trait TxType: sealed_msg_type::SealedTxType { fn tx_type_id() -> TransactionTypeId; @@ -91,7 +103,6 @@ mod sealed_msg_type { Get, Subscribe, Update, - Canceled, } impl Display for TransactionType { @@ -102,7 +113,6 @@ mod sealed_msg_type { TransactionType::Get => write!(f, "get"), TransactionType::Subscribe => write!(f, "subscribe"), TransactionType::Update => write!(f, "update"), - TransactionType::Canceled => write!(f, "canceled"), } } } diff --git a/crates/core/src/node/conn_manager/p2p_protoc.rs b/crates/core/src/node/conn_manager/p2p_protoc.rs index d2c3f49ad..68935dd6f 100644 --- a/crates/core/src/node/conn_manager/p2p_protoc.rs +++ b/crates/core/src/node/conn_manager/p2p_protoc.rs @@ -909,7 +909,12 @@ impl ConnectionHandler for Handler { if msg.track_stats() { if let Ok(Some(mut op)) = self.op_manager.pop(op_id) { op.record_transfer(); - let _ = self.op_manager.push(*op_id, op); + let fut = self.op_manager.push(*op_id, op); + futures::pin_mut!(fut); + match fut.poll_unpin(cx) { + Poll::Ready(_) => {} + Poll::Pending => return Poll::Pending, + } } } let op_id = *op_id; @@ -954,7 +959,12 @@ impl ConnectionHandler for Handler { if let Some(op_id) = op_id { if let Ok(Some(mut op)) = self.op_manager.pop(&op_id) { op.record_transfer(); - let _ = self.op_manager.push(op_id, op); + let fut = self.op_manager.push(op_id, op); + futures::pin_mut!(fut); + match fut.poll_unpin(cx) { + Poll::Ready(_) => {} + Poll::Pending => return Poll::Pending, + } } } stream = SubstreamState::WaitingMsg { substream, conn_id }; @@ -983,7 +993,12 @@ impl ConnectionHandler for Handler { let op_id = msg.id(); if let Ok(Some(mut op)) = self.op_manager.pop(op_id) { op.record_transfer(); - let _ = self.op_manager.push(*op_id, op); + let fut = self.op_manager.push(*op_id, op); + futures::pin_mut!(fut); + match fut.poll_unpin(cx) { + Poll::Ready(_) => {} + Poll::Pending => return Poll::Pending, + } } if !msg.terminal() { // received a message, the other peer is waiting for an answer diff --git a/crates/core/src/node/op_state_manager.rs b/crates/core/src/node/op_state_manager.rs index 24750589c..2804f6f8a 100644 --- a/crates/core/src/node/op_state_manager.rs +++ b/crates/core/src/node/op_state_manager.rs @@ -1,11 +1,11 @@ -use std::{collections::BTreeMap, time::Instant}; +use std::{cmp::Reverse, collections::BTreeSet, sync::Arc, time::Duration}; use dashmap::{DashMap, DashSet}; use either::Either; -use parking_lot::RwLock; -use tokio::sync::{mpsc::error::SendError, Mutex}; +use tokio::sync::Mutex; use crate::{ + config::GlobalExecutor, contract::{ ContractError, ContractHandlerEvent, ContractHandlerToEventLoopChannel, NetEventListenerHalve, @@ -13,7 +13,11 @@ use crate::{ dev_tool::ClientId, message::{Message, Transaction, TransactionType}, operations::{ - connect::ConnectOp, get::GetOp, put::PutOp, subscribe::SubscribeOp, update::UpdateOp, + connect::ConnectOp, + get::{self, GetOp}, + put::PutOp, + subscribe::SubscribeOp, + update::UpdateOp, OpEnum, OpError, }, ring::Ring, @@ -21,6 +25,15 @@ use crate::{ use super::{conn_manager::EventLoopNotificationsSender, PeerKey}; +#[cfg(debug_assertions)] +macro_rules! check_id_op { + ($get_ty:expr, $var:path) => { + if !matches!($get_ty, $var) { + return Err(OpError::IncorrectTxType($var, $get_ty)); + } + }; +} + #[derive(Debug, thiserror::Error)] pub(crate) enum OpNotAvailable { #[error("operation running")] @@ -32,49 +45,58 @@ pub(crate) enum OpNotAvailable { /// Thread safe and friendly data structure to maintain state of the different operations /// and enable their execution. pub(crate) struct OpManager { - join_ring: DashMap, - put: DashMap, - get: DashMap, - subscribe: DashMap, - update: DashMap, + connect: Arc>, + put: Arc>, + get: Arc>, + subscribe: Arc>, + update: Arc>, + completed: Arc>, + under_progress: Arc>, to_event_listener: EventLoopNotificationsSender, - // todo: remove the need for a mutex here + // todo: remove the need for a mutex here if possible ch_outbound: Mutex>, - // FIXME: think of an optimal strategy to check for timeouts and clean up garbage - _ops_ttl: RwLock>>, - // todo: improve this when the anti-write amplification functionality is added - completed: DashSet, - in_progress: DashSet, + new_transactions: tokio::sync::mpsc::Sender, pub ring: Ring, } -#[cfg(debug_assertions)] -macro_rules! check_id_op { - ($get_ty:expr, $var:path) => { - if !matches!($get_ty, $var) { - return Err(OpError::IncorrectTxType($var, $get_ty)); - } - }; -} - impl OpManager { pub(super) fn new( ring: Ring, notification_channel: EventLoopNotificationsSender, contract_handler: ContractHandlerToEventLoopChannel, ) -> Self { + let connect = Arc::new(DashMap::new()); + let put = Arc::new(DashMap::new()); + let get = Arc::new(DashMap::new()); + let subscribe = Arc::new(DashMap::new()); + let update = Arc::new(DashMap::new()); + let completed = Arc::new(DashSet::new()); + let under_progress = Arc::new(DashSet::new()); + + let (new_transactions, rx) = tokio::sync::mpsc::channel(100); + GlobalExecutor::spawn(garbage_cleanup_task( + rx, + connect.clone(), + put.clone(), + get.clone(), + subscribe.clone(), + update.clone(), + completed.clone(), + under_progress.clone(), + )); + Self { - join_ring: DashMap::default(), - put: DashMap::default(), - get: DashMap::default(), - subscribe: DashMap::default(), - update: DashMap::default(), - ring, + connect, + put, + get, + subscribe, + update, + completed, + under_progress, to_event_listener: notification_channel, ch_outbound: Mutex::new(contract_handler), - completed: DashSet::new(), - in_progress: DashSet::new(), - _ops_ttl: RwLock::new(BTreeMap::new()), + new_transactions, + ring, } } @@ -88,13 +110,13 @@ impl OpManager { msg: Message, op: OpEnum, client_id: Option, - ) -> Result<(), SendError<(Message, Option)>> { + ) -> Result<(), OpError> { // push back the state to the stack - self.push(*msg.id(), op).expect("infallible"); + self.push(*msg.id(), op).await?; self.to_event_listener .send(Either::Left((msg, client_id))) .await - .map_err(|err| SendError(err.0.unwrap_left())) + .map_err(Into::into) } /// Send an event to the contract handler and await a response event from it if successful. @@ -114,13 +136,14 @@ impl OpManager { todo!() } - pub fn push(&self, id: Transaction, op: OpEnum) -> Result<(), OpError> { - self.in_progress.remove(&id); + pub async fn push(&self, id: Transaction, op: OpEnum) -> Result<(), OpError> { + self.under_progress.remove(&id); + self.new_transactions.send(id).await?; match op { OpEnum::Connect(op) => { #[cfg(debug_assertions)] check_id_op!(id.tx_type(), TransactionType::Connect); - self.join_ring.insert(id, *op); + self.connect.insert(id, *op); } OpEnum::Put(op) => { #[cfg(debug_assertions)] @@ -150,12 +173,12 @@ impl OpManager { if self.completed.contains(id) { return Err(OpNotAvailable::Completed); } - if self.in_progress.contains(id) { + if self.under_progress.contains(id) { return Err(OpNotAvailable::Running); } let op = match id.tx_type() { TransactionType::Connect => self - .join_ring + .connect .remove(id) .map(|(_k, v)| v) .map(|op| OpEnum::Connect(Box::new(op))), @@ -167,9 +190,8 @@ impl OpManager { .map(|(_k, v)| v) .map(OpEnum::Subscribe), TransactionType::Update => self.update.remove(id).map(|(_k, v)| v).map(OpEnum::Update), - TransactionType::Canceled => unreachable!(), }; - self.in_progress.insert(*id); + self.under_progress.insert(*id); Ok(op) } @@ -182,3 +204,85 @@ impl OpManager { self.ring.prune_connection(peer); } } + +#[allow(clippy::too_many_arguments)] +async fn garbage_cleanup_task( + mut new_transactions: tokio::sync::mpsc::Receiver, + connect: Arc>, + put: Arc>, + get: Arc>, + subscribe: Arc>, + update: Arc>, + completed: Arc>, + under_progress: Arc>, +) { + const CLEANUP_INTERVAL: Duration = Duration::from_secs(5); + let mut tick = tokio::time::interval(CLEANUP_INTERVAL); + tick.tick().await; + + let mut ttl_set = BTreeSet::new(); + + let remove_old = move |ttl_set: &mut BTreeSet>, + delayed: &mut Vec| { + // generate a random id, since those are sortable by time + // it will allow to get any older transactions, notice the use of reverse + // so the older transactions are removed instead of the newer ones + let older_than: Reverse = Reverse(Transaction::new::()); + let mut old_missing = std::mem::replace(delayed, Vec::with_capacity(200)); + for tx in old_missing.drain(..) { + if completed.remove(&tx).is_some() { + continue; + } + let still_waiting = match tx.tx_type() { + TransactionType::Connect => connect.remove(&tx).is_none(), + TransactionType::Put => put.remove(&tx).is_none(), + TransactionType::Get => get.remove(&tx).is_none(), + TransactionType::Subscribe => subscribe.remove(&tx).is_none(), + TransactionType::Update => update.remove(&tx).is_none(), + }; + if still_waiting { + delayed.push(tx); + } + } + for Reverse(tx) in ttl_set.split_off(&older_than).into_iter() { + if under_progress.contains(&tx) { + delayed.push(tx); + continue; + } + if completed.remove(&tx).is_some() { + continue; + } + match tx.tx_type() { + TransactionType::Connect => { + connect.remove(&tx); + } + TransactionType::Put => { + put.remove(&tx); + } + TransactionType::Get => { + get.remove(&tx); + } + TransactionType::Subscribe => { + subscribe.remove(&tx); + } + TransactionType::Update => { + update.remove(&tx); + } + } + } + }; + + let mut delayed = vec![]; + loop { + tokio::select! { + tx = new_transactions.recv() => { + if let Some(tx) = tx { + ttl_set.insert(Reverse(tx)); + } + } + _ = tick.tick() => { + remove_old(&mut ttl_set, &mut delayed); + } + } + } +} diff --git a/crates/core/src/operations.rs b/crates/core/src/operations.rs index 786af2f0e..cab23f2b4 100644 --- a/crates/core/src/operations.rs +++ b/crates/core/src/operations.rs @@ -44,7 +44,7 @@ where let sender; let tx = *msg.id(); let result = { - let OpInitialization { sender: s, op } = Op::load_or_init(op_storage, msg)?; + let OpInitialization { sender: s, op } = Op::load_or_init(op_storage, msg).await?; sender = s; op.process_message(conn_manager, op_storage, msg, client_id) .await @@ -88,7 +88,7 @@ where if let Some(target) = msg.target().cloned() { conn_manager.send(&target.peer, msg).await?; } - op_storage.push(id, updated_state)?; + op_storage.push(id, updated_state).await?; } Ok(OperationResult { @@ -104,7 +104,7 @@ where }) => { // interim state let id = *updated_state.id(); - op_storage.push(id, updated_state)?; + op_storage.push(id, updated_state).await?; } Ok(OperationResult { return_msg: Some(msg), @@ -193,8 +193,8 @@ pub(crate) enum OpError { UnexpectedOpState, #[error("cannot perform a state transition from the current state with the provided input (tx: {0})")] InvalidStateTransition(Transaction), - #[error("failed notifying back to the node message loop, channel closed")] - NotificationError(#[from] Box)>>), + #[error("failed notifying, channel closed")] + NotificationError, #[error("unspected transaction type, trying to get a {0:?} from a {1:?}")] IncorrectTxType(TransactionType, TransactionType), #[error("op not present: {0}")] @@ -211,8 +211,8 @@ pub(crate) enum OpError { StatePushed, } -impl From)>> for OpError { - fn from(err: SendError<(Message, Option)>) -> OpError { - OpError::NotificationError(Box::new(err)) +impl From> for OpError { + fn from(_: SendError) -> OpError { + OpError::NotificationError } } diff --git a/crates/core/src/operations/connect.rs b/crates/core/src/operations/connect.rs index 5eb8eb7f2..67ceda40c 100644 --- a/crates/core/src/operations/connect.rs +++ b/crates/core/src/operations/connect.rs @@ -1,5 +1,6 @@ //! Operation which seeks new connections in the ring. -use futures::Future; +use futures::future::BoxFuture; +use futures::{Future, FutureExt}; use std::pin::Pin; use std::{collections::HashSet, time::Duration}; @@ -79,40 +80,43 @@ impl Operation for ConnectOp { type Message = ConnectMsg; type Result = ConnectResult; - fn load_or_init( - op_storage: &OpManager, - msg: &Self::Message, - ) -> Result, OpError> { - let sender; - let tx = *msg.id(); - match op_storage.pop(msg.id()) { - Ok(Some(OpEnum::Connect(connect_op))) => { - sender = msg.sender().cloned(); - // was an existing operation, the other peer messaged back - Ok(OpInitialization { - op: *connect_op, - sender, - }) - } - Ok(Some(op)) => { - let _ = op_storage.push(tx, op); - Err(OpError::OpNotPresent(tx)) - } - Ok(None) => { - // new request to join this node, initialize the machine - Ok(OpInitialization { - op: Self { - id: tx, - state: Some(ConnectState::Initializing), - backoff: None, - gateway: Box::new(op_storage.ring.own_location()), - _ttl: PEER_TIMEOUT, - }, - sender: None, - }) + fn load_or_init<'a>( + op_storage: &'a OpManager, + msg: &'a Self::Message, + ) -> BoxFuture<'a, Result, OpError>> { + async move { + let sender; + let tx = *msg.id(); + match op_storage.pop(msg.id()) { + Ok(Some(OpEnum::Connect(connect_op))) => { + sender = msg.sender().cloned(); + // was an existing operation, the other peer messaged back + Ok(OpInitialization { + op: *connect_op, + sender, + }) + } + Ok(Some(op)) => { + let _ = op_storage.push(tx, op).await; + Err(OpError::OpNotPresent(tx)) + } + Ok(None) => { + // new request to join this node, initialize the machine + Ok(OpInitialization { + op: Self { + id: tx, + state: Some(ConnectState::Initializing), + backoff: None, + gateway: Box::new(op_storage.ring.own_location()), + _ttl: PEER_TIMEOUT, + }, + sender: None, + }) + } + Err(err) => Err(err.into()), } - Err(err) => Err(err.into()), } + .boxed() } fn id(&self) -> &Transaction { @@ -830,20 +834,22 @@ where }, }); conn_manager.send(&gateway.peer, join_req).await?; - op_storage.push( - tx, - OpEnum::Connect(Box::new(ConnectOp { - id, - state: Some(ConnectState::Connecting(ConnectionInfo { - gateway, - this_peer, - max_hops_to_live, + op_storage + .push( + tx, + OpEnum::Connect(Box::new(ConnectOp { + id, + state: Some(ConnectState::Connecting(ConnectionInfo { + gateway, + this_peer, + max_hops_to_live, + })), + gateway: Box::new(gateway), + backoff, + _ttl, })), - gateway: Box::new(gateway), - backoff, - _ttl, - })), - )?; + ) + .await?; Ok(()) } diff --git a/crates/core/src/operations/get.rs b/crates/core/src/operations/get.rs index 97dff1e17..ef2595ce8 100644 --- a/crates/core/src/operations/get.rs +++ b/crates/core/src/operations/get.rs @@ -3,6 +3,8 @@ use std::time::Duration; use std::{future::Future, time::Instant}; use freenet_stdlib::prelude::*; +use futures::future::BoxFuture; +use futures::FutureExt; use crate::{ client_events::ClientId, @@ -138,39 +140,42 @@ impl Operation for GetOp { type Message = GetMsg; type Result = GetResult; - fn load_or_init( - op_storage: &OpManager, - msg: &Self::Message, - ) -> Result, OpError> { - let mut sender: Option = None; - if let Some(peer_key_loc) = msg.sender().cloned() { - sender = Some(peer_key_loc.peer); - }; - let tx = *msg.id(); - match op_storage.pop(msg.id()) { - Ok(Some(OpEnum::Get(get_op))) => { - Ok(OpInitialization { op: get_op, sender }) - // was an existing operation, other peer messaged back - } - Ok(Some(op)) => { - let _ = op_storage.push(tx, op); - Err(OpError::OpNotPresent(tx)) - } - Ok(None) => { - // new request to get a value for a contract, initialize the machine - Ok(OpInitialization { - op: Self { - state: Some(GetState::ReceivedRequest), - id: tx, - result: None, - stats: None, // don't care about stats in target peers - _ttl: PEER_TIMEOUT, - }, - sender, - }) + fn load_or_init<'a>( + op_storage: &'a OpManager, + msg: &'a Self::Message, + ) -> BoxFuture<'a, Result, OpError>> { + async move { + let mut sender: Option = None; + if let Some(peer_key_loc) = msg.sender().cloned() { + sender = Some(peer_key_loc.peer); + }; + let tx = *msg.id(); + match op_storage.pop(msg.id()) { + Ok(Some(OpEnum::Get(get_op))) => { + Ok(OpInitialization { op: get_op, sender }) + // was an existing operation, other peer messaged back + } + Ok(Some(op)) => { + let _ = op_storage.push(tx, op).await; + Err(OpError::OpNotPresent(tx)) + } + Ok(None) => { + // new request to get a value for a contract, initialize the machine + Ok(OpInitialization { + op: Self { + state: Some(GetState::ReceivedRequest), + id: tx, + result: None, + stats: None, // don't care about stats in target peers + _ttl: PEER_TIMEOUT, + }, + sender, + }) + } + Err(err) => Err(err.into()), } - Err(err) => Err(err.into()), } + .boxed() } fn id(&self) -> &Transaction { diff --git a/crates/core/src/operations/op_trait.rs b/crates/core/src/operations/op_trait.rs index 3c906106f..5931e6038 100644 --- a/crates/core/src/operations/op_trait.rs +++ b/crates/core/src/operations/op_trait.rs @@ -2,7 +2,7 @@ use std::pin::Pin; -use futures::Future; +use futures::{future::BoxFuture, Future}; use crate::{ client_events::ClientId, @@ -19,10 +19,10 @@ where type Result; - fn load_or_init( - op_storage: &OpManager, - msg: &Self::Message, - ) -> Result, OpError>; + fn load_or_init<'a>( + op_storage: &'a OpManager, + msg: &'a Self::Message, + ) -> BoxFuture<'a, Result, OpError>>; fn id(&self) -> &Transaction; diff --git a/crates/core/src/operations/put.rs b/crates/core/src/operations/put.rs index c4480836d..02f235f92 100644 --- a/crates/core/src/operations/put.rs +++ b/crates/core/src/operations/put.rs @@ -9,6 +9,8 @@ use std::{collections::HashSet, time::Instant}; pub(crate) use self::messages::PutMsg; use freenet_stdlib::prelude::*; +use futures::future::BoxFuture; +use futures::FutureExt; use super::{OpEnum, OpError, OpOutcome, OperationResult}; use crate::{ @@ -126,39 +128,42 @@ impl Operation for PutOp { type Message = PutMsg; type Result = PutResult; - fn load_or_init( - op_storage: &OpManager, - msg: &Self::Message, - ) -> Result, OpError> { - let mut sender: Option = None; - if let Some(peer_key_loc) = msg.sender().cloned() { - sender = Some(peer_key_loc.peer); - }; + fn load_or_init<'a>( + op_storage: &'a OpManager, + msg: &'a Self::Message, + ) -> BoxFuture<'a, Result, OpError>> { + async move { + let mut sender: Option = None; + if let Some(peer_key_loc) = msg.sender().cloned() { + sender = Some(peer_key_loc.peer); + }; - let tx = *msg.id(); - match op_storage.pop(msg.id()) { - Ok(Some(OpEnum::Put(put_op))) => { - // was an existing operation, the other peer messaged back - Ok(OpInitialization { op: put_op, sender }) - } - Ok(Some(op)) => { - let _ = op_storage.push(tx, op); - Err(OpError::OpNotPresent(tx)) - } - Ok(None) => { - // new request to put a new value for a contract, initialize the machine - Ok(OpInitialization { - op: Self { - state: Some(PutState::ReceivedRequest), - stats: None, // don't care for stats in the target peers - id: tx, - _ttl: PEER_TIMEOUT, - }, - sender, - }) + let tx = *msg.id(); + match op_storage.pop(msg.id()) { + Ok(Some(OpEnum::Put(put_op))) => { + // was an existing operation, the other peer messaged back + Ok(OpInitialization { op: put_op, sender }) + } + Ok(Some(op)) => { + let _ = op_storage.push(tx, op).await; + Err(OpError::OpNotPresent(tx)) + } + Ok(None) => { + // new request to put a new value for a contract, initialize the machine + Ok(OpInitialization { + op: Self { + state: Some(PutState::ReceivedRequest), + stats: None, // don't care for stats in the target peers + id: tx, + _ttl: PEER_TIMEOUT, + }, + sender, + }) + } + Err(err) => Err(err.into()), } - Err(err) => Err(err.into()), } + .boxed() } fn id(&self) -> &Transaction { diff --git a/crates/core/src/operations/subscribe.rs b/crates/core/src/operations/subscribe.rs index 38d6b08e5..9d5510110 100644 --- a/crates/core/src/operations/subscribe.rs +++ b/crates/core/src/operations/subscribe.rs @@ -3,6 +3,7 @@ use std::pin::Pin; use std::time::Duration; use freenet_stdlib::prelude::*; +use futures::{future::BoxFuture, FutureExt}; use serde::{Deserialize, Serialize}; use crate::{ @@ -56,41 +57,44 @@ impl Operation for SubscribeOp { type Message = SubscribeMsg; type Result = SubscribeResult; - fn load_or_init( - op_storage: &OpManager, - msg: &Self::Message, - ) -> Result, OpError> { - let mut sender: Option = None; - if let Some(peer_key_loc) = msg.sender().cloned() { - sender = Some(peer_key_loc.peer); - }; - let id = *msg.id(); - - match op_storage.pop(msg.id()) { - Ok(Some(OpEnum::Subscribe(subscribe_op))) => { - // was an existing operation, the other peer messaged back - Ok(OpInitialization { - op: subscribe_op, - sender, - }) - } - Ok(Some(op)) => { - let _ = op_storage.push(id, op); - Err(OpError::OpNotPresent(id)) - } - Ok(None) => { - // new request to subcribe to a contract, initialize the machine - Ok(OpInitialization { - op: Self { - state: Some(SubscribeState::ReceivedRequest), - id, - _ttl: PEER_TIMEOUT, - }, - sender, - }) + fn load_or_init<'a>( + op_storage: &'a OpManager, + msg: &'a Self::Message, + ) -> BoxFuture<'a, Result, OpError>> { + async move { + let mut sender: Option = None; + if let Some(peer_key_loc) = msg.sender().cloned() { + sender = Some(peer_key_loc.peer); + }; + let id = *msg.id(); + + match op_storage.pop(msg.id()) { + Ok(Some(OpEnum::Subscribe(subscribe_op))) => { + // was an existing operation, the other peer messaged back + Ok(OpInitialization { + op: subscribe_op, + sender, + }) + } + Ok(Some(op)) => { + let _ = op_storage.push(id, op).await; + Err(OpError::OpNotPresent(id)) + } + Ok(None) => { + // new request to subcribe to a contract, initialize the machine + Ok(OpInitialization { + op: Self { + state: Some(SubscribeState::ReceivedRequest), + id, + _ttl: PEER_TIMEOUT, + }, + sender, + }) + } + Err(err) => Err(err.into()), } - Err(err) => Err(err.into()), } + .boxed() } fn id(&self) -> &Transaction { diff --git a/crates/core/src/operations/update.rs b/crates/core/src/operations/update.rs index bfdae46a6..5d6cd0a89 100644 --- a/crates/core/src/operations/update.rs +++ b/crates/core/src/operations/update.rs @@ -1,5 +1,7 @@ // TODO: complete update logic in the network +use futures::future::BoxFuture; + pub(crate) use self::messages::UpdateMsg; use crate::{client_events::ClientId, node::ConnectionBridge}; @@ -33,10 +35,10 @@ impl Operation for UpdateOp { type Message = UpdateMsg; type Result = UpdateResult; - fn load_or_init( - _op_storage: &crate::node::OpManager, - _msg: &Self::Message, - ) -> Result, OpError> { + fn load_or_init<'a>( + _op_storage: &'a crate::node::OpManager, + _msg: &'a Self::Message, + ) -> BoxFuture<'a, Result, OpError>> { todo!() } diff --git a/crates/core/src/ring.rs b/crates/core/src/ring.rs index 9f429e8b6..6fd9622e1 100644 --- a/crates/core/src/ring.rs +++ b/crates/core/src/ring.rs @@ -64,7 +64,7 @@ impl From for PeerKeyLocation { /// Thread safe and friendly data structure to keep track of the local knowledge /// of the state of the ring. -#[derive(Debug, Clone)] +#[derive(Debug)] pub(crate) struct Ring { pub rnd_if_htl_above: usize, pub max_hops_to_live: usize, @@ -72,25 +72,25 @@ pub(crate) struct Ring { max_connections: usize, min_connections: usize, router: Arc>, - connections_by_location: Arc>>, - location_for_peer: Arc>>, + connections_by_location: RwLock>, + location_for_peer: RwLock>, /// contracts in the ring cached by this node cached_contracts: DashSet, - own_location: Arc, + own_location: AtomicU64, /// The container for subscriber is a vec instead of something like a hashset /// that would allow for blind inserts of duplicate peers subscribing because /// of data locality, since we are likely to end up iterating over the whole sequence /// of subscribers more often than inserting, and anyways is a relatively short sequence /// then is more optimal to just use a vector for it's compact memory layout. - subscribers: Arc>>, - subscriptions: Arc>>, + subscribers: DashMap>, + subscriptions: RwLock>, // A peer which has been blacklisted to perform actions regarding a given contract. // todo: add blacklist // contract_blacklist: Arc>>, /// Interim connections ongoing haandshake or successfully open connections /// Is important to keep track of this so no more connections are accepted prematurely. - open_connections: Arc, + open_connections: AtomicUsize, } // /// A data type that represents the fact that a peer has been blacklisted @@ -124,7 +124,7 @@ impl Ring { let peer_key = PeerKey::from(config.local_key.public()); // for location here consider -1 == None - let own_location = Arc::new(AtomicU64::new(u64::from_le_bytes((-1f64).to_le_bytes()))); + let own_location = AtomicU64::new(u64::from_le_bytes((-1f64).to_le_bytes())); let max_hops_to_live = if let Some(v) = config.max_hops_to_live { v @@ -159,15 +159,15 @@ impl Ring { max_connections, min_connections, router, - connections_by_location: Arc::new(RwLock::new(BTreeMap::new())), - location_for_peer: Arc::new(RwLock::new(BTreeMap::new())), + connections_by_location: RwLock::new(BTreeMap::new()), + location_for_peer: RwLock::new(BTreeMap::new()), cached_contracts: DashSet::new(), own_location, peer_key, - subscribers: Arc::new(DashMap::new()), - subscriptions: Arc::new(RwLock::new(Vec::new())), + subscribers: DashMap::new(), + subscriptions: RwLock::new(Vec::new()), // contract_blacklist: Arc::new(DashMap::new()), - open_connections: Arc::new(AtomicUsize::new(0)), + open_connections: AtomicUsize::new(0), }; if let Some(loc) = config.location { From f88fc06717391a1da455c4dc7c69ef2233e2bbbb Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Tue, 24 Oct 2023 16:39:19 +0200 Subject: [PATCH 68/76] Update deps --- Cargo.lock | 967 +++++++++++++++++++++++++++++------------------------ 1 file changed, 527 insertions(+), 440 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b3fb5b4ae..2052d0aa4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,25 +64,26 @@ dependencies = [ [[package]] name = "ahash" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" dependencies = [ - "getrandom 0.2.10", + "getrandom", "once_cell", "version_check", ] [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", - "getrandom 0.2.10", + "getrandom", "once_cell", "version_check", + "zerocopy", ] [[package]] @@ -180,9 +181,9 @@ dependencies = [ [[package]] name = "arbitrary" -version = "1.3.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" dependencies = [ "derive_arbitrary", ] @@ -266,9 +267,9 @@ dependencies = [ "log", "parking", "polling", - "rustix 0.37.24", + "rustix 0.37.27", "slab", - "socket2 0.4.9", + "socket2 0.4.10", "waker-fn", ] @@ -283,13 +284,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.73" +version = "0.1.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -329,6 +330,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "attohttpc" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9a9bf8b79a749ee0b911b91b671cc2b6c670bdbc7e3dfd537576ddc94bb2a2" +dependencies = [ + "http", + "log", + "url", +] + [[package]] name = "autocfg" version = "0.1.8" @@ -352,7 +364,7 @@ checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", "axum-core", - "base64 0.21.4", + "base64 0.21.5", "bitflags 1.3.2", "bytes", "futures-util", @@ -424,9 +436,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.4" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64ct" @@ -461,7 +473,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -472,9 +484,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" dependencies = [ "serde", ] @@ -497,7 +509,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] @@ -511,7 +523,7 @@ dependencies = [ "cc", "cfg-if", "constant_time_eq", - "digest 0.10.7", + "digest", ] [[package]] @@ -717,9 +729,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.6" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" +checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" dependencies = [ "clap_builder", "clap_derive", @@ -727,9 +739,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.6" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" +checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" dependencies = [ "anstream", "anstyle", @@ -739,21 +751,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.4.2" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] name = "clap_lex" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "cloudabi" @@ -860,9 +872,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] @@ -961,9 +973,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crossbeam" @@ -1073,15 +1085,15 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "socket2 0.4.9", + "socket2 0.4.10", "winapi", ] [[package]] name = "curl-sys" -version = "0.4.67+curl-8.3.0" +version = "0.4.68+curl-8.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cc35d066510b197a0f72de863736641539957628c8a42e70e27c66849e77c34" +checksum = "b4a0d18d88360e374b16b2273c832b5e57258ffc1d4aa4f96b108e0738d5752f" dependencies = [ "cc", "libc", @@ -1092,19 +1104,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "curve25519-dalek" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.5.1", - "subtle", - "zeroize", -] - [[package]] name = "curve25519-dalek" version = "4.1.1" @@ -1114,7 +1113,7 @@ dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", - "digest 0.10.7", + "digest", "fiat-crypto", "platforms", "rustc_version", @@ -1124,13 +1123,13 @@ dependencies = [ [[package]] name = "curve25519-dalek-derive" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -1154,7 +1153,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -1165,7 +1164,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -1175,7 +1174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.1", + "hashbrown 0.14.2", "lock_api", "once_cell", "parking_lot_core", @@ -1245,10 +1244,11 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" dependencies = [ + "powerfmt", "serde", ] @@ -1265,22 +1265,13 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e0efad4403bfc52dc201159c4b842a246a14b98c64b55dfd0f2d89729dfeb8" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", + "syn 2.0.39", ] [[package]] @@ -1324,7 +1315,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -1347,9 +1338,9 @@ checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" [[package]] name = "ed25519" -version = "2.2.2" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60f6d271ca33075c88028be6f04d502853d63a5ece419d269c15315d4fc1cf1d" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", "signature", @@ -1361,7 +1352,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" dependencies = [ - "curve25519-dalek 4.1.1", + "curve25519-dalek", "ed25519", "rand_core 0.6.4", "serde", @@ -1390,6 +1381,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "enum-as-inner" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "enum-iterator" version = "0.7.0" @@ -1412,9 +1415,9 @@ dependencies = [ [[package]] name = "enumset" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e875f1719c16de097dee81ed675e2d9bb63096823ed3f0ca827b7dea3028bbbb" +checksum = "226c0da7462c13fb57e5cc9e0dc8f0635e7d27f276a3a7fd30054647f669007d" dependencies = [ "enumset_derive", ] @@ -1428,7 +1431,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -1506,7 +1509,7 @@ dependencies = [ "tar", "thiserror", "tokio", - "toml 0.8.2", + "toml 0.8.6", "tracing", "tracing-subscriber", "xz2", @@ -1514,9 +1517,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" +checksum = "a481586acf778f1b1455424c343f71124b048ffa5f4fc3f8f6ae9dc432dcb3c7" [[package]] name = "filetime" @@ -1607,7 +1610,7 @@ dependencies = [ "once_cell", "opentelemetry", "opentelemetry-jaeger", - "ordered-float 3.9.1", + "ordered-float 4.1.1", "parking_lot", "pav_regression", "pico-args", @@ -1638,7 +1641,7 @@ version = "0.0.5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -1660,7 +1663,7 @@ dependencies = [ "rand 0.8.5", "semver", "serde", - "serde-wasm-bindgen 0.6.0", + "serde-wasm-bindgen 0.6.1", "serde_bytes", "serde_json", "serde_with", @@ -1697,9 +1700,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" dependencies = [ "futures-channel", "futures-core", @@ -1710,11 +1713,21 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-bounded" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b07bbbe7d7e78809544c6f718d875627addc73a7c3582447abc052cd3dc67e0" +dependencies = [ + "futures-timer", + "futures-util", +] + [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", "futures-sink", @@ -1722,15 +1735,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" dependencies = [ "futures-core", "futures-task", @@ -1751,9 +1764,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" [[package]] name = "futures-lite" @@ -1772,13 +1785,13 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -1793,15 +1806,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-timer" @@ -1811,9 +1824,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-channel", "futures-core", @@ -1846,17 +1859,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.10" @@ -1864,8 +1866,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", + "wasm-bindgen", ] [[package]] @@ -1902,30 +1906,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] -name = "hashbrown" -version = "0.12.3" +name = "h2" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ - "ahash 0.7.6", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", ] [[package]] name = "hashbrown" -version = "0.13.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.8.3", + "ahash 0.7.7", ] [[package]] name = "hashbrown" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.6", "allocator-api2", ] @@ -1935,7 +1949,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.1", + "hashbrown 0.14.2", ] [[package]] @@ -1944,7 +1958,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64 0.21.4", + "base64 0.21.5", "bytes", "headers-core", "http", @@ -1998,7 +2012,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] @@ -2071,13 +2085,14 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", + "h2", "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.9", + "socket2 0.4.10", "tokio", "tower-service", "tracing", @@ -2086,16 +2101,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows 0.48.0", + "windows-core", ] [[package]] @@ -2146,9 +2161,9 @@ dependencies = [ [[package]] name = "if-watch" -version = "3.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9465340214b296cd17a0009acdb890d6160010b8adf8f78a00d0d7ab270f79f" +checksum = "bbb892e5777fe09e16f3d44de7802f4daa7267ecbe8c466f19d94e25bb0c303e" dependencies = [ "async-io", "core-foundation", @@ -2195,12 +2210,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.1", + "hashbrown 0.14.2", "serde", ] @@ -2265,7 +2280,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2 0.5.4", + "socket2 0.5.5", "widestring", "windows-sys 0.48.0", "winreg", @@ -2273,9 +2288,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "isahc" @@ -2319,18 +2334,18 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jobserver" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" dependencies = [ "wasm-bindgen", ] @@ -2389,9 +2404,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.149" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libloading" @@ -2411,14 +2426,15 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libp2p" -version = "0.52.3" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d07d1502a027366d55afe187621c2d7895dc111a3df13b35fed698049681d7" +checksum = "e94495eb319a85b70a68b85e2389a95bb3555c71c49025b78c691a854a7e6464" dependencies = [ "bytes", + "either", "futures", "futures-timer", - "getrandom 0.2.10", + "getrandom", "instant", "libp2p-allow-block-list", "libp2p-autonat", @@ -2434,9 +2450,12 @@ dependencies = [ "libp2p-quic", "libp2p-swarm", "libp2p-tcp", + "libp2p-upnp", "libp2p-yamux", "multiaddr", "pin-project", + "rw-stream-sink", + "thiserror", ] [[package]] @@ -2512,10 +2531,11 @@ dependencies = [ [[package]] name = "libp2p-dns" -version = "0.40.0" +version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd4394c81c0c06d7b4a60f3face7e8e8a9b246840f98d2c80508d0721b032147" +checksum = "e6a18db73084b4da2871438f6239fef35190b05023de7656e877c18a00541a3b" dependencies = [ + "async-trait", "futures", "libp2p-core", "libp2p-identity", @@ -2527,13 +2547,14 @@ dependencies = [ [[package]] name = "libp2p-identify" -version = "0.43.0" +version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a29675a32dbcc87790db6cf599709e64308f1ae9d5ecea2d259155889982db8" +checksum = "45a96638a0a176bec0a4bcaebc1afa8cf909b114477209d7456ade52c61cd9cd" dependencies = [ "asynchronous-codec", "either", "futures", + "futures-bounded", "futures-timer", "libp2p-core", "libp2p-identity", @@ -2580,9 +2601,9 @@ dependencies = [ "log", "rand 0.8.5", "smallvec", - "socket2 0.5.4", + "socket2 0.5.5", "tokio", - "trust-dns-proto", + "trust-dns-proto 0.22.0", "void", ] @@ -2604,12 +2625,12 @@ dependencies = [ [[package]] name = "libp2p-noise" -version = "0.43.1" +version = "0.43.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71ce70757f2c0d82e9a3ef738fb10ea0723d16cec37f078f719e2c247704c1bb" +checksum = "d2eeec39ad3ad0677551907dd304b2f13f17208ccebe333bef194076cd2e8921" dependencies = [ "bytes", - "curve25519-dalek 4.1.1", + "curve25519-dalek", "futures", "libp2p-core", "libp2p-identity", @@ -2647,9 +2668,9 @@ dependencies = [ [[package]] name = "libp2p-quic" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cb763e88f9a043546bfebd3575f340e7dd3d6c1b2cf2629600ec8965360c63a" +checksum = "130d451d83f21b81eb7b35b360bc7972aeafb15177784adc56528db082e6b927" dependencies = [ "bytes", "futures", @@ -2662,18 +2683,18 @@ dependencies = [ "parking_lot", "quinn", "rand 0.8.5", - "ring", + "ring 0.16.20", "rustls", - "socket2 0.5.4", + "socket2 0.5.5", "thiserror", "tokio", ] [[package]] name = "libp2p-request-response" -version = "0.25.1" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e2cb9befb57e55f53d9463a6ea9b1b8a09a48174ad7be149c9cbebaa5e8e9b" +checksum = "d8e3b4d67870478db72bac87bfc260ee6641d0734e0e3e275798f089c3fecfd4" dependencies = [ "async-trait", "futures", @@ -2689,9 +2710,9 @@ dependencies = [ [[package]] name = "libp2p-swarm" -version = "0.43.5" +version = "0.43.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab94183f8fc2325817835b57946deb44340c99362cd4606c0a5717299b2ba369" +checksum = "580189e0074af847df90e75ef54f3f30059aedda37ea5a1659e8b9fca05c0141" dependencies = [ "either", "fnv", @@ -2720,14 +2741,14 @@ dependencies = [ "proc-macro-warning", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] name = "libp2p-tcp" -version = "0.40.0" +version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09bfdfb6f945c5c014b87872a0bdb6e0aef90e92f380ef57cd9013f118f9289d" +checksum = "b558dd40d1bcd1aaaed9de898e9ec6a436019ecc2420dd0016e712fbb61c5508" dependencies = [ "futures", "futures-timer", @@ -2736,7 +2757,7 @@ dependencies = [ "libp2p-core", "libp2p-identity", "log", - "socket2 0.5.4", + "socket2 0.5.5", "tokio", ] @@ -2751,7 +2772,7 @@ dependencies = [ "libp2p-core", "libp2p-identity", "rcgen", - "ring", + "ring 0.16.20", "rustls", "rustls-webpki", "thiserror", @@ -2759,6 +2780,22 @@ dependencies = [ "yasna", ] +[[package]] +name = "libp2p-upnp" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82775a47b34f10f787ad3e2a22e2c1541e6ebef4fe9f28f3ac553921554c94c1" +dependencies = [ + "futures", + "futures-timer", + "igd-next", + "libp2p-core", + "libp2p-swarm", + "log", + "tokio", + "void", +] + [[package]] name = "libp2p-yamux" version = "0.44.1" @@ -2772,6 +2809,17 @@ dependencies = [ "yamux", ] +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall 0.4.1", +] + [[package]] name = "librocksdb-sys" version = "0.11.0+8.1.1" @@ -2829,9 +2877,9 @@ checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg 1.1.0", "scopeguard", @@ -2845,11 +2893,11 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "lru" -version = "0.10.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718e8fae447df0c7e1ba7f5189829e63fd536945c8988d61444c19039f16b670" +checksum = "1efa59af2ddfad1854ae27d75009d538d0998b4b2fd47083e743ac1a10e46c60" dependencies = [ - "hashbrown 0.13.2", + "hashbrown 0.14.2", ] [[package]] @@ -2914,7 +2962,7 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" dependencies = [ - "autocfg", + "autocfg 1.1.0", "rawpointer", ] @@ -2925,7 +2973,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest 0.10.7", + "digest", ] [[package]] @@ -3003,13 +3051,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.48.0", ] @@ -3085,7 +3133,7 @@ dependencies = [ "num-complex", "num-rational", "num-traits", - "rand", + "rand 0.8.5", "rand_distr", "simba", "typenum", @@ -3185,7 +3233,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "cfg-if", "libc", ] @@ -3212,7 +3260,7 @@ version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "crossbeam-channel", "filetime", "fsevent-sys", @@ -3299,7 +3347,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ - "autocfg", + "autocfg 1.1.0", "num-integer", "num-traits", ] @@ -3362,9 +3410,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.93" +version = "0.9.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9" dependencies = [ "cc", "libc", @@ -3450,7 +3498,7 @@ dependencies = [ "futures-util", "once_cell", "opentelemetry_api", - "ordered-float 3.9.1", + "ordered-float 3.9.2", "percent-encoding", "rand 0.8.5", "regex", @@ -3467,18 +3515,18 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ordered-float" -version = "2.10.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" dependencies = [ "num-traits", ] [[package]] name = "ordered-float" -version = "3.9.1" +version = "3.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a54938017eacd63036332b4ae5c8a49fc8c0c1d6d629893057e4f13609edd06" +checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc" dependencies = [ "num-traits", ] @@ -3510,9 +3558,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" @@ -3526,13 +3574,13 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", + "redox_syscall 0.4.1", "smallvec", "windows-targets", ] @@ -3555,7 +3603,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55e602c9eaa5bd2b89f5214636de0a845ddb2000db1cb429887eac12dbcd5996" dependencies = [ - "ordered-float 3.9.1", + "ordered-float 3.9.2", "serde", ] @@ -3591,9 +3639,9 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pest" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c022f1e7b65d6a24c0dbbd5fb344c66881bc01f3e5ae74a1c8100f2f985d98a4" +checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" dependencies = [ "memchr", "thiserror", @@ -3602,9 +3650,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35513f630d46400a977c4cb58f78e1bfbe01434316e60c37d27b9ad6139c66d8" +checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" dependencies = [ "pest", "pest_generator", @@ -3612,22 +3660,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9fc1b9e7057baba189b5c626e2d6f40681ae5b6eb064dc7c7834101ec8123a" +checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] name = "pest_meta" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df74e9e7ec4053ceb980e7c0c8bd3594e977fde1af91daba9c928e8e8c6708d" +checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" dependencies = [ "once_cell", "pest", @@ -3657,7 +3705,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -3701,9 +3749,9 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "platforms" -version = "3.1.2" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8" +checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0" [[package]] name = "polling" @@ -3755,6 +3803,12 @@ dependencies = [ "universal-hash 0.4.0", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -3768,7 +3822,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -3803,7 +3857,7 @@ checksum = "3d1eaa7fa0aa1929ffdf7eeb6eac234dde6268914a14ad44d23521ab6a9b258e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -3835,7 +3889,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -3912,7 +3966,7 @@ checksum = "2c78e758510582acc40acb90458401172d41f1016f8c9dde89e49677afb7eec1" dependencies = [ "bytes", "rand 0.8.5", - "ring", + "ring 0.16.20", "rustc-hash", "rustls", "slab", @@ -3929,7 +3983,7 @@ checksum = "055b4e778e8feb9f93c4e439f71dc2156ef13360b432b799e179a8c4cdf0b1d7" dependencies = [ "bytes", "libc", - "socket2 0.5.4", + "socket2 0.5.5", "tracing", "windows-sys 0.48.0", ] @@ -4020,7 +4074,17 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.10", + "getrandom", +] + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", ] [[package]] @@ -4086,6 +4150,12 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "rayon" version = "1.8.0" @@ -4113,7 +4183,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b" dependencies = [ "pem", - "ring", + "ring 0.16.20", "time", "yasna", ] @@ -4129,30 +4199,30 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_users" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ - "getrandom 0.2.10", - "redox_syscall 0.2.16", + "getrandom", + "libredox", "thiserror", ] @@ -4170,14 +4240,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.0" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d119d7c7ca818f8a53c300863d4f87566aac09943aef5b355bb83969dae75d87" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.1", - "regex-syntax 0.8.0", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", ] [[package]] @@ -4191,13 +4261,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465c6fc0621e4abc4187a2bda0937bfd4f722c2730b29562e19689ea796c9a4b" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.0", + "regex-syntax 0.8.2", ] [[package]] @@ -4208,9 +4278,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3cbb081b9784b07cceb8824c8583f86db4814d172ab043f3c23f7dc600bf83d" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "region" @@ -4253,11 +4323,25 @@ dependencies = [ "libc", "once_cell", "spin 0.5.2", - "untrusted", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.48.0", +] + [[package]] name = "rkyv" version = "0.7.42" @@ -4310,16 +4394,14 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" +checksum = "86ef35bf3e7fe15a53c4ab08a998e42271eab13eb0db224126bc7bc4c4bad96d" dependencies = [ - "byteorder", "const-oid", - "digest 0.10.7", + "digest", "num-bigint-dig", "num-integer", - "num-iter", "num-traits", "pkcs1", "pkcs8", @@ -4387,9 +4469,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.24" +version = "0.37.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4279d76516df406a8bd37e7dff53fd37d1a093f997a3c34a5c21658c126db06d" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ "bitflags 1.3.2", "errno", @@ -4401,11 +4483,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.18" +version = "0.38.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a74ee2d7c2581cd139b42447d7d9389b889bdaad3a73f1ebb16f2a3237bb19c" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "errno", "libc", "linux-raw-sys 0.4.10", @@ -4414,12 +4496,12 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.7" +version = "0.21.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" dependencies = [ "log", - "ring", + "ring 0.17.5", "rustls-webpki", "sct", ] @@ -4430,17 +4512,17 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64 0.21.4", + "base64 0.21.5", ] [[package]] name = "rustls-webpki" -version = "0.101.6" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring", - "untrusted", + "ring 0.17.5", + "untrusted 0.9.0", ] [[package]] @@ -4501,12 +4583,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring", - "untrusted", + "ring 0.17.5", + "untrusted 0.9.0", ] [[package]] @@ -4532,9 +4614,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.188" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" dependencies = [ "serde_derive", ] @@ -4552,9 +4634,9 @@ dependencies = [ [[package]] name = "serde-wasm-bindgen" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30c9933e5689bd420dc6c87b7a1835701810cbc10cd86a26e4da45b73e6b1d78" +checksum = "17ba92964781421b6cef36bf0d7da26d201e96d84e1b10e7ae6ed416e516906d" dependencies = [ "js-sys", "serde", @@ -4572,20 +4654,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -4594,9 +4676,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" dependencies = [ "serde", ] @@ -4615,15 +4697,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237" +checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" dependencies = [ - "base64 0.21.4", + "base64 0.21.5", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.0.2", + "indexmap 2.1.0", "serde", "serde_json", "serde_with_macros", @@ -4632,14 +4714,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c" +checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -4650,7 +4732,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", + "digest", ] [[package]] @@ -4661,7 +4743,7 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", + "digest", ] [[package]] @@ -4708,6 +4790,19 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simba" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b7840f121a46d63066ee7a99fc81dcabbc6105e437cae43528cea199b5a05f" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + [[package]] name = "simdutf8" version = "0.1.4" @@ -4757,7 +4852,7 @@ dependencies = [ "chacha20poly1305 0.9.1", "curve25519-dalek", "rand_core 0.6.4", - "ring", + "ring 0.16.20", "rustc_version", "sha2", "subtle", @@ -4765,9 +4860,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", @@ -4775,9 +4870,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", "windows-sys 0.48.0", @@ -4838,7 +4933,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.6", "atoi", "byteorder", "bytes", @@ -4854,7 +4949,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.0.2", + "indexmap 2.1.0", "log", "memchr", "once_cell", @@ -4920,12 +5015,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" dependencies = [ "atoi", - "base64 0.21.4", - "bitflags 2.4.0", + "base64 0.21.5", + "bitflags 2.4.1", "byteorder", "bytes", "crc", - "digest 0.10.7", + "digest", "dotenvy", "either", "futures-channel", @@ -4962,8 +5057,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" dependencies = [ "atoi", - "base64 0.21.4", - "bitflags 2.4.0", + "base64 0.21.5", + "bitflags 2.4.1", "byteorder", "crc", "dotenvy", @@ -5038,20 +5133,21 @@ dependencies = [ "lazy_static", "nalgebra", "num-traits", - "rand", + "rand 0.8.5", ] [[package]] name = "stretto" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63eada6d62b660f5c1d4862c180ae70193de86df12386eee74da694ae2177583" +checksum = "51481a2abc8af47b7447b39b221ce806728e1dcbbb1354b0d5bacb1e5e1f72de" dependencies = [ "async-channel", "async-io", "atomic", "crossbeam-channel", "futures", + "getrandom", "parking_lot", "rand 0.8.5", "seahash", @@ -5097,9 +5193,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -5164,41 +5260,41 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.11" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" +checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" [[package]] name = "tempfile" -version = "3.8.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand 2.0.1", - "redox_syscall 0.3.5", - "rustix 0.38.18", + "redox_syscall 0.4.1", + "rustix 0.38.21", "windows-sys 0.48.0", ] [[package]] name = "thiserror" -version = "1.0.49" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.49" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -5229,18 +5325,19 @@ dependencies = [ "byteorder", "integer-encoding", "log", - "ordered-float 2.10.0", + "ordered-float 2.10.1", "threadpool", ] [[package]] name = "time" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", "itoa", + "powerfmt", "serde", "time-core", "time-macros", @@ -5290,7 +5387,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.4", + "socket2 0.5.5", "tokio-macros", "windows-sys 0.48.0", ] @@ -5303,7 +5400,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -5331,15 +5428,16 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", + "tracing", ] [[package]] @@ -5353,11 +5451,11 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.2" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +checksum = "8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc" dependencies = [ - "indexmap 2.0.2", + "indexmap 2.1.0", "serde", "serde_spanned", "toml_datetime", @@ -5366,20 +5464,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.20.2" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.0.2", + "indexmap 2.1.0", "serde", "serde_spanned", "toml_datetime", @@ -5408,7 +5506,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "bytes", "futures-core", "futures-util", @@ -5441,11 +5539,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -5454,20 +5551,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", @@ -5485,12 +5582,12 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" dependencies = [ - "lazy_static", "log", + "once_cell", "tracing-core", ] @@ -5528,16 +5625,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "triomphe" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee8098afad3fb0c54a9007aab6804558410503ad676d4633f9c2559a00ac0f" -dependencies = [ - "serde", - "stable_deref_trait", -] - [[package]] name = "trust-dns-proto" version = "0.22.0" @@ -5547,7 +5634,7 @@ dependencies = [ "async-trait", "cfg-if", "data-encoding", - "enum-as-inner", + "enum-as-inner 0.5.1", "futures-channel", "futures-io", "futures-util", @@ -5556,7 +5643,7 @@ dependencies = [ "lazy_static", "rand 0.8.5", "smallvec", - "socket2 0.4.9", + "socket2 0.4.10", "thiserror", "tinyvec", "tokio", @@ -5566,9 +5653,9 @@ dependencies = [ [[package]] name = "trust-dns-proto" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "559ac980345f7f5020883dd3bcacf176355225e01916f8c2efecad7534f682c6" +checksum = "3119112651c157f4488931a01e586aa459736e9d6046d3bd9105ffb69352d374" dependencies = [ "async-trait", "cfg-if", @@ -5591,15 +5678,15 @@ dependencies = [ [[package]] name = "trust-dns-resolver" -version = "0.22.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe" +checksum = "10a3e6c3aff1718b3c73e395d1f35202ba2ffa847c6a62eea0db8fb4cfe30be6" dependencies = [ "cfg-if", "futures-util", "ipconfig", - "lazy_static", "lru-cache", + "once_cell", "parking_lot", "rand 0.8.5", "resolv-conf", @@ -5607,7 +5694,7 @@ dependencies = [ "thiserror", "tokio", "tracing", - "trust-dns-proto", + "trust-dns-proto 0.23.2", ] [[package]] @@ -5749,6 +5836,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.4.1" @@ -5780,7 +5873,7 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" @@ -5833,12 +5926,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -5847,9 +5934,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" dependencies = [ "cfg-if", "serde", @@ -5859,16 +5946,16 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", "wasm-bindgen-shared", ] @@ -5897,9 +5984,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5907,37 +5994,37 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" [[package]] name = "wasm-encoder" -version = "0.33.2" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34180c89672b3e4825c3a8db4b61a674f1447afd5fe2445b2d22c3d8b6ea086c" +checksum = "1ba64e81215916eaeb48fee292f29401d69235d62d8b8fd92a7b2844ec5ae5f7" dependencies = [ "leb128", ] [[package]] name = "wasmer" -version = "4.2.2" +version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e626f958755a90a6552b9528f59b58a62ae288e6c17fcf40e99495bc33c60f0" +checksum = "50cb1ae2956aac1fbbcf334c543c1143cdf7d5b0a5fb6c3d23a17bf37dd1f47b" dependencies = [ "bytes", "cfg-if", @@ -5964,9 +6051,9 @@ dependencies = [ [[package]] name = "wasmer-compiler" -version = "4.2.2" +version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "848e1922694cf97f4df680a0534c9d72c836378b5eb2313c1708fe1a75b40044" +checksum = "12fd9aeef339095798d1e04957d5657d97490b1112f145cbf08b98f6393b4a0a" dependencies = [ "backtrace", "bytes", @@ -5991,9 +6078,9 @@ dependencies = [ [[package]] name = "wasmer-compiler-cranelift" -version = "4.2.2" +version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d96bce6fad15a954edcfc2749b59e47ea7de524b6ef3df392035636491a40b4" +checksum = "344f5f1186c122756232fe7f156cc8d2e7bf333d5a658e81e25efa3415c26d07" dependencies = [ "cranelift-codegen", "cranelift-entity", @@ -6010,9 +6097,9 @@ dependencies = [ [[package]] name = "wasmer-derive" -version = "4.2.2" +version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f08f80d166a9279671b7af7a09409c28ede2e0b4e3acabbf0e3cb22c8038ba7" +checksum = "2ac8c1f2dc0ed3c7412a5546e468365184a461f8ce7dfe2a707b621724339f91" dependencies = [ "proc-macro-error", "proc-macro2", @@ -6022,9 +6109,9 @@ dependencies = [ [[package]] name = "wasmer-types" -version = "4.2.2" +version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae2c892882f0b416783fb4310e5697f5c30587f6f9555f9d4f2be85ab39d5d3d" +checksum = "5a57ecbf218c0a9348d4dfbdac0f9d42d9201ae276dffb13e61ea4ff939ecce7" dependencies = [ "bytecheck", "enum-iterator", @@ -6038,9 +6125,9 @@ dependencies = [ [[package]] name = "wasmer-vm" -version = "4.2.2" +version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c0a9a57b627fb39e5a491058d4365f099bc9b140031c000fded24a3306d9480" +checksum = "60c3513477bc0097250f6e34a640e2a903bb0ee57e6bb518c427f72c06ac7728" dependencies = [ "backtrace", "cc", @@ -6076,9 +6163,9 @@ dependencies = [ [[package]] name = "wast" -version = "66.0.0" +version = "64.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0da7529bb848d58ab8bf32230fc065b363baee2bd338d5e58c589a1e7d83ad07" +checksum = "a259b226fd6910225aa7baeba82f9d9933b6d00f2ce1b49b80fa4214328237cc" dependencies = [ "leb128", "memchr", @@ -6088,18 +6175,18 @@ dependencies = [ [[package]] name = "wat" -version = "1.0.75" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4780374047c65b6b6e86019093fe80c18b66825eb684df778a4e068282a780e7" +checksum = "53253d920ab413fca1c7dc2161d601c79b4fdf631d0ba51dd4343bf9b556c3f6" dependencies = [ "wast", ] [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" dependencies = [ "js-sys", "wasm-bindgen", @@ -6116,13 +6203,11 @@ dependencies = [ [[package]] name = "wg" -version = "0.3.2" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f390449c16e0679435fc97a6b49d24e67f09dd05fea1de54db1b60902896d273" +checksum = "232b936590098c53587078a25ed087d5cd3aa18ec65c85bb42c6ae335263e15b" dependencies = [ "atomic-waker", - "parking_lot", - "triomphe", ] [[package]] @@ -6133,9 +6218,9 @@ checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" [[package]] name = "wide" -version = "0.7.11" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa469ffa65ef7e0ba0f164183697b89b854253fd31aeb92358b7b6155177d62f" +checksum = "c68938b57b33da363195412cfc5fc37c9ed49aa9cfe2156fde64b8d2c9498242" dependencies = [ "bytemuck", "safe_arch", @@ -6180,22 +6265,19 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.34.0" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45296b64204227616fdbf2614cefa4c236b98ee64dfaaaa435207ed99fe7829f" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" dependencies = [ - "windows_aarch64_msvc 0.34.0", - "windows_i686_gnu 0.34.0", - "windows_i686_msvc 0.34.0", - "windows_x86_64_gnu 0.34.0", - "windows_x86_64_msvc 0.34.0", + "windows-core", + "windows-targets", ] [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ "windows-targets", ] @@ -6249,12 +6331,6 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd761fd3eb9ab8cc1ed81e56e567f02dd82c4c837e48ac3b2181b9ffc5060807" -[[package]] -name = "windows_aarch64_msvc" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -6267,12 +6343,6 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cab0cf703a96bab2dc0c02c0fa748491294bf9b7feb27e1f4f96340f208ada0e" -[[package]] -name = "windows_i686_gnu" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -6285,12 +6355,6 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cfdbe89cc9ad7ce618ba34abc34bbb6c36d99e96cae2245b7943cd75ee773d0" -[[package]] -name = "windows_i686_msvc" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -6303,12 +6367,6 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4dd9b0c0e9ece7bb22e84d70d01b71c6d6248b81a3c60d11869451b4cb24784" -[[package]] -name = "windows_x86_64_gnu" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -6327,12 +6385,6 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff1e4aa646495048ec7f3ffddc411e1d829c026a2ec62b39da15c1055e406eaa" -[[package]] -name = "windows_x86_64_msvc" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -6341,9 +6393,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.16" +version = "0.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711d82167854aff2018dfd193aa0fef5370f456732f0d5a0c59b0f1b4b907" +checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" dependencies = [ "memchr", ] @@ -6369,9 +6421,9 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "1.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" +checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" dependencies = [ "curve25519-dalek", "rand_core 0.6.4", @@ -6405,6 +6457,21 @@ dependencies = [ "libc", ] +[[package]] +name = "xml-rs" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + [[package]] name = "xxhash-rust" version = "0.8.7" @@ -6453,6 +6520,26 @@ dependencies = [ "time", ] +[[package]] +name = "zerocopy" +version = "0.7.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "zeroize" version = "1.6.0" @@ -6470,5 +6557,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] From c8cc46d474946fa4dbbb13849f76b73ece1fa5d9 Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Wed, 25 Oct 2023 10:11:25 -0500 Subject: [PATCH 69/76] tm: sliding windows --- .../core/src/topology/connection_evaluator.rs | 89 -------- crates/core/src/topology/metric.rs | 103 --------- crates/core/src/topology/mod.rs | 110 ++------- crates/core/src/topology/sliding_window.rs | 216 ++++++++++++++++++ crates/core/src/topology/small_world_rand.rs | 23 -- 5 files changed, 229 insertions(+), 312 deletions(-) delete mode 100644 crates/core/src/topology/connection_evaluator.rs delete mode 100644 crates/core/src/topology/metric.rs create mode 100644 crates/core/src/topology/sliding_window.rs diff --git a/crates/core/src/topology/connection_evaluator.rs b/crates/core/src/topology/connection_evaluator.rs deleted file mode 100644 index d5e3c2f25..000000000 --- a/crates/core/src/topology/connection_evaluator.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::collections::VecDeque; -use std::time::{Duration, Instant}; - -/// `ConnectionEvaluator` is used to evaluate connection scores within a specified time window. -/// -/// The evaluator records scores and determines whether a given score is better (i.e., lower) than -/// any other scores within a predefined time window. A score is considered better if it's lower -/// than all other scores in the time window, or if no scores were recorded within the window's -/// duration. -/// -/// In the Freenet context, this will be used to titrate the rate of new connection requests accepted -/// by a node. The node will only accept a new connection if the score of the connection is better -/// than all other scores within the time window. -struct ConnectionEvaluator { - scores: VecDeque<(Instant, f64)>, - window_duration: Duration, -} - -impl ConnectionEvaluator { - pub fn new(window_duration: Duration) -> Self { - ConnectionEvaluator { - scores: VecDeque::new(), - window_duration, - } - } - - pub fn record(&mut self, score: f64) -> bool { - self.record_with_current_time(score, Instant::now()) - } - - fn record_with_current_time(&mut self, score: f64, current_time: Instant) -> bool { - self.remove_outdated_scores(current_time); - - let is_better = self.scores.is_empty() || self.scores.iter().all(|&(_, s)| score < s); - self.scores.push_back((current_time, score)); - - is_better - } - - fn remove_outdated_scores(&mut self, current_time: Instant) { - while let Some(&(time, _)) = self.scores.front() { - if current_time.duration_since(time) > self.window_duration { - self.scores.pop_front(); - } else { - break; - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_record_first_score() { - let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(10)); - assert_eq!(evaluator.record(5.0), true); - } - - #[test] - fn test_record_within_window_duration() { - let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(10)); - - let start_time = Instant::now(); - evaluator.record_with_current_time(5.0, start_time); - assert_eq!(evaluator.record_with_current_time(6.0, start_time + Duration::from_secs(5)), false); - } - - #[test] - fn test_record_outside_window_duration() { - let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(10)); - - let start_time = Instant::now(); - evaluator.record_with_current_time(5.0, start_time); - assert_eq!(evaluator.record_with_current_time(6.0, start_time + Duration::from_secs(11)), true); - } - - #[test] - fn test_remove_outdated_scores() { - let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(10)); - - let start_time = Instant::now(); - evaluator.record_with_current_time(5.0, start_time); - evaluator.record_with_current_time(6.0, start_time + Duration::from_secs(5)); - evaluator.record_with_current_time(4.5, start_time + Duration::from_secs(11)); - assert_eq!(evaluator.scores.len(), 2); - } -} diff --git a/crates/core/src/topology/metric.rs b/crates/core/src/topology/metric.rs deleted file mode 100644 index d6676a401..000000000 --- a/crates/core/src/topology/metric.rs +++ /dev/null @@ -1,103 +0,0 @@ -extern crate nalgebra as na; - -/* -This module provides a metric called `SmallWorldDeviationMetric` to quantify -how well a given peer's connections in a peer-to-peer network approximate an -ideal small-world topology. The ideal topology is based on a 1D ring model -where the probability density of a connection at distance `r` is proportional -to `r^-1`. - -The metric is calculated as follows: -1. A normalization constant `C` is calculated to ensure that the total - probability of the ideal distribution is 1. -2. For a range of distances `X`, the ideal and actual proportions of peers - within `X` are calculated. -3. The area between the ideal and actual curves is calculated using trapezoidal - integration, yielding the `SmallWorldDeviationMetric`. - -A metric value close to 0 indicates that the actual distribution closely matches -the ideal. A positive value indicates a lack of long-range links, and a negative -value indicates a lack of short-range links. -*/ - -use crate::ring::Distance; - -/// Calculate the normalization constant C for the ideal r^-1 distribution. -/// The integral is approximated using trapezoidal rule. -fn calculate_normalization_constant() -> f64 { - let mut sum = 0.0; - let step = 0.01; - let mut r = step; - while r <= 0.5 { - sum += 1.0 / r; - r += step; - } - sum *= step; // Multiply by step size to complete the trapezoidal rule - 1.0 / sum // Normalize so that total probability is 1 -} - -/// Calculate the ideal proportion of peers within distance X for the r^-1 distribution. -fn ideal_proportion_within_x(x: f64, c: f64) -> f64 { - c * (x.ln() - 0.01f64.ln()) -} - -/// Calculate the actual proportion of peers within distance X. -fn actual_proportion_within_x(connection_distances: &[Distance], x: &Distance) -> f64 { - let count = connection_distances.iter().filter(|&&r| r <= *x).count(); - count as f64 / connection_distances.len() as f64 -} - -/// Calculate the SmallWorldDeviationMetric as the area between the ideal and actual curves. -/// Uses trapezoidal rule for integration. -/// -/// Returns a metric where: -/// - 0.0 is ideal, indicating a perfect match with the ideal small-world topology. -/// - A negative value indicates the network is not clustered enough (lacks short-range links). -/// - A positive value indicates the network is too clustered (lacks long-range links). -pub(crate) fn measure_small_worldness(connection_distances: &[Distance]) -> f64 { - let c = calculate_normalization_constant(); - let mut sum = 0.0; - let step = 0.01; - let mut x = step; - while x <= 0.5 { - let ideal = ideal_proportion_within_x(x, c); - let actual = actual_proportion_within_x(&connection_distances, &Distance::new(x)); - sum += actual - ideal; - x += step; - } - sum * step // Multiply by step size to complete the trapezoidal rule -} - -#[cfg(test)] -mod tests { - use itertools::Itertools; - - use super::*; - - #[test] - fn test_small_world_deviation_metric() { - // Ideal case: distances drawn from an r^-1 distribution - let ideal_distances: Vec = vec![0.1, 0.2, 0.05, 0.4, 0.3] - .iter() - .map(|d| Distance::new(*d)) - .collect_vec(); - let metric_ideal = measure_small_worldness(&ideal_distances); - assert!(metric_ideal.abs() < 0.1); // The metric should be close to zero for the ideal case - - // Non-ideal case 1: mostly short distances - let non_ideal_1: Vec = vec![0.01, 0.02, 0.03, 0.04, 0.05] - .iter() - .map(|d| Distance::new(*d)) - .collect_vec(); - let metric_non_ideal_1 = measure_small_worldness(&non_ideal_1); - assert!(metric_non_ideal_1 > 0.1); // The metric should be significantly positive - - // Non-ideal case 2: mostly long distances - let non_ideal_2: Vec = vec![0.4, 0.45, 0.48, 0.49, 0.5] - .iter() - .map(|d| Distance::new(*d)) - .collect_vec(); - let metric_non_ideal_2 = measure_small_worldness(&non_ideal_2); - assert!(metric_non_ideal_2 < -0.1); // The metric should be significantly negative - } -} diff --git a/crates/core/src/topology/mod.rs b/crates/core/src/topology/mod.rs index 45a27bf58..2e27dd917 100644 --- a/crates/core/src/topology/mod.rs +++ b/crates/core/src/topology/mod.rs @@ -1,108 +1,24 @@ #![allow(unused_variables, dead_code)] -mod metric; -mod small_world_rand; -mod connection_evaluator; - -use rand::Rng; - -use crate::ring::*; - -use self::small_world_rand::random_link_distance; - -const DEFAULT_MIN_DISTANCE: f64 = 1.0 / 1_000.0; +/* + NOTES -pub(crate) enum TopologyStrategy { - Random, - LoadBalancing, -} +Distribution of connections should mirror the distribution of inbound and locally generated requests. -pub(crate) struct JoinTargetInfo { - pub target: Location, - pub threshold: Distance, - pub strategy: TopologyStrategy, -} +For outbound join requests this means random selection of outbound requests in proportion to outbound request density. -impl TopologyStrategy { - pub(crate) fn select_join_target_location( - &self, - my_location: &Location, - peer_statistics: &PeerStatistics, - ) -> JoinTargetInfo { - match self { - TopologyStrategy::Random => random_strategy(my_location, peer_statistics), - TopologyStrategy::LoadBalancing => { - load_balancing_strategy(my_location, peer_statistics) - } - } - } -} +For inbound requests this means selecting the inbound request in the highest density region of the keyspace. -pub(crate) fn random_strategy( - my_location: &Location, - peer_statistics: &PeerStatistics, -) -> JoinTargetInfo { - // Determine distance to closest neighboring peer - let mut min_distance = Distance::new(DEFAULT_MIN_DISTANCE); - for peer in peer_statistics.peers.iter() { - let distance = my_location.distance(&peer.location); - if distance < min_distance { - min_distance = distance; - } - } - let distance_to_target = random_link_distance(min_distance); +a - 100 requests / sec +b - 50 requests / sec - let direction = if rand::thread_rng().gen_bool(0.5) { - 1.0 - } else { - -1.0 - }; - let target = Location::new_rounded(my_location.as_f64() * direction); - let threshold = Distance::new(distance_to_target.as_f64() / 2.0); - JoinTargetInfo { - target, - threshold, - strategy: TopologyStrategy::Random, - } -} +Distance: 0.01 -pub(crate) fn small_world_metric_strategy( - my_location: &Location, - peer_statistics: &PeerStatistics, -) -> JoinTargetInfo { - unimplemented!() -} +Density at point c between a and b = ((distance(a, c) * req(b) + distance(b, c) * req(a)) / (distance(a, b)) / distance(a, b) +distance(a, b) cancel out -pub(crate) fn load_balancing_strategy( - my_location: &Location, - peer_statistics: &PeerStatistics, -) -> JoinTargetInfo { - unimplemented!() -} -pub(crate) fn select_strategy(num_neighbors: usize) -> TopologyStrategy { - if num_neighbors < 10 { - TopologyStrategy::Random - } else { - // Randomly select between the three strategies based on your criteria - unimplemented!() - } -} +*/ -pub(crate) struct RequestsPerMinute(f64); - -pub(crate) struct PeerInfo { - pub location: Location, - pub requests_per_minute: RequestsPerMinute, - pub strategy: TopologyStrategy, -} - -pub(crate) struct PeerStatistics { - pub(crate) peers: Vec, -} - -impl PeerStatistics { - pub(crate) fn new() -> Self { - Self { peers: Vec::new() } - } -} +mod sliding_window; +mod small_world_rand; diff --git a/crates/core/src/topology/sliding_window.rs b/crates/core/src/topology/sliding_window.rs new file mode 100644 index 000000000..4b3363230 --- /dev/null +++ b/crates/core/src/topology/sliding_window.rs @@ -0,0 +1,216 @@ +use std::collections::{BTreeMap, LinkedList}; +use thiserror::Error; +use crate::ring::{Location, Distance}; + +/// Sliding window data structure for calculating location density in a ring +struct SlidingWindow { + ordered_map: BTreeMap, + list: LinkedList, + window_size: usize, + samples: usize, +} + +impl SlidingWindow { + pub fn new(window_size: usize) -> Self { + Self { + ordered_map: BTreeMap::new(), + list: LinkedList::new(), + window_size, + samples: 0, + } + } + + pub fn sample(&mut self, value: Location) { + self.samples += 1; + + self.list.push_back(value); + *self.ordered_map.entry(value).or_insert(0) += 1; + + if self.list.len() > self.window_size { + if let Some(oldest) = self.list.pop_front() { + if let Some(count) = self.ordered_map.get_mut(&oldest) { + *count -= 1; + if *count == 0 { + self.ordered_map.remove(&oldest); + } + } + } + } + } + + pub fn density(&self, position: Location, window_radius: usize) -> std::result::Result { + if window_radius > self.samples / 2 || window_radius > self.window_size / 2 { + return Err(DensityError::WindowTooBig { + samples: self.samples, + window_size: self.window_size, + }); + } + + // Create ranges for lower and upper bounds + let lower_range = (std::ops::Bound::Unbounded::, std::ops::Bound::Included(position)); + let upper_range = (std::ops::Bound::Excluded(position), std::ops::Bound::Unbounded::); + + // Iterate down from position accumulating the count to obtain the lower bound after window_radius samples + let lower_iter = self.ordered_map.range(lower_range).rev(); + // Lower iter size is the total of the values in lower_iter + let lower_iter_sz = lower_iter.map(|(_, v)| v).sum::(); + + let lower_iter = self.ordered_map.range(lower_range).rev(); + + let lower_bound : Option = if lower_iter_sz >= window_radius { + let mut count = 0; + let mut lower_bound = None; + for (sample_location, sample_count) in lower_iter { + count += sample_count; + if count >= window_radius { + lower_bound = Some(*sample_location); + break; + } + } + lower_bound + } else { + // Wrap around to the top of the map and iterate down from the top of the map accumulating the + // count to obtain the lower bound after window_radius-lower_iter_sz samples + let mut count = lower_iter_sz; + let mut lower_bound = None; + for (sample_location, sample_count) in self.ordered_map.iter().rev() { + count += sample_count; + if count >= window_radius { + lower_bound = Some(*sample_location); + break; + } + } + lower_bound + }; + + // Iterate up from position accumulating the count to obtain the upper bound after window_radius samples + let upper_iter = self.ordered_map.range(upper_range); + let upper_iter_sz = upper_iter.map(|(_, v)| v).sum::(); + let upper_iter = self.ordered_map.range(upper_range); + + let upper_bound : Option = if upper_iter_sz >= window_radius { + let mut count = 0; + let mut upper_bound = None; + for (sample_location, sample_count) in upper_iter { + count += sample_count; + if count >= window_radius { + upper_bound = Some(*sample_location); + break; + } + } + upper_bound + } else { + // Wrap around to the bottom of the map and iterate up from the bottom of the map accumulating the + // count to obtain the upper bound after window_radius-upper_iter_sz samples + let mut count = upper_iter_sz; + let mut upper_bound = None; + for (sample_location, sample_count) in self.ordered_map.iter() { + count += sample_count; + if count >= window_radius { + upper_bound = Some(*sample_location); + break; + } + } + upper_bound + }; + + // Now we have the lower and upper bounds, we can calculate the density which is + // the distance between them + return match (lower_bound, upper_bound) { + (Some(lower), Some(upper)) => Ok(lower.distance(upper)), + _ => Err(DensityError::CantFindBounds), + } + + } + +} + +// Define the custom error type using thiserror +#[derive(Error, Debug)] +pub enum DensityError { + #[error("Not enough samples to determine lower and upper bounds")] + CantFindBounds, + + #[error("Window radius too big. Window radius should be <= 50% of the number of samples ({samples}) and window size ({window_size}).")] + WindowTooBig { + samples: usize, + window_size: usize, + }, +} + +// Unit tests +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sliding_window() { + let mut sw = SlidingWindow::new(5); + + sw.sample(Location::new(0.0)); + sw.sample(Location::new(0.1)); + sw.sample(Location::new(0.2)); + sw.sample(Location::new(0.3)); + sw.sample(Location::new(0.4)); + // Previous samples should have been evicted + sw.sample(Location::new(0.5)); + sw.sample(Location::new(0.6)); // <-- bottom of range + // <-- position + sw.sample(Location::new(0.7)); // <-- top of range + sw.sample(Location::new(0.8)); + sw.sample(Location::new(0.9)); + + // Verify that there are now only 5 samples + assert_eq!(sw.ordered_map.len(), 5); + + // Verify that 0.4 was evicted + assert_eq!(sw.ordered_map.contains_key(&Location::new(0.4)), false); + + // Verify density + assert_eq!(sw.density(Location::new(0.65), 1).unwrap(), Location::new(0.6).distance(Location::new(0.7))); + } + + #[test] + fn test_sliding_window_overlap() { + let mut sw = SlidingWindow::new(5); + + sw.sample(Location::new(0.0)); + sw.sample(Location::new(0.1)); + sw.sample(Location::new(0.2)); + sw.sample(Location::new(0.3)); + sw.sample(Location::new(0.4)); + // Previous samples should have been evicted + sw.sample(Location::new(0.5)); + // <-- position + sw.sample(Location::new(0.6)); + sw.sample(Location::new(0.7)); // <-- top of range + sw.sample(Location::new(0.8)); + sw.sample(Location::new(0.9)); // <-- bottom of range + + assert_eq!(sw.density(Location::new(0.55), 2).unwrap(), Location::new(0.9).distance(Location::new(0.7))); + } + + #[test] + fn test_duplicate_locations() { + let mut sw = SlidingWindow::new(5); + + sw.sample(Location::new(0.0)); + sw.sample(Location::new(0.1)); + sw.sample(Location::new(0.1)); + sw.sample(Location::new(0.3)); + sw.sample(Location::new(0.4)); + // Previous samples should have been evicted + sw.sample(Location::new(0.6)); // <-- bottom of range + sw.sample(Location::new(0.6)); + // <-- position + sw.sample(Location::new(0.7)); + sw.sample(Location::new(0.8)); // <-- top of range + sw.sample(Location::new(0.9)); + + // Verify 0.4 was evicted + assert_eq!(sw.ordered_map.contains_key(&Location::new(0.4)), false); + + // Verify density + assert_eq!(sw.density(Location::new(0.65), 2).unwrap(), Location::new(0.6).distance(Location::new(0.8))); + } +} \ No newline at end of file diff --git a/crates/core/src/topology/small_world_rand.rs b/crates/core/src/topology/small_world_rand.rs index c7d27a953..c0f84868d 100644 --- a/crates/core/src/topology/small_world_rand.rs +++ b/crates/core/src/topology/small_world_rand.rs @@ -17,7 +17,6 @@ pub(crate) fn random_link_distance(d_min: Distance) -> Distance { #[cfg(test)] mod tests { - use crate::topology::metric::measure_small_worldness; use super::*; use statrs::distribution::*; @@ -68,26 +67,4 @@ mod tests { p_value ); } - - #[test] - fn metric_test() { - let d_min = Distance::new(0.01); - let n = 1000; // Number of samples - let mut distances = vec![]; - // Generate a bunch of link distances - for _ in 0..n { - distances.push(random_link_distance(d_min)); - } - - let metric = measure_small_worldness(&distances); - - println!("Small-world deviation metric = {}", metric); - - // Check if metric is close to 0.0, indicating that the network is close to the ideal small-world topology - assert!( - metric.abs() < 0.1, - "Small-world deviation metric is too high, metric = {}", - metric - ); - } } From 130e815af74d3fe856695aad23d534cac8848955 Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Tue, 31 Oct 2023 23:53:21 -0500 Subject: [PATCH 70/76] tm: RequestDensityTracker complete and tested --- crates/core/src/ring.rs | 25 ++- crates/core/src/topology/mod.rs | 2 +- crates/core/src/topology/sliding_window.rs | 216 --------------------- 3 files changed, 22 insertions(+), 221 deletions(-) delete mode 100644 crates/core/src/topology/sliding_window.rs diff --git a/crates/core/src/ring.rs b/crates/core/src/ring.rs index 6fd9622e1..79f3412ae 100644 --- a/crates/core/src/ring.rs +++ b/crates/core/src/ring.rs @@ -20,7 +20,7 @@ use std::{ atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst}, Arc, }, - time::Duration, + time::Duration, ops::Add, }; use anyhow::bail; @@ -545,10 +545,14 @@ impl Distance { pub fn new(value: f64) -> Self { debug_assert!(!value.is_nan(), "Distance cannot be NaN"); debug_assert!( - (0.0..=0.5).contains(&value), - "Distance must be in the range [0, 0.5]" + (0.0..=1.0).contains(&value), + "Distance must be in the range [0, 1.0]" ); - Distance(value) + if value <= 0.5 { + Distance(value) + } else { + Distance(1.0 - value) + } } pub fn as_f64(&self) -> f64 { @@ -556,6 +560,19 @@ impl Distance { } } +impl Add for Distance { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + let d = self.0 + rhs.0; + if d > 0.5 { + Distance::new(1.0 - d) + } else { + Distance::new(d) + } + } +} + impl PartialEq for Distance { fn eq(&self, other: &Self) -> bool { (self.0 - other.0).abs() < f64::EPSILON diff --git a/crates/core/src/topology/mod.rs b/crates/core/src/topology/mod.rs index 2e27dd917..2137f6444 100644 --- a/crates/core/src/topology/mod.rs +++ b/crates/core/src/topology/mod.rs @@ -20,5 +20,5 @@ distance(a, b) cancel out */ -mod sliding_window; +mod request_density_tracker; mod small_world_rand; diff --git a/crates/core/src/topology/sliding_window.rs b/crates/core/src/topology/sliding_window.rs deleted file mode 100644 index 4b3363230..000000000 --- a/crates/core/src/topology/sliding_window.rs +++ /dev/null @@ -1,216 +0,0 @@ -use std::collections::{BTreeMap, LinkedList}; -use thiserror::Error; -use crate::ring::{Location, Distance}; - -/// Sliding window data structure for calculating location density in a ring -struct SlidingWindow { - ordered_map: BTreeMap, - list: LinkedList, - window_size: usize, - samples: usize, -} - -impl SlidingWindow { - pub fn new(window_size: usize) -> Self { - Self { - ordered_map: BTreeMap::new(), - list: LinkedList::new(), - window_size, - samples: 0, - } - } - - pub fn sample(&mut self, value: Location) { - self.samples += 1; - - self.list.push_back(value); - *self.ordered_map.entry(value).or_insert(0) += 1; - - if self.list.len() > self.window_size { - if let Some(oldest) = self.list.pop_front() { - if let Some(count) = self.ordered_map.get_mut(&oldest) { - *count -= 1; - if *count == 0 { - self.ordered_map.remove(&oldest); - } - } - } - } - } - - pub fn density(&self, position: Location, window_radius: usize) -> std::result::Result { - if window_radius > self.samples / 2 || window_radius > self.window_size / 2 { - return Err(DensityError::WindowTooBig { - samples: self.samples, - window_size: self.window_size, - }); - } - - // Create ranges for lower and upper bounds - let lower_range = (std::ops::Bound::Unbounded::, std::ops::Bound::Included(position)); - let upper_range = (std::ops::Bound::Excluded(position), std::ops::Bound::Unbounded::); - - // Iterate down from position accumulating the count to obtain the lower bound after window_radius samples - let lower_iter = self.ordered_map.range(lower_range).rev(); - // Lower iter size is the total of the values in lower_iter - let lower_iter_sz = lower_iter.map(|(_, v)| v).sum::(); - - let lower_iter = self.ordered_map.range(lower_range).rev(); - - let lower_bound : Option = if lower_iter_sz >= window_radius { - let mut count = 0; - let mut lower_bound = None; - for (sample_location, sample_count) in lower_iter { - count += sample_count; - if count >= window_radius { - lower_bound = Some(*sample_location); - break; - } - } - lower_bound - } else { - // Wrap around to the top of the map and iterate down from the top of the map accumulating the - // count to obtain the lower bound after window_radius-lower_iter_sz samples - let mut count = lower_iter_sz; - let mut lower_bound = None; - for (sample_location, sample_count) in self.ordered_map.iter().rev() { - count += sample_count; - if count >= window_radius { - lower_bound = Some(*sample_location); - break; - } - } - lower_bound - }; - - // Iterate up from position accumulating the count to obtain the upper bound after window_radius samples - let upper_iter = self.ordered_map.range(upper_range); - let upper_iter_sz = upper_iter.map(|(_, v)| v).sum::(); - let upper_iter = self.ordered_map.range(upper_range); - - let upper_bound : Option = if upper_iter_sz >= window_radius { - let mut count = 0; - let mut upper_bound = None; - for (sample_location, sample_count) in upper_iter { - count += sample_count; - if count >= window_radius { - upper_bound = Some(*sample_location); - break; - } - } - upper_bound - } else { - // Wrap around to the bottom of the map and iterate up from the bottom of the map accumulating the - // count to obtain the upper bound after window_radius-upper_iter_sz samples - let mut count = upper_iter_sz; - let mut upper_bound = None; - for (sample_location, sample_count) in self.ordered_map.iter() { - count += sample_count; - if count >= window_radius { - upper_bound = Some(*sample_location); - break; - } - } - upper_bound - }; - - // Now we have the lower and upper bounds, we can calculate the density which is - // the distance between them - return match (lower_bound, upper_bound) { - (Some(lower), Some(upper)) => Ok(lower.distance(upper)), - _ => Err(DensityError::CantFindBounds), - } - - } - -} - -// Define the custom error type using thiserror -#[derive(Error, Debug)] -pub enum DensityError { - #[error("Not enough samples to determine lower and upper bounds")] - CantFindBounds, - - #[error("Window radius too big. Window radius should be <= 50% of the number of samples ({samples}) and window size ({window_size}).")] - WindowTooBig { - samples: usize, - window_size: usize, - }, -} - -// Unit tests -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_sliding_window() { - let mut sw = SlidingWindow::new(5); - - sw.sample(Location::new(0.0)); - sw.sample(Location::new(0.1)); - sw.sample(Location::new(0.2)); - sw.sample(Location::new(0.3)); - sw.sample(Location::new(0.4)); - // Previous samples should have been evicted - sw.sample(Location::new(0.5)); - sw.sample(Location::new(0.6)); // <-- bottom of range - // <-- position - sw.sample(Location::new(0.7)); // <-- top of range - sw.sample(Location::new(0.8)); - sw.sample(Location::new(0.9)); - - // Verify that there are now only 5 samples - assert_eq!(sw.ordered_map.len(), 5); - - // Verify that 0.4 was evicted - assert_eq!(sw.ordered_map.contains_key(&Location::new(0.4)), false); - - // Verify density - assert_eq!(sw.density(Location::new(0.65), 1).unwrap(), Location::new(0.6).distance(Location::new(0.7))); - } - - #[test] - fn test_sliding_window_overlap() { - let mut sw = SlidingWindow::new(5); - - sw.sample(Location::new(0.0)); - sw.sample(Location::new(0.1)); - sw.sample(Location::new(0.2)); - sw.sample(Location::new(0.3)); - sw.sample(Location::new(0.4)); - // Previous samples should have been evicted - sw.sample(Location::new(0.5)); - // <-- position - sw.sample(Location::new(0.6)); - sw.sample(Location::new(0.7)); // <-- top of range - sw.sample(Location::new(0.8)); - sw.sample(Location::new(0.9)); // <-- bottom of range - - assert_eq!(sw.density(Location::new(0.55), 2).unwrap(), Location::new(0.9).distance(Location::new(0.7))); - } - - #[test] - fn test_duplicate_locations() { - let mut sw = SlidingWindow::new(5); - - sw.sample(Location::new(0.0)); - sw.sample(Location::new(0.1)); - sw.sample(Location::new(0.1)); - sw.sample(Location::new(0.3)); - sw.sample(Location::new(0.4)); - // Previous samples should have been evicted - sw.sample(Location::new(0.6)); // <-- bottom of range - sw.sample(Location::new(0.6)); - // <-- position - sw.sample(Location::new(0.7)); - sw.sample(Location::new(0.8)); // <-- top of range - sw.sample(Location::new(0.9)); - - // Verify 0.4 was evicted - assert_eq!(sw.ordered_map.contains_key(&Location::new(0.4)), false); - - // Verify density - assert_eq!(sw.density(Location::new(0.65), 2).unwrap(), Location::new(0.6).distance(Location::new(0.8))); - } -} \ No newline at end of file From c3042af1a8a094e05f6de10892aa25c8c6a9615f Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Wed, 1 Nov 2023 15:27:02 -0500 Subject: [PATCH 71/76] tm: unit tests + logging --- .../src/topology/connection_evaluator/mod.rs | 63 ++++++ .../topology/connection_evaluator/tests.rs | 42 ++++ crates/core/src/topology/mod.rs | 200 +++++++++++++++-- .../cached_density_map.rs | 32 +++ .../topology/request_density_tracker/mod.rs | 175 +++++++++++++++ .../topology/request_density_tracker/tests.rs | 201 ++++++++++++++++++ 6 files changed, 700 insertions(+), 13 deletions(-) create mode 100644 crates/core/src/topology/connection_evaluator/mod.rs create mode 100644 crates/core/src/topology/connection_evaluator/tests.rs create mode 100644 crates/core/src/topology/request_density_tracker/cached_density_map.rs create mode 100644 crates/core/src/topology/request_density_tracker/mod.rs create mode 100644 crates/core/src/topology/request_density_tracker/tests.rs diff --git a/crates/core/src/topology/connection_evaluator/mod.rs b/crates/core/src/topology/connection_evaluator/mod.rs new file mode 100644 index 000000000..18826c218 --- /dev/null +++ b/crates/core/src/topology/connection_evaluator/mod.rs @@ -0,0 +1,63 @@ +use std::collections::VecDeque; +use std::time::{Duration, Instant}; + +/// `ConnectionEvaluator` is used to evaluate connection scores within a specified time window. +/// +/// The evaluator records scores and determines whether a given score is better (higher) than +/// any other scores within a predefined time window. A score is considered better if it's higher +/// than all other scores in the time window, or if no scores were recorded within the window's +/// duration. +/// +/// In the Freenet context, this will be used to titrate the rate of new connection requests accepted +/// by a node. The node will only accept a new connection if the score of the connection is better +/// than all other scores within the time window. +pub(crate) struct ConnectionEvaluator { + scores: VecDeque<(Instant, f64)>, + window_duration: Duration, +} + +impl ConnectionEvaluator { + pub fn new(window_duration: Duration) -> Self { + ConnectionEvaluator { + scores: VecDeque::new(), + window_duration, + } + } + + pub fn record_only(&mut self, score: f64) { + self.record_only_with_current_time(score, Instant::now()); + } + + pub fn record_only_with_current_time(&mut self, score: f64, current_time: Instant) { + self.remove_outdated_scores(current_time); + self.scores.push_back((current_time, score)); + } + + pub fn record_and_eval(&mut self, score: f64) -> bool { + self.record_and_eval_with_current_time(score, Instant::now()) + } + + pub fn record_and_eval_with_current_time(&mut self, score: f64, current_time: Instant) -> bool { + self.remove_outdated_scores(current_time); + + let is_better = self.scores.is_empty() || self.scores.iter().all(|&(_, s)| score > s); + + // Important to add new score *after* checking if it's better than all other scores + self.record_only_with_current_time(score, current_time); + + is_better + } + + fn remove_outdated_scores(&mut self, current_time: Instant) { + while let Some(&(time, _)) = self.scores.front() { + if current_time.duration_since(time) > self.window_duration { + self.scores.pop_front(); + } else { + break; + } + } + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/core/src/topology/connection_evaluator/tests.rs b/crates/core/src/topology/connection_evaluator/tests.rs new file mode 100644 index 000000000..63576efc5 --- /dev/null +++ b/crates/core/src/topology/connection_evaluator/tests.rs @@ -0,0 +1,42 @@ +use super::*; + +#[test] +fn test_record_first_score() { + let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(10)); + let current_time = Instant::now(); + assert_eq!(evaluator.record_and_eval_with_current_time(5.0, current_time), true); + // Assert evaluator.scores contains the new score + assert_eq!(evaluator.scores.len(), 1); + assert_eq!(evaluator.scores[0].1, 5.0); + assert_eq!(evaluator.scores[0].0, current_time); + +} + +#[test] +fn test_record_within_window_duration() { + let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(10)); + + let start_time = Instant::now(); + evaluator.record_and_eval_with_current_time(5.0, start_time); + assert_eq!(evaluator.record_and_eval_with_current_time(6.0, start_time + Duration::from_secs(5)), false); +} + +#[test] +fn test_record_outside_window_duration() { + let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(10)); + + let start_time = Instant::now(); + evaluator.record_and_eval_with_current_time(5.0, start_time); + assert_eq!(evaluator.record_and_eval_with_current_time(6.0, start_time + Duration::from_secs(11)), true); +} + +#[test] +fn test_remove_outdated_scores() { + let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(10)); + + let start_time = Instant::now(); + evaluator.record_and_eval_with_current_time(5.0, start_time); + evaluator.record_and_eval_with_current_time(6.0, start_time + Duration::from_secs(5)); + evaluator.record_and_eval_with_current_time(4.5, start_time + Duration::from_secs(11)); + assert_eq!(evaluator.scores.len(), 2); +} diff --git a/crates/core/src/topology/mod.rs b/crates/core/src/topology/mod.rs index 2137f6444..413bfc10a 100644 --- a/crates/core/src/topology/mod.rs +++ b/crates/core/src/topology/mod.rs @@ -1,24 +1,198 @@ #![allow(unused_variables, dead_code)] -/* - NOTES +use std::{collections::BTreeMap, rc::Rc, time::{Duration, Instant}}; +use tracing::{debug, error, info}; +use request_density_tracker::cached_density_map::CachedDensityMap; +use crate::ring::{Distance, Location}; -Distribution of connections should mirror the distribution of inbound and locally generated requests. +use self::{request_density_tracker::DensityMapError, small_world_rand::random_link_distance}; -For outbound join requests this means random selection of outbound requests in proportion to outbound request density. +mod request_density_tracker; +mod small_world_rand; +mod connection_evaluator; -For inbound requests this means selecting the inbound request in the highest density region of the keyspace. +const SLOW_CONNECTION_EVALUATOR_WINDOW_DURATION: Duration = Duration::from_secs(5 * 60); +const FAST_CONNECTION_EVALUATOR_WINDOW_DURATION: Duration = Duration::from_secs(1 * 60); +const REQUEST_DENSITY_TRACKER_WINDOW_SIZE: usize = 10_000; +const REGENERATE_DENSITY_MAP_INTERVAL: Duration = Duration::from_secs(60); +const RANDOM_CLOSEST_DISTANCE: f64 = 1.0 / 1000.0; -a - 100 requests / sec -b - 50 requests / sec +/// The goal of `TopologyManager` is to select new connections such that the +/// distribution of connections in the network is as close as possible to the +/// distribution of requests in the network. +/// +/// This is done by maintaining a `RequestDensityTracker` which tracks the +/// distribution of requests in the network. The `TopologyManager` uses this +/// tracker to create a `DensityMap` which is used to evaluate the density of +/// requests at a given location. +/// +/// The `TopologyManager` uses the density map to select the best candidate +/// location, which is assumed to be close to peer connections that are +/// currently receiving a lot of requests. This should have the effect of +/// "balancing" out requests over time. +/// +/// The `TopologyManager` also uses a `ConnectionEvaluator` to evaluate whether +/// a given connection is better than all other connections within a predefined +/// time window. The goal of this is to select the best connections over time +/// from incoming join requests. +pub(crate) struct TopologyManager { + slow_connection_evaluator: connection_evaluator::ConnectionEvaluator, + fast_connection_evaluator: connection_evaluator::ConnectionEvaluator, + request_density_tracker: request_density_tracker::RequestDensityTracker, + cached_density_map: CachedDensityMap, + this_peer_location: Location, +} -Distance: 0.01 +impl TopologyManager { + pub(crate) fn new(this_peer_location : Location) -> Self { + info!("Creating a new TopologyManager instance"); + TopologyManager { + slow_connection_evaluator: connection_evaluator::ConnectionEvaluator::new(SLOW_CONNECTION_EVALUATOR_WINDOW_DURATION), + fast_connection_evaluator: connection_evaluator::ConnectionEvaluator::new(FAST_CONNECTION_EVALUATOR_WINDOW_DURATION), + request_density_tracker: request_density_tracker::RequestDensityTracker::new(REQUEST_DENSITY_TRACKER_WINDOW_SIZE), + cached_density_map: CachedDensityMap::new(REGENERATE_DENSITY_MAP_INTERVAL), + this_peer_location, + } + } -Density at point c between a and b = ((distance(a, c) * req(b) + distance(b, c) * req(a)) / (distance(a, b)) / distance(a, b) -distance(a, b) cancel out + pub(crate) fn record_request(&mut self, requested_location: Location, request_type : RequestType) { + debug!("Recording request for location: {:?}", requested_location); + self.request_density_tracker.sample(requested_location); + } + pub(crate) fn evaluate_new_connection(&mut self, current_neighbors: &BTreeMap, candidate_location: Location, acquisition_strategy : AcquisitionStrategy) -> Result { + self.evaluate_new_connection_with_current_time(current_neighbors, candidate_location, acquisition_strategy, Instant::now()) + } -*/ + fn evaluate_new_connection_with_current_time(&mut self, current_neighbors: &BTreeMap, candidate_location: Location, acquisition_strategy : AcquisitionStrategy, current_time : Instant) -> Result { + debug!("Evaluating new connection for candidate location: {:?}", candidate_location); + let density_map = self.get_or_create_density_map(current_neighbors)?; + let score = density_map.get_density_at(candidate_location)?; -mod request_density_tracker; -mod small_world_rand; + let accept = match acquisition_strategy { + AcquisitionStrategy::Slow => { + self.fast_connection_evaluator.record_only_with_current_time(score, current_time); + self.slow_connection_evaluator.record_and_eval_with_current_time(score, current_time) + }, + AcquisitionStrategy::Fast => { + self.slow_connection_evaluator.record_only_with_current_time(score, current_time); + self.fast_connection_evaluator.record_and_eval_with_current_time(score, current_time) + }, + }; + + Ok(accept) + } + + pub(crate) fn get_best_candidate_location(&mut self, current_neighbors: &BTreeMap) -> Result { + debug!("Retrieving best candidate location"); + let density_map = self.get_or_create_density_map(current_neighbors)?; + + let best_location = match density_map.get_max_density() { + Ok(location) => { + debug!("Max density found at location: {:?}", location); + location + }, + Err(_) => { + error!("An error occurred while getting max density, falling back to random location"); + self.random_location() + }, + }; + + Ok(best_location) + } + + /// Generates a random location that is close to the current peer location with a small + /// world distribution. + fn random_location(&self) -> Location { + debug!("Generating random location"); + let distance = random_link_distance(Distance::new(RANDOM_CLOSEST_DISTANCE)); + let location_f64 = if rand::random() { + self.this_peer_location.as_f64() - distance.as_f64() + } else { + self.this_peer_location.as_f64() + distance.as_f64() + }; + let location_f64 = location_f64.rem_euclid(1.0); // Ensure result is in [0.0, 1.0) + Location::new(location_f64) + } + + fn get_or_create_density_map(&mut self, current_neighbors: &BTreeMap) -> Result, DensityMapError> { + debug!("Getting or creating density map"); + self.cached_density_map.get_or_create(&self.request_density_tracker, current_neighbors) + } +} + +pub(crate) enum RequestType { + Get, + Put, + Join, + Subscribe +} + +pub(crate) enum AcquisitionStrategy { + /// Acquire new connections slowly, be picky + Slow, + + /// Acquire new connections aggressively, be less picky + Fast, +} + +#[cfg(test)] +mod tests { + use crate::ring::Location; + use super::TopologyManager; + + #[test] + fn test_topology_manager() { + let mut topology_manager = TopologyManager::new(Location::new(0.39)); + let mut current_neighbors = std::collections::BTreeMap::new(); + + // Insert neighbors from 0.0 to 0.9 + for i in 0..10 { + current_neighbors.insert(Location::new(i as f64 / 10.0), 0); + } + + let mut requests = vec![]; + // Simulate a bunch of random requests clustered around 0.35 + for _ in 0..1000 { + let requested_location = topology_manager.random_location(); + topology_manager.record_request(requested_location, super::RequestType::Get); + requests.push(requested_location); + } + + let best_candidate_location = topology_manager.get_best_candidate_location(¤t_neighbors).unwrap(); + // Should be half way between 0.3 and 0.4 as that is where the most requests were + assert_eq!(best_candidate_location, Location::new(0.35)); + + // call evaluate_new_connection for locations 0.0 to 1.0 at 0.01 intervals and find the + // location with the highest score + let mut best_score = 0.0; + let mut best_location = Location::new(0.0); + for i in 0..100 { + let candidate_location = Location::new(i as f64 / 100.0); + let score = topology_manager + .get_or_create_density_map(¤t_neighbors) + .unwrap().get_density_at(candidate_location).unwrap(); + if score > best_score { + best_score = score; + best_location = candidate_location; + } + } + + // Best location should be 0.4 as that is closest to 0.39 which is the peer's location and + // the request epicenter + assert_eq!(best_location, Location::new(0.4)); + } +} + +/* + // Dump histogram of requests with 0.01 intervals + let mut histogram = vec![0; 100]; + for request in requests { + let index = (request.as_f64() * 100.0).floor() as usize; + histogram[index] += 1; + } + println!("Histogram of requests:"); + for i in 0..100 { + println!("{}\t{}", i as f64 / 100.0, histogram[i]); + } + */ \ No newline at end of file diff --git a/crates/core/src/topology/request_density_tracker/cached_density_map.rs b/crates/core/src/topology/request_density_tracker/cached_density_map.rs new file mode 100644 index 000000000..2869c7884 --- /dev/null +++ b/crates/core/src/topology/request_density_tracker/cached_density_map.rs @@ -0,0 +1,32 @@ +use std::{time::{Duration, Instant}, collections::BTreeMap, rc::Rc}; +use crate::ring::Location; +use crate::topology::request_density_tracker::{self, DensityMapError}; + +/// Struct to handle caching of DensityMap +pub(in crate::topology) struct CachedDensityMap { + density_map: Option<(Rc, Instant)>, + regenerate_interval: Duration, +} + +impl CachedDensityMap { + pub(in crate::topology) fn new(regenerate_interval: Duration) -> Self { + CachedDensityMap { + density_map: None, + regenerate_interval, + } + } + + pub(in crate::topology) fn get_or_create(&mut self, tracker: &request_density_tracker::RequestDensityTracker, current_neighbors: &BTreeMap) -> Result, DensityMapError> { + let now = Instant::now(); + if let Some((density_map, last_update)) = &self.density_map { + if now.duration_since(*last_update) < self.regenerate_interval { + return Ok(density_map.clone()); + } + } + + let density_map = Rc::new(tracker.create_density_map(current_neighbors)?); + self.density_map = Some((density_map.clone(), now)); + + Ok(density_map) + } +} diff --git a/crates/core/src/topology/request_density_tracker/mod.rs b/crates/core/src/topology/request_density_tracker/mod.rs new file mode 100644 index 000000000..99994fdeb --- /dev/null +++ b/crates/core/src/topology/request_density_tracker/mod.rs @@ -0,0 +1,175 @@ +pub mod cached_density_map; + +#[cfg(test)] +mod tests; + +use std::collections::{BTreeMap, LinkedList}; +use thiserror::Error; +use crate::ring::Location; + +/// Tracks requests sent by a node to its neighbors and creates a density map, which +/// is useful for determining which new neighbors to connect to based on their +/// location. +pub(crate) struct RequestDensityTracker { + ordered_map: BTreeMap, + list: LinkedList, + window_size: usize, + samples: usize, +} + +impl RequestDensityTracker { + pub(crate) fn new(window_size: usize) -> Self { + Self { + ordered_map: BTreeMap::new(), + list: LinkedList::new(), + window_size, + samples: 0, + } + } + + pub(crate) fn sample(&mut self, value: Location) { + self.samples += 1; + + self.list.push_back(value); + *self.ordered_map.entry(value).or_insert(0) += 1; + + if self.list.len() > self.window_size { + if let Some(oldest) = self.list.pop_front() { + if let Some(count) = self.ordered_map.get_mut(&oldest) { + *count -= 1; + if *count == 0 { + self.ordered_map.remove(&oldest); + } + } + } + } + } + + pub(crate) fn create_density_map(&self, neighbors: &BTreeMap) -> Result { + if neighbors.is_empty() { + return Err(DensityMapError::EmptyNeighbors); + } + + let smoothing_radius = 2; + let mut density_map = DensityMap { + neighbor_request_counts: BTreeMap::new(), + }; + + for (sample_location, sample_count) in self.ordered_map.iter() { + let previous_neighbor = neighbors.range(..*sample_location).rev().next() + .or_else(|| neighbors.iter().rev().next()); + let next_neighbor = neighbors.range(*sample_location..).next() + .or_else(|| neighbors.iter().next()); + + match (previous_neighbor, next_neighbor) { + (Some((previous_neighbor_location, _)), Some((next_neighbor_location, _))) => { + if sample_location.distance(*previous_neighbor_location) < sample_location.distance(*next_neighbor_location) { + *density_map.neighbor_request_counts.entry(*previous_neighbor_location).or_insert(0) += sample_count; + } else { + *density_map.neighbor_request_counts.entry(*next_neighbor_location).or_insert(0) += sample_count; + } + }, + // The None cases have been removed as they should not occur given the new logic + _ => unreachable!("This shouldn't be possible given that we verify neighbors is not empty"), + } + } + + Ok(density_map) + } +} + +pub(crate) struct DensityMap { + neighbor_request_counts: BTreeMap, +} + +impl DensityMap { + pub(crate) fn get_density_at(&self, location: Location) -> Result { + if self.neighbor_request_counts.is_empty() { + return Err(DensityMapError::EmptyNeighbors); + } + + // Determine the locations below and above the given location + let previous_neighbor = self.neighbor_request_counts.range(..location).rev().next() + .or_else(|| self.neighbor_request_counts.iter().rev().next()); + + let next_neighbor = self.neighbor_request_counts.range(location..).next() + .or_else(|| self.neighbor_request_counts.iter().next()); + + // Determine the value proportionate to the distance to the previous and next neighbor + let count_estimate = match (previous_neighbor, next_neighbor) { + (Some((previous_neighbor_location, previous_neighbor_count)), Some((next_neighbor_location, next_neighbor_count))) => { + let previous_neighbor_dist = location.distance(*previous_neighbor_location).as_f64(); + let next_neighbor_dist = location.distance(*next_neighbor_location).as_f64(); + let total_dist = previous_neighbor_dist + next_neighbor_dist; + let previous_neighbor_prop = previous_neighbor_dist / total_dist; + let next_neighbor_prop = next_neighbor_dist / total_dist; + next_neighbor_prop * *previous_neighbor_count as f64 + previous_neighbor_prop * *next_neighbor_count as f64 + }, + // The None cases have been removed as they should not occur given the new logic + _ => unreachable!("This shouldn't be possible given that we verify neighbors is not empty"), + }; + + Ok(count_estimate) + } + + pub(crate) fn get_max_density(&self) -> Result { + if self.neighbor_request_counts.is_empty() { + return Err(DensityMapError::EmptyNeighbors); + } + + // Identify the midpoint Location between the pair of neighbors + // with the highest combined request count + let mut max_density_location = Location::new(0.0); + let mut max_density = 0; + + for ( + (previous_neighbor_location, previous_neighbor_count), (next_neighbor_location, next_neighbor_count)) + in + self.neighbor_request_counts.iter().zip(self.neighbor_request_counts.iter().skip(1)) { + let combined_count = previous_neighbor_count + next_neighbor_count; + if combined_count > max_density { + max_density = combined_count; + max_density_location = Location::new((previous_neighbor_location.as_f64() + next_neighbor_location.as_f64()) / 2.0); + } + } + + // We need to also check the first and last neighbors as locations are circular + let first_neighbor = self.neighbor_request_counts.iter().next(); + let last_neighbor = self.neighbor_request_counts.iter().rev().next(); + if let (Some((first_neighbor_location, first_neighbor_count)), Some((last_neighbor_location, last_neighbor_count))) = (first_neighbor, last_neighbor) { + let combined_count = first_neighbor_count + last_neighbor_count; + if combined_count > max_density { + // max_density = combined_count; Not needed as this is the last check + let distance = first_neighbor_location.distance(*last_neighbor_location); + let mut mp = first_neighbor_location.as_f64() - (distance.as_f64()/2.0); + if mp < 0.0 { + mp += 1.0; + } + max_density_location = Location::new(mp); + } + } + + Ok(max_density_location) + } +} + +// Define the custom error type using thiserror +#[derive(Error, Debug)] +pub enum DensityError { + #[error("Not enough samples to determine lower and upper bounds")] + CantFindBounds, + + #[error("Window radius too big. Window radius should be <= 50% of the number of samples ({samples}) and window size ({window_size}).")] + WindowTooBig { + samples: usize, + window_size: usize, + }, +} + +#[derive(Error, Debug)] +pub enum DensityMapError { + #[error("The neighbors BTreeMap is empty.")] + EmptyNeighbors, +} + + diff --git a/crates/core/src/topology/request_density_tracker/tests.rs b/crates/core/src/topology/request_density_tracker/tests.rs new file mode 100644 index 000000000..3e6d0f93d --- /dev/null +++ b/crates/core/src/topology/request_density_tracker/tests.rs @@ -0,0 +1,201 @@ +use super::*; + +#[test] +fn test_create_density_map() { + let mut sw = RequestDensityTracker::new(5); + sw.sample(Location::new(0.21)); + sw.sample(Location::new(0.22)); + sw.sample(Location::new(0.23)); + sw.sample(Location::new(0.61)); + sw.sample(Location::new(0.62)); + + let mut neighbors = BTreeMap::new(); + neighbors.insert(Location::new(0.2), 1); + neighbors.insert(Location::new(0.6), 1); + + let result = sw.create_density_map(&neighbors); + assert!(result.is_ok()); + let result = result.unwrap(); + assert_eq!(result.neighbor_request_counts.get(&Location::new(0.2)), Some(&3)); + assert_eq!(result.neighbor_request_counts.get(&Location::new(0.6)), Some(&2)); +} + +#[test] +fn test_wrap_around() { + let mut sw = RequestDensityTracker::new(5); + sw.sample(Location::new(0.21)); + sw.sample(Location::new(0.22)); + sw.sample(Location::new(0.23)); + sw.sample(Location::new(0.61)); + sw.sample(Location::new(0.62)); + + let mut neighbors = BTreeMap::new(); + neighbors.insert(Location::new(0.6), 1); + neighbors.insert(Location::new(0.9), 1); + + let result = sw.create_density_map(&neighbors); + assert!(result.is_ok()); + let result = result.unwrap(); + assert_eq!(result.neighbor_request_counts.get(&Location::new(0.9)), Some(&3)); + assert_eq!(result.neighbor_request_counts.get(&Location::new(0.6)), Some(&2)); +} + +#[test] +fn test_interpolate() { + let mut sw = RequestDensityTracker::new(10); + sw.sample(Location::new(0.19)); + sw.sample(Location::new(0.20)); + sw.sample(Location::new(0.21)); + sw.sample(Location::new(0.59)); + sw.sample(Location::new(0.60)); + + let mut neighbors = BTreeMap::new(); + neighbors.insert(Location::new(0.2), 1); + neighbors.insert(Location::new(0.6), 1); + + let result = sw.create_density_map(&neighbors); + assert!(result.is_ok()); + let result = result.unwrap(); + + // Scan and dumb densities 0.0 to 1.0 at 0.01 intervals + println!("Location\tDensity"); + for i in 0..100 { + let location = Location::new(i as f64 / 100.0); + let density = result.get_density_at(location).unwrap(); + // Print and round density to 2 decimals + println!("{}\t{}", location.as_f64(), (density * 100.0).round() / 100.0); + } + + assert_eq!(result.get_density_at(Location::new(0.2)).unwrap(), 3.0); + assert_eq!(result.get_density_at(Location::new(0.6)).unwrap(), 2.0); + assert_eq!(result.get_density_at(Location::new(0.4)).unwrap(), 2.5); + assert_eq!(result.get_density_at(Location::new(0.5)).unwrap(), 2.25); +} + +#[test] +fn test_drop() { + let mut sw = RequestDensityTracker::new(4); + sw.sample(Location::new(0.21)); + sw.sample(Location::new(0.22)); + sw.sample(Location::new(0.23)); + sw.sample(Location::new(0.61)); + sw.sample(Location::new(0.62)); + + let mut neighbors = BTreeMap::new(); + neighbors.insert(Location::new(0.2), 1); + neighbors.insert(Location::new(0.6), 1); + + let result = sw.create_density_map(&neighbors); + assert!(result.is_ok()); + let result = result.unwrap(); + assert_eq!(result.neighbor_request_counts.get(&Location::new(0.2)), Some(&2)); + assert_eq!(result.neighbor_request_counts.get(&Location::new(0.6)), Some(&2)); + +} + +#[test] +fn test_empty_neighbors_error() { + let sw = RequestDensityTracker::new(10); + let empty_neighbors = BTreeMap::new(); + let result = sw.create_density_map(&empty_neighbors); + assert!(matches!(result, Err(DensityMapError::EmptyNeighbors))); +} + +#[test] +fn test_get_max_density() { + let mut density_map = DensityMap { + neighbor_request_counts: BTreeMap::new(), + }; + + density_map.neighbor_request_counts.insert(Location::new(0.2), 1); + density_map.neighbor_request_counts.insert(Location::new(0.6), 2); + density_map.neighbor_request_counts.insert(Location::new(0.8), 2); + + let result = density_map.get_max_density(); + assert!(result.is_ok()); + let result = result.unwrap(); + assert_eq!(result, Location::new(0.7)); +} + +#[test] +fn test_get_max_density_2() { + let mut density_map = DensityMap { + neighbor_request_counts: BTreeMap::new(), + }; + + density_map.neighbor_request_counts.insert(Location::new(0.2), 1); + density_map.neighbor_request_counts.insert(Location::new(0.6), 2); + density_map.neighbor_request_counts.insert(Location::new(0.8), 2); + density_map.neighbor_request_counts.insert(Location::new(0.9), 1); + + let result = density_map.get_max_density(); + assert!(result.is_ok()); + let result = result.unwrap(); + assert_eq!(result, Location::new(0.7)); +} + +#[test] +fn test_get_max_density_first_last() { + let mut density_map = DensityMap { + neighbor_request_counts: BTreeMap::new(), + }; + + density_map.neighbor_request_counts.insert(Location::new(0.0), 2); + density_map.neighbor_request_counts.insert(Location::new(0.2), 1); + density_map.neighbor_request_counts.insert(Location::new(0.6), 1); + density_map.neighbor_request_counts.insert(Location::new(0.8), 1); + density_map.neighbor_request_counts.insert(Location::new(0.9), 2); + + let result = density_map.get_max_density(); + assert!(result.is_ok()); + let result = result.unwrap(); + assert_eq!(result, Location::new(0.95)); +} + +#[test] +fn test_get_max_density_first_last_2() { + // Verify the other case in max_density_location calculation + let mut density_map = DensityMap { + neighbor_request_counts: BTreeMap::new(), + }; + + density_map.neighbor_request_counts.insert(Location::new(0.3), 2); + density_map.neighbor_request_counts.insert(Location::new(0.4), 1); + density_map.neighbor_request_counts.insert(Location::new(0.6), 1); + density_map.neighbor_request_counts.insert(Location::new(0.8), 1); + density_map.neighbor_request_counts.insert(Location::new(0.9), 2); + + let result = density_map.get_max_density(); + assert!(result.is_ok()); + let result = result.unwrap(); + assert_eq!(result, Location::new(0.1)); +} + +#[test] +fn test_get_max_density_first_last_3() { + // Verify the other case in max_density_location calculation + let mut density_map = DensityMap { + neighbor_request_counts: BTreeMap::new(), + }; + + density_map.neighbor_request_counts.insert(Location::new(0.1), 2); + density_map.neighbor_request_counts.insert(Location::new(0.2), 1); + density_map.neighbor_request_counts.insert(Location::new(0.3), 1); + density_map.neighbor_request_counts.insert(Location::new(0.4), 1); + density_map.neighbor_request_counts.insert(Location::new(0.7), 2); + + let result = density_map.get_max_density(); + assert!(result.is_ok()); + let result = result.unwrap(); + assert_eq!(result, Location::new(0.9)); +} + +#[test] +fn test_get_max_density_empty_neighbors_error() { + let density_map = DensityMap { + neighbor_request_counts: BTreeMap::new(), + }; + + let result = density_map.get_max_density(); + assert!(matches!(result, Err(DensityMapError::EmptyNeighbors))); +} From 3eb859c57bc799b818b6b6ed01187e5c21245e93 Mon Sep 17 00:00:00 2001 From: dowdyma1 Date: Thu, 2 Nov 2023 17:17:01 -0400 Subject: [PATCH 72/76] Improving Developer Onboarding Documentation (#883) * add comments to explain tutorials more * modify target directory command * add webassembly target command * change working app microblogging -> email. Has verbose readme --- apps/freenet-email-app/README.md | 24 ++++++++++++++++++++++++ docs/src/tutorial.md | 12 ++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/apps/freenet-email-app/README.md b/apps/freenet-email-app/README.md index 2acc42a24..2dfa69d0a 100644 --- a/apps/freenet-email-app/README.md +++ b/apps/freenet-email-app/README.md @@ -15,11 +15,30 @@ local node, which simulates a node on the network. ```bash curl https://sh.rustup.rs -sSf | sh ``` +- (Ubuntu) + ```bash + sudo apt-get update + sudo apt-get install libssl-dev libclang-dev pkg-config + ``` - Install the Freeenet development tool (fdev) and a working Freenet kernel that can be used for local development. Use cargo to install it: ```bash cargo install freenet cargo install fdev ``` +- Install Dioxus-CLI, a GUI library for rust + ```bash + cargo install dioxus-cli + ``` +- Add WebAssembly target + ```bash + rustup target add wasm32-unknown-unknown + ``` +- Initializing & Fetching Submodules + ```bash + git submodule update --init --recursive + ``` +### Note about MacOS +Email account creation currently does not work on MacOS ## Prepare the Freenet email contracts and delegates @@ -39,6 +58,11 @@ This delegate is located inside the modules folder of freenet-core: - `Makefile` <-- this file contains the build instructions for the delegate - ... +Add the target directory for the project. This should be an absolute file path to freenet-core/target. +```bash +export CARGO_TARGET_DIR="... freenet-core/target" +``` + To build the delegate, go to the `identity-management` folder and run the following command: ```bash diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index 9f7ba6df9..96fd1da0f 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -1,6 +1,6 @@ # Getting Started -This tutorial will show you how to build decentralized software on Freenet. For a similar working and up to date example check the `freenet-microblogging` app (located in under the `apps/freenet-microblogging` directory in the `freenet-core` repository). +This tutorial will show you how to build decentralized software on Freenet. For a similar working and up to date example check the `freenet-email` app (located in under the `apps/freenet-email-app` directory in the `freenet-core` repository). @@ -15,18 +15,26 @@ Mac (for Windows see [here](https://rustup.rs)): curl https://sh.rustup.rs -sSf | sh ``` +#### Note for MacOS install +Do not have the `brew` version of rust installed as it will cause compications with `fdev`. + ### Freenet development tool (fdev) Once you have a working installation of Cargo you can install the Freenet dev tools: ```bash -cargo install freenet +cargo install freenet fdev ``` This command will install `fdev` (Freenet development tool) and a working Freenet kernel that can be used for local development. +### Add WebAssembly target +```bash +rustup target add wasm32-unknown-unknown +``` + ### Node.js and TypeScript To build user interfaces in JavaScript or TypeScript, you need to have Node.js From 7dba1d29a8420fba6376dd892035e7439d9512b6 Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Sun, 5 Nov 2023 14:47:14 -0600 Subject: [PATCH 73/76] tm: topology manager tests pass --- .../topology/connection_evaluator/tests.rs | 45 +++++++++++++++++-- crates/core/src/topology/mod.rs | 13 ------ 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/crates/core/src/topology/connection_evaluator/tests.rs b/crates/core/src/topology/connection_evaluator/tests.rs index 63576efc5..1e968a049 100644 --- a/crates/core/src/topology/connection_evaluator/tests.rs +++ b/crates/core/src/topology/connection_evaluator/tests.rs @@ -13,21 +13,21 @@ fn test_record_first_score() { } #[test] -fn test_record_within_window_duration() { +fn test_not_best_in_time_window() { let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(10)); let start_time = Instant::now(); evaluator.record_and_eval_with_current_time(5.0, start_time); - assert_eq!(evaluator.record_and_eval_with_current_time(6.0, start_time + Duration::from_secs(5)), false); + assert_eq!(evaluator.record_and_eval_with_current_time(4.0, start_time + Duration::from_secs(5)), false); } #[test] -fn test_record_outside_window_duration() { +fn test_best_in_time_window() { let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(10)); let start_time = Instant::now(); evaluator.record_and_eval_with_current_time(5.0, start_time); - assert_eq!(evaluator.record_and_eval_with_current_time(6.0, start_time + Duration::from_secs(11)), true); + assert_eq!(evaluator.record_and_eval_with_current_time(4.0, start_time + Duration::from_secs(11)), true); } #[test] @@ -40,3 +40,40 @@ fn test_remove_outdated_scores() { evaluator.record_and_eval_with_current_time(4.5, start_time + Duration::from_secs(11)); assert_eq!(evaluator.scores.len(), 2); } + +#[test] +fn test_empty_window_duration() { + let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(0)); + let current_time = Instant::now(); + assert_eq!(evaluator.record_and_eval_with_current_time(5.0, current_time), true); + assert_eq!(evaluator.record_and_eval_with_current_time(4.0, current_time), false); +} + +#[test] +fn test_multiple_scores_same_timestamp() { + let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(10)); + let current_time = Instant::now(); + evaluator.record_only_with_current_time(5.0, current_time); + evaluator.record_only_with_current_time(6.0, current_time); + assert_eq!(evaluator.scores.len(), 2); + assert_eq!(evaluator.record_and_eval_with_current_time(4.0, current_time + Duration::from_secs(5)), false); +} + +#[test] +fn test_negative_scores() { + let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(10)); + let start_time = Instant::now(); + assert_eq!(evaluator.record_and_eval_with_current_time(-5.0, start_time), true); + assert_eq!(evaluator.record_and_eval_with_current_time(-4.0, start_time + Duration::from_secs(5)), true); + assert_eq!(evaluator.record_and_eval_with_current_time(-6.0, start_time + Duration::from_secs(5)), false); +} + +#[test] +fn test_large_number_of_scores() { + let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(10)); + let start_time = Instant::now(); + for i in 0..1000 { + evaluator.record_only_with_current_time(i as f64, start_time + Duration::from_secs(i)); + } + assert_eq!(evaluator.record_and_eval_with_current_time(1000.0, start_time + Duration::from_secs(1001)), true); +} diff --git a/crates/core/src/topology/mod.rs b/crates/core/src/topology/mod.rs index 413bfc10a..7c59bca8d 100644 --- a/crates/core/src/topology/mod.rs +++ b/crates/core/src/topology/mod.rs @@ -183,16 +183,3 @@ mod tests { assert_eq!(best_location, Location::new(0.4)); } } - -/* - // Dump histogram of requests with 0.01 intervals - let mut histogram = vec![0; 100]; - for request in requests { - let index = (request.as_f64() * 100.0).floor() as usize; - histogram[index] += 1; - } - println!("Histogram of requests:"); - for i in 0..100 { - println!("{}\t{}", i as f64 / 100.0, histogram[i]); - } - */ \ No newline at end of file From 97d2649ecf3d0e0cda054eac13b6624da836b98e Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Sun, 5 Nov 2023 14:55:11 -0600 Subject: [PATCH 74/76] tm: improve docs --- crates/core/src/topology/mod.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/core/src/topology/mod.rs b/crates/core/src/topology/mod.rs index 7c59bca8d..ef3c4f289 100644 --- a/crates/core/src/topology/mod.rs +++ b/crates/core/src/topology/mod.rs @@ -18,8 +18,8 @@ const REGENERATE_DENSITY_MAP_INTERVAL: Duration = Duration::from_secs(60); const RANDOM_CLOSEST_DISTANCE: f64 = 1.0 / 1000.0; /// The goal of `TopologyManager` is to select new connections such that the -/// distribution of connections in the network is as close as possible to the -/// distribution of requests in the network. +/// distribution of connections is as close as possible to the +/// distribution of outbound requests. /// /// This is done by maintaining a `RequestDensityTracker` which tracks the /// distribution of requests in the network. The `TopologyManager` uses this @@ -44,6 +44,7 @@ pub(crate) struct TopologyManager { } impl TopologyManager { + /// Create a new TopologyManager specifying the peer's own Location pub(crate) fn new(this_peer_location : Location) -> Self { info!("Creating a new TopologyManager instance"); TopologyManager { @@ -55,11 +56,15 @@ impl TopologyManager { } } + /// Record a request and the location it's targeting pub(crate) fn record_request(&mut self, requested_location: Location, request_type : RequestType) { debug!("Recording request for location: {:?}", requested_location); self.request_density_tracker.sample(requested_location); } + /// Decide whether to accept a connection from a new candidate peer based on its location + /// and current neighbors and request density, along with how it compares to other + /// recent candidates. pub(crate) fn evaluate_new_connection(&mut self, current_neighbors: &BTreeMap, candidate_location: Location, acquisition_strategy : AcquisitionStrategy) -> Result { self.evaluate_new_connection_with_current_time(current_neighbors, candidate_location, acquisition_strategy, Instant::now()) } @@ -83,6 +88,7 @@ impl TopologyManager { Ok(accept) } + /// Get the ideal location for a new connection based on current neighbors and request density pub(crate) fn get_best_candidate_location(&mut self, current_neighbors: &BTreeMap) -> Result { debug!("Retrieving best candidate location"); let density_map = self.get_or_create_density_map(current_neighbors)?; From fe8b0ca144b14714994d4ef94d50bc79e563d17a Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Mon, 6 Nov 2023 07:44:25 +0100 Subject: [PATCH 75/76] tm: cleanup clippy warnings --- .../src/topology/connection_evaluator/mod.rs | 4 +- .../topology/connection_evaluator/tests.rs | 27 ++-- crates/core/src/topology/mod.rs | 2 +- .../cached_density_map.rs | 12 +- .../topology/request_density_tracker/mod.rs | 117 ++++++++++------ .../topology/request_density_tracker/tests.rs | 125 +++++++++++++----- crates/core/src/topology/small_world_rand.rs | 16 +-- 7 files changed, 208 insertions(+), 95 deletions(-) diff --git a/crates/core/src/topology/connection_evaluator/mod.rs b/crates/core/src/topology/connection_evaluator/mod.rs index 18826c218..c7b46d004 100644 --- a/crates/core/src/topology/connection_evaluator/mod.rs +++ b/crates/core/src/topology/connection_evaluator/mod.rs @@ -7,10 +7,10 @@ use std::time::{Duration, Instant}; /// any other scores within a predefined time window. A score is considered better if it's higher /// than all other scores in the time window, or if no scores were recorded within the window's /// duration. -/// +/// /// In the Freenet context, this will be used to titrate the rate of new connection requests accepted /// by a node. The node will only accept a new connection if the score of the connection is better -/// than all other scores within the time window. +/// than all other scores within the time window. pub(crate) struct ConnectionEvaluator { scores: VecDeque<(Instant, f64)>, window_duration: Duration, diff --git a/crates/core/src/topology/connection_evaluator/tests.rs b/crates/core/src/topology/connection_evaluator/tests.rs index 1e968a049..52bbf4974 100644 --- a/crates/core/src/topology/connection_evaluator/tests.rs +++ b/crates/core/src/topology/connection_evaluator/tests.rs @@ -4,12 +4,11 @@ use super::*; fn test_record_first_score() { let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(10)); let current_time = Instant::now(); - assert_eq!(evaluator.record_and_eval_with_current_time(5.0, current_time), true); + assert!(evaluator.record_and_eval_with_current_time(5.0, current_time)); // Assert evaluator.scores contains the new score assert_eq!(evaluator.scores.len(), 1); assert_eq!(evaluator.scores[0].1, 5.0); assert_eq!(evaluator.scores[0].0, current_time); - } #[test] @@ -18,7 +17,7 @@ fn test_not_best_in_time_window() { let start_time = Instant::now(); evaluator.record_and_eval_with_current_time(5.0, start_time); - assert_eq!(evaluator.record_and_eval_with_current_time(4.0, start_time + Duration::from_secs(5)), false); + assert!(!evaluator.record_and_eval_with_current_time(4.0, start_time + Duration::from_secs(5)),); } #[test] @@ -27,7 +26,7 @@ fn test_best_in_time_window() { let start_time = Instant::now(); evaluator.record_and_eval_with_current_time(5.0, start_time); - assert_eq!(evaluator.record_and_eval_with_current_time(4.0, start_time + Duration::from_secs(11)), true); + assert!(evaluator.record_and_eval_with_current_time(4.0, start_time + Duration::from_secs(11)),); } #[test] @@ -45,8 +44,8 @@ fn test_remove_outdated_scores() { fn test_empty_window_duration() { let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(0)); let current_time = Instant::now(); - assert_eq!(evaluator.record_and_eval_with_current_time(5.0, current_time), true); - assert_eq!(evaluator.record_and_eval_with_current_time(4.0, current_time), false); + assert!(evaluator.record_and_eval_with_current_time(5.0, current_time)); + assert!(!evaluator.record_and_eval_with_current_time(4.0, current_time)); } #[test] @@ -56,16 +55,20 @@ fn test_multiple_scores_same_timestamp() { evaluator.record_only_with_current_time(5.0, current_time); evaluator.record_only_with_current_time(6.0, current_time); assert_eq!(evaluator.scores.len(), 2); - assert_eq!(evaluator.record_and_eval_with_current_time(4.0, current_time + Duration::from_secs(5)), false); + assert!( + !evaluator.record_and_eval_with_current_time(4.0, current_time + Duration::from_secs(5)), + ); } #[test] fn test_negative_scores() { let mut evaluator = ConnectionEvaluator::new(Duration::from_secs(10)); let start_time = Instant::now(); - assert_eq!(evaluator.record_and_eval_with_current_time(-5.0, start_time), true); - assert_eq!(evaluator.record_and_eval_with_current_time(-4.0, start_time + Duration::from_secs(5)), true); - assert_eq!(evaluator.record_and_eval_with_current_time(-6.0, start_time + Duration::from_secs(5)), false); + assert!(evaluator.record_and_eval_with_current_time(-5.0, start_time),); + assert!(evaluator.record_and_eval_with_current_time(-4.0, start_time + Duration::from_secs(5)),); + assert!( + !evaluator.record_and_eval_with_current_time(-6.0, start_time + Duration::from_secs(5)), + ); } #[test] @@ -75,5 +78,7 @@ fn test_large_number_of_scores() { for i in 0..1000 { evaluator.record_only_with_current_time(i as f64, start_time + Duration::from_secs(i)); } - assert_eq!(evaluator.record_and_eval_with_current_time(1000.0, start_time + Duration::from_secs(1001)), true); + assert!( + evaluator.record_and_eval_with_current_time(1000.0, start_time + Duration::from_secs(1001)), + ); } diff --git a/crates/core/src/topology/mod.rs b/crates/core/src/topology/mod.rs index ef3c4f289..42d1e7be8 100644 --- a/crates/core/src/topology/mod.rs +++ b/crates/core/src/topology/mod.rs @@ -12,7 +12,7 @@ mod small_world_rand; mod connection_evaluator; const SLOW_CONNECTION_EVALUATOR_WINDOW_DURATION: Duration = Duration::from_secs(5 * 60); -const FAST_CONNECTION_EVALUATOR_WINDOW_DURATION: Duration = Duration::from_secs(1 * 60); +const FAST_CONNECTION_EVALUATOR_WINDOW_DURATION: Duration = Duration::from_secs(60); const REQUEST_DENSITY_TRACKER_WINDOW_SIZE: usize = 10_000; const REGENERATE_DENSITY_MAP_INTERVAL: Duration = Duration::from_secs(60); const RANDOM_CLOSEST_DISTANCE: f64 = 1.0 / 1000.0; diff --git a/crates/core/src/topology/request_density_tracker/cached_density_map.rs b/crates/core/src/topology/request_density_tracker/cached_density_map.rs index 2869c7884..db18ff85c 100644 --- a/crates/core/src/topology/request_density_tracker/cached_density_map.rs +++ b/crates/core/src/topology/request_density_tracker/cached_density_map.rs @@ -1,6 +1,10 @@ -use std::{time::{Duration, Instant}, collections::BTreeMap, rc::Rc}; use crate::ring::Location; use crate::topology::request_density_tracker::{self, DensityMapError}; +use std::{ + collections::BTreeMap, + rc::Rc, + time::{Duration, Instant}, +}; /// Struct to handle caching of DensityMap pub(in crate::topology) struct CachedDensityMap { @@ -16,7 +20,11 @@ impl CachedDensityMap { } } - pub(in crate::topology) fn get_or_create(&mut self, tracker: &request_density_tracker::RequestDensityTracker, current_neighbors: &BTreeMap) -> Result, DensityMapError> { + pub(in crate::topology) fn get_or_create( + &mut self, + tracker: &request_density_tracker::RequestDensityTracker, + current_neighbors: &BTreeMap, + ) -> Result, DensityMapError> { let now = Instant::now(); if let Some((density_map, last_update)) = &self.density_map { if now.duration_since(*last_update) < self.regenerate_interval { diff --git a/crates/core/src/topology/request_density_tracker/mod.rs b/crates/core/src/topology/request_density_tracker/mod.rs index 99994fdeb..8449162ae 100644 --- a/crates/core/src/topology/request_density_tracker/mod.rs +++ b/crates/core/src/topology/request_density_tracker/mod.rs @@ -3,9 +3,9 @@ pub mod cached_density_map; #[cfg(test)] mod tests; +use crate::ring::Location; use std::collections::{BTreeMap, LinkedList}; use thiserror::Error; -use crate::ring::Location; /// Tracks requests sent by a node to its neighbors and creates a density map, which /// is useful for determining which new neighbors to connect to based on their @@ -45,35 +45,52 @@ impl RequestDensityTracker { } } - pub(crate) fn create_density_map(&self, neighbors: &BTreeMap) -> Result { + pub(crate) fn create_density_map( + &self, + neighbors: &BTreeMap, + ) -> Result { if neighbors.is_empty() { return Err(DensityMapError::EmptyNeighbors); } - + let smoothing_radius = 2; let mut density_map = DensityMap { neighbor_request_counts: BTreeMap::new(), }; - + for (sample_location, sample_count) in self.ordered_map.iter() { - let previous_neighbor = neighbors.range(..*sample_location).rev().next() - .or_else(|| neighbors.iter().rev().next()); - let next_neighbor = neighbors.range(*sample_location..).next() + let previous_neighbor = neighbors + .range(..*sample_location) + .next_back() + .or_else(|| neighbors.iter().next_back()); + let next_neighbor = neighbors + .range(*sample_location..) + .next() .or_else(|| neighbors.iter().next()); - - match (previous_neighbor, next_neighbor) { - (Some((previous_neighbor_location, _)), Some((next_neighbor_location, _))) => { - if sample_location.distance(*previous_neighbor_location) < sample_location.distance(*next_neighbor_location) { - *density_map.neighbor_request_counts.entry(*previous_neighbor_location).or_insert(0) += sample_count; - } else { - *density_map.neighbor_request_counts.entry(*next_neighbor_location).or_insert(0) += sample_count; - } - }, - // The None cases have been removed as they should not occur given the new logic - _ => unreachable!("This shouldn't be possible given that we verify neighbors is not empty"), + + match (previous_neighbor, next_neighbor) { + (Some((previous_neighbor_location, _)), Some((next_neighbor_location, _))) => { + if sample_location.distance(*previous_neighbor_location) + < sample_location.distance(*next_neighbor_location) + { + *density_map + .neighbor_request_counts + .entry(*previous_neighbor_location) + .or_insert(0) += sample_count; + } else { + *density_map + .neighbor_request_counts + .entry(*next_neighbor_location) + .or_insert(0) += sample_count; + } } + // The None cases have been removed as they should not occur given the new logic + _ => unreachable!( + "This shouldn't be possible given that we verify neighbors is not empty" + ), + } } - + Ok(density_map) } } @@ -89,24 +106,37 @@ impl DensityMap { } // Determine the locations below and above the given location - let previous_neighbor = self.neighbor_request_counts.range(..location).rev().next() - .or_else(|| self.neighbor_request_counts.iter().rev().next()); - - let next_neighbor = self.neighbor_request_counts.range(location..).next() + let previous_neighbor = self + .neighbor_request_counts + .range(..location) + .next_back() + .or_else(|| self.neighbor_request_counts.iter().next_back()); + + let next_neighbor = self + .neighbor_request_counts + .range(location..) + .next() .or_else(|| self.neighbor_request_counts.iter().next()); // Determine the value proportionate to the distance to the previous and next neighbor let count_estimate = match (previous_neighbor, next_neighbor) { - (Some((previous_neighbor_location, previous_neighbor_count)), Some((next_neighbor_location, next_neighbor_count))) => { - let previous_neighbor_dist = location.distance(*previous_neighbor_location).as_f64(); + ( + Some((previous_neighbor_location, previous_neighbor_count)), + Some((next_neighbor_location, next_neighbor_count)), + ) => { + let previous_neighbor_dist = + location.distance(*previous_neighbor_location).as_f64(); let next_neighbor_dist = location.distance(*next_neighbor_location).as_f64(); let total_dist = previous_neighbor_dist + next_neighbor_dist; let previous_neighbor_prop = previous_neighbor_dist / total_dist; let next_neighbor_prop = next_neighbor_dist / total_dist; - next_neighbor_prop * *previous_neighbor_count as f64 + previous_neighbor_prop * *next_neighbor_count as f64 - }, + next_neighbor_prop * *previous_neighbor_count as f64 + + previous_neighbor_prop * *next_neighbor_count as f64 + } // The None cases have been removed as they should not occur given the new logic - _ => unreachable!("This shouldn't be possible given that we verify neighbors is not empty"), + _ => unreachable!( + "This shouldn't be possible given that we verify neighbors is not empty" + ), }; Ok(count_estimate) @@ -123,25 +153,35 @@ impl DensityMap { let mut max_density = 0; for ( - (previous_neighbor_location, previous_neighbor_count), (next_neighbor_location, next_neighbor_count)) - in - self.neighbor_request_counts.iter().zip(self.neighbor_request_counts.iter().skip(1)) { + (previous_neighbor_location, previous_neighbor_count), + (next_neighbor_location, next_neighbor_count), + ) in self + .neighbor_request_counts + .iter() + .zip(self.neighbor_request_counts.iter().skip(1)) + { let combined_count = previous_neighbor_count + next_neighbor_count; if combined_count > max_density { max_density = combined_count; - max_density_location = Location::new((previous_neighbor_location.as_f64() + next_neighbor_location.as_f64()) / 2.0); + max_density_location = Location::new( + (previous_neighbor_location.as_f64() + next_neighbor_location.as_f64()) / 2.0, + ); } } // We need to also check the first and last neighbors as locations are circular let first_neighbor = self.neighbor_request_counts.iter().next(); - let last_neighbor = self.neighbor_request_counts.iter().rev().next(); - if let (Some((first_neighbor_location, first_neighbor_count)), Some((last_neighbor_location, last_neighbor_count))) = (first_neighbor, last_neighbor) { + let last_neighbor = self.neighbor_request_counts.iter().next_back(); + if let ( + Some((first_neighbor_location, first_neighbor_count)), + Some((last_neighbor_location, last_neighbor_count)), + ) = (first_neighbor, last_neighbor) + { let combined_count = first_neighbor_count + last_neighbor_count; if combined_count > max_density { // max_density = combined_count; Not needed as this is the last check let distance = first_neighbor_location.distance(*last_neighbor_location); - let mut mp = first_neighbor_location.as_f64() - (distance.as_f64()/2.0); + let mut mp = first_neighbor_location.as_f64() - (distance.as_f64() / 2.0); if mp < 0.0 { mp += 1.0; } @@ -160,10 +200,7 @@ pub enum DensityError { CantFindBounds, #[error("Window radius too big. Window radius should be <= 50% of the number of samples ({samples}) and window size ({window_size}).")] - WindowTooBig { - samples: usize, - window_size: usize, - }, + WindowTooBig { samples: usize, window_size: usize }, } #[derive(Error, Debug)] @@ -171,5 +208,3 @@ pub enum DensityMapError { #[error("The neighbors BTreeMap is empty.")] EmptyNeighbors, } - - diff --git a/crates/core/src/topology/request_density_tracker/tests.rs b/crates/core/src/topology/request_density_tracker/tests.rs index 3e6d0f93d..8f9a3fb39 100644 --- a/crates/core/src/topology/request_density_tracker/tests.rs +++ b/crates/core/src/topology/request_density_tracker/tests.rs @@ -16,8 +16,14 @@ fn test_create_density_map() { let result = sw.create_density_map(&neighbors); assert!(result.is_ok()); let result = result.unwrap(); - assert_eq!(result.neighbor_request_counts.get(&Location::new(0.2)), Some(&3)); - assert_eq!(result.neighbor_request_counts.get(&Location::new(0.6)), Some(&2)); + assert_eq!( + result.neighbor_request_counts.get(&Location::new(0.2)), + Some(&3) + ); + assert_eq!( + result.neighbor_request_counts.get(&Location::new(0.6)), + Some(&2) + ); } #[test] @@ -36,8 +42,14 @@ fn test_wrap_around() { let result = sw.create_density_map(&neighbors); assert!(result.is_ok()); let result = result.unwrap(); - assert_eq!(result.neighbor_request_counts.get(&Location::new(0.9)), Some(&3)); - assert_eq!(result.neighbor_request_counts.get(&Location::new(0.6)), Some(&2)); + assert_eq!( + result.neighbor_request_counts.get(&Location::new(0.9)), + Some(&3) + ); + assert_eq!( + result.neighbor_request_counts.get(&Location::new(0.6)), + Some(&2) + ); } #[test] @@ -63,7 +75,11 @@ fn test_interpolate() { let location = Location::new(i as f64 / 100.0); let density = result.get_density_at(location).unwrap(); // Print and round density to 2 decimals - println!("{}\t{}", location.as_f64(), (density * 100.0).round() / 100.0); + println!( + "{}\t{}", + location.as_f64(), + (density * 100.0).round() / 100.0 + ); } assert_eq!(result.get_density_at(Location::new(0.2)).unwrap(), 3.0); @@ -88,9 +104,14 @@ fn test_drop() { let result = sw.create_density_map(&neighbors); assert!(result.is_ok()); let result = result.unwrap(); - assert_eq!(result.neighbor_request_counts.get(&Location::new(0.2)), Some(&2)); - assert_eq!(result.neighbor_request_counts.get(&Location::new(0.6)), Some(&2)); - + assert_eq!( + result.neighbor_request_counts.get(&Location::new(0.2)), + Some(&2) + ); + assert_eq!( + result.neighbor_request_counts.get(&Location::new(0.6)), + Some(&2) + ); } #[test] @@ -107,9 +128,15 @@ fn test_get_max_density() { neighbor_request_counts: BTreeMap::new(), }; - density_map.neighbor_request_counts.insert(Location::new(0.2), 1); - density_map.neighbor_request_counts.insert(Location::new(0.6), 2); - density_map.neighbor_request_counts.insert(Location::new(0.8), 2); + density_map + .neighbor_request_counts + .insert(Location::new(0.2), 1); + density_map + .neighbor_request_counts + .insert(Location::new(0.6), 2); + density_map + .neighbor_request_counts + .insert(Location::new(0.8), 2); let result = density_map.get_max_density(); assert!(result.is_ok()); @@ -123,10 +150,18 @@ fn test_get_max_density_2() { neighbor_request_counts: BTreeMap::new(), }; - density_map.neighbor_request_counts.insert(Location::new(0.2), 1); - density_map.neighbor_request_counts.insert(Location::new(0.6), 2); - density_map.neighbor_request_counts.insert(Location::new(0.8), 2); - density_map.neighbor_request_counts.insert(Location::new(0.9), 1); + density_map + .neighbor_request_counts + .insert(Location::new(0.2), 1); + density_map + .neighbor_request_counts + .insert(Location::new(0.6), 2); + density_map + .neighbor_request_counts + .insert(Location::new(0.8), 2); + density_map + .neighbor_request_counts + .insert(Location::new(0.9), 1); let result = density_map.get_max_density(); assert!(result.is_ok()); @@ -140,11 +175,21 @@ fn test_get_max_density_first_last() { neighbor_request_counts: BTreeMap::new(), }; - density_map.neighbor_request_counts.insert(Location::new(0.0), 2); - density_map.neighbor_request_counts.insert(Location::new(0.2), 1); - density_map.neighbor_request_counts.insert(Location::new(0.6), 1); - density_map.neighbor_request_counts.insert(Location::new(0.8), 1); - density_map.neighbor_request_counts.insert(Location::new(0.9), 2); + density_map + .neighbor_request_counts + .insert(Location::new(0.0), 2); + density_map + .neighbor_request_counts + .insert(Location::new(0.2), 1); + density_map + .neighbor_request_counts + .insert(Location::new(0.6), 1); + density_map + .neighbor_request_counts + .insert(Location::new(0.8), 1); + density_map + .neighbor_request_counts + .insert(Location::new(0.9), 2); let result = density_map.get_max_density(); assert!(result.is_ok()); @@ -159,11 +204,21 @@ fn test_get_max_density_first_last_2() { neighbor_request_counts: BTreeMap::new(), }; - density_map.neighbor_request_counts.insert(Location::new(0.3), 2); - density_map.neighbor_request_counts.insert(Location::new(0.4), 1); - density_map.neighbor_request_counts.insert(Location::new(0.6), 1); - density_map.neighbor_request_counts.insert(Location::new(0.8), 1); - density_map.neighbor_request_counts.insert(Location::new(0.9), 2); + density_map + .neighbor_request_counts + .insert(Location::new(0.3), 2); + density_map + .neighbor_request_counts + .insert(Location::new(0.4), 1); + density_map + .neighbor_request_counts + .insert(Location::new(0.6), 1); + density_map + .neighbor_request_counts + .insert(Location::new(0.8), 1); + density_map + .neighbor_request_counts + .insert(Location::new(0.9), 2); let result = density_map.get_max_density(); assert!(result.is_ok()); @@ -178,11 +233,21 @@ fn test_get_max_density_first_last_3() { neighbor_request_counts: BTreeMap::new(), }; - density_map.neighbor_request_counts.insert(Location::new(0.1), 2); - density_map.neighbor_request_counts.insert(Location::new(0.2), 1); - density_map.neighbor_request_counts.insert(Location::new(0.3), 1); - density_map.neighbor_request_counts.insert(Location::new(0.4), 1); - density_map.neighbor_request_counts.insert(Location::new(0.7), 2); + density_map + .neighbor_request_counts + .insert(Location::new(0.1), 2); + density_map + .neighbor_request_counts + .insert(Location::new(0.2), 1); + density_map + .neighbor_request_counts + .insert(Location::new(0.3), 1); + density_map + .neighbor_request_counts + .insert(Location::new(0.4), 1); + density_map + .neighbor_request_counts + .insert(Location::new(0.7), 2); let result = density_map.get_max_density(); assert!(result.is_ok()); diff --git a/crates/core/src/topology/small_world_rand.rs b/crates/core/src/topology/small_world_rand.rs index c0f84868d..57fb309fe 100644 --- a/crates/core/src/topology/small_world_rand.rs +++ b/crates/core/src/topology/small_world_rand.rs @@ -39,14 +39,14 @@ mod tests { } // Perform chi-squared test - let mut expected_counts = vec![0.0; num_bins]; - for i in 0..num_bins { - let lower = d_min + (d_max - d_min) * (i as f64 / num_bins as f64); - let upper = d_min + (d_max - d_min) * ((i as f64 + 1.0) / num_bins as f64); - expected_counts[i] = ((upper - lower) / (upper.powf(-1.0) - lower.powf(-1.0))).floor() - * n as f64 - / num_bins as f64; - } + let expected_counts: Vec<_> = (0..num_bins) + .map(|i| { + let lower = d_min + (d_max - d_min) * (i as f64 / num_bins as f64); + let upper = d_min + (d_max - d_min) * ((i as f64 + 1.0) / num_bins as f64); + ((upper - lower) / (upper.powf(-1.0) - lower.powf(-1.0))).floor() * n as f64 + / num_bins as f64 + }) + .collect(); let chi_squared = expected_counts .iter() From 9b71bdad97e053e2741b4e5ee97487dec70ec94c Mon Sep 17 00:00:00 2001 From: Ignacio Duart Date: Mon, 6 Nov 2023 11:38:31 +0100 Subject: [PATCH 76/76] tm: cargo fmt --- crates/core/src/ring.rs | 3 +- crates/core/src/topology/mod.rs | 124 ++++++++++++++++++++++---------- 2 files changed, 89 insertions(+), 38 deletions(-) diff --git a/crates/core/src/ring.rs b/crates/core/src/ring.rs index 79f3412ae..664f2bdd9 100644 --- a/crates/core/src/ring.rs +++ b/crates/core/src/ring.rs @@ -16,11 +16,12 @@ use std::{ convert::TryFrom, fmt::Display, hash::Hasher, + ops::Add, sync::{ atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst}, Arc, }, - time::Duration, ops::Add, + time::Duration, }; use anyhow::bail; diff --git a/crates/core/src/topology/mod.rs b/crates/core/src/topology/mod.rs index 42d1e7be8..3135ae4f8 100644 --- a/crates/core/src/topology/mod.rs +++ b/crates/core/src/topology/mod.rs @@ -1,15 +1,19 @@ #![allow(unused_variables, dead_code)] -use std::{collections::BTreeMap, rc::Rc, time::{Duration, Instant}}; -use tracing::{debug, error, info}; -use request_density_tracker::cached_density_map::CachedDensityMap; use crate::ring::{Distance, Location}; +use request_density_tracker::cached_density_map::CachedDensityMap; +use std::{ + collections::BTreeMap, + rc::Rc, + time::{Duration, Instant}, +}; +use tracing::{debug, error, info}; use self::{request_density_tracker::DensityMapError, small_world_rand::random_link_distance}; +mod connection_evaluator; mod request_density_tracker; mod small_world_rand; -mod connection_evaluator; const SLOW_CONNECTION_EVALUATOR_WINDOW_DURATION: Duration = Duration::from_secs(5 * 60); const FAST_CONNECTION_EVALUATOR_WINDOW_DURATION: Duration = Duration::from_secs(60); @@ -20,17 +24,17 @@ const RANDOM_CLOSEST_DISTANCE: f64 = 1.0 / 1000.0; /// The goal of `TopologyManager` is to select new connections such that the /// distribution of connections is as close as possible to the /// distribution of outbound requests. -/// +/// /// This is done by maintaining a `RequestDensityTracker` which tracks the /// distribution of requests in the network. The `TopologyManager` uses this /// tracker to create a `DensityMap` which is used to evaluate the density of /// requests at a given location. -/// +/// /// The `TopologyManager` uses the density map to select the best candidate /// location, which is assumed to be close to peer connections that are /// currently receiving a lot of requests. This should have the effect of /// "balancing" out requests over time. -/// +/// /// The `TopologyManager` also uses a `ConnectionEvaluator` to evaluate whether /// a given connection is better than all other connections within a predefined /// time window. The goal of this is to select the best connections over time @@ -45,19 +49,29 @@ pub(crate) struct TopologyManager { impl TopologyManager { /// Create a new TopologyManager specifying the peer's own Location - pub(crate) fn new(this_peer_location : Location) -> Self { + pub(crate) fn new(this_peer_location: Location) -> Self { info!("Creating a new TopologyManager instance"); TopologyManager { - slow_connection_evaluator: connection_evaluator::ConnectionEvaluator::new(SLOW_CONNECTION_EVALUATOR_WINDOW_DURATION), - fast_connection_evaluator: connection_evaluator::ConnectionEvaluator::new(FAST_CONNECTION_EVALUATOR_WINDOW_DURATION), - request_density_tracker: request_density_tracker::RequestDensityTracker::new(REQUEST_DENSITY_TRACKER_WINDOW_SIZE), + slow_connection_evaluator: connection_evaluator::ConnectionEvaluator::new( + SLOW_CONNECTION_EVALUATOR_WINDOW_DURATION, + ), + fast_connection_evaluator: connection_evaluator::ConnectionEvaluator::new( + FAST_CONNECTION_EVALUATOR_WINDOW_DURATION, + ), + request_density_tracker: request_density_tracker::RequestDensityTracker::new( + REQUEST_DENSITY_TRACKER_WINDOW_SIZE, + ), cached_density_map: CachedDensityMap::new(REGENERATE_DENSITY_MAP_INTERVAL), this_peer_location, } } /// Record a request and the location it's targeting - pub(crate) fn record_request(&mut self, requested_location: Location, request_type : RequestType) { + pub(crate) fn record_request( + &mut self, + requested_location: Location, + request_type: RequestType, + ) { debug!("Recording request for location: {:?}", requested_location); self.request_density_tracker.sample(requested_location); } @@ -65,45 +79,73 @@ impl TopologyManager { /// Decide whether to accept a connection from a new candidate peer based on its location /// and current neighbors and request density, along with how it compares to other /// recent candidates. - pub(crate) fn evaluate_new_connection(&mut self, current_neighbors: &BTreeMap, candidate_location: Location, acquisition_strategy : AcquisitionStrategy) -> Result { - self.evaluate_new_connection_with_current_time(current_neighbors, candidate_location, acquisition_strategy, Instant::now()) + pub(crate) fn evaluate_new_connection( + &mut self, + current_neighbors: &BTreeMap, + candidate_location: Location, + acquisition_strategy: AcquisitionStrategy, + ) -> Result { + self.evaluate_new_connection_with_current_time( + current_neighbors, + candidate_location, + acquisition_strategy, + Instant::now(), + ) } - fn evaluate_new_connection_with_current_time(&mut self, current_neighbors: &BTreeMap, candidate_location: Location, acquisition_strategy : AcquisitionStrategy, current_time : Instant) -> Result { - debug!("Evaluating new connection for candidate location: {:?}", candidate_location); + fn evaluate_new_connection_with_current_time( + &mut self, + current_neighbors: &BTreeMap, + candidate_location: Location, + acquisition_strategy: AcquisitionStrategy, + current_time: Instant, + ) -> Result { + debug!( + "Evaluating new connection for candidate location: {:?}", + candidate_location + ); let density_map = self.get_or_create_density_map(current_neighbors)?; let score = density_map.get_density_at(candidate_location)?; let accept = match acquisition_strategy { AcquisitionStrategy::Slow => { - self.fast_connection_evaluator.record_only_with_current_time(score, current_time); - self.slow_connection_evaluator.record_and_eval_with_current_time(score, current_time) - }, + self.fast_connection_evaluator + .record_only_with_current_time(score, current_time); + self.slow_connection_evaluator + .record_and_eval_with_current_time(score, current_time) + } AcquisitionStrategy::Fast => { - self.slow_connection_evaluator.record_only_with_current_time(score, current_time); - self.fast_connection_evaluator.record_and_eval_with_current_time(score, current_time) - }, + self.slow_connection_evaluator + .record_only_with_current_time(score, current_time); + self.fast_connection_evaluator + .record_and_eval_with_current_time(score, current_time) + } }; - + Ok(accept) } /// Get the ideal location for a new connection based on current neighbors and request density - pub(crate) fn get_best_candidate_location(&mut self, current_neighbors: &BTreeMap) -> Result { + pub(crate) fn get_best_candidate_location( + &mut self, + current_neighbors: &BTreeMap, + ) -> Result { debug!("Retrieving best candidate location"); let density_map = self.get_or_create_density_map(current_neighbors)?; - + let best_location = match density_map.get_max_density() { Ok(location) => { debug!("Max density found at location: {:?}", location); location - }, + } Err(_) => { - error!("An error occurred while getting max density, falling back to random location"); + error!( + "An error occurred while getting max density, falling back to random location" + ); self.random_location() - }, + } }; - + Ok(best_location) } @@ -117,13 +159,17 @@ impl TopologyManager { } else { self.this_peer_location.as_f64() + distance.as_f64() }; - let location_f64 = location_f64.rem_euclid(1.0); // Ensure result is in [0.0, 1.0) + let location_f64 = location_f64.rem_euclid(1.0); // Ensure result is in [0.0, 1.0) Location::new(location_f64) - } + } - fn get_or_create_density_map(&mut self, current_neighbors: &BTreeMap) -> Result, DensityMapError> { + fn get_or_create_density_map( + &mut self, + current_neighbors: &BTreeMap, + ) -> Result, DensityMapError> { debug!("Getting or creating density map"); - self.cached_density_map.get_or_create(&self.request_density_tracker, current_neighbors) + self.cached_density_map + .get_or_create(&self.request_density_tracker, current_neighbors) } } @@ -131,7 +177,7 @@ pub(crate) enum RequestType { Get, Put, Join, - Subscribe + Subscribe, } pub(crate) enum AcquisitionStrategy { @@ -144,8 +190,8 @@ pub(crate) enum AcquisitionStrategy { #[cfg(test)] mod tests { - use crate::ring::Location; use super::TopologyManager; + use crate::ring::Location; #[test] fn test_topology_manager() { @@ -165,7 +211,9 @@ mod tests { requests.push(requested_location); } - let best_candidate_location = topology_manager.get_best_candidate_location(¤t_neighbors).unwrap(); + let best_candidate_location = topology_manager + .get_best_candidate_location(¤t_neighbors) + .unwrap(); // Should be half way between 0.3 and 0.4 as that is where the most requests were assert_eq!(best_candidate_location, Location::new(0.35)); @@ -177,7 +225,9 @@ mod tests { let candidate_location = Location::new(i as f64 / 100.0); let score = topology_manager .get_or_create_density_map(¤t_neighbors) - .unwrap().get_density_at(candidate_location).unwrap(); + .unwrap() + .get_density_at(candidate_location) + .unwrap(); if score > best_score { best_score = score; best_location = candidate_location;