diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index cd5806edb..013f8fe17 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -33,7 +33,9 @@ jobs: - uses: actions-rust-lang/setup-rust-toolchain@v1 with: components: clippy - - run: cargo clippy --workspace -- -D warnings + # TODO: Enable avail-light-web once issue with rocksdb feature being applied + # accross the workspace is resolved + - run: cargo clippy --workspace --exclude avail-light-web -- -D warnings test: name: cargo test @@ -42,7 +44,9 @@ jobs: - uses: actions/checkout@v4 - uses: arduino/setup-protoc@v2 - uses: actions-rust-lang/setup-rust-toolchain@v1 - - run: cargo test --workspace --benches --tests + # TODO: Enable avail-light-web once issue with rocksdb feature being applied + # accross the workspace is resolved + - run: cargo test --workspace --benches --tests --exclude avail-light-web env: RUSTFLAGS: "-C instrument-coverage" LLVM_PROFILE_FILE: "profile-%p-%m.profraw" diff --git a/Cargo.lock b/Cargo.lock index c6fb9e384..79b531299 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1106,6 +1106,28 @@ dependencies = [ "warp", ] +[[package]] +name = "avail-light-web" +version = "0.1.0" +dependencies = [ + "avail-light-core", + "avail-rust", + "clap", + "console_error_panic_hook", + "futures", + "libp2p", + "sp-io", + "tokio", + "tokio_with_wasm", + "tracing", + "tracing-subscriber 0.3.18", + "tracing-wasm", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", +] + [[package]] name = "avail-rust" version = "0.1.0" @@ -1806,6 +1828,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -9276,6 +9308,17 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber 0.3.18", + "wasm-bindgen", +] + [[package]] name = "trie-db" version = "0.28.0" diff --git a/Cargo.toml b/Cargo.toml index b949c6ca0..36d822fbb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crawler", "fat", "relay", + "web", ] default-members = ["client"] resolver = "2" @@ -25,7 +26,7 @@ anyhow = "1.0.71" async-std = { version = "1.12.0", features = ["attributes"] } async-trait = "0.1.73" clap = { version = "4.4.4", features = ["derive", "cargo"] } -color-eyre = "0.6.2" +color-eyre = { version = "0.6.2", default-features = false } confy = "0.5.1" hex = "0.4.3" rand = "0.8.4" diff --git a/core/Cargo.toml b/core/Cargo.toml index 81ba17faa..8ee3032da 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -20,7 +20,6 @@ better-panic = "0.3.0" blake2b_simd = "1.0.2" clap = { workspace = true } codec = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive", "full", "bit-vec"] } -color-eyre = { workspace = true } confy = { workspace = true } derive_more = { version = "1", features = ["from"] } dusk-bytes = { version = "0.1.6", default-features = false } @@ -49,6 +48,7 @@ uuid = { workspace = true } async-std = { workspace = true } chrono = "0.4.19" libp2p = { workspace = true } +color-eyre = { workspace = true, default-features = true } hyper = { version = "0.14.23", features = ["full", "http1"] } jsonrpsee-core = { version = "0.21.0", features = ["client"] } libc = "0.2.150" @@ -76,6 +76,7 @@ thiserror-no-std = "2.0.2" rand = { version = "0.8.4", default-features = false } libp2p = { workspace = true, features = ["wasm-bindgen"] } libp2p-webrtc-websys = { workspace = true } +color-eyre = { workspace = true } wasm-bindgen = "0.2.90" wasm-timer = "0.2.5" web-time = "1.1.0" diff --git a/core/src/light_client.rs b/core/src/light_client.rs index 37e4d87b1..9f356935c 100644 --- a/core/src/light_client.rs +++ b/core/src/light_client.rs @@ -43,6 +43,7 @@ use crate::{ utils::{blake2_256, calculate_confidence, extract_kate}, }; +#[derive(Debug)] pub enum OutputEvent { RecordBlockProcessingDelay(f64), CountSessionBlocks, diff --git a/core/src/network/p2p.rs b/core/src/network/p2p.rs index 77f0fada8..a3e6cee07 100644 --- a/core/src/network/p2p.rs +++ b/core/src/network/p2p.rs @@ -317,10 +317,9 @@ async fn build_swarm( }; #[cfg(target_arch = "wasm32")] { + use libp2p_webrtc_websys as webrtc; swarm = tokio_swarm - .with_other_transport(|key| { - libp2p_webrtc_websys::Transport::new(libp2p_webrtc_websys::Config::new(&key)) - })? + .with_other_transport(|key| webrtc::Transport::new(webrtc::Config::new(&key)))? .with_relay_client(noise::Config::new, yamux::Config::default)? .with_behaviour(behaviour)? .with_swarm_config(|c| generate_config(c, cfg)) diff --git a/core/src/network/rpc/client.rs b/core/src/network/rpc/client.rs index 5b3c5b61b..5f2a01b39 100644 --- a/core/src/network/rpc/client.rs +++ b/core/src/network/rpc/client.rs @@ -105,9 +105,11 @@ impl GenesisHash { } let bytes: [u8; 32] = from_hex(hex_str) - .map_err(|_| ClientCreationError::InvalidGenesisHash(hex_str.to_string()))? + .map_err(|_| ClientCreationError::InvalidGenesisHash(hex_str.to_string())) + .map_err(Report::msg)? .try_into() - .map_err(|_| ClientCreationError::InvalidGenesisHash(hex_str.to_string()))?; + .map_err(|_| ClientCreationError::InvalidGenesisHash(hex_str.to_string())) + .map_err(Report::msg)?; Ok(Self::Hash(H256::from(bytes))) } @@ -262,8 +264,8 @@ impl Client { match connection_result { Ok(Ok(ConnectionAttempt { client, node, .. })) => Ok((client, node)), - Ok(Err(err)) => Err(RetryError::ConnectionFailed(err).into()), - Err(err) => Err(RetryError::Shutdown(err.to_string()).into()), + Ok(Err(err)) => Err(Report::msg(RetryError::ConnectionFailed(err))), + Err(err) => Err(Report::msg(RetryError::Shutdown(err.to_string()))), } } @@ -289,7 +291,7 @@ impl Client { Fut: std::future::Future>, { if nodes.is_empty() { - return Err(ClientCreationError::NoNodesAvailable.into()); + return Err(Report::msg(ClientCreationError::NoNodesAvailable)); } let mut last_error = None; @@ -306,10 +308,9 @@ impl Client { } } - Err(ClientCreationError::AllNodesFailed { + Err(Report::msg(ClientCreationError::AllNodesFailed { last_error: last_error.unwrap_or_else(|| eyre!("No error recorded")), - } - .into()) + })) } // Tries to connect to the provided RPC host, verifies the genesis hash, @@ -326,13 +327,12 @@ impl Client { match Self::create_rpc_client(&node.host, expected_genesis_hash).await { Ok((client, node)) => { // Execute the provided RPC function call with the created client - let result = - f(client.clone()) - .await - .map_err(|e| ClientCreationError::RpcCallFailed { - host: node.host.clone(), - error: e, - })?; + let result = f(client.clone()).await.map_err(|e| { + Report::msg(ClientCreationError::RpcCallFailed { + host: node.host.clone(), + error: e, + }) + })?; Ok(ConnectionAttempt { client, @@ -340,11 +340,10 @@ impl Client { result, }) }, - Err(err) => Err(ClientCreationError::ConnectionFailed { + Err(err) => Err(Report::msg(ClientCreationError::ConnectionFailed { host: node.host.clone(), error: err, - } - .into()), + })), } } @@ -352,7 +351,8 @@ impl Client { async fn create_rpc_client(host: &str, expected_genesis_hash: &str) -> Result<(SDK, Node)> { let client = SDK::new_insecure(host) .await - .map_err(|e| ClientCreationError::SdkFailure(eyre!("{e}")))?; + .map_err(|e| ClientCreationError::SdkFailure(eyre!("{e}"))) + .map_err(Report::msg)?; // Verify genesis hash let genesis_hash = client.api.genesis_hash(); @@ -361,12 +361,11 @@ impl Client { let expected_hash = GenesisHash::from_hex(expected_genesis_hash)?; if !expected_hash.matches(&genesis_hash) { - return Err(ClientCreationError::GenesisHashMismatch { + return Err(Report::msg(ClientCreationError::GenesisHashMismatch { host: host.to_string(), expected: expected_genesis_hash.to_string(), found: format!("{:?}", genesis_hash), - } - .into()); + })); } // Fetch system and runtime information @@ -375,7 +374,8 @@ impl Client { .system .version() .await - .map_err(|e| ClientCreationError::SystemVersionError(e.into()))?; + .map_err(|e| ClientCreationError::SystemVersionError(e.into())) + .map_err(Report::msg)?; let runtime_version = client.api.runtime_version(); @@ -402,7 +402,11 @@ impl Client { } // If current client failed try to reconnect using stored node - let connected_node = self.db.get(RpcNodeKey).ok_or(RetryError::NoPreviousNode)?; + let connected_node = self + .db + .get(RpcNodeKey) + .ok_or(RetryError::NoPreviousNode) + .map_err(Report::msg)?; warn!( "Executing RPC call with host: {} failed. Trying to create a new RPC connection.", @@ -436,8 +440,8 @@ impl Client { match retry_result { Ok(Ok(result)) => Ok(result), - Ok(Err(err)) => Err(RetryError::ConnectionFailed(err).into()), - Err(err) => Err(RetryError::Shutdown(err.to_string()).into()), + Ok(Err(err)) => Err(Report::msg(RetryError::ConnectionFailed(err))), + Err(err) => Err(Report::msg(RetryError::Shutdown(err.to_string()))), } } @@ -464,8 +468,8 @@ impl Client { self.db.put(RpcNodeKey, node); Ok(result) }, - Ok(Err(err)) => Err(RetryError::ConnectionFailed(err).into()), - Err(err) => Err(RetryError::Shutdown(err.to_string()).into()), + Ok(Err(err)) => Err(Report::msg(RetryError::ConnectionFailed(err))), + Err(err) => Err(Report::msg(RetryError::Shutdown(err.to_string()))), } } diff --git a/core/src/utils.rs b/core/src/utils.rs index a98529bed..97c86d9c5 100644 --- a/core/src/utils.rs +++ b/core/src/utils.rs @@ -115,12 +115,19 @@ pub fn filter_auth_set_changes(header: &AvailHeader) -> Vec) -> Result<()> { + #[cfg(not(target_arch = "wasm32"))] // initialize color-eyre hooks let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() .display_location_section(true) .display_env_section(true) .into_hooks(); + #[cfg(target_arch = "wasm32")] + // initialize color-eyre hooks + let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() + .display_env_section(true) + .into_hooks(); + // install hook as global handler eyre_hook.install()?; diff --git a/web/Cargo.toml b/web/Cargo.toml new file mode 100644 index 000000000..32a9f7909 --- /dev/null +++ b/web/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "avail-light-web" +version = "0.1.0" +authors.workspace = true +edition = "2021" +repository.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +avail-light-core = { workspace = true } +avail-rust = { workspace = true } +clap = { workspace = true } +console_error_panic_hook = { version = "0.1.7", optional = true } +futures = { workspace = true } +libp2p = { workspace = true } +sp-io = { version = "30", features = ["disable_allocator", "disable_panic_handler"], default-features = false } +tokio = { version = "^1", default-features = false, features = ["sync", "macros", "io-util", "rt"] } +tokio_with_wasm = { version = "0.7.1", features = ["sync", "macros", "rt"] } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +tracing-wasm = "0.2.1" +wasm-bindgen = "0.2.93" +wasm-bindgen-futures = "0.4.43" +web-sys = { version = "0.3.70", features = ["console", "Window", "UrlSearchParams"] } +web-time = "1.1.0" diff --git a/web/README.md b/web/README.md new file mode 100644 index 000000000..c442bb09b --- /dev/null +++ b/web/README.md @@ -0,0 +1,16 @@ +# Avail Light Client (Web) + +## Compile + +`wasm-pack build --target web --dev` + +## Run + +`cp www/index.html pkg/` +`cd pkg` +`python3 -m http.server --directory .` + +# Start LC + +- Safari: http://localhost:8000/?network=hex&bootstrap=%2Fip4%2F209.38.38.158%2Fudp%2F39001%2Fwebrtc-direct%2Fcerthash%2FuEiCVz-CTCrMq4I2xwW_WznQPML3dos4GNWiXE_fJjvHiIg +- Firefox: 0.0.0.0:8000/?network=hex&bootstrap=%2Fip4%2F209.38.38.158%2Fudp%2F39001%2Fwebrtc-direct%2Fcerthash%2FuEiCVz-CTCrMq4I2xwW_WznQPML3dos4GNWiXE_fJjvHiIg diff --git a/web/src/lib.rs b/web/src/lib.rs new file mode 100644 index 000000000..f94724f0f --- /dev/null +++ b/web/src/lib.rs @@ -0,0 +1,155 @@ +#![cfg(target_arch = "wasm32")] + +use std::sync::Arc; + +use avail_light_core::data; +use avail_light_core::light_client::OutputEvent as LcEvent; +use avail_light_core::network::{self, p2p, rpc, Network}; +use avail_light_core::shutdown::Controller; +use avail_light_core::types::{Delay, MultiaddrConfig}; +use avail_light_core::utils::spawn_in_span; +use avail_rust::kate_recovery::couscous; +use clap::ValueEnum; +use libp2p::Multiaddr; +use std::str::FromStr; +use tokio::sync::{broadcast, mpsc}; +use tokio_with_wasm::alias as tokio; +use tracing::{error, info, warn}; +use wasm_bindgen::prelude::*; +use web_sys::{window, UrlSearchParams}; +use web_time::Duration; + +#[tokio::main(flavor = "current_thread")] +#[wasm_bindgen(start)] +async fn main_js() {} + +#[wasm_bindgen] +pub async fn run() { + console_error_panic_hook::set_once(); + tracing_wasm::set_as_global_default(); + + let search = window().unwrap().location().search().unwrap(); + let params = UrlSearchParams::new_with_str(&search).unwrap(); + + let mut network = network::Network::Local; + if let Some(value) = params.get("network") { + network = Network::from_str(&value, true).unwrap(); + }; + + let mut bootstrap_multiaddr = network.bootstrap_multiaddr(); + if let Some(value) = params.get("bootstrap") { + bootstrap_multiaddr = Multiaddr::from_str(&value).unwrap(); + }; + + let version = clap::crate_version!(); + let shutdown = Controller::new(); + let db = data::DB::default(); + // TODO: Store and read client_id from local storage + // let client_id = Uuid::new_v4(); + // let execution_id = Uuid::new_v4(); + + let pp = Arc::new(couscous::public_params()); + + let cfg_rpc = rpc::configuration::RPCConfig { + full_node_ws: network.full_node_ws(), + ..Default::default() + }; + + let cfg_libp2p = p2p::configuration::LibP2PConfig { + bootstraps: vec![MultiaddrConfig::PeerIdAndMultiaddr(( + network.bootstrap_peer_id(), + bootstrap_multiaddr, + ))], + ..Default::default() + }; + + let genesis_hash = &network.genesis_hash().to_string(); + + let (rpc_client, rpc_events, rpc_subscriptions) = + rpc::init(db.clone(), genesis_hash, &cfg_rpc, shutdown.clone()) + .await + .unwrap(); + + let (id_keys, _peer_id) = p2p::identity(&cfg_libp2p, db.clone()).unwrap(); + + let project_name = "avail".to_string(); + let (p2p_client, p2p_event_loop, _p2p_event_receiver) = p2p::init( + cfg_libp2p.clone(), + project_name, + id_keys, + version, + "DEV", + false, + shutdown.clone(), + ) + .await + .unwrap(); + + let network_client = network::new(p2p_client.clone(), rpc_client, pp, false); + + // spawn the RPC Network task for Event Loop to run in the background + // and shut it down, without delays + let _rpc_subscriptions_handle = spawn_in_span(shutdown.with_cancel(shutdown.with_trigger( + "Subscription loop failure triggered shutdown".to_string(), + async { + let result = rpc_subscriptions.run().await; + if let Err(ref err) = result { + error!(%err, "Subscription loop ended with error"); + }; + result + }, + ))); + + let (lc_sender, mut lc_receiver) = mpsc::unbounded_channel::(); + let (block_tx, _block_rx) = + broadcast::channel::(1 << 7); + + let channels = avail_light_core::types::ClientChannels { + block_sender: block_tx, + rpc_event_receiver: rpc_events.subscribe(), + }; + + spawn_in_span(async move { + loop { + let Some(message) = lc_receiver.recv().await else { + info!("Exiting..."); + break; + }; + info!("{message:?}"); + } + }); + + let light_client_handle = tokio::task::spawn(avail_light_core::light_client::run( + db.clone(), + network_client, + 99.9, + Delay(Some(Duration::from_secs(20))), + channels, + shutdown.clone(), + lc_sender, + )); + + tokio::task::spawn(p2p_event_loop.run()); + + let bootstraps = cfg_libp2p.bootstraps.clone(); + let bootstrap_p2p_client = p2p_client.clone(); + spawn_in_span(shutdown.with_cancel(async move { + info!("Bootstraping the DHT with bootstrap nodes..."); + let bs_result = bootstrap_p2p_client + .clone() + .bootstrap_on_startup(&bootstraps) + .await; + match bs_result { + Ok(_) => { + info!("Bootstrap done."); + }, + Err(e) => { + warn!("Bootstrap process: {e:?}."); + }, + } + })); + + if let Err(error) = light_client_handle.await { + error!("Error running light client: {error}") + }; +} diff --git a/web/www/index.html b/web/www/index.html new file mode 100644 index 000000000..20a995854 --- /dev/null +++ b/web/www/index.html @@ -0,0 +1,18 @@ + + + + + + Avail Light Client Web + + +

Avail Light Client Web

+ + +