From ec2d0b91b93bfeb9f88ec477298ab8cb306ba058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Zwoli=C5=84ski?= Date: Fri, 5 Jul 2024 13:14:25 +0200 Subject: [PATCH] feat: add support for dnsaddr resolving in browser (#319) --- Cargo.lock | 5 +- cli/src/server.rs | 5 +- node-wasm/Cargo.toml | 7 +++ node-wasm/src/error.rs | 10 ++++ node-wasm/src/node.rs | 23 ++++---- node-wasm/src/utils.rs | 125 ++++++++++++++++++++++++++++++++++++++++- node/src/network.rs | 40 +++++++------ 7 files changed, 176 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47b4303e..2fe159de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3179,6 +3179,7 @@ dependencies = [ "lumina-node", "serde", "serde-wasm-bindgen", + "serde_json", "serde_repr", "thiserror", "time", @@ -4662,9 +4663,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", diff --git a/cli/src/server.rs b/cli/src/server.rs index ab1fda43..8689d15e 100644 --- a/cli/src/server.rs +++ b/cli/src/server.rs @@ -8,7 +8,6 @@ use axum::response::Response; use axum::routing::get; use axum::{Json, Router}; use clap::Args; -use libp2p::multiaddr::Protocol; use libp2p::Multiaddr; use lumina_node::network::canonical_network_bootnodes; use rust_embed::RustEmbed; @@ -52,9 +51,7 @@ pub(crate) struct Params { pub(crate) async fn run(args: Params) -> Result<()> { let network = args.network.into(); let bootnodes = if args.bootnodes.is_empty() { - canonical_network_bootnodes(network) - .filter(|addr| addr.iter().any(|proto| proto == Protocol::WebTransport)) - .collect() + canonical_network_bootnodes(network).collect() } else { args.bootnodes }; diff --git a/node-wasm/Cargo.toml b/node-wasm/Cargo.toml index e0ec67db..d9efdc46 100644 --- a/node-wasm/Cargo.toml +++ b/node-wasm/Cargo.toml @@ -37,6 +37,7 @@ gloo-timers = "0.3.0" instant = "0.1.13" js-sys = "0.3.69" serde = { version = "1.0.203", features = ["derive"] } +serde_json = "1.0.120" serde-wasm-bindgen = "0.6.5" serde_repr = "0.1.19" thiserror = "1.0.61" @@ -51,12 +52,18 @@ web-sys = { version = "0.3.69", features = [ "BroadcastChannel", "Crypto", "DedicatedWorkerGlobalScope", + "Headers", "MessageEvent", "MessagePort", "Navigator", + "Request", + "RequestInit", + "RequestMode", + "Response", "SharedWorker", "SharedWorkerGlobalScope", "StorageManager", + "Window", "Worker", "WorkerGlobalScope", "WorkerOptions", diff --git a/node-wasm/src/error.rs b/node-wasm/src/error.rs index 62f76a20..1de3ad83 100644 --- a/node-wasm/src/error.rs +++ b/node-wasm/src/error.rs @@ -147,6 +147,16 @@ pub trait Context { fn context(self, context: C) -> Result where C: Display; + + /// Adds more context to the [`Error`] that is evaluated lazily. + fn with_context(self, context_fn: F) -> Result + where + C: Display, + F: FnOnce() -> C, + Self: Sized, + { + self.context(context_fn()) + } } impl Context for Result diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 21b2784f..2bf46ed9 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -2,7 +2,6 @@ use js_sys::Array; use libp2p::identity::Keypair; -use libp2p::multiaddr::Protocol; use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::to_value; use tracing::error; @@ -15,7 +14,10 @@ use lumina_node::node::NodeConfig; use lumina_node::store::IndexedDbStore; use crate::error::{Context, Result}; -use crate::utils::{is_chrome, js_value_from_display, request_storage_persistence, Network}; +use crate::utils::{ + is_chrome, js_value_from_display, request_storage_persistence, resolve_dnsaddr_multiaddress, + Network, +}; use crate::worker::commands::{CheckableResponseExt, NodeCommand, SingleHeaderQuery}; use crate::worker::{AnyWorker, WorkerClient}; use crate::wrapper::libp2p::NetworkInfoSnapshot; @@ -127,7 +129,7 @@ impl NodeDriver { pub async fn start(&self, config: WasmNodeConfig) -> Result<()> { let command = NodeCommand::StartNode(config); let response = self.client.exec(command).await?; - let _ = response.into_node_started().check_variant()?; + response.into_node_started().check_variant()??; Ok(()) } @@ -354,7 +356,6 @@ impl WasmNodeConfig { WasmNodeConfig { network, bootnodes: canonical_network_bootnodes(network.into()) - .filter(|addr| addr.iter().any(|proto| proto == Protocol::WebTransport)) .map(|addr| addr.to_string()) .collect::>(), } @@ -373,12 +374,14 @@ impl WasmNodeConfig { let p2p_local_keypair = Keypair::generate_ed25519(); - let p2p_bootnodes = self - .bootnodes - .iter() - .map(|addr| addr.parse()) - .collect::>() - .context("bootstrap multiaddr invalid")?; + let mut p2p_bootnodes = Vec::with_capacity(self.bootnodes.len()); + for addr in self.bootnodes { + let addr = addr + .parse() + .with_context(|| format!("invalid multiaddr: '{addr}"))?; + let resolved_addrs = resolve_dnsaddr_multiaddress(addr).await?; + p2p_bootnodes.extend(resolved_addrs.into_iter()); + } Ok(NodeConfig { network_id: network_id.to_string(), diff --git a/node-wasm/src/utils.rs b/node-wasm/src/utils.rs index 95b00ba2..9e857bb9 100644 --- a/node-wasm/src/utils.rs +++ b/node-wasm/src/utils.rs @@ -1,6 +1,10 @@ //! Various utilities for interacting with node from wasm. +use std::borrow::Cow; use std::fmt::{self, Debug}; +use std::net::{IpAddr, Ipv4Addr}; +use libp2p::multiaddr::Protocol; +use libp2p::{Multiaddr, PeerId}; use lumina_node::network; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -12,8 +16,10 @@ use tracing_subscriber::fmt::time::UtcTime; use tracing_subscriber::prelude::*; use tracing_web::{performance_layer, MakeConsoleWriter}; use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; use web_sys::{ - Crypto, DedicatedWorkerGlobalScope, Navigator, SharedWorker, SharedWorkerGlobalScope, Worker, + Crypto, DedicatedWorkerGlobalScope, Navigator, Request, RequestInit, RequestMode, Response, + SharedWorker, SharedWorkerGlobalScope, Worker, }; use crate::error::{Context, Error, Result}; @@ -202,6 +208,121 @@ pub(crate) fn get_crypto() -> Result { js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("crypto")) .context("failed to get `crypto` from global object")? .dyn_into::() - .ok() .context("`crypto` is not `Crypto` type") } + +async fn fetch(url: &str, opts: &RequestInit, headers: &[(&str, &str)]) -> Result { + let request = Request::new_with_str_and_init(url, opts) + .with_context(|| format!("failed to create a request to {url}"))?; + + for (name, value) in headers { + request + .headers() + .set(name, value) + .with_context(|| format!("failed setting header: '{name}: {value}'"))?; + } + + let fetch_promise = if let Some(window) = web_sys::window() { + window.fetch_with_request(&request) + } else if Worker::is_worker_type() { + Worker::worker_self().fetch_with_request(&request) + } else if SharedWorker::is_worker_type() { + SharedWorker::worker_self().fetch_with_request(&request) + } else { + return Err(Error::new("`fetch` not found in global scope")); + }; + + JsFuture::from(fetch_promise) + .await + .with_context(|| format!("failed fetching {url}"))? + .dyn_into() + .context("`response` is not `Response` type") +} + +/// If provided multiaddress uses dnsaddr protocol, resolve it using dns-over-https. +/// Otherwise returns the provided address. +pub(crate) async fn resolve_dnsaddr_multiaddress(ma: Multiaddr) -> Result> { + const TXT_TYPE: u16 = 16; + // cloudflare dns + const DEFAULT_DNS_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)); + + #[derive(Debug, Deserialize)] + struct DohEntry { + r#type: u16, + data: String, + } + + #[derive(Debug, Deserialize)] + #[serde(rename_all = "PascalCase")] + struct DohResponse { + answer: Vec, + } + + let Some(dnsaddr) = get_dnsaddr(&ma) else { + // not a dnsaddr multiaddr + return Ok(vec![ma]); + }; + let Some(peer_id) = get_peer_id(&ma) else { + return Err(Error::new("Peer id not found")); + }; + + let mut opts = RequestInit::new(); + opts.method("GET"); + opts.mode(RequestMode::Cors); + + let url = + format!("https://{DEFAULT_DNS_ADDR}/dns-query?type={TXT_TYPE}&name=_dnsaddr.{dnsaddr}"); + let response = fetch(&url, &opts, &[("Accept", "application/dns-json")]).await?; + + let json_promise = response.json().context("`Response::json()` failed")?; + let json = JsFuture::from(json_promise) + .await + .context("failed parsing response as json")?; + + let doh_response: DohResponse = serde_wasm_bindgen::from_value(json) + .context("failed deserializing dns-over-https response")?; + + let mut resolved_addrs = Vec::with_capacity(3); + for entry in doh_response.answer { + if entry.r#type == TXT_TYPE { + // we receive data as json encoded strings in this format: + // "data": "\"dnsaddr=/dns/da-bridge-1.celestia-arabica-11.com/tcp/2121/p2p/12D3KooWGqwzdEqM54Dce6LXzfFr97Bnhvm6rN7KM7MFwdomfm4S\"" + let Ok(data) = serde_json::from_str::(&entry.data) else { + continue; + }; + let Some((_, ma)) = data.split_once('=') else { + continue; + }; + let Ok(ma) = ma.parse() else { + continue; + }; + // only take results with the same peer id + if Some(peer_id) == get_peer_id(&ma) { + // TODO: handle recursive dnsaddr queries + resolved_addrs.push(ma); + } + } + } + + Ok(resolved_addrs) +} + +fn get_peer_id(ma: &Multiaddr) -> Option { + ma.iter().find_map(|protocol| { + if let Protocol::P2p(peer_id) = protocol { + Some(peer_id) + } else { + None + } + }) +} + +fn get_dnsaddr(ma: &Multiaddr) -> Option> { + ma.iter().find_map(|protocol| { + if let Protocol::Dnsaddr(addr) = protocol { + Some(addr) + } else { + None + } + }) +} diff --git a/node/src/network.rs b/node/src/network.rs index f95a3306..24055261 100644 --- a/node/src/network.rs +++ b/node/src/network.rs @@ -30,7 +30,7 @@ impl FromStr for Network { fn from_str(network_id: &str) -> Result { match network_id { - "arabica-10" => Ok(Network::Arabica), + "arabica-11" => Ok(Network::Arabica), "mocha-4" => Ok(Network::Mocha), "private" => Ok(Network::Private), network => Err(UnknownNetworkError(network.to_string())), @@ -41,7 +41,7 @@ impl FromStr for Network { /// Get the string id of the given network. pub fn network_id(network: Network) -> &'static str { match network { - Network::Arabica => "arabica-10", + Network::Arabica => "arabica-11", Network::Mocha => "mocha-4", Network::Private => "private", Network::Mainnet => "celestia", @@ -52,29 +52,27 @@ pub fn network_id(network: Network) -> &'static str { pub fn canonical_network_bootnodes(network: Network) -> impl Iterator { let peers: &[_] = match network { Network::Mainnet => &[ - "/dns4/lumina.eiger.co/tcp/2121/p2p/12D3KooW9z4jLqwodwNRcSa5qgcSgtJ13kN7CYLcwZQjPRYodqWx", - "/dns4/lumina.eiger.co/udp/2121/quic-v1/webtransport/p2p/12D3KooW9z4jLqwodwNRcSa5qgcSgtJ13kN7CYLcwZQjPRYodqWx", - "/dns4/da-bridge-1.celestia-bootstrap.net/tcp/2121/p2p/12D3KooWSqZaLcn5Guypo2mrHr297YPJnV8KMEMXNjs3qAS8msw8", - "/dns4/da-bridge-2.celestia-bootstrap.net/tcp/2121/p2p/12D3KooWQpuTFELgsUypqp9N4a1rKBccmrmQVY8Em9yhqppTJcXf", - "/dns4/da-bridge-3.celestia-bootstrap.net/tcp/2121/p2p/12D3KooWSGa4huD6ts816navn7KFYiStBiy5LrBQH1HuEahk4TzQ", - "/dns4/da-bridge-4.celestia-bootstrap.net/tcp/2121/p2p/12D3KooWHBXCmXaUNat6ooynXG837JXPsZpSTeSzZx6DpgNatMmR", - "/dns4/da-bridge-5.celestia-bootstrap.net/tcp/2121/p2p/12D3KooWDGTBK1a2Ru1qmnnRwP6Dmc44Zpsxi3xbgFk7ATEPfmEU", - "/dns4/da-bridge-6.celestia-bootstrap.net/tcp/2121/p2p/12D3KooWLTUFyf3QEGqYkHWQS2yCtuUcL78vnKBdXU5gABM1YDeH", - "/dns4/da-full-1.celestia-bootstrap.net/tcp/2121/p2p/12D3KooWKZCMcwGCYbL18iuw3YVpAZoyb1VBGbx9Kapsjw3soZgr", - "/dns4/da-full-2.celestia-bootstrap.net/tcp/2121/p2p/12D3KooWE3fmRtHgfk9DCuQFfY3H3JYEnTU3xZozv1Xmo8KWrWbK", - "/dns4/da-full-3.celestia-bootstrap.net/tcp/2121/p2p/12D3KooWK6Ftsd4XsWCsQZgZPNhTrE5urwmkoo5P61tGvnKmNVyv", + "/dnsaddr/da-bridge-1.celestia-bootstrap.net/p2p/12D3KooWSqZaLcn5Guypo2mrHr297YPJnV8KMEMXNjs3qAS8msw8", + "/dnsaddr/da-bridge-2.celestia-bootstrap.net/p2p/12D3KooWQpuTFELgsUypqp9N4a1rKBccmrmQVY8Em9yhqppTJcXf", + "/dnsaddr/da-bridge-3.celestia-bootstrap.net/p2p/12D3KooWSGa4huD6ts816navn7KFYiStBiy5LrBQH1HuEahk4TzQ", + "/dnsaddr/da-bridge-4.celestia-bootstrap.net/p2p/12D3KooWHBXCmXaUNat6ooynXG837JXPsZpSTeSzZx6DpgNatMmR", + "/dnsaddr/da-bridge-5.celestia-bootstrap.net/p2p/12D3KooWDGTBK1a2Ru1qmnnRwP6Dmc44Zpsxi3xbgFk7ATEPfmEU", + "/dnsaddr/da-bridge-6.celestia-bootstrap.net/p2p/12D3KooWLTUFyf3QEGqYkHWQS2yCtuUcL78vnKBdXU5gABM1YDeH", + "/dnsaddr/da-full-1.celestia-bootstrap.net/p2p/12D3KooWKZCMcwGCYbL18iuw3YVpAZoyb1VBGbx9Kapsjw3soZgr", + "/dnsaddr/da-full-2.celestia-bootstrap.net/p2p/12D3KooWE3fmRtHgfk9DCuQFfY3H3JYEnTU3xZozv1Xmo8KWrWbK", + "/dnsaddr/da-full-3.celestia-bootstrap.net/p2p/12D3KooWK6Ftsd4XsWCsQZgZPNhTrE5urwmkoo5P61tGvnKmNVyv", ], Network::Arabica => &[ - "/dns4/da-bridge-1.celestia-arabica-11.com/tcp/2121/p2p/12D3KooWGqwzdEqM54Dce6LXzfFr97Bnhvm6rN7KM7MFwdomfm4S", - "/dns4/da-bridge-2.celestia-arabica-11.com/tcp/2121/p2p/12D3KooWCMGM5eZWVfCN9ZLAViGfLUWAfXP5pCm78NFKb9jpBtua", - "/dns4/da-bridge-3.celestia-arabica-11.com/tcp/2121/p2p/12D3KooWEWuqrjULANpukDFGVoHW3RoeUU53Ec9t9v5cwW3MkVdQ", - "/dns4/da-bridge-4.celestia-arabica-11.com/tcp/2121/p2p/12D3KooWLT1ysSrD7XWSBjh7tU1HQanF5M64dHV6AuM6cYEJxMPk", + "/dnsaddr/da-bridge-1.celestia-arabica-11.com/p2p/12D3KooWGqwzdEqM54Dce6LXzfFr97Bnhvm6rN7KM7MFwdomfm4S", + "/dnsaddr/da-bridge-2.celestia-arabica-11.com/p2p/12D3KooWCMGM5eZWVfCN9ZLAViGfLUWAfXP5pCm78NFKb9jpBtua", + "/dnsaddr/da-bridge-3.celestia-arabica-11.com/p2p/12D3KooWEWuqrjULANpukDFGVoHW3RoeUU53Ec9t9v5cwW3MkVdQ", + "/dnsaddr/da-bridge-4.celestia-arabica-11.com/p2p/12D3KooWLT1ysSrD7XWSBjh7tU1HQanF5M64dHV6AuM6cYEJxMPk", ], Network::Mocha => &[ - "/dns4/da-bridge-mocha-4.celestia-mocha.com/tcp/2121/p2p/12D3KooWCBAbQbJSpCpCGKzqz3rAN4ixYbc63K68zJg9aisuAajg", - "/dns4/da-bridge-mocha-4-2.celestia-mocha.com/tcp/2121/p2p/12D3KooWK6wJkScGQniymdWtBwBuU36n6BRXp9rCDDUD6P5gJr3G", - "/dns4/da-full-1-mocha-4.celestia-mocha.com/tcp/2121/p2p/12D3KooWCUHPLqQXZzpTx1x3TAsdn3vYmTNDhzg66yG8hqoxGGN8", - "/dns4/da-full-2-mocha-4.celestia-mocha.com/tcp/2121/p2p/12D3KooWR6SHsXPkkvhCRn6vp1RqSefgaT1X1nMNvrVjU2o3GoYy", + "/dnsaddr/da-bridge-mocha-4.celestia-mocha.com/p2p/12D3KooWCBAbQbJSpCpCGKzqz3rAN4ixYbc63K68zJg9aisuAajg", + "/dnsaddr/da-bridge-mocha-4-2.celestia-mocha.com/p2p/12D3KooWK6wJkScGQniymdWtBwBuU36n6BRXp9rCDDUD6P5gJr3G", + "/dnsaddr/da-full-1-mocha-4.celestia-mocha.com/p2p/12D3KooWCUHPLqQXZzpTx1x3TAsdn3vYmTNDhzg66yG8hqoxGGN8", + "/dnsaddr/da-full-2-mocha-4.celestia-mocha.com/p2p/12D3KooWR6SHsXPkkvhCRn6vp1RqSefgaT1X1nMNvrVjU2o3GoYy", ], Network::Private => &[], };