Skip to content

Commit e581dcb

Browse files
authored
feat: tor isolated circuits (#1064)
fixes + tor feature in cdk-cli fix: call `clone_with_prefs` to get a new isolation token format remove `new_isolated` from Transport trait fix: remove tor dependencies under wasm32, disallow compilation with tor feature and wasm32 tor_transport in its own file fixes fmt format tor: implement Transport::resolve_dns_txt for TorAsync using DoH over Tor; fix tor transport trait changes after rebase; remove unused as_str() call for TorToggle in cdk-cli. Ensure compilation with features: tor,bip353 format remove double reference format feat: circuits pool format tor_transport: deterministically select Tor client per request using index_for_request(endpoint path + query + payload)\n\n- Add index_for_request(&Url, Option<&[u8]>) using FNV-1a 64-bit (dependency-free)\n- Replace round-robin next_index() usage in request() with deterministic index\n- Adjust request() to accept Option<Vec<u8>> body to hash payload bytes\n- Update http_get/http_post/resolve_dns_txt to call new request signature\n- Keep next_index() as dead_code for potential fallback tor_transport: implement Default by bootstrapping with default pool size (blocking)\n\n- Default now attempts to use existing Tokio runtime handle, or creates a temporary runtime\n- Preserves previous behavior for async constructors (new/with_pool_size) tor_transport: fix Default to avoid nested runtime panic by initializing on a new thread when no Handle available\n\n- If a runtime is present, block_on via current handle\n- Otherwise, spawn a new OS thread and create a runtime inside it, then join tor_transport: rework Default to use block_in_place + background thread runtime to avoid nested block_on inside tokio\n\n- Always create runtime on a separate OS thread; if inside tokio, enter block_in_place first\n- Avoids 'Cannot start a runtime from within a runtime' panic fix more fixes tor_transport: lazy-initialize Tor client pool on first use via ensure_pool; make Default non-blocking and remove runtime gymnastics\n\n- Introduce Inner with OnceCell<Arc<Vec<TorClient>>> and configured size\n- Default/new/with_pool_size now cheap; actual arti bootstrap happens on first request\n- request() calls ensure_pool() and uses deterministic index with pool.len()\n- Keeps deterministic endpoint/method/body affinity and DoH TXT resolution\n\nThis avoids nested-runtime/block_in_place complexity and makes Default trivial. tor_transport: make DEFAULT_TOR_POOL_SIZE public and support custom pool sizes via TorAsync::with_pool_size() (lazy)} remove unneeded async add salt to keyed circuit selection
1 parent b5dfa12 commit e581dcb

10 files changed

Lines changed: 539 additions & 40 deletions

File tree

crates/cdk-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ default = []
1515
sqlcipher = ["cdk-sqlite/sqlcipher"]
1616
# MSRV is not tracked with redb enabled
1717
redb = ["dep:cdk-redb"]
18+
tor = ["cdk/tor"]
1819

1920
[dependencies]
2021
anyhow.workspace = true

crates/cdk-cli/src/main.rs

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ use cdk::wallet::MultiMintWallet;
1313
#[cfg(feature = "redb")]
1414
use cdk_redb::WalletRedbDatabase;
1515
use cdk_sqlite::WalletSqliteDatabase;
16+
#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
17+
use clap::ValueEnum;
1618
use clap::{Parser, Subcommand};
1719
use tracing::Level;
1820
use tracing_subscriber::EnvFilter;
@@ -27,11 +29,15 @@ const DEFAULT_WORK_DIR: &str = ".cdk-cli";
2729
const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
2830

2931
/// Simple CLI application to interact with cashu
32+
#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
33+
#[derive(Copy, Clone, Debug, ValueEnum)]
34+
enum TorToggle {
35+
On,
36+
Off,
37+
}
38+
3039
#[derive(Parser)]
31-
#[command(name = "cdk-cli")]
32-
#[command(author = "thesimplekid <tsk@thesimplekid.com>")]
33-
#[command(version = CARGO_PKG_VERSION.unwrap_or("Unknown"))]
34-
#[command(author, version, about, long_about = None)]
40+
#[command(name = "cdk-cli", author = "thesimplekid <tsk@thesimplekid.com>", version = CARGO_PKG_VERSION.unwrap_or("Unknown"), about, long_about = None)]
3541
struct Cli {
3642
/// Database engine to use (sqlite/redb)
3743
#[arg(short, long, default_value = "sqlite")]
@@ -52,6 +58,11 @@ struct Cli {
5258
/// Currency unit to use for the wallet
5359
#[arg(short, long, default_value = "sat")]
5460
unit: String,
61+
/// Use Tor transport (only when built with --features tor). Defaults to 'on' when feature is enabled.
62+
#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
63+
#[arg(long = "tor", value_enum, default_value_t = TorToggle::On)]
64+
transport: TorToggle,
65+
/// Subcommand to run
5566
#[command(subcommand)]
5667
command: Commands,
5768
}
@@ -120,8 +131,6 @@ async fn main() -> Result<()> {
120131
}
121132
};
122133

123-
fs::create_dir_all(&work_dir)?;
124-
125134
let localstore: Arc<dyn WalletDatabase<Err = cdk_database::Error> + Send + Sync> =
126135
match args.engine.as_str() {
127136
"sqlite" => {
@@ -181,7 +190,6 @@ async fn main() -> Result<()> {
181190
// The constructor will automatically load wallets for this currency unit
182191
let multi_mint_wallet = match &args.proxy {
183192
Some(proxy_url) => {
184-
// Create MultiMintWallet with proxy configuration
185193
MultiMintWallet::new_with_proxy(
186194
localstore.clone(),
187195
seed,
@@ -190,7 +198,29 @@ async fn main() -> Result<()> {
190198
)
191199
.await?
192200
}
193-
None => MultiMintWallet::new(localstore.clone(), seed, currency_unit.clone()).await?,
201+
None => {
202+
#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
203+
{
204+
match args.transport {
205+
TorToggle::On => {
206+
MultiMintWallet::new_with_tor(
207+
localstore.clone(),
208+
seed,
209+
currency_unit.clone(),
210+
)
211+
.await?
212+
}
213+
TorToggle::Off => {
214+
MultiMintWallet::new(localstore.clone(), seed, currency_unit.clone())
215+
.await?
216+
}
217+
}
218+
}
219+
#[cfg(not(all(feature = "tor", not(target_arch = "wasm32"))))]
220+
{
221+
MultiMintWallet::new(localstore.clone(), seed, currency_unit.clone()).await?
222+
}
223+
}
194224
};
195225

196226
match &args.command {

crates/cdk/Cargo.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ bip353 = ["dep:hickory-resolver"]
2121
swagger = ["mint", "dep:utoipa", "cdk-common/swagger"]
2222
bench = []
2323
http_subscription = []
24+
tor = [
25+
"wallet",
26+
"dep:arti-client",
27+
"dep:arti-hyper",
28+
"dep:hyper",
29+
"dep:http",
30+
"dep:rustls",
31+
"dep:tor-rtcompat",
32+
"dep:tls-api",
33+
"dep:tls-api-native-tls",
34+
]
2435
prometheus = ["dep:cdk-prometheus"]
2536

2637
[dependencies]
@@ -40,6 +51,7 @@ serde_json.workspace = true
4051
serde_with.workspace = true
4152
tracing.workspace = true
4253
thiserror.workspace = true
54+
4355
futures = { workspace = true, optional = true, features = ["alloc"] }
4456
url.workspace = true
4557
utoipa = { workspace = true, optional = true }
@@ -70,13 +82,23 @@ tokio-tungstenite = { workspace = true, features = [
7082
"rustls-tls-native-roots",
7183
"connect"
7284
] }
85+
# Tor dependencies (optional; enabled by feature "tor")
86+
hyper = { version = "0.14", optional = true, features = ["client", "http1", "http2"] }
87+
http = { version = "0.2", optional = true }
88+
arti-client = { version = "0.19.0", optional = true, default-features = false, features = ["tokio", "rustls"] }
89+
arti-hyper = { version = "0.19.0", optional = true }
7390
rustls = { workspace = true, optional = true }
91+
tor-rtcompat = { version = "0.19.0", optional = true, features = ["tokio", "rustls"] }
92+
tls-api = { version = "0.9", optional = true }
93+
tls-api-native-tls = { version = "0.9", optional = true }
7494

7595
[target.'cfg(target_arch = "wasm32")'.dependencies]
7696
tokio = { workspace = true, features = ["rt", "macros", "sync", "time"] }
7797
cdk-signatory = { workspace = true, default-features = false }
7898
getrandom = { version = "0.2", features = ["js"] }
7999
ring = { version = "0.17.14", features = ["wasm32_unknown_unknown_js"] }
100+
rustls = { workspace = true, optional = true }
101+
80102
uuid = { workspace = true, features = ["js"] }
81103
wasm-bindgen = "0.2"
82104
wasm-bindgen-futures = "0.4"

crates/cdk/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
#![warn(missing_docs)]
44
#![warn(rustdoc::bare_urls)]
55

6+
// Disallow enabling `tor` feature on wasm32 with a clear error.
7+
#[cfg(all(target_arch = "wasm32", feature = "tor"))]
8+
compile_error!("The 'tor' feature is not supported on wasm32 targets (browser). Disable the 'tor' feature or use a non-wasm32 target.");
9+
610
pub mod cdk_database {
711
//! CDK Database
812
pub use cdk_common::database::Error;

crates/cdk/src/wallet/mint_connector/http_client.rs

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,31 @@ impl<T> HttpClient<T>
4747
where
4848
T: Transport + Send + Sync + 'static,
4949
{
50+
/// Create new [`HttpClient`] with a provided transport implementation.
51+
#[cfg(feature = "auth")]
52+
pub fn with_transport(
53+
mint_url: MintUrl,
54+
transport: T,
55+
auth_wallet: Option<AuthWallet>,
56+
) -> Self {
57+
Self {
58+
transport: transport.into(),
59+
mint_url,
60+
auth_wallet: Arc::new(RwLock::new(auth_wallet)),
61+
cache_support: Default::default(),
62+
}
63+
}
64+
65+
/// Create new [`HttpClient`] with a provided transport implementation.
66+
#[cfg(not(feature = "auth"))]
67+
pub fn with_transport(mint_url: MintUrl, transport: T) -> Self {
68+
Self {
69+
transport: transport.into(),
70+
mint_url,
71+
cache_support: Default::default(),
72+
}
73+
}
74+
5075
/// Create new [`HttpClient`]
5176
#[cfg(feature = "auth")]
5277
pub fn new(mint_url: MintUrl, auth_wallet: Option<AuthWallet>) -> Self {
@@ -137,22 +162,20 @@ where
137162
.map(Duration::from_secs)
138163
.unwrap_or_default();
139164

165+
let transport = self.transport.clone();
140166
loop {
141167
let url = self.mint_url.join_paths(&match path {
142168
nut19::Path::MintBolt11 => vec!["v1", "mint", "bolt11"],
143169
nut19::Path::MeltBolt11 => vec!["v1", "melt", "bolt11"],
144170
nut19::Path::MintBolt12 => vec!["v1", "mint", "bolt12"],
171+
145172
nut19::Path::MeltBolt12 => vec!["v1", "melt", "bolt12"],
146173
nut19::Path::Swap => vec!["v1", "swap"],
147174
})?;
148175

149176
let result = match method {
150-
nut19::Method::Get => self.transport.http_get(url, auth_token.clone()).await,
151-
nut19::Method::Post => {
152-
self.transport
153-
.http_post(url, auth_token.clone(), payload)
154-
.await
155-
}
177+
nut19::Method::Get => transport.http_get(url, auth_token.clone()).await,
178+
nut19::Method::Post => transport.http_post(url, auth_token.clone(), payload).await,
156179
};
157180

158181
if result.is_ok() {
@@ -197,12 +220,9 @@ where
197220
#[instrument(skip(self), fields(mint_url = %self.mint_url))]
198221
async fn get_mint_keys(&self) -> Result<Vec<KeySet>, Error> {
199222
let url = self.mint_url.join_paths(&["v1", "keys"])?;
223+
let transport = self.transport.clone();
200224

201-
Ok(self
202-
.transport
203-
.http_get::<KeysResponse>(url, None)
204-
.await?
205-
.keysets)
225+
Ok(transport.http_get::<KeysResponse>(url, None).await?.keysets)
206226
}
207227

208228
/// Get Keyset Keys [NUT-01]
@@ -212,7 +232,8 @@ where
212232
.mint_url
213233
.join_paths(&["v1", "keys", &keyset_id.to_string()])?;
214234

215-
let keys_response = self.transport.http_get::<KeysResponse>(url, None).await?;
235+
let transport = self.transport.clone();
236+
let keys_response = transport.http_get::<KeysResponse>(url, None).await?;
216237

217238
Ok(keys_response.keysets.first().unwrap().clone())
218239
}
@@ -221,7 +242,8 @@ where
221242
#[instrument(skip(self), fields(mint_url = %self.mint_url))]
222243
async fn get_mint_keysets(&self) -> Result<KeysetResponse, Error> {
223244
let url = self.mint_url.join_paths(&["v1", "keysets"])?;
224-
self.transport.http_get(url, None).await
245+
let transport = self.transport.clone();
246+
transport.http_get(url, None).await
225247
}
226248

227249
/// Mint Quote [NUT-04]
@@ -368,7 +390,8 @@ where
368390
/// Helper to get mint info
369391
async fn get_mint_info(&self) -> Result<MintInfo, Error> {
370392
let url = self.mint_url.join_paths(&["v1", "info"])?;
371-
let info: MintInfo = self.transport.http_get(url, None).await?;
393+
let transport = self.transport.clone();
394+
let info: MintInfo = transport.http_get(url, None).await?;
372395

373396
if let Ok(mut cache_support) = self.cache_support.write() {
374397
*cache_support = (

crates/cdk/src/wallet/mint_connector/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ pub mod transport;
2121
/// Auth HTTP Client with async transport
2222
#[cfg(feature = "auth")]
2323
pub type AuthHttpClient = http_client::AuthHttpClient<transport::Async>;
24-
/// Http Client with async transport
24+
/// Default Http Client with async transport (non-Tor)
2525
pub type HttpClient = http_client::HttpClient<transport::Async>;
26+
/// Tor Http Client with async transport (only when `tor` feature is enabled and not on wasm32)
27+
#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
28+
pub type TorHttpClient = http_client::HttpClient<transport::tor_transport::TorAsync>;
2629

2730
/// Interface that connects a wallet to a mint. Typically represents an [HttpClient].
2831
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]

crates/cdk/src/wallet/mint_connector/transport.rs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,30 @@ pub trait Transport: Default + Send + Sync + Debug + Clone {
2727
/// Make the transport to use a given proxy
2828
fn with_proxy(
2929
&mut self,
30-
proxy: Url,
30+
proxy: url::Url,
3131
host_matcher: Option<&str>,
3232
accept_invalid_certs: bool,
33-
) -> Result<(), Error>;
33+
) -> Result<(), super::Error>;
3434

3535
/// HTTP Get request
36-
async fn http_get<R>(&self, url: Url, auth: Option<AuthToken>) -> Result<R, Error>
36+
async fn http_get<R>(
37+
&self,
38+
url: url::Url,
39+
auth: Option<cdk_common::AuthToken>,
40+
) -> Result<R, super::Error>
3741
where
38-
R: DeserializeOwned;
42+
R: serde::de::DeserializeOwned;
3943

4044
/// HTTP Post request
4145
async fn http_post<P, R>(
4246
&self,
43-
url: Url,
44-
auth_token: Option<AuthToken>,
47+
url: url::Url,
48+
auth_token: Option<cdk_common::AuthToken>,
4549
payload: &P,
46-
) -> Result<R, Error>
50+
) -> Result<R, super::Error>
4751
where
48-
P: Serialize + ?Sized + Send + Sync,
49-
R: DeserializeOwned;
52+
P: serde::Serialize + ?Sized + Send + Sync,
53+
R: serde::de::DeserializeOwned;
5054
}
5155

5256
/// Async transport for Http
@@ -212,3 +216,6 @@ impl Transport for Async {
212216
})
213217
}
214218
}
219+
220+
#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
221+
pub mod tor_transport;

0 commit comments

Comments
 (0)