Using BDK with Tor
# Introduction
It’s easy to underestimate the importance of privacy tech for Bitcoin, especially when connecting to third party services. They can learn your IP address and associate the transactions you sent over it. You can only hope that this information will not be leaked anytime in the future with unpredictable consequences. In order to use Bitcoin privately, you need to encrypt and anonymize the data you send over the Internet.
Tor is one of the must-have privacy preserving tools for the Internet in general, and for Bitcoin in particular. Tor network consists of nodes that use clever cryptographic methods to encrypt user data and transfer them as anonymously as possible.
In this article we show how to integrate Tor with your BDK application.
# Prerequisite
First, you would need to have a Tor daemon up and running.
On Mac OS X you can install with Homebrew.
brew install tor
brew services start tor
On Ubuntu or other Debian-based distributions.
sudo apt install tor
In some cases you'll need to wait a minute or two for the bootstrapping to finish. In general, Tor is not the fastest network, so if any of the examples below fail due to timeout, simply restart it.
At the very end of the article we’ll show how to integrate Tor directly to your application.
By default, Tor creates a SOCKS5 (opens new window) proxy
endpoint and listens on port 9050. Your application should connect to the
proxy on localhost:9050
and use it for its network activities.
# Setting Up
Create a new cargo project.
mkdir ~/tutorial
cd tutorial
cargo new bdk-tor
cd bdk-tor
Open src/main.rs
file remove all its contents and add these lines.
use std::str::FromStr;
use bdk::bitcoin::util::bip32;
use bdk::bitcoin::util::bip32::ExtendedPrivKey;
use bdk::bitcoin::Network;
use bdk::database::MemoryDatabase;
use bdk::template::Bip84;
use bdk::{KeychainKind, SyncOptions, Wallet};
// add additional imports here
fn main() {
let network = Network::Testnet;
let xpriv = "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy";
let xpriv = bip32::ExtendedPrivKey::from_str(xpriv).unwrap();
let blockchain = create_blockchain();
let wallet = create_wallet(&network, &xpriv);
println!("Syncing the wallet...");
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
println!(
"The wallet synced. Height: {}",
blockchain.get_height().unwrap()
);
}
fn create_wallet(network: &Network, xpriv: &ExtendedPrivKey) -> Wallet<MemoryDatabase> {
Wallet::new(
Bip84(*xpriv, KeychainKind::External),
Some(Bip84(*xpriv, KeychainKind::Internal)),
*network,
MemoryDatabase::default(),
)
.unwrap()
}
In this code we create a testnet wallet with create_wallet()
function and
try to sync it with a specific blockchain client implementation. We create a
blockchain client using create_blockchain()
function. We’ll implement it
later for each type of blockchain client supported by BDK.
# ElectrumBlockchain
The Electrum client is enabled by default so the Cargo.toml
dependencies
section will look like this.
[dependencies]
bdk = { version = "^0.26"}
And the imports look like this.
use bdk::blockchain::{ElectrumBlockchain, GetHeight};
use bdk::electrum_client::{Client, ConfigBuilder, Socks5Config};
Here is the implementation of create_blockchain()
function for the
Electrum client.
fn create_blockchain() -> ElectrumBlockchain {
let url = "ssl://electrum.blockstream.info:60002";
let socks_addr = "127.0.0.1:9050";
println!("Connecting to {} via {}", &url, &socks_addr);
let config = ConfigBuilder::new()
.socks5(Some(Socks5Config {
addr: socks_addr.to_string(),
credentials: None,
}))
.unwrap()
.build();
let client = Client::from_config(url, config).unwrap();
ElectrumBlockchain::from(client)
}
In this example we create an instance of Socks5Config
which defines the
Tor proxy parameters for ElectrumBlockchain
.
# Blocking EsploraBlockchain
The blocking version of EsploraBlockchain
uses ureq
crate to send HTTP
requests to Eslora backends. By default, its SOCKS5 feature is disabled,
so we need to enable it in Cargo.toml
.
[dependencies]
bdk = { version = "^0.26", default-features = false, features = ["use-esplora-blocking"]}
The imports are
use bdk::blockchain::{EsploraBlockchain, GetHeight};
use bdk::blockchain::esplora::EsploraBlockchainConfig;
use bdk::blockchain::ConfigurableBlockchain;
And create_blockchain()
implementation is
fn create_blockchain() -> EsploraBlockchain {
let url = "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/testnet/api";
let socks_url = "socks5://127.0.0.1:9050";
println!("Connecting to {} via {}", &url, &socks_url);
let config = EsploraBlockchainConfig {
base_url: url.into(),
proxy: Some(socks_url.into()),
concurrency: None,
stop_gap: 20,
timeout: Some(120),
};
EsploraBlockchain::from_config(&config).unwrap()
}
Here we use proxy()
method of the config builder to set the Tor proxy
address. Please note, that unlike the previous examples, the Esplora client
builder requires not just a proxy address, but a URL
“socks5://127.0.0.1:9050”.
# Asynchronous EsploraBlockchain
There’s no need in enabling SOCKS5 for the asynchronous Esplora client, so we are good to go without additional dependencies.
[dependencies]
bdk = { version = "^0.26", default-features = false, features = ["use-esplora-async"]}
The imports are the same as in previous example.
use bdk::blockchain::{EsploraBlockchain, GetHeight};
use bdk::blockchain::esplora::EsploraBlockchainConfig;
use bdk::blockchain::ConfigurableBlockchain;
create_blockchain()
is almost identical.
fn create_blockchain() -> EsploraBlockchain {
let url = "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/testnet/api";
let socks_url = "socks5h://127.0.0.1:9050";
println!("Connecting to {} via {}", &url, &socks_url);
let config = EsploraBlockchainConfig {
base_url: url.into(),
proxy: Some(socks_url.into()),
concurrency: None,
stop_gap: 20,
timeout: Some(120),
};
EsploraBlockchain::from_config(&config).unwrap()
}
There are two notable differences though. First, we call build_async()
to
create an asynchronous Esplora client. Second the SOCKS5 URL scheme is
“socks5h”. It’s not a typo. The async client supports two SOCKS5 schemes
“socks5” and “socks5h”. The difference between them is that the former
makes the client to resolve domain names, and the latter does not, so the
client passes them to the proxy as is. A regular DNS resolver cannot
resolve Tor onion addresses, so we should use “socks5h” here.
# CompactFiltersBlockchain
Add these lines to the dependencies section of Cargo.toml
file to enable
BIP-157/BIP-158 compact filter support.
It can take a while to sync a wallet using compact filters over Tor, so be patient.
[dependencies]
bdk = { version = "^0.26", default-features = false, features = ["compact_filters"]}
Now add the required imports into src/main.rs
.
use std::sync::Arc;
use bdk::blockchain::compact_filters::{Mempool, Peer};
use bdk::blockchain::{CompactFiltersBlockchain, GetHeight};
create_blockchain()
function will look like this.
fn create_blockchain() -> CompactFiltersBlockchain {
let peer_addr = "neutrino.testnet3.suredbits.com:18333";
let socks_addr = "127.0.0.1:9050";
let mempool = Arc::new(Mempool::default());
println!("Connecting to {} via {}", peer_addr, socks_addr);
let peer =
Peer::connect_proxy(peer_addr, socks_addr, None, mempool, Network::Testnet).unwrap();
CompactFiltersBlockchain::new(vec![peer], "./wallet-filters", Some(500_000)).unwrap()
}
Here we use Peer::connect_proxy()
which accepts the address to the SOCKS5
proxy and performs all the heavy lifting for us.
# Integrated Tor daemon
As an application developer you don’t have to rely on your users to install
and start Tor to use your application. Using libtor
crate you can bundle
Tor daemon with your app.
libtor
builds a Tor binary from the source files. Since Tor is written in C
you'll need a C compiler and build tools.
Install these packages on Mac OS X:
xcode-select --install
brew install autoconf
brew install automake
brew install libtool
brew install openssl
brew install pkg-config
export LDFLAGS="-L/opt/homebrew/opt/openssl/lib"
export CPPFLAGS="-I/opt/homebrew/opt/openssl/include"
Or these packages on Ubuntu or another Debian-based Linux distribution:
sudo apt install autoconf automake clang file libtool openssl pkg-config
Then add these dependencies to the Cargo.toml
file.
[dependencies]
bdk = { version = "^0.26" }
libtor = "47.8.0+0.4.7.x"
This is an example of how we can use libtor
to start a Tor daemon.
use std::fs::File;
use std::io::prelude::*;
use std::thread;
use std::time::Duration;
use libtor::LogDestination;
use libtor::LogLevel;
use libtor::{HiddenServiceVersion, Tor, TorAddress, TorFlag};
use std::env;
pub fn start_tor() -> String {
let socks_port = 19050;
let data_dir = format!("{}/{}", env::temp_dir().display(), "bdk-tor");
let log_file_name = format!("{}/{}", &data_dir, "log");
println!("Staring Tor in {}", &data_dir);
truncate_log(&log_file_name);
Tor::new()
.flag(TorFlag::DataDirectory(data_dir.into()))
.flag(TorFlag::LogTo(
LogLevel::Notice,
LogDestination::File(log_file_name.as_str().into()),
))
.flag(TorFlag::SocksPort(socks_port))
.flag(TorFlag::Custom("ExitPolicy reject *:*".into()))
.flag(TorFlag::Custom("BridgeRelay 0".into()))
.start_background();
let mut started = false;
let mut tries = 0;
while !started {
tries += 1;
if tries > 120 {
panic!(
"It took too long to start Tor. See {} for details",
&log_file_name
);
}
thread::sleep(Duration::from_millis(1000));
started = find_string_in_log(&log_file_name, &"Bootstrapped 100%".into());
}
println!("Tor started");
format!("127.0.0.1:{}", socks_port)
}
First, we create a Tor object, and then we call start_background()
method
to start it in the background. After that, we continuously try to find
“Bootstrapped 100%” string in the log file. Once we find it, Tor is
ready to proxy our connections. We use port 19050 because, the user can
have their own instance of Tor running already.
Next you can modify create_blockchain()
like this
fn create_blockchain() -> ElectrumBlockchain {
let url = "ssl://electrum.blockstream.info:60002";
let socks_addr = start_tor();
...
}
In this example we start Tor first, then use the address returned by
start_tor()
function as proxy address.
We omitted find_string_in_log()
and truncate_log()
for brevity. You
can find their implementations in esplora_backend_with_tor.rs (opens new window)