├── .gitignore ├── codecov.yaml ├── .github ├── bitcoin.conf └── workflows │ ├── lint.yaml │ ├── build.yaml │ └── coverage.yaml ├── LICENSE.md ├── Cargo.toml ├── src ├── error.rs ├── directory_servers.rs ├── watchtower_client.rs ├── offerbook_sync.rs ├── messages.rs ├── direct_send.rs ├── main.rs ├── fidelity_bonds.rs ├── funding_tx.rs ├── lib.rs └── watchtower_protocol.rs ├── LICENSE-MIT ├── tests ├── init.sh └── test_standard_coinswap.rs ├── LICENSE-APACHE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.swp 3 | -------------------------------------------------------------------------------- /codecov.yaml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 1% 7 | base: auto 8 | informational: false 9 | patch: 10 | default: 11 | target: auto 12 | threshold: 100% 13 | base: auto -------------------------------------------------------------------------------- /.github/bitcoin.conf: -------------------------------------------------------------------------------- 1 | # bitcoind configuration 2 | 3 | # remove the following line to enable Bitcoin mainnet 4 | regtest=1 5 | fallbackfee=0.0001 6 | # Bitcoind options 7 | server=1 8 | 9 | 10 | # Connection settings 11 | rpcuser=regtestrpcuser 12 | rpcpassword=regtestrpcpass 13 | # blockfilterindex=1 14 | # peerblockfilters=1 15 | 16 | [regtest] 17 | rpcbind=0.0.0.0 18 | rpcallowip=0.0.0.0/0 19 | 20 | zmqpubrawblock=tcp://127.0.0.1:28332 21 | zmqpubrawtx=tcp://127.0.0.1:28333 -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | 2 | on: [push, pull_request] 3 | 4 | name: lint 5 | 6 | jobs: 7 | 8 | fmt: 9 | name: rust fmt 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Set default toolchain 15 | run: rustup default stable 16 | - name: Set profile 17 | run: rustup set profile minimal 18 | - name: Add rustfmt 19 | run: rustup component add rustfmt 20 | - name: Update toolchain 21 | run: rustup update 22 | - name: Check fmt 23 | run: cargo fmt --all -- --check -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This software is licensed under [Apache 2.0](LICENSE-APACHE) or 2 | [MIT](LICENSE-MIT), at your option. 3 | 4 | Some files retain their own copyright notice, however, for full authorship 5 | information, see version control history. 6 | 7 | Except as otherwise noted in individual files, all files in this repository are 8 | licensed under the Apache License, Version 2.0 or the MIT license , at your option. 11 | 12 | You may not use, copy, modify, merge, publish, distribute, sublicense, and/or 13 | sell copies of this software or any files in this repository except in 14 | accordance with one or both of these licenses. 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "teleport" 3 | version = "0.1.0" 4 | authors = ["chris-belcher "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | bitcoincore-rpc = "0.13" 11 | #bitcoincore-rpc = {git="https://github.com/rust-bitcoin/rust-bitcoincore-rpc"} 12 | bitcoin-wallet = "1.1.0" 13 | bitcoin = "0.26" 14 | serde = { version = "1.0", features = ["derive"] } 15 | serde_json = "1.0" 16 | tokio = { version = "1.16.1", features = ["full"] } 17 | log = "^0.4" 18 | env_logger = "0.7" 19 | futures = "0.3" 20 | rand = "0.7.3" 21 | itertools = "0.9.0" 22 | structopt = "0.3.21" 23 | dirs = "3.0.1" 24 | tokio-socks = "0.5" 25 | reqwest = { version = "0.11", features = ["socks"] } 26 | chrono = "0.4" 27 | 28 | #Empty default feature set, (helpful to generalise in github actions) 29 | [features] 30 | default = [] 31 | 32 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error; 2 | use std::io; 3 | 4 | // error enum for the whole project 5 | // try to make functions return this 6 | #[derive(Debug)] 7 | pub enum Error { 8 | Network(Box), 9 | Disk(io::Error), 10 | Protocol(&'static str), 11 | Rpc(bitcoincore_rpc::Error), 12 | Socks(tokio_socks::Error), 13 | } 14 | 15 | impl From> for Error { 16 | fn from(e: Box) -> Error { 17 | Error::Network(e) 18 | } 19 | } 20 | 21 | impl From for Error { 22 | fn from(e: io::Error) -> Error { 23 | Error::Disk(e) 24 | } 25 | } 26 | 27 | impl From for Error { 28 | fn from(e: bitcoincore_rpc::Error) -> Error { 29 | Error::Rpc(e) 30 | } 31 | } 32 | 33 | impl From for Error { 34 | fn from(e: tokio_socks::Error) -> Error { 35 | Error::Socks(e) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: continuous_builds 4 | 5 | jobs: 6 | 7 | build: 8 | name: Build 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | rust: 13 | - stable 14 | - nightly 15 | features: 16 | - default 17 | steps: 18 | - name: checkout 19 | uses: actions/checkout@v2 20 | - name: Generate cache key 21 | run: echo "${{ matrix.rust }} ${{ matrix.features }}" | tee .cache_key 22 | - name: cache 23 | uses: actions/cache@v2 24 | with: 25 | path: | 26 | ~/.cargo/registry 27 | ~/.cargo/git 28 | target 29 | key: ${{ runner.os }}-cargo-${{ hashFiles('.cache_key') }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} 30 | - name: Set default toolchain 31 | run: rustup default ${{ matrix.rust }} 32 | - name: Set profile 33 | run: rustup set profile minimal 34 | - name: Add clippy 35 | run: rustup component add clippy 36 | - name: Update toolchain 37 | run: rustup update 38 | - name: Build 39 | run: cargo build --features ${{ matrix.features }} --no-default-features 40 | # Uncomment clippy check after failures are removed 41 | # - name: Clippy 42 | # run: cargo clippy --all-targets --features ${{ matrix.features }} --no-default-features -- -D warnings -------------------------------------------------------------------------------- /tests/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname "$0")" || exit 3 | 4 | # Helper scripts to initialize test wallets 5 | # Usage: 6 | # - Start bitcoind in your system with regtest mode 7 | # - ensure bitcoin-cli is in $PATH 8 | # - create a new wallet: `bitcoin-cli createwallet teleport` 9 | # - run this script: `$ path-to-project-repo/tests/init.sh` 10 | # 11 | # This will set up 1 taker and 2 maker wallet in tests dir. 12 | # If it works you should see the usual wallet balances 13 | # run the taker and makers as usual. 14 | 15 | coinbaseaddr=$(bitcoin-cli getnewaddress) 16 | 17 | bitcoin-cli generatetoaddress 101 "$coinbaseaddr" 18 | 19 | 20 | taker='../target/debug/teleport --wallet-file-name taker-wallet' 21 | maker1='../target/debug/teleport --wallet-file-name maker-wallet-1' 22 | maker2='../target/debug/teleport --wallet-file-name maker-wallet-2' 23 | 24 | 25 | echo -ne "\n" | $taker generate-wallet 26 | echo -ne "\n" | $maker1 generate-wallet 27 | echo -ne "\n" | $maker2 generate-wallet 28 | 29 | for number in {0..2} 30 | do 31 | takeraddr=$($taker get-receive-invoice) 32 | bitcoin-cli sendtoaddress $takeraddr 0.05 33 | 34 | maker1addr=$($maker1 get-receive-invoice) 35 | bitcoin-cli sendtoaddress $maker1addr 0.05 36 | 37 | maker2addr=$($maker2 get-receive-invoice) 38 | bitcoin-cli sendtoaddress $maker2addr 0.05 39 | done 40 | 41 | bitcoin-cli generatetoaddress 1 "$coinbaseaddr" 42 | 43 | echo 'Taker Balance: ' 44 | $taker wallet-balance 45 | 46 | 47 | echo 'Maker1 Balance: ' 48 | $maker1 wallet-balance 49 | 50 | echo 'Maker2 balance: ' 51 | $maker2 wallet-balance -------------------------------------------------------------------------------- /.github/workflows/coverage.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: test_codecov 4 | 5 | jobs: 6 | test_with_codecov: 7 | name: Run tests with coverage reporting 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | 13 | - name: Set default toolchain 14 | run: rustup default nightly 15 | - name: Set profile 16 | run: rustup set profile minimal 17 | 18 | - name: Install bitcoind 19 | run: | 20 | wget https://bitcoincore.org/bin/bitcoin-core-0.21.1/bitcoin-0.21.1-x86_64-linux-gnu.tar.gz 21 | tar -xvf bitcoin-0.21.1-x86_64-linux-gnu.tar.gz 22 | sudo cp bitcoin-0.21.1/bin/* /usr/local/bin 23 | which bitcoind 24 | 25 | # bitcoind setups are required for integration test 26 | # TODO: Separate unit and integration tests to different process. 27 | - name: Run and bitcoind 28 | run: | 29 | mkdir -p .bitcoin 30 | cp .github/bitcoin.conf .bitcoin/ 31 | bitcoind -daemon -datadir=.bitcoin 32 | 33 | - name: Sleep for 5 secs # Wait until bitcoind starts 34 | uses: juliangruber/sleep-action@v1 35 | with: 36 | time: 5s 37 | 38 | - name: Setup core wallet 39 | run: | 40 | bitcoin-cli -datadir=.bitcoin createwallet teleport 41 | addrs=$(bitcoin-cli -datadir=.bitcoin getnewaddress) 42 | bitcoin-cli -datadir=.bitcoin generatetoaddress 101 $addrs 43 | 44 | # Pin grcov to v0.8.2 because of build failure at 0.8.3 45 | - name: Install grcov 46 | run: cargo install grcov --force --version 0.8.2 47 | 48 | # Tests are run with code coverage support 49 | - name: Run cargo test 50 | env: 51 | CARGO_INCREMENTAL: '0' 52 | RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off' 53 | RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off' 54 | run: cargo test 55 | - id: coverage 56 | name: Generate coverage 57 | uses: actions-rs/grcov@v0.1.5 58 | 59 | # Upload coverage report 60 | - name: Upload coverage to Codecov 61 | uses: codecov/codecov-action@v1 62 | with: 63 | file: ${{ steps.coverage.outputs.report }} 64 | directory: ./coverage/reports/ -------------------------------------------------------------------------------- /src/directory_servers.rs: -------------------------------------------------------------------------------- 1 | //configure this with your own tor port 2 | pub const TOR_ADDR: &str = "127.0.0.1:9050"; 3 | 4 | use bitcoin::Network; 5 | 6 | use crate::offerbook_sync::MakerAddress; 7 | 8 | //for now just one of these, but later we'll need multiple for good decentralization 9 | const DIRECTORY_SERVER_ADDR: &str = 10 | "zfwo4t5yfuf6epu7rhjbmkr6kiysi6v7kibta4i55zlp4y6xirpcr7qd.onion:8080"; 11 | 12 | #[derive(Debug)] 13 | pub enum DirectoryServerError { 14 | Reqwest(reqwest::Error), 15 | Other(&'static str), 16 | } 17 | 18 | impl From for DirectoryServerError { 19 | fn from(e: reqwest::Error) -> DirectoryServerError { 20 | DirectoryServerError::Reqwest(e) 21 | } 22 | } 23 | 24 | fn network_enum_to_string(network: Network) -> &'static str { 25 | match network { 26 | Network::Bitcoin => "mainnet", 27 | Network::Testnet => "testnet", 28 | Network::Signet => "signet", 29 | Network::Regtest => panic!("dont use directory servers if using regtest"), 30 | } 31 | } 32 | 33 | pub async fn sync_maker_addresses_from_directory_servers( 34 | network: Network, 35 | ) -> Result, DirectoryServerError> { 36 | // https://github.com/seanmonstar/reqwest/blob/master/examples/tor_socks.rs 37 | let proxy = 38 | reqwest::Proxy::all(format!("socks5h://{}", TOR_ADDR)).expect("tor proxy should be there"); 39 | let client = reqwest::Client::builder() 40 | .proxy(proxy) 41 | .build() 42 | .expect("should be able to build reqwest client"); 43 | let res = client 44 | .get(format!( 45 | "http://{}/makers-{}.txt", 46 | DIRECTORY_SERVER_ADDR, 47 | network_enum_to_string(network) 48 | )) 49 | .send() 50 | .await?; 51 | if res.status().as_u16() != 200 { 52 | return Err(DirectoryServerError::Other("status code not success")); 53 | } 54 | let mut maker_addresses = Vec::::new(); 55 | for makers in res.text().await?.split("\n") { 56 | let csv_chunks = makers.split(",").collect::>(); 57 | if csv_chunks.len() < 2 { 58 | continue; 59 | } 60 | maker_addresses.push(MakerAddress::Tor { 61 | address: String::from(csv_chunks[1]), 62 | }); 63 | log::debug!(target:"directory_servers", "expiry timestamp = {} address = {}", 64 | csv_chunks[0], csv_chunks[1]); 65 | } 66 | Ok(maker_addresses) 67 | } 68 | 69 | pub async fn post_maker_address_to_directory_servers( 70 | network: Network, 71 | address: &str, 72 | ) -> Result { 73 | let proxy = 74 | reqwest::Proxy::all(format!("socks5h://{}", TOR_ADDR)).expect("tor proxy should be there"); 75 | let client = reqwest::Client::builder() 76 | .proxy(proxy) 77 | .build() 78 | .expect("should be able to build reqwest client"); 79 | let params = [ 80 | ("address", address), 81 | ("net", network_enum_to_string(network)), 82 | ]; 83 | let res = client 84 | .post(format!("http://{}/directoryserver", DIRECTORY_SERVER_ADDR)) 85 | .form(¶ms) 86 | .send() 87 | .await?; 88 | if res.status().as_u16() != 200 { 89 | return Err(DirectoryServerError::Other("status code not success")); 90 | } 91 | let body = res.text().await?; 92 | let start_bytes = body 93 | .find("") 94 | .ok_or(DirectoryServerError::Other("expiry time not parsable1"))? 95 | + 3; 96 | let end_bytes = body 97 | .find("") 98 | .ok_or(DirectoryServerError::Other("expiry time not parsable2"))?; 99 | let expiry_time_str = &body[start_bytes..end_bytes]; 100 | let expiry_time = expiry_time_str 101 | .parse::() 102 | .map_err(|_| DirectoryServerError::Other("expiry time not parsable3"))?; 103 | Ok(expiry_time) 104 | } 105 | -------------------------------------------------------------------------------- /src/watchtower_client.rs: -------------------------------------------------------------------------------- 1 | const WATCHTOWER_HOSTPORT: &str = "localhost:6103"; 2 | 3 | use std::time::Duration; 4 | 5 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; 6 | use tokio::net::TcpStream; 7 | use tokio::select; 8 | use tokio::time::sleep; 9 | 10 | use crate::error::Error; 11 | use crate::watchtower_protocol::{ 12 | ContractsInfo, MakerToWatchtowerMessage, Ping, WatchContractTxes, WatchtowerToMakerMessage, 13 | }; 14 | 15 | pub const CONNECT_ATTEMPTS: u32 = 10; 16 | pub const CONNECT_SLEEP_DELAY_SEC: u64 = 5; 17 | pub const CONNECT_ATTEMPT_TIMEOUT_SEC: u64 = 10; 18 | 19 | #[tokio::main] 20 | pub async fn test_watchtower_client(contracts_to_watch: ContractsInfo) { 21 | ping_watchtowers().await.unwrap(); 22 | register_coinswap_with_watchtowers(contracts_to_watch) 23 | .await 24 | .unwrap(); 25 | } 26 | 27 | fn parse_message(line: &str) -> Result { 28 | serde_json::from_str::(line) 29 | .map_err(|_| Error::Protocol("watchtower sent invalid message")) 30 | } 31 | 32 | pub async fn register_coinswap_with_watchtowers( 33 | contracts_to_watch: ContractsInfo, 34 | ) -> Result<(), Error> { 35 | send_message_to_watchtowers(&MakerToWatchtowerMessage::WatchContractTxes( 36 | WatchContractTxes { 37 | protocol_version_min: 0, 38 | protocol_version_max: 0, 39 | contracts_to_watch, 40 | }, 41 | )) 42 | .await?; 43 | log::info!("Successfully registered contract txes with watchtower"); 44 | Ok(()) 45 | } 46 | 47 | pub async fn ping_watchtowers() -> Result<(), Error> { 48 | log::debug!("pinging watchtowers"); 49 | send_message_to_watchtowers(&MakerToWatchtowerMessage::Ping(Ping { 50 | protocol_version_min: 0, 51 | protocol_version_max: 0, 52 | })) 53 | .await 54 | } 55 | 56 | async fn send_message_to_watchtowers_once(message: &MakerToWatchtowerMessage) -> Result<(), Error> { 57 | //TODO add support for registering with multiple watchtowers concurrently 58 | 59 | let mut socket = TcpStream::connect(WATCHTOWER_HOSTPORT).await?; 60 | 61 | let (socket_reader, mut socket_writer) = socket.split(); 62 | let mut socket_reader = BufReader::new(socket_reader); 63 | 64 | let mut message_packet = serde_json::to_vec(message).unwrap(); 65 | message_packet.push(b'\n'); 66 | socket_writer.write_all(&message_packet).await?; 67 | 68 | let mut line1 = String::new(); 69 | if socket_reader.read_line(&mut line1).await? == 0 { 70 | return Err(Error::Protocol("watchtower eof")); 71 | } 72 | let _watchtower_hello = 73 | if let WatchtowerToMakerMessage::WatchtowerHello(h) = parse_message(&line1)? { 74 | h 75 | } else { 76 | log::trace!(target: "watchtower_client", "wrong protocol message"); 77 | return Err(Error::Protocol("wrong protocol message from watchtower")); 78 | }; 79 | log::trace!(target: "watchtower_client", "watchtower hello = {:?}", _watchtower_hello); 80 | 81 | let mut line2 = String::new(); 82 | if socket_reader.read_line(&mut line2).await? == 0 { 83 | return Err(Error::Protocol("watchtower eof")); 84 | } 85 | let _success = if let WatchtowerToMakerMessage::Success(s) = parse_message(&line2)? { 86 | s 87 | } else { 88 | log::trace!(target: "watchtower_client", "wrong protocol message2"); 89 | return Err(Error::Protocol("wrong protocol message2 from watchtower")); 90 | }; 91 | 92 | Ok(()) 93 | } 94 | 95 | async fn send_message_to_watchtowers(message: &MakerToWatchtowerMessage) -> Result<(), Error> { 96 | let mut ii = 0; 97 | loop { 98 | ii += 1; 99 | select! { 100 | ret = send_message_to_watchtowers_once(message) => { 101 | match ret { 102 | Ok(_) => return Ok(()), 103 | Err(e) => { 104 | log::warn!( 105 | "Failed to send message to watchtower, reattempting... error={:?}", 106 | e 107 | ); 108 | if ii <= CONNECT_ATTEMPTS { 109 | sleep(Duration::from_secs(CONNECT_SLEEP_DELAY_SEC)).await; 110 | continue; 111 | } else { 112 | return Err(e); 113 | } 114 | } 115 | } 116 | }, 117 | _ = sleep(Duration::from_secs(CONNECT_ATTEMPT_TIMEOUT_SEC)) => { 118 | log::warn!( 119 | "Timeout for sending message to watchtower, reattempting...", 120 | ); 121 | if ii <= CONNECT_ATTEMPTS { 122 | continue; 123 | } else { 124 | return Err(Error::Protocol( 125 | "Timed out of sending message to watchtower")); 126 | } 127 | }, 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/offerbook_sync.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::time::Duration; 3 | 4 | use tokio::net::TcpStream; 5 | use tokio::select; 6 | use tokio::sync::mpsc; 7 | use tokio::time::sleep; 8 | 9 | use bitcoin::Network; 10 | 11 | use crate::directory_servers::{ 12 | sync_maker_addresses_from_directory_servers, DirectoryServerError, TOR_ADDR, 13 | }; 14 | use crate::error::Error; 15 | use crate::messages::{GiveOffer, MakerToTakerMessage, Offer, TakerToMakerMessage}; 16 | use crate::taker_protocol::{ 17 | handshake_maker, read_message, send_message, FIRST_CONNECT_ATTEMPTS, 18 | FIRST_CONNECT_ATTEMPT_TIMEOUT_SEC, FIRST_CONNECT_SLEEP_DELAY_SEC, 19 | }; 20 | 21 | #[derive(Debug, Clone)] 22 | pub enum MakerAddress { 23 | Clearnet { address: String }, 24 | Tor { address: String }, 25 | } 26 | 27 | #[derive(Debug, Clone)] 28 | pub struct OfferAndAddress { 29 | pub offer: Offer, 30 | pub address: MakerAddress, 31 | } 32 | 33 | const REGTEST_MAKER_ADDRESSES: &'static [&'static str] = &[ 34 | "localhost:6102", 35 | "localhost:16102", 36 | "localhost:26102", 37 | "localhost:36102", 38 | "localhost:46102", 39 | ]; 40 | 41 | fn get_regtest_maker_addresses() -> Vec { 42 | REGTEST_MAKER_ADDRESSES 43 | .iter() 44 | .map(|h| MakerAddress::Clearnet { 45 | address: h.to_string(), 46 | }) 47 | .collect::>() 48 | } 49 | 50 | impl MakerAddress { 51 | pub fn get_tcpstream_address(&self) -> String { 52 | match &self { 53 | MakerAddress::Clearnet { address } => address.to_string(), 54 | MakerAddress::Tor { address: _ } => String::from(TOR_ADDR), 55 | } 56 | } 57 | } 58 | 59 | impl fmt::Display for MakerAddress { 60 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 61 | match &self { 62 | MakerAddress::Clearnet { address } => write!(f, "{}", address), 63 | MakerAddress::Tor { address } => write!(f, "{}", address), 64 | } 65 | } 66 | } 67 | 68 | async fn download_maker_offer_attempt_once(addr: &MakerAddress) -> Result { 69 | log::debug!(target: "offerbook", "Connecting to {}", addr); 70 | let mut socket = TcpStream::connect(addr.get_tcpstream_address()).await?; 71 | let (mut socket_reader, mut socket_writer) = handshake_maker(&mut socket, addr).await?; 72 | 73 | send_message( 74 | &mut socket_writer, 75 | TakerToMakerMessage::GiveOffer(GiveOffer), 76 | ) 77 | .await?; 78 | 79 | let offer = if let MakerToTakerMessage::Offer(o) = read_message(&mut socket_reader).await? { 80 | o 81 | } else { 82 | return Err(Error::Protocol("expected method offer")); 83 | }; 84 | 85 | log::debug!(target: "offerbook", "Obtained offer from {}", addr); 86 | Ok(offer) 87 | } 88 | 89 | async fn download_maker_offer(address: MakerAddress) -> Option { 90 | let mut ii = 0; 91 | loop { 92 | ii += 1; 93 | select! { 94 | ret = download_maker_offer_attempt_once(&address) => { 95 | match ret { 96 | Ok(offer) => return Some(OfferAndAddress { offer, address }), 97 | Err(e) => { 98 | log::debug!(target: "offerbook", 99 | "Failed to request offer from maker {}, \ 100 | reattempting... error={:?}", 101 | address, 102 | e 103 | ); 104 | if ii <= FIRST_CONNECT_ATTEMPTS { 105 | sleep(Duration::from_secs(FIRST_CONNECT_SLEEP_DELAY_SEC)).await; 106 | continue; 107 | } else { 108 | return None; 109 | } 110 | } 111 | } 112 | }, 113 | _ = sleep(Duration::from_secs(FIRST_CONNECT_ATTEMPT_TIMEOUT_SEC)) => { 114 | log::debug!(target: "offerbook", 115 | "Timeout for request offer from maker {}, reattempting...", 116 | address 117 | ); 118 | if ii <= FIRST_CONNECT_ATTEMPTS { 119 | continue; 120 | } else { 121 | return None; 122 | } 123 | }, 124 | } 125 | } 126 | } 127 | 128 | pub async fn sync_offerbook_with_addresses( 129 | maker_addresses: Vec, 130 | ) -> Vec { 131 | let (offers_writer_m, mut offers_reader) = mpsc::channel::>(100); 132 | //unbounded_channel makes more sense here, but results in a compile 133 | //error i cant figure out 134 | 135 | let maker_addresses_len = maker_addresses.len(); 136 | for addr in maker_addresses { 137 | let offers_writer = offers_writer_m.clone(); 138 | tokio::spawn(async move { 139 | if let Err(_e) = offers_writer.send(download_maker_offer(addr).await).await { 140 | panic!("mpsc failed"); 141 | } 142 | }); 143 | } 144 | let mut result = Vec::::new(); 145 | for _ in 0..maker_addresses_len { 146 | if let Some(offer_addr) = offers_reader.recv().await.unwrap() { 147 | result.push(offer_addr); 148 | } 149 | } 150 | result 151 | } 152 | 153 | pub async fn get_advertised_maker_addresses( 154 | network: Network, 155 | ) -> Result, DirectoryServerError> { 156 | Ok(if network == Network::Regtest { 157 | get_regtest_maker_addresses() 158 | } else { 159 | sync_maker_addresses_from_directory_servers(network).await? 160 | }) 161 | } 162 | 163 | pub async fn sync_offerbook( 164 | network: Network, 165 | ) -> Result, DirectoryServerError> { 166 | Ok(sync_offerbook_with_addresses(get_advertised_maker_addresses(network).await?).await) 167 | } 168 | -------------------------------------------------------------------------------- /src/messages.rs: -------------------------------------------------------------------------------- 1 | //we make heavy use of serde_json's de/serialization for the benefits of 2 | //having the compiler check for us, extra type checking and readability 3 | 4 | //this works because of enum representations in serde 5 | //see https://serde.rs/enum-representations.html 6 | 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use bitcoin::hashes::hash160::Hash as Hash160; 10 | use bitcoin::secp256k1::{SecretKey, Signature}; 11 | use bitcoin::util::ecdsa::PublicKey; 12 | use bitcoin::{OutPoint, Script, Transaction}; 13 | 14 | pub const PREIMAGE_LEN: usize = 32; 15 | pub type Preimage = [u8; PREIMAGE_LEN]; 16 | 17 | //TODO the structs here which are actual messages should have the word Message 18 | //added to their name e.g. SignSendersContractTx 19 | //to distinguish them from structs which just collect together 20 | //data e.g. SenderContractTxInfo 21 | 22 | #[derive(Debug, Serialize, Deserialize)] 23 | pub struct TakerHello { 24 | pub protocol_version_min: u32, 25 | pub protocol_version_max: u32, 26 | } 27 | 28 | #[derive(Debug, Serialize, Deserialize)] 29 | pub struct GiveOffer; 30 | 31 | #[derive(Debug, Serialize, Deserialize)] 32 | pub struct SenderContractTxNoncesInfo { 33 | pub multisig_key_nonce: SecretKey, 34 | pub hashlock_key_nonce: SecretKey, 35 | pub timelock_pubkey: PublicKey, 36 | pub senders_contract_tx: Transaction, 37 | pub multisig_redeemscript: Script, 38 | pub funding_input_value: u64, 39 | } 40 | 41 | #[derive(Debug, Serialize, Deserialize)] 42 | pub struct SignSendersContractTx { 43 | pub txes_info: Vec, 44 | pub hashvalue: Hash160, 45 | pub locktime: u16, 46 | } 47 | 48 | #[derive(Debug, Serialize, Deserialize)] 49 | pub struct ConfirmedCoinSwapTxInfo { 50 | pub funding_tx: Transaction, 51 | pub funding_tx_merkleproof: String, 52 | pub multisig_redeemscript: Script, 53 | pub multisig_key_nonce: SecretKey, 54 | pub contract_redeemscript: Script, 55 | pub hashlock_key_nonce: SecretKey, 56 | } 57 | 58 | #[derive(Debug, Serialize, Deserialize)] 59 | pub struct NextCoinSwapTxInfo { 60 | pub next_coinswap_multisig_pubkey: PublicKey, 61 | pub next_hashlock_pubkey: PublicKey, 62 | } 63 | 64 | #[derive(Debug, Serialize, Deserialize)] 65 | pub struct ProofOfFunding { 66 | pub confirmed_funding_txes: Vec, 67 | pub next_coinswap_info: Vec, 68 | pub next_locktime: u16, 69 | pub next_fee_rate: u64, 70 | } 71 | 72 | #[derive(Debug, Serialize, Deserialize)] 73 | pub struct SendersAndReceiversContractSigs { 74 | pub receivers_sigs: Vec, 75 | pub senders_sigs: Vec, 76 | } 77 | 78 | #[derive(Debug, Serialize, Deserialize)] 79 | pub struct ReceiversContractTxInfo { 80 | pub multisig_redeemscript: Script, 81 | pub contract_tx: Transaction, 82 | } 83 | 84 | #[derive(Debug, Serialize, Deserialize)] 85 | pub struct SignReceiversContractTx { 86 | pub txes: Vec, 87 | } 88 | 89 | #[derive(Debug, Serialize, Deserialize)] 90 | pub struct HashPreimage { 91 | pub senders_multisig_redeemscripts: Vec