Skip to content

Commit

Permalink
feat: add support for dnsaddr resolving in browser (#319)
Browse files Browse the repository at this point in the history
  • Loading branch information
zvolin authored Jul 5, 2024
1 parent e5e278e commit ec2d0b9
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 39 deletions.
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions cli/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
};
Expand Down
7 changes: 7 additions & 0 deletions node-wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions node-wasm/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,16 @@ pub trait Context<T> {
fn context<C>(self, context: C) -> Result<T, Error>
where
C: Display;

/// Adds more context to the [`Error`] that is evaluated lazily.
fn with_context<F, C>(self, context_fn: F) -> Result<T, Error>
where
C: Display,
F: FnOnce() -> C,
Self: Sized,
{
self.context(context_fn())
}
}

impl<T, E> Context<T> for Result<T, E>
Expand Down
23 changes: 13 additions & 10 deletions node-wasm/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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(())
}
Expand Down Expand Up @@ -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::<Vec<_>>(),
}
Expand All @@ -373,12 +374,14 @@ impl WasmNodeConfig {

let p2p_local_keypair = Keypair::generate_ed25519();

let p2p_bootnodes = self
.bootnodes
.iter()
.map(|addr| addr.parse())
.collect::<std::result::Result<_, _>>()
.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(),
Expand Down
125 changes: 123 additions & 2 deletions node-wasm/src/utils.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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};
Expand Down Expand Up @@ -202,6 +208,121 @@ pub(crate) fn get_crypto() -> Result<Crypto, Error> {
js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("crypto"))
.context("failed to get `crypto` from global object")?
.dyn_into::<web_sys::Crypto>()
.ok()
.context("`crypto` is not `Crypto` type")
}

async fn fetch(url: &str, opts: &RequestInit, headers: &[(&str, &str)]) -> Result<Response, Error> {
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<Vec<Multiaddr>> {
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<DohEntry>,
}

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::<String>(&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<PeerId> {
ma.iter().find_map(|protocol| {
if let Protocol::P2p(peer_id) = protocol {
Some(peer_id)
} else {
None
}
})
}

fn get_dnsaddr(ma: &Multiaddr) -> Option<Cow<'_, str>> {
ma.iter().find_map(|protocol| {
if let Protocol::Dnsaddr(addr) = protocol {
Some(addr)
} else {
None
}
})
}
40 changes: 19 additions & 21 deletions node/src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ impl FromStr for Network {

fn from_str(network_id: &str) -> Result<Self, Self::Err> {
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())),
Expand All @@ -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",
Expand All @@ -52,29 +52,27 @@ pub fn network_id(network: Network) -> &'static str {
pub fn canonical_network_bootnodes(network: Network) -> impl Iterator<Item = Multiaddr> {
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 => &[],
};
Expand Down

0 comments on commit ec2d0b9

Please sign in to comment.