├── .github ├── CODEOWNERS ├── renovate.json5 └── workflows │ └── ci.yml ├── waku-relay ├── tests │ ├── gossipsub │ │ └── mod.rs │ ├── it_gossipsub.rs │ └── testlib │ │ ├── mod.rs │ │ ├── transport.rs │ │ └── swarm.rs ├── src │ ├── gossipsub │ │ ├── rpc │ │ │ ├── proto.rs │ │ │ ├── validation.rs │ │ │ ├── types.rs │ │ │ ├── fragmentation.rs │ │ │ └── traits.rs │ │ ├── signing.rs │ │ ├── rpc.rs │ │ ├── codec.rs │ │ ├── event.rs │ │ ├── heartbeat.rs │ │ ├── seq_no.rs │ │ ├── peer_score.rs │ │ ├── message_id.rs │ │ ├── transform.rs │ │ ├── connection_manager.rs │ │ ├── gossip_promises.rs │ │ ├── topic.rs │ │ ├── error.rs │ │ ├── protocol.rs │ │ ├── time_cache.rs │ │ ├── backoff.rs │ │ ├── subscription_filter.rs │ │ ├── types.rs │ │ └── signing │ │ │ └── signer.rs │ ├── proto.rs │ ├── lib.rs │ ├── error.rs │ ├── event.rs │ ├── behaviour.rs │ ├── message_id.rs │ └── gossipsub.rs ├── buf.gen.yaml ├── build.rs └── Cargo.toml ├── waku-core ├── src │ ├── common.rs │ ├── message │ │ ├── proto.rs │ │ ├── traits.rs │ │ └── message.rs │ ├── pubsub_topic.rs │ ├── lib.rs │ ├── message.rs │ ├── common │ │ ├── protobuf_codec.rs │ │ └── quick_protobuf_codec.rs │ ├── content_topic.rs │ └── pubsub_topic │ │ ├── topic.rs │ │ └── namespaced.rs ├── buf.gen.yaml ├── Cargo.toml └── build.rs ├── .gitattributes ├── apps ├── waku-cli │ ├── src │ │ ├── cmd.rs │ │ ├── cmd │ │ │ ├── relay.rs │ │ │ ├── main.rs │ │ │ └── relay │ │ │ │ ├── subscribe.rs │ │ │ │ └── publish.rs │ │ └── main.rs │ ├── scripts │ │ ├── subscribe.sh │ │ └── publish.sh │ └── Cargo.toml └── wakunode2 │ ├── Cargo.toml │ └── src │ ├── main.rs │ ├── config.rs │ └── app.rs ├── waku-node ├── src │ ├── behaviour.rs │ ├── config.rs │ ├── event_loop.rs │ ├── lib.rs │ ├── event_loop │ │ ├── event.rs │ │ ├── command.rs │ │ └── event_loop.rs │ ├── config │ │ ├── waku_relay_config.rs │ │ └── config.rs │ ├── behaviour │ │ ├── event.rs │ │ └── behaviour.rs │ ├── transport.rs │ └── node.rs ├── Cargo.toml ├── examples │ └── simple.rs └── tests │ └── it_waku_relay.rs ├── waku-enr ├── README.md ├── src │ ├── lib.rs │ ├── capabilities.rs │ ├── multiaddrs.rs │ └── enr_ext.rs ├── Cargo.toml └── tests │ └── integration_test.rs ├── README.md ├── Cargo.toml ├── .gitignore └── .editorconfig /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @lnsd -------------------------------------------------------------------------------- /waku-relay/tests/gossipsub/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod pubsub; 2 | -------------------------------------------------------------------------------- /waku-relay/tests/it_gossipsub.rs: -------------------------------------------------------------------------------- 1 | mod gossipsub; 2 | mod testlib; 3 | -------------------------------------------------------------------------------- /waku-core/src/common.rs: -------------------------------------------------------------------------------- 1 | pub mod protobuf_codec; 2 | pub mod quick_protobuf_codec; 3 | -------------------------------------------------------------------------------- /waku-core/src/message/proto.rs: -------------------------------------------------------------------------------- 1 | include!(concat!(env!("OUT_DIR"), "/proto/mod.rs")); 2 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/rpc/proto.rs: -------------------------------------------------------------------------------- 1 | include!(concat!(env!("OUT_DIR"), "/proto/mod.rs")); 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto eol=lf -------------------------------------------------------------------------------- /apps/waku-cli/src/cmd.rs: -------------------------------------------------------------------------------- 1 | pub use main::*; 2 | pub use relay::*; 3 | 4 | mod main; 5 | pub(crate) mod relay; 6 | -------------------------------------------------------------------------------- /waku-node/src/behaviour.rs: -------------------------------------------------------------------------------- 1 | pub use behaviour::*; 2 | pub use event::*; 3 | 4 | mod behaviour; 5 | mod event; 6 | -------------------------------------------------------------------------------- /waku-core/src/pubsub_topic.rs: -------------------------------------------------------------------------------- 1 | pub use namespaced::*; 2 | pub use topic::*; 3 | 4 | mod namespaced; 5 | mod topic; 6 | -------------------------------------------------------------------------------- /waku-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod common; 2 | 3 | pub mod content_topic; 4 | pub mod message; 5 | pub mod pubsub_topic; 6 | -------------------------------------------------------------------------------- /waku-node/src/config.rs: -------------------------------------------------------------------------------- 1 | pub use config::*; 2 | pub use waku_relay_config::*; 3 | 4 | mod config; 5 | mod waku_relay_config; 6 | -------------------------------------------------------------------------------- /waku-node/src/event_loop.rs: -------------------------------------------------------------------------------- 1 | pub use command::*; 2 | pub use event::*; 3 | pub use event_loop::*; 4 | 5 | mod command; 6 | mod event; 7 | mod event_loop; 8 | -------------------------------------------------------------------------------- /waku-relay/src/proto.rs: -------------------------------------------------------------------------------- 1 | use waku_core::message::MAX_WAKU_MESSAGE_SIZE; 2 | 3 | pub const MAX_WAKU_RELAY_MESSAGE_SIZE: usize = 100 + MAX_WAKU_MESSAGE_SIZE; 4 | -------------------------------------------------------------------------------- /waku-enr/README.md: -------------------------------------------------------------------------------- 1 | Waku ENR 2 | -------- 3 | > RFC 31/WAKU2-ENR: https://rfc.vac.dev/spec/31/ 4 | 5 | Waku v2 ENR collection of functions and extension traits. 6 | 7 | -------------------------------------------------------------------------------- /waku-relay/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use behaviour::*; 2 | pub use event::*; 3 | 4 | mod behaviour; 5 | pub mod error; 6 | mod event; 7 | pub mod gossipsub; 8 | mod message_id; 9 | pub mod proto; 10 | -------------------------------------------------------------------------------- /waku-node/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use config::*; 2 | pub use event_loop::*; 3 | pub use node::*; 4 | pub use transport::*; 5 | 6 | pub mod behaviour; 7 | mod config; 8 | mod event_loop; 9 | mod node; 10 | pub mod transport; 11 | -------------------------------------------------------------------------------- /waku-core/src/message.rs: -------------------------------------------------------------------------------- 1 | use byte_unit::MEBIBYTE; 2 | 3 | pub use message::*; 4 | pub use traits::*; 5 | 6 | mod message; 7 | pub mod proto; 8 | mod traits; 9 | 10 | pub const MAX_WAKU_MESSAGE_SIZE: usize = 1 * MEBIBYTE as usize; 11 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "https://docs.renovatebot.com/renovate-schema.json", 3 | extends: ["config:base"], 4 | schedule: ["At 8:00 AM on Monday, Wednesday and Friday"], 5 | timezone: "Europe/Madrid", 6 | rebaseStalePrs: true, 7 | automerge: false, 8 | } 9 | -------------------------------------------------------------------------------- /waku-relay/tests/testlib/mod.rs: -------------------------------------------------------------------------------- 1 | pub use swarm::secp256k1_keypair; 2 | pub use transport::*; 3 | 4 | pub mod swarm; 5 | pub mod transport; 6 | 7 | pub fn init_logger() { 8 | let _ = pretty_env_logger::formatted_builder() 9 | .is_test(true) 10 | .try_init(); 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | rust-waku 2 | --------- 3 | [![ci](https://github.com/LNSD/rust-waku/actions/workflows/ci.yml/badge.svg)](https://github.com/LNSD/rust-waku/actions/workflows/ci.yml) 4 | 5 | Waku is a suite of privacy-preserving, peer-to-peer messaging protocols. 6 | 7 | [Learn more about Waku](https://waku.org/) 8 | -------------------------------------------------------------------------------- /waku-enr/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Waku v2 ENR (EIP-778) collection of functions and an extension trait. 2 | //! RFC 31/WAKU2-ENR: https://rfc.vac.dev/spec/31/ 3 | 4 | pub use crate::capabilities::*; 5 | pub use crate::enr_ext::*; 6 | pub use enr; 7 | 8 | mod capabilities; 9 | mod enr_ext; 10 | mod multiaddrs; 11 | -------------------------------------------------------------------------------- /apps/waku-cli/src/cmd/relay.rs: -------------------------------------------------------------------------------- 1 | pub use publish::RelayPublishCmd; 2 | pub use subscribe::RelaySubscribeCmd; 3 | 4 | pub(crate) mod publish; 5 | pub(crate) mod subscribe; 6 | 7 | #[derive(Debug, Clone, clap::Subcommand)] 8 | pub enum RelayCommand { 9 | Publish(RelayPublishCmd), 10 | Subscribe(RelaySubscribeCmd), 11 | } 12 | -------------------------------------------------------------------------------- /waku-node/src/event_loop/event.rs: -------------------------------------------------------------------------------- 1 | use strum_macros::Display; 2 | 3 | use waku_core::message::WakuMessage; 4 | use waku_core::pubsub_topic::PubsubTopic; 5 | 6 | #[derive(Debug, Display)] 7 | pub enum Event { 8 | WakuRelayMessage { 9 | pubsub_topic: PubsubTopic, 10 | message: WakuMessage, 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/signing.rs: -------------------------------------------------------------------------------- 1 | pub use signer::{AuthorOnlySigner, Libp2pSigner, MessageSigner, NoopSigner, RandomAuthorSigner}; 2 | pub use validator::{ 3 | AnonymousMessageValidator, MessageValidator, NoopMessageValidator, PermissiveMessageValidator, 4 | StrictMessageValidator, 5 | }; 6 | 7 | mod signer; 8 | mod validator; 9 | -------------------------------------------------------------------------------- /waku-core/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - remote: buf.build/prost/plugins/prost:v0.2.1-1 4 | out: proto/ 5 | opt: 6 | - bytes=. 7 | - compile_well_known_types 8 | - name: prost-crate 9 | out: . 10 | opt: 11 | - include_file=proto/mod.rs 12 | - no_features 13 | strategy: all 14 | -------------------------------------------------------------------------------- /waku-relay/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - remote: buf.build/prost/plugins/prost:v0.2.1-1 4 | out: proto/ 5 | opt: 6 | - bytes=. 7 | - compile_well_known_types 8 | - name: prost-crate 9 | out: . 10 | opt: 11 | - include_file=proto/mod.rs 12 | - no_features 13 | strategy: all 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "waku-core", 4 | "waku-enr", 5 | "waku-node", 6 | "waku-relay", 7 | "apps/wakunode2", 8 | "apps/waku-cli", 9 | ] 10 | 11 | [workspace.dependencies] 12 | anyhow = "1.0.71" 13 | bytes = "1.4.0" 14 | futures = "0.3.28" 15 | libp2p = "0.51.3" 16 | thiserror = "1.0.40" 17 | tokio = "1.28.1" 18 | -------------------------------------------------------------------------------- /apps/waku-cli/src/cmd/main.rs: -------------------------------------------------------------------------------- 1 | use crate::cmd::relay::RelayCommand; 2 | 3 | #[derive(Debug, Clone, clap::Parser)] 4 | #[command(author, version, about, long_about = None)] 5 | pub struct Cli { 6 | #[command(subcommand)] 7 | pub command: Commands, 8 | } 9 | 10 | #[derive(Debug, Clone, clap::Subcommand)] 11 | pub enum Commands { 12 | #[command(subcommand)] 13 | Relay(RelayCommand), 14 | } 15 | -------------------------------------------------------------------------------- /waku-enr/src/capabilities.rs: -------------------------------------------------------------------------------- 1 | use bitflags::bitflags; 2 | 3 | bitflags! { 4 | /// The ENR `waku2` node capabilities bitfield. 5 | #[derive(Debug, Default, PartialEq, Eq)] 6 | pub struct WakuEnrCapabilities: u8 { 7 | const RELAY = 0b00000001; 8 | const STORE = 0b00000010; 9 | const FILTER = 0b00000100; 10 | const LIGHTPUSH = 0b00001000; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /waku-enr/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "waku-enr" 3 | description = "Waku v2 ENR" 4 | version = "0.1.0" 5 | edition = "2021" 6 | authors = ["Lorenzo Delgado "] 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | bitflags = "2.2.1" 11 | enr = { version = "0.8.1", features = ["ed25519", "k256"] } 12 | multiaddr = { version = "0.17.1", default-features = false } 13 | 14 | [dev-dependencies] 15 | base64 = "0.21.0" 16 | -------------------------------------------------------------------------------- /apps/waku-cli/scripts/subscribe.sh: -------------------------------------------------------------------------------- 1 | #AMS_PEER="/dns4/node-01.gc-us-central1-a.wakuv2.test.statusim.net/tcp/30303/p2p/16Uiu2HAmJb2e28qLXxT5kZxVUUoJt72EMzNGXB47Rxx5hw3q4YjS" 2 | PEER="/dns4/localhost/tcp/10015/p2p/16Uiu2HAmKXw1VChPNBPKUttgW5mQAFATEMaLSEGhCVYvRWabjBHj" 3 | PUBSUB_TOPIC="/waku/2/default-waku/proto" 4 | CONTENT_TOPIC="/rust-waku/example/raw" 5 | 6 | ./target/debug/waku relay subscribe --peer $PEER --pubsub-topic $PUBSUB_TOPIC --content-topic $CONTENT_TOPIC 7 | -------------------------------------------------------------------------------- /apps/waku-cli/scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #AMS_PEER="/dns4/node-01.gc-us-central1-a.wakuv2.test.statusim.net/tcp/30303/p2p/16Uiu2HAmJb2e28qLXxT5kZxVUUoJt72EMzNGXB47Rxx5hw3q4YjS" 2 | PEER="/dns4/localhost/tcp/10015/p2p/16Uiu2HAmKXw1VChPNBPKUttgW5mQAFATEMaLSEGhCVYvRWabjBHj" 3 | PUBSUB_TOPIC="/waku/2/default-waku/proto" 4 | CONTENT_TOPIC="/rust-waku/example/raw" 5 | PAYLOAD="deadbeef" 6 | 7 | ./target/debug/waku relay publish --peer $PEER --pubsub-topic $PUBSUB_TOPIC --content-topic $CONTENT_TOPIC $PAYLOAD 8 | -------------------------------------------------------------------------------- /waku-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "waku-core" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = { workspace = true } 8 | assert_matches = "1.5.0" 9 | async-codec = "0.4.1" 10 | asynchronous-codec = "0.6.1" 11 | byte-unit = { version = "4.0.19", default-features = false, features = ["std"] } 12 | bytes = "1.4.0" 13 | hex = "0.4.3" 14 | prost = "0.11.9" 15 | quick-protobuf = "0.8" 16 | thiserror.workspace = true 17 | unsigned-varint = { version = "0.7.1", features = ["asynchronous-codec"] } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### IDEs and editors 2 | .vscode/ 3 | .idea/ 4 | 5 | ### Rust and Cargo 6 | # Generated by Cargo 7 | # will have compiled files and executables 8 | debug/ 9 | target/ 10 | 11 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 12 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 13 | Cargo.lock 14 | 15 | # These are backup files generated by rustfmt 16 | **/*.rs.bk 17 | 18 | # MSVC Windows builds of rustc generate these, which store debugging information 19 | *.pdb 20 | 21 | ### Project specific 22 | # Node configuration files 23 | config.toml 24 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/rpc.rs: -------------------------------------------------------------------------------- 1 | pub use fragmentation::{fragment_rpc_message, FragmentationError}; 2 | pub use proto::waku::relay::v2::{ 3 | ControlGraft as ControlGraftProto, ControlIHave as ControlIHaveProto, ControlIHave, 4 | ControlIWant as ControlIWantProto, ControlMessage as ControlMessageProto, 5 | ControlPrune as ControlPruneProto, Message as MessageProto, PeerInfo as PeerInfoProto, 6 | Rpc as RpcProto, TopicDescriptor as TopicDescriptorProto, 7 | }; 8 | pub use types::MessageRpc; 9 | pub use validation::validate_message_proto; 10 | 11 | mod fragmentation; 12 | mod proto; 13 | mod traits; 14 | mod types; 15 | mod validation; 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 4 13 | 14 | [*.{md,mdx}] 15 | # double whitespace at end of line 16 | # denotes a line break in Markdown 17 | trim_trailing_whitespace = false 18 | 19 | [*.sh] 20 | end_of_line = lf 21 | 22 | [Makefile] 23 | indent_style = tab 24 | 25 | [*.{json,json5}] 26 | indent_size = 2 27 | 28 | [*.{yml,yaml}] 29 | indent_size = 2 30 | 31 | -------------------------------------------------------------------------------- /waku-node/src/config/waku_relay_config.rs: -------------------------------------------------------------------------------- 1 | use libp2p::PeerId; 2 | 3 | #[derive(Debug, Clone, Default)] 4 | pub struct WakuRelayConfig { 5 | pub static_nodes: Vec, 6 | } 7 | 8 | #[derive(Default)] 9 | pub struct WakuRelayConfigBuilder { 10 | config: WakuRelayConfig, 11 | } 12 | 13 | impl WakuRelayConfigBuilder { 14 | pub fn new() -> Self { 15 | Default::default() 16 | } 17 | 18 | pub fn build(&self) -> WakuRelayConfig { 19 | self.config.clone() 20 | } 21 | 22 | pub fn static_nodes(&mut self, nodes: Vec) -> &mut Self { 23 | self.config.static_nodes = nodes; 24 | self 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /waku-core/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::error::Error; 3 | use std::process::{exit, Command}; 4 | 5 | fn main() -> Result<(), Box> { 6 | let out_dir = env::var_os("OUT_DIR").unwrap(); 7 | 8 | let status = Command::new("buf") 9 | .arg("generate") 10 | .arg("https://github.com/LNSD/waku-proto.git#branch=rust-waku") 11 | .arg("--path") 12 | .arg("waku/message") 13 | .arg("--output") 14 | .arg(out_dir) 15 | .current_dir(env!("CARGO_MANIFEST_DIR")) 16 | .status() 17 | .unwrap(); 18 | 19 | if !status.success() { 20 | exit(status.code().unwrap_or(-1)) 21 | } 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /waku-relay/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::error::Error; 3 | use std::process::{exit, Command}; 4 | 5 | fn main() -> Result<(), Box> { 6 | let out_dir = env::var_os("OUT_DIR").unwrap(); 7 | 8 | let status = Command::new("buf") 9 | .arg("generate") 10 | .arg("https://github.com/LNSD/waku-proto.git#branch=rust-waku") 11 | .arg("--path") 12 | .arg("waku/relay") 13 | .arg("--output") 14 | .arg(out_dir) 15 | .current_dir(env!("CARGO_MANIFEST_DIR")) 16 | .status() 17 | .unwrap(); 18 | 19 | if !status.success() { 20 | exit(status.code().unwrap_or(-1)) 21 | } 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /apps/waku-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "waku-cli" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [[bin]] 7 | name = "waku" 8 | path = "src/main.rs" 9 | 10 | [dependencies] 11 | anyhow.workspace = true 12 | bytes.workspace = true 13 | clap = { version = "4.2.7", features = ["derive"] } 14 | hex = "0.4.3" 15 | libp2p.workspace = true 16 | log = "0.4.17" 17 | multiaddr = "0.17.1" 18 | pretty_env_logger = "0.4.0" 19 | tokio = { workspace = true, features = ["macros", "rt", "signal", "time"] } 20 | ulid = "1.0.0" 21 | waku-core = { version = "0.1.0", path = "../../waku-core" } 22 | waku-node = { version = "0.1.0", path = "../../waku-node" } 23 | waku-relay = { version = "0.1.0", path = "../../waku-relay" } 24 | -------------------------------------------------------------------------------- /apps/wakunode2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wakunode2" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = { workspace = true } 8 | clap = { version = "4.2.7", features = ["derive", "env"] } 9 | config = { version = "0.13.3", default-features = false, features = ["toml", "json", "json5", "yaml"] } 10 | futures.workspace = true 11 | hex = "0.4.3" 12 | libp2p.workspace = true 13 | log = "0.4.17" 14 | multiaddr = "0.17.1" 15 | pretty_env_logger = "0.4.0" 16 | serde = { version = "1.0.163", features = ["derive"] } 17 | tokio = { workspace = true, features = ["signal", "rt", "macros"] } 18 | waku-core = { version = "0.1.0", path = "../../waku-core" } 19 | waku-node = { version = "0.1.0", path = "../../waku-node" } 20 | -------------------------------------------------------------------------------- /waku-node/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "waku-node" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = { workspace = true } 8 | bytes = { workspace = true } 9 | clap = { version = "4.2.7", features = ["derive"] } 10 | futures = { workspace = true } 11 | hex = "0.4.3" 12 | libp2p = { workspace = true, features = ["yamux", "tcp", "tokio", "identify", "dns", "ping", "noise", "macros", "secp256k1"] } 13 | libp2p-mplex = "0.39.0" 14 | log = "0.4.17" 15 | pretty_env_logger = "0.4.0" 16 | strum_macros = "0.24.3" 17 | thiserror = { workspace = true } 18 | tokio = { workspace = true, features = ["sync", "rt", "macros"] } 19 | void = "1.0.2" 20 | waku-core = { version = "0.1.0", path = "../waku-core" } 21 | waku-relay = { version = "0.1.0", path = "../waku-relay" } 22 | -------------------------------------------------------------------------------- /waku-node/src/behaviour/event.rs: -------------------------------------------------------------------------------- 1 | use libp2p::{identify, ping}; 2 | 3 | #[derive(Debug)] 4 | pub enum Event { 5 | Ping(ping::Event), 6 | Identify(identify::Event), 7 | WakuRelay(waku_relay::Event), 8 | } 9 | 10 | impl From for Event { 11 | fn from(_: void::Void) -> Self { 12 | unreachable!() 13 | } 14 | } 15 | 16 | impl From for Event { 17 | fn from(event: ping::Event) -> Self { 18 | Event::Ping(event) 19 | } 20 | } 21 | 22 | impl From for Event { 23 | fn from(event: identify::Event) -> Self { 24 | Event::Identify(event) 25 | } 26 | } 27 | 28 | impl From for Event { 29 | fn from(event: waku_relay::Event) -> Self { 30 | Event::WakuRelay(event) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /waku-core/src/message/traits.rs: -------------------------------------------------------------------------------- 1 | use crate::message::proto::waku::message::v1::WakuMessage as WakuMessageProto; 2 | use crate::message::WakuMessage; 3 | 4 | impl From for WakuMessage { 5 | fn from(proto: WakuMessageProto) -> Self { 6 | Self { 7 | payload: proto.payload, 8 | content_topic: proto.content_topic.into(), 9 | meta: proto.meta, 10 | ephemeral: proto.ephemeral.unwrap_or(false), 11 | } 12 | } 13 | } 14 | 15 | impl From for WakuMessageProto { 16 | fn from(message: WakuMessage) -> Self { 17 | WakuMessageProto { 18 | payload: message.payload, 19 | content_topic: message.content_topic.to_string(), 20 | version: None, // Deprecated 21 | timestamp: None, // Deprecated 22 | meta: message.meta, 23 | ephemeral: Some(message.ephemeral), 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/rpc/validation.rs: -------------------------------------------------------------------------------- 1 | use libp2p::PeerId; 2 | 3 | use crate::gossipsub::error::MessageValidationError; 4 | use crate::gossipsub::rpc::MessageProto; 5 | 6 | pub fn validate_message_proto(message: &MessageProto) -> Result<(), MessageValidationError> { 7 | if message.topic.is_empty() { 8 | // topic field must not be empty 9 | return Err(MessageValidationError::InvalidTopic); 10 | } 11 | 12 | // If present, from field must hold a valid PeerId 13 | if let Some(peer_id) = message.from.as_ref() { 14 | if PeerId::from_bytes(peer_id).is_err() { 15 | return Err(MessageValidationError::InvalidPeerId); 16 | } 17 | } 18 | 19 | // If present, seqno field must be a 64-bit big-endian serialized unsigned integer 20 | if let Some(seq_no) = message.seqno.as_ref() { 21 | if seq_no.len() != 8 { 22 | return Err(MessageValidationError::InvalidSequenceNumber); 23 | } 24 | } 25 | 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /waku-core/src/message/message.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Formatter}; 2 | 3 | use bytes::Bytes; 4 | 5 | use crate::content_topic::ContentTopic; 6 | 7 | #[derive(Clone, Eq, PartialEq)] 8 | pub struct WakuMessage { 9 | pub payload: Bytes, 10 | pub content_topic: ContentTopic, 11 | pub meta: Option, 12 | pub ephemeral: bool, 13 | } 14 | 15 | impl Debug for WakuMessage { 16 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 17 | let payload_fmt = match self.payload.get(0..32) { 18 | Some(slice) => format!("{}…", hex::encode(slice)), 19 | None => hex::encode(&self.payload[..]), 20 | }; 21 | let meta_fmt = &self.meta.clone().map_or("None".to_string(), hex::encode); 22 | 23 | f.debug_struct("WakuMessage") 24 | .field("content_topic", &self.content_topic) 25 | .field("meta", &meta_fmt) 26 | .field("payload", &payload_fmt) 27 | .field("ephemeral", &self.ephemeral) 28 | .finish() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /waku-relay/tests/testlib/transport.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use libp2p::core::muxing::StreamMuxerBox; 4 | use libp2p::core::transport::{Boxed, MemoryTransport, Transport}; 5 | use libp2p::core::upgrade::Version; 6 | use libp2p::identity::{Keypair, PeerId}; 7 | use libp2p::{noise, yamux, Multiaddr}; 8 | 9 | /// Type alias for libp2p transport 10 | pub type P2PTransport = (PeerId, StreamMuxerBox); 11 | /// Type alias for boxed libp2p transport 12 | pub type BoxedP2PTransport = Boxed; 13 | 14 | /// Any memory address (for testing) 15 | pub fn any_memory_addr() -> Multiaddr { 16 | "/memory/0".parse().unwrap() 17 | } 18 | 19 | /// In memory transport 20 | pub fn test_transport(keypair: &Keypair) -> std::io::Result { 21 | let transport = MemoryTransport::default(); 22 | 23 | Ok(transport 24 | .upgrade(Version::V1) 25 | .authenticate(noise::Config::new(keypair).unwrap()) 26 | .multiplex(yamux::Config::default()) 27 | .timeout(Duration::from_secs(20)) 28 | .boxed()) 29 | } 30 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/codec.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use asynchronous_codec::{Decoder, Encoder}; 4 | use bytes::BytesMut; 5 | 6 | use waku_core::common::protobuf_codec; 7 | 8 | use crate::gossipsub::handler::HandlerEvent; 9 | use crate::gossipsub::rpc::RpcProto; 10 | 11 | pub struct Codec { 12 | /// The codec to handle common encoding/decoding of protobuf messages 13 | codec: protobuf_codec::Codec, 14 | } 15 | 16 | impl Codec { 17 | pub fn new(max_len_bytes: usize) -> Self { 18 | let codec = protobuf_codec::Codec::new(max_len_bytes); 19 | Self { codec } 20 | } 21 | } 22 | 23 | impl Encoder for Codec { 24 | type Item = RpcProto; 25 | type Error = io::Error; 26 | 27 | fn encode(&mut self, item: Self::Item, dst: &mut BytesMut) -> Result<(), Self::Error> { 28 | self.codec.encode(item, dst) 29 | } 30 | } 31 | 32 | impl Decoder for Codec { 33 | type Item = HandlerEvent; 34 | type Error = io::Error; 35 | 36 | fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { 37 | self.codec.decode(src).map(|rpc| rpc.map(HandlerEvent::Rpc)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/wakunode2/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use log::{info, LevelFilter}; 3 | 4 | mod app; 5 | mod config; 6 | 7 | #[derive(Debug, Clone, PartialEq, Eq, clap::Parser)] 8 | #[clap(author, version, about, long_about = None)] 9 | pub struct Cli { 10 | #[arg(short = 'C', long, required = false, env = "WAKUNODE2_CONFIG_FILE")] 11 | pub config_file: Option, 12 | } 13 | 14 | #[tokio::main] 15 | async fn main() -> anyhow::Result<()> { 16 | let cli = Cli::parse(); 17 | let conf = config::load(cli.config_file).unwrap(); 18 | 19 | pretty_env_logger::formatted_builder() 20 | .filter_level(LevelFilter::Info) 21 | .format_timestamp_millis() 22 | .init(); 23 | 24 | let mut app = app::App::new(conf)?; 25 | app.setup().await?; 26 | 27 | loop { 28 | tokio::select! { 29 | _ = tokio::signal::ctrl_c() => { 30 | info!("ctrl-c received, shutting down"); 31 | break; 32 | } 33 | 34 | ev = app.run() => { 35 | if let Some(event) = ev { 36 | info!("{event:?}"); 37 | } 38 | } 39 | } 40 | } 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /apps/waku-cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use log::LevelFilter; 3 | use log::{error, info}; 4 | 5 | use crate::cmd::{Cli, Commands, RelayCommand}; 6 | 7 | mod cmd; 8 | 9 | async fn run_cmd(cli: Cli) -> anyhow::Result<()> { 10 | match cli.command { 11 | Commands::Relay(RelayCommand::Publish(cmd_args)) => { 12 | cmd::relay::publish::run_cmd(cmd_args).await 13 | } 14 | Commands::Relay(RelayCommand::Subscribe(cmd_args)) => { 15 | cmd::relay::subscribe::run_cmd(cmd_args).await 16 | } 17 | } 18 | } 19 | 20 | #[tokio::main] 21 | async fn main() -> anyhow::Result<()> { 22 | let cli = Cli::parse(); 23 | 24 | pretty_env_logger::formatted_builder() 25 | .filter_level(LevelFilter::Info) 26 | .format_timestamp_millis() 27 | .init(); 28 | 29 | tokio::select! { 30 | _ = tokio::signal::ctrl_c() => { 31 | info!("ctrl-c received, shutting down"); 32 | return Ok(()); 33 | } 34 | 35 | res = run_cmd(cli) => { 36 | if let Err(e) = res { 37 | error!("Error: {}", e); 38 | return Err(e); 39 | } 40 | } 41 | } 42 | 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /apps/wakunode2/src/config.rs: -------------------------------------------------------------------------------- 1 | use config::Config; 2 | 3 | #[derive(Debug, Default, Clone, PartialEq, Eq, serde::Deserialize)] 4 | pub struct Wakunode2Conf { 5 | #[serde(default)] 6 | pub agent: String, 7 | #[serde(default)] 8 | pub private_key: String, 9 | #[serde(default)] 10 | pub listen_addresses: Vec, 11 | #[serde(default)] 12 | pub bootstrap_nodes: Vec, 13 | #[serde(default)] 14 | pub keepalive: bool, 15 | 16 | #[serde(default)] 17 | pub relay: bool, 18 | #[serde(default)] 19 | pub topics: Vec, 20 | } 21 | 22 | pub fn load(config_file: Option) -> anyhow::Result { 23 | let mut conf_builder = Config::builder(); 24 | if let Some(config_file) = config_file { 25 | conf_builder = 26 | conf_builder.add_source(config::File::with_name(config_file.as_str()).required(false)); 27 | } 28 | conf_builder = conf_builder.add_source( 29 | config::Environment::with_prefix("WAKUNODE2") 30 | .ignore_empty(true) 31 | .prefix_separator("_") 32 | .separator("_"), 33 | ); 34 | 35 | let conf = conf_builder.build()?.try_deserialize::()?; 36 | Ok(conf) 37 | } 38 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/event.rs: -------------------------------------------------------------------------------- 1 | use crate::gossipsub::{Message, MessageId, TopicHash}; 2 | use libp2p::PeerId; 3 | 4 | /// Event that can be emitted by the gossipsub behaviour. 5 | #[derive(Debug)] 6 | pub enum Event { 7 | /// A message has been received. 8 | Message { 9 | /// The peer that forwarded us this message. 10 | propagation_source: PeerId, 11 | /// The [`MessageId`] of the message. This should be referenced by the application when 12 | /// validating a message (if required). 13 | message_id: MessageId, 14 | /// The decompressed message itself. 15 | message: Message, 16 | }, 17 | /// A remote subscribed to a topic. 18 | Subscribed { 19 | /// Remote that has subscribed. 20 | peer_id: PeerId, 21 | /// The topic it has subscribed to. 22 | topic: TopicHash, 23 | }, 24 | /// A remote unsubscribed from a topic. 25 | Unsubscribed { 26 | /// Remote that has unsubscribed. 27 | peer_id: PeerId, 28 | /// The topic it has subscribed from. 29 | topic: TopicHash, 30 | }, 31 | /// A peer that does not support gossipsub has connected. 32 | GossipsubNotSupported { peer_id: PeerId }, 33 | } 34 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/heartbeat.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | use std::task::{Context, Poll}; 3 | use std::time::Duration; 4 | 5 | use futures::{Stream, StreamExt}; 6 | use futures_ticker::Ticker; 7 | 8 | pub(crate) struct Heartbeat { 9 | /// Heartbeat interval stream. 10 | ticker: Ticker, 11 | 12 | /// Number of heartbeats since the beginning of time; this allows us to amortize some resource 13 | /// clean up (e.g. backoff clean up). 14 | ticks: u64, 15 | } 16 | 17 | impl Heartbeat { 18 | pub(crate) fn new(interval: Duration, delay: Duration) -> Self { 19 | Self { 20 | ticker: Ticker::new_with_next(interval, delay), 21 | ticks: 0, 22 | } 23 | } 24 | } 25 | 26 | impl Stream for Heartbeat { 27 | type Item = u64; 28 | 29 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 30 | match self.ticker.poll_next_unpin(cx) { 31 | Poll::Pending => Poll::Pending, 32 | Poll::Ready(Some(_)) => { 33 | self.ticks = self.ticks.wrapping_add(1); 34 | Poll::Ready(Some(self.ticks)) 35 | } 36 | Poll::Ready(None) => Poll::Ready(None), 37 | } 38 | } 39 | 40 | fn size_hint(&self) -> (usize, Option) { 41 | self.ticker.size_hint() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/seq_no.rs: -------------------------------------------------------------------------------- 1 | use instant::SystemTime; 2 | 3 | /// A trait for message sequence number generators. 4 | pub trait MessageSeqNumberGenerator { 5 | fn next(&mut self) -> u64; 6 | } 7 | 8 | /// A strictly linearly increasing sequence number. 9 | /// 10 | /// We start from the current time as unix timestamp in milliseconds. 11 | #[derive(Debug)] 12 | pub struct LinearSequenceNumber(u64); 13 | 14 | impl LinearSequenceNumber { 15 | pub fn new() -> Self { 16 | let unix_timestamp = SystemTime::now() 17 | .duration_since(SystemTime::UNIX_EPOCH) 18 | .expect("time to be linear") 19 | .as_nanos(); 20 | 21 | Self(unix_timestamp as u64) 22 | } 23 | } 24 | 25 | impl MessageSeqNumberGenerator for LinearSequenceNumber { 26 | fn next(&mut self) -> u64 { 27 | self.0 = self 28 | .0 29 | .checked_add(1) 30 | .expect("to not exhaust u64 space for sequence numbers"); 31 | 32 | self.0 33 | } 34 | } 35 | 36 | /// A random sequence number generator. 37 | #[derive(Debug)] 38 | pub struct RandomSequenceNumber; 39 | 40 | impl RandomSequenceNumber { 41 | pub fn new() -> Self { 42 | Self {} 43 | } 44 | } 45 | 46 | impl MessageSeqNumberGenerator for RandomSequenceNumber { 47 | fn next(&mut self) -> u64 { 48 | rand::random() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /waku-node/src/config/config.rs: -------------------------------------------------------------------------------- 1 | use libp2p::identity::Keypair; 2 | 3 | use crate::config::waku_relay_config::WakuRelayConfig; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct NodeConfig { 7 | pub keypair: Keypair, 8 | pub keepalive: bool, 9 | pub ping: bool, 10 | pub relay: Option, 11 | } 12 | 13 | impl Default for NodeConfig { 14 | fn default() -> Self { 15 | Self { 16 | keypair: Keypair::generate_secp256k1(), 17 | keepalive: false, 18 | ping: false, 19 | relay: None, 20 | } 21 | } 22 | } 23 | 24 | #[derive(Debug, Default)] 25 | pub struct NodeConfigBuilder { 26 | config: NodeConfig, 27 | } 28 | 29 | impl NodeConfigBuilder { 30 | pub fn new() -> Self { 31 | Default::default() 32 | } 33 | 34 | pub fn build(self) -> NodeConfig { 35 | self.config 36 | } 37 | 38 | pub fn keypair(mut self, keypair: Keypair) -> Self { 39 | self.config.keypair = keypair; 40 | self 41 | } 42 | 43 | pub fn with_keepalive(mut self, enable: bool) -> Self { 44 | self.config.keepalive = enable; 45 | self 46 | } 47 | 48 | pub fn with_ping(mut self, enable: bool) -> Self { 49 | self.config.ping = enable; 50 | self 51 | } 52 | 53 | pub fn with_waku_relay(mut self, config: WakuRelayConfig) -> Self { 54 | self.config.relay = Some(config); 55 | self 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /waku-relay/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "waku-relay" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow.workspace = true 8 | asynchronous-codec = "0.6" 9 | base64 = "0.21.2" 10 | byteorder = "1.3.4" 11 | bytes = "1.4.0" 12 | either = "1.5" 13 | fnv = "1.0.7" 14 | futures = "0.3.28" 15 | futures-ticker = "0.0.3" 16 | hex_fmt = "0.3.0" 17 | instant = "0.1.12" 18 | libp2p = { version = "0.51.3", features = ["macros"] } 19 | log = "0.4.18" 20 | prometheus-client = "0.21.1" 21 | prost = "0.11.9" 22 | rand = "0.8" 23 | regex = "1.8.3" 24 | serde = { version = "1", optional = true, features = ["derive"] } 25 | sha2 = "0.10.6" 26 | smallvec = "1.6.1" 27 | strum_macros = "0.24.3" 28 | thiserror.workspace = true 29 | unsigned-varint = { version = "0.7.0", features = ["asynchronous_codec"] } 30 | void = "1.0.2" 31 | waku-core = { version = "0.1.0", path = "../waku-core" } 32 | 33 | [dev-dependencies] 34 | assert_matches = "1.5.0" 35 | env_logger = "0.10.0" 36 | hex = "0.4.2" 37 | hex-literal = "0.4.1" 38 | libp2p = { version = "0.51.3", features = ["noise", "yamux", "secp256k1", "tokio", "gossipsub"] } 39 | pretty_env_logger = "0.5.0" 40 | tokio = { workspace = true, features = ["rt", "time", "macros"] } 41 | 42 | # Passing arguments to the docsrs builder in order to properly document cfg's. 43 | # More information: https://docs.rs/about/builds#cross-compiling 44 | [package.metadata.docs.rs] 45 | all-features = true 46 | rustdoc-args = ["--cfg", "docsrs"] 47 | rustc-args = ["--cfg", "docsrs"] 48 | -------------------------------------------------------------------------------- /waku-node/src/behaviour/behaviour.rs: -------------------------------------------------------------------------------- 1 | use libp2p::identity::PublicKey; 2 | use libp2p::swarm::behaviour::toggle; 3 | use libp2p::swarm::keep_alive; 4 | use libp2p::swarm::NetworkBehaviour; 5 | use libp2p::{identify, ping}; 6 | 7 | use crate::WakuRelayConfig; 8 | 9 | pub struct Config { 10 | pub local_public_key: PublicKey, 11 | pub keep_alive: Option, 12 | pub ping: Option, 13 | pub relay: Option, 14 | } 15 | 16 | #[derive(NetworkBehaviour)] 17 | #[behaviour(out_event = "crate::behaviour::event::Event")] 18 | pub struct Behaviour { 19 | pub keep_alive: toggle::Toggle, 20 | pub ping: toggle::Toggle, 21 | pub identify: identify::Behaviour, 22 | pub waku_relay: toggle::Toggle, 23 | } 24 | 25 | impl Behaviour { 26 | pub fn new(config: Config) -> Self { 27 | let keep_alive = toggle::Toggle::from(config.keep_alive.map(|_| Default::default())); 28 | let ping = toggle::Toggle::from(config.ping.map(|_| Default::default())); 29 | let identify = identify::Behaviour::new( 30 | identify::Config::new("/ipfs/id/1.0.0".to_owned(), config.local_public_key) 31 | .with_agent_version(format!("rust-waku/{}", env!("CARGO_PKG_VERSION"))), 32 | ); 33 | let waku_relay = toggle::Toggle::from(config.relay.map(|_| Default::default())); 34 | 35 | Self { 36 | keep_alive, 37 | ping, 38 | identify, 39 | waku_relay, 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/peer_score.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Sigma Prime Pty Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | //! 22 | //! Manages and stores the Scoring logic of a particular peer on the gossipsub behaviour. 23 | 24 | pub use params::{ 25 | score_parameter_decay, score_parameter_decay_with_base, PeerScoreParams, PeerScoreThresholds, 26 | TopicScoreParams, 27 | }; 28 | pub use service::*; 29 | pub use stats::*; 30 | 31 | mod params; 32 | mod service; 33 | mod stats; 34 | -------------------------------------------------------------------------------- /waku-node/src/transport.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use libp2p::core::muxing::StreamMuxerBox; 4 | use libp2p::core::transport; 5 | use libp2p::identity::Keypair; 6 | use libp2p::{core, dns, noise, tcp, yamux, PeerId, Transport}; 7 | use libp2p_mplex as mplex; 8 | 9 | /// Type alias for libp2p transport 10 | pub type P2PTransport = (PeerId, StreamMuxerBox); 11 | /// Type alias for boxed libp2p transport 12 | pub type BoxedP2PTransport = transport::Boxed; 13 | 14 | // create the libp2p transport for the node 15 | pub fn default_transport(keypair: &Keypair) -> std::io::Result { 16 | let transport = { 17 | dns::TokioDnsConfig::system(tcp::tokio::Transport::new( 18 | tcp::Config::default().nodelay(true), 19 | ))? 20 | }; 21 | 22 | Ok(transport 23 | .upgrade(core::upgrade::Version::V1) 24 | .authenticate(noise::Config::new(keypair).unwrap()) 25 | .multiplex(core::upgrade::SelectUpgrade::new( 26 | yamux::Config::default(), 27 | mplex::MplexConfig::default(), 28 | )) 29 | .timeout(Duration::from_secs(20)) 30 | .boxed()) 31 | } 32 | 33 | /// In memory transport 34 | pub fn memory_transport(keypair: &Keypair) -> std::io::Result { 35 | let transport = transport::MemoryTransport::default(); 36 | 37 | Ok(transport 38 | .upgrade(core::upgrade::Version::V1) 39 | .authenticate(noise::Config::new(keypair).unwrap()) 40 | .multiplex(core::upgrade::SelectUpgrade::new( 41 | yamux::Config::default(), 42 | mplex::MplexConfig::default(), 43 | )) 44 | .timeout(Duration::from_secs(20)) 45 | .boxed()) 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - "main" 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 10 | cancel-in-progress: true 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | 15 | jobs: 16 | build_and_test: 17 | name: "build and test" 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - name: Setup Rust toolchain 23 | uses: dtolnay/rust-toolchain@stable 24 | with: 25 | components: llvm-tools-preview 26 | 27 | - name: Install Protoc 28 | uses: arduino/setup-protoc@v1 29 | with: 30 | repo-token: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: Install Buf 33 | uses: bufbuild/buf-setup-action@v1 34 | with: 35 | github_token: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: Install Protoc's Prost plugin 38 | uses: baptiste0928/cargo-install@v2 39 | with: 40 | crate: protoc-gen-prost 41 | 42 | - name: Install Protoc's Prost crate plugin 43 | uses: baptiste0928/cargo-install@v2 44 | with: 45 | crate: protoc-gen-prost-crate 46 | 47 | - name: Install Cargo LLVM cov 48 | uses: baptiste0928/cargo-install@v2 49 | with: 50 | crate: cargo-llvm-cov 51 | 52 | - name: Install Cargo nextest 53 | uses: baptiste0928/cargo-install@v2 54 | with: 55 | crate: cargo-nextest 56 | 57 | - name: Build 58 | run: cargo build 59 | 60 | - name: Test 61 | run: cargo llvm-cov nextest --lcov --output-path lcov.info 62 | 63 | - name: Upload coverage report to codecov 64 | uses: codecov/codecov-action@v3 65 | with: 66 | files: lcov.info -------------------------------------------------------------------------------- /waku-relay/tests/testlib/swarm.rs: -------------------------------------------------------------------------------- 1 | use futures::StreamExt; 2 | use libp2p::identity::{secp256k1, Keypair}; 3 | use libp2p::swarm::SwarmEvent; 4 | use libp2p::{Multiaddr, Swarm}; 5 | 6 | use waku_relay::gossipsub::Behaviour; 7 | 8 | pub async fn poll(swarm: &mut Swarm) { 9 | loop { 10 | let event = swarm.select_next_some().await; 11 | log::trace!("Event: {:?}", event); 12 | } 13 | } 14 | 15 | pub fn secp256k1_keypair(key: &str) -> Keypair { 16 | let raw_key = hex::decode(key).expect("key to be valid"); 17 | let secret_key = secp256k1::SecretKey::try_from_bytes(raw_key).unwrap(); 18 | secp256k1::Keypair::from(secret_key).into() 19 | } 20 | 21 | pub async fn wait_for_new_listen_addr(swarm: &mut Swarm) -> Multiaddr { 22 | loop { 23 | let event = swarm.select_next_some().await; 24 | log::trace!("Event: {:?}", event); 25 | if let SwarmEvent::NewListenAddr { address, .. } = event { 26 | return address; 27 | } 28 | } 29 | } 30 | 31 | pub async fn wait_for_incoming_connection(swarm: &mut Swarm) { 32 | loop { 33 | let event = swarm.select_next_some().await; 34 | log::trace!("Event: {:?}", event); 35 | if matches!(event, SwarmEvent::IncomingConnection { .. }) { 36 | break; 37 | } 38 | } 39 | } 40 | 41 | pub async fn wait_for_dialing(swarm: &mut Swarm) { 42 | loop { 43 | let event = swarm.select_next_some().await; 44 | log::trace!("Event: {:?}", event); 45 | if matches!(event, SwarmEvent::Dialing { .. }) { 46 | break; 47 | } 48 | } 49 | } 50 | 51 | pub async fn wait_for_connection_established(swarm: &mut Swarm) { 52 | loop { 53 | let event = swarm.select_next_some().await; 54 | log::trace!("Event: {:?}", event); 55 | if matches!(event, SwarmEvent::ConnectionEstablished { .. }) { 56 | break; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /waku-core/src/common/protobuf_codec.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::marker::PhantomData; 3 | 4 | use asynchronous_codec::{Decoder, Encoder}; 5 | pub use asynchronous_codec::{FramedRead, FramedWrite}; 6 | use bytes::BytesMut; 7 | use prost::Message; 8 | use unsigned_varint::codec::UviBytes; 9 | 10 | /// [`Codec`] implements [`Encoder`] and [`Decoder`], uses [`unsigned_varint`] 11 | /// to prefix messages with their length and uses [`prost`] and a provided 12 | /// `struct` implementing [`Message`] to do the encoding. 13 | pub struct Codec { 14 | uvi: UviBytes, 15 | phantom: PhantomData<(In, Out)>, 16 | } 17 | 18 | impl Codec { 19 | /// Create new [`Codec`]. 20 | /// 21 | /// Parameter `max_message_len_bytes` determines the maximum length of the 22 | /// Protobuf message. The limit does not include the bytes needed for the 23 | /// [`unsigned_varint`]. 24 | pub fn new(max_message_len_bytes: usize) -> Self { 25 | let mut uvi = UviBytes::default(); 26 | uvi.set_max_len(max_message_len_bytes); 27 | Self { 28 | uvi, 29 | phantom: PhantomData::default(), 30 | } 31 | } 32 | } 33 | 34 | impl Encoder for Codec { 35 | type Item = In; 36 | type Error = io::Error; 37 | 38 | fn encode(&mut self, item: Self::Item, dst: &mut BytesMut) -> Result<(), Self::Error> { 39 | let mut encoded_msg = BytesMut::with_capacity(item.encoded_len()); 40 | item.encode(&mut encoded_msg) 41 | .expect("BytesMut to have sufficient capacity."); 42 | self.uvi.encode(encoded_msg.freeze(), dst) 43 | } 44 | } 45 | 46 | impl Decoder for Codec { 47 | type Item = Out; 48 | type Error = io::Error; 49 | 50 | fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { 51 | Ok(self 52 | .uvi 53 | .decode(src)? 54 | .map(|msg| Message::decode(msg)) 55 | .transpose()?) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /waku-node/examples/simple.rs: -------------------------------------------------------------------------------- 1 | use libp2p::identity::{secp256k1, Keypair}; 2 | use libp2p::Multiaddr; 3 | use log::{info, LevelFilter}; 4 | 5 | use waku_core::pubsub_topic::PubsubTopic; 6 | use waku_node::{Node, NodeConfigBuilder}; 7 | 8 | fn keypair_from_secp256k1>(private_key: S) -> anyhow::Result { 9 | let raw_key = hex::decode(private_key)?; 10 | let secret_key = secp256k1::SecretKey::try_from_bytes(raw_key)?; 11 | Ok(secp256k1::Keypair::from(secret_key).into()) 12 | } 13 | 14 | #[tokio::main] 15 | async fn main() -> anyhow::Result<()> { 16 | pretty_env_logger::formatted_builder() 17 | .filter_level(LevelFilter::Info) 18 | .format_timestamp_millis() 19 | .init(); 20 | 21 | // Init ----- 22 | let keypair = 23 | keypair_from_secp256k1("2a6ecd4041f9903e6d57fd5841bc89ee40606e78e2be4202fa7d32485b41cb8c")?; 24 | 25 | let nodes: Vec = vec![ 26 | "/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/30303/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ".parse()?, 27 | ]; 28 | 29 | let pubsub_topics: Vec = vec!["/waku/2/default-waku/proto".parse()?]; 30 | 31 | let config = NodeConfigBuilder::new() 32 | .keypair(keypair) 33 | .with_keepalive(true) 34 | .with_ping(true) 35 | .with_waku_relay(Default::default()) 36 | .build(); 37 | 38 | let mut node = Node::new(config.clone())?; 39 | 40 | // Setup ----- 41 | // Listen on a random port 42 | let addr = "/ip4/0.0.0.0/tcp/0".parse()?; 43 | node.switch_listen_on(&addr).await?; 44 | 45 | // Connect to bootstrap nodes 46 | for peer in &nodes { 47 | node.switch_dial(peer).await?; 48 | } 49 | 50 | // Subscribe to relay topics 51 | if config.relay.is_some() { 52 | for topic in pubsub_topics { 53 | node.relay_subscribe(&topic).await?; 54 | } 55 | } 56 | 57 | loop { 58 | if let Some(event) = node.recv_event().await { 59 | info!("{event:?}"); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /waku-enr/src/multiaddrs.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use multiaddr::Multiaddr; 3 | 4 | pub fn encode(multiaddrs: &[Multiaddr]) -> Vec { 5 | let mut buffer: Vec = Vec::new(); 6 | for addr in multiaddrs { 7 | let mut addr_bytes = addr.to_vec(); 8 | let length_prefix: [u8; 2] = u16::to_be_bytes(addr_bytes.len() as u16); 9 | 10 | buffer.extend_from_slice(&length_prefix); 11 | buffer.append(&mut addr_bytes); 12 | } 13 | buffer 14 | } 15 | 16 | pub fn decode(data: &[u8]) -> anyhow::Result> { 17 | let mut buffer = Vec::from(data); 18 | let mut multiaddrs: Vec = Vec::new(); 19 | 20 | while buffer.len() > 2 { 21 | let length_prefix: [u8; 2] = { 22 | let prefix_bytes = buffer.drain(..2).collect::>(); 23 | prefix_bytes.try_into().expect("2 bytes slice") 24 | }; 25 | let length = u16::from_be_bytes(length_prefix) as usize; 26 | if length > buffer.len() { 27 | return Err(anyhow!("not enough bytes")); 28 | } 29 | 30 | let addr_bytes = buffer.drain(..length).collect::>(); 31 | let addr: Multiaddr = addr_bytes.try_into()?; 32 | multiaddrs.push(addr); 33 | } 34 | 35 | Ok(multiaddrs) 36 | } 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | use multiaddr::Multiaddr; 41 | 42 | use super::{decode, encode}; 43 | 44 | #[test] 45 | fn test_multiaddrs_codec() { 46 | // Given 47 | let multiaddrs: Vec = vec![ 48 | "/dns4/example.com/tcp/443/wss".parse().unwrap(), 49 | "/dns4/quic.example.com/tcp/443/quic".parse().unwrap(), 50 | "/ip4/7.7.7.7/tcp/3003/p2p/QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N/p2p-circuit/p2p/QmUWYRp3mkQUUVeyGVjcM1fC7kbVxmDieGpGsQzopXivyk" 51 | .parse() 52 | .unwrap(), 53 | ]; 54 | 55 | // When 56 | let encoded = encode(&multiaddrs); 57 | let decoded = decode(&encoded); 58 | 59 | // Then 60 | assert!(matches!(decoded, Ok(addrs) if addrs == multiaddrs)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /waku-enr/src/enr_ext.rs: -------------------------------------------------------------------------------- 1 | use enr::{Enr, EnrBuilder, EnrKey}; 2 | use multiaddr::Multiaddr; 3 | 4 | use crate::capabilities::WakuEnrCapabilities; 5 | use crate::multiaddrs; 6 | 7 | /// The ENR field specifying the node multiaddrs. 8 | pub const WAKU2_MULTIADDR_ENR_KEY: &str = "multiaddrs"; 9 | /// The ENR field specifying the node Waku v2 capabilities. 10 | pub const WAKU2_CAPABILITIES_ENR_KEY: &str = "waku2"; 11 | 12 | /// Extension trait for Waku v2 ENRs 13 | pub trait EnrExt { 14 | /// The multiaddrs field associated with the ENR. 15 | fn multiaddrs(&self) -> Option>; 16 | 17 | /// The waku node capabilities bitfield associated with the ENR. 18 | fn waku2(&self) -> Option; 19 | } 20 | 21 | impl EnrExt for Enr { 22 | fn multiaddrs(&self) -> Option> { 23 | if let Some(multiaddrs_bytes) = self.get(WAKU2_MULTIADDR_ENR_KEY) { 24 | return multiaddrs::decode(multiaddrs_bytes).ok(); 25 | } 26 | None 27 | } 28 | 29 | fn waku2(&self) -> Option { 30 | if let Some(bitfield) = self.get(WAKU2_CAPABILITIES_ENR_KEY) { 31 | return match bitfield.len() { 32 | 1 => WakuEnrCapabilities::from_bits(bitfield[0]), 33 | _ => None, 34 | }; 35 | } 36 | None 37 | } 38 | } 39 | 40 | pub trait EnrBuilderExt { 41 | fn multiaddrs(&mut self, multiaddrs: Vec) -> &mut Self; 42 | 43 | fn waku2(&mut self, capabilities: WakuEnrCapabilities) -> &mut Self; 44 | } 45 | 46 | impl EnrBuilderExt for EnrBuilder { 47 | /// Adds a Waku `multiaddr` field to the EnrBuilder. 48 | fn multiaddrs(&mut self, addrs: Vec) -> &mut Self { 49 | let multiaddrs = multiaddrs::encode(&addrs); 50 | self.add_value(WAKU2_MULTIADDR_ENR_KEY, &multiaddrs); 51 | self 52 | } 53 | 54 | /// Adds a Waku `waku2` capabilities bitfield to the EnrBuilder. 55 | fn waku2(&mut self, cap: WakuEnrCapabilities) -> &mut Self { 56 | let cap = vec![cap.bits()]; 57 | self.add_value(WAKU2_CAPABILITIES_ENR_KEY, &cap); 58 | self 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /waku-core/src/content_topic.rs: -------------------------------------------------------------------------------- 1 | ///! Waku content topic. 2 | use std::convert::Infallible; 3 | use std::fmt; 4 | use std::str::FromStr; 5 | 6 | #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash)] 7 | pub struct ContentTopic(String); 8 | 9 | impl ContentTopic { 10 | /// Create a new `ContentTopic` from a string. 11 | pub fn new(topic: S) -> ContentTopic 12 | where 13 | S: Into, 14 | { 15 | ContentTopic(topic.into()) 16 | } 17 | 18 | /// Return the length in bytes of this `ContentTopic`. 19 | pub fn len(&self) -> usize { 20 | self.0.len() 21 | } 22 | 23 | /// Returns true if the length of this `ContentTopic`. 24 | pub fn is_empty(&self) -> bool { 25 | self.0.is_empty() 26 | } 27 | 28 | /// Convert this `ContentTopic` into a byte vector. 29 | pub fn into_bytes(self) -> Vec { 30 | self.0.into_bytes() 31 | } 32 | 33 | /// Return a byte slice of this `ContentTopic`'s content. 34 | pub fn as_bytes(&self) -> &[u8] { 35 | self.0.as_bytes() 36 | } 37 | 38 | /// Return a string slice of this `ContentTopic`'s content. 39 | pub fn as_str(&self) -> &str { 40 | &self.0 41 | } 42 | } 43 | 44 | impl fmt::Debug for ContentTopic { 45 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 46 | self.0.fmt(f) 47 | } 48 | } 49 | 50 | impl fmt::Display for ContentTopic { 51 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 52 | self.0.fmt(f) 53 | } 54 | } 55 | 56 | impl FromStr for ContentTopic { 57 | type Err = Infallible; 58 | fn from_str(s: &str) -> Result { 59 | Ok(Self(s.to_owned())) 60 | } 61 | } 62 | 63 | impl From<&str> for ContentTopic { 64 | fn from(s: &str) -> Self { 65 | Self(s.to_owned()) 66 | } 67 | } 68 | 69 | impl From for ContentTopic { 70 | fn from(s: String) -> Self { 71 | Self(s) 72 | } 73 | } 74 | 75 | impl AsRef for ContentTopic { 76 | fn as_ref(&self) -> &str { 77 | self.0.as_ref() 78 | } 79 | } 80 | 81 | impl AsRef<[u8]> for ContentTopic { 82 | fn as_ref(&self) -> &[u8] { 83 | self.0.as_ref() 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /waku-node/src/event_loop/command.rs: -------------------------------------------------------------------------------- 1 | use libp2p::Multiaddr; 2 | use strum_macros::Display; 3 | use tokio::sync::oneshot; 4 | 5 | use waku_core::message::WakuMessage; 6 | use waku_core::pubsub_topic::PubsubTopic; 7 | 8 | #[derive(Debug, Display)] 9 | pub enum Command { 10 | SwitchListenOn { 11 | address: Multiaddr, 12 | sender: oneshot::Sender>, 13 | }, 14 | SwitchDial { 15 | address: Multiaddr, 16 | sender: oneshot::Sender>, 17 | }, 18 | RelaySubscribe { 19 | pubsub_topic: PubsubTopic, 20 | sender: oneshot::Sender>, 21 | }, 22 | RelayUnsubscribe { 23 | pubsub_topic: PubsubTopic, 24 | sender: oneshot::Sender>, 25 | }, 26 | RelayPublish { 27 | pubsub_topic: PubsubTopic, 28 | message: WakuMessage, 29 | sender: oneshot::Sender>, 30 | }, 31 | } 32 | 33 | impl Command { 34 | pub fn switch_listen_on( 35 | address: Multiaddr, 36 | sender: oneshot::Sender>, 37 | ) -> Self { 38 | Command::SwitchListenOn { address, sender } 39 | } 40 | 41 | pub fn switch_dial(address: Multiaddr, sender: oneshot::Sender>) -> Self { 42 | Command::SwitchDial { address, sender } 43 | } 44 | 45 | pub fn relay_subscribe( 46 | topic: PubsubTopic, 47 | sender: oneshot::Sender>, 48 | ) -> Self { 49 | Command::RelaySubscribe { 50 | pubsub_topic: topic, 51 | sender, 52 | } 53 | } 54 | pub fn relay_unsubscribe( 55 | topic: PubsubTopic, 56 | sender: oneshot::Sender>, 57 | ) -> Self { 58 | Command::RelayUnsubscribe { 59 | pubsub_topic: topic, 60 | sender, 61 | } 62 | } 63 | pub fn relay_publish( 64 | topic: PubsubTopic, 65 | message: WakuMessage, 66 | sender: oneshot::Sender>, 67 | ) -> Self { 68 | Command::RelayPublish { 69 | pubsub_topic: topic, 70 | message, 71 | sender, 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /waku-relay/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types that can result from Waku relay. 2 | 3 | use crate::error::PublishError::{Duplicate, GossipsubError, InsufficientPeers, MessageTooLarge}; 4 | use crate::error::SubscriptionError::NotAllowed; 5 | use crate::gossipsub; 6 | 7 | /// Error associated with publishing a Waku message. 8 | #[derive(Debug, thiserror::Error)] 9 | pub enum PublishError { 10 | /// This message has already been published. 11 | #[error("duplicate message")] 12 | Duplicate, 13 | /// There were no peers to send this message to. 14 | #[error("insufficient peers")] 15 | InsufficientPeers, 16 | /// The overall message was too large. This could be due to excessive topics or an excessive 17 | /// message size. 18 | #[error("message too large")] 19 | MessageTooLarge, 20 | /// Unknown Waku relay publish error. 21 | #[error("unknown gossipsub publish error")] 22 | GossipsubError(gossipsub::PublishError), 23 | } 24 | 25 | impl From for PublishError { 26 | fn from(err: gossipsub::PublishError) -> Self { 27 | match err { 28 | gossipsub::PublishError::Duplicate => Duplicate, 29 | gossipsub::PublishError::InsufficientPeers => InsufficientPeers, 30 | gossipsub::PublishError::MessageTooLarge => MessageTooLarge, 31 | _ => GossipsubError(err), 32 | } 33 | } 34 | } 35 | 36 | /// Error associated with subscribing to a topic. 37 | #[derive(Debug, thiserror::Error)] 38 | pub enum SubscriptionError { 39 | /// Couldn't publish our subscription. 40 | #[error("subscription publication failed")] 41 | PublishError(PublishError), 42 | /// We are not allowed to subscribe to this topic by the subscription filter. 43 | #[error("subscription not allowed")] 44 | NotAllowed, 45 | } 46 | 47 | impl From for SubscriptionError { 48 | fn from(err: gossipsub::SubscriptionError) -> Self { 49 | match err { 50 | gossipsub::SubscriptionError::PublishError(e) => { 51 | SubscriptionError::PublishError(e.into()) 52 | } 53 | gossipsub::SubscriptionError::NotAllowed => NotAllowed, 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /waku-relay/src/event.rs: -------------------------------------------------------------------------------- 1 | use libp2p::identity::PeerId; 2 | use prost::Message; 3 | use strum_macros::Display; 4 | 5 | use waku_core::message::MAX_WAKU_MESSAGE_SIZE; 6 | use waku_core::message::proto::waku::message::v1::WakuMessage as WakuMessageProto; 7 | use waku_core::message::WakuMessage; 8 | use waku_core::pubsub_topic::PubsubTopic; 9 | 10 | use crate::gossipsub; 11 | 12 | #[derive(Debug, Display)] 13 | pub enum Event { 14 | InvalidMessage, 15 | Subscribed { 16 | peer_id: PeerId, 17 | pubsub_topic: PubsubTopic, 18 | }, 19 | Unsubscribed { 20 | peer_id: PeerId, 21 | pubsub_topic: PubsubTopic, 22 | }, 23 | Message { 24 | pubsub_topic: PubsubTopic, 25 | message: WakuMessage, 26 | }, 27 | WakuRelayNotSupported { 28 | peer_id: PeerId, 29 | }, 30 | } 31 | 32 | impl From for Event { 33 | fn from(event: gossipsub::Event) -> Self { 34 | match event { 35 | gossipsub::Event::Subscribed { peer_id, topic } => Self::Subscribed { 36 | peer_id, 37 | pubsub_topic: PubsubTopic::new(topic.into_string()), 38 | }, 39 | gossipsub::Event::Unsubscribed { peer_id, topic } => Self::Unsubscribed { 40 | peer_id, 41 | pubsub_topic: PubsubTopic::new(topic.into_string()), 42 | }, 43 | gossipsub::Event::Message { message, .. } => { 44 | if message.data.len() > MAX_WAKU_MESSAGE_SIZE { 45 | return Self::InvalidMessage; 46 | } 47 | 48 | let waku_message = if let Ok(msg) = WakuMessageProto::decode(&message.data[..]) { 49 | msg.into() 50 | } else { 51 | return Self::InvalidMessage; 52 | }; 53 | 54 | Self::Message { 55 | pubsub_topic: PubsubTopic::new(message.topic.to_string()), 56 | message: waku_message, 57 | } 58 | } 59 | gossipsub::Event::GossipsubNotSupported { peer_id } => { 60 | Self::WakuRelayNotSupported { peer_id } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /waku-relay/src/behaviour.rs: -------------------------------------------------------------------------------- 1 | use libp2p::identity::PeerId; 2 | use libp2p::swarm::NetworkBehaviour; 3 | use prost::Message; 4 | 5 | use waku_core::message::proto::waku::message::v1::WakuMessage as WakuMessageProto; 6 | use waku_core::message::WakuMessage; 7 | use waku_core::pubsub_topic::PubsubTopic; 8 | 9 | use crate::error::{PublishError, SubscriptionError}; 10 | use crate::event::Event; 11 | use crate::gossipsub::{self, IdentTopic, MessageAuthenticity, MessageId, ValidationMode}; 12 | use crate::message_id::deterministic_message_id_fn; 13 | use crate::proto::MAX_WAKU_RELAY_MESSAGE_SIZE; 14 | 15 | pub const PROTOCOL_ID: &str = "/vac/waku/relay/2.0.0"; 16 | 17 | #[derive(NetworkBehaviour)] 18 | #[behaviour(out_event = "Event")] 19 | pub struct Behaviour { 20 | pubsub: gossipsub::Behaviour, 21 | } 22 | 23 | impl Default for Behaviour { 24 | fn default() -> Self { 25 | let pubsub_config = gossipsub::ConfigBuilder::default() 26 | .protocol_id(PROTOCOL_ID, gossipsub::Version::V1_1) 27 | .validation_mode(ValidationMode::Anonymous) // StrictNoSign 28 | .message_id_fn(deterministic_message_id_fn) 29 | .max_transmit_size(MAX_WAKU_RELAY_MESSAGE_SIZE) 30 | .build() 31 | .expect("valid pubsub configuration"); 32 | 33 | let pubsub = gossipsub::Behaviour::new(MessageAuthenticity::Anonymous, pubsub_config) 34 | .expect("valid pubsub configuration"); 35 | 36 | Self { pubsub } 37 | } 38 | } 39 | 40 | impl Behaviour { 41 | pub fn subscribe(&mut self, topic: &PubsubTopic) -> Result { 42 | let ident_topic = IdentTopic::new(topic.to_string()); 43 | self.pubsub.subscribe(&ident_topic).map_err(Into::into) 44 | } 45 | 46 | pub fn unsubscribe(&mut self, topic: &PubsubTopic) -> Result { 47 | let ident_topic = IdentTopic::new(topic.to_string()); 48 | self.pubsub.unsubscribe(&ident_topic).map_err(Into::into) 49 | } 50 | 51 | pub fn publish( 52 | &mut self, 53 | topic: &PubsubTopic, 54 | msg: WakuMessage, 55 | ) -> Result { 56 | let ident_topic = IdentTopic::new(topic.to_string()); 57 | let message_proto: WakuMessageProto = msg.into(); 58 | self.pubsub 59 | .publish(ident_topic, message_proto.encode_to_vec()) 60 | .map_err(Into::into) 61 | } 62 | 63 | pub fn add_peer(&mut self, peer_id: &PeerId) { 64 | self.pubsub.add_explicit_peer(peer_id); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /waku-core/src/common/quick_protobuf_codec.rs: -------------------------------------------------------------------------------- 1 | // Code borrowed from: https://github.com/libp2p/rust-libp2p/blob/master/misc/quick-protobuf-codec/ 2 | 3 | #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] 4 | 5 | use asynchronous_codec::{Decoder, Encoder}; 6 | use bytes::{Bytes, BytesMut}; 7 | use quick_protobuf::{BytesReader, MessageRead, MessageWrite, Writer}; 8 | use std::marker::PhantomData; 9 | use unsigned_varint::codec::UviBytes; 10 | 11 | /// [`Codec`] implements [`Encoder`] and [`Decoder`], uses [`unsigned_varint`] 12 | /// to prefix messages with their length and uses [`quick_protobuf`] and a provided 13 | /// `struct` implementing [`MessageRead`] and [`MessageWrite`] to do the encoding. 14 | pub struct Codec { 15 | uvi: UviBytes, 16 | phantom: PhantomData<(In, Out)>, 17 | } 18 | 19 | impl Codec { 20 | /// Create new [`Codec`]. 21 | /// 22 | /// Parameter `max_message_len_bytes` determines the maximum length of the 23 | /// Protobuf message. The limit does not include the bytes needed for the 24 | /// [`unsigned_varint`]. 25 | pub fn new(max_message_len_bytes: usize) -> Self { 26 | let mut uvi = UviBytes::default(); 27 | uvi.set_max_len(max_message_len_bytes); 28 | Self { 29 | uvi, 30 | phantom: PhantomData::default(), 31 | } 32 | } 33 | } 34 | 35 | impl Encoder for Codec { 36 | type Item = In; 37 | type Error = Error; 38 | 39 | fn encode(&mut self, item: Self::Item, dst: &mut BytesMut) -> Result<(), Self::Error> { 40 | let mut encoded_msg = Vec::new(); 41 | let mut writer = Writer::new(&mut encoded_msg); 42 | item.write_message(&mut writer) 43 | .expect("Encoding to succeed"); 44 | self.uvi.encode(Bytes::from(encoded_msg), dst)?; 45 | 46 | Ok(()) 47 | } 48 | } 49 | 50 | impl Decoder for Codec 51 | where 52 | Out: for<'a> MessageRead<'a>, 53 | { 54 | type Item = Out; 55 | type Error = Error; 56 | 57 | fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { 58 | let msg = match self.uvi.decode(src)? { 59 | None => return Ok(None), 60 | Some(msg) => msg, 61 | }; 62 | 63 | let mut reader = BytesReader::from_bytes(&msg); 64 | let message = Self::Item::from_reader(&mut reader, &msg) 65 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; 66 | Ok(Some(message)) 67 | } 68 | } 69 | 70 | #[derive(thiserror::Error, Debug)] 71 | #[error("Failed to encode/decode message")] 72 | pub struct Error(#[from] std::io::Error); 73 | 74 | impl From for std::io::Error { 75 | fn from(e: Error) -> Self { 76 | e.0 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/message_id.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use libp2p::identity::PeerId; 3 | #[cfg(feature = "serde")] 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::gossipsub::Message; 7 | 8 | /// Macro for declaring message id types 9 | macro_rules! declare_message_id_type { 10 | ($name: ident, $name_string: expr) => { 11 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 12 | #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 13 | pub struct $name(Vec); 14 | 15 | impl $name { 16 | pub fn new>>(value: T) -> Self { 17 | Self(value.into()) 18 | } 19 | 20 | pub fn new_from_slice(value: &[u8]) -> Self { 21 | Self(value.to_vec()) 22 | } 23 | } 24 | 25 | impl From> for $name { 26 | fn from(value: Vec) -> Self { 27 | Self(value) 28 | } 29 | } 30 | 31 | impl From for $name { 32 | fn from(value: Bytes) -> Self { 33 | Self(value.to_vec()) 34 | } 35 | } 36 | 37 | impl Into for $name { 38 | fn into(self) -> Bytes { 39 | Bytes::from(self.0) 40 | } 41 | } 42 | 43 | impl std::fmt::Display for $name { 44 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 45 | write!(f, "{}", hex_fmt::HexFmt(&self.0)) 46 | } 47 | } 48 | 49 | impl std::fmt::Debug for $name { 50 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 51 | write!(f, "{}({})", $name_string, hex_fmt::HexFmt(&self.0)) 52 | } 53 | } 54 | }; 55 | } 56 | 57 | // A type for gossipsub message ids. 58 | declare_message_id_type!(MessageId, "MessageId"); 59 | 60 | // A type for gossipsub fast message ids, not to confuse with "real" message ids. 61 | // 62 | // A fast-message-id is an optional message_id that can be used to filter duplicates quickly. On 63 | // high intensive networks with lots of messages, where the message_id is based on the result of 64 | // decompressed traffic, it is beneficial to specify a `fast-message-id` that can identify and 65 | // filter duplicates quickly without performing the overhead of decompression. 66 | declare_message_id_type!(FastMessageId, "FastMessageId"); 67 | 68 | pub(crate) fn default_message_id_fn(msg: &Message) -> MessageId { 69 | // default message id is: source + sequence number 70 | // NOTE: If either the peer_id or source is not provided, we set to 0; 71 | let mut source_string = if let Some(peer_id) = msg.source.as_ref() { 72 | peer_id.to_base58() 73 | } else { 74 | PeerId::from_bytes(&[0, 1, 0]) 75 | .expect("Valid peer id") 76 | .to_base58() 77 | }; 78 | source_string.push_str(&msg.sequence_number.unwrap_or_default().to_string()); 79 | MessageId::new(source_string.into_bytes()) 80 | } 81 | -------------------------------------------------------------------------------- /waku-node/tests/it_waku_relay.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use bytes::Bytes; 4 | use libp2p::identity::secp256k1; 5 | use libp2p::Multiaddr; 6 | use tokio::time::sleep; 7 | 8 | use waku_core::message::WakuMessage; 9 | use waku_core::pubsub_topic::PubsubTopic; 10 | use waku_node::{memory_transport, Event, Node, NodeConfigBuilder}; 11 | 12 | fn new_node(key: &str) -> Node { 13 | let keypair = { 14 | let raw_key = hex::decode(key).expect("key to be valid"); 15 | let secret_key = secp256k1::SecretKey::try_from_bytes(raw_key).unwrap(); 16 | secp256k1::Keypair::from(secret_key).into() 17 | }; 18 | 19 | let config = NodeConfigBuilder::new() 20 | .keypair(keypair) 21 | .with_keepalive(true) 22 | .with_waku_relay(Default::default()) 23 | .build(); 24 | 25 | let transport = memory_transport(&config.keypair).expect("create the transport"); 26 | 27 | Node::new_with_transport(config, transport).expect("node creation to succeed") 28 | } 29 | 30 | #[tokio::test] 31 | async fn it_publish_and_subscribe() { 32 | //// Setup 33 | let publisher_key = "dc404f7ed2d3cdb65b536e8d561255c84658e83775ee790ff46bf4d77690b0fe"; 34 | let publisher_addr: Multiaddr = "/memory/23".parse().unwrap(); 35 | let publisher = new_node(publisher_key); 36 | publisher 37 | .switch_listen_on(&publisher_addr) 38 | .await 39 | .expect("listen on address"); 40 | 41 | let subscriber_key = "9c0cd57a01ee12338915b42bf6232a386e467dcdbe172facd94e4623ffc9096c"; 42 | let subscriber_addr: Multiaddr = "/memory/32".parse().unwrap(); 43 | let mut subscriber = new_node(subscriber_key); 44 | subscriber 45 | .switch_listen_on(&subscriber_addr) 46 | .await 47 | .expect("listen on address"); 48 | 49 | // Dial the publisher node 50 | subscriber 51 | .switch_dial(&publisher_addr) 52 | .await 53 | .expect("dial to succeed"); 54 | 55 | // Subscribe to node 56 | let pubsub_topic: PubsubTopic = "/waku/2/it-waku/test".parse().unwrap(); 57 | publisher 58 | .relay_subscribe(&pubsub_topic) 59 | .await 60 | .expect("subscribe to topic"); 61 | subscriber 62 | .relay_subscribe(&pubsub_topic) 63 | .await 64 | .expect("subscribe to topic"); 65 | 66 | // Wait for pub-sub network to establish 67 | sleep(Duration::from_millis(100)).await; 68 | 69 | //// Given 70 | let message = WakuMessage { 71 | payload: Bytes::from_static(b"TEST"), 72 | content_topic: "/test/v1/it/text".parse().unwrap(), 73 | meta: None, 74 | ephemeral: false, 75 | }; 76 | 77 | //// When 78 | publisher 79 | .relay_publish(&pubsub_topic, message.clone()) 80 | .await 81 | .expect("publish the message"); 82 | let event = subscriber.recv_event().await; 83 | 84 | //// Then 85 | assert!(matches!(event, Some(Event::WakuRelayMessage { .. }))); 86 | if let Some(Event::WakuRelayMessage { 87 | pubsub_topic: topic, 88 | message: msg, 89 | }) = event 90 | { 91 | assert_eq!(topic, pubsub_topic); 92 | assert_eq!(msg, message); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/transform.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Sigma Prime Pty Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | //! This trait allows of extended user-level decoding that can apply to message-data before a 22 | //! message-id is calculated. 23 | //! 24 | //! This is primarily designed to allow applications to implement their own custom compression 25 | //! algorithms that can be topic-specific. Once the raw data is transformed the message-id is then 26 | //! calculated, allowing for applications to employ message-id functions post compression. 27 | 28 | use std::io; 29 | 30 | use crate::gossipsub::{Message, RawMessage, TopicHash}; 31 | 32 | /// A general trait of transforming a [`RawMessage`] into a [`Message`]. The 33 | /// [`RawMessage`] is obtained from the wire and the [`Message`] is used to 34 | /// calculate the [`crate::MessageId`] of the message and is what is sent to the application. 35 | /// 36 | /// The inbound/outbound transforms must be inverses. Applying the inbound transform and then the 37 | /// outbound transform MUST leave the underlying data un-modified. 38 | /// 39 | /// By default, this is the identity transform for all fields in [`Message`]. 40 | pub trait DataTransform { 41 | /// Takes a [`RawMessage`] received and converts it to a [`Message`]. 42 | fn inbound_transform(&self, raw_message: RawMessage) -> Result; 43 | 44 | /// Takes the data to be published (a topic and associated data) transforms the data. The 45 | /// transformed data will then be used to create a [`crate::RawMessage`] to be sent to peers. 46 | fn outbound_transform(&self, topic: &TopicHash, data: Vec) -> Result, io::Error>; 47 | } 48 | 49 | /// The default transform, the raw data is propagated as is to the application layer gossipsub. 50 | #[derive(Default, Clone)] 51 | pub struct IdentityTransform; 52 | 53 | impl DataTransform for IdentityTransform { 54 | fn inbound_transform(&self, raw_message: RawMessage) -> Result { 55 | Ok(Message { 56 | source: raw_message.source, 57 | data: raw_message.data, 58 | sequence_number: raw_message.sequence_number, 59 | topic: raw_message.topic, 60 | }) 61 | } 62 | 63 | fn outbound_transform(&self, _: &TopicHash, data: Vec) -> Result, io::Error> { 64 | Ok(data) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /waku-core/src/pubsub_topic/topic.rs: -------------------------------------------------------------------------------- 1 | ///! Waku pubsub topic. 2 | use std::convert::Infallible; 3 | use std::fmt; 4 | use std::str::FromStr; 5 | 6 | #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash)] 7 | pub struct PubsubTopic(String); 8 | 9 | impl PubsubTopic { 10 | /// Creates a new PubsubTopic from a string. 11 | pub fn new(topic: S) -> PubsubTopic 12 | where 13 | S: Into, 14 | { 15 | PubsubTopic(topic.into()) 16 | } 17 | 18 | /// Return the length in bytes of this topic. 19 | pub fn len(&self) -> usize { 20 | self.0.len() 21 | } 22 | 23 | /// Returns true if the length of this topic. 24 | pub fn is_empty(&self) -> bool { 25 | self.0.is_empty() 26 | } 27 | 28 | /// Return a copy of this topic's byte representation. 29 | pub fn into_bytes(self) -> Vec { 30 | self.0.into_bytes() 31 | } 32 | 33 | /// Return a reference to this topic's byte representation. 34 | pub fn as_bytes(&self) -> &[u8] { 35 | self.0.as_bytes() 36 | } 37 | 38 | /// Extracts a string slice containing the entire topic. 39 | pub fn as_str(&self) -> &str { 40 | &self.0 41 | } 42 | } 43 | 44 | impl fmt::Debug for PubsubTopic { 45 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 46 | self.0.fmt(f) 47 | } 48 | } 49 | 50 | impl fmt::Display for PubsubTopic { 51 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 52 | self.0.fmt(f) 53 | } 54 | } 55 | 56 | impl FromStr for PubsubTopic { 57 | type Err = Infallible; 58 | fn from_str(s: &str) -> Result { 59 | Ok(Self(String::from(s))) 60 | } 61 | } 62 | 63 | impl From<&str> for PubsubTopic { 64 | fn from(s: &str) -> Self { 65 | Self(s.to_owned()) 66 | } 67 | } 68 | 69 | impl From for PubsubTopic { 70 | fn from(s: String) -> Self { 71 | Self(s) 72 | } 73 | } 74 | 75 | impl AsRef for PubsubTopic { 76 | fn as_ref(&self) -> &str { 77 | self.0.as_ref() 78 | } 79 | } 80 | 81 | impl AsRef<[u8]> for PubsubTopic { 82 | fn as_ref(&self) -> &[u8] { 83 | self.0.as_ref() 84 | } 85 | } 86 | 87 | #[cfg(test)] 88 | mod tests { 89 | use super::*; 90 | 91 | #[test] 92 | fn test_pubsub_topic_new_from_str() { 93 | // Given 94 | let topic_str = "test"; 95 | 96 | // When 97 | let topic = PubsubTopic::new(topic_str); 98 | 99 | // Then 100 | assert_eq!(topic.to_string(), "test"); 101 | assert_eq!(topic.len(), 4); 102 | assert!(!topic.is_empty()); 103 | assert_eq!(topic.into_bytes(), vec![116, 101, 115, 116]); 104 | } 105 | 106 | #[test] 107 | fn test_pubsub_topic_from_str() { 108 | // Given 109 | let topic_str = "test"; 110 | 111 | // When 112 | let topic = PubsubTopic::from_str(topic_str).unwrap(); 113 | 114 | // Then 115 | assert_eq!(topic.to_string(), "test"); 116 | assert_eq!(topic.len(), 4); 117 | assert!(!topic.is_empty()); 118 | assert_eq!(topic.into_bytes(), vec![116, 101, 115, 116]); 119 | } 120 | 121 | #[test] 122 | fn test_pubsub_topic_from_string() { 123 | // Given 124 | let topic_str = "test".to_string(); 125 | 126 | // When 127 | let topic = PubsubTopic::from(topic_str); 128 | 129 | // Then 130 | assert_eq!(topic.to_string(), "test"); 131 | assert_eq!(topic.len(), 4); 132 | assert!(!topic.is_empty()); 133 | assert_eq!(topic.into_bytes(), vec![116, 101, 115, 116]); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /apps/wakunode2/src/app.rs: -------------------------------------------------------------------------------- 1 | use libp2p::identity::{secp256k1, Keypair}; 2 | use log::info; 3 | use multiaddr::Multiaddr; 4 | 5 | use waku_core::pubsub_topic::PubsubTopic; 6 | use waku_node::{Event, Node, NodeConfig, NodeConfigBuilder}; 7 | 8 | use crate::config::Wakunode2Conf; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct AppConf { 12 | pub node_conf: NodeConfig, 13 | pub listen_addresses: Vec, 14 | pub bootstrap_nodes: Vec, 15 | pub topics: Vec, 16 | } 17 | 18 | fn try_into_multiaddr(addr: &[String]) -> anyhow::Result> { 19 | addr.iter() 20 | .map(|addr| addr.parse::()) 21 | .collect::, _>>() 22 | .map_err(|e| anyhow::anyhow!("Failed to parse multiaddr: {}", e)) 23 | } 24 | 25 | fn to_pubsub_topic(topic: &[String]) -> anyhow::Result> { 26 | topic 27 | .iter() 28 | .map(|topic| topic.parse::()) 29 | .collect::, _>>() 30 | .map_err(|e| anyhow::anyhow!("Failed to parse topic: {}", e)) 31 | } 32 | 33 | fn keypair_from_secp256k1(private_key: String) -> anyhow::Result { 34 | let raw_key = hex::decode(private_key)?; 35 | let keypair = { 36 | let secret_key = secp256k1::SecretKey::try_from_bytes(raw_key)?; 37 | secp256k1::Keypair::from(secret_key).into() 38 | }; 39 | 40 | Ok(keypair) 41 | } 42 | 43 | impl TryFrom for NodeConfig { 44 | type Error = anyhow::Error; 45 | 46 | fn try_from(c: Wakunode2Conf) -> Result { 47 | let mut builder = NodeConfigBuilder::new() 48 | .keypair(keypair_from_secp256k1(c.private_key)?) 49 | .with_keepalive(c.keepalive); 50 | 51 | if c.relay { 52 | builder = builder.with_waku_relay(Default::default()); 53 | } 54 | 55 | Ok(builder.build()) 56 | } 57 | } 58 | 59 | impl TryFrom for AppConf { 60 | type Error = anyhow::Error; 61 | 62 | fn try_from(c: Wakunode2Conf) -> Result { 63 | let listen_addresses = try_into_multiaddr(&c.listen_addresses)?; 64 | let bootstrap_nodes = try_into_multiaddr(&c.bootstrap_nodes)?; 65 | let topics = to_pubsub_topic(&c.topics)?; 66 | 67 | let node_conf = c.try_into()?; 68 | Ok(Self { 69 | node_conf, 70 | listen_addresses, 71 | bootstrap_nodes, 72 | topics, 73 | }) 74 | } 75 | } 76 | 77 | pub struct App { 78 | conf: AppConf, 79 | node: Node, 80 | } 81 | 82 | impl App { 83 | pub fn new(conf: Wakunode2Conf) -> anyhow::Result { 84 | let app_conf: AppConf = conf.try_into()?; 85 | 86 | Ok(Self { 87 | conf: app_conf.clone(), 88 | node: Node::new(app_conf.node_conf)?, 89 | }) 90 | } 91 | 92 | pub async fn setup(&mut self) -> anyhow::Result<()> { 93 | for addr in &self.conf.listen_addresses { 94 | self.node.switch_listen_on(addr).await?; 95 | info!("Listening on {addr}"); 96 | } 97 | 98 | for peer in &self.conf.bootstrap_nodes { 99 | info!("Bootstrapping to {}", peer); 100 | self.node.switch_dial(peer).await?; 101 | } 102 | 103 | if self.conf.node_conf.relay.is_some() { 104 | for topic in &self.conf.topics { 105 | info!("Subscribing to {topic}"); 106 | self.node.relay_subscribe(topic).await?; 107 | } 108 | } 109 | 110 | info!("Node is ready: {}", self.node.peer_id()); 111 | Ok(()) 112 | } 113 | 114 | pub async fn run(&mut self) -> Option { 115 | self.node.recv_event().await 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /waku-node/src/node.rs: -------------------------------------------------------------------------------- 1 | use libp2p::swarm::SwarmBuilder; 2 | use libp2p::{Multiaddr, PeerId}; 3 | use log::debug; 4 | use tokio::sync::{mpsc, oneshot}; 5 | 6 | use waku_core::message::WakuMessage; 7 | use waku_core::pubsub_topic::PubsubTopic; 8 | 9 | use crate::behaviour::Behaviour; 10 | use crate::behaviour::Config as BehaviourConfig; 11 | use crate::event_loop::{Command, Event, EventLoop}; 12 | use crate::transport::{default_transport, BoxedP2PTransport}; 13 | use crate::NodeConfig; 14 | 15 | pub struct Node { 16 | peer_id: PeerId, 17 | command_sender: mpsc::Sender, 18 | event_receiver: mpsc::Receiver, 19 | } 20 | 21 | impl Node { 22 | pub fn new_with_transport( 23 | config: NodeConfig, 24 | transport: BoxedP2PTransport, 25 | ) -> anyhow::Result { 26 | let peer_id = PeerId::from(&config.keypair.public()); 27 | 28 | let switch = { 29 | let behaviour = Behaviour::new(BehaviourConfig { 30 | local_public_key: config.keypair.public(), 31 | keep_alive: config.keepalive.then_some(config.keepalive), 32 | ping: config.ping.then_some(config.ping), 33 | relay: config.relay, 34 | }); 35 | SwarmBuilder::with_tokio_executor(transport, behaviour, peer_id).build() 36 | }; 37 | 38 | let (command_sender, command_receiver) = mpsc::channel(32); 39 | let (event_sender, event_receiver) = mpsc::channel(32); 40 | let ev_loop = EventLoop::new(switch, command_receiver, event_sender); 41 | 42 | debug!("start node event loop"); 43 | tokio::spawn(ev_loop.dispatch()); 44 | 45 | Ok(Self { 46 | peer_id, 47 | command_sender, 48 | event_receiver, 49 | }) 50 | } 51 | 52 | pub fn new(config: NodeConfig) -> anyhow::Result { 53 | let transport = default_transport(&config.keypair)?; 54 | Self::new_with_transport(config, transport) 55 | } 56 | 57 | pub fn peer_id(&self) -> PeerId { 58 | self.peer_id 59 | } 60 | 61 | pub async fn recv_event(&mut self) -> Option { 62 | self.event_receiver.recv().await 63 | } 64 | 65 | pub async fn switch_listen_on(&self, address: &Multiaddr) -> anyhow::Result<()> { 66 | let (resp_tx, resp_rx) = oneshot::channel(); 67 | self.command_sender 68 | .send(Command::switch_listen_on(address.clone(), resp_tx)) 69 | .await?; 70 | 71 | resp_rx.await? 72 | } 73 | 74 | pub async fn switch_dial(&self, address: &Multiaddr) -> anyhow::Result<()> { 75 | let (resp_tx, resp_rx) = oneshot::channel(); 76 | self.command_sender 77 | .send(Command::switch_dial(address.clone(), resp_tx)) 78 | .await?; 79 | 80 | resp_rx.await? 81 | } 82 | 83 | pub async fn relay_subscribe(&self, topic: &PubsubTopic) -> anyhow::Result<()> { 84 | let (resp_tx, resp_rx) = oneshot::channel(); 85 | self.command_sender 86 | .send(Command::relay_subscribe(topic.clone(), resp_tx)) 87 | .await?; 88 | 89 | resp_rx.await? 90 | } 91 | 92 | pub async fn relay_unsubscribe(&self, topic: &PubsubTopic) -> anyhow::Result<()> { 93 | let (resp_tx, resp_rx) = oneshot::channel(); 94 | self.command_sender 95 | .send(Command::relay_unsubscribe(topic.clone(), resp_tx)) 96 | .await?; 97 | 98 | resp_rx.await? 99 | } 100 | 101 | pub async fn relay_publish( 102 | &self, 103 | topic: &PubsubTopic, 104 | message: WakuMessage, 105 | ) -> anyhow::Result<()> { 106 | let (resp_tx, resp_rx) = oneshot::channel(); 107 | self.command_sender 108 | .send(Command::relay_publish(topic.clone(), message, resp_tx)) 109 | .await?; 110 | 111 | resp_rx.await? 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/connection_manager.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | 3 | use libp2p::identity::PeerId; 4 | use libp2p::swarm::ConnectionId; 5 | 6 | use crate::gossipsub::types::PeerKind; 7 | 8 | #[derive(Debug, Clone, PartialEq, Eq)] 9 | pub(crate) struct PeerConnections { 10 | /// The kind of protocol the peer supports. 11 | pub(crate) kind: PeerKind, 12 | /// Its current connections. 13 | pub(crate) connections: Vec, 14 | } 15 | 16 | #[derive(Debug, Default)] 17 | pub(crate) struct ConnectionManager { 18 | /// A set of connected peers, indexed by their [`PeerId`] tracking both the [`PeerKind`] and 19 | /// the set of [`ConnectionId`]s. 20 | peers: HashMap, 21 | 22 | /// Set of connected outbound peers (we only consider true outbound peers found through 23 | /// discovery and not by peer exchange). 24 | outbound_peers: HashSet, 25 | } 26 | 27 | impl ConnectionManager { 28 | pub(crate) fn new() -> Self { 29 | Default::default() 30 | } 31 | 32 | pub(crate) fn peers(&self) -> impl Iterator { 33 | self.peers.keys() 34 | } 35 | 36 | pub(crate) fn floodsub_peers(&self) -> impl Iterator { 37 | self.peers 38 | .iter() 39 | .filter(|(_, peer)| peer.kind.is_floodsub()) 40 | .map(|(peer_id, _)| peer_id) 41 | } 42 | 43 | pub(crate) fn gossipsub_peers(&self) -> impl Iterator { 44 | self.peers 45 | .iter() 46 | .filter(|(_, peer)| peer.kind.is_gossipsub()) 47 | .map(|(peer_id, _)| peer_id) 48 | } 49 | 50 | pub(crate) fn is_outbound(&self, peer_id: &PeerId) -> bool { 51 | self.outbound_peers.contains(peer_id) 52 | } 53 | 54 | /// List all known peers and their associated protocol. 55 | pub(crate) fn peer_protocol(&self) -> impl Iterator { 56 | self.peers 57 | .iter() 58 | .map(|(peer_id, peer)| (peer_id, &peer.kind)) 59 | } 60 | 61 | pub(crate) fn kind(&self, peer_id: &PeerId) -> Option { 62 | self.peers.get(peer_id).map(|peer| peer.kind) 63 | } 64 | 65 | pub(crate) fn connections(&self, peer_id: &PeerId) -> impl Iterator { 66 | self.peers 67 | .get(peer_id) 68 | .map(|peer| peer.connections.iter()) 69 | .unwrap_or_default() 70 | } 71 | 72 | pub(crate) fn track_connection( 73 | &mut self, 74 | peer_id: PeerId, 75 | connection_id: ConnectionId, 76 | kind: PeerKind, 77 | outbound: bool, 78 | ) { 79 | self.peers 80 | .entry(peer_id) 81 | .or_insert_with(|| PeerConnections { 82 | kind, 83 | connections: Vec::new(), 84 | }) 85 | .connections 86 | .push(connection_id); 87 | 88 | if outbound { 89 | self.outbound_peers.insert(peer_id); 90 | } 91 | } 92 | 93 | pub(crate) fn remove_connection(&mut self, peer_id: &PeerId, connection_id: ConnectionId) { 94 | if let Some(peer) = self.peers.get_mut(peer_id) { 95 | peer.connections.retain(|c| *c != connection_id); 96 | if peer.connections.is_empty() { 97 | self.peers.remove(peer_id); 98 | } 99 | } 100 | } 101 | 102 | pub(crate) fn set_kind(&mut self, peer_id: &PeerId, kind: PeerKind) { 103 | if let Some(peer) = self.peers.get_mut(peer_id) { 104 | peer.kind = kind; 105 | } 106 | } 107 | 108 | pub(crate) fn remove_peer(&mut self, peer_id: &PeerId) { 109 | self.peers.remove(peer_id); 110 | self.outbound_peers.remove(peer_id); 111 | } 112 | 113 | pub(crate) fn len(&self) -> usize { 114 | self.peers.len() 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /waku-enr/tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | use std::net::{Ipv4Addr, Ipv6Addr}; 2 | 3 | use base64::{engine::general_purpose::STANDARD, Engine as _}; 4 | use multiaddr::Multiaddr; 5 | 6 | use waku_enr::enr::{CombinedKey, Enr, EnrBuilder, EnrKey}; 7 | use waku_enr::{EnrBuilderExt, EnrExt, WakuEnrCapabilities}; 8 | 9 | ///! https://rfc.vac.dev/spec/31/#many-connection-types 10 | #[test] 11 | fn test_build_waku_enr() { 12 | // Given 13 | let tcp: u16 = 10101; 14 | let udp: u16 = 20202; 15 | let tcp6: u16 = 30303; 16 | let udp6: u16 = 40404; 17 | let ip: Ipv4Addr = "1.2.3.4".parse().unwrap(); 18 | let ip6: Ipv6Addr = "1234:5600:101:1::142".parse().unwrap(); 19 | let capabilities: WakuEnrCapabilities = WakuEnrCapabilities::STORE | WakuEnrCapabilities::RELAY; 20 | let multiaddrs: Vec = vec![ 21 | "/dns4/example.com/tcp/443/wss".parse().unwrap(), 22 | "/dns4/quic.example.com/tcp/443/quic".parse().unwrap(), 23 | ]; 24 | 25 | // Signing key 26 | let key_secp256k1_base64 = "MaZivCR1kZsI2/1MuSw9mhnLQYqETWwjfcWpyiS20uw="; 27 | let mut key_secp256k1_bytes = STANDARD.decode(key_secp256k1_base64).unwrap(); 28 | let key = CombinedKey::secp256k1_from_bytes(&mut key_secp256k1_bytes).unwrap(); 29 | 30 | // When 31 | let enr = EnrBuilder::new("v4") 32 | .tcp4(tcp) 33 | .udp4(udp) 34 | .tcp6(tcp6) 35 | .udp6(udp6) 36 | .ip4(ip) 37 | .ip6(ip6) 38 | .multiaddrs(multiaddrs) 39 | .waku2(capabilities) 40 | .build(&key); 41 | 42 | // Then 43 | assert!(enr.is_ok()); 44 | } 45 | 46 | ///! https://rfc.vac.dev/spec/31/#many-connection-types 47 | #[test] 48 | fn test_decode_waku_enr() { 49 | // Given 50 | // Expected values 51 | let expected_tcp: u16 = 10101; 52 | let expected_udp: u16 = 20202; 53 | let expected_tcp6: u16 = 30303; 54 | let expected_udp6: u16 = 40404; 55 | let expected_ip: Ipv4Addr = "1.2.3.4".parse().unwrap(); 56 | let expected_ip6: Ipv6Addr = "1234:5600:101:1::142".parse().unwrap(); 57 | let expected_capabilities: WakuEnrCapabilities = 58 | WakuEnrCapabilities::STORE | WakuEnrCapabilities::RELAY; 59 | let expected_multiaddrs: Vec = vec![ 60 | "/dns4/example.com/tcp/443/wss".parse().unwrap(), 61 | "/dns4/quic.example.com/tcp/443/quic".parse().unwrap(), 62 | ]; 63 | 64 | // Signing key 65 | let key_secp256k1_base64 = "MaZivCR1kZsI2/1MuSw9mhnLQYqETWwjfcWpyiS20uw="; 66 | let mut key_secp256k1_bytes = STANDARD.decode(key_secp256k1_base64).unwrap(); 67 | let expected_key = CombinedKey::secp256k1_from_bytes(&mut key_secp256k1_bytes).unwrap(); 68 | 69 | // ENR 70 | let enr_base64 = "enr:-PC4QPdY95OvXxYSdzPnWTCEY3u0jr0t925ArgGDGJfsDemgMvl-PuXr23r9fJnJGncdx1yPYT7oB6OJoqsiUjSnF7sBgmlkgnY0gmlwhAECAwSDaXA2kBI0VgABAQABAAAAAAAAAUKKbXVsdGlhZGRyc60AEjYLZXhhbXBsZS5jb20GAbveAwAXNhBxdWljLmV4YW1wbGUuY29tBgG7zAOJc2VjcDI1NmsxoQL72vzMVCejPltbXNukOvJc8Mqj-IiawTVxiYY1WCRSX4N0Y3CCJ3WEdGNwNoJ2X4N1ZHCCTuqEdWRwNoKd1IV3YWt1MgM"; 71 | 72 | // When 73 | let enr: Enr = enr_base64.parse().expect("valid enr string"); 74 | 75 | let tcp = enr.tcp4(); 76 | let udp = enr.udp4(); 77 | let tcp6 = enr.tcp6(); 78 | let udp6 = enr.udp6(); 79 | let ip = enr.ip4(); 80 | let ip6 = enr.ip6(); 81 | let capabilities = enr.waku2(); 82 | let multiaddrs = enr.multiaddrs(); 83 | let public_key = enr.public_key(); 84 | 85 | // Then 86 | assert_eq!(public_key, expected_key.public()); 87 | assert!(matches!(tcp, Some(value) if value == expected_tcp)); 88 | assert!(matches!(udp, Some(value) if value == expected_udp)); 89 | assert!(matches!(tcp6, Some(value) if value == expected_tcp6)); 90 | assert!(matches!(udp6, Some(value) if value == expected_udp6)); 91 | assert!(matches!(ip, Some(value) if value == expected_ip)); 92 | assert!(matches!(ip6, Some(value) if value == expected_ip6)); 93 | assert!(matches!(capabilities, Some(value) if value == expected_capabilities)); 94 | assert!(matches!(multiaddrs, Some(value) if value == expected_multiaddrs)); 95 | } 96 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/gossip_promises.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Sigma Prime Pty Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | use std::collections::HashMap; 22 | 23 | use instant::Instant; 24 | use libp2p::identity::PeerId; 25 | use log::debug; 26 | 27 | use crate::gossipsub::error::MessageValidationError as ValidationError; 28 | use crate::gossipsub::peer_score::RejectReason; 29 | use crate::gossipsub::MessageId; 30 | 31 | /// Tracks recently sent `IWANT` messages and checks if peers respond to them. 32 | #[derive(Default)] 33 | pub(crate) struct GossipPromises { 34 | /// Stores for each tracked message id and peer the instant when this promise expires. 35 | /// 36 | /// If the peer didn't respond until then we consider the promise as broken and penalize the 37 | /// peer. 38 | promises: HashMap>, 39 | } 40 | 41 | impl GossipPromises { 42 | /// Returns true if the message id exists in the promises. 43 | pub(crate) fn contains(&self, message: &MessageId) -> bool { 44 | self.promises.contains_key(message) 45 | } 46 | 47 | /// Track a promise to deliver a message from a list of [`MessageId`]s we are requesting. 48 | pub(crate) fn add_promise(&mut self, peer: PeerId, messages: &[MessageId], expires: Instant) { 49 | for message_id in messages { 50 | // If a promise for this message id and peer already exists we don't update the expiry! 51 | self.promises 52 | .entry(message_id.clone()) 53 | .or_insert_with(HashMap::new) 54 | .entry(peer) 55 | .or_insert(expires); 56 | } 57 | } 58 | 59 | pub(crate) fn message_delivered(&mut self, message_id: &MessageId) { 60 | // Someone delivered a message, we can stop tracking all promises for it. 61 | self.promises.remove(message_id); 62 | } 63 | 64 | pub(crate) fn reject_message(&mut self, message_id: &MessageId, reason: &RejectReason) { 65 | // A message got rejected, so we can stop tracking promises and let the score penalty apply 66 | // from invalid message delivery. 67 | // We do take exception and apply promise penalty regardless in the following cases, where 68 | // the peer delivered an obviously invalid message. 69 | match reason { 70 | RejectReason::ValidationError(ValidationError::InvalidSignature) => (), 71 | RejectReason::SelfOrigin => (), 72 | _ => { 73 | self.promises.remove(message_id); 74 | } 75 | }; 76 | } 77 | 78 | /// Returns the number of broken promises for each peer who didn't follow up on an IWANT 79 | /// request. 80 | /// This should be called not too often relative to the expire times, since it iterates over 81 | /// the whole stored data. 82 | pub(crate) fn get_broken_promises(&mut self) -> HashMap { 83 | let now = Instant::now(); 84 | let mut result = HashMap::new(); 85 | self.promises.retain(|msg, peers| { 86 | peers.retain(|peer_id, expires| { 87 | if *expires < now { 88 | let count = result.entry(*peer_id).or_insert(0); 89 | *count += 1; 90 | debug!( 91 | "[Penalty] The peer {} broke the promise to deliver message {} in time!", 92 | peer_id, msg 93 | ); 94 | false 95 | } else { 96 | true 97 | } 98 | }); 99 | !peers.is_empty() 100 | }); 101 | result 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/rpc/types.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use libp2p::identity::PeerId; 3 | 4 | use crate::gossipsub::rpc::validation::validate_message_proto; 5 | use crate::gossipsub::rpc::MessageProto; 6 | use crate::gossipsub::TopicHash; 7 | 8 | #[derive(Clone, PartialEq, Debug)] 9 | pub struct MessageRpc { 10 | proto: MessageProto, 11 | } 12 | 13 | impl MessageRpc { 14 | pub fn new(topic: impl Into, data: impl Into>) -> Self { 15 | let topic = topic.into(); 16 | let data = data.into(); 17 | 18 | let proto = MessageProto { 19 | from: None, 20 | data: Some(data.into()), 21 | seqno: None, 22 | topic: topic.into_string(), 23 | signature: None, 24 | key: None, 25 | }; 26 | 27 | Self { proto } 28 | } 29 | 30 | pub fn new_with_sequence_number( 31 | topic: impl Into, 32 | data: impl Into>, 33 | seq_no: Option, 34 | ) -> Self { 35 | let mut rpc = Self::new(topic, data); 36 | rpc.set_sequence_number(seq_no); 37 | rpc 38 | } 39 | 40 | pub fn into_proto(self) -> MessageProto { 41 | self.proto 42 | } 43 | 44 | pub fn as_proto(&self) -> &MessageProto { 45 | &self.proto 46 | } 47 | 48 | pub fn source(&self) -> Option { 49 | self.proto 50 | .from 51 | .as_ref() 52 | .map(|bytes| PeerId::from_bytes(bytes).expect("valid peer id")) 53 | } 54 | 55 | pub fn set_source(&mut self, source: Option) { 56 | self.proto.from = source.map(|peer_id| peer_id.to_bytes().into()); 57 | } 58 | 59 | pub fn data(&self) -> &[u8] { 60 | self.proto.data.as_ref().unwrap() 61 | } 62 | 63 | pub fn sequence_number(&self) -> Option { 64 | self.proto.seqno.as_ref().map(|bytes| { 65 | // From pubsub spec: https://github.com/libp2p/specs/tree/master/pubsub#the-message 66 | // seqno field must be a 64-bit big-endian serialized unsigned integer 67 | let be_bytes = bytes[..].try_into().unwrap(); 68 | u64::from_be_bytes(be_bytes) 69 | }) 70 | } 71 | 72 | pub fn set_sequence_number(&mut self, seq_no: Option) { 73 | self.proto.seqno = seq_no.map(|no| no.to_be_bytes().to_vec().into()); 74 | } 75 | 76 | pub fn topic(&self) -> &str { 77 | self.proto.topic.as_ref() 78 | } 79 | 80 | pub fn signature(&self) -> Option<&[u8]> { 81 | self.proto.signature.as_ref().map(|bytes| bytes.as_ref()) 82 | } 83 | 84 | pub fn set_signature(&mut self, signature: Option>>) { 85 | self.proto.signature = signature.map(|bytes| bytes.into().into()); 86 | } 87 | 88 | pub fn key(&self) -> Option<&[u8]> { 89 | self.proto.key.as_ref().map(|bytes| bytes.as_ref()) 90 | } 91 | 92 | pub fn set_key(&mut self, key: Option>>) { 93 | self.proto.key = key.map(|bytes| bytes.into().into()); 94 | } 95 | } 96 | 97 | impl From for MessageRpc { 98 | /// Convert from a [`MessageProto`] into a [`MessageRpc`]. Additionally. sanitize the protobuf 99 | /// message by removing optional fields when empty. 100 | fn from(mut proto: MessageProto) -> Self { 101 | // A non-present data field should be interpreted as an empty payload. 102 | if proto.data.is_none() { 103 | proto.data = Some(Bytes::new()); 104 | } 105 | 106 | // An empty from field should be interpreted as not present. 107 | if let Some(from) = proto.from.as_ref() { 108 | if from.is_empty() { 109 | proto.from = None; 110 | } 111 | } 112 | 113 | // An empty seqno field should be interpreted as not present. 114 | if let Some(seq_no) = proto.seqno.as_ref() { 115 | if seq_no.is_empty() { 116 | proto.seqno = None; 117 | } 118 | } 119 | 120 | // An empty signature field should be interpreted as not present. 121 | if let Some(signature) = proto.signature.as_ref() { 122 | if signature.is_empty() { 123 | proto.signature = None; 124 | } 125 | } 126 | 127 | // An empty key field should be interpreted as not present. 128 | if let Some(key) = proto.key.as_ref() { 129 | if key.is_empty() { 130 | proto.key = None; 131 | } 132 | } 133 | 134 | // Assert proto validity after sanitizing (development builds only) 135 | debug_assert!( 136 | validate_message_proto(&proto).is_ok(), 137 | "invalid message proto: {proto:?}", 138 | ); 139 | 140 | Self { proto } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/topic.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Sigma Prime Pty Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | use std::convert::Infallible; 22 | use std::fmt; 23 | use std::str::FromStr; 24 | 25 | use base64::prelude::*; 26 | use prometheus_client::encoding::EncodeLabelSet; 27 | use prost::Message; 28 | use sha2::{Digest, Sha256}; 29 | 30 | use crate::gossipsub::rpc::TopicDescriptorProto; 31 | 32 | /// A generic trait that can be extended for various hashing types for a topic. 33 | pub trait Hasher { 34 | /// The function that takes a topic string and creates a topic hash. 35 | fn hash(topic_string: String) -> TopicHash; 36 | } 37 | 38 | /// A type for representing topics who use the identity hash. 39 | #[derive(Debug, Clone)] 40 | pub struct IdentityHash; 41 | 42 | impl Hasher for IdentityHash { 43 | /// Creates a [`TopicHash`] as a raw string. 44 | fn hash(topic_string: String) -> TopicHash { 45 | TopicHash { hash: topic_string } 46 | } 47 | } 48 | 49 | #[derive(Debug, Clone)] 50 | pub struct Sha256Hash; 51 | 52 | impl Hasher for Sha256Hash { 53 | /// Creates a [`TopicHash`] by SHA256 hashing the topic then base64 encoding the 54 | /// hash. 55 | fn hash(topic_string: String) -> TopicHash { 56 | let topic_descriptor = TopicDescriptorProto { 57 | name: Some(topic_string), 58 | auth: None, 59 | enc: None, 60 | }; 61 | let mut bytes = Vec::with_capacity(topic_descriptor.encoded_len()); 62 | topic_descriptor 63 | .encode(&mut bytes) 64 | .expect("Encoding to succeed"); 65 | let hash = BASE64_STANDARD.encode(Sha256::digest(&bytes)); 66 | TopicHash { hash } 67 | } 68 | } 69 | 70 | #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, EncodeLabelSet)] 71 | pub struct TopicHash { 72 | /// The topic hash. Stored as a string to align with the protobuf API. 73 | hash: String, 74 | } 75 | 76 | impl TopicHash { 77 | pub fn from_raw>(raw: T) -> Self { 78 | Self { hash: raw.into() } 79 | } 80 | 81 | pub fn into_string(self) -> String { 82 | self.hash 83 | } 84 | 85 | pub fn as_str(&self) -> &str { 86 | &self.hash 87 | } 88 | } 89 | 90 | impl> From for TopicHash { 91 | fn from(hash: T) -> Self { 92 | Self::from_raw(hash) 93 | } 94 | } 95 | 96 | impl FromStr for TopicHash { 97 | type Err = Infallible; 98 | 99 | fn from_str(s: &str) -> Result { 100 | Ok(Self::from_raw(s)) 101 | } 102 | } 103 | 104 | impl AsRef for TopicHash { 105 | fn as_ref(&self) -> &str { 106 | self.as_str() 107 | } 108 | } 109 | 110 | /// A gossipsub topic. 111 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 112 | pub struct Topic { 113 | topic: String, 114 | phantom_data: std::marker::PhantomData, 115 | } 116 | 117 | impl From> for TopicHash { 118 | fn from(topic: Topic) -> TopicHash { 119 | topic.hash() 120 | } 121 | } 122 | 123 | impl Topic { 124 | pub fn new>(topic: T) -> Self { 125 | Topic { 126 | topic: topic.into(), 127 | phantom_data: std::marker::PhantomData, 128 | } 129 | } 130 | 131 | pub fn hash(&self) -> TopicHash { 132 | H::hash(self.topic.clone()) 133 | } 134 | } 135 | 136 | impl fmt::Display for Topic { 137 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 138 | write!(f, "{}", self.topic) 139 | } 140 | } 141 | 142 | impl fmt::Display for TopicHash { 143 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 144 | write!(f, "{}", self.hash) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Sigma Prime Pty Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | //! Error types that can result from gossipsub. 22 | 23 | use std::io; 24 | 25 | use libp2p::identity::SigningError; 26 | 27 | /// Error associated with publishing a gossipsub message. 28 | #[derive(Debug)] 29 | pub enum PublishError { 30 | /// This message has already been published. 31 | Duplicate, 32 | /// An error occurred whilst signing the message. 33 | SigningError(SigningError), 34 | /// There were no peers to send this message to. 35 | InsufficientPeers, 36 | /// The overall message was too large. This could be due to excessive topics or an excessive 37 | /// message size. 38 | MessageTooLarge, 39 | /// The compression algorithm failed. 40 | TransformFailed(io::Error), 41 | } 42 | 43 | impl std::fmt::Display for PublishError { 44 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 45 | write!(f, "{self:?}") 46 | } 47 | } 48 | 49 | impl std::error::Error for PublishError { 50 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 51 | match self { 52 | Self::SigningError(err) => Some(err), 53 | Self::TransformFailed(err) => Some(err), 54 | _ => None, 55 | } 56 | } 57 | } 58 | 59 | /// Error associated with subscribing to a topic. 60 | #[derive(Debug)] 61 | pub enum SubscriptionError { 62 | /// Couldn't publish our subscription 63 | PublishError(PublishError), 64 | /// We are not allowed to subscribe to this topic by the subscription filter 65 | NotAllowed, 66 | } 67 | 68 | impl std::fmt::Display for SubscriptionError { 69 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 70 | write!(f, "{self:?}") 71 | } 72 | } 73 | 74 | impl std::error::Error for SubscriptionError { 75 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 76 | match self { 77 | Self::PublishError(err) => Some(err), 78 | _ => None, 79 | } 80 | } 81 | } 82 | 83 | impl From for PublishError { 84 | fn from(error: SigningError) -> Self { 85 | PublishError::SigningError(error) 86 | } 87 | } 88 | 89 | impl From for PublishError { 90 | fn from(error: io::Error) -> PublishError { 91 | PublishError::TransformFailed(error) 92 | } 93 | } 94 | 95 | #[derive(Debug, Clone, Copy, strum_macros::Display, thiserror::Error)] 96 | pub enum MessageValidationError { 97 | /// Empty message topic. 98 | InvalidTopic, 99 | /// The message source was invalid (invalid peer id). 100 | InvalidPeerId, 101 | /// The message source was not present. 102 | MissingMessageSource, 103 | /// The sequence number was the incorrect size 104 | InvalidSequenceNumber, 105 | /// The message sequence number was not present 106 | MissingSequenceNumber, 107 | /// The message has an invalid signature. 108 | InvalidSignature, 109 | /// The message signature was not present. 110 | MissingSignature, 111 | /// The message has an invalid key. 112 | InvalidKey, 113 | /// The message public key was not present. 114 | MissingPublicKey, 115 | /// Signature existed when validation has been sent to 116 | /// [`MessageAuthenticity::Anonymous`]. 117 | SignaturePresent, 118 | /// Key existed when validation has been sent to 119 | /// [`MessageAuthenticity::Anonymous`]. 120 | KeyPresent, 121 | /// Sequence number existed when validation has been sent to 122 | /// [`MessageAuthenticity::Anonymous`]. 123 | SequenceNumberPresent, 124 | /// Message source existed when validation has been sent to 125 | /// [`MessageAuthenticity::Anonymous`]. 126 | MessageSourcePresent, 127 | /// The data transformation failed. 128 | TransformFailed, 129 | } 130 | -------------------------------------------------------------------------------- /apps/waku-cli/src/cmd/relay/subscribe.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | use std::time::Duration; 3 | 4 | use libp2p::futures::StreamExt; 5 | use libp2p::identity::Keypair; 6 | use libp2p::swarm::{SwarmBuilder, SwarmEvent}; 7 | use libp2p::PeerId; 8 | use log::{debug, info}; 9 | use multiaddr::Multiaddr; 10 | use tokio::time::timeout; 11 | 12 | use waku_core::content_topic::ContentTopic; 13 | use waku_core::pubsub_topic::PubsubTopic; 14 | use waku_node::behaviour::{Behaviour as NodeBehaviour, Config as NodeBehaviourConfig}; 15 | use waku_node::default_transport; 16 | 17 | #[derive(Debug, Clone, clap::Args)] 18 | pub struct RelaySubscribeCmd { 19 | #[arg(long)] 20 | pub peer: String, 21 | #[arg(long)] 22 | pub pubsub_topic: String, 23 | #[arg(long)] 24 | pub content_topic: String, 25 | } 26 | 27 | pub async fn run_cmd(args: RelaySubscribeCmd) -> anyhow::Result<()> { 28 | // Parse command line arguments data 29 | let peer = args 30 | .peer 31 | .parse::() 32 | .map_err(|e| anyhow::anyhow!("Invalid peer address: {e}"))?; 33 | let pubsub_topic = args.pubsub_topic.parse::().unwrap(); 34 | let content_topic = args.content_topic.parse::().unwrap(); 35 | 36 | // Build the waku node 37 | let keypair = Keypair::generate_secp256k1(); 38 | let peer_id = PeerId::from(&keypair.public()); 39 | 40 | let mut switch = { 41 | let transport = default_transport(&keypair)?; 42 | 43 | let conf = NodeBehaviourConfig { 44 | local_public_key: keypair.public(), 45 | keep_alive: None, 46 | ping: None, 47 | relay: Some(Default::default()), 48 | }; 49 | let behaviour = NodeBehaviour::new(conf); 50 | 51 | SwarmBuilder::with_tokio_executor(transport, behaviour, peer_id).build() 52 | }; 53 | 54 | // Start node 55 | info!("Peer ID: {}", peer_id); 56 | 57 | // Start switch 58 | let listen_addr = Multiaddr::from_str("/ip4/0.0.0.0/tcp/0").expect("Valid multiaddr"); 59 | switch 60 | .listen_on(listen_addr.clone()) 61 | .map_err(|e| anyhow::anyhow!("Failed to listen: {e}"))?; 62 | 63 | // Dial peer 64 | info!("Dialing peer: {}", peer); 65 | switch 66 | .dial(peer.clone()) 67 | .map_err(|e| anyhow::anyhow!("Failed to dial peer: {e}"))?; 68 | 69 | // Await dial confirmation 70 | timeout(Duration::from_secs(5), async { 71 | loop { 72 | if let SwarmEvent::ConnectionEstablished { 73 | peer_id, endpoint, .. 74 | } = switch.select_next_some().await 75 | { 76 | let addr = endpoint.get_remote_address(); 77 | if addr == &peer { 78 | info!("Peer connection established: {addr} ({peer_id})"); 79 | return; 80 | } 81 | 82 | debug!("Connection established: {addr} ({peer_id})"); 83 | } 84 | } 85 | }) 86 | .await 87 | .map_err(|e| anyhow::anyhow!("Failed to dial peer (timeout): {e}"))?; 88 | 89 | // Join/Subscribe pubsub topic 90 | info!("Subscribing to pubsub topic: {pubsub_topic}"); 91 | switch 92 | .behaviour_mut() 93 | .waku_relay 94 | .as_mut() 95 | .expect("Waku relay behaviour is enabled") 96 | .subscribe(&pubsub_topic) 97 | .map_err(|e| anyhow::anyhow!("Failed to join pubsub topic: {e}"))?; 98 | 99 | // Await subscription confirmation 100 | timeout(Duration::from_secs(5), async { 101 | loop { 102 | if let SwarmEvent::Behaviour(waku_node::behaviour::Event::WakuRelay( 103 | waku_relay::Event::Subscribed { 104 | pubsub_topic: topic, 105 | .. 106 | }, 107 | )) = switch.select_next_some().await 108 | { 109 | if topic == pubsub_topic { 110 | info!("Joined pubsub topic: {topic}"); 111 | return; 112 | } 113 | 114 | debug!("Joined pubsub topic: {topic}"); 115 | } 116 | } 117 | }) 118 | .await 119 | .map_err(|e| anyhow::anyhow!("Failed to subscribe to pubsub topic (timeout): {e}"))?; 120 | 121 | // Log messages matching the pubsub and content topics filter criteria 122 | loop { 123 | if let SwarmEvent::Behaviour(waku_node::behaviour::Event::WakuRelay( 124 | waku_relay::Event::Message { 125 | message, 126 | pubsub_topic: topic, 127 | }, 128 | )) = switch.select_next_some().await 129 | { 130 | if pubsub_topic != topic { 131 | continue; 132 | } 133 | 134 | if message.content_topic != content_topic { 135 | continue; 136 | } 137 | 138 | info!("Message received on '{topic}': {message:?}"); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/rpc/fragmentation.rs: -------------------------------------------------------------------------------- 1 | use prost::Message; 2 | 3 | use crate::gossipsub::rpc::{ControlMessageProto, RpcProto}; 4 | 5 | pub enum FragmentationError { 6 | MessageTooLarge, 7 | } 8 | 9 | // If a message is too large to be sent as-is, this attempts to fragment it into smaller RPC 10 | // messages to be sent. 11 | pub fn fragment_rpc_message( 12 | rpc: RpcProto, 13 | max_size: usize, 14 | ) -> Result, FragmentationError> { 15 | if rpc.encoded_len() < max_size { 16 | return Ok(vec![rpc]); 17 | } 18 | 19 | let new_rpc = RpcProto { 20 | subscriptions: Vec::new(), 21 | publish: Vec::new(), 22 | control: None, 23 | }; 24 | 25 | let mut rpc_list = vec![new_rpc.clone()]; 26 | 27 | // Gets an RPC if the object size will fit, otherwise create a new RPC. The last element 28 | // will be the RPC to add an object. 29 | macro_rules! create_or_add_rpc { 30 | ($object_size: ident ) => { 31 | let list_index = rpc_list.len() - 1; // the list is never empty 32 | 33 | // create a new RPC if the new object plus 5% of its size (for length prefix 34 | // buffers) exceeds the max transmit size. 35 | if rpc_list[list_index].encoded_len() + (($object_size as f64) * 1.05) as usize 36 | > max_size 37 | && rpc_list[list_index] != new_rpc 38 | { 39 | // create a new rpc and use this as the current 40 | rpc_list.push(new_rpc.clone()); 41 | } 42 | }; 43 | } 44 | 45 | macro_rules! add_item { 46 | ($object: ident, $type: ident ) => { 47 | let object_size = $object.encoded_len(); 48 | 49 | if object_size + 2 > max_size { 50 | // This should not be possible. All received and published messages have already 51 | // been vetted to fit within the size. 52 | log::error!("Individual message too large to fragment"); 53 | return Err(FragmentationError::MessageTooLarge); 54 | } 55 | 56 | create_or_add_rpc!(object_size); 57 | rpc_list 58 | .last_mut() 59 | .expect("Must have at least one element") 60 | .$type 61 | .push($object.clone()); 62 | }; 63 | } 64 | 65 | // Add messages until the limit 66 | for message in &rpc.publish { 67 | add_item!(message, publish); 68 | } 69 | for subscription in &rpc.subscriptions { 70 | add_item!(subscription, subscriptions); 71 | } 72 | 73 | // handle the control messages. If all are within the max_transmit_size, send them without 74 | // fragmenting, otherwise, fragment the control messages 75 | let empty_control = ControlMessageProto::default(); 76 | if let Some(control) = rpc.control.as_ref() { 77 | if control.encoded_len() + 2 > max_size { 78 | // fragment the RPC 79 | for ihave in &control.ihave { 80 | let len = ihave.encoded_len(); 81 | create_or_add_rpc!(len); 82 | rpc_list 83 | .last_mut() 84 | .expect("Always an element") 85 | .control 86 | .get_or_insert_with(|| empty_control.clone()) 87 | .ihave 88 | .push(ihave.clone()); 89 | } 90 | for iwant in &control.iwant { 91 | let len = iwant.encoded_len(); 92 | create_or_add_rpc!(len); 93 | rpc_list 94 | .last_mut() 95 | .expect("Always an element") 96 | .control 97 | .get_or_insert_with(|| empty_control.clone()) 98 | .iwant 99 | .push(iwant.clone()); 100 | } 101 | for graft in &control.graft { 102 | let len = graft.encoded_len(); 103 | create_or_add_rpc!(len); 104 | rpc_list 105 | .last_mut() 106 | .expect("Always an element") 107 | .control 108 | .get_or_insert_with(|| empty_control.clone()) 109 | .graft 110 | .push(graft.clone()); 111 | } 112 | for prune in &control.prune { 113 | let len = prune.encoded_len(); 114 | create_or_add_rpc!(len); 115 | rpc_list 116 | .last_mut() 117 | .expect("Always an element") 118 | .control 119 | .get_or_insert_with(|| empty_control.clone()) 120 | .prune 121 | .push(prune.clone()); 122 | } 123 | } else { 124 | let len = control.encoded_len(); 125 | create_or_add_rpc!(len); 126 | rpc_list.last_mut().expect("Always an element").control = Some(control.clone()); 127 | } 128 | } 129 | 130 | Ok(rpc_list) 131 | } 132 | -------------------------------------------------------------------------------- /apps/waku-cli/src/cmd/relay/publish.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | use std::time::Duration; 3 | 4 | use bytes::Bytes; 5 | use libp2p::futures::StreamExt; 6 | use libp2p::identity::Keypair; 7 | use libp2p::swarm::{SwarmBuilder, SwarmEvent}; 8 | use libp2p::PeerId; 9 | use log::{debug, info}; 10 | use multiaddr::Multiaddr; 11 | use tokio::time::timeout; 12 | use ulid::Ulid; 13 | 14 | use waku_core::content_topic::ContentTopic; 15 | use waku_core::message::WakuMessage; 16 | use waku_core::pubsub_topic::PubsubTopic; 17 | use waku_node::behaviour::{Behaviour as NodeBehaviour, Config as NodeBehaviourConfig}; 18 | use waku_node::default_transport; 19 | 20 | #[derive(Debug, Clone, clap::Args)] 21 | pub struct RelayPublishCmd { 22 | #[arg(long)] 23 | pub peer: String, 24 | #[arg(long)] 25 | pub pubsub_topic: String, 26 | #[arg(long)] 27 | pub content_topic: String, 28 | 29 | #[arg(required = true)] 30 | pub payload: String, 31 | } 32 | 33 | pub async fn run_cmd(args: RelayPublishCmd) -> anyhow::Result<()> { 34 | // Parse command line arguments data 35 | let peer = args 36 | .peer 37 | .parse::() 38 | .map_err(|e| anyhow::anyhow!("Invalid peer address: {e}"))?; 39 | let pubsub_topic = args.pubsub_topic.parse::().unwrap(); 40 | let content_topic = args.content_topic.parse::().unwrap(); 41 | let payload = hex::decode(args.payload)?; 42 | 43 | // Build the waku node 44 | let keypair = Keypair::generate_secp256k1(); 45 | let peer_id = PeerId::from(&keypair.public()); 46 | 47 | let mut switch = { 48 | let transport = default_transport(&keypair)?; 49 | 50 | let conf = NodeBehaviourConfig { 51 | local_public_key: keypair.public(), 52 | keep_alive: None, 53 | ping: None, 54 | relay: Some(Default::default()), 55 | }; 56 | let behaviour = NodeBehaviour::new(conf); 57 | 58 | SwarmBuilder::with_tokio_executor(transport, behaviour, peer_id).build() 59 | }; 60 | 61 | // Start node 62 | info!("Peer ID: {}", peer_id); 63 | 64 | // Start switch 65 | let listen_addr = Multiaddr::from_str("/ip4/0.0.0.0/tcp/0").expect("Valid multiaddr"); 66 | switch 67 | .listen_on(listen_addr.clone()) 68 | .map_err(|e| anyhow::anyhow!("Failed to listen: {e}"))?; 69 | 70 | // Dial peer 71 | info!("Dialing peer: {}", peer); 72 | switch 73 | .dial(peer.clone()) 74 | .map_err(|e| anyhow::anyhow!("Failed to dial peer: {e}"))?; 75 | 76 | // Await dial confirmation 77 | timeout(Duration::from_secs(5), async { 78 | loop { 79 | if let SwarmEvent::ConnectionEstablished { 80 | peer_id, endpoint, .. 81 | } = switch.select_next_some().await 82 | { 83 | let addr = endpoint.get_remote_address(); 84 | if addr == &peer { 85 | info!("Peer connection established: {addr} ({peer_id})"); 86 | return; 87 | } 88 | 89 | debug!("Connection established: {addr} ({peer_id})"); 90 | } 91 | } 92 | }) 93 | .await 94 | .map_err(|e| anyhow::anyhow!("Failed to dial peer (timeout): {e}"))?; 95 | 96 | // Join/Subscribe pubsub topic 97 | info!("Subscribing to pubsub topic: {pubsub_topic}"); 98 | switch 99 | .behaviour_mut() 100 | .waku_relay 101 | .as_mut() 102 | .expect("Waku relay behaviour is enabled") 103 | .subscribe(&pubsub_topic) 104 | .map_err(|e| anyhow::anyhow!("Failed to join pubsub topic: {e}"))?; 105 | 106 | // Await subscription confirmation 107 | timeout(Duration::from_secs(5), async { 108 | loop { 109 | if let SwarmEvent::Behaviour(waku_node::behaviour::Event::WakuRelay( 110 | waku_relay::Event::Subscribed { 111 | pubsub_topic: topic, 112 | .. 113 | }, 114 | )) = switch.select_next_some().await 115 | { 116 | if topic == pubsub_topic { 117 | info!("Joined pubsub topic: {topic}"); 118 | return; 119 | } 120 | 121 | debug!("Joined pubsub topic: {topic}"); 122 | } 123 | } 124 | }) 125 | .await 126 | .map_err(|e| anyhow::anyhow!("Failed to subscribe to pubsub topic (timeout): {e}"))?; 127 | 128 | // Build and publish the message 129 | let meta = Bytes::from(Ulid::new().0.to_be_bytes().to_vec()); 130 | let payload = Bytes::from(payload); 131 | let message = WakuMessage { 132 | content_topic, 133 | meta: Some(meta), 134 | payload, 135 | ephemeral: true, 136 | }; 137 | 138 | info!("Publishing message: {message:?}"); 139 | switch 140 | .behaviour_mut() 141 | .waku_relay 142 | .as_mut() 143 | .expect("Waku relay behaviour is enabled") 144 | .publish(&pubsub_topic, message) 145 | .map_err(|e| anyhow::anyhow!("Failed to publish message: {e}"))?; 146 | 147 | let _ = timeout(Duration::from_millis(200), async { 148 | loop { 149 | let event = switch.select_next_some().await; 150 | debug!("{event:?}") 151 | } 152 | }) 153 | .await; 154 | 155 | info!("Message published"); 156 | 157 | Ok(()) 158 | } 159 | -------------------------------------------------------------------------------- /waku-relay/src/message_id.rs: -------------------------------------------------------------------------------- 1 | use prost::Message; 2 | use sha2::digest::generic_array::GenericArray; 3 | use sha2::digest::typenum::U32; 4 | use sha2::digest::FixedOutput; 5 | use sha2::{Digest, Sha256}; 6 | 7 | use waku_core::message::proto::waku::message::v1::WakuMessage; 8 | 9 | use crate::gossipsub; 10 | use crate::gossipsub::MessageId; 11 | 12 | /// Fallback message ID function. 13 | fn fallback_message_id_fn(message: &gossipsub::Message) -> MessageId { 14 | let mut hasher = Sha256::new(); 15 | hasher.update(&message.data); 16 | let result = hasher.finalize_fixed(); 17 | 18 | MessageId::new(result.to_vec()) 19 | } 20 | 21 | /// Compute Waku v2 message's [deterministic hash](https://rfc.vac.dev/spec/14/#deterministic-message-hashing). 22 | /// 23 | /// ```text 24 | /// message_hash = sha256(concat(pubsub_topic, message.payload, message.content_topic, message.meta)) 25 | /// ``` 26 | fn compute_deterministic_message_hash(topic: &str, message: WakuMessage) -> GenericArray { 27 | let mut hasher = Sha256::new(); 28 | hasher.update(topic); 29 | hasher.update(message.payload); 30 | hasher.update(message.content_topic); 31 | if let Some(meta) = message.meta { 32 | hasher.update(meta); 33 | } 34 | hasher.finalize_fixed() 35 | } 36 | 37 | /// Deterministic message ID function based on the message deterministic hash specification. 38 | /// See [RFC 14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/#deterministic-message-hashing). 39 | /// 40 | /// If the message is not a valid `WakuMessage` (e.g., failed deserialization), this 41 | /// function calls `fallback_message_id_fn`. 42 | pub fn deterministic_message_id_fn(message: &gossipsub::Message) -> MessageId { 43 | let pubsub_topic = message.topic.as_str(); 44 | let waku_message = match WakuMessage::decode(&message.data[..]) { 45 | Ok(msg) => msg, 46 | _ => return fallback_message_id_fn(message), 47 | }; 48 | 49 | let result = compute_deterministic_message_hash(pubsub_topic, waku_message); 50 | 51 | MessageId::new(result.to_vec()) 52 | } 53 | 54 | #[cfg(test)] 55 | mod tests { 56 | use bytes::Bytes; 57 | use hex_literal::hex; 58 | 59 | use waku_core::message::proto::waku::message::v1::WakuMessage; 60 | 61 | use super::compute_deterministic_message_hash; 62 | 63 | /// https://rfc.vac.dev/spec/14/#test-vectors (Test vector 1) 64 | #[test] 65 | fn test_deterministic_message_id_fn_rfc1_12bytes_meta() { 66 | // Given 67 | let pubsub_topic = "/waku/2/default-waku/proto"; 68 | let waku_message = WakuMessage { 69 | payload: Bytes::from_static(&hex!("010203045445535405060708")), 70 | content_topic: String::from("/waku/2/default-content/proto"), 71 | meta: Some(Bytes::from_static(&hex!("73757065722d736563726574"))), 72 | ..Default::default() 73 | }; 74 | 75 | // When 76 | let message_id = compute_deterministic_message_hash(pubsub_topic, waku_message); 77 | 78 | // Then 79 | assert_eq!( 80 | message_id.as_slice(), 81 | hex!("4fdde1099c9f77f6dae8147b6b3179aba1fc8e14a7bf35203fc253ee479f135f") 82 | ); 83 | } 84 | 85 | /// https://rfc.vac.dev/spec/14/#test-vectors (Test vector 2) 86 | #[test] 87 | fn test_deterministic_message_id_fn_rfc2_64bytes_meta() { 88 | // Given 89 | let pubsub_topic = "/waku/2/default-waku/proto"; 90 | let waku_message = WakuMessage { 91 | payload: Bytes::from_static(&hex!("010203045445535405060708")), 92 | content_topic: String::from("/waku/2/default-content/proto"), 93 | meta: Some(Bytes::from_static(&hex!( 94 | "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f 95 | 202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f" 96 | ))), 97 | ..Default::default() 98 | }; 99 | 100 | // When 101 | let message_id = compute_deterministic_message_hash(pubsub_topic, waku_message); 102 | 103 | // Then 104 | assert_eq!( 105 | message_id.as_slice(), 106 | hex!("c32ed3b51f0c432be1c7f50880110e1a1a60f6067cd8193ca946909efe1b26ad") 107 | ); 108 | } 109 | 110 | /// https://rfc.vac.dev/spec/14/#test-vectors (Test vector 3) 111 | #[test] 112 | fn test_deterministic_message_id_fn_rfc3_not_present_meta() { 113 | // Given 114 | let pubsub_topic = "/waku/2/default-waku/proto"; 115 | let waku_message = WakuMessage { 116 | payload: Bytes::from_static(&hex!("010203045445535405060708")), 117 | content_topic: String::from("/waku/2/default-content/proto"), 118 | meta: None, 119 | ..Default::default() 120 | }; 121 | 122 | // When 123 | let message_id = compute_deterministic_message_hash(pubsub_topic, waku_message); 124 | 125 | // Then 126 | assert_eq!( 127 | message_id.as_slice(), 128 | hex!("87619d05e563521d9126749b45bd4cc2430df0607e77e23572d874ed9c1aaa62") 129 | ); 130 | } 131 | 132 | /// https://rfc.vac.dev/spec/14/#test-vectors (Test vector 4) 133 | #[test] 134 | fn test_deterministic_message_id_fn_rfc4_empty_payload() { 135 | // Given 136 | let pubsub_topic = "/waku/2/default-waku/proto"; 137 | let waku_message = WakuMessage { 138 | payload: Bytes::new(), 139 | content_topic: String::from("/waku/2/default-content/proto"), 140 | meta: Some(Bytes::from_static(&hex!("73757065722d736563726574"))), 141 | ..Default::default() 142 | }; 143 | 144 | // When 145 | let message_id = compute_deterministic_message_hash(pubsub_topic, waku_message); 146 | 147 | // Then 148 | assert_eq!( 149 | message_id.as_slice(), 150 | hex!("e1a9596237dbe2cc8aaf4b838c46a7052df6bc0d42ba214b998a8bfdbe8487d6") 151 | ); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/protocol.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Sigma Prime Pty Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | use std::convert::Infallible; 22 | use std::pin::Pin; 23 | 24 | use asynchronous_codec::Framed; 25 | use futures::future; 26 | use futures::prelude::*; 27 | use libp2p::core::{InboundUpgrade, OutboundUpgrade, ProtocolName, UpgradeInfo}; 28 | 29 | use crate::gossipsub::codec::Codec; 30 | use crate::gossipsub::Config; 31 | use crate::gossipsub::config::Version; 32 | use crate::gossipsub::types::PeerKind; 33 | 34 | /// The protocol ID 35 | #[derive(Clone, Debug)] 36 | pub struct ProtocolId { 37 | /// The RPC message type/name. 38 | pub protocol_id: Vec, 39 | /// The type of protocol we support 40 | pub kind: PeerKind, 41 | } 42 | 43 | /// An RPC protocol ID. 44 | impl ProtocolId { 45 | pub fn new(id: &str, kind: PeerKind, prefix: bool) -> Self { 46 | let protocol_id = match kind { 47 | PeerKind::Gossipsubv1_1 => match prefix { 48 | true => format!("/{}/{}", id, "1.1.0"), 49 | false => id.to_string(), 50 | }, 51 | PeerKind::Gossipsub => match prefix { 52 | true => format!("/{}/{}", id, "1.0.0"), 53 | false => id.to_string(), 54 | }, 55 | PeerKind::Floodsub => format!("/{}/{}", "floodsub", "1.0.0"), 56 | // NOTE: This is used for informing the behaviour of unsupported peers. We do not 57 | // advertise this variant. 58 | PeerKind::NotSupported => unreachable!("Should never advertise NotSupported"), 59 | } 60 | .into_bytes(); 61 | ProtocolId { protocol_id, kind } 62 | } 63 | } 64 | 65 | impl ProtocolName for ProtocolId { 66 | fn protocol_name(&self) -> &[u8] { 67 | &self.protocol_id 68 | } 69 | } 70 | 71 | /// Implementation of [`InboundUpgrade`] and [`OutboundUpgrade`] for the Gossipsub protocol. 72 | #[derive(Debug, Clone)] 73 | pub struct ProtocolUpgrade { 74 | /// The Gossipsub protocol id to listen on. 75 | protocol_ids: Vec, 76 | /// The maximum transmit size for a packet. 77 | max_transmit_size: usize, 78 | } 79 | 80 | impl ProtocolUpgrade { 81 | /// Builds a new [`ProtocolUpgrade`]. 82 | /// 83 | /// Sets the maximum gossip transmission size. 84 | pub fn new(gossipsub_config: &Config) -> ProtocolUpgrade { 85 | let mut protocol_ids = match gossipsub_config.custom_id_version() { 86 | Some(v) => match v { 87 | Version::V1_0 => vec![ProtocolId::new( 88 | gossipsub_config.protocol_id(), 89 | PeerKind::Gossipsub, 90 | false, 91 | )], 92 | Version::V1_1 => vec![ProtocolId::new( 93 | gossipsub_config.protocol_id(), 94 | PeerKind::Gossipsubv1_1, 95 | false, 96 | )], 97 | }, 98 | None => { 99 | vec![ 100 | ProtocolId::new( 101 | gossipsub_config.protocol_id(), 102 | PeerKind::Gossipsubv1_1, 103 | true, 104 | ), 105 | ProtocolId::new(gossipsub_config.protocol_id(), PeerKind::Gossipsub, true), 106 | ] 107 | } 108 | }; 109 | 110 | // add floodsub support if enabled. 111 | if gossipsub_config.support_floodsub() { 112 | protocol_ids.push(ProtocolId::new("", PeerKind::Floodsub, false)); 113 | } 114 | 115 | ProtocolUpgrade { 116 | protocol_ids, 117 | max_transmit_size: gossipsub_config.max_transmit_size(), 118 | } 119 | } 120 | } 121 | 122 | impl UpgradeInfo for ProtocolUpgrade { 123 | type Info = ProtocolId; 124 | type InfoIter = Vec; 125 | 126 | fn protocol_info(&self) -> Self::InfoIter { 127 | self.protocol_ids.clone() 128 | } 129 | } 130 | 131 | impl InboundUpgrade for ProtocolUpgrade 132 | where 133 | TSocket: AsyncRead + AsyncWrite + Unpin + Send + 'static, 134 | { 135 | type Output = (Framed, PeerKind); 136 | type Error = Infallible; 137 | type Future = Pin> + Send>>; 138 | 139 | fn upgrade_inbound(self, socket: TSocket, protocol_id: Self::Info) -> Self::Future { 140 | Box::pin(future::ok(( 141 | Framed::new(socket, Codec::new(self.max_transmit_size)), 142 | protocol_id.kind, 143 | ))) 144 | } 145 | } 146 | 147 | impl OutboundUpgrade for ProtocolUpgrade 148 | where 149 | TSocket: AsyncWrite + AsyncRead + Unpin + Send + 'static, 150 | { 151 | type Output = (Framed, PeerKind); 152 | type Error = Infallible; 153 | type Future = Pin> + Send>>; 154 | 155 | fn upgrade_outbound(self, socket: TSocket, protocol_id: Self::Info) -> Self::Future { 156 | Box::pin(future::ok(( 157 | Framed::new(socket, Codec::new(self.max_transmit_size)), 158 | protocol_id.kind, 159 | ))) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/time_cache.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Sigma Prime Pty Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | //! This implements a time-based LRU cache for checking gossipsub message duplicates. 22 | 23 | use std::collections::hash_map::{ 24 | self, 25 | Entry::{Occupied, Vacant}, 26 | }; 27 | use std::collections::VecDeque; 28 | use std::time::Duration; 29 | 30 | use fnv::FnvHashMap; 31 | use instant::Instant; 32 | 33 | struct ExpiringElement { 34 | /// The element that expires 35 | element: Element, 36 | /// The expire time. 37 | expires: Instant, 38 | } 39 | 40 | pub(crate) struct TimeCache { 41 | /// Mapping a key to its value together with its latest expire time (can be updated through 42 | /// reinserts). 43 | map: FnvHashMap>, 44 | /// An ordered list of keys by expires time. 45 | list: VecDeque>, 46 | /// The time elements remain in the cache. 47 | ttl: Duration, 48 | } 49 | 50 | pub(crate) struct OccupiedEntry<'a, K, V> { 51 | entry: hash_map::OccupiedEntry<'a, K, ExpiringElement>, 52 | } 53 | 54 | impl<'a, K, V> OccupiedEntry<'a, K, V> 55 | where 56 | K: Eq + std::hash::Hash + Clone, 57 | { 58 | pub(crate) fn into_mut(self) -> &'a mut V { 59 | &mut self.entry.into_mut().element 60 | } 61 | } 62 | 63 | pub(crate) struct VacantEntry<'a, K, V> { 64 | expiration: Instant, 65 | entry: hash_map::VacantEntry<'a, K, ExpiringElement>, 66 | list: &'a mut VecDeque>, 67 | } 68 | 69 | impl<'a, K, V> VacantEntry<'a, K, V> 70 | where 71 | K: Eq + std::hash::Hash + Clone, 72 | { 73 | pub(crate) fn insert(self, value: V) -> &'a mut V { 74 | self.list.push_back(ExpiringElement { 75 | element: self.entry.key().clone(), 76 | expires: self.expiration, 77 | }); 78 | &mut self 79 | .entry 80 | .insert(ExpiringElement { 81 | element: value, 82 | expires: self.expiration, 83 | }) 84 | .element 85 | } 86 | } 87 | 88 | pub(crate) enum Entry<'a, K: 'a, V: 'a> { 89 | Occupied(OccupiedEntry<'a, K, V>), 90 | Vacant(VacantEntry<'a, K, V>), 91 | } 92 | 93 | impl<'a, K: 'a, V: 'a> Entry<'a, K, V> 94 | where 95 | K: Eq + std::hash::Hash + Clone, 96 | { 97 | pub(crate) fn or_insert_with V>(self, default: F) -> &'a mut V { 98 | match self { 99 | Entry::Occupied(entry) => entry.into_mut(), 100 | Entry::Vacant(entry) => entry.insert(default()), 101 | } 102 | } 103 | } 104 | 105 | impl TimeCache 106 | where 107 | Key: Eq + std::hash::Hash + Clone, 108 | { 109 | pub(crate) fn new(ttl: Duration) -> Self { 110 | TimeCache { 111 | map: FnvHashMap::default(), 112 | list: VecDeque::new(), 113 | ttl, 114 | } 115 | } 116 | 117 | fn remove_expired_keys(&mut self, now: Instant) { 118 | while let Some(element) = self.list.pop_front() { 119 | if element.expires > now { 120 | self.list.push_front(element); 121 | break; 122 | } 123 | if let Occupied(entry) = self.map.entry(element.element.clone()) { 124 | if entry.get().expires <= now { 125 | entry.remove(); 126 | } 127 | } 128 | } 129 | } 130 | 131 | pub(crate) fn entry(&mut self, key: Key) -> Entry { 132 | let now = Instant::now(); 133 | self.remove_expired_keys(now); 134 | match self.map.entry(key) { 135 | Occupied(entry) => Entry::Occupied(OccupiedEntry { entry }), 136 | Vacant(entry) => Entry::Vacant(VacantEntry { 137 | expiration: now + self.ttl, 138 | entry, 139 | list: &mut self.list, 140 | }), 141 | } 142 | } 143 | 144 | /// Empties the entire cache. 145 | #[cfg(test)] 146 | pub(crate) fn clear(&mut self) { 147 | self.map.clear(); 148 | self.list.clear(); 149 | } 150 | 151 | pub(crate) fn contains_key(&self, key: &Key) -> bool { 152 | self.map.contains_key(key) 153 | } 154 | 155 | pub(crate) fn get(&self, key: &Key) -> Option<&Value> { 156 | self.map.get(key).map(|e| &e.element) 157 | } 158 | } 159 | 160 | pub(crate) struct DuplicateCache(TimeCache); 161 | 162 | impl DuplicateCache 163 | where 164 | Key: Eq + std::hash::Hash + Clone, 165 | { 166 | pub(crate) fn new(ttl: Duration) -> Self { 167 | Self(TimeCache::new(ttl)) 168 | } 169 | 170 | // Inserts new elements and removes any expired elements. 171 | // 172 | // If the key was not present this returns `true`. If the value was already present this 173 | // returns `false`. 174 | pub(crate) fn insert(&mut self, key: Key) -> bool { 175 | if let Entry::Vacant(entry) = self.0.entry(key) { 176 | entry.insert(()); 177 | true 178 | } else { 179 | false 180 | } 181 | } 182 | 183 | pub(crate) fn contains(&self, key: &Key) -> bool { 184 | self.0.contains_key(key) 185 | } 186 | } 187 | 188 | #[cfg(test)] 189 | mod test { 190 | use super::*; 191 | 192 | #[test] 193 | fn cache_added_entries_exist() { 194 | let mut cache = DuplicateCache::new(Duration::from_secs(10)); 195 | 196 | cache.insert("t"); 197 | cache.insert("e"); 198 | 199 | // Should report that 't' and 't' already exists 200 | assert!(!cache.insert("t")); 201 | assert!(!cache.insert("e")); 202 | } 203 | 204 | #[test] 205 | fn cache_entries_expire() { 206 | let mut cache = DuplicateCache::new(Duration::from_millis(100)); 207 | 208 | cache.insert("t"); 209 | assert!(!cache.insert("t")); 210 | cache.insert("e"); 211 | //assert!(!cache.insert("t")); 212 | assert!(!cache.insert("e")); 213 | // sleep until cache expiry 214 | std::thread::sleep(Duration::from_millis(101)); 215 | // add another element to clear previous cache 216 | cache.insert("s"); 217 | 218 | // should be removed from the cache 219 | assert!(cache.insert("t")); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/rpc/traits.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ByteOrder}; 2 | use bytes::Bytes; 3 | use libp2p::identity::PeerId; 4 | 5 | use crate::gossipsub::{RawMessage, Rpc, TopicHash}; 6 | use crate::gossipsub::rpc::proto::waku::relay::v2::{ 7 | ControlGraft as ControlGraftProto, ControlIHave as ControlIHaveProto, 8 | ControlIHave, ControlIWant as ControlIWantProto, ControlMessage as ControlMessageProto, 9 | ControlPrune as ControlPruneProto, Message as MessageProto, 10 | PeerInfo as PeerInfoProto, Rpc as RpcProto, rpc::SubOpts as SubOptsProto, 11 | }; 12 | use crate::gossipsub::types::{ControlAction, PeerInfo, Subscription, SubscriptionAction}; 13 | 14 | impl From for MessageProto { 15 | /// Converts the message into protobuf format. 16 | fn from(msg: RawMessage) -> Self { 17 | Self { 18 | from: msg.source.map(|m| Bytes::from(m.to_bytes())), 19 | data: Some(Bytes::from(msg.data)), 20 | seqno: msg 21 | .sequence_number 22 | .map(|s| Bytes::copy_from_slice(&s.to_be_bytes())), 23 | topic: TopicHash::into_string(msg.topic), 24 | signature: msg.signature.map(Bytes::from), 25 | key: msg.key.map(Bytes::from), 26 | } 27 | } 28 | } 29 | 30 | impl From for RawMessage { 31 | fn from(msg: MessageProto) -> Self { 32 | Self { 33 | source: msg 34 | .from 35 | .map(|b| PeerId::from_bytes(&b).expect("PeerId to be valid")), 36 | data: msg.data.map(Into::into).unwrap_or_default(), 37 | sequence_number: msg.seqno.as_ref().map(|v| BigEndian::read_u64(v)), 38 | topic: TopicHash::from_raw(msg.topic), 39 | signature: msg.signature.map(Into::into), 40 | key: msg.key.map(Into::into), 41 | } 42 | } 43 | } 44 | 45 | impl From for SubOptsProto { 46 | /// Converts the subscription into protobuf format. 47 | fn from(sub: Subscription) -> Self { 48 | Self { 49 | subscribe: Some(sub.action == SubscriptionAction::Subscribe), 50 | topic_id: Some(sub.topic_hash.into_string()), 51 | } 52 | } 53 | } 54 | 55 | impl From for Subscription { 56 | fn from(sub: SubOptsProto) -> Self { 57 | Self { 58 | action: if Some(true) == sub.subscribe { 59 | SubscriptionAction::Subscribe 60 | } else { 61 | SubscriptionAction::Unsubscribe 62 | }, 63 | topic_hash: TopicHash::from_raw(sub.topic_id.unwrap_or_default()), 64 | } 65 | } 66 | } 67 | 68 | impl From for PeerInfoProto { 69 | /// Converts the peer info into protobuf format. 70 | fn from(info: PeerInfo) -> Self { 71 | Self { 72 | peer_id: info.peer_id.map(|id| Bytes::from(id.to_bytes())), 73 | /// TODO, see https://github.com/libp2p/specs/pull/217 74 | signed_peer_record: None, 75 | } 76 | } 77 | } 78 | 79 | impl TryFrom for PeerInfo { 80 | type Error = anyhow::Error; 81 | 82 | fn try_from(info: PeerInfoProto) -> Result { 83 | let peer_id = info.peer_id.unwrap(); 84 | let peer_id = PeerId::from_bytes(&peer_id[..])?; 85 | Ok(Self { 86 | peer_id: Some(peer_id), 87 | }) 88 | } 89 | } 90 | 91 | impl FromIterator for ControlMessageProto { 92 | fn from_iter>(iter: I) -> Self { 93 | let mut control = ControlMessageProto { 94 | ihave: Vec::new(), 95 | iwant: Vec::new(), 96 | graft: Vec::new(), 97 | prune: Vec::new(), 98 | }; 99 | 100 | for action in iter { 101 | match action { 102 | ControlAction::IHave { 103 | topic_hash, 104 | message_ids, 105 | } => { 106 | let rpc_ihave = ControlIHaveProto { 107 | topic_id: Some(topic_hash.into_string()), 108 | message_ids: message_ids.into_iter().map(Into::into).collect(), 109 | }; 110 | control.ihave.push(rpc_ihave); 111 | } 112 | 113 | ControlAction::IWant { message_ids } => { 114 | let rpc_iwant = ControlIWantProto { 115 | message_ids: message_ids.into_iter().map(Into::into).collect(), 116 | }; 117 | control.iwant.push(rpc_iwant); 118 | } 119 | 120 | ControlAction::Graft { topic_hash } => { 121 | let rpc_graft = ControlGraftProto { 122 | topic_id: Some(topic_hash.into_string()), 123 | }; 124 | control.graft.push(rpc_graft); 125 | } 126 | 127 | ControlAction::Prune { 128 | topic_hash, 129 | peers, 130 | backoff, 131 | } => { 132 | let rpc_prune = ControlPruneProto { 133 | topic_id: Some(topic_hash.into_string()), 134 | peers: peers.into_iter().map(Into::into).collect(), 135 | backoff, 136 | }; 137 | control.prune.push(rpc_prune); 138 | } 139 | } 140 | } 141 | 142 | control 143 | } 144 | } 145 | 146 | impl From for ControlAction { 147 | fn from(ihave: ControlIHave) -> Self { 148 | Self::IHave { 149 | topic_hash: TopicHash::from_raw(ihave.topic_id.unwrap_or_default()), 150 | message_ids: ihave.message_ids.into_iter().map(Into::into).collect(), 151 | } 152 | } 153 | } 154 | 155 | impl From for ControlAction { 156 | fn from(iwant: ControlIWantProto) -> Self { 157 | Self::IWant { 158 | message_ids: iwant.message_ids.into_iter().map(Into::into).collect(), 159 | } 160 | } 161 | } 162 | 163 | impl From for ControlAction { 164 | fn from(graft: ControlGraftProto) -> Self { 165 | Self::Graft { 166 | topic_hash: TopicHash::from_raw(graft.topic_id.unwrap_or_default()), 167 | } 168 | } 169 | } 170 | 171 | impl From for ControlAction { 172 | fn from(prune: ControlPruneProto) -> Self { 173 | let peers = prune 174 | .peers 175 | .into_iter() 176 | .filter_map(|info| PeerInfo::try_from(info).ok()) // filter out invalid peers 177 | .collect(); 178 | 179 | let topic_hash = TopicHash::from_raw(prune.topic_id.unwrap_or_default()); 180 | 181 | Self::Prune { 182 | topic_hash, 183 | peers, 184 | backoff: prune.backoff, 185 | } 186 | } 187 | } 188 | 189 | impl From for RpcProto { 190 | /// Converts the RPC into protobuf format. 191 | fn from(rpc: Rpc) -> Self { 192 | let publish = rpc.messages.into_iter().map(Into::into).collect(); 193 | let subscriptions = rpc.subscriptions.into_iter().map(Into::into).collect(); 194 | let control = rpc 195 | .control_msgs 196 | .is_empty() 197 | .then(|| rpc.control_msgs.into_iter().collect()); 198 | 199 | Self { 200 | subscriptions, 201 | publish, 202 | control, 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/backoff.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Sigma Prime Pty Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | //! Data structure for efficiently storing known back-off's when pruning peers. 22 | use std::collections::{ 23 | hash_map::{Entry, HashMap}, 24 | HashSet, 25 | }; 26 | use std::time::Duration; 27 | use instant::Instant; 28 | 29 | use libp2p::identity::PeerId; 30 | 31 | use crate::gossipsub::topic::TopicHash; 32 | 33 | #[derive(Copy, Clone)] 34 | struct HeartbeatIndex(usize); 35 | 36 | /// Stores backoffs in an efficient manner. 37 | pub(crate) struct BackoffStorage { 38 | /// Stores backoffs and the index in backoffs_by_heartbeat per peer per topic. 39 | backoffs: HashMap>, 40 | /// Stores peer topic pairs per heartbeat (this is cyclic the current index is 41 | /// heartbeat_index). 42 | backoffs_by_heartbeat: Vec>, 43 | /// The index in the backoffs_by_heartbeat vector corresponding to the current heartbeat. 44 | heartbeat_index: HeartbeatIndex, 45 | /// The heartbeat interval duration from the config. 46 | heartbeat_interval: Duration, 47 | /// Backoff slack from the config. 48 | backoff_slack: u32, 49 | } 50 | 51 | impl BackoffStorage { 52 | fn heartbeats(d: &Duration, heartbeat_interval: &Duration) -> usize { 53 | ((d.as_nanos() + heartbeat_interval.as_nanos() - 1) / heartbeat_interval.as_nanos()) 54 | as usize 55 | } 56 | 57 | pub(crate) fn new( 58 | prune_backoff: &Duration, 59 | heartbeat_interval: Duration, 60 | backoff_slack: u32, 61 | ) -> BackoffStorage { 62 | // We add one additional slot for partial heartbeat 63 | let max_heartbeats = 64 | Self::heartbeats(prune_backoff, &heartbeat_interval) + backoff_slack as usize + 1; 65 | BackoffStorage { 66 | backoffs: HashMap::new(), 67 | backoffs_by_heartbeat: vec![HashSet::new(); max_heartbeats], 68 | heartbeat_index: HeartbeatIndex(0), 69 | heartbeat_interval, 70 | backoff_slack, 71 | } 72 | } 73 | 74 | /// Updates the backoff for a peer (if there is already a more restrictive backoff then this call 75 | /// doesn't change anything). 76 | pub(crate) fn update_backoff(&mut self, topic: &TopicHash, peer: &PeerId, time: Duration) { 77 | let instant = Instant::now() + time; 78 | let insert_into_backoffs_by_heartbeat = 79 | |heartbeat_index: HeartbeatIndex, 80 | backoffs_by_heartbeat: &mut Vec>, 81 | heartbeat_interval, 82 | backoff_slack| { 83 | let pair = (topic.clone(), *peer); 84 | let index = (heartbeat_index.0 85 | + Self::heartbeats(&time, heartbeat_interval) 86 | + backoff_slack as usize) 87 | % backoffs_by_heartbeat.len(); 88 | backoffs_by_heartbeat[index].insert(pair); 89 | HeartbeatIndex(index) 90 | }; 91 | match self 92 | .backoffs 93 | .entry(topic.clone()) 94 | .or_insert_with(HashMap::new) 95 | .entry(*peer) 96 | { 97 | Entry::Occupied(mut o) => { 98 | let (backoff, index) = o.get(); 99 | if backoff < &instant { 100 | let pair = (topic.clone(), *peer); 101 | if let Some(s) = self.backoffs_by_heartbeat.get_mut(index.0) { 102 | s.remove(&pair); 103 | } 104 | let index = insert_into_backoffs_by_heartbeat( 105 | self.heartbeat_index, 106 | &mut self.backoffs_by_heartbeat, 107 | &self.heartbeat_interval, 108 | self.backoff_slack, 109 | ); 110 | o.insert((instant, index)); 111 | } 112 | } 113 | Entry::Vacant(v) => { 114 | let index = insert_into_backoffs_by_heartbeat( 115 | self.heartbeat_index, 116 | &mut self.backoffs_by_heartbeat, 117 | &self.heartbeat_interval, 118 | self.backoff_slack, 119 | ); 120 | v.insert((instant, index)); 121 | } 122 | }; 123 | } 124 | 125 | /// Checks if a given peer is backoffed for the given topic. This method respects the 126 | /// configured BACKOFF_SLACK and may return true even if the backup is already over. 127 | /// It is guaranteed to return false if the backoff is not over and eventually if enough time 128 | /// passed true if the backoff is over. 129 | /// 130 | /// This method should be used for deciding if we can already send a GRAFT to a previously 131 | /// backoffed peer. 132 | pub(crate) fn is_backoff_with_slack(&self, topic: &TopicHash, peer: &PeerId) -> bool { 133 | self.backoffs 134 | .get(topic) 135 | .map_or(false, |m| m.contains_key(peer)) 136 | } 137 | 138 | pub(crate) fn get_backoff_time(&self, topic: &TopicHash, peer: &PeerId) -> Option { 139 | Self::get_backoff_time_from_backoffs(&self.backoffs, topic, peer) 140 | } 141 | 142 | fn get_backoff_time_from_backoffs( 143 | backoffs: &HashMap>, 144 | topic: &TopicHash, 145 | peer: &PeerId, 146 | ) -> Option { 147 | backoffs 148 | .get(topic) 149 | .and_then(|m| m.get(peer).map(|(i, _)| *i)) 150 | } 151 | 152 | /// Applies a heartbeat. That should be called regularly in intervals of length 153 | /// `heartbeat_interval`. 154 | pub(crate) fn heartbeat(&mut self) { 155 | // Clean up backoffs_by_heartbeat 156 | if let Some(s) = self.backoffs_by_heartbeat.get_mut(self.heartbeat_index.0) { 157 | let backoffs = &mut self.backoffs; 158 | let slack = self.heartbeat_interval * self.backoff_slack; 159 | let now = Instant::now(); 160 | s.retain(|(topic, peer)| { 161 | let keep = match Self::get_backoff_time_from_backoffs(backoffs, topic, peer) { 162 | Some(backoff_time) => backoff_time + slack > now, 163 | None => false, 164 | }; 165 | if !keep { 166 | //remove from backoffs 167 | if let Entry::Occupied(mut m) = backoffs.entry(topic.clone()) { 168 | if m.get_mut().remove(peer).is_some() && m.get().is_empty() { 169 | m.remove(); 170 | } 171 | } 172 | } 173 | 174 | keep 175 | }); 176 | } 177 | 178 | // Increase heartbeat index 179 | self.heartbeat_index = 180 | HeartbeatIndex((self.heartbeat_index.0 + 1) % self.backoffs_by_heartbeat.len()); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /waku-node/src/event_loop/event_loop.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use futures::StreamExt; 3 | use libp2p::swarm::SwarmEvent; 4 | use log::{debug, error, info, trace}; 5 | use tokio::sync::mpsc; 6 | 7 | use crate::behaviour; 8 | use crate::event_loop::command::Command; 9 | use crate::event_loop::event::Event; 10 | 11 | pub struct EventLoop { 12 | switch: libp2p::Swarm, 13 | command_source: mpsc::Receiver, 14 | event_sink: mpsc::Sender, 15 | } 16 | 17 | impl EventLoop { 18 | pub fn new( 19 | switch: libp2p::Swarm, 20 | command_source: mpsc::Receiver, 21 | event_sink: mpsc::Sender, 22 | ) -> Self { 23 | Self { 24 | switch, 25 | command_source, 26 | event_sink, 27 | } 28 | } 29 | 30 | pub async fn dispatch(mut self) { 31 | loop { 32 | tokio::select! { 33 | command = self.command_source.recv() => match command { 34 | Some(cmd) => { self.handle_command(cmd).await; }, 35 | None => { debug!("got empty command. terminating node event loop"); return }, 36 | }, 37 | event = self.switch.select_next_some() => match event { 38 | SwarmEvent::NewListenAddr { address, .. } => { 39 | // TODO: Send this event through the event_sink 40 | info!("switch listening on: {address:?}") 41 | }, 42 | SwarmEvent::Behaviour(behaviour::Event::WakuRelay(event)) => { 43 | self.handle_waku_relay_event(event).await; 44 | }, 45 | SwarmEvent::Behaviour(event) => debug!("{event:?}"), 46 | _ => {} 47 | }, 48 | } 49 | } 50 | } 51 | 52 | async fn handle_command(&mut self, cmd: Command) { 53 | match cmd { 54 | Command::SwitchListenOn { address, sender } => { 55 | trace!("handle command: {}", "switch_listen_on"); 56 | 57 | match self.switch.listen_on(address) { 58 | Ok(_) => sender.send(Ok(())), 59 | Err(e) => sender.send(Err(e.into())), 60 | } 61 | .unwrap_or_else(|e| { 62 | error!( 63 | "send '{}' command response failed: {:?}.", 64 | "switch_listen_on", e 65 | ); 66 | }); 67 | } 68 | Command::SwitchDial { address, sender } => { 69 | trace!("handle command: {}", "switch_dial"); 70 | 71 | match self.switch.dial(address) { 72 | Ok(_) => sender.send(Ok(())), 73 | Err(e) => sender.send(Err(e.into())), 74 | } 75 | .unwrap_or_else(|e| { 76 | error!("send '{}' command response failed: {:?}.", "switch_dial", e); 77 | }); 78 | } 79 | 80 | Command::RelaySubscribe { 81 | pubsub_topic, 82 | sender, 83 | } => { 84 | trace!("handle command: {}", "relay_subscribe"); 85 | 86 | if !self.switch.behaviour().waku_relay.is_enabled() { 87 | sender 88 | .send(Err(anyhow!("relay protocol disabled"))) 89 | .unwrap_or_else(|e| { 90 | error!( 91 | "send '{}' command response failed: {:?}.", 92 | "relay_subscribe", e 93 | ); 94 | }); 95 | return; 96 | } 97 | 98 | match self 99 | .switch 100 | .behaviour_mut() 101 | .waku_relay 102 | .as_mut() 103 | .unwrap() 104 | .subscribe(&pubsub_topic) 105 | { 106 | Ok(_) => sender.send(Ok(())), 107 | Err(e) => sender.send(Err(e.into())), 108 | } 109 | .unwrap_or_else(|e| { 110 | error!( 111 | "send '{}' command response failed: {:?}.", 112 | "relay_subscribe", e 113 | ); 114 | }); 115 | } 116 | Command::RelayUnsubscribe { 117 | pubsub_topic, 118 | sender, 119 | } => { 120 | trace!("handle command: {}", "relay_unsubscribe"); 121 | 122 | if !self.switch.behaviour().waku_relay.is_enabled() { 123 | sender 124 | .send(Err(anyhow!("relay protocol disabled"))) 125 | .unwrap_or_else(|e| { 126 | error!( 127 | "send '{}' command response failed: {:?}.", 128 | "relay_unsubscribe", e 129 | ); 130 | }); 131 | return; 132 | } 133 | 134 | match self 135 | .switch 136 | .behaviour_mut() 137 | .waku_relay 138 | .as_mut() 139 | .unwrap() 140 | .unsubscribe(&pubsub_topic) 141 | { 142 | Ok(_) => sender.send(Ok(())), 143 | Err(e) => sender.send(Err(e.into())), 144 | } 145 | .unwrap_or_else(|e| { 146 | error!( 147 | "send '{}' command response failed: {:?}.", 148 | "relay_unsubscribe", e 149 | ); 150 | }); 151 | } 152 | Command::RelayPublish { 153 | pubsub_topic, 154 | message, 155 | sender, 156 | } => { 157 | trace!("handle command: {}", "relay_publish"); 158 | 159 | if !self.switch.behaviour().waku_relay.is_enabled() { 160 | sender 161 | .send(Err(anyhow!("relay protocol disabled"))) 162 | .unwrap_or_else(|e| { 163 | error!( 164 | "send '{}' command response failed: {:?}.", 165 | "relay_publish", e 166 | ); 167 | }); 168 | return; 169 | } 170 | 171 | match self 172 | .switch 173 | .behaviour_mut() 174 | .waku_relay 175 | .as_mut() 176 | .unwrap() 177 | .publish(&pubsub_topic, message) 178 | { 179 | Ok(_) => sender.send(Ok(())), 180 | Err(e) => sender.send(Err(e.into())), 181 | } 182 | .unwrap_or_else(|e| { 183 | error!( 184 | "send '{}' command response failed: {:?}.", 185 | "relay_publish", e 186 | ); 187 | }); 188 | } 189 | } 190 | } 191 | 192 | async fn handle_waku_relay_event(&mut self, event: waku_relay::Event) { 193 | match event { 194 | waku_relay::Event::Message { 195 | pubsub_topic, 196 | message, 197 | } => { 198 | trace!("handle event: {}", "waku_relay_message"); 199 | 200 | self.event_sink 201 | .send(Event::WakuRelayMessage { 202 | pubsub_topic, 203 | message, 204 | }) 205 | .await 206 | .unwrap_or_else(|e| { 207 | error!("send '{}' event failed: {:?}.", "waku_relay_message", e); 208 | }); 209 | } 210 | _ => {} 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/subscription_filter.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Sigma Prime Pty Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | use std::collections::{BTreeSet, HashMap, HashSet}; 22 | 23 | use log::debug; 24 | 25 | use crate::gossipsub::TopicHash; 26 | use crate::gossipsub::types::Subscription; 27 | 28 | pub trait TopicSubscriptionFilter { 29 | /// Returns true iff the topic is of interest and we can subscribe to it. 30 | fn can_subscribe(&mut self, topic_hash: &TopicHash) -> bool; 31 | 32 | /// Filters a list of incoming subscriptions and returns a filtered set 33 | /// By default this deduplicates the subscriptions and calls 34 | /// [`Self::filter_incoming_subscription_set`] on the filtered set. 35 | fn filter_incoming_subscriptions<'a>( 36 | &mut self, 37 | subscriptions: &'a [Subscription], 38 | currently_subscribed_topics: &BTreeSet, 39 | ) -> Result, String> { 40 | let mut filtered_subscriptions: HashMap = HashMap::new(); 41 | for subscription in subscriptions { 42 | use std::collections::hash_map::Entry::*; 43 | match filtered_subscriptions.entry(subscription.topic_hash.clone()) { 44 | Occupied(entry) => { 45 | if entry.get().action != subscription.action { 46 | entry.remove(); 47 | } 48 | } 49 | Vacant(entry) => { 50 | entry.insert(subscription); 51 | } 52 | } 53 | } 54 | self.filter_incoming_subscription_set( 55 | filtered_subscriptions.into_values().collect(), 56 | currently_subscribed_topics, 57 | ) 58 | } 59 | 60 | /// Filters a set of deduplicated subscriptions 61 | /// By default this filters the elements based on [`Self::allow_incoming_subscription`]. 62 | fn filter_incoming_subscription_set<'a>( 63 | &mut self, 64 | mut subscriptions: HashSet<&'a Subscription>, 65 | _currently_subscribed_topics: &BTreeSet, 66 | ) -> Result, String> { 67 | subscriptions.retain(|s| { 68 | if self.allow_incoming_subscription(s) { 69 | true 70 | } else { 71 | debug!("Filtered incoming subscription {:?}", s); 72 | false 73 | } 74 | }); 75 | Ok(subscriptions) 76 | } 77 | 78 | /// Returns true iff we allow an incoming subscription. 79 | /// This is used by the default implementation of filter_incoming_subscription_set to decide 80 | /// whether to filter out a subscription or not. 81 | /// By default this uses can_subscribe to decide the same for incoming subscriptions as for 82 | /// outgoing ones. 83 | fn allow_incoming_subscription(&mut self, subscription: &Subscription) -> bool { 84 | self.can_subscribe(&subscription.topic_hash) 85 | } 86 | } 87 | 88 | //some useful implementers 89 | 90 | /// Allows all subscriptions 91 | #[derive(Default, Clone)] 92 | pub struct AllowAllSubscriptionFilter {} 93 | 94 | impl TopicSubscriptionFilter for AllowAllSubscriptionFilter { 95 | fn can_subscribe(&mut self, _: &TopicHash) -> bool { 96 | true 97 | } 98 | } 99 | 100 | /// Allows only whitelisted subscriptions 101 | #[derive(Default, Clone)] 102 | pub struct WhitelistSubscriptionFilter(pub HashSet); 103 | 104 | impl TopicSubscriptionFilter for WhitelistSubscriptionFilter { 105 | fn can_subscribe(&mut self, topic_hash: &TopicHash) -> bool { 106 | self.0.contains(topic_hash) 107 | } 108 | } 109 | 110 | /// Adds a max count to a given subscription filter 111 | pub struct MaxCountSubscriptionFilter { 112 | pub filter: T, 113 | pub max_subscribed_topics: usize, 114 | pub max_subscriptions_per_request: usize, 115 | } 116 | 117 | impl TopicSubscriptionFilter for MaxCountSubscriptionFilter { 118 | fn can_subscribe(&mut self, topic_hash: &TopicHash) -> bool { 119 | self.filter.can_subscribe(topic_hash) 120 | } 121 | 122 | fn filter_incoming_subscriptions<'a>( 123 | &mut self, 124 | subscriptions: &'a [Subscription], 125 | currently_subscribed_topics: &BTreeSet, 126 | ) -> Result, String> { 127 | if subscriptions.len() > self.max_subscriptions_per_request { 128 | return Err("too many subscriptions per request".into()); 129 | } 130 | let result = self 131 | .filter 132 | .filter_incoming_subscriptions(subscriptions, currently_subscribed_topics)?; 133 | 134 | use crate::gossipsub::types::SubscriptionAction::*; 135 | 136 | let mut unsubscribed = 0; 137 | let mut new_subscribed = 0; 138 | for s in &result { 139 | let currently_contained = currently_subscribed_topics.contains(&s.topic_hash); 140 | match s.action { 141 | Unsubscribe => { 142 | if currently_contained { 143 | unsubscribed += 1; 144 | } 145 | } 146 | Subscribe => { 147 | if !currently_contained { 148 | new_subscribed += 1; 149 | } 150 | } 151 | } 152 | } 153 | 154 | if new_subscribed + currently_subscribed_topics.len() 155 | > self.max_subscribed_topics + unsubscribed 156 | { 157 | return Err("too many subscribed topics".into()); 158 | } 159 | 160 | Ok(result) 161 | } 162 | } 163 | 164 | /// Combines two subscription filters 165 | pub struct CombinedSubscriptionFilters { 166 | pub filter1: T, 167 | pub filter2: S, 168 | } 169 | 170 | impl TopicSubscriptionFilter for CombinedSubscriptionFilters 171 | where 172 | T: TopicSubscriptionFilter, 173 | S: TopicSubscriptionFilter, 174 | { 175 | fn can_subscribe(&mut self, topic_hash: &TopicHash) -> bool { 176 | self.filter1.can_subscribe(topic_hash) && self.filter2.can_subscribe(topic_hash) 177 | } 178 | 179 | fn filter_incoming_subscription_set<'a>( 180 | &mut self, 181 | subscriptions: HashSet<&'a Subscription>, 182 | currently_subscribed_topics: &BTreeSet, 183 | ) -> Result, String> { 184 | let intermediate = self 185 | .filter1 186 | .filter_incoming_subscription_set(subscriptions, currently_subscribed_topics)?; 187 | self.filter2 188 | .filter_incoming_subscription_set(intermediate, currently_subscribed_topics) 189 | } 190 | } 191 | 192 | pub struct CallbackSubscriptionFilter(pub T) 193 | where 194 | T: FnMut(&TopicHash) -> bool; 195 | 196 | impl TopicSubscriptionFilter for CallbackSubscriptionFilter 197 | where 198 | T: FnMut(&TopicHash) -> bool, 199 | { 200 | fn can_subscribe(&mut self, topic_hash: &TopicHash) -> bool { 201 | (self.0)(topic_hash) 202 | } 203 | } 204 | 205 | ///A subscription filter that filters topics based on a regular expression. 206 | pub struct RegexSubscriptionFilter(pub regex::Regex); 207 | 208 | impl TopicSubscriptionFilter for RegexSubscriptionFilter { 209 | fn can_subscribe(&mut self, topic_hash: &TopicHash) -> bool { 210 | self.0.is_match(topic_hash.as_str()) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Sigma Prime Pty Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | //! Implementation of the [Gossipsub](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/README.md) protocol. 22 | //! 23 | //! Gossipsub is a P2P pubsub (publish/subscription) routing layer designed to extend upon 24 | //! floodsub and meshsub routing protocols. 25 | //! 26 | //! # Overview 27 | //! 28 | //! *Note: The gossipsub protocol specifications 29 | //! () provide an outline for the 30 | //! routing protocol. They should be consulted for further detail.* 31 | //! 32 | //! Gossipsub is a blend of meshsub for data and randomsub for mesh metadata. It provides bounded 33 | //! degree and amplification factor with the meshsub construction and augments it using gossip 34 | //! propagation of metadata with the randomsub technique. 35 | //! 36 | //! The router maintains an overlay mesh network of peers on which to efficiently send messages and 37 | //! metadata. Peers use control messages to broadcast and request known messages and 38 | //! subscribe/unsubscribe from topics in the mesh network. 39 | //! 40 | //! # Important Discrepancies 41 | //! 42 | //! This section outlines the current implementation's potential discrepancies from that of other 43 | //! implementations, due to undefined elements in the current specification. 44 | //! 45 | //! - **Topics** - In gossipsub, topics configurable by the `hash_topics` configuration parameter. 46 | //! Topics are of type [`TopicHash`]. The current go implementation uses raw utf-8 strings, and this 47 | //! is default configuration in rust-libp2p. Topics can be hashed (SHA256 hashed then base64 48 | //! encoded) by setting the `hash_topics` configuration parameter to true. 49 | //! 50 | //! - **Sequence Numbers** - A message on the gossipsub network is identified by the source 51 | //! [`libp2p_core::PeerId`] and a nonce (sequence number) of the message. The sequence numbers in 52 | //! this implementation are sent as raw bytes across the wire. They are 64-bit big-endian unsigned 53 | //! integers. When messages are signed, they are monotonically increasing integers starting from a 54 | //! random value and wrapping around u64::MAX. When messages are unsigned, they are chosen at random. 55 | //! NOTE: These numbers are sequential in the current go implementation. 56 | //! 57 | //! # Peer Discovery 58 | //! 59 | //! Gossipsub does not provide peer discovery by itself. Peer discovery is the process by which 60 | //! peers in a p2p network exchange information about each other among other reasons to become resistant 61 | //! against the failure or replacement of the 62 | //! [boot nodes](https://docs.libp2p.io/reference/glossary/#boot-node) of the network. 63 | //! 64 | //! Peer 65 | //! discovery can e.g. be implemented with the help of the [Kademlia](https://github.com/libp2p/specs/blob/master/kad-dht/README.md) protocol 66 | //! in combination with the [Identify](https://github.com/libp2p/specs/tree/master/identify) protocol. See the 67 | //! Kademlia implementation documentation for more information. 68 | //! 69 | //! # Using Gossipsub 70 | //! 71 | //! ## Gossipsub Config 72 | //! 73 | //! The [`Config`] struct specifies various network performance/tuning configuration 74 | //! parameters. Specifically it specifies: 75 | //! 76 | //! [`Config`]: struct.Config.html 77 | //! 78 | //! This struct implements the [`Default`] trait and can be initialised via 79 | //! [`Config::default()`]. 80 | //! 81 | //! 82 | //! ## Behaviour 83 | //! 84 | //! The [`Behaviour`] struct implements the [`libp2p_swarm::NetworkBehaviour`] trait allowing it to 85 | //! act as the routing behaviour in a [`libp2p_swarm::Swarm`]. This struct requires an instance of 86 | //! [`libp2p_core::PeerId`] and [`Config`]. 87 | //! 88 | //! [`Behaviour`]: struct.Behaviour.html 89 | 90 | //! ## Example 91 | //! 92 | //! An example of initialising a gossipsub compatible swarm: 93 | //! 94 | //! ``` 95 | //! use libp2p::identity::Keypair; 96 | //! use libp2p::core::Multiaddr; 97 | //! use libp2p::core::transport::MemoryTransport; 98 | //! use libp2p::core::transport::Transport; 99 | //! use waku_relay::gossipsub::MessageAuthenticity; 100 | //! 101 | //! let local_key = Keypair::generate_ed25519(); 102 | //! let local_peer_id = libp2p::identity::PeerId::from(local_key.public()); 103 | //! 104 | //! // Set up an encrypted TCP Transport over yamux 105 | //! // This is test transport (memory). 106 | //! let transport = MemoryTransport::default() 107 | //! .upgrade(libp2p::core::upgrade::Version::V1) 108 | //! .authenticate(libp2p::noise::Config::new(&local_key).unwrap()) 109 | //! .multiplex(libp2p::yamux::Config::default()) 110 | //! .boxed(); 111 | //! 112 | //! // Create a Gossipsub topic 113 | //! let topic = waku_relay::gossipsub::IdentTopic::new("example"); 114 | //! 115 | //! // Set the message authenticity - How we expect to publish messages 116 | //! // Here we expect the publisher to sign the message with their key. 117 | //! let message_authenticity = MessageAuthenticity::Signed(local_key); 118 | //! 119 | //! // Create a Swarm to manage peers and events 120 | //! let mut swarm = { 121 | //! // set default parameters for gossipsub 122 | //! let gossipsub_config = waku_relay::gossipsub::Config::default(); 123 | //! // build a gossipsub network behaviour 124 | //! let mut gossipsub: waku_relay::gossipsub::Behaviour = 125 | //! waku_relay::gossipsub::Behaviour::new(message_authenticity, gossipsub_config).unwrap(); 126 | //! // subscribe to the topic 127 | //! gossipsub.subscribe(&topic); 128 | //! // create the swarm (use an executor in a real example) 129 | //! libp2p::swarm::SwarmBuilder::without_executor( 130 | //! transport, 131 | //! gossipsub, 132 | //! local_peer_id, 133 | //! ).build() 134 | //! }; 135 | //! 136 | //! // Listen on a memory transport. 137 | //! let memory: Multiaddr = libp2p::core::multiaddr::Protocol::Memory(10).into(); 138 | //! let addr = swarm.listen_on(memory).unwrap(); 139 | //! println!("Listening on {:?}", addr); 140 | //! ``` 141 | 142 | #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] 143 | 144 | pub use self::behaviour::Behaviour; 145 | pub use self::config::MessageAuthenticity; 146 | pub use self::config::{Config, ConfigBuilder, ValidationMode, Version}; 147 | pub use self::error::{PublishError, SubscriptionError}; 148 | pub use self::event::Event; 149 | pub use self::message_id::{FastMessageId, MessageId}; 150 | pub use self::metrics::Config as MetricsConfig; 151 | pub use self::peer_score::{ 152 | score_parameter_decay, score_parameter_decay_with_base, PeerScoreParams, PeerScoreThresholds, 153 | TopicScoreParams, 154 | }; 155 | pub use self::subscription_filter::{ 156 | AllowAllSubscriptionFilter, CallbackSubscriptionFilter, CombinedSubscriptionFilters, 157 | MaxCountSubscriptionFilter, RegexSubscriptionFilter, TopicSubscriptionFilter, 158 | WhitelistSubscriptionFilter, 159 | }; 160 | pub use self::topic::{Hasher, Topic, TopicHash}; 161 | pub use self::transform::{DataTransform, IdentityTransform}; 162 | pub use self::types::{Message, MessageAcceptance, RawMessage, Rpc}; 163 | 164 | mod backoff; 165 | mod behaviour; 166 | mod codec; 167 | mod config; 168 | mod connection_manager; 169 | mod error; 170 | mod event; 171 | mod gossip_promises; 172 | mod handler; 173 | mod heartbeat; 174 | mod mcache; 175 | mod message_id; 176 | mod metrics; 177 | mod peer_score; 178 | mod protocol; 179 | mod rpc; 180 | mod seq_no; 181 | mod signing; 182 | mod subscription_filter; 183 | mod time_cache; 184 | mod topic; 185 | mod transform; 186 | mod types; 187 | 188 | pub type IdentTopic = Topic; 189 | pub type Sha256Topic = Topic; 190 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/types.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Sigma Prime Pty Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | //! A collection of types using the Gossipsub system. 22 | use std::fmt; 23 | 24 | use libp2p::swarm::ConnectionId; 25 | use libp2p::PeerId; 26 | use prometheus_client::encoding::EncodeLabelValue; 27 | use prost::Message as _; 28 | 29 | use crate::gossipsub::mcache::CachedMessage; 30 | use crate::gossipsub::message_id::MessageId; 31 | use crate::gossipsub::rpc::{MessageProto, MessageRpc}; 32 | use crate::gossipsub::topic::TopicHash; 33 | 34 | /// Validation kinds from the application for received messages. 35 | #[derive(Debug)] 36 | pub enum MessageAcceptance { 37 | /// The message is considered valid, and it should be delivered and forwarded to the network. 38 | Accept, 39 | /// The message is considered invalid, and it should be rejected and trigger the P₄ penalty. 40 | Reject, 41 | /// The message is neither delivered nor forwarded to the network, but the router does not 42 | /// trigger the P₄ penalty. 43 | Ignore, 44 | } 45 | 46 | /// Describes the types of peers that can exist in the gossipsub context. 47 | #[derive(Debug, Copy, Clone, PartialEq, Hash, EncodeLabelValue, Eq)] 48 | pub enum PeerKind { 49 | /// A gossipsub 1.1 peer. 50 | Gossipsubv1_1, 51 | /// A gossipsub 1.0 peer. 52 | Gossipsub, 53 | /// A floodsub peer. 54 | Floodsub, 55 | /// The peer doesn't support any of the protocols. 56 | NotSupported, 57 | } 58 | 59 | impl PeerKind { 60 | pub fn is_gossipsub(&self) -> bool { 61 | matches!(self, Self::Gossipsub | Self::Gossipsubv1_1) 62 | } 63 | 64 | pub fn is_floodsub(&self) -> bool { 65 | matches!(self, Self::Floodsub) 66 | } 67 | } 68 | 69 | impl fmt::Display for PeerKind { 70 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 71 | let kind = match self { 72 | Self::NotSupported => "Not Supported", 73 | Self::Floodsub => "Floodsub", 74 | Self::Gossipsub => "Gossipsub v1.0", 75 | Self::Gossipsubv1_1 => "Gossipsub v1.1", 76 | }; 77 | f.write_str(kind) 78 | } 79 | } 80 | 81 | /// A message received by the gossipsub system and stored locally in caches.. 82 | #[derive(Clone, PartialEq, Eq, Hash, Debug)] 83 | pub struct RawMessage { 84 | /// Id of the peer that published this message. 85 | pub source: Option, 86 | /// Content of the message. Its meaning is out of scope of this library. 87 | pub data: Vec, 88 | /// A random sequence number. 89 | pub sequence_number: Option, 90 | /// The topic this message belongs to 91 | pub topic: TopicHash, 92 | /// The signature of the message if it's signed. 93 | pub signature: Option>, 94 | /// The public key of the message if it is signed and the source [`PeerId`] cannot be inlined. 95 | pub key: Option>, 96 | } 97 | 98 | impl RawMessage { 99 | /// Calculates the encoded length of this message (used for calculating metrics). 100 | pub fn raw_protobuf_len(&self) -> usize { 101 | let message: MessageProto = self.clone().into(); 102 | message.encoded_len() 103 | } 104 | } 105 | 106 | impl From for RawMessage { 107 | fn from(rpc: MessageRpc) -> Self { 108 | Self { 109 | source: rpc.source(), 110 | data: rpc.data().to_vec(), 111 | sequence_number: rpc.sequence_number(), 112 | topic: rpc.topic().into(), 113 | signature: rpc.signature().map(|s| s.to_vec()), 114 | key: rpc.key().map(|s| s.to_vec()), 115 | } 116 | } 117 | } 118 | 119 | impl From for RawMessage { 120 | fn from(message: CachedMessage) -> Self { 121 | Self { 122 | source: message.source, 123 | data: message.data, 124 | sequence_number: message.sequence_number, 125 | topic: message.topic, 126 | signature: message.signature, 127 | key: message.key, 128 | } 129 | } 130 | } 131 | 132 | /// The message sent to the user after a [`RawMessage`] has been transformed by a 133 | /// [`crate::DataTransform`]. 134 | #[derive(Clone, PartialEq, Eq, Hash)] 135 | pub struct Message { 136 | /// Id of the peer that published this message. 137 | pub source: Option, 138 | 139 | /// Content of the message. 140 | pub data: Vec, 141 | 142 | /// A random sequence number. 143 | pub sequence_number: Option, 144 | 145 | /// The topic this message belongs to 146 | pub topic: TopicHash, 147 | } 148 | 149 | impl fmt::Debug for Message { 150 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 151 | f.debug_struct("Message") 152 | .field( 153 | "data", 154 | &format_args!("{:<20}", &hex_fmt::HexFmt(&self.data)), 155 | ) 156 | .field("source", &self.source) 157 | .field("sequence_number", &self.sequence_number) 158 | .field("topic", &self.topic) 159 | .finish() 160 | } 161 | } 162 | 163 | /// A subscription received by the gossipsub system. 164 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 165 | pub struct Subscription { 166 | /// Action to perform. 167 | pub action: SubscriptionAction, 168 | /// The topic from which to subscribe or unsubscribe. 169 | pub topic_hash: TopicHash, 170 | } 171 | 172 | /// Action that a subscription wants to perform. 173 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 174 | pub enum SubscriptionAction { 175 | /// The remote wants to subscribe to the given topic. 176 | Subscribe, 177 | /// The remote wants to unsubscribe from the given topic. 178 | Unsubscribe, 179 | } 180 | 181 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 182 | pub struct PeerInfo { 183 | pub peer_id: Option, 184 | //TODO add this when RFC: Signed Address Records got added to the spec (see pull request 185 | // https://github.com/libp2p/specs/pull/217) 186 | //pub signed_peer_record: ?, 187 | } 188 | 189 | /// A Control message received by the gossipsub system. 190 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 191 | pub enum ControlAction { 192 | /// Node broadcasts known messages per topic - IHave control message. 193 | IHave { 194 | /// The topic of the messages. 195 | topic_hash: TopicHash, 196 | /// A list of known message ids (peer_id + sequence _number) as a string. 197 | message_ids: Vec, 198 | }, 199 | /// The node requests specific message ids (peer_id + sequence _number) - IWant control message. 200 | IWant { 201 | /// A list of known message ids (peer_id + sequence _number) as a string. 202 | message_ids: Vec, 203 | }, 204 | /// The node has been added to the mesh - Graft control message. 205 | Graft { 206 | /// The mesh topic the peer should be added to. 207 | topic_hash: TopicHash, 208 | }, 209 | /// The node has been removed from the mesh - Prune control message. 210 | Prune { 211 | /// The mesh topic the peer should be removed from. 212 | topic_hash: TopicHash, 213 | /// A list of peers to be proposed to the removed peer as peer exchange 214 | peers: Vec, 215 | /// The backoff time in seconds before we allow to reconnect 216 | backoff: Option, 217 | }, 218 | } 219 | 220 | /// An RPC received/sent. 221 | #[derive(Clone, PartialEq, Eq, Hash)] 222 | pub struct Rpc { 223 | /// List of messages that were part of this RPC query. 224 | pub messages: Vec, 225 | /// List of subscriptions. 226 | pub subscriptions: Vec, 227 | /// List of Gossipsub control messages. 228 | pub control_msgs: Vec, 229 | } 230 | 231 | impl fmt::Debug for Rpc { 232 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 233 | let mut b = f.debug_struct("GossipsubRpc"); 234 | if !self.messages.is_empty() { 235 | b.field("messages", &self.messages); 236 | } 237 | if !self.subscriptions.is_empty() { 238 | b.field("subscriptions", &self.subscriptions); 239 | } 240 | if !self.control_msgs.is_empty() { 241 | b.field("control_msgs", &self.control_msgs); 242 | } 243 | b.finish() 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /waku-relay/src/gossipsub/signing/signer.rs: -------------------------------------------------------------------------------- 1 | use libp2p::identity::{Keypair, PeerId, SigningError}; 2 | use prost::Message as _; 3 | 4 | use crate::gossipsub::rpc::{MessageProto, MessageRpc}; 5 | 6 | pub trait MessageSigner { 7 | fn author(&self) -> Option<&PeerId>; 8 | 9 | fn sign(&self, message: &mut MessageRpc) -> Result<(), SigningError>; 10 | } 11 | 12 | /// A [`MessageSigner`] implementation that does not sign messages. 13 | pub struct NoopSigner; 14 | 15 | impl NoopSigner { 16 | pub fn new() -> Self { 17 | Self {} 18 | } 19 | } 20 | 21 | impl MessageSigner for NoopSigner { 22 | fn author(&self) -> Option<&PeerId> { 23 | None 24 | } 25 | 26 | fn sign(&self, _message: &mut MessageRpc) -> Result<(), SigningError> { 27 | Ok(()) 28 | } 29 | } 30 | 31 | const SIGNING_PREFIX: &[u8] = b"libp2p-pubsub:"; 32 | 33 | /// Generate the Libp2p gossipsub signature for a message. 34 | /// 35 | /// The signature is calculated over the bytes "libp2p-pubsub:". 36 | pub fn generate_message_signature( 37 | message: &MessageProto, 38 | keypair: &Keypair, 39 | ) -> Result, SigningError> { 40 | let mut msg = message.clone(); 41 | msg.signature = None; 42 | msg.key = None; 43 | 44 | // Construct the signature bytes 45 | let mut sign_bytes = Vec::with_capacity(SIGNING_PREFIX.len() + msg.encoded_len()); 46 | sign_bytes.extend(SIGNING_PREFIX.to_vec()); 47 | sign_bytes.extend(msg.encode_to_vec()); 48 | 49 | keypair.sign(&sign_bytes) 50 | } 51 | 52 | /// A [`MessageSigner`] implementation that uses a [`Keypair`] to sign messages. 53 | /// 54 | /// This signer will include the public key in the [`Message::key`] field if it is too large to be 55 | /// inlined in the [`Message::from`] field. 56 | /// 57 | /// The signature is calculated over the bytes "libp2p-pubsub:". This is specified 58 | /// in the libp2p pubsub spec: https://github.com/libp2p/specs/tree/master/pubsub#message-signing 59 | pub struct Libp2pSigner { 60 | keypair: Keypair, 61 | author: PeerId, 62 | inline_key: Option>, 63 | } 64 | 65 | impl Libp2pSigner { 66 | pub fn new(keypair: &Keypair) -> Self { 67 | let peer_id = keypair.public().to_peer_id(); 68 | let key_enc = keypair.public().encode_protobuf(); 69 | let inline_key = if key_enc.len() <= 42 { 70 | // The public key can be inlined in [`Message::from`], so we don't include it 71 | // specifically in the [`Message::key`] field. 72 | None 73 | } else { 74 | // Include the protobuf encoding of the public key in the message. 75 | Some(key_enc) 76 | }; 77 | 78 | Self { 79 | keypair: keypair.clone(), 80 | author: peer_id, 81 | inline_key, 82 | } 83 | } 84 | } 85 | 86 | impl MessageSigner for Libp2pSigner { 87 | fn author(&self) -> Option<&PeerId> { 88 | Some(&self.author) 89 | } 90 | 91 | fn sign(&self, message: &mut MessageRpc) -> Result<(), SigningError> { 92 | // Libp2p's pubsub message signature generation requires the `from` field to be set. 93 | message.set_source(Some(self.author)); 94 | 95 | let signature = generate_message_signature(message.as_proto(), &self.keypair)?; 96 | message.set_signature(Some(signature)); 97 | message.set_key(self.inline_key.clone()); 98 | 99 | Ok(()) 100 | } 101 | } 102 | 103 | /// A [`MessageSigner`] implementation that uses a [`PeerId`] to sign messages. 104 | pub struct AuthorOnlySigner { 105 | author: PeerId, 106 | } 107 | 108 | impl AuthorOnlySigner { 109 | pub fn new(author: PeerId) -> Self { 110 | Self { author } 111 | } 112 | } 113 | 114 | impl MessageSigner for AuthorOnlySigner { 115 | fn author(&self) -> Option<&PeerId> { 116 | Some(&self.author) 117 | } 118 | 119 | fn sign(&self, message: &mut MessageRpc) -> Result<(), SigningError> { 120 | message.set_source(Some(self.author)); 121 | Ok(()) 122 | } 123 | } 124 | 125 | /// A [`MessageSigner`] implementation that uses a random [`PeerId`] to sign messages. 126 | pub struct RandomAuthorSigner; 127 | 128 | impl RandomAuthorSigner { 129 | pub fn new() -> Self { 130 | Self {} 131 | } 132 | } 133 | 134 | impl MessageSigner for RandomAuthorSigner { 135 | fn author(&self) -> Option<&PeerId> { 136 | None 137 | } 138 | 139 | fn sign(&self, message: &mut MessageRpc) -> Result<(), SigningError> { 140 | message.set_source(Some(PeerId::random())); 141 | Ok(()) 142 | } 143 | } 144 | 145 | #[cfg(test)] 146 | mod tests { 147 | use assert_matches::assert_matches; 148 | use bytes::Bytes; 149 | 150 | use crate::gossipsub::signing::validator::verify_message_signature; 151 | 152 | use super::*; 153 | 154 | fn test_keypair() -> Keypair { 155 | Keypair::generate_secp256k1() 156 | } 157 | 158 | fn test_message() -> MessageRpc { 159 | MessageRpc::new_with_sequence_number( 160 | "test-topic".to_string(), 161 | b"test-message".to_vec(), 162 | Some(42), 163 | ) 164 | } 165 | 166 | #[test] 167 | fn generate_signature() { 168 | //// Given 169 | let keypair = test_keypair(); 170 | let message = test_message(); 171 | 172 | //// When 173 | let signature = 174 | generate_message_signature(message.as_proto(), &keypair).expect("signing failed"); 175 | 176 | //// Then 177 | assert!(verify_message_signature( 178 | message.as_proto(), 179 | &Bytes::from(signature), 180 | &keypair.public() 181 | )); 182 | } 183 | 184 | mod libp2p_signer { 185 | use crate::gossipsub::signing::validator::{MessageValidator, StrictMessageValidator}; 186 | 187 | use super::*; 188 | 189 | #[test] 190 | fn sign() { 191 | //// Given 192 | let keypair = test_keypair(); 193 | let author = keypair.public().to_peer_id(); 194 | 195 | let signer = Libp2pSigner::new(&keypair); 196 | 197 | let mut message = test_message(); 198 | 199 | //// When 200 | signer.sign(&mut message).expect("signing failed"); 201 | 202 | //// Then 203 | assert_matches!(message.source(), Some(from_peer_id) => { 204 | assert_eq!(from_peer_id, author); 205 | }); 206 | assert_matches!(message.signature(), Some(signature) => { 207 | assert!(verify_message_signature(message.as_proto(), signature, &keypair.public())); 208 | }); 209 | assert!(message.key().is_none()); // Already inlined in `from` field 210 | 211 | // Validate with strict message validator 212 | let validator = StrictMessageValidator::new(); 213 | assert_matches!(validator.validate(&message), Ok(())); 214 | } 215 | } 216 | 217 | mod author_only_signer { 218 | use crate::gossipsub::signing::validator::{MessageValidator, PermissiveMessageValidator}; 219 | 220 | use super::*; 221 | 222 | #[test] 223 | fn sign() { 224 | //// Given 225 | let keypair = test_keypair(); 226 | let author = keypair.public().to_peer_id(); 227 | 228 | let signer = AuthorOnlySigner::new(author); 229 | 230 | let mut message = test_message(); 231 | 232 | //// When 233 | signer.sign(&mut message).expect("signing failed"); 234 | 235 | //// Then 236 | assert_matches!(message.source(), Some(from_peer_id) => { 237 | assert_eq!(from_peer_id, author); 238 | }); 239 | assert!(message.signature().is_none()); 240 | assert!(message.key().is_none()); 241 | 242 | // Validate with permissive validator 243 | let validator = PermissiveMessageValidator::new(); 244 | assert_matches!(validator.validate(&message), Ok(())); 245 | } 246 | } 247 | 248 | mod random_author_signer { 249 | use crate::gossipsub::signing::validator::{MessageValidator, PermissiveMessageValidator}; 250 | 251 | use super::*; 252 | 253 | #[test] 254 | fn sign() { 255 | //// Given 256 | let signer = RandomAuthorSigner::new(); 257 | 258 | let mut message = test_message(); 259 | 260 | //// When 261 | signer.sign(&mut message).expect("signing failed"); 262 | 263 | //// Then 264 | assert!(message.source().is_some()); 265 | assert!(message.signature().is_none()); 266 | assert!(message.key().is_none()); 267 | 268 | // Validate with permissive validator 269 | let validator = PermissiveMessageValidator::new(); 270 | assert_matches!(validator.validate(&message), Ok(())); 271 | } 272 | } 273 | 274 | mod noop_signer { 275 | use crate::gossipsub::signing::validator::{MessageValidator, PermissiveMessageValidator}; 276 | 277 | use super::*; 278 | 279 | #[test] 280 | fn sign() { 281 | //// Given 282 | let signer = NoopSigner::new(); 283 | 284 | let mut message = test_message(); 285 | 286 | //// When 287 | signer.sign(&mut message).expect("signing failed"); 288 | 289 | //// Then 290 | assert!(message.source().is_none()); 291 | assert!(message.signature().is_none()); 292 | assert!(message.key().is_none()); 293 | 294 | // Validate with anonymous validator 295 | let validator = PermissiveMessageValidator::new(); 296 | assert_matches!(validator.validate(&message), Ok(())); 297 | } 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /waku-core/src/pubsub_topic/namespaced.rs: -------------------------------------------------------------------------------- 1 | use crate::pubsub_topic::PubsubTopic; 2 | 3 | const TOPIC_NAMED_SHARDING_PREFIX: &str = "/waku/2/"; 4 | const TOPIC_STATIC_SHARDING_PREFIX: &str = "/waku/2/rs/"; 5 | 6 | pub enum NsPubsubTopic { 7 | StaticSharding { cluster: u16, shard: u16 }, 8 | NamedSharding(String), 9 | Raw(String), 10 | } 11 | 12 | fn parse_static_sharding(topic: &str) -> anyhow::Result<(u16, u16)> { 13 | let mut parts = topic 14 | .strip_prefix(TOPIC_STATIC_SHARDING_PREFIX) 15 | .ok_or_else(|| anyhow::anyhow!("invalid prefix"))? 16 | .split('/'); 17 | 18 | let cluster = parts 19 | .next() 20 | .ok_or_else(|| anyhow::anyhow!("missing cluster index"))? 21 | .parse::() 22 | .map_err(|_| anyhow::anyhow!("invalid cluster index"))?; 23 | let shard = parts 24 | .next() 25 | .ok_or_else(|| anyhow::anyhow!("missing shard index"))? 26 | .parse::() 27 | .map_err(|_| anyhow::anyhow!("invalid shard index"))?; 28 | 29 | if parts.next().is_some() { 30 | anyhow::bail!("too many parts"); 31 | } 32 | 33 | Ok((cluster, shard)) 34 | } 35 | 36 | fn parse_named_sharding(topic: &str) -> anyhow::Result { 37 | Ok(topic 38 | .strip_prefix(TOPIC_NAMED_SHARDING_PREFIX) 39 | .ok_or_else(|| anyhow::anyhow!("invalid prefix"))? 40 | .to_string()) 41 | } 42 | 43 | impl NsPubsubTopic { 44 | pub fn new_static_sharding(cluster: u16, shard: u16) -> Self { 45 | Self::StaticSharding { cluster, shard } 46 | } 47 | 48 | pub fn new_named_sharding(name: S) -> Self 49 | where 50 | S: Into, 51 | { 52 | Self::NamedSharding(name.into()) 53 | } 54 | 55 | pub fn raw(name: S) -> Self 56 | where 57 | S: Into, 58 | { 59 | Self::Raw(name.into()) 60 | } 61 | } 62 | 63 | impl std::str::FromStr for NsPubsubTopic { 64 | type Err = anyhow::Error; 65 | 66 | fn from_str(s: &str) -> Result { 67 | if s.starts_with(TOPIC_STATIC_SHARDING_PREFIX) { 68 | return parse_static_sharding(s) 69 | .map(|(cluster, shard)| Self::StaticSharding { cluster, shard }); 70 | } 71 | 72 | if s.starts_with(TOPIC_NAMED_SHARDING_PREFIX) { 73 | return parse_named_sharding(s).map(Self::NamedSharding); 74 | } 75 | 76 | Ok(Self::Raw(s.to_string())) 77 | } 78 | } 79 | 80 | impl ToString for NsPubsubTopic { 81 | fn to_string(&self) -> String { 82 | match self { 83 | Self::StaticSharding { cluster, shard } => { 84 | format!("{}{}/{}", TOPIC_STATIC_SHARDING_PREFIX, cluster, shard) 85 | } 86 | Self::NamedSharding(name) => { 87 | format!("{}{}", TOPIC_NAMED_SHARDING_PREFIX, name) 88 | } 89 | Self::Raw(name) => name.clone(), 90 | } 91 | } 92 | } 93 | 94 | impl std::fmt::Debug for NsPubsubTopic { 95 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 96 | match self { 97 | Self::StaticSharding { cluster, shard } => { 98 | write!(f, "StaticSharding(cluster={},shard={})", cluster, shard) 99 | } 100 | Self::NamedSharding(name) => write!(f, "NamedSharding({})", name), 101 | Self::Raw(name) => write!(f, "Raw({})", name), 102 | } 103 | } 104 | } 105 | 106 | impl TryFrom for NsPubsubTopic { 107 | type Error = anyhow::Error; 108 | 109 | fn try_from(topic: PubsubTopic) -> Result { 110 | topic.to_string().parse() 111 | } 112 | } 113 | 114 | #[cfg(test)] 115 | mod tests { 116 | use assert_matches::assert_matches; 117 | 118 | use crate::pubsub_topic::PubsubTopic; 119 | 120 | use super::*; 121 | 122 | #[test] 123 | fn test_parse_named_sharding_topic_valid_string() { 124 | // Given 125 | let topic = "/waku/2/abc"; 126 | 127 | // When 128 | let name = parse_named_sharding(topic).unwrap(); 129 | 130 | // Then 131 | assert_eq!(name, "abc"); 132 | } 133 | 134 | #[test] 135 | fn test_parse_named_sharding_topic_invalid_prefix() { 136 | // Given 137 | let topic = "/waku/1/1/2"; 138 | 139 | // When 140 | let result = parse_named_sharding(topic); 141 | 142 | // Then 143 | assert!(result.is_err()); 144 | } 145 | 146 | #[test] 147 | fn test_parse_static_sharding_topic_valid_string() { 148 | // Given 149 | let topic = "/waku/2/rs/1/2"; 150 | 151 | // When 152 | let (cluster, shard) = parse_static_sharding(topic).unwrap(); 153 | 154 | // Then 155 | assert_eq!(cluster, 1); 156 | assert_eq!(shard, 2); 157 | } 158 | 159 | #[test] 160 | fn test_parse_static_sharding_topic_invalid_prefix() { 161 | // Given 162 | let topic = "/waku/2/1/2"; 163 | 164 | // When 165 | let result = parse_static_sharding(topic); 166 | 167 | // Then 168 | assert!(result.is_err()); 169 | } 170 | 171 | #[test] 172 | fn test_parse_static_sharding_topic_invalid_too_many_parts() { 173 | // Given 174 | let topic = "/waku/2/rs/1/2/3"; 175 | 176 | // When 177 | let result = parse_static_sharding(topic); 178 | 179 | // Then 180 | assert!(result.is_err()); 181 | } 182 | 183 | #[test] 184 | fn test_parse_static_sharding_topic_missing_cluster_index() { 185 | // Given 186 | let topic = "/waku/2/rs/"; 187 | 188 | // When 189 | let result = parse_static_sharding(topic); 190 | 191 | // Then 192 | assert!(result.is_err()); 193 | } 194 | 195 | #[test] 196 | fn test_parse_static_sharding_topic_invalid_cluster_index() { 197 | // Given 198 | let topic = "/waku/2/rs/1a/2"; 199 | 200 | // When 201 | let result = parse_static_sharding(topic); 202 | 203 | // Then 204 | assert!(result.is_err()); 205 | } 206 | 207 | #[test] 208 | fn test_parse_static_sharding_topic_missing_shard_index() { 209 | // Given 210 | let topic = "/waku/2/rs/1"; 211 | 212 | // When 213 | let result = parse_static_sharding(topic); 214 | 215 | // Then 216 | assert!(result.is_err()); 217 | } 218 | 219 | #[test] 220 | fn test_parse_static_sharding_topic_invalid_shard() { 221 | // Given 222 | let topic = "/waku/2/rs/1/2a"; 223 | 224 | // When 225 | let result = parse_static_sharding(topic); 226 | 227 | // Then 228 | assert!(result.is_err()); 229 | } 230 | 231 | #[test] 232 | fn test_ns_topic_static_sharding_from_str() { 233 | // Given 234 | let topic = "/waku/2/rs/1/2"; 235 | 236 | // When 237 | let ns_topic = topic.parse::().unwrap(); 238 | 239 | // Then 240 | assert_matches!( 241 | ns_topic, 242 | NsPubsubTopic::StaticSharding { 243 | cluster: 1, 244 | shard: 2 245 | } 246 | ); 247 | } 248 | 249 | #[test] 250 | fn test_ns_topic_named_sharding_from_str() { 251 | // Given 252 | let topic = "/waku/2/my-topic"; 253 | 254 | // When 255 | let ns_topic = topic.parse::().unwrap(); 256 | 257 | // Then 258 | assert_matches!(ns_topic, NsPubsubTopic::NamedSharding(name) if name == "my-topic"); 259 | } 260 | 261 | #[test] 262 | fn test_ns_topic_raw_from_str() { 263 | // Given 264 | let topic = "my-topic"; 265 | 266 | // When 267 | let ns_topic = topic.parse::().unwrap(); 268 | 269 | // Then 270 | assert_matches!(ns_topic, NsPubsubTopic::Raw(name) if name == "my-topic"); 271 | } 272 | 273 | #[test] 274 | fn test_ns_topic_static_sharding_to_string() { 275 | // Given 276 | let ns_topic = NsPubsubTopic::new_static_sharding(1, 2); 277 | 278 | // When 279 | let topic = ns_topic.to_string(); 280 | 281 | // Then 282 | assert_eq!(topic, "/waku/2/rs/1/2"); 283 | } 284 | 285 | #[test] 286 | fn test_ns_topic_named_sharding_to_string() { 287 | // Given 288 | let ns_topic = NsPubsubTopic::new_named_sharding("my-topic"); 289 | 290 | // When 291 | let topic = ns_topic.to_string(); 292 | 293 | // Then 294 | assert_eq!(topic, "/waku/2/my-topic"); 295 | } 296 | 297 | #[test] 298 | fn test_ns_topic_raw_to_string() { 299 | // Given 300 | let ns_topic = NsPubsubTopic::raw("/waku/2/my-topic"); 301 | 302 | // When 303 | let topic = ns_topic.to_string(); 304 | 305 | // Then 306 | assert_eq!(topic, "/waku/2/my-topic"); 307 | } 308 | 309 | #[test] 310 | fn test_ns_pubsub_topic_from_pubsub_topic_static_sharding() { 311 | // Given 312 | let ns_topic = PubsubTopic::new("/waku/2/rs/1/2"); 313 | 314 | // When 315 | let topic = NsPubsubTopic::try_from(ns_topic).unwrap(); 316 | 317 | // Then 318 | assert_matches!( 319 | topic, 320 | NsPubsubTopic::StaticSharding { 321 | cluster: 1, 322 | shard: 2 323 | } 324 | ); 325 | } 326 | 327 | #[test] 328 | fn test_ns_pubsub_topic_from_pubsub_topic_named_sharding() { 329 | // Given 330 | let pubsub_topic = PubsubTopic::new("/waku/2/test-topic"); 331 | 332 | // When 333 | let topic = NsPubsubTopic::try_from(pubsub_topic).unwrap(); 334 | 335 | // Then 336 | assert_matches!(topic, NsPubsubTopic::NamedSharding(name) if name == "test-topic"); 337 | } 338 | 339 | #[test] 340 | fn test_ns_pubsub_topic_from_pubsub_topic_raw() { 341 | // Given 342 | let topic = PubsubTopic::new("test"); 343 | 344 | // When 345 | let ns_topic = NsPubsubTopic::try_from(topic).unwrap(); 346 | 347 | // Then 348 | assert_matches!(ns_topic, NsPubsubTopic::Raw(name) if name == "test"); 349 | } 350 | } 351 | --------------------------------------------------------------------------------