refactor(swap): Upgrade libp2p to 0.53.2, reliable retry mechanisms (#109)

Our libp2p version is out of date, and we need to tackle the upgrade even though it's a significant undertaking. This'll also fix some other [issues](https://github.com/UnstoppableSwap/core/issues/95).

## This PR includes the following changes:
- Breaking network protocol change: The libp2p version has been upgraded to 0.53 which includes breaking network protocol changes. ASBs and CLIs will not be able to swap if one of them is on the old version.
- ASB: Transfer proofs will be repeatedly sent until they are acknowledged by the other party. This fixes a bug where it'd seem to Bob as if the Alice never locked the Monero. Forcing the swap to be refunded.
- CLI: Encrypted signatures will be repeatedly sent until they are acknowledged by the other party
- CLI+ASB: Libp2p network errors in request-response protocols are now propagated throught the event loop channels. This allows the caller to retry if an error occurs (e.g timeout)

Closes https://github.com/UnstoppableSwap/core/issues/101, https://github.com/UnstoppableSwap/core/issues/95
This commit is contained in:
binarybaron 2024-11-11 00:14:42 +01:00 committed by GitHub
parent a116c27785
commit c027e51087
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 4197 additions and 7643 deletions

3
.gitignore vendored
View file

@ -1 +1,2 @@
target target
.vscode

View file

@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
- Breaking network protocol change: The libp2p version has been upgraded to 0.53 which includes breaking network protocol changes. ASBs and CLIs will not be able to swap if one of them is on the old version.
- ASB: Transfer proofs will be repeatedly sent until they are acknowledged by the other party. This fixes a bug where it'd seem to Bob as if the Alice never locked the Monero. Forcing the swap to be refunded.
- CLI: Encrypted signatures will be repeatedly sent until they are acknowledged by the other party
- ASB: We now retry indefinitely to lock Monero funds until the swap is cancelled. This fixes an issue where we would fail to lock Monero on the first try (e.g., due to the daemon not being fully synced) and would never try again, forcing the swap to be refunded. - ASB: We now retry indefinitely to lock Monero funds until the swap is cancelled. This fixes an issue where we would fail to lock Monero on the first try (e.g., due to the daemon not being fully synced) and would never try again, forcing the swap to be refunded.
- ASB + CLI: You can now use the `logs` command to retrieve logs stored in the past, redacting addresses and id's using `logs --redact`. - ASB + CLI: You can now use the `logs` command to retrieve logs stored in the past, redacting addresses and id's using `logs --redact`.
- ASB: The `--disable-timestamp` flag has been removed - ASB: The `--disable-timestamp` flag has been removed

4482
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,7 @@ WORKDIR /build/swap
RUN cargo build --release --bin=asb RUN cargo build --release --bin=asb
FROM debian:bullseye-slim FROM debian:bookworm-slim
WORKDIR /data WORKDIR /data

View file

@ -9,11 +9,11 @@ import HistoryPage from "./pages/history/HistoryPage";
import SwapPage from "./pages/swap/SwapPage"; import SwapPage from "./pages/swap/SwapPage";
import WalletPage from "./pages/wallet/WalletPage"; import WalletPage from "./pages/wallet/WalletPage";
import GlobalSnackbarProvider from "./snackbar/GlobalSnackbarProvider"; import GlobalSnackbarProvider from "./snackbar/GlobalSnackbarProvider";
import UpdaterDialog from "./modal/updater/UpdaterDialog";
import { initEventListeners } from "renderer/rpc";
import { useEffect } from "react"; import { useEffect } from "react";
import { fetchProvidersViaHttp, fetchAlertsViaHttp, fetchXmrPrice, fetchBtcPrice, fetchXmrBtcRate } from "renderer/api"; import { fetchProvidersViaHttp, fetchAlertsViaHttp, fetchXmrPrice, fetchBtcPrice, fetchXmrBtcRate } from "renderer/api";
import { initEventListeners } from "renderer/rpc";
import { store } from "renderer/store/storeRenderer"; import { store } from "renderer/store/storeRenderer";
import UpdaterDialog from "./modal/updater/UpdaterDialog";
import { setAlerts } from "store/features/alertsSlice"; import { setAlerts } from "store/features/alertsSlice";
import { setRegistryProviders, registryConnectionFailed } from "store/features/providersSlice"; import { setRegistryProviders, registryConnectionFailed } from "store/features/providersSlice";
import { setXmrPrice, setBtcPrice, setXmrBtcRate } from "store/features/ratesSlice"; import { setXmrPrice, setBtcPrice, setXmrBtcRate } from "store/features/ratesSlice";
@ -120,4 +120,4 @@ async function fetchInitialData() {
} catch (e) { } catch (e) {
logger.error(e, "Error retrieving XMR/BTC rate"); logger.error(e, "Error retrieving XMR/BTC rate");
} }
} }

View file

@ -16,7 +16,6 @@ import {
fetchXmrPrice, fetchXmrPrice,
} from "./api"; } from "./api";
import App from "./components/App"; import App from "./components/App";
import { initEventListeners } from "./rpc";
import { persistor, store } from "./store/storeRenderer"; import { persistor, store } from "./store/storeRenderer";
const container = document.getElementById("root"); const container = document.getElementById("root");

4346
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -15,13 +15,14 @@ tauri = [ "dep:tauri" ]
anyhow = "1" anyhow = "1"
async-compression = { version = "0.3", features = [ "bzip2", "tokio" ] } async-compression = { version = "0.3", features = [ "bzip2", "tokio" ] }
async-trait = "0.1" async-trait = "0.1"
asynchronous-codec = "0.7.0"
atty = "0.2" atty = "0.2"
backoff = { version = "0.4", features = [ "tokio" ] } backoff = { version = "0.4", features = [ "tokio" ] }
base64 = "0.22" base64 = "0.22"
bdk = "0.28" bdk = "0.28"
big-bytes = "1" big-bytes = "1"
bitcoin = { version = "0.29", features = [ "rand", "serde" ] } bitcoin = { version = "0.29", features = [ "rand", "serde" ] }
bmrng = "0.5" bmrng = "0.5.2"
comfy-table = "7.1" comfy-table = "7.1"
config = { version = "0.14", default-features = false, features = [ "toml" ] } config = { version = "0.14", default-features = false, features = [ "toml" ] }
conquer-once = "0.4" conquer-once = "0.4"
@ -43,19 +44,7 @@ hyper = "0.14.20"
itertools = "0.13" itertools = "0.13"
jsonrpsee = { version = "0.16.2", features = [ "server" ] } jsonrpsee = { version = "0.16.2", features = [ "server" ] }
jsonrpsee-core = "0.16.2" jsonrpsee-core = "0.16.2"
libp2p = { version = "0.42.2", default-features = false, features = [ libp2p = { version = "0.53.2", features = [ "tcp", "yamux", "dns", "noise", "request-response", "ping", "rendezvous", "identify", "macros", "cbor", "json", "tokio", "serde", "rsa" ] }
"tcp-tokio",
"yamux",
"mplex",
"dns-tokio",
"noise",
"request-response",
"websocket",
"ping",
"rendezvous",
"identify",
"serde",
] }
monero = { version = "0.12", features = [ "serde_support" ] } monero = { version = "0.12", features = [ "serde_support" ] }
monero-rpc = { path = "../monero-rpc" } monero-rpc = { path = "../monero-rpc" }
once_cell = "1.19" once_cell = "1.19"
@ -128,6 +117,7 @@ tracing-subscriber = { version = "0.3", default-features = false, features = [
"json", "json",
] } ] }
typeshare = "1.0.3" typeshare = "1.0.3"
unsigned-varint = { version = "0.8.0", features = [ "codec", "asynchronous_codec" ] }
url = { version = "2", features = [ "serde" ] } url = { version = "2", features = [ "serde" ] }
uuid = { version = "1.9", features = [ "serde", "v4" ] } uuid = { version = "1.9", features = [ "serde", "v4" ] }
void = "1" void = "1"

View file

@ -23,7 +23,6 @@ pub struct Defaults {
pub config_path: PathBuf, pub config_path: PathBuf,
data_dir: PathBuf, data_dir: PathBuf,
listen_address_tcp: Multiaddr, listen_address_tcp: Multiaddr,
listen_address_ws: Multiaddr,
electrum_rpc_url: Url, electrum_rpc_url: Url,
monero_wallet_rpc_url: Url, monero_wallet_rpc_url: Url,
price_ticker_ws_url: Url, price_ticker_ws_url: Url,
@ -38,7 +37,6 @@ impl GetDefaults for Testnet {
.join("config.toml"), .join("config.toml"),
data_dir: default_asb_data_dir()?.join("testnet"), data_dir: default_asb_data_dir()?.join("testnet"),
listen_address_tcp: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9939")?, listen_address_tcp: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9939")?,
listen_address_ws: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9940/ws")?,
electrum_rpc_url: Url::parse("ssl://electrum.blockstream.info:60002")?, electrum_rpc_url: Url::parse("ssl://electrum.blockstream.info:60002")?,
monero_wallet_rpc_url: Url::parse("http://127.0.0.1:38083/json_rpc")?, monero_wallet_rpc_url: Url::parse("http://127.0.0.1:38083/json_rpc")?,
price_ticker_ws_url: Url::parse("wss://ws.kraken.com")?, price_ticker_ws_url: Url::parse("wss://ws.kraken.com")?,
@ -57,7 +55,6 @@ impl GetDefaults for Mainnet {
.join("config.toml"), .join("config.toml"),
data_dir: default_asb_data_dir()?.join("mainnet"), data_dir: default_asb_data_dir()?.join("mainnet"),
listen_address_tcp: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9939")?, listen_address_tcp: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9939")?,
listen_address_ws: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9940/ws")?,
electrum_rpc_url: Url::parse("ssl://blockstream.info:700")?, electrum_rpc_url: Url::parse("ssl://blockstream.info:700")?,
monero_wallet_rpc_url: Url::parse("http://127.0.0.1:18083/json_rpc")?, monero_wallet_rpc_url: Url::parse("http://127.0.0.1:18083/json_rpc")?,
price_ticker_ws_url: Url::parse("wss://ws.kraken.com")?, price_ticker_ws_url: Url::parse("wss://ws.kraken.com")?,
@ -299,7 +296,7 @@ pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
let listen_addresses = Input::with_theme(&ColorfulTheme::default()) let listen_addresses = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter multiaddresses (comma separated) on which asb should list for peer-to-peer communications or hit return to use default") .with_prompt("Enter multiaddresses (comma separated) on which asb should list for peer-to-peer communications or hit return to use default")
.default( format!("{},{}", defaults.listen_address_tcp, defaults.listen_address_ws)) .default( format!("{}", defaults.listen_address_tcp))
.interact_text()?; .interact_text()?;
let listen_addresses = listen_addresses let listen_addresses = listen_addresses
.split(',') .split(',')
@ -429,7 +426,7 @@ mod tests {
network: bitcoin::Network::Testnet, network: bitcoin::Network::Testnet,
}, },
network: Network { network: Network {
listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws], listen: vec![defaults.listen_address_tcp],
rendezvous_point: vec![], rendezvous_point: vec![],
external_addresses: vec![], external_addresses: vec![],
}, },
@ -473,7 +470,7 @@ mod tests {
network: bitcoin::Network::Bitcoin, network: bitcoin::Network::Bitcoin,
}, },
network: Network { network: Network {
listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws], listen: vec![defaults.listen_address_tcp],
rendezvous_point: vec![], rendezvous_point: vec![],
external_addresses: vec![], external_addresses: vec![],
}, },

View file

@ -5,14 +5,15 @@ use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled,
use crate::network::quote::BidQuote; use crate::network::quote::BidQuote;
use crate::network::swap_setup::alice::WalletSnapshot; use crate::network::swap_setup::alice::WalletSnapshot;
use crate::network::transfer_proof; use crate::network::transfer_proof;
use crate::protocol::alice::swap::has_already_processed_enc_sig;
use crate::protocol::alice::{AliceState, State3, Swap}; use crate::protocol::alice::{AliceState, State3, Swap};
use crate::protocol::{Database, State}; use crate::protocol::{Database, State};
use crate::{bitcoin, env, kraken, monero}; use crate::{bitcoin, env, kraken, monero};
use anyhow::{Context, Result}; use anyhow::{anyhow, Context, Result};
use futures::future; use futures::future;
use futures::future::{BoxFuture, FutureExt}; use futures::future::{BoxFuture, FutureExt};
use futures::stream::{FuturesUnordered, StreamExt}; use futures::stream::{FuturesUnordered, StreamExt};
use libp2p::request_response::{RequestId, ResponseChannel}; use libp2p::request_response::{OutboundFailure, OutboundRequestId, ResponseChannel};
use libp2p::swarm::SwarmEvent; use libp2p::swarm::SwarmEvent;
use libp2p::{PeerId, Swarm}; use libp2p::{PeerId, Swarm};
use rust_decimal::Decimal; use rust_decimal::Decimal;
@ -20,19 +21,10 @@ use std::collections::HashMap;
use std::convert::{Infallible, TryInto}; use std::convert::{Infallible, TryInto};
use std::fmt::Debug; use std::fmt::Debug;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc; use std::time::Duration;
use tokio::sync::{mpsc, oneshot};
use uuid::Uuid; use uuid::Uuid;
/// A future that resolves to a tuple of `PeerId`, `transfer_proof::Request` and
/// `Responder`.
///
/// When this future resolves, the `transfer_proof::Request` shall be sent to
/// the peer identified by the `PeerId`. Once the request has been acknowledged
/// by the peer, i.e. a `()` response has been received, the `Responder` shall
/// be used to let the original sender know about the successful transfer.
type OutgoingTransferProof =
BoxFuture<'static, Result<(PeerId, transfer_proof::Request, bmrng::Responder<()>)>>;
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct EventLoop<LR> pub struct EventLoop<LR>
where where
@ -50,19 +42,70 @@ where
swap_sender: mpsc::Sender<Swap>, swap_sender: mpsc::Sender<Swap>,
/// Stores incoming [`EncryptedSignature`]s per swap. /// Stores where to send [`EncryptedSignature`]s to
/// The corresponding receiver for this channel is stored in the EventLoopHandle
/// that is responsible for the swap.
///
/// Once a [`EncryptedSignature`] has been sent to the EventLoopHandle,
/// the sender is removed from this map.
recv_encrypted_signature: HashMap<Uuid, bmrng::RequestSender<bitcoin::EncryptedSignature, ()>>, recv_encrypted_signature: HashMap<Uuid, bmrng::RequestSender<bitcoin::EncryptedSignature, ()>>,
/// Once we receive an [`EncryptedSignature`] from Bob, we forward it to the EventLoopHandle.
/// Once the EventLoopHandle acknowledges the receipt of the [`EncryptedSignature`], we need to confirm this to Bob.
/// When the EventLoopHandle acknowledges the receipt, a future in this collection resolves and returns the libp2p channel
/// which we use to confirm to Bob that we have received the [`EncryptedSignature`].
///
/// Flow:
/// 1. When signature forwarded via recv_encrypted_signature sender
/// 2. New future pushed here to await EventLoopHandle's acknowledgement
/// 3. When future completes, the EventLoop uses the ResponseChannel to send an acknowledgment to Bob
/// 4. Future is removed from this collection
inflight_encrypted_signatures: FuturesUnordered<BoxFuture<'static, ResponseChannel<()>>>, inflight_encrypted_signatures: FuturesUnordered<BoxFuture<'static, ResponseChannel<()>>>,
send_transfer_proof: FuturesUnordered<OutgoingTransferProof>, /// Channel for sending transfer proofs to Bobs. The sender is shared with every EventLoopHandle.
/// The receiver is polled by the event loop to send transfer proofs over the network to Bob.
///
/// Flow:
/// 1. EventLoopHandle sends (PeerId, Request, Responder) through sender
/// 2. Event loop receives and attempts to send to peer
/// 3. Result (Ok or network failure) is sent back to EventLoopHandle
#[allow(clippy::type_complexity)]
outgoing_transfer_proofs_requests: tokio::sync::mpsc::UnboundedReceiver<(
PeerId,
transfer_proof::Request,
oneshot::Sender<Result<(), OutboundFailure>>,
)>,
#[allow(clippy::type_complexity)]
outgoing_transfer_proofs_sender: tokio::sync::mpsc::UnboundedSender<(
PeerId,
transfer_proof::Request,
oneshot::Sender<Result<(), OutboundFailure>>,
)>,
/// Tracks [`transfer_proof::Request`]s which could not yet be sent because /// Temporarily stores transfer proof requests for peers that are currently disconnected.
/// we are currently disconnected from the peer. ///
buffered_transfer_proofs: HashMap<PeerId, Vec<(transfer_proof::Request, bmrng::Responder<()>)>>, /// When a transfer proof cannot be sent because there's no connection to the peer:
/// 1. It is moved from [`outgoing_transfer_proofs_requests`] to this buffer
/// 2. Once a connection is established with the peer, the proof is send back into the [`outgoing_transfer_proofs_sender`]
/// 3. The buffered request is then removed from this collection
#[allow(clippy::type_complexity)]
buffered_transfer_proofs: HashMap<
PeerId,
Vec<(
transfer_proof::Request,
oneshot::Sender<Result<(), OutboundFailure>>,
)>,
>,
/// Tracks [`transfer_proof::Request`]s which are currently inflight and /// Tracks [`transfer_proof::Request`]s which are currently inflight and awaiting an acknowledgement from Bob
/// awaiting an acknowledgement. ///
inflight_transfer_proofs: HashMap<RequestId, bmrng::Responder<()>>, /// When a transfer proof is sent to Bob:
/// 1. A unique request ID is generated by libp2p
/// 2. The response channel is stored in this map with the request ID as key
/// 3. When Bob acknowledges the proof, we use the stored channel to notify the EventLoopHandle
/// 4. The entry is then removed from this map
inflight_transfer_proofs:
HashMap<OutboundRequestId, oneshot::Sender<Result<(), OutboundFailure>>>,
} }
impl<LR> EventLoop<LR> impl<LR> EventLoop<LR>
@ -82,6 +125,8 @@ where
external_redeem_address: Option<bitcoin::Address>, external_redeem_address: Option<bitcoin::Address>,
) -> Result<(Self, mpsc::Receiver<Swap>)> { ) -> Result<(Self, mpsc::Receiver<Swap>)> {
let swap_channel = MpscChannels::default(); let swap_channel = MpscChannels::default();
let (outgoing_transfer_proofs_sender, outgoing_transfer_proofs_requests) =
tokio::sync::mpsc::unbounded_channel();
let event_loop = EventLoop { let event_loop = EventLoop {
swarm, swarm,
@ -96,7 +141,8 @@ where
external_redeem_address, external_redeem_address,
recv_encrypted_signature: Default::default(), recv_encrypted_signature: Default::default(),
inflight_encrypted_signatures: Default::default(), inflight_encrypted_signatures: Default::default(),
send_transfer_proof: Default::default(), outgoing_transfer_proofs_requests,
outgoing_transfer_proofs_sender,
buffered_transfer_proofs: Default::default(), buffered_transfer_proofs: Default::default(),
inflight_transfer_proofs: Default::default(), inflight_transfer_proofs: Default::default(),
}; };
@ -110,7 +156,6 @@ where
pub async fn run(mut self) { pub async fn run(mut self) {
// ensure that these streams are NEVER empty, otherwise it will // ensure that these streams are NEVER empty, otherwise it will
// terminate forever. // terminate forever.
self.send_transfer_proof.push(future::pending().boxed());
self.inflight_encrypted_signatures self.inflight_encrypted_signatures
.push(future::pending().boxed()); .push(future::pending().boxed());
@ -201,8 +246,9 @@ where
} }
SwarmEvent::Behaviour(OutEvent::TransferProofAcknowledged { peer, id }) => { SwarmEvent::Behaviour(OutEvent::TransferProofAcknowledged { peer, id }) => {
tracing::debug!(%peer, "Bob acknowledged transfer proof"); tracing::debug!(%peer, "Bob acknowledged transfer proof");
if let Some(responder) = self.inflight_transfer_proofs.remove(&id) { if let Some(responder) = self.inflight_transfer_proofs.remove(&id) {
let _ = responder.respond(()); let _ = responder.send(Ok(()));
} }
} }
SwarmEvent::Behaviour(OutEvent::EncryptedSignatureReceived{ msg, channel, peer }) => { SwarmEvent::Behaviour(OutEvent::EncryptedSignatureReceived{ msg, channel, peer }) => {
@ -231,10 +277,33 @@ where
continue; continue;
} }
// Immediately acknowledge if we've already processed this encrypted signature
// This handles the case where Bob didn't receive our previous acknowledgment
// and is retrying sending the encrypted signature
if let Ok(state) = self.db.get_state(swap_id).await {
let state: AliceState = state.try_into()
.expect("Alices database only contains Alice states");
// Check if we have already processed the encrypted signature
if has_already_processed_enc_sig(&state) {
tracing::warn!(%swap_id, "Received encrypted signature for swap in state {}. We have already processed this encrypted signature. Acknowledging immediately.", state);
// We push create a future that will resolve immediately, and returns the channel
// This will be resolved in the next iteration of the event loop, and the acknowledgment will be sent to Bob
self.inflight_encrypted_signatures.push(async move {
channel
}.boxed());
continue;
}
}
let sender = match self.recv_encrypted_signature.remove(&swap_id) { let sender = match self.recv_encrypted_signature.remove(&swap_id) {
Some(sender) => sender, Some(sender) => sender,
None => { None => {
// TODO: Don't just drop encsig if we currently don't have a running swap for it, save in db // TODO: Don't just drop encsig if we currently don't have a running swap for it, save in db
// 1. Save the encrypted signature in the database
// 2. Acknowledge the receipt of the encrypted signature
tracing::warn!(%swap_id, "No sender for encrypted signature, maybe already handled?"); tracing::warn!(%swap_id, "No sender for encrypted signature, maybe already handled?");
continue; continue;
} }
@ -310,8 +379,28 @@ where
SwarmEvent::Behaviour(OutEvent::Rendezvous(libp2p::rendezvous::client::Event::Registered { rendezvous_node, ttl, namespace })) => { SwarmEvent::Behaviour(OutEvent::Rendezvous(libp2p::rendezvous::client::Event::Registered { rendezvous_node, ttl, namespace })) => {
tracing::info!("Successfully registered with rendezvous node: {} with namespace: {} and TTL: {:?}", rendezvous_node, namespace, ttl); tracing::info!("Successfully registered with rendezvous node: {} with namespace: {} and TTL: {:?}", rendezvous_node, namespace, ttl);
} }
SwarmEvent::Behaviour(OutEvent::Rendezvous(libp2p::rendezvous::client::Event::RegisterFailed(error))) => { SwarmEvent::Behaviour(OutEvent::Rendezvous(libp2p::rendezvous::client::Event::RegisterFailed { rendezvous_node, namespace, error })) => {
tracing::error!("Registration with rendezvous node failed: {:?}", error); tracing::error!("Registration with rendezvous node {} failed for namespace {}: {:?}", rendezvous_node, namespace, error);
}
SwarmEvent::Behaviour(OutEvent::OutboundRequestResponseFailure {peer, error, request_id, protocol}) => {
tracing::error!(
%peer,
%request_id,
%error,
%protocol,
"Failed to send request-response request to peer");
if let Some(responder) = self.inflight_transfer_proofs.remove(&request_id) {
let _ = responder.send(Err(error));
}
}
SwarmEvent::Behaviour(OutEvent::InboundRequestResponseFailure {peer, error, request_id, protocol}) => {
tracing::error!(
%peer,
%request_id,
%error,
%protocol,
"Failed to receive request-response request from peer");
} }
SwarmEvent::Behaviour(OutEvent::Failure {peer, error}) => { SwarmEvent::Behaviour(OutEvent::Failure {peer, error}) => {
tracing::error!( tracing::error!(
@ -321,23 +410,27 @@ where
SwarmEvent::ConnectionEstablished { peer_id: peer, endpoint, .. } => { SwarmEvent::ConnectionEstablished { peer_id: peer, endpoint, .. } => {
tracing::debug!(%peer, address = %endpoint.get_remote_address(), "New connection established"); tracing::debug!(%peer, address = %endpoint.get_remote_address(), "New connection established");
// If we have buffered transfer proofs for this peer, we can now send them
if let Some(transfer_proofs) = self.buffered_transfer_proofs.remove(&peer) { if let Some(transfer_proofs) = self.buffered_transfer_proofs.remove(&peer) {
for (transfer_proof, responder) in transfer_proofs { for (transfer_proof, responder) in transfer_proofs {
tracing::debug!(%peer, "Found buffered transfer proof for peer"); tracing::debug!(%peer, "Found buffered transfer proof for peer");
let id = self.swarm.behaviour_mut().transfer_proof.send_request(&peer, transfer_proof); // We have an established connection to the peer, so we can add the transfer proof to the queue
self.inflight_transfer_proofs.insert(id, responder); // This is then polled in the next iteration of the event loop, and attempted to be sent to the peer
if let Err(e) = self.outgoing_transfer_proofs_sender.send((peer, transfer_proof, responder)) {
tracing::error!(%peer, error = %e, "Failed to forward buffered transfer proof to event loop channel");
}
} }
} }
} }
SwarmEvent::IncomingConnectionError { send_back_addr: address, error, .. } => { SwarmEvent::IncomingConnectionError { send_back_addr: address, error, .. } => {
tracing::warn!(%address, "Failed to set up connection with peer: {:#}", error); tracing::warn!(%address, "Failed to set up connection with peer: {:#}", error);
} }
SwarmEvent::ConnectionClosed { peer_id: peer, num_established: 0, endpoint, cause: Some(error) } => { SwarmEvent::ConnectionClosed { peer_id: peer, num_established: 0, endpoint, cause: Some(error), connection_id } => {
tracing::debug!(%peer, address = %endpoint.get_remote_address(), "Lost connection to peer: {:#}", error); tracing::debug!(%peer, address = %endpoint.get_remote_address(), %connection_id, "Lost connection to peer: {:#}", error);
} }
SwarmEvent::ConnectionClosed { peer_id: peer, num_established: 0, endpoint, cause: None } => { SwarmEvent::ConnectionClosed { peer_id: peer, num_established: 0, endpoint, cause: None, connection_id } => {
tracing::info!(%peer, address = %endpoint.get_remote_address(), "Successfully closed connection"); tracing::info!(%peer, address = %endpoint.get_remote_address(), %connection_id, "Successfully closed connection");
} }
SwarmEvent::NewListenAddr{address, ..} => { SwarmEvent::NewListenAddr{address, ..} => {
tracing::info!(%address, "New listen address reported"); tracing::info!(%address, "New listen address reported");
@ -345,26 +438,18 @@ where
_ => {} _ => {}
} }
}, },
next_transfer_proof = self.send_transfer_proof.next() => { Some((peer, transfer_proof, responder)) = self.outgoing_transfer_proofs_requests.recv() => {
match next_transfer_proof { // If we are not connected to the peer, we buffer the transfer proof
Some(Ok((peer, transfer_proof, responder))) => { if !self.swarm.behaviour_mut().transfer_proof.is_connected(&peer) {
if !self.swarm.behaviour_mut().transfer_proof.is_connected(&peer) { tracing::warn!(%peer, "No active connection to peer, buffering transfer proof");
tracing::warn!(%peer, "No active connection to peer, buffering transfer proof"); self.buffered_transfer_proofs.entry(peer).or_default().push((transfer_proof, responder));
self.buffered_transfer_proofs.entry(peer).or_default().push((transfer_proof, responder)); continue;
continue;
}
let id = self.swarm.behaviour_mut().transfer_proof.send_request(&peer, transfer_proof);
self.inflight_transfer_proofs.insert(id, responder);
},
Some(Err(error)) => {
tracing::debug!("A swap stopped without sending a transfer proof: {:#}", error);
}
None => {
unreachable!("stream of transfer proof receivers must never terminate")
}
} }
}
// If we are connected to the peer, we attempt to send the transfer proof
let id = self.swarm.behaviour_mut().transfer_proof.send_request(&peer, transfer_proof);
self.inflight_transfer_proofs.insert(id, responder);
},
Some(response_channel) = self.inflight_encrypted_signatures.next() => { Some(response_channel) = self.inflight_encrypted_signatures.next() => {
let _ = self.swarm.behaviour_mut().encrypted_signature.send_response(response_channel, ()); let _ = self.swarm.behaviour_mut().encrypted_signature.send_response(response_channel, ());
} }
@ -387,13 +472,20 @@ where
let balance = self.monero_wallet.get_balance().await?; let balance = self.monero_wallet.get_balance().await?;
// use unlocked monero balance for quote // use unlocked monero balance for quote
let xmr = Amount::from_piconero(balance.unlocked_balance); let xmr_balance = Amount::from_piconero(balance.unlocked_balance);
let max_bitcoin_for_monero = xmr.max_bitcoin_for_price(ask_price).ok_or_else(|| { let max_bitcoin_for_monero =
anyhow::anyhow!("Bitcoin price ({}) x Monero ({}) overflow", ask_price, xmr) xmr_balance
})?; .max_bitcoin_for_price(ask_price)
.ok_or_else(|| {
anyhow!(
"Bitcoin price ({}) x Monero ({}) overflow",
ask_price,
xmr_balance
)
})?;
tracing::debug!(%ask_price, %xmr, %max_bitcoin_for_monero); tracing::debug!(%ask_price, %xmr_balance, %max_bitcoin_for_monero, "Computed quote");
if min_buy > max_bitcoin_for_monero { if min_buy > max_bitcoin_for_monero {
tracing::warn!( tracing::warn!(
@ -467,32 +559,23 @@ where
/// Create a new [`EventLoopHandle`] that is scoped for communication with /// Create a new [`EventLoopHandle`] that is scoped for communication with
/// the given peer. /// the given peer.
fn new_handle(&mut self, peer: PeerId, swap_id: Uuid) -> EventLoopHandle { fn new_handle(&mut self, peer: PeerId, swap_id: Uuid) -> EventLoopHandle {
// we deliberately don't put timeouts on these channels because the swap always // Create a new channel for receiving encrypted signatures from Bob
// races these futures against a timelock // The channel has a capacity of 1 since we only expect one signature per swap
let (encrypted_signature_sender, encrypted_signature_receiver) = bmrng::channel(1);
let (transfer_proof_sender, mut transfer_proof_receiver) = bmrng::channel(1);
let encrypted_signature = bmrng::channel(1);
// The sender is stored in the EventLoop
// The receiver is stored in the EventLoopHandle
// When a signature is received, the EventLoop uses the sender to notify the EventLoopHandle
self.recv_encrypted_signature self.recv_encrypted_signature
.insert(swap_id, encrypted_signature.0); .insert(swap_id, encrypted_signature_sender);
self.send_transfer_proof.push( let transfer_proof_sender = self.outgoing_transfer_proofs_sender.clone();
async move {
let (transfer_proof, responder) = transfer_proof_receiver.recv().await?;
let request = transfer_proof::Request {
swap_id,
tx_lock_proof: transfer_proof,
};
Ok((peer, request, responder))
}
.boxed(),
);
EventLoopHandle { EventLoopHandle {
recv_encrypted_signature: Some(encrypted_signature.1), swap_id,
send_transfer_proof: Some(transfer_proof_sender), peer,
recv_encrypted_signature: Some(encrypted_signature_receiver),
transfer_proof_sender: Some(transfer_proof_sender),
} }
} }
} }
@ -561,33 +644,98 @@ impl LatestRate for KrakenRate {
#[derive(Debug)] #[derive(Debug)]
pub struct EventLoopHandle { pub struct EventLoopHandle {
swap_id: Uuid,
peer: PeerId,
recv_encrypted_signature: Option<bmrng::RequestReceiver<bitcoin::EncryptedSignature, ()>>, recv_encrypted_signature: Option<bmrng::RequestReceiver<bitcoin::EncryptedSignature, ()>>,
send_transfer_proof: Option<bmrng::RequestSender<monero::TransferProof, ()>>, #[allow(clippy::type_complexity)]
transfer_proof_sender: Option<
tokio::sync::mpsc::UnboundedSender<(
PeerId,
transfer_proof::Request,
oneshot::Sender<Result<(), OutboundFailure>>,
)>,
>,
} }
impl EventLoopHandle { impl EventLoopHandle {
pub async fn recv_encrypted_signature(&mut self) -> Result<bitcoin::EncryptedSignature> { fn build_transfer_proof_request(
let (tx_redeem_encsig, responder) = self &self,
.recv_encrypted_signature transfer_proof: monero::TransferProof,
.take() ) -> transfer_proof::Request {
.context("Encrypted signature was already received")? transfer_proof::Request {
.recv() swap_id: self.swap_id,
.await?; tx_lock_proof: transfer_proof,
}
}
/// Wait for an encrypted signature from Bob
pub async fn recv_encrypted_signature(&mut self) -> Result<bitcoin::EncryptedSignature> {
let receiver = self
.recv_encrypted_signature
.as_mut()
.context("Encrypted signature was already received")?;
let (tx_redeem_encsig, responder) = receiver.recv().await?;
// Acknowledge receipt of the encrypted signature
// This notifies the EventLoop that the signature has been processed
// The EventLoop can then send an acknowledgement back to Bob over the network
responder responder
.respond(()) .respond(())
.context("Failed to acknowledge receipt of encrypted signature")?; .context("Failed to acknowledge receipt of encrypted signature")?;
// Only take after successful receipt and acknowledgement
self.recv_encrypted_signature.take();
Ok(tx_redeem_encsig) Ok(tx_redeem_encsig)
} }
/// Send a transfer proof to Bob
///
/// This function will retry indefinitely until the transfer proof is sent successfully
/// and acknowledged by Bob
///
/// This will fail if
/// 1. the transfer proof has already been sent once
/// 2. there is an error with the bmrng channel
pub async fn send_transfer_proof(&mut self, msg: monero::TransferProof) -> Result<()> { pub async fn send_transfer_proof(&mut self, msg: monero::TransferProof) -> Result<()> {
self.send_transfer_proof let sender = self
.take() .transfer_proof_sender
.context("Transfer proof was already sent")? .as_ref()
.send_receive(msg) .context("Transfer proof was already sent")?;
.await
.context("Failed to send transfer proof")?; // We will retry indefinitely until we succeed
let backoff = backoff::ExponentialBackoffBuilder::new()
.with_max_elapsed_time(None)
.with_max_interval(Duration::from_secs(60))
.build();
let transfer_proof = self.build_transfer_proof_request(msg);
backoff::future::retry(backoff, || async {
// Create a oneshot channel to receive the acknowledgment of the transfer proof
let (singular_sender, singular_receiver) = oneshot::channel();
if let Err(err) = sender.send((self.peer, transfer_proof.clone(), singular_sender)) {
let err = anyhow!(err).context("Failed to communicate transfer proof through event loop channel");
tracing::error!(%err, swap_id = %self.swap_id, "Failed to send transfer proof");
return Err(backoff::Error::permanent(err));
}
match singular_receiver.await {
Ok(Ok(())) => Ok(()),
Ok(Err(err)) => {
tracing::warn!(%err, "Failed to send transfer proof due to a network error. We will retry");
Err(backoff::Error::transient(anyhow!(err)))
}
Err(_) => {
Err(backoff::Error::permanent(anyhow!("The sender channel should never be closed without sending a response")))
}
}
})
.await?;
self.transfer_proof_sender.take();
Ok(()) Ok(())
} }

View file

@ -11,42 +11,36 @@ use crate::network::{
use crate::protocol::alice::State3; use crate::protocol::alice::State3;
use anyhow::{anyhow, Error, Result}; use anyhow::{anyhow, Error, Result};
use futures::FutureExt; use futures::FutureExt;
use libp2p::core::connection::ConnectionId;
use libp2p::core::muxing::StreamMuxerBox; use libp2p::core::muxing::StreamMuxerBox;
use libp2p::core::transport::Boxed; use libp2p::core::transport::Boxed;
use libp2p::dns::TokioDnsConfig; use libp2p::request_response::ResponseChannel;
use libp2p::identify::{Identify, IdentifyConfig, IdentifyEvent};
use libp2p::ping::{Ping, PingConfig, PingEvent};
use libp2p::request_response::{RequestId, ResponseChannel};
use libp2p::swarm::dial_opts::PeerCondition; use libp2p::swarm::dial_opts::PeerCondition;
use libp2p::swarm::{ use libp2p::swarm::NetworkBehaviour;
IntoProtocolsHandler, NetworkBehaviour, NetworkBehaviourAction, PollParameters, use libp2p::{Multiaddr, PeerId};
ProtocolsHandler,
};
use libp2p::tcp::TokioTcpConfig;
use libp2p::websocket::WsConfig;
use libp2p::{identity, Multiaddr, NetworkBehaviour, PeerId, Transport};
use std::task::Poll; use std::task::Poll;
use std::time::Duration; use std::time::Duration;
use uuid::Uuid; use uuid::Uuid;
pub mod transport { pub mod transport {
use libp2p::{dns, identity, tcp, Transport};
use super::*; use super::*;
/// Creates the libp2p transport for the ASB. /// Creates the libp2p transport for the ASB.
pub fn new(identity: &identity::Keypair) -> Result<Boxed<(PeerId, StreamMuxerBox)>> { pub fn new(identity: &identity::Keypair) -> Result<Boxed<(PeerId, StreamMuxerBox)>> {
let tcp = TokioTcpConfig::new().nodelay(true); let tcp = tcp::tokio::Transport::new(tcp::Config::new().nodelay(true));
let tcp_with_dns = TokioDnsConfig::system(tcp)?; let tcp_with_dns = dns::tokio::Transport::system(tcp)?;
let websocket_with_dns = WsConfig::new(tcp_with_dns.clone());
let transport = tcp_with_dns.or_transport(websocket_with_dns).boxed(); authenticate_and_multiplex(tcp_with_dns.boxed(), identity)
authenticate_and_multiplex(transport, identity)
} }
} }
pub mod behaviour { pub mod behaviour {
use libp2p::swarm::behaviour::toggle::Toggle; use libp2p::{
identify, identity, ping,
request_response::{InboundFailure, InboundRequestId, OutboundFailure, OutboundRequestId},
swarm::behaviour::toggle::Toggle,
};
use super::{rendezvous::RendezvousNode, *}; use super::{rendezvous::RendezvousNode, *};
@ -71,7 +65,7 @@ pub mod behaviour {
}, },
TransferProofAcknowledged { TransferProofAcknowledged {
peer: PeerId, peer: PeerId,
id: RequestId, id: OutboundRequestId,
}, },
EncryptedSignatureReceived { EncryptedSignatureReceived {
msg: encrypted_signature::Request, msg: encrypted_signature::Request,
@ -84,6 +78,18 @@ pub mod behaviour {
peer: PeerId, peer: PeerId,
}, },
Rendezvous(libp2p::rendezvous::client::Event), Rendezvous(libp2p::rendezvous::client::Event),
OutboundRequestResponseFailure {
peer: PeerId,
error: OutboundFailure,
request_id: OutboundRequestId,
protocol: String,
},
InboundRequestResponseFailure {
peer: PeerId,
error: InboundFailure,
request_id: InboundRequestId,
protocol: String,
},
Failure { Failure {
peer: PeerId, peer: PeerId,
error: Error, error: Error,
@ -108,7 +114,6 @@ pub mod behaviour {
} }
} }
} }
/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Alice. /// A `NetworkBehaviour` that represents an XMR/BTC swap node as Alice.
#[derive(NetworkBehaviour)] #[derive(NetworkBehaviour)]
#[behaviour(out_event = "OutEvent", event_process = false)] #[behaviour(out_event = "OutEvent", event_process = false)]
@ -123,12 +128,12 @@ pub mod behaviour {
pub transfer_proof: transfer_proof::Behaviour, pub transfer_proof: transfer_proof::Behaviour,
pub cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::Behaviour, pub cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::Behaviour,
pub encrypted_signature: encrypted_signature::Behaviour, pub encrypted_signature: encrypted_signature::Behaviour,
pub identify: Identify, pub identify: identify::Behaviour,
/// Ping behaviour that ensures that the underlying network connection /// Ping behaviour that ensures that the underlying network connection
/// is still alive. If the ping fails a connection close event /// is still alive. If the ping fails a connection close event
/// will be emitted that is picked up as swarm event. /// will be emitted that is picked up as swarm event.
ping: Ping, ping: ping::Behaviour,
} }
impl<LR> Behaviour<LR> impl<LR> Behaviour<LR>
@ -147,9 +152,12 @@ pub mod behaviour {
let (identity, namespace) = identify_params; let (identity, namespace) = identify_params;
let agent_version = format!("asb/{} ({})", env!("CARGO_PKG_VERSION"), namespace); let agent_version = format!("asb/{} ({})", env!("CARGO_PKG_VERSION"), namespace);
let protocol_version = "/comit/xmr/btc/1.0.0".to_string(); let protocol_version = "/comit/xmr/btc/1.0.0".to_string();
let identifyConfig = IdentifyConfig::new(protocol_version, identity.public())
let identifyConfig = identify::Config::new(protocol_version, identity.public())
.with_agent_version(agent_version); .with_agent_version(agent_version);
let pingConfig = ping::Config::new().with_timeout(Duration::from_secs(60));
let behaviour = if rendezvous_nodes.is_empty() { let behaviour = if rendezvous_nodes.is_empty() {
None None
} else { } else {
@ -169,20 +177,20 @@ pub mod behaviour {
transfer_proof: transfer_proof::alice(), transfer_proof: transfer_proof::alice(),
encrypted_signature: encrypted_signature::alice(), encrypted_signature: encrypted_signature::alice(),
cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::alice(), cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::alice(),
ping: Ping::new(PingConfig::new().with_keep_alive(true)), ping: ping::Behaviour::new(pingConfig),
identify: Identify::new(identifyConfig), identify: identify::Behaviour::new(identifyConfig),
} }
} }
} }
impl From<PingEvent> for OutEvent { impl From<ping::Event> for OutEvent {
fn from(_: PingEvent) -> Self { fn from(_: ping::Event) -> Self {
OutEvent::Other OutEvent::Other
} }
} }
impl From<IdentifyEvent> for OutEvent { impl From<identify::Event> for OutEvent {
fn from(_: IdentifyEvent) -> Self { fn from(_: identify::Event) -> Self {
OutEvent::Other OutEvent::Other
} }
} }
@ -196,10 +204,16 @@ pub mod behaviour {
pub mod rendezvous { pub mod rendezvous {
use super::*; use super::*;
use libp2p::identity;
use libp2p::rendezvous::client::RegisterError;
use libp2p::swarm::dial_opts::DialOpts; use libp2p::swarm::dial_opts::DialOpts;
use libp2p::swarm::DialError; use libp2p::swarm::{
ConnectionDenied, ConnectionId, FromSwarm, THandler, THandlerInEvent, THandlerOutEvent,
ToSwarm,
};
use std::collections::VecDeque; use std::collections::VecDeque;
use std::pin::Pin; use std::pin::Pin;
use std::task::Context;
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
enum ConnectionStatus { enum ConnectionStatus {
@ -222,6 +236,7 @@ pub mod rendezvous {
to_dial: VecDeque<PeerId>, to_dial: VecDeque<PeerId>,
} }
/// A node running the rendezvous server protocol.
pub struct RendezvousNode { pub struct RendezvousNode {
pub address: Multiaddr, pub address: Multiaddr,
connection_status: ConnectionStatus, connection_status: ConnectionStatus,
@ -266,99 +281,151 @@ pub mod rendezvous {
} }
} }
/// Calls the rendezvous register method of the node at node_index in the Vec of rendezvous nodes /// Registers the rendezvous node at the given index.
fn register(&mut self, node_index: usize) { /// Also sets the registration status to [`RegistrationStatus::Pending`].
let node = &self.rendezvous_nodes[node_index]; pub fn register(&mut self, node_index: usize) -> Result<(), RegisterError> {
self.inner let node = &mut self.rendezvous_nodes[node_index];
.register(node.namespace.into(), node.peer_id, node.registration_ttl); node.set_registration(RegistrationStatus::Pending);
let (namespace, peer_id, ttl) =
(node.namespace.into(), node.peer_id, node.registration_ttl);
self.inner.register(namespace, peer_id, ttl)
} }
} }
impl NetworkBehaviour for Behaviour { impl NetworkBehaviour for Behaviour {
type ProtocolsHandler = type ConnectionHandler =
<libp2p::rendezvous::client::Behaviour as NetworkBehaviour>::ProtocolsHandler; <libp2p::rendezvous::client::Behaviour as NetworkBehaviour>::ConnectionHandler;
type OutEvent = libp2p::rendezvous::client::Event; type ToSwarm = libp2p::rendezvous::client::Event;
fn new_handler(&mut self) -> Self::ProtocolsHandler { fn handle_established_inbound_connection(
self.inner.new_handler() &mut self,
connection_id: ConnectionId,
peer: PeerId,
local_addr: &Multiaddr,
remote_addr: &Multiaddr,
) -> Result<THandler<Self>, ConnectionDenied> {
self.inner.handle_established_inbound_connection(
connection_id,
peer,
local_addr,
remote_addr,
)
} }
fn addresses_of_peer(&mut self, peer_id: &PeerId) -> Vec<Multiaddr> { fn handle_established_outbound_connection(
for node in self.rendezvous_nodes.iter() { &mut self,
if peer_id == &node.peer_id { connection_id: ConnectionId,
return vec![node.address.clone()]; peer: PeerId,
} addr: &Multiaddr,
} role_override: libp2p::core::Endpoint,
) -> Result<THandler<Self>, ConnectionDenied> {
vec![] self.inner.handle_established_outbound_connection(
connection_id,
peer,
addr,
role_override,
)
} }
fn inject_connected(&mut self, peer_id: &PeerId) { fn handle_pending_outbound_connection(
for i in 0..self.rendezvous_nodes.len() { &mut self,
if peer_id == &self.rendezvous_nodes[i].peer_id { connection_id: ConnectionId,
self.rendezvous_nodes[i].set_connection(ConnectionStatus::Connected); maybe_peer: Option<PeerId>,
match &self.rendezvous_nodes[i].registration_status { addresses: &[Multiaddr],
RegistrationStatus::RegisterOnNextConnection => { effective_role: libp2p::core::Endpoint,
self.register(i); ) -> std::result::Result<Vec<Multiaddr>, ConnectionDenied> {
self.rendezvous_nodes[i].set_registration(RegistrationStatus::Pending); self.inner.handle_pending_outbound_connection(
connection_id,
maybe_peer,
addresses,
effective_role,
)
}
fn on_swarm_event(&mut self, event: FromSwarm<'_>) {
match event {
FromSwarm::ConnectionEstablished(peer) => {
let peer_id = peer.peer_id;
// Find the rendezvous node that matches the peer id, else do nothing.
if let Some(index) = self
.rendezvous_nodes
.iter_mut()
.position(|node| node.peer_id == peer_id)
{
let rendezvous_node = &mut self.rendezvous_nodes[index];
rendezvous_node.set_connection(ConnectionStatus::Connected);
if let RegistrationStatus::RegisterOnNextConnection =
rendezvous_node.registration_status
{
let _ = self.register(index).inspect_err(|err| {
tracing::error!(
error=%err,
rendezvous_node=%peer_id,
"Failed to register with rendezvous node");
});
} }
RegistrationStatus::Registered { .. } => {}
RegistrationStatus::Pending => {}
} }
} }
} FromSwarm::ConnectionClosed(peer) => {
} // Update the connection status of the rendezvous node that disconnected.
if let Some(node) = self
fn inject_disconnected(&mut self, peer_id: &PeerId) { .rendezvous_nodes
for i in 0..self.rendezvous_nodes.len() { .iter_mut()
let node = &mut self.rendezvous_nodes[i]; .find(|node| node.peer_id == peer.peer_id)
if peer_id == &node.peer_id { {
node.connection_status = ConnectionStatus::Disconnected; node.set_connection(ConnectionStatus::Disconnected);
}
} }
FromSwarm::DialFailure(peer) => {
// Update the connection status of the rendezvous node that failed to connect.
if let Some(peer_id) = peer.peer_id {
if let Some(node) = self
.rendezvous_nodes
.iter_mut()
.find(|node| node.peer_id == peer_id)
{
node.set_connection(ConnectionStatus::Disconnected);
}
}
}
_ => {}
} }
self.inner.on_swarm_event(event);
} }
fn inject_event( fn on_connection_handler_event(
&mut self, &mut self,
peer_id: PeerId, peer_id: PeerId,
connection: ConnectionId, connection_id: ConnectionId,
event: <<Self::ProtocolsHandler as IntoProtocolsHandler>::Handler as ProtocolsHandler>::OutEvent, event: THandlerOutEvent<Self>,
) { ) {
self.inner.inject_event(peer_id, connection, event) self.inner
.on_connection_handler_event(peer_id, connection_id, event)
} }
fn inject_dial_failure(
&mut self,
peer_id: Option<PeerId>,
_handler: Self::ProtocolsHandler,
_error: &DialError,
) {
for i in 0..self.rendezvous_nodes.len() {
let node = &mut self.rendezvous_nodes[i];
if let Some(id) = peer_id {
if id == node.peer_id {
node.connection_status = ConnectionStatus::Disconnected;
}
}
}
}
#[allow(clippy::type_complexity)]
fn poll( fn poll(
&mut self, &mut self,
cx: &mut std::task::Context<'_>, cx: &mut Context<'_>,
params: &mut impl PollParameters, ) -> Poll<ToSwarm<Self::ToSwarm, THandlerInEvent<Self>>> {
) -> Poll<NetworkBehaviourAction<Self::OutEvent, Self::ProtocolsHandler>> {
if let Some(peer_id) = self.to_dial.pop_front() { if let Some(peer_id) = self.to_dial.pop_front() {
return Poll::Ready(NetworkBehaviourAction::Dial { return Poll::Ready(ToSwarm::Dial {
opts: DialOpts::peer_id(peer_id) opts: DialOpts::peer_id(peer_id)
.addresses(vec![self
.rendezvous_nodes
.iter()
.find(|node| node.peer_id == peer_id)
.map(|node| node.address.clone())
.expect("We should have a rendezvous node for the peer id")])
.condition(PeerCondition::Disconnected) .condition(PeerCondition::Disconnected)
// TODO: this makes the behaviour call `NetworkBehaviour::handle_pending_outbound_connection`
// but we don't implement it
.extend_addresses_through_behaviour()
.build(), .build(),
handler: Self::ProtocolsHandler::new(Duration::from_secs(30)),
}); });
} }
// check the status of each rendezvous node // Check the status of each rendezvous node
for i in 0..self.rendezvous_nodes.len() { for i in 0..self.rendezvous_nodes.len() {
let connection_status = self.rendezvous_nodes[i].connection_status.clone(); let connection_status = self.rendezvous_nodes[i].connection_status.clone();
match &mut self.rendezvous_nodes[i].registration_status { match &mut self.rendezvous_nodes[i].registration_status {
@ -369,17 +436,19 @@ pub mod rendezvous {
} }
ConnectionStatus::Dialling => {} ConnectionStatus::Dialling => {}
ConnectionStatus::Connected => { ConnectionStatus::Connected => {
self.rendezvous_nodes[i].set_registration(RegistrationStatus::Pending); let _ = self.register(i);
self.register(i);
} }
}, },
RegistrationStatus::Registered { re_register_in } => { RegistrationStatus::Registered { re_register_in } => {
if let Poll::Ready(()) = re_register_in.poll_unpin(cx) { if let Poll::Ready(()) = re_register_in.poll_unpin(cx) {
match connection_status { match connection_status {
ConnectionStatus::Connected => { ConnectionStatus::Connected => {
self.rendezvous_nodes[i] let _ = self.register(i).inspect_err(|err| {
.set_registration(RegistrationStatus::Pending); tracing::error!(
self.register(i); error=%err,
rendezvous_node=%self.rendezvous_nodes[i].peer_id,
"Failed to register with rendezvous node");
});
} }
ConnectionStatus::Disconnected => { ConnectionStatus::Disconnected => {
self.rendezvous_nodes[i].set_registration( self.rendezvous_nodes[i].set_registration(
@ -395,10 +464,10 @@ pub mod rendezvous {
} }
} }
let inner_poll = self.inner.poll(cx, params); let inner_poll = self.inner.poll(cx);
// reset the timer for the specific rendezvous node if we successfully registered // reset the timer for the specific rendezvous node if we successfully registered
if let Poll::Ready(NetworkBehaviourAction::GenerateEvent( if let Poll::Ready(ToSwarm::GenerateEvent(
libp2p::rendezvous::client::Event::Registered { libp2p::rendezvous::client::Event::Registered {
ttl, ttl,
rendezvous_node, rendezvous_node,
@ -434,7 +503,7 @@ pub mod rendezvous {
#[tokio::test] #[tokio::test]
async fn given_no_initial_connection_when_constructed_asb_connects_and_registers_with_rendezvous_node( async fn given_no_initial_connection_when_constructed_asb_connects_and_registers_with_rendezvous_node(
) { ) {
let mut rendezvous_node = new_swarm(|_, _| { let mut rendezvous_node = new_swarm(|_| {
rendezvous::server::Behaviour::new(rendezvous::server::Config::default()) rendezvous::server::Behaviour::new(rendezvous::server::Config::default())
}); });
let address = rendezvous_node.listen_on_random_memory_address().await; let address = rendezvous_node.listen_on_random_memory_address().await;
@ -445,7 +514,7 @@ pub mod rendezvous {
None, None,
); );
let mut asb = new_swarm(|_, identity| { let mut asb = new_swarm(|identity| {
super::rendezvous::Behaviour::new(identity, vec![rendezvous_point]) super::rendezvous::Behaviour::new(identity, vec![rendezvous_point])
}); });
asb.listen_on_random_memory_address().await; // this adds an external address asb.listen_on_random_memory_address().await; // this adds an external address
@ -473,7 +542,7 @@ pub mod rendezvous {
#[tokio::test] #[tokio::test]
async fn asb_automatically_re_registers() { async fn asb_automatically_re_registers() {
let mut rendezvous_node = new_swarm(|_, _| { let mut rendezvous_node = new_swarm(|_| {
rendezvous::server::Behaviour::new( rendezvous::server::Behaviour::new(
rendezvous::server::Config::default().with_min_ttl(2), rendezvous::server::Config::default().with_min_ttl(2),
) )
@ -486,7 +555,7 @@ pub mod rendezvous {
Some(5), Some(5),
); );
let mut asb = new_swarm(|_, identity| { let mut asb = new_swarm(|identity| {
super::rendezvous::Behaviour::new(identity, vec![rendezvous_point]) super::rendezvous::Behaviour::new(identity, vec![rendezvous_point])
}); });
asb.listen_on_random_memory_address().await; // this adds an external address asb.listen_on_random_memory_address().await; // this adds an external address
@ -525,7 +594,7 @@ pub mod rendezvous {
let mut registrations = HashMap::new(); let mut registrations = HashMap::new();
// register with 5 rendezvous nodes // register with 5 rendezvous nodes
for _ in 0..5 { for _ in 0..5 {
let mut rendezvous = new_swarm(|_, _| { let mut rendezvous = new_swarm(|_| {
rendezvous::server::Behaviour::new( rendezvous::server::Behaviour::new(
rendezvous::server::Config::default().with_min_ttl(2), rendezvous::server::Config::default().with_min_ttl(2),
) )
@ -546,9 +615,8 @@ pub mod rendezvous {
}); });
} }
let mut asb = new_swarm(|_, identity| { let mut asb =
super::rendezvous::Behaviour::new(identity, rendezvous_nodes) new_swarm(|identity| super::rendezvous::Behaviour::new(identity, rendezvous_nodes));
});
asb.listen_on_random_memory_address().await; // this adds an external address asb.listen_on_random_memory_address().await; // this adds an external address
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {

View file

@ -16,7 +16,6 @@ use anyhow::{bail, Context, Result};
use comfy_table::Table; use comfy_table::Table;
use libp2p::core::multiaddr::Protocol; use libp2p::core::multiaddr::Protocol;
use libp2p::core::Multiaddr; use libp2p::core::Multiaddr;
use libp2p::swarm::AddressScore;
use libp2p::Swarm; use libp2p::Swarm;
use std::convert::TryInto; use std::convert::TryInto;
use std::env; use std::env;
@ -186,18 +185,15 @@ pub async fn main() -> Result<()> {
)?; )?;
for listen in config.network.listen.clone() { for listen in config.network.listen.clone() {
Swarm::listen_on(&mut swarm, listen.clone()) if let Err(e) = Swarm::listen_on(&mut swarm, listen.clone()) {
.with_context(|| format!("Failed to listen on network interface {}", listen))?; tracing::warn!("Failed to listen on network interface {}: {}. Consider removing it from the config.", listen, e);
}
} }
tracing::info!(peer_id = %swarm.local_peer_id(), "Network layer initialized"); tracing::info!(peer_id = %swarm.local_peer_id(), "Network layer initialized");
for external_address in config.network.external_addresses { for external_address in config.network.external_addresses {
let _ = Swarm::add_external_address( Swarm::add_external_address(&mut swarm, external_address);
&mut swarm,
external_address,
AddressScore::Infinite,
);
} }
let (event_loop, mut swap_receiver) = EventLoop::new( let (event_loop, mut swap_receiver) = EventLoop::new(

View file

@ -874,6 +874,11 @@ impl EstimateFeeRate for Client {
// https://github.com/romanz/electrs/blob/f9cf5386d1b5de6769ee271df5eef324aa9491bc/src/rpc.rs#L213 // https://github.com/romanz/electrs/blob/f9cf5386d1b5de6769ee271df5eef324aa9491bc/src/rpc.rs#L213
// Returned estimated fees are per BTC/kb. // Returned estimated fees are per BTC/kb.
let fee_per_byte = self.electrum.estimate_fee(target_block.into())?; let fee_per_byte = self.electrum.estimate_fee(target_block.into())?;
if fee_per_byte < 0.0 {
bail!("Fee per byte returned by electrum server is negative: {}. This may indicate that fee estimation is not supported by this server", fee_per_byte);
}
// we do not expect fees being that high. // we do not expect fees being that high.
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_possible_truncation)]
Ok(FeeRate::from_btc_per_kvb(fee_per_byte as f32)) Ok(FeeRate::from_btc_per_kvb(fee_per_byte as f32))

View file

@ -23,12 +23,15 @@ mod tests {
use crate::network::rendezvous::XmrBtcNamespace; use crate::network::rendezvous::XmrBtcNamespace;
use crate::network::test::{new_swarm, SwarmExt}; use crate::network::test::{new_swarm, SwarmExt};
use futures::StreamExt; use futures::StreamExt;
use libp2p::core::Endpoint;
use libp2p::multiaddr::Protocol; use libp2p::multiaddr::Protocol;
use libp2p::request_response::RequestResponseEvent; use libp2p::swarm::{
use libp2p::swarm::{AddressScore, NetworkBehaviourEventProcess}; ConnectionDenied, ConnectionId, FromSwarm, THandlerInEvent, THandlerOutEvent, ToSwarm,
use libp2p::{identity, rendezvous, Multiaddr, PeerId}; };
use libp2p::{identity, rendezvous, request_response, Multiaddr, PeerId};
use std::collections::HashSet; use std::collections::HashSet;
use std::iter::FromIterator; use std::iter::FromIterator;
use std::task::Poll;
use std::time::Duration; use std::time::Duration;
#[tokio::test] #[tokio::test]
@ -57,7 +60,7 @@ mod tests {
} }
async fn setup_rendezvous_point() -> (Multiaddr, PeerId) { async fn setup_rendezvous_point() -> (Multiaddr, PeerId) {
let mut rendezvous_node = new_swarm(|_, _| RendezvousPointBehaviour::default()); let mut rendezvous_node = new_swarm(|_| RendezvousPointBehaviour::default());
let rendezvous_address = rendezvous_node.listen_on_tcp_localhost().await; let rendezvous_address = rendezvous_node.listen_on_tcp_localhost().await;
let rendezvous_peer_id = *rendezvous_node.local_peer_id(); let rendezvous_peer_id = *rendezvous_node.local_peer_id();
@ -81,22 +84,23 @@ mod tests {
max_quantity: bitcoin::Amount::from_sat(9001), max_quantity: bitcoin::Amount::from_sat(9001),
}; };
let mut asb = new_swarm(|_, identity| { let mut asb = new_swarm(|identity| {
let rendezvous_node = let rendezvous_node =
RendezvousNode::new(rendezvous_address, rendezvous_peer_id, namespace, None); RendezvousNode::new(rendezvous_address, rendezvous_peer_id, namespace, None);
let rendezvous = asb::rendezvous::Behaviour::new(identity, vec![rendezvous_node]); let rendezvous = asb::rendezvous::Behaviour::new(identity, vec![rendezvous_node]);
StaticQuoteAsbBehaviour { StaticQuoteAsbBehaviour {
rendezvous, inner: StaticQuoteAsbBehaviourInner {
ping: Default::default(), rendezvous,
quote: quote::asb(), quote: quote::asb(),
},
static_quote, static_quote,
registered: false, registered: false,
} }
}); });
let asb_address = asb.listen_on_tcp_localhost().await; let asb_address = asb.listen_on_tcp_localhost().await;
asb.add_external_address(asb_address.clone(), AddressScore::Infinite); asb.add_external_address(asb_address.clone());
let asb_peer_id = *asb.local_peer_id(); let asb_peer_id = *asb.local_peer_id();
@ -113,62 +117,114 @@ mod tests {
}); });
Seller { Seller {
multiaddr: asb_address.with(Protocol::P2p(asb_peer_id.into())), multiaddr: asb_address.with(Protocol::P2p(asb_peer_id)),
status: Status::Online(static_quote), status: Status::Online(static_quote),
} }
} }
#[derive(libp2p::NetworkBehaviour)] #[derive(libp2p::swarm::NetworkBehaviour)]
#[behaviour(event_process = true)] struct StaticQuoteAsbBehaviourInner {
struct StaticQuoteAsbBehaviour {
rendezvous: asb::rendezvous::Behaviour, rendezvous: asb::rendezvous::Behaviour,
// Support `Ping` as a workaround until https://github.com/libp2p/rust-libp2p/issues/2109 is fixed.
ping: libp2p::ping::Ping,
quote: quote::Behaviour, quote: quote::Behaviour,
}
#[behaviour(ignore)] struct StaticQuoteAsbBehaviour {
inner: StaticQuoteAsbBehaviourInner,
static_quote: BidQuote, static_quote: BidQuote,
#[behaviour(ignore)]
registered: bool, registered: bool,
} }
impl NetworkBehaviourEventProcess<rendezvous::client::Event> for StaticQuoteAsbBehaviour {
fn inject_event(&mut self, event: rendezvous::client::Event) { impl libp2p::swarm::NetworkBehaviour for StaticQuoteAsbBehaviour {
if let rendezvous::client::Event::Registered { .. } = event { type ConnectionHandler =
self.registered = true; <StaticQuoteAsbBehaviourInner as libp2p::swarm::NetworkBehaviour>::ConnectionHandler;
type ToSwarm = <StaticQuoteAsbBehaviourInner as libp2p::swarm::NetworkBehaviour>::ToSwarm;
fn handle_established_inbound_connection(
&mut self,
connection_id: ConnectionId,
peer: PeerId,
local_addr: &Multiaddr,
remote_addr: &Multiaddr,
) -> Result<libp2p::swarm::THandler<Self>, ConnectionDenied> {
self.inner.handle_established_inbound_connection(
connection_id,
peer,
local_addr,
remote_addr,
)
}
fn handle_established_outbound_connection(
&mut self,
connection_id: ConnectionId,
peer: PeerId,
addr: &Multiaddr,
role_override: Endpoint,
) -> Result<libp2p::swarm::THandler<Self>, ConnectionDenied> {
self.inner.handle_established_outbound_connection(
connection_id,
peer,
addr,
role_override,
)
}
fn on_swarm_event(&mut self, event: FromSwarm<'_>) {
self.inner.on_swarm_event(event);
}
fn on_connection_handler_event(
&mut self,
peer_id: PeerId,
connection_id: ConnectionId,
event: THandlerOutEvent<Self>,
) {
self.inner
.on_connection_handler_event(peer_id, connection_id, event);
}
fn poll(
&mut self,
cx: &mut std::task::Context<'_>,
) -> Poll<ToSwarm<Self::ToSwarm, THandlerInEvent<Self>>> {
match self.inner.poll(cx) {
Poll::Ready(ToSwarm::GenerateEvent(event)) => match event {
StaticQuoteAsbBehaviourInnerEvent::Rendezvous(rendezvous_event) => {
if let rendezvous::client::Event::Registered { .. } = rendezvous_event {
self.registered = true;
}
Poll::Ready(ToSwarm::GenerateEvent(
StaticQuoteAsbBehaviourInnerEvent::Rendezvous(rendezvous_event),
))
}
StaticQuoteAsbBehaviourInnerEvent::Quote(quote_event) => {
if let request_response::Event::Message {
message: quote::Message::Request { channel, .. },
..
} = quote_event
{
self.inner
.quote
.send_response(channel, self.static_quote)
.unwrap();
return Poll::Pending;
}
Poll::Ready(ToSwarm::GenerateEvent(
StaticQuoteAsbBehaviourInnerEvent::Quote(quote_event),
))
}
},
other => other,
} }
} }
} }
impl NetworkBehaviourEventProcess<libp2p::ping::PingEvent> for StaticQuoteAsbBehaviour { #[derive(libp2p::swarm::NetworkBehaviour)]
fn inject_event(&mut self, _: libp2p::ping::PingEvent) {}
}
impl NetworkBehaviourEventProcess<quote::OutEvent> for StaticQuoteAsbBehaviour {
fn inject_event(&mut self, event: quote::OutEvent) {
if let RequestResponseEvent::Message {
message: quote::Message::Request { channel, .. },
..
} = event
{
self.quote
.send_response(channel, self.static_quote)
.unwrap();
}
}
}
#[derive(libp2p::NetworkBehaviour)]
#[behaviour(event_process = true)]
struct RendezvousPointBehaviour { struct RendezvousPointBehaviour {
rendezvous: rendezvous::server::Behaviour, rendezvous: rendezvous::server::Behaviour,
// Support `Ping` as a workaround until https://github.com/libp2p/rust-libp2p/issues/2109 is fixed.
ping: libp2p::ping::Ping,
}
impl NetworkBehaviourEventProcess<rendezvous::server::Event> for RendezvousPointBehaviour {
fn inject_event(&mut self, _: rendezvous::server::Event) {}
}
impl NetworkBehaviourEventProcess<libp2p::ping::PingEvent> for RendezvousPointBehaviour {
fn inject_event(&mut self, _: libp2p::ping::PingEvent) {}
} }
impl Default for RendezvousPointBehaviour { impl Default for RendezvousPointBehaviour {
@ -177,7 +233,6 @@ mod tests {
rendezvous: rendezvous::server::Behaviour::new( rendezvous: rendezvous::server::Behaviour::new(
rendezvous::server::Config::default(), rendezvous::server::Config::default(),
), ),
ping: Default::default(),
} }
} }
} }

View file

@ -626,7 +626,7 @@ pub async fn buy_xmr(
) )
.await?; .await?;
swarm.behaviour_mut().add_address(seller_peer_id, seller); swarm.add_peer_address(seller_peer_id, seller);
context context
.db .db
@ -816,9 +816,7 @@ pub async fn resume_swap(
// Fetch the seller's addresses from the database and add them to the swarm // Fetch the seller's addresses from the database and add them to the swarm
for seller_address in seller_addresses { for seller_address in seller_addresses {
swarm swarm.add_peer_address(seller_peer_id, seller_address);
.behaviour_mut()
.add_address(seller_peer_id, seller_address);
} }
let (event_loop, event_loop_handle) = let (event_loop, event_loop_handle) =
@ -1222,9 +1220,17 @@ where
} }
loop { loop {
println!("max_giveable: {}", max_giveable);
println!("bid_quote.min_quantity: {}", bid_quote.min_quantity);
let min_outstanding = bid_quote.min_quantity - max_giveable; let min_outstanding = bid_quote.min_quantity - max_giveable;
println!("min_outstanding: {}", min_outstanding);
let min_bitcoin_lock_tx_fee = estimate_fee(min_outstanding).await?; let min_bitcoin_lock_tx_fee = estimate_fee(min_outstanding).await?;
println!("min_bitcoin_lock_tx_fee: {}", min_bitcoin_lock_tx_fee);
let min_deposit_until_swap_will_start = min_outstanding + min_bitcoin_lock_tx_fee; let min_deposit_until_swap_will_start = min_outstanding + min_bitcoin_lock_tx_fee;
println!(
"min_deposit_until_swap_will_start: {}",
min_deposit_until_swap_will_start
);
let max_deposit_until_maximum_amount_is_reached = let max_deposit_until_maximum_amount_is_reached =
maximum_amount - max_giveable + min_bitcoin_lock_tx_fee; maximum_amount - max_giveable + min_bitcoin_lock_tx_fee;

View file

@ -9,18 +9,18 @@ use crate::network::{
use crate::protocol::bob::State2; use crate::protocol::bob::State2;
use crate::{bitcoin, env}; use crate::{bitcoin, env};
use anyhow::{anyhow, Error, Result}; use anyhow::{anyhow, Error, Result};
use libp2p::core::Multiaddr; use libp2p::request_response::{
use libp2p::identify::{Identify, IdentifyConfig, IdentifyEvent}; InboundFailure, InboundRequestId, OutboundFailure, OutboundRequestId, ResponseChannel,
use libp2p::ping::{Ping, PingConfig, PingEvent}; };
use libp2p::request_response::{RequestId, ResponseChannel}; use libp2p::swarm::NetworkBehaviour;
use libp2p::{identity, NetworkBehaviour, PeerId}; use libp2p::{identify, identity, ping, PeerId};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
#[derive(Debug)] #[derive(Debug)]
pub enum OutEvent { pub enum OutEvent {
QuoteReceived { QuoteReceived {
id: RequestId, id: OutboundRequestId,
response: BidQuote, response: BidQuote,
}, },
SwapSetupCompleted(Box<Result<State2>>), SwapSetupCompleted(Box<Result<State2>>),
@ -30,25 +30,34 @@ pub enum OutEvent {
peer: PeerId, peer: PeerId,
}, },
EncryptedSignatureAcknowledged { EncryptedSignatureAcknowledged {
id: RequestId, id: OutboundRequestId,
}, },
CooperativeXmrRedeemFulfilled { CooperativeXmrRedeemFulfilled {
id: RequestId, id: OutboundRequestId,
s_a: Scalar, s_a: Scalar,
swap_id: uuid::Uuid, swap_id: uuid::Uuid,
}, },
CooperativeXmrRedeemRejected { CooperativeXmrRedeemRejected {
id: RequestId, id: OutboundRequestId,
reason: CooperativeXmrRedeemRejectReason, reason: CooperativeXmrRedeemRejectReason,
swap_id: uuid::Uuid, swap_id: uuid::Uuid,
}, },
AllRedialAttemptsExhausted {
peer: PeerId,
},
Failure { Failure {
peer: PeerId, peer: PeerId,
error: Error, error: Error,
}, },
OutboundRequestResponseFailure {
peer: PeerId,
error: OutboundFailure,
request_id: OutboundRequestId,
protocol: String,
},
InboundRequestResponseFailure {
peer: PeerId,
error: InboundFailure,
request_id: InboundRequestId,
protocol: String,
},
/// "Fallback" variant that allows the event mapping code to swallow certain /// "Fallback" variant that allows the event mapping code to swallow certain
/// events that we don't want the caller to deal with. /// events that we don't want the caller to deal with.
Other, Other,
@ -72,7 +81,7 @@ impl OutEvent {
/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob. /// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob.
#[derive(NetworkBehaviour)] #[derive(NetworkBehaviour)]
#[behaviour(out_event = "OutEvent", event_process = false)] #[behaviour(to_swarm = "OutEvent")]
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct Behaviour { pub struct Behaviour {
pub quote: quote::Behaviour, pub quote: quote::Behaviour,
@ -81,12 +90,12 @@ pub struct Behaviour {
pub cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::Behaviour, pub cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::Behaviour,
pub encrypted_signature: encrypted_signature::Behaviour, pub encrypted_signature: encrypted_signature::Behaviour,
pub redial: redial::Behaviour, pub redial: redial::Behaviour,
pub identify: Identify, pub identify: identify::Behaviour,
/// Ping behaviour that ensures that the underlying network connection is /// Ping behaviour that ensures that the underlying network connection is
/// still alive. If the ping fails a connection close event will be /// still alive. If the ping fails a connection close event will be
/// emitted that is picked up as swarm event. /// emitted that is picked up as swarm event.
ping: Ping, ping: ping::Behaviour,
} }
impl Behaviour { impl Behaviour {
@ -98,37 +107,37 @@ impl Behaviour {
) -> Self { ) -> Self {
let agentVersion = format!("cli/{} ({})", env!("CARGO_PKG_VERSION"), identify_params.1); let agentVersion = format!("cli/{} ({})", env!("CARGO_PKG_VERSION"), identify_params.1);
let protocolVersion = "/comit/xmr/btc/1.0.0".to_string(); let protocolVersion = "/comit/xmr/btc/1.0.0".to_string();
let identifyConfig = IdentifyConfig::new(protocolVersion, identify_params.0.public())
let identifyConfig = identify::Config::new(protocolVersion, identify_params.0.public())
.with_agent_version(agentVersion); .with_agent_version(agentVersion);
let pingConfig = ping::Config::new().with_timeout(Duration::from_secs(60));
Self { Self {
quote: quote::cli(), quote: quote::cli(),
swap_setup: bob::Behaviour::new(env_config, bitcoin_wallet), swap_setup: bob::Behaviour::new(env_config, bitcoin_wallet),
transfer_proof: transfer_proof::bob(), transfer_proof: transfer_proof::bob(),
encrypted_signature: encrypted_signature::bob(), encrypted_signature: encrypted_signature::bob(),
cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::bob(), cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::bob(),
redial: redial::Behaviour::new(alice, Duration::from_secs(2)), redial: redial::Behaviour::new(
ping: Ping::new(PingConfig::new().with_keep_alive(true)), alice,
identify: Identify::new(identifyConfig), Duration::from_secs(2),
Duration::from_secs(5 * 60),
),
ping: ping::Behaviour::new(pingConfig),
identify: identify::Behaviour::new(identifyConfig),
} }
} }
/// Add a known address for the given peer
pub fn add_address(&mut self, peer_id: PeerId, address: Multiaddr) {
self.quote.add_address(&peer_id, address.clone());
self.transfer_proof.add_address(&peer_id, address.clone());
self.encrypted_signature.add_address(&peer_id, address);
}
} }
impl From<PingEvent> for OutEvent { impl From<ping::Event> for OutEvent {
fn from(_: PingEvent) -> Self { fn from(_: ping::Event) -> Self {
OutEvent::Other OutEvent::Other
} }
} }
impl From<IdentifyEvent> for OutEvent { impl From<identify::Event> for OutEvent {
fn from(_: IdentifyEvent) -> Self { fn from(_: identify::Event) -> Self {
OutEvent::Other OutEvent::Other
} }
} }

View file

@ -1,16 +1,17 @@
use crate::bitcoin::EncryptedSignature; use crate::bitcoin::EncryptedSignature;
use crate::cli::behaviour::{Behaviour, OutEvent}; use crate::cli::behaviour::{Behaviour, OutEvent};
use crate::monero; use crate::monero;
use crate::network::cooperative_xmr_redeem_after_punish::{Request, Response}; use crate::network::cooperative_xmr_redeem_after_punish::{self, Request, Response};
use crate::network::encrypted_signature; use crate::network::encrypted_signature;
use crate::network::quote::BidQuote; use crate::network::quote::BidQuote;
use crate::network::swap_setup::bob::NewSwap; use crate::network::swap_setup::bob::NewSwap;
use crate::protocol::bob::State2; use crate::protocol::bob::swap::has_already_processed_transfer_proof;
use crate::protocol::bob::{BobState, State2};
use crate::protocol::Database; use crate::protocol::Database;
use anyhow::{Context, Result}; use anyhow::{anyhow, Context, Result};
use futures::future::{BoxFuture, OptionFuture}; use futures::future::{BoxFuture, OptionFuture};
use futures::{FutureExt, StreamExt}; use futures::{FutureExt, StreamExt};
use libp2p::request_response::{RequestId, ResponseChannel}; use libp2p::request_response::{OutboundFailure, OutboundRequestId, ResponseChannel};
use libp2p::swarm::dial_opts::DialOpts; use libp2p::swarm::dial_opts::DialOpts;
use libp2p::swarm::SwarmEvent; use libp2p::swarm::SwarmEvent;
use libp2p::{PeerId, Swarm}; use libp2p::{PeerId, Swarm};
@ -19,6 +20,9 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use uuid::Uuid; use uuid::Uuid;
static REQUEST_RESPONSE_PROTOCOL_TIMEOUT: Duration = Duration::from_secs(60);
static EXECUTION_SETUP_PROTOCOL_TIMEOUT: Duration = Duration::from_secs(120);
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct EventLoop { pub struct EventLoop {
swap_id: Uuid, swap_id: Uuid,
@ -26,21 +30,35 @@ pub struct EventLoop {
alice_peer_id: PeerId, alice_peer_id: PeerId,
db: Arc<dyn Database + Send + Sync>, db: Arc<dyn Database + Send + Sync>,
// these streams represents outgoing requests that we have to make // These streams represents outgoing requests that we have to make
quote_requests: bmrng::RequestReceiverStream<(), BidQuote>, // These are essentially queues of requests that we will send to Alice once we are connected to her.
cooperative_xmr_redeem_requests: bmrng::RequestReceiverStream<Uuid, Response>, quote_requests: bmrng::RequestReceiverStream<(), Result<BidQuote, OutboundFailure>>,
encrypted_signatures: bmrng::RequestReceiverStream<EncryptedSignature, ()>, cooperative_xmr_redeem_requests: bmrng::RequestReceiverStream<
swap_setup_requests: bmrng::RequestReceiverStream<NewSwap, Result<State2>>, (),
Result<cooperative_xmr_redeem_after_punish::Response, OutboundFailure>,
>,
encrypted_signatures_requests:
bmrng::RequestReceiverStream<EncryptedSignature, Result<(), OutboundFailure>>,
execution_setup_requests: bmrng::RequestReceiverStream<NewSwap, Result<State2>>,
// these represents requests that are currently in-flight. // These represents requests that are currently in-flight.
// once we get a response to a matching [`RequestId`], we will use the responder to relay the // Meaning that we have sent them to Alice, but we have not yet received a response.
// Once we get a response to a matching [`RequestId`], we will use the responder to relay the
// response. // response.
inflight_quote_requests: HashMap<RequestId, bmrng::Responder<BidQuote>>, inflight_quote_requests:
inflight_encrypted_signature_requests: HashMap<RequestId, bmrng::Responder<()>>, HashMap<OutboundRequestId, bmrng::Responder<Result<BidQuote, OutboundFailure>>>,
inflight_encrypted_signature_requests:
HashMap<OutboundRequestId, bmrng::Responder<Result<(), OutboundFailure>>>,
inflight_swap_setup: Option<bmrng::Responder<Result<State2>>>, inflight_swap_setup: Option<bmrng::Responder<Result<State2>>>,
inflight_cooperative_xmr_redeem_requests: HashMap<RequestId, bmrng::Responder<Response>>, inflight_cooperative_xmr_redeem_requests: HashMap<
/// The sender we will use to relay incoming transfer proofs. OutboundRequestId,
transfer_proof: bmrng::RequestSender<monero::TransferProof, ()>, bmrng::Responder<Result<cooperative_xmr_redeem_after_punish::Response, OutboundFailure>>,
>,
/// The sender we will use to relay incoming transfer proofs to the EventLoopHandle
/// The corresponding receiver is stored in the EventLoopHandle
transfer_proof_sender: bmrng::RequestSender<monero::TransferProof, ()>,
/// The future representing the successful handling of an incoming transfer /// The future representing the successful handling of an incoming transfer
/// proof. /// proof.
/// ///
@ -58,20 +76,26 @@ impl EventLoop {
alice_peer_id: PeerId, alice_peer_id: PeerId,
db: Arc<dyn Database + Send + Sync>, db: Arc<dyn Database + Send + Sync>,
) -> Result<(Self, EventLoopHandle)> { ) -> Result<(Self, EventLoopHandle)> {
let execution_setup = bmrng::channel_with_timeout(1, Duration::from_secs(60)); // We still use a timeout here, because this protocol does not dial Alice itself
let transfer_proof = bmrng::channel_with_timeout(1, Duration::from_secs(60)); // and we want to fail if we cannot reach Alice
let encrypted_signature = bmrng::channel(1); let (execution_setup_sender, execution_setup_receiver) =
let quote = bmrng::channel_with_timeout(1, Duration::from_secs(60)); bmrng::channel_with_timeout(1, EXECUTION_SETUP_PROTOCOL_TIMEOUT);
let cooperative_xmr_redeem = bmrng::channel_with_timeout(1, Duration::from_secs(60));
// It is okay to not have a timeout here, as timeouts are enforced by the request-response protocol
let (transfer_proof_sender, transfer_proof_receiver) = bmrng::channel(1);
let (encrypted_signature_sender, encrypted_signature_receiver) = bmrng::channel(1);
let (quote_sender, quote_receiver) = bmrng::channel(1);
let (cooperative_xmr_redeem_sender, cooperative_xmr_redeem_receiver) = bmrng::channel(1);
let event_loop = EventLoop { let event_loop = EventLoop {
swap_id, swap_id,
swarm, swarm,
alice_peer_id, alice_peer_id,
swap_setup_requests: execution_setup.1.into(), execution_setup_requests: execution_setup_receiver.into(),
transfer_proof: transfer_proof.0, transfer_proof_sender,
encrypted_signatures: encrypted_signature.1.into(), encrypted_signatures_requests: encrypted_signature_receiver.into(),
cooperative_xmr_redeem_requests: cooperative_xmr_redeem.1.into(), cooperative_xmr_redeem_requests: cooperative_xmr_redeem_receiver.into(),
quote_requests: quote.1.into(), quote_requests: quote_receiver.into(),
inflight_quote_requests: HashMap::default(), inflight_quote_requests: HashMap::default(),
inflight_swap_setup: None, inflight_swap_setup: None,
inflight_encrypted_signature_requests: HashMap::default(), inflight_encrypted_signature_requests: HashMap::default(),
@ -81,11 +105,11 @@ impl EventLoop {
}; };
let handle = EventLoopHandle { let handle = EventLoopHandle {
swap_setup: execution_setup.0, execution_setup_sender,
transfer_proof: transfer_proof.1, transfer_proof_receiver,
encrypted_signature: encrypted_signature.0, encrypted_signature_sender,
cooperative_xmr_redeem: cooperative_xmr_redeem.0, cooperative_xmr_redeem_sender,
quote: quote.0, quote_sender,
}; };
Ok((event_loop, handle)) Ok((event_loop, handle))
@ -107,7 +131,7 @@ impl EventLoop {
match swarm_event { match swarm_event {
SwarmEvent::Behaviour(OutEvent::QuoteReceived { id, response }) => { SwarmEvent::Behaviour(OutEvent::QuoteReceived { id, response }) => {
if let Some(responder) = self.inflight_quote_requests.remove(&id) { if let Some(responder) = self.inflight_quote_requests.remove(&id) {
let _ = responder.respond(response); let _ = responder.respond(Ok(response));
} }
} }
SwarmEvent::Behaviour(OutEvent::SwapSetupCompleted(response)) => { SwarmEvent::Behaviour(OutEvent::SwapSetupCompleted(response)) => {
@ -128,7 +152,27 @@ impl EventLoop {
continue; continue;
} }
let mut responder = match self.transfer_proof.send(msg.tx_lock_proof).await { // Immediately acknowledge if we've already processed this transfer proof
// This handles the case where Alice didn't receive our previous acknowledgment
// and is retrying sending the transfer proof
if let Ok(state) = self.db.get_state(swap_id).await {
let state: BobState = state.try_into()
.expect("Bobs database only contains Bob states");
if has_already_processed_transfer_proof(&state) {
tracing::warn!("Received transfer proof for swap {} but we are already in state {}. Acknowledging immediately. Alice most likely did not receive the acknowledgment when we sent it before", swap_id, state);
// We set this to a future that will resolve immediately, and returns the channel
// This will be resolved in the next iteration of the event loop, and a response will be sent to Alice
self.pending_transfer_proof = OptionFuture::from(Some(async move {
channel
}.boxed()));
continue;
}
}
let mut responder = match self.transfer_proof_sender.send(msg.tx_lock_proof).await {
Ok(responder) => responder, Ok(responder) => responder,
Err(e) => { Err(e) => {
tracing::warn!("Failed to pass on transfer proof: {:#}", e); tracing::warn!("Failed to pass on transfer proof: {:#}", e);
@ -178,23 +222,19 @@ impl EventLoop {
} }
SwarmEvent::Behaviour(OutEvent::EncryptedSignatureAcknowledged { id }) => { SwarmEvent::Behaviour(OutEvent::EncryptedSignatureAcknowledged { id }) => {
if let Some(responder) = self.inflight_encrypted_signature_requests.remove(&id) { if let Some(responder) = self.inflight_encrypted_signature_requests.remove(&id) {
let _ = responder.respond(()); let _ = responder.respond(Ok(()));
} }
} }
SwarmEvent::Behaviour(OutEvent::CooperativeXmrRedeemFulfilled { id, swap_id, s_a }) => { SwarmEvent::Behaviour(OutEvent::CooperativeXmrRedeemFulfilled { id, swap_id, s_a }) => {
if let Some(responder) = self.inflight_cooperative_xmr_redeem_requests.remove(&id) { if let Some(responder) = self.inflight_cooperative_xmr_redeem_requests.remove(&id) {
let _ = responder.respond(Response::Fullfilled { s_a, swap_id }); let _ = responder.respond(Ok(Response::Fullfilled { s_a, swap_id }));
} }
} }
SwarmEvent::Behaviour(OutEvent::CooperativeXmrRedeemRejected { id, swap_id, reason }) => { SwarmEvent::Behaviour(OutEvent::CooperativeXmrRedeemRejected { id, swap_id, reason }) => {
if let Some(responder) = self.inflight_cooperative_xmr_redeem_requests.remove(&id) { if let Some(responder) = self.inflight_cooperative_xmr_redeem_requests.remove(&id) {
let _ = responder.respond(Response::Rejected { reason, swap_id }); let _ = responder.respond(Ok(Response::Rejected { reason, swap_id }));
} }
} }
SwarmEvent::Behaviour(OutEvent::AllRedialAttemptsExhausted { peer }) if peer == self.alice_peer_id => {
tracing::error!("Exhausted all re-dial attempts to Alice");
return;
}
SwarmEvent::Behaviour(OutEvent::Failure { peer, error }) => { SwarmEvent::Behaviour(OutEvent::Failure { peer, error }) => {
tracing::warn!(%peer, err = %error, "Communication error"); tracing::warn!(%peer, err = %error, "Communication error");
return; return;
@ -202,40 +242,75 @@ impl EventLoop {
SwarmEvent::ConnectionEstablished { peer_id, endpoint, .. } if peer_id == self.alice_peer_id => { SwarmEvent::ConnectionEstablished { peer_id, endpoint, .. } if peer_id == self.alice_peer_id => {
tracing::info!(peer_id = %endpoint.get_remote_address(), "Connected to Alice"); tracing::info!(peer_id = %endpoint.get_remote_address(), "Connected to Alice");
} }
SwarmEvent::Dialing(peer_id) if peer_id == self.alice_peer_id => { SwarmEvent::Dialing { peer_id: Some(alice_peer_id), connection_id } if alice_peer_id == self.alice_peer_id => {
tracing::debug!(%peer_id, "Dialling Alice"); tracing::debug!(%alice_peer_id, %connection_id, "Dialing Alice");
} }
SwarmEvent::ConnectionClosed { peer_id, endpoint, num_established, cause: Some(error) } if peer_id == self.alice_peer_id && num_established == 0 => { SwarmEvent::ConnectionClosed { peer_id, endpoint, num_established, cause: Some(error), connection_id } if peer_id == self.alice_peer_id && num_established == 0 => {
tracing::warn!(peer_id = %endpoint.get_remote_address(), cause = %error, "Lost connection to Alice"); tracing::warn!(peer_id = %endpoint.get_remote_address(), cause = %error, %connection_id, "Lost connection to Alice");
if let Some(duration) = self.swarm.behaviour_mut().redial.until_next_redial() {
tracing::info!(seconds_until_next_redial = %duration.as_secs(), "Waiting for next redial attempt");
}
} }
SwarmEvent::ConnectionClosed { peer_id, num_established, cause: None, .. } if peer_id == self.alice_peer_id && num_established == 0 => { SwarmEvent::ConnectionClosed { peer_id, num_established, cause: None, .. } if peer_id == self.alice_peer_id && num_established == 0 => {
// no error means the disconnection was requested // no error means the disconnection was requested
tracing::info!("Successfully closed connection to Alice"); tracing::info!("Successfully closed connection to Alice");
return; return;
} }
SwarmEvent::OutgoingConnectionError { peer_id: Some(alice_peer_id), error } if alice_peer_id == self.alice_peer_id => { SwarmEvent::OutgoingConnectionError { peer_id: Some(alice_peer_id), error, connection_id } if alice_peer_id == self.alice_peer_id => {
tracing::warn!(%error, "Failed to dial Alice"); tracing::warn!(%alice_peer_id, %connection_id, %error, "Failed to connect to Alice");
if let Some(duration) = self.swarm.behaviour_mut().redial.until_next_redial() { if let Some(duration) = self.swarm.behaviour_mut().redial.until_next_redial() {
tracing::info!(seconds_until_next_redial = %duration.as_secs(), "Waiting for next redial attempt"); tracing::info!(seconds_until_next_redial = %duration.as_secs(), "Waiting for next redial attempt");
} }
}
SwarmEvent::Behaviour(OutEvent::OutboundRequestResponseFailure {peer, error, request_id, protocol}) => {
tracing::error!(
%peer,
%request_id,
%error,
%protocol,
"Failed to send request-response request to peer");
// If we fail to send a request-response request, we should notify the responder that the request failed
// We will remove the responder from the inflight requests and respond with an error
// Check for encrypted signature requests
if let Some(responder) = self.inflight_encrypted_signature_requests.remove(&request_id) {
let _ = responder.respond(Err(error));
continue;
}
// Check for quote requests
if let Some(responder) = self.inflight_quote_requests.remove(&request_id) {
let _ = responder.respond(Err(error));
continue;
}
// Check for cooperative xmr redeem requests
if let Some(responder) = self.inflight_cooperative_xmr_redeem_requests.remove(&request_id) {
let _ = responder.respond(Err(error));
continue;
}
}
SwarmEvent::Behaviour(OutEvent::InboundRequestResponseFailure {peer, error, request_id, protocol}) => {
tracing::error!(
%peer,
%request_id,
%error,
%protocol,
"Failed to receive request-response request from peer");
} }
_ => {} _ => {}
} }
}, },
// Handle to-be-sent requests for all our network protocols. // Handle to-be-sent outgoing requests for all our network protocols.
// Use `self.is_connected_to_alice` as a guard to "buffer" requests until we are connected. Some(((), responder)) = self.quote_requests.next().fuse() => {
Some(((), responder)) = self.quote_requests.next().fuse(), if self.is_connected_to_alice() => {
let id = self.swarm.behaviour_mut().quote.send_request(&self.alice_peer_id, ()); let id = self.swarm.behaviour_mut().quote.send_request(&self.alice_peer_id, ());
self.inflight_quote_requests.insert(id, responder); self.inflight_quote_requests.insert(id, responder);
}, },
Some((swap, responder)) = self.swap_setup_requests.next().fuse(), if self.is_connected_to_alice() => { Some((tx_redeem_encsig, responder)) = self.encrypted_signatures_requests.next().fuse() => {
self.swarm.behaviour_mut().swap_setup.start(self.alice_peer_id, swap).await;
self.inflight_swap_setup = Some(responder);
},
Some((tx_redeem_encsig, responder)) = self.encrypted_signatures.next().fuse(), if self.is_connected_to_alice() => {
let request = encrypted_signature::Request { let request = encrypted_signature::Request {
swap_id: self.swap_id, swap_id: self.swap_id,
tx_redeem_encsig tx_redeem_encsig
@ -244,19 +319,33 @@ impl EventLoop {
let id = self.swarm.behaviour_mut().encrypted_signature.send_request(&self.alice_peer_id, request); let id = self.swarm.behaviour_mut().encrypted_signature.send_request(&self.alice_peer_id, request);
self.inflight_encrypted_signature_requests.insert(id, responder); self.inflight_encrypted_signature_requests.insert(id, responder);
}, },
Some((_, responder)) = self.cooperative_xmr_redeem_requests.next().fuse() => {
Some(response_channel) = &mut self.pending_transfer_proof => {
let _ = self.swarm.behaviour_mut().transfer_proof.send_response(response_channel, ());
self.pending_transfer_proof = OptionFuture::from(None);
},
Some((swap_id, responder)) = self.cooperative_xmr_redeem_requests.next().fuse(), if self.is_connected_to_alice() => {
let id = self.swarm.behaviour_mut().cooperative_xmr_redeem.send_request(&self.alice_peer_id, Request { let id = self.swarm.behaviour_mut().cooperative_xmr_redeem.send_request(&self.alice_peer_id, Request {
swap_id swap_id: self.swap_id
}); });
self.inflight_cooperative_xmr_redeem_requests.insert(id, responder); self.inflight_cooperative_xmr_redeem_requests.insert(id, responder);
}, },
// We use `self.is_connected_to_alice` as a guard to "buffer" requests until we are connected.
// because the protocol does not dial Alice itself
// (unlike request-response above)
Some((swap, responder)) = self.execution_setup_requests.next().fuse(), if self.is_connected_to_alice() => {
self.swarm.behaviour_mut().swap_setup.start(self.alice_peer_id, swap).await;
self.inflight_swap_setup = Some(responder);
},
// Send an acknowledgement to Alice once the EventLoopHandle has processed a received transfer proof
// We use `self.is_connected_to_alice` as a guard to "buffer" requests until we are connected.
//
// Why do we do this here but not for the other request-response channels?
// This is the only request, we don't have a retry mechanism for. We lazily send this.
Some(response_channel) = &mut self.pending_transfer_proof, if self.is_connected_to_alice() => {
if self.swarm.behaviour_mut().transfer_proof.send_response(response_channel, ()).is_err() {
tracing::warn!("Failed to send acknowledgment to Alice that we have received the transfer proof");
} else {
self.pending_transfer_proof = OptionFuture::from(None);
}
},
} }
} }
} }
@ -268,24 +357,84 @@ impl EventLoop {
#[derive(Debug)] #[derive(Debug)]
pub struct EventLoopHandle { pub struct EventLoopHandle {
swap_setup: bmrng::RequestSender<NewSwap, Result<State2>>, /// When a NewSwap object is sent into this channel, the EventLoop will:
transfer_proof: bmrng::RequestReceiver<monero::TransferProof, ()>, /// 1. Trigger the swap setup protocol with Alice to negotiate the swap parameters
encrypted_signature: bmrng::RequestSender<EncryptedSignature, ()>, /// 2. Return the resulting State2 if successful
quote: bmrng::RequestSender<(), BidQuote>, /// 3. Return an anyhow error if the request fails
cooperative_xmr_redeem: bmrng::RequestSender<Uuid, Response>, execution_setup_sender: bmrng::RequestSender<NewSwap, Result<State2>>,
/// Receiver for incoming Monero transfer proofs from Alice.
/// When a proof is received, we process it and acknowledge receipt back to the EventLoop
/// The EventLoop will then send an acknowledgment back to Alice over the network
transfer_proof_receiver: bmrng::RequestReceiver<monero::TransferProof, ()>,
/// When an encrypted signature is sent into this channel, the EventLoop will:
/// 1. Send the encrypted signature to Alice over the network
/// 2. Return Ok(()) if Alice acknowledges receipt, or
/// 3. Return an OutboundFailure error if the request fails
encrypted_signature_sender:
bmrng::RequestSender<EncryptedSignature, Result<(), OutboundFailure>>,
/// When a () is sent into this channel, the EventLoop will:
/// 1. Request a price quote from Alice
/// 2. Return the quote if successful
/// 3. Return an OutboundFailure error if the request fails
quote_sender: bmrng::RequestSender<(), Result<BidQuote, OutboundFailure>>,
/// When a () is sent into this channel, the EventLoop will:
/// 1. Request Alice's cooperation in redeeming the Monero
/// 2. Return the a response object (Fullfilled or Rejected), if the network request is successful
/// The Fullfilled object contains the keys required to redeem the Monero
/// 3. Return an OutboundFailure error if the network request fails
cooperative_xmr_redeem_sender: bmrng::RequestSender<
(),
Result<cooperative_xmr_redeem_after_punish::Response, OutboundFailure>,
>,
} }
impl EventLoopHandle { impl EventLoopHandle {
fn create_retry_config(max_elapsed_time: Duration) -> backoff::ExponentialBackoff {
backoff::ExponentialBackoffBuilder::new()
.with_max_elapsed_time(max_elapsed_time.into())
.with_max_interval(Duration::from_secs(5))
.build()
}
pub async fn setup_swap(&mut self, swap: NewSwap) -> Result<State2> { pub async fn setup_swap(&mut self, swap: NewSwap) -> Result<State2> {
self.swap_setup.send_receive(swap).await? tracing::debug!(swap = ?swap, "Sending swap setup request");
let backoff = Self::create_retry_config(EXECUTION_SETUP_PROTOCOL_TIMEOUT);
backoff::future::retry(backoff, || async {
match self.execution_setup_sender.send_receive(swap.clone()).await {
Ok(Ok(state2)) => Ok(state2),
// These are errors thrown by the swap_setup/bob behaviour
Ok(Err(err)) => {
tracing::warn!(%err, "Failed to setup swap. Will retry");
Err(backoff::Error::transient(err))
}
// This will happen if we don't establish a connection to Alice within the timeout of the MPSC channel
// The protocol does not dial Alice it self
// This is handled by redial behaviour
Err(bmrng::error::RequestError::RecvTimeoutError) => {
Err(backoff::Error::permanent(anyhow!("We failed to setup the swap in the allotted time by the event loop channel")))
}
Err(_) => {
unreachable!("We never drop the receiver of the execution setup channel, so this should never happen")
}
}
})
.await
.context("Failed to setup swap after retries")
} }
pub async fn recv_transfer_proof(&mut self) -> Result<monero::TransferProof> { pub async fn recv_transfer_proof(&mut self) -> Result<monero::TransferProof> {
let (transfer_proof, responder) = self let (transfer_proof, responder) = self
.transfer_proof .transfer_proof_receiver
.recv() .recv()
.await .await
.context("Failed to receive transfer proof")?; .context("Failed to receive transfer proof")?;
responder responder
.respond(()) .respond(())
.context("Failed to acknowledge receipt of transfer proof")?; .context("Failed to acknowledge receipt of transfer proof")?;
@ -295,18 +444,71 @@ impl EventLoopHandle {
pub async fn request_quote(&mut self) -> Result<BidQuote> { pub async fn request_quote(&mut self) -> Result<BidQuote> {
tracing::debug!("Requesting quote"); tracing::debug!("Requesting quote");
Ok(self.quote.send_receive(()).await?)
let backoff = Self::create_retry_config(REQUEST_RESPONSE_PROTOCOL_TIMEOUT);
backoff::future::retry(backoff, || async {
match self.quote_sender.send_receive(()).await {
Ok(Ok(quote)) => Ok(quote),
Ok(Err(err)) => {
tracing::warn!(%err, "Failed to request quote due to network error. Will retry");
Err(backoff::Error::transient(err))
}
Err(_) => {
unreachable!("We initiate the quote channel without a timeout and store both the sender and receiver in the same struct, so this should never happen");
}
}
})
.await
.context("Failed to request quote after retries")
} }
pub async fn request_cooperative_xmr_redeem(&mut self, swap_id: Uuid) -> Result<Response> {
Ok(self.cooperative_xmr_redeem.send_receive(swap_id).await?) pub async fn request_cooperative_xmr_redeem(&mut self) -> Result<Response> {
tracing::debug!("Requesting cooperative XMR redeem");
let backoff = Self::create_retry_config(REQUEST_RESPONSE_PROTOCOL_TIMEOUT);
backoff::future::retry(backoff, || async {
match self.cooperative_xmr_redeem_sender.send_receive(()).await {
Ok(Ok(response)) => Ok(response),
Ok(Err(err)) => {
tracing::warn!(%err, "Failed to request cooperative XMR redeem due to network error. Will retry");
Err(backoff::Error::transient(err))
}
Err(_) => {
unreachable!("We initiate the cooperative xmr redeem channel without a timeout and store both the sender and receiver in the same struct, so this should never happen");
}
}
})
.await
.context("Failed to request cooperative XMR redeem after retries")
} }
pub async fn send_encrypted_signature( pub async fn send_encrypted_signature(
&mut self, &mut self,
tx_redeem_encsig: EncryptedSignature, tx_redeem_encsig: EncryptedSignature,
) -> Result<(), bmrng::error::RequestError<EncryptedSignature>> { ) -> Result<()> {
self.encrypted_signature tracing::debug!("Sending encrypted signature");
.send_receive(tx_redeem_encsig)
.await // We will retry indefinitely until we succeed
let backoff = backoff::ExponentialBackoffBuilder::new()
.with_max_elapsed_time(None)
.with_max_interval(REQUEST_RESPONSE_PROTOCOL_TIMEOUT)
.build();
backoff::future::retry(backoff, || async {
match self.encrypted_signature_sender.send_receive(tx_redeem_encsig.clone()).await {
Ok(Ok(_)) => Ok(()),
Ok(Err(err)) => {
tracing::warn!(%err, "Failed to send encrypted signature due to a network error. Will retry");
Err(backoff::Error::transient(err))
}
Err(_) => {
unreachable!("We initiate the encrypted signature channel without a timeout and store both the sender and receiver in the same struct, so this should never happen");
}
}
})
.await
.context("Failed to send encrypted signature after retries")
} }
} }

View file

@ -4,11 +4,10 @@ use crate::network::{quote, swarm};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use futures::StreamExt; use futures::StreamExt;
use libp2p::multiaddr::Protocol; use libp2p::multiaddr::Protocol;
use libp2p::ping::{Ping, PingConfig, PingEvent}; use libp2p::request_response;
use libp2p::request_response::{RequestResponseEvent, RequestResponseMessage};
use libp2p::swarm::dial_opts::DialOpts; use libp2p::swarm::dial_opts::DialOpts;
use libp2p::swarm::SwarmEvent; use libp2p::swarm::{NetworkBehaviour, SwarmEvent};
use libp2p::{identity, rendezvous, Multiaddr, PeerId, Swarm}; use libp2p::{identity, ping, rendezvous, Multiaddr, PeerId, Swarm};
use serde::Serialize; use serde::Serialize;
use serde_with::{serde_as, DisplayFromStr}; use serde_with::{serde_as, DisplayFromStr};
use std::collections::hash_map::Entry; use std::collections::hash_map::Entry;
@ -32,18 +31,11 @@ pub async fn list_sellers(
let behaviour = Behaviour { let behaviour = Behaviour {
rendezvous: rendezvous::client::Behaviour::new(identity.clone()), rendezvous: rendezvous::client::Behaviour::new(identity.clone()),
quote: quote::cli(), quote: quote::cli(),
ping: Ping::new( ping: ping::Behaviour::new(ping::Config::new().with_timeout(Duration::from_secs(60))),
PingConfig::new()
.with_keep_alive(false)
.with_interval(Duration::from_secs(86_400)),
),
}; };
let mut swarm = swarm::cli(identity, tor_socks5_port, behaviour).await?; let mut swarm = swarm::cli(identity, tor_socks5_port, behaviour).await?;
swarm swarm.add_peer_address(rendezvous_node_peer_id, rendezvous_node_addr.clone());
.behaviour_mut()
.quote
.add_address(&rendezvous_node_peer_id, rendezvous_node_addr.clone());
swarm swarm
.dial(DialOpts::from(rendezvous_node_peer_id)) .dial(DialOpts::from(rendezvous_node_peer_id))
@ -83,7 +75,7 @@ pub enum Status {
enum OutEvent { enum OutEvent {
Rendezvous(rendezvous::client::Event), Rendezvous(rendezvous::client::Event),
Quote(quote::OutEvent), Quote(quote::OutEvent),
Ping(PingEvent), Ping(ping::Event),
} }
impl From<rendezvous::client::Event> for OutEvent { impl From<rendezvous::client::Event> for OutEvent {
@ -98,13 +90,13 @@ impl From<quote::OutEvent> for OutEvent {
} }
} }
#[derive(libp2p::NetworkBehaviour)] #[derive(NetworkBehaviour)]
#[behaviour(event_process = false)] #[behaviour(event_process = false)]
#[behaviour(out_event = "OutEvent")] #[behaviour(out_event = "OutEvent")]
struct Behaviour { struct Behaviour {
rendezvous: rendezvous::client::Behaviour, rendezvous: rendezvous::client::Behaviour,
quote: quote::Behaviour, quote: quote::Behaviour,
ping: Ping, ping: ping::Behaviour,
} }
#[derive(Debug)] #[derive(Debug)]
@ -173,7 +165,7 @@ impl EventLoop {
self.reachable_asb_address.insert(peer_id, address.clone()); self.reachable_asb_address.insert(peer_id, address.clone());
} }
} }
SwarmEvent::OutgoingConnectionError { peer_id, error } => { SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => {
if let Some(peer_id) = peer_id { if let Some(peer_id) = peer_id {
if peer_id == self.rendezvous_peer_id { if peer_id == self.rendezvous_peer_id {
tracing::error!( tracing::error!(
@ -216,7 +208,7 @@ impl EventLoop {
for address in registration.record.addresses() { for address in registration.record.addresses() {
tracing::info!(peer_id=%peer, address=%address, "Discovered peer"); tracing::info!(peer_id=%peer, address=%address, "Discovered peer");
let p2p_suffix = Protocol::P2p(*peer.as_ref()); let p2p_suffix = Protocol::P2p(peer);
let _address_with_p2p = if !address let _address_with_p2p = if !address
.ends_with(&Multiaddr::empty().with(p2p_suffix.clone())) .ends_with(&Multiaddr::empty().with(p2p_suffix.clone()))
{ {
@ -228,7 +220,7 @@ impl EventLoop {
self.asb_quote_status.insert(peer, QuoteStatus::Pending); self.asb_quote_status.insert(peer, QuoteStatus::Pending);
// add all external addresses of that peer to the quote behaviour // add all external addresses of that peer to the quote behaviour
self.swarm.behaviour_mut().quote.add_address(&peer, address.clone()); self.swarm.add_peer_address(peer, address.clone());
} }
// request the quote, if we are not connected to the peer it will be dialed automatically // request the quote, if we are not connected to the peer it will be dialed automatically
@ -237,18 +229,18 @@ impl EventLoop {
} }
SwarmEvent::Behaviour(OutEvent::Quote(quote_response)) => { SwarmEvent::Behaviour(OutEvent::Quote(quote_response)) => {
match quote_response { match quote_response {
RequestResponseEvent::Message { peer, message } => { request_response::Event::Message { peer, message } => {
match message { match message {
RequestResponseMessage::Response { response, .. } => { request_response::Message::Response { response, .. } => {
if self.asb_quote_status.insert(peer, QuoteStatus::Received(Status::Online(response))).is_none() { if self.asb_quote_status.insert(peer, QuoteStatus::Received(Status::Online(response))).is_none() {
tracing::error!(%peer, "Received bid quote from unexpected peer, this record will be removed!"); tracing::error!(%peer, "Received bid quote from unexpected peer, this record will be removed!");
self.asb_quote_status.remove(&peer); self.asb_quote_status.remove(&peer);
} }
} }
RequestResponseMessage::Request { .. } => unreachable!() request_response::Message::Request { .. } => unreachable!()
} }
} }
RequestResponseEvent::OutboundFailure { peer, error, .. } => { request_response::Event::OutboundFailure { peer, error, .. } => {
if peer == self.rendezvous_peer_id { if peer == self.rendezvous_peer_id {
tracing::debug!(%peer, "Outbound failure when communicating with rendezvous node: {:#}", error); tracing::debug!(%peer, "Outbound failure when communicating with rendezvous node: {:#}", error);
} else { } else {
@ -256,7 +248,7 @@ impl EventLoop {
self.asb_quote_status.remove(&peer); self.asb_quote_status.remove(&peer);
} }
} }
RequestResponseEvent::InboundFailure { peer, error, .. } => { request_response::Event::InboundFailure { peer, error, .. } => {
if peer == self.rendezvous_peer_id { if peer == self.rendezvous_peer_id {
tracing::debug!(%peer, "Inbound failure when communicating with rendezvous node: {:#}", error); tracing::debug!(%peer, "Inbound failure when communicating with rendezvous node: {:#}", error);
} else { } else {
@ -264,7 +256,7 @@ impl EventLoop {
self.asb_quote_status.remove(&peer); self.asb_quote_status.remove(&peer);
} }
}, },
RequestResponseEvent::ResponseSent { .. } => unreachable!() request_response::Event::ResponseSent { .. } => unreachable!()
} }
} }
_ => {} _ => {}
@ -323,8 +315,8 @@ impl EventLoop {
#[derive(Debug)] #[derive(Debug)]
struct StillPending {} struct StillPending {}
impl From<PingEvent> for OutEvent { impl From<ping::Event> for OutEvent {
fn from(event: PingEvent) -> Self { fn from(event: ping::Event) -> Self {
OutEvent::Ping(event) OutEvent::Ping(event)
} }
} }

View file

@ -3,8 +3,8 @@ use crate::network::transport::authenticate_and_multiplex;
use anyhow::Result; use anyhow::Result;
use libp2p::core::muxing::StreamMuxerBox; use libp2p::core::muxing::StreamMuxerBox;
use libp2p::core::transport::{Boxed, OptionalTransport}; use libp2p::core::transport::{Boxed, OptionalTransport};
use libp2p::dns::TokioDnsConfig; use libp2p::dns;
use libp2p::tcp::TokioTcpConfig; use libp2p::tcp;
use libp2p::{identity, PeerId, Transport}; use libp2p::{identity, PeerId, Transport};
/// Creates the libp2p transport for the swap CLI. /// Creates the libp2p transport for the swap CLI.
@ -19,8 +19,9 @@ pub fn new(
identity: &identity::Keypair, identity: &identity::Keypair,
maybe_tor_socks5_port: Option<u16>, maybe_tor_socks5_port: Option<u16>,
) -> Result<Boxed<(PeerId, StreamMuxerBox)>> { ) -> Result<Boxed<(PeerId, StreamMuxerBox)>> {
let tcp = TokioTcpConfig::new().nodelay(true); let tcp = tcp::tokio::Transport::new(tcp::Config::new().nodelay(true));
let tcp_with_dns = TokioDnsConfig::system(tcp)?; let tcp_with_dns = dns::tokio::Transport::system(tcp)?;
let maybe_tor_transport = match maybe_tor_socks5_port { let maybe_tor_transport = match maybe_tor_socks5_port {
Some(port) => OptionalTransport::some(TorDialOnlyTransport::new(port)), Some(port) => OptionalTransport::some(TorDialOnlyTransport::new(port)),
None => OptionalTransport::none(), None => OptionalTransport::none(),

View file

@ -8,7 +8,7 @@ pub trait MultiAddrExt {
impl MultiAddrExt for Multiaddr { impl MultiAddrExt for Multiaddr {
fn extract_peer_id(&self) -> Option<PeerId> { fn extract_peer_id(&self) -> Option<PeerId> {
match self.iter().last()? { match self.iter().last()? {
Protocol::P2p(multihash) => PeerId::from_multihash(multihash).ok(), Protocol::P2p(peer_id) => Some(peer_id),
_ => None, _ => None,
} }
} }

View file

@ -1,9 +1,7 @@
mod impl_from_rr_event; mod impl_from_rr_event;
pub mod cbor_request_response;
pub mod cooperative_xmr_redeem_after_punish; pub mod cooperative_xmr_redeem_after_punish;
pub mod encrypted_signature; pub mod encrypted_signature;
pub mod json_pull_codec;
pub mod quote; pub mod quote;
pub mod redial; pub mod redial;
pub mod rendezvous; pub mod rendezvous;

View file

@ -1,98 +0,0 @@
use async_trait::async_trait;
use futures::prelude::*;
use libp2p::core::upgrade;
use libp2p::request_response::{ProtocolName, RequestResponseCodec};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::fmt::Debug;
use std::io;
use std::marker::PhantomData;
/// Message receive buffer.
pub const BUF_SIZE: usize = 1024 * 1024;
#[derive(Clone, Copy, Debug)]
pub struct CborCodec<P, Req, Res> {
phantom: PhantomData<(P, Req, Res)>,
}
impl<P, Req, Res> Default for CborCodec<P, Req, Res> {
fn default() -> Self {
Self {
phantom: PhantomData,
}
}
}
#[async_trait]
impl<P, Req, Res> RequestResponseCodec for CborCodec<P, Req, Res>
where
P: ProtocolName + Send + Sync + Clone,
Req: DeserializeOwned + Serialize + Send,
Res: DeserializeOwned + Serialize + Send,
{
type Protocol = P;
type Request = Req;
type Response = Res;
async fn read_request<T>(&mut self, _: &Self::Protocol, io: &mut T) -> io::Result<Self::Request>
where
T: AsyncRead + Unpin + Send,
{
let message = upgrade::read_length_prefixed(io, BUF_SIZE).await?;
let mut de = serde_cbor::Deserializer::from_slice(&message);
let msg = Req::deserialize(&mut de)
.map_err(|error| io::Error::new(io::ErrorKind::Other, error))?;
Ok(msg)
}
async fn read_response<T>(
&mut self,
_: &Self::Protocol,
io: &mut T,
) -> io::Result<Self::Response>
where
T: AsyncRead + Unpin + Send,
{
let message = upgrade::read_length_prefixed(io, BUF_SIZE).await?;
let mut de = serde_cbor::Deserializer::from_slice(&message);
let msg = Res::deserialize(&mut de)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
Ok(msg)
}
async fn write_request<T>(
&mut self,
_: &Self::Protocol,
io: &mut T,
req: Self::Request,
) -> io::Result<()>
where
T: AsyncWrite + Unpin + Send,
{
let bytes =
serde_cbor::to_vec(&req).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
upgrade::write_length_prefixed(io, &bytes).await?;
Ok(())
}
async fn write_response<T>(
&mut self,
_: &Self::Protocol,
io: &mut T,
res: Self::Response,
) -> io::Result<()>
where
T: AsyncWrite + Unpin + Send,
{
let bytes = serde_cbor::to_vec(&res)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
upgrade::write_length_prefixed(io, &bytes).await?;
Ok(())
}
}

View file

@ -1,27 +1,23 @@
use crate::monero::Scalar; use crate::monero::Scalar;
use crate::network::cbor_request_response::CborCodec;
use crate::{asb, cli}; use crate::{asb, cli};
use libp2p::core::ProtocolName; use libp2p::request_response::ProtocolSupport;
use libp2p::request_response::{ use libp2p::{request_response, PeerId, StreamProtocol};
ProtocolSupport, RequestResponse, RequestResponseConfig, RequestResponseEvent,
RequestResponseMessage,
};
use libp2p::PeerId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::Duration;
use uuid::Uuid; use uuid::Uuid;
const PROTOCOL: &str = "/comit/xmr/btc/cooperative_xmr_redeem_after_punish/1.0.0"; const PROTOCOL: &str = "/comit/xmr/btc/cooperative_xmr_redeem_after_punish/1.0.0";
type OutEvent = RequestResponseEvent<Request, Response>; type OutEvent = request_response::Event<Request, Response>;
type Message = RequestResponseMessage<Request, Response>; type Message = request_response::Message<Request, Response>;
pub type Behaviour = RequestResponse<CborCodec<CooperativeXmrRedeemProtocol, Request, Response>>; pub type Behaviour = request_response::cbor::Behaviour<Request, Response>;
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
pub struct CooperativeXmrRedeemProtocol; pub struct CooperativeXmrRedeemProtocol;
impl ProtocolName for CooperativeXmrRedeemProtocol { impl AsRef<str> for CooperativeXmrRedeemProtocol {
fn protocol_name(&self) -> &[u8] { fn as_ref(&self) -> &str {
PROTOCOL.as_bytes() PROTOCOL
} }
} }
@ -51,19 +47,24 @@ pub enum Response {
reason: CooperativeXmrRedeemRejectReason, reason: CooperativeXmrRedeemRejectReason,
}, },
} }
pub fn alice() -> Behaviour { pub fn alice() -> Behaviour {
Behaviour::new( Behaviour::new(
CborCodec::default(), vec![(
vec![(CooperativeXmrRedeemProtocol, ProtocolSupport::Inbound)], StreamProtocol::new(CooperativeXmrRedeemProtocol.as_ref()),
RequestResponseConfig::default(), ProtocolSupport::Inbound,
)],
request_response::Config::default().with_request_timeout(Duration::from_secs(60)),
) )
} }
pub fn bob() -> Behaviour { pub fn bob() -> Behaviour {
Behaviour::new( Behaviour::new(
CborCodec::default(), vec![(
vec![(CooperativeXmrRedeemProtocol, ProtocolSupport::Outbound)], StreamProtocol::new(CooperativeXmrRedeemProtocol.as_ref()),
RequestResponseConfig::default(), ProtocolSupport::Outbound,
)],
request_response::Config::default().with_request_timeout(Duration::from_secs(60)),
) )
} }

View file

@ -1,26 +1,22 @@
use crate::network::cbor_request_response::CborCodec;
use crate::{asb, cli}; use crate::{asb, cli};
use libp2p::core::ProtocolName; use libp2p::request_response::{self};
use libp2p::request_response::{ use libp2p::{PeerId, StreamProtocol};
ProtocolSupport, RequestResponse, RequestResponseConfig, RequestResponseEvent,
RequestResponseMessage,
};
use libp2p::PeerId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::Duration;
use uuid::Uuid; use uuid::Uuid;
const PROTOCOL: &str = "/comit/xmr/btc/encrypted_signature/1.0.0"; const PROTOCOL: &str = "/comit/xmr/btc/encrypted_signature/1.0.0";
type OutEvent = RequestResponseEvent<Request, ()>; type OutEvent = request_response::Event<Request, ()>;
type Message = RequestResponseMessage<Request, ()>; type Message = request_response::Message<Request, ()>;
pub type Behaviour = RequestResponse<CborCodec<EncryptedSignatureProtocol, Request, ()>>; pub type Behaviour = request_response::cbor::Behaviour<Request, ()>;
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
pub struct EncryptedSignatureProtocol; pub struct EncryptedSignatureProtocol;
impl ProtocolName for EncryptedSignatureProtocol { impl AsRef<str> for EncryptedSignatureProtocol {
fn protocol_name(&self) -> &[u8] { fn as_ref(&self) -> &str {
PROTOCOL.as_bytes() PROTOCOL
} }
} }
@ -32,17 +28,21 @@ pub struct Request {
pub fn alice() -> Behaviour { pub fn alice() -> Behaviour {
Behaviour::new( Behaviour::new(
CborCodec::default(), vec![(
vec![(EncryptedSignatureProtocol, ProtocolSupport::Inbound)], StreamProtocol::new(EncryptedSignatureProtocol.as_ref()),
RequestResponseConfig::default(), request_response::ProtocolSupport::Inbound,
)],
request_response::Config::default().with_request_timeout(Duration::from_secs(60)),
) )
} }
pub fn bob() -> Behaviour { pub fn bob() -> Behaviour {
Behaviour::new( Behaviour::new(
CborCodec::default(), vec![(
vec![(EncryptedSignatureProtocol, ProtocolSupport::Outbound)], StreamProtocol::new(EncryptedSignatureProtocol.as_ref()),
RequestResponseConfig::default(), request_response::ProtocolSupport::Outbound,
)],
request_response::Config::default().with_request_timeout(Duration::from_secs(60)),
) )
} }

View file

@ -1,4 +1,4 @@
/// Helper macro to map a [`RequestResponseEvent`] to our [`OutEvent`]. /// Helper macro to map a [`request_response::Event`] to our [`OutEvent`].
/// ///
/// This is primarily a macro and not a regular function because we use it for /// This is primarily a macro and not a regular function because we use it for
/// Alice and Bob and they have different [`OutEvent`]s that just happen to /// Alice and Bob and they have different [`OutEvent`]s that just happen to
@ -8,59 +8,33 @@ macro_rules! impl_from_rr_event {
($protocol_event:ty, $behaviour_out_event:ty, $protocol:ident) => { ($protocol_event:ty, $behaviour_out_event:ty, $protocol:ident) => {
impl From<$protocol_event> for $behaviour_out_event { impl From<$protocol_event> for $behaviour_out_event {
fn from(event: $protocol_event) -> Self { fn from(event: $protocol_event) -> Self {
use ::libp2p::request_response::RequestResponseEvent::*; use ::libp2p::request_response::Event::*;
use anyhow::anyhow;
match event { match event {
Message { message, peer, .. } => Self::from((peer, message)), Message { message, peer, .. } => Self::from((peer, message)),
ResponseSent { .. } => Self::Other, ResponseSent { .. } => Self::Other,
InboundFailure { peer, error, .. } => { InboundFailure {
use libp2p::request_response::InboundFailure::*; peer,
error,
match error { request_id,
Timeout => { } => Self::InboundRequestResponseFailure {
Self::Failure { peer,
error: anyhow!("{} failed because of an inbound timeout", $protocol), error,
peer, request_id,
} protocol: $protocol.to_string(),
} },
ConnectionClosed => { OutboundFailure {
Self::Failure { peer,
error: anyhow!("{} failed because the connection was closed before a response could be sent", $protocol), error,
peer, request_id,
} } => Self::OutboundRequestResponseFailure {
} peer,
UnsupportedProtocols => Self::Other, // TODO: Report this and disconnected / ban the peer? error,
ResponseOmission => Self::Other request_id,
} protocol: $protocol.to_string(),
} },
OutboundFailure { peer, error, .. } => {
use libp2p::request_response::OutboundFailure::*;
match error {
Timeout => {
Self::Failure {
error: anyhow!("{} failed because we did not receive a response within the configured timeout", $protocol),
peer,
}
}
ConnectionClosed => {
Self::Failure {
error: anyhow!("{} failed because the connection was closed we received a response", $protocol),
peer,
}
}
UnsupportedProtocols => Self::Other, // TODO: Report this and disconnected / ban the peer?
DialFailure => {
Self::Failure {
error: anyhow!("{} failed because we failed to dial", $protocol),
peer,
}
}
}
}
} }
} }
} }
} };
} }

View file

@ -1,95 +0,0 @@
use async_trait::async_trait;
use futures::prelude::*;
use libp2p::core::upgrade;
use libp2p::request_response::{ProtocolName, RequestResponseCodec};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::fmt::Debug;
use std::io;
use std::marker::PhantomData;
/// Message receive buffer.
pub const BUF_SIZE: usize = 1024 * 1024;
/// A [`RequestResponseCodec`] for pull-based protocols where the response is
/// encoded using JSON.
///
/// A pull-based protocol is a protocol where the dialer doesn't send any
/// message and expects the listener to directly send the response as the
/// substream is opened.
#[derive(Clone, Copy, Debug)]
pub struct JsonPullCodec<P, Res> {
phantom: PhantomData<(P, Res)>,
}
impl<P, Res> Default for JsonPullCodec<P, Res> {
fn default() -> Self {
Self {
phantom: PhantomData,
}
}
}
#[async_trait]
impl<P, Res> RequestResponseCodec for JsonPullCodec<P, Res>
where
P: ProtocolName + Send + Sync + Clone,
Res: DeserializeOwned + Serialize + Send,
{
type Protocol = P;
type Request = ();
type Response = Res;
async fn read_request<T>(&mut self, _: &Self::Protocol, _: &mut T) -> io::Result<Self::Request>
where
T: AsyncRead + Unpin + Send,
{
Ok(())
}
async fn read_response<T>(
&mut self,
_: &Self::Protocol,
io: &mut T,
) -> io::Result<Self::Response>
where
T: AsyncRead + Unpin + Send,
{
let message = upgrade::read_length_prefixed(io, BUF_SIZE)
.await
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let mut de = serde_json::Deserializer::from_slice(&message);
let msg = Res::deserialize(&mut de)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
Ok(msg)
}
async fn write_request<T>(
&mut self,
_: &Self::Protocol,
_: &mut T,
_: Self::Request,
) -> io::Result<()>
where
T: AsyncWrite + Unpin + Send,
{
Ok(())
}
async fn write_response<T>(
&mut self,
_: &Self::Protocol,
io: &mut T,
res: Self::Response,
) -> io::Result<()>
where
T: AsyncWrite + Unpin + Send,
{
let bytes = serde_json::to_vec(&res)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
upgrade::write_length_prefixed(io, &bytes).await?;
Ok(())
}
}

View file

@ -1,26 +1,23 @@
use crate::network::json_pull_codec::JsonPullCodec; use std::time::Duration;
use crate::{asb, bitcoin, cli}; use crate::{asb, bitcoin, cli};
use libp2p::core::ProtocolName; use libp2p::request_response::{self, ProtocolSupport};
use libp2p::request_response::{ use libp2p::{PeerId, StreamProtocol};
ProtocolSupport, RequestResponse, RequestResponseConfig, RequestResponseEvent,
RequestResponseMessage,
};
use libp2p::PeerId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use typeshare::typeshare; use typeshare::typeshare;
const PROTOCOL: &str = "/comit/xmr/btc/bid-quote/1.0.0"; const PROTOCOL: &str = "/comit/xmr/btc/bid-quote/1.0.0";
pub type OutEvent = RequestResponseEvent<(), BidQuote>; pub type OutEvent = request_response::Event<(), BidQuote>;
pub type Message = RequestResponseMessage<(), BidQuote>; pub type Message = request_response::Message<(), BidQuote>;
pub type Behaviour = RequestResponse<JsonPullCodec<BidQuoteProtocol, BidQuote>>; pub type Behaviour = request_response::json::Behaviour<(), BidQuote>;
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
pub struct BidQuoteProtocol; pub struct BidQuoteProtocol;
impl ProtocolName for BidQuoteProtocol { impl AsRef<str> for BidQuoteProtocol {
fn protocol_name(&self) -> &[u8] { fn as_ref(&self) -> &str {
PROTOCOL.as_bytes() PROTOCOL
} }
} }
@ -53,9 +50,8 @@ pub struct ZeroQuoteReceived;
/// handing out quotes. /// handing out quotes.
pub fn asb() -> Behaviour { pub fn asb() -> Behaviour {
Behaviour::new( Behaviour::new(
JsonPullCodec::default(), vec![(StreamProtocol::new(PROTOCOL), ProtocolSupport::Inbound)],
vec![(BidQuoteProtocol, ProtocolSupport::Inbound)], request_response::Config::default().with_request_timeout(Duration::from_secs(60)),
RequestResponseConfig::default(),
) )
} }
@ -65,9 +61,8 @@ pub fn asb() -> Behaviour {
/// requesting quotes. /// requesting quotes.
pub fn cli() -> Behaviour { pub fn cli() -> Behaviour {
Behaviour::new( Behaviour::new(
JsonPullCodec::default(), vec![(StreamProtocol::new(PROTOCOL), ProtocolSupport::Outbound)],
vec![(BidQuoteProtocol, ProtocolSupport::Outbound)], request_response::Config::default().with_request_timeout(Duration::from_secs(60)),
RequestResponseConfig::default(),
) )
} }

View file

@ -1,12 +1,9 @@
use crate::cli;
use backoff::backoff::Backoff; use backoff::backoff::Backoff;
use backoff::ExponentialBackoff; use backoff::ExponentialBackoff;
use futures::future::FutureExt; use futures::future::FutureExt;
use libp2p::core::connection::ConnectionId;
use libp2p::core::Multiaddr; use libp2p::core::Multiaddr;
use libp2p::swarm::dial_opts::{DialOpts, PeerCondition}; use libp2p::swarm::dial_opts::{DialOpts, PeerCondition};
use libp2p::swarm::protocols_handler::DummyProtocolsHandler; use libp2p::swarm::{NetworkBehaviour, ToSwarm};
use libp2p::swarm::{NetworkBehaviour, NetworkBehaviourAction, PollParameters};
use libp2p::PeerId; use libp2p::PeerId;
use std::pin::Pin; use std::pin::Pin;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
@ -14,9 +11,7 @@ use std::time::Duration;
use tokio::time::{Instant, Sleep}; use tokio::time::{Instant, Sleep};
use void::Void; use void::Void;
pub enum OutEvent { use crate::cli;
AllAttemptsExhausted { peer: PeerId },
}
/// A [`NetworkBehaviour`] that tracks whether we are connected to the given /// A [`NetworkBehaviour`] that tracks whether we are connected to the given
/// peer and attempts to re-establish a connection with an exponential backoff /// peer and attempts to re-establish a connection with an exponential backoff
@ -31,15 +26,15 @@ pub struct Behaviour {
} }
impl Behaviour { impl Behaviour {
pub fn new(peer: PeerId, interval: Duration) -> Self { pub fn new(peer: PeerId, interval: Duration, max_interval: Duration) -> Self {
Self { Self {
peer, peer,
sleep: None, sleep: None,
backoff: ExponentialBackoff { backoff: ExponentialBackoff {
initial_interval: interval, initial_interval: interval,
current_interval: interval, current_interval: interval,
// give up dialling after 5 minutes max_interval,
max_elapsed_time: Some(Duration::from_secs(5 * 60)), max_elapsed_time: None, // We never give up on re-dialling
..ExponentialBackoff::default() ..ExponentialBackoff::default()
}, },
} }
@ -57,44 +52,57 @@ impl Behaviour {
} }
impl NetworkBehaviour for Behaviour { impl NetworkBehaviour for Behaviour {
type ProtocolsHandler = DummyProtocolsHandler; type ConnectionHandler = libp2p::swarm::dummy::ConnectionHandler;
type OutEvent = OutEvent; type ToSwarm = ();
fn new_handler(&mut self) -> Self::ProtocolsHandler { fn handle_established_inbound_connection(
DummyProtocolsHandler::default()
}
fn addresses_of_peer(&mut self, _: &PeerId) -> Vec<Multiaddr> {
Vec::new()
}
fn inject_connected(&mut self, peer_id: &PeerId) {
if peer_id != &self.peer {
return;
}
// established a connection to the desired peer, cancel any active re-dialling
self.sleep = None;
}
fn inject_disconnected(&mut self, peer_id: &PeerId) {
if peer_id != &self.peer {
return;
}
// lost connection to the configured peer, trigger re-dialling with an
// exponential backoff
self.backoff.reset();
self.sleep = Some(Box::pin(tokio::time::sleep(self.backoff.initial_interval)));
}
fn inject_event(&mut self, _: PeerId, _: ConnectionId, _: Void) {}
fn poll(
&mut self, &mut self,
cx: &mut Context<'_>, _connection_id: libp2p::swarm::ConnectionId,
_: &mut impl PollParameters, peer: PeerId,
) -> Poll<NetworkBehaviourAction<Self::OutEvent, Self::ProtocolsHandler>> { _local_addr: &Multiaddr,
_remote_addr: &Multiaddr,
) -> Result<libp2p::swarm::THandler<Self>, libp2p::swarm::ConnectionDenied> {
// We establish an inbound connection to the peer we are interested in.
// We stop re-dialling.
// Reset the backoff state to start with the initial interval again once we disconnect again
if peer == self.peer {
self.backoff.reset();
self.sleep = None;
}
Ok(Self::ConnectionHandler {})
}
fn handle_established_outbound_connection(
&mut self,
_connection_id: libp2p::swarm::ConnectionId,
peer: PeerId,
_addr: &Multiaddr,
_role_override: libp2p::core::Endpoint,
) -> Result<libp2p::swarm::THandler<Self>, libp2p::swarm::ConnectionDenied> {
// We establish an outbound connection to the peer we are interested in.
// We stop re-dialling.
// Reset the backoff state to start with the initial interval again once we disconnect again
if peer == self.peer {
self.backoff.reset();
self.sleep = None;
}
Ok(Self::ConnectionHandler {})
}
fn on_swarm_event(&mut self, event: libp2p::swarm::FromSwarm<'_>) {
let redial = match event {
libp2p::swarm::FromSwarm::ConnectionClosed(e) if e.peer_id == self.peer => true,
libp2p::swarm::FromSwarm::DialFailure(e) if e.peer_id == Some(self.peer) => true,
_ => false,
};
if redial && self.sleep.is_none() {
self.sleep = Some(Box::pin(tokio::time::sleep(self.backoff.initial_interval)));
tracing::info!(seconds_until_next_redial = %self.until_next_redial().unwrap().as_secs(), "Waiting for next redial attempt");
}
}
fn poll(&mut self, cx: &mut Context<'_>) -> std::task::Poll<ToSwarm<Self::ToSwarm, Void>> {
let sleep = match self.sleep.as_mut() { let sleep = match self.sleep.as_mut() {
None => return Poll::Pending, // early exit if we shouldn't be re-dialling None => return Poll::Pending, // early exit if we shouldn't be re-dialling
Some(future) => future, Some(future) => future,
@ -105,29 +113,31 @@ impl NetworkBehaviour for Behaviour {
let next_dial_in = match self.backoff.next_backoff() { let next_dial_in = match self.backoff.next_backoff() {
Some(next_dial_in) => next_dial_in, Some(next_dial_in) => next_dial_in,
None => { None => {
return Poll::Ready(NetworkBehaviourAction::GenerateEvent( unreachable!("The backoff should never run out of attempts");
OutEvent::AllAttemptsExhausted { peer: self.peer },
));
} }
}; };
self.sleep = Some(Box::pin(tokio::time::sleep(next_dial_in))); self.sleep = Some(Box::pin(tokio::time::sleep(next_dial_in)));
Poll::Ready(NetworkBehaviourAction::Dial { Poll::Ready(ToSwarm::Dial {
opts: DialOpts::peer_id(self.peer) opts: DialOpts::peer_id(self.peer)
.condition(PeerCondition::Disconnected) .condition(PeerCondition::Disconnected)
.build(), .build(),
handler: Self::ProtocolsHandler::default(),
}) })
} }
fn on_connection_handler_event(
&mut self,
_peer_id: PeerId,
_connection_id: libp2p::swarm::ConnectionId,
_event: libp2p::swarm::THandlerOutEvent<Self>,
) {
unreachable!("The re-dial dummy connection handler does not produce any events");
}
} }
impl From<OutEvent> for cli::OutEvent { impl From<()> for cli::OutEvent {
fn from(event: OutEvent) -> Self { fn from(_: ()) -> Self {
match event { Self::Other
OutEvent::AllAttemptsExhausted { peer } => {
cli::OutEvent::AllRedialAttemptsExhausted { peer }
}
}
} }
} }

View file

@ -1,39 +1,36 @@
use crate::monero; use crate::monero;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use libp2p::core::upgrade; use asynchronous_codec::{Bytes, Framed};
use libp2p::swarm::NegotiatedSubstream; use futures::{SinkExt, StreamExt};
use libp2p::swarm::Stream;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub mod alice; pub mod alice;
pub mod bob; pub mod bob;
mod vendor_from_fn;
pub const BUF_SIZE: usize = 1024 * 1024; pub const BUF_SIZE: usize = 1024 * 1024;
pub mod protocol { pub mod protocol {
use futures::future; use futures::future;
use libp2p::core::upgrade::{from_fn, FromFnUpgrade};
use libp2p::core::Endpoint; use libp2p::core::Endpoint;
use libp2p::swarm::NegotiatedSubstream; use libp2p::swarm::Stream;
use void::Void; use void::Void;
use super::vendor_from_fn::{from_fn, FromFnUpgrade};
pub fn new() -> SwapSetup { pub fn new() -> SwapSetup {
from_fn( from_fn(
b"/comit/xmr/btc/swap_setup/1.0.0", "/comit/xmr/btc/swap_setup/1.0.0",
Box::new(|socket, _| future::ready(Ok(socket))), Box::new(|socket, _| future::ready(Ok(socket))),
) )
} }
pub type SwapSetup = FromFnUpgrade< pub type SwapSetup = FromFnUpgrade<
&'static [u8], &'static str,
Box< Box<dyn Fn(Stream, Endpoint) -> future::Ready<Result<Stream, Void>> + Send + 'static>,
dyn Fn(
NegotiatedSubstream,
Endpoint,
) -> future::Ready<Result<NegotiatedSubstream, Void>>
+ Send
+ 'static,
>,
>; >;
} }
@ -86,13 +83,23 @@ pub enum SpotPriceError {
Other, Other,
} }
pub async fn read_cbor_message<T>(substream: &mut NegotiatedSubstream) -> Result<T> fn codec() -> unsigned_varint::codec::UviBytes<Bytes> {
let mut codec = unsigned_varint::codec::UviBytes::<Bytes>::default();
codec.set_max_len(BUF_SIZE);
codec
}
pub async fn read_cbor_message<T>(stream: &mut Stream) -> Result<T>
where where
T: DeserializeOwned, T: DeserializeOwned,
{ {
let bytes = upgrade::read_length_prefixed(substream, BUF_SIZE) let mut frame = Framed::new(stream, codec());
let bytes = frame
.next()
.await .await
.context("Failed to read length-prefixed message from stream")?; .context("Failed to read length-prefixed message from stream")??;
let mut de = serde_cbor::Deserializer::from_slice(&bytes); let mut de = serde_cbor::Deserializer::from_slice(&bytes);
let message = let message =
T::deserialize(&mut de).context("Failed to deserialize bytes into message using CBOR")?; T::deserialize(&mut de).context("Failed to deserialize bytes into message using CBOR")?;
@ -100,13 +107,17 @@ where
Ok(message) Ok(message)
} }
pub async fn write_cbor_message<T>(substream: &mut NegotiatedSubstream, message: T) -> Result<()> pub async fn write_cbor_message<T>(stream: &mut Stream, message: T) -> Result<()>
where where
T: Serialize, T: Serialize,
{ {
let bytes = let bytes =
serde_cbor::to_vec(&message).context("Failed to serialize message as bytes using CBOR")?; serde_cbor::to_vec(&message).context("Failed to serialize message as bytes using CBOR")?;
upgrade::write_length_prefixed(substream, &bytes)
let mut frame = Framed::new(stream, codec());
frame
.send(Bytes::from(bytes))
.await .await
.context("Failed to write bytes as length-prefixed message")?; .context("Failed to write bytes as length-prefixed message")?;

View file

@ -9,20 +9,18 @@ use crate::protocol::{Message0, Message2, Message4};
use crate::{asb, bitcoin, env, monero}; use crate::{asb, bitcoin, env, monero};
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use futures::future::{BoxFuture, OptionFuture}; use futures::future::{BoxFuture, OptionFuture};
use futures::{AsyncWriteExt, FutureExt}; use futures::AsyncWriteExt;
use libp2p::core::connection::ConnectionId; use futures::FutureExt;
use libp2p::core::upgrade; use libp2p::core::upgrade;
use libp2p::swarm::{ use libp2p::swarm::handler::ConnectionEvent;
KeepAlive, NegotiatedSubstream, NetworkBehaviour, NetworkBehaviourAction, PollParameters, use libp2p::swarm::{ConnectionHandler, ConnectionId};
ProtocolsHandler, ProtocolsHandlerEvent, ProtocolsHandlerUpgrErr, SubstreamProtocol, use libp2p::swarm::{ConnectionHandlerEvent, NetworkBehaviour, SubstreamProtocol, ToSwarm};
};
use libp2p::{Multiaddr, PeerId}; use libp2p::{Multiaddr, PeerId};
use std::collections::VecDeque; use std::collections::VecDeque;
use std::fmt::Debug; use std::fmt::Debug;
use std::task::Poll; use std::task::Poll;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use uuid::Uuid; use uuid::Uuid;
use void::Void;
#[derive(Debug)] #[derive(Debug)]
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
@ -147,28 +145,59 @@ impl<LR> NetworkBehaviour for Behaviour<LR>
where where
LR: LatestRate + Send + 'static + Clone, LR: LatestRate + Send + 'static + Clone,
{ {
type ProtocolsHandler = Handler<LR>; type ConnectionHandler = Handler<LR>;
type OutEvent = OutEvent; type ToSwarm = OutEvent;
fn new_handler(&mut self) -> Self::ProtocolsHandler { fn handle_established_inbound_connection(
Handler::new( &mut self,
_connection_id: libp2p::swarm::ConnectionId,
_peer: PeerId,
_local_addr: &Multiaddr,
_remote_addr: &Multiaddr,
) -> std::result::Result<libp2p::swarm::THandler<Self>, libp2p::swarm::ConnectionDenied> {
// A new inbound connection has been established by Bob
// He wants to negotiate a swap setup with us
// We create a new Handler to handle the negotiation
let handler = Handler::new(
self.min_buy, self.min_buy,
self.max_buy, self.max_buy,
self.env_config, self.env_config,
self.latest_rate.clone(), self.latest_rate.clone(),
self.resume_only, self.resume_only,
) );
Ok(handler)
} }
fn addresses_of_peer(&mut self, _: &PeerId) -> Vec<Multiaddr> { fn handle_established_outbound_connection(
Vec::new() &mut self,
_connection_id: libp2p::swarm::ConnectionId,
_peer: PeerId,
_addr: &Multiaddr,
_role_override: libp2p::core::Endpoint,
) -> std::result::Result<libp2p::swarm::THandler<Self>, libp2p::swarm::ConnectionDenied> {
// A new outbound connection has been established (probably to a rendezvous node because we dont dial Bob)
// We still return a handler, because we dont want to close the connection
let handler = Handler::new(
self.min_buy,
self.max_buy,
self.env_config,
self.latest_rate.clone(),
self.resume_only,
);
Ok(handler)
} }
fn inject_connected(&mut self, _: &PeerId) {} fn on_connection_handler_event(
&mut self,
fn inject_disconnected(&mut self, _: &PeerId) {} peer_id: PeerId,
_: ConnectionId,
fn inject_event(&mut self, peer_id: PeerId, _: ConnectionId, event: HandlerOutEvent) { event: HandlerOutEvent,
) {
// Here we receive events from the Handler, add some context and forward them to the swarm
// This is done by pushing the event to the [`events`] queue
// The queue is then polled in the [`poll`] function, and the events are sent to the swarm
match event { match event {
HandlerOutEvent::Initiated(send_wallet_snapshot) => { HandlerOutEvent::Initiated(send_wallet_snapshot) => {
self.events.push_back(OutEvent::Initiated { self.events.push_back(OutEvent::Initiated {
@ -188,17 +217,18 @@ where
} }
} }
fn poll( fn poll(&mut self, _cx: &mut std::task::Context<'_>) -> Poll<ToSwarm<Self::ToSwarm, ()>> {
&mut self, // Poll events from the queue and send them to the swarm
_cx: &mut std::task::Context<'_>,
_params: &mut impl PollParameters,
) -> Poll<NetworkBehaviourAction<Self::OutEvent, Self::ProtocolsHandler>> {
if let Some(event) = self.events.pop_front() { if let Some(event) = self.events.pop_front() {
return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event)); return Poll::Ready(ToSwarm::GenerateEvent(event));
} }
Poll::Pending Poll::Pending
} }
fn on_swarm_event(&mut self, _event: libp2p::swarm::FromSwarm<'_>) {
// We do not need to handle any swarm events here
}
} }
type InboundStream = BoxFuture<'static, Result<(Uuid, State3)>>; type InboundStream = BoxFuture<'static, Result<(Uuid, State3)>>;
@ -214,8 +244,12 @@ pub struct Handler<LR> {
latest_rate: LR, latest_rate: LR,
resume_only: bool, resume_only: bool,
timeout: Duration, // This is the timeout for the negotiation phase where Alice and Bob exchange messages
keep_alive: KeepAlive, negotiation_timeout: Duration,
// If set to None, we will keep the connection alive indefinitely
// If set to Some, we will keep the connection alive until the given instant
keep_alive_until: Option<Instant>,
} }
impl<LR> Handler<LR> { impl<LR> Handler<LR> {
@ -234,8 +268,8 @@ impl<LR> Handler<LR> {
env_config, env_config,
latest_rate, latest_rate,
resume_only, resume_only,
timeout: Duration::from_secs(120), negotiation_timeout: Duration::from_secs(120),
keep_alive: KeepAlive::Until(Instant::now() + Duration::from_secs(10)), keep_alive_until: Some(Instant::now() + Duration::from_secs(30)),
} }
} }
} }
@ -247,13 +281,12 @@ pub enum HandlerOutEvent {
Completed(Result<(Uuid, State3)>), Completed(Result<(Uuid, State3)>),
} }
impl<LR> ProtocolsHandler for Handler<LR> impl<LR> ConnectionHandler for Handler<LR>
where where
LR: LatestRate + Send + 'static, LR: LatestRate + Send + 'static,
{ {
type InEvent = (); type FromBehaviour = ();
type OutEvent = HandlerOutEvent; type ToBehaviour = HandlerOutEvent;
type Error = Error;
type InboundProtocol = protocol::SwapSetup; type InboundProtocol = protocol::SwapSetup;
type OutboundProtocol = upgrade::DeniedUpgrade; type OutboundProtocol = upgrade::DeniedUpgrade;
type InboundOpenInfo = (); type InboundOpenInfo = ();
@ -263,178 +296,194 @@ where
SubstreamProtocol::new(protocol::new(), ()) SubstreamProtocol::new(protocol::new(), ())
} }
fn inject_fully_negotiated_inbound( fn on_connection_event(
&mut self, &mut self,
mut substream: NegotiatedSubstream, event: libp2p::swarm::handler::ConnectionEvent<
_: Self::InboundOpenInfo, '_,
Self::InboundProtocol,
Self::OutboundProtocol,
Self::InboundOpenInfo,
Self::OutboundOpenInfo,
>,
) { ) {
self.keep_alive = KeepAlive::Yes; match event {
ConnectionEvent::FullyNegotiatedInbound(substream) => {
self.keep_alive_until = None;
let (sender, receiver) = bmrng::channel_with_timeout::<bitcoin::Amount, WalletSnapshot>( let mut substream = substream.protocol;
1,
Duration::from_secs(5),
);
let resume_only = self.resume_only;
let min_buy = self.min_buy;
let max_buy = self.max_buy;
let latest_rate = self.latest_rate.latest_rate();
let env_config = self.env_config;
let protocol = tokio::time::timeout(self.timeout, async move { let (sender, receiver) = bmrng::channel_with_timeout::<
let request = swap_setup::read_cbor_message::<SpotPriceRequest>(&mut substream) bitcoin::Amount,
.await WalletSnapshot,
.context("Failed to read spot price request")?; >(1, Duration::from_secs(60));
let wallet_snapshot = sender let resume_only = self.resume_only;
.send_receive(request.btc) let min_buy = self.min_buy;
.await let max_buy = self.max_buy;
.context("Failed to receive wallet snapshot")?; let latest_rate = self.latest_rate.latest_rate();
let env_config = self.env_config;
// wrap all of these into another future so we can `return` from all the // We wrap the entire handshake in a timeout future
// different blocks let protocol = tokio::time::timeout(self.negotiation_timeout, async move {
let validate = async { let request = swap_setup::read_cbor_message::<SpotPriceRequest>(&mut substream)
if resume_only { .await
return Err(Error::ResumeOnlyMode); .context("Failed to read spot price request")?;
};
let blockchain_network = BlockchainNetwork { let wallet_snapshot = sender
bitcoin: env_config.bitcoin_network, .send_receive(request.btc)
monero: env_config.monero_network, .await
}; .context("Failed to receive wallet snapshot")?;
if request.blockchain_network != blockchain_network { // wrap all of these into another future so we can `return` from all the
return Err(Error::BlockchainNetworkMismatch { // different blocks
cli: request.blockchain_network, let validate = async {
asb: blockchain_network, if resume_only {
}); return Err(Error::ResumeOnlyMode);
} };
let btc = request.btc; let blockchain_network = BlockchainNetwork {
bitcoin: env_config.bitcoin_network,
monero: env_config.monero_network,
};
if btc < min_buy { if request.blockchain_network != blockchain_network {
return Err(Error::AmountBelowMinimum { return Err(Error::BlockchainNetworkMismatch {
min: min_buy, cli: request.blockchain_network,
buy: btc, asb: blockchain_network,
}); });
} }
if btc > max_buy { let btc = request.btc;
return Err(Error::AmountAboveMaximum {
max: max_buy,
buy: btc,
});
}
let rate = latest_rate.map_err(|e| Error::LatestRateFetchFailed(Box::new(e)))?; if btc < min_buy {
let xmr = rate return Err(Error::AmountBelowMinimum {
.sell_quote(btc) min: min_buy,
.map_err(Error::SellQuoteCalculationFailed)?; buy: btc,
});
}
let unlocked = Amount::from_piconero(wallet_snapshot.balance.unlocked_balance); if btc > max_buy {
if unlocked < xmr + wallet_snapshot.lock_fee { return Err(Error::AmountAboveMaximum {
return Err(Error::BalanceTooLow { max: max_buy,
balance: wallet_snapshot.balance, buy: btc,
buy: btc, });
}); }
}
Ok(xmr) let rate =
}; latest_rate.map_err(|e| Error::LatestRateFetchFailed(Box::new(e)))?;
let xmr = rate
.sell_quote(btc)
.map_err(Error::SellQuoteCalculationFailed)?;
let result = validate.await; let unlocked =
Amount::from_piconero(wallet_snapshot.balance.unlocked_balance);
swap_setup::write_cbor_message( if unlocked < xmr + wallet_snapshot.lock_fee {
&mut substream, return Err(Error::BalanceTooLow {
SpotPriceResponse::from_result_ref(&result), balance: wallet_snapshot.balance,
) buy: btc,
.await });
.context("Failed to write spot price response")?; }
let xmr = result?; Ok(xmr)
};
let state0 = State0::new( let result = validate.await;
request.btc,
xmr,
env_config,
wallet_snapshot.redeem_address,
wallet_snapshot.punish_address,
wallet_snapshot.redeem_fee,
wallet_snapshot.punish_fee,
&mut rand::thread_rng(),
);
let message0 = swap_setup::read_cbor_message::<Message0>(&mut substream) swap_setup::write_cbor_message(
.await &mut substream,
.context("Failed to read message0")?; SpotPriceResponse::from_result_ref(&result),
let (swap_id, state1) = state0 )
.receive(message0) .await
.context("Failed to transition state0 -> state1 using message0")?; .context("Failed to write spot price response")?;
swap_setup::write_cbor_message(&mut substream, state1.next_message()) let xmr = result?;
.await
.context("Failed to send message1")?;
let message2 = swap_setup::read_cbor_message::<Message2>(&mut substream) let state0 = State0::new(
.await request.btc,
.context("Failed to read message2")?; xmr,
let state2 = state1 env_config,
.receive(message2) wallet_snapshot.redeem_address,
.context("Failed to transition state1 -> state2 using message2")?; wallet_snapshot.punish_address,
wallet_snapshot.redeem_fee,
wallet_snapshot.punish_fee,
&mut rand::thread_rng(),
);
swap_setup::write_cbor_message(&mut substream, state2.next_message()) let message0 = swap_setup::read_cbor_message::<Message0>(&mut substream)
.await .await
.context("Failed to send message3")?; .context("Failed to read message0")?;
let (swap_id, state1) = state0
.receive(message0)
.context("Failed to transition state0 -> state1 using message0")?;
let message4 = swap_setup::read_cbor_message::<Message4>(&mut substream) swap_setup::write_cbor_message(&mut substream, state1.next_message())
.await .await
.context("Failed to read message4")?; .context("Failed to send message1")?;
let state3 = state2
.receive(message4)
.context("Failed to transition state2 -> state3 using message4")?;
substream let message2 = swap_setup::read_cbor_message::<Message2>(&mut substream)
.flush() .await
.await .context("Failed to read message2")?;
.context("Failed to flush substream after all messages were sent")?; let state2 = state1
substream .receive(message2)
.close() .context("Failed to transition state1 -> state2 using message2")?;
.await
.context("Failed to close substream after all messages were sent")?;
Ok((swap_id, state3)) swap_setup::write_cbor_message(&mut substream, state2.next_message())
}); .await
.context("Failed to send message3")?;
let max_seconds = self.timeout.as_secs(); let message4 = swap_setup::read_cbor_message::<Message4>(&mut substream)
self.inbound_stream = OptionFuture::from(Some( .await
async move { .context("Failed to read message4")?;
protocol.await.with_context(|| { let state3 = state2
format!("Failed to complete execution setup within {}s", max_seconds) .receive(message4)
})? .context("Failed to transition state2 -> state3 using message4")?;
substream
.flush()
.await
.context("Failed to flush substream after all messages were sent")?;
substream
.close()
.await
.context("Failed to close substream after all messages were sent")?;
Ok((swap_id, state3))
});
let max_seconds = self.negotiation_timeout.as_secs();
self.inbound_stream = OptionFuture::from(Some(
async move {
protocol.await.with_context(|| {
format!("Failed to complete execution setup within {}s", max_seconds)
})?
}
.boxed(),
));
self.events.push_back(HandlerOutEvent::Initiated(receiver));
} }
.boxed(), ConnectionEvent::DialUpgradeError(..) => {
)); unreachable!("Alice does not dial")
}
self.events.push_back(HandlerOutEvent::Initiated(receiver)); ConnectionEvent::FullyNegotiatedOutbound(..) => {
unreachable!("Alice does not support outbound connections")
}
_ => {}
}
} }
fn inject_fully_negotiated_outbound(&mut self, _: Void, _: Self::OutboundOpenInfo) { fn on_behaviour_event(&mut self, _event: Self::FromBehaviour) {
unreachable!("Alice does not support outbound in the handler")
}
fn inject_event(&mut self, _: Self::InEvent) {
unreachable!("Alice does not receive events from the Behaviour in the handler") unreachable!("Alice does not receive events from the Behaviour in the handler")
} }
fn inject_dial_upgrade_error( fn connection_keep_alive(&self) -> bool {
&mut self, // If keep_alive_until is None, we keep the connection alive indefinitely
_: Self::OutboundOpenInfo, // If keep_alive_until is Some, we keep the connection alive until the given instant
_: ProtocolsHandlerUpgrErr<Void>, match self.keep_alive_until {
) { None => true,
unreachable!("Alice does not dial") Some(keep_alive_until) => Instant::now() < keep_alive_until,
} }
fn connection_keep_alive(&self) -> KeepAlive {
self.keep_alive
} }
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
@ -442,22 +491,21 @@ where
&mut self, &mut self,
cx: &mut std::task::Context<'_>, cx: &mut std::task::Context<'_>,
) -> Poll< ) -> Poll<
ProtocolsHandlerEvent< ConnectionHandlerEvent<Self::OutboundProtocol, Self::OutboundOpenInfo, Self::ToBehaviour>,
Self::OutboundProtocol,
Self::OutboundOpenInfo,
Self::OutEvent,
Self::Error,
>,
> { > {
// Send events in the queue to the behaviour
// This is currently only used to notify the behaviour that the negotiation phase has been initiated
if let Some(event) = self.events.pop_front() { if let Some(event) = self.events.pop_front() {
return Poll::Ready(ProtocolsHandlerEvent::Custom(event)); return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(event));
} }
if let Some(result) = futures::ready!(self.inbound_stream.poll_unpin(cx)) { if let Some(result) = futures::ready!(self.inbound_stream.poll_unpin(cx)) {
self.inbound_stream = OptionFuture::from(None); self.inbound_stream = OptionFuture::from(None);
return Poll::Ready(ProtocolsHandlerEvent::Custom(HandlerOutEvent::Completed(
result, // Notify the behaviour that the negotiation phase has been completed
))); return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(
HandlerOutEvent::Completed(result),
));
} }
Poll::Pending Poll::Pending

View file

@ -1,27 +1,24 @@
use crate::network::swap_setup::{ use crate::network::swap_setup::{protocol, BlockchainNetwork, SpotPriceError, SpotPriceResponse};
protocol, read_cbor_message, write_cbor_message, BlockchainNetwork, SpotPriceError,
SpotPriceRequest, SpotPriceResponse,
};
use crate::protocol::bob::{State0, State2}; use crate::protocol::bob::{State0, State2};
use crate::protocol::{Message1, Message3}; use crate::protocol::{Message1, Message3};
use crate::{bitcoin, cli, env, monero}; use crate::{bitcoin, cli, env, monero};
use anyhow::Result; use anyhow::{Context, Result};
use futures::future::{BoxFuture, OptionFuture}; use futures::future::{BoxFuture, OptionFuture};
use futures::{AsyncWriteExt, FutureExt}; use futures::AsyncWriteExt;
use libp2p::core::connection::ConnectionId; use futures::FutureExt;
use libp2p::core::upgrade; use libp2p::core::upgrade;
use libp2p::swarm::{ use libp2p::swarm::{
KeepAlive, NegotiatedSubstream, NetworkBehaviour, NetworkBehaviourAction, NotifyHandler, ConnectionDenied, ConnectionHandler, ConnectionHandlerEvent, ConnectionId, FromSwarm,
PollParameters, ProtocolsHandler, ProtocolsHandlerEvent, ProtocolsHandlerUpgrErr, NetworkBehaviour, SubstreamProtocol, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm,
SubstreamProtocol,
}; };
use libp2p::{Multiaddr, PeerId}; use libp2p::{Multiaddr, PeerId};
use std::collections::VecDeque; use std::collections::VecDeque;
use std::sync::Arc; use std::sync::Arc;
use std::task::{Context, Poll}; use std::task::Poll;
use std::time::Duration; use std::time::Duration;
use uuid::Uuid; use uuid::Uuid;
use void::Void;
use super::{read_cbor_message, write_cbor_message, SpotPriceRequest};
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct Behaviour { pub struct Behaviour {
@ -53,38 +50,56 @@ impl From<Completed> for cli::OutEvent {
} }
impl NetworkBehaviour for Behaviour { impl NetworkBehaviour for Behaviour {
type ProtocolsHandler = Handler; type ConnectionHandler = Handler;
type OutEvent = Completed; type ToSwarm = Completed;
fn new_handler(&mut self) -> Self::ProtocolsHandler { fn handle_established_inbound_connection(
Handler::new(self.env_config, self.bitcoin_wallet.clone()) &mut self,
_connection_id: ConnectionId,
_peer: PeerId,
_local_addr: &Multiaddr,
_remote_addr: &Multiaddr,
) -> Result<THandler<Self>, ConnectionDenied> {
Ok(Handler::new(self.env_config, self.bitcoin_wallet.clone()))
} }
fn addresses_of_peer(&mut self, _: &PeerId) -> Vec<Multiaddr> { fn handle_established_outbound_connection(
Vec::new() &mut self,
_connection_id: ConnectionId,
_peer: PeerId,
_addr: &Multiaddr,
_role_override: libp2p::core::Endpoint,
) -> Result<THandler<Self>, ConnectionDenied> {
Ok(Handler::new(self.env_config, self.bitcoin_wallet.clone()))
} }
fn inject_connected(&mut self, _: &PeerId) {} fn on_swarm_event(&mut self, _event: FromSwarm<'_>) {
// We do not need to handle swarm events
}
fn inject_disconnected(&mut self, _: &PeerId) {} fn on_connection_handler_event(
&mut self,
fn inject_event(&mut self, peer: PeerId, _: ConnectionId, completed: Completed) { peer_id: PeerId,
self.completed_swaps.push_back((peer, completed)); _connection_id: libp2p::swarm::ConnectionId,
event: THandlerOutEvent<Self>,
) {
self.completed_swaps.push_back((peer_id, event));
} }
fn poll( fn poll(
&mut self, &mut self,
_cx: &mut Context<'_>, _cx: &mut std::task::Context<'_>,
_params: &mut impl PollParameters, ) -> Poll<ToSwarm<Self::ToSwarm, THandlerInEvent<Self>>> {
) -> Poll<NetworkBehaviourAction<Self::OutEvent, Self::ProtocolsHandler>> { // Forward completed swaps from the connection handler to the swarm
if let Some((_, event)) = self.completed_swaps.pop_front() { if let Some((_peer, completed)) = self.completed_swaps.pop_front() {
return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event)); return Poll::Ready(ToSwarm::GenerateEvent(completed));
} }
// If there is a new swap to be started, send it to the connection handler
if let Some((peer, event)) = self.new_swaps.pop_front() { if let Some((peer, event)) = self.new_swaps.pop_front() {
return Poll::Ready(NetworkBehaviourAction::NotifyHandler { return Poll::Ready(ToSwarm::NotifyHandler {
peer_id: peer, peer_id: peer,
handler: NotifyHandler::Any, handler: libp2p::swarm::NotifyHandler::Any,
event, event,
}); });
} }
@ -93,7 +108,7 @@ impl NetworkBehaviour for Behaviour {
} }
} }
type OutboundStream = BoxFuture<'static, Result<State2>>; type OutboundStream = BoxFuture<'static, Result<State2, Error>>;
pub struct Handler { pub struct Handler {
outbound_stream: OptionFuture<OutboundStream>, outbound_stream: OptionFuture<OutboundStream>,
@ -101,7 +116,7 @@ pub struct Handler {
timeout: Duration, timeout: Duration,
new_swaps: VecDeque<NewSwap>, new_swaps: VecDeque<NewSwap>,
bitcoin_wallet: Arc<bitcoin::Wallet>, bitcoin_wallet: Arc<bitcoin::Wallet>,
keep_alive: KeepAlive, keep_alive: bool,
} }
impl Handler { impl Handler {
@ -112,12 +127,12 @@ impl Handler {
timeout: Duration::from_secs(120), timeout: Duration::from_secs(120),
new_swaps: VecDeque::default(), new_swaps: VecDeque::default(),
bitcoin_wallet, bitcoin_wallet,
keep_alive: KeepAlive::Yes, keep_alive: true,
} }
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct NewSwap { pub struct NewSwap {
pub swap_id: Uuid, pub swap_id: Uuid,
pub btc: bitcoin::Amount, pub btc: bitcoin::Amount,
@ -129,123 +144,173 @@ pub struct NewSwap {
#[derive(Debug)] #[derive(Debug)]
pub struct Completed(Result<State2>); pub struct Completed(Result<State2>);
impl ProtocolsHandler for Handler { impl ConnectionHandler for Handler {
type InEvent = NewSwap; type FromBehaviour = NewSwap;
type OutEvent = Completed; type ToBehaviour = Completed;
type Error = Void;
type InboundProtocol = upgrade::DeniedUpgrade; type InboundProtocol = upgrade::DeniedUpgrade;
type OutboundProtocol = protocol::SwapSetup; type OutboundProtocol = protocol::SwapSetup;
type InboundOpenInfo = (); type InboundOpenInfo = ();
type OutboundOpenInfo = NewSwap; type OutboundOpenInfo = NewSwap;
fn listen_protocol(&self) -> SubstreamProtocol<Self::InboundProtocol, Self::InboundOpenInfo> { fn listen_protocol(&self) -> SubstreamProtocol<Self::InboundProtocol, Self::InboundOpenInfo> {
// Bob does not support inbound substreams
SubstreamProtocol::new(upgrade::DeniedUpgrade, ()) SubstreamProtocol::new(upgrade::DeniedUpgrade, ())
} }
fn inject_fully_negotiated_inbound(&mut self, _: Void, _: Self::InboundOpenInfo) { fn on_connection_event(
unreachable!("Bob does not support inbound substreams")
}
fn inject_fully_negotiated_outbound(
&mut self, &mut self,
mut substream: NegotiatedSubstream, event: libp2p::swarm::handler::ConnectionEvent<
info: Self::OutboundOpenInfo, '_,
Self::InboundProtocol,
Self::OutboundProtocol,
Self::InboundOpenInfo,
Self::OutboundOpenInfo,
>,
) { ) {
let bitcoin_wallet = self.bitcoin_wallet.clone(); match event {
let env_config = self.env_config; libp2p::swarm::handler::ConnectionEvent::FullyNegotiatedInbound(_) => {
unreachable!("Bob does not support inbound substreams")
let protocol = tokio::time::timeout(self.timeout, async move {
write_cbor_message(
&mut substream,
SpotPriceRequest {
btc: info.btc,
blockchain_network: BlockchainNetwork {
bitcoin: env_config.bitcoin_network,
monero: env_config.monero_network,
},
},
)
.await?;
let xmr = Result::from(read_cbor_message::<SpotPriceResponse>(&mut substream).await?)?;
let state0 = State0::new(
info.swap_id,
&mut rand::thread_rng(),
info.btc,
xmr,
env_config.bitcoin_cancel_timelock,
env_config.bitcoin_punish_timelock,
info.bitcoin_refund_address,
env_config.monero_finality_confirmations,
info.tx_refund_fee,
info.tx_cancel_fee,
);
write_cbor_message(&mut substream, state0.next_message()).await?;
let message1 = read_cbor_message::<Message1>(&mut substream).await?;
let state1 = state0.receive(bitcoin_wallet.as_ref(), message1).await?;
write_cbor_message(&mut substream, state1.next_message()).await?;
let message3 = read_cbor_message::<Message3>(&mut substream).await?;
let state2 = state1.receive(message3)?;
write_cbor_message(&mut substream, state2.next_message()).await?;
substream.flush().await?;
substream.close().await?;
Ok(state2)
});
let max_seconds = self.timeout.as_secs();
self.outbound_stream = OptionFuture::from(Some(
async move {
protocol.await.map_err(|_| Error::Timeout {
seconds: max_seconds,
})?
} }
.boxed(), libp2p::swarm::handler::ConnectionEvent::FullyNegotiatedOutbound(outbound) => {
)); let mut substream = outbound.protocol;
let new_swap_request = outbound.info;
let bitcoin_wallet = self.bitcoin_wallet.clone();
let env_config = self.env_config;
let protocol = tokio::time::timeout(self.timeout, async move {
let result = async {
// Here we request the spot price from Alice
write_cbor_message(
&mut substream,
SpotPriceRequest {
btc: new_swap_request.btc,
blockchain_network: BlockchainNetwork {
bitcoin: env_config.bitcoin_network,
monero: env_config.monero_network,
},
},
)
.await
.context("Failed to send spot price request to Alice")?;
// Here we read the spot price response from Alice
// The outer ? checks if Alice responded with an error (SpotPriceError)
let xmr = Result::from(
// The inner ? is for the read_cbor_message function
// It will return an error if the deserialization fails
read_cbor_message::<SpotPriceResponse>(&mut substream)
.await
.context("Failed to read spot price response from Alice")?,
)?;
let state0 = State0::new(
new_swap_request.swap_id,
&mut rand::thread_rng(),
new_swap_request.btc,
xmr,
env_config.bitcoin_cancel_timelock,
env_config.bitcoin_punish_timelock,
new_swap_request.bitcoin_refund_address,
env_config.monero_finality_confirmations,
new_swap_request.tx_refund_fee,
new_swap_request.tx_cancel_fee,
);
write_cbor_message(&mut substream, state0.next_message())
.await
.context("Failed to send state0 message to Alice")?;
let message1 = read_cbor_message::<Message1>(&mut substream)
.await
.context("Failed to read message1 from Alice")?;
let state1 = state0
.receive(bitcoin_wallet.as_ref(), message1)
.await
.context("Failed to receive state1")?;
write_cbor_message(&mut substream, state1.next_message())
.await
.context("Failed to send state1 message")?;
let message3 = read_cbor_message::<Message3>(&mut substream)
.await
.context("Failed to read message3 from Alice")?;
let state2 = state1
.receive(message3)
.context("Failed to receive state2")?;
write_cbor_message(&mut substream, state2.next_message())
.await
.context("Failed to send state2 message")?;
substream
.flush()
.await
.context("Failed to flush substream")?;
substream
.close()
.await
.context("Failed to close substream")?;
Ok(state2)
}
.await;
result.map_err(|e: anyhow::Error| {
tracing::error!("Error occurred during swap setup protocol: {:#}", e);
Error::Other
})
});
let max_seconds = self.timeout.as_secs();
self.outbound_stream = OptionFuture::from(Some(Box::pin(async move {
protocol.await.map_err(|_| Error::Timeout {
seconds: max_seconds,
})?
})
as OutboundStream));
// Once the outbound stream is created, we keep the connection alive
self.keep_alive = true;
}
_ => {}
}
} }
fn inject_event(&mut self, new_swap: Self::InEvent) { fn on_behaviour_event(&mut self, new_swap: Self::FromBehaviour) {
self.new_swaps.push_back(new_swap); self.new_swaps.push_back(new_swap);
} }
fn inject_dial_upgrade_error( fn connection_keep_alive(&self) -> bool {
&mut self,
_: Self::OutboundOpenInfo,
_: ProtocolsHandlerUpgrErr<Void>,
) {
}
fn connection_keep_alive(&self) -> KeepAlive {
self.keep_alive self.keep_alive
} }
#[allow(clippy::type_complexity)]
fn poll( fn poll(
&mut self, &mut self,
cx: &mut Context<'_>, cx: &mut std::task::Context<'_>,
) -> Poll< ) -> Poll<
ProtocolsHandlerEvent< ConnectionHandlerEvent<Self::OutboundProtocol, Self::OutboundOpenInfo, Self::ToBehaviour>,
Self::OutboundProtocol,
Self::OutboundOpenInfo,
Self::OutEvent,
Self::Error,
>,
> { > {
// Check if there is a new swap to be started
if let Some(new_swap) = self.new_swaps.pop_front() { if let Some(new_swap) = self.new_swaps.pop_front() {
self.keep_alive = KeepAlive::Yes; self.keep_alive = true;
return Poll::Ready(ProtocolsHandlerEvent::OutboundSubstreamRequest {
// We instruct the swarm to start a new outbound substream
return Poll::Ready(ConnectionHandlerEvent::OutboundSubstreamRequest {
protocol: SubstreamProtocol::new(protocol::new(), new_swap), protocol: SubstreamProtocol::new(protocol::new(), new_swap),
}); });
} }
if let Some(result) = futures::ready!(self.outbound_stream.poll_unpin(cx)) { // Check if the outbound stream has completed
self.outbound_stream = OptionFuture::from(None); if let Poll::Ready(Some(result)) = self.outbound_stream.poll_unpin(cx) {
return Poll::Ready(ProtocolsHandlerEvent::Custom(Completed(result))); self.outbound_stream = None.into();
// Once the outbound stream is completed, we no longer keep the connection alive
self.keep_alive = false;
// We notify the swarm that the swap setup is completed / failed
return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(Completed(
result.map_err(anyhow::Error::from),
)));
} }
Poll::Pending Poll::Pending

View file

@ -0,0 +1,117 @@
// Copyright 2020 Parity Technologies (UK) Ltd.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
// These functions have been vendored from https://github.com/libp2p/rust-libp2p/blob/v0.51.0/core/src/upgrade/from_fn.rs because they were removed from the library itself
// This is recommended, see: https://github.com/libp2p/rust-libp2p/pull/3747
// We replaced ProtocolName with AsRef<str>. See: https://github.com/libp2p/rust-libp2p/pull/3746/files
use futures::prelude::*;
use libp2p::{
core::{Endpoint, UpgradeInfo},
InboundUpgrade, OutboundUpgrade,
};
use std::iter;
/// Initializes a new [`FromFnUpgrade`].
///
/// # Example
///
/// ```
/// # use libp2p_core::transport::{Transport, MemoryTransport, memory::Channel};
/// # use libp2p_core::{upgrade, Negotiated};
/// # use std::io;
/// # use futures::AsyncWriteExt;
/// let _transport = MemoryTransport::default()
/// .and_then(move |out, cp| {
/// upgrade::apply(out, upgrade::from_fn("/foo/1", move |mut sock: Negotiated<Channel<Vec<u8>>>, endpoint| async move {
/// if endpoint.is_dialer() {
/// upgrade::write_length_prefixed(&mut sock, "some handshake data").await?;
/// sock.close().await?;
/// } else {
/// let handshake_data = upgrade::read_length_prefixed(&mut sock, 1024).await?;
/// if handshake_data != b"some handshake data" {
/// return Err(io::Error::new(io::ErrorKind::Other, "bad handshake"));
/// }
/// }
/// Ok(sock)
/// }), cp, upgrade::Version::V1)
/// });
/// ```
///
pub fn from_fn<P, F, C, Fut, Out, Err>(protocol_name: P, fun: F) -> FromFnUpgrade<P, F>
where
// Note: these bounds are there in order to help the compiler infer types
P: AsRef<str> + Clone,
F: FnOnce(C, Endpoint) -> Fut,
Fut: Future<Output = Result<Out, Err>>,
{
FromFnUpgrade { protocol_name, fun }
}
/// Implements the `UpgradeInfo`, `InboundUpgrade` and `OutboundUpgrade` traits.
///
/// The upgrade consists in calling the function passed when creating this struct.
#[derive(Debug, Clone)]
pub struct FromFnUpgrade<P, F> {
protocol_name: P,
fun: F,
}
impl<P, F> UpgradeInfo for FromFnUpgrade<P, F>
where
P: AsRef<str> + Clone,
{
type Info = P;
type InfoIter = iter::Once<P>;
fn protocol_info(&self) -> Self::InfoIter {
iter::once(self.protocol_name.clone())
}
}
impl<C, P, F, Fut, Err, Out> InboundUpgrade<C> for FromFnUpgrade<P, F>
where
P: AsRef<str> + Clone,
F: FnOnce(C, Endpoint) -> Fut,
Fut: Future<Output = Result<Out, Err>>,
{
type Output = Out;
type Error = Err;
type Future = Fut;
fn upgrade_inbound(self, sock: C, _: Self::Info) -> Self::Future {
(self.fun)(sock, Endpoint::Listener)
}
}
impl<C, P, F, Fut, Err, Out> OutboundUpgrade<C> for FromFnUpgrade<P, F>
where
P: AsRef<str> + Clone,
F: FnOnce(C, Endpoint) -> Fut,
Fut: Future<Output = Result<Out, Err>>,
{
type Output = Out;
type Error = Err;
type Future = Fut;
fn upgrade_outbound(self, sock: C, _: Self::Info) -> Self::Future {
(self.fun)(sock, Endpoint::Dialer)
}
}

View file

@ -4,9 +4,11 @@ use crate::network::rendezvous::XmrBtcNamespace;
use crate::seed::Seed; use crate::seed::Seed;
use crate::{asb, bitcoin, cli, env, tor}; use crate::{asb, bitcoin, cli, env, tor};
use anyhow::Result; use anyhow::Result;
use libp2p::swarm::{NetworkBehaviour, SwarmBuilder}; use libp2p::swarm::NetworkBehaviour;
use libp2p::SwarmBuilder;
use libp2p::{identity, Multiaddr, Swarm}; use libp2p::{identity, Multiaddr, Swarm};
use std::fmt::Debug; use std::fmt::Debug;
use std::time::Duration;
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn asb<LR>( pub fn asb<LR>(
@ -46,12 +48,12 @@ where
); );
let transport = asb::transport::new(&identity)?; let transport = asb::transport::new(&identity)?;
let peer_id = identity.public().into();
let swarm = SwarmBuilder::new(transport, behaviour, peer_id) let swarm = SwarmBuilder::with_existing_identity(identity)
.executor(Box::new(|f| { .with_tokio()
tokio::spawn(f); .with_other_transport(|_| transport)?
})) .with_behaviour(|_| behaviour)?
.with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::MAX))
.build(); .build();
Ok(swarm) Ok(swarm)
@ -71,12 +73,12 @@ where
}; };
let transport = cli::transport::new(&identity, maybe_tor_socks5_port)?; let transport = cli::transport::new(&identity, maybe_tor_socks5_port)?;
let peer_id = identity.public().into();
let swarm = SwarmBuilder::new(transport, behaviour, peer_id) let swarm = SwarmBuilder::with_existing_identity(identity)
.executor(Box::new(|f| { .with_tokio()
tokio::spawn(f); .with_other_transport(|_| transport)?
})) .with_behaviour(|_| behaviour)?
.with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::MAX))
.build(); .build();
Ok(swarm) Ok(swarm)

View file

@ -1,59 +1,45 @@
use async_trait::async_trait; use async_trait::async_trait;
use futures::stream::FusedStream; use futures::StreamExt;
use futures::{future, Future, StreamExt};
use libp2p::core::muxing::StreamMuxerBox; use libp2p::core::muxing::StreamMuxerBox;
use libp2p::core::transport::upgrade::Version; use libp2p::core::transport::upgrade::Version;
use libp2p::core::transport::MemoryTransport; use libp2p::core::transport::MemoryTransport;
use libp2p::core::upgrade::SelectUpgrade; use libp2p::core::{Multiaddr, Transport};
use libp2p::core::{identity, Executor, Multiaddr, PeerId, Transport}; use libp2p::identity;
use libp2p::mplex::MplexConfig; use libp2p::noise;
use libp2p::noise::{Keypair, NoiseConfig, X25519Spec}; use libp2p::swarm::dial_opts::DialOpts;
use libp2p::swarm::{AddressScore, NetworkBehaviour, Swarm, SwarmBuilder, SwarmEvent}; use libp2p::swarm::{NetworkBehaviour, Swarm, SwarmEvent};
use libp2p::tcp::TokioTcpConfig; use libp2p::tcp;
use libp2p::yamux::YamuxConfig; use libp2p::yamux;
use libp2p::SwarmBuilder;
use std::fmt::Debug; use std::fmt::Debug;
use std::pin::Pin;
use std::time::Duration; use std::time::Duration;
/// An adaptor struct for libp2p that spawns futures into the current
/// thread-local runtime.
struct GlobalSpawnTokioExecutor;
impl Executor for GlobalSpawnTokioExecutor {
fn exec(&self, future: Pin<Box<dyn Future<Output = ()> + Send>>) {
tokio::spawn(future);
}
}
pub fn new_swarm<B, F>(behaviour_fn: F) -> Swarm<B> pub fn new_swarm<B, F>(behaviour_fn: F) -> Swarm<B>
where where
B: NetworkBehaviour, B: NetworkBehaviour,
<B as NetworkBehaviour>::OutEvent: Debug, <B as NetworkBehaviour>::ToSwarm: Debug,
B: NetworkBehaviour, B: NetworkBehaviour,
F: FnOnce(PeerId, identity::Keypair) -> B, F: FnOnce(identity::Keypair) -> B,
{ {
let identity = identity::Keypair::generate_ed25519(); let identity = identity::Keypair::generate_ed25519();
let peer_id = PeerId::from(identity.public()); let noise = noise::Config::new(&identity).unwrap();
let tcp = tcp::tokio::Transport::new(tcp::Config::new());
let dh_keys = Keypair::<X25519Spec>::new() let transport = MemoryTransport::new()
.into_authentic(&identity) .or_transport(tcp)
.expect("failed to create dh_keys");
let noise = NoiseConfig::xx(dh_keys).into_authenticated();
let transport = MemoryTransport
.or_transport(TokioTcpConfig::new())
.upgrade(Version::V1) .upgrade(Version::V1)
.authenticate(noise) .authenticate(noise)
.multiplex(SelectUpgrade::new( .multiplex(yamux::Config::default())
YamuxConfig::default(),
MplexConfig::new(),
))
.timeout(Duration::from_secs(5)) .timeout(Duration::from_secs(5))
.map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer))) .map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer)))
.boxed(); .boxed();
SwarmBuilder::new(transport, behaviour_fn(peer_id, identity), peer_id) SwarmBuilder::with_existing_identity(identity)
.executor(Box::new(GlobalSpawnTokioExecutor)) .with_tokio()
.with_other_transport(|_| Ok(transport))
.unwrap()
.with_behaviour(|keypair| Ok(behaviour_fn(keypair.clone())))
.unwrap()
.build() .build()
} }
@ -74,29 +60,6 @@ async fn get_local_tcp_address() -> Multiaddr {
.unwrap() .unwrap()
} }
pub async fn await_events_or_timeout<A, B, E1, E2>(
swarm_1: &mut (impl FusedStream<Item = SwarmEvent<A, E1>> + FusedStream + Unpin),
swarm_2: &mut (impl FusedStream<Item = SwarmEvent<B, E2>> + FusedStream + Unpin),
) -> (SwarmEvent<A, E1>, SwarmEvent<B, E2>)
where
SwarmEvent<A, E1>: Debug,
SwarmEvent<B, E2>: Debug,
{
tokio::time::timeout(
Duration::from_secs(30),
future::join(
swarm_1
.inspect(|event| tracing::debug!("Swarm1 emitted {:?}", event))
.select_next_some(),
swarm_2
.inspect(|event| tracing::debug!("Swarm2 emitted {:?}", event))
.select_next_some(),
),
)
.await
.expect("network behaviours to emit an event within 10 seconds")
}
/// An extension trait for [`Swarm`] that makes it easier to set up a network of /// An extension trait for [`Swarm`] that makes it easier to set up a network of
/// [`Swarm`]s for tests. /// [`Swarm`]s for tests.
#[async_trait] #[async_trait]
@ -105,8 +68,8 @@ pub trait SwarmExt {
/// until the connection is established. /// until the connection is established.
async fn block_on_connection<T>(&mut self, other: &mut Swarm<T>) async fn block_on_connection<T>(&mut self, other: &mut Swarm<T>)
where where
T: NetworkBehaviour, T: NetworkBehaviour + Send,
<T as NetworkBehaviour>::OutEvent: Debug; <T as NetworkBehaviour>::ToSwarm: Debug;
/// Listens on a random memory address, polling the [`Swarm`] until the /// Listens on a random memory address, polling the [`Swarm`] until the
/// transport is ready to accept connections. /// transport is ready to accept connections.
@ -120,18 +83,24 @@ pub trait SwarmExt {
#[async_trait] #[async_trait]
impl<B> SwarmExt for Swarm<B> impl<B> SwarmExt for Swarm<B>
where where
B: NetworkBehaviour, B: NetworkBehaviour + Send,
<B as NetworkBehaviour>::OutEvent: Debug, <B as NetworkBehaviour>::ToSwarm: Debug,
{ {
async fn block_on_connection<T>(&mut self, other: &mut Swarm<T>) async fn block_on_connection<T>(&mut self, other: &mut Swarm<T>)
where where
T: NetworkBehaviour, T: NetworkBehaviour + Send,
<T as NetworkBehaviour>::OutEvent: Debug, <T as NetworkBehaviour>::ToSwarm: Debug,
{ {
let addr_to_dial = other.external_addresses().next().unwrap().addr.clone(); let addr_to_dial = other.external_addresses().next().unwrap().clone();
let local_peer_id = *other.local_peer_id(); let local_peer_id = *other.local_peer_id();
self.dial(addr_to_dial).unwrap(); self.dial(
DialOpts::peer_id(local_peer_id)
.addresses(vec![addr_to_dial])
.extend_addresses_through_behaviour()
.build(),
)
.unwrap();
let mut dialer_done = false; let mut dialer_done = false;
let mut listener_done = false; let mut listener_done = false;
@ -145,7 +114,7 @@ where
SwarmEvent::ConnectionEstablished { .. } => { SwarmEvent::ConnectionEstablished { .. } => {
dialer_done = true; dialer_done = true;
} }
SwarmEvent::OutgoingConnectionError { peer_id, error } if matches!(peer_id, Some(alice_peer_id) if alice_peer_id == local_peer_id) => { SwarmEvent::OutgoingConnectionError { peer_id, error, .. } if matches!(peer_id, Some(alice_peer_id) if alice_peer_id == local_peer_id) => {
panic!("Failed to dial address {}: {}", peer_id.unwrap(), error) panic!("Failed to dial address {}: {}", peer_id.unwrap(), error)
} }
other => { other => {
@ -182,7 +151,7 @@ where
// Memory addresses are externally reachable because they all share the same // Memory addresses are externally reachable because they all share the same
// memory-space. // memory-space.
self.add_external_address(multiaddr.clone(), AddressScore::Infinite); self.add_external_address(multiaddr.clone());
multiaddr multiaddr
} }
@ -200,7 +169,7 @@ where
async fn block_until_listening_on<B>(swarm: &mut Swarm<B>, multiaddr: &Multiaddr) async fn block_until_listening_on<B>(swarm: &mut Swarm<B>, multiaddr: &Multiaddr)
where where
B: NetworkBehaviour, B: NetworkBehaviour,
<B as NetworkBehaviour>::OutEvent: Debug, <B as NetworkBehaviour>::ToSwarm: Debug,
{ {
loop { loop {
match swarm.select_next_some().await { match swarm.select_next_some().await {

View file

@ -1,11 +1,10 @@
use anyhow::Result; use anyhow::Result;
use data_encoding::BASE32; use data_encoding::BASE32;
use futures::future::{BoxFuture, FutureExt, Ready}; use futures::future::{BoxFuture, Ready};
use libp2p::core::multiaddr::{Multiaddr, Protocol}; use libp2p::core::multiaddr::{Multiaddr, Protocol};
use libp2p::core::transport::TransportError; use libp2p::core::transport::{ListenerId, TransportError};
use libp2p::core::Transport; use libp2p::core::Transport;
use libp2p::tcp::tokio::{Tcp, TcpStream}; use libp2p::tcp::tokio::TcpStream;
use libp2p::tcp::TcpListenStream;
use std::borrow::Cow; use std::borrow::Cow;
use std::net::{Ipv4Addr, Ipv6Addr}; use std::net::{Ipv4Addr, Ipv6Addr};
use std::{fmt, io}; use std::{fmt, io};
@ -26,61 +25,87 @@ impl TorDialOnlyTransport {
impl Transport for TorDialOnlyTransport { impl Transport for TorDialOnlyTransport {
type Output = TcpStream; type Output = TcpStream;
type Error = io::Error; type Error = io::Error;
type Listener = TcpListenStream<Tcp>;
type ListenerUpgrade = Ready<Result<Self::Output, Self::Error>>; type ListenerUpgrade = Ready<Result<Self::Output, Self::Error>>;
type Dial = BoxFuture<'static, Result<Self::Output, Self::Error>>; type Dial = BoxFuture<'static, Result<Self::Output, Self::Error>>;
fn listen_on(self, addr: Multiaddr) -> Result<Self::Listener, TransportError<Self::Error>> { fn listen_on(
&mut self,
_id: ListenerId,
addr: Multiaddr,
) -> Result<(), TransportError<Self::Error>> {
Err(TransportError::MultiaddrNotSupported(addr)) Err(TransportError::MultiaddrNotSupported(addr))
} }
fn dial(self, addr: Multiaddr) -> Result<Self::Dial, TransportError<Self::Error>> { fn dial(&mut self, addr: Multiaddr) -> Result<Self::Dial, TransportError<Self::Error>> {
let address = TorCompatibleAddress::from_multiaddr(Cow::Borrowed(&addr))?; let address = TorCompatibleAddress::from_multiaddr(Cow::Borrowed(&addr))?;
if address.is_certainly_not_reachable_via_tor_daemon() { if address.is_certainly_not_reachable_via_tor_daemon() {
return Err(TransportError::MultiaddrNotSupported(addr)); return Err(TransportError::MultiaddrNotSupported(addr));
} }
let dial_future = async move { let socks_port = self.socks_port;
Ok(Box::pin(async move {
tracing::debug!(address = %addr, "Establishing connection through Tor proxy"); tracing::debug!(address = %addr, "Establishing connection through Tor proxy");
let stream = let stream =
Socks5Stream::connect((Ipv4Addr::LOCALHOST, self.socks_port), address.to_string()) Socks5Stream::connect((Ipv4Addr::LOCALHOST, socks_port), address.to_string())
.await .await
.map_err(|e| io::Error::new(io::ErrorKind::ConnectionRefused, e))?; .map_err(|e| io::Error::new(io::ErrorKind::ConnectionRefused, e))?;
tracing::debug!("Connection through Tor established"); tracing::debug!("Connection through Tor established");
Ok(TcpStream(stream.into_inner())) Ok(TcpStream(stream.into_inner()))
}; }))
Ok(dial_future.boxed())
} }
fn address_translation(&self, _: &Multiaddr, _: &Multiaddr) -> Option<Multiaddr> { fn address_translation(&self, _: &Multiaddr, _: &Multiaddr) -> Option<Multiaddr> {
None None
} }
fn dial_as_listener(self, addr: Multiaddr) -> Result<Self::Dial, TransportError<Self::Error>> {
fn dial_as_listener(
&mut self,
addr: Multiaddr,
) -> Result<Self::Dial, TransportError<Self::Error>> {
let address = TorCompatibleAddress::from_multiaddr(Cow::Borrowed(&addr))?; let address = TorCompatibleAddress::from_multiaddr(Cow::Borrowed(&addr))?;
if address.is_certainly_not_reachable_via_tor_daemon() { if address.is_certainly_not_reachable_via_tor_daemon() {
return Err(TransportError::MultiaddrNotSupported(addr)); return Err(TransportError::MultiaddrNotSupported(addr));
} }
let dial_future = async move { let socks_port = self.socks_port;
Ok(Box::pin(async move {
tracing::debug!(address = %addr, "Establishing connection through Tor proxy"); tracing::debug!(address = %addr, "Establishing connection through Tor proxy");
let stream = let stream =
Socks5Stream::connect((Ipv4Addr::LOCALHOST, self.socks_port), address.to_string()) Socks5Stream::connect((Ipv4Addr::LOCALHOST, socks_port), address.to_string())
.await .await
.map_err(|e| io::Error::new(io::ErrorKind::ConnectionRefused, e))?; .map_err(|e| io::Error::new(io::ErrorKind::ConnectionRefused, e))?;
tracing::debug!("Connection through Tor established"); tracing::debug!("Connection through Tor established");
Ok(TcpStream(stream.into_inner())) Ok(TcpStream(stream.into_inner()))
}; }))
}
Ok(dial_future.boxed()) fn remove_listener(&mut self, _id: ListenerId) -> bool {
// TODO(Libp2p Migration): What do we need to do here?
// I believe nothing because we are not using the transport to listen.
false
}
fn poll(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<libp2p::core::transport::TransportEvent<Self::ListenerUpgrade, Self::Error>>
{
// TODO(Libp2p Migration): What do we need to do here?
// See: https://github.com/libp2p/rust-libp2p/pull/2652
// I believe we do not need to do anything here because we are not using the transport to listen.
// But we need to verify this before merging.
std::task::Poll::Pending
} }
} }

View file

@ -1,26 +1,23 @@
use crate::network::cbor_request_response::CborCodec; use std::time::Duration;
use crate::{asb, cli, monero}; use crate::{asb, cli, monero};
use libp2p::core::ProtocolName; use libp2p::request_response::{self, ProtocolSupport};
use libp2p::request_response::{ use libp2p::{PeerId, StreamProtocol};
ProtocolSupport, RequestResponse, RequestResponseConfig, RequestResponseEvent,
RequestResponseMessage,
};
use libp2p::PeerId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
const PROTOCOL: &str = "/comit/xmr/btc/transfer_proof/1.0.0"; const PROTOCOL: &str = "/comit/xmr/btc/transfer_proof/1.0.0";
type OutEvent = RequestResponseEvent<Request, ()>; type OutEvent = request_response::Event<Request, ()>;
type Message = RequestResponseMessage<Request, ()>; type Message = request_response::Message<Request, ()>;
pub type Behaviour = RequestResponse<CborCodec<TransferProofProtocol, Request, ()>>; pub type Behaviour = request_response::cbor::Behaviour<Request, ()>;
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
pub struct TransferProofProtocol; pub struct TransferProofProtocol;
impl ProtocolName for TransferProofProtocol { impl AsRef<str> for TransferProofProtocol {
fn protocol_name(&self) -> &[u8] { fn as_ref(&self) -> &str {
PROTOCOL.as_bytes() PROTOCOL
} }
} }
@ -32,17 +29,15 @@ pub struct Request {
pub fn alice() -> Behaviour { pub fn alice() -> Behaviour {
Behaviour::new( Behaviour::new(
CborCodec::default(), vec![(StreamProtocol::new(PROTOCOL), ProtocolSupport::Outbound)],
vec![(TransferProofProtocol, ProtocolSupport::Outbound)], request_response::Config::default().with_request_timeout(Duration::from_secs(60)),
RequestResponseConfig::default(),
) )
} }
pub fn bob() -> Behaviour { pub fn bob() -> Behaviour {
Behaviour::new( Behaviour::new(
CborCodec::default(), vec![(StreamProtocol::new(PROTOCOL), ProtocolSupport::Inbound)],
vec![(TransferProofProtocol, ProtocolSupport::Inbound)], request_response::Config::default().with_request_timeout(Duration::from_secs(60)),
RequestResponseConfig::default(),
) )
} }
@ -57,6 +52,7 @@ impl From<(PeerId, Message)> for asb::OutEvent {
} }
} }
} }
crate::impl_from_rr_event!(OutEvent, asb::OutEvent, PROTOCOL); crate::impl_from_rr_event!(OutEvent, asb::OutEvent, PROTOCOL);
impl From<(PeerId, Message)> for cli::OutEvent { impl From<(PeerId, Message)> for cli::OutEvent {

View file

@ -2,9 +2,8 @@ use anyhow::Result;
use futures::{AsyncRead, AsyncWrite}; use futures::{AsyncRead, AsyncWrite};
use libp2p::core::muxing::StreamMuxerBox; use libp2p::core::muxing::StreamMuxerBox;
use libp2p::core::transport::Boxed; use libp2p::core::transport::Boxed;
use libp2p::core::upgrade::{SelectUpgrade, Version}; use libp2p::core::upgrade::Version;
use libp2p::mplex::MplexConfig; use libp2p::noise;
use libp2p::noise::{self, NoiseConfig, X25519Spec};
use libp2p::{identity, yamux, PeerId, Transport}; use libp2p::{identity, yamux, PeerId, Transport};
use std::time::Duration; use std::time::Duration;
@ -21,11 +20,8 @@ pub fn authenticate_and_multiplex<T>(
where where
T: AsyncRead + AsyncWrite + Unpin + Send + 'static, T: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{ {
let auth_upgrade = { let auth_upgrade = noise::Config::new(identity)?;
let noise_identity = noise::Keypair::<X25519Spec>::new().into_authentic(identity)?; let multiplex_upgrade = yamux::Config::default();
NoiseConfig::xx(noise_identity).into_authenticated()
};
let multiplex_upgrade = SelectUpgrade::new(yamux::YamuxConfig::default(), MplexConfig::new());
let transport = transport let transport = transport
.upgrade(Version::V1) .upgrade(Version::V1)

View file

@ -226,6 +226,11 @@ where
state3, state3,
} }
}, },
// TODO: We should already listen for the encrypted signature here.
//
// If we send Bob the transfer proof, but for whatever reason we do not receive an acknoledgement from him
// we would be stuck in this state forever (deadlock). By listening for the encrypted signature here we
// can still proceed to the next state even if Bob does not respond with an acknoledgement.
result = tx_lock_status.wait_until_confirmed_with(state3.cancel_timelock) => { result = tx_lock_status.wait_until_confirmed_with(state3.cancel_timelock) => {
result?; result?;
AliceState::CancelTimelockExpired { AliceState::CancelTimelockExpired {
@ -245,7 +250,6 @@ where
select! { select! {
biased; // make sure the cancel timelock expiry future is polled first biased; // make sure the cancel timelock expiry future is polled first
result = tx_lock_status.wait_until_confirmed_with(state3.cancel_timelock) => { result = tx_lock_status.wait_until_confirmed_with(state3.cancel_timelock) => {
result?; result?;
AliceState::CancelTimelockExpired { AliceState::CancelTimelockExpired {
@ -275,6 +279,7 @@ where
ExpiredTimelocks::None { .. } => { ExpiredTimelocks::None { .. } => {
let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await;
match state3.signed_redeem_transaction(*encrypted_signature) { match state3.signed_redeem_transaction(*encrypted_signature) {
// TODO: We should retry publishing the redeem transaction if it fails
Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await { Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await {
Ok((_, subscription)) => match subscription.wait_until_seen().await { Ok((_, subscription)) => match subscription.wait_until_seen().await {
Ok(_) => AliceState::BtcRedeemTransactionPublished { state3 }, Ok(_) => AliceState::BtcRedeemTransactionPublished { state3 },
@ -457,3 +462,15 @@ pub(crate) fn is_complete(state: &AliceState) -> bool {
| AliceState::SafelyAborted | AliceState::SafelyAborted
) )
} }
/// This function is used to check if Alice is in a state where it is clear that she has already received the encrypted signature from Bob.
/// This allows us to acknowledge the encrypted signature multiple times
/// If our acknowledgement does not reach Bob, he might send the encrypted signature again.
pub(crate) fn has_already_processed_enc_sig(state: &AliceState) -> bool {
matches!(
state,
AliceState::EncSigLearned { .. }
| AliceState::BtcRedeemTransactionPublished { .. }
| AliceState::BtcRedeemed
)
}

View file

@ -19,6 +19,22 @@ pub fn is_complete(state: &BobState) -> bool {
) )
} }
/// Identifies states that have already processed the transfer proof.
/// This is used to be able to acknowledge the transfer proof multiple times (if it was already processed).
/// This is necessary because sometimes our acknowledgement might not reach Alice.
pub fn has_already_processed_transfer_proof(state: &BobState) -> bool {
// This match statement MUST match all states which Bob can enter after receiving the transfer proof.
// We do not match any of the cancel / refund states because in those, the swap cannot be successfull anymore.
matches!(
state,
BobState::XmrLockProofReceived { .. }
| BobState::XmrLocked(..)
| BobState::EncSigSent(..)
| BobState::BtcRedeemed(..)
| BobState::XmrRedeemed { .. }
)
}
// Identifies states that should be run at most once before exiting. // Identifies states that should be run at most once before exiting.
// This is used to prevent infinite retry loops while still allowing manual resumption. // This is used to prevent infinite retry loops while still allowing manual resumption.
// //
@ -334,8 +350,10 @@ async fn next_state(
result = event_loop_handle.send_encrypted_signature(state.tx_redeem_encsig()) => { result = event_loop_handle.send_encrypted_signature(state.tx_redeem_encsig()) => {
match result { match result {
Ok(_) => BobState::EncSigSent(state), Ok(_) => BobState::EncSigSent(state),
Err(bmrng::error::RequestError::RecvError | bmrng::error::RequestError::SendError(_)) => bail!("Failed to communicate encrypted signature through event loop channel"), Err(err) => {
Err(bmrng::error::RequestError::RecvTimeoutError) => unreachable!("We construct the channel with no timeout"), tracing::error!(%err, "Failed to send encrypted signature to Alice");
bail!("Failed to send encrypted signature to Alice");
}
} }
}, },
result = tx_lock_status.wait_until_confirmed_with(state.cancel_timelock) => { result = tx_lock_status.wait_until_confirmed_with(state.cancel_timelock) => {
@ -457,9 +475,7 @@ async fn next_state(
); );
tracing::info!("Attempting to cooperatively redeem XMR after being punished"); tracing::info!("Attempting to cooperatively redeem XMR after being punished");
let response = event_loop_handle let response = event_loop_handle.request_cooperative_xmr_redeem().await;
.request_cooperative_xmr_redeem(swap_id)
.await;
match response { match response {
Ok(Fullfilled { s_a, .. }) => { Ok(Fullfilled { s_a, .. }) => {

View file

@ -43,9 +43,8 @@ impl Seed {
pub fn derive_libp2p_identity(&self) -> identity::Keypair { pub fn derive_libp2p_identity(&self) -> identity::Keypair {
let bytes = self.derive(b"NETWORK").derive(b"LIBP2P_IDENTITY").bytes(); let bytes = self.derive(b"NETWORK").derive(b"LIBP2P_IDENTITY").bytes();
let key = identity::ed25519::SecretKey::from_bytes(bytes).expect("we always pass 32 bytes");
identity::Keypair::Ed25519(key.into()) identity::Keypair::ed25519_from_bytes(bytes).expect("we always pass 32 bytes")
} }
pub fn derive_torv3_key(&self) -> TorSecretKeyV3 { pub fn derive_torv3_key(&self) -> TorSecretKeyV3 {

View file

@ -420,7 +420,7 @@ impl BobParams {
format!( format!(
"{}/p2p/{}", "{}/p2p/{}",
self.alice_address.clone(), self.alice_address.clone(),
self.alice_peer_id.clone().to_base58() self.alice_peer_id.to_base58()
) )
} }
@ -505,9 +505,7 @@ impl BobParams {
(identity.clone(), XmrBtcNamespace::Testnet), (identity.clone(), XmrBtcNamespace::Testnet),
); );
let mut swarm = swarm::cli(identity.clone(), tor_socks5_port, behaviour).await?; let mut swarm = swarm::cli(identity.clone(), tor_socks5_port, behaviour).await?;
swarm swarm.add_peer_address(self.alice_peer_id, self.alice_address.clone());
.behaviour_mut()
.add_address(self.alice_peer_id, self.alice_address.clone());
cli::EventLoop::new(swap_id, swarm, self.alice_peer_id, db.clone()) cli::EventLoop::new(swap_id, swarm, self.alice_peer_id, db.clone())
} }