From 3cbe98940381266ea19546d874df0aae11b41093 Mon Sep 17 00:00:00 2001 From: adlrocha Date: Mon, 16 Jan 2023 19:27:49 +0100 Subject: [PATCH 01/82] Initial commit --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..11df919e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 ConsensusLab + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 9e1dd1c1310bee6ef304b09d7fb0e5e31d955787 Mon Sep 17 00:00:00 2001 From: Henrique Moniz <1785239+hmoniz@users.noreply.github.com> Date: Mon, 23 Jan 2023 14:54:36 +0000 Subject: [PATCH 02/82] Adds a simple async JSON-RPC client that can make requests via HTTP and subscribe to notifications via WS (#1) * Sketch of JSON-RPC client * filecoin websockets example added * add example for async websockets * Adds a simple async JSON-RPC client that can make requests via HTTP and subscribe to notifications via WS. * Clean up dependencies * Clean up dependencies * fix missing newlines * Add optional authorization bearer token to JSON-RPC requests --- .gitignore | 4 ++++ Cargo.toml | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..ba6e34331 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..27161ebf1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "ipc-client" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.68" +async-channel = "1.8.0" +async-trait = "0.1.61" +futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } +log = "0.4.17" +reqwest = { version = "0.11.13", features = ["json"] } +serde_json = "1.0.91" +tokio = { version = "1.16", features = ["full"] } +tokio-tungstenite = { version = "0.18.0", features = ["native-tls"] } +url = "2.3.1" From 1fc8c62d30347a8e319fe1dc857ec27d5c9443ff Mon Sep 17 00:00:00 2001 From: Henrique Moniz <1785239+hmoniz@users.noreply.github.com> Date: Wed, 15 Feb 2023 15:25:07 +0000 Subject: [PATCH 03/82] A module for reading the agent config file (#8) --- Cargo.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 27161ebf1..0157a85d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,14 @@ anyhow = "1.0.68" async-channel = "1.8.0" async-trait = "0.1.61" futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } +indoc = "2.0.0" log = "0.4.17" reqwest = { version = "0.11.13", features = ["json"] } +serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.91" tokio = { version = "1.16", features = ["full"] } tokio-tungstenite = { version = "0.18.0", features = ["native-tls"] } -url = "2.3.1" +toml = "0.7.2" +url = { version = "2.3.1", features = ["serde"] } +ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git" } +fvm_shared = { version = "3.0.0-alpha.17", default-features = false } From f9df742ba389eec4654f4a5876895f035f3c98a3 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Thu, 16 Feb 2023 11:20:40 +0800 Subject: [PATCH 04/82] Add lotus API (#4) * add submit to pool * resolve review feedbacks * add more log * add more log * update token amount to string conversion * update state wait * add nonce * add wallet apis * add read state * handle json rpc errors * clean up code * add install actor * use default wallet * convert from to string * remove version * field processing * update field names * update types * update types * remove bin * restruct code * rename types * add TODO * add lotus client doc * update naming * update error message * change to exmaple * rename sample example to wallet new * update doc test * update naming of traits and structs --- Cargo.toml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 0157a85d8..ae1600278 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,9 +15,23 @@ log = "0.4.17" reqwest = { version = "0.11.13", features = ["json"] } serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.91" +cid = { version = "0.8.3", default-features = false, features = ["serde-codec"] } tokio = { version = "1.16", features = ["full"] } tokio-tungstenite = { version = "0.18.0", features = ["native-tls"] } +derive_builder = "0.12.0" +num-traits = "0.2.15" +env_logger = "0.10.0" +base64 = "0.21.0" +strum = { version = "0.24", features = ["derive"] } toml = "0.7.2" url = { version = "2.3.1", features = ["serde"] } + +fvm_shared = {workspace = true} +fil_actors_runtime = {workspace = true} +ipc-sdk = { workspace = true } + +[workspace.dependencies] +fvm_shared = { version = "=3.0.0-alpha.17", default-features = false } +fil_actors_runtime = { git = "https://github.com/consensus-shipyard/fvm-utils", features = ["fil-actor"] } ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git" } -fvm_shared = { version = "3.0.0-alpha.17", default-features = false } + From 1a6fab355116cda8d9008b47dac30a7f2237721e Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Fri, 17 Feb 2023 19:30:41 +0800 Subject: [PATCH 05/82] Add cli command handler interface (#11) * support cli handler interface * remove unconstructed error * update readme * Update src/cli/mod.rs Co-authored-by: adlrocha * Update src/cli/mod.rs Co-authored-by: adlrocha * rename to arguments * Update mod.rs --------- Co-authored-by: adlrocha --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index ae1600278..bdae6b5db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ base64 = "0.21.0" strum = { version = "0.24", features = ["derive"] } toml = "0.7.2" url = { version = "2.3.1", features = ["serde"] } +clap = {version = "4.1.4", features = ["env", "derive"] } +thiserror = "1.0.38" fvm_shared = {workspace = true} fil_actors_runtime = {workspace = true} From 51a045d73a2499f3b1832f3d075c9d1bc819bb37 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Tue, 21 Feb 2023 21:53:38 +0800 Subject: [PATCH 06/82] Add `warp` based Http JSON RPC node (#9) * update marcos * add sample binary * add more exmaples * add more doc * use examples folder * add warp json rpc server * add rpc server only * update naming * Update src/node/jsonrpc.rs Co-authored-by: adlrocha * update based on review * Update src/node/jsonrpc.rs Co-authored-by: adlrocha * Update src/node/jsonrpc.rs Co-authored-by: adlrocha * Update src/node/jsonrpc.rs Co-authored-by: adlrocha * update based on review comments * format code * revert changes to jsonrpc tests * revert changes to lotus client * Json rpc server config (#22) * migrate json rpc config * revert changes to lotus client * shift parameter location * update doc * update config parsing tests * add more test error messages * change test structure --------- Co-authored-by: adlrocha --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index bdae6b5db..38e7517a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ base64 = "0.21.0" strum = { version = "0.24", features = ["derive"] } toml = "0.7.2" url = { version = "2.3.1", features = ["serde"] } +warp = "0.3.3" +bytes = "1.4.0" clap = {version = "4.1.4", features = ["env", "derive"] } thiserror = "1.0.38" From 85e3cc0e31510369f83a0ac6bb758c12bfc0d2ff Mon Sep 17 00:00:00 2001 From: adlrocha Date: Wed, 22 Feb 2023 09:57:19 +0100 Subject: [PATCH 07/82] subnet manager interface and lotus impl placeholder (#25) * subnet manager interface and lotus impl placeholder * minor fixes * address comments * move to subnet mod * patch interface * minor fixes --------- Co-authored-by: cryptoAtwill --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 38e7517a3..200d1904b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,9 +33,13 @@ thiserror = "1.0.38" fvm_shared = {workspace = true} fil_actors_runtime = {workspace = true} ipc-sdk = { workspace = true } +ipc-subnet-actor = { workspace = true } +ipc-gateway = { workspace = true } [workspace.dependencies] fvm_shared = { version = "=3.0.0-alpha.17", default-features = false } fil_actors_runtime = { git = "https://github.com/consensus-shipyard/fvm-utils", features = ["fil-actor"] } ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git" } +ipc-subnet-actor = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [] } +ipc-gateway = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [] } From 562ee685e78e38e3e51bf90ab87ff45e015421eb Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Thu, 23 Feb 2023 15:39:46 +0000 Subject: [PATCH 08/82] IPC-17: Create ipld/resolver crate (#32) * IPC-17: Add rust toolchain file. * IPC-17: Create ipld/resolver crate in the workspace * IPC-17: Use license-file --- Cargo.toml | 18 +++++++++++++----- ipld/resolver/Cargo.toml | 11 +++++++++++ ipld/resolver/src/lib.rs | 14 ++++++++++++++ rust-toolchain.toml | 4 ++++ 4 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 ipld/resolver/Cargo.toml create mode 100644 ipld/resolver/src/lib.rs create mode 100644 rust-toolchain.toml diff --git a/Cargo.toml b/Cargo.toml index 200d1904b..5afe0516e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,16 @@ +[workspace] +members = [".", "ipld/resolver"] + +[workspace.package] +authors = ["Protocol Labs"] +edition = "2021" +license-file = "LICENSE" + [package] name = "ipc-client" version = "0.1.0" -edition = "2021" +edition.workspace = true +license-file.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -27,11 +36,11 @@ toml = "0.7.2" url = { version = "2.3.1", features = ["serde"] } warp = "0.3.3" bytes = "1.4.0" -clap = {version = "4.1.4", features = ["env", "derive"] } +clap = { version = "4.1.4", features = ["env", "derive"] } thiserror = "1.0.38" -fvm_shared = {workspace = true} -fil_actors_runtime = {workspace = true} +fvm_shared = { workspace = true } +fil_actors_runtime = { workspace = true } ipc-sdk = { workspace = true } ipc-subnet-actor = { workspace = true } ipc-gateway = { workspace = true } @@ -42,4 +51,3 @@ fil_actors_runtime = { git = "https://github.com/consensus-shipyard/fvm-utils", ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git" } ipc-subnet-actor = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [] } ipc-gateway = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [] } - diff --git a/ipld/resolver/Cargo.toml b/ipld/resolver/Cargo.toml new file mode 100644 index 000000000..50659c9de --- /dev/null +++ b/ipld/resolver/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ipc_ipld_resolver" +description = "P2P library to resolve IPLD content across IPC subnets." +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license-file.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/ipld/resolver/src/lib.rs b/ipld/resolver/src/lib.rs new file mode 100644 index 000000000..7d12d9af8 --- /dev/null +++ b/ipld/resolver/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 000000000..4518cf19b --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "nightly-2022-10-03" +components = ["clippy", "llvm-tools-preview", "rustfmt"] +targets = ["wasm32-unknown-unknown"] From cf5839df8373bd1cf8e3346102598bf2df6b9053 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Fri, 24 Feb 2023 19:38:21 +0800 Subject: [PATCH 09/82] move to lib (#30) * move to lib * Update src/config/mod.rs Co-authored-by: Akosh Farkash * rename config function name * import path * rename crate --------- Co-authored-by: Akosh Farkash --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5afe0516e..9dd894777 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" license-file = "LICENSE" [package] -name = "ipc-client" +name = "ipc_agent" version = "0.1.0" edition.workspace = true license-file.workspace = true From 5f4e53c372aa2625e653f1e65571d0e2ab76ff05 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Mon, 27 Feb 2023 10:08:26 +0800 Subject: [PATCH 10/82] Create subnet actor (#28) * subnet manager interface and lotus impl placeholder * minor fixes * address comments * move to subnet mod * wip * add network name and version * add more comments * add state_actor_manifest_cid * wip * update param name * integrate with code cids * add manifest id * clean up code * revert changes * Update src/manager/lotus.rs Co-authored-by: adlrocha * Update Cargo.toml Co-authored-by: adlrocha * update review * add integration tests * update integration test * add integration tests * update local lotus url * add debug log * make log print string * fix network check * update return message * parse response * base64 decode return * set test network * add doc to test * shift file locations --------- Co-authored-by: Alfonso de la Rocha --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 9dd894777..9f6d34966 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,14 +38,17 @@ warp = "0.3.3" bytes = "1.4.0" clap = { version = "4.1.4", features = ["env", "derive"] } thiserror = "1.0.38" +serde_tuple = "0.5.0" fvm_shared = { workspace = true } fil_actors_runtime = { workspace = true } ipc-sdk = { workspace = true } ipc-subnet-actor = { workspace = true } ipc-gateway = { workspace = true } +fvm_ipld_encoding = { workspace = true } [workspace.dependencies] +fvm_ipld_encoding = "0.3.3" fvm_shared = { version = "=3.0.0-alpha.17", default-features = false } fil_actors_runtime = { git = "https://github.com/consensus-shipyard/fvm-utils", features = ["fil-actor"] } ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git" } From 719431b55e4d145e10b7e29b948d4570c7b35115 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Mon, 27 Feb 2023 10:43:24 +0000 Subject: [PATCH 11/82] IPC-34: Discovery (#40) * IPC-34: Discovery behaviour * IPC-34: Add TODO with ticket numbers * IPC-34: Less hacky way of omitting Kademlia events. * IPC-34: DiscoveryConfig * IPC-34: Background lookup * IPC-35: Rename to target_connections * IPC-35: Log UnroutablePeer. Comments about ignoring Kademlia events. * IPC-35: Rename discovery::DiscoveryThing to discovery::Thing --- Cargo.toml | 12 +- ipld/resolver/Cargo.toml | 7 + ipld/resolver/src/behaviour/discovery.rs | 295 +++++++++++++++++++++++ ipld/resolver/src/behaviour/mod.rs | 25 ++ ipld/resolver/src/lib.rs | 19 +- ipld/resolver/src/service.rs | 15 ++ 6 files changed, 356 insertions(+), 17 deletions(-) create mode 100644 ipld/resolver/src/behaviour/discovery.rs create mode 100644 ipld/resolver/src/behaviour/mod.rs create mode 100644 ipld/resolver/src/service.rs diff --git a/Cargo.toml b/Cargo.toml index 9f6d34966..399bf8947 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,17 +15,17 @@ license-file.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0.68" +anyhow = { workspace = true } async-channel = "1.8.0" async-trait = "0.1.61" futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } indoc = "2.0.0" -log = "0.4.17" +log = { workspace = true } reqwest = { version = "0.11.13", features = ["json"] } serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.91" cid = { version = "0.8.3", default-features = false, features = ["serde-codec"] } -tokio = { version = "1.16", features = ["full"] } +tokio = { workspace = true } tokio-tungstenite = { version = "0.18.0", features = ["native-tls"] } derive_builder = "0.12.0" num-traits = "0.2.15" @@ -48,9 +48,15 @@ ipc-gateway = { workspace = true } fvm_ipld_encoding = { workspace = true } [workspace.dependencies] +anyhow = "1.0" +thiserror = "1.0" +tokio = { version = "1.16", features = ["full"] } +log = "0.4" + fvm_ipld_encoding = "0.3.3" fvm_shared = { version = "=3.0.0-alpha.17", default-features = false } fil_actors_runtime = { git = "https://github.com/consensus-shipyard/fvm-utils", features = ["fil-actor"] } ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git" } ipc-subnet-actor = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [] } ipc-gateway = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [] } +libipld = { version = "0.14", default-features = false, features = ["dag-cbor"] } diff --git a/ipld/resolver/Cargo.toml b/ipld/resolver/Cargo.toml index 50659c9de..e02fe36e3 100644 --- a/ipld/resolver/Cargo.toml +++ b/ipld/resolver/Cargo.toml @@ -9,3 +9,10 @@ license-file.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +libp2p = { version = "0.50", default-features = false, features = ["gossipsub", "kad", "identify", "ping", "noise", "yamux", "tcp", "dns", "mplex", "request-response", "metrics", "tokio", "macros"] } +libp2p-bitswap = "0.25" +libipld = { workspace = true } +log = { workspace = true } diff --git a/ipld/resolver/src/behaviour/discovery.rs b/ipld/resolver/src/behaviour/discovery.rs new file mode 100644 index 000000000..a16b54695 --- /dev/null +++ b/ipld/resolver/src/behaviour/discovery.rs @@ -0,0 +1,295 @@ +// Copyright 2019-2022 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT +use std::{ + borrow::Cow, + cmp, + collections::VecDeque, + task::{Context, Poll}, + time::Duration, +}; + +use libp2p::{ + core::{connection::ConnectionId, identity::PublicKey}, + kad::{ + handler::KademliaHandlerProto, store::MemoryStore, Kademlia, KademliaConfig, KademliaEvent, + QueryId, + }, + multiaddr::Protocol, + swarm::{ + behaviour::toggle::{Toggle, ToggleIntoConnectionHandler}, + derive_prelude::FromSwarm, + ConnectionHandler, IntoConnectionHandler, NetworkBehaviour, NetworkBehaviourAction, + PollParameters, + }, + Multiaddr, PeerId, +}; +use log::debug; +use thiserror::Error; +use tokio::time::Interval; + +// NOTE: The Discovery behaviour is largely based on what exists in Forest. If it ain't broken... +// NOTE: Not sure if emitting events is going to be useful yet, but for now it's an example of having one. + +/// Event generated by the `Discovery` behaviour. +#[derive(Debug)] +pub enum Event { + /// Event emitted when we first connect to a peer. + Connected(PeerId, Vec), + + /// Event emitted when the last connection to a peer is closed. + Disconnected(PeerId, Vec), +} + +/// `Discovery` behaviour configuration. +#[derive(Clone, Debug)] +pub struct Config { + /// Our own peer ID, needed to bootstrap Kademlia. + local_peer_id: PeerId, + /// Static list of addresses to bootstrap from. + user_defined: Vec<(PeerId, Multiaddr)>, + /// Number of connections at which point we pause further discovery lookups. + target_connections: usize, + /// Option to disable Kademlia, for example in a fixed static network. + enable_kademlia: bool, + /// Name of the network in the Kademlia protocol. + network_name: String, +} + +#[derive(Error, Debug)] +pub enum ConfigError { + #[error("invalid network: {0}")] + InvalidNetwork(String), + #[error("invalid bootstrap address: {0}")] + InvalidBootstrapAddress(Multiaddr), + #[error("no bootstrap address")] + NoBootstrapAddress, +} + +pub struct ConfigBuilder(Config); + +impl ConfigBuilder { + /// Create a default configuration with the given public key. + pub fn new(local_public_key: PublicKey, network_name: String) -> Result { + if network_name.is_empty() { + Err(ConfigError::InvalidNetwork(network_name)) + } else { + Ok(Self(Config { + local_peer_id: local_public_key.to_peer_id(), + user_defined: Vec::new(), + target_connections: usize::MAX, + enable_kademlia: true, + network_name, + })) + } + } + + /// Set the number of active connections at which we pause discovery. + pub fn with_max_connections(&mut self, limit: usize) -> &mut Self { + self.0.target_connections = limit; + self + } + + /// Set custom nodes which never expire, e.g. bootstrap or reserved nodes. + /// + /// The addresses must end with a `/p2p/` part. + pub fn with_user_defined(&mut self, user_defined: I) -> Result<&mut Self, ConfigError> + where + I: IntoIterator, + { + for multiaddr in user_defined { + let mut addr = multiaddr.clone(); + if let Some(Protocol::P2p(mh)) = addr.pop() { + let peer_id = PeerId::from_multihash(mh).unwrap(); + self.0.user_defined.push((peer_id, addr)) + } else { + return Err(ConfigError::InvalidBootstrapAddress(multiaddr)); + } + } + Ok(self) + } + + /// Configures if Kademlia is enabled. + pub fn with_kademlia(&mut self, value: bool) -> &mut Self { + self.0.enable_kademlia = value; + self + } + + /// Finish configuration and do a final check. + pub fn build(self) -> Result { + if self.0.enable_kademlia && self.0.user_defined.is_empty() { + Err(ConfigError::NoBootstrapAddress) + } else { + Ok(self.0) + } + } +} + +/// Discovery behaviour, periodically running a random lookup with Kademlia to find new peers. +/// +/// Our other option for peer discovery would be to rely on the Peer Exchange of Gossipsub. +/// However, the required Signed Records feature is not available in the Rust version of the library, as of v0.50. +pub struct Behaviour { + /// User-defined list of nodes and their addresses. + /// Typically includes bootstrap nodes, or it can be used for a static network. + user_defined: Vec<(PeerId, Multiaddr)>, + /// Kademlia behaviour, if enabled. + inner: Toggle>, + /// Number of current connections. + num_connections: usize, + /// Number of connections where further lookups are paused. + target_connections: usize, + /// Interval between random lookups. + lookup_interval: Interval, + /// Events to return when polled. + outbox: VecDeque, +} + +impl Behaviour { + /// Create a [`discovery::Behaviour`] from this configuration. + pub fn new(config: Config) -> Self { + let kademlia_opt = if config.enable_kademlia { + let mut kad_config = KademliaConfig::default(); + let protocol_name = format!("/ipc/kad/{}/kad/1.0.0", config.network_name); + kad_config.set_protocol_names(vec![Cow::Owned(protocol_name.as_bytes().to_vec())]); + + let store = MemoryStore::new(config.local_peer_id); + + let mut kademlia = Kademlia::with_config(config.local_peer_id, store, kad_config); + + for (peer_id, addr) in config.user_defined.iter() { + kademlia.add_address(peer_id, addr.clone()); + } + + // This shouldn't happen, we already checked the config. + kademlia + .bootstrap() + .unwrap_or_else(|e| panic!("Kademlia bootstrap failed: {}", e)); + + Some(kademlia) + } else { + None + }; + + Self { + user_defined: config.user_defined, + inner: kademlia_opt.into(), + lookup_interval: tokio::time::interval(Duration::from_secs(1)), + outbox: VecDeque::new(), + num_connections: 0, + target_connections: config.target_connections, + } + } + + /// Lookup a peer, unless we already know their address, so that we have a chance to connect to them later. + pub fn background_lookup(&mut self, peer_id: PeerId) { + if self.addresses_of_peer(&peer_id).is_empty() { + if let Some(kademlia) = self.inner.as_mut() { + kademlia.get_closest_peers(peer_id); + } + } + } +} + +impl NetworkBehaviour for Behaviour { + type ConnectionHandler = ToggleIntoConnectionHandler>; + + type OutEvent = Event; + + fn new_handler(&mut self) -> Self::ConnectionHandler { + self.inner.new_handler() + } + + fn addresses_of_peer(&mut self, peer_id: &PeerId) -> Vec { + let mut addrs = self + .user_defined + .iter() + .filter(|(p, _)| p == peer_id) + .map(|(_, a)| a.clone()) + .collect::>(); + + addrs.extend(self.inner.addresses_of_peer(peer_id)); + addrs + } + + fn on_swarm_event(&mut self, event: FromSwarm) { + match &event { + FromSwarm::ConnectionEstablished(e) => { + self.num_connections += 1; + if e.other_established == 0 { + let addrs = self.addresses_of_peer(&e.peer_id); + self.outbox.push_back(Event::Connected(e.peer_id, addrs)); + } + } + FromSwarm::ConnectionClosed(e) => { + self.num_connections -= 1; + if e.remaining_established == 0 { + let addrs = self.addresses_of_peer(&e.peer_id); + self.outbox.push_back(Event::Disconnected(e.peer_id, addrs)); + } + } + _ => {} + }; + self.inner.on_swarm_event(event) + } + + fn on_connection_handler_event( + &mut self, + peer_id: PeerId, + connection_id: ConnectionId, + event: <::Handler as ConnectionHandler>::OutEvent, + ) { + self.inner + .on_connection_handler_event(peer_id, connection_id, event) + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + params: &mut impl PollParameters, + ) -> std::task::Poll> { + // Emit own events first. + if let Some(ev) = self.outbox.pop_front() { + return Poll::Ready(NetworkBehaviourAction::GenerateEvent(ev)); + } + + // Trigger periodic queries. + if self.lookup_interval.poll_tick(cx).is_ready() { + if self.num_connections < self.target_connections { + if let Some(k) = self.inner.as_mut() { + let random_peer_id = PeerId::random(); + k.get_closest_peers(random_peer_id); + } + } + + // Schedule the next random query with exponentially increasing delay, capped at 60 seconds. + self.lookup_interval = tokio::time::interval(cmp::min( + self.lookup_interval.period() * 2, + Duration::from_secs(60), + )); + // we need to reset the interval, otherwise the next tick completes immediately. + self.lookup_interval.reset(); + } + + // Poll Kademlia. + while let Poll::Ready(ev) = self.inner.poll(cx, params) { + match ev { + // Not propagating Kademlia specific events, just the ones meant for the Swarm. + // The Kademlia configuration should ensure that peers are added automatically to the bucket, + // without need for manual action, so this should be informational only. + // The only event which could be a warning is the `UnroutablePeer`; I don't fully understand + // under which conditions it can arise though. It might be a good idea to log that. + NetworkBehaviourAction::GenerateEvent(out) => { + if let ev @ KademliaEvent::UnroutablePeer { .. } = out { + debug!("unexpected Kademlia event: {ev:?}") + } + continue; + } + other => { + return Poll::Ready(other.map_out(|_| unreachable!("continue'd"))); + } + } + } + + Poll::Pending + } +} diff --git a/ipld/resolver/src/behaviour/mod.rs b/ipld/resolver/src/behaviour/mod.rs new file mode 100644 index 000000000..6d6d4564e --- /dev/null +++ b/ipld/resolver/src/behaviour/mod.rs @@ -0,0 +1,25 @@ +use libipld::store::StoreParams; +use libp2p::{gossipsub::Gossipsub, identify, ping, swarm::NetworkBehaviour}; +use libp2p_bitswap::Bitswap; + +mod discovery; + +/// Libp2p behaviour to manage content resolution from other subnets, using: +/// +/// * Kademlia for peer discovery +/// * Gossipsub to advertise subnet membership +/// * Bitswap to resolve CIDs +#[derive(NetworkBehaviour)] +pub struct IpldResolver { + ping: ping::Behaviour, + identify: identify::Behaviour, + discovery: discovery::Behaviour, + gossipsub: Gossipsub, // TODO (IPC-35): Wrap into Membership + bitswap: Bitswap

, // TODO (IPC-36): Wrap into Resolve +} + +// Unfortunately by using `#[derive(NetworkBehaviour)]` we cannot easily inspects events +// from the inner behaviours, e.g. we cannot poll a behaviour and if it returns something +// of interest then call a method on another behaviour. We can do this in another wrapper +// where we manually implement `NetworkBehaviour`, or the outer service where we drive the +// Swarm; there we are free to call any of the behaviours. diff --git a/ipld/resolver/src/lib.rs b/ipld/resolver/src/lib.rs index 7d12d9af8..369f8ffa9 100644 --- a/ipld/resolver/src/lib.rs +++ b/ipld/resolver/src/lib.rs @@ -1,14 +1,5 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +// TODO (IPC-38): Remove dead code allowances. +#[allow(dead_code)] +mod behaviour; +#[allow(dead_code)] +mod service; diff --git a/ipld/resolver/src/service.rs b/ipld/resolver/src/service.rs new file mode 100644 index 000000000..047a049cc --- /dev/null +++ b/ipld/resolver/src/service.rs @@ -0,0 +1,15 @@ +use libipld::store::StoreParams; +use libp2p::Swarm; + +use crate::behaviour::IpldResolver; + +pub struct IpldResolverService { + swarm: Swarm>, +} + +impl IpldResolverService

{ + /// Start the swarm listening for incoming connections and drive the events forward. + pub async fn run(self) -> anyhow::Result<()> { + todo!("IPC-37") + } +} From df19217f439555e2243d64f44dde5b46b8fa2890 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Wed, 1 Mar 2023 17:21:03 +0800 Subject: [PATCH 12/82] add json rpc handler trait (#45) * add json rpc handler trait * format code * update response to serialize * change pass request by ownership * create subnet (#46) * init commit * wip * add handlers * enable tests * remove logic first * add gateway as const * remove gateway addr in test * rename functions --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 399bf8947..9ef3a7a3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ bytes = "1.4.0" clap = { version = "4.1.4", features = ["env", "derive"] } thiserror = "1.0.38" serde_tuple = "0.5.0" +once_cell = "1.17.1" fvm_shared = { workspace = true } fil_actors_runtime = { workspace = true } From 1071692999693fe42690eb0aa1d9750aefaf28da Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Wed, 1 Mar 2023 11:58:56 +0000 Subject: [PATCH 13/82] CHORE: Setup a CI workflow and add license headers (#50) * LICENSE: Add scripts and makefile. * LICENSE: Add license headers. * CI: Add github workflow * LICENSE: Remove Apache * CHORE: Format codebase. * CHORE: Fix clippy * CI: Add components * CI: Test all workspaces * CI: Fix branch name. * CI: Check in Cargo.lock; this is an application, not a library after all. * CI: Try removing the non-existing file from the hash; perhaps it is created on CI * CI: Leave just the lock file, maybe that won't change * CI: Remove the path wildcard --- .gitignore | 1 - Makefile | 26 +++++++++++++++ ipld/resolver/src/behaviour/discovery.rs | 3 +- ipld/resolver/src/behaviour/mod.rs | 2 ++ ipld/resolver/src/lib.rs | 2 ++ ipld/resolver/src/service.rs | 2 ++ scripts/add_license.sh | 42 ++++++++++++++++++++++++ scripts/copyright.txt | 2 ++ 8 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 Makefile create mode 100755 scripts/add_license.sh create mode 100644 scripts/copyright.txt diff --git a/.gitignore b/.gitignore index ba6e34331..4ef6b74f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ .idea/ *.iml /target -/Cargo.lock diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..e031659cb --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +.PHONY: all build test lint license check-fmt check-clippy + +all: test build + +build: + cargo build --release + +test: + cargo test --release --workspace + +clean: + cargo clean + +lint: \ + license \ + check-fmt \ + check-clippy + +license: + ./scripts/add_license.sh + +check-fmt: + cargo fmt --all --check + +check-clippy: + cargo clippy --all --tests -- -D clippy::all diff --git a/ipld/resolver/src/behaviour/discovery.rs b/ipld/resolver/src/behaviour/discovery.rs index a16b54695..4e3dba650 100644 --- a/ipld/resolver/src/behaviour/discovery.rs +++ b/ipld/resolver/src/behaviour/discovery.rs @@ -1,5 +1,6 @@ +// Copyright 2022-2023 Protocol Labs // Copyright 2019-2022 ChainSafe Systems -// SPDX-License-Identifier: Apache-2.0, MIT +// SPDX-License-Identifier: MIT use std::{ borrow::Cow, cmp, diff --git a/ipld/resolver/src/behaviour/mod.rs b/ipld/resolver/src/behaviour/mod.rs index 6d6d4564e..9466e150b 100644 --- a/ipld/resolver/src/behaviour/mod.rs +++ b/ipld/resolver/src/behaviour/mod.rs @@ -1,3 +1,5 @@ +// Copyright 2022-2023 Protocol Labs +// SPDX-License-Identifier: MIT use libipld::store::StoreParams; use libp2p::{gossipsub::Gossipsub, identify, ping, swarm::NetworkBehaviour}; use libp2p_bitswap::Bitswap; diff --git a/ipld/resolver/src/lib.rs b/ipld/resolver/src/lib.rs index 369f8ffa9..067b9e674 100644 --- a/ipld/resolver/src/lib.rs +++ b/ipld/resolver/src/lib.rs @@ -1,3 +1,5 @@ +// Copyright 2022-2023 Protocol Labs +// SPDX-License-Identifier: MIT // TODO (IPC-38): Remove dead code allowances. #[allow(dead_code)] mod behaviour; diff --git a/ipld/resolver/src/service.rs b/ipld/resolver/src/service.rs index 047a049cc..24362bd79 100644 --- a/ipld/resolver/src/service.rs +++ b/ipld/resolver/src/service.rs @@ -1,3 +1,5 @@ +// Copyright 2022-2023 Protocol Labs +// SPDX-License-Identifier: MIT use libipld::store::StoreParams; use libp2p::Swarm; diff --git a/scripts/add_license.sh b/scripts/add_license.sh new file mode 100755 index 000000000..d011bd418 --- /dev/null +++ b/scripts/add_license.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# +# Checks if the source code contains required license and adds it if necessary. +# Returns 1 if there was a missing license, 0 otherwise. +COPYRIGHT_TXT=$(dirname $0)/copyright.txt + +# Any year is fine. We can update the year as a single PR in all files that have it up to last year. +PAT_PL=".*// Copyright 2022-202\d Protocol Labs.*" +PAT_SPDX="/*// SPDX-License-Identifier: MIT.*" + +# Look at enough lines so that we can include multiple copyright holders. +LINES=4 + +ret=0 + +# NOTE: When files are moved/split/deleted, the following queries would find and recreate them in the original place. +# To avoid that, first commit the changes, then run the linter; that way only the new places are affected. + +# Look for files without headers. +for file in $(git grep --cached -Il '' -- '*.rs'); do + header=$(head -$LINES "$file") + if ! echo "$header" | grep -q -P "$PAT_SPDX"; then + echo "$file was missing header" + cat $COPYRIGHT_TXT "$file" > temp + mv temp "$file" + ret=1 + fi +done + +# Look for changes that don't have the new copyright holder. +for file in $(git diff --diff-filter=d --name-only main -- '*.rs'); do + header=$(head -$LINES "$file") + if ! echo "$header" | grep -q -P "$PAT_PL"; then + echo "$file was missing Protocol Labs" + head -1 $COPYRIGHT_TXT > temp + cat "$file" >> temp + mv temp "$file" + ret=1 + fi +done + +exit $ret diff --git a/scripts/copyright.txt b/scripts/copyright.txt new file mode 100644 index 000000000..754c135d2 --- /dev/null +++ b/scripts/copyright.txt @@ -0,0 +1,2 @@ +// Copyright 2022-2023 Protocol Labs +// SPDX-License-Identifier: MIT From 61f31c8854f4df6e80f16767be215f09e2ddcdac Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Fri, 3 Mar 2023 11:25:04 +0000 Subject: [PATCH 14/82] IPC-35: Membership (#44) * IPC-35: Membership wrapper. * IPC-35: WIP * IPC-35: ProviderRecord. Membership skeleton. * IPC-35: Test provider record roundtrip * IPC-35: Test provider record is tamper proof * IPC-35: Add/remove/publish methods. * IPC-35: Add/remove events from Kademlia. * IPC-35: SubnetProviderCache * IPC-35: Message handling. * IPC-35: Change the events to contain delta. Add SkippedProvider event. * IPC-35: Test that providers are listed. Fix timestamp. * IPC-35: Only keep the latest. Return delta. * IPC-35: Test pruning by timestamp. * CI-35: Add headers. * IPC-35: Update Cargo.lock * IPC-35: Clippy fixes --- Cargo.toml | 10 +- ipld/resolver/Cargo.toml | 34 ++- ipld/resolver/src/arb.rs | 35 +++ ipld/resolver/src/behaviour/discovery.rs | 46 ++- ipld/resolver/src/behaviour/membership.rs | 241 ++++++++++++++++ ipld/resolver/src/behaviour/mod.rs | 7 +- ipld/resolver/src/lib.rs | 7 + ipld/resolver/src/provider_cache.rs | 327 ++++++++++++++++++++++ ipld/resolver/src/provider_record.rs | 232 +++++++++++++++ 9 files changed, 922 insertions(+), 17 deletions(-) create mode 100644 ipld/resolver/src/arb.rs create mode 100644 ipld/resolver/src/behaviour/membership.rs create mode 100644 ipld/resolver/src/provider_cache.rs create mode 100644 ipld/resolver/src/provider_record.rs diff --git a/Cargo.toml b/Cargo.toml index 9ef3a7a3e..6dc859a52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ futures-util = { version = "0.3", default-features = false, features = ["sink", indoc = "2.0.0" log = { workspace = true } reqwest = { version = "0.11.13", features = ["json"] } -serde = { version = "1.0.152", features = ["derive"] } +serde = { workspace = true } serde_json = "1.0.91" cid = { version = "0.8.3", default-features = false, features = ["serde-codec"] } tokio = { workspace = true } @@ -50,11 +50,15 @@ fvm_ipld_encoding = { workspace = true } [workspace.dependencies] anyhow = "1.0" +log = "0.4" +serde = { version = "1.0", features = ["derive"] } thiserror = "1.0" tokio = { version = "1.16", features = ["full"] } -log = "0.4" +quickcheck = "1" +quickcheck_macros = "1" + -fvm_ipld_encoding = "0.3.3" +fvm_ipld_encoding = "0.3" fvm_shared = { version = "=3.0.0-alpha.17", default-features = false } fil_actors_runtime = { git = "https://github.com/consensus-shipyard/fvm-utils", features = ["fil-actor"] } ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git" } diff --git a/ipld/resolver/Cargo.toml b/ipld/resolver/Cargo.toml index e02fe36e3..31a3bcbd7 100644 --- a/ipld/resolver/Cargo.toml +++ b/ipld/resolver/Cargo.toml @@ -12,7 +12,39 @@ license-file.workspace = true anyhow = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } -libp2p = { version = "0.50", default-features = false, features = ["gossipsub", "kad", "identify", "ping", "noise", "yamux", "tcp", "dns", "mplex", "request-response", "metrics", "tokio", "macros"] } +libp2p = { version = "0.50", default-features = false, features = [ + "gossipsub", + "kad", + "identify", + "ping", + "noise", + "yamux", + "tcp", + "dns", + "mplex", + "request-response", + "metrics", + "tokio", + "macros", + "serde", + "secp256k1", +] } libp2p-bitswap = "0.25" libipld = { workspace = true } log = { workspace = true } +serde = { workspace = true } +quickcheck = { workspace = true, optional = true } + +ipc-sdk = { workspace = true } +fvm_ipld_encoding = { workspace = true } +fvm_shared = { workspace = true, optional = true } + +[dev-dependencies] +quickcheck = { workspace = true } +quickcheck_macros = { workspace = true } +fvm_shared = { workspace = true, features = ["arb"] } + + +[features] +default = ["arb"] +arb = ["quickcheck", "fvm_shared/arb"] diff --git a/ipld/resolver/src/arb.rs b/ipld/resolver/src/arb.rs new file mode 100644 index 000000000..77da083a5 --- /dev/null +++ b/ipld/resolver/src/arb.rs @@ -0,0 +1,35 @@ +// Copyright 2022-2023 Protocol Labs +// SPDX-License-Identifier: MIT +use fvm_shared::address::Address; +use ipc_sdk::subnet_id::{SubnetID, ROOTNET_ID}; +use quickcheck::Arbitrary; + +/// Unfortunately an arbitrary `DelegatedAddress` can be inconsistent +/// with bytes that do not correspond to its length. This struct fixes +/// that so we can generate arbitrary addresses that don't fail equality +/// after a roundtrip. +#[derive(Clone, Debug)] +pub struct ArbAddress(pub Address); + +impl Arbitrary for ArbAddress { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let addr = Address::arbitrary(g); + let bz = addr.to_bytes(); + let addr = Address::from_bytes(&bz).expect("address roundtrip works"); + Self(addr) + } +} + +#[derive(Clone, Debug)] +pub struct ArbSubnetID(pub SubnetID); + +impl Arbitrary for ArbSubnetID { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let mut parent = ROOTNET_ID.clone(); + for _ in 0..=u8::arbitrary(g) % 5 { + let addr = ArbAddress::arbitrary(g).0; + parent = SubnetID::new_from_parent(&parent, addr); + } + Self(parent) + } +} diff --git a/ipld/resolver/src/behaviour/discovery.rs b/ipld/resolver/src/behaviour/discovery.rs index 4e3dba650..e0f98fa72 100644 --- a/ipld/resolver/src/behaviour/discovery.rs +++ b/ipld/resolver/src/behaviour/discovery.rs @@ -39,6 +39,13 @@ pub enum Event { /// Event emitted when the last connection to a peer is closed. Disconnected(PeerId, Vec), + + /// Event emitted when a peer is added or updated in the routing table, + /// which means if we later ask for its addresses, they should be known. + Added(PeerId, Vec), + + /// Event emitted when a peer is removed from the routing table. + Removed(PeerId), } /// `Discovery` behaviour configuration. @@ -274,19 +281,38 @@ impl NetworkBehaviour for Behaviour { // Poll Kademlia. while let Poll::Ready(ev) = self.inner.poll(cx, params) { match ev { - // Not propagating Kademlia specific events, just the ones meant for the Swarm. - // The Kademlia configuration should ensure that peers are added automatically to the bucket, - // without need for manual action, so this should be informational only. - // The only event which could be a warning is the `UnroutablePeer`; I don't fully understand - // under which conditions it can arise though. It might be a good idea to log that. - NetworkBehaviourAction::GenerateEvent(out) => { - if let ev @ KademliaEvent::UnroutablePeer { .. } = out { - debug!("unexpected Kademlia event: {ev:?}") + NetworkBehaviourAction::GenerateEvent(ev) => { + match ev { + // Not expecting unroutable peers + KademliaEvent::UnroutablePeer { .. } => { + debug!("unexpected Kademlia event: {ev:?}") + } + // Information only. + KademliaEvent::InboundRequest { .. } + | KademliaEvent::OutboundQueryProgressed { .. } => {} + // The config ensures peers are added to the table if there's room. + // We're not emitting these as known peers because the address will probably not be returned by `addresses_of_peer`, + // so the outside service would have to keep track, which is not what we want. + KademliaEvent::PendingRoutablePeer { .. } + | KademliaEvent::RoutablePeer { .. } => {} + // This event should ensure that we will be able to answer address lookups later. + KademliaEvent::RoutingUpdated { + peer, + addresses, + old_peer, + .. + } => { + // There are two events here; we can only return one, so let's defer them to the outbox. + if let Some(peer_id) = old_peer { + self.outbox.push_back(Event::Removed(peer_id)) + } + self.outbox + .push_back(Event::Added(peer, addresses.into_vec())) + } } - continue; } other => { - return Poll::Ready(other.map_out(|_| unreachable!("continue'd"))); + return Poll::Ready(other.map_out(|_| unreachable!("already handled"))); } } } diff --git a/ipld/resolver/src/behaviour/membership.rs b/ipld/resolver/src/behaviour/membership.rs new file mode 100644 index 000000000..ae95edbae --- /dev/null +++ b/ipld/resolver/src/behaviour/membership.rs @@ -0,0 +1,241 @@ +// Copyright 2022-2023 Protocol Labs +// SPDX-License-Identifier: MIT +use std::collections::VecDeque; +use std::task::{Context, Poll}; +use std::time::Duration; + +use ipc_sdk::subnet_id::SubnetID; +use libp2p::core::connection::ConnectionId; +use libp2p::gossipsub::{GossipsubEvent, GossipsubMessage, IdentTopic}; +use libp2p::identity::Keypair; +use libp2p::swarm::derive_prelude::FromSwarm; +use libp2p::swarm::{NetworkBehaviourAction, PollParameters}; +use libp2p::Multiaddr; +use libp2p::{ + gossipsub::Gossipsub, + swarm::{ConnectionHandler, IntoConnectionHandler, NetworkBehaviour}, + PeerId, +}; +use log::{debug, error, warn}; +use tokio::time::Interval; + +use crate::provider_cache::{ProviderDelta, SubnetProviderCache}; +use crate::provider_record::{SignedProviderRecord, Timestamp}; + +/// `Gossipsub` subnet membership topic identifier. +const PUBSUB_MEMBERSHIP: &str = "/ipc/membership"; + +struct Config { + /// Network name to be combined into the Gossipsub topic. + network_name: String, +} + +/// Events emitted by the [`membership::Behaviour`] behaviour. +#[derive(Debug)] +pub enum Event { + /// Indicate a change in the subnets a peer is known to support. + Updated((PeerId, ProviderDelta)), + + /// Indicate that we no longer treat a peer as routable and removed all their supported subnet associations. + Removed(PeerId), + + /// We could not add a provider record to the cache because the chache hasn't + /// been told yet that the provider peer is routable. This event can be used + /// to trigger a lookup by the discovery module to learn the address. + Skipped(PeerId), +} + +/// A [`NetworkBehaviour`] internally using [`Gossipsub`] to learn which +/// peer is able to resolve CIDs in different subnets. +pub struct Behaviour { + /// [`Gossipsub`] behaviour to spread the information about subnet membership. + inner: Gossipsub, + /// Events to return when polled. + outbox: VecDeque, + /// [`Keypair`] used to construct [`SignedProviderRecord`] instances. + keypair: Keypair, + /// Name of the [`Gossipsub`] topic where subnet memberships are published. + membership_topic: IdentTopic, // Topic::new(format!("{}/{}", PUBSUB_MEMBERSHIP, network_name) + /// List of subnet IDs this agent is providing data for. + subnet_ids: Vec, + /// Caching the latest state of subnet providers. + provider_cache: SubnetProviderCache, + /// Interval between publishing the currently supported subnets. + /// + /// This acts like a heartbeat; if a peer doesn't publish its snapshot for a long time, + /// other agents can prune it from their cache and not try to contact for resolution. + publish_interval: Interval, + /// Maximum time a provider can be without an update before it's pruned from the cache. + max_provider_age: Duration, +} + +impl Behaviour { + /// Set all the currently supported subnet IDs, then publish the updated list. + pub fn set_subnet_ids(&mut self, subnet_ids: Vec) -> anyhow::Result<()> { + self.subnet_ids = subnet_ids; + self.publish_membership() + } + + /// Add a subnet to the list of supported subnets, then publish the updated list. + pub fn add_subnet_id(&mut self, subnet_id: SubnetID) -> anyhow::Result<()> { + if self.subnet_ids.contains(&subnet_id) { + return Ok(()); + } + self.subnet_ids.push(subnet_id); + self.publish_membership() + } + + /// Remove a subnet from the list of supported subnets, then publish the updated list. + pub fn remove_subnet_id(&mut self, subnet_id: SubnetID) -> anyhow::Result<()> { + if !self.subnet_ids.contains(&subnet_id) { + return Ok(()); + } + self.subnet_ids.retain(|id| id != &subnet_id); + self.publish_membership() + } + + /// Send a message through Gossipsub to let everyone know about the current configuration. + fn publish_membership(&mut self) -> anyhow::Result<()> { + let record = SignedProviderRecord::new(&self.keypair, self.subnet_ids.clone())?; + let data = record.into_envelope().into_protobuf_encoding(); + let _msg_id = self.inner.publish(self.membership_topic.clone(), data)?; + Ok(()) + } + + /// Mark a peer as routable in the cache. + /// + /// Call this method when the discovery service learns the address of a peer. + pub fn set_routable(&mut self, peer_id: PeerId) { + self.provider_cache.set_routable(peer_id) + } + + /// Mark a peer as unroutable in the cache. + /// + /// Call this method when the discovery service forgets the address of a peer. + pub fn set_unroutable(&mut self, peer_id: PeerId) { + self.provider_cache.set_unroutable(peer_id); + self.outbox.push_back(Event::Removed(peer_id)) + } + + /// List the current providers of a subnet. + /// + /// Call this method when looking for a peer to resolve content from. + pub fn providers_of_subnet(&self, subnet_id: &SubnetID) -> Vec { + self.provider_cache.providers_of_subnet(subnet_id) + } + + /// Parse and handle a [`GossipsubMessage`]. If it's from the expected topic, + /// then raise domain event to let the rest of the application know about a + /// provider. Also update all the book keeping in the behaviour that we use + /// to answer future queries about the topic. + fn handle_message(&mut self, msg: GossipsubMessage) -> Option { + if msg.topic == self.membership_topic.hash() { + match SignedProviderRecord::from_bytes(&msg.data).map(|r| r.into_record()) { + Ok(record) => match self.provider_cache.add_provider(&record) { + None => return Some(Event::Skipped(record.peer_id)), + Some(d) if d.is_empty() => return None, + Some(d) => return Some(Event::Updated((record.peer_id, d))), + }, + Err(e) => { + warn!( + "Gossip message from peer {:?} could not be deserialized: {e}", + msg.source + ); + } + } + } else { + warn!("unknown gossipsub topic: {}", msg.topic); + } + None + } + + /// Remove any membership record that hasn't been updated for a long time. + fn prune_membership(&mut self) { + let cutoff_timestamp = Timestamp::now() - self.max_provider_age; + let pruned = self.provider_cache.prune_providers(cutoff_timestamp); + for peer_id in pruned { + self.outbox.push_back(Event::Removed(peer_id)) + } + } +} + +impl NetworkBehaviour for Behaviour { + type ConnectionHandler = ::ConnectionHandler; + type OutEvent = Event; + + fn new_handler(&mut self) -> Self::ConnectionHandler { + self.inner.new_handler() + } + + fn addresses_of_peer(&mut self, peer_id: &PeerId) -> Vec { + self.inner.addresses_of_peer(peer_id) + } + + fn on_swarm_event(&mut self, event: FromSwarm) { + self.inner.on_swarm_event(event) + } + + fn on_connection_handler_event( + &mut self, + peer_id: PeerId, + connection_id: ConnectionId, + event: <::Handler as ConnectionHandler>::OutEvent, + ) { + self.inner + .on_connection_handler_event(peer_id, connection_id, event) + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + params: &mut impl PollParameters, + ) -> std::task::Poll> { + // Emit own events first. + if let Some(ev) = self.outbox.pop_front() { + return Poll::Ready(NetworkBehaviourAction::GenerateEvent(ev)); + } + + // Republish our current peer record snapshot and prune old records. + if self.publish_interval.poll_tick(cx).is_ready() { + if let Err(e) = self.publish_membership() { + error!("error publishing membership: {e}") + }; + self.prune_membership(); + } + + // Poll Gossipsub for events; this is where we can handle Gossipsub messages and + // store the associations from peers to subnets. + while let Poll::Ready(ev) = self.inner.poll(cx, params) { + match ev { + NetworkBehaviourAction::GenerateEvent(ev) => { + match ev { + // NOTE: We could (ab)use the Gossipsub mechanism itself to signal subnet membership, + // however I think the information would only spread to our nearest neighbours we are + // connected to. If we assume there are hundreds of agents in each subnet which may + // or may not overlap, and each agent is connected to ~50 other agents, then the chance + // that there are subnets from which there are no or just a few connections is not + // insignificant. For this reason I oped to use messages instead, and let the content + // carry the information, spreading through the Gossipsub network regardless of the + // number of connected peers. + GossipsubEvent::Subscribed { .. } | GossipsubEvent::Unsubscribed { .. } => { + } + // Log potential misconfiguration. + GossipsubEvent::GossipsubNotSupported { peer_id } => { + debug!("peer {peer_id} doesn't support gossipsub"); + } + GossipsubEvent::Message { message, .. } => { + if let Some(ev) = self.handle_message(message) { + return Poll::Ready(NetworkBehaviourAction::GenerateEvent(ev)); + } + } + } + } + other => { + return Poll::Ready(other.map_out(|_| unreachable!("already handled"))); + } + } + } + + Poll::Pending + } +} diff --git a/ipld/resolver/src/behaviour/mod.rs b/ipld/resolver/src/behaviour/mod.rs index 9466e150b..931fd0596 100644 --- a/ipld/resolver/src/behaviour/mod.rs +++ b/ipld/resolver/src/behaviour/mod.rs @@ -1,10 +1,11 @@ // Copyright 2022-2023 Protocol Labs // SPDX-License-Identifier: MIT use libipld::store::StoreParams; -use libp2p::{gossipsub::Gossipsub, identify, ping, swarm::NetworkBehaviour}; +use libp2p::{identify, ping, swarm::NetworkBehaviour}; use libp2p_bitswap::Bitswap; mod discovery; +mod membership; /// Libp2p behaviour to manage content resolution from other subnets, using: /// @@ -16,8 +17,8 @@ pub struct IpldResolver { ping: ping::Behaviour, identify: identify::Behaviour, discovery: discovery::Behaviour, - gossipsub: Gossipsub, // TODO (IPC-35): Wrap into Membership - bitswap: Bitswap

, // TODO (IPC-36): Wrap into Resolve + membership: membership::Behaviour, + bitswap: Bitswap

, // TODO (IPC-36): Wrap } // Unfortunately by using `#[derive(NetworkBehaviour)]` we cannot easily inspects events diff --git a/ipld/resolver/src/lib.rs b/ipld/resolver/src/lib.rs index 067b9e674..b1c8f3504 100644 --- a/ipld/resolver/src/lib.rs +++ b/ipld/resolver/src/lib.rs @@ -4,4 +4,11 @@ #[allow(dead_code)] mod behaviour; #[allow(dead_code)] +mod provider_cache; +#[allow(dead_code)] +mod provider_record; +#[allow(dead_code)] mod service; + +#[cfg(any(test, feature = "arb"))] +mod arb; diff --git a/ipld/resolver/src/provider_cache.rs b/ipld/resolver/src/provider_cache.rs new file mode 100644 index 000000000..42dc741ac --- /dev/null +++ b/ipld/resolver/src/provider_cache.rs @@ -0,0 +1,327 @@ +// Copyright 2022-2023 Protocol Labs +// SPDX-License-Identifier: MIT +use std::collections::{HashMap, HashSet}; + +use ipc_sdk::subnet_id::SubnetID; +use libp2p::PeerId; + +use crate::provider_record::{ProviderRecord, Timestamp}; + +/// Change in the supported subnets of a peer. +#[derive(Debug, Default)] +pub struct ProviderDelta { + pub added: Vec, + pub removed: Vec, +} + +impl ProviderDelta { + pub fn is_empty(&self) -> bool { + self.added.is_empty() && self.removed.is_empty() + } +} + +pub struct SubnetProviderCache { + /// Maximum number of subnets to track, to protect against DoS attacks, trying to + /// flood someone with subnets that don't actually exist. When the number of subnets + /// reaches this value, we remove the subnet with the smallest number of providers; + /// hopefully this would be a subnet + max_subnets: usize, + /// Set of peers with known addresses. Only such peers can be added to the cache. + routable_peers: HashSet, + /// List of peer IDs supporting each subnet. + subnet_providers: HashMap>, + /// Timestamp of the last record received about a peer. + peer_timestamps: HashMap, +} + +impl SubnetProviderCache { + pub fn new(max_subnets: usize) -> Self { + Self { + max_subnets, + routable_peers: Default::default(), + subnet_providers: Default::default(), + peer_timestamps: Default::default(), + } + } + + /// Mark a peer as routable. + /// + /// Once routable, the cache will keep track of provided subnets. + pub fn set_routable(&mut self, peer_id: PeerId) { + self.routable_peers.insert(peer_id); + } + + /// Mark a previously routable peer as unroutable. + /// + /// Once unroutable, the cache will stop tracking the provided subnets. + pub fn set_unroutable(&mut self, peer_id: PeerId) { + self.routable_peers.remove(&peer_id); + self.peer_timestamps.remove(&peer_id); + for providers in self.subnet_providers.values_mut() { + providers.remove(&peer_id); + } + } + + /// Check if a peer has been marked as routable. + pub fn is_routable(&self, peer_id: PeerId) -> bool { + self.routable_peers.contains(&peer_id) + } + + /// Try to add a provider to the cache. + /// + /// Returns `None` if the peer is not routable and nothing could be added. + /// + /// Returns `Some` if the peer is routable, containing the newly added + /// and newly removed associations for this peer. + pub fn add_provider(&mut self, record: &ProviderRecord) -> Option { + if !self.is_routable(record.peer_id) { + return None; + } + + let mut delta = ProviderDelta::default(); + + let timestamp = self.peer_timestamps.entry(record.peer_id).or_default(); + + if *timestamp < record.timestamp { + *timestamp = record.timestamp; + + // The currently supported subnets of the peer. + let mut subnet_ids = HashSet::new(); + subnet_ids.extend(record.subnet_ids.iter()); + + // Remove the peer from subnets it no longer supports. + for (subnet_id, peer_ids) in self.subnet_providers.iter_mut() { + if !subnet_ids.contains(subnet_id) && peer_ids.remove(&record.peer_id) { + delta.removed.push(subnet_id.clone()); + } + } + + // Add peer to new subnets it supports now. + for subnet_id in record.subnet_ids.iter() { + let peer_ids = self.subnet_providers.entry(subnet_id.clone()).or_default(); + if peer_ids.insert(record.peer_id) { + delta.added.push(subnet_id.clone()); + } + } + + // Remove subnets that have been added but are too small to survive a pruning. + let removed_subnet_ids = self.prune_subnets(); + delta.added.retain(|id| !removed_subnet_ids.contains(id)) + } + + Some(delta) + } + + /// Ensure we don't have more than `max_subnets` number of subnets in the cache. + /// + /// Returns the removed subnet IDs. + fn prune_subnets(&mut self) -> HashSet { + let mut removed_subnet_ids = HashSet::new(); + + let to_prune = self.subnet_providers.len().saturating_sub(self.max_subnets); + if to_prune > 0 { + let mut counts = self + .subnet_providers + .iter() + .map(|(id, ps)| (id.clone(), ps.len())) + .collect::>(); + + counts.sort_by_key(|(_, count)| *count); + + for (subnet_id, _) in counts.into_iter().take(to_prune) { + self.subnet_providers.remove(&subnet_id); + removed_subnet_ids.insert(subnet_id); + } + } + + removed_subnet_ids + } + + /// Prune any provider which hasn't provided an update since a cutoff timestamp. + /// + /// Returns the list of pruned peers. + pub fn prune_providers(&mut self, cutoff_timestamp: Timestamp) -> Vec { + let to_prune = self + .peer_timestamps + .iter() + .filter_map(|(id, ts)| { + if *ts < cutoff_timestamp { + Some(*id) + } else { + None + } + }) + .collect::>(); + + for peer_id in to_prune.iter() { + self.set_unroutable(*peer_id); + } + + to_prune + } + + /// List any known providers of a subnet. + pub fn providers_of_subnet(&self, subnet_id: &SubnetID) -> Vec { + self.subnet_providers + .get(subnet_id) + .map(|hs| hs.iter().cloned().collect()) + .unwrap_or_default() + } +} + +#[cfg(test)] +mod tests { + use std::collections::{HashMap, HashSet}; + + use ipc_sdk::subnet_id::SubnetID; + use libp2p::{identity::Keypair, PeerId}; + use quickcheck::Arbitrary; + use quickcheck_macros::quickcheck; + + use crate::{ + arb::ArbSubnetID, + provider_record::{ProviderRecord, Timestamp}, + }; + + use super::SubnetProviderCache; + + #[derive(Debug, Clone)] + struct TestRecords(Vec); + + // Limited number of records from a limited set of peers. + impl Arbitrary for TestRecords { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let rc = usize::arbitrary(g) % 20; + let pc = 1 + rc / 2; + + let mut ps = Vec::new(); + let mut rs = Vec::new(); + + for _ in 0..pc { + let pk = Keypair::generate_ed25519(); + let peer_id = pk.public().to_peer_id(); + ps.push(peer_id) + } + + for _ in 0..rc { + let peer_id = ps[usize::arbitrary(g) % ps.len()]; + let mut subnet_ids = Vec::new(); + for _ in 0..usize::arbitrary(g) % 5 { + subnet_ids.push(ArbSubnetID::arbitrary(g).0) + } + let record = ProviderRecord { + peer_id, + subnet_ids, + timestamp: Timestamp::arbitrary(g), + }; + rs.push(record) + } + + Self(rs) + } + } + + type Providers = HashMap>; + + /// Build a provider mapping to check the cache against. + fn build_providers(records: &Vec) -> Providers { + // Only the last timestamp should be kept, but it might not be unique. + let mut max_timestamps: HashMap = Default::default(); + for record in records { + let mts = max_timestamps.entry(record.peer_id).or_default(); + if *mts < record.timestamp { + *mts = record.timestamp; + } + } + + let mut providers: HashMap> = Default::default(); + let mut seen: HashSet = Default::default(); + + for record in records { + if record.timestamp != max_timestamps[&record.peer_id] { + continue; + } + if !seen.insert(record.peer_id) { + continue; + } + for subnet_id in record.subnet_ids.iter() { + providers + .entry(subnet_id.clone()) + .or_default() + .insert(record.peer_id); + } + } + + providers + } + + /// Check the cache against the reference built in the test. + fn check_providers(providers: &Providers, cache: &SubnetProviderCache) -> Result<(), String> { + for (subnet_id, exp_peer_ids) in providers { + let peer_ids = cache.providers_of_subnet(subnet_id); + if peer_ids.len() != exp_peer_ids.len() { + return Err(format!( + "expected {} peers, got {} in subnet {:?}", + exp_peer_ids.len(), + peer_ids.len(), + subnet_id + )); + } + for peer_id in peer_ids { + if !exp_peer_ids.contains(&peer_id) { + return Err("wrong peer ID".into()); + } + } + } + Ok(()) + } + + #[quickcheck] + fn prop_subnets_pruned(records: TestRecords, max_subnets: usize) -> bool { + let max_subnets = max_subnets % 10; + let mut cache = SubnetProviderCache::new(max_subnets); + for record in records.0 { + cache.set_routable(record.peer_id); + if cache.add_provider(&record).is_none() { + return false; + } + } + cache.subnet_providers.len() <= max_subnets + } + + #[quickcheck] + fn prop_providers_listed(records: TestRecords) -> Result<(), String> { + let records = records.0; + let mut cache = SubnetProviderCache::new(usize::MAX); + + for record in records.iter() { + cache.set_routable(record.peer_id); + cache.add_provider(record); + } + + let providers = build_providers(&records); + + check_providers(&providers, &cache) + } + + #[quickcheck] + fn prop_providers_pruned( + records: TestRecords, + cutoff_timestamp: Timestamp, + ) -> Result<(), String> { + let mut records = records.0; + let mut cache = SubnetProviderCache::new(usize::MAX); + for record in records.iter() { + cache.set_routable(record.peer_id); + cache.add_provider(record); + } + cache.prune_providers(cutoff_timestamp); + + // Build a reference from only what has come after the cutoff timestamp. + records.retain(|r| r.timestamp >= cutoff_timestamp); + + let providers = build_providers(&records); + + check_providers(&providers, &cache) + } +} diff --git a/ipld/resolver/src/provider_record.rs b/ipld/resolver/src/provider_record.rs new file mode 100644 index 000000000..280678d97 --- /dev/null +++ b/ipld/resolver/src/provider_record.rs @@ -0,0 +1,232 @@ +// Copyright 2022-2023 Protocol Labs +// SPDX-License-Identifier: MIT +use std::ops::{Add, Sub}; +use std::time::{Duration, SystemTime}; + +use fvm_ipld_encoding::serde::{Deserialize, Serialize}; +use ipc_sdk::subnet_id::SubnetID; +use libipld::multihash; +use libp2p::core::{signed_envelope, SignedEnvelope}; +use libp2p::identity::Keypair; +use libp2p::PeerId; + +const DOMAIN_SEP: &str = "ipc-membership"; +const PAYLOAD_TYPE: &str = "/ipc/provider-record"; + +/// Unix timestamp in seconds since epoch, which we can use to select the +/// more recent message during gossiping. +#[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Debug, Serialize, Deserialize, Default)] +pub struct Timestamp(u64); + +impl Timestamp { + /// Current timestamp. + pub fn now() -> Self { + let secs = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("now() is never before UNIX_EPOCH") + .as_secs(); + Self(secs) + } + + /// Seconds elapsed since Unix epoch. + pub fn as_secs(&self) -> u64 { + self.0 + } +} + +impl Sub for Timestamp { + type Output = Self; + + fn sub(self, rhs: Duration) -> Self { + Self(self.0.saturating_sub(rhs.as_secs())) + } +} + +impl Add for Timestamp { + type Output = Self; + + fn add(self, rhs: Duration) -> Self { + Self(self.0.saturating_add(rhs.as_secs())) + } +} + +/// Record of the ability to provide data from a list of subnets. +/// +/// Note that each the record contains the snapshot of the currently provided +/// subnets, not a delta. This means that if there were two peers using the +/// same keys running on different addresses, e.g. if the same operator ran +/// something supporting subnet A on one address, and another process supporting +/// subnet B on a different address, these would override each other, unless +/// they have different public keys (and thus peer IDs) associated with them. +/// +/// This should be okay, as in practice there is no significance to these +/// peer IDs, we can even generate a fresh key-pair every time we run the +/// resolver. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct ProviderRecord { + /// The ID of the peer we can contact to pull data from. + pub peer_id: PeerId, + /// The IDs of the subnets they are participating in. + pub subnet_ids: Vec, + /// Timestamp from when the peer published this record. + /// + /// We use a timestamp instead of just a nonce so that we + /// can drop records which are too old, indicating that + /// the peer has dropped off. + pub timestamp: Timestamp, +} + +/// A [`ProviderRecord`] with a [`SignedEnvelope`] proving that the +/// peer indeed is ready to provide the data for the listed subnets. +#[derive(Debug, Clone)] +pub struct SignedProviderRecord { + /// The deserialized and validated [`ProviderRecord`]. + record: ProviderRecord, + /// The [`SignedEnvelope`] from which the record was deserialized from. + envelope: SignedEnvelope, +} + +// Based on `libp2p_core::peer_record::PeerRecord` +impl SignedProviderRecord { + /// Create a new [`SignedProviderRecord`] with the current timestamp + /// and a signed envelope which can be shared with others. + pub fn new(key: &Keypair, subnet_ids: Vec) -> anyhow::Result { + let timestamp = Timestamp::now(); + let peer_id = key.public().to_peer_id(); + let record = ProviderRecord { + peer_id, + subnet_ids, + timestamp, + }; + let payload = fvm_ipld_encoding::to_vec(&record)?; + let envelope = SignedEnvelope::new( + key, + DOMAIN_SEP.to_owned(), + PAYLOAD_TYPE.as_bytes().to_vec(), + payload, + )?; + Ok(Self { record, envelope }) + } + + pub fn from_signed_envelope(envelope: SignedEnvelope) -> Result { + let (payload, signing_key) = + envelope.payload_and_signing_key(DOMAIN_SEP.to_owned(), PAYLOAD_TYPE.as_bytes())?; + + let record = fvm_ipld_encoding::from_slice::(payload)?; + + if record.peer_id != signing_key.to_peer_id() { + return Err(FromEnvelopeError::MismatchedSignature); + } + + Ok(Self { record, envelope }) + } + + /// Deserialize then check the domain tags and the signature. + pub fn from_bytes(bytes: &[u8]) -> anyhow::Result { + let envelope = SignedEnvelope::from_protobuf_encoding(bytes)?; + let signed_record = Self::from_signed_envelope(envelope)?; + Ok(signed_record) + } + + pub fn record(&self) -> &ProviderRecord { + &self.record + } + + pub fn envelope(&self) -> &SignedEnvelope { + &self.envelope + } + + pub fn into_record(self) -> ProviderRecord { + self.record + } + + pub fn into_envelope(self) -> SignedEnvelope { + self.envelope + } +} + +#[derive(thiserror::Error, Debug)] +pub enum FromEnvelopeError { + /// Failed to extract the payload from the envelope. + #[error("Failed to extract payload from envelope")] + BadPayload(#[from] signed_envelope::ReadPayloadError), + /// Failed to decode the provided bytes as a [`ProviderRecord`]. + #[error("Failed to decode bytes as ProviderRecord")] + InvalidProviderRecord(#[from] fvm_ipld_encoding::Error), + /// Failed to decode the peer ID. + #[error("Failed to decode bytes as PeerId")] + InvalidPeerId(#[from] multihash::Error), + /// The signer of the envelope is different than the peer id in the record. + #[error("The signer of the envelope is different than the peer id in the record")] + MismatchedSignature, +} + +#[cfg(any(test, feature = "arb"))] +mod arb { + use libp2p::identity::Keypair; + use quickcheck::Arbitrary; + + use crate::arb::ArbSubnetID; + + use super::{SignedProviderRecord, Timestamp}; + + impl Arbitrary for Timestamp { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self(u64::arbitrary(g).saturating_add(1)) + } + } + + /// Create a valid [`SignedProviderRecord`] with a random key. + impl Arbitrary for SignedProviderRecord { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + // NOTE: Unfortunately the keys themselves are not deterministic, nor is the Timestamp. + let key = match u8::arbitrary(g) % 2 { + 0 => Keypair::generate_ed25519(), + _ => Keypair::generate_secp256k1(), + }; + + // Limit the number of subnets and the depth of keys so data generation doesn't take too long. + let mut subnet_ids = Vec::new(); + for _ in 0..u8::arbitrary(g) % 5 { + let subnet_id = ArbSubnetID::arbitrary(g); + subnet_ids.push(subnet_id.0) + } + + Self::new(&key, subnet_ids).expect("error creating signed envelope") + } + } +} + +#[cfg(test)] +mod tests { + use libp2p::core::SignedEnvelope; + use quickcheck_macros::quickcheck; + + use super::SignedProviderRecord; + + #[quickcheck] + fn prop_roundtrip(signed_record: SignedProviderRecord) -> bool { + let envelope_bytes = signed_record.envelope().clone().into_protobuf_encoding(); + + let envelope = + SignedEnvelope::from_protobuf_encoding(&envelope_bytes).expect("envelope roundtrip"); + + let signed_record2 = + SignedProviderRecord::from_signed_envelope(envelope).expect("record roundtrip"); + + signed_record2.record == signed_record.record + } + + #[quickcheck] + fn prop_tamper_proof(signed_record: SignedProviderRecord, idx: usize) -> bool { + let mut envelope_bytes = signed_record.envelope().clone().into_protobuf_encoding(); + // Do some kind of mutation to a random byte in the envelope; after that it should not validate. + let idx = idx % envelope_bytes.len(); + envelope_bytes[idx] = u8::MAX - envelope_bytes[idx]; + + match SignedEnvelope::from_protobuf_encoding(&envelope_bytes) { + Err(_) => true, // Corrupted the protobuf itself. + Ok(envelope) => SignedProviderRecord::from_signed_envelope(envelope).is_err(), + } + } +} From 7be32b9fb1f1a6e4b441803ad7cd2b42bea259e1 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Fri, 3 Mar 2023 11:36:40 +0000 Subject: [PATCH 15/82] IPC-35: Config (#49) * IPC-35: Clippy fixes * IPC-35: Config. * IPC-35: Get rid of ConfigBuilder * IPC-35: Extract common config. * IPC-35: Rename to static addresses. * IPC-35: Pin subnet * IPC-35: Rename some methods. * IPC-35: Remove private example * IPC-35: Test pinning. * IPC-35: Update Cargo.lock * IPC-35: Add header * IPC-35: Clippy fixes * Update ipld/resolver/src/behaviour/discovery.rs Co-authored-by: adlrocha --------- Co-authored-by: adlrocha --- Cargo.toml | 2 +- ipld/resolver/Cargo.toml | 1 + ipld/resolver/src/behaviour/discovery.rs | 125 +++++++--------------- ipld/resolver/src/behaviour/membership.rs | 122 ++++++++++++++++++--- ipld/resolver/src/behaviour/mod.rs | 24 ++++- ipld/resolver/src/hash.rs | 32 ++++++ ipld/resolver/src/lib.rs | 2 + ipld/resolver/src/provider_cache.rs | 58 +++++++++- 8 files changed, 261 insertions(+), 105 deletions(-) create mode 100644 ipld/resolver/src/hash.rs diff --git a/Cargo.toml b/Cargo.toml index 6dc859a52..000498f3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,7 @@ thiserror = "1.0" tokio = { version = "1.16", features = ["full"] } quickcheck = "1" quickcheck_macros = "1" - +blake2b_simd = "1.0" fvm_ipld_encoding = "0.3" fvm_shared = { version = "=3.0.0-alpha.17", default-features = false } diff --git a/ipld/resolver/Cargo.toml b/ipld/resolver/Cargo.toml index 31a3bcbd7..0fc2ad68b 100644 --- a/ipld/resolver/Cargo.toml +++ b/ipld/resolver/Cargo.toml @@ -10,6 +10,7 @@ license-file.workspace = true [dependencies] anyhow = { workspace = true } +blake2b_simd = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } libp2p = { version = "0.50", default-features = false, features = [ diff --git a/ipld/resolver/src/behaviour/discovery.rs b/ipld/resolver/src/behaviour/discovery.rs index e0f98fa72..1855a0641 100644 --- a/ipld/resolver/src/behaviour/discovery.rs +++ b/ipld/resolver/src/behaviour/discovery.rs @@ -10,7 +10,7 @@ use std::{ }; use libp2p::{ - core::{connection::ConnectionId, identity::PublicKey}, + core::connection::ConnectionId, kad::{ handler::KademliaHandlerProto, store::MemoryStore, Kademlia, KademliaConfig, KademliaEvent, QueryId, @@ -25,9 +25,10 @@ use libp2p::{ Multiaddr, PeerId, }; use log::debug; -use thiserror::Error; use tokio::time::Interval; +use super::NetworkConfig; + // NOTE: The Discovery behaviour is largely based on what exists in Forest. If it ain't broken... // NOTE: Not sure if emitting events is going to be useful yet, but for now it's an example of having one. @@ -48,22 +49,20 @@ pub enum Event { Removed(PeerId), } -/// `Discovery` behaviour configuration. +/// Configuration for [`discovery::Behaviour`]. #[derive(Clone, Debug)] pub struct Config { - /// Our own peer ID, needed to bootstrap Kademlia. - local_peer_id: PeerId, - /// Static list of addresses to bootstrap from. - user_defined: Vec<(PeerId, Multiaddr)>, + /// Custom nodes which never expire, e.g. bootstrap or reserved nodes. + /// + /// The addresses must end with a `/p2p/` part. + pub static_addresses: Vec, /// Number of connections at which point we pause further discovery lookups. - target_connections: usize, + pub target_connections: usize, /// Option to disable Kademlia, for example in a fixed static network. - enable_kademlia: bool, - /// Name of the network in the Kademlia protocol. - network_name: String, + pub enable_kademlia: bool, } -#[derive(Error, Debug)] +#[derive(thiserror::Error, Debug)] pub enum ConfigError { #[error("invalid network: {0}")] InvalidNetwork(String), @@ -73,65 +72,6 @@ pub enum ConfigError { NoBootstrapAddress, } -pub struct ConfigBuilder(Config); - -impl ConfigBuilder { - /// Create a default configuration with the given public key. - pub fn new(local_public_key: PublicKey, network_name: String) -> Result { - if network_name.is_empty() { - Err(ConfigError::InvalidNetwork(network_name)) - } else { - Ok(Self(Config { - local_peer_id: local_public_key.to_peer_id(), - user_defined: Vec::new(), - target_connections: usize::MAX, - enable_kademlia: true, - network_name, - })) - } - } - - /// Set the number of active connections at which we pause discovery. - pub fn with_max_connections(&mut self, limit: usize) -> &mut Self { - self.0.target_connections = limit; - self - } - - /// Set custom nodes which never expire, e.g. bootstrap or reserved nodes. - /// - /// The addresses must end with a `/p2p/` part. - pub fn with_user_defined(&mut self, user_defined: I) -> Result<&mut Self, ConfigError> - where - I: IntoIterator, - { - for multiaddr in user_defined { - let mut addr = multiaddr.clone(); - if let Some(Protocol::P2p(mh)) = addr.pop() { - let peer_id = PeerId::from_multihash(mh).unwrap(); - self.0.user_defined.push((peer_id, addr)) - } else { - return Err(ConfigError::InvalidBootstrapAddress(multiaddr)); - } - } - Ok(self) - } - - /// Configures if Kademlia is enabled. - pub fn with_kademlia(&mut self, value: bool) -> &mut Self { - self.0.enable_kademlia = value; - self - } - - /// Finish configuration and do a final check. - pub fn build(self) -> Result { - if self.0.enable_kademlia && self.0.user_defined.is_empty() { - Err(ConfigError::NoBootstrapAddress) - } else { - Ok(self.0) - } - } -} - /// Discovery behaviour, periodically running a random lookup with Kademlia to find new peers. /// /// Our other option for peer discovery would be to rely on the Peer Exchange of Gossipsub. @@ -139,7 +79,7 @@ impl ConfigBuilder { pub struct Behaviour { /// User-defined list of nodes and their addresses. /// Typically includes bootstrap nodes, or it can be used for a static network. - user_defined: Vec<(PeerId, Multiaddr)>, + static_addresses: Vec<(PeerId, Multiaddr)>, /// Kademlia behaviour, if enabled. inner: Toggle>, /// Number of current connections. @@ -153,39 +93,54 @@ pub struct Behaviour { } impl Behaviour { - /// Create a [`discovery::Behaviour`] from this configuration. - pub fn new(config: Config) -> Self { - let kademlia_opt = if config.enable_kademlia { + /// Create a [`discovery::Behaviour`] from the configuration. + pub fn new(nc: NetworkConfig, dc: Config) -> Result { + if nc.network_name.is_empty() { + return Err(ConfigError::InvalidNetwork(nc.network_name)); + } + // Parse static addresses. + let mut static_addresses = Vec::new(); + for multiaddr in dc.static_addresses { + let mut addr = multiaddr.clone(); + if let Some(Protocol::P2p(mh)) = addr.pop() { + let peer_id = PeerId::from_multihash(mh).unwrap(); + static_addresses.push((peer_id, addr)) + } else { + return Err(ConfigError::InvalidBootstrapAddress(multiaddr)); + } + } + + let kademlia_opt = if dc.enable_kademlia { let mut kad_config = KademliaConfig::default(); - let protocol_name = format!("/ipc/kad/{}/kad/1.0.0", config.network_name); + let protocol_name = format!("/ipc/{}/kad/1.0.0", nc.network_name); kad_config.set_protocol_names(vec![Cow::Owned(protocol_name.as_bytes().to_vec())]); - let store = MemoryStore::new(config.local_peer_id); + let store = MemoryStore::new(nc.local_peer_id()); - let mut kademlia = Kademlia::with_config(config.local_peer_id, store, kad_config); + let mut kademlia = Kademlia::with_config(nc.local_peer_id(), store, kad_config); - for (peer_id, addr) in config.user_defined.iter() { + for (peer_id, addr) in static_addresses.iter() { kademlia.add_address(peer_id, addr.clone()); } // This shouldn't happen, we already checked the config. kademlia .bootstrap() - .unwrap_or_else(|e| panic!("Kademlia bootstrap failed: {}", e)); + .map_err(|_| ConfigError::NoBootstrapAddress)?; Some(kademlia) } else { None }; - Self { - user_defined: config.user_defined, + Ok(Self { + static_addresses, inner: kademlia_opt.into(), lookup_interval: tokio::time::interval(Duration::from_secs(1)), outbox: VecDeque::new(), num_connections: 0, - target_connections: config.target_connections, - } + target_connections: dc.target_connections, + }) } /// Lookup a peer, unless we already know their address, so that we have a chance to connect to them later. @@ -209,7 +164,7 @@ impl NetworkBehaviour for Behaviour { fn addresses_of_peer(&mut self, peer_id: &PeerId) -> Vec { let mut addrs = self - .user_defined + .static_addresses .iter() .filter(|(p, _)| p == peer_id) .map(|(_, a)| a.clone()) diff --git a/ipld/resolver/src/behaviour/membership.rs b/ipld/resolver/src/behaviour/membership.rs index ae95edbae..c9bfbd98f 100644 --- a/ipld/resolver/src/behaviour/membership.rs +++ b/ipld/resolver/src/behaviour/membership.rs @@ -6,7 +6,10 @@ use std::time::Duration; use ipc_sdk::subnet_id::SubnetID; use libp2p::core::connection::ConnectionId; -use libp2p::gossipsub::{GossipsubEvent, GossipsubMessage, IdentTopic}; +use libp2p::gossipsub::{ + GossipsubConfigBuilder, GossipsubEvent, GossipsubMessage, IdentTopic, MessageAuthenticity, + MessageId, Topic, +}; use libp2p::identity::Keypair; use libp2p::swarm::derive_prelude::FromSwarm; use libp2p::swarm::{NetworkBehaviourAction, PollParameters}; @@ -19,17 +22,15 @@ use libp2p::{ use log::{debug, error, warn}; use tokio::time::Interval; +use crate::hash::blake2b_256; use crate::provider_cache::{ProviderDelta, SubnetProviderCache}; use crate::provider_record::{SignedProviderRecord, Timestamp}; +use super::NetworkConfig; + /// `Gossipsub` subnet membership topic identifier. const PUBSUB_MEMBERSHIP: &str = "/ipc/membership"; -struct Config { - /// Network name to be combined into the Gossipsub topic. - network_name: String, -} - /// Events emitted by the [`membership::Behaviour`] behaviour. #[derive(Debug)] pub enum Event { @@ -45,6 +46,26 @@ pub enum Event { Skipped(PeerId), } +/// Configuration for [`membership::Behaviour`]. +pub struct Config { + /// User defined list of subnets which will never be pruned from the cache. + pub static_subnets: Vec, + /// Maximum number of subnets to track in the cache. + pub max_subnets: usize, + /// Publish interval for supported subnets. + pub publish_interval: Duration, + /// Maximum age of provider records before the peer is removed without an update. + pub max_provider_age: Duration, +} + +#[derive(thiserror::Error, Debug)] +pub enum ConfigError { + #[error("invalid network: {0}")] + InvalidNetwork(String), + #[error("invalid gossipsub config: {0}")] + InvalidGossipsubConfig(String), +} + /// A [`NetworkBehaviour`] internally using [`Gossipsub`] to learn which /// peer is able to resolve CIDs in different subnets. pub struct Behaviour { @@ -52,10 +73,10 @@ pub struct Behaviour { inner: Gossipsub, /// Events to return when polled. outbox: VecDeque, - /// [`Keypair`] used to construct [`SignedProviderRecord`] instances. - keypair: Keypair, + /// [`Keypair`] used to sign [`SignedProviderRecord`] instances. + local_key: Keypair, /// Name of the [`Gossipsub`] topic where subnet memberships are published. - membership_topic: IdentTopic, // Topic::new(format!("{}/{}", PUBSUB_MEMBERSHIP, network_name) + membership_topic: IdentTopic, /// List of subnet IDs this agent is providing data for. subnet_ids: Vec, /// Caching the latest state of subnet providers. @@ -70,14 +91,61 @@ pub struct Behaviour { } impl Behaviour { + pub fn new(nc: NetworkConfig, mc: Config) -> Result { + if nc.network_name.is_empty() { + return Err(ConfigError::InvalidNetwork(nc.network_name)); + } + let membership_topic = Topic::new(format!("{}/{}", PUBSUB_MEMBERSHIP, nc.network_name)); + + let mut gossipsub_config = GossipsubConfigBuilder::default(); + // Set the maximum message size to 2MB. + gossipsub_config.max_transmit_size(2 << 20); + gossipsub_config.message_id_fn(|msg: &GossipsubMessage| { + let s = blake2b_256(&msg.data); + MessageId::from(s) + }); + + let gossipsub_config = gossipsub_config + .build() + .map_err(|s| ConfigError::InvalidGossipsubConfig(s.into()))?; + + let mut gossipsub = Gossipsub::new( + MessageAuthenticity::Signed(nc.local_key.clone()), + gossipsub_config, + ) + .map_err(|s| ConfigError::InvalidGossipsubConfig(s.into()))?; + + gossipsub + .with_peer_score( + scoring::build_peer_score_params(membership_topic.clone()), + scoring::build_peer_score_thresholds(), + ) + .map_err(ConfigError::InvalidGossipsubConfig)?; + + // Don't publish immediately, it's empty. Let the creator call `set_subnet_ids` to trigger initially. + let mut interval = tokio::time::interval(mc.publish_interval); + interval.reset(); + + Ok(Self { + inner: gossipsub, + outbox: Default::default(), + local_key: nc.local_key, + membership_topic, + subnet_ids: Default::default(), + provider_cache: SubnetProviderCache::new(mc.max_subnets, mc.static_subnets), + publish_interval: interval, + max_provider_age: mc.max_provider_age, + }) + } + /// Set all the currently supported subnet IDs, then publish the updated list. - pub fn set_subnet_ids(&mut self, subnet_ids: Vec) -> anyhow::Result<()> { + pub fn set_provided_subnets(&mut self, subnet_ids: Vec) -> anyhow::Result<()> { self.subnet_ids = subnet_ids; self.publish_membership() } /// Add a subnet to the list of supported subnets, then publish the updated list. - pub fn add_subnet_id(&mut self, subnet_id: SubnetID) -> anyhow::Result<()> { + pub fn add_provided_subnet(&mut self, subnet_id: SubnetID) -> anyhow::Result<()> { if self.subnet_ids.contains(&subnet_id) { return Ok(()); } @@ -86,7 +154,7 @@ impl Behaviour { } /// Remove a subnet from the list of supported subnets, then publish the updated list. - pub fn remove_subnet_id(&mut self, subnet_id: SubnetID) -> anyhow::Result<()> { + pub fn remove_provided_subnet(&mut self, subnet_id: SubnetID) -> anyhow::Result<()> { if !self.subnet_ids.contains(&subnet_id) { return Ok(()); } @@ -94,9 +162,18 @@ impl Behaviour { self.publish_membership() } + /// Make sure a subnet is not pruned. + /// + /// This method could be called in a parent subnet when the ledger indicates + /// there is a known child subnet, so we make sure this subnet cannot be + /// crowded out during the initial phase of bootstrapping the network. + pub fn pin_subnet(&mut self, subnet_id: SubnetID) { + self.provider_cache.pin_subnet(subnet_id) + } + /// Send a message through Gossipsub to let everyone know about the current configuration. fn publish_membership(&mut self) -> anyhow::Result<()> { - let record = SignedProviderRecord::new(&self.keypair, self.subnet_ids.clone())?; + let record = SignedProviderRecord::new(&self.local_key, self.subnet_ids.clone())?; let data = record.into_envelope().into_protobuf_encoding(); let _msg_id = self.inner.publish(self.membership_topic.clone(), data)?; Ok(()) @@ -239,3 +316,22 @@ impl NetworkBehaviour for Behaviour { Poll::Pending } } + +// Forest has Filecoin specific values copied from Lotus. Not sure what values to use, +// so I'll leave everything on default for now. Or maybe they should be left empty? +mod scoring { + + use libp2p::gossipsub::{IdentTopic, PeerScoreParams, PeerScoreThresholds, TopicScoreParams}; + + pub fn build_peer_score_params(membership_topic: IdentTopic) -> PeerScoreParams { + let mut params = PeerScoreParams::default(); + params + .topics + .insert(membership_topic.hash(), TopicScoreParams::default()); + params + } + + pub fn build_peer_score_thresholds() -> PeerScoreThresholds { + PeerScoreThresholds::default() + } +} diff --git a/ipld/resolver/src/behaviour/mod.rs b/ipld/resolver/src/behaviour/mod.rs index 931fd0596..9478133cd 100644 --- a/ipld/resolver/src/behaviour/mod.rs +++ b/ipld/resolver/src/behaviour/mod.rs @@ -1,12 +1,34 @@ // Copyright 2022-2023 Protocol Labs // SPDX-License-Identifier: MIT use libipld::store::StoreParams; -use libp2p::{identify, ping, swarm::NetworkBehaviour}; +use libp2p::{ + identify, + identity::{Keypair, PublicKey}, + ping, + swarm::NetworkBehaviour, + PeerId, +}; use libp2p_bitswap::Bitswap; mod discovery; mod membership; +pub struct NetworkConfig { + /// Cryptographic key used to sign messages. + pub local_key: Keypair, + /// Network name to be differentiate this peer group. + pub network_name: String, +} + +impl NetworkConfig { + pub fn local_public_key(&self) -> PublicKey { + self.local_key.public() + } + pub fn local_peer_id(&self) -> PeerId { + self.local_public_key().to_peer_id() + } +} + /// Libp2p behaviour to manage content resolution from other subnets, using: /// /// * Kademlia for peer discovery diff --git a/ipld/resolver/src/hash.rs b/ipld/resolver/src/hash.rs new file mode 100644 index 000000000..7586ea4ea --- /dev/null +++ b/ipld/resolver/src/hash.rs @@ -0,0 +1,32 @@ +// Copyright 2022-2023 Protocol Labs +// SPDX-License-Identifier: MIT +// Copyright 2019-2022 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use blake2b_simd::Params; + +/// Generates BLAKE2b hash of fixed 32 bytes size. +pub fn blake2b_256(ingest: &[u8]) -> [u8; 32] { + let digest = Params::new() + .hash_length(32) + .to_state() + .update(ingest) + .finalize(); + + let mut ret = [0u8; 32]; + ret.clone_from_slice(digest.as_bytes()); + ret +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn vector_hashing() { + let ing_vec = vec![1, 2, 3]; + + assert_eq!(blake2b_256(&ing_vec), blake2b_256(&[1, 2, 3])); + assert_ne!(blake2b_256(&ing_vec), blake2b_256(&[1, 2, 3, 4])); + } +} diff --git a/ipld/resolver/src/lib.rs b/ipld/resolver/src/lib.rs index b1c8f3504..b7943ec7f 100644 --- a/ipld/resolver/src/lib.rs +++ b/ipld/resolver/src/lib.rs @@ -12,3 +12,5 @@ mod service; #[cfg(any(test, feature = "arb"))] mod arb; + +mod hash; diff --git a/ipld/resolver/src/provider_cache.rs b/ipld/resolver/src/provider_cache.rs index 42dc741ac..8e61af7c4 100644 --- a/ipld/resolver/src/provider_cache.rs +++ b/ipld/resolver/src/provider_cache.rs @@ -26,6 +26,10 @@ pub struct SubnetProviderCache { /// reaches this value, we remove the subnet with the smallest number of providers; /// hopefully this would be a subnet max_subnets: usize, + /// User defined list of subnets which will never be pruned. This can be used to + /// ward off attacks that would prevent us from adding subnets we know we want to + /// support, and not rely on dynamic discovery of their peers. + pinned_subnets: HashSet, /// Set of peers with known addresses. Only such peers can be added to the cache. routable_peers: HashSet, /// List of peer IDs supporting each subnet. @@ -35,8 +39,9 @@ pub struct SubnetProviderCache { } impl SubnetProviderCache { - pub fn new(max_subnets: usize) -> Self { + pub fn new(max_subnets: usize, static_subnets: Vec) -> Self { Self { + pinned_subnets: HashSet::from_iter(static_subnets.into_iter()), max_subnets, routable_peers: Default::default(), subnet_providers: Default::default(), @@ -44,6 +49,16 @@ impl SubnetProviderCache { } } + /// Pin a subnet, after which it won't be pruned. + pub fn pin_subnet(&mut self, subnet_id: SubnetID) { + self.pinned_subnets.insert(subnet_id); + } + + /// Unpin a subnet, which allows it to be pruned. + pub fn unpin_subnet(&mut self, subnet_id: &SubnetID) { + self.pinned_subnets.remove(subnet_id); + } + /// Mark a peer as routable. /// /// Once routable, the cache will keep track of provided subnets. @@ -128,9 +143,15 @@ impl SubnetProviderCache { counts.sort_by_key(|(_, count)| *count); - for (subnet_id, _) in counts.into_iter().take(to_prune) { + for (subnet_id, _) in counts { + if self.pinned_subnets.contains(&subnet_id) { + continue; + } self.subnet_providers.remove(&subnet_id); removed_subnet_ids.insert(subnet_id); + if removed_subnet_ids.len() == to_prune { + break; + } } } @@ -279,7 +300,7 @@ mod tests { #[quickcheck] fn prop_subnets_pruned(records: TestRecords, max_subnets: usize) -> bool { let max_subnets = max_subnets % 10; - let mut cache = SubnetProviderCache::new(max_subnets); + let mut cache = SubnetProviderCache::new(max_subnets, Vec::new()); for record in records.0 { cache.set_routable(record.peer_id); if cache.add_provider(&record).is_none() { @@ -289,10 +310,37 @@ mod tests { cache.subnet_providers.len() <= max_subnets } + #[quickcheck] + fn prop_subnets_pinned(records: TestRecords) -> Result<(), String> { + // Find two subnets to pin. + let providers = build_providers(&records.0); + if providers.len() < 2 { + return Ok(()); + } + + let subnets = providers.keys().take(2).collect::>(); + + let mut cache = SubnetProviderCache::new(3, vec![subnets[0].clone()]); + cache.pin_subnet(subnets[1].clone()); + + for record in records.0 { + cache.set_routable(record.peer_id); + cache.add_provider(&record); + } + + if !cache.subnet_providers.contains_key(subnets[0]) { + return Err("static subnet not found".into()); + } + if !cache.subnet_providers.contains_key(subnets[1]) { + return Err("pinned subnet not found".into()); + } + Ok(()) + } + #[quickcheck] fn prop_providers_listed(records: TestRecords) -> Result<(), String> { let records = records.0; - let mut cache = SubnetProviderCache::new(usize::MAX); + let mut cache = SubnetProviderCache::new(usize::MAX, Vec::new()); for record in records.iter() { cache.set_routable(record.peer_id); @@ -310,7 +358,7 @@ mod tests { cutoff_timestamp: Timestamp, ) -> Result<(), String> { let mut records = records.0; - let mut cache = SubnetProviderCache::new(usize::MAX); + let mut cache = SubnetProviderCache::new(usize::MAX, Vec::new()); for record in records.iter() { cache.set_routable(record.peer_id); cache.add_provider(record); From ab88089249d13d4330ad77bfe9e7b670ccaeecc5 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Mon, 6 Mar 2023 09:35:40 +0000 Subject: [PATCH 16/82] IPC-36: Content behaviour (#53) * IPC-36: Content behaviour * IPC-36: Add header. --- Cargo.toml | 1 + ipld/resolver/Cargo.toml | 4 +- ipld/resolver/src/behaviour/content.rs | 126 +++++++++++++++++++++++++ ipld/resolver/src/behaviour/mod.rs | 4 +- ipld/resolver/src/lib.rs | 3 + ipld/resolver/src/missing_blocks.rs | 33 +++++++ 6 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 ipld/resolver/src/behaviour/content.rs create mode 100644 ipld/resolver/src/missing_blocks.rs diff --git a/Cargo.toml b/Cargo.toml index 000498f3a..a375bcdae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ quickcheck = "1" quickcheck_macros = "1" blake2b_simd = "1.0" +fvm_ipld_blockstore = "0.1" fvm_ipld_encoding = "0.3" fvm_shared = { version = "=3.0.0-alpha.17", default-features = false } fil_actors_runtime = { git = "https://github.com/consensus-shipyard/fvm-utils", features = ["fil-actor"] } diff --git a/ipld/resolver/Cargo.toml b/ipld/resolver/Cargo.toml index 0fc2ad68b..a3d4c40f5 100644 --- a/ipld/resolver/Cargo.toml +++ b/ipld/resolver/Cargo.toml @@ -39,6 +39,7 @@ quickcheck = { workspace = true, optional = true } ipc-sdk = { workspace = true } fvm_ipld_encoding = { workspace = true } fvm_shared = { workspace = true, optional = true } +fvm_ipld_blockstore = { workspace = true, optional = true } [dev-dependencies] quickcheck = { workspace = true } @@ -47,5 +48,6 @@ fvm_shared = { workspace = true, features = ["arb"] } [features] -default = ["arb"] +default = ["arb", "missing_blocks"] arb = ["quickcheck", "fvm_shared/arb"] +missing_blocks = ["fvm_ipld_blockstore"] diff --git a/ipld/resolver/src/behaviour/content.rs b/ipld/resolver/src/behaviour/content.rs new file mode 100644 index 000000000..3ecfeb2f0 --- /dev/null +++ b/ipld/resolver/src/behaviour/content.rs @@ -0,0 +1,126 @@ +// Copyright 2022-2023 Protocol Labs +// SPDX-License-Identifier: MIT + +use std::task::{Context, Poll}; + +use libipld::{store::StoreParams, Cid}; +use libp2p::{ + swarm::{ + derive_prelude::{ConnectionId, FromSwarm}, + ConnectionHandler, IntoConnectionHandler, NetworkBehaviour, NetworkBehaviourAction, + PollParameters, + }, + Multiaddr, PeerId, +}; +use libp2p_bitswap::{Bitswap, BitswapConfig, BitswapEvent, BitswapStore, QueryId}; + +// Not much to do here, just hiding the `Progress` event as I don't think we'll need it. +// We can't really turn it into anything more meaningful; the outer Service, which drives +// the Swarm events, will have to store the `QueryId` and figure out which CID it was about +// (there could be multiple queries running over the same CID) and how to respond to the +// original requestor (e.g. by completing a channel). +#[derive(Debug)] +pub enum Event { + /// Event raised when a resolution request is finished. + /// + /// The result will indicate either success, or arbitrary failure. + /// If it is a success, the CID can be found in the [`BitswapStore`] + /// instance the behaviour was created with. + /// + /// Note that it is possible that the synchronization completed + /// partially, but some recursive constituent is missing. The + /// caller can use the [`missing_blocks`] function to check + /// whether a retry is necessary. + Complete(QueryId, anyhow::Result<()>), +} + +/// Behaviour built on [`Bitswap`] to resolve IPLD content from [`Cid`] to raw bytes. +pub struct Behaviour { + inner: Bitswap

, +} + +impl Behaviour

{ + pub fn new(store: S) -> Self + where + S: BitswapStore, + { + let bitswap = Bitswap::new(BitswapConfig::default(), store); + // TODO: `bitswap.register_metrics(prometheus::default_registry())` + Self { inner: bitswap } + } + + /// Recursively resolve a [`Cid`] and all underlying CIDs into blocks. + /// + /// The [`Bitswap`] behaviour will call the [`BitswapStore`] to ask for + /// blocks which are missing, ie. find CIDs which aren't available locally. + /// It is up to the store implementation to decide which links need to be + /// followed. + /// + /// It is also up to the store implementation to decide which CIDs requests + /// to responds to, e.g. if we only want to resolve certain type of content, + /// then the store can look up in a restricted collection, rather than the + /// full IPLD store. + /// + /// Resolution will be attempted from the peers passed to the method, + /// starting with the first one with `WANT-BLOCK`, then whoever responds + /// positively to `WANT-HAVE` requests. The caller should talk to the + /// `membership::Behaviour` first to find suitable peers, and then + /// prioritise peers which are connected. + /// + /// The underlying [`libp2p_request_response::RequestResponse`] behaviour + /// will initiate connections to the peers which aren't connected at the moment. + pub fn resolve(&mut self, cid: Cid, peers: Vec) -> QueryId { + // Not passing any missing items, which will result in a call to `BitswapStore::missing_blocks`. + self.inner.sync(cid, peers, [].into_iter()) + } +} + +impl NetworkBehaviour for Behaviour

{ + type ConnectionHandler = as NetworkBehaviour>::ConnectionHandler; + type OutEvent = Event; + + fn new_handler(&mut self) -> Self::ConnectionHandler { + self.inner.new_handler() + } + + fn addresses_of_peer(&mut self, peer_id: &PeerId) -> Vec { + self.inner.addresses_of_peer(peer_id) + } + + fn on_swarm_event(&mut self, event: FromSwarm) { + self.inner.on_swarm_event(event) + } + + fn on_connection_handler_event( + &mut self, + peer_id: PeerId, + connection_id: ConnectionId, + event: <::Handler as ConnectionHandler>::OutEvent, + ) { + self.inner + .on_connection_handler_event(peer_id, connection_id, event) + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + params: &mut impl PollParameters, + ) -> Poll> { + while let Poll::Ready(ev) = self.inner.poll(cx, params) { + match ev { + NetworkBehaviourAction::GenerateEvent(ev) => match ev { + BitswapEvent::Progress(_, _) => {} + BitswapEvent::Complete(id, result) => { + let out = Event::Complete(id, result); + return Poll::Ready(NetworkBehaviourAction::GenerateEvent(out)); + } + }, + other => { + return Poll::Ready(other.map_out(|_| unreachable!("already handled"))); + } + } + } + + Poll::Pending + } +} diff --git a/ipld/resolver/src/behaviour/mod.rs b/ipld/resolver/src/behaviour/mod.rs index 9478133cd..8297e064d 100644 --- a/ipld/resolver/src/behaviour/mod.rs +++ b/ipld/resolver/src/behaviour/mod.rs @@ -8,8 +8,8 @@ use libp2p::{ swarm::NetworkBehaviour, PeerId, }; -use libp2p_bitswap::Bitswap; +mod content; mod discovery; mod membership; @@ -40,7 +40,7 @@ pub struct IpldResolver { identify: identify::Behaviour, discovery: discovery::Behaviour, membership: membership::Behaviour, - bitswap: Bitswap

, // TODO (IPC-36): Wrap + bitswap: content::Behaviour

, } // Unfortunately by using `#[derive(NetworkBehaviour)]` we cannot easily inspects events diff --git a/ipld/resolver/src/lib.rs b/ipld/resolver/src/lib.rs index b7943ec7f..1281817c2 100644 --- a/ipld/resolver/src/lib.rs +++ b/ipld/resolver/src/lib.rs @@ -14,3 +14,6 @@ mod service; mod arb; mod hash; + +#[cfg(feature = "missing_blocks")] +pub mod missing_blocks; diff --git a/ipld/resolver/src/missing_blocks.rs b/ipld/resolver/src/missing_blocks.rs new file mode 100644 index 000000000..d7dd1c93a --- /dev/null +++ b/ipld/resolver/src/missing_blocks.rs @@ -0,0 +1,33 @@ +// Copyright 2022-2023 Protocol Labs +// SPDX-License-Identifier: MIT +// Copyright 2019-2022 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use fvm_ipld_blockstore::Blockstore; +use libipld::Cid; +use libipld::{prelude::*, store::StoreParams, Ipld}; + +/// Recursively find all [`Cid`] fields in the [`Block`] structures stored in the +/// [`Blockstore`] and return all CIDs which could *not* be retrieved from the store. +/// +/// This function is available as a convenience, to be used by any [`BitswapStore`] +/// implementation as they see fit. +pub fn missing_blocks( + bs: &mut BS, + cid: &Cid, +) -> anyhow::Result> +where + Ipld: References<

::Codecs>, +{ + let mut stack = vec![*cid]; + let mut missing = vec![]; + while let Some(cid) = stack.pop() { + if let Some(data) = bs.get(&cid)? { + let block = libipld::Block::

::new_unchecked(cid, data); + block.references(&mut stack)?; + } else { + missing.push(cid); + } + } + Ok(missing) +} From a941a09b9a381757130e7a6793153df948d808b1 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Mon, 6 Mar 2023 09:47:50 +0000 Subject: [PATCH 17/82] IPC-37: Resolver Service and Client (#55) * IPC-37: Constructor for the service. * IPC-37: Subscribe to the membership topic * IPC-37: Swarm event handlign skeleton * IPC-37: Complete query response * IPC-37: Service Client interface * IPC-37: Handle internal events. * IPC-37: Query connected first, then go to all. * IPC-37: Comments * IPC-37: Add Bloom filter. * IPC-37: Remove dead code * IPC-37: Emit Added for static peers and never emit Removed --- ipld/resolver/Cargo.toml | 1 + ipld/resolver/src/behaviour/content.rs | 4 +- ipld/resolver/src/behaviour/discovery.rs | 20 +- ipld/resolver/src/behaviour/membership.rs | 16 +- ipld/resolver/src/behaviour/mod.rs | 64 +++- ipld/resolver/src/lib.rs | 11 +- ipld/resolver/src/provider_record.rs | 16 +- ipld/resolver/src/service.rs | 428 +++++++++++++++++++++- 8 files changed, 521 insertions(+), 39 deletions(-) diff --git a/ipld/resolver/Cargo.toml b/ipld/resolver/Cargo.toml index a3d4c40f5..f123d17e7 100644 --- a/ipld/resolver/Cargo.toml +++ b/ipld/resolver/Cargo.toml @@ -11,6 +11,7 @@ license-file.workspace = true [dependencies] anyhow = { workspace = true } blake2b_simd = { workspace = true } +bloom = "0.3" thiserror = { workspace = true } tokio = { workspace = true } libp2p = { version = "0.50", default-features = false, features = [ diff --git a/ipld/resolver/src/behaviour/content.rs b/ipld/resolver/src/behaviour/content.rs index 3ecfeb2f0..7342f85ef 100644 --- a/ipld/resolver/src/behaviour/content.rs +++ b/ipld/resolver/src/behaviour/content.rs @@ -12,7 +12,9 @@ use libp2p::{ }, Multiaddr, PeerId, }; -use libp2p_bitswap::{Bitswap, BitswapConfig, BitswapEvent, BitswapStore, QueryId}; +use libp2p_bitswap::{Bitswap, BitswapConfig, BitswapEvent, BitswapStore}; + +pub type QueryId = libp2p_bitswap::QueryId; // Not much to do here, just hiding the `Progress` event as I don't think we'll need it. // We can't really turn it into anything more meaningful; the outer Service, which drives diff --git a/ipld/resolver/src/behaviour/discovery.rs b/ipld/resolver/src/behaviour/discovery.rs index 1855a0641..34822a307 100644 --- a/ipld/resolver/src/behaviour/discovery.rs +++ b/ipld/resolver/src/behaviour/discovery.rs @@ -98,6 +98,7 @@ impl Behaviour { if nc.network_name.is_empty() { return Err(ConfigError::InvalidNetwork(nc.network_name)); } + // Parse static addresses. let mut static_addresses = Vec::new(); for multiaddr in dc.static_addresses { @@ -133,11 +134,19 @@ impl Behaviour { None }; + let mut outbox = VecDeque::new(); + + // It would be nice to use `.group_by` here but it's unstable. + // Make sure static peers are reported as routable. + for (peer_id, addr) in static_addresses.iter() { + outbox.push_back(Event::Added(*peer_id, vec![addr.clone()])) + } + Ok(Self { static_addresses, inner: kademlia_opt.into(), lookup_interval: tokio::time::interval(Duration::from_secs(1)), - outbox: VecDeque::new(), + outbox, num_connections: 0, target_connections: dc.target_connections, }) @@ -151,6 +160,11 @@ impl Behaviour { } } } + + /// Check if a peer has a user defined addresses. + fn is_static(&self, peer_id: PeerId) -> bool { + self.static_addresses.iter().any(|(id, _)| *id == peer_id) + } } impl NetworkBehaviour for Behaviour { @@ -259,7 +273,9 @@ impl NetworkBehaviour for Behaviour { } => { // There are two events here; we can only return one, so let's defer them to the outbox. if let Some(peer_id) = old_peer { - self.outbox.push_back(Event::Removed(peer_id)) + if self.is_static(peer_id) { + self.outbox.push_back(Event::Removed(peer_id)) + } } self.outbox .push_back(Event::Added(peer, addresses.into_vec())) diff --git a/ipld/resolver/src/behaviour/membership.rs b/ipld/resolver/src/behaviour/membership.rs index c9bfbd98f..d3ade85d2 100644 --- a/ipld/resolver/src/behaviour/membership.rs +++ b/ipld/resolver/src/behaviour/membership.rs @@ -6,6 +6,7 @@ use std::time::Duration; use ipc_sdk::subnet_id::SubnetID; use libp2p::core::connection::ConnectionId; +use libp2p::gossipsub::error::SubscriptionError; use libp2p::gossipsub::{ GossipsubConfigBuilder, GossipsubEvent, GossipsubMessage, IdentTopic, MessageAuthenticity, MessageId, Topic, @@ -35,7 +36,7 @@ const PUBSUB_MEMBERSHIP: &str = "/ipc/membership"; #[derive(Debug)] pub enum Event { /// Indicate a change in the subnets a peer is known to support. - Updated((PeerId, ProviderDelta)), + Updated(PeerId, ProviderDelta), /// Indicate that we no longer treat a peer as routable and removed all their supported subnet associations. Removed(PeerId), @@ -47,6 +48,7 @@ pub enum Event { } /// Configuration for [`membership::Behaviour`]. +#[derive(Clone, Debug)] pub struct Config { /// User defined list of subnets which will never be pruned from the cache. pub static_subnets: Vec, @@ -64,6 +66,8 @@ pub enum ConfigError { InvalidNetwork(String), #[error("invalid gossipsub config: {0}")] InvalidGossipsubConfig(String), + #[error("error subscribing to topic")] + Subscription(#[from] SubscriptionError), } /// A [`NetworkBehaviour`] internally using [`Gossipsub`] to learn which @@ -122,6 +126,9 @@ impl Behaviour { ) .map_err(ConfigError::InvalidGossipsubConfig)?; + // Subscribe to the topic. + gossipsub.subscribe(&membership_topic)?; + // Don't publish immediately, it's empty. Let the creator call `set_subnet_ids` to trigger initially. let mut interval = tokio::time::interval(mc.publish_interval); interval.reset(); @@ -171,6 +178,11 @@ impl Behaviour { self.provider_cache.pin_subnet(subnet_id) } + /// Make a subnet pruneable. + pub fn unpin_subnet(&mut self, subnet_id: &SubnetID) { + self.provider_cache.unpin_subnet(subnet_id) + } + /// Send a message through Gossipsub to let everyone know about the current configuration. fn publish_membership(&mut self) -> anyhow::Result<()> { let record = SignedProviderRecord::new(&self.local_key, self.subnet_ids.clone())?; @@ -211,7 +223,7 @@ impl Behaviour { Ok(record) => match self.provider_cache.add_provider(&record) { None => return Some(Event::Skipped(record.peer_id)), Some(d) if d.is_empty() => return None, - Some(d) => return Some(Event::Updated((record.peer_id, d))), + Some(d) => return Some(Event::Updated(record.peer_id, d)), }, Err(e) => { warn!( diff --git a/ipld/resolver/src/behaviour/mod.rs b/ipld/resolver/src/behaviour/mod.rs index 8297e064d..e2124e527 100644 --- a/ipld/resolver/src/behaviour/mod.rs +++ b/ipld/resolver/src/behaviour/mod.rs @@ -8,11 +8,16 @@ use libp2p::{ swarm::NetworkBehaviour, PeerId, }; +use libp2p_bitswap::BitswapStore; -mod content; -mod discovery; -mod membership; +pub mod content; +pub mod discovery; +pub mod membership; +pub use discovery::Config as DiscoveryConfig; +pub use membership::Config as MembershipConfig; + +#[derive(Clone, Debug)] pub struct NetworkConfig { /// Cryptographic key used to sign messages. pub local_key: Keypair, @@ -29,22 +34,65 @@ impl NetworkConfig { } } -/// Libp2p behaviour to manage content resolution from other subnets, using: +#[derive(thiserror::Error, Debug)] +pub enum ConfigError { + #[error("Error in the discovery configuration")] + Discovery(#[from] discovery::ConfigError), + #[error("Error in the membership configuration")] + Membership(#[from] membership::ConfigError), +} + +/// Libp2p behaviour bundle to manage content resolution from other subnets, using: /// /// * Kademlia for peer discovery /// * Gossipsub to advertise subnet membership /// * Bitswap to resolve CIDs #[derive(NetworkBehaviour)] -pub struct IpldResolver { +pub struct Behaviour { ping: ping::Behaviour, identify: identify::Behaviour, discovery: discovery::Behaviour, membership: membership::Behaviour, - bitswap: content::Behaviour

, + content: content::Behaviour

, } // Unfortunately by using `#[derive(NetworkBehaviour)]` we cannot easily inspects events // from the inner behaviours, e.g. we cannot poll a behaviour and if it returns something -// of interest then call a method on another behaviour. We can do this in another wrapper +// of interest then call a method on another behaviour. We can do this in yet another wrapper // where we manually implement `NetworkBehaviour`, or the outer service where we drive the -// Swarm; there we are free to call any of the behaviours. +// Swarm; there we are free to call any of the behaviours as well as the Swarm. + +impl Behaviour

{ + pub fn new( + nc: NetworkConfig, + dc: DiscoveryConfig, + mc: MembershipConfig, + store: S, + ) -> Result + where + S: BitswapStore, + { + Ok(Self { + ping: Default::default(), + identify: identify::Behaviour::new(identify::Config::new( + "ipfs/0.1.0".into(), + nc.local_public_key(), + )), + discovery: discovery::Behaviour::new(nc.clone(), dc)?, + membership: membership::Behaviour::new(nc, mc)?, + content: content::Behaviour::new(store), + }) + } + + pub fn discovery_mut(&mut self) -> &mut discovery::Behaviour { + &mut self.discovery + } + + pub fn membership_mut(&mut self) -> &mut membership::Behaviour { + &mut self.membership + } + + pub fn content_mut(&mut self) -> &mut content::Behaviour

{ + &mut self.content + } +} diff --git a/ipld/resolver/src/lib.rs b/ipld/resolver/src/lib.rs index 1281817c2..829f2981e 100644 --- a/ipld/resolver/src/lib.rs +++ b/ipld/resolver/src/lib.rs @@ -1,19 +1,16 @@ // Copyright 2022-2023 Protocol Labs // SPDX-License-Identifier: MIT -// TODO (IPC-38): Remove dead code allowances. -#[allow(dead_code)] mod behaviour; -#[allow(dead_code)] +mod hash; mod provider_cache; -#[allow(dead_code)] mod provider_record; -#[allow(dead_code)] mod service; #[cfg(any(test, feature = "arb"))] mod arb; -mod hash; - #[cfg(feature = "missing_blocks")] pub mod missing_blocks; + +pub use behaviour::{DiscoveryConfig, MembershipConfig}; +pub use service::{Client, Config, ConnectionConfig, NoKnownPeers, Service}; diff --git a/ipld/resolver/src/provider_record.rs b/ipld/resolver/src/provider_record.rs index 280678d97..e30c07c4b 100644 --- a/ipld/resolver/src/provider_record.rs +++ b/ipld/resolver/src/provider_record.rs @@ -38,7 +38,7 @@ impl Sub for Timestamp { type Output = Self; fn sub(self, rhs: Duration) -> Self { - Self(self.0.saturating_sub(rhs.as_secs())) + Self(self.as_secs().saturating_sub(rhs.as_secs())) } } @@ -46,7 +46,7 @@ impl Add for Timestamp { type Output = Self; fn add(self, rhs: Duration) -> Self { - Self(self.0.saturating_add(rhs.as_secs())) + Self(self.as_secs().saturating_add(rhs.as_secs())) } } @@ -128,14 +128,6 @@ impl SignedProviderRecord { Ok(signed_record) } - pub fn record(&self) -> &ProviderRecord { - &self.record - } - - pub fn envelope(&self) -> &SignedEnvelope { - &self.envelope - } - pub fn into_record(self) -> ProviderRecord { self.record } @@ -206,7 +198,7 @@ mod tests { #[quickcheck] fn prop_roundtrip(signed_record: SignedProviderRecord) -> bool { - let envelope_bytes = signed_record.envelope().clone().into_protobuf_encoding(); + let envelope_bytes = signed_record.envelope.into_protobuf_encoding(); let envelope = SignedEnvelope::from_protobuf_encoding(&envelope_bytes).expect("envelope roundtrip"); @@ -219,7 +211,7 @@ mod tests { #[quickcheck] fn prop_tamper_proof(signed_record: SignedProviderRecord, idx: usize) -> bool { - let mut envelope_bytes = signed_record.envelope().clone().into_protobuf_encoding(); + let mut envelope_bytes = signed_record.envelope.into_protobuf_encoding(); // Do some kind of mutation to a random byte in the envelope; after that it should not validate. let idx = idx % envelope_bytes.len(); envelope_bytes[idx] = u8::MAX - envelope_bytes[idx]; diff --git a/ipld/resolver/src/service.rs b/ipld/resolver/src/service.rs index 24362bd79..927b7bd53 100644 --- a/ipld/resolver/src/service.rs +++ b/ipld/resolver/src/service.rs @@ -1,17 +1,431 @@ // Copyright 2022-2023 Protocol Labs // SPDX-License-Identifier: MIT +use std::collections::HashMap; +use std::time::Duration; + +use anyhow::anyhow; +use bloom::{BloomFilter, ASMS}; +use ipc_sdk::subnet_id::SubnetID; use libipld::store::StoreParams; -use libp2p::Swarm; +use libipld::Cid; +use libp2p::futures::StreamExt; +use libp2p::swarm::SwarmEvent; +use libp2p::{ + core::{muxing::StreamMuxerBox, transport::Boxed}, + identity::Keypair, + mplex, noise, + swarm::{ConnectionLimits, SwarmBuilder}, + yamux, Multiaddr, PeerId, Swarm, Transport, +}; +use libp2p::{identify, ping}; +use libp2p_bitswap::BitswapStore; +use log::{debug, error, trace, warn}; +use tokio::select; +use tokio::sync::oneshot::{self, Sender}; + +use crate::behaviour::{ + self, content, discovery, membership, Behaviour, BehaviourEvent, ConfigError, DiscoveryConfig, + MembershipConfig, NetworkConfig, +}; + +/// Result of attempting to resolve a CID. +pub type ResolveResult = anyhow::Result<()>; + +/// State of a query. The fallback peers can be used +/// if the current attempt fails. +struct Query { + cid: Cid, + subnet_id: SubnetID, + fallback_peer_ids: Vec, + response_channel: oneshot::Sender, +} + +/// Keeps track of where to send query responses to. +type QueryMap = HashMap; + +/// Error returned when we tried to get a CID from a subnet for +/// which we currently have no peers to contact +#[derive(thiserror::Error, Debug)] +#[error("No known peers for subnet {0}")] +pub struct NoKnownPeers(SubnetID); + +pub struct ConnectionConfig { + /// The address where we will listen to incoming connections. + listen_addr: Multiaddr, + /// Maximum number of incoming connections. + max_incoming: u32, + /// Expected number of peers, for sizing the Bloom filter. + expected_peer_count: u32, +} + +pub struct Config { + network: NetworkConfig, + discovery: DiscoveryConfig, + membership: MembershipConfig, + connection: ConnectionConfig, +} -use crate::behaviour::IpldResolver; +/// Internal requests to enqueue to the [`Service`] +enum Request { + SetProvidedSubnets(Vec), + AddProvidedSubnet(SubnetID), + RemoveProvidedSubnet(SubnetID), + PinSubnet(SubnetID), + UnpinSubnet(SubnetID), + Resolve(Cid, SubnetID, oneshot::Sender), +} -pub struct IpldResolverService { - swarm: Swarm>, +/// A facade to the [`Service`] to provide a nicer interface than message passing would allow on its own. +#[derive(Clone)] +pub struct Client { + request_tx: tokio::sync::mpsc::UnboundedSender, } -impl IpldResolverService

{ +impl Client { + /// Send a request to the [`Service`], unless it has stopped listening. + fn send_request(&self, req: Request) -> anyhow::Result<()> { + self.request_tx + .send(req) + .map_err(|_| anyhow!("disconnected")) + } + + /// Set the complete list of subnets currently supported by this node. + pub fn set_provided_subnets(&self, subnet_ids: Vec) -> anyhow::Result<()> { + let req = Request::SetProvidedSubnets(subnet_ids); + self.send_request(req) + } + + /// Add a subnet supported by this node. + pub fn add_provided_subnets(&self, subnet_id: SubnetID) -> anyhow::Result<()> { + let req = Request::AddProvidedSubnet(subnet_id); + self.send_request(req) + } + + /// Remove a subnet no longer supported by this node. + pub fn remove_provided_subnets(&self, subnet_id: SubnetID) -> anyhow::Result<()> { + let req = Request::RemoveProvidedSubnet(subnet_id); + self.send_request(req) + } + + /// Add a subnet we know really exist and we are interested in them. + pub fn pin_subnet(&self, subnet_id: SubnetID) -> anyhow::Result<()> { + let req = Request::PinSubnet(subnet_id); + self.send_request(req) + } + + /// Unpin a we are no longer interested in. + pub fn unpin_subnet(&self, subnet_id: SubnetID) -> anyhow::Result<()> { + let req = Request::UnpinSubnet(subnet_id); + self.send_request(req) + } + + /// Send a CID for resolution from a specific subnet, await its completion, + /// then return the result, to be inspected by the caller. + /// + /// Upon success, the data should be found in the store. + pub async fn resolve(&self, cid: Cid, subnet_id: SubnetID) -> anyhow::Result { + let (tx, rx) = oneshot::channel(); + let req = Request::Resolve(cid, subnet_id, tx); + self.send_request(req)?; + let res = rx.await?; + Ok(res) + } +} + +/// The `Service` handles P2P communication to resolve IPLD content by wrapping and driving a number of `libp2p` behaviours. +pub struct Service { + listen_addr: Multiaddr, + swarm: Swarm>, + queries: QueryMap, + request_rx: tokio::sync::mpsc::UnboundedReceiver, + background_lookup_filter: BloomFilter, +} + +impl Service

{ + pub fn new(config: Config, store: S) -> Result<(Self, Client), ConfigError> + where + S: BitswapStore, + { + let peer_id = config.network.local_peer_id(); + let transport = build_transport(config.network.local_key.clone()); + let behaviour = Behaviour::new(config.network, config.discovery, config.membership, store)?; + + // NOTE: Hardcoded values from Forest. Will leave them as is until we know we need to change. + + let limits = ConnectionLimits::default() + .with_max_pending_incoming(Some(10)) + .with_max_pending_outgoing(Some(30)) + .with_max_established_incoming(Some(config.connection.max_incoming)) + .with_max_established_outgoing(None) // Allow bitswap to connect to subnets we did not anticipate when we started. + .with_max_established_per_peer(Some(5)); + + let swarm = SwarmBuilder::with_tokio_executor(transport, behaviour, peer_id) + .connection_limits(limits) + .notify_handler_buffer_size(std::num::NonZeroUsize::new(20).expect("Not zero")) + .connection_event_buffer_size(64) + .build(); + + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + + let service = Self { + listen_addr: config.connection.listen_addr, + swarm, + queries: Default::default(), + request_rx: rx, + background_lookup_filter: BloomFilter::with_rate( + 0.1, + config.connection.expected_peer_count, + ), + }; + + let client = Client { request_tx: tx }; + + Ok((service, client)) + } + /// Start the swarm listening for incoming connections and drive the events forward. - pub async fn run(self) -> anyhow::Result<()> { - todo!("IPC-37") + pub async fn run(mut self) -> anyhow::Result<()> { + // Start the swarm. + Swarm::listen_on(&mut self.swarm, self.listen_addr.clone())?; + + loop { + select! { + swarm_event = self.swarm.next() => match swarm_event { + // Events raised by our behaviours. + Some(SwarmEvent::Behaviour(event)) => { + self.handle_behaviour_event(event) + }, + // Connection events are handled by the behaviours, passed directly from the Swarm. + Some(_) => { }, + // The connection is closed. + None => { break; }, + }, + request = self.request_rx.recv() => match request { + // A Client sent us a request. + Some(req) => self.handle_request(req), + // All Client instances have been dropped. + // We could keep the Swarm alive to serve content to others, + // but we ourselves are unable to send requests. Let's treat + // this as time to quit. + None => { break; } + } + }; + } + Ok(()) } + + /// Handle events that the [`NetworkBehaviour`] for our [`Behaviour`] macro generated, one for each field. + fn handle_behaviour_event(&mut self, event: BehaviourEvent

) { + match event { + BehaviourEvent::Ping(e) => self.handle_ping_event(e), + BehaviourEvent::Identify(e) => self.handle_identify_event(e), + BehaviourEvent::Discovery(e) => self.handle_discovery_event(e), + BehaviourEvent::Membership(e) => self.handle_membership_event(e), + BehaviourEvent::Content(e) => self.handle_content_event(e), + } + } + + // Copied from Forest. + fn handle_ping_event(&mut self, event: ping::Event) { + let peer_id = event.peer.to_base58(); + match event.result { + Ok(ping::Success::Ping { rtt }) => { + trace!( + "PingSuccess::Ping rtt to {} is {} ms", + peer_id, + rtt.as_millis() + ); + } + Ok(ping::Success::Pong) => { + trace!("PingSuccess::Pong from {peer_id}"); + } + Err(ping::Failure::Timeout) => { + debug!("PingFailure::Timeout from {peer_id}"); + } + Err(ping::Failure::Other { error }) => { + warn!("PingFailure::Other from {peer_id}: {error}"); + } + Err(ping::Failure::Unsupported) => { + warn!("Banning peer {peer_id} due to protocol error"); + self.swarm.ban_peer_id(event.peer); + } + } + } + + fn handle_identify_event(&mut self, event: identify::Event) { + if let identify::Event::Error { peer_id, error } = event { + warn!("Error identifying {peer_id}: {error}") + } + } + + fn handle_discovery_event(&mut self, event: discovery::Event) { + match event { + discovery::Event::Added(peer_id, _) => self.membership_mut().set_routable(peer_id), + discovery::Event::Removed(peer_id) => self.membership_mut().set_unroutable(peer_id), + discovery::Event::Connected(_, _) => {} + discovery::Event::Disconnected(_, _) => {} + } + } + + fn handle_membership_event(&mut self, event: membership::Event) { + match event { + membership::Event::Skipped(peer_id) => { + // Don't repeatedly look up peers we can't add to the routing table. + if self.background_lookup_filter.insert(&peer_id) { + self.discovery_mut().background_lookup(peer_id) + } + } + membership::Event::Updated(_, _) => {} + membership::Event::Removed(_) => {} + } + } + + /// Handle Bitswap lookup result. + fn handle_content_event(&mut self, event: content::Event) { + match event { + content::Event::Complete(query_id, result) => { + if let Some(query) = self.queries.remove(&query_id) { + self.handle_query_result(query, result); + } else { + warn!("query ID not found"); + } + } + } + } + + /// Handle the results from a resolve attempt. If it succeeded, notify the + /// listener. Otherwise if we have fallback peers to try, start another + /// query and send the result to them. By default these are the peers + /// we know support the subnet, but weren't connected to when the we + /// first attempted the resolution. + fn handle_query_result(&mut self, mut query: Query, result: ResolveResult) { + match result { + Ok(_) => send_resolve_result(query.response_channel, result), + Err(_) if query.fallback_peer_ids.is_empty() => { + send_resolve_result(query.response_channel, result) + } + Err(e) => { + debug!( + "resolving {} from {} failed with {}, but there are {} fallback peers to try", + query.cid, + query.subnet_id, + e, + query.fallback_peer_ids.len() + ); + + // Now we can go all in; alternatively we could take the next N peers. + let peers = std::mem::take(&mut query.fallback_peer_ids); + + let query_id = self.content_mut().resolve(query.cid, peers); + + self.queries.insert(query_id, query); + } + } + } + + /// Handle an internal request coming from a [`Client`]. + fn handle_request(&mut self, request: Request) { + match request { + Request::SetProvidedSubnets(ids) => { + if let Err(e) = self.membership_mut().set_provided_subnets(ids) { + error!("error setting provided subnets: {e}") + } + } + Request::AddProvidedSubnet(id) => { + if let Err(e) = self.membership_mut().add_provided_subnet(id) { + error!("error adding provided subnet: {e}") + } + } + Request::RemoveProvidedSubnet(id) => { + if let Err(e) = self.membership_mut().remove_provided_subnet(id) { + error!("error removing provided subnet: {e}") + } + } + Request::PinSubnet(id) => self.membership_mut().pin_subnet(id), + Request::UnpinSubnet(id) => self.membership_mut().unpin_subnet(&id), + + Request::Resolve(cid, subnet_id, response_channel) => { + let peers = self.membership_mut().providers_of_subnet(&subnet_id); + if peers.is_empty() { + send_resolve_result(response_channel, Err(anyhow!(NoKnownPeers(subnet_id)))); + } else { + let (connected, known) = peers + .into_iter() + .partition::, _>(|id| self.swarm.is_connected(id)); + + let (peers, fallback) = if connected.is_empty() { + (known, vec![]) + } else { + // Use just the connected ones, however many there are. + // Alternatively we could take the first N combined. + (connected, known) + }; + + let query = Query { + cid, + subnet_id, + response_channel, + fallback_peer_ids: fallback, + }; + + let query_id = self.content_mut().resolve(cid, peers); + + self.queries.insert(query_id, query); + } + } + } + } + + // The following are helper functions because Rust Analyzer has trouble with recognising that `swarm.behaviour_mut()` is a legal call. + + fn discovery_mut(&mut self) -> &mut behaviour::discovery::Behaviour { + self.swarm.behaviour_mut().discovery_mut() + } + fn membership_mut(&mut self) -> &mut behaviour::membership::Behaviour { + self.swarm.behaviour_mut().membership_mut() + } + fn content_mut(&mut self) -> &mut behaviour::content::Behaviour

{ + self.swarm.behaviour_mut().content_mut() + } +} + +/// Respond to the sender of the query, if they are still listening. +fn send_resolve_result(tx: Sender, res: ResolveResult) { + if tx.send(res).is_err() { + error!("error sending resolve result; listener closed") + } +} + +/// Builds the transport stack that libp2p will communicate over. +/// +/// Based on the equivalent in Forest. +pub fn build_transport(local_key: Keypair) -> Boxed<(PeerId, StreamMuxerBox)> { + let tcp_transport = + || libp2p::tcp::tokio::Transport::new(libp2p::tcp::Config::new().nodelay(true)); + let transport = libp2p::dns::TokioDnsConfig::system(tcp_transport()).unwrap(); + let auth_config = { + let dh_keys = noise::Keypair::::new() + .into_authentic(&local_key) + .expect("Noise key generation failed"); + + noise::NoiseConfig::xx(dh_keys).into_authenticated() + }; + + let mplex_config = { + let mut mplex_config = mplex::MplexConfig::new(); + mplex_config.set_max_buffer_size(usize::MAX); + + let mut yamux_config = yamux::YamuxConfig::default(); + yamux_config.set_max_buffer_size(16 * 1024 * 1024); + yamux_config.set_receive_window_size(16 * 1024 * 1024); + // yamux_config.set_window_update_mode(WindowUpdateMode::OnRead); + libp2p::core::upgrade::SelectUpgrade::new(yamux_config, mplex_config) + }; + + transport + .upgrade(libp2p::core::upgrade::Version::V1) + .authenticate(auth_config) + .multiplex(mplex_config) + .timeout(Duration::from_secs(20)) + .boxed() } From 9114d8bc1823b0a8d98716b02988354574100c4b Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Mon, 6 Mar 2023 10:16:04 +0000 Subject: [PATCH 18/82] IPC-38: Disable Kademlia storage inserts (#62) --- ipld/resolver/src/behaviour/discovery.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/ipld/resolver/src/behaviour/discovery.rs b/ipld/resolver/src/behaviour/discovery.rs index 34822a307..bd5b9287b 100644 --- a/ipld/resolver/src/behaviour/discovery.rs +++ b/ipld/resolver/src/behaviour/discovery.rs @@ -12,8 +12,8 @@ use std::{ use libp2p::{ core::connection::ConnectionId, kad::{ - handler::KademliaHandlerProto, store::MemoryStore, Kademlia, KademliaConfig, KademliaEvent, - QueryId, + handler::KademliaHandlerProto, store::MemoryStore, InboundRequest, Kademlia, + KademliaConfig, KademliaEvent, KademliaStoreInserts, QueryId, }, multiaddr::Protocol, swarm::{ @@ -24,7 +24,7 @@ use libp2p::{ }, Multiaddr, PeerId, }; -use log::debug; +use log::{debug, warn}; use tokio::time::Interval; use super::NetworkConfig; @@ -116,6 +116,10 @@ impl Behaviour { let protocol_name = format!("/ipc/{}/kad/1.0.0", nc.network_name); kad_config.set_protocol_names(vec![Cow::Owned(protocol_name.as_bytes().to_vec())]); + // Disable inserting records into the memory store, so peers cannot send `PutRecord` + // messages to store content in the memory of our node. + kad_config.set_record_filtering(KademliaStoreInserts::FilterBoth); + let store = MemoryStore::new(nc.local_peer_id()); let mut kademlia = Kademlia::with_config(nc.local_peer_id(), store, kad_config); @@ -256,6 +260,11 @@ impl NetworkBehaviour for Behaviour { KademliaEvent::UnroutablePeer { .. } => { debug!("unexpected Kademlia event: {ev:?}") } + KademliaEvent::InboundRequest { + request: InboundRequest::PutRecord { source, .. }, + } => { + warn!("disallowed Kademlia requests from {source}",) + } // Information only. KademliaEvent::InboundRequest { .. } | KademliaEvent::OutboundQueryProgressed { .. } => {} From 7d93359ee3357cc41582a8cf9273bca40e371d16 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Mon, 6 Mar 2023 20:27:25 +0800 Subject: [PATCH 19/82] Reload config (#63) * broadcast changes * add headers * remote tokio threading: * remove unused Arc * add more comments * correct comment --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index a375bcdae..a5e81330c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,9 @@ ipc-subnet-actor = { workspace = true } ipc-gateway = { workspace = true } fvm_ipld_encoding = { workspace = true } +[dev-dependencies] +tempfile = "3.4.0" + [workspace.dependencies] anyhow = "1.0" log = "0.4" From 7ade3a62e7d228eaf4cfd38b035e89aa05b81f9c Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Mon, 6 Mar 2023 23:22:37 +0800 Subject: [PATCH 20/82] Create subnet with reloading config (#71) * add json rpc handler trait * format code * update response to serialize * change pass request by ownership * create subnet (#46) * init commit * wip * add handlers * enable tests * remove logic first * add gateway as const * remove gateway addr in test * rename functions * add implementation of create subnet * add create subnet logic * remove unused fields * remove unused crate * add create subnet cli * migrate config * broadcast changes * add headers * remote tokio threading: * update method name * rename * reload config * add more comments * add more comments * add global params * update comment --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a5e81330c..cd7b74a26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ tokio = { workspace = true } tokio-tungstenite = { version = "0.18.0", features = ["native-tls"] } derive_builder = "0.12.0" num-traits = "0.2.15" +num-derive = "0.3.3" env_logger = "0.10.0" base64 = "0.21.0" strum = { version = "0.24", features = ["derive"] } @@ -39,7 +40,6 @@ bytes = "1.4.0" clap = { version = "4.1.4", features = ["env", "derive"] } thiserror = "1.0.38" serde_tuple = "0.5.0" -once_cell = "1.17.1" fvm_shared = { workspace = true } fil_actors_runtime = { workspace = true } From e333b22870a8d675894fb8b30950ba0911df4cb5 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 7 Mar 2023 10:03:07 +0000 Subject: [PATCH 21/82] IPC-38: IPLD Resolver smoke test (#64) * IPC-38: Service::new_with_transport * IPC-38: Build MemoryTransport * IPC-38: Allow empty peers for the first seed. * IPC-38: Constructors. * IPC-38: Cluster builder * IPC-38: Implement test stores * IPC-38: Fix bootstrap addresses * IPC-38: Start cluster * IPC-38: Failing test. * IPC-38: Smaller cluster, more gossip. * IPC-38: Fix config, still nothing. Kademlia doesn't work. * IPC-38: Disable gossip for debugging Kademlia. * IPC-38: Add address from Identify * IPC-38: Fix timed sleeps in smoke test. * IPC-38: Add header * IPC-38: Remove commented code. * IPC-38: Only add address if protocol is supported. * IPC-38: Remove Discovery::Connected and Disconnected, unused events --- Cargo.toml | 2 + ipld/resolver/Cargo.toml | 5 +- ipld/resolver/src/behaviour/discovery.rs | 93 +++++--- ipld/resolver/src/behaviour/membership.rs | 2 +- ipld/resolver/src/behaviour/mod.rs | 2 +- ipld/resolver/src/lib.rs | 2 +- ipld/resolver/src/service.rs | 79 +++++-- ipld/resolver/tests/smoke.rs | 250 ++++++++++++++++++++++ ipld/resolver/tests/store/mod.rs | 54 +++++ 9 files changed, 431 insertions(+), 58 deletions(-) create mode 100644 ipld/resolver/tests/smoke.rs create mode 100644 ipld/resolver/tests/store/mod.rs diff --git a/Cargo.toml b/Cargo.toml index cd7b74a26..941737d3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,12 +54,14 @@ tempfile = "3.4.0" [workspace.dependencies] anyhow = "1.0" log = "0.4" +env_logger = "0.10" serde = { version = "1.0", features = ["derive"] } thiserror = "1.0" tokio = { version = "1.16", features = ["full"] } quickcheck = "1" quickcheck_macros = "1" blake2b_simd = "1.0" +rand = "0.8" fvm_ipld_blockstore = "0.1" fvm_ipld_encoding = "0.3" diff --git a/ipld/resolver/Cargo.toml b/ipld/resolver/Cargo.toml index f123d17e7..794298fd3 100644 --- a/ipld/resolver/Cargo.toml +++ b/ipld/resolver/Cargo.toml @@ -30,6 +30,7 @@ libp2p = { version = "0.50", default-features = false, features = [ "macros", "serde", "secp256k1", + "plaintext", ] } libp2p-bitswap = "0.25" libipld = { workspace = true } @@ -45,8 +46,10 @@ fvm_ipld_blockstore = { workspace = true, optional = true } [dev-dependencies] quickcheck = { workspace = true } quickcheck_macros = { workspace = true } +rand = { workspace = true } +env_logger = { workspace = true } fvm_shared = { workspace = true, features = ["arb"] } - +fvm_ipld_hamt = "0.6" [features] default = ["arb", "missing_blocks"] diff --git a/ipld/resolver/src/behaviour/discovery.rs b/ipld/resolver/src/behaviour/discovery.rs index bd5b9287b..f3b3385e8 100644 --- a/ipld/resolver/src/behaviour/discovery.rs +++ b/ipld/resolver/src/behaviour/discovery.rs @@ -11,6 +11,7 @@ use std::{ use libp2p::{ core::connection::ConnectionId, + identify::Info, kad::{ handler::KademliaHandlerProto, store::MemoryStore, InboundRequest, Kademlia, KademliaConfig, KademliaEvent, KademliaStoreInserts, QueryId, @@ -35,12 +36,6 @@ use super::NetworkConfig; /// Event generated by the `Discovery` behaviour. #[derive(Debug)] pub enum Event { - /// Event emitted when we first connect to a peer. - Connected(PeerId, Vec), - - /// Event emitted when the last connection to a peer is closed. - Disconnected(PeerId, Vec), - /// Event emitted when a peer is added or updated in the routing table, /// which means if we later ask for its addresses, they should be known. Added(PeerId, Vec), @@ -77,9 +72,13 @@ pub enum ConfigError { /// Our other option for peer discovery would be to rely on the Peer Exchange of Gossipsub. /// However, the required Signed Records feature is not available in the Rust version of the library, as of v0.50. pub struct Behaviour { + /// Local peer ID. + peer_id: PeerId, /// User-defined list of nodes and their addresses. /// Typically includes bootstrap nodes, or it can be used for a static network. static_addresses: Vec<(PeerId, Multiaddr)>, + /// Name of the peer discovery protocol. + protocol_name: String, /// Kademlia behaviour, if enabled. inner: Toggle>, /// Number of current connections. @@ -111,9 +110,11 @@ impl Behaviour { } } + let mut outbox = VecDeque::new(); + let protocol_name = format!("/ipc/{}/kad/1.0.0", nc.network_name); + let kademlia_opt = if dc.enable_kademlia { let mut kad_config = KademliaConfig::default(); - let protocol_name = format!("/ipc/{}/kad/1.0.0", nc.network_name); kad_config.set_protocol_names(vec![Cow::Owned(protocol_name.as_bytes().to_vec())]); // Disable inserting records into the memory store, so peers cannot send `PutRecord` @@ -124,30 +125,31 @@ impl Behaviour { let mut kademlia = Kademlia::with_config(nc.local_peer_id(), store, kad_config); - for (peer_id, addr) in static_addresses.iter() { - kademlia.add_address(peer_id, addr.clone()); + // Bootstrap from the seeds. The first seed to stand up might have nobody to bootstrap from, + // although ideally there would be at least another peer, so we can easily restart it and come back. + if !static_addresses.is_empty() { + for (peer_id, addr) in static_addresses.iter() { + kademlia.add_address(peer_id, addr.clone()); + } + kademlia + .bootstrap() + .map_err(|_| ConfigError::NoBootstrapAddress)?; } - // This shouldn't happen, we already checked the config. - kademlia - .bootstrap() - .map_err(|_| ConfigError::NoBootstrapAddress)?; - Some(kademlia) } else { + // It would be nice to use `.group_by` here but it's unstable. + // Make sure static peers are reported as routable. + for (peer_id, addr) in static_addresses.iter() { + outbox.push_back(Event::Added(*peer_id, vec![addr.clone()])) + } None }; - let mut outbox = VecDeque::new(); - - // It would be nice to use `.group_by` here but it's unstable. - // Make sure static peers are reported as routable. - for (peer_id, addr) in static_addresses.iter() { - outbox.push_back(Event::Added(*peer_id, vec![addr.clone()])) - } - Ok(Self { + peer_id: nc.local_peer_id(), static_addresses, + protocol_name, inner: kademlia_opt.into(), lookup_interval: tokio::time::interval(Duration::from_secs(1)), outbox, @@ -169,6 +171,26 @@ impl Behaviour { fn is_static(&self, peer_id: PeerId) -> bool { self.static_addresses.iter().any(|(id, _)| *id == peer_id) } + + /// Add addresses we learned from the `Identify` protocol to Kademlia. + /// + /// This seems to be the only way, because Kademlia rightfully treats + /// incoming connections as ephemeral addresses, but doesn't have an + /// alternative exchange mechanism. + pub fn add_identified(&mut self, peer_id: &PeerId, info: &Info) { + if info.protocols.contains(&self.protocol_name) { + for addr in info.listen_addrs.iter().cloned() { + self.add_address(peer_id, addr); + } + } + } + + /// Add a known address to Kademlia. + pub fn add_address(&mut self, peer_id: &PeerId, address: Multiaddr) { + if let Some(kademlia) = self.inner.as_mut() { + kademlia.add_address(peer_id, address); + } + } } impl NetworkBehaviour for Behaviour { @@ -195,17 +217,13 @@ impl NetworkBehaviour for Behaviour { fn on_swarm_event(&mut self, event: FromSwarm) { match &event { FromSwarm::ConnectionEstablished(e) => { - self.num_connections += 1; if e.other_established == 0 { - let addrs = self.addresses_of_peer(&e.peer_id); - self.outbox.push_back(Event::Connected(e.peer_id, addrs)); + self.num_connections += 1; } } FromSwarm::ConnectionClosed(e) => { - self.num_connections -= 1; if e.remaining_established == 0 { - let addrs = self.addresses_of_peer(&e.peer_id); - self.outbox.push_back(Event::Disconnected(e.peer_id, addrs)); + self.num_connections -= 1; } } _ => {} @@ -237,6 +255,7 @@ impl NetworkBehaviour for Behaviour { if self.lookup_interval.poll_tick(cx).is_ready() { if self.num_connections < self.target_connections { if let Some(k) = self.inner.as_mut() { + debug!("looking up a random peer"); let random_peer_id = PeerId::random(); k.get_closest_peers(random_peer_id); } @@ -256,9 +275,9 @@ impl NetworkBehaviour for Behaviour { match ev { NetworkBehaviourAction::GenerateEvent(ev) => { match ev { - // Not expecting unroutable peers - KademliaEvent::UnroutablePeer { .. } => { - debug!("unexpected Kademlia event: {ev:?}") + // We get this event for inbound connections, where the remote address may be ephemeral. + KademliaEvent::UnroutablePeer { peer } => { + debug!("{peer} unroutable from {}", self.peer_id); } KademliaEvent::InboundRequest { request: InboundRequest::PutRecord { source, .. }, @@ -271,8 +290,15 @@ impl NetworkBehaviour for Behaviour { // The config ensures peers are added to the table if there's room. // We're not emitting these as known peers because the address will probably not be returned by `addresses_of_peer`, // so the outside service would have to keep track, which is not what we want. - KademliaEvent::PendingRoutablePeer { .. } - | KademliaEvent::RoutablePeer { .. } => {} + KademliaEvent::RoutablePeer { peer, .. } => { + debug!("Kademlia in manual mode or bucket full, cannot add {peer}"); + } + // Unfortunately, looking at the Kademlia behaviour, it looks like when it goes from pending to active, + // it won't emit another event, so we might as well tentatively emit an event here. + KademliaEvent::PendingRoutablePeer { peer, address } => { + debug!("{peer} pending to the routing table of {}", self.peer_id); + self.outbox.push_back(Event::Added(peer, vec![address])) + } // This event should ensure that we will be able to answer address lookups later. KademliaEvent::RoutingUpdated { peer, @@ -280,6 +306,7 @@ impl NetworkBehaviour for Behaviour { old_peer, .. } => { + debug!("{peer} added to the routing table of {}", self.peer_id); // There are two events here; we can only return one, so let's defer them to the outbox. if let Some(peer_id) = old_peer { if self.is_static(peer_id) { diff --git a/ipld/resolver/src/behaviour/membership.rs b/ipld/resolver/src/behaviour/membership.rs index d3ade85d2..d09f5df7b 100644 --- a/ipld/resolver/src/behaviour/membership.rs +++ b/ipld/resolver/src/behaviour/membership.rs @@ -287,7 +287,7 @@ impl NetworkBehaviour for Behaviour { // Republish our current peer record snapshot and prune old records. if self.publish_interval.poll_tick(cx).is_ready() { if let Err(e) = self.publish_membership() { - error!("error publishing membership: {e}") + warn!("failed to publish membership: {e}") }; self.prune_membership(); } diff --git a/ipld/resolver/src/behaviour/mod.rs b/ipld/resolver/src/behaviour/mod.rs index e2124e527..2818dc87c 100644 --- a/ipld/resolver/src/behaviour/mod.rs +++ b/ipld/resolver/src/behaviour/mod.rs @@ -75,7 +75,7 @@ impl Behaviour

{ Ok(Self { ping: Default::default(), identify: identify::Behaviour::new(identify::Config::new( - "ipfs/0.1.0".into(), + "ipfs/1.0.0".into(), nc.local_public_key(), )), discovery: discovery::Behaviour::new(nc.clone(), dc)?, diff --git a/ipld/resolver/src/lib.rs b/ipld/resolver/src/lib.rs index 829f2981e..5710414dc 100644 --- a/ipld/resolver/src/lib.rs +++ b/ipld/resolver/src/lib.rs @@ -12,5 +12,5 @@ mod arb; #[cfg(feature = "missing_blocks")] pub mod missing_blocks; -pub use behaviour::{DiscoveryConfig, MembershipConfig}; +pub use behaviour::{DiscoveryConfig, MembershipConfig, NetworkConfig}; pub use service::{Client, Config, ConnectionConfig, NoKnownPeers, Service}; diff --git a/ipld/resolver/src/service.rs b/ipld/resolver/src/service.rs index 927b7bd53..dce70d8dc 100644 --- a/ipld/resolver/src/service.rs +++ b/ipld/resolver/src/service.rs @@ -49,20 +49,22 @@ type QueryMap = HashMap; #[error("No known peers for subnet {0}")] pub struct NoKnownPeers(SubnetID); +#[derive(Debug, Clone)] pub struct ConnectionConfig { /// The address where we will listen to incoming connections. - listen_addr: Multiaddr, + pub listen_addr: Multiaddr, /// Maximum number of incoming connections. - max_incoming: u32, + pub max_incoming: u32, /// Expected number of peers, for sizing the Bloom filter. - expected_peer_count: u32, + pub expected_peer_count: u32, } +#[derive(Debug, Clone)] pub struct Config { - network: NetworkConfig, - discovery: DiscoveryConfig, - membership: MembershipConfig, - connection: ConnectionConfig, + pub network: NetworkConfig, + pub discovery: DiscoveryConfig, + pub membership: MembershipConfig, + pub connection: ConnectionConfig, } /// Internal requests to enqueue to the [`Service`] @@ -96,13 +98,13 @@ impl Client { } /// Add a subnet supported by this node. - pub fn add_provided_subnets(&self, subnet_id: SubnetID) -> anyhow::Result<()> { + pub fn add_provided_subnet(&self, subnet_id: SubnetID) -> anyhow::Result<()> { let req = Request::AddProvidedSubnet(subnet_id); self.send_request(req) } /// Remove a subnet no longer supported by this node. - pub fn remove_provided_subnets(&self, subnet_id: SubnetID) -> anyhow::Result<()> { + pub fn remove_provided_subnet(&self, subnet_id: SubnetID) -> anyhow::Result<()> { let req = Request::RemoveProvidedSubnet(subnet_id); self.send_request(req) } @@ -134,6 +136,7 @@ impl Client { /// The `Service` handles P2P communication to resolve IPLD content by wrapping and driving a number of `libp2p` behaviours. pub struct Service { + peer_id: PeerId, listen_addr: Multiaddr, swarm: Swarm>, queries: QueryMap, @@ -142,12 +145,28 @@ pub struct Service { } impl Service

{ + /// Build a [`Service`] and a [`Client`] with the default `tokio` transport. pub fn new(config: Config, store: S) -> Result<(Self, Client), ConfigError> where S: BitswapStore, + { + Self::new_with_transport(config, store, build_transport) + } + + /// Build a [`Service`] and a [`Client`] by passing in a transport factory function. + /// + /// The main goal is to be facilitate testing with a [`MemoryTransport`]. + pub fn new_with_transport( + config: Config, + store: S, + transport: F, + ) -> Result<(Self, Client), ConfigError> + where + S: BitswapStore, + F: FnOnce(Keypair) -> Boxed<(PeerId, StreamMuxerBox)>, { let peer_id = config.network.local_peer_id(); - let transport = build_transport(config.network.local_key.clone()); + let transport = transport(config.network.local_key.clone()); let behaviour = Behaviour::new(config.network, config.discovery, config.membership, store)?; // NOTE: Hardcoded values from Forest. Will leave them as is until we know we need to change. @@ -168,6 +187,7 @@ impl Service

{ let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); let service = Self { + peer_id, listen_addr: config.connection.listen_addr, swarm, queries: Default::default(), @@ -231,19 +251,23 @@ impl Service

{ match event.result { Ok(ping::Success::Ping { rtt }) => { trace!( - "PingSuccess::Ping rtt to {} is {} ms", + "PingSuccess::Ping rtt to {} from {} is {} ms", peer_id, + self.peer_id, rtt.as_millis() ); } Ok(ping::Success::Pong) => { - trace!("PingSuccess::Pong from {peer_id}"); + trace!("PingSuccess::Pong from {peer_id} to {}", self.peer_id); } Err(ping::Failure::Timeout) => { - debug!("PingFailure::Timeout from {peer_id}"); + debug!("PingFailure::Timeout from {peer_id} to {}", self.peer_id); } Err(ping::Failure::Other { error }) => { - warn!("PingFailure::Other from {peer_id}: {error}"); + warn!( + "PingFailure::Other from {peer_id} to {}: {error}", + self.peer_id + ); } Err(ping::Failure::Unsupported) => { warn!("Banning peer {peer_id} due to protocol error"); @@ -255,23 +279,36 @@ impl Service

{ fn handle_identify_event(&mut self, event: identify::Event) { if let identify::Event::Error { peer_id, error } = event { warn!("Error identifying {peer_id}: {error}") + } else if let identify::Event::Received { peer_id, info } = event { + debug!("protocols supported by {peer_id}: {:?}", info.protocols); + debug!("adding identified address of {peer_id} to {}", self.peer_id); + self.discovery_mut().add_identified(&peer_id, &info); } } fn handle_discovery_event(&mut self, event: discovery::Event) { match event { - discovery::Event::Added(peer_id, _) => self.membership_mut().set_routable(peer_id), - discovery::Event::Removed(peer_id) => self.membership_mut().set_unroutable(peer_id), - discovery::Event::Connected(_, _) => {} - discovery::Event::Disconnected(_, _) => {} + discovery::Event::Added(peer_id, _) => { + debug!("adding routable peer {peer_id} to {}", self.peer_id); + self.membership_mut().set_routable(peer_id) + } + discovery::Event::Removed(peer_id) => { + debug!("removing unroutable peer {peer_id} from {}", self.peer_id); + self.membership_mut().set_unroutable(peer_id) + } } } fn handle_membership_event(&mut self, event: membership::Event) { match event { membership::Event::Skipped(peer_id) => { + debug!("skipped adding provider {peer_id} to {}", self.peer_id); // Don't repeatedly look up peers we can't add to the routing table. if self.background_lookup_filter.insert(&peer_id) { + debug!( + "triggering background lookup of {peer_id} on {}", + self.peer_id + ); self.discovery_mut().background_lookup(peer_id) } } @@ -328,17 +365,17 @@ impl Service

{ match request { Request::SetProvidedSubnets(ids) => { if let Err(e) = self.membership_mut().set_provided_subnets(ids) { - error!("error setting provided subnets: {e}") + warn!("failed to publish set provided subnets: {e}") } } Request::AddProvidedSubnet(id) => { if let Err(e) = self.membership_mut().add_provided_subnet(id) { - error!("error adding provided subnet: {e}") + warn!("failed to publish added provided subnet: {e}") } } Request::RemoveProvidedSubnet(id) => { if let Err(e) = self.membership_mut().remove_provided_subnet(id) { - error!("error removing provided subnet: {e}") + warn!("failed to publish removed provided subnet: {e}") } } Request::PinSubnet(id) => self.membership_mut().pin_subnet(id), diff --git a/ipld/resolver/tests/smoke.rs b/ipld/resolver/tests/smoke.rs new file mode 100644 index 000000000..378b1b98a --- /dev/null +++ b/ipld/resolver/tests/smoke.rs @@ -0,0 +1,250 @@ +// Copyright 2022-2023 Protocol Labs +// SPDX-License-Identifier: MIT +//! Test that a cluster of IPLD resolver can be started in memory, +//! that they bootstrap from each other and are able to resolve CIDs. +//! +//! Run the tests as follows: +//! ```ignore +//! cargo test -p ipc_ipld_resolver --test smoke +//! ``` + +// For inspiration on testing libp2p look at: +// * https://github.com/libp2p/rust-libp2p/blob/v0.50.0/misc/multistream-select/tests/transport.rs +// * https://github.com/libp2p/rust-libp2p/blob/v0.50.0/protocols/ping/tests/ping.rs +// * https://github.com/libp2p/rust-libp2p/blob/v0.50.0/protocols/gossipsub/tests/smoke.rs +// They all use a different combination of `MemoryTransport` and executors. +// These tests attempt to use `MemoryTransport` so it's quicker, with `Swarm::with_tokio_executor` +// so we can leave the polling to the `Service` running in a `Task`, rather than do it from the test +// (although these might be orthogonal). + +use std::time::Duration; + +use anyhow::anyhow; +use fvm_ipld_hamt::Hamt; +use fvm_shared::{address::Address, ActorID}; +use ipc_ipld_resolver::{ + Client, Config, ConnectionConfig, DiscoveryConfig, MembershipConfig, NetworkConfig, Service, +}; +use ipc_sdk::subnet_id::{SubnetID, ROOTNET_ID}; +use libipld::Cid; +use libp2p::{ + core::{ + muxing::StreamMuxerBox, + transport::{Boxed, MemoryTransport}, + }, + identity::Keypair, + mplex, + multiaddr::Protocol, + plaintext::PlainText2Config, + yamux, Multiaddr, PeerId, Transport, +}; +use rand::{rngs::StdRng, Rng, SeedableRng}; + +mod store; +use store::*; + +struct Agent { + config: Config, + client: Client, + store: TestBlockstore, +} + +struct Cluster { + agents: Vec, +} + +struct ClusterBuilder { + size: u32, + rng: StdRng, + services: Vec>, + agents: Vec, +} + +impl ClusterBuilder { + fn new(size: u32, seed: u64) -> Self { + Self { + size, + rng: rand::rngs::StdRng::seed_from_u64(seed), + services: Default::default(), + agents: Default::default(), + } + } + + /// Add a node with randomized address, optionally bootstrapping from an existing node. + fn add_node(&mut self, bootstrap: Option) { + let bootstrap_addr = bootstrap.map(|i| { + let config = &self.agents[i].config; + let peer_id = config.network.local_peer_id(); + let mut addr = config.connection.listen_addr.clone(); + addr.push(Protocol::P2p(peer_id.into())); + addr + }); + let config = make_config(&mut self.rng, self.size, bootstrap_addr); + let (service, client, store) = make_service(config.clone()); + self.services.push(service); + self.agents.push(Agent { + config, + client, + store, + }); + } + + /// Start running all services + fn run(self) -> Cluster { + for service in self.services { + tokio::task::spawn(async move { service.run().await.expect("error running service") }); + } + Cluster { + agents: self.agents, + } + } +} + +/// Start a cluster of agents from a single bootstrap node, +/// make available some content on one agent and resolve it from another. +#[tokio::test] +async fn single_bootstrap_single_provider_resolve_one() { + let _ = env_logger::builder().is_test(true).try_init(); + //env_logger::init(); + + // Choose agents. + let cluster_size = 3; + let bootstrap_idx = 0; + let provider_idx = 1; + let resolver_idx = 2; + + // TODO: Get the seed from QuickCheck + let mut builder = ClusterBuilder::new(cluster_size, 123456u64); + + // Build a cluster of nodes. + for i in 0..builder.size { + builder.add_node(if i == 0 { None } else { Some(bootstrap_idx) }); + } + + // Start the swarms. + let mut cluster = builder.run(); + + // Insert a CID of a complex recursive data structure. + let cid = insert_test_data(&mut cluster.agents[provider_idx]).expect("failed to insert data"); + + // Sanity check that we can read the data back. + check_test_data(&mut cluster.agents[provider_idx], &cid).expect("failed to read back the data"); + + // Wait a little for the cluster to connect. + // TODO: Wait on some condition instead of sleep. + tokio::time::sleep(Duration::from_secs(1)).await; + + // Announce the support of some subnet. + let subnet_id = make_subnet_id(1001); + + cluster.agents[provider_idx] + .client + .add_provided_subnet(subnet_id.clone()) + .expect("failed to add provided subnet"); + + // Wait a little for the gossip to spread and peer lookups to happen, then another round of gossip. + // TODO: Wait on some condition instead of sleep. + tokio::time::sleep(Duration::from_secs(2)).await; + + // Ask for the CID to be resolved from by another peer. + cluster.agents[resolver_idx] + .client + .resolve(cid, subnet_id.clone()) + .await + .expect("failed to send request") + .expect("failed to resolve content"); + + // Check that the CID is deposited into the store of the requestor. + check_test_data(&mut cluster.agents[resolver_idx], &cid).expect("failed to resolve from store"); +} + +fn make_service(config: Config) -> (Service, Client, TestBlockstore) { + let store = TestBlockstore::default(); + let (svc, cli) = Service::new_with_transport(config, store.clone(), build_transport).unwrap(); + (svc, cli, store) +} + +fn make_config(rng: &mut StdRng, cluster_size: u32, bootstrap_addr: Option) -> Config { + let config = Config { + connection: ConnectionConfig { + listen_addr: Multiaddr::from(Protocol::Memory(rng.gen::())), + expected_peer_count: cluster_size, + max_incoming: cluster_size, + }, + network: NetworkConfig { + local_key: Keypair::generate_secp256k1(), + network_name: "smoke-test".to_owned(), + }, + discovery: DiscoveryConfig { + static_addresses: bootstrap_addr.iter().cloned().collect(), + target_connections: cluster_size.try_into().unwrap(), + enable_kademlia: true, + }, + membership: MembershipConfig { + static_subnets: vec![], + max_subnets: 10, + publish_interval: Duration::from_secs(1), + max_provider_age: Duration::from_secs(60), + }, + }; + + config +} + +/// Builds an in-memory transport for libp2p to communicate over. +fn build_transport(local_key: Keypair) -> Boxed<(PeerId, StreamMuxerBox)> { + let auth_config = PlainText2Config { + local_public_key: local_key.public(), + }; + + let mplex_config = { + let mut mplex_config = mplex::MplexConfig::new(); + mplex_config.set_max_buffer_size(usize::MAX); + + let mut yamux_config = yamux::YamuxConfig::default(); + yamux_config.set_max_buffer_size(16 * 1024 * 1024); + yamux_config.set_receive_window_size(16 * 1024 * 1024); + // yamux_config.set_window_update_mode(WindowUpdateMode::OnRead); + libp2p::core::upgrade::SelectUpgrade::new(yamux_config, mplex_config) + }; + + MemoryTransport::default() + .upgrade(libp2p::core::upgrade::Version::V1) + .authenticate(auth_config) + .multiplex(mplex_config) + .boxed() +} + +/// Make a subnet under a rootnet. +fn make_subnet_id(actor_id: ActorID) -> SubnetID { + let act = Address::new_id(actor_id); + SubnetID::new_from_parent(&ROOTNET_ID, act) +} + +/// Insert a HAMT into the block store of an agent. +fn insert_test_data(agent: &mut Agent) -> anyhow::Result { + let mut hamt: Hamt<_, String, u32> = Hamt::new(&agent.store); + + // Insert enough data into the HAMT to make sure it grows from a single `Node`. + for i in 0..1000 { + hamt.set(i, format!("value {i}"))?; + } + let cid = hamt.flush()?; + + Ok(cid) +} + +fn check_test_data(agent: &mut Agent, cid: &Cid) -> anyhow::Result<()> { + let hamt: Hamt<_, String, u32> = Hamt::load(cid, &agent.store)?; + + // Check all the data inserted by `insert_test_data`. + for i in 0..1000 { + match hamt.get(&i)? { + None => return Err(anyhow!("key {i} is missing")), + Some(v) if *v != format!("value {i}") => return Err(anyhow!("unexpected value: {v}")), + _ => {} + } + } + + Ok(()) +} diff --git a/ipld/resolver/tests/store/mod.rs b/ipld/resolver/tests/store/mod.rs new file mode 100644 index 000000000..414399b70 --- /dev/null +++ b/ipld/resolver/tests/store/mod.rs @@ -0,0 +1,54 @@ +// Copyright 2022-2023 Protocol Labs +// SPDX-License-Identifier: MIT +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; + +use anyhow::Result; +use fvm_ipld_blockstore::Blockstore; +use ipc_ipld_resolver::missing_blocks::missing_blocks; +use libipld::Cid; +use libp2p_bitswap::BitswapStore; + +#[derive(Debug, Clone, Default)] +pub struct TestBlockstore { + blocks: Arc>>>, +} + +impl Blockstore for TestBlockstore { + fn has(&self, k: &Cid) -> Result { + Ok(self.blocks.read().unwrap().contains_key(k)) + } + + fn get(&self, k: &Cid) -> Result>> { + Ok(self.blocks.read().unwrap().get(k).cloned()) + } + + fn put_keyed(&self, k: &Cid, block: &[u8]) -> Result<()> { + self.blocks.write().unwrap().insert(*k, block.into()); + Ok(()) + } +} + +pub type TestStoreParams = libipld::DefaultParams; + +impl BitswapStore for TestBlockstore { + type Params = TestStoreParams; + + fn contains(&mut self, cid: &Cid) -> Result { + Blockstore::has(self, cid) + } + + fn get(&mut self, cid: &Cid) -> Result>> { + Blockstore::get(self, cid) + } + + fn insert(&mut self, block: &libipld::Block) -> Result<()> { + Blockstore::put_keyed(self, block.cid(), block.data()) + } + + fn missing_blocks(&mut self, cid: &Cid) -> Result> { + missing_blocks::(self, cid) + } +} From a24eaa687acc8eefbc4622285fada6566fbe3568 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 7 Mar 2023 10:12:00 +0000 Subject: [PATCH 22/82] IPC-68: Change peer selection strategy to be of limited size, and shuffled (#73) --- ipld/resolver/Cargo.toml | 2 +- ipld/resolver/src/service.rs | 139 +++++++++++++++++++++-------------- ipld/resolver/tests/smoke.rs | 1 + 3 files changed, 85 insertions(+), 57 deletions(-) diff --git a/ipld/resolver/Cargo.toml b/ipld/resolver/Cargo.toml index 794298fd3..fc9142e98 100644 --- a/ipld/resolver/Cargo.toml +++ b/ipld/resolver/Cargo.toml @@ -35,6 +35,7 @@ libp2p = { version = "0.50", default-features = false, features = [ libp2p-bitswap = "0.25" libipld = { workspace = true } log = { workspace = true } +rand = { workspace = true } serde = { workspace = true } quickcheck = { workspace = true, optional = true } @@ -46,7 +47,6 @@ fvm_ipld_blockstore = { workspace = true, optional = true } [dev-dependencies] quickcheck = { workspace = true } quickcheck_macros = { workspace = true } -rand = { workspace = true } env_logger = { workspace = true } fvm_shared = { workspace = true, features = ["arb"] } fvm_ipld_hamt = "0.6" diff --git a/ipld/resolver/src/service.rs b/ipld/resolver/src/service.rs index dce70d8dc..e36f7f0d3 100644 --- a/ipld/resolver/src/service.rs +++ b/ipld/resolver/src/service.rs @@ -20,6 +20,7 @@ use libp2p::{ use libp2p::{identify, ping}; use libp2p_bitswap::BitswapStore; use log::{debug, error, trace, warn}; +use rand::seq::SliceRandom; use tokio::select; use tokio::sync::oneshot::{self, Sender}; @@ -31,13 +32,16 @@ use crate::behaviour::{ /// Result of attempting to resolve a CID. pub type ResolveResult = anyhow::Result<()>; +/// Channel to complete the results with. +type ResponseChannel = oneshot::Sender; + /// State of a query. The fallback peers can be used /// if the current attempt fails. struct Query { cid: Cid, subnet_id: SubnetID, fallback_peer_ids: Vec, - response_channel: oneshot::Sender, + response_channel: ResponseChannel, } /// Keeps track of where to send query responses to. @@ -57,6 +61,8 @@ pub struct ConnectionConfig { pub max_incoming: u32, /// Expected number of peers, for sizing the Bloom filter. pub expected_peer_count: u32, + /// Maximum number of peers to send Bitswap requests to in a single attempt. + pub max_peers_per_query: u32, } #[derive(Debug, Clone)] @@ -142,6 +148,7 @@ pub struct Service { queries: QueryMap, request_rx: tokio::sync::mpsc::UnboundedReceiver, background_lookup_filter: BloomFilter, + max_peers_per_query: usize, } impl Service

{ @@ -196,6 +203,11 @@ impl Service

{ 0.1, config.connection.expected_peer_count, ), + max_peers_per_query: config + .connection + .max_peers_per_query + .try_into() + .expect("u32 should be usize"), }; let client = Client { request_tx: tx }; @@ -322,7 +334,7 @@ impl Service

{ match event { content::Event::Complete(query_id, result) => { if let Some(query) = self.queries.remove(&query_id) { - self.handle_query_result(query, result); + self.resolve_query(query, result); } else { warn!("query ID not found"); } @@ -330,12 +342,70 @@ impl Service

{ } } + /// Handle an internal request coming from a [`Client`]. + fn handle_request(&mut self, request: Request) { + match request { + Request::SetProvidedSubnets(ids) => { + if let Err(e) = self.membership_mut().set_provided_subnets(ids) { + warn!("failed to publish set provided subnets: {e}") + } + } + Request::AddProvidedSubnet(id) => { + if let Err(e) = self.membership_mut().add_provided_subnet(id) { + warn!("failed to publish added provided subnet: {e}") + } + } + Request::RemoveProvidedSubnet(id) => { + if let Err(e) = self.membership_mut().remove_provided_subnet(id) { + warn!("failed to publish removed provided subnet: {e}") + } + } + Request::PinSubnet(id) => self.membership_mut().pin_subnet(id), + Request::UnpinSubnet(id) => self.membership_mut().unpin_subnet(&id), + + Request::Resolve(cid, subnet_id, response_channel) => { + self.start_query(cid, subnet_id, response_channel) + } + } + } + + /// Start a CID resolution. + fn start_query(&mut self, cid: Cid, subnet_id: SubnetID, response_channel: ResponseChannel) { + let mut peers = self.membership_mut().providers_of_subnet(&subnet_id); + + if peers.is_empty() { + send_resolve_result(response_channel, Err(anyhow!(NoKnownPeers(subnet_id)))); + } else { + // Connect to them in a random order, so as not to overwhelm any specific peer. + peers.shuffle(&mut rand::thread_rng()); + + // Prioritize peers we already have an established connection with. + let (connected, known) = peers + .into_iter() + .partition::, _>(|id| self.swarm.is_connected(id)); + + let peers = [connected, known].into_iter().flatten().collect(); + let (peers, fallback) = self.split_peers_for_query(peers); + + let query = Query { + cid, + subnet_id, + response_channel, + fallback_peer_ids: fallback, + }; + + let query_id = self.content_mut().resolve(cid, peers); + + self.queries.insert(query_id, query); + } + } + /// Handle the results from a resolve attempt. If it succeeded, notify the /// listener. Otherwise if we have fallback peers to try, start another /// query and send the result to them. By default these are the peers /// we know support the subnet, but weren't connected to when the we /// first attempted the resolution. - fn handle_query_result(&mut self, mut query: Query, result: ResolveResult) { + fn resolve_query(&mut self, mut query: Query, result: ResolveResult) { match result { Ok(_) => send_resolve_result(query.response_channel, result), Err(_) if query.fallback_peer_ids.is_empty() => { @@ -350,67 +420,24 @@ impl Service

{ query.fallback_peer_ids.len() ); - // Now we can go all in; alternatively we could take the next N peers. + // Try to resolve from the next batch of peers. let peers = std::mem::take(&mut query.fallback_peer_ids); - + let (peers, fallback) = self.split_peers_for_query(peers); let query_id = self.content_mut().resolve(query.cid, peers); + // Leave the rest for later. + query.fallback_peer_ids = fallback; + self.queries.insert(query_id, query); } } } - /// Handle an internal request coming from a [`Client`]. - fn handle_request(&mut self, request: Request) { - match request { - Request::SetProvidedSubnets(ids) => { - if let Err(e) = self.membership_mut().set_provided_subnets(ids) { - warn!("failed to publish set provided subnets: {e}") - } - } - Request::AddProvidedSubnet(id) => { - if let Err(e) = self.membership_mut().add_provided_subnet(id) { - warn!("failed to publish added provided subnet: {e}") - } - } - Request::RemoveProvidedSubnet(id) => { - if let Err(e) = self.membership_mut().remove_provided_subnet(id) { - warn!("failed to publish removed provided subnet: {e}") - } - } - Request::PinSubnet(id) => self.membership_mut().pin_subnet(id), - Request::UnpinSubnet(id) => self.membership_mut().unpin_subnet(&id), - - Request::Resolve(cid, subnet_id, response_channel) => { - let peers = self.membership_mut().providers_of_subnet(&subnet_id); - if peers.is_empty() { - send_resolve_result(response_channel, Err(anyhow!(NoKnownPeers(subnet_id)))); - } else { - let (connected, known) = peers - .into_iter() - .partition::, _>(|id| self.swarm.is_connected(id)); - - let (peers, fallback) = if connected.is_empty() { - (known, vec![]) - } else { - // Use just the connected ones, however many there are. - // Alternatively we could take the first N combined. - (connected, known) - }; - - let query = Query { - cid, - subnet_id, - response_channel, - fallback_peer_ids: fallback, - }; - - let query_id = self.content_mut().resolve(cid, peers); - - self.queries.insert(query_id, query); - } - } - } + /// Split peers into a group we query now and a group we fall back on if the current batch fails. + fn split_peers_for_query(&self, mut peers: Vec) -> (Vec, Vec) { + let size = std::cmp::min(self.max_peers_per_query, peers.len()); + let fallback = peers.split_off(size); + (peers, fallback) } // The following are helper functions because Rust Analyzer has trouble with recognising that `swarm.behaviour_mut()` is a legal call. diff --git a/ipld/resolver/tests/smoke.rs b/ipld/resolver/tests/smoke.rs index 378b1b98a..a153b3218 100644 --- a/ipld/resolver/tests/smoke.rs +++ b/ipld/resolver/tests/smoke.rs @@ -170,6 +170,7 @@ fn make_config(rng: &mut StdRng, cluster_size: u32, bootstrap_addr: Option())), expected_peer_count: cluster_size, max_incoming: cluster_size, + max_peers_per_query: cluster_size, }, network: NetworkConfig { local_key: Keypair::generate_secp256k1(), From f7397c252e6d0055d33b569b4c7d49a5d5a20066 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 7 Mar 2023 10:28:43 +0000 Subject: [PATCH 23/82] IPC-70: Eclipse attack protection in Discovery during Bootstrap (#74) --- ipld/resolver/src/behaviour/discovery.rs | 42 ++++++++++++++++++++---- ipld/resolver/src/service.rs | 2 +- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/ipld/resolver/src/behaviour/discovery.rs b/ipld/resolver/src/behaviour/discovery.rs index f3b3385e8..663cafda2 100644 --- a/ipld/resolver/src/behaviour/discovery.rs +++ b/ipld/resolver/src/behaviour/discovery.rs @@ -14,7 +14,7 @@ use libp2p::{ identify::Info, kad::{ handler::KademliaHandlerProto, store::MemoryStore, InboundRequest, Kademlia, - KademliaConfig, KademliaEvent, KademliaStoreInserts, QueryId, + KademliaConfig, KademliaEvent, KademliaStoreInserts, QueryId, QueryResult, }, multiaddr::Protocol, swarm::{ @@ -87,6 +87,8 @@ pub struct Behaviour { target_connections: usize, /// Interval between random lookups. lookup_interval: Interval, + /// Buffer incoming identify requests until we have finished the bootstrap. + bootstrap_buffer: Option>, /// Events to return when polled. outbox: VecDeque, } @@ -113,6 +115,8 @@ impl Behaviour { let mut outbox = VecDeque::new(); let protocol_name = format!("/ipc/{}/kad/1.0.0", nc.network_name); + let mut bootstrap_buffer = None; + let kademlia_opt = if dc.enable_kademlia { let mut kad_config = KademliaConfig::default(); kad_config.set_protocol_names(vec![Cow::Owned(protocol_name.as_bytes().to_vec())]); @@ -134,6 +138,8 @@ impl Behaviour { kademlia .bootstrap() .map_err(|_| ConfigError::NoBootstrapAddress)?; + + bootstrap_buffer = Some(Vec::new()); } Some(kademlia) @@ -154,6 +160,7 @@ impl Behaviour { lookup_interval: tokio::time::interval(Duration::from_secs(1)), outbox, num_connections: 0, + bootstrap_buffer, target_connections: dc.target_connections, }) } @@ -177,10 +184,20 @@ impl Behaviour { /// This seems to be the only way, because Kademlia rightfully treats /// incoming connections as ephemeral addresses, but doesn't have an /// alternative exchange mechanism. - pub fn add_identified(&mut self, peer_id: &PeerId, info: &Info) { + pub fn add_identified(&mut self, peer_id: &PeerId, info: Info) { if info.protocols.contains(&self.protocol_name) { - for addr in info.listen_addrs.iter().cloned() { - self.add_address(peer_id, addr); + // If we are still in the process of bootstrapping peers, buffer the incoming self-identify records, + // to protect against eclipse attacks that could fill the k-table with entries to crowd out honest peers. + if let Some(buffer) = self.bootstrap_buffer.as_mut() { + if buffer.len() < self.target_connections + && !buffer.iter().any(|(id, _)| id == peer_id) + { + buffer.push((*peer_id, info)) + } + } else { + for addr in info.listen_addrs.iter().cloned() { + self.add_address(peer_id, addr); + } } } } @@ -285,8 +302,21 @@ impl NetworkBehaviour for Behaviour { warn!("disallowed Kademlia requests from {source}",) } // Information only. - KademliaEvent::InboundRequest { .. } - | KademliaEvent::OutboundQueryProgressed { .. } => {} + KademliaEvent::InboundRequest { .. } => {} + // Finish bootstrapping. + KademliaEvent::OutboundQueryProgressed { result, step, .. } => match result + { + QueryResult::Bootstrap(result) if step.last => { + debug!("Bootstrapping finished with {result:?}"); + if let Some(buffer) = self.bootstrap_buffer.take() { + debug!("Adding {} self-identified peers.", buffer.len()); + for (peer_id, info) in buffer { + self.add_identified(&peer_id, info) + } + } + } + _ => {} + }, // The config ensures peers are added to the table if there's room. // We're not emitting these as known peers because the address will probably not be returned by `addresses_of_peer`, // so the outside service would have to keep track, which is not what we want. diff --git a/ipld/resolver/src/service.rs b/ipld/resolver/src/service.rs index e36f7f0d3..090efb87c 100644 --- a/ipld/resolver/src/service.rs +++ b/ipld/resolver/src/service.rs @@ -294,7 +294,7 @@ impl Service

{ } else if let identify::Event::Received { peer_id, info } = event { debug!("protocols supported by {peer_id}: {:?}", info.protocols); debug!("adding identified address of {peer_id} to {}", self.peer_id); - self.discovery_mut().add_identified(&peer_id, &info); + self.discovery_mut().add_identified(&peer_id, info); } } From 3a03abfd4384399cc9bea5353a12bbabe4fac79b Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 7 Mar 2023 10:34:27 +0000 Subject: [PATCH 24/82] IPC-67: Gossip to new joiners (#75) * IPC-67: Publishing for new peers on routable and subscribed * IPC-67: Publish when the first record is received * IPC-67: Don't reactively publish empty records. --- ipld/resolver/src/behaviour/membership.rs | 107 ++++++++++++++++++---- ipld/resolver/src/provider_cache.rs | 12 ++- ipld/resolver/tests/smoke.rs | 3 +- 3 files changed, 102 insertions(+), 20 deletions(-) diff --git a/ipld/resolver/src/behaviour/membership.rs b/ipld/resolver/src/behaviour/membership.rs index d09f5df7b..8337ce6ff 100644 --- a/ipld/resolver/src/behaviour/membership.rs +++ b/ipld/resolver/src/behaviour/membership.rs @@ -9,7 +9,7 @@ use libp2p::core::connection::ConnectionId; use libp2p::gossipsub::error::SubscriptionError; use libp2p::gossipsub::{ GossipsubConfigBuilder, GossipsubEvent, GossipsubMessage, IdentTopic, MessageAuthenticity, - MessageId, Topic, + MessageId, Topic, TopicHash, }; use libp2p::identity::Keypair; use libp2p::swarm::derive_prelude::FromSwarm; @@ -21,11 +21,11 @@ use libp2p::{ PeerId, }; use log::{debug, error, warn}; -use tokio::time::Interval; +use tokio::time::{Instant, Interval}; use crate::hash::blake2b_256; use crate::provider_cache::{ProviderDelta, SubnetProviderCache}; -use crate::provider_record::{SignedProviderRecord, Timestamp}; +use crate::provider_record::{ProviderRecord, SignedProviderRecord, Timestamp}; use super::NetworkConfig; @@ -56,6 +56,8 @@ pub struct Config { pub max_subnets: usize, /// Publish interval for supported subnets. pub publish_interval: Duration, + /// Minimum time between publishing own provider record in reaction to new joiners. + pub min_time_between_publish: Duration, /// Maximum age of provider records before the peer is removed without an update. pub max_provider_age: Duration, } @@ -90,6 +92,12 @@ pub struct Behaviour { /// This acts like a heartbeat; if a peer doesn't publish its snapshot for a long time, /// other agents can prune it from their cache and not try to contact for resolution. publish_interval: Interval, + /// Minimum time between publishing own provider record in reaction to new joiners. + min_time_between_publish: Duration, + /// Last time we gossiped our own provider record. + last_publish_timestamp: Timestamp, + /// Next time we will gossip our own provider record. + next_publish_timestamp: Timestamp, /// Maximum time a provider can be without an update before it's pruned from the cache. max_provider_age: Duration, } @@ -141,6 +149,9 @@ impl Behaviour { subnet_ids: Default::default(), provider_cache: SubnetProviderCache::new(mc.max_subnets, mc.static_subnets), publish_interval: interval, + min_time_between_publish: mc.min_time_between_publish, + last_publish_timestamp: Timestamp::default(), + next_publish_timestamp: Timestamp::now() + mc.publish_interval, max_provider_age: mc.max_provider_age, }) } @@ -188,6 +199,9 @@ impl Behaviour { let record = SignedProviderRecord::new(&self.local_key, self.subnet_ids.clone())?; let data = record.into_envelope().into_protobuf_encoding(); let _msg_id = self.inner.publish(self.membership_topic.clone(), data)?; + self.last_publish_timestamp = Timestamp::now(); + self.next_publish_timestamp = self.last_publish_timestamp + self.publish_interval.period(); + self.publish_interval.reset(); // In case the change wasn't tiggered by the schedule. Ok(()) } @@ -195,7 +209,8 @@ impl Behaviour { /// /// Call this method when the discovery service learns the address of a peer. pub fn set_routable(&mut self, peer_id: PeerId) { - self.provider_cache.set_routable(peer_id) + self.provider_cache.set_routable(peer_id); + self.publish_for_new_peer(peer_id); } /// Mark a peer as unroutable in the cache. @@ -217,14 +232,10 @@ impl Behaviour { /// then raise domain event to let the rest of the application know about a /// provider. Also update all the book keeping in the behaviour that we use /// to answer future queries about the topic. - fn handle_message(&mut self, msg: GossipsubMessage) -> Option { + fn handle_message(&mut self, msg: GossipsubMessage) { if msg.topic == self.membership_topic.hash() { match SignedProviderRecord::from_bytes(&msg.data).map(|r| r.into_record()) { - Ok(record) => match self.provider_cache.add_provider(&record) { - None => return Some(Event::Skipped(record.peer_id)), - Some(d) if d.is_empty() => return None, - Some(d) => return Some(Event::Updated(record.peer_id, d)), - }, + Ok(record) => self.handle_provider_record(record), Err(e) => { warn!( "Gossip message from peer {:?} could not be deserialized: {e}", @@ -233,9 +244,72 @@ impl Behaviour { } } } else { - warn!("unknown gossipsub topic: {}", msg.topic); + warn!( + "unknown gossipsub topic in message from {:?}: {}", + msg.source, msg.topic + ); + } + } + + /// Try to add a provider record to the cache. + /// + /// If this is the first time we receive a record from the peer, + /// reciprocate by publishing our own. + fn handle_provider_record(&mut self, record: ProviderRecord) { + let is_new = !self.provider_cache.has_timestamp(&record.peer_id); + let (event, publish) = match self.provider_cache.add_provider(&record) { + None => (Some(Event::Skipped(record.peer_id)), false), + Some(d) if d.is_empty() => (None, false), + Some(d) => (Some(Event::Updated(record.peer_id, d)), is_new), + }; + + if let Some(event) = event { + self.outbox.push_back(event); + } + + if publish { + self.publish_for_new_peer(record.peer_id) + } + } + + /// Handle new subscribers to the membership topic. + fn handle_subscriber(&mut self, peer_id: PeerId, topic: TopicHash) { + if topic == self.membership_topic.hash() { + self.publish_for_new_peer(peer_id) + } else { + warn!( + "unknown gossipsub topic in subscription from {}: {}", + peer_id, topic + ) + } + } + + /// Publish our provider record when we encounter a new peer, unless we have recently done so. + fn publish_for_new_peer(&mut self, peer_id: PeerId) { + if self.subnet_ids.is_empty() { + // We have nothing, so there's no need for them to know this ASAP. + // The reason we shouldn't disable periodic publishing of empty + // records completely is because it would also remove one of + // triggers for non-connected peers to eagerly publish their + // subnets when they see our empty records. Plus they could + // be good to show on metrics, to have a single source of + // the cluster size available on any node. + return; + } + let now = Timestamp::now(); + if self.last_publish_timestamp > now - self.min_time_between_publish { + debug!("recently published, not publishing again for peer {peer_id}"); + } else if self.next_publish_timestamp <= now + self.min_time_between_publish { + debug!("publishing soon for new peer {peer_id}"); // don't let new joiners delay it forever by hitting the next block + } else { + debug!("publishing for new peer {peer_id}"); + // Create a new timer, rather than publish and reset. This way we don't repeat error handling. + // Give some time for Kademlia and Identify to do their bit on both sides. Works better in tests. + let delayed = Instant::now() + self.min_time_between_publish; + self.next_publish_timestamp = now + self.min_time_between_publish; + self.publish_interval = + tokio::time::interval_at(delayed, self.publish_interval.period()) } - None } /// Remove any membership record that hasn't been updated for a long time. @@ -306,16 +380,17 @@ impl NetworkBehaviour for Behaviour { // insignificant. For this reason I oped to use messages instead, and let the content // carry the information, spreading through the Gossipsub network regardless of the // number of connected peers. - GossipsubEvent::Subscribed { .. } | GossipsubEvent::Unsubscribed { .. } => { + GossipsubEvent::Subscribed { peer_id, topic } => { + self.handle_subscriber(peer_id, topic) } + + GossipsubEvent::Unsubscribed { .. } => {} // Log potential misconfiguration. GossipsubEvent::GossipsubNotSupported { peer_id } => { debug!("peer {peer_id} doesn't support gossipsub"); } GossipsubEvent::Message { message, .. } => { - if let Some(ev) = self.handle_message(message) { - return Poll::Ready(NetworkBehaviourAction::GenerateEvent(ev)); - } + self.handle_message(message); } } } diff --git a/ipld/resolver/src/provider_cache.rs b/ipld/resolver/src/provider_cache.rs index 8e61af7c4..9b9e6e383 100644 --- a/ipld/resolver/src/provider_cache.rs +++ b/ipld/resolver/src/provider_cache.rs @@ -20,6 +20,7 @@ impl ProviderDelta { } } +/// Track which subnets are provided for by which set of peers. pub struct SubnetProviderCache { /// Maximum number of subnets to track, to protect against DoS attacks, trying to /// flood someone with subnets that don't actually exist. When the number of subnets @@ -78,8 +79,13 @@ impl SubnetProviderCache { } /// Check if a peer has been marked as routable. - pub fn is_routable(&self, peer_id: PeerId) -> bool { - self.routable_peers.contains(&peer_id) + pub fn is_routable(&self, peer_id: &PeerId) -> bool { + self.routable_peers.contains(peer_id) + } + + /// Check whether we have received recent updates from a peer. + pub fn has_timestamp(&self, peer_id: &PeerId) -> bool { + self.peer_timestamps.contains_key(peer_id) } /// Try to add a provider to the cache. @@ -89,7 +95,7 @@ impl SubnetProviderCache { /// Returns `Some` if the peer is routable, containing the newly added /// and newly removed associations for this peer. pub fn add_provider(&mut self, record: &ProviderRecord) -> Option { - if !self.is_routable(record.peer_id) { + if !self.is_routable(&record.peer_id) { return None; } diff --git a/ipld/resolver/tests/smoke.rs b/ipld/resolver/tests/smoke.rs index a153b3218..b2f88171e 100644 --- a/ipld/resolver/tests/smoke.rs +++ b/ipld/resolver/tests/smoke.rs @@ -184,7 +184,8 @@ fn make_config(rng: &mut StdRng, cluster_size: u32, bootstrap_addr: Option Date: Wed, 8 Mar 2023 13:47:05 +0000 Subject: [PATCH 25/82] IPC-69: IPLD Resolver documentation (#82) * IPC-69: WIP * IPC-69: Checkpoint schema * IPC-69: Checkpoint workflow * IPC-69: Resolution workflow * IPC-69: Automation notes. * IPC-69: CI check-diagrams * IPC-69: Note about missing_blocks * IPC-69: Point back to docs from the source dir * IPC-69: Change the checkpoint schema to have configurations. * IPC-69: Move comments down to reduce width. * IPC-69: Add FK from sig to ckpt. Remove FK from ckpt to cross_msgs * IPC-69: Add notes to checkpoint workflow * IPC-69: Link to the docs from the top. * Update docs/README.md Co-authored-by: adlrocha * IPC-69: Rewording --------- Co-authored-by: adlrocha --- .gitignore | 1 + Makefile | 11 ++- docs/README.md | 89 ++++++++++++++++++ docs/diagrams/Makefile | 16 ++++ docs/diagrams/checkpoint_schema.png | Bin 0 -> 106871 bytes docs/diagrams/checkpoint_schema.puml | 112 +++++++++++++++++++++++ docs/diagrams/checkpoint_submission.png | Bin 0 -> 112693 bytes docs/diagrams/checkpoint_submission.puml | 87 ++++++++++++++++++ docs/diagrams/ipld_resolver.png | Bin 0 -> 156590 bytes docs/diagrams/ipld_resolver.puml | 100 ++++++++++++++++++++ ipld/resolver/README.md | 58 ++++++++++++ 11 files changed, 473 insertions(+), 1 deletion(-) create mode 100644 docs/README.md create mode 100644 docs/diagrams/Makefile create mode 100644 docs/diagrams/checkpoint_schema.png create mode 100644 docs/diagrams/checkpoint_schema.puml create mode 100644 docs/diagrams/checkpoint_submission.png create mode 100644 docs/diagrams/checkpoint_submission.puml create mode 100644 docs/diagrams/ipld_resolver.png create mode 100644 docs/diagrams/ipld_resolver.puml create mode 100644 ipld/resolver/README.md diff --git a/.gitignore b/.gitignore index 4ef6b74f2..568c53bd1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea/ *.iml /target +docs/diagrams/plantuml.jar diff --git a/Makefile b/Makefile index e031659cb..65c7b029b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build test lint license check-fmt check-clippy +.PHONY: all build test lint license check-fmt check-clippy diagrams all: test build @@ -24,3 +24,12 @@ check-fmt: check-clippy: cargo clippy --all --tests -- -D clippy::all + +diagrams: + $(MAKE) -C docs/diagrams + +check-diagrams: diagrams + if git diff --name-only docs/diagrams | grep .png; then \ + echo "There are uncommitted changes to the diagrams"; \ + exit 1; \ + fi diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..20887b0ac --- /dev/null +++ b/docs/README.md @@ -0,0 +1,89 @@ +# IPC Agent + +The IPC Agent is a process faciliting the participation of Filecoin clients like Lotus in the InterPlanetary Consensus (formerly Hierarchical Consensus). + +Please refer to the [IPD Agent Design](https://docs.google.com/document/d/14lkRRv6MQYnuEfp2GoGngdD8Q5YgfE38D8HTZWKgKf4) document for details on the agent. + + +# IPLD Resolver + +The [IPLD Resolver](../ipld/resolver) is a library that IPC Agents can use to exchange data between subnets in IPLD format. + +## Checkpointing + +The most typical use case would be the propagation of checkpoints from child subnets to the parent subnet. + +### Checkpoint Schema + +One possible conceptual model of checkpointing is depicted by the following Entity Relationship diagram: + +![Checkpoint Schema](diagrams/checkpoint_schema.png) + +It shows that the Subnet Actor in the parent subnet governs the power of validators in the child subnet by proposing _Configurations_, which the child subnet is free to adopt in its _Epochs_ when the time is right, communicating back the next adopted config via _Checkpoints_. + +At the end of an epoch, the validators in the child subnet produce a checkpoint over some contents, notably the cross-messages they want to propagate towards the parent subnet. Through the cross-messages, the checkpoint indirectly points to individual messages that users or actors wanted to send. + +Once enough signatures are collected to form a Quorum Certificate over the checkpoint (the specific rules are in the jurisdiction of the Subnet Actor), the checkpoint is submitted to the parent ledger. + +However, the submitted checkpoint does not contain the raw messages, only the meta-data. The content needs to be resolved using the IPC Resolver, as indicated by the dotted line. + +### Checkpoint Submission and Resolution + +The following sequence diagram shows one possible way how checkpoints can be submitted from the child to the parent subnet. + +It depicts two validators: one only participating on the parent subnet, and the other on the child subnet; the latter has to also run at least a full node on the parent subnet. Both validators run one IPC Agent each. + +The diagram shows that at the end of the epoch the child subnet validators produce a Quorum Certificate over the checkpoint, which some of their agents submit to the parent subnet. + +After that, the parent subnet nodes reach out to their associated IPC Agent to resolve the messages referenced by the checkpoint, which the Agent does by communicating with some of its child-subnet peers. + +![Checkpoint Submission](diagrams/checkpoint_submission.png) + +This is just a high level view of what happens during message resolution. In the next section we will delve deeper into the internals of the IPLD Resolver. + + +## IPLD Resolver Sub-components + +The IPLD Resolver uses libp2p to form a Peer-to-Peer network, using the following protocols: +* [Ping](https://github.com/libp2p/rust-libp2p/tree/v0.50.1/protocols/ping) +* [Identify](https://github.com/libp2p/rust-libp2p/tree/v0.50.1/protocols/ping) is used to learn the listening address of the remote peers +* [Kademlia](https://github.com/libp2p/rust-libp2p/tree/v0.50.1/protocols/kad) is used for peer discovery +* [Gossipsub](https://github.com/libp2p/rust-libp2p/tree/v0.50.1/protocols/gossipsub) is used to announce information about subnets the peers provide data for +* [Bitswap](https://github.com/ipfs-rust/libp2p-bitswap) is used to resolve CIDs to content + +See the libp2p [specs](https://github.com/libp2p/specs) and [docs](https://docs.libp2p.io/concepts/fundamentals/protocols/) for details on each protocol, and look [here](https://docs.ipfs.tech/concepts/bitswap/) for Bitswap. + +The Resolver is completely agnostic over what content it can resolve, as long as it's based on CIDs; it's not aware of the checkpointing use case above. + +The interface with the host system is through a host-provided implementation of the [BitswapStore](https://github.com/ipfs-rust/libp2p-bitswap/blob/7dd9cececda3e4a8f6e14c200a4b457159d8db33/src/behaviour.rs#L55) which the library uses to retrieve and store content. Implementors can make use of the [missing_blocks](https://github.com/consensus-shipyard/ipc-agent/blob/main/ipld/resolver/src/missing_blocks.rs) helper method which recursively collects all CIDs from an IPLD `Blockstore`, starting from the root CID we are looking for. + +Internally the protocols are wrapped into behaviours that interpret their events and manage their associated state: +* `Discovery` wraps `Kademlia` +* `Membership` wraps `Gossipsub` +* `Content` wraps `Bitswap` + +The following diagram shows a typical sequence of events within the IPLD Resolver. For brevity, only one peer is shown in detail; it's counterpart is represented as a single boundary. + +![IPLD Resolver](diagrams/ipld_resolver.png) + +# Automation + +The diagrams in this directory can be rendered with `make diagrams`. + +Adding the following script to `.git/hooks/pre-commit` automatically renders and checks in the images when we commit changes to the them. CI should also check that there are no uncommitted changes. + +```bash +#!/usr/bin/env bash + +# If any command fails, exit immediately with that command's exit status +set -eo pipefail + +# Redirect output to stderr. +exec 1>&2 + +if git diff --cached --name-only | grep .puml +then + make diagrams + git add docs/diagrams/*.png +fi +``` diff --git a/docs/diagrams/Makefile b/docs/diagrams/Makefile new file mode 100644 index 000000000..5d83b2f6a --- /dev/null +++ b/docs/diagrams/Makefile @@ -0,0 +1,16 @@ +PUMLS = $(shell find . -type f -name "*.puml") +PNGS = $(PUMLS:.puml=.png) +PUML_VER=1.2023.2 + +.PHONY: all +all: diagrams + +.PHONY: diagrams +diagrams: $(PNGS) + +plantuml.jar: + wget -O $@ https://github.com/plantuml/plantuml/releases/download/v$(PUML_VER)/plantuml-$(PUML_VER).jar --no-check-certificate --quiet + +%.png: plantuml.jar %.puml + @# Using pipelining to preserve file names. + cat $*.puml | java -jar plantuml.jar -pipe > $*.png diff --git a/docs/diagrams/checkpoint_schema.png b/docs/diagrams/checkpoint_schema.png new file mode 100644 index 0000000000000000000000000000000000000000..f7f10b1779d8ffb2759404d00ad1784359debd73 GIT binary patch literal 106871 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%0Dy2pN=iyc`&;Ut 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/scT)SO_h2a`kq1^WRB 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

^X@#KQDJDz{g!pbZgIaE&EE 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 KluEZRlm7+3JKaJ6 literal 0 HcmV?d00001 diff --git a/docs/diagrams/checkpoint_schema.puml b/docs/diagrams/checkpoint_schema.puml new file mode 100644 index 000000000..1232d428e --- /dev/null +++ b/docs/diagrams/checkpoint_schema.puml @@ -0,0 +1,112 @@ + +@startuml Checkpointing Schema + +package "Subnet Actor in parent subnet" #79ADDC { + entity "Validator" as validator { + * public_key <> + -- + * power: delegated stake + } + + entity "Configuration" as config { + * config_number <> + -- + } + + entity "Validator Snapshot" as validator_snapshot { + * config_number <> + * public_key <> + -- + * power: delegated stake in the config + } + + entity "Submitted Checkpoint" as submit_ckpt { + * checkpoint <> + } + note bottom of submit_ckpt + Such that the signatures + form a Quorum in the config. + end note +} + +package "checkpointing" #FFEE93 { + entity "Checkpoint" as ckpt { + * epoch_number <> + -- + * next_config_number <> + * state_hash: CID + ... + } + note bottom of ckpt + Next config indicates who will + sign the next checkpoint. + end note + + entity "Signature" as sig { + * public_key <>: validator public key + * checkpoint <> + -- + * signature + } +} + + +package "child subnet" #FFC09F { + entity "Epoch" as epoch { + * epoch_number <> + -- + * config_number <> + * start_block_height <> + * epoch_length + } + + entity "Cross Messages" as cross_msgs { + * epoch_number <> + -- + * messages <>: CID + } + note bottom of cross_msgs + An AMT containing CIDs + end note + + entity "Messsage" as msg { + * id: CID <> + -- + * from: address <> + * to: address <> + * nonce + * payload + ... + } + + entity "Block" as block { + * hash <>: CID + -- + * height + * messages <>: CID + } +} + +block |o--o{ msg + +validator_snapshot }|--|| config +validator_snapshot }o--|| validator + +epoch }o--|| config +epoch |o--|| block +epoch ||--|| cross_msgs +epoch ||--o| ckpt + +sig |o--|| ckpt +sig }o--|| validator + +ckpt }o--|| config +ckpt ||--o| submit_ckpt + + +cross_msgs |o--o{ msg + +submit_ckpt .. cross_msgs : can be resolved with the IPLD Resolver + + +@enduml diff --git a/docs/diagrams/checkpoint_submission.png b/docs/diagrams/checkpoint_submission.png new file mode 100644 index 0000000000000000000000000000000000000000..299c1735609fd3ac281311e16dea612164620086 GIT binary patch literal 112693 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

m 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�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 pT*jY&!t*S}q parent_lotus: subscribe + +validator --> child_lotus: start +validator --> full_lotus: start +validator --> child_agent: start + +child_agent --> full_lotus: subscribe +child_agent --> child_lotus: subscribe + +validator -> child_agent ++: join subnet +child_agent -> parent_lotus --: join subnet +parent_lotus -> parent_lotus: create block and\nexecute transaction + +== During Epoch == + +loop + parent_lotus --> full_lotus: broadcast block + alt if contains top-down messages + full_lotus --> child_agent: observe finalized top-down message + child_agent -> child_lotus: submit finalized top-down message + child_lotus -> child_agent: is finalized on parent? + note right + Check messages proposed by others. + end note + end + + alt if has power to create block + child_lotus -> child_lotus: create block + end + + child_lotus -> child_lotus: receive block +end + +== End of Epoch == + +child_lotus -> child_lotus: next block producer\ncreates checkpoint +note left + Ledger rules dictate + checkpoint contents. +end note +child_lotus --> child_agent: observe checkpoint + +alt if validator in epoch + child_agent -> child_lotus: submit signature over checkpoint +end + +loop + child_lotus -> child_lotus: create block + note left + Accumulate signatures + in the ledger. + end note + ... wait for quorum of signatures ... +end + +child_lotus --> child_agent: observe quorum +child_agent -> parent_lotus: submit checkpoint with quorum certificate + +parent_lotus -> parent_agent ++: resolve checkpoint CID +parent_agent -> child_agent ++: resolve checkpoint CID +note right +This is where the IPLD Resolver +comes into play. +end note +child_agent -> child_lotus: fetch checkpoint contents +return checkpoint contents +return checkpoint contents + +parent_lotus -> parent_lotus: create block and\nexecute checkpoint +parent_lotus --> full_lotus: broadcast block + +@enduml diff --git a/docs/diagrams/ipld_resolver.png b/docs/diagrams/ipld_resolver.png new file mode 100644 index 0000000000000000000000000000000000000000..cec82894a1b5ab467eb4e5240c25012cf0e2f204 GIT binary patch literal 156590 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 literal 0 HcmV?d00001 diff --git a/docs/diagrams/ipld_resolver.puml b/docs/diagrams/ipld_resolver.puml new file mode 100644 index 000000000..48903712e --- /dev/null +++ b/docs/diagrams/ipld_resolver.puml @@ -0,0 +1,100 @@ +@startuml IPLD Resolver +actor Host +boundary Client +control Service +database SubnetProvidersCache +database BitswapStore +participant "Content (Bitswap)" as Content +participant "Membership (Gossipsub)" as Membership +participant "Discovery (Kademlia)" as Discovery +participant Identify +boundary Remote + + +Host -> Service ++: new +Service -> Identify: create +Service -> Discovery: create +Service -> Membership: create +Service -> Content: create +return (Service, Client) + +Host -> Service ++: run + +== Bootstrapping == + +Membership -> Remote: subscribe to membership topic +Service -> Discovery ++: bootstrap from seed peers +Discovery -> Remote: find neighbours +Remote -> Discovery: peer addresses + +Discovery -> Discovery: add address +Discovery -> Service ++: peer routable +Service -> Membership --: peer routable + +Host -> Client ++: set provided subnets +Client -> Service --++: set provided subnets +Service -> Membership --++: set provided subnets +Membership -> Remote --: publish subnets to membership topic + +Discovery -> Service --: bootstrap finished + +Remote -> Identify: listening address +Identify -> Service ++: listening address +Service -> Discovery ++: add address +Discovery -> Service --: peer routable +Service -> Membership --: peer routable + +== Gossiping == + +loop + alt publish interval tick + Membership -> Remote: publish SignedProviderRecord + Membership -> SubnetProvidersCache: prune expired records + else + Remote -> Membership ++: SignedProviderRecord + alt if peer routable + Membership -> SubnetProvidersCache --: add provider + end + end +end + +== Resolution == + +Host -> Client ++: resolve CID from subnet +Client -> Service ++: resolve CID from subnet +Service -> Membership: get providers of subnet +Service -> Service: prioritize peers, connected first +loop + Service -> Content ++: resolve CID from first N peers + Content -> BitswapStore: get missing blocks of root CID + loop while has missing CID + loop for each peer + Content -> Remote: want-have CID + Remote -> Content: have-block true/false + note left + Gather peers who can be asked. + end note + end + loop until have block or no more peers to try + Content -> Remote: want-block CID + alt block is received + Remote -> Content: block + Content -> BitswapStore: insert block + Content -> BitswapStore: get missing blocks of retrieved CID + end + end + end + Content -> Service --: resolution result + alt if failed to resolve but has fallback peers + Service -> Service: pick next N peers + else + Service -> Client --: resolution result + end +end +Client -> Host --: resolution result + +alt if succeeded + Host -> BitswapStore: retrieve content by CID +end + +@enduml diff --git a/ipld/resolver/README.md b/ipld/resolver/README.md new file mode 100644 index 000000000..52470a849 --- /dev/null +++ b/ipld/resolver/README.md @@ -0,0 +1,58 @@ +# IPLD Resolver + +The IPLD Resolver is a Peer-to-Peer library which can be used to resolve arbitrary CIDs from subnets in InterPlanetary Consensus. + +See the [docs](../../docs/) for a conceptual overview. + +## Usage + +Please have a look at the [smoke test](tests/smoke.rs) for an example of using the library. + +The following snippet demonstrates how one would create a resolver instance and use it: + +```rust +async fn main() { + let config = Config { + connection: ConnectionConfig { + listen_addr: "/ip4/127.0.0.1/tcp/0".parse().unwrap(), + expected_peer_count: 1000, + max_incoming: 25, + max_peers_per_query: 10, + }, + network: NetworkConfig { + local_key: Keypair::generate_secp256k1(), + network_name: "example".to_owned(), + }, + discovery: DiscoveryConfig { + static_addresses: vec!["/ip4/95.217.194.97/tcp/8008/p2p/12D3KooWC1EaEEpghwnPdd89LaPTKEweD1PRLz4aRBkJEA9UiUuS".parse().unwrap()] + target_connections: 50, + enable_kademlia: true, + }, + membership: MembershipConfig { + static_subnets: vec![], + max_subnets: 10, + publish_interval: Duration::from_secs(300), + min_time_between_publish: Duration::from_secs(5), + max_provider_age: Duration::from_secs(60), + }, + }; + + let store = todo!("implement BitswapStore and a Blockstore"); + + let (service, client) = Service::new(config, store.clone()); + + tokio::task::spawn(async move { service.run().await }); + + let cid: Cid = todo!("the CID we want to resolve"); + let subnet_id: SubnetID = todo!("the SubnetID from where the CID can be resolved"); + + match client.resolve(cid, subnet_id).await.unwrap() { + Ok(()) => { + let _content: MyContent = store.get(cid).unwrap(); + } + Err(e) => { + println!("{cid} could not be resolved from {subnet_id}: {e}") + } + } +} +``` From fad4bfe15ac1ce6cecf4ca5c99afce6103aae596 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Fri, 10 Mar 2023 09:53:22 +0000 Subject: [PATCH 26/82] IPC-65: Metrics (#84) * IPC-64: Metrics * IPC-64: Use a macro so we don't forget to register --- Cargo.toml | 2 + ipld/resolver/Cargo.toml | 2 + ipld/resolver/src/behaviour/content.rs | 11 ++- ipld/resolver/src/behaviour/discovery.rs | 5 + ipld/resolver/src/behaviour/membership.rs | 41 ++++++-- ipld/resolver/src/lib.rs | 1 + ipld/resolver/src/provider_cache.rs | 14 ++- ipld/resolver/src/service.rs | 27 +++++- ipld/resolver/src/stats.rs | 110 ++++++++++++++++++++++ 9 files changed, 200 insertions(+), 13 deletions(-) create mode 100644 ipld/resolver/src/stats.rs diff --git a/Cargo.toml b/Cargo.toml index 941737d3c..64a40ab89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,8 +53,10 @@ tempfile = "3.4.0" [workspace.dependencies] anyhow = "1.0" +lazy_static = "1.4" log = "0.4" env_logger = "0.10" +prometheus = "0.13" serde = { version = "1.0", features = ["derive"] } thiserror = "1.0" tokio = { version = "1.16", features = ["full"] } diff --git a/ipld/resolver/Cargo.toml b/ipld/resolver/Cargo.toml index fc9142e98..32628bc52 100644 --- a/ipld/resolver/Cargo.toml +++ b/ipld/resolver/Cargo.toml @@ -14,6 +14,7 @@ blake2b_simd = { workspace = true } bloom = "0.3" thiserror = { workspace = true } tokio = { workspace = true } +lazy_static = { workspace = true } libp2p = { version = "0.50", default-features = false, features = [ "gossipsub", "kad", @@ -35,6 +36,7 @@ libp2p = { version = "0.50", default-features = false, features = [ libp2p-bitswap = "0.25" libipld = { workspace = true } log = { workspace = true } +prometheus = { workspace = true } rand = { workspace = true } serde = { workspace = true } quickcheck = { workspace = true, optional = true } diff --git a/ipld/resolver/src/behaviour/content.rs b/ipld/resolver/src/behaviour/content.rs index 7342f85ef..f57ce7db3 100644 --- a/ipld/resolver/src/behaviour/content.rs +++ b/ipld/resolver/src/behaviour/content.rs @@ -13,6 +13,9 @@ use libp2p::{ Multiaddr, PeerId, }; use libp2p_bitswap::{Bitswap, BitswapConfig, BitswapEvent, BitswapStore}; +use prometheus::Registry; + +use crate::stats; pub type QueryId = libp2p_bitswap::QueryId; @@ -47,10 +50,14 @@ impl Behaviour

{ S: BitswapStore, { let bitswap = Bitswap::new(BitswapConfig::default(), store); - // TODO: `bitswap.register_metrics(prometheus::default_registry())` Self { inner: bitswap } } + /// Register Prometheus metrics. + pub fn register_metrics(&self, registry: &Registry) -> anyhow::Result<()> { + self.inner.register_metrics(registry) + } + /// Recursively resolve a [`Cid`] and all underlying CIDs into blocks. /// /// The [`Bitswap`] behaviour will call the [`BitswapStore`] to ask for @@ -72,6 +79,7 @@ impl Behaviour

{ /// The underlying [`libp2p_request_response::RequestResponse`] behaviour /// will initiate connections to the peers which aren't connected at the moment. pub fn resolve(&mut self, cid: Cid, peers: Vec) -> QueryId { + stats::CONTENT_RESOLVE_RUNNING.inc(); // Not passing any missing items, which will result in a call to `BitswapStore::missing_blocks`. self.inner.sync(cid, peers, [].into_iter()) } @@ -113,6 +121,7 @@ impl NetworkBehaviour for Behaviour

{ NetworkBehaviourAction::GenerateEvent(ev) => match ev { BitswapEvent::Progress(_, _) => {} BitswapEvent::Complete(id, result) => { + stats::CONTENT_RESOLVE_RUNNING.dec(); let out = Event::Complete(id, result); return Poll::Ready(NetworkBehaviourAction::GenerateEvent(out)); } diff --git a/ipld/resolver/src/behaviour/discovery.rs b/ipld/resolver/src/behaviour/discovery.rs index 663cafda2..875154707 100644 --- a/ipld/resolver/src/behaviour/discovery.rs +++ b/ipld/resolver/src/behaviour/discovery.rs @@ -28,6 +28,8 @@ use libp2p::{ use log::{debug, warn}; use tokio::time::Interval; +use crate::stats; + use super::NetworkConfig; // NOTE: The Discovery behaviour is largely based on what exists in Forest. If it ain't broken... @@ -169,6 +171,7 @@ impl Behaviour { pub fn background_lookup(&mut self, peer_id: PeerId) { if self.addresses_of_peer(&peer_id).is_empty() { if let Some(kademlia) = self.inner.as_mut() { + stats::DISCOVERY_BACKGROUND_LOOKUP.inc(); kademlia.get_closest_peers(peer_id); } } @@ -235,11 +238,13 @@ impl NetworkBehaviour for Behaviour { match &event { FromSwarm::ConnectionEstablished(e) => { if e.other_established == 0 { + stats::DISCOVERY_CONNECTED_PEERS.inc(); self.num_connections += 1; } } FromSwarm::ConnectionClosed(e) => { if e.remaining_established == 0 { + stats::DISCOVERY_CONNECTED_PEERS.dec(); self.num_connections -= 1; } } diff --git a/ipld/resolver/src/behaviour/membership.rs b/ipld/resolver/src/behaviour/membership.rs index 8337ce6ff..81a50f9ed 100644 --- a/ipld/resolver/src/behaviour/membership.rs +++ b/ipld/resolver/src/behaviour/membership.rs @@ -4,6 +4,7 @@ use std::collections::VecDeque; use std::task::{Context, Poll}; use std::time::Duration; +use anyhow::anyhow; use ipc_sdk::subnet_id::SubnetID; use libp2p::core::connection::ConnectionId; use libp2p::gossipsub::error::SubscriptionError; @@ -26,6 +27,7 @@ use tokio::time::{Instant, Interval}; use crate::hash::blake2b_256; use crate::provider_cache::{ProviderDelta, SubnetProviderCache}; use crate::provider_record::{ProviderRecord, SignedProviderRecord, Timestamp}; +use crate::stats; use super::NetworkConfig; @@ -198,11 +200,20 @@ impl Behaviour { fn publish_membership(&mut self) -> anyhow::Result<()> { let record = SignedProviderRecord::new(&self.local_key, self.subnet_ids.clone())?; let data = record.into_envelope().into_protobuf_encoding(); - let _msg_id = self.inner.publish(self.membership_topic.clone(), data)?; - self.last_publish_timestamp = Timestamp::now(); - self.next_publish_timestamp = self.last_publish_timestamp + self.publish_interval.period(); - self.publish_interval.reset(); // In case the change wasn't tiggered by the schedule. - Ok(()) + match self.inner.publish(self.membership_topic.clone(), data) { + Err(e) => { + stats::MEMBERSHIP_PUBLISH_FAILURE.inc(); + Err(anyhow!(e)) + } + Ok(_msg_id) => { + stats::MEMBERSHIP_PUBLISH_SUCCESS.inc(); + self.last_publish_timestamp = Timestamp::now(); + self.next_publish_timestamp = + self.last_publish_timestamp + self.publish_interval.period(); + self.publish_interval.reset(); // In case the change wasn't tiggered by the schedule. + Ok(()) + } + } } /// Mark a peer as routable in the cache. @@ -210,6 +221,8 @@ impl Behaviour { /// Call this method when the discovery service learns the address of a peer. pub fn set_routable(&mut self, peer_id: PeerId) { self.provider_cache.set_routable(peer_id); + stats::MEMBERSHIP_ROUTABLE_PEERS + .set(self.provider_cache.num_routable().try_into().unwrap()); self.publish_for_new_peer(peer_id); } @@ -237,6 +250,7 @@ impl Behaviour { match SignedProviderRecord::from_bytes(&msg.data).map(|r| r.into_record()) { Ok(record) => self.handle_provider_record(record), Err(e) => { + stats::MEMBERSHIP_INVALID_MESSAGE.inc(); warn!( "Gossip message from peer {:?} could not be deserialized: {e}", msg.source @@ -244,6 +258,7 @@ impl Behaviour { } } } else { + stats::MEMBERSHIP_UNKNOWN_TOPIC.inc(); warn!( "unknown gossipsub topic in message from {:?}: {}", msg.source, msg.topic @@ -256,11 +271,16 @@ impl Behaviour { /// If this is the first time we receive a record from the peer, /// reciprocate by publishing our own. fn handle_provider_record(&mut self, record: ProviderRecord) { - let is_new = !self.provider_cache.has_timestamp(&record.peer_id); let (event, publish) = match self.provider_cache.add_provider(&record) { - None => (Some(Event::Skipped(record.peer_id)), false), - Some(d) if d.is_empty() => (None, false), - Some(d) => (Some(Event::Updated(record.peer_id, d)), is_new), + None => { + stats::MEMBERSHIP_SKIPPED_PEERS.inc(); + (Some(Event::Skipped(record.peer_id)), false) + } + Some(d) if d.is_empty() && !d.is_new => (None, false), + Some(d) => { + let publish = d.is_new; + (Some(Event::Updated(record.peer_id, d)), publish) + } }; if let Some(event) = event { @@ -268,6 +288,7 @@ impl Behaviour { } if publish { + stats::MEMBERSHIP_PROVIDER_PEERS.inc(); self.publish_for_new_peer(record.peer_id) } } @@ -277,6 +298,7 @@ impl Behaviour { if topic == self.membership_topic.hash() { self.publish_for_new_peer(peer_id) } else { + stats::MEMBERSHIP_UNKNOWN_TOPIC.inc(); warn!( "unknown gossipsub topic in subscription from {}: {}", peer_id, topic @@ -317,6 +339,7 @@ impl Behaviour { let cutoff_timestamp = Timestamp::now() - self.max_provider_age; let pruned = self.provider_cache.prune_providers(cutoff_timestamp); for peer_id in pruned { + stats::MEMBERSHIP_PROVIDER_PEERS.dec(); self.outbox.push_back(Event::Removed(peer_id)) } } diff --git a/ipld/resolver/src/lib.rs b/ipld/resolver/src/lib.rs index 5710414dc..98fee9c96 100644 --- a/ipld/resolver/src/lib.rs +++ b/ipld/resolver/src/lib.rs @@ -5,6 +5,7 @@ mod hash; mod provider_cache; mod provider_record; mod service; +mod stats; #[cfg(any(test, feature = "arb"))] mod arb; diff --git a/ipld/resolver/src/provider_cache.rs b/ipld/resolver/src/provider_cache.rs index 9b9e6e383..9f8941aeb 100644 --- a/ipld/resolver/src/provider_cache.rs +++ b/ipld/resolver/src/provider_cache.rs @@ -8,8 +8,9 @@ use libp2p::PeerId; use crate::provider_record::{ProviderRecord, Timestamp}; /// Change in the supported subnets of a peer. -#[derive(Debug, Default)] +#[derive(Debug)] pub struct ProviderDelta { + pub is_new: bool, pub added: Vec, pub removed: Vec, } @@ -78,6 +79,11 @@ impl SubnetProviderCache { } } + /// Number of routable peers. + pub fn num_routable(&mut self) -> usize { + self.routable_peers.len() + } + /// Check if a peer has been marked as routable. pub fn is_routable(&self, peer_id: &PeerId) -> bool { self.routable_peers.contains(peer_id) @@ -99,7 +105,11 @@ impl SubnetProviderCache { return None; } - let mut delta = ProviderDelta::default(); + let mut delta = ProviderDelta { + is_new: !self.has_timestamp(&record.peer_id), + added: Vec::new(), + removed: Vec::new(), + }; let timestamp = self.peer_timestamps.entry(record.peer_id).or_default(); diff --git a/ipld/resolver/src/service.rs b/ipld/resolver/src/service.rs index 090efb87c..f8cdfa72f 100644 --- a/ipld/resolver/src/service.rs +++ b/ipld/resolver/src/service.rs @@ -20,6 +20,7 @@ use libp2p::{ use libp2p::{identify, ping}; use libp2p_bitswap::BitswapStore; use log::{debug, error, trace, warn}; +use prometheus::Registry; use rand::seq::SliceRandom; use tokio::select; use tokio::sync::oneshot::{self, Sender}; @@ -28,6 +29,7 @@ use crate::behaviour::{ self, content, discovery, membership, Behaviour, BehaviourEvent, ConfigError, DiscoveryConfig, MembershipConfig, NetworkConfig, }; +use crate::stats; /// Result of attempting to resolve a CID. pub type ResolveResult = anyhow::Result<()>; @@ -215,6 +217,13 @@ impl Service

{ Ok((service, client)) } + /// Register Prometheus metrics. + pub fn register_metrics(&mut self, registry: &Registry) -> anyhow::Result<()> { + self.content_mut().register_metrics(registry)?; + stats::register_metrics(registry)?; + Ok(()) + } + /// Start the swarm listening for incoming connections and drive the events forward. pub async fn run(mut self) -> anyhow::Result<()> { // Start the swarm. @@ -262,6 +271,8 @@ impl Service

{ let peer_id = event.peer.to_base58(); match event.result { Ok(ping::Success::Ping { rtt }) => { + stats::PING_SUCCESS.inc(); + stats::PING_RTT.observe(rtt.as_millis() as f64); trace!( "PingSuccess::Ping rtt to {} from {} is {} ms", peer_id, @@ -273,9 +284,11 @@ impl Service

{ trace!("PingSuccess::Pong from {peer_id} to {}", self.peer_id); } Err(ping::Failure::Timeout) => { + stats::PING_TIMEOUT.inc(); debug!("PingFailure::Timeout from {peer_id} to {}", self.peer_id); } Err(ping::Failure::Other { error }) => { + stats::PING_FAILURE.inc(); warn!( "PingFailure::Other from {peer_id} to {}: {error}", self.peer_id @@ -290,8 +303,10 @@ impl Service

{ fn handle_identify_event(&mut self, event: identify::Event) { if let identify::Event::Error { peer_id, error } = event { + stats::IDENTIFY_FAILURE.inc(); warn!("Error identifying {peer_id}: {error}") } else if let identify::Event::Received { peer_id, info } = event { + stats::IDENTIFY_RECEIVED.inc(); debug!("protocols supported by {peer_id}: {:?}", info.protocols); debug!("adding identified address of {peer_id} to {}", self.peer_id); self.discovery_mut().add_identified(&peer_id, info); @@ -373,7 +388,10 @@ impl Service

{ fn start_query(&mut self, cid: Cid, subnet_id: SubnetID, response_channel: ResponseChannel) { let mut peers = self.membership_mut().providers_of_subnet(&subnet_id); + stats::CONTENT_RESOLVE_PEERS.observe(peers.len() as f64); + if peers.is_empty() { + stats::CONTENT_RESOLVE_NO_PEERS.inc(); send_resolve_result(response_channel, Err(anyhow!(NoKnownPeers(subnet_id)))); } else { // Connect to them in a random order, so as not to overwhelm any specific peer. @@ -384,6 +402,8 @@ impl Service

{ .into_iter() .partition::, _>(|id| self.swarm.is_connected(id)); + stats::CONTENT_CONNECTED_PEERS.observe(connected.len() as f64); + let peers = [connected, known].into_iter().flatten().collect(); let (peers, fallback) = self.split_peers_for_query(peers); @@ -407,11 +427,16 @@ impl Service

{ /// first attempted the resolution. fn resolve_query(&mut self, mut query: Query, result: ResolveResult) { match result { - Ok(_) => send_resolve_result(query.response_channel, result), + Ok(_) => { + stats::CONTENT_RESOLVE_SUCCESS.inc(); + send_resolve_result(query.response_channel, result) + } Err(_) if query.fallback_peer_ids.is_empty() => { + stats::CONTENT_RESOLVE_FAILURE.inc(); send_resolve_result(query.response_channel, result) } Err(e) => { + stats::CONTENT_RESOLVE_FALLBACK.inc(); debug!( "resolving {} from {} failed with {}, but there are {} fallback peers to try", query.cid, diff --git a/ipld/resolver/src/stats.rs b/ipld/resolver/src/stats.rs new file mode 100644 index 000000000..62553ba73 --- /dev/null +++ b/ipld/resolver/src/stats.rs @@ -0,0 +1,110 @@ +// Copyright 2022-2023 Protocol Labs +// SPDX-License-Identifier: MIT +use lazy_static::lazy_static; +use prometheus::{Histogram, HistogramOpts, IntCounter, IntGauge, Registry}; + +macro_rules! metrics { + ($($name:ident : $type:ty = $make:expr);* $(;)?) => { + $( + lazy_static! { + pub static ref $name: $type = $make.unwrap(); + } + )* + + pub fn register_metrics(registry: &Registry) -> anyhow::Result<()> { + $(registry.register(Box::new($name.clone()))?;)* + Ok(()) + } + }; +} + +metrics! { + PING_RTT: Histogram = + Histogram::with_opts(HistogramOpts::new("ping_rtt", "Ping roundtrip time")); + + PING_TIMEOUT: IntCounter = + IntCounter::new("ping_timeouts", "Number of timed out pings"); + + PING_FAILURE: IntCounter = + IntCounter::new("ping_failure", "Number of failed pings"); + + PING_SUCCESS: IntCounter = + IntCounter::new("ping_success", "Number of successful pings",); + + IDENTIFY_FAILURE: IntCounter = + IntCounter::new("identify_failure", "Number of Identify errors",); + + IDENTIFY_RECEIVED: IntCounter = + IntCounter::new("identify_received", "Number of Identify infos received",); + + DISCOVERY_BACKGROUND_LOOKUP: IntCounter = IntCounter::new( + "discovery_background_lookup", + "Number of background lookups started", + ); + + DISCOVERY_CONNECTED_PEERS: IntGauge = + IntGauge::new("discovery_connected_peers", "Number of connections",); + + MEMBERSHIP_SKIPPED_PEERS: IntCounter = + IntCounter::new("membership_skipped_peers", "Number of providers skipped",); + + MEMBERSHIP_ROUTABLE_PEERS: IntGauge = + IntGauge::new("membership_routable_peers", "Number of routable peers"); + + MEMBERSHIP_PROVIDER_PEERS: IntGauge = + IntGauge::new("membership_provider_peers", "Number of unique providers"); + + MEMBERSHIP_UNKNOWN_TOPIC: IntCounter = IntCounter::new( + "membership_unknown_topic", + "Number of messages with unknown topic" + ); + + MEMBERSHIP_INVALID_MESSAGE: IntCounter = IntCounter::new( + "membership_invalid_message", + "Number of invalid messages received" + ); + + MEMBERSHIP_PUBLISH_SUCCESS: IntCounter = IntCounter::new( + "membership_publish_total", "Number of published messages" + ); + + MEMBERSHIP_PUBLISH_FAILURE: IntCounter = IntCounter::new( + "membership_publish_failure", + "Number of failed publish attempts" + ); + + CONTENT_RESOLVE_RUNNING: IntGauge = IntGauge::new( + "content_resolve_running", + "Number of currently running content resolutions" + ); + + CONTENT_RESOLVE_NO_PEERS: IntCounter = IntCounter::new( + "content_resolve_no_peers", + "Number of resolutions with no known peers" + ); + + CONTENT_RESOLVE_SUCCESS: IntCounter = IntCounter::new( + "content_resolve_success", + "Number of successful resolutions" + ); + + CONTENT_RESOLVE_FAILURE: IntCounter = IntCounter::new( + "content_resolve_success", + "Number of failed resolutions" + ); + + CONTENT_RESOLVE_FALLBACK: IntCounter = IntCounter::new( + "content_resolve_fallback", + "Number of resolutions that fall back on secondary peers" + ); + + CONTENT_RESOLVE_PEERS: Histogram = Histogram::with_opts(HistogramOpts::new( + "content_resolve_peers", + "Number of peers found for resolution from a subnet" + )); + + CONTENT_CONNECTED_PEERS: Histogram = Histogram::with_opts(HistogramOpts::new( + "content_connected_peers", + "Number of connected peers in a resolution" + )); +} From 6b43a896fcff32be80a7cbcc55ceb2331645c548 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Fri, 10 Mar 2023 09:55:25 +0000 Subject: [PATCH 27/82] IPC-86: Rate limit (#90) * IPC-86: RateLimiter type * IPC-86: Check rate limit on incoming bitswap requests. * IPC-86: Update rate limit after responding to Bitswap * IPC-86: Update rate limit from the Client * IPC-86: Add header * IPC-86: Clippy fixes * IPC-86: Select the non-ephemeral parts of the incoming address --- ipld/resolver/Cargo.toml | 12 +- ipld/resolver/src/behaviour/content.rs | 212 ++++++++++++++++++++++++- ipld/resolver/src/behaviour/mod.rs | 4 +- ipld/resolver/src/lib.rs | 3 +- ipld/resolver/src/limiter.rs | 88 ++++++++++ ipld/resolver/src/service.rs | 53 ++++++- ipld/resolver/src/stats.rs | 5 + ipld/resolver/tests/smoke.rs | 7 +- 8 files changed, 363 insertions(+), 21 deletions(-) create mode 100644 ipld/resolver/src/limiter.rs diff --git a/ipld/resolver/Cargo.toml b/ipld/resolver/Cargo.toml index 32628bc52..070a16dc0 100644 --- a/ipld/resolver/Cargo.toml +++ b/ipld/resolver/Cargo.toml @@ -12,8 +12,7 @@ license-file.workspace = true anyhow = { workspace = true } blake2b_simd = { workspace = true } bloom = "0.3" -thiserror = { workspace = true } -tokio = { workspace = true } +gcra = "0.3" lazy_static = { workspace = true } libp2p = { version = "0.50", default-features = false, features = [ "gossipsub", @@ -33,19 +32,24 @@ libp2p = { version = "0.50", default-features = false, features = [ "secp256k1", "plaintext", ] } -libp2p-bitswap = "0.25" libipld = { workspace = true } log = { workspace = true } prometheus = { workspace = true } +quickcheck = { workspace = true, optional = true } rand = { workspace = true } serde = { workspace = true } -quickcheck = { workspace = true, optional = true } +thiserror = { workspace = true } +tokio = { workspace = true } ipc-sdk = { workspace = true } fvm_ipld_encoding = { workspace = true } fvm_shared = { workspace = true, optional = true } fvm_ipld_blockstore = { workspace = true, optional = true } +# Using a fork of libp2p-bitswap so that we can do rate limiting. +#libp2p-bitswap = "0.25" +libp2p-bitswap = { git = "https://github.com/consensus-shipyard/libp2p-bitswap", branch = "req-res-pub" } + [dev-dependencies] quickcheck = { workspace = true } quickcheck_macros = { workspace = true } diff --git a/ipld/resolver/src/behaviour/content.rs b/ipld/resolver/src/behaviour/content.rs index f57ce7db3..7e723d8c5 100644 --- a/ipld/resolver/src/behaviour/content.rs +++ b/ipld/resolver/src/behaviour/content.rs @@ -1,10 +1,18 @@ // Copyright 2022-2023 Protocol Labs // SPDX-License-Identifier: MIT -use std::task::{Context, Poll}; +use std::{ + collections::{HashMap, VecDeque}, + task::{Context, Poll}, + time::Duration, +}; use libipld::{store::StoreParams, Cid}; use libp2p::{ + core::ConnectedPoint, + futures::channel::oneshot, + multiaddr::Protocol, + request_response::handler::RequestResponseHandlerEvent, swarm::{ derive_prelude::{ConnectionId, FromSwarm}, ConnectionHandler, IntoConnectionHandler, NetworkBehaviour, NetworkBehaviourAction, @@ -12,10 +20,14 @@ use libp2p::{ }, Multiaddr, PeerId, }; -use libp2p_bitswap::{Bitswap, BitswapConfig, BitswapEvent, BitswapStore}; +use libp2p_bitswap::{Bitswap, BitswapConfig, BitswapEvent, BitswapResponse, BitswapStore}; +use log::warn; use prometheus::Registry; -use crate::stats; +use crate::{ + limiter::{RateLimit, RateLimiter}, + stats, +}; pub type QueryId = libp2p_bitswap::QueryId; @@ -37,20 +49,59 @@ pub enum Event { /// caller can use the [`missing_blocks`] function to check /// whether a retry is necessary. Complete(QueryId, anyhow::Result<()>), + + /// Event raised when we want to execute some logic with the `BitswapResponse`. + /// This is only raised if we are tracking rate limits. The service has to + /// do the forwarding between the two oneshot channels, and call this module + /// back between doing so. + BitswapForward { + peer_id: PeerId, + /// Receive response from the [`Bitswap`] behaviour. + /// Normally this goes straight to the handler. + response_rx: oneshot::Receiver, + /// Forward the response to the handler. + response_tx: oneshot::Sender, + }, +} + +/// Configuration for [`content::Behaviour`]. +#[derive(Debug, Clone)] +pub struct Config { + /// Number of bytes that can be consumed remote peers in a time period. + /// + /// 0 means no limit. + pub rate_limit_bytes: u32, + /// Length of the time period at which the consumption limit fills. + /// + /// 0 means no limit. + pub rate_limit_period: Duration, } /// Behaviour built on [`Bitswap`] to resolve IPLD content from [`Cid`] to raw bytes. pub struct Behaviour { inner: Bitswap

, + /// Remember which address peers connected from, so we can apply the rate limit + /// on the address, and not on the peer ID which they can change easily. + peer_addresses: HashMap, + /// Limit the amount of data served by remote address. + rate_limiter: RateLimiter, + rate_limit: RateLimit, + outbox: VecDeque, } impl Behaviour

{ - pub fn new(store: S) -> Self + pub fn new(config: Config, store: S) -> Self where S: BitswapStore, { let bitswap = Bitswap::new(BitswapConfig::default(), store); - Self { inner: bitswap } + Self { + inner: bitswap, + peer_addresses: Default::default(), + rate_limiter: RateLimiter::new(config.rate_limit_period), + rate_limit: RateLimit::new(config.rate_limit_bytes, config.rate_limit_period), + outbox: Default::default(), + } } /// Register Prometheus metrics. @@ -83,6 +134,41 @@ impl Behaviour

{ // Not passing any missing items, which will result in a call to `BitswapStore::missing_blocks`. self.inner.sync(cid, peers, [].into_iter()) } + + /// Check if we are using rate limiting. + fn has_rate_limits(&self) -> bool { + !(self.rate_limit.resource_limit == 0 || self.rate_limit.period.is_zero()) + } + + /// Check whether the peer has already exhaused their rate limit. + fn check_rate_limit(&mut self, peer_id: &PeerId, cid: &Cid) -> bool { + if !self.has_rate_limits() { + return true; + } + if let Some(addr) = self.peer_addresses.get(peer_id).cloned() { + let bytes = cid.to_bytes().len().try_into().unwrap_or(u32::MAX); + + if !self.rate_limiter.add(&self.rate_limit, addr, bytes) { + return false; + } + } + true + } + + /// Callback by the service after [`Event::BitswapForward`]. + pub fn rate_limit_used(&mut self, peer_id: PeerId, bytes: usize) { + if self.has_rate_limits() { + if let Some(addr) = self.peer_addresses.get(&peer_id).cloned() { + let bytes = bytes.try_into().unwrap_or(u32::MAX); + let _ = self.rate_limiter.add(&self.rate_limit, addr, bytes); + } + } + } + + /// Update the rate limit to a new value, keeping the period as-is. + pub fn update_rate_limit(&mut self, bytes: u32) { + self.rate_limit = RateLimit::new(bytes, self.rate_limit.period) + } } impl NetworkBehaviour for Behaviour

{ @@ -98,6 +184,33 @@ impl NetworkBehaviour for Behaviour

{ } fn on_swarm_event(&mut self, event: FromSwarm) { + // Store the remote address. + match &event { + FromSwarm::ConnectionEstablished(c) => { + if c.other_established == 0 { + let peer_addr = match c.endpoint { + ConnectedPoint::Dialer { + address: listen_addr, + .. + } => listen_addr.clone(), + ConnectedPoint::Listener { + send_back_addr: ephemeral_addr, + .. + } => select_non_ephemeral(ephemeral_addr.clone()), + }; + self.peer_addresses.insert(c.peer_id, peer_addr); + } + } + FromSwarm::ConnectionClosed(c) => { + if c.remaining_established == 0 { + self.peer_addresses.remove(&c.peer_id); + } + } + // Note: Ignoring FromSwarm::AddressChange - as long as the same peer connects, + // not updating the address provides continuity of resource consumption. + _ => {} + } + self.inner.on_swarm_event(event) } @@ -107,8 +220,39 @@ impl NetworkBehaviour for Behaviour

{ connection_id: ConnectionId, event: <::Handler as ConnectionHandler>::OutEvent, ) { - self.inner - .on_connection_handler_event(peer_id, connection_id, event) + match event { + RequestResponseHandlerEvent::Request { + request_id, + request, + sender, + } if self.has_rate_limits() => { + if !self.check_rate_limit(&peer_id, &request.cid) { + warn!("rate limiting {peer_id}"); + stats::CONTENT_RATE_LIMITED.inc(); + return; + } + // We need to hijack the response channel to record the size, otherwise it goes straight to the handler. + let (tx, rx) = libp2p::futures::channel::oneshot::channel(); + let event = RequestResponseHandlerEvent::Request { + request_id, + request, + sender: tx, + }; + + self.inner + .on_connection_handler_event(peer_id, connection_id, event); + + let forward = Event::BitswapForward { + peer_id, + response_rx: rx, + response_tx: sender, + }; + self.outbox.push_back(forward); + } + _ => self + .inner + .on_connection_handler_event(peer_id, connection_id, event), + } } fn poll( @@ -116,6 +260,11 @@ impl NetworkBehaviour for Behaviour

{ cx: &mut Context<'_>, params: &mut impl PollParameters, ) -> Poll> { + // Emit own events first. + if let Some(ev) = self.outbox.pop_front() { + return Poll::Ready(NetworkBehaviourAction::GenerateEvent(ev)); + } + // Poll Bitswap. while let Poll::Ready(ev) = self.inner.poll(cx, params) { match ev { NetworkBehaviourAction::GenerateEvent(ev) => match ev { @@ -135,3 +284,52 @@ impl NetworkBehaviour for Behaviour

{ Poll::Pending } } + +/// Get rid of parts of an address which are considered ephemeral, +/// keeping just the parts which would stay the same if for example +/// the same peer opened another connection from a different random port. +fn select_non_ephemeral(mut addr: Multiaddr) -> Multiaddr { + let mut keep = Vec::new(); + while let Some(proto) = addr.pop() { + match proto { + // Some are valid on their own right. + Protocol::Ip4(_) | Protocol::Ip6(_) => { + keep.clear(); + keep.push(proto); + break; + } + // Skip P2P peer ID, they might use a different identity. + Protocol::P2p(_) => {} + // Skip ephemeral parts. + Protocol::Tcp(_) | Protocol::Udp(_) => {} + // Everything else we keep until we see better options. + _ => { + keep.push(proto); + } + } + } + keep.reverse(); + Multiaddr::from_iter(keep.into_iter()) +} + +#[cfg(test)] +mod tests { + use libp2p::Multiaddr; + + use super::select_non_ephemeral; + + #[test] + fn non_ephemeral_addr() { + let examples = [ + ("/ip4/127.0.0.1/udt/sctp/5678", "/ip4/127.0.0.1"), + ("/ip4/95.217.194.97/tcp/8008/p2p/12D3KooWC1EaEEpghwnPdd89LaPTKEweD1PRLz4aRBkJEA9UiUuS", "/ip4/95.217.194.97"), + ("/udt/memory/10/p2p/12D3KooWC1EaEEpghwnPdd89LaPTKEweD1PRLz4aRBkJEA9UiUuS", "/udt/memory/10") + ]; + + for (addr, exp) in examples { + let addr: Multiaddr = addr.parse().unwrap(); + let exp: Multiaddr = exp.parse().unwrap(); + assert_eq!(select_non_ephemeral(addr), exp); + } + } +} diff --git a/ipld/resolver/src/behaviour/mod.rs b/ipld/resolver/src/behaviour/mod.rs index 2818dc87c..a94339761 100644 --- a/ipld/resolver/src/behaviour/mod.rs +++ b/ipld/resolver/src/behaviour/mod.rs @@ -14,6 +14,7 @@ pub mod content; pub mod discovery; pub mod membership; +pub use content::Config as ContentConfig; pub use discovery::Config as DiscoveryConfig; pub use membership::Config as MembershipConfig; @@ -67,6 +68,7 @@ impl Behaviour

{ nc: NetworkConfig, dc: DiscoveryConfig, mc: MembershipConfig, + cc: ContentConfig, store: S, ) -> Result where @@ -80,7 +82,7 @@ impl Behaviour

{ )), discovery: discovery::Behaviour::new(nc.clone(), dc)?, membership: membership::Behaviour::new(nc, mc)?, - content: content::Behaviour::new(store), + content: content::Behaviour::new(cc, store), }) } diff --git a/ipld/resolver/src/lib.rs b/ipld/resolver/src/lib.rs index 98fee9c96..d610ede13 100644 --- a/ipld/resolver/src/lib.rs +++ b/ipld/resolver/src/lib.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MIT mod behaviour; mod hash; +mod limiter; mod provider_cache; mod provider_record; mod service; @@ -13,5 +14,5 @@ mod arb; #[cfg(feature = "missing_blocks")] pub mod missing_blocks; -pub use behaviour::{DiscoveryConfig, MembershipConfig, NetworkConfig}; +pub use behaviour::{ContentConfig, DiscoveryConfig, MembershipConfig, NetworkConfig}; pub use service::{Client, Config, ConnectionConfig, NoKnownPeers, Service}; diff --git a/ipld/resolver/src/limiter.rs b/ipld/resolver/src/limiter.rs new file mode 100644 index 000000000..694d5c237 --- /dev/null +++ b/ipld/resolver/src/limiter.rs @@ -0,0 +1,88 @@ +// Copyright 2022-2023 Protocol Labs +// SPDX-License-Identifier: MIT +use std::hash::Hash; +use std::time::{Duration, Instant}; + +use gcra::GcraState; +pub use gcra::RateLimit; +use libp2p::gossipsub::time_cache::TimeCache; + +/// Track the rate limit of resources (e.g. bytes) consumed per key. +/// +/// Forgets keys after long periods of inactivity. +pub struct RateLimiter { + // `TimeCache` uses `Instant::now()` internally. + // It's less testable than `gcra` which allows the time to be passed in, + // but it's only used for cleaning up, so it should be okay. + cache: TimeCache, +} + +impl RateLimiter +where + K: Eq + Hash + Clone, +{ + pub fn new(ttl: Duration) -> Self { + Self { + cache: TimeCache::new(ttl), + } + } + + /// Try to add a certain amount of resources consumed to a key. + /// + /// Return `true` if the key was within limits, `false` if it needs to wait. + /// + /// The [`RateLimit`] is passed in so that we can update it dynamically + /// based on how much data we anticipate we will have to serve. + pub fn add(&mut self, limit: &RateLimit, key: K, cost: u32) -> bool { + self.add_at(limit, key, cost, Instant::now()) + } + + /// Same as [`RateLimiter::add`] but allows passing in the time, for testing. + pub fn add_at(&mut self, limit: &RateLimit, key: K, cost: u32, at: Instant) -> bool { + let state = self.cache.entry(key).or_insert_with(GcraState::default); + + state.check_and_modify_at(limit, at, cost).is_ok() + } +} + +#[cfg(test)] +mod tests { + use std::time::{Duration, Instant}; + + use super::{RateLimit, RateLimiter}; + + #[test] + fn basics() { + // 10Mb per hour. + let one_hour = Duration::from_secs(60 * 60); + let rate_limit = RateLimit::new(10 * 1024 * 1024, one_hour); + let mut rate_limiter = RateLimiter::<&'static str>::new(one_hour); + + assert!(rate_limiter.add(&rate_limit, "foo", 1024)); + assert!(rate_limiter.add(&rate_limit, "foo", 5 * 1024 * 1024)); + assert!( + !rate_limiter.add(&rate_limit, "foo", 5 * 1024 * 1024), + "can't over consume" + ); + assert!( + rate_limiter.add(&rate_limit, "bar", 5 * 1024 * 1024), + "others can consume" + ); + + assert!( + rate_limiter.add_at( + &rate_limit, + "foo", + 5 * 1024 * 1024, + Instant::now() + one_hour + Duration::from_secs(1) + ), + "can consume again in the future" + ); + + let rate_limit = RateLimit::new(50 * 1024 * 1024, one_hour); + assert!( + rate_limiter.add(&rate_limit, "bar", 15 * 1024 * 1024), + "can raise quota" + ); + } +} diff --git a/ipld/resolver/src/service.rs b/ipld/resolver/src/service.rs index f8cdfa72f..e28b667dc 100644 --- a/ipld/resolver/src/service.rs +++ b/ipld/resolver/src/service.rs @@ -18,7 +18,7 @@ use libp2p::{ yamux, Multiaddr, PeerId, Swarm, Transport, }; use libp2p::{identify, ping}; -use libp2p_bitswap::BitswapStore; +use libp2p_bitswap::{BitswapResponse, BitswapStore}; use log::{debug, error, trace, warn}; use prometheus::Registry; use rand::seq::SliceRandom; @@ -26,8 +26,8 @@ use tokio::select; use tokio::sync::oneshot::{self, Sender}; use crate::behaviour::{ - self, content, discovery, membership, Behaviour, BehaviourEvent, ConfigError, DiscoveryConfig, - MembershipConfig, NetworkConfig, + self, content, discovery, membership, Behaviour, BehaviourEvent, ConfigError, ContentConfig, + DiscoveryConfig, MembershipConfig, NetworkConfig, }; use crate::stats; @@ -73,6 +73,7 @@ pub struct Config { pub discovery: DiscoveryConfig, pub membership: MembershipConfig, pub connection: ConnectionConfig, + pub content: ContentConfig, } /// Internal requests to enqueue to the [`Service`] @@ -83,6 +84,8 @@ enum Request { PinSubnet(SubnetID), UnpinSubnet(SubnetID), Resolve(Cid, SubnetID, oneshot::Sender), + RateLimitUsed(PeerId, usize), + UpdateRateLimit(u32), } /// A facade to the [`Service`] to provide a nicer interface than message passing would allow on its own. @@ -140,6 +143,16 @@ impl Client { let res = rx.await?; Ok(res) } + + /// Update the rate limit based on new projections for the same timeframe + /// the `content::Behaviour` was originally configured with. This can be + /// used if we can't come up with a good estimate for the amount of data + /// we have to serve from the subnets we participate in, but we can adjust + /// them on the fly based on what we observe on chain. + pub fn update_rate_limit(&self, bytes: u32) -> anyhow::Result<()> { + let req = Request::UpdateRateLimit(bytes); + self.send_request(req) + } } /// The `Service` handles P2P communication to resolve IPLD content by wrapping and driving a number of `libp2p` behaviours. @@ -149,6 +162,7 @@ pub struct Service { swarm: Swarm>, queries: QueryMap, request_rx: tokio::sync::mpsc::UnboundedReceiver, + request_tx: tokio::sync::mpsc::UnboundedSender, background_lookup_filter: BloomFilter, max_peers_per_query: usize, } @@ -176,7 +190,13 @@ impl Service

{ { let peer_id = config.network.local_peer_id(); let transport = transport(config.network.local_key.clone()); - let behaviour = Behaviour::new(config.network, config.discovery, config.membership, store)?; + let behaviour = Behaviour::new( + config.network, + config.discovery, + config.membership, + config.content, + store, + )?; // NOTE: Hardcoded values from Forest. Will leave them as is until we know we need to change. @@ -201,6 +221,7 @@ impl Service

{ swarm, queries: Default::default(), request_rx: rx, + request_tx: tx.clone(), background_lookup_filter: BloomFilter::with_rate( 0.1, config.connection.expected_peer_count, @@ -244,10 +265,8 @@ impl Service

{ request = self.request_rx.recv() => match request { // A Client sent us a request. Some(req) => self.handle_request(req), + // This shouldn't happen because the service has a copy of the sender. // All Client instances have been dropped. - // We could keep the Swarm alive to serve content to others, - // but we ourselves are unable to send requests. Let's treat - // this as time to quit. None => { break; } } }; @@ -354,6 +373,22 @@ impl Service

{ warn!("query ID not found"); } } + content::Event::BitswapForward { + peer_id, + response_rx, + response_tx, + } => { + let request_tx = self.request_tx.clone(); + tokio::task::spawn(async move { + if let Ok(res) = response_rx.await { + if let BitswapResponse::Block(bz) = &res { + let _ = request_tx.send(Request::RateLimitUsed(peer_id, bz.len())); + } + // Forward, if the listener is still open. + let _ = response_tx.send(res); + } + }); + } } } @@ -381,6 +416,10 @@ impl Service

{ Request::Resolve(cid, subnet_id, response_channel) => { self.start_query(cid, subnet_id, response_channel) } + Request::RateLimitUsed(peer_id, bytes) => { + self.content_mut().rate_limit_used(peer_id, bytes) + } + Request::UpdateRateLimit(bytes) => self.content_mut().update_rate_limit(bytes), } } diff --git a/ipld/resolver/src/stats.rs b/ipld/resolver/src/stats.rs index 62553ba73..0a1493ca6 100644 --- a/ipld/resolver/src/stats.rs +++ b/ipld/resolver/src/stats.rs @@ -107,4 +107,9 @@ metrics! { "content_connected_peers", "Number of connected peers in a resolution" )); + + CONTENT_RATE_LIMITED: IntCounter = IntCounter::new( + "content_rate_limited", + "Number of rate limited requests" + ); } diff --git a/ipld/resolver/tests/smoke.rs b/ipld/resolver/tests/smoke.rs index b2f88171e..65d436000 100644 --- a/ipld/resolver/tests/smoke.rs +++ b/ipld/resolver/tests/smoke.rs @@ -23,7 +23,8 @@ use anyhow::anyhow; use fvm_ipld_hamt::Hamt; use fvm_shared::{address::Address, ActorID}; use ipc_ipld_resolver::{ - Client, Config, ConnectionConfig, DiscoveryConfig, MembershipConfig, NetworkConfig, Service, + Client, Config, ConnectionConfig, ContentConfig, DiscoveryConfig, MembershipConfig, + NetworkConfig, Service, }; use ipc_sdk::subnet_id::{SubnetID, ROOTNET_ID}; use libipld::Cid; @@ -188,6 +189,10 @@ fn make_config(rng: &mut StdRng, cluster_size: u32, bootstrap_addr: Option Date: Fri, 10 Mar 2023 19:53:37 +0000 Subject: [PATCH 28/82] IPC-86: Fix rate limiting with 0 to avoid division by zero (#93) --- ipld/resolver/src/behaviour/content.rs | 44 +++++++++++++++----------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/ipld/resolver/src/behaviour/content.rs b/ipld/resolver/src/behaviour/content.rs index 7e723d8c5..1f25f9a8c 100644 --- a/ipld/resolver/src/behaviour/content.rs +++ b/ipld/resolver/src/behaviour/content.rs @@ -85,7 +85,8 @@ pub struct Behaviour { peer_addresses: HashMap, /// Limit the amount of data served by remote address. rate_limiter: RateLimiter, - rate_limit: RateLimit, + rate_limit_period: Duration, + rate_limit: Option, outbox: VecDeque, } @@ -95,11 +96,20 @@ impl Behaviour

{ S: BitswapStore, { let bitswap = Bitswap::new(BitswapConfig::default(), store); + let rate_limit = if config.rate_limit_bytes == 0 || config.rate_limit_period.is_zero() { + None + } else { + Some(RateLimit::new( + config.rate_limit_bytes, + config.rate_limit_period, + )) + }; Self { inner: bitswap, peer_addresses: Default::default(), rate_limiter: RateLimiter::new(config.rate_limit_period), - rate_limit: RateLimit::new(config.rate_limit_bytes, config.rate_limit_period), + rate_limit_period: config.rate_limit_period, + rate_limit, outbox: Default::default(), } } @@ -135,21 +145,15 @@ impl Behaviour

{ self.inner.sync(cid, peers, [].into_iter()) } - /// Check if we are using rate limiting. - fn has_rate_limits(&self) -> bool { - !(self.rate_limit.resource_limit == 0 || self.rate_limit.period.is_zero()) - } - /// Check whether the peer has already exhaused their rate limit. fn check_rate_limit(&mut self, peer_id: &PeerId, cid: &Cid) -> bool { - if !self.has_rate_limits() { - return true; - } - if let Some(addr) = self.peer_addresses.get(peer_id).cloned() { - let bytes = cid.to_bytes().len().try_into().unwrap_or(u32::MAX); + if let Some(ref rate_limit) = self.rate_limit { + if let Some(addr) = self.peer_addresses.get(peer_id).cloned() { + let bytes = cid.to_bytes().len().try_into().unwrap_or(u32::MAX); - if !self.rate_limiter.add(&self.rate_limit, addr, bytes) { - return false; + if !self.rate_limiter.add(rate_limit, addr, bytes) { + return false; + } } } true @@ -157,17 +161,21 @@ impl Behaviour

{ /// Callback by the service after [`Event::BitswapForward`]. pub fn rate_limit_used(&mut self, peer_id: PeerId, bytes: usize) { - if self.has_rate_limits() { + if let Some(ref rate_limit) = self.rate_limit { if let Some(addr) = self.peer_addresses.get(&peer_id).cloned() { let bytes = bytes.try_into().unwrap_or(u32::MAX); - let _ = self.rate_limiter.add(&self.rate_limit, addr, bytes); + let _ = self.rate_limiter.add(rate_limit, addr, bytes); } } } /// Update the rate limit to a new value, keeping the period as-is. pub fn update_rate_limit(&mut self, bytes: u32) { - self.rate_limit = RateLimit::new(bytes, self.rate_limit.period) + if bytes == 0 || self.rate_limit_period.is_zero() { + self.rate_limit = None; + } else { + self.rate_limit = Some(RateLimit::new(bytes, self.rate_limit_period)) + } } } @@ -225,7 +233,7 @@ impl NetworkBehaviour for Behaviour

{ request_id, request, sender, - } if self.has_rate_limits() => { + } if self.rate_limit.is_some() => { if !self.check_rate_limit(&peer_id, &request.cid) { warn!("rate limiting {peer_id}"); stats::CONTENT_RATE_LIMITED.inc(); From 55189c10311ce718c85eb90a6875cf19aebdf65e Mon Sep 17 00:00:00 2001 From: Henrique Moniz <1785239+hmoniz@users.noreply.github.com> Date: Mon, 13 Mar 2023 11:09:30 +0000 Subject: [PATCH 29/82] Implements logic for actively checkpointing the state of managed subnets. (#61) --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 64a40ab89..79454bee5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ serde = { workspace = true } serde_json = "1.0.91" cid = { version = "0.8.3", default-features = false, features = ["serde-codec"] } tokio = { workspace = true } +tokio-graceful-shutdown = "0.12.1" tokio-tungstenite = { version = "0.18.0", features = ["native-tls"] } derive_builder = "0.12.0" num-traits = "0.2.15" @@ -47,6 +48,7 @@ ipc-sdk = { workspace = true } ipc-subnet-actor = { workspace = true } ipc-gateway = { workspace = true } fvm_ipld_encoding = { workspace = true } +primitives = { workspace = true } [dev-dependencies] tempfile = "3.4.0" @@ -73,3 +75,4 @@ ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git" } ipc-subnet-actor = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [] } ipc-gateway = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [] } libipld = { version = "0.14", default-features = false, features = ["dag-cbor"] } +primitives = { git = "https://github.com/consensus-shipyard/fvm-utils"} From f2412cc0524477e0877f40c20b7bcc9f9e03c575 Mon Sep 17 00:00:00 2001 From: Alfonso de la Rocha Date: Mon, 13 Mar 2023 19:21:50 +0100 Subject: [PATCH 30/82] wip: config init and testing --- .gitignore | 1 + Makefile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 568c53bd1..472afa284 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.iml /target docs/diagrams/plantuml.jar +bin \ No newline at end of file diff --git a/Makefile b/Makefile index 65c7b029b..e3b53d1de 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ all: test build build: - cargo build --release + cargo build -Z unstable-options --release --out-dir ./bin test: cargo test --release --workspace From f3fe747b9e6cac1f8b783b83037896946c023970 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 14 Mar 2023 11:10:45 +0800 Subject: [PATCH 31/82] add deserialization --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 79454bee5..182c053bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ indoc = "2.0.0" log = { workspace = true } reqwest = { version = "0.11.13", features = ["json"] } serde = { workspace = true } -serde_json = "1.0.91" +serde_json = { version = "1.0.91", features = ["raw_value"] } cid = { version = "0.8.3", default-features = false, features = ["serde-codec"] } tokio = { workspace = true } tokio-graceful-shutdown = "0.12.1" From 9412814d7f649e422c643ad9f3dd6f9759656df0 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Tue, 14 Mar 2023 18:12:15 +0800 Subject: [PATCH 32/82] Add whitelist and propagate methods (#109) * add fund impl * add fund and release * add value * add whitelist and propagate methods * add postman collection --- docs/ipc_postman_collection.json | 207 +++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 docs/ipc_postman_collection.json diff --git a/docs/ipc_postman_collection.json b/docs/ipc_postman_collection.json new file mode 100644 index 000000000..f89d9d2c6 --- /dev/null +++ b/docs/ipc_postman_collection.json @@ -0,0 +1,207 @@ +{ + "info": { + "_postman_id": "9d6fd94c-2851-4d49-9ba0-68e3f31161ba", + "name": "ipc", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "15161140" + }, + "item": [ + { + "name": "Lotus json rpc", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"method\": \"Filecoin.IPCListChildSubnets\",\n \"params\": [\n \"f064\"\n ],\n \"jsonrpc\": \"2.0\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:1233/rpc/v1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "1233", + "path": [ + "rpc", + "v1" + ] + } + }, + "response": [] + }, + { + "name": "List subnets", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"method\": \"ipc_listChildSubnets\",\n \"params\": {\n \"gateway_address\": \"f064\",\n \"subnet_id\": \"/root\"\n },\n \"jsonrpc\": \"2.0\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3030/json_rpc", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3030", + "path": [ + "json_rpc" + ] + } + }, + "response": [] + }, + { + "name": "Join subnet", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"method\": \"ipc_joinSubnet\",\n \"params\": {\n \"subnet\": \"/root/t01003\",\n \"collateral\": 10,\n \"min_validators\": 0,\n \"validator_net_addr\": \"test\"\n },\n \"jsonrpc\": \"2.0\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3030/json_rpc", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3030", + "path": [ + "json_rpc" + ] + } + }, + "response": [] + }, + { + "name": "Leave subnet", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"method\": \"ipc_leaveSubnet\",\n \"params\": {\n \"subnet\": \"/root/t01003\"\n },\n \"jsonrpc\": \"2.0\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3030/json_rpc", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3030", + "path": [ + "json_rpc" + ] + } + }, + "response": [] + }, + { + "name": "Kill subnet", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"method\": \"ipc_killSubnet\",\n \"params\": {\n \"subnet\": \"/root/t01003\"\n },\n \"jsonrpc\": \"2.0\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3030/json_rpc", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3030", + "path": [ + "json_rpc" + ] + } + }, + "response": [] + }, + { + "name": "Query validator set", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"method\": \"ipc_queryValidatorSet\",\n \"params\": {\n \"subnet\": \"/root/t01003\",\n \"tip_set\": \"bafy2bzaced5izm5ns454dlu5niyhunullvjgyepft3gsycziwbem75zom5mx4\"\n },\n \"jsonrpc\": \"2.0\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3030/json_rpc", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3030", + "path": [ + "json_rpc" + ] + } + }, + "response": [] + }, + { + "name": "Create subnet", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"method\": \"ipc_createSubnet\",\n \"params\": {\n \"parent\": \"/root\",\n \"name\": \"test2\",\n \"min_validator_stake\": 1,\n \"min_validators\": 0,\n \"finality_threshold\": 2,\n \"check_period\": 10\n },\n \"jsonrpc\": \"2.0\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3030/json_rpc", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3030", + "path": [ + "json_rpc" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file From f1be9d056261b823c8a20d10f372cff64df204ad Mon Sep 17 00:00:00 2001 From: adlrocha Date: Mon, 27 Mar 2023 17:57:53 +0200 Subject: [PATCH 33/82] Several checkpoint-related fixes (#128) * wip: serializing * fix network issues * fix lint * fix key type * remove subnet id * update subnet info type * update casing * update casing * use temp struct for subnet info * add more comments * update token amount serialization * update string type * improve checkpoint logic * minor fixes * assume test network as default for tests * minor fix --------- Co-authored-by: cryptoAtwill --- docs/ipc_postman_collection.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ipc_postman_collection.json b/docs/ipc_postman_collection.json index f89d9d2c6..5fabf02f9 100644 --- a/docs/ipc_postman_collection.json +++ b/docs/ipc_postman_collection.json @@ -13,7 +13,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"id\": 1,\n \"method\": \"Filecoin.IPCListChildSubnets\",\n \"params\": [\n \"f064\"\n ],\n \"jsonrpc\": \"2.0\"\n}", + "raw": "{\n \"id\": 1,\n \"method\": \"Filecoin.IPCListChildSubnets\",\n \"params\": [\n \"t064\"\n ],\n \"jsonrpc\": \"2.0\"\n}", "options": { "raw": { "language": "json" @@ -42,7 +42,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"id\": 1,\n \"method\": \"ipc_listChildSubnets\",\n \"params\": {\n \"gateway_address\": \"f064\",\n \"subnet_id\": \"/root\"\n },\n \"jsonrpc\": \"2.0\"\n}", + "raw": "{\n \"id\": 1,\n \"method\": \"ipc_listChildSubnets\",\n \"params\": {\n \"gateway_address\": \"t064\",\n \"subnet_id\": \"/root\"\n },\n \"jsonrpc\": \"2.0\"\n}", "options": { "raw": { "language": "json" From b0d33850bf63c8fe7a8e2ca999468e58262a7347 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Wed, 29 Mar 2023 01:34:09 +0800 Subject: [PATCH 34/82] Fix checkpoint serialization (#136) * fix checkpoint serialization * minor fixes and update dependencies * make linter happy :) --------- Co-authored-by: Alfonso de la Rocha --- Cargo.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 182c053bd..d587e8968 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ toml = "0.7.2" url = { version = "2.3.1", features = ["serde"] } warp = "0.3.3" bytes = "1.4.0" +serde_bytes = "0.11.9" clap = { version = "4.1.4", features = ["env", "derive"] } thiserror = "1.0.38" serde_tuple = "0.5.0" @@ -71,8 +72,8 @@ fvm_ipld_blockstore = "0.1" fvm_ipld_encoding = "0.3" fvm_shared = { version = "=3.0.0-alpha.17", default-features = false } fil_actors_runtime = { git = "https://github.com/consensus-shipyard/fvm-utils", features = ["fil-actor"] } -ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git" } -ipc-subnet-actor = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [] } -ipc-gateway = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [] } +ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git"} +ipc-subnet-actor = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = []} +ipc-gateway = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = []} libipld = { version = "0.14", default-features = false, features = ["dag-cbor"] } primitives = { git = "https://github.com/consensus-shipyard/fvm-utils"} From 78c5af0a47172dbcd84e744638c9ee19f39a19f7 Mon Sep 17 00:00:00 2001 From: adlrocha Date: Fri, 31 Mar 2023 09:30:45 +0200 Subject: [PATCH 35/82] Getting started end-to-end IPC tutorial (#130) * README for subnet lifecycle * Apply suggestions from code review Co-authored-by: Jorge Soares <547492+jsoares@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Jorge Soares <547492+jsoares@users.noreply.github.com> * address comments and set validator net addr * wip: checkpoints * update checkpoint section * rename ipc_agent to ipc-agent * pretty print list-subnets. Address comments * update README. Minor fix in list-subnets * add instructions for send-value * Update README.md * Update README.md * Apply suggestions from code review Co-authored-by: Jorge Soares <547492+jsoares@users.noreply.github.com> --------- Co-authored-by: Jorge Soares <547492+jsoares@users.noreply.github.com> --- Cargo.toml | 2 +- Makefile | 5 ++++- scripts/install_infra.sh | 12 ++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100755 scripts/install_infra.sh diff --git a/Cargo.toml b/Cargo.toml index d587e8968..e7139d4d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" license-file = "LICENSE" [package] -name = "ipc_agent" +name = "ipc-agent" version = "0.1.0" edition.workspace = true license-file.workspace = true diff --git a/Makefile b/Makefile index e3b53d1de..d33578fb0 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build test lint license check-fmt check-clippy diagrams +.PHONY: all build test lint license check-fmt check-clippy diagrams install_infra all: test build @@ -19,6 +19,9 @@ lint: \ license: ./scripts/add_license.sh +install-infra: + ./scripts/install_infra.sh + check-fmt: cargo fmt --all --check diff --git a/scripts/install_infra.sh b/scripts/install_infra.sh new file mode 100755 index 000000000..35c4e65ae --- /dev/null +++ b/scripts/install_infra.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# +# Builds docker image and install the ipc-scripts required to conveniently +# deploy the infrastructure for IPC subnets. +rm -rf ./lotus +git clone https://github.com/consensus-shipyard/lotus.git +cd ./lotus +docker build -t eudico . +cd .. +mkdir -p ./bin +cp -r ./lotus/scripts/ipc ./bin/ipc-infra +rm -rf ./lotus From cc87af57a6cf050f33dc3bbd3adcca10dac0cc70 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Fri, 31 Mar 2023 08:44:22 +0100 Subject: [PATCH 36/82] IPC-101: Gossip votes (#106) * IPC-101: Move Client to its own module. * IPC-101: Change Service::new to not return a Client * IPC-101: Add an event queue to the service with a subscribe method * IPC-101: Vote type * IPC-101: Factor out signing from SignedProviderRecord * IPC-101: Move Timestamp to own module. * IPC-101: Into instead of pub field. * IPC-101: Test vote roundtrip * IPC-101: Publish vote * IPC-101: Fix clippy. * IPC-101: Subscribe to supported subnets. * IPC-101: Smoke test voting. --- ipld/resolver/README.md | 4 +- ipld/resolver/src/arb.rs | 22 +++ ipld/resolver/src/behaviour/membership.rs | 107 ++++++++++++-- ipld/resolver/src/client.rs | 88 +++++++++++ ipld/resolver/src/lib.rs | 14 +- ipld/resolver/src/provider_cache.rs | 7 +- ipld/resolver/src/provider_record.rs | 151 ++++--------------- ipld/resolver/src/service.rs | 169 +++++++++++----------- ipld/resolver/src/signed_record.rs | 120 +++++++++++++++ ipld/resolver/src/timestamp.rs | 53 +++++++ ipld/resolver/src/vote_record.rs | 124 ++++++++++++++++ ipld/resolver/tests/smoke.rs | 106 ++++++++++++-- 12 files changed, 725 insertions(+), 240 deletions(-) create mode 100644 ipld/resolver/src/client.rs create mode 100644 ipld/resolver/src/signed_record.rs create mode 100644 ipld/resolver/src/timestamp.rs create mode 100644 ipld/resolver/src/vote_record.rs diff --git a/ipld/resolver/README.md b/ipld/resolver/README.md index 52470a849..cfa6d5f03 100644 --- a/ipld/resolver/README.md +++ b/ipld/resolver/README.md @@ -18,6 +18,7 @@ async fn main() { expected_peer_count: 1000, max_incoming: 25, max_peers_per_query: 10, + event_buffer_capacity: 100, }, network: NetworkConfig { local_key: Keypair::generate_secp256k1(), @@ -39,7 +40,8 @@ async fn main() { let store = todo!("implement BitswapStore and a Blockstore"); - let (service, client) = Service::new(config, store.clone()); + let service = Service::new(config, store.clone()); + let client = service.client(); tokio::task::spawn(async move { service.run().await }); diff --git a/ipld/resolver/src/arb.rs b/ipld/resolver/src/arb.rs index 77da083a5..e049e8cfb 100644 --- a/ipld/resolver/src/arb.rs +++ b/ipld/resolver/src/arb.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MIT use fvm_shared::address::Address; use ipc_sdk::subnet_id::{SubnetID, ROOTNET_ID}; +use libipld::{Cid, Multihash}; use quickcheck::Arbitrary; /// Unfortunately an arbitrary `DelegatedAddress` can be inconsistent @@ -33,3 +34,24 @@ impl Arbitrary for ArbSubnetID { Self(parent) } } + +/// Unfortunately ref-fvm depends on cid:0.8.6, which depends on quickcheck:0.9 +/// whereas here we use quickcheck:1.0. This causes conflicts and the `Arbitrary` +/// implementations for `Cid` are not usable to us, nor can we patch all `cid` +/// dependencies to use 0.9 because then the IPLD and other FVM traits don't work. +/// +/// TODO: Remove this module when the `cid` dependency is updated. +/// +/// NOTE: This is based on the [simpler version](https://github.com/ChainSafe/forest/blob/v0.6.0/blockchain/blocks/src/lib.rs) in Forest. +/// The original uses weighted distributions to generate more plausible CIDs. +#[derive(Clone)] +pub struct ArbCid(pub Cid); + +impl Arbitrary for ArbCid { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self(Cid::new_v1( + u64::arbitrary(g), + Multihash::wrap(u64::arbitrary(g), &[u8::arbitrary(g)]).unwrap(), + )) + } +} diff --git a/ipld/resolver/src/behaviour/membership.rs b/ipld/resolver/src/behaviour/membership.rs index 81a50f9ed..2581e2bca 100644 --- a/ipld/resolver/src/behaviour/membership.rs +++ b/ipld/resolver/src/behaviour/membership.rs @@ -1,6 +1,6 @@ // Copyright 2022-2023 Protocol Labs // SPDX-License-Identifier: MIT -use std::collections::VecDeque; +use std::collections::{HashSet, VecDeque}; use std::task::{Context, Poll}; use std::time::Duration; @@ -10,7 +10,7 @@ use libp2p::core::connection::ConnectionId; use libp2p::gossipsub::error::SubscriptionError; use libp2p::gossipsub::{ GossipsubConfigBuilder, GossipsubEvent, GossipsubMessage, IdentTopic, MessageAuthenticity, - MessageId, Topic, TopicHash, + MessageId, Sha256Topic, Topic, TopicHash, }; use libp2p::identity::Keypair; use libp2p::swarm::derive_prelude::FromSwarm; @@ -26,13 +26,16 @@ use tokio::time::{Instant, Interval}; use crate::hash::blake2b_256; use crate::provider_cache::{ProviderDelta, SubnetProviderCache}; -use crate::provider_record::{ProviderRecord, SignedProviderRecord, Timestamp}; -use crate::stats; +use crate::provider_record::{ProviderRecord, SignedProviderRecord}; +use crate::vote_record::{SignedVoteRecord, VoteRecord}; +use crate::{stats, Timestamp}; use super::NetworkConfig; -/// `Gossipsub` subnet membership topic identifier. +/// `Gossipsub` topic identifier for subnet membership. const PUBSUB_MEMBERSHIP: &str = "/ipc/membership"; +/// `Gossipsub` topic identifier for voting about content. +const PUBSUB_VOTES: &str = "/ipc/ipld/votes"; /// Events emitted by the [`membership::Behaviour`] behaviour. #[derive(Debug)] @@ -47,6 +50,9 @@ pub enum Event { /// been told yet that the provider peer is routable. This event can be used /// to trigger a lookup by the discovery module to learn the address. Skipped(PeerId), + + /// We received a [`VoteRecord`] in one of the subnets we are providing data for. + ReceivedVote(Box), } /// Configuration for [`membership::Behaviour`]. @@ -83,10 +89,14 @@ pub struct Behaviour { outbox: VecDeque, /// [`Keypair`] used to sign [`SignedProviderRecord`] instances. local_key: Keypair, + /// Name of the P2P network, used to separate `Gossipsub` topics. + network_name: String, /// Name of the [`Gossipsub`] topic where subnet memberships are published. membership_topic: IdentTopic, /// List of subnet IDs this agent is providing data for. subnet_ids: Vec, + /// Voting topics we are currently subscribed to. + voting_topics: HashSet, /// Caching the latest state of subnet providers. provider_cache: SubnetProviderCache, /// Interval between publishing the currently supported subnets. @@ -147,8 +157,10 @@ impl Behaviour { inner: gossipsub, outbox: Default::default(), local_key: nc.local_key, + network_name: nc.network_name, membership_topic, subnet_ids: Default::default(), + voting_topics: Default::default(), provider_cache: SubnetProviderCache::new(mc.max_subnets, mc.static_subnets), publish_interval: interval, min_time_between_publish: mc.min_time_between_publish, @@ -158,8 +170,49 @@ impl Behaviour { }) } + /// Construct the topic used to gossip about votes. + /// + /// Replaces "/" with "_" to avoid clashes from prefix/suffix overlap. + fn voting_topic(&self, subnet_id: &SubnetID) -> Sha256Topic { + Topic::new(format!( + "{}/{}/{}", + PUBSUB_VOTES, + self.network_name.replace('/', "_"), + subnet_id.to_string().replace('/', "_") + )) + } + + /// Subscribe to a voting topic. + fn voting_subscribe(&mut self, subnet_id: &SubnetID) -> anyhow::Result<()> { + let topic = self.voting_topic(subnet_id); + self.voting_topics.insert(topic.hash()); + self.inner.subscribe(&topic)?; + Ok(()) + } + + /// Unsubscribe from a voting topic. + fn voting_unsubscribe(&mut self, subnet_id: &SubnetID) -> anyhow::Result<()> { + let topic = self.voting_topic(subnet_id); + self.voting_topics.remove(&topic.hash()); + self.inner.unsubscribe(&topic)?; + Ok(()) + } + /// Set all the currently supported subnet IDs, then publish the updated list. pub fn set_provided_subnets(&mut self, subnet_ids: Vec) -> anyhow::Result<()> { + let old_subnet_ids = std::mem::take(&mut self.subnet_ids); + // Unsubscribe from removed. + for subnet_id in old_subnet_ids.iter() { + if !subnet_ids.contains(subnet_id) { + self.voting_unsubscribe(subnet_id)?; + } + } + // Subscribe to added. + for subnet_id in subnet_ids.iter() { + if !old_subnet_ids.contains(subnet_id) { + self.voting_subscribe(subnet_id)?; + } + } self.subnet_ids = subnet_ids; self.publish_membership() } @@ -169,6 +222,7 @@ impl Behaviour { if self.subnet_ids.contains(&subnet_id) { return Ok(()); } + self.voting_subscribe(&subnet_id)?; self.subnet_ids.push(subnet_id); self.publish_membership() } @@ -178,6 +232,7 @@ impl Behaviour { if !self.subnet_ids.contains(&subnet_id) { return Ok(()); } + self.voting_unsubscribe(&subnet_id)?; self.subnet_ids.retain(|id| id != &subnet_id); self.publish_membership() } @@ -198,7 +253,7 @@ impl Behaviour { /// Send a message through Gossipsub to let everyone know about the current configuration. fn publish_membership(&mut self) -> anyhow::Result<()> { - let record = SignedProviderRecord::new(&self.local_key, self.subnet_ids.clone())?; + let record = ProviderRecord::signed(&self.local_key, self.subnet_ids.clone())?; let data = record.into_envelope().into_protobuf_encoding(); match self.inner.publish(self.membership_topic.clone(), data) { Err(e) => { @@ -216,6 +271,22 @@ impl Behaviour { } } + /// Publish the vote of the validator running the agent about a CID to a subnet. + pub fn publish_vote(&mut self, vote: SignedVoteRecord) -> anyhow::Result<()> { + let topic = self.voting_topic(&vote.record().subnet_id); + let data = vote.into_envelope().into_protobuf_encoding(); + match self.inner.publish(topic, data) { + Err(e) => { + stats::MEMBERSHIP_PUBLISH_FAILURE.inc(); + Err(anyhow!(e)) + } + Ok(_msg_id) => { + stats::MEMBERSHIP_PUBLISH_SUCCESS.inc(); + Ok(()) + } + } + } + /// Mark a peer as routable in the cache. /// /// Call this method when the discovery service learns the address of a peer. @@ -252,7 +323,18 @@ impl Behaviour { Err(e) => { stats::MEMBERSHIP_INVALID_MESSAGE.inc(); warn!( - "Gossip message from peer {:?} could not be deserialized: {e}", + "Gossip message from peer {:?} could not be deserialized as ProviderRecord: {e}", + msg.source + ); + } + } + } else if self.voting_topics.contains(&msg.topic) { + match SignedVoteRecord::from_bytes(&msg.data).map(|r| r.into_record()) { + Ok(record) => self.handle_vote_record(record), + Err(e) => { + stats::MEMBERSHIP_INVALID_MESSAGE.inc(); + warn!( + "Gossip message from peer {:?} could not be deserialized as VoteRecord: {e}", msg.source ); } @@ -293,16 +375,15 @@ impl Behaviour { } } + /// Raise an event to tell we received a new vote. + fn handle_vote_record(&mut self, record: VoteRecord) { + self.outbox.push_back(Event::ReceivedVote(Box::new(record))) + } + /// Handle new subscribers to the membership topic. fn handle_subscriber(&mut self, peer_id: PeerId, topic: TopicHash) { if topic == self.membership_topic.hash() { self.publish_for_new_peer(peer_id) - } else { - stats::MEMBERSHIP_UNKNOWN_TOPIC.inc(); - warn!( - "unknown gossipsub topic in subscription from {}: {}", - peer_id, topic - ) } } diff --git a/ipld/resolver/src/client.rs b/ipld/resolver/src/client.rs new file mode 100644 index 000000000..4160b2f0d --- /dev/null +++ b/ipld/resolver/src/client.rs @@ -0,0 +1,88 @@ +// Copyright 2022-2023 Protocol Labs +// SPDX-License-Identifier: MIT +use anyhow::anyhow; +use ipc_sdk::subnet_id::SubnetID; +use libipld::Cid; +use tokio::sync::mpsc::UnboundedSender; +use tokio::sync::oneshot; + +use crate::{ + service::{Request, ResolveResult}, + vote_record::SignedVoteRecord, +}; + +/// A facade to the [`Service`] to provide a nicer interface than message passing would allow on its own. +#[derive(Clone)] +pub struct Client { + request_tx: UnboundedSender, +} + +impl Client { + pub(crate) fn new(request_tx: UnboundedSender) -> Self { + Self { request_tx } + } + + /// Send a request to the [`Service`], unless it has stopped listening. + fn send_request(&self, req: Request) -> anyhow::Result<()> { + self.request_tx + .send(req) + .map_err(|_| anyhow!("disconnected")) + } + + /// Set the complete list of subnets currently supported by this node. + pub fn set_provided_subnets(&self, subnet_ids: Vec) -> anyhow::Result<()> { + let req = Request::SetProvidedSubnets(subnet_ids); + self.send_request(req) + } + + /// Add a subnet supported by this node. + pub fn add_provided_subnet(&self, subnet_id: SubnetID) -> anyhow::Result<()> { + let req = Request::AddProvidedSubnet(subnet_id); + self.send_request(req) + } + + /// Remove a subnet no longer supported by this node. + pub fn remove_provided_subnet(&self, subnet_id: SubnetID) -> anyhow::Result<()> { + let req = Request::RemoveProvidedSubnet(subnet_id); + self.send_request(req) + } + + /// Add a subnet we know really exist and we are interested in them. + pub fn pin_subnet(&self, subnet_id: SubnetID) -> anyhow::Result<()> { + let req = Request::PinSubnet(subnet_id); + self.send_request(req) + } + + /// Unpin a we are no longer interested in. + pub fn unpin_subnet(&self, subnet_id: SubnetID) -> anyhow::Result<()> { + let req = Request::UnpinSubnet(subnet_id); + self.send_request(req) + } + + /// Send a CID for resolution from a specific subnet, await its completion, + /// then return the result, to be inspected by the caller. + /// + /// Upon success, the data should be found in the store. + pub async fn resolve(&self, cid: Cid, subnet_id: SubnetID) -> anyhow::Result { + let (tx, rx) = oneshot::channel(); + let req = Request::Resolve(cid, subnet_id, tx); + self.send_request(req)?; + let res = rx.await?; + Ok(res) + } + + /// Update the rate limit based on new projections for the same timeframe + /// the `content::Behaviour` was originally configured with. This can be + /// used if we can't come up with a good estimate for the amount of data + /// we have to serve from the subnets we participate in, but we can adjust + /// them on the fly based on what we observe on chain. + pub fn update_rate_limit(&self, bytes: u32) -> anyhow::Result<()> { + let req = Request::UpdateRateLimit(bytes); + self.send_request(req) + } + + pub fn publish_vote(&self, vote: SignedVoteRecord) -> anyhow::Result<()> { + let req = Request::PublishVote(Box::new(vote)); + self.send_request(req) + } +} diff --git a/ipld/resolver/src/lib.rs b/ipld/resolver/src/lib.rs index d610ede13..17331e47c 100644 --- a/ipld/resolver/src/lib.rs +++ b/ipld/resolver/src/lib.rs @@ -1,12 +1,17 @@ // Copyright 2022-2023 Protocol Labs // SPDX-License-Identifier: MIT mod behaviour; +mod client; mod hash; mod limiter; -mod provider_cache; -mod provider_record; mod service; mod stats; +mod timestamp; + +mod provider_cache; +mod provider_record; +mod signed_record; +mod vote_record; #[cfg(any(test, feature = "arb"))] mod arb; @@ -15,4 +20,7 @@ mod arb; pub mod missing_blocks; pub use behaviour::{ContentConfig, DiscoveryConfig, MembershipConfig, NetworkConfig}; -pub use service::{Client, Config, ConnectionConfig, NoKnownPeers, Service}; +pub use client::Client; +pub use service::{Config, ConnectionConfig, Event, NoKnownPeers, Service}; +pub use timestamp::Timestamp; +pub use vote_record::VoteRecord; diff --git a/ipld/resolver/src/provider_cache.rs b/ipld/resolver/src/provider_cache.rs index 9f8941aeb..5eb978beb 100644 --- a/ipld/resolver/src/provider_cache.rs +++ b/ipld/resolver/src/provider_cache.rs @@ -5,7 +5,7 @@ use std::collections::{HashMap, HashSet}; use ipc_sdk::subnet_id::SubnetID; use libp2p::PeerId; -use crate::provider_record::{ProviderRecord, Timestamp}; +use crate::{provider_record::ProviderRecord, Timestamp}; /// Change in the supported subnets of a peer. #[derive(Debug)] @@ -215,10 +215,7 @@ mod tests { use quickcheck::Arbitrary; use quickcheck_macros::quickcheck; - use crate::{ - arb::ArbSubnetID, - provider_record::{ProviderRecord, Timestamp}, - }; + use crate::{arb::ArbSubnetID, provider_record::ProviderRecord, Timestamp}; use super::SubnetProviderCache; diff --git a/ipld/resolver/src/provider_record.rs b/ipld/resolver/src/provider_record.rs index e30c07c4b..61826f656 100644 --- a/ipld/resolver/src/provider_record.rs +++ b/ipld/resolver/src/provider_record.rs @@ -1,54 +1,15 @@ // Copyright 2022-2023 Protocol Labs // SPDX-License-Identifier: MIT -use std::ops::{Add, Sub}; -use std::time::{Duration, SystemTime}; -use fvm_ipld_encoding::serde::{Deserialize, Serialize}; use ipc_sdk::subnet_id::SubnetID; -use libipld::multihash; -use libp2p::core::{signed_envelope, SignedEnvelope}; use libp2p::identity::Keypair; use libp2p::PeerId; +use serde::{Deserialize, Serialize}; -const DOMAIN_SEP: &str = "ipc-membership"; -const PAYLOAD_TYPE: &str = "/ipc/provider-record"; - -/// Unix timestamp in seconds since epoch, which we can use to select the -/// more recent message during gossiping. -#[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Debug, Serialize, Deserialize, Default)] -pub struct Timestamp(u64); - -impl Timestamp { - /// Current timestamp. - pub fn now() -> Self { - let secs = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("now() is never before UNIX_EPOCH") - .as_secs(); - Self(secs) - } - - /// Seconds elapsed since Unix epoch. - pub fn as_secs(&self) -> u64 { - self.0 - } -} - -impl Sub for Timestamp { - type Output = Self; - - fn sub(self, rhs: Duration) -> Self { - Self(self.as_secs().saturating_sub(rhs.as_secs())) - } -} - -impl Add for Timestamp { - type Output = Self; - - fn add(self, rhs: Duration) -> Self { - Self(self.as_secs().saturating_add(rhs.as_secs())) - } -} +use crate::{ + signed_record::{Record, SignedRecord}, + Timestamp, +}; /// Record of the ability to provide data from a list of subnets. /// @@ -76,21 +37,25 @@ pub struct ProviderRecord { pub timestamp: Timestamp, } -/// A [`ProviderRecord`] with a [`SignedEnvelope`] proving that the -/// peer indeed is ready to provide the data for the listed subnets. -#[derive(Debug, Clone)] -pub struct SignedProviderRecord { - /// The deserialized and validated [`ProviderRecord`]. - record: ProviderRecord, - /// The [`SignedEnvelope`] from which the record was deserialized from. - envelope: SignedEnvelope, +impl Record for ProviderRecord { + fn payload_type() -> &'static str { + "/ipc/provider-record" + } + + fn check_signing_key(&self, key: &libp2p::identity::PublicKey) -> bool { + self.peer_id == key.to_peer_id() + } } -// Based on `libp2p_core::peer_record::PeerRecord` -impl SignedProviderRecord { +pub type SignedProviderRecord = SignedRecord; + +impl ProviderRecord { /// Create a new [`SignedProviderRecord`] with the current timestamp /// and a signed envelope which can be shared with others. - pub fn new(key: &Keypair, subnet_ids: Vec) -> anyhow::Result { + pub fn signed( + key: &Keypair, + subnet_ids: Vec, + ) -> anyhow::Result { let timestamp = Timestamp::now(); let peer_id = key.public().to_peer_id(); let record = ProviderRecord { @@ -98,61 +63,11 @@ impl SignedProviderRecord { subnet_ids, timestamp, }; - let payload = fvm_ipld_encoding::to_vec(&record)?; - let envelope = SignedEnvelope::new( - key, - DOMAIN_SEP.to_owned(), - PAYLOAD_TYPE.as_bytes().to_vec(), - payload, - )?; - Ok(Self { record, envelope }) - } - - pub fn from_signed_envelope(envelope: SignedEnvelope) -> Result { - let (payload, signing_key) = - envelope.payload_and_signing_key(DOMAIN_SEP.to_owned(), PAYLOAD_TYPE.as_bytes())?; - - let record = fvm_ipld_encoding::from_slice::(payload)?; - - if record.peer_id != signing_key.to_peer_id() { - return Err(FromEnvelopeError::MismatchedSignature); - } - - Ok(Self { record, envelope }) - } - - /// Deserialize then check the domain tags and the signature. - pub fn from_bytes(bytes: &[u8]) -> anyhow::Result { - let envelope = SignedEnvelope::from_protobuf_encoding(bytes)?; - let signed_record = Self::from_signed_envelope(envelope)?; - Ok(signed_record) - } - - pub fn into_record(self) -> ProviderRecord { - self.record - } - - pub fn into_envelope(self) -> SignedEnvelope { - self.envelope + let signed = SignedRecord::new(key, record)?; + Ok(signed) } } -#[derive(thiserror::Error, Debug)] -pub enum FromEnvelopeError { - /// Failed to extract the payload from the envelope. - #[error("Failed to extract payload from envelope")] - BadPayload(#[from] signed_envelope::ReadPayloadError), - /// Failed to decode the provided bytes as a [`ProviderRecord`]. - #[error("Failed to decode bytes as ProviderRecord")] - InvalidProviderRecord(#[from] fvm_ipld_encoding::Error), - /// Failed to decode the peer ID. - #[error("Failed to decode bytes as PeerId")] - InvalidPeerId(#[from] multihash::Error), - /// The signer of the envelope is different than the peer id in the record. - #[error("The signer of the envelope is different than the peer id in the record")] - MismatchedSignature, -} - #[cfg(any(test, feature = "arb"))] mod arb { use libp2p::identity::Keypair; @@ -160,13 +75,7 @@ mod arb { use crate::arb::ArbSubnetID; - use super::{SignedProviderRecord, Timestamp}; - - impl Arbitrary for Timestamp { - fn arbitrary(g: &mut quickcheck::Gen) -> Self { - Self(u64::arbitrary(g).saturating_add(1)) - } - } + use super::{ProviderRecord, SignedProviderRecord}; /// Create a valid [`SignedProviderRecord`] with a random key. impl Arbitrary for SignedProviderRecord { @@ -184,7 +93,7 @@ mod arb { subnet_ids.push(subnet_id.0) } - Self::new(&key, subnet_ids).expect("error creating signed envelope") + ProviderRecord::signed(&key, subnet_ids).expect("error creating signed envelope") } } } @@ -198,20 +107,12 @@ mod tests { #[quickcheck] fn prop_roundtrip(signed_record: SignedProviderRecord) -> bool { - let envelope_bytes = signed_record.envelope.into_protobuf_encoding(); - - let envelope = - SignedEnvelope::from_protobuf_encoding(&envelope_bytes).expect("envelope roundtrip"); - - let signed_record2 = - SignedProviderRecord::from_signed_envelope(envelope).expect("record roundtrip"); - - signed_record2.record == signed_record.record + crate::signed_record::tests::prop_roundtrip(signed_record) } #[quickcheck] fn prop_tamper_proof(signed_record: SignedProviderRecord, idx: usize) -> bool { - let mut envelope_bytes = signed_record.envelope.into_protobuf_encoding(); + let mut envelope_bytes = signed_record.into_envelope().into_protobuf_encoding(); // Do some kind of mutation to a random byte in the envelope; after that it should not validate. let idx = idx % envelope_bytes.len(); envelope_bytes[idx] = u8::MAX - envelope_bytes[idx]; diff --git a/ipld/resolver/src/service.rs b/ipld/resolver/src/service.rs index e28b667dc..b7e329bfe 100644 --- a/ipld/resolver/src/service.rs +++ b/ipld/resolver/src/service.rs @@ -19,17 +19,21 @@ use libp2p::{ }; use libp2p::{identify, ping}; use libp2p_bitswap::{BitswapResponse, BitswapStore}; -use log::{debug, error, trace, warn}; +use log::{debug, error, info, trace, warn}; use prometheus::Registry; use rand::seq::SliceRandom; use tokio::select; +use tokio::sync::broadcast; +use tokio::sync::mpsc; use tokio::sync::oneshot::{self, Sender}; use crate::behaviour::{ self, content, discovery, membership, Behaviour, BehaviourEvent, ConfigError, ContentConfig, DiscoveryConfig, MembershipConfig, NetworkConfig, }; +use crate::client::Client; use crate::stats; +use crate::vote_record::{SignedVoteRecord, VoteRecord}; /// Result of attempting to resolve a CID. pub type ResolveResult = anyhow::Result<()>; @@ -65,6 +69,9 @@ pub struct ConnectionConfig { pub expected_peer_count: u32, /// Maximum number of peers to send Bitswap requests to in a single attempt. pub max_peers_per_query: u32, + /// Maximum number of events in the push-based broadcast channel before a slow + /// consumer gets an error because it's falling behind. + pub event_buffer_capacity: u32, } #[derive(Debug, Clone)] @@ -77,82 +84,24 @@ pub struct Config { } /// Internal requests to enqueue to the [`Service`] -enum Request { +pub(crate) enum Request { SetProvidedSubnets(Vec), AddProvidedSubnet(SubnetID), RemoveProvidedSubnet(SubnetID), + PublishVote(Box), PinSubnet(SubnetID), UnpinSubnet(SubnetID), - Resolve(Cid, SubnetID, oneshot::Sender), + Resolve(Cid, SubnetID, ResponseChannel), RateLimitUsed(PeerId, usize), UpdateRateLimit(u32), } -/// A facade to the [`Service`] to provide a nicer interface than message passing would allow on its own. -#[derive(Clone)] -pub struct Client { - request_tx: tokio::sync::mpsc::UnboundedSender, -} - -impl Client { - /// Send a request to the [`Service`], unless it has stopped listening. - fn send_request(&self, req: Request) -> anyhow::Result<()> { - self.request_tx - .send(req) - .map_err(|_| anyhow!("disconnected")) - } - - /// Set the complete list of subnets currently supported by this node. - pub fn set_provided_subnets(&self, subnet_ids: Vec) -> anyhow::Result<()> { - let req = Request::SetProvidedSubnets(subnet_ids); - self.send_request(req) - } - - /// Add a subnet supported by this node. - pub fn add_provided_subnet(&self, subnet_id: SubnetID) -> anyhow::Result<()> { - let req = Request::AddProvidedSubnet(subnet_id); - self.send_request(req) - } - - /// Remove a subnet no longer supported by this node. - pub fn remove_provided_subnet(&self, subnet_id: SubnetID) -> anyhow::Result<()> { - let req = Request::RemoveProvidedSubnet(subnet_id); - self.send_request(req) - } - - /// Add a subnet we know really exist and we are interested in them. - pub fn pin_subnet(&self, subnet_id: SubnetID) -> anyhow::Result<()> { - let req = Request::PinSubnet(subnet_id); - self.send_request(req) - } - - /// Unpin a we are no longer interested in. - pub fn unpin_subnet(&self, subnet_id: SubnetID) -> anyhow::Result<()> { - let req = Request::UnpinSubnet(subnet_id); - self.send_request(req) - } - - /// Send a CID for resolution from a specific subnet, await its completion, - /// then return the result, to be inspected by the caller. - /// - /// Upon success, the data should be found in the store. - pub async fn resolve(&self, cid: Cid, subnet_id: SubnetID) -> anyhow::Result { - let (tx, rx) = oneshot::channel(); - let req = Request::Resolve(cid, subnet_id, tx); - self.send_request(req)?; - let res = rx.await?; - Ok(res) - } - - /// Update the rate limit based on new projections for the same timeframe - /// the `content::Behaviour` was originally configured with. This can be - /// used if we can't come up with a good estimate for the amount of data - /// we have to serve from the subnets we participate in, but we can adjust - /// them on the fly based on what we observe on chain. - pub fn update_rate_limit(&self, bytes: u32) -> anyhow::Result<()> { - let req = Request::UpdateRateLimit(bytes); - self.send_request(req) - } +/// Events that arise from the subnets, pushed to the clients, +/// not part of a request-response action. +#[derive(Clone, Debug)] +pub enum Event { + /// Received a vote about in a subnet about a CID. + ReceivedVote(Box), } /// The `Service` handles P2P communication to resolve IPLD content by wrapping and driving a number of `libp2p` behaviours. @@ -160,16 +109,23 @@ pub struct Service { peer_id: PeerId, listen_addr: Multiaddr, swarm: Swarm>, + /// To match finished queries to response channels. queries: QueryMap, - request_rx: tokio::sync::mpsc::UnboundedReceiver, - request_tx: tokio::sync::mpsc::UnboundedSender, + /// For receiving requests from the clients and self. + request_rx: mpsc::UnboundedReceiver, + /// For creating new clients and sending messages to self. + request_tx: mpsc::UnboundedSender, + /// For broadcasting events to all clients. + event_tx: broadcast::Sender, + /// To avoid looking up the same peer over and over. background_lookup_filter: BloomFilter, + /// To limit the number of peers contacted in a Bitswap resolution attempt. max_peers_per_query: usize, } impl Service

{ /// Build a [`Service`] and a [`Client`] with the default `tokio` transport. - pub fn new(config: Config, store: S) -> Result<(Self, Client), ConfigError> + pub fn new(config: Config, store: S) -> Result where S: BitswapStore, { @@ -183,7 +139,7 @@ impl Service

{ config: Config, store: S, transport: F, - ) -> Result<(Self, Client), ConfigError> + ) -> Result where S: BitswapStore, F: FnOnce(Keypair) -> Boxed<(PeerId, StreamMuxerBox)>, @@ -213,29 +169,66 @@ impl Service

{ .connection_event_buffer_size(64) .build(); - let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + let (request_tx, request_rx) = mpsc::unbounded_channel(); + let (event_tx, _) = broadcast::channel(config.connection.event_buffer_capacity as usize); let service = Self { peer_id, listen_addr: config.connection.listen_addr, swarm, queries: Default::default(), - request_rx: rx, - request_tx: tx.clone(), + request_rx, + request_tx, + event_tx, background_lookup_filter: BloomFilter::with_rate( 0.1, config.connection.expected_peer_count, ), - max_peers_per_query: config - .connection - .max_peers_per_query - .try_into() - .expect("u32 should be usize"), + max_peers_per_query: config.connection.max_peers_per_query as usize, }; - let client = Client { request_tx: tx }; + Ok(service) + } + + /// Create a new [`Client`] instance bound to this `Service`. + /// + /// The [`Client`] is geared towards request-response interactions, + /// while the `Receiver` returned by `subscribe` is used for events + /// which weren't initiated by the `Client`. + pub fn client(&self) -> Client { + Client::new(self.request_tx.clone()) + } - Ok((service, client)) + /// Create a new [`broadcast::Receiver`] instance bound to this `Service`, + /// which will be notified upon each event coming from any of the subnets + /// the `Service` is subscribed to. + /// + /// The consumers are expected to process events quick enough to be within + /// the configured capacity of the broadcast channel, or otherwise be able + /// to deal with message loss if they fall behind. + /// + /// # Notes + /// + /// This is not part of the [`Client`] because `Receiver::recv` takes + /// a mutable reference and it would prevent the [`Client`] being used + /// for anything else. + /// + /// One alternative design would be to accept an interface similar to + /// [`BitswapStore`] that we can pass events to. In that case we would + /// have to create an internal event queue to stand in front of it, + /// and because these events arrive from the outside, it would still + /// have to have limited capacity. + /// + /// Because the channel has limited capacity, we have to take care not + /// to use it for signaling critical events that we want to await upon. + /// For example if we used this to signal the readiness of bootstrapping, + /// we should make sure we have not yet subscribed to external events + /// which could drown it out. + /// + /// One way to achieve this is for the consumer of the events to redistribute + /// them into priorities event queues, some bounded, some unbounded. + pub fn subscribe(&self) -> broadcast::Receiver { + self.event_tx.subscribe() } /// Register Prometheus metrics. @@ -248,6 +241,7 @@ impl Service

{ /// Start the swarm listening for incoming connections and drive the events forward. pub async fn run(mut self) -> anyhow::Result<()> { // Start the swarm. + info!("running service on {}", self.listen_addr); Swarm::listen_on(&mut self.swarm, self.listen_addr.clone())?; loop { @@ -360,6 +354,12 @@ impl Service

{ } membership::Event::Updated(_, _) => {} membership::Event::Removed(_) => {} + membership::Event::ReceivedVote(vote) => { + let event = Event::ReceivedVote(vote); + if self.event_tx.send(event).is_err() { + debug!("dropped received vote because there are no subscribers") + } + } } } @@ -410,6 +410,11 @@ impl Service

{ warn!("failed to publish removed provided subnet: {e}") } } + Request::PublishVote(vote) => { + if let Err(e) = self.membership_mut().publish_vote(*vote) { + warn!("failed to publish vote: {e}") + } + } Request::PinSubnet(id) => self.membership_mut().pin_subnet(id), Request::UnpinSubnet(id) => self.membership_mut().unpin_subnet(&id), diff --git a/ipld/resolver/src/signed_record.rs b/ipld/resolver/src/signed_record.rs new file mode 100644 index 000000000..a6afea078 --- /dev/null +++ b/ipld/resolver/src/signed_record.rs @@ -0,0 +1,120 @@ +// Copyright 2022-2023 Protocol Labs +// SPDX-License-Identifier: MIT + +use libp2p::core::signed_envelope; +use libp2p::identity::PublicKey; +use libp2p::{core::SignedEnvelope, identity::Keypair}; +use serde::de::DeserializeOwned; +use serde::Serialize; + +const DOMAIN_SEP: &str = "/ipc/ipld/resolver"; + +pub trait Record { + /// Payload type for the [`SignedEnvelope`]. + fn payload_type() -> &'static str; + /// Check that the [`PublicKey`] recovered from the [`SignedEnvelope`] + /// is consistent with the payload. + fn check_signing_key(&self, key: &PublicKey) -> bool; +} + +/// A [`ProviderRecord`] with a [`SignedEnvelope`] proving that the +/// peer indeed is ready to provide the data for the listed subnets. +#[derive(Debug, Clone)] +pub struct SignedRecord { + /// The deserialized and validated record. + record: R, + /// The [`SignedEnvelope`] from which the record was deserialized from. + envelope: SignedEnvelope, +} + +// Based on `libp2p_core::peer_record::PeerRecord` +impl SignedRecord +where + R: Record + Serialize + DeserializeOwned, +{ + /// Create a new [`SignedRecord`] with a signed envelope + /// which can be shared with others. + pub fn new(key: &Keypair, record: R) -> anyhow::Result { + let payload = fvm_ipld_encoding::to_vec(&record)?; + let envelope = SignedEnvelope::new( + key, + DOMAIN_SEP.to_owned(), + R::payload_type().as_bytes().to_vec(), + payload, + )?; + Ok(Self { record, envelope }) + } + + pub fn from_signed_envelope(envelope: SignedEnvelope) -> Result { + let (payload, signing_key) = envelope + .payload_and_signing_key(DOMAIN_SEP.to_owned(), R::payload_type().as_bytes())?; + + let record = fvm_ipld_encoding::from_slice::(payload)?; + + if !record.check_signing_key(signing_key) { + return Err(FromEnvelopeError::MismatchedSignature); + } + + Ok(Self { record, envelope }) + } + + /// Deserialize then check the domain tags and the signature. + pub fn from_bytes(bytes: &[u8]) -> anyhow::Result { + let envelope = SignedEnvelope::from_protobuf_encoding(bytes)?; + let signed_record = Self::from_signed_envelope(envelope)?; + Ok(signed_record) + } + + pub fn record(&self) -> &R { + &self.record + } + + pub fn envelope(&self) -> &SignedEnvelope { + &self.envelope + } + + pub fn into_record(self) -> R { + self.record + } + + pub fn into_envelope(self) -> SignedEnvelope { + self.envelope + } +} + +#[derive(thiserror::Error, Debug)] +pub enum FromEnvelopeError { + /// Failed to extract the payload from the envelope. + #[error("Failed to extract payload from envelope")] + BadPayload(#[from] signed_envelope::ReadPayloadError), + /// Failed to decode the provided bytes as the record type. + #[error("Failed to decode bytes as record")] + InvalidRecord(#[from] fvm_ipld_encoding::Error), + /// The signer of the envelope is different than the peer id in the record. + #[error("The signer of the envelope is different than the peer id in the record")] + MismatchedSignature, +} + +#[cfg(test)] +pub mod tests { + use fvm_ipld_encoding::de::DeserializeOwned; + use libp2p::core::SignedEnvelope; + use serde::Serialize; + + use super::{Record, SignedRecord}; + + pub fn prop_roundtrip(signed_record: SignedRecord) -> bool + where + R: Serialize + DeserializeOwned + Record + PartialEq, + { + let envelope_bytes = signed_record.envelope().clone().into_protobuf_encoding(); + + let envelope = + SignedEnvelope::from_protobuf_encoding(&envelope_bytes).expect("envelope roundtrip"); + + let signed_record2 = + SignedRecord::::from_signed_envelope(envelope).expect("record roundtrip"); + + signed_record2.into_record() == *signed_record.record() + } +} diff --git a/ipld/resolver/src/timestamp.rs b/ipld/resolver/src/timestamp.rs new file mode 100644 index 000000000..d531a6253 --- /dev/null +++ b/ipld/resolver/src/timestamp.rs @@ -0,0 +1,53 @@ +// Copyright 2022-2023 Protocol Labs +// SPDX-License-Identifier: MIT +use serde::{Deserialize, Serialize}; +use std::ops::{Add, Sub}; +use std::time::{Duration, SystemTime}; + +/// Unix timestamp in seconds since epoch, which we can use to select the +/// more recent message during gossiping. +#[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Debug, Serialize, Deserialize, Default)] +pub struct Timestamp(u64); + +impl Timestamp { + /// Current timestamp. + pub fn now() -> Self { + let secs = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("now() is never before UNIX_EPOCH") + .as_secs(); + Self(secs) + } + + /// Seconds elapsed since Unix epoch. + pub fn as_secs(&self) -> u64 { + self.0 + } +} + +impl Sub for Timestamp { + type Output = Self; + + fn sub(self, rhs: Duration) -> Self { + Self(self.as_secs().saturating_sub(rhs.as_secs())) + } +} + +impl Add for Timestamp { + type Output = Self; + + fn add(self, rhs: Duration) -> Self { + Self(self.as_secs().saturating_add(rhs.as_secs())) + } +} + +#[cfg(any(test, feature = "arb"))] +mod arb { + use super::Timestamp; + + impl quickcheck::Arbitrary for Timestamp { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self(u64::arbitrary(g).saturating_add(1)) + } + } +} diff --git a/ipld/resolver/src/vote_record.rs b/ipld/resolver/src/vote_record.rs new file mode 100644 index 000000000..6302f8398 --- /dev/null +++ b/ipld/resolver/src/vote_record.rs @@ -0,0 +1,124 @@ +// Copyright 2022-2023 Protocol Labs +// SPDX-License-Identifier: MIT +use ipc_sdk::subnet_id::SubnetID; +use libipld::Cid; +use libp2p::identity::{Keypair, PublicKey}; +use serde::de::Error; +use serde::{Deserialize, Serialize}; + +use crate::{ + signed_record::{Record, SignedRecord}, + Timestamp, +}; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ValidatorKey(PublicKey); + +impl Serialize for ValidatorKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let bz = self.0.to_protobuf_encoding(); + bz.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for ValidatorKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let bz = Vec::::deserialize(deserializer)?; + match PublicKey::from_protobuf_encoding(&bz) { + Ok(pk) => Ok(Self(pk)), + Err(e) => Err(D::Error::custom(format!("error decoding PublicKey: {e}"))), + } + } +} + +/// Vote by a validator about the validity/availability/finality +/// of a CID in a given subnet. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct VoteRecord { + /// Public key of the validator. + pub public_key: ValidatorKey, + /// The subnet in which the vote is valid, to prevent a vote on the same CID + /// in one subnet being replayed by an attacker on a different subnet. + pub subnet_id: SubnetID, + /// The CID of the content the vote is about. + pub cid: Cid, + /// The claim of the vote, in case there can be votes about multiple facets + /// regarding the CID. + pub claim: String, + /// Timestamp to thwart potential replay attacks. + pub timestamp: Timestamp, +} + +impl Record for VoteRecord { + fn payload_type() -> &'static str { + "/ipc/vote-record" + } + + fn check_signing_key(&self, key: &libp2p::identity::PublicKey) -> bool { + self.public_key.0 == *key + } +} + +pub type SignedVoteRecord = SignedRecord; + +impl VoteRecord { + /// Create a new [`SignedVoteRecord`] with the current timestamp + /// and a signed envelope which can be shared with others. + pub fn signed( + key: &Keypair, + subnet_id: SubnetID, + cid: Cid, + claim: String, + ) -> anyhow::Result { + let timestamp = Timestamp::now(); + let record = VoteRecord { + public_key: ValidatorKey(key.public()), + subnet_id, + cid, + claim, + timestamp, + }; + let signed = SignedRecord::new(key, record)?; + Ok(signed) + } +} + +#[cfg(any(test, feature = "arb"))] +mod arb { + use libp2p::identity::Keypair; + use quickcheck::Arbitrary; + + use crate::arb::{ArbCid, ArbSubnetID}; + + use super::{SignedVoteRecord, VoteRecord}; + + /// Create a valid [`SignedVoteRecord`] with a random key. + impl Arbitrary for SignedVoteRecord { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let key = Keypair::generate_secp256k1(); + let subnet_id = ArbSubnetID::arbitrary(g).0; + let cid = ArbCid::arbitrary(g).0; + let claim = String::arbitrary(g); + + VoteRecord::signed(&key, subnet_id, cid, claim).expect("error creating signed envelope") + } + } +} + +#[cfg(test)] +mod tests { + use quickcheck_macros::quickcheck; + + use super::SignedVoteRecord; + + #[quickcheck] + fn prop_roundtrip(signed_record: SignedVoteRecord) -> bool { + crate::signed_record::tests::prop_roundtrip(signed_record) + } +} diff --git a/ipld/resolver/tests/smoke.rs b/ipld/resolver/tests/smoke.rs index 65d436000..06e4de90f 100644 --- a/ipld/resolver/tests/smoke.rs +++ b/ipld/resolver/tests/smoke.rs @@ -5,7 +5,7 @@ //! //! Run the tests as follows: //! ```ignore -//! cargo test -p ipc_ipld_resolver --test smoke +//! RUST_LOG=debug cargo test -p ipc_ipld_resolver --test smoke resolve //! ``` // For inspiration on testing libp2p look at: @@ -17,17 +17,24 @@ // so we can leave the polling to the `Service` running in a `Task`, rather than do it from the test // (although these might be orthogonal). -use std::time::Duration; +use std::{ + sync::atomic::{AtomicU64, Ordering}, + time::Duration, +}; use anyhow::anyhow; +use fvm_ipld_encoding::IPLD_RAW; use fvm_ipld_hamt::Hamt; use fvm_shared::{address::Address, ActorID}; use ipc_ipld_resolver::{ - Client, Config, ConnectionConfig, ContentConfig, DiscoveryConfig, MembershipConfig, - NetworkConfig, Service, + Client, Config, ConnectionConfig, ContentConfig, DiscoveryConfig, Event, MembershipConfig, + NetworkConfig, Service, VoteRecord, }; use ipc_sdk::subnet_id::{SubnetID, ROOTNET_ID}; -use libipld::Cid; +use libipld::{ + multihash::{Code, MultihashDigest}, + Cid, +}; use libp2p::{ core::{ muxing::StreamMuxerBox, @@ -43,10 +50,12 @@ use rand::{rngs::StdRng, Rng, SeedableRng}; mod store; use store::*; +use tokio::{sync::broadcast, time::timeout}; struct Agent { config: Config, client: Client, + events: broadcast::Receiver, store: TestBlockstore, } @@ -54,6 +63,12 @@ struct Cluster { agents: Vec, } +impl Cluster { + pub fn size(&self) -> usize { + self.agents.len() + } +} + struct ClusterBuilder { size: u32, rng: StdRng, @@ -62,7 +77,15 @@ struct ClusterBuilder { } impl ClusterBuilder { - fn new(size: u32, seed: u64) -> Self { + fn new(size: u32) -> Self { + // Each port has to be unique, so each test must use a different seed. + // This is shared between all instances. + static COUNTER: AtomicU64 = AtomicU64::new(0); + let seed = COUNTER.fetch_add(1, Ordering::Relaxed); + Self::new_with_seed(size, seed) + } + + fn new_with_seed(size: u32, seed: u64) -> Self { Self { size, rng: rand::rngs::StdRng::seed_from_u64(seed), @@ -81,11 +104,14 @@ impl ClusterBuilder { addr }); let config = make_config(&mut self.rng, self.size, bootstrap_addr); - let (service, client, store) = make_service(config.clone()); + let (service, store) = make_service(config.clone()); + let client = service.client(); + let events = service.subscribe(); self.services.push(service); self.agents.push(Agent { config, client, + events, store, }); } @@ -115,7 +141,7 @@ async fn single_bootstrap_single_provider_resolve_one() { let resolver_idx = 2; // TODO: Get the seed from QuickCheck - let mut builder = ClusterBuilder::new(cluster_size, 123456u64); + let mut builder = ClusterBuilder::new(cluster_size); // Build a cluster of nodes. for i in 0..builder.size { @@ -159,10 +185,67 @@ async fn single_bootstrap_single_provider_resolve_one() { check_test_data(&mut cluster.agents[resolver_idx], &cid).expect("failed to resolve from store"); } -fn make_service(config: Config) -> (Service, Client, TestBlockstore) { +/// Start two agents, subscribe to the same subnet, publish and receive a vote. +#[tokio::test] +async fn single_bootstrap_publish_receive_vote() { + let _ = env_logger::builder().is_test(true).try_init(); + //env_logger::init(); + + // TODO: Get the seed from QuickCheck + let mut builder = ClusterBuilder::new(2); + + // Build a cluster of nodes. + for i in 0..builder.size { + builder.add_node(if i == 0 { None } else { Some(0) }); + } + + // Start the swarms. + let mut cluster = builder.run(); + + // Wait a little for the cluster to connect. + // TODO: Wait on some condition instead of sleep. + tokio::time::sleep(Duration::from_secs(1)).await; + + // Announce the support of some subnet. + let subnet_id = make_subnet_id(1001); + + for i in 0..cluster.size() { + cluster.agents[i] + .client + .add_provided_subnet(subnet_id.clone()) + .expect("failed to add provided subnet"); + } + + // Wait a little for the gossip to spread and peer lookups to happen, then another round of gossip. + // TODO: Wait on some condition instead of sleep. + tokio::time::sleep(Duration::from_secs(2)).await; + + // Vote on some random CID. + let validator_key = Keypair::generate_secp256k1(); + let cid = Cid::new_v1(IPLD_RAW, Code::Sha2_256.digest(b"foo")); + let vote = VoteRecord::signed(&validator_key, subnet_id, cid, "finalized".into()) + .expect("failed to sign vote"); + + // Pubilish vote + cluster.agents[0] + .client + .publish_vote(vote.clone()) + .expect("failed to send vote"); + + // Receive vote. + let event = timeout(Duration::from_secs(2), cluster.agents[1].events.recv()) + .await + .expect("timeout receiving vote") + .expect("error receiving vote"); + + let Event::ReceivedVote(v) = event; + assert_eq!(&*v, vote.record()); +} + +fn make_service(config: Config) -> (Service, TestBlockstore) { let store = TestBlockstore::default(); - let (svc, cli) = Service::new_with_transport(config, store.clone(), build_transport).unwrap(); - (svc, cli, store) + let svc = Service::new_with_transport(config, store.clone(), build_transport).unwrap(); + (svc, store) } fn make_config(rng: &mut StdRng, cluster_size: u32, bootstrap_addr: Option) -> Config { @@ -172,6 +255,7 @@ fn make_config(rng: &mut StdRng, cluster_size: u32, bootstrap_addr: Option Date: Fri, 31 Mar 2023 09:19:10 +0100 Subject: [PATCH 37/82] IPC-39: Publish preemptive data (#112) * IPC-101: Move Client to its own module. * IPC-101: Change Service::new to not return a Client * IPC-101: Add an event queue to the service with a subscribe method * IPC-101: Vote type * IPC-101: Factor out signing from SignedProviderRecord * IPC-101: Move Timestamp to own module. * IPC-101: Into instead of pub field. * IPC-101: Test vote roundtrip * IPC-101: Publish vote * IPC-101: Fix clippy. * IPC-101: Subscribe to supported subnets. * IPC-101: Smoke test voting. * IPC-39: Subscribe to pre-emptive topic when pinning. * IPC-39: Subscribe to starting subnets * IPC-39: Keep track of preemptive topics * IPC-39: Receive pre-emptive data and publish it through the Service events * IPC-39: Add publish_preemptive to Client * IPC-39: Smoke test for preemptive data * minor fix --------- Co-authored-by: Alfonso de la Rocha --- ipld/resolver/src/behaviour/membership.rs | 100 +++++++++++++++++++--- ipld/resolver/src/client.rs | 9 ++ ipld/resolver/src/service.rs | 27 +++++- ipld/resolver/tests/smoke.rs | 59 ++++++++++++- 4 files changed, 177 insertions(+), 18 deletions(-) diff --git a/ipld/resolver/src/behaviour/membership.rs b/ipld/resolver/src/behaviour/membership.rs index 2581e2bca..0923459a1 100644 --- a/ipld/resolver/src/behaviour/membership.rs +++ b/ipld/resolver/src/behaviour/membership.rs @@ -1,6 +1,6 @@ // Copyright 2022-2023 Protocol Labs // SPDX-License-Identifier: MIT -use std::collections::{HashSet, VecDeque}; +use std::collections::{HashMap, HashSet, VecDeque}; use std::task::{Context, Poll}; use std::time::Duration; @@ -36,6 +36,8 @@ use super::NetworkConfig; const PUBSUB_MEMBERSHIP: &str = "/ipc/membership"; /// `Gossipsub` topic identifier for voting about content. const PUBSUB_VOTES: &str = "/ipc/ipld/votes"; +/// `Gossipsub` topic identifier for pre-emptively published blocks of data. +const PUBSUB_PREEMPTIVE: &str = "/ipc/ipld/pre-emptive"; /// Events emitted by the [`membership::Behaviour`] behaviour. #[derive(Debug)] @@ -53,6 +55,9 @@ pub enum Event { /// We received a [`VoteRecord`] in one of the subnets we are providing data for. ReceivedVote(Box), + + /// We received preemptive data published in a subnet we were interested in. + ReceivedPreemptive(SubnetID, Vec), } /// Configuration for [`membership::Behaviour`]. @@ -97,6 +102,8 @@ pub struct Behaviour { subnet_ids: Vec, /// Voting topics we are currently subscribed to. voting_topics: HashSet, + /// Remember which subnet a topic was about. + preemptive_topics: HashMap, /// Caching the latest state of subnet providers. provider_cache: SubnetProviderCache, /// Interval between publishing the currently supported subnets. @@ -153,7 +160,10 @@ impl Behaviour { let mut interval = tokio::time::interval(mc.publish_interval); interval.reset(); - Ok(Self { + // Not passing static subnets here; using pinning below instead so it subscribes as well + let provider_cache = SubnetProviderCache::new(mc.max_subnets, vec![]); + + let mut membership = Self { inner: gossipsub, outbox: Default::default(), local_key: nc.local_key, @@ -161,13 +171,48 @@ impl Behaviour { membership_topic, subnet_ids: Default::default(), voting_topics: Default::default(), - provider_cache: SubnetProviderCache::new(mc.max_subnets, mc.static_subnets), + preemptive_topics: Default::default(), + provider_cache, publish_interval: interval, min_time_between_publish: mc.min_time_between_publish, last_publish_timestamp: Timestamp::default(), next_publish_timestamp: Timestamp::now() + mc.publish_interval, max_provider_age: mc.max_provider_age, - }) + }; + + for subnet_id in mc.static_subnets { + membership.pin_subnet(subnet_id)?; + } + + Ok(membership) + } + + /// Construct the topic used to gossip about pre-emptively published data. + /// + /// Replaces "/" with "_" to avoid clashes from prefix/suffix overlap. + fn preemptive_topic(&self, subnet_id: &SubnetID) -> Sha256Topic { + Topic::new(format!( + "{}/{}/{}", + PUBSUB_PREEMPTIVE, + self.network_name.replace('/', "_"), + subnet_id.to_string().replace('/', "_") + )) + } + + /// Subscribe to a preemptive topic. + fn preemptive_subscribe(&mut self, subnet_id: SubnetID) -> Result<(), SubscriptionError> { + let topic = self.preemptive_topic(&subnet_id); + self.inner.subscribe(&topic)?; + self.preemptive_topics.insert(topic.hash(), subnet_id); + Ok(()) + } + + /// Subscribe to a preemptive topic. + fn preemptive_unsubscribe(&mut self, subnet_id: &SubnetID) -> anyhow::Result<()> { + let topic = self.preemptive_topic(subnet_id); + self.inner.unsubscribe(&topic)?; + self.preemptive_topics.remove(&topic.hash()); + Ok(()) } /// Construct the topic used to gossip about votes. @@ -183,18 +228,18 @@ impl Behaviour { } /// Subscribe to a voting topic. - fn voting_subscribe(&mut self, subnet_id: &SubnetID) -> anyhow::Result<()> { + fn voting_subscribe(&mut self, subnet_id: &SubnetID) -> Result<(), SubscriptionError> { let topic = self.voting_topic(subnet_id); - self.voting_topics.insert(topic.hash()); self.inner.subscribe(&topic)?; + self.voting_topics.insert(topic.hash()); Ok(()) } /// Unsubscribe from a voting topic. fn voting_unsubscribe(&mut self, subnet_id: &SubnetID) -> anyhow::Result<()> { let topic = self.voting_topic(subnet_id); - self.voting_topics.remove(&topic.hash()); self.inner.unsubscribe(&topic)?; + self.voting_topics.remove(&topic.hash()); Ok(()) } @@ -237,18 +282,23 @@ impl Behaviour { self.publish_membership() } - /// Make sure a subnet is not pruned. + /// Make sure a subnet is not pruned, so we always track its providers. + /// Also subscribe to pre-emptively published blocks of data. /// /// This method could be called in a parent subnet when the ledger indicates /// there is a known child subnet, so we make sure this subnet cannot be /// crowded out during the initial phase of bootstrapping the network. - pub fn pin_subnet(&mut self, subnet_id: SubnetID) { - self.provider_cache.pin_subnet(subnet_id) + pub fn pin_subnet(&mut self, subnet_id: SubnetID) -> Result<(), SubscriptionError> { + self.preemptive_subscribe(subnet_id.clone())?; + self.provider_cache.pin_subnet(subnet_id); + Ok(()) } - /// Make a subnet pruneable. - pub fn unpin_subnet(&mut self, subnet_id: &SubnetID) { - self.provider_cache.unpin_subnet(subnet_id) + /// Make a subnet pruneable and unsubscribe from pre-emptive data. + pub fn unpin_subnet(&mut self, subnet_id: &SubnetID) -> anyhow::Result<()> { + self.preemptive_unsubscribe(subnet_id)?; + self.provider_cache.unpin_subnet(subnet_id); + Ok(()) } /// Send a message through Gossipsub to let everyone know about the current configuration. @@ -287,6 +337,23 @@ impl Behaviour { } } + /// Publish arbitrary data to the pre-emptive topic of a subnet. + /// + /// We are not expected to be subscribed to this topic, only agents on the parent subnet are. + pub fn publish_preemptive(&mut self, subnet_id: SubnetID, data: Vec) -> anyhow::Result<()> { + let topic = self.preemptive_topic(&subnet_id); + match self.inner.publish(topic, data) { + Err(e) => { + stats::MEMBERSHIP_PUBLISH_FAILURE.inc(); + Err(anyhow!(e)) + } + Ok(_msg_id) => { + stats::MEMBERSHIP_PUBLISH_SUCCESS.inc(); + Ok(()) + } + } + } + /// Mark a peer as routable in the cache. /// /// Call this method when the discovery service learns the address of a peer. @@ -339,6 +406,8 @@ impl Behaviour { ); } } + } else if let Some(subnet_id) = self.preemptive_topics.get(&msg.topic) { + self.handle_preemptive_data(subnet_id.clone(), msg.data) } else { stats::MEMBERSHIP_UNKNOWN_TOPIC.inc(); warn!( @@ -380,6 +449,11 @@ impl Behaviour { self.outbox.push_back(Event::ReceivedVote(Box::new(record))) } + fn handle_preemptive_data(&mut self, subnet_id: SubnetID, data: Vec) { + self.outbox + .push_back(Event::ReceivedPreemptive(subnet_id, data)) + } + /// Handle new subscribers to the membership topic. fn handle_subscriber(&mut self, peer_id: PeerId, topic: TopicHash) { if topic == self.membership_topic.hash() { diff --git a/ipld/resolver/src/client.rs b/ipld/resolver/src/client.rs index 4160b2f0d..39c9ec001 100644 --- a/ipld/resolver/src/client.rs +++ b/ipld/resolver/src/client.rs @@ -81,8 +81,17 @@ impl Client { self.send_request(req) } + /// Publish a signed vote into a topic based on its subnet. pub fn publish_vote(&self, vote: SignedVoteRecord) -> anyhow::Result<()> { let req = Request::PublishVote(Box::new(vote)); self.send_request(req) } + + /// Publish pre-emptively to a subnet that agents in the parent subnet + /// would be subscribed to if they are interested in receiving data + /// before they would have to use [`Client::resolve`] instead. + pub fn publish_preemptive(&self, subnet_id: SubnetID, data: Vec) -> anyhow::Result<()> { + let req = Request::PublishPreemptive(subnet_id, data); + self.send_request(req) + } } diff --git a/ipld/resolver/src/service.rs b/ipld/resolver/src/service.rs index b7e329bfe..a88890f2a 100644 --- a/ipld/resolver/src/service.rs +++ b/ipld/resolver/src/service.rs @@ -89,6 +89,7 @@ pub(crate) enum Request { AddProvidedSubnet(SubnetID), RemoveProvidedSubnet(SubnetID), PublishVote(Box), + PublishPreemptive(SubnetID, Vec), PinSubnet(SubnetID), UnpinSubnet(SubnetID), Resolve(Cid, SubnetID, ResponseChannel), @@ -102,6 +103,8 @@ pub(crate) enum Request { pub enum Event { /// Received a vote about in a subnet about a CID. ReceivedVote(Box), + /// Received raw pre-emptive data published to a pinned subnet. + ReceivedPreemptive(SubnetID, Vec), } /// The `Service` handles P2P communication to resolve IPLD content by wrapping and driving a number of `libp2p` behaviours. @@ -360,6 +363,12 @@ impl Service

{ debug!("dropped received vote because there are no subscribers") } } + membership::Event::ReceivedPreemptive(subnet_id, data) => { + let event = Event::ReceivedPreemptive(subnet_id, data); + if self.event_tx.send(event).is_err() { + debug!("dropped received preemptive data because there are no subscribers") + } + } } } @@ -415,9 +424,21 @@ impl Service

{ warn!("failed to publish vote: {e}") } } - Request::PinSubnet(id) => self.membership_mut().pin_subnet(id), - Request::UnpinSubnet(id) => self.membership_mut().unpin_subnet(&id), - + Request::PublishPreemptive(subnet_id, data) => { + if let Err(e) = self.membership_mut().publish_preemptive(subnet_id, data) { + warn!("failed to publish pre-emptive data: {e}") + } + } + Request::PinSubnet(id) => { + if let Err(e) = self.membership_mut().pin_subnet(id) { + warn!("error pinning subnet: {e}") + } + } + Request::UnpinSubnet(id) => { + if let Err(e) = self.membership_mut().unpin_subnet(&id) { + warn!("error unpinning subnet: {e}") + } + } Request::Resolve(cid, subnet_id, response_channel) => { self.start_query(cid, subnet_id, response_channel) } diff --git a/ipld/resolver/tests/smoke.rs b/ipld/resolver/tests/smoke.rs index 06e4de90f..69cff36c5 100644 --- a/ipld/resolver/tests/smoke.rs +++ b/ipld/resolver/tests/smoke.rs @@ -238,8 +238,63 @@ async fn single_bootstrap_publish_receive_vote() { .expect("timeout receiving vote") .expect("error receiving vote"); - let Event::ReceivedVote(v) = event; - assert_eq!(&*v, vote.record()); + if let Event::ReceivedVote(v) = event { + assert_eq!(&*v, vote.record()); + } else { + panic!("unexpected {event:?}") + } +} + +/// Start two agents, pin a subnet, publish preemptively and receive. +#[tokio::test] +async fn single_bootstrap_publish_receive_preemptive() { + let _ = env_logger::builder().is_test(true).try_init(); + + // TODO: Get the seed from QuickCheck + let mut builder = ClusterBuilder::new(2); + + // Build a cluster of nodes. + for i in 0..builder.size { + builder.add_node(if i == 0 { None } else { Some(0) }); + } + + // Start the swarms. + let mut cluster = builder.run(); + + // Wait a little for the cluster to connect. + // TODO: Wait on some condition instead of sleep. + tokio::time::sleep(Duration::from_secs(1)).await; + + // Pin a subnet on the bootstrap node. + let subnet_id = make_subnet_id(1001); + + cluster.agents[0] + .client + .pin_subnet(subnet_id.clone()) + .expect("failed to pin subnet"); + + // TODO: Wait on some condition instead of sleep. + tokio::time::sleep(Duration::from_secs(1)).await; + + // Publish some content from the other agent. + let data = vec![1, 2, 3]; + cluster.agents[1] + .client + .publish_preemptive(subnet_id.clone(), data.clone()) + .expect("failed to send vote"); + + // Receive pre-emptive data.. + let event = timeout(Duration::from_secs(2), cluster.agents[0].events.recv()) + .await + .expect("timeout receiving data") + .expect("error receiving data"); + + if let Event::ReceivedPreemptive(s, d) = event { + assert_eq!(s, subnet_id); + assert_eq!(d, data); + } else { + panic!("unexpected {event:?}") + } } fn make_service(config: Config) -> (Service, TestBlockstore) { From 6ad6cfc615ba69a89a6f1b18e7d4265861257c68 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Mon, 3 Apr 2023 16:05:39 +0800 Subject: [PATCH 38/82] Integration tests (#143) * update config * format code * Update checkpoint.rs * use addr instead of id * initial commit * merge with main * initial commit (#142) * add integration testing * rename tests * update readme * move readme to /tests --------- Co-authored-by: Alfonso de la Rocha --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d33578fb0..a4dd869d9 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ build: cargo build -Z unstable-options --release --out-dir ./bin test: - cargo test --release --workspace + cargo test --release --workspace --lib # only run unit tests clean: cargo clean From 3aed5eb36bfd63e9d494fe8de993c8e67f912539 Mon Sep 17 00:00:00 2001 From: adlrocha Date: Wed, 5 Apr 2023 09:05:54 +0200 Subject: [PATCH 39/82] Instructions to run a multi-node subnet (#146) * chain head request dependent of checkpoint period * minor fix * remove outdated comment * use right type * revert dynamic wait timeout. It doesn't work * add multi-node subnet instructions to README * Update README.md * Update README.md cross-link to multi-validator * fix cp to overwrite existing scripts * Apply suggestions from code review Co-authored-by: Jorge Soares <547492+jsoares@users.noreply.github.com> * clarify list-subnets * Update README.md Co-authored-by: Jorge Soares <547492+jsoares@users.noreply.github.com> * Update daemon instructions * Update README.md Fix spaces * Clarify mining instructions * remove diagram ci check --------- Co-authored-by: Jorge Soares <547492+jsoares@users.noreply.github.com> --- scripts/install_infra.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install_infra.sh b/scripts/install_infra.sh index 35c4e65ae..56bc61825 100755 --- a/scripts/install_infra.sh +++ b/scripts/install_infra.sh @@ -8,5 +8,5 @@ cd ./lotus docker build -t eudico . cd .. mkdir -p ./bin -cp -r ./lotus/scripts/ipc ./bin/ipc-infra +cp -rf ./lotus/scripts/ipc/* ./bin/ipc-infra rm -rf ./lotus From c6e30c1077188ce45af1c7547ed1fb3cb0e42f7c Mon Sep 17 00:00:00 2001 From: adlrocha Date: Mon, 10 Apr 2023 15:36:22 +0200 Subject: [PATCH 40/82] use tag versioning for ipc-actors (#149) --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e7139d4d4..5defce241 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,8 +72,8 @@ fvm_ipld_blockstore = "0.1" fvm_ipld_encoding = "0.3" fvm_shared = { version = "=3.0.0-alpha.17", default-features = false } fil_actors_runtime = { git = "https://github.com/consensus-shipyard/fvm-utils", features = ["fil-actor"] } -ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git"} -ipc-subnet-actor = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = []} -ipc-gateway = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = []} +ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git", tag = "v0.1.0"} +ipc-subnet-actor = { git = "https://github.com/consensus-shipyard/ipc-actors.git", tag = "v0.1.0", features = []} +ipc-gateway = { git = "https://github.com/consensus-shipyard/ipc-actors.git",tag = "v0.1.0", features = []} libipld = { version = "0.14", default-features = false, features = ["dag-cbor"] } primitives = { git = "https://github.com/consensus-shipyard/fvm-utils"} From 2a4cef86b451c028fb6a9cb2dde8a660c5fbe8c7 Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Wed, 12 Apr 2023 16:01:55 +0200 Subject: [PATCH 41/82] Update install_infra.sh fix infra script --- scripts/install_infra.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/install_infra.sh b/scripts/install_infra.sh index 56bc61825..60e98aa66 100755 --- a/scripts/install_infra.sh +++ b/scripts/install_infra.sh @@ -8,5 +8,6 @@ cd ./lotus docker build -t eudico . cd .. mkdir -p ./bin +mkdir -p ./bin/ipc-infra cp -rf ./lotus/scripts/ipc/* ./bin/ipc-infra rm -rf ./lotus From e2529f7774a8017a1f42ef081500bf958844558d Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Fri, 14 Apr 2023 19:38:42 +0200 Subject: [PATCH 42/82] Split readme --- docs/{README.md => architecture.md} | 0 docs/subnet.md | 204 ++++++++++++++++++++++++++++ docs/troubleshooting.md | 40 ++++++ docs/usage.md | 73 ++++++++++ 4 files changed, 317 insertions(+) rename docs/{README.md => architecture.md} (100%) create mode 100644 docs/subnet.md create mode 100644 docs/troubleshooting.md create mode 100644 docs/usage.md diff --git a/docs/README.md b/docs/architecture.md similarity index 100% rename from docs/README.md rename to docs/architecture.md diff --git a/docs/subnet.md b/docs/subnet.md new file mode 100644 index 000000000..de0f45c51 --- /dev/null +++ b/docs/subnet.md @@ -0,0 +1,204 @@ +# Deploying a subnet + +To spawn a new subnet, our IPC agent should be connected to at least the subnet of the parent we want to spawn the subnet from. You can refer to the [readme](/readme) for information on how to run or connect to a rootnet. This instructions will assume the deployment of a subnet from `/root`, but the steps are equivalent for any other parent subnet. + +## Preliminaries + +### Exporting wallet keys + +In order to run a validator in a subnet, we'll need a set of keys to handle that validator. To export the validator key from a wallet that may live in another network into a file (like the wallet address we are using in the rootnet), we can use the following Lotus command: + +*Example*: +```bash +$ ./eudico wallet export --lotus-json > + +# Example execution +$ ./eudico wallet export --lotus-json t1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq > ~/.ipc-agent/wallet.key +``` + +If your daemon is running on a docker container, you can get the container id or name (provided also in the output of the infra scripts), and run the following command above inside a container outputting the exported private key into a file locally: +```bash +$ docker exec -it eudico wallet export --lotus-json > ~/.ipc-agent/wallet.key + +# Example execution +$ docker exec -it 84711d67cf162e30747c4525d69728c4dea8c6b4b35cd89f6d0947fee14bf908 eudico wallet export --lotus-json t1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq > ~/.ipc-agent/wallet.key +``` + +### Importing wallet keys + +Depending on whether the subnet is running inside a docker container or not, you may need to import keys into a node. You may use the following commands to import a wallet to a subnet node: + +```bash +# Bare: Import directly into eudico +$ ./eudico wallet import --lotus-json + +# Example execution +$ ./eudico wallet import --lotus-json ~/.ipc-agent/t1ivy6mo2ofxw4fdmft22nel66w63fb7cuyslm4cy.key +``` + +```bash +# Docker: Copy the wallet key into the container and import into eudico +$ docker cp : && docker exec -it eudico wallet import --format=json-lotus + +# Example execution +$ docker cp ~/.ipc-agent/t1ivy6mo2ofxw4fdmft22nel66w63fb7cuyslm4cy.key 91d2af805346:/input.key && docker exec -it 91d2af805346 eudico wallet import --format=json-lotus input.key +``` + +## Running a simple subnet with a single validator + +This section provides instructions for spawning a simple subnet with a single validator. If you'd like to spawn a subnet with multiple validators in a Docker setup, read and understand this section first but then follow the steps under [the multi-validator section below](#running-a-subnet-with-several-validators). + +### Spawning a subnet actor + +To run a subnet the first thing is to configure and create the subnet actor that will govern the subnet's operation. + +```bash +$ ./bin/ipc-agent subnet create -p -n --min-validator-stake 1 --min-validators --finality-threshold --check-period + +# Example execution +$ ./bin/ipc-agent subnet create -p /root -n test --min-validator-stake 1 --min-validators 0 --finality-threshold 10 --check-period 10 +[2023-03-21T09:32:58Z INFO ipc_agent::cli::commands::manager::create] created subnet actor with id: /root/t01002 +``` +This command deploys a subnet actor for a new subnet from the `root`, with a human-readable name `test`, that requires at least `1` validator to join the subnet to be able to mine new blocks, and with a checkpointing period to the parent of `10` blocks. We can see that the output of this command is the ID of the new subnet. + +### Exporting your wallet + +Let's export the default wallet (or other wallet you'd like to use) for use in the subnet validator. +```bash +$ ./eudico wallet export --lotus-json `./eudico wallet default` > ~/.ipc-agent/wallet.key +``` + +Make sure that your wallet has enough funds to put up the collateral to join the subnet. + +### Deploying a subnet node + +Before joining a new subnet, our node for that subnet must be initialised. For the deployment of subnet daemons we also provide a convenient infra script: +```bash +$ ./bin/ipc-infra/run-subnet-docker.sh + +# Example execution +$ ./bin/ipc-infra/run-subnet-docker.sh 1250 1350 /root/t01002 ~/.ipc-agent/wallet.key +(...) +>>> Subnet /root/t01002 daemon running in container: 22312347b743f1e95e50a31c1f47736580c9a84819f41cb4ed3d80161a0d750f (friendly name: ipc_root_t01002_1239) +>>> Token to /root/t01002 daemon: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJBbGxvdyI6WyJyZWFkIiwid3JpdGUiLCJzaWduIiwiYWRtaW4iXX0.TnoDqZJ1fqdkr_oCHFEXvdwU6kYR7Va_ALyEuoPnksA +>>> Default wallet: t1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq +>>> Subnet validator info: +/dns/host.docker.internal/tcp/1349/p2p/12D3KooWN5hbWkCxwvrX9xYxMwFbWm2Jpa1o4qhwifmSw3Fb +>>> API listening in host port 1250 +>>> Validator listening in host port 1350 +``` +> 💡 Beware: This script doesn't support the use of relative paths for the wallet path. + +The end of the log of the execution of this script provides a bit more of information than the previous one as it is implemented to be used for production deployments: API and auth tokens for the daemon, default validator wallet used, the multiaddress where the validator is listening, etc. To configure our IPC agent with this subnet daemon, we need to once again update our IPC agent with the relevant information. In this case, for the Example execution above we need to add the following section to the end of our config file. + +*Example*: +```toml +[[subnets]] +id = "/root/t01002" +gateway_addr = "t064" +network_name = "test" +jsonrpc_api_http = "http://127.0.0.1:1250/rpc/v1" +auth_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJBbGxvdyI6WyJyZWFkIiwid3JpdGUiLCJzaWduIiwiYWRtaW4iXX0.TnoDqZJ1fqdkr_oCHFEXvdwU6kYR7Va_ALyEuoPnksA" +accounts = ["t1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq"] +``` +> 💡 Remember to run `./bin/ipc-agent config reload` for changes in the config of the agent to be picked up by the daemon. + +### Joining a subnet + +With the daemon for the subnet deployed, we can join the subnet: +```bash +$ ./bin/ipc-agent subnet join --subnet --collateral --validator-net-addr + +# Example execution +$ ./bin/ipc-agent subnet join --subnet /root/t01002 --collateral 2 --validator-net-addr /dns/host.docker.internal/tcp/1349/p2p/12D3KooWN5hbWkCxwvrX9xYxMwFbWm2Jpa1o4qhwifmSw3Fb +``` +This command specifies the subnet to join, the amount of collateral to provide and the validator net address used by other validators to dial them. We can pick up this information from the execution of the script above or running `eudico mir validator config validator-addr` from your deployment. Bear in mind that the multiaddress provided for the validator needs to be accessible publicly by other validators. + +### Mining in a subnet + +With our subnet daemon deployed, and having joined the network, as the minimum number of validators we set for our subnet is 0, we can start mining and creating new blocks in the subnet. Doing so is a simple as running the following script using as an argument the container of our subnet node: +```bash +$ ./bin/ipc-infra/mine-subnet.sh + +# Example execution +$ ./bin/ipc-infra/mine-subnet.sh 84711d67cf162e30747c4525d69728c4dea8c6b4b35cd89f6d0947fee14bf908 +``` + +The mining process is currently run in the foreground in interactive mode. Consider using `nohup ./bin/ipc-infra/mine-subnet.sh` or tmux to run the process in the background and redirect the logs to some file. + +## Running a subnet with several validators + +In this section, we will deploy a subnet where the IPC agent is responsible for handling more than one validator in the subnet. We are going to deploy a subnet with 3 validators. The first thing we'll need to do is create a new wallet for every validator we want to run. We can do this directly through the agent with the following command (3x): +```bash +$ ./bin/ipc-agent wallet new --key-type secp256k1 --subnet /root +``` + +We also need to provide with some funds our wallets so they can put collateral to join the subnet. According to the rootnet you are connected to, you may need to get some funds from the faucet, or send some from your main wallet. Funds can be sent from your main wallet also through the agent with (3x, adjusting `target-wallet` for each): +```bash +$ ./bin/ipc-agent subnet send-value --subnet /root --to +``` + +With this, we can already create the subnet with `/root` as its parent. We are going to set the `--min-validators 2` so no new blocks can be created without this number of validators in the subnet. +```bash +./bin/ipc-agent subnet create -p /root -n test --min-validator-stake 1 --min-validators 2 --finality-threshold 10 --check-period 10 +``` +### Deploying the infrastructure + +In order to deploy the 3 validators for the subnet, we will have to first export the keys from our root node so we can import them to our validators. Depending on how you are running your rootnet node you'll have to make a call to the docker container, or your nodes API. More information about exporting keys from your node can be found under [this section](#Exporting-wallet-keys). + +With the keys conveniently exported, we can deploy the subnet nodes using the `infra-scripts`. The following code snippet showcases the deployment of five Example nodes. Note that each node should be importing a different wallet key for their validator, and should be exposing different ports for their API and validators. + +*Example*: +```bash +$ ./bin/ipc-infra/run-subnet-docker.sh 1251 1351 /root/t01002 ~/.ipc-agent/wallet1.key +$ ./bin/ipc-infra/run-subnet-docker.sh 1252 1352 /root/t01002 ~/.ipc-agent/wallet2.key +$ ./bin/ipc-infra/run-subnet-docker.sh 1253 1353 /root/t01002 ~/.ipc-agent/wallet3.key +``` +If the deployment is successful, each of these nodes should return the following output at the end of their logs. Note down this information somewhere as we will need it to conveniently join our validators to the subnet. + +*Example*: +``` +>>> Subnet /root/t01002 daemon running in container: 91d2af80534665a8d9a20127e480c16136d352a79563e74ee3c5497d50b9eda8 (friendly name: ipc_root_t01002_1240) +>>> Token to /root/t01002 daemon: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJBbGxvdyI6WyJyZWFkIiwid3JpdGUiLCJzaWduIiwiYWRtaW4iXX0.JTiumQwFIutkTb0gUC5JWTATs-lUvDaopEDE0ewgzLk +>>> Default wallet: t1ivy6mo2ofxw4fdmft22nel66w63fb7cuyslm4cy +>>> Subnet subnet validator info: +/dns/host.docker.internal/tcp/1359/p2p/12D3KooWEJXcSPw6Yv4jDk52xvp2rdeG3J6jCPX9AgBJE2mRCVoR +>>> API listening in host port 1251 +>>> Validator listening in host port 1351 +``` + +### Configuring the agent +To configure the agent for its use with all the validators, we need to connect to the RPC API of one of the validators, and import all of the wallets of the validators in that node, so the agent is able through the same API to act on behalf of any validator. More information about importing keys can be found in [this section](#Importing-wallet-keys). + +Here's an example of the configuration connecting to the RPC of the first validator, and configuring all the wallets for the validators in the subnet. +```toml +[[subnets]] +id = "/root/t01002" +gateway_addr = "t064" +network_name = "test" +jsonrpc_api_http = "http://127.0.0.1:1240/rpc/v1" +auth_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJBbGxvdyI6WyJyZWFkIiwid3JpdGUiLCJzaWduIiwiYWRtaW4iXX0.JTiumQwFIutkTb0gUC5JWTATs-lUvDaopEDE0ewgzLk" +accounts = ["t1ivy6mo2ofxw4fdmft22nel66w63fb7cuyslm4cy", "t1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq", "t1nv5jrdxk4ljzndaecfjgmu35k6iz54pkufktvua"] +``` +Remember to run `./bin/ipc-agent config reload` for your agent to pick up the latest changes for the config. + +### Joining the subnet +All the infrastructure for the subnet is now deployed, and we can join our validators to the subnet. For this, we need to send a `join` command from each of our validators from their validator wallet addresses providing the validators multiaddress. We can get the validator multiaddress from the output of the script we ran to deploy the infrastructure. + +This is the command that needs to be executed for every validator to join the subnet: +```bash +$ ./bin/ipc-agent subnet join --from --subnet /root/t01002 --collateral --validator-net-addr + +# Example execution +$ ./bin/ipc-agent subnet join --from t1ivy6mo2ofxw4fdmft22nel66w63fb7cuyslm4cy --subnet /root/t01002 --collateral 2 --validator-net-addr /dns/host.docker.internal/tcp/1359/p2p/12D3KooWEJXcSPw6Yv4jDk52xvp2rdeG3J6jCPX9AgBJE2mRCVoR +``` +Remember doing the above step for the 3 validators. + +### Mining in subnet +We have everything in place now to start mining. Mining is as simple as running the following script for each of the validators, passing the container id/name: +```bash +$ ./bin/ipc-infra/mine-subnet.sh +``` + +The mining process is currently run in the foreground in interactive mode. Consider using `nohup ./bin/ipc-infra/mine-subnet.sh` or screen to run the process in the background and redirect the logs to some file as handling the mining process of the three validators in the foreground may be quite cumbersome. + diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 000000000..d3a2aa568 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,40 @@ +# Troubleshooting + +## I need to upgrade my IPC agent + +Sometimes, things break, and we'll need to push a quick path to fix some bug. If this happens, and you need to upgrade your agent version, kill you agent daemon if you have any running, pull the latest changes from this repo, build the binary, and start your daemon again. This should pick up the latest version for the agent. In the future, we will provide a better way to upgrade your agent. +```bash +# Pull latest changes +$ git pull +# Build the agent +$ make build +# Restart the daemon +$ ./bin/ipc-agent daemon +``` + +## The eudico image is not building successful + +`make install-infra` may fail and not build the `eudico` image if your system is not configured correctly. If this happens, you can always try to build the image yourself to have a finer-grain report of the issues to help you debug them. For this you can [follow these instructions](https://github.com/consensus-shipyard/lotus/blob/spacenet/scripts/ipc/README.md). + +High-level you just need to clone the [eudico repo](https://github.com/consensus-shipyard/lotus), and run `docker build -t eudico .` in the root of the repo. + +## My subnet node doesn't start + +Either because the dockerized subnet node after running `./bin/ipc-infra/run-subnet-docker.sh` gets stuck waiting for the API to be started with the following message: +``` +Not online yet... (could not get API info for FullNode: could not get api endpoint: API not running (no endpoint)) +``` +Or because when the script finishes no validator address has been reported as expected by the logs, the best way to debug this situation is to attach to the docker container and check the logs with the following command: +```bash +$ docker exec -it bash + +# Inside the container +tmux a +``` +Generally, the issue is that: +- You haven't passed the validator key correctly and it couldn't be imported. +- There was some network instability, and lotus params couldn't be downloaded successfully. + +## My agent is not submitting checkpoints after an error + +Try running `./bin/ipc-agent config reload`, this should pick up the latest config and restart all checkpointing processes. If the error has been fixed or it was an network instability between the agent and your subnet daemon, checkpoints should start being committed again seamlessly. diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 000000000..8a65d2f3e --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,73 @@ +# Using the IPC Agent + +## Joining a subnet + +With the daemon for a subnet deployed, we can join the subnet: +```bash +$ ./bin/ipc-agent subnet join --subnet --collateral --validator-net-addr + +# Example execution +$ ./bin/ipc-agent subnet join --subnet /root/t01002 --collateral 2 --validator-net-addr /dns/host.docker.internal/tcp/1349/p2p/12D3KooWN5hbWkCxwvrX9xYxMwFbWm2Jpa1o4qhwifmSw3Fb +``` +This command specifies the subnet to join, the amount of collateral to provide and the validator net address used by other validators to dial them. + +## Sending funds in a subnet + +The agent provides a command to conveniently exchange funds between addresses of the same subnet. This can be achieved through the following command: +```bash +$ ./bin/ipc-agent subnet send-value --subnet --to + +# Example execution +$ ./bin/ipc-agent subnet send-value --subnet /root/t01002 --to t1xbevqterae2tanmh2kaqksnoacflrv6w2dflq4i 10 +``` + +## Leaving a subnet + +To leave a subnet, the following agent command can be used: +```bash +$ ./bin/ipc-agent subnet leave --subnet + +# Example execution +$ ./bin/ipc-agent subnet leave --subnet /root/t01002 +``` +Leaving a subnet will release the collateral for the validator and remove all the validation rights from its account. This means that if you have a validator running in that subnet, its validation process will immediately terminate. + +### Listing active subnets + +As a sanity-check that we have joined the subnet successfully and that we provided enough collateral to register the subnet to IPC, we can list the child subnets of our parent with the following command: + +```bash +$ ./bin/ipc-agent list-subnets --gateway-address= --subnet= + +# Sample execution +$ ./bin/ipc-agent list-subnets --gateway-address=t064 --subnet=/root +[2023-03-30T17:00:25Z INFO ipc_agent::cli::commands::manager::list_subnets] /root/t01003 - status: 0, collateral: 2 FIL, circ.supply: 0.0 FIL +``` + +This command only shows subnets that have been registered to the gateway, i.e. that have provided enough collateral to participate in the IPC protocol and haven't been killed. It is not an exhaustive list of all of the subnet actors deployed over the network. + +### Changing subnet validator network address + +It may be the case that while joining the subnet, you didn't set the multiaddress for your validator correctly and you need to update it. You'll realize that the network address of your validator is not configured correctly, because your agent throws an error when trying to connect to your subnet node, or starting the validator in your subnet throws a network-related error. + +Changing the validator is as simple as running the following command: +```bash +$ ./bin/ipc-agent subnet set-validator-net-addr --subnet --validator-net-addr + +# Example execution +$ ./bin/ipc-agent subnet set-validator-net-addr --subnet /root/t01002 --validator-net-addr "/dns/host.docker.internal/tcp/1349/p2p/12D3KooWDeN3bTvZEH11s9Gq5bDeZZLKgRZiMDcy2KmA6mUaT9KE" +``` + +### Listing checkpoints from a subnet + +Subnets are periodically committing checkpoints to their parent every `check-period` (parameter defined when creating the subnet). If you want to inspect the information of a range of checkpoints committed in the parent for a subnet, you can use the `checkpoint list` command provided by the agent as follows: +```bash +# List checkpoints between two epochs for a subnet +$ ./bin/ipc-agent checkpoint list --from-epoch --to-epoch --subnet + +# Example execution +$ ./bin/ipc-agent checkpoint list --from-epoch 0 --to-epoch 100 --subnet root/t01002 +[2023-03-29T12:43:42Z INFO ipc_agent::cli::commands::manager::list_checkpoints] epoch 0 - prev_check={"/":"bafy2bzacedkoa623kvi5gfis2yks7xxjl73vg7xwbojz4tpq63dd5jpfz757i"}, cross_msgs=null, child_checks=null +[2023-03-29T12:43:42Z INFO ipc_agent::cli::commands::manager::list_checkpoints] epoch 10 - prev_check={"/":"bafy2bzacecsatvda6lodrorh7y7foxjt3a2dexxx5jiyvtl7gimrrvywb7l5m"}, cross_msgs=null, child_checks=null +[2023-03-29T12:43:42Z INFO ipc_agent::cli::commands::manager::list_checkpoints] epoch 30 - prev_check={"/":"bafy2bzaceauzdx22hna4e4cqf55jqmd64a4fx72sxprzj72qhrwuxhdl7zexu"}, cross_msgs=null, child_checks=null +``` \ No newline at end of file From 6f8c3b76cb0950a8013f4373dcffb0157f8a8791 Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Fri, 14 Apr 2023 20:31:53 +0200 Subject: [PATCH 43/82] Update arch title --- docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture.md b/docs/architecture.md index 20887b0ac..34b5558e3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,4 +1,4 @@ -# IPC Agent +# IPC Agent Architecture The IPC Agent is a process faciliting the participation of Filecoin clients like Lotus in the InterPlanetary Consensus (formerly Hierarchical Consensus). From 1994119b51618b02bc6fa70ae021f7364bd11013 Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Mon, 17 Apr 2023 00:52:45 +0200 Subject: [PATCH 44/82] Update usage.md --- docs/usage.md | 43 ++++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 8a65d2f3e..fbebdb724 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,8 +1,22 @@ # Using the IPC Agent +## Listing active subnets + +As a sanity-check that we have joined the subnet successfully and that we provided enough collateral to register the subnet to IPC, we can list the child subnets of our parent with the following command: + +```bash +$ ./bin/ipc-agent list-subnets --gateway-address= --subnet= + +# Sample execution +$ ./bin/ipc-agent list-subnets --gateway-address=t064 --subnet=/root +[2023-03-30T17:00:25Z INFO ipc_agent::cli::commands::manager::list_subnets] /root/t01003 - status: 0, collateral: 2 FIL, circ.supply: 0.0 FIL +``` + +This command only shows subnets that have been registered to the gateway, i.e. that have provided enough collateral to participate in the IPC protocol and haven't been killed. It is not an exhaustive list of all of the subnet actors deployed over the network. + ## Joining a subnet -With the daemon for a subnet deployed, we can join the subnet: +With the daemon for a subnet deployed (see [instructions](/docs/subnet.md)), one can join the subnet: ```bash $ ./bin/ipc-agent subnet join --subnet --collateral --validator-net-addr @@ -32,31 +46,6 @@ $ ./bin/ipc-agent subnet leave --subnet /root/t01002 ``` Leaving a subnet will release the collateral for the validator and remove all the validation rights from its account. This means that if you have a validator running in that subnet, its validation process will immediately terminate. -### Listing active subnets - -As a sanity-check that we have joined the subnet successfully and that we provided enough collateral to register the subnet to IPC, we can list the child subnets of our parent with the following command: - -```bash -$ ./bin/ipc-agent list-subnets --gateway-address= --subnet= - -# Sample execution -$ ./bin/ipc-agent list-subnets --gateway-address=t064 --subnet=/root -[2023-03-30T17:00:25Z INFO ipc_agent::cli::commands::manager::list_subnets] /root/t01003 - status: 0, collateral: 2 FIL, circ.supply: 0.0 FIL -``` - -This command only shows subnets that have been registered to the gateway, i.e. that have provided enough collateral to participate in the IPC protocol and haven't been killed. It is not an exhaustive list of all of the subnet actors deployed over the network. - -### Changing subnet validator network address - -It may be the case that while joining the subnet, you didn't set the multiaddress for your validator correctly and you need to update it. You'll realize that the network address of your validator is not configured correctly, because your agent throws an error when trying to connect to your subnet node, or starting the validator in your subnet throws a network-related error. - -Changing the validator is as simple as running the following command: -```bash -$ ./bin/ipc-agent subnet set-validator-net-addr --subnet --validator-net-addr - -# Example execution -$ ./bin/ipc-agent subnet set-validator-net-addr --subnet /root/t01002 --validator-net-addr "/dns/host.docker.internal/tcp/1349/p2p/12D3KooWDeN3bTvZEH11s9Gq5bDeZZLKgRZiMDcy2KmA6mUaT9KE" -``` ### Listing checkpoints from a subnet @@ -70,4 +59,4 @@ $ ./bin/ipc-agent checkpoint list --from-epoch 0 --to-epoch 100 --subnet root/t0 [2023-03-29T12:43:42Z INFO ipc_agent::cli::commands::manager::list_checkpoints] epoch 0 - prev_check={"/":"bafy2bzacedkoa623kvi5gfis2yks7xxjl73vg7xwbojz4tpq63dd5jpfz757i"}, cross_msgs=null, child_checks=null [2023-03-29T12:43:42Z INFO ipc_agent::cli::commands::manager::list_checkpoints] epoch 10 - prev_check={"/":"bafy2bzacecsatvda6lodrorh7y7foxjt3a2dexxx5jiyvtl7gimrrvywb7l5m"}, cross_msgs=null, child_checks=null [2023-03-29T12:43:42Z INFO ipc_agent::cli::commands::manager::list_checkpoints] epoch 30 - prev_check={"/":"bafy2bzaceauzdx22hna4e4cqf55jqmd64a4fx72sxprzj72qhrwuxhdl7zexu"}, cross_msgs=null, child_checks=null -``` \ No newline at end of file +``` From 44892c9df801114311bc895cd511a1e779664638 Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Mon, 17 Apr 2023 00:54:02 +0200 Subject: [PATCH 45/82] Update troubleshooting.md --- docs/troubleshooting.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index d3a2aa568..078f66f9c 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -38,3 +38,15 @@ Generally, the issue is that: ## My agent is not submitting checkpoints after an error Try running `./bin/ipc-agent config reload`, this should pick up the latest config and restart all checkpointing processes. If the error has been fixed or it was an network instability between the agent and your subnet daemon, checkpoints should start being committed again seamlessly. + +### I set the wrong validator address or need to change it + +It may be the case that while joining the subnet, you didn't set the multiaddress for your validator correctly and you need to update it. You'll realize that the network address of your validator is not configured correctly, because your agent throws an error when trying to connect to your subnet node, or starting the validator in your subnet throws a network-related error. + +Changing the validator is as simple as running the following command: +```bash +$ ./bin/ipc-agent subnet set-validator-net-addr --subnet --validator-net-addr + +# Example execution +$ ./bin/ipc-agent subnet set-validator-net-addr --subnet /root/t01002 --validator-net-addr "/dns/host.docker.internal/tcp/1349/p2p/12D3KooWDeN3bTvZEH11s9Gq5bDeZZLKgRZiMDcy2KmA6mUaT9KE" +``` From 838dcfafaefc0148fd530678eb1c7cfbda976913 Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Mon, 17 Apr 2023 14:31:13 +0200 Subject: [PATCH 46/82] Reorg docs --- docs/architecture.md | 4 +- docs/quickstart.md | 220 ++++++++++++++++++++++++++++++++++++++++ docs/subnet.md | 6 +- docs/troubleshooting.md | 4 +- 4 files changed, 230 insertions(+), 4 deletions(-) create mode 100644 docs/quickstart.md diff --git a/docs/architecture.md b/docs/architecture.md index 34b5558e3..b90cc9608 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,8 +1,10 @@ # IPC Agent Architecture +>💡 For background and setup information, make sure to start with the [README](/README.md). + The IPC Agent is a process faciliting the participation of Filecoin clients like Lotus in the InterPlanetary Consensus (formerly Hierarchical Consensus). -Please refer to the [IPD Agent Design](https://docs.google.com/document/d/14lkRRv6MQYnuEfp2GoGngdD8Q5YgfE38D8HTZWKgKf4) document for details on the agent. +Please refer to the [IPC Agent Design](https://docs.google.com/document/d/14lkRRv6MQYnuEfp2GoGngdD8Q5YgfE38D8HTZWKgKf4) document for details on the agent. # IPLD Resolver diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 000000000..5f951d995 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,220 @@ +# IPC Quick Start: zero-to-subnet + +Ready to test the waters with your first subnet? This guide will deploy a subnet with multiple local validators orchestrated by the same IPC agent. This subnet will be anchored to the public Spacenet. This will be a minimal example and may not work on all systems. The full documentation provides more details on each step. + +Several steps in this guide involve running long-lived processes. These commands are usually prefaced with `nohup` so that they run in the background. For improved usability, we recommend instead using a `screen` session and spawning a new window for each of these commands. In that case, you should omit `nohup` and `&`. + +>💡 For more background and information, make sure to start with the [README](/README.md). + +## Step 0: Prepare your system + +We assume a Ubuntu Linux instance when discussing prerequisites, but annotate steps with system-specificity and links to detailed multi-OS instructions. Exact procedures will vary for other systems, so please follow the links if running something different. More details on IPC-specific requirements can also be found in the [IPC Agent readme](https://github.com/consensus-shipyard/ipc-agent). + +* Install basic dependencies [Ubuntu/Debian] ([details](https://lotus.filecoin.io/lotus/install/prerequisites/#supported-platforms)) +```bash +$ sudo apt update && sudo apt install build-essential libssl-dev mesa-opencl-icd ocl-icd-opencl-dev gcc git bzr jq pkg-config curl clang hwloc libhwloc-dev wget ca-certificates gnupg -y +``` + +* Install Rust [Linux] ([details](https://www.rust-lang.org/tools/install)) +```bash +$ curl https://sh.rustup.rs -sSf | sh +$ source "$HOME/.cargo/env" +$ rustup target add wasm32-unknown-unknown +``` + +* Install Go [Linux] ([details](https://go.dev/doc/install)) +```bash +$ curl -fsSL https://golang.org/dl/go1.19.7.linux-amd64.tar.gz | sudo tar -xz -C /usr/local +$ echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc && source ~/.bashrc +``` + +* Install Docker Engine [Ubuntu] ([details](https://docs.docker.com/engine/install/)) +```bash +$ sudo install -m 0755 -d /etc/apt/keyrings +$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg +$ sudo chmod a+r /etc/apt/keyrings/docker.gpg +$ echo \ + "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ + "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +$ sudo apt-get update && sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y +$ sudo usermod -aG docker $USER && newgrp docker +``` + + +## Step 1: Build the IPC stack + +Next, we'll download and build the different components (IPC agent, docker images, and eudico). + +* Pick a folder where to build the IPC stack. In this example, we'll go with `~/ipc/`. +```bash +$ mkdir -p ~/ipc/ && cd ~/ipc/ +``` +* Download and compile the IPC Agent (might take a while) +```bash +$ git clone https://github.com/consensus-shipyard/ipc-agent.git +$ (cd ipc-agent && make build && make install-infra) +``` +* Download and compile eudico (might take a while) +```bash +$ git clone https://github.com/consensus-shipyard/lotus.git +$ (cd lotus && make spacenet) +``` + + +## Step 2: Deploy a Spacenet node + +Let's deploy a eudico instance on Spacenet and configure the IPC Agent to interact with it. + +* Start your eudico instance (might take a while to sync the chain) +```bash +$ nohup ./lotus/eudico mir daemon --bootstrap & +``` +* Get configuration parameters +```bash +$ ./lotus/eudico auth create-token --perm admin +$ ./lotus/eudico wallet new +``` +* Configure your IPC Agent +```bash +$ ./ipc-agent/bin/ipc-agent config init +$ nano ~/.ipc-agent/config.toml +``` +* Replace the content of `config.toml` with the text below, substituting the token and wallet retrieved above. +```toml +[server] +json_rpc_address = "0.0.0.0:3030" + +[[subnets]] +id = "/root" +gateway_addr = "t064" +network_name = "root" +jsonrpc_api_http = "http://127.0.0.1:1234/rpc/v1" +auth_token = "" +accounts = [""] +``` +* Start your IPC Agent +```bash +$ nohup ./ipc-agent/bin/ipc-agent daemon & +``` + + +## Step 3: Fund your account + +* Obtain some Spacenet FIL by requesting it from the [faucet](https://spacenet.consensus.ninja/), using your wallet address. + + +## Step 4: Create the subnet + +* The next step is to create a subnet under `/root` +```bash +$ ./ipc-agent/bin/ipc-agent subnet create -p /root -n andromeda --min-validator-stake 1 --min-validators 2 --finality-threshold 10 --check-period 10 +``` +* Make a note of the address of the subnet you created (`/root/`) + + +## Step 5: Create and export validator wallets + +Although we set a minimum of 2 active validators in the previous, we'll deploy 3 validators to add some redundancy. + +* First, we'll need to create a wallet for each validator +```bash +$ ./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 --subnet /root +$ ./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 --subnet /root +$ ./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 --subnet /root +``` +* Export each wallet (WALLET_1, WALLET_2, and WALLET_3) by substituting their addresses below +```bash +$ ./lotus/eudico wallet export --lotus-json > ~/.ipc-agent/wallet1.key +$ ./lotus/eudico wallet export --lotus-json > ~/.ipc-agent/wallet2.key +$ ./lotus/eudico wallet export --lotus-json > ~/.ipc-agent/wallet3.key +``` +* We also need to fund the wallets with enough collateral to; we'll send the funds from our default wallet +```bash +$ ./ipc-agent/bin/ipc-agent subnet send-value --subnet /root --to 2 +$ ./ipc-agent/bin/ipc-agent subnet send-value --subnet /root --to 2 +$ ./ipc-agent/bin/ipc-agent subnet send-value --subnet /root --to 2 +``` + + +## Step 6: Deploy the infrastructure + +We can deploy the subnet nodes. Note that each node should be importing a different wallet key for their validator, and should be exposing different ports. If these ports are unavailable in your system, please pick different ones. + +* Deploy and run a container for each validator, importing the corresponding wallet keys +```bash +$ ./ipc-agent/bin/ipc-infra/run-subnet-docker.sh 1251 1351 /root/ ~/.ipc-agent/wallet1.key +$ ./ipc-agent/bin/ipc-infra/run-subnet-docker.sh 1252 1352 /root/ ~/.ipc-agent/wallet2.key +$ ./ipc-agent/bin/ipc-infra/run-subnet-docker.sh 1253 1353 /root/ ~/.ipc-agent/wallet3.key +``` +* If the deployment is successful, each of these nodes should return the following output at the end of their logs. Save the information for the next step. +``` +>>> Subnet /root/ daemon running in container: (friendly name: ) +>>> Token to /root/ daemon: +>>> Default wallet: +>>> Subnet subnet validator info: + +>>> API listening in host port +>>> Validator listening in host port +``` + + +## Step 7: Configure the IPC agent + +For ease of use, we'll import the remaining keys into the first validator, via which the IPC Agent will act on behalf of all. + +* Copy the wallet keys into the docker container and import them +```bash +$ docker cp ~/.ipc-agent/wallet2.key :/input.key && docker exec -it eudico wallet import --format=json-lotus input.key +$ docker cp ~/.ipc-agent/wallet3.key :/input.key && docker exec -it eudico wallet import --format=json-lotus input.key +``` +* Edit the IPC agent configuration `config.toml` +```bash +$ nano ~/.ipc-agent/config.toml +``` +* Append the new subnet to the configuration +```toml +[[subnets]] +id = "/root/" +gateway_addr = "t064" +network_name = "andromeda" +jsonrpc_api_http = "http://127.0.0.1:1251/rpc/v1" +auth_token = "" +accounts = ["", "", ""] +``` +* Reload the config +```bash +$ ./ipc-agent/bin/ipc-agent config reload +``` + + +## Step 8: Join the subnet + +All the infrastructure for the subnet is now deployed, and we can join our validators to the subnet. For this, we need to send a `join` command from each of our validators from their validator wallet addresses providing the validators multiaddress. + +* Join the subnet with each validators +```bash +$ ./ipc-agent/bin/ipc-agent subnet join --from --subnet /root/ --collateral 1 --validator-net-addr +$ ./ipc-agent/bin/ipc-agent subnet join --from --subnet /root/ --collateral 1 --validator-net-addr +$ ./ipc-agent/bin/ipc-agent subnet join --from --subnet /root/ --collateral 1 --validator-net-addr +``` + + +## Step 9: Start validating! + +We have everything in place now to start validating. This is as simple as running the following script for each of the validators, passing the container name (or id): +```bash +$ nohup ./ipc-agent/bin/ipc-infra/mine-subnet.sh & +$ nohup ./ipc-agent/bin/ipc-infra/mine-subnet.sh & +$ nohup ./ipc-agent/bin/ipc-infra/mine-subnet.sh & +``` + + +## Step 10: What now? + +* Check that the subnet is running +```bash +$ ./ipc-agent/bin/ipc-agent subnet list --gateway-address t064 --subnet /root +``` +* If something went wrong, please have a look at the [README](https://github.com/consensus-shipyard/ipc-agent). If it doesn't help, please join us in #ipc-help. In either case, let us know your experience! +* Please note that to repeat this guide or spawn a new subnet, you may need to change the parameters or reset your system. diff --git a/docs/subnet.md b/docs/subnet.md index de0f45c51..bfc1471cc 100644 --- a/docs/subnet.md +++ b/docs/subnet.md @@ -1,6 +1,8 @@ -# Deploying a subnet +# Deploying an IPC subnet -To spawn a new subnet, our IPC agent should be connected to at least the subnet of the parent we want to spawn the subnet from. You can refer to the [readme](/readme) for information on how to run or connect to a rootnet. This instructions will assume the deployment of a subnet from `/root`, but the steps are equivalent for any other parent subnet. +>💡 For background and setup information, make sure to start with the [README](/README.md). + +To spawn a new subnet, our IPC agent should be connected to at least the subnet of the parent we want to spawn the subnet from. You can refer to the [readme](/README.md) for information on how to run or connect to a rootnet. This instructions will assume the deployment of a subnet from `/root`, but the steps are equivalent for any other parent subnet. ## Preliminaries diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 078f66f9c..77ccbf497 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,4 +1,6 @@ -# Troubleshooting +# Troubleshooting IPC + +>💡 For background and setup information, make sure to start with the [README](/README.md). ## I need to upgrade my IPC agent From b764c5e57cf0961b8ad63b579faecc609b735979 Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Mon, 17 Apr 2023 14:31:22 +0200 Subject: [PATCH 47/82] Reorg docs --- docs/usage.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/usage.md b/docs/usage.md index fbebdb724..451e005bb 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,5 +1,7 @@ # Using the IPC Agent +>💡 For background and setup information, make sure to start with the [README](/README.md). + ## Listing active subnets As a sanity-check that we have joined the subnet successfully and that we provided enough collateral to register the subnet to IPC, we can list the child subnets of our parent with the following command: From 7c8b738749f166460ffda78ac7cdd37e058072c8 Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Mon, 17 Apr 2023 14:48:59 +0200 Subject: [PATCH 48/82] Update links --- docs/quickstart.md | 6 +++--- docs/subnet.md | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 5f951d995..c7af3a3d3 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -1,14 +1,14 @@ # IPC Quick Start: zero-to-subnet +>💡 Background and detailed are available in the [README](/README.md). + Ready to test the waters with your first subnet? This guide will deploy a subnet with multiple local validators orchestrated by the same IPC agent. This subnet will be anchored to the public Spacenet. This will be a minimal example and may not work on all systems. The full documentation provides more details on each step. Several steps in this guide involve running long-lived processes. These commands are usually prefaced with `nohup` so that they run in the background. For improved usability, we recommend instead using a `screen` session and spawning a new window for each of these commands. In that case, you should omit `nohup` and `&`. ->💡 For more background and information, make sure to start with the [README](/README.md). - ## Step 0: Prepare your system -We assume a Ubuntu Linux instance when discussing prerequisites, but annotate steps with system-specificity and links to detailed multi-OS instructions. Exact procedures will vary for other systems, so please follow the links if running something different. More details on IPC-specific requirements can also be found in the [IPC Agent readme](https://github.com/consensus-shipyard/ipc-agent). +We assume a Ubuntu Linux instance when discussing prerequisites, but annotate steps with system-specificity and links to detailed multi-OS instructions. Exact procedures will vary for other systems, so please follow the links if running something different. Details on IPC-specific requirements can also be found in the [README](/README.md). * Install basic dependencies [Ubuntu/Debian] ([details](https://lotus.filecoin.io/lotus/install/prerequisites/#supported-platforms)) ```bash diff --git a/docs/subnet.md b/docs/subnet.md index bfc1471cc..ba1e8f655 100644 --- a/docs/subnet.md +++ b/docs/subnet.md @@ -2,7 +2,9 @@ >💡 For background and setup information, make sure to start with the [README](/README.md). -To spawn a new subnet, our IPC agent should be connected to at least the subnet of the parent we want to spawn the subnet from. You can refer to the [readme](/README.md) for information on how to run or connect to a rootnet. This instructions will assume the deployment of a subnet from `/root`, but the steps are equivalent for any other parent subnet. +To spawn a new subnet, our IPC agent should be connected to the parent subnet (or rootnet) from which we plan to deploy a new subnet. Please refer to the [README](/README.md) for information on how to run or connect to a rootnet. This instructions will assume the deployment of a subnet from `/root`, but the steps are equivalent for any other parent subnet. + +We provide instructions for running both a [simple single-validator subnet](#running-a-simple-subnet-with-a-single-validator) and a more useful [multi-validator subnet](#running-a-subnet-with-several-validators). The two sets mostly overlap. ## Preliminaries From f198c29ec9a2dc029abefaf22fbc011ce73c27f6 Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Mon, 17 Apr 2023 14:49:34 +0200 Subject: [PATCH 49/82] rm design link --- docs/architecture.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index b90cc9608..ea8fbab5b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,9 +4,6 @@ The IPC Agent is a process faciliting the participation of Filecoin clients like Lotus in the InterPlanetary Consensus (formerly Hierarchical Consensus). -Please refer to the [IPC Agent Design](https://docs.google.com/document/d/14lkRRv6MQYnuEfp2GoGngdD8Q5YgfE38D8HTZWKgKf4) document for details on the agent. - - # IPLD Resolver The [IPLD Resolver](../ipld/resolver) is a library that IPC Agents can use to exchange data between subnets in IPLD format. From 719604944fc9fe25ccaed3c5109474ffc0ecf725 Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Mon, 17 Apr 2023 14:58:35 +0200 Subject: [PATCH 50/82] Link design doc --- docs/architecture.md | 2 +- docs/subnet.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index ea8fbab5b..e83c6063b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -6,7 +6,7 @@ The IPC Agent is a process faciliting the participation of Filecoin clients like # IPLD Resolver -The [IPLD Resolver](../ipld/resolver) is a library that IPC Agents can use to exchange data between subnets in IPLD format. +The [IPLD Resolver](/ipld/resolver) is a library that IPC Agents can use to exchange data between subnets in IPLD format. ## Checkpointing diff --git a/docs/subnet.md b/docs/subnet.md index ba1e8f655..ccebcf59b 100644 --- a/docs/subnet.md +++ b/docs/subnet.md @@ -1,4 +1,4 @@ -# Deploying an IPC subnet +# Deploying IPC subnet infrastructure >💡 For background and setup information, make sure to start with the [README](/README.md). From 98f24c413dd1955956ec069f3e3a3408abdabe20 Mon Sep 17 00:00:00 2001 From: adlrocha Date: Tue, 18 Apr 2023 18:56:08 +0200 Subject: [PATCH 51/82] Feature: Cross-net message support (#155) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * restruct cli * refactor cli * add skeleton * remove checkpoint * update method naming * simplify code * make it compile * update serialization * Update Checkpoint serialization/deserialization (#154) * json serialization * lint * rename struct * updated types for actor refactor * Adapt bottom-up checkpoints to actor refactor (#156) * adapt checkpointing to actor refactor * fix submission logic * ¨ * minor fixes to logic * subnet state serialization fixes * add get top-down message call * Top-down Checkpoints (#160) * Move topdown commit prototype to new branch * Function to manage topdown checkpoints * Update install_infra.sh fix infra script * restruct cli * refactor cli * add skeleton * remove checkpoint * update method naming * simplify code * make it compile * update serialization * Update Checkpoint serialization/deserialization (#154) * json serialization * lint * rename struct * updated types for actor refactor * Adapt bottom-up checkpoints to actor refactor (#156) * adapt checkpointing to actor refactor * fix submission logic * ¨ * minor fixes to logic * subnet state serialization fixes * add get top-down message call * Move topdown commit prototype to new branch * Function to manage topdown checkpoints * Implementation of topdown checkpoints * wip: adapting logic to correct submission logic * finalize top-down logic including pending endpoints * add logging to bottom up * add more logging * info use as_ref * We don't stop the top-down (resp. bottom-up) checkpoint manager if the bottom-up (resp. top-down) manager fails * add new genesis_epoch endpoint * check if subnet initialized for top-down checkpoint * add top down checkpoint submission looping (#164) * Fix error logging of manage checkpoint futures * Topdown loop submission (#165) * add top down checkpoint submission looping * fix state wait * update nonce * fix top down nonce * cleaning code * catch up bu checkpoint (#166) * catch up bu checkpoint * format code * last topdown executed checkpoint handler * fmt --------- Co-authored-by: Jorge Soares <547492+jsoares@users.noreply.github.com> Co-authored-by: cryptoAtwill Co-authored-by: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Co-authored-by: Alfonso de la Rocha * minor fix --------- Co-authored-by: cryptoAtwill Co-authored-by: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Co-authored-by: Henrique Moniz <1785239+hmoniz@users.noreply.github.com> Co-authored-by: Jorge Soares <547492+jsoares@users.noreply.github.com> --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5defce241..e7139d4d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,8 +72,8 @@ fvm_ipld_blockstore = "0.1" fvm_ipld_encoding = "0.3" fvm_shared = { version = "=3.0.0-alpha.17", default-features = false } fil_actors_runtime = { git = "https://github.com/consensus-shipyard/fvm-utils", features = ["fil-actor"] } -ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git", tag = "v0.1.0"} -ipc-subnet-actor = { git = "https://github.com/consensus-shipyard/ipc-actors.git", tag = "v0.1.0", features = []} -ipc-gateway = { git = "https://github.com/consensus-shipyard/ipc-actors.git",tag = "v0.1.0", features = []} +ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git"} +ipc-subnet-actor = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = []} +ipc-gateway = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = []} libipld = { version = "0.14", default-features = false, features = ["dag-cbor"] } primitives = { git = "https://github.com/consensus-shipyard/fvm-utils"} From d3e5aabe4a841d363fe5d3e805faa8aaef900270 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Thu, 20 Apr 2023 00:34:25 +0800 Subject: [PATCH 52/82] Wallet list (#168) * add wallet list functions * fix lint * add balance * update cli --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index e7139d4d4..0197d1088 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ license-file.workspace = true anyhow = { workspace = true } async-channel = "1.8.0" async-trait = "0.1.61" +futures = "0.3.28" futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } indoc = "2.0.0" log = { workspace = true } From d225d8d17e3908a8a63b5dbdc2d733bec2e349f1 Mon Sep 17 00:00:00 2001 From: adlrocha Date: Thu, 20 Apr 2023 08:44:24 +0200 Subject: [PATCH 53/82] docs for cross-net messages (#167) * docs for cross-net messages * Update usage.md * Edits and improvements * Update usage.md * Update wallet export step * Fix subnet join syntax and use friendly names * Update default port to match the one in docs * Fix broken command * Clean up topdown description * address review and wallet list * Update docs/usage.md Co-authored-by: Jorge Soares <547492+jsoares@users.noreply.github.com> * Minor edit * Apply suggestions from code review Co-authored-by: Jorge Soares <547492+jsoares@users.noreply.github.com> --------- Co-authored-by: Jorge Soares <547492+jsoares@users.noreply.github.com> --- docs/quickstart.md | 2 +- docs/subnet.md | 21 +++++------ docs/troubleshooting.md | 2 +- docs/usage.md | 77 +++++++++++++++++++++++++++++++++++------ 4 files changed, 77 insertions(+), 25 deletions(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index c7af3a3d3..f179e65f7 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -108,7 +108,7 @@ $ nohup ./ipc-agent/bin/ipc-agent daemon & * The next step is to create a subnet under `/root` ```bash -$ ./ipc-agent/bin/ipc-agent subnet create -p /root -n andromeda --min-validator-stake 1 --min-validators 2 --finality-threshold 10 --check-period 10 +$ ./ipc-agent/bin/ipc-agent subnet create --parent /root --name andromeda --min-validator-stake 1 --min-validators 2 --bottomup-check-period 30 --topdown-check-period 30 ``` * Make a note of the address of the subnet you created (`/root/`) diff --git a/docs/subnet.md b/docs/subnet.md index ccebcf59b..c9d73f518 100644 --- a/docs/subnet.md +++ b/docs/subnet.md @@ -25,7 +25,7 @@ If your daemon is running on a docker container, you can get the container id or $ docker exec -it eudico wallet export --lotus-json > ~/.ipc-agent/wallet.key # Example execution -$ docker exec -it 84711d67cf162e30747c4525d69728c4dea8c6b4b35cd89f6d0947fee14bf908 eudico wallet export --lotus-json t1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq > ~/.ipc-agent/wallet.key +$ docker exec -it ipc_root_1234 eudico wallet export --lotus-json t1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq > ~/.ipc-agent/wallet.key ``` ### Importing wallet keys @@ -37,7 +37,7 @@ Depending on whether the subnet is running inside a docker container or not, you $ ./eudico wallet import --lotus-json # Example execution -$ ./eudico wallet import --lotus-json ~/.ipc-agent/t1ivy6mo2ofxw4fdmft22nel66w63fb7cuyslm4cy.key +$ ./eudico wallet import --lotus-json ~/.ipc-agent/wallet.key ``` ```bash @@ -45,7 +45,7 @@ $ ./eudico wallet import --lotus-json ~/.ipc-agent/t1ivy6mo2ofxw4fdmft22nel66w63 $ docker cp : && docker exec -it eudico wallet import --format=json-lotus # Example execution -$ docker cp ~/.ipc-agent/t1ivy6mo2ofxw4fdmft22nel66w63fb7cuyslm4cy.key 91d2af805346:/input.key && docker exec -it 91d2af805346 eudico wallet import --format=json-lotus input.key +$ docker cp ~/.ipc-agent/wallet.key ipc_root_t01002_1250:/input.key && docker exec -it ipc_root_t01002_1250 eudico wallet import --format=json-lotus input.key ``` ## Running a simple subnet with a single validator @@ -57,22 +57,17 @@ This section provides instructions for spawning a simple subnet with a single va To run a subnet the first thing is to configure and create the subnet actor that will govern the subnet's operation. ```bash -$ ./bin/ipc-agent subnet create -p -n --min-validator-stake 1 --min-validators --finality-threshold --check-period +$ ./bin/ipc-agent subnet create --parent --name --min-validator-stake --min-validators --bottomup-check-period --topdown-check-period # Example execution -$ ./bin/ipc-agent subnet create -p /root -n test --min-validator-stake 1 --min-validators 0 --finality-threshold 10 --check-period 10 +$ ./bin/ipc-agent subnet create --parent /root --name test --min-validator-stake 1 --min-validators 0 --bottomup-check-period 30 --topdown-check-period 30 [2023-03-21T09:32:58Z INFO ipc_agent::cli::commands::manager::create] created subnet actor with id: /root/t01002 ``` -This command deploys a subnet actor for a new subnet from the `root`, with a human-readable name `test`, that requires at least `1` validator to join the subnet to be able to mine new blocks, and with a checkpointing period to the parent of `10` blocks. We can see that the output of this command is the ID of the new subnet. +This command deploys a subnet actor for a new subnet from the `root`, with a human-readable name `test`, that requires at least `1` validator to join the subnet to be able to mine new blocks, and with a checkpointing period (both bottom-up and top-down) of `30` blocks. We can see that the output of this command is the ID of the new subnet. ### Exporting your wallet -Let's export the default wallet (or other wallet you'd like to use) for use in the subnet validator. -```bash -$ ./eudico wallet export --lotus-json `./eudico wallet default` > ~/.ipc-agent/wallet.key -``` - -Make sure that your wallet has enough funds to put up the collateral to join the subnet. +We will need to export the wallet key from our root node so that we can import them to our validators. Depending on how you are running your rootnet node you'll have to make a call to the docker container, or your nodes API. More information about exporting keys from your node can be found under [this section](#Exporting-wallet-keys). Make sure that the wallet holds enough funds to meet the subnet collateral requirements. ### Deploying a subnet node @@ -144,7 +139,7 @@ $ ./bin/ipc-agent subnet send-value --subnet /root --to bash +$ docker exec -it bash # Inside the container tmux a diff --git a/docs/usage.md b/docs/usage.md index 451e005bb..f4080e111 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -9,7 +9,7 @@ As a sanity-check that we have joined the subnet successfully and that we provid ```bash $ ./bin/ipc-agent list-subnets --gateway-address= --subnet= -# Sample execution +# Example execution $ ./bin/ipc-agent list-subnets --gateway-address=t064 --subnet=/root [2023-03-30T17:00:25Z INFO ipc_agent::cli::commands::manager::list_subnets] /root/t01003 - status: 0, collateral: 2 FIL, circ.supply: 0.0 FIL ``` @@ -27,6 +27,16 @@ $ ./bin/ipc-agent subnet join --subnet /root/t01002 --collateral 2 --validator-n ``` This command specifies the subnet to join, the amount of collateral to provide and the validator net address used by other validators to dial them. +## Listing your balance in a subnet +In order to send messages in a subnet, you'll need to have funds in your subnt account. You can use the following command to list the balance of your wallets in a subnet: +```bash +$ ./bin/ipc-agent wallet list --subnet= + +# Example execution +$ ./bin/ipc-agent wallet list --subnet=/root/t01002 +ipc_agent::cli::commands::wallet::list] wallets in subnet /root are {"t1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq": "500.0"} +``` + ## Sending funds in a subnet The agent provides a command to conveniently exchange funds between addresses of the same subnet. This can be achieved through the following command: @@ -37,28 +47,75 @@ $ ./bin/ipc-agent subnet send-value --subnet --to $ ./bin/ipc-agent subnet send-value --subnet /root/t01002 --to t1xbevqterae2tanmh2kaqksnoacflrv6w2dflq4i 10 ``` -## Leaving a subnet +## Sending funds between subnets -To leave a subnet, the following agent command can be used: +At the moment, the IPC agent only expose commands to perform the basic IPC interoperability primitives for cross-net communication, which is the exchange of FIL (the native token for IPC) between the same address of a subnet. Mainly: +- `fund`, which sends FIL from one public key address, to the same public key address in the child. +- `release` that moves FIL from one account in a child subnet to its counter-part in the parent. + +Complex behavior can be implemented using these primitives: sending value to a user in another subnet can be implemented a set of `release/fund` and `sendValue` operations. Calling smart contract from one subnet to another works by providing funds to one account in the destination subnet, and then calling the contract. The agent doesn't currently include abstractions for this complex operations, but it will in the future. That being said, users can still leverage the agent's API to easily compose the basic primitives into complex functionality. + +>💡 All cross-net operations need to pay an additional cross-msg fee (apart from the gas cost of the message). This is reason why even if you sent `X FIL` you may see `X - fee FIL` arriving to you account at destination. This fee is used to reward subnet validators for their work committing the checkpoint that carries the message. + +### Fund +Funding a subnet can be performed by using the following command: ```bash -$ ./bin/ipc-agent subnet leave --subnet +$ ./bin/ipc-agent cross-msg fund --subnet= # Example execution -$ ./bin/ipc-agent subnet leave --subnet /root/t01002 +$ ./bin/ipc-agent cross-msg fund --subnet=/root/t01002 100 + ``` -Leaving a subnet will release the collateral for the validator and remove all the validation rights from its account. This means that if you have a validator running in that subnet, its validation process will immediately terminate. +This command includes the cross-net message into the next top-down checkpoint after the current epoch. Once the top-down checkpoint is committed, you should see the funds in your account of the child subnet. +>💡 Top-down checkpoints are not used to anchor the security of the parent into the child (as is the case for bottom-up checkpoints). They just include information of the top-down messages that need to be executed in the child subnet, and are a way for validators in the subnet to reach consensus on the finality on their parent. + +### Release +In order to release funds from a subnet, your account must hold enough funds inside it. Releasing funds to the parent subnet can be permformed with the following comand: +```bash +$ ./bin/ipc-agent cross-msg release --subnet= + +# Example execution +$ ./bin/ipc-agent cross-msg release --subnet=/root/t01002 100 +``` +This command includes the cross-net message into a bottom-up checkpoint after the current epoch. Once the bottom-up checkpoint is committed, you should see the funds in your account in the parent. -### Listing checkpoints from a subnet -Subnets are periodically committing checkpoints to their parent every `check-period` (parameter defined when creating the subnet). If you want to inspect the information of a range of checkpoints committed in the parent for a subnet, you can use the `checkpoint list` command provided by the agent as follows: +## Listing checkpoints from a subnet + +Subnets are periodically committing checkpoints to their parent every `bottomup-check-period` (parameter defined when creating the subnet). If you want to inspect the information of a range of bottom-up checkpoints committed in the parent for a subnet, you can use the `checkpoint list-bottomup` command provided by the agent as follows: ```bash # List checkpoints between two epochs for a subnet -$ ./bin/ipc-agent checkpoint list --from-epoch --to-epoch --subnet +$ ./bin/ipc-agent checkpoint list-bottomup --from-epoch --to-epoch --subnet # Example execution -$ ./bin/ipc-agent checkpoint list --from-epoch 0 --to-epoch 100 --subnet root/t01002 +$ ./bin/ipc-agent checkpoint list-bottomup --from-epoch 0 --to-epoch 100 --subnet /root/t01002 [2023-03-29T12:43:42Z INFO ipc_agent::cli::commands::manager::list_checkpoints] epoch 0 - prev_check={"/":"bafy2bzacedkoa623kvi5gfis2yks7xxjl73vg7xwbojz4tpq63dd5jpfz757i"}, cross_msgs=null, child_checks=null [2023-03-29T12:43:42Z INFO ipc_agent::cli::commands::manager::list_checkpoints] epoch 10 - prev_check={"/":"bafy2bzacecsatvda6lodrorh7y7foxjt3a2dexxx5jiyvtl7gimrrvywb7l5m"}, cross_msgs=null, child_checks=null [2023-03-29T12:43:42Z INFO ipc_agent::cli::commands::manager::list_checkpoints] epoch 30 - prev_check={"/":"bafy2bzaceauzdx22hna4e4cqf55jqmd64a4fx72sxprzj72qhrwuxhdl7zexu"}, cross_msgs=null, child_checks=null ``` +You can find the checkpoint where your cross-message was included by listing the checkpoints around the epoch where your message was sent. + +## Checking the health of top-down checkpoints +In order to check the health of top-down checkpointing in a subnet, the following command can be run: +```bash +$./bin/ipc-agent checkpoint last-topdown --subnet= + +# Example execution +$./bin/ipc-agent checkpoint last-topdown --subnet /root/t01002 +[2023-04-18T17:11:34Z INFO ipc_agent::cli::commands::checkpoint::topdown_executed] Last top-down checkpoint executed in epoch: 9866 +``` + +This command returns the epoch of the last top-down checkpoint executed in the child. If you see that this epoch is way below the current epoch of the parent subnet, then top-down checkpointing may be lagging, validators need to catch-up, and the forwarding of top-down messages (from parent to child) may take longer to be committed. + + +## Leaving a subnet + +To leave a subnet, the following agent command can be used: +```bash +$ ./bin/ipc-agent subnet leave --subnet + +# Example execution +$ ./bin/ipc-agent subnet leave --subnet /root/t01002 +``` +Leaving a subnet will release the collateral for the validator and remove all the validation rights from its account. This means that if you have a validator running in that subnet, its validation process will immediately terminate. From 7c0534c5da94bf8dc90e559dc765557f6599e7d7 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Thu, 20 Apr 2023 09:16:58 +0100 Subject: [PATCH 54/82] RF: Separate e2e testing crate (#153) * RF: Move e2e test to its own crate. * RF: Re-enable integration tests for the IPLD resolver --------- Co-authored-by: Alfonso de la Rocha --- Cargo.toml | 10 +++++----- Makefile | 5 ++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0197d1088..7fa1692a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [".", "ipld/resolver"] +members = [".", "ipld/resolver", "testing/e2e"] [workspace.package] authors = ["Protocol Labs"] @@ -73,8 +73,8 @@ fvm_ipld_blockstore = "0.1" fvm_ipld_encoding = "0.3" fvm_shared = { version = "=3.0.0-alpha.17", default-features = false } fil_actors_runtime = { git = "https://github.com/consensus-shipyard/fvm-utils", features = ["fil-actor"] } -ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git"} -ipc-subnet-actor = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = []} -ipc-gateway = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = []} +ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git" } +ipc-subnet-actor = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [] } +ipc-gateway = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [] } libipld = { version = "0.14", default-features = false, features = ["dag-cbor"] } -primitives = { git = "https://github.com/consensus-shipyard/fvm-utils"} +primitives = { git = "https://github.com/consensus-shipyard/fvm-utils" } diff --git a/Makefile b/Makefile index a4dd869d9..e31d7ad6e 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,10 @@ build: cargo build -Z unstable-options --release --out-dir ./bin test: - cargo test --release --workspace --lib # only run unit tests + cargo test --release --workspace --exclude ipc_e2e + +e2e: + cargo test --release -p ipc_e2e clean: cargo clean From d23c7b0ac84e00e135ad5b3092c680787f63a956 Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Thu, 20 Apr 2023 13:02:11 +0200 Subject: [PATCH 55/82] Remove $, move examples into console blocks --- docs/subnet.md | 70 ++++++++++++++++++++++++----------------- docs/troubleshooting.md | 20 ++++++------ docs/usage.md | 48 +++++++++++++++------------- 3 files changed, 79 insertions(+), 59 deletions(-) diff --git a/docs/subnet.md b/docs/subnet.md index c9d73f518..d306676c0 100644 --- a/docs/subnet.md +++ b/docs/subnet.md @@ -14,16 +14,18 @@ In order to run a validator in a subnet, we'll need a set of keys to handle that *Example*: ```bash -$ ./eudico wallet export --lotus-json > - +./eudico wallet export --lotus-json > +``` +```console # Example execution $ ./eudico wallet export --lotus-json t1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq > ~/.ipc-agent/wallet.key ``` If your daemon is running on a docker container, you can get the container id or name (provided also in the output of the infra scripts), and run the following command above inside a container outputting the exported private key into a file locally: ```bash -$ docker exec -it eudico wallet export --lotus-json > ~/.ipc-agent/wallet.key - +docker exec -it eudico wallet export --lotus-json > ~/.ipc-agent/wallet.key +``` +```console # Example execution $ docker exec -it ipc_root_1234 eudico wallet export --lotus-json t1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq > ~/.ipc-agent/wallet.key ``` @@ -34,16 +36,18 @@ Depending on whether the subnet is running inside a docker container or not, you ```bash # Bare: Import directly into eudico -$ ./eudico wallet import --lotus-json - +./eudico wallet import --lotus-json +``` +```console # Example execution $ ./eudico wallet import --lotus-json ~/.ipc-agent/wallet.key ``` ```bash # Docker: Copy the wallet key into the container and import into eudico -$ docker cp : && docker exec -it eudico wallet import --format=json-lotus - +docker cp : && docker exec -it eudico wallet import --format=json-lotus +``` +```console # Example execution $ docker cp ~/.ipc-agent/wallet.key ipc_root_t01002_1250:/input.key && docker exec -it ipc_root_t01002_1250 eudico wallet import --format=json-lotus input.key ``` @@ -57,8 +61,9 @@ This section provides instructions for spawning a simple subnet with a single va To run a subnet the first thing is to configure and create the subnet actor that will govern the subnet's operation. ```bash -$ ./bin/ipc-agent subnet create --parent --name --min-validator-stake --min-validators --bottomup-check-period --topdown-check-period - +./bin/ipc-agent subnet create --parent --name --min-validator-stake --min-validators --bottomup-check-period --topdown-check-period +``` +```console # Example execution $ ./bin/ipc-agent subnet create --parent /root --name test --min-validator-stake 1 --min-validators 0 --bottomup-check-period 30 --topdown-check-period 30 [2023-03-21T09:32:58Z INFO ipc_agent::cli::commands::manager::create] created subnet actor with id: /root/t01002 @@ -73,8 +78,9 @@ We will need to export the wallet key from our root node so that we can import t Before joining a new subnet, our node for that subnet must be initialised. For the deployment of subnet daemons we also provide a convenient infra script: ```bash -$ ./bin/ipc-infra/run-subnet-docker.sh - +./bin/ipc-infra/run-subnet-docker.sh +``` +```console # Example execution $ ./bin/ipc-infra/run-subnet-docker.sh 1250 1350 /root/t01002 ~/.ipc-agent/wallet.key (...) @@ -106,8 +112,9 @@ accounts = ["t1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq"] With the daemon for the subnet deployed, we can join the subnet: ```bash -$ ./bin/ipc-agent subnet join --subnet --collateral --validator-net-addr - +./bin/ipc-agent subnet join --subnet --collateral --validator-net-addr +``` +```console # Example execution $ ./bin/ipc-agent subnet join --subnet /root/t01002 --collateral 2 --validator-net-addr /dns/host.docker.internal/tcp/1349/p2p/12D3KooWN5hbWkCxwvrX9xYxMwFbWm2Jpa1o4qhwifmSw3Fb ``` @@ -117,8 +124,9 @@ This command specifies the subnet to join, the amount of collateral to provide a With our subnet daemon deployed, and having joined the network, as the minimum number of validators we set for our subnet is 0, we can start mining and creating new blocks in the subnet. Doing so is a simple as running the following script using as an argument the container of our subnet node: ```bash -$ ./bin/ipc-infra/mine-subnet.sh - +./bin/ipc-infra/mine-subnet.sh +``` +```console # Example execution $ ./bin/ipc-infra/mine-subnet.sh 84711d67cf162e30747c4525d69728c4dea8c6b4b35cd89f6d0947fee14bf908 ``` @@ -129,12 +137,12 @@ The mining process is currently run in the foreground in interactive mode. Consi In this section, we will deploy a subnet where the IPC agent is responsible for handling more than one validator in the subnet. We are going to deploy a subnet with 3 validators. The first thing we'll need to do is create a new wallet for every validator we want to run. We can do this directly through the agent with the following command (3x): ```bash -$ ./bin/ipc-agent wallet new --key-type secp256k1 --subnet /root +./bin/ipc-agent wallet new --key-type secp256k1 --subnet /root ``` We also need to provide with some funds our wallets so they can put collateral to join the subnet. According to the rootnet you are connected to, you may need to get some funds from the faucet, or send some from your main wallet. Funds can be sent from your main wallet also through the agent with (3x, adjusting `target-wallet` for each): ```bash -$ ./bin/ipc-agent subnet send-value --subnet /root --to +./bin/ipc-agent subnet send-value --subnet /root --to ``` With this, we can already create the subnet with `/root` as its parent. We are going to set the `--min-validators 2` so no new blocks can be created without this number of validators in the subnet. @@ -145,18 +153,21 @@ With this, we can already create the subnet with `/root` as its parent. We are g In order to deploy the 3 validators for the subnet, we will have to first export the keys from our root node so we can import them to our validators. Depending on how you are running your rootnet node you'll have to make a call to the docker container, or your nodes API. More information about exporting keys from your node can be found under [this section](#Exporting-wallet-keys). -With the keys conveniently exported, we can deploy the subnet nodes using the `infra-scripts`. The following code snippet showcases the deployment of five Example nodes. Note that each node should be importing a different wallet key for their validator, and should be exposing different ports for their API and validators. +With the keys conveniently exported, we can deploy the subnet nodes using the `infra-scripts`. Note that each node should be importing a different wallet key for their validator, and should be exposing different ports for their API and validators. -*Example*: ```bash -$ ./bin/ipc-infra/run-subnet-docker.sh 1251 1351 /root/t01002 ~/.ipc-agent/wallet1.key -$ ./bin/ipc-infra/run-subnet-docker.sh 1252 1352 /root/t01002 ~/.ipc-agent/wallet2.key -$ ./bin/ipc-infra/run-subnet-docker.sh 1253 1353 /root/t01002 ~/.ipc-agent/wallet3.key +./bin/ipc-infra/run-subnet-docker.sh +``` +```console +# Example execution +./bin/ipc-infra/run-subnet-docker.sh 1251 1351 /root/t01002 ~/.ipc-agent/wallet1.key +./bin/ipc-infra/run-subnet-docker.sh 1252 1352 /root/t01002 ~/.ipc-agent/wallet2.key +./bin/ipc-infra/run-subnet-docker.sh 1253 1353 /root/t01002 ~/.ipc-agent/wallet3.key ``` If the deployment is successful, each of these nodes should return the following output at the end of their logs. Note down this information somewhere as we will need it to conveniently join our validators to the subnet. *Example*: -``` +```console >>> Subnet /root/t01002 daemon running in container: 91d2af80534665a8d9a20127e480c16136d352a79563e74ee3c5497d50b9eda8 (friendly name: ipc_root_t01002_1240) >>> Token to /root/t01002 daemon: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJBbGxvdyI6WyJyZWFkIiwid3JpdGUiLCJzaWduIiwiYWRtaW4iXX0.JTiumQwFIutkTb0gUC5JWTATs-lUvDaopEDE0ewgzLk >>> Default wallet: t1ivy6mo2ofxw4fdmft22nel66w63fb7cuyslm4cy @@ -186,17 +197,18 @@ All the infrastructure for the subnet is now deployed, and we can join our valid This is the command that needs to be executed for every validator to join the subnet: ```bash -$ ./bin/ipc-agent subnet join --from --subnet /root/t01002 --collateral --validator-net-addr - +./bin/ipc-agent subnet join --from --subnet /root/t01002 --collateral --validator-net-addr +``` +```console # Example execution $ ./bin/ipc-agent subnet join --from t1ivy6mo2ofxw4fdmft22nel66w63fb7cuyslm4cy --subnet /root/t01002 --collateral 2 --validator-net-addr /dns/host.docker.internal/tcp/1359/p2p/12D3KooWEJXcSPw6Yv4jDk52xvp2rdeG3J6jCPX9AgBJE2mRCVoR ``` Remember doing the above step for the 3 validators. -### Mining in subnet +### Mining in a subnet We have everything in place now to start mining. Mining is as simple as running the following script for each of the validators, passing the container id/name: -```bash -$ ./bin/ipc-infra/mine-subnet.sh +```bash +./bin/ipc-infra/mine-subnet.sh ``` The mining process is currently run in the foreground in interactive mode. Consider using `nohup ./bin/ipc-infra/mine-subnet.sh` or screen to run the process in the background and redirect the logs to some file as handling the mining process of the three validators in the foreground may be quite cumbersome. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 82c3725a4..b972007d7 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -7,11 +7,11 @@ Sometimes, things break, and we'll need to push a quick path to fix some bug. If this happens, and you need to upgrade your agent version, kill you agent daemon if you have any running, pull the latest changes from this repo, build the binary, and start your daemon again. This should pick up the latest version for the agent. In the future, we will provide a better way to upgrade your agent. ```bash # Pull latest changes -$ git pull +git pull # Build the agent -$ make build +make build # Restart the daemon -$ ./bin/ipc-agent daemon +./bin/ipc-agent daemon ``` ## The eudico image is not building successful @@ -26,11 +26,12 @@ Either because the dockerized subnet node after running `./bin/ipc-infra/run-sub ``` Not online yet... (could not get API info for FullNode: could not get api endpoint: API not running (no endpoint)) ``` -Or because when the script finishes no validator address has been reported as expected by the logs, the best way to debug this situation is to attach to the docker container and check the logs with the following command: +Or because when the script finishes no validator address has been reported as expected by the logs, the best way to debug this situation is to attach to the docker container: +```bash +docker exec -it bash +``` + And check the logs with the following command, inside the container ```bash -$ docker exec -it bash - -# Inside the container tmux a ``` Generally, the issue is that: @@ -47,8 +48,9 @@ It may be the case that while joining the subnet, you didn't set the multiaddres Changing the validator is as simple as running the following command: ```bash -$ ./bin/ipc-agent subnet set-validator-net-addr --subnet --validator-net-addr - +./bin/ipc-agent subnet set-validator-net-addr --subnet --validator-net-addr +``` +```console # Example execution $ ./bin/ipc-agent subnet set-validator-net-addr --subnet /root/t01002 --validator-net-addr "/dns/host.docker.internal/tcp/1349/p2p/12D3KooWDeN3bTvZEH11s9Gq5bDeZZLKgRZiMDcy2KmA6mUaT9KE" ``` diff --git a/docs/usage.md b/docs/usage.md index f4080e111..a3cbdbcc9 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -7,8 +7,9 @@ As a sanity-check that we have joined the subnet successfully and that we provided enough collateral to register the subnet to IPC, we can list the child subnets of our parent with the following command: ```bash -$ ./bin/ipc-agent list-subnets --gateway-address= --subnet= - +./bin/ipc-agent list-subnets --gateway-address= --subnet= +``` +```console # Example execution $ ./bin/ipc-agent list-subnets --gateway-address=t064 --subnet=/root [2023-03-30T17:00:25Z INFO ipc_agent::cli::commands::manager::list_subnets] /root/t01003 - status: 0, collateral: 2 FIL, circ.supply: 0.0 FIL @@ -20,8 +21,9 @@ This command only shows subnets that have been registered to the gateway, i.e. t With the daemon for a subnet deployed (see [instructions](/docs/subnet.md)), one can join the subnet: ```bash -$ ./bin/ipc-agent subnet join --subnet --collateral --validator-net-addr - +./bin/ipc-agent subnet join --subnet --collateral --validator-net-addr +``` +```console # Example execution $ ./bin/ipc-agent subnet join --subnet /root/t01002 --collateral 2 --validator-net-addr /dns/host.docker.internal/tcp/1349/p2p/12D3KooWN5hbWkCxwvrX9xYxMwFbWm2Jpa1o4qhwifmSw3Fb ``` @@ -30,8 +32,9 @@ This command specifies the subnet to join, the amount of collateral to provide a ## Listing your balance in a subnet In order to send messages in a subnet, you'll need to have funds in your subnt account. You can use the following command to list the balance of your wallets in a subnet: ```bash -$ ./bin/ipc-agent wallet list --subnet= - +./bin/ipc-agent wallet list --subnet= +``` +```console # Example execution $ ./bin/ipc-agent wallet list --subnet=/root/t01002 ipc_agent::cli::commands::wallet::list] wallets in subnet /root are {"t1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq": "500.0"} @@ -41,8 +44,9 @@ ipc_agent::cli::commands::wallet::list] wallets in subnet /root are {"t1cp4q4lqs The agent provides a command to conveniently exchange funds between addresses of the same subnet. This can be achieved through the following command: ```bash -$ ./bin/ipc-agent subnet send-value --subnet --to - +./bin/ipc-agent subnet send-value --subnet --to +``` +```console # Example execution $ ./bin/ipc-agent subnet send-value --subnet /root/t01002 --to t1xbevqterae2tanmh2kaqksnoacflrv6w2dflq4i 10 ``` @@ -60,11 +64,11 @@ Complex behavior can be implemented using these primitives: sending value to a u ### Fund Funding a subnet can be performed by using the following command: ```bash -$ ./bin/ipc-agent cross-msg fund --subnet= - +./bin/ipc-agent cross-msg fund --subnet= +``` +```console # Example execution $ ./bin/ipc-agent cross-msg fund --subnet=/root/t01002 100 - ``` This command includes the cross-net message into the next top-down checkpoint after the current epoch. Once the top-down checkpoint is committed, you should see the funds in your account of the child subnet. @@ -73,8 +77,9 @@ This command includes the cross-net message into the next top-down checkpoint af ### Release In order to release funds from a subnet, your account must hold enough funds inside it. Releasing funds to the parent subnet can be permformed with the following comand: ```bash -$ ./bin/ipc-agent cross-msg release --subnet= - +./bin/ipc-agent cross-msg release --subnet= +``` +```console # Example execution $ ./bin/ipc-agent cross-msg release --subnet=/root/t01002 100 ``` @@ -85,9 +90,9 @@ This command includes the cross-net message into a bottom-up checkpoint after th Subnets are periodically committing checkpoints to their parent every `bottomup-check-period` (parameter defined when creating the subnet). If you want to inspect the information of a range of bottom-up checkpoints committed in the parent for a subnet, you can use the `checkpoint list-bottomup` command provided by the agent as follows: ```bash -# List checkpoints between two epochs for a subnet -$ ./bin/ipc-agent checkpoint list-bottomup --from-epoch --to-epoch --subnet - +./bin/ipc-agent checkpoint list-bottomup --from-epoch --to-epoch --subnet +``` +```console # Example execution $ ./bin/ipc-agent checkpoint list-bottomup --from-epoch 0 --to-epoch 100 --subnet /root/t01002 [2023-03-29T12:43:42Z INFO ipc_agent::cli::commands::manager::list_checkpoints] epoch 0 - prev_check={"/":"bafy2bzacedkoa623kvi5gfis2yks7xxjl73vg7xwbojz4tpq63dd5jpfz757i"}, cross_msgs=null, child_checks=null @@ -99,8 +104,9 @@ You can find the checkpoint where your cross-message was included by listing the ## Checking the health of top-down checkpoints In order to check the health of top-down checkpointing in a subnet, the following command can be run: ```bash -$./bin/ipc-agent checkpoint last-topdown --subnet= - +./bin/ipc-agent checkpoint last-topdown --subnet= +``` +```console # Example execution $./bin/ipc-agent checkpoint last-topdown --subnet /root/t01002 [2023-04-18T17:11:34Z INFO ipc_agent::cli::commands::checkpoint::topdown_executed] Last top-down checkpoint executed in epoch: 9866 @@ -108,13 +114,13 @@ $./bin/ipc-agent checkpoint last-topdown --subnet /root/t01002 This command returns the epoch of the last top-down checkpoint executed in the child. If you see that this epoch is way below the current epoch of the parent subnet, then top-down checkpointing may be lagging, validators need to catch-up, and the forwarding of top-down messages (from parent to child) may take longer to be committed. - ## Leaving a subnet To leave a subnet, the following agent command can be used: ```bash -$ ./bin/ipc-agent subnet leave --subnet - +./bin/ipc-agent subnet leave --subnet +``` +```console # Example execution $ ./bin/ipc-agent subnet leave --subnet /root/t01002 ``` From de3eeea3801f96ec6b86baa69c5fea30cc582f0a Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Thu, 20 Apr 2023 13:06:46 +0200 Subject: [PATCH 56/82] Forgot quickstart --- docs/quickstart.md | 94 +++++++++++++++++++++++----------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index f179e65f7..ed518e95c 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -12,33 +12,33 @@ We assume a Ubuntu Linux instance when discussing prerequisites, but annotate st * Install basic dependencies [Ubuntu/Debian] ([details](https://lotus.filecoin.io/lotus/install/prerequisites/#supported-platforms)) ```bash -$ sudo apt update && sudo apt install build-essential libssl-dev mesa-opencl-icd ocl-icd-opencl-dev gcc git bzr jq pkg-config curl clang hwloc libhwloc-dev wget ca-certificates gnupg -y +sudo apt update && sudo apt install build-essential libssl-dev mesa-opencl-icd ocl-icd-opencl-dev gcc git bzr jq pkg-config curl clang hwloc libhwloc-dev wget ca-certificates gnupg -y ``` * Install Rust [Linux] ([details](https://www.rust-lang.org/tools/install)) ```bash -$ curl https://sh.rustup.rs -sSf | sh -$ source "$HOME/.cargo/env" -$ rustup target add wasm32-unknown-unknown +curl https://sh.rustup.rs -sSf | sh +source "$HOME/.cargo/env" +rustup target add wasm32-unknown-unknown ``` * Install Go [Linux] ([details](https://go.dev/doc/install)) ```bash -$ curl -fsSL https://golang.org/dl/go1.19.7.linux-amd64.tar.gz | sudo tar -xz -C /usr/local -$ echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc && source ~/.bashrc +curl -fsSL https://golang.org/dl/go1.19.7.linux-amd64.tar.gz | sudo tar -xz -C /usr/local +echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc && source ~/.bashrc ``` * Install Docker Engine [Ubuntu] ([details](https://docs.docker.com/engine/install/)) ```bash -$ sudo install -m 0755 -d /etc/apt/keyrings -$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg -$ sudo chmod a+r /etc/apt/keyrings/docker.gpg -$ echo \ +sudo install -m 0755 -d /etc/apt/keyrings +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg +sudo chmod a+r /etc/apt/keyrings/docker.gpg +echo \ "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null -$ sudo apt-get update && sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y -$ sudo usermod -aG docker $USER && newgrp docker +sudo apt-get update && sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y +sudo usermod -aG docker $USER && newgrp docker ``` @@ -48,17 +48,17 @@ Next, we'll download and build the different components (IPC agent, docker image * Pick a folder where to build the IPC stack. In this example, we'll go with `~/ipc/`. ```bash -$ mkdir -p ~/ipc/ && cd ~/ipc/ +mkdir -p ~/ipc/ && cd ~/ipc/ ``` * Download and compile the IPC Agent (might take a while) ```bash -$ git clone https://github.com/consensus-shipyard/ipc-agent.git -$ (cd ipc-agent && make build && make install-infra) +git clone https://github.com/consensus-shipyard/ipc-agent.git +(cd ipc-agent && make build && make install-infra) ``` * Download and compile eudico (might take a while) ```bash -$ git clone https://github.com/consensus-shipyard/lotus.git -$ (cd lotus && make spacenet) +git clone https://github.com/consensus-shipyard/lotus.git +(cd lotus && make spacenet) ``` @@ -68,17 +68,17 @@ Let's deploy a eudico instance on Spacenet and configure the IPC Agent to intera * Start your eudico instance (might take a while to sync the chain) ```bash -$ nohup ./lotus/eudico mir daemon --bootstrap & +nohup ./lotus/eudico mir daemon --bootstrap & ``` * Get configuration parameters ```bash -$ ./lotus/eudico auth create-token --perm admin -$ ./lotus/eudico wallet new +./lotus/eudico auth create-token --perm admin +./lotus/eudico wallet new ``` * Configure your IPC Agent ```bash -$ ./ipc-agent/bin/ipc-agent config init -$ nano ~/.ipc-agent/config.toml +./ipc-agent/bin/ipc-agent config init +nano ~/.ipc-agent/config.toml ``` * Replace the content of `config.toml` with the text below, substituting the token and wallet retrieved above. ```toml @@ -95,7 +95,7 @@ accounts = [""] ``` * Start your IPC Agent ```bash -$ nohup ./ipc-agent/bin/ipc-agent daemon & +nohup ./ipc-agent/bin/ipc-agent daemon & ``` @@ -108,7 +108,7 @@ $ nohup ./ipc-agent/bin/ipc-agent daemon & * The next step is to create a subnet under `/root` ```bash -$ ./ipc-agent/bin/ipc-agent subnet create --parent /root --name andromeda --min-validator-stake 1 --min-validators 2 --bottomup-check-period 30 --topdown-check-period 30 +./ipc-agent/bin/ipc-agent subnet create --parent /root --name andromeda --min-validator-stake 1 --min-validators 2 --bottomup-check-period 30 --topdown-check-period 30 ``` * Make a note of the address of the subnet you created (`/root/`) @@ -119,21 +119,21 @@ Although we set a minimum of 2 active validators in the previous, we'll deploy 3 * First, we'll need to create a wallet for each validator ```bash -$ ./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 --subnet /root -$ ./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 --subnet /root -$ ./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 --subnet /root +./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 --subnet /root +./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 --subnet /root +./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 --subnet /root ``` * Export each wallet (WALLET_1, WALLET_2, and WALLET_3) by substituting their addresses below ```bash -$ ./lotus/eudico wallet export --lotus-json > ~/.ipc-agent/wallet1.key -$ ./lotus/eudico wallet export --lotus-json > ~/.ipc-agent/wallet2.key -$ ./lotus/eudico wallet export --lotus-json > ~/.ipc-agent/wallet3.key +./lotus/eudico wallet export --lotus-json > ~/.ipc-agent/wallet1.key +./lotus/eudico wallet export --lotus-json > ~/.ipc-agent/wallet2.key +./lotus/eudico wallet export --lotus-json > ~/.ipc-agent/wallet3.key ``` * We also need to fund the wallets with enough collateral to; we'll send the funds from our default wallet ```bash -$ ./ipc-agent/bin/ipc-agent subnet send-value --subnet /root --to 2 -$ ./ipc-agent/bin/ipc-agent subnet send-value --subnet /root --to 2 -$ ./ipc-agent/bin/ipc-agent subnet send-value --subnet /root --to 2 +./ipc-agent/bin/ipc-agent subnet send-value --subnet /root --to 2 +./ipc-agent/bin/ipc-agent subnet send-value --subnet /root --to 2 +./ipc-agent/bin/ipc-agent subnet send-value --subnet /root --to 2 ``` @@ -143,9 +143,9 @@ We can deploy the subnet nodes. Note that each node should be importing a differ * Deploy and run a container for each validator, importing the corresponding wallet keys ```bash -$ ./ipc-agent/bin/ipc-infra/run-subnet-docker.sh 1251 1351 /root/ ~/.ipc-agent/wallet1.key -$ ./ipc-agent/bin/ipc-infra/run-subnet-docker.sh 1252 1352 /root/ ~/.ipc-agent/wallet2.key -$ ./ipc-agent/bin/ipc-infra/run-subnet-docker.sh 1253 1353 /root/ ~/.ipc-agent/wallet3.key +./ipc-agent/bin/ipc-infra/run-subnet-docker.sh 1251 1351 /root/ ~/.ipc-agent/wallet1.key +./ipc-agent/bin/ipc-infra/run-subnet-docker.sh 1252 1352 /root/ ~/.ipc-agent/wallet2.key +./ipc-agent/bin/ipc-infra/run-subnet-docker.sh 1253 1353 /root/ ~/.ipc-agent/wallet3.key ``` * If the deployment is successful, each of these nodes should return the following output at the end of their logs. Save the information for the next step. ``` @@ -165,12 +165,12 @@ For ease of use, we'll import the remaining keys into the first validator, via w * Copy the wallet keys into the docker container and import them ```bash -$ docker cp ~/.ipc-agent/wallet2.key :/input.key && docker exec -it eudico wallet import --format=json-lotus input.key -$ docker cp ~/.ipc-agent/wallet3.key :/input.key && docker exec -it eudico wallet import --format=json-lotus input.key +docker cp ~/.ipc-agent/wallet2.key :/input.key && docker exec -it eudico wallet import --format=json-lotus input.key +docker cp ~/.ipc-agent/wallet3.key :/input.key && docker exec -it eudico wallet import --format=json-lotus input.key ``` * Edit the IPC agent configuration `config.toml` ```bash -$ nano ~/.ipc-agent/config.toml +nano ~/.ipc-agent/config.toml ``` * Append the new subnet to the configuration ```toml @@ -184,7 +184,7 @@ accounts = ["", "", ""] ``` * Reload the config ```bash -$ ./ipc-agent/bin/ipc-agent config reload +./ipc-agent/bin/ipc-agent config reload ``` @@ -194,9 +194,9 @@ All the infrastructure for the subnet is now deployed, and we can join our valid * Join the subnet with each validators ```bash -$ ./ipc-agent/bin/ipc-agent subnet join --from --subnet /root/ --collateral 1 --validator-net-addr -$ ./ipc-agent/bin/ipc-agent subnet join --from --subnet /root/ --collateral 1 --validator-net-addr -$ ./ipc-agent/bin/ipc-agent subnet join --from --subnet /root/ --collateral 1 --validator-net-addr +./ipc-agent/bin/ipc-agent subnet join --from --subnet /root/ --collateral 1 --validator-net-addr +./ipc-agent/bin/ipc-agent subnet join --from --subnet /root/ --collateral 1 --validator-net-addr +./ipc-agent/bin/ipc-agent subnet join --from --subnet /root/ --collateral 1 --validator-net-addr ``` @@ -204,9 +204,9 @@ $ ./ipc-agent/bin/ipc-agent subnet join --from --subnet /root/ & -$ nohup ./ipc-agent/bin/ipc-infra/mine-subnet.sh & -$ nohup ./ipc-agent/bin/ipc-infra/mine-subnet.sh & +nohup ./ipc-agent/bin/ipc-infra/mine-subnet.sh & +nohup ./ipc-agent/bin/ipc-infra/mine-subnet.sh & +nohup ./ipc-agent/bin/ipc-infra/mine-subnet.sh & ``` @@ -214,7 +214,7 @@ $ nohup ./ipc-agent/bin/ipc-infra/mine-subnet.sh & * Check that the subnet is running ```bash -$ ./ipc-agent/bin/ipc-agent subnet list --gateway-address t064 --subnet /root +./ipc-agent/bin/ipc-agent subnet list --gateway-address t064 --subnet /root ``` * If something went wrong, please have a look at the [README](https://github.com/consensus-shipyard/ipc-agent). If it doesn't help, please join us in #ipc-help. In either case, let us know your experience! * Please note that to repeat this guide or spawn a new subnet, you may need to change the parameters or reset your system. From b8be21b056eca34daf70daffd5906511c85b1cd4 Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Thu, 20 Apr 2023 14:04:46 +0200 Subject: [PATCH 57/82] Update docs/usage.md Co-authored-by: Akosh Farkash --- docs/usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage.md b/docs/usage.md index a3cbdbcc9..9998145c4 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -108,7 +108,7 @@ In order to check the health of top-down checkpointing in a subnet, the followin ``` ```console # Example execution -$./bin/ipc-agent checkpoint last-topdown --subnet /root/t01002 +$ ./bin/ipc-agent checkpoint last-topdown --subnet /root/t01002 [2023-04-18T17:11:34Z INFO ipc_agent::cli::commands::checkpoint::topdown_executed] Last top-down checkpoint executed in epoch: 9866 ``` From d6657fd7b9301bc61c3f6154feab3ce2605cc7d9 Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Thu, 20 Apr 2023 14:07:11 +0200 Subject: [PATCH 58/82] Address review comments --- docs/subnet.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/subnet.md b/docs/subnet.md index d306676c0..77cb6927d 100644 --- a/docs/subnet.md +++ b/docs/subnet.md @@ -160,9 +160,9 @@ With the keys conveniently exported, we can deploy the subnet nodes using the `i ``` ```console # Example execution -./bin/ipc-infra/run-subnet-docker.sh 1251 1351 /root/t01002 ~/.ipc-agent/wallet1.key -./bin/ipc-infra/run-subnet-docker.sh 1252 1352 /root/t01002 ~/.ipc-agent/wallet2.key -./bin/ipc-infra/run-subnet-docker.sh 1253 1353 /root/t01002 ~/.ipc-agent/wallet3.key +$ ./bin/ipc-infra/run-subnet-docker.sh 1251 1351 /root/t01002 ~/.ipc-agent/wallet1.key +$ ./bin/ipc-infra/run-subnet-docker.sh 1252 1352 /root/t01002 ~/.ipc-agent/wallet2.key +$ ./bin/ipc-infra/run-subnet-docker.sh 1253 1353 /root/t01002 ~/.ipc-agent/wallet3.key ``` If the deployment is successful, each of these nodes should return the following output at the end of their logs. Note down this information somewhere as we will need it to conveniently join our validators to the subnet. From a12b3bda3b7c4024deb351a8b52b62de4bcd7fea Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Thu, 20 Apr 2023 15:03:10 +0200 Subject: [PATCH 59/82] Add tail suggestion --- docs/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index ed518e95c..af8e36fe8 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -4,7 +4,7 @@ Ready to test the waters with your first subnet? This guide will deploy a subnet with multiple local validators orchestrated by the same IPC agent. This subnet will be anchored to the public Spacenet. This will be a minimal example and may not work on all systems. The full documentation provides more details on each step. -Several steps in this guide involve running long-lived processes. These commands are usually prefaced with `nohup` so that they run in the background. For improved usability, we recommend instead using a `screen` session and spawning a new window for each of these commands. In that case, you should omit `nohup` and `&`. +Several steps in this guide involve running long-lived processes. These commands are usually prefaced with `nohup` so that they run in the background. For improved usability, we recommend instead using a `screen` session and spawning a new window for each of these commands. In that case, you should omit `nohup` and `&`. Alternatively, open a new terminal and `tail -f nohup.out` to track the combined output of the different daemons. ## Step 0: Prepare your system From 086f8f20ca0b39885ffb3b191fe622dd9e9c960a Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Thu, 20 Apr 2023 15:19:07 +0200 Subject: [PATCH 60/82] Update wallet address --- docs/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index af8e36fe8..4b0a8f20d 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -101,7 +101,7 @@ nohup ./ipc-agent/bin/ipc-agent daemon & ## Step 3: Fund your account -* Obtain some Spacenet FIL by requesting it from the [faucet](https://spacenet.consensus.ninja/), using your wallet address. +* Obtain some Spacenet FIL by requesting it from the [faucet](https://faucet.spacenet.ipc.space/), using your wallet address. ## Step 4: Create the subnet From 885cb158eb7805a39e0eb82662f1d33a115bf4ba Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Thu, 20 Apr 2023 16:32:28 +0200 Subject: [PATCH 61/82] Replace nohup suggestions --- docs/quickstart.md | 18 +++++++++--------- docs/subnet.md | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 4b0a8f20d..83152f48b 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -4,7 +4,7 @@ Ready to test the waters with your first subnet? This guide will deploy a subnet with multiple local validators orchestrated by the same IPC agent. This subnet will be anchored to the public Spacenet. This will be a minimal example and may not work on all systems. The full documentation provides more details on each step. -Several steps in this guide involve running long-lived processes. These commands are usually prefaced with `nohup` so that they run in the background. For improved usability, we recommend instead using a `screen` session and spawning a new window for each of these commands. In that case, you should omit `nohup` and `&`. Alternatively, open a new terminal and `tail -f nohup.out` to track the combined output of the different daemons. +Several steps in this guide involve running long-lived processes. In each of these cases, the guide advises starting a new *session*. Depending on your set-up, you may do this using tools like `screen` or `tmux`, or, if using a graphical environment, by opening a new terminal tab, pane, or window. ## Step 0: Prepare your system @@ -66,9 +66,9 @@ git clone https://github.com/consensus-shipyard/lotus.git Let's deploy a eudico instance on Spacenet and configure the IPC Agent to interact with it. -* Start your eudico instance (might take a while to sync the chain) +* [**In a new session**] Start your eudico instance (might take a while to sync the chain) ```bash -nohup ./lotus/eudico mir daemon --bootstrap & +./lotus/eudico mir daemon --bootstrap ``` * Get configuration parameters ```bash @@ -93,9 +93,9 @@ jsonrpc_api_http = "http://127.0.0.1:1234/rpc/v1" auth_token = "" accounts = [""] ``` -* Start your IPC Agent +* [**In a new session**] Start your IPC Agent ```bash -nohup ./ipc-agent/bin/ipc-agent daemon & +./ipc-agent/bin/ipc-agent daemon ``` @@ -202,11 +202,11 @@ All the infrastructure for the subnet is now deployed, and we can join our valid ## Step 9: Start validating! -We have everything in place now to start validating. This is as simple as running the following script for each of the validators, passing the container name (or id): +We have everything in place now to start validating. Run the following script for each of the validators [**each in a new session**], passing the container names: ```bash -nohup ./ipc-agent/bin/ipc-infra/mine-subnet.sh & -nohup ./ipc-agent/bin/ipc-infra/mine-subnet.sh & -nohup ./ipc-agent/bin/ipc-infra/mine-subnet.sh & +./ipc-agent/bin/ipc-infra/mine-subnet.sh +./ipc-agent/bin/ipc-infra/mine-subnet.sh +./ipc-agent/bin/ipc-infra/mine-subnet.sh ``` diff --git a/docs/subnet.md b/docs/subnet.md index 77cb6927d..766da2355 100644 --- a/docs/subnet.md +++ b/docs/subnet.md @@ -131,7 +131,7 @@ With our subnet daemon deployed, and having joined the network, as the minimum n $ ./bin/ipc-infra/mine-subnet.sh 84711d67cf162e30747c4525d69728c4dea8c6b4b35cd89f6d0947fee14bf908 ``` -The mining process is currently run in the foreground in interactive mode. Consider using `nohup ./bin/ipc-infra/mine-subnet.sh` or tmux to run the process in the background and redirect the logs to some file. +The mining process is currently run in the foreground in interactive mode. Consider using screen, tmux, or nohup so as to not block your terminal. ## Running a subnet with several validators @@ -211,5 +211,5 @@ We have everything in place now to start mining. Mining is as simple as running ./bin/ipc-infra/mine-subnet.sh ``` -The mining process is currently run in the foreground in interactive mode. Consider using `nohup ./bin/ipc-infra/mine-subnet.sh` or screen to run the process in the background and redirect the logs to some file as handling the mining process of the three validators in the foreground may be quite cumbersome. +The mining process is currently run in the foreground in interactive mode. The mining process is currently run in the foreground in interactive mode. Consider using screen, tmux, or nohup so as to not block your terminal. From c95487fe868ea8e80f4e2242acefffff655949b6 Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Thu, 20 Apr 2023 18:23:50 +0200 Subject: [PATCH 62/82] Fix typo --- docs/subnet.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/subnet.md b/docs/subnet.md index 766da2355..0adaf601f 100644 --- a/docs/subnet.md +++ b/docs/subnet.md @@ -131,7 +131,7 @@ With our subnet daemon deployed, and having joined the network, as the minimum n $ ./bin/ipc-infra/mine-subnet.sh 84711d67cf162e30747c4525d69728c4dea8c6b4b35cd89f6d0947fee14bf908 ``` -The mining process is currently run in the foreground in interactive mode. Consider using screen, tmux, or nohup so as to not block your terminal. +The mining process is currently run in the foreground in interactive mode. Consider using screen or tmux so as to not block your terminal. ## Running a subnet with several validators @@ -211,5 +211,5 @@ We have everything in place now to start mining. Mining is as simple as running ./bin/ipc-infra/mine-subnet.sh ``` -The mining process is currently run in the foreground in interactive mode. The mining process is currently run in the foreground in interactive mode. Consider using screen, tmux, or nohup so as to not block your terminal. +The mining process is currently run in the foreground in interactive mode. Consider using screen or tmux so as to not block your terminal. From fe31b297db68fc3c88149468d968dcf8657f3713 Mon Sep 17 00:00:00 2001 From: adlrocha Date: Thu, 20 Apr 2023 18:53:16 +0200 Subject: [PATCH 63/82] Fix tag for actor version v0.2.0 (#173) * work around serialization error while fetching votes for a checkpoint * tag v0.2.0 of actors --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7fa1692a8..b060eb1fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,8 +73,8 @@ fvm_ipld_blockstore = "0.1" fvm_ipld_encoding = "0.3" fvm_shared = { version = "=3.0.0-alpha.17", default-features = false } fil_actors_runtime = { git = "https://github.com/consensus-shipyard/fvm-utils", features = ["fil-actor"] } -ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git" } -ipc-subnet-actor = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [] } -ipc-gateway = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [] } +ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git", tag = "v0.2.0" } +ipc-subnet-actor = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [], tag = "v0.2.0" } +ipc-gateway = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [], tag = "v0.2.0" } libipld = { version = "0.14", default-features = false, features = ["dag-cbor"] } primitives = { git = "https://github.com/consensus-shipyard/fvm-utils" } From 493d25e0a5d3f3e927694d469d7a8fa57582ec1f Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Thu, 20 Apr 2023 23:22:48 +0200 Subject: [PATCH 64/82] Add video walkthrough link --- docs/quickstart.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/quickstart.md b/docs/quickstart.md index 83152f48b..e4c892424 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -6,6 +6,8 @@ Ready to test the waters with your first subnet? This guide will deploy a subnet Several steps in this guide involve running long-lived processes. In each of these cases, the guide advises starting a new *session*. Depending on your set-up, you may do this using tools like `screen` or `tmux`, or, if using a graphical environment, by opening a new terminal tab, pane, or window. +>💡A video walkthrough of this guide is also [available](https://www.youtube.com/watch?v=J9Y4_bzGue4). We still encourage you to try it for yourself! + ## Step 0: Prepare your system We assume a Ubuntu Linux instance when discussing prerequisites, but annotate steps with system-specificity and links to detailed multi-OS instructions. Exact procedures will vary for other systems, so please follow the links if running something different. Details on IPC-specific requirements can also be found in the [README](/README.md). From 2d5b067194f0621dc7f43c7057172203bcbd2006 Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Fri, 21 Apr 2023 17:07:22 +0200 Subject: [PATCH 65/82] Fix typo --- docs/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index e4c892424..5d72510e6 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -194,7 +194,7 @@ accounts = ["", "", ""] All the infrastructure for the subnet is now deployed, and we can join our validators to the subnet. For this, we need to send a `join` command from each of our validators from their validator wallet addresses providing the validators multiaddress. -* Join the subnet with each validators +* Join the subnet with each validator ```bash ./ipc-agent/bin/ipc-agent subnet join --from --subnet /root/ --collateral 1 --validator-net-addr ./ipc-agent/bin/ipc-agent subnet join --from --subnet /root/ --collateral 1 --validator-net-addr From 3586fe124d50a4bcad1c4c67d6b84794f9d3addb Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Wed, 26 Apr 2023 12:46:36 +0100 Subject: [PATCH 66/82] IPC-91: Test env (Part 1) (#157) * IPC-91: Fix infra directory mkdir. Exit on fail. * IPC-91: Update init config command * IPC-91: Makefile to build the agent and eudico * IPC-91: Allow empty subnets list * IPC-91: Create agent docker-compose, up and down * IPC-91: make clean * IPC-91: Trying to get a eudico node going * IPC-91: Separate daemon and validator * IPC-91: Docs and clean * IPC-91: Fix API address for the validator * IPC-91: Remove volumes * IPC-91: Copy the infra scripts that needed modification. * IPC-91: Connect script. Fix clean * IPC-91: Try reload the agent config * IPC-91: Fix host to accept connections * IPC-91: Sleep after wait. Fix finding files and config when no subnets yet * IPC-91: Tab to spaces * IPC-91: Spaces to tabs * IPC-91: Config not needed to call reload --- scripts/install_infra.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/install_infra.sh b/scripts/install_infra.sh index 60e98aa66..a60b44b9c 100755 --- a/scripts/install_infra.sh +++ b/scripts/install_infra.sh @@ -2,12 +2,14 @@ # # Builds docker image and install the ipc-scripts required to conveniently # deploy the infrastructure for IPC subnets. + +set -e + rm -rf ./lotus git clone https://github.com/consensus-shipyard/lotus.git cd ./lotus docker build -t eudico . cd .. -mkdir -p ./bin mkdir -p ./bin/ipc-infra cp -rf ./lotus/scripts/ipc/* ./bin/ipc-infra rm -rf ./lotus From 4ea23ef95ee4f346056cdd549cadbc58ed39975c Mon Sep 17 00:00:00 2001 From: Denis Kolegov Date: Mon, 8 May 2023 10:14:43 +0200 Subject: [PATCH 67/82] Fix script for mac --- .gitignore | 3 ++- scripts/install_infra.sh | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 472afa284..d9cc06244 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ *.iml /target docs/diagrams/plantuml.jar -bin \ No newline at end of file +bin +/lotus \ No newline at end of file diff --git a/scripts/install_infra.sh b/scripts/install_infra.sh index a60b44b9c..bf846c0ff 100755 --- a/scripts/install_infra.sh +++ b/scripts/install_infra.sh @@ -8,7 +8,13 @@ set -e rm -rf ./lotus git clone https://github.com/consensus-shipyard/lotus.git cd ./lotus -docker build -t eudico . + +uname=$(uname); +case "$uname" in + (*Darwin*) docker build -t eudico --build-arg FFI_BUILD_FROM_SOURCE=1 . ;; + (*) docker build -t eudico . ;; +esac; + cd .. mkdir -p ./bin/ipc-infra cp -rf ./lotus/scripts/ipc/* ./bin/ipc-infra From 804ce9f7b59a09a00283ee157d654995ff25b5b6 Mon Sep 17 00:00:00 2001 From: adlrocha Date: Wed, 17 May 2023 18:12:35 +0200 Subject: [PATCH 68/82] ID-1: Migrate and adapt `forest` key management crates (#187) * wip: keystore impl * wip: tests pass * add license * fix clippy. slight refactor * Identity API integration (#189) * wip: identity API integration * add import/export server handlers * cargo fmt * cargo clippy * Move identity to its own crate (#192) * move identity to its own crate * fix clippy * Identity management CLI commands (#194) * move identity to its own crate * fix clippy * import/export cli commands * Sign transaction in agent (#196) * move identity to its own crate * fix clippy * import/export cli commands * sign message * Estimate gas (#197) * Return chain epoch for fund and release (#191) * return chain epoch for fund and release * remove unused attributes * Fix script for mac * Replace u64 with f64 for FIL amount (#190) * replace u64 with f64 for amount * update comment * convert all amount to f64 * add gas estimation * add more logs * update rpc endpoints * fix gas * update gas params * fix typo * update import wallet * update import and export methods * update nonce * update deserailzation * update deseriliazation * parse integer * udpate deserialization * integrate wallet store * update type * fix version * update sign * update param encoding * update param encoding * increase nonce * user cid * update nonce --------- Co-authored-by: Denis Kolegov * Estimate gas (#198) * add gas estimation * add more logs * update rpc endpoints * fix gas * update gas params * fix typo * update import wallet * update import and export methods * update nonce * update deserailzation * update deseriliazation * parse integer * udpate deserialization * integrate wallet store * update type * fix version * update sign * update param encoding * update param encoding * increase nonce * user cid * update nonce * format code * remove unused tests --------- Co-authored-by: Alfonso de la Rocha Co-authored-by: Denis Kolegov --------- Co-authored-by: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Co-authored-by: Denis Kolegov --- Cargo.toml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b060eb1fe..9ea25025f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [".", "ipld/resolver", "testing/e2e"] +members = [".", "ipld/resolver", "testing/e2e", "identity"] [workspace.package] authors = ["Protocol Labs"] @@ -24,7 +24,7 @@ indoc = "2.0.0" log = { workspace = true } reqwest = { version = "0.11.13", features = ["json"] } serde = { workspace = true } -serde_json = { version = "1.0.91", features = ["raw_value"] } +serde_json = { workspace = true } cid = { version = "0.8.3", default-features = false, features = ["serde-codec"] } tokio = { workspace = true } tokio-graceful-shutdown = "0.12.1" @@ -33,7 +33,7 @@ derive_builder = "0.12.0" num-traits = "0.2.15" num-derive = "0.3.3" env_logger = "0.10.0" -base64 = "0.21.0" +base64 = { workspace = true } strum = { version = "0.24", features = ["derive"] } toml = "0.7.2" url = { version = "2.3.1", features = ["serde"] } @@ -41,7 +41,7 @@ warp = "0.3.3" bytes = "1.4.0" serde_bytes = "0.11.9" clap = { version = "4.1.4", features = ["env", "derive"] } -thiserror = "1.0.38" +thiserror = { workspace = true } serde_tuple = "0.5.0" fvm_shared = { workspace = true } @@ -52,26 +52,30 @@ ipc-gateway = { workspace = true } fvm_ipld_encoding = { workspace = true } primitives = { workspace = true } +ipc-identity = { path = "identity/." } + [dev-dependencies] tempfile = "3.4.0" [workspace.dependencies] anyhow = "1.0" +base64 = "0.21.0" lazy_static = "1.4" log = "0.4" env_logger = "0.10" prometheus = "0.13" serde = { version = "1.0", features = ["derive"] } -thiserror = "1.0" tokio = { version = "1.16", features = ["full"] } +thiserror = "1.0.38" quickcheck = "1" quickcheck_macros = "1" blake2b_simd = "1.0" rand = "0.8" +serde_json = { version = "1.0.91", features = ["raw_value"] } fvm_ipld_blockstore = "0.1" fvm_ipld_encoding = "0.3" -fvm_shared = { version = "=3.0.0-alpha.17", default-features = false } +fvm_shared = { version = "=3.0.0-alpha.17", default-features = false, features = ["crypto"] } fil_actors_runtime = { git = "https://github.com/consensus-shipyard/fvm-utils", features = ["fil-actor"] } ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git", tag = "v0.2.0" } ipc-subnet-actor = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [], tag = "v0.2.0" } From fecff1999949ceed97a972b24fcdf1f10def7fc2 Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Wed, 17 May 2023 18:53:55 +0200 Subject: [PATCH 69/82] Checkout dev branch for spacenet infra --- scripts/install_infra.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install_infra.sh b/scripts/install_infra.sh index bf846c0ff..d5a637d85 100755 --- a/scripts/install_infra.sh +++ b/scripts/install_infra.sh @@ -6,7 +6,7 @@ set -e rm -rf ./lotus -git clone https://github.com/consensus-shipyard/lotus.git +git clone --branch dev https://github.com/consensus-shipyard/lotus.git cd ./lotus uname=$(uname); From 23de6040905a7959ee663062dfc621792f95e25f Mon Sep 17 00:00:00 2001 From: adlrocha Date: Wed, 17 May 2023 20:34:54 +0200 Subject: [PATCH 70/82] basic support for evm smart contracts in agent (#200) * basic support for evm smart contracts in agent * fix clippy * Update format * Link contracts from readme * Update format --------- Co-authored-by: Jorge Soares <547492+jsoares@users.noreply.github.com> --- Cargo.toml | 5 +++ docs/contracts.md | 57 ++++++++++++++++++++++++++++++++++ docs/img/metamask_add.png | Bin 0 -> 33837 bytes docs/img/metamask_network.png | Bin 0 -> 84690 bytes docs/img/metamask_rpc.png | Bin 0 -> 38265 bytes 5 files changed, 62 insertions(+) create mode 100644 docs/contracts.md create mode 100644 docs/img/metamask_add.png create mode 100644 docs/img/metamask_network.png create mode 100644 docs/img/metamask_rpc.png diff --git a/Cargo.toml b/Cargo.toml index 9ea25025f..ae3f89447 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,3 +82,8 @@ ipc-subnet-actor = { git = "https://github.com/consensus-shipyard/ipc-actors.git ipc-gateway = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [], tag = "v0.2.0" } libipld = { version = "0.14", default-features = false, features = ["dag-cbor"] } primitives = { git = "https://github.com/consensus-shipyard/fvm-utils" } + +# Uncomment to point to you local versions +# [patch."https://github.com/consensus-shipyard/fvm-utils"] +# primitives = { path = "../fvm-utils/primitives" } +# fil_actors_runtime = { path = "../fvm-utils/runtime" } \ No newline at end of file diff --git a/docs/contracts.md b/docs/contracts.md new file mode 100644 index 000000000..14888b2e3 --- /dev/null +++ b/docs/contracts.md @@ -0,0 +1,57 @@ +# Working with EVM smart contracts in subnets + +IPC subnets have the same exact support for the deployment of EVM contracts as the Filecoin network. We highly recommend refering to [the following docs](https://docs.filecoin.io/smart-contracts/fundamentals/overview/) for a detailed description of FEVM and the steps and tooling to use EVM in Filecoin. In this section, we will present the additional steps required to follow the docs in a subnet. + + +## Configuring EVM tooling with your subnet + +In order to connect the Ethereum tooling to your subnet, you'll need to get the RPC endpoint of your subnet peer and the subnet's `chainID`. For this, you can use the following command from your IPC agent to retrieve the RPC endpoint for a specific subnet: + +```bash +./bin/ipc-agent subnet rpc --subnet= + +# Sample command +$ ./bin/ipc-agent subnet rpc --subnet=/root/t01002 +[2023-05-17T15:10:57Z INFO ipc_agent::cli::commands::subnet::rpc] rpc endpoint for subnet /root/t01002: http://127.0.0.1:1240/rpc/v1 +[2023-05-17T15:10:57Z INFO ipc_agent::cli::commands::subnet::rpc] chainID for subnet /root/t01002: 31415926 +``` + +You can also inspect the `json_rpcapi_http` field of your subnet on your config directly to get the RPC endpoint for your subnet. +This RPC endpoint and `chainID will be the ones needed to configure any EVM tooling to connect to your subnet. + + +### Example: Connect Metamask to your subnet + +To connect Metamask to your subnet, you need to add it as a new network. To do this you need to: + +- Click `Add network` in networks section of Metamask. + +![](./img/metamask_add.png) + +- Add a network manually + +![](./img/metamask_network.png) + +- Configure your network by passing to the form the RPC endpoint and `chainID` of your subnet. The `chainID` of IPC subnets will always be `31415926` until [this issue](https://github.com/consensus-shipyard/lotus/issues/178) is implemented. + +![](./img/metamask_rpc.png) + +With this your Metamask should be successfully connected to your subnet, and you should be able to interact with it seamlessly as described in the [Filecoin docs](https://docs.filecoin.io/smart-contracts/fundamentals/overview/). + + +## Deploying a contract in your subnet + +To deploy a smart contract in your subnet the only pre-requirement is to have some funds in the subnet to pay for the gas. To inject funds in your subnet you can follow the steps described [here](./usage.md). + +It is important to note that the IPC agent doesn't understand Ethereum addresses directly, which means that to send funds to an Ethereum address, you will need to send funds to their underlying f4 address. You can use the following command from the IPC agent to get the f4 address for an Ethereum address: + +```bash +./bin/ipc-agent util eth-to-f4-addr --addr= + +$ ./bin/ipc-agent util eth-to-f4-addr --addr=0x6BE1Ccf648c74800380d0520D797a170c808b624 +[2023-05-17T13:37:37Z INFO ipc_agent::cli::commands::util::f4] f4 address: t410fnpq4z5siy5eaaoanauqnpf5bodearnren5fxyoi +``` + +>💡 For more information about the relationship between `f4` and Ethereum addresses refer to [this page](https://docs.filecoin.io/smart-contracts/filecoin-evm-runtime/address-types/). + +From there on, you should be able to follow the same steps currently used to deploy EVM contract in the Filecoin mainnet. You can find here the steps to [deploy an ERC20 contract using Remix](https://docs.filecoin.io/smart-contracts/fundamentals/erc-20-quickstart/). \ No newline at end of file diff --git a/docs/img/metamask_add.png b/docs/img/metamask_add.png new file mode 100644 index 0000000000000000000000000000000000000000..7ac4d133361829ce6ffaf0de47e8cf611c319979 GIT binary patch literal 33837 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

54Cb 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

oxNV|Eor>MPy`y={v6?ku=znx> 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 Date: Thu, 18 May 2023 19:10:32 +0800 Subject: [PATCH 71/82] Refactor checkpoint submission (#175) * refactored code * add sleep * update sleep sec * reorg code * simplify * migrate bottom up checkpoint (#182) * Fix typo * IPC-91: Test env (Part 1) (#157) * IPC-91: Fix infra directory mkdir. Exit on fail. * IPC-91: Update init config command * IPC-91: Makefile to build the agent and eudico * IPC-91: Allow empty subnets list * IPC-91: Create agent docker-compose, up and down * IPC-91: make clean * IPC-91: Trying to get a eudico node going * IPC-91: Separate daemon and validator * IPC-91: Docs and clean * IPC-91: Fix API address for the validator * IPC-91: Remove volumes * IPC-91: Copy the infra scripts that needed modification. * IPC-91: Connect script. Fix clean * IPC-91: Try reload the agent config * IPC-91: Fix host to accept connections * IPC-91: Sleep after wait. Fix finding files and config when no subnets yet * IPC-91: Tab to spaces * IPC-91: Spaces to tabs * IPC-91: Config not needed to call reload * IPC-91: Test environment (Part 2) (#161) * IPC-91: make wallet * IPC-91: Create a wallet for a child node * IPC-91: Keep just one subnet-validator script * IPC-91: Create new subnet * IPC-91: Refer to the wallet key rather than copy. * IPC-91: Start agent by number * IPC-91: Create topology * IPC-91: Fixes, stop on error * IPC-91: Fix target in setup * IPC-91: Remove agent NR from node env * IPC-91: Comments and docs * IPC-91: Simpler subnet to demonstrate the error * IPC-91: Trying to fund the wallet created * IPC-91: Copy genesis files. Use the one with funds in root * IPC-91: Allow adding subnet variables to the topology * IPC-91: Move wallet out of genesis directory * IPC-91: Various fixes * IPC-91: Rename to topologies * IPC-91: Join subnet * IPC-91: Trying to fund the subnet * IPC-91: Add explicit step tombstones so funding only happens once * IPC-91: Add explicit step tombstone to the agent as well. * IPC-91: Join first, then fund * IPC-91: Add env var to construct agent URL to the node * IPC-91: Fix CLI to exit with error code * IPC-91: Fix equality check * IPC-91: Get rid of the wallet address from the validator net addr. * IPC-91: Fix address in root * IPC-91: Give the validator another chance to start * IPC-91: Remove any newlines after docker capture * IPC-91: Fix shadowing of env vars * IPC-91: Ephasize topologies * IPC-91: Remove leftover script * migrate bottom up checkpoint * add get validators and submit * Update src/lotus/client.rs Co-authored-by: adlrocha * Update src/lotus/mod.rs Co-authored-by: adlrocha * make obtain validator common * handle error responses * abstract common methods * more logs and concurrent futures * add pre submission checks: * make lint * remove unused method * Migrate top down (#184) * wip * merge with bottom up * merge with bottom up * Update src/manager/checkpoint/manager/topdown.rs Co-authored-by: adlrocha * Update src/manager/checkpoint/manager/topdown.rs Co-authored-by: adlrocha * Update src/manager/checkpoint/manager/topdown.rs Co-authored-by: adlrocha * Integrate new checkpoint (#185) * integrate new checkpoint system * format code * add error logging * add more logging * add epoch of fund and release * add more logging * update info * update bottom up * update comment * update log * update logging * remove subsystem file --------- Co-authored-by: adlrocha * remove default gateway address const (#188) --------- Co-authored-by: Jorge Soares <547492+jsoares@users.noreply.github.com> Co-authored-by: Akosh Farkash Co-authored-by: adlrocha * update comments * Bottom up checkpoint proof (#199) * add bottom up proof * format proof * wrap inner proof * update comment * Update src/manager/checkpoint/proof/mod.rs Co-authored-by: adlrocha * update proof state * Update src/manager/checkpoint/proof/v1.rs --------- Co-authored-by: adlrocha * update mem push method * update response * share key store * add subnet to lotus client * fix docs --------- Co-authored-by: Jorge Soares <547492+jsoares@users.noreply.github.com> Co-authored-by: Akosh Farkash Co-authored-by: adlrocha --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index ae3f89447..aa087099f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ serde = { workspace = true } serde_json = { workspace = true } cid = { version = "0.8.3", default-features = false, features = ["serde-codec"] } tokio = { workspace = true } +tokio-stream = "0.1.12" tokio-graceful-shutdown = "0.12.1" tokio-tungstenite = { version = "0.18.0", features = ["native-tls"] } derive_builder = "0.12.0" From cb2fc4650068ebaccbc6a328281644269bce4e94 Mon Sep 17 00:00:00 2001 From: adlrocha Date: Mon, 22 May 2023 16:40:16 +0200 Subject: [PATCH 72/82] fix export and docs (#204) * wip: fix export and docs * wrap up docs and fix export key format * fix clippy * Update docs/quickstart.md Co-authored-by: Jorge Soares <547492+jsoares@users.noreply.github.com> * address comments and fix clippy --------- Co-authored-by: Jorge Soares <547492+jsoares@users.noreply.github.com> --- docs/quickstart.md | 39 ++++++++++++++++++++++++--------------- docs/subnet.md | 13 +++++++++++-- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 5d72510e6..b635b2685 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -75,14 +75,13 @@ Let's deploy a eudico instance on Spacenet and configure the IPC Agent to intera * Get configuration parameters ```bash ./lotus/eudico auth create-token --perm admin -./lotus/eudico wallet new ``` * Configure your IPC Agent ```bash ./ipc-agent/bin/ipc-agent config init nano ~/.ipc-agent/config.toml ``` -* Replace the content of `config.toml` with the text below, substituting the token and wallet retrieved above. +* Replace the content of `config.toml` with the text below, substituting the token retrieved above. ```toml [server] json_rpc_address = "0.0.0.0:3030" @@ -93,13 +92,29 @@ gateway_addr = "t064" network_name = "root" jsonrpc_api_http = "http://127.0.0.1:1234/rpc/v1" auth_token = "" -accounts = [""] +accounts = [] ``` * [**In a new session**] Start your IPC Agent ```bash ./ipc-agent/bin/ipc-agent daemon ``` +* Create a new wallet in your agent +```bash +./ipc-agent/bin/ipc-agent wallet new --key-type=secp256k1 +``` + +* Add your new wallet address in the accounts field of your config: +```toml +... +accounts = [""] +... +``` +* And reload your config: +```bash +./ipc-agent/bin/ipc-agent config reload +``` + ## Step 3: Fund your account @@ -121,15 +136,15 @@ Although we set a minimum of 2 active validators in the previous, we'll deploy 3 * First, we'll need to create a wallet for each validator ```bash -./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 --subnet /root -./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 --subnet /root -./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 --subnet /root +./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 +./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 +./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 ``` * Export each wallet (WALLET_1, WALLET_2, and WALLET_3) by substituting their addresses below ```bash -./lotus/eudico wallet export --lotus-json > ~/.ipc-agent/wallet1.key -./lotus/eudico wallet export --lotus-json > ~/.ipc-agent/wallet2.key -./lotus/eudico wallet export --lotus-json > ~/.ipc-agent/wallet3.key +./ipc-agent/bin/ipc-agent wallet export --address= --output=~/.ipc-agent/wallet1.key +./ipc-agent/bin/ipc-agent wallet export --address= --output=~/.ipc-agent/wallet2.key +./ipc-agent/bin/ipc-agent wallet export --address= --output=~/.ipc-agent/wallet3.key ``` * We also need to fund the wallets with enough collateral to; we'll send the funds from our default wallet ```bash @@ -164,12 +179,6 @@ We can deploy the subnet nodes. Note that each node should be importing a differ ## Step 7: Configure the IPC agent For ease of use, we'll import the remaining keys into the first validator, via which the IPC Agent will act on behalf of all. - -* Copy the wallet keys into the docker container and import them -```bash -docker cp ~/.ipc-agent/wallet2.key :/input.key && docker exec -it eudico wallet import --format=json-lotus input.key -docker cp ~/.ipc-agent/wallet3.key :/input.key && docker exec -it eudico wallet import --format=json-lotus input.key -``` * Edit the IPC agent configuration `config.toml` ```bash nano ~/.ipc-agent/config.toml diff --git a/docs/subnet.md b/docs/subnet.md index 0adaf601f..fbc5aed4f 100644 --- a/docs/subnet.md +++ b/docs/subnet.md @@ -10,7 +10,12 @@ We provide instructions for running both a [simple single-validator subnet](#run ### Exporting wallet keys -In order to run a validator in a subnet, we'll need a set of keys to handle that validator. To export the validator key from a wallet that may live in another network into a file (like the wallet address we are using in the rootnet), we can use the following Lotus command: +In order to run a validator in a subnet, we'll need a set of keys to handle that validator. To export the validator key from your agent you need to run: +```bash +./ipc-agent/bin/ipc-agent wallet export --address= --output= +``` + +If for some reason, you want to use for your validator a set of keys that are not managed by the IPC agent, and are held in a raw Eudico node of another network, you can export the wallet key into a file (like the wallet address we are using in the rootnet), with the following Lotus command: *Example*: ```bash @@ -31,8 +36,12 @@ $ docker exec -it ipc_root_1234 eudico wallet export --lotus-json t1cp4q4lqsdhob ``` ### Importing wallet keys +Your agent handles the keys for all of your addresses in IPC and is responsible for signing the transactions to the different networks. To import a key to the agent you can use: +```bash +`./ipc-agent/bin/ipc-agent wallet import --path=` +``` -Depending on whether the subnet is running inside a docker container or not, you may need to import keys into a node. You may use the following commands to import a wallet to a subnet node: +The only operation that requres importing the keys into your raw Eudico node is when running a subnet validator. Subnet validators need to hold the validator keys in their wallets in order to be able to sign new blocks. You may use the following commands to import a wallet directly into the raw subnet node of your validator: ```bash # Bare: Import directly into eudico From 7e11b56c63a7a1a973544c9955d8b9d72db1fca5 Mon Sep 17 00:00:00 2001 From: Jorge Soares <547492+jsoares@users.noreply.github.com> Date: Tue, 23 May 2023 12:45:31 +0200 Subject: [PATCH 73/82] Clean up docs (#207) * Clean up quickstart * Docs fixes * docs cnsistency * Update usage docs * Mark froms as optional --- docs/contracts.md | 8 ++++---- docs/quickstart.md | 12 ++++++------ docs/subnet.md | 11 ++++++----- docs/usage.md | 14 +++++++------- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/docs/contracts.md b/docs/contracts.md index 14888b2e3..837ae82ff 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -8,10 +8,10 @@ IPC subnets have the same exact support for the deployment of EVM contracts as t In order to connect the Ethereum tooling to your subnet, you'll need to get the RPC endpoint of your subnet peer and the subnet's `chainID`. For this, you can use the following command from your IPC agent to retrieve the RPC endpoint for a specific subnet: ```bash -./bin/ipc-agent subnet rpc --subnet= +./bin/ipc-agent subnet rpc --subnet # Sample command -$ ./bin/ipc-agent subnet rpc --subnet=/root/t01002 +$ ./bin/ipc-agent subnet rpc --subnet /root/t01002 [2023-05-17T15:10:57Z INFO ipc_agent::cli::commands::subnet::rpc] rpc endpoint for subnet /root/t01002: http://127.0.0.1:1240/rpc/v1 [2023-05-17T15:10:57Z INFO ipc_agent::cli::commands::subnet::rpc] chainID for subnet /root/t01002: 31415926 ``` @@ -46,9 +46,9 @@ To deploy a smart contract in your subnet the only pre-requirement is to have so It is important to note that the IPC agent doesn't understand Ethereum addresses directly, which means that to send funds to an Ethereum address, you will need to send funds to their underlying f4 address. You can use the following command from the IPC agent to get the f4 address for an Ethereum address: ```bash -./bin/ipc-agent util eth-to-f4-addr --addr= +./bin/ipc-agent util eth-to-f4-addr --addr -$ ./bin/ipc-agent util eth-to-f4-addr --addr=0x6BE1Ccf648c74800380d0520D797a170c808b624 +$ ./bin/ipc-agent util eth-to-f4-addr --addr 0x6BE1Ccf648c74800380d0520D797a170c808b624 [2023-05-17T13:37:37Z INFO ipc_agent::cli::commands::util::f4] f4 address: t410fnpq4z5siy5eaaoanauqnpf5bodearnren5fxyoi ``` diff --git a/docs/quickstart.md b/docs/quickstart.md index b635b2685..b64daa14e 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -59,7 +59,7 @@ git clone https://github.com/consensus-shipyard/ipc-agent.git ``` * Download and compile eudico (might take a while) ```bash -git clone https://github.com/consensus-shipyard/lotus.git +git clone --branch spacenet https://github.com/consensus-shipyard/lotus.git (cd lotus && make spacenet) ``` @@ -101,7 +101,7 @@ accounts = [] * Create a new wallet in your agent ```bash -./ipc-agent/bin/ipc-agent wallet new --key-type=secp256k1 +./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 ``` * Add your new wallet address in the accounts field of your config: @@ -142,9 +142,9 @@ Although we set a minimum of 2 active validators in the previous, we'll deploy 3 ``` * Export each wallet (WALLET_1, WALLET_2, and WALLET_3) by substituting their addresses below ```bash -./ipc-agent/bin/ipc-agent wallet export --address= --output=~/.ipc-agent/wallet1.key -./ipc-agent/bin/ipc-agent wallet export --address= --output=~/.ipc-agent/wallet2.key -./ipc-agent/bin/ipc-agent wallet export --address= --output=~/.ipc-agent/wallet3.key +./ipc-agent/bin/ipc-agent wallet export --address --output ~/.ipc-agent/wallet1.key +./ipc-agent/bin/ipc-agent wallet export --address --output ~/.ipc-agent/wallet2.key +./ipc-agent/bin/ipc-agent wallet export --address --output ~/.ipc-agent/wallet3.key ``` * We also need to fund the wallets with enough collateral to; we'll send the funds from our default wallet ```bash @@ -169,7 +169,7 @@ We can deploy the subnet nodes. Note that each node should be importing a differ >>> Subnet /root/ daemon running in container: (friendly name: ) >>> Token to /root/ daemon: >>> Default wallet: ->>> Subnet subnet validator info: +>>> Subnet validator info: >>> API listening in host port >>> Validator listening in host port diff --git a/docs/subnet.md b/docs/subnet.md index fbc5aed4f..3edbeb7a8 100644 --- a/docs/subnet.md +++ b/docs/subnet.md @@ -12,7 +12,7 @@ We provide instructions for running both a [simple single-validator subnet](#run In order to run a validator in a subnet, we'll need a set of keys to handle that validator. To export the validator key from your agent you need to run: ```bash -./ipc-agent/bin/ipc-agent wallet export --address= --output= +./ipc-agent/bin/ipc-agent wallet export --address --output ``` If for some reason, you want to use for your validator a set of keys that are not managed by the IPC agent, and are held in a raw Eudico node of another network, you can export the wallet key into a file (like the wallet address we are using in the rootnet), with the following Lotus command: @@ -38,14 +38,14 @@ $ docker exec -it ipc_root_1234 eudico wallet export --lotus-json t1cp4q4lqsdhob ### Importing wallet keys Your agent handles the keys for all of your addresses in IPC and is responsible for signing the transactions to the different networks. To import a key to the agent you can use: ```bash -`./ipc-agent/bin/ipc-agent wallet import --path=` +`./ipc-agent/bin/ipc-agent wallet import --path ` ``` The only operation that requres importing the keys into your raw Eudico node is when running a subnet validator. Subnet validators need to hold the validator keys in their wallets in order to be able to sign new blocks. You may use the following commands to import a wallet directly into the raw subnet node of your validator: ```bash # Bare: Import directly into eudico -./eudico wallet import --lotus-json +./eudico wallet import --format lotus-json ``` ```console # Example execution @@ -54,11 +54,11 @@ $ ./eudico wallet import --lotus-json ~/.ipc-agent/wallet.key ```bash # Docker: Copy the wallet key into the container and import into eudico -docker cp : && docker exec -it eudico wallet import --format=json-lotus +docker cp : && docker exec -it eudico wallet import --format json-lotus ``` ```console # Example execution -$ docker cp ~/.ipc-agent/wallet.key ipc_root_t01002_1250:/input.key && docker exec -it ipc_root_t01002_1250 eudico wallet import --format=json-lotus input.key +$ docker cp ~/.ipc-agent/wallet.key ipc_root_t01002_1250:/input.key && docker exec -it ipc_root_t01002_1250 eudico wallet import --format json-lotus input.key ``` ## Running a simple subnet with a single validator @@ -158,6 +158,7 @@ With this, we can already create the subnet with `/root` as its parent. We are g ```bash ./bin/ipc-agent subnet create --parent /root --name test --min-validator-stake 1 --min-validators 2 --bottomup-check-period 30 --topdown-check-period 30 ``` + ### Deploying the infrastructure In order to deploy the 3 validators for the subnet, we will have to first export the keys from our root node so we can import them to our validators. Depending on how you are running your rootnet node you'll have to make a call to the docker container, or your nodes API. More information about exporting keys from your node can be found under [this section](#Exporting-wallet-keys). diff --git a/docs/usage.md b/docs/usage.md index 9998145c4..fa7cb38a3 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -7,7 +7,7 @@ As a sanity-check that we have joined the subnet successfully and that we provided enough collateral to register the subnet to IPC, we can list the child subnets of our parent with the following command: ```bash -./bin/ipc-agent list-subnets --gateway-address= --subnet= +./bin/ipc-agent list-subnets --gateway-address --subnet ``` ```console # Example execution @@ -32,7 +32,7 @@ This command specifies the subnet to join, the amount of collateral to provide a ## Listing your balance in a subnet In order to send messages in a subnet, you'll need to have funds in your subnt account. You can use the following command to list the balance of your wallets in a subnet: ```bash -./bin/ipc-agent wallet list --subnet= +./bin/ipc-agent wallet list --subnet ``` ```console # Example execution @@ -44,7 +44,7 @@ ipc_agent::cli::commands::wallet::list] wallets in subnet /root are {"t1cp4q4lqs The agent provides a command to conveniently exchange funds between addresses of the same subnet. This can be achieved through the following command: ```bash -./bin/ipc-agent subnet send-value --subnet --to +./bin/ipc-agent subnet send-value --subnet [--from ] --to ``` ```console # Example execution @@ -64,11 +64,11 @@ Complex behavior can be implemented using these primitives: sending value to a u ### Fund Funding a subnet can be performed by using the following command: ```bash -./bin/ipc-agent cross-msg fund --subnet= +./bin/ipc-agent cross-msg fund --subnet [--from ] ``` ```console # Example execution -$ ./bin/ipc-agent cross-msg fund --subnet=/root/t01002 100 +$ ./bin/ipc-agent cross-msg fund --subnet /root/t01002 100 ``` This command includes the cross-net message into the next top-down checkpoint after the current epoch. Once the top-down checkpoint is committed, you should see the funds in your account of the child subnet. @@ -77,7 +77,7 @@ This command includes the cross-net message into the next top-down checkpoint af ### Release In order to release funds from a subnet, your account must hold enough funds inside it. Releasing funds to the parent subnet can be permformed with the following comand: ```bash -./bin/ipc-agent cross-msg release --subnet= +./bin/ipc-agent cross-msg release --subnet [--from ] ``` ```console # Example execution @@ -104,7 +104,7 @@ You can find the checkpoint where your cross-message was included by listing the ## Checking the health of top-down checkpoints In order to check the health of top-down checkpointing in a subnet, the following command can be run: ```bash -./bin/ipc-agent checkpoint last-topdown --subnet= +./bin/ipc-agent checkpoint last-topdown --subnet ``` ```console # Example execution From 3f75a6aef20c248226537cea0fae0637da97c6eb Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Tue, 30 May 2023 14:02:39 +0800 Subject: [PATCH 74/82] Integration testing infra (#206) * wip infra * add infra spawn * log errors * unwrap error * slepp * fix typo * update command * update command * update genes path * update command format * update string * update string to str * use full paht * update command * pipe logs * update genesis for spawn * update logging * already created genesis * update net listne * ignore output * log error status * update stdout * full process * add more error logging * add more error logging * update lotus path * force wait wallet create * force wait wallet create * add more logs * trim new line * update default port * update lotus path * add more logs * revert command * update command * update subnet * update lotus binary path * update eudico path * update nonce * update env * update config validator * force init validator * update config validator * use home env variable * use home env variable * update validator address * export wallet * process validator * update subnet id * add env * update auth token * lint * update sleep * update print * update subnet * print config * update lint * update based on review * Add ipc agent sdk (#208) * add ipc sdk * add headers * Checkpoint integration tests (#210) * add ipc sdk * add headers * refactor for checkpoint testing * make lint * execude itest from unit tests * add examples * add wo creating subnet * add more logs * add config serialization * more logs: * add more scripts * rename files * fix lint * update no tear down * support ctrl c * add clean up * add fund and release * update logging * add sleep: * change to println in tests * add more print * update readme --- Cargo.toml | 3 ++- Makefile | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aa087099f..a4ef67e70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [".", "ipld/resolver", "testing/e2e", "identity"] +members = [".", "ipld/resolver", "testing/e2e", "identity", "testing/itest"] [workspace.package] authors = ["Protocol Labs"] @@ -44,6 +44,7 @@ serde_bytes = "0.11.9" clap = { version = "4.1.4", features = ["env", "derive"] } thiserror = { workspace = true } serde_tuple = "0.5.0" +zeroize = "1.6.0" fvm_shared = { workspace = true } fil_actors_runtime = { workspace = true } diff --git a/Makefile b/Makefile index e31d7ad6e..99a0b07c2 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,10 @@ build: cargo build -Z unstable-options --release --out-dir ./bin test: - cargo test --release --workspace --exclude ipc_e2e + cargo test --release --workspace --exclude ipc_e2e itest + +itest: + cargo test -p itest --test checkpoint -- --nocapture e2e: cargo test --release -p ipc_e2e From 863e8320e7c92cc65554b3c34f3f6629e01be519 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Fri, 2 Jun 2023 11:06:22 +0100 Subject: [PATCH 75/82] CHORE: Cleanup after splitting out from IPC Agent --- .github/workflows/ci.yaml | 91 +++++++++++ .gitignore | 3 +- Cargo.toml | 64 +------- Makefile | 15 +- ipld/resolver/README.md => README.md | 4 +- docs/architecture.md | 8 +- docs/contracts.md | 57 ------- docs/img/metamask_add.png | Bin 33837 -> 0 bytes docs/img/metamask_network.png | Bin 84690 -> 0 bytes docs/img/metamask_rpc.png | Bin 38265 -> 0 bytes docs/ipc_postman_collection.json | 207 ------------------------ docs/quickstart.md | 231 --------------------------- docs/subnet.md | 225 -------------------------- docs/troubleshooting.md | 56 ------- docs/usage.md | 127 --------------- ipld/resolver/Cargo.toml | 4 +- scripts/install_infra.sh | 21 --- 17 files changed, 100 insertions(+), 1013 deletions(-) create mode 100644 .github/workflows/ci.yaml rename ipld/resolver/README.md => README.md (91%) delete mode 100644 docs/contracts.md delete mode 100644 docs/img/metamask_add.png delete mode 100644 docs/img/metamask_network.png delete mode 100644 docs/img/metamask_rpc.png delete mode 100644 docs/ipc_postman_collection.json delete mode 100644 docs/quickstart.md delete mode 100644 docs/subnet.md delete mode 100644 docs/troubleshooting.md delete mode 100644 docs/usage.md delete mode 100755 scripts/install_infra.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 000000000..354e1c275 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,91 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - '**' + +jobs: + # Check code formatting; anything that doesn't require compilation. + pre-compile-checks: + name: Pre-compile checks + runs-on: ubuntu-latest + steps: + - name: Check out the project + uses: actions/checkout@v3 + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + components: rustfmt + - name: Check code formatting + run: make check-fmt + - name: Check license headers + run: make license + # - name: Check diagrams + # run: make check-diagrams + + # Test matrix, running tasks from the Makefile. + tests: + needs: [pre-compile-checks] + name: ${{ matrix.make.name }} (${{ matrix.os }}, ${{ matrix.rust }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + rust: [nightly] + make: + - name: Lint + task: lint + - name: Test + task: test + exclude: + - rust: stable + make: + name: Lint + + env: + RUST_BACKTRACE: full + RUSTFLAGS: -Dwarnings + CARGO_INCREMENTAL: '0' + SCCACHE_CACHE_SIZE: 10G + CC: "sccache clang" + CXX: "sccache clang++" + + steps: + - name: Check out the project + uses: actions/checkout@v3 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + target: wasm32-unknown-unknown + toolchain: ${{ matrix.rust }} + components: rustfmt,clippy + + # Protobuf compiler required by libp2p-core + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup sccache + uses: hanabi1224/sccache-action@v1.2.0 # https://github.com/hanabi1224/sccache-action used by Forest. + timeout-minutes: 5 + continue-on-error: true + with: + release-name: v0.3.1 + # Caching everything separately, in case they don't ask for the same things to be compiled. + cache-key: ${{ matrix.make.name }}-${{ matrix.os }}-${{matrix.rust}}-${{ hashFiles('Cargo.lock', 'rust-toolchain', 'rust-toolchain.toml') }} + # Not sure why we should ever update a cache that has the hash of the lock file in it. + # In Forest it only contains the rust-toolchain, so it makes sense to update because dependencies could have changed. + cache-update: false + + - name: ${{ matrix.make.name }} + run: make ${{ matrix.make.task }} diff --git a/.gitignore b/.gitignore index d9cc06244..a5c943bd7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,4 @@ *.iml /target docs/diagrams/plantuml.jar -bin -/lotus \ No newline at end of file +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index a4ef67e70..785ed2647 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,64 +1,11 @@ [workspace] -members = [".", "ipld/resolver", "testing/e2e", "identity", "testing/itest"] +members = ["ipld/resolver"] [workspace.package] authors = ["Protocol Labs"] edition = "2021" license-file = "LICENSE" -[package] -name = "ipc-agent" -version = "0.1.0" -edition.workspace = true -license-file.workspace = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -anyhow = { workspace = true } -async-channel = "1.8.0" -async-trait = "0.1.61" -futures = "0.3.28" -futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } -indoc = "2.0.0" -log = { workspace = true } -reqwest = { version = "0.11.13", features = ["json"] } -serde = { workspace = true } -serde_json = { workspace = true } -cid = { version = "0.8.3", default-features = false, features = ["serde-codec"] } -tokio = { workspace = true } -tokio-stream = "0.1.12" -tokio-graceful-shutdown = "0.12.1" -tokio-tungstenite = { version = "0.18.0", features = ["native-tls"] } -derive_builder = "0.12.0" -num-traits = "0.2.15" -num-derive = "0.3.3" -env_logger = "0.10.0" -base64 = { workspace = true } -strum = { version = "0.24", features = ["derive"] } -toml = "0.7.2" -url = { version = "2.3.1", features = ["serde"] } -warp = "0.3.3" -bytes = "1.4.0" -serde_bytes = "0.11.9" -clap = { version = "4.1.4", features = ["env", "derive"] } -thiserror = { workspace = true } -serde_tuple = "0.5.0" -zeroize = "1.6.0" - -fvm_shared = { workspace = true } -fil_actors_runtime = { workspace = true } -ipc-sdk = { workspace = true } -ipc-subnet-actor = { workspace = true } -ipc-gateway = { workspace = true } -fvm_ipld_encoding = { workspace = true } -primitives = { workspace = true } - -ipc-identity = { path = "identity/." } - -[dev-dependencies] -tempfile = "3.4.0" - [workspace.dependencies] anyhow = "1.0" base64 = "0.21.0" @@ -78,14 +25,5 @@ serde_json = { version = "1.0.91", features = ["raw_value"] } fvm_ipld_blockstore = "0.1" fvm_ipld_encoding = "0.3" fvm_shared = { version = "=3.0.0-alpha.17", default-features = false, features = ["crypto"] } -fil_actors_runtime = { git = "https://github.com/consensus-shipyard/fvm-utils", features = ["fil-actor"] } ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git", tag = "v0.2.0" } -ipc-subnet-actor = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [], tag = "v0.2.0" } -ipc-gateway = { git = "https://github.com/consensus-shipyard/ipc-actors.git", features = [], tag = "v0.2.0" } libipld = { version = "0.14", default-features = false, features = ["dag-cbor"] } -primitives = { git = "https://github.com/consensus-shipyard/fvm-utils" } - -# Uncomment to point to you local versions -# [patch."https://github.com/consensus-shipyard/fvm-utils"] -# primitives = { path = "../fvm-utils/primitives" } -# fil_actors_runtime = { path = "../fvm-utils/runtime" } \ No newline at end of file diff --git a/Makefile b/Makefile index 99a0b07c2..c5ef24e9c 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,12 @@ -.PHONY: all build test lint license check-fmt check-clippy diagrams install_infra +.PHONY: all build test lint license check-fmt check-clippy diagrams all: test build build: - cargo build -Z unstable-options --release --out-dir ./bin + cargo build -Z unstable-options --release test: - cargo test --release --workspace --exclude ipc_e2e itest - -itest: - cargo test -p itest --test checkpoint -- --nocapture - -e2e: - cargo test --release -p ipc_e2e + cargo test --release --workspace clean: cargo clean @@ -25,9 +19,6 @@ lint: \ license: ./scripts/add_license.sh -install-infra: - ./scripts/install_infra.sh - check-fmt: cargo fmt --all --check diff --git a/ipld/resolver/README.md b/README.md similarity index 91% rename from ipld/resolver/README.md rename to README.md index cfa6d5f03..0b78263f5 100644 --- a/ipld/resolver/README.md +++ b/README.md @@ -2,11 +2,11 @@ The IPLD Resolver is a Peer-to-Peer library which can be used to resolve arbitrary CIDs from subnets in InterPlanetary Consensus. -See the [docs](../../docs/) for a conceptual overview. +See the [docs](./docs/) for a conceptual overview. ## Usage -Please have a look at the [smoke test](tests/smoke.rs) for an example of using the library. +Please have a look at the [smoke test](./ipld/resolver/tests/smoke.rs) for an example of using the library. The following snippet demonstrates how one would create a resolver instance and use it: diff --git a/docs/architecture.md b/docs/architecture.md index e83c6063b..9892a7da7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,12 +1,6 @@ -# IPC Agent Architecture - ->💡 For background and setup information, make sure to start with the [README](/README.md). - -The IPC Agent is a process faciliting the participation of Filecoin clients like Lotus in the InterPlanetary Consensus (formerly Hierarchical Consensus). - # IPLD Resolver -The [IPLD Resolver](/ipld/resolver) is a library that IPC Agents can use to exchange data between subnets in IPLD format. +The [IPLD Resolver](/ipld/resolver) is a library that [IPC Agents](https://github.com/consensus-shipyard/ipc-agent/) can use to exchange data between subnets in IPLD format. ## Checkpointing diff --git a/docs/contracts.md b/docs/contracts.md deleted file mode 100644 index 837ae82ff..000000000 --- a/docs/contracts.md +++ /dev/null @@ -1,57 +0,0 @@ -# Working with EVM smart contracts in subnets - -IPC subnets have the same exact support for the deployment of EVM contracts as the Filecoin network. We highly recommend refering to [the following docs](https://docs.filecoin.io/smart-contracts/fundamentals/overview/) for a detailed description of FEVM and the steps and tooling to use EVM in Filecoin. In this section, we will present the additional steps required to follow the docs in a subnet. - - -## Configuring EVM tooling with your subnet - -In order to connect the Ethereum tooling to your subnet, you'll need to get the RPC endpoint of your subnet peer and the subnet's `chainID`. For this, you can use the following command from your IPC agent to retrieve the RPC endpoint for a specific subnet: - -```bash -./bin/ipc-agent subnet rpc --subnet - -# Sample command -$ ./bin/ipc-agent subnet rpc --subnet /root/t01002 -[2023-05-17T15:10:57Z INFO ipc_agent::cli::commands::subnet::rpc] rpc endpoint for subnet /root/t01002: http://127.0.0.1:1240/rpc/v1 -[2023-05-17T15:10:57Z INFO ipc_agent::cli::commands::subnet::rpc] chainID for subnet /root/t01002: 31415926 -``` - -You can also inspect the `json_rpcapi_http` field of your subnet on your config directly to get the RPC endpoint for your subnet. -This RPC endpoint and `chainID will be the ones needed to configure any EVM tooling to connect to your subnet. - - -### Example: Connect Metamask to your subnet - -To connect Metamask to your subnet, you need to add it as a new network. To do this you need to: - -- Click `Add network` in networks section of Metamask. - -![](./img/metamask_add.png) - -- Add a network manually - -![](./img/metamask_network.png) - -- Configure your network by passing to the form the RPC endpoint and `chainID` of your subnet. The `chainID` of IPC subnets will always be `31415926` until [this issue](https://github.com/consensus-shipyard/lotus/issues/178) is implemented. - -![](./img/metamask_rpc.png) - -With this your Metamask should be successfully connected to your subnet, and you should be able to interact with it seamlessly as described in the [Filecoin docs](https://docs.filecoin.io/smart-contracts/fundamentals/overview/). - - -## Deploying a contract in your subnet - -To deploy a smart contract in your subnet the only pre-requirement is to have some funds in the subnet to pay for the gas. To inject funds in your subnet you can follow the steps described [here](./usage.md). - -It is important to note that the IPC agent doesn't understand Ethereum addresses directly, which means that to send funds to an Ethereum address, you will need to send funds to their underlying f4 address. You can use the following command from the IPC agent to get the f4 address for an Ethereum address: - -```bash -./bin/ipc-agent util eth-to-f4-addr --addr - -$ ./bin/ipc-agent util eth-to-f4-addr --addr 0x6BE1Ccf648c74800380d0520D797a170c808b624 -[2023-05-17T13:37:37Z INFO ipc_agent::cli::commands::util::f4] f4 address: t410fnpq4z5siy5eaaoanauqnpf5bodearnren5fxyoi -``` - ->💡 For more information about the relationship between `f4` and Ethereum addresses refer to [this page](https://docs.filecoin.io/smart-contracts/filecoin-evm-runtime/address-types/). - -From there on, you should be able to follow the same steps currently used to deploy EVM contract in the Filecoin mainnet. You can find here the steps to [deploy an ERC20 contract using Remix](https://docs.filecoin.io/smart-contracts/fundamentals/erc-20-quickstart/). \ No newline at end of file diff --git a/docs/img/metamask_add.png b/docs/img/metamask_add.png deleted file mode 100644 index 7ac4d133361829ce6ffaf0de47e8cf611c319979..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33837 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

54Cb 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

oxNV|Eor>MPy`y={v6?ku=znx> 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💡 Background and detailed are available in the [README](/README.md). - -Ready to test the waters with your first subnet? This guide will deploy a subnet with multiple local validators orchestrated by the same IPC agent. This subnet will be anchored to the public Spacenet. This will be a minimal example and may not work on all systems. The full documentation provides more details on each step. - -Several steps in this guide involve running long-lived processes. In each of these cases, the guide advises starting a new *session*. Depending on your set-up, you may do this using tools like `screen` or `tmux`, or, if using a graphical environment, by opening a new terminal tab, pane, or window. - ->💡A video walkthrough of this guide is also [available](https://www.youtube.com/watch?v=J9Y4_bzGue4). We still encourage you to try it for yourself! - -## Step 0: Prepare your system - -We assume a Ubuntu Linux instance when discussing prerequisites, but annotate steps with system-specificity and links to detailed multi-OS instructions. Exact procedures will vary for other systems, so please follow the links if running something different. Details on IPC-specific requirements can also be found in the [README](/README.md). - -* Install basic dependencies [Ubuntu/Debian] ([details](https://lotus.filecoin.io/lotus/install/prerequisites/#supported-platforms)) -```bash -sudo apt update && sudo apt install build-essential libssl-dev mesa-opencl-icd ocl-icd-opencl-dev gcc git bzr jq pkg-config curl clang hwloc libhwloc-dev wget ca-certificates gnupg -y -``` - -* Install Rust [Linux] ([details](https://www.rust-lang.org/tools/install)) -```bash -curl https://sh.rustup.rs -sSf | sh -source "$HOME/.cargo/env" -rustup target add wasm32-unknown-unknown -``` - -* Install Go [Linux] ([details](https://go.dev/doc/install)) -```bash -curl -fsSL https://golang.org/dl/go1.19.7.linux-amd64.tar.gz | sudo tar -xz -C /usr/local -echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc && source ~/.bashrc -``` - -* Install Docker Engine [Ubuntu] ([details](https://docs.docker.com/engine/install/)) -```bash -sudo install -m 0755 -d /etc/apt/keyrings -curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg -sudo chmod a+r /etc/apt/keyrings/docker.gpg -echo \ - "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ - "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \ - sudo tee /etc/apt/sources.list.d/docker.list > /dev/null -sudo apt-get update && sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y -sudo usermod -aG docker $USER && newgrp docker -``` - - -## Step 1: Build the IPC stack - -Next, we'll download and build the different components (IPC agent, docker images, and eudico). - -* Pick a folder where to build the IPC stack. In this example, we'll go with `~/ipc/`. -```bash -mkdir -p ~/ipc/ && cd ~/ipc/ -``` -* Download and compile the IPC Agent (might take a while) -```bash -git clone https://github.com/consensus-shipyard/ipc-agent.git -(cd ipc-agent && make build && make install-infra) -``` -* Download and compile eudico (might take a while) -```bash -git clone --branch spacenet https://github.com/consensus-shipyard/lotus.git -(cd lotus && make spacenet) -``` - - -## Step 2: Deploy a Spacenet node - -Let's deploy a eudico instance on Spacenet and configure the IPC Agent to interact with it. - -* [**In a new session**] Start your eudico instance (might take a while to sync the chain) -```bash -./lotus/eudico mir daemon --bootstrap -``` -* Get configuration parameters -```bash -./lotus/eudico auth create-token --perm admin -``` -* Configure your IPC Agent -```bash -./ipc-agent/bin/ipc-agent config init -nano ~/.ipc-agent/config.toml -``` -* Replace the content of `config.toml` with the text below, substituting the token retrieved above. -```toml -[server] -json_rpc_address = "0.0.0.0:3030" - -[[subnets]] -id = "/root" -gateway_addr = "t064" -network_name = "root" -jsonrpc_api_http = "http://127.0.0.1:1234/rpc/v1" -auth_token = "" -accounts = [] -``` -* [**In a new session**] Start your IPC Agent -```bash -./ipc-agent/bin/ipc-agent daemon -``` - -* Create a new wallet in your agent -```bash -./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 -``` - -* Add your new wallet address in the accounts field of your config: -```toml -... -accounts = [""] -... -``` -* And reload your config: -```bash -./ipc-agent/bin/ipc-agent config reload -``` - - -## Step 3: Fund your account - -* Obtain some Spacenet FIL by requesting it from the [faucet](https://faucet.spacenet.ipc.space/), using your wallet address. - - -## Step 4: Create the subnet - -* The next step is to create a subnet under `/root` -```bash -./ipc-agent/bin/ipc-agent subnet create --parent /root --name andromeda --min-validator-stake 1 --min-validators 2 --bottomup-check-period 30 --topdown-check-period 30 -``` -* Make a note of the address of the subnet you created (`/root/`) - - -## Step 5: Create and export validator wallets - -Although we set a minimum of 2 active validators in the previous, we'll deploy 3 validators to add some redundancy. - -* First, we'll need to create a wallet for each validator -```bash -./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 -./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 -./ipc-agent/bin/ipc-agent wallet new --key-type secp256k1 -``` -* Export each wallet (WALLET_1, WALLET_2, and WALLET_3) by substituting their addresses below -```bash -./ipc-agent/bin/ipc-agent wallet export --address --output ~/.ipc-agent/wallet1.key -./ipc-agent/bin/ipc-agent wallet export --address --output ~/.ipc-agent/wallet2.key -./ipc-agent/bin/ipc-agent wallet export --address --output ~/.ipc-agent/wallet3.key -``` -* We also need to fund the wallets with enough collateral to; we'll send the funds from our default wallet -```bash -./ipc-agent/bin/ipc-agent subnet send-value --subnet /root --to 2 -./ipc-agent/bin/ipc-agent subnet send-value --subnet /root --to 2 -./ipc-agent/bin/ipc-agent subnet send-value --subnet /root --to 2 -``` - - -## Step 6: Deploy the infrastructure - -We can deploy the subnet nodes. Note that each node should be importing a different wallet key for their validator, and should be exposing different ports. If these ports are unavailable in your system, please pick different ones. - -* Deploy and run a container for each validator, importing the corresponding wallet keys -```bash -./ipc-agent/bin/ipc-infra/run-subnet-docker.sh 1251 1351 /root/ ~/.ipc-agent/wallet1.key -./ipc-agent/bin/ipc-infra/run-subnet-docker.sh 1252 1352 /root/ ~/.ipc-agent/wallet2.key -./ipc-agent/bin/ipc-infra/run-subnet-docker.sh 1253 1353 /root/ ~/.ipc-agent/wallet3.key -``` -* If the deployment is successful, each of these nodes should return the following output at the end of their logs. Save the information for the next step. -``` ->>> Subnet /root/ daemon running in container: (friendly name: ) ->>> Token to /root/ daemon: ->>> Default wallet: ->>> Subnet validator info: - ->>> API listening in host port ->>> Validator listening in host port -``` - - -## Step 7: Configure the IPC agent - -For ease of use, we'll import the remaining keys into the first validator, via which the IPC Agent will act on behalf of all. -* Edit the IPC agent configuration `config.toml` -```bash -nano ~/.ipc-agent/config.toml -``` -* Append the new subnet to the configuration -```toml -[[subnets]] -id = "/root/" -gateway_addr = "t064" -network_name = "andromeda" -jsonrpc_api_http = "http://127.0.0.1:1251/rpc/v1" -auth_token = "" -accounts = ["", "", ""] -``` -* Reload the config -```bash -./ipc-agent/bin/ipc-agent config reload -``` - - -## Step 8: Join the subnet - -All the infrastructure for the subnet is now deployed, and we can join our validators to the subnet. For this, we need to send a `join` command from each of our validators from their validator wallet addresses providing the validators multiaddress. - -* Join the subnet with each validator -```bash -./ipc-agent/bin/ipc-agent subnet join --from --subnet /root/ --collateral 1 --validator-net-addr -./ipc-agent/bin/ipc-agent subnet join --from --subnet /root/ --collateral 1 --validator-net-addr -./ipc-agent/bin/ipc-agent subnet join --from --subnet /root/ --collateral 1 --validator-net-addr -``` - - -## Step 9: Start validating! - -We have everything in place now to start validating. Run the following script for each of the validators [**each in a new session**], passing the container names: -```bash -./ipc-agent/bin/ipc-infra/mine-subnet.sh -./ipc-agent/bin/ipc-infra/mine-subnet.sh -./ipc-agent/bin/ipc-infra/mine-subnet.sh -``` - - -## Step 10: What now? - -* Check that the subnet is running -```bash -./ipc-agent/bin/ipc-agent subnet list --gateway-address t064 --subnet /root -``` -* If something went wrong, please have a look at the [README](https://github.com/consensus-shipyard/ipc-agent). If it doesn't help, please join us in #ipc-help. In either case, let us know your experience! -* Please note that to repeat this guide or spawn a new subnet, you may need to change the parameters or reset your system. diff --git a/docs/subnet.md b/docs/subnet.md deleted file mode 100644 index 3edbeb7a8..000000000 --- a/docs/subnet.md +++ /dev/null @@ -1,225 +0,0 @@ -# Deploying IPC subnet infrastructure - ->💡 For background and setup information, make sure to start with the [README](/README.md). - -To spawn a new subnet, our IPC agent should be connected to the parent subnet (or rootnet) from which we plan to deploy a new subnet. Please refer to the [README](/README.md) for information on how to run or connect to a rootnet. This instructions will assume the deployment of a subnet from `/root`, but the steps are equivalent for any other parent subnet. - -We provide instructions for running both a [simple single-validator subnet](#running-a-simple-subnet-with-a-single-validator) and a more useful [multi-validator subnet](#running-a-subnet-with-several-validators). The two sets mostly overlap. - -## Preliminaries - -### Exporting wallet keys - -In order to run a validator in a subnet, we'll need a set of keys to handle that validator. To export the validator key from your agent you need to run: -```bash -./ipc-agent/bin/ipc-agent wallet export --address --output -``` - -If for some reason, you want to use for your validator a set of keys that are not managed by the IPC agent, and are held in a raw Eudico node of another network, you can export the wallet key into a file (like the wallet address we are using in the rootnet), with the following Lotus command: - -*Example*: -```bash -./eudico wallet export --lotus-json > -``` -```console -# Example execution -$ ./eudico wallet export --lotus-json t1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq > ~/.ipc-agent/wallet.key -``` - -If your daemon is running on a docker container, you can get the container id or name (provided also in the output of the infra scripts), and run the following command above inside a container outputting the exported private key into a file locally: -```bash -docker exec -it eudico wallet export --lotus-json > ~/.ipc-agent/wallet.key -``` -```console -# Example execution -$ docker exec -it ipc_root_1234 eudico wallet export --lotus-json t1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq > ~/.ipc-agent/wallet.key -``` - -### Importing wallet keys -Your agent handles the keys for all of your addresses in IPC and is responsible for signing the transactions to the different networks. To import a key to the agent you can use: -```bash -`./ipc-agent/bin/ipc-agent wallet import --path ` -``` - -The only operation that requres importing the keys into your raw Eudico node is when running a subnet validator. Subnet validators need to hold the validator keys in their wallets in order to be able to sign new blocks. You may use the following commands to import a wallet directly into the raw subnet node of your validator: - -```bash -# Bare: Import directly into eudico -./eudico wallet import --format lotus-json -``` -```console -# Example execution -$ ./eudico wallet import --lotus-json ~/.ipc-agent/wallet.key -``` - -```bash -# Docker: Copy the wallet key into the container and import into eudico -docker cp : && docker exec -it eudico wallet import --format json-lotus -``` -```console -# Example execution -$ docker cp ~/.ipc-agent/wallet.key ipc_root_t01002_1250:/input.key && docker exec -it ipc_root_t01002_1250 eudico wallet import --format json-lotus input.key -``` - -## Running a simple subnet with a single validator - -This section provides instructions for spawning a simple subnet with a single validator. If you'd like to spawn a subnet with multiple validators in a Docker setup, read and understand this section first but then follow the steps under [the multi-validator section below](#running-a-subnet-with-several-validators). - -### Spawning a subnet actor - -To run a subnet the first thing is to configure and create the subnet actor that will govern the subnet's operation. - -```bash -./bin/ipc-agent subnet create --parent --name --min-validator-stake --min-validators --bottomup-check-period --topdown-check-period -``` -```console -# Example execution -$ ./bin/ipc-agent subnet create --parent /root --name test --min-validator-stake 1 --min-validators 0 --bottomup-check-period 30 --topdown-check-period 30 -[2023-03-21T09:32:58Z INFO ipc_agent::cli::commands::manager::create] created subnet actor with id: /root/t01002 -``` -This command deploys a subnet actor for a new subnet from the `root`, with a human-readable name `test`, that requires at least `1` validator to join the subnet to be able to mine new blocks, and with a checkpointing period (both bottom-up and top-down) of `30` blocks. We can see that the output of this command is the ID of the new subnet. - -### Exporting your wallet - -We will need to export the wallet key from our root node so that we can import them to our validators. Depending on how you are running your rootnet node you'll have to make a call to the docker container, or your nodes API. More information about exporting keys from your node can be found under [this section](#Exporting-wallet-keys). Make sure that the wallet holds enough funds to meet the subnet collateral requirements. - -### Deploying a subnet node - -Before joining a new subnet, our node for that subnet must be initialised. For the deployment of subnet daemons we also provide a convenient infra script: -```bash -./bin/ipc-infra/run-subnet-docker.sh -``` -```console -# Example execution -$ ./bin/ipc-infra/run-subnet-docker.sh 1250 1350 /root/t01002 ~/.ipc-agent/wallet.key -(...) ->>> Subnet /root/t01002 daemon running in container: 22312347b743f1e95e50a31c1f47736580c9a84819f41cb4ed3d80161a0d750f (friendly name: ipc_root_t01002_1239) ->>> Token to /root/t01002 daemon: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJBbGxvdyI6WyJyZWFkIiwid3JpdGUiLCJzaWduIiwiYWRtaW4iXX0.TnoDqZJ1fqdkr_oCHFEXvdwU6kYR7Va_ALyEuoPnksA ->>> Default wallet: t1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq ->>> Subnet validator info: -/dns/host.docker.internal/tcp/1349/p2p/12D3KooWN5hbWkCxwvrX9xYxMwFbWm2Jpa1o4qhwifmSw3Fb ->>> API listening in host port 1250 ->>> Validator listening in host port 1350 -``` -> 💡 Beware: This script doesn't support the use of relative paths for the wallet path. - -The end of the log of the execution of this script provides a bit more of information than the previous one as it is implemented to be used for production deployments: API and auth tokens for the daemon, default validator wallet used, the multiaddress where the validator is listening, etc. To configure our IPC agent with this subnet daemon, we need to once again update our IPC agent with the relevant information. In this case, for the Example execution above we need to add the following section to the end of our config file. - -*Example*: -```toml -[[subnets]] -id = "/root/t01002" -gateway_addr = "t064" -network_name = "test" -jsonrpc_api_http = "http://127.0.0.1:1250/rpc/v1" -auth_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJBbGxvdyI6WyJyZWFkIiwid3JpdGUiLCJzaWduIiwiYWRtaW4iXX0.TnoDqZJ1fqdkr_oCHFEXvdwU6kYR7Va_ALyEuoPnksA" -accounts = ["t1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq"] -``` -> 💡 Remember to run `./bin/ipc-agent config reload` for changes in the config of the agent to be picked up by the daemon. - -### Joining a subnet - -With the daemon for the subnet deployed, we can join the subnet: -```bash -./bin/ipc-agent subnet join --subnet --collateral --validator-net-addr -``` -```console -# Example execution -$ ./bin/ipc-agent subnet join --subnet /root/t01002 --collateral 2 --validator-net-addr /dns/host.docker.internal/tcp/1349/p2p/12D3KooWN5hbWkCxwvrX9xYxMwFbWm2Jpa1o4qhwifmSw3Fb -``` -This command specifies the subnet to join, the amount of collateral to provide and the validator net address used by other validators to dial them. We can pick up this information from the execution of the script above or running `eudico mir validator config validator-addr` from your deployment. Bear in mind that the multiaddress provided for the validator needs to be accessible publicly by other validators. - -### Mining in a subnet - -With our subnet daemon deployed, and having joined the network, as the minimum number of validators we set for our subnet is 0, we can start mining and creating new blocks in the subnet. Doing so is a simple as running the following script using as an argument the container of our subnet node: -```bash -./bin/ipc-infra/mine-subnet.sh -``` -```console -# Example execution -$ ./bin/ipc-infra/mine-subnet.sh 84711d67cf162e30747c4525d69728c4dea8c6b4b35cd89f6d0947fee14bf908 -``` - -The mining process is currently run in the foreground in interactive mode. Consider using screen or tmux so as to not block your terminal. - -## Running a subnet with several validators - -In this section, we will deploy a subnet where the IPC agent is responsible for handling more than one validator in the subnet. We are going to deploy a subnet with 3 validators. The first thing we'll need to do is create a new wallet for every validator we want to run. We can do this directly through the agent with the following command (3x): -```bash -./bin/ipc-agent wallet new --key-type secp256k1 --subnet /root -``` - -We also need to provide with some funds our wallets so they can put collateral to join the subnet. According to the rootnet you are connected to, you may need to get some funds from the faucet, or send some from your main wallet. Funds can be sent from your main wallet also through the agent with (3x, adjusting `target-wallet` for each): -```bash -./bin/ipc-agent subnet send-value --subnet /root --to -``` - -With this, we can already create the subnet with `/root` as its parent. We are going to set the `--min-validators 2` so no new blocks can be created without this number of validators in the subnet. -```bash -./bin/ipc-agent subnet create --parent /root --name test --min-validator-stake 1 --min-validators 2 --bottomup-check-period 30 --topdown-check-period 30 -``` - -### Deploying the infrastructure - -In order to deploy the 3 validators for the subnet, we will have to first export the keys from our root node so we can import them to our validators. Depending on how you are running your rootnet node you'll have to make a call to the docker container, or your nodes API. More information about exporting keys from your node can be found under [this section](#Exporting-wallet-keys). - -With the keys conveniently exported, we can deploy the subnet nodes using the `infra-scripts`. Note that each node should be importing a different wallet key for their validator, and should be exposing different ports for their API and validators. - -```bash -./bin/ipc-infra/run-subnet-docker.sh -``` -```console -# Example execution -$ ./bin/ipc-infra/run-subnet-docker.sh 1251 1351 /root/t01002 ~/.ipc-agent/wallet1.key -$ ./bin/ipc-infra/run-subnet-docker.sh 1252 1352 /root/t01002 ~/.ipc-agent/wallet2.key -$ ./bin/ipc-infra/run-subnet-docker.sh 1253 1353 /root/t01002 ~/.ipc-agent/wallet3.key -``` -If the deployment is successful, each of these nodes should return the following output at the end of their logs. Note down this information somewhere as we will need it to conveniently join our validators to the subnet. - -*Example*: -```console ->>> Subnet /root/t01002 daemon running in container: 91d2af80534665a8d9a20127e480c16136d352a79563e74ee3c5497d50b9eda8 (friendly name: ipc_root_t01002_1240) ->>> Token to /root/t01002 daemon: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJBbGxvdyI6WyJyZWFkIiwid3JpdGUiLCJzaWduIiwiYWRtaW4iXX0.JTiumQwFIutkTb0gUC5JWTATs-lUvDaopEDE0ewgzLk ->>> Default wallet: t1ivy6mo2ofxw4fdmft22nel66w63fb7cuyslm4cy ->>> Subnet subnet validator info: -/dns/host.docker.internal/tcp/1359/p2p/12D3KooWEJXcSPw6Yv4jDk52xvp2rdeG3J6jCPX9AgBJE2mRCVoR ->>> API listening in host port 1251 ->>> Validator listening in host port 1351 -``` - -### Configuring the agent -To configure the agent for its use with all the validators, we need to connect to the RPC API of one of the validators, and import all of the wallets of the validators in that node, so the agent is able through the same API to act on behalf of any validator. More information about importing keys can be found in [this section](#Importing-wallet-keys). - -Here's an example of the configuration connecting to the RPC of the first validator, and configuring all the wallets for the validators in the subnet. -```toml -[[subnets]] -id = "/root/t01002" -gateway_addr = "t064" -network_name = "test" -jsonrpc_api_http = "http://127.0.0.1:1240/rpc/v1" -auth_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJBbGxvdyI6WyJyZWFkIiwid3JpdGUiLCJzaWduIiwiYWRtaW4iXX0.JTiumQwFIutkTb0gUC5JWTATs-lUvDaopEDE0ewgzLk" -accounts = ["t1ivy6mo2ofxw4fdmft22nel66w63fb7cuyslm4cy", "t1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq", "t1nv5jrdxk4ljzndaecfjgmu35k6iz54pkufktvua"] -``` -Remember to run `./bin/ipc-agent config reload` for your agent to pick up the latest changes for the config. - -### Joining the subnet -All the infrastructure for the subnet is now deployed, and we can join our validators to the subnet. For this, we need to send a `join` command from each of our validators from their validator wallet addresses providing the validators multiaddress. We can get the validator multiaddress from the output of the script we ran to deploy the infrastructure. - -This is the command that needs to be executed for every validator to join the subnet: -```bash -./bin/ipc-agent subnet join --from --subnet /root/t01002 --collateral --validator-net-addr -``` -```console -# Example execution -$ ./bin/ipc-agent subnet join --from t1ivy6mo2ofxw4fdmft22nel66w63fb7cuyslm4cy --subnet /root/t01002 --collateral 2 --validator-net-addr /dns/host.docker.internal/tcp/1359/p2p/12D3KooWEJXcSPw6Yv4jDk52xvp2rdeG3J6jCPX9AgBJE2mRCVoR -``` -Remember doing the above step for the 3 validators. - -### Mining in a subnet -We have everything in place now to start mining. Mining is as simple as running the following script for each of the validators, passing the container id/name: -```bash -./bin/ipc-infra/mine-subnet.sh -``` - -The mining process is currently run in the foreground in interactive mode. Consider using screen or tmux so as to not block your terminal. - diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md deleted file mode 100644 index b972007d7..000000000 --- a/docs/troubleshooting.md +++ /dev/null @@ -1,56 +0,0 @@ -# Troubleshooting IPC - ->💡 For background and setup information, make sure to start with the [README](/README.md). - -## I need to upgrade my IPC agent - -Sometimes, things break, and we'll need to push a quick path to fix some bug. If this happens, and you need to upgrade your agent version, kill you agent daemon if you have any running, pull the latest changes from this repo, build the binary, and start your daemon again. This should pick up the latest version for the agent. In the future, we will provide a better way to upgrade your agent. -```bash -# Pull latest changes -git pull -# Build the agent -make build -# Restart the daemon -./bin/ipc-agent daemon -``` - -## The eudico image is not building successful - -`make install-infra` may fail and not build the `eudico` image if your system is not configured correctly. If this happens, you can always try to build the image yourself to have a finer-grain report of the issues to help you debug them. For this you can [follow these instructions](https://github.com/consensus-shipyard/lotus/blob/spacenet/scripts/ipc/README.md). - -High-level you just need to clone the [eudico repo](https://github.com/consensus-shipyard/lotus), and run `docker build -t eudico .` in the root of the repo. - -## My subnet node doesn't start - -Either because the dockerized subnet node after running `./bin/ipc-infra/run-subnet-docker.sh` gets stuck waiting for the API to be started with the following message: -``` -Not online yet... (could not get API info for FullNode: could not get api endpoint: API not running (no endpoint)) -``` -Or because when the script finishes no validator address has been reported as expected by the logs, the best way to debug this situation is to attach to the docker container: -```bash -docker exec -it bash -``` - And check the logs with the following command, inside the container -```bash -tmux a -``` -Generally, the issue is that: -- You haven't passed the validator key correctly and it couldn't be imported. -- There was some network instability, and lotus params couldn't be downloaded successfully. - -## My agent is not submitting checkpoints after an error - -Try running `./bin/ipc-agent config reload`, this should pick up the latest config and restart all checkpointing processes. If the error has been fixed or it was an network instability between the agent and your subnet daemon, checkpoints should start being committed again seamlessly. - -### I set the wrong validator address or need to change it - -It may be the case that while joining the subnet, you didn't set the multiaddress for your validator correctly and you need to update it. You'll realize that the network address of your validator is not configured correctly, because your agent throws an error when trying to connect to your subnet node, or starting the validator in your subnet throws a network-related error. - -Changing the validator is as simple as running the following command: -```bash -./bin/ipc-agent subnet set-validator-net-addr --subnet --validator-net-addr -``` -```console -# Example execution -$ ./bin/ipc-agent subnet set-validator-net-addr --subnet /root/t01002 --validator-net-addr "/dns/host.docker.internal/tcp/1349/p2p/12D3KooWDeN3bTvZEH11s9Gq5bDeZZLKgRZiMDcy2KmA6mUaT9KE" -``` diff --git a/docs/usage.md b/docs/usage.md deleted file mode 100644 index fa7cb38a3..000000000 --- a/docs/usage.md +++ /dev/null @@ -1,127 +0,0 @@ -# Using the IPC Agent - ->💡 For background and setup information, make sure to start with the [README](/README.md). - -## Listing active subnets - -As a sanity-check that we have joined the subnet successfully and that we provided enough collateral to register the subnet to IPC, we can list the child subnets of our parent with the following command: - -```bash -./bin/ipc-agent list-subnets --gateway-address --subnet -``` -```console -# Example execution -$ ./bin/ipc-agent list-subnets --gateway-address=t064 --subnet=/root -[2023-03-30T17:00:25Z INFO ipc_agent::cli::commands::manager::list_subnets] /root/t01003 - status: 0, collateral: 2 FIL, circ.supply: 0.0 FIL -``` - -This command only shows subnets that have been registered to the gateway, i.e. that have provided enough collateral to participate in the IPC protocol and haven't been killed. It is not an exhaustive list of all of the subnet actors deployed over the network. - -## Joining a subnet - -With the daemon for a subnet deployed (see [instructions](/docs/subnet.md)), one can join the subnet: -```bash -./bin/ipc-agent subnet join --subnet --collateral --validator-net-addr -``` -```console -# Example execution -$ ./bin/ipc-agent subnet join --subnet /root/t01002 --collateral 2 --validator-net-addr /dns/host.docker.internal/tcp/1349/p2p/12D3KooWN5hbWkCxwvrX9xYxMwFbWm2Jpa1o4qhwifmSw3Fb -``` -This command specifies the subnet to join, the amount of collateral to provide and the validator net address used by other validators to dial them. - -## Listing your balance in a subnet -In order to send messages in a subnet, you'll need to have funds in your subnt account. You can use the following command to list the balance of your wallets in a subnet: -```bash -./bin/ipc-agent wallet list --subnet -``` -```console -# Example execution -$ ./bin/ipc-agent wallet list --subnet=/root/t01002 -ipc_agent::cli::commands::wallet::list] wallets in subnet /root are {"t1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq": "500.0"} -``` - -## Sending funds in a subnet - -The agent provides a command to conveniently exchange funds between addresses of the same subnet. This can be achieved through the following command: -```bash -./bin/ipc-agent subnet send-value --subnet [--from ] --to -``` -```console -# Example execution -$ ./bin/ipc-agent subnet send-value --subnet /root/t01002 --to t1xbevqterae2tanmh2kaqksnoacflrv6w2dflq4i 10 -``` - -## Sending funds between subnets - -At the moment, the IPC agent only expose commands to perform the basic IPC interoperability primitives for cross-net communication, which is the exchange of FIL (the native token for IPC) between the same address of a subnet. Mainly: -- `fund`, which sends FIL from one public key address, to the same public key address in the child. -- `release` that moves FIL from one account in a child subnet to its counter-part in the parent. - -Complex behavior can be implemented using these primitives: sending value to a user in another subnet can be implemented a set of `release/fund` and `sendValue` operations. Calling smart contract from one subnet to another works by providing funds to one account in the destination subnet, and then calling the contract. The agent doesn't currently include abstractions for this complex operations, but it will in the future. That being said, users can still leverage the agent's API to easily compose the basic primitives into complex functionality. - ->💡 All cross-net operations need to pay an additional cross-msg fee (apart from the gas cost of the message). This is reason why even if you sent `X FIL` you may see `X - fee FIL` arriving to you account at destination. This fee is used to reward subnet validators for their work committing the checkpoint that carries the message. - -### Fund -Funding a subnet can be performed by using the following command: -```bash -./bin/ipc-agent cross-msg fund --subnet [--from ] -``` -```console -# Example execution -$ ./bin/ipc-agent cross-msg fund --subnet /root/t01002 100 -``` -This command includes the cross-net message into the next top-down checkpoint after the current epoch. Once the top-down checkpoint is committed, you should see the funds in your account of the child subnet. - ->💡 Top-down checkpoints are not used to anchor the security of the parent into the child (as is the case for bottom-up checkpoints). They just include information of the top-down messages that need to be executed in the child subnet, and are a way for validators in the subnet to reach consensus on the finality on their parent. - -### Release -In order to release funds from a subnet, your account must hold enough funds inside it. Releasing funds to the parent subnet can be permformed with the following comand: -```bash -./bin/ipc-agent cross-msg release --subnet [--from ] -``` -```console -# Example execution -$ ./bin/ipc-agent cross-msg release --subnet=/root/t01002 100 -``` -This command includes the cross-net message into a bottom-up checkpoint after the current epoch. Once the bottom-up checkpoint is committed, you should see the funds in your account in the parent. - - -## Listing checkpoints from a subnet - -Subnets are periodically committing checkpoints to their parent every `bottomup-check-period` (parameter defined when creating the subnet). If you want to inspect the information of a range of bottom-up checkpoints committed in the parent for a subnet, you can use the `checkpoint list-bottomup` command provided by the agent as follows: -```bash -./bin/ipc-agent checkpoint list-bottomup --from-epoch --to-epoch --subnet -``` -```console -# Example execution -$ ./bin/ipc-agent checkpoint list-bottomup --from-epoch 0 --to-epoch 100 --subnet /root/t01002 -[2023-03-29T12:43:42Z INFO ipc_agent::cli::commands::manager::list_checkpoints] epoch 0 - prev_check={"/":"bafy2bzacedkoa623kvi5gfis2yks7xxjl73vg7xwbojz4tpq63dd5jpfz757i"}, cross_msgs=null, child_checks=null -[2023-03-29T12:43:42Z INFO ipc_agent::cli::commands::manager::list_checkpoints] epoch 10 - prev_check={"/":"bafy2bzacecsatvda6lodrorh7y7foxjt3a2dexxx5jiyvtl7gimrrvywb7l5m"}, cross_msgs=null, child_checks=null -[2023-03-29T12:43:42Z INFO ipc_agent::cli::commands::manager::list_checkpoints] epoch 30 - prev_check={"/":"bafy2bzaceauzdx22hna4e4cqf55jqmd64a4fx72sxprzj72qhrwuxhdl7zexu"}, cross_msgs=null, child_checks=null -``` -You can find the checkpoint where your cross-message was included by listing the checkpoints around the epoch where your message was sent. - -## Checking the health of top-down checkpoints -In order to check the health of top-down checkpointing in a subnet, the following command can be run: -```bash -./bin/ipc-agent checkpoint last-topdown --subnet -``` -```console -# Example execution -$ ./bin/ipc-agent checkpoint last-topdown --subnet /root/t01002 -[2023-04-18T17:11:34Z INFO ipc_agent::cli::commands::checkpoint::topdown_executed] Last top-down checkpoint executed in epoch: 9866 -``` - -This command returns the epoch of the last top-down checkpoint executed in the child. If you see that this epoch is way below the current epoch of the parent subnet, then top-down checkpointing may be lagging, validators need to catch-up, and the forwarding of top-down messages (from parent to child) may take longer to be committed. - -## Leaving a subnet - -To leave a subnet, the following agent command can be used: -```bash -./bin/ipc-agent subnet leave --subnet -``` -```console -# Example execution -$ ./bin/ipc-agent subnet leave --subnet /root/t01002 -``` -Leaving a subnet will release the collateral for the validator and remove all the validation rights from its account. This means that if you have a validator running in that subnet, its validation process will immediately terminate. diff --git a/ipld/resolver/Cargo.toml b/ipld/resolver/Cargo.toml index 070a16dc0..39590b94c 100644 --- a/ipld/resolver/Cargo.toml +++ b/ipld/resolver/Cargo.toml @@ -33,6 +33,7 @@ libp2p = { version = "0.50", default-features = false, features = [ "plaintext", ] } libipld = { workspace = true } +libp2p-bitswap = "0.25.1" log = { workspace = true } prometheus = { workspace = true } quickcheck = { workspace = true, optional = true } @@ -46,9 +47,6 @@ fvm_ipld_encoding = { workspace = true } fvm_shared = { workspace = true, optional = true } fvm_ipld_blockstore = { workspace = true, optional = true } -# Using a fork of libp2p-bitswap so that we can do rate limiting. -#libp2p-bitswap = "0.25" -libp2p-bitswap = { git = "https://github.com/consensus-shipyard/libp2p-bitswap", branch = "req-res-pub" } [dev-dependencies] quickcheck = { workspace = true } diff --git a/scripts/install_infra.sh b/scripts/install_infra.sh deleted file mode 100755 index d5a637d85..000000000 --- a/scripts/install_infra.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -# -# Builds docker image and install the ipc-scripts required to conveniently -# deploy the infrastructure for IPC subnets. - -set -e - -rm -rf ./lotus -git clone --branch dev https://github.com/consensus-shipyard/lotus.git -cd ./lotus - -uname=$(uname); -case "$uname" in - (*Darwin*) docker build -t eudico --build-arg FFI_BUILD_FROM_SOURCE=1 . ;; - (*) docker build -t eudico . ;; -esac; - -cd .. -mkdir -p ./bin/ipc-infra -cp -rf ./lotus/scripts/ipc/* ./bin/ipc-infra -rm -rf ./lotus From 8237d1dd8a9954aa6c13dec887bb47f294de3da2 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Fri, 2 Jun 2023 14:07:45 +0100 Subject: [PATCH 76/82] CHORE: No more workspace --- Cargo.toml | 61 ++++++++++++++----- README.md | 2 +- docs/{architecture.md => README.md} | 8 +-- ipld/resolver/Cargo.toml | 61 ------------------- {ipld/resolver/src => src}/arb.rs | 0 .../resolver/src => src}/behaviour/content.rs | 0 .../src => src}/behaviour/discovery.rs | 0 .../src => src}/behaviour/membership.rs | 0 {ipld/resolver/src => src}/behaviour/mod.rs | 0 {ipld/resolver/src => src}/client.rs | 0 {ipld/resolver/src => src}/hash.rs | 0 {ipld/resolver/src => src}/lib.rs | 0 {ipld/resolver/src => src}/limiter.rs | 0 {ipld/resolver/src => src}/missing_blocks.rs | 0 {ipld/resolver/src => src}/provider_cache.rs | 0 {ipld/resolver/src => src}/provider_record.rs | 0 {ipld/resolver/src => src}/service.rs | 0 {ipld/resolver/src => src}/signed_record.rs | 0 {ipld/resolver/src => src}/stats.rs | 0 {ipld/resolver/src => src}/timestamp.rs | 0 {ipld/resolver/src => src}/vote_record.rs | 0 {ipld/resolver/tests => tests}/smoke.rs | 0 {ipld/resolver/tests => tests}/store/mod.rs | 0 23 files changed, 51 insertions(+), 81 deletions(-) rename docs/{architecture.md => README.md} (89%) delete mode 100644 ipld/resolver/Cargo.toml rename {ipld/resolver/src => src}/arb.rs (100%) rename {ipld/resolver/src => src}/behaviour/content.rs (100%) rename {ipld/resolver/src => src}/behaviour/discovery.rs (100%) rename {ipld/resolver/src => src}/behaviour/membership.rs (100%) rename {ipld/resolver/src => src}/behaviour/mod.rs (100%) rename {ipld/resolver/src => src}/client.rs (100%) rename {ipld/resolver/src => src}/hash.rs (100%) rename {ipld/resolver/src => src}/lib.rs (100%) rename {ipld/resolver/src => src}/limiter.rs (100%) rename {ipld/resolver/src => src}/missing_blocks.rs (100%) rename {ipld/resolver/src => src}/provider_cache.rs (100%) rename {ipld/resolver/src => src}/provider_record.rs (100%) rename {ipld/resolver/src => src}/service.rs (100%) rename {ipld/resolver/src => src}/signed_record.rs (100%) rename {ipld/resolver/src => src}/stats.rs (100%) rename {ipld/resolver/src => src}/timestamp.rs (100%) rename {ipld/resolver/src => src}/vote_record.rs (100%) rename {ipld/resolver/tests => tests}/smoke.rs (100%) rename {ipld/resolver/tests => tests}/store/mod.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 785ed2647..c1fd2cac2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,29 +1,60 @@ -[workspace] -members = ["ipld/resolver"] - -[workspace.package] +[package] +name = "ipc_ipld_resolver" +version = "0.1.0" +description = "P2P library to resolve IPLD content across IPC subnets." authors = ["Protocol Labs"] edition = "2021" license-file = "LICENSE" -[workspace.dependencies] +[dependencies] anyhow = "1.0" base64 = "0.21.0" +blake2b_simd = "1.0" +bloom = "0.3" +gcra = "0.3" lazy_static = "1.4" +libipld = { version = "0.14", default-features = false, features = ["dag-cbor"] } +libp2p = { version = "0.50", default-features = false, features = [ + "gossipsub", + "kad", + "identify", + "ping", + "noise", + "yamux", + "tcp", + "dns", + "mplex", + "request-response", + "metrics", + "tokio", + "macros", + "serde", + "secp256k1", + "plaintext", +] } +libp2p-bitswap = "0.25.1" log = "0.4" -env_logger = "0.10" prometheus = "0.13" -serde = { version = "1.0", features = ["derive"] } -tokio = { version = "1.16", features = ["full"] } -thiserror = "1.0.38" -quickcheck = "1" -quickcheck_macros = "1" -blake2b_simd = "1.0" +quickcheck = { version = "1", optional = true } rand = "0.8" +serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0.91", features = ["raw_value"] } +thiserror = "1.0.38" +tokio = { version = "1.16", features = ["full"] } -fvm_ipld_blockstore = "0.1" fvm_ipld_encoding = "0.3" -fvm_shared = { version = "=3.0.0-alpha.17", default-features = false, features = ["crypto"] } +fvm_shared = { version = "=3.0.0-alpha.17", default-features = false, features = ["crypto"], optional = true } +fvm_ipld_blockstore = { version = "0.1", optional = true } ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git", tag = "v0.2.0" } -libipld = { version = "0.14", default-features = false, features = ["dag-cbor"] } + +[dev-dependencies] +quickcheck_macros = "1" +env_logger = "0.10" +fvm_ipld_hamt = "0.6" + +ipc_ipld_resolver = { path = ".", features = ["arb"] } + +[features] +default = ["arb", "missing_blocks"] +arb = ["quickcheck", "fvm_shared/arb"] +missing_blocks = ["fvm_ipld_blockstore"] diff --git a/README.md b/README.md index 0b78263f5..b9325eaaa 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ See the [docs](./docs/) for a conceptual overview. ## Usage -Please have a look at the [smoke test](./ipld/resolver/tests/smoke.rs) for an example of using the library. +Please have a look at the [smoke test](./tests/smoke.rs) for an example of using the library. The following snippet demonstrates how one would create a resolver instance and use it: diff --git a/docs/architecture.md b/docs/README.md similarity index 89% rename from docs/architecture.md rename to docs/README.md index 9892a7da7..19ff49c65 100644 --- a/docs/architecture.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # IPLD Resolver -The [IPLD Resolver](/ipld/resolver) is a library that [IPC Agents](https://github.com/consensus-shipyard/ipc-agent/) can use to exchange data between subnets in IPLD format. +The IPLD Resolver is a library that [IPC Agents](https://github.com/consensus-shipyard/ipc-agent/) can use to exchange data between subnets in IPLD format. ## Checkpointing @@ -48,7 +48,7 @@ See the libp2p [specs](https://github.com/libp2p/specs) and [docs](https://docs. The Resolver is completely agnostic over what content it can resolve, as long as it's based on CIDs; it's not aware of the checkpointing use case above. -The interface with the host system is through a host-provided implementation of the [BitswapStore](https://github.com/ipfs-rust/libp2p-bitswap/blob/7dd9cececda3e4a8f6e14c200a4b457159d8db33/src/behaviour.rs#L55) which the library uses to retrieve and store content. Implementors can make use of the [missing_blocks](https://github.com/consensus-shipyard/ipc-agent/blob/main/ipld/resolver/src/missing_blocks.rs) helper method which recursively collects all CIDs from an IPLD `Blockstore`, starting from the root CID we are looking for. +The interface with the host system is through a host-provided implementation of the [BitswapStore](https://github.com/ipfs-rust/libp2p-bitswap/blob/7dd9cececda3e4a8f6e14c200a4b457159d8db33/src/behaviour.rs#L55) which the library uses to retrieve and store content. Implementors can make use of the [missing_blocks](../src/missing_blocks.rs) helper method which recursively collects all CIDs from an IPLD `Blockstore`, starting from the root CID we are looking for. Internally the protocols are wrapped into behaviours that interpret their events and manage their associated state: * `Discovery` wraps `Kademlia` @@ -59,7 +59,7 @@ The following diagram shows a typical sequence of events within the IPLD Resolve ![IPLD Resolver](diagrams/ipld_resolver.png) -# Automation +# Diagram Automation The diagrams in this directory can be rendered with `make diagrams`. @@ -74,7 +74,7 @@ set -eo pipefail # Redirect output to stderr. exec 1>&2 -if git diff --cached --name-only | grep .puml +if git diff --cached --name-only --diff-filter=d | grep .puml then make diagrams git add docs/diagrams/*.png diff --git a/ipld/resolver/Cargo.toml b/ipld/resolver/Cargo.toml deleted file mode 100644 index 39590b94c..000000000 --- a/ipld/resolver/Cargo.toml +++ /dev/null @@ -1,61 +0,0 @@ -[package] -name = "ipc_ipld_resolver" -description = "P2P library to resolve IPLD content across IPC subnets." -version = "0.1.0" -authors.workspace = true -edition.workspace = true -license-file.workspace = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -anyhow = { workspace = true } -blake2b_simd = { workspace = true } -bloom = "0.3" -gcra = "0.3" -lazy_static = { workspace = true } -libp2p = { version = "0.50", default-features = false, features = [ - "gossipsub", - "kad", - "identify", - "ping", - "noise", - "yamux", - "tcp", - "dns", - "mplex", - "request-response", - "metrics", - "tokio", - "macros", - "serde", - "secp256k1", - "plaintext", -] } -libipld = { workspace = true } -libp2p-bitswap = "0.25.1" -log = { workspace = true } -prometheus = { workspace = true } -quickcheck = { workspace = true, optional = true } -rand = { workspace = true } -serde = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true } - -ipc-sdk = { workspace = true } -fvm_ipld_encoding = { workspace = true } -fvm_shared = { workspace = true, optional = true } -fvm_ipld_blockstore = { workspace = true, optional = true } - - -[dev-dependencies] -quickcheck = { workspace = true } -quickcheck_macros = { workspace = true } -env_logger = { workspace = true } -fvm_shared = { workspace = true, features = ["arb"] } -fvm_ipld_hamt = "0.6" - -[features] -default = ["arb", "missing_blocks"] -arb = ["quickcheck", "fvm_shared/arb"] -missing_blocks = ["fvm_ipld_blockstore"] diff --git a/ipld/resolver/src/arb.rs b/src/arb.rs similarity index 100% rename from ipld/resolver/src/arb.rs rename to src/arb.rs diff --git a/ipld/resolver/src/behaviour/content.rs b/src/behaviour/content.rs similarity index 100% rename from ipld/resolver/src/behaviour/content.rs rename to src/behaviour/content.rs diff --git a/ipld/resolver/src/behaviour/discovery.rs b/src/behaviour/discovery.rs similarity index 100% rename from ipld/resolver/src/behaviour/discovery.rs rename to src/behaviour/discovery.rs diff --git a/ipld/resolver/src/behaviour/membership.rs b/src/behaviour/membership.rs similarity index 100% rename from ipld/resolver/src/behaviour/membership.rs rename to src/behaviour/membership.rs diff --git a/ipld/resolver/src/behaviour/mod.rs b/src/behaviour/mod.rs similarity index 100% rename from ipld/resolver/src/behaviour/mod.rs rename to src/behaviour/mod.rs diff --git a/ipld/resolver/src/client.rs b/src/client.rs similarity index 100% rename from ipld/resolver/src/client.rs rename to src/client.rs diff --git a/ipld/resolver/src/hash.rs b/src/hash.rs similarity index 100% rename from ipld/resolver/src/hash.rs rename to src/hash.rs diff --git a/ipld/resolver/src/lib.rs b/src/lib.rs similarity index 100% rename from ipld/resolver/src/lib.rs rename to src/lib.rs diff --git a/ipld/resolver/src/limiter.rs b/src/limiter.rs similarity index 100% rename from ipld/resolver/src/limiter.rs rename to src/limiter.rs diff --git a/ipld/resolver/src/missing_blocks.rs b/src/missing_blocks.rs similarity index 100% rename from ipld/resolver/src/missing_blocks.rs rename to src/missing_blocks.rs diff --git a/ipld/resolver/src/provider_cache.rs b/src/provider_cache.rs similarity index 100% rename from ipld/resolver/src/provider_cache.rs rename to src/provider_cache.rs diff --git a/ipld/resolver/src/provider_record.rs b/src/provider_record.rs similarity index 100% rename from ipld/resolver/src/provider_record.rs rename to src/provider_record.rs diff --git a/ipld/resolver/src/service.rs b/src/service.rs similarity index 100% rename from ipld/resolver/src/service.rs rename to src/service.rs diff --git a/ipld/resolver/src/signed_record.rs b/src/signed_record.rs similarity index 100% rename from ipld/resolver/src/signed_record.rs rename to src/signed_record.rs diff --git a/ipld/resolver/src/stats.rs b/src/stats.rs similarity index 100% rename from ipld/resolver/src/stats.rs rename to src/stats.rs diff --git a/ipld/resolver/src/timestamp.rs b/src/timestamp.rs similarity index 100% rename from ipld/resolver/src/timestamp.rs rename to src/timestamp.rs diff --git a/ipld/resolver/src/vote_record.rs b/src/vote_record.rs similarity index 100% rename from ipld/resolver/src/vote_record.rs rename to src/vote_record.rs diff --git a/ipld/resolver/tests/smoke.rs b/tests/smoke.rs similarity index 100% rename from ipld/resolver/tests/smoke.rs rename to tests/smoke.rs diff --git a/ipld/resolver/tests/store/mod.rs b/tests/store/mod.rs similarity index 100% rename from ipld/resolver/tests/store/mod.rs rename to tests/store/mod.rs From 9662e5ab27dbc844ccd80db1b26f10ed1333084a Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Thu, 17 Aug 2023 13:50:36 +0100 Subject: [PATCH 77/82] CHORE: Update ipc-sdk dependency --- Cargo.toml | 7 +++++-- src/arb.rs | 23 ++++++++++++++++------- tests/smoke.rs | 5 +++-- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c1fd2cac2..4a5c3a13f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,9 +43,12 @@ thiserror = "1.0.38" tokio = { version = "1.16", features = ["full"] } fvm_ipld_encoding = "0.3" -fvm_shared = { version = "=3.0.0-alpha.17", default-features = false, features = ["crypto"], optional = true } +fvm_shared = { version = "~3.2", default-features = false, features = ["crypto"], optional = true } fvm_ipld_blockstore = { version = "0.1", optional = true } -ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git", tag = "v0.2.0" } + +# Using the IPC SDK without the `fil-actor` feature so as not to depend on the actor `Runtime`. +# Using the `main` branch instead of the highest available tag `v0.3.0` because the latter doesn't have a feature flag for the `Runtime`. +ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git", default-features = false, branch = "main" } [dev-dependencies] quickcheck_macros = "1" diff --git a/src/arb.rs b/src/arb.rs index e049e8cfb..ae1a1a7b8 100644 --- a/src/arb.rs +++ b/src/arb.rs @@ -1,7 +1,7 @@ // Copyright 2022-2023 Protocol Labs // SPDX-License-Identifier: MIT use fvm_shared::address::Address; -use ipc_sdk::subnet_id::{SubnetID, ROOTNET_ID}; +use ipc_sdk::subnet_id::SubnetID; use libipld::{Cid, Multihash}; use quickcheck::Arbitrary; @@ -26,12 +26,21 @@ pub struct ArbSubnetID(pub SubnetID); impl Arbitrary for ArbSubnetID { fn arbitrary(g: &mut quickcheck::Gen) -> Self { - let mut parent = ROOTNET_ID.clone(); - for _ in 0..=u8::arbitrary(g) % 5 { - let addr = ArbAddress::arbitrary(g).0; - parent = SubnetID::new_from_parent(&parent, addr); - } - Self(parent) + let child_count = usize::arbitrary(g) % 4; + + let children = (0..child_count) + .map(|_| { + if bool::arbitrary(g) { + Address::new_id(u64::arbitrary(g)) + } else { + // Only expectign EAM managed delegated addresses. + let subaddr: [u8; 20] = std::array::from_fn(|_| Arbitrary::arbitrary(g)); + Address::new_delegated(10, &subaddr).unwrap() + } + }) + .collect::>(); + + Self(SubnetID::new(u64::arbitrary(g), children)) } } diff --git a/tests/smoke.rs b/tests/smoke.rs index 69cff36c5..b38684397 100644 --- a/tests/smoke.rs +++ b/tests/smoke.rs @@ -30,7 +30,7 @@ use ipc_ipld_resolver::{ Client, Config, ConnectionConfig, ContentConfig, DiscoveryConfig, Event, MembershipConfig, NetworkConfig, Service, VoteRecord, }; -use ipc_sdk::subnet_id::{SubnetID, ROOTNET_ID}; +use ipc_sdk::subnet_id::SubnetID; use libipld::{ multihash::{Code, MultihashDigest}, Cid, @@ -364,7 +364,8 @@ fn build_transport(local_key: Keypair) -> Boxed<(PeerId, StreamMuxerBox)> { /// Make a subnet under a rootnet. fn make_subnet_id(actor_id: ActorID) -> SubnetID { let act = Address::new_id(actor_id); - SubnetID::new_from_parent(&ROOTNET_ID, act) + let root = SubnetID::new_root(0); + SubnetID::new_from_parent(&root, act) } /// Insert a HAMT into the block store of an agent. From 9ec1b693072b2087060c90fd07c1753e421c95d3 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Thu, 17 Aug 2023 13:50:51 +0100 Subject: [PATCH 78/82] CHORE: Update rust action dependency --- .github/workflows/ci.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 354e1c275..b15e12a82 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -62,10 +62,9 @@ jobs: uses: actions/checkout@v3 - name: Install Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - profile: minimal - target: wasm32-unknown-unknown + targets: wasm32-unknown-unknown toolchain: ${{ matrix.rust }} components: rustfmt,clippy From f52316517559206927a0d49cd60d4fd29e240272 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Thu, 17 Aug 2023 16:08:59 +0100 Subject: [PATCH 79/82] FIX: Use our stable fork of gcra --- Cargo.toml | 6 +++++- rust-toolchain.toml | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4a5c3a13f..a5fbb8977 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ anyhow = "1.0" base64 = "0.21.0" blake2b_simd = "1.0" bloom = "0.3" -gcra = "0.3" +gcra = "0.4" lazy_static = "1.4" libipld = { version = "0.14", default-features = false, features = ["dag-cbor"] } libp2p = { version = "0.50", default-features = false, features = [ @@ -61,3 +61,7 @@ ipc_ipld_resolver = { path = ".", features = ["arb"] } default = ["arb", "missing_blocks"] arb = ["quickcheck", "fvm_shared/arb"] missing_blocks = ["fvm_ipld_blockstore"] + +[patch.crates-io] +# Use stable-only features. +gcra = { git = "https://github.com/consensus-shipyard/gcra-rs.git", branch = "main" } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 4518cf19b..a59cf37c5 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "nightly-2022-10-03" -components = ["clippy", "llvm-tools-preview", "rustfmt"] +channel = "stable" +components = ["clippy", "llvm-tools", "rustfmt"] targets = ["wasm32-unknown-unknown"] From 36e11ec8b4f656623c3e940800fbca22a684aa59 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:08:05 +0800 Subject: [PATCH 80/82] Migrate ipc-sdk (#3) * migrate ipc-sdk * clippy --- Cargo.toml | 2 +- src/behaviour/content.rs | 2 +- src/provider_cache.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a5fbb8977..d94db0bc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ fvm_ipld_blockstore = { version = "0.1", optional = true } # Using the IPC SDK without the `fil-actor` feature so as not to depend on the actor `Runtime`. # Using the `main` branch instead of the highest available tag `v0.3.0` because the latter doesn't have a feature flag for the `Runtime`. -ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-actors.git", default-features = false, branch = "main" } +ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-agent.git", default-features = false, branch = "dev" } [dev-dependencies] quickcheck_macros = "1" diff --git a/src/behaviour/content.rs b/src/behaviour/content.rs index 1f25f9a8c..3a2e9caa1 100644 --- a/src/behaviour/content.rs +++ b/src/behaviour/content.rs @@ -317,7 +317,7 @@ fn select_non_ephemeral(mut addr: Multiaddr) -> Multiaddr { } } keep.reverse(); - Multiaddr::from_iter(keep.into_iter()) + Multiaddr::from_iter(keep) } #[cfg(test)] diff --git a/src/provider_cache.rs b/src/provider_cache.rs index 5eb978beb..1d36353a9 100644 --- a/src/provider_cache.rs +++ b/src/provider_cache.rs @@ -43,7 +43,7 @@ pub struct SubnetProviderCache { impl SubnetProviderCache { pub fn new(max_subnets: usize, static_subnets: Vec) -> Self { Self { - pinned_subnets: HashSet::from_iter(static_subnets.into_iter()), + pinned_subnets: HashSet::from_iter(static_subnets), max_subnets, routable_peers: Default::default(), subnet_providers: Default::default(), From c226744b9465b0d199668004c1bc0d8d8f640ee2 Mon Sep 17 00:00:00 2001 From: Alfonso de la Rocha Date: Wed, 18 Oct 2023 14:13:21 +0200 Subject: [PATCH 81/82] rename ipc-agent deps to ipc --- Cargo.toml | 2 +- docs/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d94db0bc5..3263ec61f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ fvm_ipld_blockstore = { version = "0.1", optional = true } # Using the IPC SDK without the `fil-actor` feature so as not to depend on the actor `Runtime`. # Using the `main` branch instead of the highest available tag `v0.3.0` because the latter doesn't have a feature flag for the `Runtime`. -ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc-agent.git", default-features = false, branch = "dev" } +ipc-sdk = { git = "https://github.com/consensus-shipyard/ipc.git", default-features = false, branch = "dev" } [dev-dependencies] quickcheck_macros = "1" diff --git a/docs/README.md b/docs/README.md index 19ff49c65..ad8c4f7de 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # IPLD Resolver -The IPLD Resolver is a library that [IPC Agents](https://github.com/consensus-shipyard/ipc-agent/) can use to exchange data between subnets in IPLD format. +The IPLD Resolver is a library that [IPC Agents](https://github.com/consensus-shipyard/ipc/) can use to exchange data between subnets in IPLD format. ## Checkpointing From 4888d016b639ad6519c17cd3d8ff3ea8cda94122 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Wed, 18 Oct 2023 15:24:51 +0100 Subject: [PATCH 82/82] FIX: Relax clippy, the suggestion does not compile. --- src/limiter.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/limiter.rs b/src/limiter.rs index 694d5c237..438aa10ee 100644 --- a/src/limiter.rs +++ b/src/limiter.rs @@ -39,6 +39,7 @@ where /// Same as [`RateLimiter::add`] but allows passing in the time, for testing. pub fn add_at(&mut self, limit: &RateLimit, key: K, cost: u32, at: Instant) -> bool { + #[allow(clippy::unwrap_or_default)] let state = self.cache.entry(key).or_insert_with(GcraState::default); state.check_and_modify_at(limit, at, cost).is_ok()