diff --git a/Cargo.toml b/Cargo.toml index 416f8b02..d546125f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] diff --git a/src/directory_servers.rs b/src/directory_servers.rs new file mode 100644 index 00000000..3701616a --- /dev/null +++ b/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 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, 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::::new(); + for makers in res.text().await?.split("\n") { + let csv_chunks = makers.split(",").collect::>(); + 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 { + 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(¶ms) + .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("") + .ok_or(DirectoryServerError::Other("expiry time not parsable1"))? + + 3; + let end_bytes = body + .find("") + .ok_or(DirectoryServerError::Other("expiry time not parsable2"))?; + let expiry_time_str = &body[start_bytes..end_bytes]; + let expiry_time = expiry_time_str + .parse::() + .map_err(|_| DirectoryServerError::Other("expiry time not parsable3"))?; + Ok(expiry_time) +} diff --git a/src/lib.rs b/src/lib.rs index 10dd172e..f71ad4df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; @@ -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)) diff --git a/src/maker_protocol.rs b/src/maker_protocol.rs index 58e13bac..c2aa2b82 100644 --- a/src/maker_protocol.rs +++ b/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}; @@ -22,6 +33,7 @@ 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, @@ -29,18 +41,10 @@ use crate::messages::{ 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 { @@ -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>, pub idle_connection_timeout: u64, @@ -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::(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(); @@ -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; }, }; diff --git a/src/offerbook_sync.rs b/src/offerbook_sync.rs index ad3d5e2e..3783e817 100644 --- a/src/offerbook_sync.rs +++ b/src/offerbook_sync.rs @@ -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 { @@ -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 { + REGTEST_MAKER_HOSTS + .iter() + .map(|h| MakerAddress::Clearnet { + address: h.to_string(), + }) + .collect::>() } impl MakerAddress { @@ -117,27 +126,36 @@ async fn download_maker_offer(address: MakerAddress) -> Option } } -pub async fn sync_offerbook(maker_hostnames: &Vec<&str>) -> Vec { +async fn sync_offerbook_with_hostnames(maker_addresses: Vec) -> Vec { let (offers_writer_m, mut offers_reader) = mpsc::channel::>(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::::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, DirectoryServerError> { + Ok( + sync_offerbook_with_hostnames(if NETWORK == Network::Regtest { + get_regtest_maker_addresses() + } else { + sync_maker_hosts_from_directory_servers(NETWORK).await? + }) + .await, + ) +} diff --git a/src/taker_protocol.rs b/src/taker_protocol.rs index 1658ed30..2c84f2f6 100644 --- a/src/taker_protocol.rs +++ b/src/taker_protocol.rs @@ -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, }; @@ -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?;