Arti: A pure-Rust Tor Implementation for Zcash and beyond

I recently spent about a day adding Tor support to Zebra’s isolated connection API using arti-client.

When Zebra adds support for sending user-generated transactions, we can use this API to make sure they aren’t linked to the sender’s IP address. (Or to any other transactions we send.)

Overall the process went really smoothly. The API was easy to understand, the code is short and easy to read, and basic tests seem to work. (I wasn’t able to do comprehensive end-to-end tests, because Zebra doesn’t support creating transactions yet.)

Here’s some detailed feedback about some tricky parts of the integration.

Dependency Versions

The dependency versions that arti requires seem quite strict for a library crate. We needed to do a few dependency upgrades for Zebra as part of integration. These upgrades were simple using cargo upgrade. But sometimes dependency upgrades might need code changes.

Can arti depend on the earliest supported minor release of each dependency in Cargo.toml?
You can use Cargo.lock to specify exact dependencies for binary crates.

Dependency Troubleshooting

It would be helpful to mention:

  • installing pkg-config, and
  • setting OPENSSL_DIR and SQLITE3_LIB_DIR for troubleshooting header or linker errors.

I also needed to set LD_LIBRARY_PATH, but that’s probably a Nix derivation bug.

The troubleshooting doc was a bit hard to find, maybe it could be linked from the readme.

Useful APIs

There are a few missing APIs that would help make our code more efficient, and might reduce our load on the Tor network:

An API that bootstraps Tor if needed, then returns a cloned Tor client

Making a shared TorClient instance was a bit tricky, because it’s hard to lazily call async code in Rust, and then share the result. So I ended up wrapping the shared value in an Arc<Mutex<_>>. But this seems like a bit of a waste, because there’s already a lot of locking inside TorClient.

Here are some API changes that might have made that easier:

  • an API or example code that initializes a shared TorClient when it is first used, then clones the shared instance for future requests
  • a non-async API that configures and allocates a shared TorClient, then bootstraps it when it actually gets its first request (or when an async bootstrap method gets called)
  • if possible, making arti_client::Error cloneable, so arti futures be used with FutureExt::shared

A TorClient config that makes sure all connections are isolated

In Zebra, we don’t plan on sending blockchain blocks over Tor, because it’s all public information. So we’ll just be occasionally sending new user-generated transactions over Tor.

So it would be helpful to have a TorClient config that guarantees all connections are isolated. It might also save some preemptive circuit load on the network. (Unless all preemptive circuits are guaranteed to be isolated already - I can’t remember.)

But I can understand if you don’t want to expose this as a configurable client mode. Because it will place more load on the network.

Minor API nitpicks

arti_client::Result is a useful type alias, it would be nice for it to be available outside the arti crates. That way, our tor-specific APIs don’t have to use BoxError everywhere.

It seems like the stream timeout config builder isn’t exposed by the client config builder. But we didn’t need it anyway.

In Zebra we’ll probably end up creating a tower::Service that takes hostnames, and returns arti_client::DataStreams. So we might not actually need any of these API changes - we can do it all in the service. Cloneable errors would be nice though!

10 Likes