Skip to content

Commit

Permalink
Implement directory servers for finding makers
Browse files Browse the repository at this point in the history
Directory servers are http servers running somewhere on the tor
dark net. Makers can publish their own tor onions to these servers.
Takers can download the list of these maker onions and from there
obtain a list of makers they can do coinswaps with.

This commit adds code for obtaining the list of onions, and publishing
your own onion. The commit also has the taker code obtain the list
of onions, and also has the maker code publish and refresh its own
entry.
  • Loading branch information
chris-belcher committed Feb 13, 2022
1 parent 5f9d50b commit 173a1c0
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 23 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -22,6 +22,7 @@ itertools = "0.9.0"
structopt = "0.3.21"
dirs = "3.0.1"
tokio-socks = "0.5"
reqwest = { version = "0.11", features = ["socks"] }

#Empty default feature set, (helpful to generalise in github actions)
[features]
Expand Down
103 changes: 103 additions & 0 deletions src/directory_servers.rs
@@ -0,0 +1,103 @@
//configure this with your own tor port
pub const TOR_ADDR: &str = "127.0.0.1:9150";

use bitcoin::Network;

use crate::offerbook_sync::MakerAddress;

//for now just one of these, but later we'll need multiple for good decentralization
const DIRECTORY_SERVER_ADDR: &str =
"zfwo4t5yfuf6epu7rhjbmkr6kiysi6v7kibta4i55zlp4y6xirpcr7qd.onion:8080";

#[derive(Debug)]
pub enum DirectoryServerError {
Reqwest(reqwest::Error),
Other(&'static str),
}

impl From<reqwest::Error> for DirectoryServerError {
fn from(e: reqwest::Error) -> DirectoryServerError {
DirectoryServerError::Reqwest(e)
}
}

fn network_enum_to_string(network: Network) -> &'static str {
match network {
Network::Bitcoin => "mainnet",
Network::Testnet => "testnet",
Network::Regtest => panic!("dont use directory servers if using regtest"),
}
}

pub async fn sync_maker_hosts_from_directory_servers(
network: Network,
) -> Result<Vec<MakerAddress>, DirectoryServerError> {
// https://github.com/seanmonstar/reqwest/blob/master/examples/tor_socks.rs
let proxy =
reqwest::Proxy::all(format!("socks5h://{}", TOR_ADDR)).expect("tor proxy should be there");
let client = reqwest::Client::builder()
.proxy(proxy)
.build()
.expect("should be able to build reqwest client");
let res = client
.get(format!(
"http://{}/makers-{}.txt",
DIRECTORY_SERVER_ADDR,
network_enum_to_string(network)
))
.send()
.await?;
if res.status().as_u16() != 200 {
return Err(DirectoryServerError::Other("status code not success"));
}
let mut maker_addresses = Vec::<MakerAddress>::new();
for makers in res.text().await?.split("\n") {
let csv_chunks = makers.split(",").collect::<Vec<&str>>();
if csv_chunks.len() < 2 {
continue;
}
maker_addresses.push(MakerAddress::Tor {
address: String::from(csv_chunks[1]),
});
log::debug!(target:"directory_servers", "expiry timestamp = {} hostname = {}",
csv_chunks[0], csv_chunks[1]);
}
Ok(maker_addresses)
}

pub async fn post_maker_host_to_directory_servers(
network: Network,
address: &str,
) -> Result<u64, DirectoryServerError> {
let proxy =
reqwest::Proxy::all(format!("socks5h://{}", TOR_ADDR)).expect("tor proxy should be there");
let client = reqwest::Client::builder()
.proxy(proxy)
.build()
.expect("should be able to build reqwest client");
let params = [
("address", address),
("net", network_enum_to_string(network)),
];
let res = client
.post(format!("http://{}/directoryserver", DIRECTORY_SERVER_ADDR))
.form(&params)
.send()
.await?;
if res.status().as_u16() != 200 {
return Err(DirectoryServerError::Other("status code not success"));
}
let body = res.text().await?;
let start_bytes = body
.find("<b>")
.ok_or(DirectoryServerError::Other("expiry time not parsable1"))?
+ 3;
let end_bytes = body
.find("</b>")
.ok_or(DirectoryServerError::Other("expiry time not parsable2"))?;
let expiry_time_str = &body[start_bytes..end_bytes];
let expiry_time = expiry_time_str
.parse::<u64>()
.map_err(|_| DirectoryServerError::Other("expiry time not parsable3"))?;
Ok(expiry_time)
}
2 changes: 2 additions & 0 deletions src/lib.rs
Expand Up @@ -28,6 +28,7 @@ use maker_protocol::MakerBehavior;
pub mod taker_protocol;
use taker_protocol::TakerConfig;

pub mod directory_servers;
pub mod error;
pub mod messages;
pub mod offerbook_sync;
Expand Down Expand Up @@ -385,6 +386,7 @@ pub fn run_maker(
port,
rpc_ping_interval: 60,
watchtower_ping_interval: 300,
directory_servers_refresh_interval: 60 * 60 * 12, //12 hours
maker_behavior,
kill_flag: if kill_flag.is_none() {
Arc::new(RwLock::new(false))
Expand Down
44 changes: 35 additions & 9 deletions src/maker_protocol.rs
@@ -1,3 +1,14 @@
//put your onion hostname and port here
const MAKER_ONION_ADDR: &str = "myhiddenservicehostname.onion:6102";
const ABSOLUTE_FEE_SAT: u64 = 1000;
const AMOUNT_RELATIVE_FEE_PPB: u64 = 10_000_000;
const TIME_RELATIVE_FEE_PPB: u64 = 100_000;
const REQUIRED_CONFIRMS: i32 = 1;
const MINIMUM_LOCKTIME: u16 = 3;
const MIN_SIZE: u64 = 10000;

//TODO this goes in the config file

use std::net::{Ipv4Addr, SocketAddr};
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
Expand All @@ -22,25 +33,18 @@ use crate::contracts::{
calculate_coinswap_fee, find_funding_output, read_hashvalue_from_contract,
read_locktime_from_contract, MAKER_FUNDING_TX_VBYTE_SIZE,
};
use crate::directory_servers::post_maker_host_to_directory_servers;
use crate::error::Error;
use crate::messages::{
HashPreimage, MakerHello, MakerToTakerMessage, Offer, PrivateKeyHandover, ProofOfFunding,
ReceiversContractSig, SenderContractTxInfo, SendersAndReceiversContractSigs,
SendersContractSig, SignReceiversContractTx, SignSendersAndReceiversContractTxes,
SignSendersContractTx, SwapCoinPrivateKey, TakerToMakerMessage,
};
use crate::wallet_sync::{IncomingSwapCoin, OutgoingSwapCoin, Wallet, WalletSwapCoin};
use crate::wallet_sync::{IncomingSwapCoin, OutgoingSwapCoin, Wallet, WalletSwapCoin, NETWORK};
use crate::watchtower_client::{ping_watchtowers, register_coinswap_with_watchtowers};
use crate::watchtower_protocol::{ContractTransaction, ContractsInfo};

//TODO this goes in the config file
const ABSOLUTE_FEE_SAT: u64 = 1000;
const AMOUNT_RELATIVE_FEE_PPB: u64 = 10_000_000;
const TIME_RELATIVE_FEE_PPB: u64 = 100_000;
const REQUIRED_CONFIRMS: i32 = 1;
const MINIMUM_LOCKTIME: u16 = 3;
const MIN_SIZE: u64 = 10000;

//used to configure the maker do weird things for testing
#[derive(Debug, Clone, Copy)]
pub enum MakerBehavior {
Expand All @@ -53,6 +57,7 @@ pub struct MakerConfig {
pub port: u16,
pub rpc_ping_interval: u64,
pub watchtower_ping_interval: u64,
pub directory_servers_refresh_interval: u64,
pub maker_behavior: MakerBehavior,
pub kill_flag: Arc<RwLock<bool>>,
pub idle_connection_timeout: u64,
Expand Down Expand Up @@ -106,12 +111,19 @@ async fn run(

log::info!("Pinging watchtowers. . .");
ping_watchtowers().await?;

log::info!("Adding my address at the directory servers. . .");
post_maker_host_to_directory_servers(NETWORK, MAKER_ONION_ADDR)
.await
.unwrap();

let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, config.port)).await?;
log::info!("Listening On Port {}", config.port);

let (server_loop_comms_tx, mut server_loop_comms_rx) = mpsc::channel::<Error>(100);
let mut accepting_clients = true;
let mut last_watchtowers_ping = Instant::now();
let mut last_directory_servers_refresh = Instant::now();

let my_kill_flag = config.kill_flag.clone();

Expand Down Expand Up @@ -148,6 +160,20 @@ async fn run(
if *my_kill_flag.read().unwrap() {
break Err(Error::Protocol("kill flag is true"));
}

let directory_servers_refresh_interval = Duration::from_secs(
config.directory_servers_refresh_interval
);
if Instant::now().saturating_duration_since(last_directory_servers_refresh)
> directory_servers_refresh_interval {
last_directory_servers_refresh = Instant::now();
let result_expiry_time = post_maker_host_to_directory_servers(
NETWORK,
MAKER_ONION_ADDR
).await;
log::info!("Refreshing my address at the directory servers = {:?}",
result_expiry_time);
}
continue;
},
};
Expand Down
38 changes: 28 additions & 10 deletions src/offerbook_sync.rs
Expand Up @@ -6,14 +6,18 @@ use tokio::select;
use tokio::sync::mpsc;
use tokio::time::sleep;

use bitcoin::Network;

use crate::directory_servers::{
sync_maker_hosts_from_directory_servers, DirectoryServerError, TOR_ADDR,
};
use crate::error::Error;
use crate::messages::{GiveOffer, MakerToTakerMessage, Offer, TakerToMakerMessage};
use crate::taker_protocol::{
handshake_maker, read_message, send_message, FIRST_CONNECT_ATTEMPTS,
FIRST_CONNECT_ATTEMPT_TIMEOUT_SEC, FIRST_CONNECT_SLEEP_DELAY_SEC,
};

const TOR_ADDR: &str = "127.0.0.1:9150";
use crate::wallet_sync::NETWORK;

#[derive(Debug, Clone)]
pub enum MakerAddress {
Expand All @@ -35,8 +39,13 @@ const REGTEST_MAKER_HOSTS: &'static [&'static str] = &[
"localhost:46102",
];

pub fn get_regtest_maker_hosts() -> Vec<&'static str> {
Vec::from(REGTEST_MAKER_HOSTS)
fn get_regtest_maker_addresses() -> Vec<MakerAddress> {
REGTEST_MAKER_HOSTS
.iter()
.map(|h| MakerAddress::Clearnet {
address: h.to_string(),
})
.collect::<Vec<MakerAddress>>()
}

impl MakerAddress {
Expand Down Expand Up @@ -117,27 +126,36 @@ async fn download_maker_offer(address: MakerAddress) -> Option<OfferAndAddress>
}
}

pub async fn sync_offerbook(maker_hostnames: &Vec<&str>) -> Vec<OfferAndAddress> {
async fn sync_offerbook_with_hostnames(maker_addresses: Vec<MakerAddress>) -> Vec<OfferAndAddress> {
let (offers_writer_m, mut offers_reader) = mpsc::channel::<Option<OfferAndAddress>>(100);
//unbounded_channel makes more sense here, but results in a compile
//error i cant figure out

for host in maker_hostnames {
let maker_addresses_len = maker_addresses.len();
for addr in maker_addresses {
let offers_writer = offers_writer_m.clone();
let address = host.to_string();
tokio::spawn(async move {
let addr = MakerAddress::Clearnet { address };
if let Err(_e) = offers_writer.send(download_maker_offer(addr).await).await {
panic!("mpsc failed");
}
});
}

let mut result = Vec::<OfferAndAddress>::new();
for _ in 0..maker_hostnames.len() {
for _ in 0..maker_addresses_len {
if let Some(offer_addr) = offers_reader.recv().await.unwrap() {
result.push(offer_addr);
}
}
result
}

pub async fn sync_offerbook() -> Result<Vec<OfferAndAddress>, DirectoryServerError> {
Ok(
sync_offerbook_with_hostnames(if NETWORK == Network::Regtest {
get_regtest_maker_addresses()
} else {
sync_maker_hosts_from_directory_servers(NETWORK).await?
})
.await,
)
}
8 changes: 4 additions & 4 deletions src/taker_protocol.rs
Expand Up @@ -39,9 +39,7 @@ use crate::messages::{
SignSendersContractTx, SwapCoinPrivateKey, TakerHello, TakerToMakerMessage, PREIMAGE_LEN,
};

use crate::offerbook_sync::{
get_regtest_maker_hosts, sync_offerbook, MakerAddress, OfferAndAddress,
};
use crate::offerbook_sync::{sync_offerbook, MakerAddress, OfferAndAddress};
use crate::wallet_sync::{
generate_keypair, import_watchonly_redeemscript, IncomingSwapCoin, OutgoingSwapCoin, Wallet,
};
Expand Down Expand Up @@ -93,7 +91,9 @@ pub async fn start_taker(rpc: &Client, wallet: &mut Wallet, config: TakerConfig)
}

async fn run(rpc: &Client, wallet: &mut Wallet, config: TakerConfig) -> Result<(), Error> {
let offers_addresses = sync_offerbook(&get_regtest_maker_hosts()).await;
let offers_addresses = sync_offerbook()
.await
.expect("unable to sync maker hosts from directory servers");
log::info!("<=== Got Offers");
log::debug!("Offers : {:#?}", offers_addresses);
send_coinswap(rpc, wallet, config, &offers_addresses).await?;
Expand Down

0 comments on commit 173a1c0

Please sign in to comment.